summary refs log tree commit diff
path: root/bin
diff options
context:
space:
mode:
authorRobert Günzler <r@gnzler.io>2026-09-27 17:38:39 +0200
committerRobert Günzler <r@gnzler.io>2026-09-27 17:38:39 +0200
commit0860db73ae6fd9a6f6347219d820bf026a6b8b91 (patch)
tree11318c3670d1c32c5aa0bf404c22fba3bf63fc53 /bin
parent4e34cbf9673aeaba252e01df554a0d204e83c5e2 (diff)
downloaddotfiles-0860db73ae6fd9a6f6347219d820bf026a6b8b91.tar.gz
Automated-commit-by: dots
Signed-off-by: Robert Günzler <r@gnzler.io>
Diffstat (limited to 'bin')
-rwxr-xr-xbin/2av113
-rwxr-xr-xbin/audm31
-rwxr-xr-xbin/blum40
-rwxr-xr-xbin/dots10
-rwxr-xr-xbin/genup8
-rwxr-xr-xbin/git-sync58
-rw-r--r--bin/libfuzl.sh39
-rwxr-xr-xbin/netm37
-rwxr-xr-xbin/pasm48
-rwxr-xr-xbin/play-mpv26
-rwxr-xr-xbin/play-music9
-rwxr-xr-xbin/screenrec1
-rwxr-xr-xbin/svcm1
-rwxr-xr-xbin/todo6
-rwxr-xr-xbin/urlopen31
-rwxr-xr-xbin/vpnm74
16 files changed, 287 insertions, 145 deletions
diff --git a/bin/2av1 b/bin/2av1
index 19ace0b..287323a 100755
--- a/bin/2av1
+++ b/bin/2av1
@@ -3,8 +3,13 @@ set -eu
 [ -n "${DEBUG:-}" ] && set -x
 [ -z "${DEBUG:-}" ] && export SVT_LOG=1
 
-BITRATE=${BITRATE:-128000}
-EXT=${EXT:-webm}
+: ${KEEP:=}
+: ${OUTDIR:=}
+: ${BITRATE:=128k}
+: ${EXT:=webm}
+
+# expand
+BITRATE=${BITRATE%k*}000
 
 case "${VCODEC:-av1}" in
 vp9) vcodec_args=(-c:v libvpx-vp9 -crf "${CRF:-31}" -b:v 0) ;;
@@ -76,6 +81,10 @@ for f in "$@"; do
 	awk -v fs="$in_size" -v os="$out_size" \
 		'BEGIN{p=((100/fs) * (fs-os)); printf("♻️  %.2f%% reduction\n", p);}'
 
+	if [ -n "$KEEP" ]; then
+		return
+	fi
+
 	if [ $in_size -gt $out_size ]; then
 		trash-put -v "${f}"
 	else
diff --git a/bin/audm b/bin/audm
index 58a5a91..33abb34 100755
--- a/bin/audm
+++ b/bin/audm
@@ -17,7 +17,9 @@ main() {
 	source=$(inspect_node '@DEFAULT_SOURCE@' | awk -F\\t '{print $2}')
 	fuzl_action_add 'change_source' "󰍰" "$source"
 
-	fuzl_action_add 'change_profile' "󰔡" "Change profile"
+	fuzl_action_add 'change_profile' "󱟲" "Change profile"
+
+	fuzl_action_add 'mixsource' "󱡫" "Mix source into sink"
 
 	fuzl_action_menu || exit 1
 }
@@ -67,11 +69,11 @@ audm_change_profile() {
 
 	device=$(pw-dump | jq -rc --arg class "Audio/Device" \
 		'.[]|select((.info.props["media.class"]//"")|test($class))|.info.props|[.["object.id"],.["device.description"],.["device.nick"]]|join("\t")' |
-		FUZL_PROMPT='device' fuzl_menu $FUZL_MENU_FILTER --with-nth='{2}  {3}' --accept-nth='{1}\t{2}')
+		FUZL_PROMPT='device' fuzl_menu $FUZL_MENU_FILTER --with-nth='{2}  {3}')
 	test -n "$device" || main
 
