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 = {