summary refs log tree commit diff
diff options
context:
space:
mode:
authorRobert Günzler <r@gnzler.io>2026-04-08 13:18:14 +0900
committerRobert Günzler <r@gnzler.io>2026-04-08 13:18:14 +0900
commitb61b58ce2c1371d6f904f108fa7feed3abfbe5c7 (patch)
tree008a5cf54ecbfffb97ddac30e44c8ab224395ab0
parentd4620f3ac119263fee90bd819eadaf84fc8d50b7 (diff)
*
Automated-commit-by: dots
Signed-off-by: Robert Günzler <r@gnzler.io>
-rw-r--r--.config/foot/colors.ini1
-rw-r--r--.config/foot/foot.ini50
-rw-r--r--.config/foot/themes/koda55
-rw-r--r--.config/foot/themes/nvim56
-rw-r--r--.config/kanshi/config8
-rw-r--r--.config/mako/colors1
-rw-r--r--.config/mako/colors-dark25
-rw-r--r--.config/mako/colors-light25
-rw-r--r--.config/mako/config35
-rw-r--r--.config/mbsyncrc66
-rw-r--r--.config/mimeapps.list7
-rw-r--r--.config/pimsync/pimsync.conf63
-rw-r--r--.config/rbw/config.json1
-rw-r--r--.config/rc/conf.d/swaybg1
-rwxr-xr-x.config/rc/init.d/fcitx57
-rwxr-xr-x.config/rc/init.d/kanshi4
-rwxr-xr-x.config/rc/init.d/mako27
-rwxr-xr-x.config/rc/init.d/pimsync10
-rwxr-xr-x.config/rc/init.d/swaybg4
-rwxr-xr-x.config/rc/init.d/syncthing14
-rwxr-xr-x.config/rc/init.d/udiskie5
-rwxr-xr-x.config/rc/init.d/waybar8
-rwxr-xr-x.config/rc/init.d/xdg-desktop-portal14
-rwxr-xr-x.config/rc/init.d/xdg-desktop-portal-darkman13
-rwxr-xr-x.config/rc/init.d/xdg-desktop-portal-gtk11
-rwxr-xr-x.config/rc/init.d/xdg-desktop-portal-wlr11
-rw-r--r--.config/rc/rc.conf2
l---------.config/rc/runlevels/default/dbus1
l---------.config/rc/runlevels/desktop/default1
l---------.config/rc/runlevels/desktop/fcitx51
l---------.config/rc/runlevels/desktop/foot1
l---------.config/rc/runlevels/desktop/kanshi1
l---------.config/rc/runlevels/desktop/mako1
l---------.config/rc/runlevels/desktop/pipewire1
l---------.config/rc/runlevels/desktop/pipewire-pulse1
l---------.config/rc/runlevels/desktop/swaybg1
l---------.config/rc/runlevels/desktop/udiskie1
l---------.config/rc/runlevels/desktop/waybar1
l---------.config/rc/runlevels/desktop/wireplumber1
l---------.config/rc/runlevels/desktop/xdg-desktop-portal1
l---------.config/rc/runlevels/desktop/xdg-desktop-portal-darkman1
l---------.config/rc/runlevels/desktop/xdg-desktop-portal-gtk1
l---------.config/rc/runlevels/desktop/xdg-desktop-portal-wlr1
l---------.config/rc/runlevels/extras/desktop1
l---------.config/rc/runlevels/extras/pimsync1
l---------.config/rc/runlevels/extras/syncthing1
-rwxr-xr-x.config/senpai/highlight9
-rw-r--r--.config/senpai/senpai.scfg15
-rw-r--r--.config/starship.toml118
-rw-r--r--.config/tridactyl/themes/r.css84
-rw-r--r--.config/tridactyl/tridactylrc141
-rw-r--r--.config/tridactyl/tridactylrc.default38
-rw-r--r--.config/tridactyl/tridactylrc.work4
-rw-r--r--.config/xdg-desktop-portal-wlr/config5
l---------.config/xdg-desktop-portal/portals.conf1
-rw-r--r--.config/xdg-desktop-portal/river-portals.conf2
-rw-r--r--.config/yt-dlp.conf1
-rw-r--r--.local/share/applications/desktop-actions.desktop30
-rw-r--r--.local/share/applications/senpai.desktop14
-rw-r--r--.local/share/applications/urlopen.desktop9
-rw-r--r--.local/share/applications/workfox.desktop24
-rwxr-xr-x.local/share/darkman/bg.sh10
-rwxr-xr-x.local/share/darkman/foot.sh14
-rwxr-xr-x.local/share/darkman/fuzzel.sh12
-rwxr-xr-x.local/share/darkman/gtk3.sh12
-rwxr-xr-x.local/share/darkman/mako.sh14
-rwxr-xr-xbin/2av184
-rwxr-xr-xbin/audm84
-rwxr-xr-xbin/blum100
-rwxr-xr-xbin/dots28
-rwxr-xr-xbin/dskm26
-rwxr-xr-xbin/genup91
-rwxr-xr-xbin/hr22
-rw-r--r--bin/libfuzl.sh114
-rwxr-xr-xbin/mailsync148
-rwxr-xr-xbin/mktoes27
l---------bin/msmtp-queue1
-rwxr-xr-xbin/msmtp-queue-stat17
l---------bin/msmtpq1
-rwxr-xr-xbin/netm1560
-rwxr-xr-xbin/pasm71
-rwxr-xr-xbin/play-mpv112
-rwxr-xr-xbin/play-music23
-rwxr-xr-xbin/rsyncez19
-rwxr-xr-xbin/susm30
-rwxr-xr-xbin/svcm108
-rwxr-xr-xbin/todo216
-rwxr-xr-xbin/urlopen89
-rwxr-xr-xbin/vpnm124
89 files changed, 4195 insertions, 0 deletions
diff --git a/.config/foot/colors.ini b/.config/foot/colors.ini
new file mode 100644
index 0000000..cfa0f38
--- /dev/null
+++ b/.config/foot/colors.ini
@@ -0,0 +1 @@
+initial-color-theme=2
diff --git a/.config/foot/foot.ini b/.config/foot/foot.ini
new file mode 100644
index 0000000..1265616
--- /dev/null
+++ b/.config/foot/foot.ini
@@ -0,0 +1,50 @@
+# vim: commentstring=#\ %s
+include=~/.config/foot/colors.ini
+include=~/.config/foot/themes/koda
+
+# ::start style
+font=DejaVu Sans Mono, Symbols Nerd Font Mono, Noto Color Emoji
+# ::end style
+
+term=foot-direct
+bold-text-in-bright=no
+box-drawings-uses-font-glyphs=no
+resize-delay-ms=0
+initial-window-size-pixels=2560x1440
+dpi-aware=no
+pad=2x2
+
+[tweak]
+grapheme-shaping=yes
+overflowing-glyphs=yes
+font-monospace-warn=no
+max-shm-pool-size-mb=1024
+
+[colors]
+# color overwrites
+alpha=0.99
+
+[bell]
+urgent=yes
+notify=no
+
+[scrollback]
+lines=1000
+
+[mouse]
+hide-when-typing=no
+alternate-scroll-mode=no
+
+[url]
+osc8-underline=always
+
+[url-bindings]
+cancel=Escape
+
+[regex:paths]
+regex=^((\/|\~\/)([a-zA-Z0-9_\-\.]*\/?)+)
+launch=urlopen '${match}'
+
+[key-bindings]
+regex-launch=[paths] Control+Shift+p
+pipe-scrollback=[sh -c "f=$(mktemp) && cat - > $f && foot $EDITOR -t $f; rm $f"] Control+Shift+e
diff --git a/.config/foot/themes/koda b/.config/foot/themes/koda
new file mode 100644
index 0000000..d7a7591
--- /dev/null
+++ b/.config/foot/themes/koda
@@ -0,0 +1,55 @@
+# -*- conf -*-
+# Koda theme
+# See: https://github.com/oskarnurm/koda.nvim
+
+[colors]
+cursor=101010 b0b0b0
+foreground=ffffff
+background=101010
+
+selection-foreground=b0b0b0
+selection-background=272727
+
+regular0=101010
+regular1=ff7676
+regular2=a3d6a3
+regular3=ffffff
+regular4=b3b3b3
+regular5=f4b8e4
+regular6=fafafa
+regular7=a5adce
+
+bright0=666666
+bright1=ff5733
+bright2=86cd82
+bright3=d9ba73
+bright4=ffffff
+bright5=f2a4db
+bright6=5abfb5
+bright7=b5bfe2
+
+[colors2]
+cursor=ebebeb 6a6a6a
+foreground=101010
+background=faf9f5
+
+selection-foreground=101010
+selection-background=ebebeb
+
+regular0=e2e2e2
+regular1=ca0043
+regular2=407f00
+regular3=926200
+regular4=3d3d3d
+regular5=a200d1
+regular6=007d7d
+regular7=6a6a6a
+
+bright0=ababab
+bright1=f30052
+bright2=4f9a00
+bright3=b07700
+bright4=000000
+bright5=c301fb
+bright6=009797
+bright7=101010
diff --git a/.config/foot/themes/nvim b/.config/foot/themes/nvim
new file mode 100644
index 0000000..bf629c0
--- /dev/null
+++ b/.config/foot/themes/nvim
@@ -0,0 +1,56 @@
+# -*- conf -*-
+# Neovim Dark theme
+# Uses the dark color palette from the default Neovim color scheme
+# See: https://github.com/neovim/neovim/blob/fb6c059dc55c8d594102937be4dd70f5ff51614a/src/nvim/highlight_group.c#L419
+
+[colors]
+cursor=14161b e0e2ea # NvimDarkGrey2 NvimLightGrey2
+foreground=e0e2ea # NvimLightGrey2
+background=14161b # NvimDarkGrey2
+
+selection-foreground=e0e2ea # NvimLightGrey2
+selection-background=4f5258 # NvimDarkGrey4
+
+regular0=07080d # NvimDarkGrey1
+regular1=ffc0b9 # NvimLightRed
+regular2=b3f6c0 # NvimLightGreen
+regular3=fce094 # NvimLightYellow
+regular4=a6dbff # NvimLightBlue
+regular5=ffcaff # NvimLightMagenta
+regular6=8cf8f7 # NvimLightCyan
+regular7=c4c6cd # NvimLightGrey3
+
+bright0=2c2e33 # NvimDarkGrey3
+bright1=ffc0b9 # NvimLightRed
+bright2=b3f6c0 # NvimLightGreen
+bright3=fce094 # NvimLightYellow
+bright4=a6dbff # NvimLightBlue
+bright5=ffcaff # NvimLightMagenta
+bright6=8cf8f7 # NvimLightCyan
+bright7=eef1f8 # NvimLightGrey1
+
+[colors2]
+cursor=e0e2ea 14161b # NvimLightGrey2 NvimDarkGrey2
+foreground=14161b # NvimDarkGrey2
+background=e0e2ea # NvimLightGrey2
+
+selection-foreground=14161b # NvimDarkGrey2
+selection-background=9b9ea4 # NvimLightGrey4
+
+regular0=eef1f8 # NvimLightGrey1
+regular1=590008 # NvimDarkRed
+regular2=005523 # NvimDarkGreen
+regular3=6b5300 # NvimDarkYellow
+regular4=004c73 # NvimDarkBlue
+regular5=470045 # NvimDarkMagenta
+regular6=007373 # NvimDarkCyan
+regular7=2c2e33 # NvimDarkGrey3
+
+bright0=c4c6cd # NvimLightGrey3
+bright1=590008 # NvimDarkRed
+bright2=005523 # NvimDarkGreen
+bright3=6b5300 # NvimDarkYellow
+bright4=004c73 # NvimDarkBlue
+bright5=470045 # NvimDarkMagenta
+bright6=007373 # NvimDarkCyan
+bright7=07080d # NvimDarkGrey1
diff --git a/.config/kanshi/config b/.config/kanshi/config
new file mode 100644
index 0000000..9fac1c6
--- /dev/null
+++ b/.config/kanshi/config
@@ -0,0 +1,8 @@
+profile laptop {
+    output eDP-1 scale 2.0 enable
+}
+
+profile single {
+    output "Dell Inc. DELL U2518D 3C4YP87AAK3L" mode 2560x1440 position 0,0 transform normal scale 1 enable
+    output eDP-1 position 2560,0 scale 2.0 enable
+}
diff --git a/.config/mako/colors b/.config/mako/colors
new file mode 100644
index 0000000..8d074ce
--- /dev/null
+++ b/.config/mako/colors
@@ -0,0 +1 @@
+include=~/.config/mako/colors-light
diff --git a/.config/mako/colors-dark b/.config/mako/colors-dark
new file mode 100644
index 0000000..cca216d
--- /dev/null
+++ b/.config/mako/colors-dark
@@ -0,0 +1,25 @@
+font=DejaVu Sans Mono 8
+background-color=#080808
+text-color=#e0e2ea
+border-color=#70649a
+progress-color=source #70649a
+
+[urgency=high]
+border-size=2
+border-color=#ffc0b9
+
+[category=error]
+border-size=2
+border-color=#ffc0b9
+
+[category=warning]
+border-size=2
+border-color=#fce094
+
+[category=success]
+border-size=2
+border-color=#b3f6c0
+
+[category=alert]
+border-size=0
+background-color=#590008
diff --git a/.config/mako/colors-light b/.config/mako/colors-light
new file mode 100644
index 0000000..a07a751
--- /dev/null
+++ b/.config/mako/colors-light
@@ -0,0 +1,25 @@
+font=DejaVu Sans Mono 8
+background-color=#fffffb
+text-color=#080808
+border-color=#70649a
+progress-color=source #70649a
+
+[urgency=high]
+border-size=2
+border-color=#ffc0b9
+
+[category=error]
+border-size=2
+border-color=#ffc0b9
+
+[category=warning]
+border-size=2
+border-color=#fce094
+
+[category=success]
+border-size=2
+border-color=#b3f6c0
+
+[category=alert]
+border-size=0
+background-color=#ffc0b9
diff --git a/.config/mako/config b/.config/mako/config
new file mode 100644
index 0000000..0329579
--- /dev/null
+++ b/.config/mako/config
@@ -0,0 +1,35 @@
+include=~/.config/mako/colors
+
+group-by=app-name,summary
+default-timeout=2000
+max-history=20
+max-icon-size=32
+max-visible=6
+
+border-size=1
+padding=6
+
+# on-button-right=exec makoctl menu -- fuzzel -d -p '󱧌 select action '
+
+# [urgency=high]
+# # on-notify=exec pw-play --media-role=event --volume=0.5 $HOME/music/bell.wav
+#
+# [category=error]
+# # on-notify=exec pw-play --media-role=event --volume=0.5 $HOME/music/bell.wav
+#
+# [category=warning]
+# # on-notify=exec pw-play --media-role=event --volume=0.5 $HOME/music/bell.wav
+#
+# [category=success]
+# # on-notify=exec pw-play --media-role=event --volume=0.5 $HOME/music/bell.wav
+#
+# [category=alert]
+# anchor=top-center
+# # on-notify=exec pw-play --media-role=event --volume=0.5 $HOME/music/bell.wav
+#
+# [summary=reminder]
+# # on-notify=exec pw-play --media-role=event --volume=0.5 $HOME/music/bell.wav
+#
+#
+# [mode=dnd]
+# invisible=1
diff --git a/.config/mbsyncrc b/.config/mbsyncrc
new file mode 100644
index 0000000..7ace06a
--- /dev/null
+++ b/.config/mbsyncrc
@@ -0,0 +1,66 @@
+## remote
+
+IMAPStore gnzler
+Host imap.mailfence.com
+User robert@gnzler.de
+PassCmd "rbw get 68b0ea26-733a-43cd-b8f8-e8ecc98de072"
+TLSType IMAPS
+
+IMAPStore gandi
+Host mail.gandi.net
+User robert@gnzler.io
+PassCmd "rbw get 67436053-fde1-43c3-9544-6a5289f08c76"
+TLSType IMAPS
+
+# IMAPStore selfhosted
+# Host localhost:1430
+# User postmaster@gnzler.de
+# PassCmd "secretmenu -P postmaster@gnzler.io | head -n1"
+# TLSType None
+
+## on-disk
+
+MaildirStore local
+Path ~/mail/gnzler/
+INBOX ~/mail/gnzler/INBOX
+SubFolders Verbatim
+
+MaildirStore local.bak
+Path ~/mail/gnzler.bak/
+INBOX ~/mail/gnzler.bak/INBOX
+SubFolders Verbatim
+
+## channel
+
+Channel gnzler
+Far :gnzler:
+Near :local:
+Patterns *
+# Sync Pull New
+SyncState *
+# WATCH OUT WHAT YOU'RE DOING
+Create Both
+Remove Both
+Expunge Both
+
+## backup/restore
+
+Channel pull
+Far :gandi:
+Near :local.bak:
+Patterns *
+SyncState *
+Sync Pull
+Create Near
+Remove Near
+Expunge Near
+
+Channel restore
+Far :gnzler:
+Near :local:
+Patterns lists lists/*
+SyncState *
+Sync Push
+Create Far
+Remove Far
+Expunge Far
diff --git a/.config/mimeapps.list b/.config/mimeapps.list
new file mode 100644
index 0000000..013ffc9
--- /dev/null
+++ b/.config/mimeapps.list
@@ -0,0 +1,7 @@
+[Default Applications]
+x-scheme-handler/https=urlopen.desktop
+x-scheme-handler/http=urlopen.desktop
+application/pdf=org.gnome.Papers.desktop;
+
+[Added Associations]
+application/rss+xml=nvim.desktop;
diff --git a/.config/pimsync/pimsync.conf b/.config/pimsync/pimsync.conf
new file mode 100644
index 0000000..8fed018
--- /dev/null
+++ b/.config/pimsync/pimsync.conf
@@ -0,0 +1,63 @@
+# A folder where vdirsyncer can store some metadata about each pair.
+status_path "~/.local/state/pimsync/status/"
+
+# PAIRS
+
+pair calendars_robert {
+	storage_a calendars_robert_local
+	storage_b gzr_robert_calendar_remote
+	collections all
+}
+
+pair contacts_robert {
+	storage_a contacts_local
+	storage_b gzr_robert_contacts_remote
+	collections all
+}
+
+pair calendars_goro {
+	storage_a calendars_goro_local
+	storage_b gzr_goro_calendar_remote
+	# NOTE: only sync events
+	collection events
+}
+
+# STORAGE LOCAL
+
+storage calendars_robert_local {
+	type vdir/icalendar
+	path ~/calendars/robert/
+	fileext ics
+}
+
+storage calendars_goro_local {
+	type vdir/icalendar
+	path ~/calendars/goro/
+	fileext ics
+}
+
+storage contacts_local {
+	type vdir/vcard
+	path ~/contacts/
+	fileext vcf
+}
+
+# STORAGE GZR
+
+storage gzr_robert_calendar_remote {
+	type caldav
+	url https://cal.gzr.im/dav/
+	username robert
+}
+
+storage gzr_robert_contacts_remote {
+	type carddav
+	url https://cal.gzr.im/dav/
+	username robert
+}
+
+storage gzr_goro_calendar_remote {
+	type caldav
+	url https://cal.gzr.im/dav/
+	username goro
+}
diff --git a/.config/rbw/config.json b/.config/rbw/config.json
new file mode 100644
index 0000000..e85dafe
--- /dev/null
+++ b/.config/rbw/config.json
@@ -0,0 +1 @@
+{"email":"robert@gnzler.io","base_url":"https://vault.gzr.im","identity_url":null,"lock_timeout":3600}
diff --git a/.config/rc/conf.d/swaybg b/.config/rc/conf.d/swaybg
new file mode 100644
index 0000000..7a1d4db
--- /dev/null
+++ b/.config/rc/conf.d/swaybg
@@ -0,0 +1 @@
+command_args="--output=* --color=#70649a --image='${XDG_PICTURES_DIR}/bg.jpg' --mode=fill"
diff --git a/.config/rc/init.d/fcitx5 b/.config/rc/init.d/fcitx5
new file mode 100755
index 0000000..c28da7d
--- /dev/null
+++ b/.config/rc/init.d/fcitx5
@@ -0,0 +1,7 @@
+#!/sbin/openrc-run
+DBUS_SESSION_BUS_ADDRESS="unix:path=${XDG_RUNTIME_DIR}/bus"
+
+command="/usr/bin/fcitx5"
+command_args="--replace"
+supervisor=supervise-daemon
+error_logger="logger -t '${RC_SVCNAME}' -p daemon.error"
diff --git a/.config/rc/init.d/kanshi b/.config/rc/init.d/kanshi
new file mode 100755
index 0000000..dae7403
--- /dev/null
+++ b/.config/rc/init.d/kanshi
@@ -0,0 +1,4 @@
+#!/sbin/openrc-run
+command="/usr/bin/kanshi"
+supervisor=supervise-daemon
+error_logger="logger -t '${RC_SVCNAME}' -p daemon.error"
diff --git a/.config/rc/init.d/mako b/.config/rc/init.d/mako
new file mode 100755
index 0000000..40e8ec3
--- /dev/null
+++ b/.config/rc/init.d/mako
@@ -0,0 +1,27 @@
+#!/sbin/openrc-run
+DBUS_SESSION_BUS_ADDRESS="unix:path=${XDG_RUNTIME_DIR}/bus"
+
+description="Lightweight Wayland notification daemon"
+
+command="/usr/bin/mako"
+supervisor=supervise-daemon
+error_logger="logger -t '${RC_SVCNAME}' -p daemon.error"
+
+depend() {
+	need dbus
+	provide notification-daemon
+}
+
+start_pre() {
+	if [ -z "$WAYLAND_DISPLAY" ]; then
+		eerror "\$WAYLAND_DISPLAY unset, can't proceed."
+		return 1
+	fi
+}
+
+extra_started_commands="reload"
+description_reload="Reload mako configuration using makoctl"
+reload() {
+	makoctl reload
+	eend $? "Reloaded mako configuration"
+}
diff --git a/.config/rc/init.d/pimsync b/.config/rc/init.d/pimsync
new file mode 100755
index 0000000..3ff1112
--- /dev/null
+++ b/.config/rc/init.d/pimsync
@@ -0,0 +1,10 @@
+#!/sbin/openrc-run
+
+description="Synchronise calendars and contacts"
+
+command="/bin/pimsync"
+command_args="daemon -r 3"
+notify=fd:3
+
+supervisor=supervise-daemon
+error_logger="logger -t '${RC_SVCNAME}' -p daemon.error"
diff --git a/.config/rc/init.d/swaybg b/.config/rc/init.d/swaybg
new file mode 100755
index 0000000..02f85e8
--- /dev/null
+++ b/.config/rc/init.d/swaybg
@@ -0,0 +1,4 @@
+#!/sbin/openrc-run
+command="/usr/bin/swaybg"
+supervisor=supervise-daemon
+error_logger="logger -t '${RC_SVCNAME}' -p daemon.error"
diff --git a/.config/rc/init.d/syncthing b/.config/rc/init.d/syncthing
new file mode 100755
index 0000000..083b092
--- /dev/null
+++ b/.config/rc/init.d/syncthing
@@ -0,0 +1,14 @@
+#!/sbin/openrc-run
+# Copyright 1999-2025 Gentoo Authors
+# Distributed under the terms of the GNU General Public License, v2 or later
+
+supervisor=supervise-daemon
+command="/usr/bin/syncthing"
+command_args="serve --no-browser --no-restart --logflags=0"
+error_logger="logger -t '${RC_SVCNAME}' -p daemon.error"
+
+extra_started_commands="browser"
+browser() {
+	syncthing browser
+	eend $? "Opened syncthing gui in the browser"
+}
diff --git a/.config/rc/init.d/udiskie b/.config/rc/init.d/udiskie
new file mode 100755
index 0000000..55a521b
--- /dev/null
+++ b/.config/rc/init.d/udiskie
@@ -0,0 +1,5 @@
+#!/sbin/openrc-run
+command="/usr/bin/udiskie"
+command_args="--config=$XDG_CONFIG_HOME/udiskie/config.yml --smart-tray"
+supervisor=supervise-daemon
+error_logger="logger -t '${RC_SVCNAME}' -p daemon.error"
diff --git a/.config/rc/init.d/waybar b/.config/rc/init.d/waybar
new file mode 100755
index 0000000..7eba0da
--- /dev/null
+++ b/.config/rc/init.d/waybar
@@ -0,0 +1,8 @@
+#!/sbin/openrc-run
+command="/usr/bin/waybar"
+supervisor=supervise-daemon
+error_logger="logger -t '${RC_SVCNAME}' -p daemon.error"
+
+depend() {
+	need pipewire-pulse wireplumber
+}
diff --git a/.config/rc/init.d/xdg-desktop-portal b/.config/rc/init.d/xdg-desktop-portal
new file mode 100755
index 0000000..aeacb00
--- /dev/null
+++ b/.config/rc/init.d/xdg-desktop-portal
@@ -0,0 +1,14 @@
+#!/sbin/openrc-run
+DBUS_SESSION_BUS_ADDRESS="unix:path=${XDG_RUNTIME_DIR}/bus"
+
+supervisor=supervise-daemon
+command="/usr/libexec/xdg-desktop-portal"
+command_args="--replace $command_args"
+error_logger="logger -t '${RC_SVCNAME}' -p daemon.error"
+
+depend() {
+	need dbus \
+		xdg-desktop-portal-gtk \
+		xdg-desktop-portal-wlr \
+		xdg-desktop-portal-darkman
+}
diff --git a/.config/rc/init.d/xdg-desktop-portal-darkman b/.config/rc/init.d/xdg-desktop-portal-darkman
new file mode 100755
index 0000000..8067c95
--- /dev/null
+++ b/.config/rc/init.d/xdg-desktop-portal-darkman
@@ -0,0 +1,13 @@
+#!/sbin/openrc-run
+
+name="darkman"
+description="Control dark mode and light mode transitions"
+
+supervisor=supervise-daemon
+command="/usr/bin/darkman"
+command_args="run"
+error_logger="logger -t '${RC_SVCNAME}' -p daemon.error"
+
+depend() {
+	need dbus
+}
diff --git a/.config/rc/init.d/xdg-desktop-portal-gtk b/.config/rc/init.d/xdg-desktop-portal-gtk
new file mode 100755
index 0000000..91bcae1
--- /dev/null
+++ b/.config/rc/init.d/xdg-desktop-portal-gtk
@@ -0,0 +1,11 @@
+#!/sbin/openrc-run
+DBUS_SESSION_BUS_ADDRESS="unix:path=${XDG_RUNTIME_DIR}/bus"
+
+supervisor=supervise-daemon
+command="/usr/libexec/xdg-desktop-portal-gtk"
+command_args="--replace"
+error_logger="logger -t '${RC_SVCNAME}' -p daemon.error"
+
+depend() {
+	need dbus
+}
diff --git a/.config/rc/init.d/xdg-desktop-portal-wlr b/.config/rc/init.d/xdg-desktop-portal-wlr
new file mode 100755
index 0000000..c041a05
--- /dev/null
+++ b/.config/rc/init.d/xdg-desktop-portal-wlr
@@ -0,0 +1,11 @@
+#!/sbin/openrc-run
+DBUS_SESSION_BUS_ADDRESS="unix:path=${XDG_RUNTIME_DIR}/bus"
+
+supervisor=supervise-daemon
+command="/usr/libexec/xdg-desktop-portal-wlr"
+command_args="--replace"
+error_logger="logger -t '${RC_SVCNAME}' -p daemon.error"
+
+depend() {
+	need dbus
+}
diff --git a/.config/rc/rc.conf b/.config/rc/rc.conf
new file mode 100644
index 0000000..8e5e305
--- /dev/null
+++ b/.config/rc/rc.conf
@@ -0,0 +1,2 @@
+rc_default_runlevel="default"
+rc_env_allow="*"
diff --git a/.config/rc/runlevels/default/dbus b/.config/rc/runlevels/default/dbus
new file mode 120000
index 0000000..779b9fb
--- /dev/null
+++ b/.config/rc/runlevels/default/dbus
@@ -0,0 +1 @@
+/etc/user/init.d/dbus
\ No newline at end of file
diff --git a/.config/rc/runlevels/desktop/default b/.config/rc/runlevels/desktop/default
new file mode 120000
index 0000000..446d58f
--- /dev/null
+++ b/.config/rc/runlevels/desktop/default
@@ -0,0 +1 @@
+../default
\ No newline at end of file
diff --git a/.config/rc/runlevels/desktop/fcitx5 b/.config/rc/runlevels/desktop/fcitx5
new file mode 120000
index 0000000..a7b382d
--- /dev/null
+++ b/.config/rc/runlevels/desktop/fcitx5
@@ -0,0 +1 @@
+/home/robert/.config/rc/init.d/fcitx5
\ No newline at end of file
diff --git a/.config/rc/runlevels/desktop/foot b/.config/rc/runlevels/desktop/foot
new file mode 120000
index 0000000..81b4c9f
--- /dev/null
+++ b/.config/rc/runlevels/desktop/foot
@@ -0,0 +1 @@
+/etc/user/init.d/foot
\ No newline at end of file
diff --git a/.config/rc/runlevels/desktop/kanshi b/.config/rc/runlevels/desktop/kanshi
new file mode 120000
index 0000000..2cb87ce
--- /dev/null
+++ b/.config/rc/runlevels/desktop/kanshi
@@ -0,0 +1 @@
+/home/robert/.config/rc/init.d/kanshi
\ No newline at end of file
diff --git a/.config/rc/runlevels/desktop/mako b/.config/rc/runlevels/desktop/mako
new file mode 120000
index 0000000..5b74415
--- /dev/null
+++ b/.config/rc/runlevels/desktop/mako
@@ -0,0 +1 @@
+/home/robert/.config/rc/init.d/mako
\ No newline at end of file
diff --git a/.config/rc/runlevels/desktop/pipewire b/.config/rc/runlevels/desktop/pipewire
new file mode 120000
index 0000000..1eeaabd
--- /dev/null
+++ b/.config/rc/runlevels/desktop/pipewire
@@ -0,0 +1 @@
+/etc/user/init.d/pipewire
\ No newline at end of file
diff --git a/.config/rc/runlevels/desktop/pipewire-pulse b/.config/rc/runlevels/desktop/pipewire-pulse
new file mode 120000
index 0000000..516d346
--- /dev/null
+++ b/.config/rc/runlevels/desktop/pipewire-pulse
@@ -0,0 +1 @@
+/etc/user/init.d/pipewire-pulse
\ No newline at end of file
diff --git a/.config/rc/runlevels/desktop/swaybg b/.config/rc/runlevels/desktop/swaybg
new file mode 120000
index 0000000..0718212
--- /dev/null
+++ b/.config/rc/runlevels/desktop/swaybg
@@ -0,0 +1 @@
+/home/robert/.config/rc/init.d/swaybg
\ No newline at end of file
diff --git a/.config/rc/runlevels/desktop/udiskie b/.config/rc/runlevels/desktop/udiskie
new file mode 120000
index 0000000..079b70f
--- /dev/null
+++ b/.config/rc/runlevels/desktop/udiskie
@@ -0,0 +1 @@
+/home/robert/.config/rc/init.d/udiskie
\ No newline at end of file
diff --git a/.config/rc/runlevels/desktop/waybar b/.config/rc/runlevels/desktop/waybar
new file mode 120000
index 0000000..8a3aa9e
--- /dev/null
+++ b/.config/rc/runlevels/desktop/waybar
@@ -0,0 +1 @@
+/home/robert/.config/rc/init.d/waybar
\ No newline at end of file
diff --git a/.config/rc/runlevels/desktop/wireplumber b/.config/rc/runlevels/desktop/wireplumber
new file mode 120000
index 0000000..a90f273
--- /dev/null
+++ b/.config/rc/runlevels/desktop/wireplumber
@@ -0,0 +1 @@
+/etc/user/init.d/wireplumber
\ No newline at end of file
diff --git a/.config/rc/runlevels/desktop/xdg-desktop-portal b/.config/rc/runlevels/desktop/xdg-desktop-portal
new file mode 120000
index 0000000..23c0d95
--- /dev/null
+++ b/.config/rc/runlevels/desktop/xdg-desktop-portal
@@ -0,0 +1 @@
+/home/robert/.config/rc/init.d/xdg-desktop-portal
\ No newline at end of file
diff --git a/.config/rc/runlevels/desktop/xdg-desktop-portal-darkman b/.config/rc/runlevels/desktop/xdg-desktop-portal-darkman
new file mode 120000
index 0000000..c08fa19
--- /dev/null
+++ b/.config/rc/runlevels/desktop/xdg-desktop-portal-darkman
@@ -0,0 +1 @@
+/home/robert/.config/rc/init.d/xdg-desktop-portal-darkman
\ No newline at end of file
diff --git a/.config/rc/runlevels/desktop/xdg-desktop-portal-gtk b/.config/rc/runlevels/desktop/xdg-desktop-portal-gtk
new file mode 120000
index 0000000..1712823
--- /dev/null
+++ b/.config/rc/runlevels/desktop/xdg-desktop-portal-gtk
@@ -0,0 +1 @@
+/home/robert/.config/rc/init.d/xdg-desktop-portal-gtk
\ No newline at end of file
diff --git a/.config/rc/runlevels/desktop/xdg-desktop-portal-wlr b/.config/rc/runlevels/desktop/xdg-desktop-portal-wlr
new file mode 120000
index 0000000..683895c
--- /dev/null
+++ b/.config/rc/runlevels/desktop/xdg-desktop-portal-wlr
@@ -0,0 +1 @@
+/home/robert/.config/rc/init.d/xdg-desktop-portal-wlr
\ No newline at end of file
diff --git a/.config/rc/runlevels/extras/desktop b/.config/rc/runlevels/extras/desktop
new file mode 120000
index 0000000..2c7c348
--- /dev/null
+++ b/.config/rc/runlevels/extras/desktop
@@ -0,0 +1 @@
+../desktop
\ No newline at end of file
diff --git a/.config/rc/runlevels/extras/pimsync b/.config/rc/runlevels/extras/pimsync
new file mode 120000
index 0000000..daafb20
--- /dev/null
+++ b/.config/rc/runlevels/extras/pimsync
@@ -0,0 +1 @@
+/home/robert/.config/rc/init.d/pimsync
\ No newline at end of file
diff --git a/.config/rc/runlevels/extras/syncthing b/.config/rc/runlevels/extras/syncthing
new file mode 120000
index 0000000..d63c652
--- /dev/null
+++ b/.config/rc/runlevels/extras/syncthing
@@ -0,0 +1 @@
+/home/robert/.config/rc/init.d/syncthing
\ No newline at end of file
diff --git a/.config/senpai/highlight b/.config/senpai/highlight
new file mode 100755
index 0000000..3f2f3e9
--- /dev/null
+++ b/.config/senpai/highlight
@@ -0,0 +1,9 @@
+#!/bin/sh
+set -ex
+escape() {
+	printf "%s" "$1" | sed 's#\\#\\\\#g'
+}
+
+fyi -a senpai -i senpai -c reminder \
+	"[$BUFFER] $SENDER" \
+	"$(escape "$MESSAGE")"
diff --git a/.config/senpai/senpai.scfg b/.config/senpai/senpai.scfg
new file mode 100644
index 0000000..6d5044f
--- /dev/null
+++ b/.config/senpai/senpai.scfg
@@ -0,0 +1,15 @@
+address chat.sr.ht
+username robertgzr@hibiki
+nickname robertgzr
+password-cmd rbw get f283788a-7798-4fa8-82c5-1a3e7f560fbf
+
+pane-widths {
+	nicknames 12
+}
+
+colors {
+	nicks base
+	nicks fixed {
+		self cyan
+	}
+}
diff --git a/.config/starship.toml b/.config/starship.toml
new file mode 100644
index 0000000..724526a
--- /dev/null
+++ b/.config/starship.toml
@@ -0,0 +1,118 @@
+"$schema" = 'https://starship.rs/config-schema.json'
+
+format = """
+⋮ \
+$container\
+$username\
+$hostname\
+$directory\
+\
+$git_branch\
+$git_commit\
+$git_state\
+\
+$golang\
+$nodejs\
+$python\
+$rust\
+$zig\
+\
+$jobs\
+$status\
+$cmd_duration\
+\
+$character\
+"""
+
+add_newline = false
+command_timeout = 100
+
+[container]
+symbol = ' '
+format = '[$symbol\[$name\]]($style) '
+disabled = false
+
+[username]
+format = '[$user@]($style)'
+style_user = 'dimmed'
+disabled = false
+
+[hostname]
+format = '[$hostname]($style) '
+style = 'dimmed'
+disabled = false
+
+[directory]
+truncation_length = 0
+truncate_to_repo = false
+style = "yellow"
+read_only = ''
+disabled = false
+
+[git_branch]
+format = '[$symbol$branch]($style) '
+disabled = false
+
+[git_commit]
+disabled = true
+
+[git_state]
+disabled = false
+
+[python]
+detect_extensions = []
+symbol = '󰌠 '
+format = '[${symbol}${pyenv_prefix}(${version})(\($virtualenv\))]($style) '
+disabled = true
+
+[nodejs]
+detect_extensions = []
+symbol = '󰌞 '
+format = '[$symbol($version)]($style) '
+disabled = true
+
+[golang]
+detect_extensions = []
+symbol = ' '
+format = '[$symbol($version)]($style) '
+disabled = true
+
+[rust]
+detect_extensions = []
+symbol = ' '
+format = '[$symbol($version)]($style) '
+disabled = true
+
+[zig]
+detect_extensions = []
+detect_files = ['build.zig.zon']
+symbol = ' '
+format = '[$symbol($version)]($style) '
+disabled = true
+
+[jobs]
+symbol = '∆'
+disabled = false
+
+[shlvl]
+symbol = '☰'
+disabled = false
+
+[status]
+symbol = '󰐰 $status'
+not_executable_symbol = '󰜺 '
+not_found_symbol = '󰍉 '
+sigint_symbol = '^C'
+signal_symbol = ' $status'
+format = '[$symbol]($style) '
+map_symbol = true
+disabled = false
+
+[cmd_duration]
+min_time = 1_200_000 # 100 * 60 * 20
+disabled = false
+
+[character]
+format = '$symbol '
+success_symbol = '[❯](bold)'
+error_symbol = '[❯](bold red)'
diff --git a/.config/tridactyl/themes/r.css b/.config/tridactyl/themes/r.css
new file mode 100644
index 0000000..05b47a6
--- /dev/null
+++ b/.config/tridactyl/themes/r.css
@@ -0,0 +1,84 @@
+/* vim: fdm=marker
+ * */
+:root.TridactylThemeR {
+    --tridactyl-fg: #f9f9fa;
+    --tridactyl-bg: #2a2a2e;
+
+    --tridactyl-scrollbar-color: var(--tridactyl-fg);
+
+    --tridactyl-cmdl-fg: #d7d7db;
+    --tridactyl-cmdl-bg: var(--tridactyl-bg);
+
+    --tridactyl-header-first-bg: #38383d;
+    --tridactyl-header-second-bg: #2a2a2e;
+    --tridactyl-header-third-bg: #0c0c0d;
+
+    --tridactyl-cmplt-border-top: 1px solid #2a2a2e;
+
+    --tridactyl-url-fg: #86de74;
+    --tridactyl-url-bg: var(--tridactyl-bg);
+
+    --tridactyl-of-fg: #fce19f;
+    --tridactyl-of-bg: var(--tridactyl-bg);
+
+    --tridactyl-hintspan-fg: #f9f9fa;
+    --tridactyl-hintspan-bg: rgb(185, 142, 255);
+
+    --tridactyl-hint-fg: var(--tridactyl-hintspan-fg);
+    --tridactyl-hint-bg: rgba(185, 142, 255, 0.15);
+    --tridactyl-hint-outline: 1px solid var(--tridactyl-hintspan-bg);
+
+    --tridactyl-hint-active-fg: var(--tridactyl-hint-fg);
+    --tridactyl-hint-active-bg: #fff89e;
+    --tridactyl-hint-active-outline: 2px solid #fff89e;
+
+
+    --tridactyl-highlight-box-fg: var(--tridactyl-bg);
+    --tridactyl-highlight-box-bg: var(--tridactyl-fg);
+}
+
+:root.TridactylThemeR.TridactylOwnNamespace input[id^="spoiler"] ~ .spoiler {
+    border-color: var(--tridactyl-bg);
+}
+
+:root.TridactylThemeR.TridactylOwnNamespace a {
+    color: var(--tridactyl-hintspan-bg);
+}
+
+:root.TridactylThemeR #command-line-holder {
+    order: 1;
+}
+
+:root.TridactylThemeR #completions {
+    order: 2;
+    border-top: 1px solid var(--tridactyl-complt-bg);
+}
+
+:root.TridactylThemeR #completions .sectionHeader {
+    display: none;
+}
+
+:root.TridactylThemeR #tridactyl-colon::before {
+    margin-left: 2px;
+    content: " > ";
+}
+
+:root.TridactylThemeR #completions .focused,
+:root.TridactylThemeR #completions .focused .url {
+    color: var(--tridactyl-hintspan-bg);
+    background: var(--tridactyl-hint-bg);
+    text-decoration: italic;
+}
+
+:root.TridactylThemeR #cmdline_iframe {
+    position: fixed !important;
+    bottom: unset;
+    top: -1px !important;
+    left: 0% !important;
+    box-shadow: rgba(0, 0, 0, 0.5) 0px 0px 15px !important;
+    /* opacity: 0.9; */
+}
+
+:root.TridactylThemeR.TridactylOwnNamespace input[id^="spoiler"] ~ .spoiler {
+    border-color: var(--tridactyl-hintspan-bg);
+}
diff --git a/.config/tridactyl/tridactylrc b/.config/tridactyl/tridactylrc
new file mode 100644
index 0000000..526b7eb
--- /dev/null
+++ b/.config/tridactyl/tridactylrc
@@ -0,0 +1,141 @@
+" vim: ft=vim
+sanitise tridactyllocal tridactylsync
+colors r
+
+set browser librewolf
+set allowautofocus false
+set searchengine duckduckgonoai
+set editorcmd foot -a foot-float -T tridactyl-editor -- nvim
+set historyresults 50
+" set newtab https://html.duckduckgo.com
+set allowautofocus true
+set newtabfocus page
+set leavegithubalone true
+" set blacklistkeys ["/","<Space>",";"]
+
+set modeindicator true
+set modeindicatorshowkeys true
+
+" remove annoying binds
+unbind <C-f>
+unbind <C-a>
+unbind --mode=ex <C-a> " select all in cmdline
+unbind --mode=browser <C-,>
+
+bind n findnext 1
+bind N findnext -1
+
+" hint: mpv/video
+set exaliases.mpvsafe js -p tri.excmds.shellescape(JS_ARG).then(u => tri.excmds.exclaim_quiet(`play-mpv ${u}`))
+bind gv composite get_current_url | mpvsafe
+bind yv composite getclip | mpvsafe
+bind ;v hint -W mpvsafe
+
+"bind ;e hint -hW js -p tri.excmds.exclaim_quiet(`${tri.config.get('editorcmd')} /tmp/tridactl.${document.location.hostname}.txt '+normal!i${JS_ARG}'`)
+
+set exaliases.imgsafe js -p tri.excmds.shellescape(JS_ARG).then(u => tri.excmds.exclaim_quiet(`fyi 'tridactyl::mvi' ${u} && mvi ${u}`))
+bind ;i composite hint -Jpipe img | js -p JS_ARG.src | imgsafe
+
+bind gp composite get_current_url | winopen -popup
+bind g1 tab 2
+bind g2 tab 3
+bind g3 tab 4
+bind g4 tab 5
+bind gb tab #
+
+# mapswap
+command mapswap js tri.excmds.tabopen("https://mapswap.trailsta.sh/swap/#type="+(()=>{let q=x=>document.querySelector(x);return q('.leaflet-container')?'l':q('.maplibregl-map')||q('.mapboxgl-map')?'m':q('.ol-viewport')?'ol':''})()+"&url="+encodeURIComponent(window.location))
+bind gms mapswap
+
+" hackernews hinting
+bindurl news.ycombinator.com ;c hint -fr \d+\scomments
+bindurl news.ycombinator.com gr hint -c [class*="storylink"]
+
+" ddg result hinting
+bindurl duckduckgo.com gr hint -c article > div > h2
+
+" fix github
+unbindurl github.com /
+
+" fix gitlab
+unbindurl gitlab.freedesktop.org /
+
+" fix youtube
+seturl youtube.com noiframe true
+unbindurl youtube.com f
+unbindurl youtube.com 1
+unbindurl youtube.com 2
+unbindurl youtube.com 3
+unbindurl youtube.com 4
+unbindurl youtube.com 5
+unbindurl youtube.com 6
+unbindurl youtube.com 7
+unbindurl youtube.com 8
+unbindurl youtube.com 9
+unbindurl youtube.com <Space>
+bindurl youtube.com h hint
+bindurl youtube.com T tabopen ddg.gg
+bindurl youtube.com C recontain personal
+
+" fix twitch
+unbindurl twitch.tv <Space>
+
+" fix reddit
+seturl reddit.com allowautofocus true
+unbindurl reddit.com <Escape>
+unbindurl reddit.com j
+unbindurl reddit.com k
+unbindurl reddit.com N
+unbindurl reddit.com P
+unbindurl reddit.com n
+unbindurl reddit.com p
+unbindurl reddit.com a
+unbindurl reddit.com z
+unbindurl reddit.com c
+" unbindurl reddit.com r
+unbindurl reddit.com s
+unbindurl reddit.com h
+unbindurl reddit.com x
+unbindurl reddit.com l
+
+" fix 4chan
+unbindurl boards.4chan.*.org x
+unbindurl boards.4chan.*.org T
+unbindurl boards.4chan.*.org B
+unbindurl boards.4chan.*.org n
+unbindurl boards.4chan.*.org p
+unbindurl boards.4chan.*.org F
+unbindurl boards.4chan.*.org W
+unbindurl boards.4chan.*.org <C-j>
+unbindurl boards.4chan.*.org <C-k>
+
+" fix nebula
+unbindurl nebula.tv f
+unbindurl nebula.tv k
+unbindurl nebula.tv m
+unbindurl nebula.tv c
+unbindurl nebula.tv s
+
+" fix zigdocs
+unbindurl ziglang.org s
+
+bindurl wiki.openstreetmap.org --mode=normal E editor
+bindurl wiki.openstreetmap.org --mode=input E editor
+bindurl wikipedia.org --mode=normal E editor
+bindurl wikipedia.org --mode=input E editor
+
+" start in ignore mode
+blacklistadd console.hetzner.cloud/console
+blacklistadd docs.google.com
+blacklistadd gather.town
+blacklistadd monkeytype.com
+blacklistadd typeform.com
+blacklistadd typingclub.com
+blacklistadd maplibre.org
+blacklistadd excalidraw.com
+
+" almost fully disable tridactyl
+seturl localhost* superignore true
+
+" put autocontains into the per-profile config
+js tri.native.getProfile().then(p => tri.excmds.source_quiet(`~/.config/tridactyl/tridactylrc.${last(p.Path.split('.'))}`))
diff --git a/.config/tridactyl/tridactylrc.default b/.config/tridactyl/tridactylrc.default
new file mode 100644
index 0000000..e505eec
--- /dev/null
+++ b/.config/tridactyl/tridactylrc.default
@@ -0,0 +1,38 @@
+autocontain -u ^https?://www\.google\.com/(maps|mail) google
+autocontain -s lh3\.google\.com google
+autocontain -s accounts\.google\.com google
+autocontain -s myaccount\.google\.com google
+autocontain -s myadcenter\.google\.com google
+autocontain -s myactivity\.google\.com google
+autocontain -s calendar\.google\.com google
+autocontain -s maps\.google\.com google
+autocontain -s gmail\.com google
+autocontain -s mail\.google\.com google
+autocontain -s photos\.google\.com google
+autocontain -s photos\.fife\.usercontent\.google\.com google
+autocontain -s takeout\.google\.com google
+autocontain -s timeline\.google\.com google
+autocontain -s youtube\.com google
+autocontain -s youtu\.be google
+autocontain -s docs\.google\.com google
+autocontain -s takeout\.google\.com google
+autocontain -s cloud\.google\.com google
+autocontain -s lens\.google\.com google
+autocontain -s lens\.google\.com google
+autocontain -s chat\.google\.com google
+autocontain -s docs\.google\.com google
+autocontain -s takeout-.*\.google\.com google
+autocontain -s drive\.google\.com google
+
+autocontain -s banking\.dkb\.de banking
+autocontain -s app\.n26\.com banking
+
+autocontain -s amazon\.(com|de|co\.jp) amazon
+
+autocontain -s bakabt\.me weeb
+autocontain -s nyaa\.si weeb
+autocontain -s feralhosting\.com weeb
+autocontain -s reddit\.com weeb
+autocontain -s kitsu\.app weeb
+autocontain -s trakt\.tv weeb
+autocontain -s anilist\.co weeb
diff --git a/.config/tridactyl/tridactylrc.work b/.config/tridactyl/tridactylrc.work
new file mode 100644
index 0000000..93b6f33
--- /dev/null
+++ b/.config/tridactyl/tridactylrc.work
@@ -0,0 +1,4 @@
+blacklistadd linear.app
+bind gm tab meet.google.com
+unbindurl meet\.google\.com --mode=normal <C-d>
+" blacklistadd meet.google.com
diff --git a/.config/xdg-desktop-portal-wlr/config b/.config/xdg-desktop-portal-wlr/config
new file mode 100644
index 0000000..2a9cbb3
--- /dev/null
+++ b/.config/xdg-desktop-portal-wlr/config
@@ -0,0 +1,5 @@
+[screencast]
+#chooser_type=simple
+#chooser_cmd=slurp -b '#000000aa' -c '#4E437188' -w 50 -f '%o' -or
+exec_before=makoctl mode -a off
+exec_after=makoctl mode -r off
diff --git a/.config/xdg-desktop-portal/portals.conf b/.config/xdg-desktop-portal/portals.conf
new file mode 120000
index 0000000..c0cf083
--- /dev/null
+++ b/.config/xdg-desktop-portal/portals.conf
@@ -0,0 +1 @@
+river-portals.conf
\ No newline at end of file
diff --git a/.config/xdg-desktop-portal/river-portals.conf b/.config/xdg-desktop-portal/river-portals.conf
new file mode 100644
index 0000000..29949c0
--- /dev/null
+++ b/.config/xdg-desktop-portal/river-portals.conf
@@ -0,0 +1,2 @@
+[preferred]
+default=darkman;wlr;gtk;
diff --git a/.config/yt-dlp.conf b/.config/yt-dlp.conf
new file mode 100644
index 0000000..457cd5e
--- /dev/null
+++ b/.config/yt-dlp.conf
@@ -0,0 +1 @@
+--embed-metadata
diff --git a/.local/share/applications/desktop-actions.desktop b/.local/share/applications/desktop-actions.desktop
new file mode 100644
index 0000000..f74ef69
--- /dev/null
+++ b/.local/share/applications/desktop-actions.desktop
@@ -0,0 +1,30 @@
+[Desktop Entry]
+Version=1.0
+Name=Actions
+Exec=true
+Terminal=false
+Type=Application
+Actions=play-music-shuffle;play-music-select;play-music-skip;screenrec-start;screenrec-stop;
+
+[Desktop Action play-music-shuffle]
+Icon=play-music
+Name=play-music shuffle
+Exec=play-music shuffle
+
+[Desktop Action play-music-select]
+Icon=play-music
+Name=play-music select
+Exec=play-music select
+
+[Desktop Action play-music-skip]
+Icon=play-music
+Name=play-music skip
+Exec=playerctl next
+
+[Desktop Action screenrec-start]
+Name=screenrec start
+Exec=screenrec
+
+[Desktop Action screenrec-stop]
+Name=screenrec stop
+Exec=screenrec stop
diff --git a/.local/share/applications/senpai.desktop b/.local/share/applications/senpai.desktop
new file mode 100644
index 0000000..a95c799
--- /dev/null
+++ b/.local/share/applications/senpai.desktop
@@ -0,0 +1,14 @@
+[Desktop Entry]
+Version=1.5
+Name=senpai
+
+GenericName=IRC Client
+Comment=Chat on IRC with senpai
+Keywords=Chat;IRC;IM;Messaging
+Categories=Network;InstantMessaging;Chat;IRCClient;ConsoleOnly
+
+Type=Application
+MimeType=x-scheme-handler/irc;x-scheme-handler/ircs
+Icon=senpai
+Terminal=true
+Exec=senpai %u
diff --git a/.local/share/applications/urlopen.desktop b/.local/share/applications/urlopen.desktop
new file mode 100644
index 0000000..71c21e4
--- /dev/null
+++ b/.local/share/applications/urlopen.desktop
@@ -0,0 +1,9 @@
+[Desktop Entry]
+Version=1.0
+Name=urlopen
+Exec=urlopen %u
+Icon=urlopen
+Terminal=false
+Type=Application
+MimeType=x-scheme-handler/http;x-scheme-handler/https;
+NoDisplay=true
diff --git a/.local/share/applications/workfox.desktop b/.local/share/applications/workfox.desktop
new file mode 100644
index 0000000..6f401fc
--- /dev/null
+++ b/.local/share/applications/workfox.desktop
@@ -0,0 +1,24 @@
+[Desktop Entry]
+Version=1.0
+Name=Workfox
+Exec=env MOZ_APP_REMOTINGNAME=workfox librewolf --name workfox --profile /home/robert/.librewolf/hibiki.work %u
+Comment=Browse the World Wide Web (for Work)
+GenericName=Web Browser (for Work)
+Keywords=Internet;WWW;Browser;Web;Explorer
+Icon=workfox
+Terminal=false
+X-MultipleArgs=false
+Type=Application
+MimeType=text/html;text/xml;application/xhtml+xml;x-scheme-handler/http;x-scheme-handler/https;application/x-xpinstall;application/pdf;application/json;
+StartupNotify=true
+StartupWMClass=LibreWolf
+Categories=Network;WebBrowser;
+Actions=new-window;new-private-window;
+
+[Desktop Action new-window]
+Name=Open a New Window
+Exec=env MOZ_APP_REMOTINGNAME=workfox librewolf --name workfox --profile /home/robert/.librewolf/hibiki.work --new-window %u
+
+[Desktop Action new-private-window]
+Name=Open a New Private Window
+Exec=env MOZ_APP_REMOTINGNAME=workfox librewolf --name workfox --profile /home/robert/.librewolf/hibiki.work --private-window %u
diff --git a/.local/share/darkman/bg.sh b/.local/share/darkman/bg.sh
new file mode 100755
index 0000000..a6d4c3e
--- /dev/null
+++ b/.local/share/darkman/bg.sh
@@ -0,0 +1,10 @@
+#!/bin/sh
+set -eu
+
+case "$1" in
+light|dark)
+	ln -sf ${XDG_PICTURES_DIR}/bg_${1}.jpg ${XDG_PICTURES_DIR}/bg.jpg
+	rc-service -U swaybg restart
+	;;
+default) exit 1 ;;
+esac
diff --git a/.local/share/darkman/foot.sh b/.local/share/darkman/foot.sh
new file mode 100755
index 0000000..3f9ac81
--- /dev/null
+++ b/.local/share/darkman/foot.sh
@@ -0,0 +1,14 @@
+#!/bin/sh
+set -eu
+
+case "$1" in
+dark)
+	echo "initial-color-theme=1" > ~/.config/foot/colors.ini
+	killall -s SIGUSR1 foot
+	;;
+light)
+	echo "initial-color-theme=2" > ~/.config/foot/colors.ini
+	killall -s SIGUSR2 foot
+	;;
+default) exit 1 ;;
+esac
diff --git a/.local/share/darkman/fuzzel.sh b/.local/share/darkman/fuzzel.sh
new file mode 100755
index 0000000..a51cd39
--- /dev/null
+++ b/.local/share/darkman/fuzzel.sh
@@ -0,0 +1,12 @@
+#!/bin/sh
+set -eu
+
+case "$1" in
+dark)
+	echo "include=~/.config/fuzzel/colors-dark.ini" > ~/.config/fuzzel/colors.ini
+	;;
+light)
+	echo "include=~/.config/fuzzel/colors-light.ini" > ~/.config/fuzzel/colors.ini
+	;;
+default) exit 1 ;;
+esac
diff --git a/.local/share/darkman/gtk3.sh b/.local/share/darkman/gtk3.sh
new file mode 100755
index 0000000..1b6ff21
--- /dev/null
+++ b/.local/share/darkman/gtk3.sh
@@ -0,0 +1,12 @@
+#!/bin/sh
+set -eu
+
+case "$1" in
+dark)
+	gsettings set org.gnome.desktop.interface gtk-theme adw-gtk3-dark
+	;;
+light)
+	gsettings set org.gnome.desktop.interface gtk-theme adw-gtk3
+	;;
+default) exit 1 ;;
+esac
diff --git a/.local/share/darkman/mako.sh b/.local/share/darkman/mako.sh
new file mode 100755
index 0000000..dd50242
--- /dev/null
+++ b/.local/share/darkman/mako.sh
@@ -0,0 +1,14 @@
+#!/bin/sh
+set -eu
+
+case "$1" in
+dark)
+	echo "include=~/.config/mako/colors-dark" > ~/.config/mako/colors
+	makoctl reload
+	;;
+light)
+	echo "include=~/.config/mako/colors-light" > ~/.config/mako/colors
+	makoctl reload
+	;;
+default) exit 1 ;;
+esac
diff --git a/bin/2av1 b/bin/2av1
new file mode 100755
index 0000000..19ace0b
--- /dev/null
+++ b/bin/2av1
@@ -0,0 +1,84 @@
+#!/bin/bash
+set -eu
+[ -n "${DEBUG:-}" ] && set -x
+[ -z "${DEBUG:-}" ] && export SVT_LOG=1
+
+BITRATE=${BITRATE:-128000}
+EXT=${EXT:-webm}
+
+case "${VCODEC:-av1}" in
+vp9) vcodec_args=(-c:v libvpx-vp9 -crf "${CRF:-31}" -b:v 0) ;;
+av1) vcodec_args=(-c:v libsvtav1 -crf "${CRF:-30}" -svtav1-params "tune=0:fast-decode=1") ;;
+esac
+for f in "$@"; do
+	f=$(readlink -e "$f")
+	op=${OUTDIR:-${f%/*}}
+	on=${f##*/}
+	on=${on%.*}.${EXT}
+	o="${op}/${on}"
+	[ "$f" = "$o" ] && o="${op}/${on%.*}.new.${EXT}"
+
+	hr
+
+	printf '🏷️  %s\n' "$on"
+
+	# duration
+	duration=$(ffprobe -v error -of csv=p=0 -show_entries format=duration -sexagesimal "$f")
+
+	# bitrate
+	br=$(ffprobe -v error -of default=noprint_wrappers=1:nokey=1 \
+		-select_streams a \
+		-show_entries stream=bit_rate \
+		"${f}")
+	[ -z "$br" ] && br=$BITRATE
+	[ $br = "N/A" ] && br=$((BITRATE + 1))
+	[ $br -gt $BITRATE ] && br="$BITRATE"
+
+	printf '🧾  duration=%s bitrate=%d\n' \
+		"$duration" \
+		"$br"
+
+	startt=$(date +%s)
+
+	# transcode
+	ffmpeg -hide_banner -loglevel warning -stats \
+		-i "$f" \
+		${OPTS:-} \
+		-c:a libopus \
+		-b:a "$br" \
+		${vcodec_args[@]} \
+		-map 0 \
+		-map_metadata 0 \
+		"${o}"
+
+	set +x
+
+	# # progress
+	# total_frames=$(ffprobe -v error -select_streams v:0 -show_entries stream=nb_frames -of csv=p=0 -i "${1:?}")
+	# while [ -e "/proc/$ffmpeg_pid" ]; do
+	# 	sleep 2
+	# 	[ -e "$vstats_file" ] || continue
+	# 	current_frames=$(tail -1 "$vstats_file" | grep -o 'frame=[ 0-9]*' | sed 's,frame=[ ]*,,')
+	# 	[ -z "$current_frames" ] && continue
+	# 	current_percent=$((100 * current_frames / total_frames))
+	# 	printf '==> %d/%d %d%% ]]\n\n' "$current_frames" "$total_frames" "$current_percent"
+	# done
+
+	# duration
+	endt=$(date +%s)
+	printf '🏁  took='
+	date -u -d@"$((endt - startt))" +%H:%M:%S
+
+	# results
+	du -h "$f" "$o"
+	in_size=$(stat -c '%s' "$f")
+	out_size=$(stat -c '%s' "$o")
+	awk -v fs="$in_size" -v os="$out_size" \
+		'BEGIN{p=((100/fs) * (fs-os)); printf("♻️  %.2f%% reduction\n", p);}'
+
+	if [ $in_size -gt $out_size ]; then
+		trash-put -v "${f}"
+	else
+		trash-put -v "${o}"
+	fi || true
+done
diff --git a/bin/audm b/bin/audm
new file mode 100755
index 0000000..58a5a91
--- /dev/null
+++ b/bin/audm
@@ -0,0 +1,84 @@
+#!/bin/bash
+set -eu
+
+# shellcheck disable=2034
+description="audio/sound manager"
+
+export FUZL_PROMPT=audm
+export FUZL_ICON=󰟥
+. libfuzl.sh
+
+main() {
+	local sink source
+
+	sink=$(inspect_node '@DEFAULT_SINK@' | awk -F\\t '{print $2}')
+	fuzl_action_add 'change_sink' "󰓃" "$sink"
+
+	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_menu || exit 1
+}
+
+#
+# helpers
+#
+
+inspect_node() {
+	local nid
+	nid=$(wpctl inspect "${1:?}" | awk '/^id/{match($0,/[0-9]+,/); print substr($0,RSTART,RLENGTH-1);}')
+
+	test -n "$nid" || exit 1 # TODO handle error
+	pw-dump | jq -r --argjson nid "$nid" \
+		'.[]|select(.id==$nid)|.info.props|[.["object.id"],.["node.description"],.["node.name"]]|join("\t")'
+}
+
+list_nodes() {
+	pw-dump | jq -rc --arg class "${1:?}" \
+		'.[]|select((.info.props["media.class"]//"")|test($class))|.info.props|[.["object.id"],.["node.description"],.["node.name"]]|join("\t")'
+}
+
+#
+# actions
+#
+
+audm_change_sink() {
+	local id
+	id=$(list_nodes 'Audio/Sink' |
+		FUZL_PROMPT='sink' fuzl_menu $FUZL_MENU_FILTER --with-nth='{2}   [{3}]' --accept-nth=1)
+
+	test -n "$id" || main
+	fuzl_try -- wpctl set-default "$id"
+}
+
+audm_change_source() {
+	local id
+	id=$(list_nodes 'Audio/Source' |
+		FUZL_PROMPT='source' fuzl_menu $FUZL_MENU_FILTER --with-nth='{2}   [{3}]' --accept-nth=1)
+
+	test -n "$id" || main
+	fuzl_try -- wpctl set-default "$id"
+}
+
+audm_change_profile() {
+	local device did dname 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}')
+	test -n "$device" || main
+
+	did=$(printf %s "$device" | awk -F\\t '{print $1}')
+	dname=$(printf %s "$device" | awk -F\\t '{print $2}')
+
+	profile=$(pw-dump | jq -r --argjson did "$did" \
+		'.[]|select(.id==$did)|.info.params.EnumProfile[]|[.index,.description]|join("\t")' |
+		FUZL_PROMPT="profile ($dname)" fuzl_menu $FUZL_MENU_FILTER --with-nth=2 --accept-nth=1)
+	test -n "$profile" || audm_change_profile
+
+	fuzl_try -- wpctl set-profile "$did" "$profile"
+}
+
+main
diff --git a/bin/blum b/bin/blum
new file mode 100755
index 0000000..19c2aef
--- /dev/null
+++ b/bin/blum
@@ -0,0 +1,100 @@
+#!/bin/bash
+set -eu
+
+description="bluetooth manager"
+
+export FUZL_PROMPT=blum
+export FUZL_ICON=
+. libfuzl.sh
+
+main() {
+	local connected
+
+	if ! bt_enabled; then
+		fuzl_action_add "enable" "󰷗" "Enable Bluetooth"
+	else
+		connected=$(bt_devices Connected | bt_devices_getname)
+		if [ -n "$connected" ]; then
+			fuzl_action_add "disconnect" "󰂱" "Connected to $connected"
+		fi
+		fuzl_action_add "devices" "󰾰" "Available devices"
+		fuzl_action_add "scan" "󱄙" "Scan"
+		fuzl_action_add "repl" "" "Run REPL"
+		fuzl_action_add "disable" "󰷘" "Disable Bluetooth"
+		fuzl_action_add "tether_amagiri" "󰀃" "Tether amagiri"
+	fi
+
+	fuzl_action_menu || exit 1
+}
+
+bt_enabled() {
+	pgrep -cx bluetoothd >/dev/null 2>&1
+}
+
+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}'
+}
+
+bt_devices_getname() {
+	awk -F '\t' '{print $2}'
+}
+
+#
+# actions
+#
+
+blum_enable() {
+	fuzl_try -- doas -n rc-service bluetooth start
+	fuzl_notification "enabled" "-"
+	main
+}
+
+blum_disable() {
+	fuzl_try -- doas -n rc-service bluetooth stop
+	fuzl_notification "disabled" "-"
+}
+
+blum_disconnect() {
+	echo "disconnect" | fuzl_try -- bluetoothctl
+	fuzl_notification "disconnected" "-"
+}
+
+blum_devices() {
+	device=$(bt_devices |
+		FUZL_PROMPT='devices' fuzl_menu $FUZL_MENU_FILTER)
+
+	[ -n "$device" ] || main
+	device_mac=$(printf %s "$device" | bt_devices_getmac)
+
+	echo "connect" "$device_mac" | fuzl_try -- bluetoothctl
+	fuzl_notification "connected" "$device"
+
+}
+
+blum_scan() {
+	fuzl_loading "scanning"
+	echo "scan" "on" | bluetoothctl || return
+	sleep 2
+	fuzl_loading_cancel # TODO: this kills everything
+	main
+}
+
+blum_repl() {
+	riverctl spawn 'footclient -- bluetoothctl'
+}
+
+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"
+}
+
+main
diff --git a/bin/dots b/bin/dots
new file mode 100755
index 0000000..cee0d73
--- /dev/null
+++ b/bin/dots
@@ -0,0 +1,28 @@
+#!/bin/sh
+set -eu
+[ -n "${DEBUG:-}" ] && set -x
+case "${1:-status}" in
+git)
+	[ $# -gt 0 ] && shift
+	exec /usr/bin/git --git-dir=$HOME/.dotfiles --work-tree=$HOME $*
+	;;
+st | status)
+	exec $0 git status -sb --untracked-files=no
+	;;
+add)
+	shift
+	exec $0 git add -f $*
+	;;
+commit | snapshot | record)
+	shift
+	$0 git commit --all --trailer=Automated-commit-by:dots --trailer=signoff --gpg-sign --message=* $*
+	echo
+	$0 git log --stat --format=full -1
+	echo
+	;;
+push)
+	set -x
+	$0 git push inazuma || true
+	$0 git push origin || true
+	;;
+esac
diff --git a/bin/dskm b/bin/dskm
new file mode 100755
index 0000000..c5a0abf
--- /dev/null
+++ b/bin/dskm
@@ -0,0 +1,26 @@
+#!/bin/bash
+set -eu
+
+# shellcheck disable=2034
+description="desk height manager"
+
+export FUZL_PROMPT=dskm
+export FUZL_ICON=󱈹
+. libfuzl.sh
+
+main() {
+	local pos
+
+	pos=$(
+		idesk config |
+			awk '/positions:/{ok=1; next} ok==1{print gensub(":", "", "g", $1)}' |
+			fuzl_menu $FUZL_MENU_FILTER
+	)
+	test -n "$pos" || return
+
+	fuzl_loading "moving desk to position: $pos"
+	idesk "$pos"
+	fuzl_loading_cancel
+}
+
+main
diff --git a/bin/genup b/bin/genup
new file mode 100755
index 0000000..c759a10
--- /dev/null
+++ b/bin/genup
@@ -0,0 +1,91 @@
+#!/bin/sh
+. /lib/gentoo/functions.sh
+
+set -e
+[ -n "${DEBUG:-}" ] && set -x
+
+if [ "$UID" != 0 ]; then
+	eerror "need to be root"
+	exit 1
+fi
+
+# builtin emerge args
+eargs=
+
+# order:
+# sync -> portage update -> update -> update unstable -> clean -> flatpak
+
+if test $# -eq 0; then
+	dosync=1
+	doupdate=1
+	dolive=1
+	doclean=1
+	doflatpak=1
+fi
+
+printonly=
+while getopts spulcfKXBh opt; do
+	case ${opt} in
+	s) dosync=1 ;;
+	u) doupdate=1 ;;
+	l) dolive=1 ;;
+	c) doclean=1 ;;
+	f) doflatpak=1 ;;
+	B) eargs="$eargs -X www-client/librewolf" ;;
+	K) eargs="$eargs -X sys-kernel/* -X virtual/dist-kernel" ;;
+	X) printonly=1 ;;
+	h | ?)
+		printf "usage: %s [-hsulcfBKX]\n" "$(basename "$0")"
+		printf "\n"
+		exit 2
+		;;
+	esac
+done
+shift $((OPTIND - 1))
+
+_() {
+	[ -n "$printonly" ] && printf "%s\n" "$*" && return
+	eval $*
+}
+
+ebegin syncing
+[ -n "${dosync:-}" ] &&
+	_ emerge --sync &&
+	eend
+
+ebegin updating portage
+emerge --pretend --quiet --update sys-apps/portage | grep -qE '\[.*U.*\]' &&
+	_ emerge -a --oneshot --getbinpkg sys-apps/portage &&
+	eend
+
+ebegin updating @world
+[ -n "${doupdate:-}" ] &&
+	_ emerge -ag --update --deep --newuse --keep-going "$@" $eargs @world &&
+	eend
+
+# --binpkg-respect-use=y
+# _ revdep-ebuild -- "$@" $eargs &&
+
+ebegin updating unstable
+[ -n "${dolive:-}" ] &&
+	_ emerge -a1 --keep-going @live-rebuild &&
+	eend
+
+ebegin depcleaning
+[ -n "${doclean:-}" ] &&
+	_ emerge -a --depclean "$@" &&
+	_ eclean --deep packages &&
+	_ eclean --deep distfiles &&
+	eend
+
+ebegin updating @preserved-rebuild
+[ -n "${doupdate:-}" ] &&
+	_ emerge -a --keep-going "$@" $eargs @preserved-rebuild &&
+	eend
+
+ebegin updating flatpaks
+[ -x "$(command -v flatpak)" ] && [ -n "${doflatpak:-}" ] &&
+	_ flatpak upgrade &&
+	_ flatpak uninstall --unused &&
+	_ flatpak repair &&
+	eend
diff --git a/bin/hr b/bin/hr
new file mode 100755
index 0000000..8231ae1
--- /dev/null
+++ b/bin/hr
@@ -0,0 +1,22 @@
+#!/bin/bash
+set -eu
+function print_hr() {
+	local str cols
+	local start line end
+
+	str="${1:-}"
+	[[ -n $str ]] && str=" $str "
+	cols=$((${COLUMNS:-$(tput cols)} - ${#str} - 3))
+	# start=$'\e(0'; end=$'\e(B'; line="qqq"
+	start=''; end=''; line="─"
+	while ((${#line}<$cols)); do line+=$line; done
+
+	printf "╾"
+	printf "%s%s%s" "$start" "${line:0:$((cols/2))}" "$end"
+	printf "%s%s%s" "$start" "${line:0:$((cols/2))}" "$end"
+	printf "%s" "$str"
+	printf "%s%s%s" "$start" "${line:0:1}" "$end"
+	printf "╼"
+	printf "\n"
+}
+print_hr "$@"
diff --git a/bin/libfuzl.sh b/bin/libfuzl.sh
new file mode 100644
index 0000000..a28c861
--- /dev/null
+++ b/bin/libfuzl.sh
@@ -0,0 +1,114 @@
+#!/bin/bash
+# local is well supported
+# shellcheck disable=3043
+
+set -eu
+
+# pick up configuration
+FUZL_PROMPT=${FUZL_PROMPT:-fuzl}
+FUZL_ICON=${FUZL_ICON:-󰈲}
+
+fuzl_try() {
+	[ "$1" == "--" ] && shift 1
+	if ! { stderr=$(
+		set +x
+		eval "$*" 2>&1 1>&3
+	); } 3>&1; then
+		fuzl_notification "$* failed :(" "${stderr:--}" -c error
+		exit 1
+	fi
+}
+
+export FUZL_MENU_FILTER=filter
+export FUZL_MENU_PASSWORD=passw
+export FUZL_MENU_MESSAGE=msg
+export FUZL_MENU_INPUT=input
+
+# Shows a menu
+fuzl_menu() {
+	local form="$1"
+	shift 1
+
+	local prompt options_list maxw
+
+	# If a prompt_icon is provided, prepend it.
+	prompt="$FUZL_PROMPT "
+	[ -n "$FUZL_ICON" ] && prompt="$FUZL_ICON $FUZL_PROMPT "
+
+	options_list=$(cat)
+	maxw=$(echo -e "$prompt\n$options_list" | wc -L)
+	[ "$maxw" -lt 15 ] && maxw=15
+	[ "$maxw" -gt 150 ] && maxw=100
+
+	case "$form" in
+	"$FUZL_MENU_FILTER") # Selection menu with a list of options passed via STDIN
+		echo -ne "$options_list" | fuzzel -d -w "$maxw" -p "$prompt" "$@"
+		;;
+	"$FUZL_MENU_PASSWORD") # Password input menu (minimalist)
+		echo -n | fuzzel -d -w "$maxw" -p "$prompt" --password
+		;;
+	"$FUZL_MENU_MESSAGE") # Message box menu (no input bar)
+		echo -ne "$options_list" | fuzzel -d -w "$maxw" -p "$prompt" "$@"
+		;;
+	"$FUZL_MENU_INPUT") # Text input menu (no list view)
+		echo -n | fuzzel -d -p "$prompt" "$@" --lines 0
+		;;
+	esac
+}
+
+FUZL_NOTIFICATION_ID=
+
+fuzl_notification() {
+	local summary="${1:?missing summary}"
+	local body="${2:?missing body}"
+	shift 2
+
+	FUZL_NOTIFICATION_ID=$(
+		fyi "$@" -p -a "${FUZL_PROMPT}" "${FUZL_PROMPT}: $summary" "$body" |
+			cut -d= -f2
+	)
+}
+fuzl_notification_cancel() {
+	fyi --close "$FUZL_NOTIFICATION_ID" || true
+	FUZL_NOTIFICATION_ID=
+}
+
+FUZL_LOADING_PID=
+
+fuzl_loading() {
+	local summary="${1:?missing summary}"
+	echo -e "..." | FUZL_PROMPT="$summary" fuzl_menu $FUZL_MENU_MESSAGE &
+	FUZL_LOADING_PID=$!
+}
+fuzl_loading_cancel() {
+	local pgid
+	pgid="$(ps -o pgid= "$FUZL_LOADING_PID" || return)"
+	kill -- -"$pgid" || true
+	FUZL_LOADING_PID=
+}
+
+FUZL_ACTIONS=()
+
+fuzl_action_add() {
+	local action="${1:?}" icon="${2:?}"
+	shift 2
+	# prevent multiple occurences
+	printf '%s\n' "${FUZL_ACTIONS[@]}" | grep -q "^$action" && return
+	FUZL_ACTIONS+=("${action}\t${icon}\t${*}")
+}
+
+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)
+
+	# reset actions
+	FUZL_ACTIONS=()
+
+	test -n "$action" || return
+	eval "${FUZL_PROMPT}_${action}" "'$*'"
+}
+
+# setup common debug helper
+[ -z "${DEBUG:-}" ] || set -x
diff --git a/bin/mailsync b/bin/mailsync
new file mode 100755
index 0000000..2100962
--- /dev/null
+++ b/bin/mailsync
@@ -0,0 +1,148 @@
+#!/bin/bash
+set -eu
+[ -n "${DEBUG:-}" ] && set -x
+
+# check requirements
+for tool in mbsync fyi python awk; do
+	if ! command -v "$tool" >/dev/null; then
+		printf '%s: %s not installed but required\n' "$(basename "$0")" "$tool" >&2
+		exit 1
+	fi
+done
+
+maildir="${HOME}/mail"
+cachefile=${XDG_CACHE_HOME:-$HOME/.cache}/mailsync.lastrun
+
+accounts=(gnzler)
+list=
+verbose=
+while getopts a:lvh opt; do
+	case "${opt}" in
+	a) accounts=("${OPTARG}") ;;
+	l) list=1 ;;
+	v) verbose=1 ;;
+	h | ?)
+		printf "usage: %s [-h] [ACCOUNT...]\n" "$(basename "$0")"
+		printf "\n"
+		printf "  -a ACCOUNTS, accunts to sync (default: \"%s\")\n" "${accounts[*]}"
+		printf "  -l list mail\n"
+		printf "  -v be verbose"
+		printf "\n"
+		exit 2
+		;;
+	esac
+done
+shift $((OPTIND - 1))
+
+if [ -n "${1:-}" ]; then
+	accounts=("$*")
+fi
+
+notifyclose() {
+	fyi --close "$1"
+}
+notifysend() {
+	local summary="${1:?missing summary}" body="${2:?missing body}"
+	shift 2
+
+	fyi --print-id $@ -a mailsync \
+		-i "${ico:-}" \
+		"$summary" "<i>$body</i>" | cut -d= -f2
+}
+try() {
+	[ "$1" = "--" ] && shift 1
+	if ! { error=$(
+		set +x
+		eval $* 2>&3
+	); } 3>&1; then
+		notifysend "failed :(" "$error" -c error
+		return 1
+	fi
+}
+
+
+decode() {
+	python -c 'import sys;import email.header;print(("".join(list(map(lambda s: s[0] if (type(s[0]) == str) else s[0].decode(s[1] or "utf-8"), email.header.decode_header(((sys.stdin.readlines() or [""])[0]).strip()))))))'
+}
+
+list_msg() {
+	local acct=$2
+	local args=(-type f -not -name '\.*')
+	local cachefile="${cachefile}.${acct}"
+
+	find "${maildir}"/"${acct}"/{INBOX,lists}/new/ "${args[@]}" 2>/dev/null || true
+}
+
+run_notify() {
+	local acct=$1
+	local new_mail
+	local num
+	local nid
+
+	new_mail=$(list_msg new "${acct}")
+	num=$(echo "$new_mail" | wc -l)
+	nid=$(notifysend "${acct}: new mail" "found ${num} new messages" --expire-time=6000)
+}
+
+run_sync() {
+	local acct=$1
+	local nid
+	local ret
+
+	# exit if we're already running
+	pgrep mbsync >/dev/null && {
+		echo "mbsync is already running."
+		notifysend "${acct}: mailsync stopped" "mbsync is already running"
+		return
+	}
+
+	# flush msmtp
+	nid=$(notifysend "${acct}: flushing queue" "running msmtp-queue" --expire-time=0)
+	try -- msmtp-queue -r || true
+
+	# run sync
+	nid=$(notifysend "${acct}: syncing mail" "running mbsync" --expire-time=0 --replaces=$nid)
+	mbsync_args=
+	[ -n "${verbose}" ] && mbsync_args="--verbose"
+	try -- mbsync $mbsync_args --config="$HOME/.config/mbsyncrc" "$acct"
+
+	# create cachefile
+	touch "${cachefile}.${acct}"
+
+	# index new messages
+	if command -v notmuch >/dev/null 2>&1; then
+		nid=$(notifysend "${acct}: syncing mail" "running notmuch" --expire-time=0 --replaces=$nid)
+		try -- notmuch new
+	fi
+
+	# update address book
+	if command -v maildir-rank-addr >/dev/null 2>&1; then
+		nid=$(notifysend "${acct}: syncing mail" "running maildir-rank-addr" --expire-time=0 --replaces=$nid)
+		maildir-rank-addr \
+			--maildir=${maildir}/${acct} \
+			--outputpath=${maildir}/addressbook \
+			--addresses='(r|robert)@gnzler\.(de|io),robert(|\.)guenzler@gmail.com,therobotbase@gmail.com,robertg@balena.io,robert.gunzler@postman.com,robert@unikraft.io' \
+			--filters='r\+.*@gnzler\.(de|io)'
+	fi
+
+	notifyclose "$nid"
+}
+
+# only list new mail, don't sync
+if [ -z "${accounts[*]}" ]; then
+	echo "no account specified" >&2
+	exit 1
+fi
+
+for acct in "${accounts[@]}"; do
+	(
+		if [ -n "${list}" ]; then
+			list_msg "${list}" "${acct}"
+			exit 0 # exit ()& subshell
+		fi
+		run_sync "${acct}"
+		run_notify "${acct}"
+	) &
+done
+
+wait
diff --git a/bin/mktoes b/bin/mktoes
new file mode 100755
index 0000000..7eb47cf
--- /dev/null
+++ b/bin/mktoes
@@ -0,0 +1,27 @@
+#!/bin/sh
+set -eu
+target=${1:?target}
+size=${2:-normal}
+
+case $size in
+normal) px=128 ;;
+large) px=256 ;;
+x-large) px=512 ;;
+xx-large) px=1024 ;;
+*) exit 1 ;;
+esac
+
+md5=$(echo "file://$(readlink -e "$target")" | md5sum | cut -d' ' -f1)
+dir=${XDG_CACHE_HOME:-$HOME/.cache}/thumbnails/$size
+path=$dir/$md5.png
+printf '%s' $path
+[ -n "${MKTOES_NOGEN:-}" ] && exit 0
+
+test -f $path && exit 0
+mkdir -p $dir
+chmod 700 $dir
+
+tmp=$dir/.$$.$md5.png
+ffmpeg -hide_banner -i "$target" -vf scale=$px:$px:force_original_aspect_ratio=decrease $tmp 2>/dev/null
+mv -f $tmp $path
+chmod 600 $path
diff --git a/bin/msmtp-queue b/bin/msmtp-queue
new file mode 120000
index 0000000..2604954
--- /dev/null
+++ b/bin/msmtp-queue
@@ -0,0 +1 @@
+/usr/share/msmtp/msmtpq/msmtp-queue
\ No newline at end of file
diff --git a/bin/msmtp-queue-stat b/bin/msmtp-queue-stat
new file mode 100755
index 0000000..bb83784
--- /dev/null
+++ b/bin/msmtp-queue-stat
@@ -0,0 +1,17 @@
+#!/bin/sh
+set -eu
+
+throttle_by=10
+throttle_cookie=/tmp/throttle.msmtp-queue-stat
+throttle() {
+	last_called=$(stat -c %Y $throttle_cookie 2>/dev/null || printf 0)
+	now=$(date +%s)
+	touch $throttle_cookie
+	if [ $((now - last_called)) -gt $throttle_by ]; then
+		"$@"
+	else
+		false
+	fi
+}
+
+throttle sh -c "msmtpq --q-mgmt -d | grep id= | wc -l | tee $throttle_cookie" || cat "$throttle_cookie"
diff --git a/bin/msmtpq b/bin/msmtpq
new file mode 120000
index 0000000..2e81615
--- /dev/null
+++ b/bin/msmtpq
@@ -0,0 +1 @@
+/usr/share/msmtp/msmtpq/msmtpq
\ No newline at end of file
diff --git a/bin/netm b/bin/netm
new file mode 100755
index 0000000..fab7e95
--- /dev/null
+++ b/bin/netm
@@ -0,0 +1,1560 @@
+#!/usr/bin/env bash
+#    __  __                 __   ________  ___       _   __    __
+#   / / / /_  ______  _____/ /  /_  __/  |/  /      / | / /___/ /_
+#  / /_/ / / / / __ \/ ___/ /    / / / /|_/ /_____ /  |/ / _  / __/
+# / __  / /_/ / /_/ / /  / /___ / / / /  / /_____ / /|  /  __/ /_
+#/_/ /_/\__, / .___/_/  /_____//_/ /_/  /_/      /_/ |_/\___/\__/
+#      /____/_/
+#
+
+# Copyright © 2025 Djalel Oukid (sniper1720)
+#
+# Mods:
+# - replace rofi with fuzzel
+# - remove all theme references
+# - fix wifi interface detection
+# - hide extra search icon
+# - replace close and check icons (due to problematic width)
+
+# --- Dependencies Check ---
+if ! command -v fuzzel &>/dev/null; then
+	echo "Error: fuzzel is not installed. Please install it to use this script." >&2
+	exit 1
+fi
+if ! command -v nmcli &>/dev/null; then
+	echo "Error: nmcli is not installed. Please install NetworkManager and nmcli." >&2
+	exit 1
+fi
+
+# --- Icon Variables (Nerd Fonts - Minimalist & Beautiful) ---
+# --- Icons: General UI ---
+icon_search="${icon_search:-""}"
+icon_close="${icon_close:-"󰅗 "}"
+icon_check="${icon_check:-" "}"
+icon_on="${icon_on:-""}"
+icon_off="${icon_off:-""}"
+icon_info="${icon_info:-"󰖐"}"
+icon_refresh="${icon_refresh:-"󰑐"}"
+icon_config="${icon_config:-""}"
+
+# --- Icons: Network Types ---
+icon_network="${icon_network:-"󱂇"}"
+icon_wifi_prompt="${icon_wifi_prompt:-"󱚾"}"
+icon_ethernet="${icon_ethernet:-"󰈀"}"
+icon_vpn="${icon_vpn:-"󰖃"}"
+icon_wireguard="${icon_wireguard:-"󰖆"}"
+icon_hotspot="${icon_hotspot:-"󱄙"}"
+icon_airplane="${icon_airplane:-"󰀝"}"
+
+# --- Icons: Wi-Fi Signal ---
+icon_wifi_full="${icon_wifi_full:-"󰤨"}"
+icon_wifi_good="${icon_wifi_good:-"󰤥"}"
+icon_wifi_medium="${icon_wifi_medium:-"󰤢"}"
+icon_wifi_low="${icon_wifi_low:-"󰤠"}"
+icon_wifi_disconnected="${icon_wifi_disconnected:-"󰤮"}"
+icon_wifi_enable="${icon_wifi_enable:-"󰖩"}"
+icon_wifi_disable="${icon_wifi_disable:-"󰖪"}"
+
+# --- Icons: Security & Status ---
+icon_wifi_secure="${icon_wifi_secure:-""}"
+icon_wifi_open="${icon_wifi_open:-"󰤠"}"
+icon_unlock="${icon_unlock:-""}"
+icon_password="${icon_password:-""}"
+icon_eye="${icon_eye:-"󰛐"}"
+icon_eye_closed="${icon_eye_closed:-"󰛑"}"
+icon_bookmark_saved="${icon_bookmark_saved:-"󰢭"}"
+icon_saved="${icon_saved:-"󰋋"}"
+
+# --- Icons: Menus & Actions ---
+icon_connect="${icon_connect:-"󰚫"}"
+icon_disconnect="${icon_disconnect:-"qa"}"
+icon_vpn_disconnect="${icon_vpn_disconnect:-"󰖂"}"
+icon_trash="${icon_trash:-""}"
+icon_pen="${icon_pen:-"󰑕"}"
+icon_import="${icon_import:-"󰋺"}"
+icon_qrcode="${icon_qrcode:-"󰐲"}"
+
+# --- Icons: Details & Config ---
+icon_active_details="${icon_active_details:-"󰋼"}"
+icon_status_chart="${icon_status_chart:-"󱖫"}"
+icon_interface="${icon_interface:-"󰛨"}"
+icon_devices="${icon_devices:-"󰋽"}"
+icon_chip="${icon_chip:-"󰢮"}"
+icon_ipv4_config="${icon_ipv4_config:-"󰒓"}"
+icon_ipv4_dns="${icon_ipv4_dns:-"󰒍"}"
+icon_ipv6_config="${icon_ipv6_config:-"󰒓"}" # Reuse
+icon_ipv6_dns="${icon_ipv6_dns:-"󰒍"}"       # Reuse
+icon_auto_ip="${icon_auto_ip:-"󰑘"}"
+icon_auto_dns="${icon_auto_dns:-"󰒍"}"
+icon_address="${icon_address:-"󰒓"}"
+icon_gateway="${icon_gateway:-"󰞡"}"
+icon_plug="${icon_plug:-"󱘖"}"
+icon_wireless="${icon_wireless:-"󰑩"}"
+icon_automatic="${icon_automatic:-"󰑘"}"
+
+# --- Icons: Miscellaneous ---
+icon_hidden_network="${icon_hidden_network:-"󰲊"}"
+icon_connect_wired="${icon_connect_wired:-"󱂇"}"
+icon_wired_status="${icon_wired_status:-"󰈁"}"
+
+# --- Translatable / Customizable Messages ---
+tr_checking_wifi_status='Checking Wi-Fi status... Please be patient.'
+tr_scanning_networks='Scanning networks... Please be patient.'
+tr_connecting_to='Connecting... Please be patient.'
+tr_disconnecting_from='Disconnecting... Please be patient.'
+tr_submenu_message='More options'
+tr_disable_message='Disable Wi-Fi'
+tr_enable_message='Enable Wi-Fi'
+tr_interface_message='Interface:'
+tr_known_connections_message='Known connections' # FIX: Removed colon
+tr_available_networks_message='Available Networks'
+tr_available_vpn_profiles_message='Available VPN Profiles'
+tr_autoconnect_message='Autoconnect'
+tr_ipv4_config_message='IPv4 Configuration'
+tr_ipv6_config_message='IPv6 Configuration'
+tr_dns4_message='DNS IPv4'
+tr_dns6_message='DNS IPv6'
+tr_connection_details_message='Connection Details' # New Header
+tr_ip_addr='IP Address'
+tr_gateway='Gateway'
+tr_signal_strength='Signal Strength'
+tr_speed='Link Speed'
+tr_frequency='Frequency'
+tr_mac_addr='MAC Address'
+tr_device='Interface'
+tr_autoip_message='Automatic IP'
+tr_autodns_message='Automatic DNS'
+tr_address_message='Addresses'
+tr_gateway_message='Gateway:'
+tr_forget_message='Forget connection'
+tr_wireguard_enable_message='Toggle VPN'
+tr_rename_connection_message='Rename connection'
+tr_hidden_message='Connect to a hidden network'
+tr_refresh_scan_message='Refresh Scan'                     # New message for refresh option
+tr_no_known_wifi_connections='No known Wi-Fi connections.' # New message
+tr_no_configured_vpns='No VPN connections configured.'     # New message
+tr_no_active_vpns='No active VPN connections.'             # New message
+tr_no_saved_connections='No saved connections found.'      # New message
+tr_no_active_connection='No active connection.'            # New message
+tr_no_ethernet_device='No Ethernet device found.'          # New message
+tr_no_wifi_networks_found='No Wi-Fi networks found.'       # New message for empty scan
+tr_wired_status_message='Wired Status'                     # New message for wired status option
+tr_manage_wired_profile='Manage Active Wired Profile'      # New message for wired profile management
+tr_connect_wired_connection='Connect to Wired Connection:' # Refined prompt
+tr_manage_wired_connections='Manage Wired Connections'     # New message for wired connections management
+tr_import_vpn_message='Import VPN from file'
+tr_import_vpn_prompt='Enter full path to .conf or .ovpn file:'
+tr_status_message=''
+tr_status_connected_to='Connected to'
+tr_status_connected='Connected'
+tr_status_disconnected='Disconnected'
+
+tr_status_disabled='Disabled'
+tr_connect_now_message='Connect Now'
+tr_disconnect_message='Disconnect Now'
+tr_notice_import_success_summary='VPN Imported'
+tr_notice_import_success_body='Successfully imported VPN connection.'
+tr_notice_import_error_summary='Import Error'
+tr_notice_import_error_body='Failed to import VPN connection.'
+tr_notice_file_not_found_body='File not found at specified path.'
+
+# Airplane Mode
+tr_airplane_mode_message='Airplane Mode'
+tr_airplane_on='Airplane Mode Enabled'
+tr_airplane_off='Airplane Mode Disabled'
+
+# QR Code
+tr_edit_password_message='Edit Password' # New translation
+tr_password_prompt='Enter password for'
+tr_password_updated='Password Updated'
+tr_password_update_failed='Failed to update password'
+tr_connection_failed_retry='Connection failed. Update password?'
+tr_qrcode_message='Share via QR Code'
+tr_qrcode_generating='Generating QR Code...'
+tr_qrcode_error='Could not generate QR code. Is qrencode installed?'
+tr_qrcode_no_password='Cannot share network without saved password.'
+
+# Hotspot
+tr_hotspot_message='Create Hotspot'
+tr_hotspot_ssid_prompt='Enter Hotspot SSID:'
+tr_hotspot_password_prompt='Enter Hotspot Password (min 8 chars):'
+tr_hotspot_creating='Creating Hotspot...'
+tr_hotspot_success='Hotspot created successfully!'
+tr_hotspot_error='Failed to create hotspot.'
+tr_notice_unknown_vpn_type_body='Unknown VPN file type. Use .conf or .ovpn.'
+
+# Main menu prompt now includes network and search icons
+tr_main_menu_prompt="$icon_network Network Manager: $icon_search"
+tr_wifi_menu_prompt="$icon_wifi_prompt Wi-Fi: $icon_search"
+tr_wired_menu_prompt="$icon_ethernet Wired: $icon_search"                               # NEW: Wired sub-menu prompt
+tr_vpn_menu_prompt="$icon_vpn_disconnect VPN: $icon_search"                             # NEW: VPN sub-menu prompt
+tr_saved_connections_menu_prompt="$icon_bookmark_saved Saved Connections: $icon_search" # NEW: Saved Connections sub-menu prompt
+tr_status_menu_prompt="$icon_status_chart Status: $icon_search"                         # NEW: Status sub-menu prompt
+
+tr_select_interface_prompt='Select Interface:'
+tr_ask_password_prompt='Enter password:'        # Refined prompt
+tr_menu_dns_prompt='Enter DNS (e.g., 8.8.8.8):' # Refined prompt
+tr_menu_dns_sure_prompt_1='Remove DNS '
+tr_menu_dns_sure_prompt_2='?'
+tr_menu_ip_config_addresses_prompt='Enter address (e.g., 192.168.1.10/24):' # Refined prompt
+tr_menu_ip_config_gateway_prompt='Enter gateway (e.g., 192.168.1.1):'       # Refined prompt
+tr_menu_addresses_prompt='Type or select address (e.g., 192.168.1.10/24):'  # Refined prompt
+tr_menu_addresses_sure_prompt_1='Remove address '
+tr_menu_addresses_sure_prompt_2='?'
+tr_forget_connection_sure_prompt_1='Forget '
+tr_forget_connection_sure_prompt_2='?'
+tr_forget_connection_confirm='Yes, forget'                   # New confirmation text
+tr_rename_connection_prompt='Enter new name for connection:' # Refined prompt
+tr_connect_hidden_prompt='Enter hidden network name:'        # Refined prompt
+
+tr_notice_connected_summary='Connected'
+tr_notice_disconnected_summary='Disconnected'
+tr_notice_error_summary='Connection Error'
+tr_notice_connected_body='Successfully connected to'
+tr_notice_disconnected_body='Successfully disconnected from'
+tr_notice_error_body='Failed to connect to'
+tr_notice_error_disconnect_body='Failed to disconnect from'
+
+# NEW: Messages for password input actions
+tr_show_password_message='Show Password'
+tr_hide_password_message='Hide Password'
+tr_confirm_password_message='Confirm Password'
+tr_edit_password_message='Edit Password'
+tr_edit_password_prompt='Enter new password:'
+
+# --- Global Variables ---
+program_name="$(basename "$0")"
+LOADING_ROFI_PID=""
+mapfile -t interfaces < <(nmcli --colors no -t -f TYPE,DEVICE device status | awk -F ':' '$1 == "wifi" {print $2}')
+if [ -z "${interfaces[0]}" ]; then
+	echo "$program_name: No Wi-Fi interfaces detected." >&2
+	exit 2
+fi
+interface_to_use="${interfaces[0]}"
+
+# --- Helper Functions ---
+
+# Function to display a menu (form 1: selection, 2: text input with list, 3: password input, 4: message box, 5: text input no list)
+# Now accepts optional extra flags to pass to rofi.
+display_menu() {
+	local form="$1"
+	local prompt_text="$2"
+	local prompt_icon="${3:-}" # Optional: icon to prepend to prompt
+	local extra_flags="${4:-}" # Optional: extra flags for the fuzzel command
+	local rofi_prompt rofi_flags options_list
+
+	# If a prompt_icon is provided and the prompt_text doesn't already start with it, prepend it.
+	if [ -n "$prompt_icon" ] && ! echo "$prompt_text" | grep -qE "^$prompt_icon"; then
+		rofi_prompt="$prompt_icon $prompt_text"
+	else
+		rofi_prompt="$prompt_text"
+	fi
+
+	local result
+
+	case $form in
+	1 | 2 | 4)
+		options_list=$(cat)
+		maxw=$(($(wc -L <<<"$rofi_prompt\n$options_list") + 2))
+		;&
+	1) # Selection menu with a list of options passed via STDIN
+		result=$(echo -e "$options_list" | fuzzel -di -w $maxw $extra_flags -p "$rofi_prompt")
+		;;
+	2) # Generic text input menu with a list
+		result=$(echo -e "$options_list" | fuzzel -d -w $maxw $extra_flags -p "$rofi_prompt")
+		;;
+	3) # Password input menu (minimalist)
+		result=$(fuzzel -d --password $extra_flags -p "$rofi_prompt")
+		;;
+	4) # Message box menu (no input bar)
+		result=$(echo -e "$options_list" | fuzzel -di -w $maxw --hide-prompt $extra_flags -p "$rofi_prompt")
+		;;
+	5) # Text input menu (no list view)
+		result=$(fuzzel -d $extra_flags -p "$rofi_prompt")
+		;;
+	esac
+	echo "$result"
+}
+
+# --- Rofi-based Notification System ---
+
+# Shows a non-blocking "loading" Rofi window and stores its PID.
+show_loading_notification() {
+	local message="$1"
+	# Use dmenu mode to get the themed window, but completely override the
+	# mainbox layout to show only a single, centered textbox.
+	echo "..." | fuzzel -d -p "$message" &
+	LOADING_ROFI_PID=$!
+}
+
+# Kills the loading notification Rofi window if it's running.
+kill_loading_notification() {
+	if [ -n "$LOADING_ROFI_PID" ] && ps -p "$LOADING_ROFI_PID" >/dev/null; then
+		kill "$LOADING_ROFI_PID"
+		LOADING_ROFI_PID=""
+	fi
+}
+
+# Shows a final, blocking Rofi message for results (Success, Error, etc.).
+# Optionally sends a system notification if notify-send is available.
+send_notification() {
+	local summary="$1"
+	local body="$2"
+
+	# Try system notification first (non-blocking)
+	if command -v notify-send &>/dev/null; then
+		fyi -a netm "$icon_network $summary" "$body"
+	else
+		# Fallback to Rofi message
+		show_message "$body" "$summary"
+	fi
+}
+
+# Function to show a message (stays open until dismissed, with "OK" button)
+# Now accepts an optional custom_prompt and a flag to include the "OK" button.
+show_message() {
+	local msg="$1"
+	local custom_prompt="${2:-Info}" # Default to "Info" if no custom prompt is provided
+	local include_ok="${3:-true}"    # Default to true
+	local options="$msg"
+
+	if [ "$include_ok" = "true" ]; then
+		options+="\n$icon_check OK"
+	fi
+	# Use form 4 to disable the input bar for these messages
+	echo -e "$options" | display_menu 4 "$custom_prompt" ""
+}
+
+# Function to display an informational message with a "Back" button
+# Now accepts an optional custom_prompt
+display_info_message() {
+	local msg="$1"
+	local custom_prompt="${2:-Info}" # Default to "Info" if no custom prompt is provided
+	local options="$msg\n$icon_close Back"
+	# Use form 4 to disable the input bar for these messages
+	local chosen=$(echo -e "$options" | display_menu 4 "$custom_prompt" "")
+	if [[ "$chosen" =~ "^$icon_close Back" ]]; then
+		return
+	fi
+}
+
+# Handles password input with a dynamic, interactive menu for showing, hiding, and editing.
+ask_password() {
+	local password_input
+	local password_shown="false" # 'true' or 'false'
+
+	# Step 1: Initial password prompt
+	password_input=$(echo "" | display_menu 3 "$tr_ask_password_prompt" "")
+
+	# If user cancels the initial prompt (presses Esc or enters nothing), return empty.
+	if [ -z "$password_input" ]; then
+		return 1
+	fi
+
+	# Step 2: Interactive action menu loop
+	while true; do
+		local options=""
+		local password_display=""
+
+		# Determine how to display the password
+		if [ "$password_shown" = "true" ]; then
+			password_display="$password_input"
+			options+="$icon_eye_closed $tr_hide_password_message: $password_display\n"
+		else
+			# Create a bullet point string of the same length as the password
+			password_display=$(printf '•%.0s' $(seq 1 ${#password_input}))
+			options+="$icon_eye $tr_show_password_message: $password_display\n"
+		fi
+
+		# Add other actions
+		options+="$icon_pen $tr_edit_password_message\n"
+		options+="$icon_unlock $tr_confirm_password_message\n"
+		options+="$icon_close Back"
+
+		# Display the interactive menu
+		local action_choice=$(echo -e "$options" | display_menu 1 "Password Actions" "")
+
+		# Handle user's choice
+		if [ -z "$action_choice" ] || [[ "$action_choice" =~ "^$icon_close Back" ]]; then
+			# User cancelled at action menu
+			return 1
+		fi
+
+		case "$action_choice" in
+		*"$tr_show_password_message"* | *"$tr_hide_password_message"*)
+			# Toggle password visibility
+			if [ "$password_shown" = "true" ]; then
+				password_shown="false"
+			else
+				password_shown="true"
+			fi
+			# Loop continues, redrawing the menu
+			;;
+		*"$tr_edit_password_message"*)
+			# Ask for a new password, pre-filling the input with the current password.
+			local new_password=$(echo "" | display_menu 2 "$tr_edit_password_prompt" "" "-filter \"$password_input\"")
+			# Only update if the user entered something and didn't cancel
+			if [ -n "$new_password" ]; then
+				password_input="$new_password"
+				password_shown="false" # Hide new password by default
+			fi
+			# Loop continues, redrawing the menu
+			;;
+		*"$tr_confirm_password_message"*)
+			# Return the confirmed password
+			echo "$password_input"
+			return
+			;;
+		esac
+	done
+}
+
+# Show details for the active connection
+show_connection_details() {
+	local active_ssid="$1"
+	local device="$2"
+
+	show_loading_notification "Gathering details..."
+
+	# Get details using nmcli device show
+	# We grep for specific fields
+	local info=$(nmcli -t -f GENERAL,IP4,IP6 device show "$device")
+
+	# Extract fields (hacky but effective for standard nmcli output)
+	# MAC often contains colons, so we strip the field name "GENERAL.HWADDR:"
+	local ipv4=$(echo "$info" | grep "IP4.ADDRESS\[1\]" | cut -d':' -f2)
+	local gateway=$(echo "$info" | grep "IP4.GATEWAY" | cut -d':' -f2)
+	local hwaddr=$(echo "$info" | grep "GENERAL.HWADDR" | sed 's/^GENERAL.HWADDR://')
+	local state=$(echo "$info" | grep "GENERAL.STATE" | cut -d':' -f2)
+
+	# Wifi specific details
+	local wifi_info=$(nmcli -t -f IN-USE,SSID,MODE,CHAN,RATE,SIGNAL,BARS,SECURITY device wifi list | grep "^\*")
+	# Format: *:SSID:Infra:Chan:Rate:Signal:Bars:Sec
+	local chan=$(echo "$wifi_info" | cut -d':' -f4)
+	local rate=$(echo "$wifi_info" | cut -d':' -f5)
+	local signal=$(echo "$wifi_info" | cut -d':' -f6)
+	local bars=$(echo "$wifi_info" | cut -d':' -f7)
+	local freq=""
+
+	# Determine frequency band roughly by channel
+	if [ "$chan" -gt 14 ]; then freq="5 GHz"; else freq="2.4 GHz"; fi
+
+	kill_loading_notification
+
+	local details=""
+	details+="$icon_address $tr_ip_addr: $ipv4\n"
+	details+="$icon_gateway $tr_gateway: $gateway\n"
+	details+="$icon_wifi_full $tr_signal_strength: $signal% ($bars)\n"
+	details+="$icon_on $tr_speed: $rate\n"
+	details+="$icon_wireless $tr_frequency: $freq (Ch $chan)\n"
+	details+="$icon_devices $tr_mac_addr: $hwaddr\n"
+	details+="$icon_chip $tr_device: $device"
+
+	display_info_message "$details" "$tr_connection_details_message"
+}
+
+# --- Dedicated Wi-Fi Scan Function ---
+perform_wifi_scan() {
+	show_loading_notification "$tr_scanning_networks"
+
+	# Get list of known SSIDs, delimited by |
+	# We use tr to replace newline with |
+	local known_ssids=$(nmcli -t -f 802-11-wireless.ssid connection show | tr '\n' '|')
+	# Add leading/trailing pipe for exact matching
+	known_ssids="|$known_ssids"
+
+	local scan_output=$(nmcli --colors no --get-values SECURITY,SIGNAL,SSID,IN-USE device wifi list --rescan auto | awk -F ':' \
+		-v icon_wifi_secure="$icon_wifi_secure" \
+		-v icon_wifi_open="$icon_wifi_open" \
+		-v icon_wifi_full="$icon_wifi_full" \
+		-v icon_wifi_good="$icon_wifi_good" \
+		-v icon_wifi_medium="$icon_wifi_medium" \
+		-v icon_wifi_low="$icon_wifi_low" \
+		-v icon_check="$icon_check" \
+		-v icon_unlock="$icon_unlock" \
+		-v known_ssids="$known_ssids" \
+		'
+    BEGIN { x = 1 }
+    {
+        # 1. First Icon: Signal Strength (Always shown)
+        wifi_signal_icon = icon_wifi_low;
+        if ($2 > 75) wifi_signal_icon = icon_wifi_full;
+        else if ($2 > 50) wifi_signal_icon = icon_wifi_good;
+        else if ($2 > 25) wifi_signal_icon = icon_wifi_medium;
+
+        ssid = $3;
+        if (ssid == "") ssid = "<hidden>";
+
+        # 2. Second Icon: Status (Priority: Active > Secure > Open/Known)
+        # User Logic: 
+        # - Active -> Checkmark
+        # - Secure (Even if known) -> Lock
+        # - Open -> Unlock
+
+        status_icon = icon_unlock; # Default to Unlock (Open)
+
+        if ($4 == "*") {
+             status_icon = icon_check; # Active -> Check
+        } else if ($1 ~ /^WPA/) {
+             status_icon = icon_wifi_secure; # Secure -> Lock
+        }
+        # Else remains Unlock (Open)
+
+        # Layout: [Signal] [Status] SSID (Sig%)
+        # Note: We purposely put two icons on the left as requested.
+        formatted_entry = wifi_signal_icon " " status_icon " " ssid " (" $2 "%)";
+
+        if ($4 == "*") {
+            networks[0] = formatted_entry;
+        } else {
+            networks[x++] = formatted_entry;
+        }
+    }
+    END {
+        if (networks[0] != "") {
+            print networks[0];
+        }
+        for (i = 1; i < x; i++) {
+            print networks[i];
+        }
+    }
+    ')
+	kill_loading_notification
+	echo "$scan_output"
+}
+
+# --- Menu for Available Wi-Fi Networks ---
+menu_available_wifi_networks() {
+	local wifi_list options chosen
+	while true; do
+		wifi_list=$(perform_wifi_scan)
+
+		options="$icon_refresh  $tr_refresh_scan_message\n"
+		if [ -z "$wifi_list" ]; then
+			options+="$icon_wifi_disconnected  $tr_no_wifi_networks_found\n"
+		else
+			options+="$wifi_list\n"
+		fi
+		options+="$icon_close Back" # Always at the bottom
+
+		# Prompt for this menu is just the search icon
+		chosen=$(echo -e "$options" | display_menu 1 "$tr_available_networks_message" "$icon_search")
+
+		# Handle Esc or "Back" selection
+		if [ -z "$chosen" ] || [[ "$chosen" =~ "^$icon_close Back" ]]; then
+			return # Go back to parent menu (menu_wifi)
+		fi
+
+		case "$chosen" in
+		"$icon_refresh  $tr_refresh_scan_message")
+			continue # Restart the while loop to re-run scan and redisplay menu
+			;;
+		"$icon_wifi_disconnected  $tr_no_wifi_networks_found")
+			# Do nothing if "No Wi-Fi networks found" is selected
+			continue
+			;;
+		*)
+			# This block should only be reached when a network is explicitly chosen
+			connect_wifi "$chosen"
+			;;
+		esac
+	done
+}
+
+# --- Main Wi-Fi menu (toggle, hidden, known, available networks) ---
+menu_wifi() {
+	local connection_state options chosen
+	while true; do
+		show_loading_notification "$tr_checking_wifi_status"
+		connection_state=$(nmcli --colors no --get-values WIFI general)
+		local active_connection_info=$(nmcli -t -f NAME,DEVICE connection show --active | grep -E "wl*" | head -n 1)
+		kill_loading_notification
+
+		local status_line=""
+		if [ "$connection_state" = "disabled" ]; then
+			status_line="$icon_wifi_disable $tr_status_message $tr_status_disabled"
+			options="$icon_wifi_enable  $tr_enable_message\n"
+		else
+			if [ -n "$active_connection_info" ]; then
+				local active_ssid=$(echo "$active_connection_info" | cut -d':' -f1)
+				local active_device=$(echo "$active_connection_info" | cut -d':' -f2)
+				status_line="$icon_wifi_full $tr_status_message $tr_status_connected_to '$active_ssid' ($active_device)"
+			else
+				status_line="$icon_wifi_disconnected $tr_status_message $tr_status_disconnected"
+			fi
+			options="$icon_wifi_disable  $tr_disable_message\n"
+			${interfaces[1]:+options+="$icon_interface  $tr_interface_message ${interface_to_use}\n"}
+			# Add "Available Networks" as a sub-menu
+			options+="$icon_wireless  $tr_available_networks_message\n"
+		fi
+
+		local full_options="$status_line\n"
+		full_options+="$options"
+		full_options+="$icon_hotspot  $tr_hotspot_message\n"                  # NEW: Create Hotspot
+		full_options+="$icon_bookmark_saved  $tr_known_connections_message\n" # FIX: Using icon_bookmark_saved
+		full_options+="$icon_hidden_network  $tr_hidden_message\n"            # FIX: Using icon_hidden_network
+		full_options+="$icon_close Back"                                      # Always at the bottom
+
+		chosen=$(echo -e "$full_options" | display_menu 1 "$tr_wifi_menu_prompt" "") # Pass "" for prompt_icon
+
+		# Handle Esc or "Back" selection
+		if [ -z "$chosen" ] || [[ "$chosen" =~ "^$icon_close Back" ]]; then
+			return # Go back to parent menu (main_menu)
+		fi
+
+		case "$chosen" in
+		"$icon_wifi_enable  $tr_enable_message")
+			nmcli radio wifi on
+			;;
+		"$icon_wifi_disable  $tr_disable_message")
+			nmcli radio wifi off
+			;;
+		*"$tr_interface_message"*) select_interface ;;
+		"$icon_wireless  $tr_available_networks_message") menu_available_wifi_networks ;;
+		*"$tr_known_connections_message"*) menu_known_connections "wifi" ;;
+		*"$tr_hidden_message"*) connect_hidden ;;
+		*"$tr_hotspot_message"*) create_hotspot ;;
+		*"$tr_status_message"*)
+			# Show details if connected
+			if [ -n "$active_connection_info" ]; then
+				local active_ssid=$(echo "$active_connection_info" | cut -d':' -f1)
+				local active_device=$(echo "$active_connection_info" | cut -d':' -f2)
+				show_connection_details "$active_ssid" "$active_device"
+			fi
+			;;
+		*)
+			show_message "Invalid option selected: $chosen"
+			;;
+		esac
+	done
+}
+
+select_interface() {
+	local chosen_interface=$( (
+		for ((i = 0; i < ${#interfaces[@]}; i++)); do echo "$icon_interface  ${interfaces[$i]}"; done
+		echo "$icon_close Back"
+	) | display_menu 1 "$tr_select_interface_prompt" "") # Pass "" for prompt_icon
+
+	if [ -z "$chosen_interface" ]; then
+		return
+	elif [[ "$chosen" =~ ^"$icon_close Back" ]]; then
+		return
+	else
+		interface_to_use="${chosen_interface:3}"
+	fi
+}
+
+# Connect to a hidden Wi-Fi network
+connect_hidden() {
+	local wifi_name=$(echo "" | display_menu 5 "$tr_connect_hidden_prompt" "") # Pass "" for prompt_icon
+
+	if [ -z "$wifi_name" ]; then
+		return # User cancelled or pressed back
+	fi
+
+	local wifi_password=$(ask_password)
+
+	if [ -z "$wifi_password" ]; then
+		show_message "Connection cancelled. No password provided."
+		return
+	fi
+
+	show_loading_notification "$tr_connecting_to"
+	if nmcli --wait 15 device wifi connect "$wifi_name" hidden yes password "$wifi_password"; then
+		kill_loading_notification
+		send_notification "$tr_notice_connected_summary" "$tr_notice_connected_body '$wifi_name'"
+		exit 0
+	else
+		kill_loading_notification
+		send_notification "$tr_notice_error_summary" "$tr_notice_error_body '$wifi_name'"
+	fi
+}
+
+# Connect to a visible Wi-Fi network
+connect_wifi() {
+	local chosen_entry="$1"
+
+	if [[ "$chosen_entry" == *"$icon_check"* ]]; then
+		local ssid_from_active=$(nmcli -t -f active,ssid dev wifi | grep "^yes" | cut -d':' -f2)
+		local active_uuid=$(nmcli -t -f UUID,TYPE,ACTIVE connection show | grep ":802-11-wireless:yes" | cut -d':' -f1 | head -n1)
+
+		if [ -n "$active_uuid" ]; then
+			if [ -z "$ssid_from_active" ]; then
+				local temp_ssid=$(echo "$chosen_entry" | sed -E 's/ \([0-9]+%\).*$//')
+				ssid_from_active=$(echo "$temp_ssid" | sed -E 's/^(󰄬 |󰤫 |󰤪 |󰤩 |󰤨 |󰤧 |󰤦 |󰤥 |󰤤 |󰤣 |󰤢 |󰤡 |󰤠 )+//' | xargs)
+			fi
+			menu_connection "$ssid_from_active" "$active_uuid"
+			return
+		fi
+	fi
+
+	local temp_ssid=$(echo "$chosen_entry" | sed -E 's/ \([0-9]+%\).*$//')
+	local wifi_ssid=$(echo "$temp_ssid" | sed -E 's/^(󰤨 |󰤥 |󰤢 |󰤯 |󰤫 |󰤪 | | | )+//' | xargs)
+
+	local is_secure=$(echo "$chosen_entry" | grep -q "$icon_wifi_secure" && echo "yes" || echo "no")
+
+	# 1. Check if already connected
+	local active_ssid=$(nmcli -t -f active,ssid dev wifi | grep "^yes" | cut -d':' -f2)
+	if [ "$wifi_ssid" = "$active_ssid" ]; then
+		local active_uuid=$(nmcli -t -f UUID,TYPE,ACTIVE connection show | grep ":802-11-wireless:yes" | cut -d':' -f1 | head -n1)
+		if [ -n "$active_uuid" ]; then
+			menu_connection "$wifi_ssid" "$active_uuid"
+			return
+		fi
+		show_message "Already connected to $wifi_ssid."
+		return
+	fi
+
+	# 2. Check if we have a saved connection (even if not active)
+	local saved_uuid=""
+	# Use standard fields (UUID, TYPE) that are guaranteed to exist
+	local saved_list=$(nmcli -t -f UUID,TYPE connection show)
+
+	while IFS=: read -r uuid type; do
+		# Check against both known Wi-Fi type strings
+		if [[ "$type" == "802-11-wireless" || "$type" == "wifi" ]]; then
+			# Get the SSID for this connection specifically
+			local ssid_check=$(nmcli -t -f 802-11-wireless.ssid connection show "$uuid" 2>/dev/null)
+			# FIX: output is '802-11-wireless.ssid:SSID', we must strip the prefix
+			ssid_check="${ssid_check#*:}"
+
+			if [ "$ssid_check" = "$wifi_ssid" ]; then
+				saved_uuid="$uuid"
+				break
+			fi
+		fi
+	done <<<"$saved_list"
+
+	if [ -n "$saved_uuid" ]; then
+		menu_connection "$wifi_ssid" "$saved_uuid"
+		return
+	fi
+
+	# 3. New Connection: Ask for password if secure
+	local connection_result
+	if [ "$is_secure" = "yes" ]; then
+		local wifi_password=$(ask_password)
+		if [ -z "$wifi_password" ]; then
+			show_message "Connection cancelled. No password provided."
+			return
+		fi
+
+		show_loading_notification "$tr_connecting_to"
+		nmcli --wait 15 device wifi connect "$wifi_ssid" ifname "$interface_to_use" password "$wifi_password"
+		connection_result=$?
+	else
+		show_loading_notification "$tr_connecting_to"
+		nmcli --wait 15 device wifi connect "$wifi_ssid" ifname "$interface_to_use"
+		connection_result=$?
+	fi
+	kill_loading_notification
+
+	if [ $connection_result -eq 0 ]; then
+		send_notification "$tr_notice_connected_summary" "$tr_notice_connected_body '$wifi_ssid'"
+		exit 0
+	else
+		send_notification "$tr_notice_error_summary" "$tr_notice_error_body '$wifi_ssid'"
+	fi
+}
+
+# --- IP Configuration Menus ---
+
+# Manage individual IP addresses for a connection
+menu_addresses() {
+	local connection_uuid="$1"
+	local ipv="$2"
+	local -a addresses_list
+	local sure chosen
+
+	while true; do
+		mapfile -t addresses_list < <(nmcli --get-values ipv${ipv}.addresses connection show "$connection_uuid" | sed 's/,/\n/g')
+		local options=$(printf "%s\n" "${addresses_list[@]}")
+		options+="$icon_close Back"
+
+		chosen=$(echo -e "$options" | display_menu 1 "$tr_menu_addresses_prompt" "") # Pass "" for prompt_icon
+
+		if [ -z "$chosen" ] || [[ "$chosen" =~ ^"$icon_close Back" ]]; then
+			return # User cancelled or pressed back
+		fi
+
+		local found=0
+		for addr in "${addresses_list[@]}"; do
+			if [ "$chosen" = "$addr" ]; then
+				found=1
+				sure=$(echo -e "$icon_check\n$icon_close Back" | display_menu 1 "$tr_menu_addresses_sure_prompt_1 ${chosen}$tr_menu_addresses_sure_prompt_2" "") # Pass "" for prompt_icon
+				if [ -z "$sure" ] || [[ "$sure" =~ ^"$icon_close Back" ]]; then continue; fi                                                                     # User cancelled confirmation
+				if [[ "$sure" =~ ^"$icon_check" ]]; then
+					nmcli connection modify uuid "$connection_uuid" -ipv${ipv}.addresses "$chosen"
+					if [ ${#addresses_list[@]} -eq 1 ]; then
+						nmcli connection modify uuid "$connection_uuid" ipv${ipv}.gateway ''
+						nmcli connection modify uuid "$connection_uuid" ipv${ipv}.method auto
+					fi
+				fi
+				break
+			fi
+		done
+
+		if [ "$found" -eq 0 ]; then
+			nmcli connection modify uuid "$connection_uuid" +ipv${ipv}.addresses "$chosen"
+			nmcli connection modify uuid "$connection_uuid" ipv${ipv}.method manual
+		fi
+	done
+}
+
+# Configure IP method (auto/manual), gateway
+menu_ip_config() {
+	local chosen_connection_name="$1"
+	local connection_uuid="$2"
+	local ipv="$3"
+	local autoip_state autodns_state message new_gateway chosen
+
+	while true; do
+		autoip_state="$([ "$(nmcli --get-values ipv${ipv}.method connection show "$connection_uuid")" = "auto" ] && echo "$icon_on" || echo "$icon_off")"
+
+		local current_gateway=$(nmcli --get-values ipv${ipv}.gateway connection show "$connection_uuid")
+		local current_addresses=$(nmcli --get-values ipv${ipv}.addresses connection show "$connection_uuid" | sed 's/,/\n/g')
+
+		local options=""
+		options+="$icon_auto_ip  $tr_autoip_message  $autoip_state\n"
+
+		if [ "$autoip_state" = "$icon_on" ]; then
+			autodns_state="$([ "$(nmcli --get-values ipv${ipv}.ignore-auto-dns connection show "$connection_uuid")" = "no" ] && echo "$icon_on" || echo "$icon_off")"
+			options+="$icon_auto_dns  $tr_autodns_message  $autodns_state\n"
+		else
+			options+="$icon_address  $tr_address_message: ${current_addresses:-N/A}\n"
+			options+="$icon_gateway  $tr_gateway_message ${current_gateway:-N/A}\n"
+			options+="$icon_gateway  $tr_gateway_message ${current_gateway:-N/A}\n"
+		fi
+		options+="$icon_ipv4_dns  DNS Configuration\n"
+		options+="$icon_close Back"
+
+		chosen=$(echo -e "$options" | display_menu 1 "$chosen_connection_name (IPv$ipv)" "") # Pass "" for prompt_icon
+
+		if [ -z "$chosen" ] || [[ "$chosen" =~ ^"$icon_close Back" ]]; then
+			return # User cancelled or pressed back
+		fi
+
+		case "$chosen" in
+		*"DNS Configuration"*)
+			menu_dns "$connection_uuid" "$ipv"
+			;;
+		*"$tr_autoip_message"*)
+			if [ "$autoip_state" = "$icon_on" ]; then
+				nmcli connection modify uuid "$connection_uuid" ipv${ipv}.method manual
+			else
+				nmcli connection modify uuid "$connection_uuid" ipv${ipv}.method auto
+				nmcli connection modify uuid "$connection_uuid" ipv${ipv}.gateway ''
+				nmcli connection modify uuid "$connection_uuid" ipv${ipv}.addresses ''
+			fi
+			;;
+		*"$tr_autodns_message"*)
+			if [ "$autodns_state" = "$icon_on" ]; then
+				nmcli connection modify uuid "$connection_uuid" ipv${ipv}.ignore-auto-dns yes
+			else
+				nmcli connection modify uuid "$connection_uuid" ipv${ipv}.ignore-auto-dns no
+			fi
+			;;
+		*"$tr_address_message"*)
+			menu_addresses "$connection_uuid" "$ipv"
+			;;
+		*"$tr_gateway_message"*)
+			new_gateway=$(echo "$icon_close Back" | display_menu 1 "$tr_menu_ip_config_gateway_prompt" "") # Pass "" for prompt_icon
+			if [ -z "$new_gateway" ] || [[ "$new_gateway" =~ ^"$icon_close Back" ]]; then continue; fi
+			nmcli connection modify uuid "$connection_uuid" ipv${ipv}.gateway "$new_gateway"
+			;;
+		esac
+	done
+}
+
+# Configure DNS servers for a connection
+menu_dns() {
+	local connection_uuid="$1"
+	local ipv="$2"
+	local -a dns_list
+	local sure chosen
+
+	while true; do
+		mapfile -t dns_list < <(nmcli --get-values ipv${ipv}.dns connection show "$connection_uuid" | sed 's/,/\n/g')
+		local options=$(printf "%s\n" "${dns_list[@]}")
+		options+="$icon_close Back"
+
+		chosen=$(echo -e "$options" | display_menu 1 "$tr_menu_dns_prompt" "") # Pass "" for prompt_icon
+
+		if [ -z "$chosen" ] || [[ "$chosen" =~ ^"$icon_close Back" ]]; then
+			return # User cancelled or pressed back
+		fi
+
+		local found=0
+		for dns_entry in "${dns_list[@]}"; do
+			if [ "$chosen" = "$dns_entry" ]; then
+				found=1
+				sure=$(echo -e "$icon_check\n$icon_close Back" | display_menu 1 "$tr_menu_dns_sure_prompt_1 ${chosen}$tr_menu_dns_sure_prompt_2" "") # Pass "" for prompt_icon
+				if [ -z "$sure" ] || [[ "$sure" =~ ^"$icon_close Back" ]]; then continue; fi                                                         # User cancelled confirmation
+				if [[ "$sure" =~ ^"$icon_check" ]]; then
+					nmcli connection modify uuid "$connection_uuid" -ipv${ipv}.dns "$chosen"
+				fi
+				break
+			fi
+		done
+
+		if [ "$found" -eq 0 ]; then
+			nmcli connection modify uuid "$connection_uuid" +ipv${ipv}.dns "$chosen"
+		fi
+	done
+}
+
+forget_connection() {
+	local chosen_connection_name="$1"
+	local connection_uuid="$2"
+	local options="$icon_check $tr_forget_connection_confirm\n$icon_close Back"
+	local sure=$(echo -e "$options" | display_menu 4 "$tr_forget_connection_sure_prompt_1 ${chosen_connection_name}$tr_forget_connection_sure_prompt_2" "")
+
+	if [ -z "$sure" ] || [[ "$sure" =~ ^"$icon_close Back" ]]; then return 1; fi # User cancelled or pressed back
+	if [[ "$sure" =~ ^"$icon_check $tr_forget_connection_confirm" ]]; then
+		nmcli connection delete uuid "$connection_uuid" && return 0
+	fi
+	return 1
+}
+
+rename_connection() {
+	local connection_uuid="$1"
+	local new_name=$(echo "" | display_menu 5 "$tr_rename_connection_prompt" "") # Pass "" for prompt_icon
+
+	if [ -z "$new_name" ]; then return 1; fi # User cancelled or pressed back
+
+	nmcli connection modify uuid "$connection_uuid" connection.id "$new_name"
+	return $?
+}
+
+edit_connection_password() {
+	local connection_uuid="$1"
+	local connection_name="$2"
+
+	# Prompt for new password
+	local pass=$(ask_password "$connection_name")
+
+	if [ -z "$pass" ]; then return 1; fi # Cancelled
+
+	# Update the connection
+	if nmcli connection modify uuid "$connection_uuid" wifi-sec.psk "$pass"; then
+		send_notification "$tr_password_updated" "$tr_notice_connected_body '$connection_name'"
+		return 0
+	else
+		send_notification "$tr_password_update_failed" "$tr_notice_error_body '$connection_name'"
+		return 1
+	fi
+}
+
+# Menu for individual connection settings (Wi-Fi)
+menu_connection() {
+	local chosen_connection_name="$1"
+	local connection_uuid="$2"
+	local autoconnect_state chosen
+
+	while true; do
+		autoconnect_state="$([ "$(nmcli --get-values connection.autoconnect connection show "$connection_uuid")" = "yes" ] && echo "$icon_on" || echo "$icon_off")"
+
+		# Check if this connection is currently active
+		local is_active=$(nmcli -t -f UUID connection show --active | grep -q "$connection_uuid" && echo "yes" || echo "no")
+
+		local options="$icon_automatic  $tr_autoconnect_message  $autoconnect_state\n"
+
+		if [ "$is_active" = "yes" ]; then
+			options+="$icon_wifi_disconnected  $tr_disconnect_message\n"
+		else
+			options+="$icon_connect  $tr_connect_now_message\n"
+		fi
+
+		options+="$icon_ipv4_config  $tr_ipv4_config_message\n"
+		options+="$icon_ipv6_config  $tr_ipv6_config_message\n"
+		options+="$icon_trash  $tr_forget_message\n"
+		options+="$icon_pen  $tr_rename_connection_message\n"
+		options+="$icon_password  $tr_edit_password_message\n" # NEW: Edit Password option
+		# Only show QR code for Wi-Fi connections
+		if [ "$(nmcli -g connection.type connection show "$connection_uuid")" = "802-11-wireless" ]; then
+			options+="$icon_qrcode  $tr_qrcode_message\n"
+		fi
+		options+="$icon_close Back"
+
+		chosen=$(echo -e "$options" | display_menu 1 "$chosen_connection_name" "") # Pass "" for prompt_icon
+
+		if [ -z "$chosen" ] || [[ "$chosen" =~ ^"$icon_close Back" ]]; then
+			return # User cancelled or pressed back
+		fi
+
+		case "$chosen" in
+		*"$tr_autoconnect_message"*)
+			if [ "$autoconnect_state" = "$icon_on" ]; then
+				nmcli connection modify uuid "$connection_uuid" autoconnect no
+			else
+				nmcli connection modify uuid "$connection_uuid" autoconnect yes
+			fi
+			;;
+		"$icon_connect  $tr_connect_now_message")
+			show_loading_notification "$tr_connecting_to"
+			if nmcli connection up uuid "$connection_uuid"; then
+				kill_loading_notification
+				send_notification "$tr_notice_connected_summary" "$tr_notice_connected_body '$chosen_connection_name'"
+				exit 0
+			else
+				kill_loading_notification
+				# Smart Recovery: Prompt to update password
+				# Check if user wants to retry with new password
+				local retry_choice=$(echo -e "$icon_check Yes\n$icon_close No" | display_menu 2 "$tr_connection_failed_retry" "")
+				if [[ "$retry_choice" =~ ^"$icon_check" ]]; then
+					if edit_connection_password "$connection_uuid" "$chosen_connection_name"; then
+						# Retry connection
+						show_loading_notification "$tr_connecting_to"
+						if nmcli connection up uuid "$connection_uuid"; then
+							kill_loading_notification
+							send_notification "$tr_notice_connected_summary" "$tr_notice_connected_body '$chosen_connection_name'"
+							exit 0
+						fi
+						kill_loading_notification
+					fi
+				fi
+				send_notification "$tr_notice_error_summary" "$tr_notice_error_body '$chosen_connection_name'"
+			fi
+			;;
+		"$icon_wifi_disconnected  $tr_disconnect_message")
+			# Disconnect logic
+			show_loading_notification "Disconnecting..."
+			if nmcli connection down uuid "$connection_uuid"; then
+				kill_loading_notification
+				send_notification "Disconnected" "Disconnected from '$chosen_connection_name'"
+				return
+			else
+				kill_loading_notification
+				show_message "Failed to disconnect."
+			fi
+			;;
+		*"$tr_ipv4_config_message"*) menu_ip_config "$chosen_connection_name" "$connection_uuid" "4" ;;
+		*"$tr_ipv6_config_message"*) menu_ip_config "$chosen_connection_name" "$connection_uuid" "6" ;;
+		*"$tr_forget_message"*) forget_connection "$chosen_connection_name" "$connection_uuid" && return ;;
+		*"$tr_rename_connection_message"*)
+			if rename_connection "$connection_uuid"; then
+				show_message "Connection renamed."
+				chosen_connection_name=$(nmcli --get-values connection.id connection show "$connection_uuid")
+			else
+				show_message "Failed to rename connection."
+			fi
+			;;
+		*"$tr_edit_password_message"*)
+			edit_connection_password "$connection_uuid" "$chosen_connection_name"
+			;;
+		*"$tr_qrcode_message"*)
+			local ssid=$(nmcli -g 802-11-wireless.ssid connection show "$connection_uuid")
+			local security=$(nmcli -g 802-11-wireless-security.key-mgmt connection show "$connection_uuid" | sed 's/wpa-psk/WPA/; s/None/nopass/')
+			local password=$(nmcli -s -g 802-11-wireless-security.psk connection show "$connection_uuid")
+			show_qrcode "$ssid" "$security" "$password"
+			;;
+		esac
+	done
+}
+
+# Menu for Wireguard connection settings
+menu_wireguard_connection() {
+	local chosen_connection_name="$1"
+	local connection_uuid="$2"
+	local state autoconnect_state chosen
+
+	while true; do
+		state="$([ "$(nmcli --get-values GENERAL.STATE connection show "$connection_uuid")" = "activated" ] && echo "$icon_on" || echo "$icon_off")"
+		autoconnect_state="$([ "$(nmcli --get-values connection.autoconnect connection show "$connection_uuid")" = "yes" ] && echo "$icon_on" || echo "$icon_off")"
+		local options="$icon_plug  $tr_wireguard_enable_message  $state\n"
+		options+="$icon_automatic  $tr_autoconnect_message  $autoconnect_state\n"
+		options+="$icon_pen  $tr_rename_connection_message\n"
+		options+="$icon_trash  $tr_forget_message\n"
+		options+="$icon_close Back"
+
+		chosen=$(echo -e "$options" | display_menu 1 "$chosen_connection_name" "") # Pass "" for prompt_icon
+
+		if [ -z "$chosen" ] || [[ "$chosen" =~ ^"$icon_close Back" ]]; then
+			return # User cancelled or pressed back
+		fi
+
+		case "$chosen" in
+		*"$tr_wireguard_enable_message"*)
+			if [ "$state" = "$icon_on" ]; then
+				nmcli connection down uuid "$connection_uuid"
+			else
+				nmcli connection up uuid "$connection_uuid"
+			fi
+			;;
+		*"$tr_autoconnect_message"*)
+			if [ "$autoconnect_state" = "$icon_on" ]; then
+				nmcli connection modify uuid "$connection_uuid" autoconnect no
+			else
+				nmcli connection modify uuid "$connection_uuid" autoconnect yes
+			fi
+			;;
+		*"$tr_rename_connection_message"*)
+			if rename_connection "$connection_uuid"; then
+				show_message "Connection renamed."
+				chosen_connection_name=$(nmcli --get-values connection.id connection show "$connection_uuid")
+			else
+				show_message "Failed to rename connection."
+			fi
+			;;
+		*"$tr_forget_message"*) forget_connection "$chosen_connection_name" "$connection_uuid" && return ;;
+		esac
+	done
+}
+
+# Menu for listing and managing known/saved connections
+menu_known_connections() {
+	local connection_filter="$1"
+	local icon_for_type
+	local menu_type_function
+	local profiles_list_raw chosen
+	local prompt_to_use
+
+	case "$connection_filter" in
+	"wifi")
+		icon_for_type="$icon_wireless"
+		menu_type_function="menu_connection"
+		profiles_list_raw=$(nmcli --colors no -t -f TYPE,UUID,NAME connection show | awk -F ':' -v icon="$icon_for_type" '$1 ~ /^(wifi|802-11-wireless).*/ {print $2 "\\0" icon "  " $3}')
+		prompt_to_use="$tr_known_connections_message" # Use descriptive text for prompt
+		;;
+	"wireguard")
+		icon_for_type="$icon_wireguard"
+		menu_type_function="menu_wireguard_connection"
+		profiles_list_raw=$(nmcli --colors no -t -f TYPE,UUID,NAME connection show | awk -F ':' -v icon="$icon_for_type" '$1 == "wireguard" {print $2 "\\0" icon "  " $3}')
+		prompt_to_use="$tr_vpn_menu_prompt" # Use new VPN prompt
+		;;
+	"ethernet")
+		icon_for_type="$icon_ethernet"
+		menu_type_function="menu_connection"
+		profiles_list_raw=$(nmcli --colors no -t -f TYPE,UUID,NAME connection show | awk -F ':' -v icon="$icon_for_type" '$1 ~ /^(ethernet|802-3-ethernet).*/ {print $2 "\\0" icon "  " $3}')
+		prompt_to_use="$tr_manage_wired_connections"
+		;;
+	*) # All saved connections
+		icon_for_type="$icon_saved"
+		menu_type_function="menu_connection"
+		profiles_list_raw=$(nmcli --colors no -t -f TYPE,UUID,NAME connection show | awk -F ':' -v icon_saved="$icon_saved" -v icon_wireless="$icon_wireless" -v icon_ethernet="$icon_ethernet" -v icon_vpn_disconnect="$icon_vpn_disconnect" '{
+                icon_local = icon_saved;
+                if ($1 == "wifi" || $1 == "802-11-wireless") icon_local = icon_wireless;
+                else if ($1 == "ethernet" || $1 == "802-3-ethernet") icon_local = icon_ethernet;
+                else if ($1 == "vpn" || $1 == "wireguard") icon_local = icon_vpn_disconnect;
+                print $2 "\\0" icon_local "  " $3 " (" $1 ")"
+            }')
+		prompt_to_use="$tr_saved_connections_menu_prompt" # Use new Saved Connections prompt
+		;;
+	esac
+
+	mapfile -t profiles_list < <(echo "$profiles_list_raw")
+
+	if [ "${#profiles_list[@]}" -eq 0 ] || ([ "${#profiles_list[@]}" -eq 1 ] && [ -z "${profiles_list[0]}" ]); then
+		display_info_message "$tr_no_saved_connections" "$prompt_to_use" # Pass custom prompt
+		return
+	fi
+
+	while true; do
+		local options=$(for i in "${profiles_list[@]}"; do echo -e "$i" | cut --delimiter $'\0' --fields 2; done)
+		options+="\n$icon_close Back" # FIX: Added newline before the Back entry
+
+		# Prompt for this menu is explicitly set by prompt_to_use, no extra icon prepended by display_menu
+		chosen=$(echo -e "$options" | display_menu 1 "$prompt_to_use" "") # Pass "" for prompt_icon
+
+		if [ -z "$chosen" ] || [[ "$chosen" =~ ^"$icon_close Back" ]]; then
+			return # User cancelled or pressed back
+		fi
+
+		for i in "${profiles_list[@]}"; do
+			if [ "$chosen" = "$(echo -e "$i" | cut --delimiter $'\0' --fields 2)" ]; then
+				local conn_uuid=$(echo -e "$i" | cut --delimiter $'\0' --fields 1)
+				local conn_name_display=$(echo "$chosen" | sed -E 's/^(󰋋|󰖩|󰈀|󰖂) //; s/ \(.+\)$//') # Changed 󰖃 to 󰖂
+				eval "$menu_type_function \"$(sed 's/"/\\"/g' <<<"$conn_name_display")\" \"$conn_uuid\""
+				break
+			fi
+		done
+	done
+}
+
+# NEW: Menu for connecting to a specific wired connection
+menu_connect_wired_connection() {
+	local profiles_list_raw=$(nmcli --colors no -t -f TYPE,UUID,NAME connection show | awk -F ':' -v icon="$icon_ethernet" '$1 ~ /^(ethernet|802-3-ethernet).*/ {print $2 "\\0" icon "  " " " $3}')
+	mapfile -t profiles_list < <(echo "$profiles_list_raw")
+
+	if [ "${#profiles_list[@]}" -eq 0 ] || ([ "${#profiles_list[@]}" -eq 1 ] && [ -z "${profiles_list[0]}" ]); then
+		display_info_message "No saved wired connections to connect to." "$tr_connect_wired_connection" # Pass custom prompt
+		return
+	fi
+
+	local options=$(for i in "${profiles_list[@]}"; do echo -e "$i" | cut --delimiter $'\0' --fields 2; done)
+	options+="\n$icon_close Back"
+
+	# Prompt for this menu is just the search icon
+	local chosen=$(echo -e "$options" | display_menu 1 "$tr_connect_wired_connection" "$icon_search")
+
+	if [ -z "$chosen" ] || [[ "$chosen" =~ ^"$icon_close Back" ]]; then
+		return # User cancelled or pressed back
+	fi
+
+	local conn_uuid_to_connect=""
+	local conn_name_to_connect=""
+
+	for i in "${profiles_list[@]}"; do
+		if [ "$chosen" = "$(echo -e "$i" | cut --delimiter $'\0' --fields 2)" ]; then
+			conn_uuid_to_connect=$(echo -e "$i" | cut --delimiter $'\0' --fields 1)
+			conn_name_to_connect=$(echo -e "$i" | cut --delimiter $'\0' --fields 2 | sed -E 's/^(󰈀) //') # Remove icon
+			break
+		fi
+	done
+
+	if [ -n "$conn_uuid_to_connect" ]; then
+		show_loading_notification "$tr_connecting_to '$conn_name_to_connect'..."
+		if nmcli connection up uuid "$conn_uuid_to_connect"; then
+			kill_loading_notification
+			send_notification "$tr_notice_connected_summary" "$tr_notice_connected_body '$conn_name_to_connect'"
+			exit 0
+		else
+			kill_loading_notification
+			send_notification "$tr_notice_error_summary" "$tr_notice_error_body '$conn_name_to_connect'"
+		fi
+	else
+		show_message "Could not find connection profile for $conn_name_to_connect." "$tr_connect_wired_connection" # Pass custom prompt
+	fi
+}
+
+# NEW: Wired Menu
+menu_wired() {
+	while true; do
+		local active_wired_conn=$(nmcli -t -f TYPE,DEVICE connection show --active | grep -vE "(wireless|vpn|wireguard)" | head -n 1)
+		local status_line=""
+		if [ -n "$active_wired_conn" ]; then
+			local active_device=$(echo "$active_wired_conn" | cut -d':' -f2)
+			status_line="$icon_ethernet $tr_status_message $tr_status_connected ($active_device)"
+		else
+			status_line="$icon_ethernet $tr_status_message $tr_status_disconnected"
+		fi
+
+		local options="$status_line\n"
+		options+="$icon_connect_wired Connect to Wired Connection\n" # FIX: Using icon_connect_wired
+		options+="$icon_config $tr_manage_wired_connections\n"       # Option to manage profiles
+		options+="$icon_close Back"
+
+		local choice=$(echo -e "$options" | display_menu 1 "$tr_wired_menu_prompt" "") # Pass "" for prompt_icon
+
+		if [ -z "$choice" ] || [[ "$choice" =~ ^"$icon_close Back" ]]; then
+			return # Go back to parent menu (main_menu)
+		fi
+
+		case "$choice" in
+		*"Connect to Wired Connection"*)
+			menu_connect_wired_connection # Call the function to connect to a wired network
+			;;
+		*"$tr_manage_wired_connections"*)
+			menu_known_connections "ethernet" # Reuse existing function for managing wired profiles
+			;;
+		*"$tr_status_message"*) # Do nothing if status line is selected
+			;;
+		*)
+			show_message "Invalid option selected: $choice" "$tr_wired_menu_prompt" # Pass custom prompt
+			;;
+		esac
+	done
+}
+
+# --- Status Functions ---
+
+show_active_connection_details() {
+	local device="$1"
+	local details_raw=$(nmcli --colors no device show "$device")
+
+	# Display the raw list in a filterable menu
+	local prompt_text="$icon_active_details Active Connection Details: $icon_search"
+	echo -e "${details_raw}\n${icon_close} Back" | display_menu 1 "$prompt_text" ""
+}
+
+status_menu() {
+	local options="$icon_active_details Active Connection Details\n$icon_devices All Device Status\n$icon_close Back"
+	# Use new status menu prompt
+	local choice=$(echo -e "$options" | display_menu 1 "$tr_status_menu_prompt" "") # Pass "" for prompt_icon
+
+	if [ -z "$choice" ] || [[ "$choice" =~ ^"$icon_close Back" ]]; then
+		return # User cancelled or pressed back
+	fi
+
+	case "$choice" in
+	*"Active Connection Details")
+		local active_conn_device=$(nmcli -t -f DEVICE connection show --active | head -n 1)
+		if [ -z "$active_conn_device" ]; then
+			display_info_message "$tr_no_active_connection"
+		else
+			show_active_connection_details "$active_conn_device"
+		fi
+		;;
+	*"All Device Status")
+		local device_status=$(nmcli device status)
+		display_info_message "$device_status" "$tr_status_menu_prompt" # Pass custom prompt
+		;;
+	esac
+}
+
+# --- Airplane Mode ---
+toggle_airplane_mode() {
+	local wifi_state=$(nmcli radio wifi)
+	local wwan_state=$(nmcli radio wwan 2>/dev/null || echo "disabled") # WWAN may not exist
+
+	if [ "$wifi_state" = "enabled" ] || [ "$wwan_state" = "enabled" ]; then
+		# Disable all radios
+		nmcli radio wifi off
+		nmcli radio wwan off 2>/dev/null
+		show_message "$tr_airplane_on" "$tr_airplane_mode_message"
+	else
+		# Enable all radios
+		nmcli radio wifi on
+		nmcli radio wwan on 2>/dev/null
+		show_message "$tr_airplane_off" "$tr_airplane_mode_message"
+	fi
+}
+
+# --- QR Code Sharing ---
+show_qrcode() {
+	local ssid="$1"
+	local security="$2"
+	local password="$3"
+
+	if ! command -v qrencode &>/dev/null; then
+		show_message "$tr_qrcode_error" "$tr_qrcode_message"
+		return
+	fi
+
+	if [ -z "$password" ]; then
+		show_message "$tr_qrcode_no_password" "$tr_qrcode_message"
+		return
+	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
+}
+
+# Get password for a known Wi-Fi connection
+get_wifi_password() {
+	local ssid="$1"
+	local uuid=$(nmcli -t -f NAME,UUID connection show | grep "^${ssid}:" | cut -d':' -f2)
+	if [ -n "$uuid" ]; then
+		nmcli -s -g 802-11-wireless-security.psk connection show "$uuid" 2>/dev/null
+	fi
+}
+
+# --- Hotspot Creation ---
+create_hotspot() {
+	local ssid=$(echo "" | display_menu 5 "$tr_hotspot_ssid_prompt" "")
+	if [ -z "$ssid" ]; then
+		return
+	fi
+
+	local password=$(echo "" | display_menu 3 "$tr_hotspot_password_prompt" "")
+	if [ -z "$password" ] || [ ${#password} -lt 8 ]; then
+		show_message "Password must be at least 8 characters." "$tr_hotspot_message"
+		return
+	fi
+
+	show_loading_notification "$tr_hotspot_creating"
+	if nmcli device wifi hotspot ifname "$interface_to_use" ssid "$ssid" password "$password"; then
+		kill_loading_notification
+		show_message "$tr_hotspot_success\nSSID: $ssid" "$tr_hotspot_message"
+	else
+		kill_loading_notification
+		show_message "$tr_hotspot_error" "$tr_hotspot_message"
+	fi
+}
+
+# --- VPN Functions ---
+
+import_vpn() {
+	local vpn_file_path=$(echo "" | display_menu 5 "$tr_import_vpn_prompt" "")
+
+	if [ -z "$vpn_file_path" ]; then
+		return
+	fi
+
+	if [ ! -f "$vpn_file_path" ]; then
+		send_notification "$tr_notice_import_error_summary" "$tr_notice_file_not_found_body"
+		return
+	fi
+
+	local vpn_type=""
+	case "$vpn_file_path" in
+	*.conf) vpn_type="wireguard" ;;
+	*.ovpn) vpn_type="openvpn" ;;
+	*)
+		send_notification "$tr_notice_import_error_summary" "$tr_notice_unknown_vpn_type_body"
+		return
+		;;
+	esac
+
+	if nmcli connection import type "$vpn_type" file "$vpn_file_path"; then
+		send_notification "$tr_notice_import_success_summary" "$tr_notice_import_success_body"
+	else
+		send_notification "$tr_notice_import_error_summary" "$tr_notice_import_error_body"
+	fi
+}
+
+toggle_vpn_connection() {
+	local uuid="$1"
+	local name="$2"
+	local state="$3"
+
+	if [ "$state" = "activated" ]; then
+		show_loading_notification "$tr_disconnecting_from '$name'..."
+		if nmcli connection down uuid "$uuid"; then
+			kill_loading_notification
+			send_notification "$tr_notice_disconnected_summary" "$tr_notice_disconnected_body '$name'"
+			exit 0
+		else
+			kill_loading_notification
+			send_notification "$tr_notice_error_summary" "$tr_notice_error_disconnect_body '$name'"
+		fi
+	else
+		show_loading_notification "$tr_connecting_to"
+		if nmcli connection up uuid "$uuid"; then
+			kill_loading_notification
+			send_notification "$tr_notice_connected_summary" "$tr_notice_connected_body '$name'"
+			exit 0
+		else
+			kill_loading_notification
+			send_notification "$tr_notice_error_summary" "$tr_notice_error_body '$name'"
+		fi
+	fi
+}
+
+menu_available_vpns() {
+	while true; do
+		mapfile -t vpn_list < <(nmcli --colors no -t -f TYPE,UUID,NAME connection show | awk -F ':' '$1 == "vpn" || $1 == "wireguard" {print $2 "\\0" $3}')
+
+		if [ "${#vpn_list[@]}" -eq 0 ] || [ -z "${vpn_list[0]}" ]; then
+			display_info_message "$tr_no_configured_vpns" "$tr_available_vpn_profiles_message"
+			return
+		fi
+
+		local options=""
+		for i in "${vpn_list[@]}"; do
+			local uuid=$(echo -e "$i" | awk 'BEGIN{FS="\x00"}{print$1}')
+			local name=$(echo -e "$i" | awk 'BEGIN{FS="\x00"}{print$2}')
+			local state=$(nmcli --get-values GENERAL.STATE connection show uuid "$uuid")
+			local state_icon="$([ "$state" = "activated" ] && echo "$icon_on" || echo "$icon_off")"
+			options+="$state_icon  $name\n"
+		done
+		options+="$icon_close Back"
+
+		local chosen=$(echo -e "$options" | display_menu 1 "$tr_available_vpn_profiles_message" "$icon_search")
+
+		if [ -z "$chosen" ] || [[ "$chosen" =~ ^"$icon_close Back" ]]; then
+			return
+		fi
+
+		local chosen_name=$(echo "$chosen" | sed -E 's/^(|)  //')
+		for i in "${vpn_list[@]}"; do
+			local uuid=$(echo -e "$i" | awk 'BEGIN{FS="\x00"}{print$1}')
+			local name=$(echo -e "$i" | awk 'BEGIN{FS="\x00"}{print$2}')
+			if [ "$name" = "$chosen_name" ]; then
+				local state=$(nmcli --get-values GENERAL.STATE connection show uuid "$uuid")
+				toggle_vpn_connection "$uuid" "$name" "$state"
+				break
+			fi
+		done
+	done
+}
+
+vpn_menu() {
+	while true; do
+		local options="$icon_vpn_disconnect  $tr_available_vpn_profiles_message\n"
+		options+="$icon_import  $tr_import_vpn_message\n"
+		options+="$icon_close Back"
+
+		local choice=$(echo -e "$options" | display_menu 1 "$tr_vpn_menu_prompt" "")
+
+		if [ -z "$choice" ] || [[ "$choice" =~ ^"$icon_close Back" ]]; then
+			return
+		fi
+
+		case "$choice" in
+		*"$tr_available_vpn_profiles_message"*)
+			menu_available_vpns
+			;;
+		*"$tr_import_vpn_message"*)
+			import_vpn
+			;;
+		esac
+	done
+}
+
+main_menu() {
+	local options="$icon_wifi_full  Wi-Fi\n"
+	options+="$icon_ethernet  Wired\n"
+	options+="$icon_vpn_disconnect  VPN\n"
+	options+="$icon_bookmark_saved  Saved Connections\n"
+	options+="$icon_status_chart  Status\n"
+	options+="$icon_airplane  $tr_airplane_mode_message\n"
+	options+="$icon_close Exit"
+
+	while true; do                                                                 # Keep looping for the main menu
+		local choice=$(echo -e "$options" | display_menu 1 "$tr_main_menu_prompt" "") # Pass "" for prompt_icon
+
+		if [ -z "$choice" ]; then
+			# If user presses Esc on the main menu, exit the script
+			exit 0
+		fi
+
+		case "$choice" in
+		"$icon_close Exit") exit 0 ;; # Only exit if 'Exit' is explicitly chosen
+		*"Wi-Fi")
+			menu_wifi
+			;;
+		*"Wired")
+			menu_wired # Call the new wired sub-menu
+			;;
+		*"VPN")
+			vpn_menu
+			;;
+		*"Saved Connections")
+			menu_known_connections "all"
+			;;
+		*"Status")
+			status_menu
+			;;
+		*"$tr_airplane_mode_message")
+			toggle_airplane_mode
+			;;
+		esac
+	done # End of main menu while loop
+}
+
+# --- Run ---
+[ -n "${DEBUG:-}" ] && set -x
+main_menu
diff --git a/bin/pasm b/bin/pasm
new file mode 100755
index 0000000..7555167
--- /dev/null
+++ b/bin/pasm
@@ -0,0 +1,71 @@
+#!/bin/sh
+# local is well supported
+# shellcheck disable=3043
+set -eux
+
+# shellcheck disable=2034
+description="rbw-based password manager"
+
+FUZL_PROMPT=pasm
+FUZL_ICON=󰈲
+. libfuzl.sh
+
+main() {
+	rbw unlocked 2>/dev/null ||
+		rbw unlock || exit 1
+
+	local eid
+	eid=$(rbw_select_entry)
+
+	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_menu "$eid" || exit 1
+}
+
+#
+# helpers
+#
+
+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)}' |
+		fuzl_menu $FUZL_MENU_FILTER --with-nth='{2}       {3..}' --accept-nth=1 -w 80
+}
+
+rbw_has_totp() {
+	local eid=${1:?missing eid}
+	# can't use fuzl_try here because we need the exit status
+	rbw get -l "$eid" | grep -q totp
+}
+rbw_entry() {
+	local eid=${1:?missing eid}
+	fuzl_try -- rbw get --raw "$eid" | jq -r '.name'
+}
+
+#
+# actions
+#
+
+pasm_copy() {
+	local eid=${1:?missing eid}
+	fuzl_try -- rbw get -f password "$eid" | wl-copy -n
+	fuzl_try -- rbw get -f username "$eid" | wl-copy -p -n
+	fuzl_notification "$(rbw_entry "$eid")" "copied to clipboard"
+}
+
+pasm_totp() {
+	local eid=${1:?missing eid}
+	fuzl_try -- rbw totp "$eid" | wl-copy -n
+	fuzl_notification "$(rbw_entry "$eid")" "copied totp to clipboard"
+}
+
+pasm_url() {
+	local eid=${1:?missing eid}
+	fuzl_try -- rbw get -f uris "$eid" | head -n1 | wl-copy -n
+	fuzl_notification "$(rbw_entry "$eid")" "copied url to clipboard"
+}
+
+main
diff --git a/bin/play-mpv b/bin/play-mpv
new file mode 100755
index 0000000..21f20c6
--- /dev/null
+++ b/bin/play-mpv
@@ -0,0 +1,112 @@
+#!/usr/bin/env python3
+
+"""
+This script emulates "unique application" functionality. When starting
+playback with this script, it will try to reuse an already running instance of
+mpv (but only if that was started with umpv). Other mpv instances (not started
+by umpv) are ignored, and the script doesn't know about them.
+
+This only takes filenames as arguments. Custom options can't be used; the script
+interprets them as filenames. If mpv is already running, the files passed to
+umpv are appended to mpv's internal playlist. If a file does not exist or is
+otherwise not playable, mpv will skip the playlist entry when attempting to
+play it (from the GUI perspective, it's silently ignored).
+
+If mpv isn't running yet, this script will start mpv and let it control the
+current terminal. It will not write output to stdout/stderr, because this
+will typically just fill ~/.xsession-errors with garbage.
+
+mpv will terminate if there are no more files to play, and running the umpv
+script after that will start a new mpv instance.
+
+Note: you can supply custom mpv path and options with the MPV environment
+      variable. The environment variable will be split on whitespace, and the
+      first item is used as path to mpv binary and the rest is passed as options
+      _if_ the script starts mpv. If mpv is not started by the script (i.e. mpv
+      is already running), this will be ignored.
+"""
+
+import os
+import shlex
+import socket
+import string
+import subprocess
+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)
+
+def is_url(filename: str) -> bool:
+    parts = filename.split("://", 1)
+    if len(parts) < 2:
+        return False
+    # protocol prefix has no special characters => it's an URL
+    allowed_symbols = string.ascii_letters + string.digits + "_"
+    prefix = parts[0]
+    return all(c in allowed_symbols for c in prefix)
+
+def get_socket_path() -> str:
+    base_dir = (
+        os.getenv("UMPV_SOCKET_DIR") or
+        os.getenv("XDG_RUNTIME_DIR") or
+        os.getenv("HOME") or
+        os.getenv("TMPDIR")
+    )
+
+    if not base_dir:
+        raise Exception("Could not determine a base directory for the socket. "
+                        "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")
+
+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())
+
+    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([
+        "--profile=builtin-pseudo-gui",
+        f"--input-ipc-server={socket_path}",
+        "--",
+    ])
+    mpv_command.extend(files)
+    print(mpv_command)
+    # 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:])
+    socket_path = get_socket_path()
+
+    try:
+        with socket.socket(socket.AF_UNIX) as sock:
+            sock.connect(socket_path)
+            send_files_to_mpv(sock, files)
+    except (FileNotFoundError, ConnectionRefusedError):
+        start_mpv(files, socket_path)
+
+if __name__ == "__main__":
+    main()
diff --git a/bin/play-music b/bin/play-music
new file mode 100755
index 0000000..14e91f1
--- /dev/null
+++ b/bin/play-music
@@ -0,0 +1,23 @@
+#!/bin/sh
+set -eux
+
+music_dir=/run/media/robert/fwdrive/music
+
+mpa() {
+	mpv --profile=mpa \
+		--directory-filter-types=audio \
+		--autocreate-playlist=filter \
+		--wayland-app-id=mpv.play-music \
+		"$@"
+}
+
+case "$1" in
+shuffle)
+	mpa --shuffle $music_dir
+	;;
+select)
+	cd $music_dir
+	subdir=$(find . -type d -printf "%P\n" | shuf | fuzzel -d -w 80)
+	mpa "$subdir"
+	;;
+esac
diff --git a/bin/rsyncez b/bin/rsyncez
new file mode 100755
index 0000000..0275971
--- /dev/null
+++ b/bin/rsyncez
@@ -0,0 +1,19 @@
+#!/bin/sh
+
+set -e
+[ -n "$DEBUG" ] && set -x
+
+[ "$1" = "-mac" ] && shift && mac_args='--rsync-path=/usr/local/bin/rsync'     # force brew version
+[ "$1" = "-mac2" ] && shift && mac_args='--rsync-path=/opt/homebrew/bin/rsync' # force brew version
+exec rsync \
+	--verbose \
+	-hhh \
+	--info=flist2,name,progress \
+	--partial \
+	--rsh="ssh -x -T -o Compression=yes -o RemoteCommand=none" \
+	--archive \
+	--protect-args \
+	${mac_args} \
+	"$@"
+
+# -c chacha20-poly1305@openssh.com
diff --git a/bin/susm b/bin/susm
new file mode 100755
index 0000000..197013a
--- /dev/null
+++ b/bin/susm
@@ -0,0 +1,30 @@
+#!/bin/bash
+set -eu
+
+# shellcheck disable=2034
+description="suspend manager"
+
+FUZL_PROMPT=susm
+. libfuzl.sh
+
+main() {
+	local action
+
+	action=$( (
+		printf '%s\t%s\n' 󰒲 'suspend'
+		printf '%s\t%s\n' 󰤁 'reboot'
+		printf '%s\t%s\n'  'poweroff'
+		printf '%s\t%s\n' 󰌾 'lock'
+	) | fuzl_menu $FUZL_MENU_FILTER --with='{1}  {2}' --accept-nth=2)
+
+	case "$action" in
+	suspend | poweroff | reboot)
+		exec loginctl $action
+		;;
+	lock)
+		exec waylock
+		;;
+	esac
+}
+
+main
diff --git a/bin/svcm b/bin/svcm
new file mode 100755
index 0000000..b7daf39
--- /dev/null
+++ b/bin/svcm
@@ -0,0 +1,108 @@
+#!/bin/bash
+set -eu
+
+# shellcheck disable=2034
+description="service manager"
+
+export FUZL_PROMPT=svcm
+export FUZL_ICON=󰲽
+. libfuzl.sh
+
+SVCM_SCOPE=usr
+
+main() {
+	fuzl_action_add "usr" "󰀄" "User"
+	fuzl_action_add "sys" "󰟀" "System"
+	fuzl_action_menu || return
+
+	select_service
+}
+
+#
+# helpers
+#
+
+select_service() {
+	local rl
+	rl=$(runlevel_current)
+
+	local srv
+	srv=$(get_services |
+		FUZL_PROMPT="$FUZL_PROMPT [$rl]" fuzl_menu $FUZL_MENU_FILTER || true)
+
+	test -n "$srv" || main
+
+	if [[ "$srv" = runlevel:* ]]; then
+		runlevel_start "$(cut -c10- <<<"$srv")"
+		return
+	fi
+
+	fuzl_action_add "restart" "󰜉" "Restart"
+	fuzl_action_add "start" "󰈸" "Start"
+	fuzl_action_add "stop" "" "Stop"
+	fuzl_action_menu "$srv" || select_service
+}
+
+get_services() {
+	case "${SVCM_SCOPE:?missing scope}" in
+	usr)
+		fuzl_try -- rc-service -U -l | sort -u
+		find ~/.config/rc/runlevels/ -mindepth 1 -type d -printf 'runlevel:%P\n'
+		;;
+	sys)
+		fuzl_try -- rc-service -l | sort -u
+		find /etc/runlevels/ -mindepth 1 -type d -printf 'runlevel:%P\n'
+		;;
+	esac
+}
+
+runlevel_current() {
+	case "${SVCM_SCOPE:?missing scope}" in
+	usr) fuzl_try -- rc-status -U -r ;;
+	sys) fuzl_try -- rc-status -r ;;
+	esac
+}
+runlevel_start() {
+	local rl=${1:?missing runlevel}
+	case "${SVCM_SCOPE:?missing scope}" in
+	usr) fuzl_try -- openrc -U "$rl" ;;
+	sys) fuzl_try -- doas -n openrc "$rl" ;;
+	esac
+}
+
+#
+# actions
+#
+
+svcm_usr() {
+	SVCM_SCOPE=usr
+}
+svcm_sys() {
+	SVCM_SCOPE=sys
+}
+
+svcm_start() {
+	local srv=${1:?missing service}
+	case "${SVCM_SCOPE:?missing scope}" in
+	usr) fuzl_try -- rc-service -U "$srv" start ;;
+	sys) fuzl_try -- doas -n rc-service "$srv" start ;;
+	esac
+}
+
+svcm_stop() {
+	local srv=${1:?missing service}
+	case "${SVCM_SCOPE:?missing scope}" in
+	usr) fuzl_try -- rc-service -U "$srv" stop ;;
+	sys) fuzl_try -- doas -n rc-service "$srv" stop ;;
+	esac
+}
+
+svcm_restart() {
+	local srv=${1:?missing service}
+	case "${SVCM_SCOPE:?missing scope}" in
+	usr) fuzl_try -- rc-service -U "$srv" restart ;;
+	sys) fuzl_try -- doas -n rc-service "$srv" stop ;;
+	esac
+}
+
+main
diff --git a/bin/todo b/bin/todo
new file mode 100755
index 0000000..7436779
--- /dev/null
+++ b/bin/todo
@@ -0,0 +1,216 @@
+#!/bin/bash
+set -e
+[ -n "$DEBUG" ] && set -x
+
+_todo_files=("todo.txt" "TODO")
+_todo_file=""
+
+function has_todo_file() {
+	cwd=$(pwd)
+	for f in "${_todo_files[@]}"; do
+		test -f "$cwd/$f" && _todo_file="$cwd/$f" && return 0
+	done
+	return 1
+}
+
+function print_org() {
+	awk '
+function filename() {
+    n = split(FILENAME, a, "/")
+    return a[n]
+}
+function res() {
+    return "\033[0m"
+}
+function bold(s) {
+    return "\033[1m" s "\033[22m"
+}
+function strike(s) {
+    return "\033[9m" s "\033[29m"
+}
+function red(s) {
+    return "\033[31m" s res()
+}
+function green(s) {
+    return "\033[32m" s res()
+}
+function blue(s) {
+    return "\033[34m" s res()
+}
+function magenta(s) {
+    return "\033[35m" s res()
+}
+function osc8(uri, s) {
+    return "\033]8;;" uri "\033\\\\" s "\033]8;;\033\\"
+}
+function link(uri) {
+    return osc8(uri, "\033[2m" uri res())
+}
+{
+    if ($1 ~ /^#/) next
+
+    // support todotxt files
+    if (filename() == "todo.txt") {
+        status = "TODO"
+        if ($1 ~ /^x/) {
+            status = "DONE"
+            $0 = gensub(/^[x] /, "", "1")
+            $0 = strike($0)
+        }
+
+	$0 = gensub(/ (\+[^ ]+)/, bold(blue(" \\1")), "g")
+        $0 = gensub(/ (@[^ ]+)/, bold(blue(" \\1")), "g")
+        $0 = gensub(/(https?:\/\/[^ ]+)/, link("\\1"), "g")
+        tasks[status][i++] = $0
+        next
+    }
+
+    // support TODO files
+    if ($1 ~ /^(-|*)/) {
+        // detect status strings
+        match($0, /\s(TODO|WIP|WAITING|DONE)\s/, arr)
+
+        if (filename() == "TODO" && arr[1] == "") {
+	    $0 = gensub(/(^[\w]*(-|\*))/, "\\1 TODO", "1")
+        }
+
+        $0 = gensub(/(TODO)/, red("\\1"), "1")
+        $0 = gensub(/(WIP)/, blue("\\1"), "1")
+        $0 = gensub(/(DONE)/, green("\\1"), "1")
+        $0 = gensub(/(\?\?\?)/, bold(magenta("\\1")), "1")
+        tasks[arr[1]][i++] = $0
+    }
+}
+END {
+    order[1] = "TODO"
+    order[2] = "WIP"
+    order[3] = "DONE"
+    for (i in order) {
+        for (t in tasks[order[i]]) {
+            print "  " tasks[order[i]][t]
+        }
+    }
+}' $1
+}
+
+function print_hr() {
+	local str cols
+	local start line end
+
+	str="$1"
+	[[ -n $str ]] && str=" $str "
+	cols=$((${COLUMNS:-$(tput cols)} - ${#str} - 3))
+	start=$'\e(0'
+	end=$'\e(B'
+	line="qqq"
+	while ((${#line} < $cols)); do line+=$line; done
+
+	printf "╾"
+	printf "%s%s%s" "$start" "${line:0:$((cols / 2))}" "$end"
+	printf "%s%s%s" "$start" "${line:0:$((cols / 2))}" "$end"
+	printf "%s" "$str"
+	printf "%s%s%s" "$start" "${line:0:1}" "$end"
+	printf "╼"
+	printf "\n"
+}
+
+function print_shell_hook() {
+	case $1 in
+	bash)
+		cat <<-EOF
+			# Hook to run todo on directory change.
+			__todo_oldpwd="\$(\builtin pwd -L)"
+
+			function __todo_hook() {
+			    \builtin local -r retval="$?"
+			    \builtin local pwd_tmp
+			    pwd_tmp="\$(\builtin pwd -L)"
+			    if [[ \${__todo_oldpwd} != "\${pwd_tmp}" ]]; then
+			        __todo_oldpwd="\${pwd_tmp}"
+			        \command todo
+			    fi
+			    return "\${retval}"
+			}
+
+			# Initialize hook.
+			if [[ \${PROMPT_COMMAND:=} != *'__todo_hook'* ]]; then
+			    PROMPT_COMMAND="__todo_hook;\${PROMPT_COMMAND#;}"
+			fi
+
+			# Run at startup.
+			\command sleep .1 && \command todo
+		EOF
+		;;
+	zsh)
+		cat <<-EOF
+			__todo_oldpwd="$(\builtin pwd -L)"
+			function __todo_hook() {
+				\builtin local -r retval="$?"
+				\builtin local pwd_tmp
+				if [[ \${__todo_oldpwd} != "\${pwd_tmp}" ]]; then
+					__todo_oldpwd="\${pwd_tmp}"
+					\command ${0}
+				fi
+				return "\${retval}"
+			}
+			typeset -ag precmd_functions;
+			if [[ -z \${precmd_functions[(r)__todo_hook]} ]]; then
+				precmd_functions+=__todo_hook;
+			fi
+		EOF
+		;;
+	cd)
+		echo "cd() { builtin cd \"\$@\" && $0; }"
+		;;
+	-h | --help | *)
+		echo "usage: todo hook <integration>"
+		echo ""
+		echo "use like this: eval \$(todo hook <integration>)"
+		echo ""
+		echo "supported integrations:"
+		echo " - zsh: run todo as a precmd hook"
+		echo "   (this can get quite annoying when you're in a direcory that has a todo file)"
+		echo " - cd: run todo as a post-cd hook"
+		echo ""
+		exit 1
+		;;
+	esac
+}
+
+function main() {
+	case "$1" in
+	hook)
+		shift
+		print_shell_hook "$1"
+		;;
+	-h | --help)
+		echo "usage: todo [-r] [hook <integration>]"
+		echo "A quite stupid todo/agenda script that reminds you of things you need to do"
+		echo ""
+		echo " -r, --recursive    walk the file tree, searching for todos"
+		echo " <integration>      shell integration, you should eval this"
+		echo ""
+		exit 0
+		;;
+	-r | --recursive | -R)
+		while read FILE; do
+			print_hr "[$FILE]"
+			print_org $FILE
+		done < <(rg --files ${_todo_files[@]/#/-g })
+		;;
+	*)
+		if [ -f "$1" ]; then
+			print_hr "[$1]"
+			print_org $1
+			exit 0
+		fi
+		if has_todo_file; then
+			print_hr "[$(basename $_todo_file)]"
+			print_org $_todo_file
+			print_hr
+		fi
+		;;
+	esac
+}
+
+main $@
diff --git a/bin/urlopen b/bin/urlopen
new file mode 100755
index 0000000..2b0479c
--- /dev/null
+++ b/bin/urlopen
@@ -0,0 +1,89 @@
+#!/bin/bash
+set -eu
+
+# shellcheck disable=2034
+description="url handler"
+
+FUZL_PROMPT=urlopen
+. libfuzl.sh
+
+main() {
+	case "$#" in
+	1)
+		fuzl_action_add "open" "" "open"
+		fuzl_action_add "ask" "󰮫" "ask"
+		fuzl_action_add "clipboard" "󰅌" "clipboard"
+		fuzl_action_add "primary" "󱊁" "primary"
+		fuzl_action_menu "$(get_text "$*")"
+		;;
+	2)
+		local action
+
+		action=${1:?missing action}
+		shift
+		eval "urlopen_$action" "'$(get_text "$*")'"
+		;;
+	esac
+}
+
+#
+# helpers
+#
+
+get_text() {
+	local text="$*"
+	[ -z "$text" ] && read -r text
+	printf '%s' "$text"
+}
+
+get_mimetype() {
+	local text=${1:?missing url}
+	case "$text" in
+	http*) printf %s 'x-scheme-handler/http' ;;
+	*) file -bdE --mime-type "$text" ;;
+	esac
+}
+open_default() {
+	local text=${1:?missing url}
+	local application
+	case "$(get_mimetype "$text")" in
+	x-scheme-handler/http*) application=librewolf ;;
+	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"
+	fi
+}
+#
+# actions
+#
+
+urlopen_ask() {
+	local text=${1:?missing url}
+	env porta open-uri "$text" ask
+}
+
+urlopen_open() {
+	local text=${1:?missing url}
+	open_default "$text"
+}
+
+urlopen_clipboard() {
+	local text=${1:?missing url}
+	wl-copy -n "$text"
+	# fuzl_notification "copied to clipboard" "$text"
+}
+
+urlopen_primary() {
+	local text=${1:?missing url}
+	wl-copy -np "$text"
+	# fuzl_notification "copied to primary" "$text"
+}
+
+main $@
diff --git a/bin/vpnm b/bin/vpnm
new file mode 100755
index 0000000..f09b41a
--- /dev/null
+++ b/bin/vpnm
@@ -0,0 +1,124 @@
+#!/bin/bash
+set -eu
+
+# shellcheck disable=2034
+description="vpn manager"
+
+FUZL_PROMPT=vpnm
+FUZL_ICON=󰦝
+. libfuzl.sh
+
+main() {
+	local conn
+
+	conn=$(get_current_connection)
+	[ -n "$conn" ] && fuzl_action_add "down" "󰦝" "Connected to $conn"
+	fuzl_action_add "networks" "󰾰" "Available networks"
+	fuzl_action_add "profile" "󱗼" "Switch profile"
+
+	fuzl_action_menu || return
+}
+
+#
+# helpers
+#
+
+get_emoji_flag() {
+	code=
+	# TODO: calc flag
+	# U\1F1E6 A
+	# U\200D zero-w joiner
+	case "$*" in
+	au-*) code=$'🦘' ;;
+	be-*) code=$'🇧🇪' ;;
+	br-*) code=$'🇧🇷' ;;
+	ca-*) code=$'🇨🇦' ;;
+	de-*) code=$'🇩🇪' ;;
+	fr-*) code=$'🇫🇷' ;;
+	gb-*) code=$'🇬🇧' ;;
+	jp-*) code=$'🇯🇵' ;;
+	no-*) code=$'🇳🇴' ;;
+	se-*) code=$'🇸🇪' ;;
+	us-*) code=$'🇺🇸' ;;
+	*tail2d6ce*) code=$'󰠜' ;;
+	*) code=$'🌐' ;;
+	esac
+	echo -n $code
+}
+
+get_vpn_connections() {
+	(
+		tailscale exit-node list |
+			awk '/.tail2d6ce.ts.net/{print $2, $1}'
+		tailscale exit-node list |
+			awk -F '[ ]{2,}' '/.mullvad.ts.net/{print $2, $1, ($3 "/" $4)}' |
+			sort -u
+	) | while read -r conn ip location; do
+		printf '%s\t' "$ip"
+		printf '%s\t' "$(get_emoji_flag "$conn")"
+		if grep -q 'mullvad.ts.net' <<<"$conn"; then
+			printf '%s\t' "$location"
+		else
+			printf '%s\t' "$conn"
+		fi
+		printf '\n'
+	done
+}
+
+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
+}
+
+vpnup() {
+	fuzl_try -- tailscale set --exit-node=${1:?}
+	fuzl_try -- pkill -RTMIN+2 -x waybar
+}
+
+vpndown() {
+	fuzl_try -- tailscale set --exit-node=
+	fuzl_try -- pkill -RTMIN+2 -x waybar
+}
+
+#
+# actions
+#
+
+vpnm_down() {
+	fuzl_try -- vpndown
+}
+
+vpnm_networks() {
+	ip=$(get_vpn_connections |
+		fuzl_menu $FUZL_MENU_FILTER --with-nth='{2}  {3}' --accept-nth=1)
+
+	[ -n "$ip" ] || main
+	fuzl_try -- vpnup "$ip"
+
+}
+
+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)
+
+	[ -n "$profile" ] || main
+	fuzl_try -- tailscale switch "$profile"
+	sleep 2
+	fuzl_try -- pkill -SIGRTMIN+2 waybar
+}
+
+main