-	did=$(printf %s "$device" | awk -F\\t '{print $1}')
-	dname=$(printf %s "$device" | awk -F\\t '{print $2}')
+	did=$(awk -F\\t '{print $1}' <<<"$device")
+	dname=$(awk -F\\t '{print $2}' <<<"$device")
 
 	profile=$(pw-dump | jq -r --argjson did "$did" \
 		'.[]|select(.id==$did)|.info.params.EnumProfile[]|[.index,.description]|join("\t")' |
@@ -81,4 +83,25 @@ audm_change_profile() {
 	fuzl_try -- wpctl set-profile "$did" "$profile"
 }
 
+audm_mixsource() {
+	sink=$(inspect_node '@DEFAULT_SINK@' | awk -F\\t '{print $1}')
+	sink_json=$(pw-dump -R "$sink" | jq -rc --argjson id "$sink" '.[]|select(.id==$id)')
+	sink_node=$(jq -rc '.info.props["node.name"]' <<<$sink_json)
+	sink_ports=$(jq -r '.info.params.PortConfig[]|select(.direction=="Input")|.format.position[]' <<<$sink_json)
+
+	source=$(inspect_node '@DEFAULT_SOURCE@' | awk -F\\t '{print $1}')
+	source_json=$(pw-dump -R "$source" | jq -rc --argjson id "$source" '.[]|select(.id==$id)')
+	source_node=$(jq -rc '.info.props["node.name"]' <<<$source_json)
+	source_ports=$(jq -rc '.info.params.PortConfig[]|select(.direction=="Output")|.format.position[]' <<<$source_json)
+
+	for source_port in $source_ports; do
+		for sink_port in $sink_ports; do
+			(
+				pw-link --passive "$source_node":capture_"$source_port" "$sink_node":playback_"$sink_port" ||
+					pw-link --disconnect "$source_node":capture_"$source_port" "$sink_node":playback_"$sink_port"
+			) &
+		done
+	done
+}
+
 main
diff --git a/bin/blum b/bin/blum
index 19c2aef..6f65f55 100755
--- a/bin/blum
+++ b/bin/blum
@@ -13,7 +13,8 @@ main() {
 	if ! bt_enabled; then
 		fuzl_action_add "enable" "󰷗" "Enable Bluetooth"
 	else
-		connected=$(bt_devices Connected | bt_devices_getname)
+		# XXX: handle multiple
+		connected=$(bt_devices Connected | tail -1 | bt_devices_getname)
 		if [ -n "$connected" ]; then
 			fuzl_action_add "disconnect" "󰂱" "Connected to $connected"
 		fi
@@ -35,7 +36,6 @@ bt_devices() {
 	echo "devices" "${1:-}" | bluetoothctl |
 		awk '/^Device /{match($0, /[0-9A-F:]{17}/); mac=substr($0,RSTART,RLENGTH); name=substr($0,RSTART+RLENGTH+1); print mac "\t" name}'
 }
-
 bt_devices_getmac() {
 	awk -F '\t' '{print $1}'
 }
@@ -44,19 +44,40 @@ bt_devices_getname() {
 	awk -F '\t' '{print $2}'
 }
 
+amagiri_mac='3C:38:F4:5D:3D:49'
+bt_tether_amagiri_connected() {
+	nmcli device show "$amagiri_mac" |
+		grep -i '^general.state' |
+		grep -q 'connected'
+}
+bt_tether_amagiri_up() {
+	echo "connect" "$amagiri_mac" |
+		fuzl_try -- bluetoothctl
+	fuzl_try -- nmcli device connect "$amagiri_mac"
+	fuzl_try -- nmcli radio wifi off
+	fuzl_notification "tethered" "amagiri"
+}
+bt_tether_amagiri_down() {
+	fuzl_try -- nmcli device disconnect "$amagiri_mac"
+	echo "disconnect" "$amagiri_mac" |
+		fuzl_try -- bluetoothctl
+	fuzl_try -- nmcli radio wifi on
+	fuzl_notification "untethered" "amagiri"
+}
+
 #
 # actions
 #
 
 blum_enable() {
 	fuzl_try -- doas -n rc-service bluetooth start
-	fuzl_notification "enabled" "-"
+	fuzl_notification "enabled" "service started"
 	main
 }
 
 blum_disable() {
 	fuzl_try -- doas -n rc-service bluetooth stop
-	fuzl_notification "disabled" "-"
+	fuzl_notification "disabled" "service stopped"
 }
 
 blum_disconnect() {
@@ -89,12 +110,11 @@ blum_repl() {
 }
 
 blum_tether_amagiri() {
-	local amagiri_mac='3C:38:F4:5D:3D:49'
-	local amagiri_net='e4e75ff7-b907-43b7-98b7-e0bf94a56d08'
-	echo "connect" "$amagiri_mac" | fuzl_try -- bluetoothctl
-	fuzl_notification "connected" "amagiri"
-	fuzl_try -- nmcli connection up "$amagiri_net"
-	fuzl_notification "network up" "amagiri"
+	if ! bt_tether_amagiri_connected; then
+		bt_tether_amagiri_up
+	else
+		bt_tether_amagiri_down
+	fi
 }
 
 main
diff --git a/bin/dots b/bin/dots
index 7076ece..c455f58 100755
--- a/bin/dots
+++ b/bin/dots
@@ -7,7 +7,7 @@ git)
 	exec /usr/bin/git --git-dir=$HOME/.dotfiles --work-tree=$HOME $*
 	;;
 st | status)
-	exec $0 git status -sb --untracked-files=no
+	exec $0 git status -sb --untracked-files=yes
 	;;
 add)
 	shift
