diff --git a/bin/playerctl.sh b/bin/playerctl.sh
new file mode 100644
index 0000000..9fc3b02
--- /dev/null
+++ b/bin/playerctl.sh
@@ -0,0 +1,53 @@
+#!/usr/bin/env bash
+
+# source: https://gitlab.com/xPMo/dotfiles.cli/-/blob/dots/.local/lib/waybar/playerctl.sh
+
+exec 2>"$XDG_RUNTIME_DIR/waybar-playerctl.log"
+IFS=$'\n\t'
+
+while true; do
+
+ while read -r playing position length name artist title arturl hpos hlen; do
+ # remove leaders
+ playing=${playing:1} position=${position:1} length=${length:1} name=${name:1}
+ artist=${artist:1} title=${title:1} arturl=${arturl:1} hpos=${hpos:1} hlen=${hlen:1}
+
+ # build line
+ line="${artist:+$artist ${title:+- }}${title:+$title }${hpos:+$hpos${hlen:+|}}$hlen"
+
+ # json escaping
+ line="${line//\"/\\\"}"
+ ((percentage = length ? (100 * (position % length)) / length : 0))
+ case $playing in
+ ⏸️ | Paused) text=''"$line"'' ;;
+ ▶️ | Playing) text="$line" ;;
+ *) text='⏹' ;;
+ esac
+
+ # integrations for other services (nwg-wrapper)
+ if [[ $title != "$ptitle" || $artist != "$partist" || $parturl != "$arturl" ]]; then
+ typeset -p playing length name artist title arturl >"$XDG_RUNTIME_DIR/waybar-playerctl.info"
+ pkill -8 nwg-wrapper
+ ptitle=$title partist=$artist parturl=$arturl
+ fi
+
+ # exit if print fails
+ printf '{"text":"%s","tooltip":"%s","class":"%s","percentage":%s}\n' \
+ "$text" "$playing $name | $line" "$percentage" "$percentage" || break 2
+
+ done < <(
+ # requires playerctl>=2.0
+ # Add non-space character ":" before each parameter to prevent 'read' from skipping over them
+ playerctl --follow metadata --player playerctld --format \
+ $':{{emoji(status)}}\t:{{position}}\t:{{mpris:length}}\t:{{playerName}}\t:{{markup_escape(artist)}}\t:{{markup_escape(title)}}\t:{{mpris:artUrl}}\t:{{duration(position)}}\t:{{duration(mpris:length)}}' &
+ echo $! >"$XDG_RUNTIME_DIR/waybar-playerctl.pid"
+ )
+
+ # no current players
+ # exit if print fails
+ echo '⏹' || break
+ sleep 15
+
+done
+
+kill "$(<"$XDG_RUNTIME_DIR/waybar-playerctl.pid")"
diff --git a/home-surtur.nix b/home-surtur.nix
index bea46c2..85046ca 100644
--- a/home-surtur.nix
+++ b/home-surtur.nix
@@ -442,6 +442,146 @@ in {
'';
executable = true;
};
+
+ # ref: https://go.dev/blog/pprof
+ ".local/bin/xtime" = {
+ text = ''
+ #!/bin/sh
+ /usr/bin/time -f '%Uu %Ss %er %MkB %C' "$@"
+ '';
+ executable = true;
+ };
+
+ ".local/bin/xdp-screen-cast" = {
+ text = ''
+ #!/usr/bin/python3
+
+ # ref: https://gitlab.gnome.org/-/snippets/19
+
+ import re
+ import signal
+ import dbus
+ from gi.repository import GLib
+ from dbus.mainloop.glib import DBusGMainLoop
+
+ import gi
+ gi.require_version('Gst', '1.0')
+ from gi.repository import GObject, Gst
+
+ DBusGMainLoop(set_as_default=True)
+ Gst.init(None)
+
+ loop = GLib.MainLoop()
+
+ bus = dbus.SessionBus()
+ request_iface = 'org.freedesktop.portal.Request'
+ screen_cast_iface = 'org.freedesktop.portal.ScreenCast'
+
+ pipeline = None
+
+ def terminate():
+ if pipeline is not None:
+ self.player.set_state(Gst.State.NULL)
+ loop.quit()
+
+ request_token_counter = 0
+ session_token_counter = 0
+ sender_name = re.sub(r'\.', r'_', bus.get_unique_name()[1:])
+
+ def new_request_path():
+ global request_token_counter
+ request_token_counter = request_token_counter + 1
+ token = 'u%d'%request_token_counter
+ path = '/org/freedesktop/portal/desktop/request/%s/%s'%(sender_name, token)
+ return (path, token)
+
+ def new_session_path():
+ global session_token_counter
+ session_token_counter = session_token_counter + 1
+ token = 'u%d'%session_token_counter
+ path = '/org/freedesktop/portal/desktop/session/%s/%s'%(sender_name, token)
+ return (path, token)
+
+ def screen_cast_call(method, callback, *args, options={}):
+ (request_path, request_token) = new_request_path()
+ bus.add_signal_receiver(callback,
+ 'Response',
+ request_iface,
+ 'org.freedesktop.portal.Desktop',
+ request_path)
+ options['handle_token'] = request_token
+ method(*(args + (options, )),
+ dbus_interface=screen_cast_iface)
+
+ def on_gst_message(bus, message):
+ type = message.type
+ if type == Gst.MessageType.EOS or type == Gst.MessageType.ERROR:
+ terminate()
+
+ def play_pipewire_stream(node_id):
+ empty_dict = dbus.Dictionary(signature="sv")
+ fd_object = portal.OpenPipeWireRemote(session, empty_dict,
+ dbus_interface=screen_cast_iface)
+ fd = fd_object.take()
+ pipeline = Gst.parse_launch('pipewiresrc fd=%d path=%u ! videoconvert ! xvimagesink'%(fd, node_id))
+ pipeline.set_state(Gst.State.PLAYING)
+ pipeline.get_bus().connect('message', on_gst_message)
+
+ def on_start_response(response, results):
+ if response != 0:
+ print("Failed to start: %s"%response)
+ terminate()
+ return
+
+ print("streams:")
+ for (node_id, stream_properties) in results['streams']:
+ print("stream {}".format(node_id))
+ play_pipewire_stream(node_id)
+
+ def on_select_sources_response(response, results):
+ if response != 0:
+ print("Failed to select sources: %d"%response)
+ terminate()
+ return
+
+ print("sources selected")
+ global session
+ screen_cast_call(portal.Start, on_start_response,
+ session, ''')
+
+ def on_create_session_response(response, results):
+ if response != 0:
+ print("Failed to create session: %d"%response)
+ terminate()
+ return
+
+ global session
+ session = results['session_handle']
+ print("session %s created"%session)
+
+ screen_cast_call(portal.SelectSources, on_select_sources_response,
+ session,
+ options={ 'multiple': False,
+ 'types': dbus.UInt32(1|2) })
+
+ portal = bus.get_object('org.freedesktop.portal.Desktop',
+ '/org/freedesktop/portal/desktop')
+
+ (session_path, session_token) = new_session_path()
+ screen_cast_call(portal.CreateSession, on_create_session_response,
+ options={ 'session_handle_token': session_token })
+
+ try:
+ loop.run()
+ except KeyboardInterrupt:
+ terminate()
+ '';
+ executable = true;
+ };
+ ".local/bin/playerctl.sh" = {
+ source = ./bin/playerctl.sh;
+ executable = true;
+ };
};
xdg = {