@@ -29,4 +29,12 @@ untracked)
 	shift
 	exec $0 git ls-files -o $*
 	;;
+edit)
+	$0 git ls-files --full-name $HOME |
+		fzf --layout=reverse --height='~60%' \
+			--bind=enter:replace-query+become:"nvim $HOME/{q}"
+	;;
+*)
+	echo "dots has its own subcommands, if you want git, run dots git [git command]"
+	;;
 esac
diff --git a/bin/genup b/bin/genup
index c759a10..ad8d621 100755
--- a/bin/genup
+++ b/bin/genup
@@ -68,7 +68,7 @@ ebegin updating @world
 
 ebegin updating unstable
 [ -n "${dolive:-}" ] &&
-	_ emerge -a1 --keep-going @live-rebuild &&
+	_ emerge -a1 --keep-going "$@" $eargs @live-rebuild &&
 	eend
 
 ebegin depcleaning
@@ -85,7 +85,7 @@ ebegin updating @preserved-rebuild
 
 ebegin updating flatpaks
 [ -x "$(command -v flatpak)" ] && [ -n "${doflatpak:-}" ] &&
-	_ flatpak upgrade &&
-	_ flatpak uninstall --unused &&
-	_ flatpak repair &&
+	_ doas -u robert flatpak upgrade &&
+	_ doas -u robert flatpak uninstall --unused &&
+	_ doas -u robert flatpak repair &&
 	eend
diff --git a/bin/git-sync b/bin/git-sync
index b18c94d..9a41c5c 100755
--- a/bin/git-sync
+++ b/bin/git-sync
@@ -2,37 +2,45 @@
 set -eu
 [ -n "${DEBUG:-}" ] && set -x
 
-# use cli arg
-base_branch=${1:-}
+sync() {
+	local this_branch="${1:?}"
+	local base_branch=""
+	local upstream=""
 
-# detect base branch (when git-new was used)
-[ -n "$base_branch" ] || base_branch=$(git symbolic-ref --short refs/bases/"$(git symbolic-ref --short HEAD)" 2>/dev/null || :)
+	# detect base branch (when git-new was used)
+	[ -n "$base_branch" ] || base_branch=$(git symbolic-ref --short refs/baseof/"$this_branch" 2>/dev/null || :)
 
-# detect trunk via origin/HEAD
-[ -n "$base_branch" ] || base_branch=$(git symbolic-ref --short refs/remotes/origin/HEAD 2>/dev/null || :)
+	# recursive
+	[ -n "$base_branch" ] && sync "$base_branch" && git switch "$this_branch"
 
-# fallback to base branch => current branch
-[ -n "$base_branch" ] || base_branch=$(git symbolic-ref --short HEAD 2>/dev/null || :)
+	# detect trunk via origin/HEAD
+	[ -n "$base_branch" ] || base_branch=$(git symbolic-ref --short refs/remotes/origin/HEAD 2>/dev/null || :)
 
-# resolve upstream
-if [[ "$base_branch" = "origin/"* ]]; then
-	upstream=$base_branch
-else
-	upstream=$(git rev-parse --abbrev-ref --symbolic-full-name "$base_branch"'@{u}')
-fi
+	# fallback to base branch == current branch
+	[ -n "$base_branch" ] || base_branch=$this_branch
 
-[ -n "${base_branch:?}" ] || exit 1
-[ -n "${upstream:?}" ] || exit 1
+	# resolve upstream
+	if [[ "$base_branch" != "origin/"* ]]; then
+		upstream=$(git rev-parse --abbrev-ref --symbolic-full-name "$base_branch"'@{u}')
+	else
+		upstream=origin
+	fi
 
-echo "Syncing with $upstream (base: $base_branch)"
+	echo "Syncing $this_branch with $base_branch from $upstream"
 
-# fetch upstream
-git fetch ${upstream/\// }
+	[ -n "${base_branch:?}" ] || exit 1
+	[ -n "${upstream:?}" ] || exit 1
 
-# check if base and upstream have diverged
-if ! git diff --quiet "$upstream" "$base_branch"; then
-	git diff --stat "$upstream" "$base_branch"
-fi
+	# fetch upstream
+	git fetch ${upstream/\// }
 
-# rebase
-git rebase --interactive "$upstream"
+	# check if base and upstream have diverged
+	if ! git diff --quiet "$upstream" "$base_branch"; then
+		git diff --stat "$upstream" "$base_branch"
+	fi
+
+	# rebase
+	git rebase --interactive "$upstream" "$this_branch"
+}
+
+sync "$(git symbolic-ref --short HEAD)"
diff --git a/bin/libfuzl.sh b/bin/libfuzl.sh
index a28c861..c53a71f 100644
--- a/bin/libfuzl.sh
+++ b/bin/libfuzl.sh
@@ -4,9 +4,12 @@
 
 set -eu
 
+# TODO: use custom-1 somehow
+
 # pick up configuration
 FUZL_PROMPT=${FUZL_PROMPT:-fuzl}
 FUZL_ICON=${FUZL_ICON:-󰈲}
+FUZL_CLI_ARGS=${FUZL_CLI_ARGS:-$@}
 
 fuzl_try() {
 	[ "$1" == "--" ] && shift 1
@@ -89,6 +92,11 @@ fuzl_loading_cancel() {
 
 FUZL_ACTIONS=()
 
+fuzl_action_reset() {
+	# reset actions
+	FUZL_ACTIONS=()
+}
+
 fuzl_action_add() {
 	local action="${1:?}" icon="${2:?}"
 	shift 2
@@ -97,17 +105,36 @@ fuzl_action_add() {
 	FUZL_ACTIONS+=("${action}\t${icon}\t${*}")
 }
 
+fuzl_action_help() {
+	echo "usage: $FUZL_PROMPT [action]"
+	echo
+	for action in "${FUZL_ACTIONS[@]}"; do
+		echo -e "       $action"
+	done
+	echo
+	exit 0
+}
+
 fuzl_action_menu() {
 	local action
 
-	action=$(printf '%s\n' "${FUZL_ACTIONS[@]}" |
-		FUZL_PROMPT="$FUZL_PROMPT: $*" fuzl_menu $FUZL_MENU_FILTER --with-nth='{2}  {3..}' --accept-nth=1)
+	# only actually ask if action wasn't provided on cli
+	action=${FUZL_CLI_ARGS}
+	if [[ "$action" == "--help" ]]; then
+		fuzl_action_help
+	fi
+	if test -z "$action"; then
+		action=$(printf '%s\n' "${FUZL_ACTIONS[@]}" |
+			FUZL_PROMPT="$FUZL_PROMPT: $*" fuzl_menu $FUZL_MENU_FILTER --with-nth='{2}  {3..}' --accept-nth=1)
+	fi
 
-	# reset actions
-	FUZL_ACTIONS=()
+	fuzl_action_reset
 
-	test -n "$action" || return
-	eval "${FUZL_PROMPT}_${action}" "'$*'"
+	if declare -f "${FUZL_PROMPT}_${action}" >/dev/null; then
+		eval "${FUZL_PROMPT}_${action}" "'$*'"
+	else
+		return 1
+	fi
 }
 
 # setup common debug helper
diff --git a/bin/netm b/bin/netm
index fab7e95..0ea9d2e 100755
--- a/bin/netm
+++ b/bin/netm
@@ -1327,39 +1327,12 @@ show_qrcode() {
 	fi
 
 	local qr_string="WIFI:T:${security};S:${ssid};P:${password};;"
-	local qr_file="/tmp/hyprltm-net-qr-${ssid}.png"
 
-	show_loading_notification "$tr_qrcode_generating"
-	qrencode -o "$qr_file" -s 10 -m 2 "$qr_string"
-	kill_loading_notification
-
-	# Display QR code inside Rofi
-	# -i: Enable icons
-	# -theme-str: Override theme to show large icon and no text input
-	# Display QR code inside Rofi
-	# -i: Enable icons
-	# -show-icons: Required to show icons in dmenu mode
-	# -theme-str: Override theme to show large icon and no text input
-
-	# We use the successful configuration from the test script
-	local rofi_override="
-        window { width: 500px; }
-        listview { lines: 1; scrollbar: false; }
-        element { orientation: vertical; padding: 20px; children: [ element-icon, element-text ]; }
-        element-icon { enabled: true; size: 300px; horizontal-align: 0.5; }
-        element-text { horizontal-align: 0.5; }
-        entry { enabled: false; } 
-        inputbar { enabled: false; }
-    "
-
-	# We pass an entry with the icon set to the QR file path
-	# Format: Valid Text \0icon\x1f/path/to/image.png
-	# We use "Scan to Connect" as the text under the QR code
-	echo -e "Scan to Connect\0icon\x1f${qr_file}" |
-		fuzzel --dmenu -i -show-icons -p "$tr_qrcode_message"
-
-	# Cleanup after Rofi closes
-	rm -f "$qr_file" 2>/dev/null
+	{
+		echo "" # spacer
+		qrencode -t UTF8i -s 10 -m 2 "$qr_string"
+	} |
+		display_menu 4 "Scan to connect..." "" '-l 18'
 }
 
 # Get password for a known Wi-Fi connection
diff --git a/bin/pasm b/bin/pasm
index 7555167..60c0394 100755
--- a/bin/pasm
+++ b/bin/pasm
@@ -16,12 +16,33 @@ main() {
 
 	local eid
 	eid=$(rbw_select_entry)
+	case "$eid" in
+	cards)
+		eid=$(rbw list --raw |
+			jq -r '.[]|select(.type=="Card")|"\(.id)\t\(.name)"' |
+			fuzl_menu $FUZL_MENU_FILTER --with-nth=2 --accept-nth=1)
 
-	fuzl_action_add "copy" "󰀄" "copy username/password"
-	rbw_has_totp "$eid" && fuzl_action_add "totp" "󰎃" "copy totp code"
-	fuzl_action_add "url" "󰌹" "copy url"
+		fuzl_action_add "raw" "󱃖" "view json"
+		fuzl_action_menu "$eid" || exit 1
+		;;
+	identities)
+		eid=$(rbw list --raw |
+			jq -r '.[]|select(.type=="Identity")|"\(.id)\t\(.name)"' |
+			fuzl_menu $FUZL_MENU_FILTER --with-nth=2 --accept-nth=1)
+
+		fuzl_action_add "raw" "󱃖" "view json"
+		fuzl_action_menu "$eid" || exit 1
+		;;
+	*)
+		fuzl_action_add "copy" "󰀄" "copy username/password"
+		rbw_has_totp "$eid" && fuzl_action_add "totp" "󰎃" "copy totp code"
+		fuzl_action_add "url" "󰌹" "copy url"
+		fuzl_action_add "raw" "󱃖" "view json"
+
+		fuzl_action_menu "$eid" || exit 1
+		;;
+	esac
 
-	fuzl_action_menu "$eid" || exit 1
 }
 
 #
@@ -29,9 +50,15 @@ main() {
 #
 
 rbw_select_entry() {
-	rbw list --raw |
-		jq -r '.[] | select(.type=="Login") | [.id,.name,.user,(.uris[0]//"-")]|join("%")' |
-		awk -F% '{print $1 "\t" ($2 " →") "\t" ("󰀄 " $3) "\t" ("󰌹 " $4)}' |
+	(
+		rbw list --raw |
+			jq -r '.[] | select(.type=="Login") | [.id,.name,.user,(.uris[0]//"-")]|join("%")' |
+			awk -F% '{print $1 "\t" ($2 " →") "\t" ("󰀄 " $3) "\t" ("󰌹 " $4)}'
+		printf 'cards\t~> C\tcards\n'
+		printf 'identities\t~> I\tidentities\n'
+		printf 'keys\t~> K\tkeys\n'
+		printf 'notes\t~> N\tnotes\n'
+	) |
 		fuzl_menu $FUZL_MENU_FILTER --with-nth='{2}       {3..}' --accept-nth=1 -w 80
 }
 
@@ -68,4 +95,11 @@ pasm_url() {
 	fuzl_notification "$(rbw_entry "$eid")" "copied url to clipboard"
 }
 
+pasm_raw() {
+	local eid=${1:?missing eid}
+	local tmpfile="/tmp/$(printf '%s' "$eid" | sha1sum | cut -d\  -f1).json"
+	fuzl_try -- rbw get --raw "$eid" >"$tmpfile"
+	fuzl_try -- urlopen open "$tmpfile"
+}
+
 main
diff --git a/bin/play-mpv b/bin/play-mpv
index 21f20c6..bb85c02 100755
--- a/bin/play-mpv
+++ b/bin/play-mpv
@@ -35,15 +35,9 @@ import sys
 from collections.abc import Iterable
 from typing import BinaryIO
 
-from itertools import tee
 
 def notify(files: Iterable[str]) -> None:
-    for f in files:
-        subprocess.Popen([
-            'fyi',
-            'play-mpv',
-            f,
-        ], start_new_session=True)
+    for f in files: subprocess.Popen(['fyi', 'play-mpv', f], start_new_session=True)
 
 def is_url(filename: str) -> bool:
     parts = filename.split("://", 1)
@@ -67,25 +61,22 @@ def get_socket_path() -> str:
                         "Ensure that one of the following environment variables is set: "
                         "UMPV_SOCKET_DIR, XDG_RUNTIME_DIR, HOME or TMPDIR.")
 
-    return os.path.join(base_dir, ".umpv")
+    sockid = "0"
+    return os.path.join(base_dir, "umpv-" + os.getenv("SOCKID", sockid))
 
 def send_files_to_mpv(conn: socket.socket | BinaryIO, files: Iterable[str]) -> None:
-    notify(tee(files)[0])
-
     try:
         send = conn.send if isinstance(conn, socket.socket) else conn.write
         for f in files:
             f = f.replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\n")
-            send(f'raw loadfile "{f}" append-play\n'.encode())
+            opt = os.getenv("LOADFILE", "append-play")
+            send(f'raw loadfile "{f}" {opt}\n'.encode())
 
     except Exception:
         print("mpv is terminating or the connection was lost.", file=sys.stderr)
         sys.exit(1)
 
 def start_mpv(files: Iterable[str], socket_path: str) -> None:
-    [files, files_copy] = tee(files, 2)
-    notify(files_copy)
-
     mpv = "mpv"
     mpv_command = shlex.split(os.getenv("MPV", mpv))
     mpv_command.extend([
@@ -94,13 +85,14 @@ def start_mpv(files: Iterable[str], socket_path: str) -> None:
         "--",
     ])
     mpv_command.extend(files)
-    print(mpv_command)
-    # subprocess.Popen(mpv_command, start_new_session=True)
+    subprocess.Popen(mpv_command, start_new_session=True)
 
 def main() -> None:
-    files = (os.path.abspath(f) if not is_url(f) else f for f in sys.argv[1:])
+    files = list(os.path.abspath(f) if not is_url(f) else f for f in sys.argv[1:])
     socket_path = get_socket_path()
 
+    os.getenv("NONOTIFY") or notify(files)
+
     try:
         with socket.socket(socket.AF_UNIX) as sock:
             sock.connect(socket_path)
diff --git a/bin/play-music b/bin/play-music
index 14e91f1..554d93f 100755
--- a/bin/play-music
+++ b/bin/play-music
@@ -20,4 +20,13 @@ select)
 	subdir=$(find . -type d -printf "%P\n" | shuf | fuzzel -d -w 80)
 	mpa "$subdir"
 	;;
+playlists)
+	cd $music_dir
+	playlist=$(find . -maxdepth 1 -name '*.m3u' -printf "%P\n" | shuf | fuzzel -d -w 80)
+	mpa "$playlist"
+	;;
+*)
+	shift
+	mpa "$@"
+	;;
 esac
diff --git a/bin/screenrec b/bin/screenrec
index 5324ead..3b221c5 100755
--- a/bin/screenrec
+++ b/bin/screenrec
@@ -1,5 +1,6 @@
 #!/bin/sh
 set -eu
+[ -n "${DEBUG:-}" ] && set -x
 
 update_waybar() {
 	sleep .5
diff --git a/bin/svcm b/bin/svcm
index b7daf39..1c818fe 100755
--- a/bin/svcm
+++ b/bin/svcm
@@ -37,6 +37,7 @@ select_service() {
 		return
 	fi
 
+	fuzl_action_reset
 	fuzl_action_add "restart" "󰜉" "Restart"
 	fuzl_action_add "start" "󰈸" "Start"
 	fuzl_action_add "stop" "" "Stop"
diff --git a/bin/todo b/bin/todo
index 7436779..51f60c0 100755
--- a/bin/todo
+++ b/bin/todo
@@ -52,9 +52,9 @@ function link(uri) {
     // support todotxt files
     if (filename() == "todo.txt") {
         status = "TODO"
-        if ($1 ~ /^x/) {
+        if ($1 == "x") {
             status = "DONE"
-            $0 = gensub(/^[x] /, "", "1")
+            $0 = gensub(/^x /, "", "1")
             $0 = strike($0)
         }
 
@@ -66,7 +66,7 @@ function link(uri) {
     }
 
     // support TODO files
-    if ($1 ~ /^(-|*)/) {
+    if ($1 == "*" || $1 == "-") {
         // detect status strings
         match($0, /\s(TODO|WIP|WAITING|DONE)\s/, arr)
 
diff --git a/bin/urlopen b/bin/urlopen
index 2b0479c..0f2e6dd 100755
--- a/bin/urlopen
+++ b/bin/urlopen
@@ -14,7 +14,7 @@ main() {
 		fuzl_action_add "ask" "󰮫" "ask"
 		fuzl_action_add "clipboard" "󰅌" "clipboard"
 		fuzl_action_add "primary" "󱊁" "primary"
-		fuzl_action_menu "$(get_text "$*")"
+		fuzl_action_menu "$(get_text "$*")" || open_default "$(get_text "$*")"
 		;;
 	2)
 		local action
@@ -30,6 +30,8 @@ main() {
 # helpers
 #
 
+DEFAULT_BROWSER=librewolf.desktop
+
 get_text() {
 	local text="$*"
 	[ -z "$text" ] && read -r text
@@ -40,25 +42,30 @@ get_mimetype() {
 	local text=${1:?missing url}
 	case "$text" in
 	http*) printf %s 'x-scheme-handler/http' ;;
-	*) file -bdE --mime-type "$text" ;;
+	*) xdg-mime query filetype "$text" 2>/dev/null || file -bE --mime-type "$text" ;;
+	esac
+}
+get_x-scheme-handler_override() {
+	case "$1" in
+	slack.com) printf %s 'workfox.desktop' ;;
+	*) return 1 ;;
 	esac
 }
 open_default() {
 	local text=${1:?missing url}
 	local application
 	case "$(get_mimetype "$text")" in
-	x-scheme-handler/http*) application=librewolf ;;
+	x-scheme-handler/http*) application=$DEFAULT_BROWSER ;;
 	esac
-	case "$text" in
-	*linear.app*) application=linear ;;
-	*) application=librewolf ;;
-	esac
-	if [ -z "$application" ]; then
-		gio open "$text"
-	else
-		desktop_file=$(rg -Lil -e "$application" -- /usr/share/applications/ /var/lib/flatpak/exports/share/applications/ ~/.local/share/applications/ | tail -1)
-		gio launch "$desktop_file" "$text"
+	application_override=$(get_x-scheme-handler_override "$text" || :)
+	if [ -n "${application_override}" ]; then
+		application=$application_override
+	fi
+	if [ -n "${application:-}" ]; then
+		desktop_file=$(rg -Lil -g "$application" . -- /usr/share/applications/ /var/lib/flatpak/exports/share/applications/ ~/.local/share/applications/ | head -1)
+		exec gio launch "$desktop_file" "$text"
 	fi
+	exec gio open "$text"
 }
 #
 # actions
diff --git a/bin/vpnm b/bin/vpnm
index f09b41a..0fe1224 100755
--- a/bin/vpnm
+++ b/bin/vpnm
@@ -12,9 +12,17 @@ main() {
 	local conn
 
 	conn=$(get_current_connection)
-	[ -n "$conn" ] && fuzl_action_add "down" "󰦝" "Connected to $conn"
+	[ -n "$conn" ] && fuzl_action_add "exitnode_down" "󰦝" "Connected to $conn"
 	fuzl_action_add "networks" "󰾰" "Available networks"
-	fuzl_action_add "profile" "󱗼" "Switch profile"
+
+	profile=$(ts_profile_current)
+	fuzl_action_add "profile" "󱗼" "Profile: $profile"
+
+	if ts_state; then
+		fuzl_action_add "down" "" "Disable tailscale"
+	else
+		fuzl_action_add "up" "" "Enable tailscale"
+	fi
 
 	fuzl_action_menu || return
 }
@@ -23,6 +31,23 @@ main() {
 # helpers
 #
 
+ts_state() {
+	tailscale wait --timeout=1s
+}
+ts_profile_list() {
+	fuzl_try -- tailscale switch --list --json
+}
+ts_profile_current() {
+	ts_profile_list | jq -rc '.[]|select(.selected)|.nickname'
+}
+ts_profile_switch() {
+	local profile=${1?}
+	fuzl_try -- tailscale switch "$profile"
+	sleep 2
+	fuzl_try -- pkill -SIGRTMIN+2 waybar
+
+}
+
 get_emoji_flag() {
 	code=
 	# TODO: calc flag
@@ -68,18 +93,17 @@ get_vpn_connections() {
 get_current_connection() {
 	tailscale exit-node list |
 		grep -e selected |
-		grep -v -e Any | (
-		awk '/.tail2d6ce.ts.net/{print $2, $1}'
-		awk -F '[ ]{2,}' '/.mullvad.ts.net/{print $2, $1, ($3 "/" $4)}'
-	) | while read -r conn ip location; do
-		printf '%s' "$(get_emoji_flag "$conn")"
-		printf ' '
-		if grep -q 'mullvad.ts.net' <<<"$conn"; then
-			printf '%s' "$location"
-		else
-			printf '%s' "$conn"
-		fi
-	done
+		grep -v -e Any |
+		awk -F '[ ]{2,}' '/.mullvad.ts.net/{print $2, $1, ($3 "/" $4)} /.tail2d6ce.ts.net/{print $2, $1}' |
+		while read -r conn ip location; do
+			printf '%s' "$(get_emoji_flag "$conn")"
+			printf ' '
+			if grep -q 'mullvad.ts.net' <<<"$conn"; then
+				printf '%s' "$location"
+			else
+				printf '%s' "$conn"
+			fi
+		done
 }
 
 vpnup() {
@@ -96,7 +120,7 @@ vpndown() {
 # actions
 #
 
-vpnm_down() {
+vpnm_exitnode_down() {
 	fuzl_try -- vpndown
 }
 
@@ -110,15 +134,21 @@ vpnm_networks() {
 }
 
 vpnm_profile() {
-	profile=$(tailscale switch --list 2>/dev/null |
-		tail -n+2 |
-		awk '{printf "%s\t%s\n", $3, $2}' |
-		fuzl_menu $FUZL_MENU_FILTER --accept-nth=1)
+	profile=$(
+		ts_profile_list |
+			jq -rc '.[]|"\(.nickname)\t\(.tailnet)\t\(if .selected then "*" else "" end)"' |
+			fuzl_menu $FUZL_MENU_FILTER --accept-nth=1
+	)
 
 	[ -n "$profile" ] || main
-	fuzl_try -- tailscale switch "$profile"
-	sleep 2
-	fuzl_try -- pkill -SIGRTMIN+2 waybar
+	ts_profile_switch "$profile"
+}
+
+vpnm_up() {
+	fuzl_try -- tailscale up
+}
+vpnm_down() {
+	fuzl_try -- tailscale down
 }
 
 main