From feaeff3464dfd682fff3e1b645734854837c957a Mon Sep 17 00:00:00 2001
From: Barrett Ruth
Date: Mon, 9 Feb 2026 13:59:37 -0500
Subject: [PATCH] cleanup
---
home/home.nix | 4 +-
scripts/ctl | 110 ++++++++++++++++-
scripts/doc | 37 +++++-
scripts/hypr | 335 +++++++++++++++++++++++++++++++++++++++++++++++++-
scripts/mux | 242 +++++++++++++++++++++++++++++++++++-
scripts/pipes | 228 +++++++++++++++++++++++++++++++++-
scripts/theme | 135 +++++++++++++++++++-
scripts/wp | 149 +++++++++++++++++++++-
scripts/x | 69 ++++++++++-
9 files changed, 1299 insertions(+), 10 deletions(-)
mode change 120000 => 100755 scripts/ctl
mode change 120000 => 100755 scripts/doc
mode change 120000 => 100755 scripts/hypr
mode change 120000 => 100755 scripts/mux
mode change 120000 => 100755 scripts/pipes
mode change 120000 => 100755 scripts/theme
mode change 120000 => 100755 scripts/wp
mode change 120000 => 100755 scripts/x
diff --git a/home/home.nix b/home/home.nix
index 4dd5674..16a3a6c 100644
--- a/home/home.nix
+++ b/home/home.nix
@@ -32,8 +32,8 @@ in
news.display = "silent";
home.file.".local/bin/scripts" = {
- source = ../scripts;
- recursive = true;
+ source = config.lib.file.mkOutOfStoreSymlink "${config.home.homeDirectory}/nix-config/scripts";
+ recursive = false;
executable = true;
};
diff --git a/scripts/ctl b/scripts/ctl
deleted file mode 120000
index 4181d44..0000000
--- a/scripts/ctl
+++ /dev/null
@@ -1 +0,0 @@
-/nix/store/i53klf9mw9p2k5nfawr6r2b0v3jyq1si-home-manager-files/.local/bin/scripts/ctl
\ No newline at end of file
diff --git a/scripts/ctl b/scripts/ctl
new file mode 100755
index 0000000..2e6ba10
--- /dev/null
+++ b/scripts/ctl
@@ -0,0 +1,109 @@
+#!/bin/sh
+
+require() {
+ for cmd in "$@"; do
+ command -v "$cmd" >/dev/null 2>&1 || {
+ echo "ctl: missing dependency: $cmd" >&2
+ exit 1
+ }
+ done
+}
+
+case "$1" in
+screenshot)
+ dir="$HOME/img/ss"
+ mkdir -p "$dir"
+ file="$dir/$(openssl rand -hex 10)-$(date +'%Y-%m-%d_%H-%M-%S').png"
+ if [ "${XDG_SESSION_TYPE:-}" = "wayland" ]; then
+ require grim slurp wl-copy
+ grim -g "$(slurp)" "$file" && wl-copy < "$file"
+ else
+ require maim xclip
+ maim -s "$file" && xclip -selection clipboard -t image/png -i "$file" &
+ fi
+ ;;
+ocr)
+ require tesseract
+ dir="$HOME/img/ss"
+ mkdir -p "$dir"
+ file="$dir/$(openssl rand -hex 10)-$(date +'%Y-%m-%d_%H-%M-%S').png"
+ if [ "${XDG_SESSION_TYPE:-}" = "wayland" ]; then
+ require grim slurp wl-copy
+ (
+ region="$(slurp)"
+ [ -n "$region" ] || exit 0
+ grim -g "$region" "$file" &&
+ tesseract -l eng "$file" - 2>/dev/null | wl-copy
+ ) /dev/null 2>&1 &
+ else
+ require maim xclip
+ (
+ maim -s "$file" &&
+ tesseract -l eng "$file" - 2>/dev/null | xclip -selection clipboard -in
+ ) /dev/null 2>&1 &
+ fi
+ ;;
+keyboard)
+ case "$2" in
+ toggle)
+ if [ "${XDG_SESSION_TYPE:-}" = "wayland" ]; then
+ if [ "$XDG_CURRENT_DESKTOP" = "Hyprland" ]; then
+ hyprctl switchxkblayout current next
+ elif [ "$XDG_CURRENT_DESKTOP" = "sway" ]; then
+ if swaymsg -t get_inputs | grep -qi "Colemak"; then
+ swaymsg input type:keyboard xkb_variant "''"
+ else
+ swaymsg input type:keyboard xkb_variant colemak
+ fi
+ fi
+ else
+ current="$(setxkbmap -query | awk '/variant/{print $2}')"
+ if [ "$current" = "colemak" ]; then
+ setxkbmap -layout us
+ else
+ setxkbmap -layout us -variant colemak
+ fi
+ fi
+ ;;
+ *)
+ echo "Usage: ctl keyboard {toggle}" >&2
+ exit 1
+ ;;
+ esac
+ ;;
+audio)
+ require pactl
+ case "$2" in
+ out)
+ sinks="$(pactl list short sinks | awk '{print $1": "$2}')"
+ [ -z "$sinks" ] && exit 0
+ count="$(printf "%s\n" "$sinks" | wc -l)"
+ if [ "$XDG_SESSION_TYPE" = x11 ]; then
+ choice="$(printf "%s\n" "$sinks" | dmenu -i -l "$count" -p "select sink:" | cut -d: -f1)"
+ else
+ choice="$(printf "%s\n" "$sinks" | rofi -dmenu -i -lines "$count" -p "select sink" | cut -d: -f1)"
+ fi
+ [ "$choice" ] && pactl set-default-sink "$choice"
+ ;;
+ in)
+ sources="$(pactl list short sources | awk '{print $1": "$2}')"
+ [ -z "$sources" ] && exit 0
+ count="$(printf "%s\n" "$sources" | wc -l)"
+ if [ "$XDG_SESSION_TYPE" = x11 ]; then
+ choice="$(printf "%s\n" "$sources" | dmenu -i -l "$count" -p "select source:" | cut -d: -f1)"
+ else
+ choice="$(printf "%s\n" "$sources" | rofi -dmenu -i -lines "$count" -p "select source" | cut -d: -f1)"
+ fi
+ [ "$choice" ] && pactl set-default-source "$choice"
+ ;;
+ *)
+ echo "Usage: ctl audio {in|out}" >&2
+ exit 1
+ ;;
+ esac
+ ;;
+*)
+ echo "Usage: ctl {screenshot|ocr|keyboard|audio}" >&2
+ exit 1
+ ;;
+esac
diff --git a/scripts/doc b/scripts/doc
deleted file mode 120000
index 66bd887..0000000
--- a/scripts/doc
+++ /dev/null
@@ -1 +0,0 @@
-/nix/store/i53klf9mw9p2k5nfawr6r2b0v3jyq1si-home-manager-files/.local/bin/scripts/doc
\ No newline at end of file
diff --git a/scripts/doc b/scripts/doc
new file mode 100755
index 0000000..2167604
--- /dev/null
+++ b/scripts/doc
@@ -0,0 +1,36 @@
+#!/bin/sh
+
+require() {
+ for cmd in "$@"; do
+ command -v "$cmd" >/dev/null 2>&1 || {
+ echo "doc: missing dependency: $cmd" >&2
+ exit 1
+ }
+ done
+}
+
+require sioyek
+
+dir="$HOME/doc"
+test -d "$dir" || exit
+
+if [ "$XDG_SESSION_TYPE" = x11 ]; then
+ require dmenu
+ picker() { dmenu -i -l 10 -p "Select file or folder: "; }
+else
+ require rofi
+ picker() { rofi -dmenu -i -l 10 -p "Select file or folder"; }
+fi
+
+while :; do
+ choice="$(find "$dir" -not -path "$dir/.*" -mindepth 1 -maxdepth 1 \( -type d -printf "%f/\n" -o -type f -printf "%f\n" \) | picker)"
+
+ [ -n "$choice" ] || break
+
+ if [ -d "$dir/${choice%/}" ]; then
+ dir="$dir/${choice%/}"
+ elif [ -f "$dir/$choice" ]; then
+ sioyek "$dir/$choice" &
+ break
+ fi
+done
diff --git a/scripts/hypr b/scripts/hypr
deleted file mode 120000
index af972eb..0000000
--- a/scripts/hypr
+++ /dev/null
@@ -1 +0,0 @@
-/nix/store/i53klf9mw9p2k5nfawr6r2b0v3jyq1si-home-manager-files/.local/bin/scripts/hypr
\ No newline at end of file
diff --git a/scripts/hypr b/scripts/hypr
new file mode 100755
index 0000000..bcb32c7
--- /dev/null
+++ b/scripts/hypr
@@ -0,0 +1,334 @@
+#!/bin/sh
+
+require() {
+ for cmd in "$@"; do
+ command -v "$cmd" >/dev/null 2>&1 || {
+ echo "hypr: missing dependency: $cmd" >&2
+ exit 1
+ }
+ done
+}
+
+usage() {
+ cat < [app] [args...]
+
+Commands:
+ volume {up,down,toggle} Adjust volume accordingly and notify
+ brightness {up,down} Adjust brightness accordingly and notify
+ spawnfocus [args...] Focus existing window or spawn app with args
+ pull [app] Pull window to current workspace (picker if no app)
+ windowrules Apply dynamic window rules
+ exit Safely exit hyprland
+
+Options:
+ -h, --help Show this help message
+EOF
+}
+
+[ -z "$1" ] && {
+ usage
+ exit 1
+}
+
+cmd="$1"
+shift
+
+case "$cmd" in
+-h | --help)
+ usage
+ exit 0
+ ;;
+exit)
+ require hyprctl
+ pkill hypridle
+ pkill hyprpaper
+ hyprctl dispatch exit
+ exit 0
+ ;;
+brightness)
+ require brightnessctl notify-send
+ BRIGHT_STEP=5
+ max_brightness="$(brightnessctl max)"
+ case "$1" in
+ up)
+ brightnessctl set "$BRIGHT_STEP"%+
+ notify-send -u low -t 2500 -r 5555 "brightness $(brightnessctl get | awk -v max="$max_brightness" '{printf "%d%%", $1*100/max}')"
+ ;;
+ down)
+ brightnessctl set "$BRIGHT_STEP"%-
+ notify-send -u low -t 2500 -r 5555 "brightness $(brightnessctl get | awk -v max="$max_brightness" '{printf "%d%%", $1*100/max}')"
+ ;;
+ *)
+ echo "Invalid subcommand: $1" >&2
+ exit 1
+ ;;
+ esac
+ ;;
+volume)
+ require pactl notify-send
+ SINK="@DEFAULT_SINK@"
+ VOL_STEP=5
+ get_vol() { pactl get-sink-volume "$SINK" | awk 'NR==1{print $5+0}'; }
+
+ case "$1" in
+ up)
+ vol=$(get_vol)
+ [ "$vol" -lt 100 ] && vol=$((vol + VOL_STEP))
+ [ "$vol" -gt 100 ] && vol=100
+ pactl set-sink-volume "$SINK" "${vol}%"
+ ;;
+ down)
+ vol=$(get_vol)
+ vol=$((vol - VOL_STEP))
+ [ "$vol" -lt 0 ] && vol=0
+ pactl set-sink-volume "$SINK" "${vol}%"
+ ;;
+ toggle)
+ pactl set-sink-mute "$SINK" toggle
+ ;;
+ *)
+ echo "Invalid subcommand: $1" >&2
+ exit 1
+ ;;
+ esac
+
+ muted=$(pactl get-sink-mute "$SINK" | awk '{print $2}')
+ vol=$(get_vol)
+ [ "$vol" -gt 100 ] && vol=100
+
+ if [ "$muted" = "yes" ]; then
+ notify-send -u low -t 2500 -r 5556 "volume: ${vol}% (muted)"
+ else
+ notify-send -u low -t 2500 -r 5556 "volume: ${vol}%"
+ fi
+ ;;
+pull)
+ require hyprctl jq rofi
+ APP="$1"
+ case "$APP" in
+ google-chrome | google-chrome-stable) CLASS="google-chrome" ;;
+ zen | zen-browser) CLASS="zen" ;;
+ chromium | ungoogled-chromium) CLASS="Chromium" ;;
+ firefox) CLASS="firefox" ;;
+ alacritty) CLASS="Alacritty" ;;
+ code | vscodium) CLASS="Code" ;;
+ signal-desktop | signal) CLASS="signal" ;;
+ telegram-desktop | telegram) CLASS="TelegramDesktop" ;;
+ ghostty) CLASS="com.mitchellh.ghostty" ;;
+ bitwarden-desktop | bitwarden) CLASS="Bitwarden" ;;
+ slack) CLASS="Slack" ;;
+ discord) CLASS="discord" ;;
+ vesktop) CLASS="vesktop" ;;
+ element-desktop | element) CLASS="element" ;;
+ *) CLASS="$APP" ;;
+ esac
+
+ CUR_WS=$(hyprctl activeworkspace -j | jq -r '.id')
+
+ CUR_ADDR=$(hyprctl -j activewindow | jq -r '.address // empty')
+
+ WIN_ADDRS=$(hyprctl -j clients 2>/dev/null | jq -r --arg pat "$CLASS" '
+ .[]?
+ | select(
+ (.class? | ascii_downcase | contains($pat | ascii_downcase)) or
+ (.initialClass? | ascii_downcase | contains($pat | ascii_downcase)) or
+ (.xdgTag? // "" | ascii_downcase | contains($pat | ascii_downcase))
+ )
+ | "\(.class)\t\(.title // "no title")\t\(.address)"
+ ')
+
+ WIN_COUNT=$(echo "$WIN_ADDRS" | grep -c '^' || :)
+
+ if [ "$WIN_COUNT" -eq 0 ]; then
+ exit 0
+ fi
+
+ if [ "$WIN_COUNT" -eq 1 ]; then
+ WIN_ADDR=$(echo "$WIN_ADDRS" | awk -F'\t' '{print $3}')
+ else
+ ROFI_LINES=$(
+ echo "$WIN_ADDRS" |
+ awk -F'\t' -v cur="$CUR_ADDR" '
+ $3 != cur { printf "%s : %s\t%s\n", $1, $2, $3 }
+ '
+ )
+
+ [ -z "$ROFI_LINES" ] && exit 0
+
+ SELECTED=$(echo "$ROFI_LINES" | rofi -dmenu -i -p "pull window")
+
+ [ -z "$SELECTED" ] && exit 0
+
+ WIN_ADDR=$(echo "$SELECTED" | awk -F'\t' '{print $2}')
+ fi
+
+ [ -z "$WIN_ADDR" ] && exit 0
+
+ hyprctl dispatch movetoworkspace "${CUR_WS},address:${WIN_ADDR}"
+ hyprctl dispatch focuswindow "address:${WIN_ADDR}"
+ ;;
+spawnfocus)
+ require hyprctl jq rofi socat
+ WS=""
+ while [ $# -gt 0 ]; do
+ case "$1" in
+ --ws)
+ if [ $# -lt 2 ]; then
+ echo "Missing workspace number after --ws"
+ exit 1
+ fi
+ WS="$2"
+ shift 2
+ ;;
+ -*)
+ echo "Unknown option $1"
+ exit 1
+ ;;
+ *)
+ break
+ ;;
+ esac
+ done
+
+ APP="$1"
+ [ -z "$APP" ] && {
+ echo 'Specify an app'
+ exit 1
+ }
+ shift
+
+ case "$APP" in
+ google-chrome | google-chrome-stable) CLASS="google-chrome" ;;
+ chromium | ungoogled-chromium) CLASS="Chromium" ;;
+ zen | zen-browser) CLASS="zen" ;;
+ firefox) CLASS="firefox" ;;
+ alacritty) CLASS="Alacritty" ;;
+ code | vscodium) CLASS="Code" ;;
+ signal-desktop | signal) CLASS="signal" ;;
+ telegram-desktop | telegram) CLASS="TelegramDesktop" ;;
+ ghostty) CLASS="com.mitchellh.ghostty" ;;
+ bitwarden-desktop | bitwarden) CLASS="Bitwarden" ;;
+ slack) CLASS="Slack" ;;
+ discord) CLASS="discord" ;;
+ vesktop) CLASS="vesktop" ;;
+ element-desktop | element) CLASS="element" ;;
+ *) CLASS="$APP" ;;
+ esac
+
+ WIN_ADDRS=$(hyprctl -j clients 2>/dev/null | jq -r --arg class "$CLASS" '
+ .[]? | select(
+ ((.xdgTag // "") | ascii_downcase | contains($class | ascii_downcase)) or
+ ((.initialClass // "") | ascii_downcase | contains($class | ascii_downcase)) or
+ ((.class // "") | ascii_downcase | contains($class | ascii_downcase))
+ ) | "\(.class)\t\(.title)\t\(.address)"
+ ')
+
+ WIN_COUNT=$(echo "$WIN_ADDRS" | grep -c .)
+
+ if [ "$WIN_COUNT" -eq 0 ]; then
+ WIN_ADDR=""
+ elif [ "$WIN_COUNT" -eq 1 ]; then
+ WIN_ADDR=$(echo "$WIN_ADDRS" | awk -F'\t' '{print $3}')
+ elif [ "$WIN_COUNT" -gt 1 ]; then
+ SELECTED=$(echo "$WIN_ADDRS" | awk -F'\t' '{print $1 ": " $2 "\t" $3}' | rofi -dmenu -i -p "select window")
+
+ if [ -z "$SELECTED" ]; then
+ exit 0
+ fi
+
+ WIN_ADDR=$(echo "$SELECTED" | awk -F'\t' '{print $2}')
+ fi
+ if [ -n "$WIN_ADDR" ]; then
+ if [ -n "$WS" ]; then
+ WIN_WS=$(hyprctl clients -j | jq -r --arg addr "$WIN_ADDR" '.[] | select(.address == $addr) | .workspace.id')
+
+ if [ "$WIN_WS" != "$WS" ]; then
+ hyprctl dispatch movetoworkspacesilent "$WS,address:$WIN_ADDR"
+ fi
+
+ hyprctl dispatch workspace "$WS"
+ else
+ WIN_WS=$(hyprctl clients -j | jq -r --arg addr "$WIN_ADDR" '.[] | select(.address == $addr) | .workspace.id')
+
+ hyprctl dispatch workspace "$WIN_WS"
+ fi
+
+ hyprctl dispatch focuswindow "address:$WIN_ADDR"
+ else
+ EXISTING_HEX=$(hyprctl -j clients 2>/dev/null | jq -r --arg class "$CLASS" '
+ .[]? | select(
+ ((.xdgTag // "") | ascii_downcase | contains($class | ascii_downcase)) or
+ ((.initialClass // "") | ascii_downcase | contains($class | ascii_downcase)) or
+ ((.class // "") | ascii_downcase | contains($class | ascii_downcase))
+ ) | .address | ltrimstr("0x")
+ ' | tr '\n' ' ')
+
+ if [ -n "$WS" ]; then
+ hyprctl dispatch workspace "$WS"
+ fi
+
+ if [ $# -eq 0 ]; then
+ "$APP" &
+ else
+ "$APP" "$@" &
+ fi
+
+ socat -u UNIX-CONNECT:"$XDG_RUNTIME_DIR/hypr/$HYPRLAND_INSTANCE_SIGNATURE/.socket2.sock" - | while read -r line; do
+ case "$line" in
+ openwindow*)
+ window_id=$(echo "$line" | cut -d'>' -f3 | cut -d',' -f1)
+ event_class=$(echo "$line" | cut -d'>' -f3 | cut -d',' -f3)
+ class_lower=$(echo "$event_class" | tr '[:upper:]' '[:lower:]')
+ target_lower=$(echo "$CLASS" | tr '[:upper:]' '[:lower:]')
+ case "$class_lower" in
+ *"$target_lower"*)
+ case " $EXISTING_HEX " in
+ *" $window_id "*) ;;
+ *)
+ addr="0x$window_id"
+ if [ -n "$WS" ]; then
+ hyprctl dispatch movetoworkspacesilent "$WS,address:$addr"
+ fi
+ hyprctl dispatch focuswindow "address:$addr"
+ break
+ ;;
+ esac
+ ;;
+ esac
+ ;;
+ esac
+ done
+ fi
+ ;;
+windowrules)
+ require hyprctl socat
+ socat -u UNIX-CONNECT:"$XDG_RUNTIME_DIR/hypr/$HYPRLAND_INSTANCE_SIGNATURE/.socket2.sock" - | while IFS= read -r line; do
+ event="${line%%>>*}"
+ data="${line#*>>}"
+ case "$event" in
+ windowtitlev2)
+ window_id="${data%%,*}"
+ window_title="${data#*,}"
+ window_title=$(echo "$window_title" | tr '[:upper:]' '[:lower:]')
+ case "$window_title" in
+ *'extension: (bitwarden password manager) - bitwarden'*)
+ hyprctl --batch "dispatch setfloating address:0x$window_id; dispatch centerwindow address:0x$window_id"
+ ;;
+ *'sign in - google accounts '*)
+ hyprctl --batch "dispatch setfloating address:0x$window_id; \
+ dispatch resizewindowpixel exact 30% 70%,address:0x$window_id; \
+ dispatch centerwindow address:0x$window_id"
+ ;;
+ esac
+ ;;
+ esac
+ done
+ ;;
+
+*)
+ echo "Unknown subcommand: $cmd"
+ usage
+ exit 1
+ ;;
+esac
diff --git a/scripts/mux b/scripts/mux
deleted file mode 120000
index 733312c..0000000
--- a/scripts/mux
+++ /dev/null
@@ -1 +0,0 @@
-/nix/store/i53klf9mw9p2k5nfawr6r2b0v3jyq1si-home-manager-files/.local/bin/scripts/mux
\ No newline at end of file
diff --git a/scripts/mux b/scripts/mux
new file mode 100755
index 0000000..019238e
--- /dev/null
+++ b/scripts/mux
@@ -0,0 +1,241 @@
+#!/bin/sh
+
+require() {
+ for cmd in "$@"; do
+ command -v "$cmd" >/dev/null 2>&1 || {
+ echo "mux: missing dependency: $cmd" >&2
+ exit 1
+ }
+ done
+}
+
+require tmux
+
+get_scope() {
+ _wname=$(tmux display-message -p '#{window_name}')
+ case "$_wname" in
+ ai@*|code@*|git@*|run@*|term@*|misc@*) printf '%s' "${_wname#*@}" ;;
+ *) printf '%s' "$_wname" ;;
+ esac
+}
+
+spawn_or_focus() {
+ scope=$(get_scope)
+ name="${1}@${scope}"
+ cmd="$2"
+
+ if tmux list-windows -F '#{window_name}' | grep -Fx "$name" >/dev/null; then
+ tmux select-window -t "${name}"
+ else
+ if [ -n "$cmd" ]; then
+ tmux new-window -c '#{pane_current_path}' -n "${name}" "$cmd"
+ else
+ tmux new-window -c '#{pane_current_path}' -n "${name}"
+ fi
+ fi
+}
+
+pick_session() {
+ require fzf column
+ sel=$({
+ printf 'name\twindows\tstatus\n'
+ tmux list-sessions -F '#{session_name} #{session_windows}w #{?session_attached,*,}'
+ } | column -t -s "$(printf '\t')" |
+ fzf --reverse --header-lines 1 --prompt 'select-session> ')
+ [ -n "$sel" ] && tmux switch-client -t "${sel%% *}"
+}
+
+pick_window() {
+ require fzf column
+ sel=$({
+ printf 'target\tname\tcommand\tpanes\n'
+ tmux list-windows -a -F '#{window_index}:#{session_name} #{window_name} #{pane_current_command} #{window_panes}p'
+ } | column -t -s "$(printf '\t')" |
+ fzf --reverse --header-lines 1 --prompt 'select-window> ')
+ sel="${sel%% *}"
+ [ -n "$sel" ] && tmux switch-client -t "${sel#*:}:${sel%%:*}"
+}
+
+pick_pane() {
+ require fzf column
+ sel=$({
+ printf 'target\tcommand\tpath\n'
+ tmux list-panes -a -F '#{window_index}.#{pane_index}:#{session_name} #{pane_current_command} #{pane_current_path}' |
+ sed "s|$HOME|~|g"
+ } | column -t -s "$(printf '\t')" |
+ fzf --reverse --header-lines 1 --prompt 'select-pane> ')
+ sel="${sel%% *}"
+ [ -n "$sel" ] && tmux switch-client -t "${sel#*:}:${sel%%:*}"
+}
+
+target_pane() {
+ require fzf column
+ sel=$({
+ printf 'target\tcommand\tpath\n'
+ tmux list-panes -a -F '#{window_index}.#{pane_index}:#{session_name} #{pane_current_command} #{pane_current_path}' |
+ sed "s|$HOME|~|g"
+ } | column -t -s "$(printf '\t')" |
+ fzf --reverse --header-lines 1 --prompt "$1> ")
+ sel="${sel%% *}"
+ [ -n "$sel" ] && printf '%s' "${sel#*:}:${sel%%:*}"
+}
+
+confirm() {
+ printf '%s ' "$1"
+ old=$(stty -g)
+ stty raw -echo
+ c=$(dd bs=1 count=1 2>/dev/null)
+ stty "$old"
+ printf '\n'
+ [ "$c" = "y" ]
+}
+
+case "$1" in
+bar)
+ session=$(tmux display-message -p '#S')
+ [ "$session" ] || exit
+ if [ "$(tmux show-options -gv mouse)" = "on" ]; then
+ indicator='#{?pane_in_mode,[mouse#{@c}copy],[mouse]}'
+ else
+ indicator='#{?pane_in_mode,[copy],}'
+ fi
+ set -f
+ keys="H J K L"
+ bar_content=""
+ i=0
+ total=$(tmux ls -F x | wc -l)
+ for line in $(tmux ls -F '#{session_id}:#{session_name}'); do
+ sid="${line%%:*}"
+ sname="${line#*:}"
+ if [ $i -lt 4 ]; then
+ key=$(echo "$keys" | cut -d' ' -f $((i + 1)))
+ elif [ $i -eq $((total - 1)) ]; then
+ key='$'
+ else
+ key='?'
+ fi
+ star=""
+ [ "$sname" = "$session" ] && star="*"
+ [ -n "$bar_content" ] && bar_content="$bar_content │ "
+ bar_content="$bar_content#[range=session|${sid}]$key:$sname$star#[norange]"
+ i=$((i + 1))
+ done
+ set +f
+ left='#[align=left list=on] #{W:#[range=window|#{window_index}]#{window_index}:#{window_name}#{window_flags}#[norange]#{?window_end_flag,, │ }}#[nolist]'
+ right="#[align=right]$indicator $bar_content"
+ tmux set -g 'status-format[0]' "$left$right"
+ ;;
+switch)
+ session="$(tmux ls -F '#S' | tail -n "+$(($2 + 1))" | head -1)"
+ tmux switch -t "$session"
+ ;;
+pick-session)
+ pick_session
+ ;;
+pick-window)
+ pick_window
+ ;;
+pick-pane)
+ pick_pane
+ ;;
+ai)
+ require claude
+ spawn_or_focus ai 'claude'
+ ;;
+code)
+ require nvim
+ spawn_or_focus code 'nvim -c "lua require([[config.tmux]]).run([[nvim]])"'
+ ;;
+git)
+ require nvim git
+ pane_path=$(tmux display-message -p '#{pane_current_path}')
+ if ! git -C "$pane_path" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
+ tmux display-message "Not a git repository"
+ else
+ spawn_or_focus git 'nvim -c "lua require([[config.tmux]]).run([[git]])"'
+ fi
+ ;;
+run)
+ require nvim
+ spawn_or_focus run 'nvim -c "lua require([[config.tmux]]).run([[run]])"'
+ ;;
+term)
+ spawn_or_focus term
+ ;;
+misc)
+ spawn_or_focus misc
+ ;;
+cmd)
+ require fzf
+ result=$(tmux list-commands |
+ sed 's/ / /' |
+ fzf --reverse --prompt ':' --print-query \
+ --delimiter '\t' --with-nth '1,2' --accept-nth '1' \
+ --bind 'ctrl-y:transform-query(echo {1})+disable-search')
+ rc=$?
+ query=$(printf '%s' "$result" | head -1)
+ action=$(printf '%s' "$result" | sed -n '2p')
+ [ $rc -eq 130 ] && exit
+ if [ -n "$action" ]; then
+ case "$query" in
+ "$action "*)
+ tmux $query
+ exit
+ ;;
+ esac
+ case "$action" in
+ switch-client) pick_session ;;
+ select-window) pick_window ;;
+ select-pane) pick_pane ;;
+ rename-window)
+ cur=$(tmux display-message -p '#{window_name}')
+ printf 'name [%s]: ' "$cur"
+ read -r name
+ [ -n "$name" ] && tmux rename-window "$name"
+ ;;
+ rename-session)
+ cur=$(tmux display-message -p '#S')
+ printf 'name [%s]: ' "$cur"
+ read -r name
+ [ -n "$name" ] && tmux rename-session "$name"
+ ;;
+ join-pane)
+ target=$(target_pane 'join-pane')
+ [ -n "$target" ] && tmux join-pane -s "$target"
+ ;;
+ swap-pane)
+ target=$(target_pane 'swap-pane')
+ [ -n "$target" ] && tmux swap-pane -t "$target"
+ ;;
+ select-layout)
+ layout=$(printf '%s\n' \
+ 'even-horizontal' \
+ 'even-vertical' \
+ 'main-horizontal' \
+ 'main-vertical' \
+ 'tiled' |
+ fzf --reverse --prompt 'layout> ')
+ [ -n "$layout" ] && tmux select-layout "$layout"
+ ;;
+ kill-pane)
+ confirm 'kill-pane? [y/N]:' && tmux kill-pane
+ ;;
+ kill-window)
+ confirm "kill-window \"$(tmux display-message -p '#{window_name}')\"? [y/N]:" && tmux kill-window
+ ;;
+ kill-session)
+ confirm "kill-session \"$(tmux display-message -p '#S')\"? [y/N]:" && tmux kill-session
+ ;;
+ kill-server)
+ confirm 'kill-server? [y/N]:' && tmux kill-server
+ ;;
+ *) tmux "$action" ;;
+ esac
+ elif [ -n "$query" ]; then
+ tmux $query
+ fi
+ ;;
+*)
+ tmux attach
+ ;;
+esac
diff --git a/scripts/pipes b/scripts/pipes
deleted file mode 120000
index c009f8f..0000000
--- a/scripts/pipes
+++ /dev/null
@@ -1 +0,0 @@
-/nix/store/i53klf9mw9p2k5nfawr6r2b0v3jyq1si-home-manager-files/.local/bin/scripts/pipes
\ No newline at end of file
diff --git a/scripts/pipes b/scripts/pipes
new file mode 100755
index 0000000..5ee0b51
--- /dev/null
+++ b/scripts/pipes
@@ -0,0 +1,227 @@
+#!/usr/bin/env bash
+# pipes.sh: Animated pipes terminal screensaver.
+# https://github.com/pipeseroni/pipes.sh
+#
+# Copyright (c) 2015-2018 Pipeseroni/pipes.sh contributors
+# Copyright (c) 2013-2015 Yu-Jie Lin
+# Copyright (c) 2010 Matthew Simpson
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+
+
+VERSION=1.3.0
+
+M=32768 # Bash RANDOM maximum + 1
+p=1 # number of pipes
+f=75 # frame rate
+s=13 # probability of straight fitting
+r=2000 # characters limit
+t=0 # iteration counter for -r character limit
+w=80 # terminal size
+h=24
+
+# ab -> sets[][idx] = a*4 + b
+# 0: up, 1: right, 2: down, 3: left
+# 00 means going up , then going up -> ┃
+# 12 means going right, then going down -> ┓
+sets=(
+ "┃┏ ┓┛━┓ ┗┃┛┗ ┏━"
+ "│╭ ╮╯─╮ ╰│╯╰ ╭─"
+ "│┌ ┐┘─┐ └│┘└ ┌─"
+ "║╔ ╗╝═╗ ╚║╝╚ ╔═"
+ "|+ ++-+ +|++ +-"
+ "|/ \/-\ \|/\ /-"
+ ".. .... .... .."
+ ".o oo.o o.oo o."
+ "-\ /\|/ /-\/ \|"
+ "╿┍ ┑┚╼┒ ┕╽┙┖ ┎╾"
+ "████▀▀███▀█▀▀██▀"
+)
+
+# pipes'
+x=() # current position
+y=()
+l=() # current directions
+ # 0: up, 1: right, 2: down, 3: left
+n=() # new directions
+v=() # current types
+c=() # current color
+
+# selected pipes'
+V=() # types (indexes to sets[])
+C=() # colors
+VN=0 # number of selected types
+CN=0 # number of selected colors
+
+# switches
+RNDSTART=0 # randomize starting position and direction
+BOLD=1
+NOCOLOR=0
+KEEPCT=0 # keep pipe color and type
+
+
+parse() {
+ OPTIND=1
+ while getopts "p:t:c:f:s:r:RBCKhv" arg; do
+ case $arg in
+ p) ((p = (OPTARG > 0) ? OPTARG : p));;
+ t)
+ if [[ "$OPTARG" = c???????????????? ]]; then
+ V+=(${#sets[@]})
+ sets+=("${OPTARG:1}")
+ else
+ ((OPTARG >= 0 && OPTARG < ${#sets[@]})) && V+=($OPTARG)
+ fi
+ ;;
+ c) [[ $OPTARG =~ ^[0-7]$ ]] && C+=($OPTARG);;
+ f) ((f = (OPTARG > 19 && OPTARG < 101) ? OPTARG : f));;
+ s) ((s = (OPTARG > 4 && OPTARG < 16) ? OPTARG : s));;
+ r) ((r = (OPTARG >= 0) ? OPTARG : r));;
+ R) RNDSTART=1;;
+ B) BOLD=0;;
+ C) NOCOLOR=1;;
+ K) KEEPCT=1;;
+ h) echo -e "Usage: $(basename $0) [OPTION]..."
+ echo -e "Animated pipes terminal screensaver.\n"
+ echo -e " -p [1-]\tnumber of pipes (D=1)."
+ echo -e " -t [0-$((${#sets[@]} - 1))]\ttype of pipes, can be used more than once (D=0)."
+ echo -e " -c [0-7]\tcolor of pipes, can be used more than once (D=1 2 3 4 5 6 7 0)."
+ echo -e " -t c[16 chars]\tcustom type of pipes."
+ echo -e " -f [20-100]\tframerate (D=75)."
+ echo -e " -s [5-15]\tprobability of a straight fitting (D=13)."
+ echo -e " -r LIMIT\treset after x characters, 0 if no limit (D=2000)."
+ echo -e " -R \t\trandomize starting position and direction."
+ echo -e " -B \t\tno bold effect."
+ echo -e " -C \t\tno color."
+ echo -e " -K \t\tpipes keep their color and type when hitting the screen edge."
+ echo -e " -h\t\thelp (this screen)."
+ echo -e " -v\t\tprint version number.\n"
+ exit 0;;
+ v) echo "$(basename -- "$0") $VERSION"
+ exit 0
+ esac
+ done
+
+ # set default values if not by options
+ ((${#V[@]})) || V=(0)
+ VN=${#V[@]}
+ ((${#C[@]})) || C=(1 2 3 4 5 6 7 0)
+ CN=${#C[@]}
+}
+
+
+cleanup() {
+ # clear out standard input
+ read -t 0.001 && cat /dev/null
+
+ # terminal has no smcup and rmcup capabilities
+ ((FORCE_RESET)) && reset && exit 0
+
+ tput reset # fix for konsole, see pipeseroni/pipes.sh#43
+ tput rmcup
+ tput cnorm
+ # stty echo
+ ((NOCOLOR)) && echo -ne '\e[0m'
+ exit 0
+}
+
+
+resize() {
+ w=$(tput cols) h=$(tput lines)
+}
+
+
+init() {
+ local i
+
+ resize
+ trap resize SIGWINCH
+ ci=$((KEEPCT ? 0 : CN * RANDOM / M))
+ vi=$((KEEPCT ? 0 : VN * RANDOM / M))
+ for ((i = 0; i < p; i++)); {((
+ n[i] = 0,
+ l[i] = RNDSTART ? RANDOM % 4 : 0,
+ x[i] = RNDSTART ? w * RANDOM / M : w / 2,
+ y[i] = RNDSTART ? h * RANDOM / M : h / 2,
+ c[i] = C[ci],
+ v[i] = V[vi],
+ ci = (ci + 1) % CN,
+ vi = (vi + 1) % VN
+ ));}
+
+ # stty -echo
+ tput smcup || FORCE_RESET=1
+ tput civis
+ tput clear
+ trap cleanup HUP TERM
+}
+
+
+main() {
+ local i
+
+ parse "$@"
+ init "$@"
+
+ # any key press exits the loop and this script
+ trap 'break 2' INT
+ while REPLY=; do
+ read -t 0.0$((1000 / f)) -n 1 2>/dev/null
+ case "$REPLY" in
+ P) ((s = s < 15 ? s + 1 : s));;
+ O) ((s = s > 3 ? s - 1 : s));;
+ F) ((f = f < 100 ? f + 1 : f));;
+ D) ((f = f > 20 ? f - 1 : f));;
+ B) ((BOLD = (BOLD + 1) % 2));;
+ C) ((NOCOLOR = (NOCOLOR + 1) % 2));;
+ K) ((KEEPCT = (KEEPCT + 1) % 2));;
+ ?) break;;
+ esac
+ for ((i = 0; i < p; i++)); do
+ # New position:
+ # l[] direction = 0: up, 1: right, 2: down, 3: left
+ ((l[i] % 2)) && ((x[i] += -l[i] + 2, 1)) || ((y[i] += l[i] - 1))
+
+ # Loop on edges (change color on loop):
+ ((!KEEPCT && (x[i] >= w || x[i] < 0 || y[i] >= h || y[i] < 0))) \
+ && ((c[i] = C[CN * RANDOM / M], v[i] = V[VN * RANDOM / M]))
+ ((x[i] = (x[i] + w) % w))
+ ((y[i] = (y[i] + h) % h))
+
+ # New random direction:
+ ((n[i] = s * RANDOM / M - 1))
+ ((n[i] = (n[i] > 1 || n[i] == 0) ? l[i] : l[i] + n[i]))
+ ((n[i] = (n[i] < 0) ? 3 : n[i] % 4))
+
+ # Print:
+ tput cup ${y[i]} ${x[i]}
+ echo -ne "\e[${BOLD}m"
+ ((NOCOLOR)) && echo -ne "\e[0m" || echo -ne "\e[3${c[i]}m"
+ echo -n "${sets[v[i]]:l[i]*4+n[i]:1}"
+ l[i]=${n[i]}
+ done
+ ((r > 0 && t * p >= r)) && tput reset && tput civis && t=0 || ((t++))
+ done
+
+ cleanup
+}
+
+
+main "$@"
+
diff --git a/scripts/theme b/scripts/theme
deleted file mode 120000
index 97b76dc..0000000
--- a/scripts/theme
+++ /dev/null
@@ -1 +0,0 @@
-/nix/store/i53klf9mw9p2k5nfawr6r2b0v3jyq1si-home-manager-files/.local/bin/scripts/theme
\ No newline at end of file
diff --git a/scripts/theme b/scripts/theme
new file mode 100755
index 0000000..4f0e633
--- /dev/null
+++ b/scripts/theme
@@ -0,0 +1,134 @@
+#!/bin/sh
+
+require() {
+ for cmd in "$@"; do
+ command -v "$cmd" >/dev/null 2>&1 || {
+ echo "theme: missing dependency: $cmd" >&2
+ exit 1
+ }
+ done
+}
+
+themes="daylight
+midnight"
+
+as_list="$(printf "%s\n" "$themes" | awk 'NF{printf "\"%s\",", $0}' | sed 's/,$//')"
+
+case "$(uname)" in
+Linux)
+ if [ -n "$1" ]; then
+ theme="$1"
+ else
+ if [ "$XDG_SESSION_TYPE" = "wayland" ]; then
+ require rofi
+ theme="$(printf "%s\n" "$themes" | rofi -dmenu -p 'theme')"
+ else
+ require dmenu
+ theme="$(printf "%s\n" "$themes" | dmenu -p 'select theme: ')"
+ fi
+ fi
+ ;;
+Darwin)
+ if [ -n "$1" ]; then
+ theme="$1"
+ else
+ theme="$(
+ osascript </dev/null 2>&1; then
+ case "$theme" in
+ midnight)
+ gsettings set org.gnome.desktop.interface color-scheme 'prefer-dark'
+ ;;
+ daylight)
+ gsettings set org.gnome.desktop.interface color-scheme 'prefer-light'
+ ;;
+ *) ;;
+ esac
+ fi
+
+ [ "$XDG_SESSION_TYPE" = "wayland" ] && {
+ if [ "$XDG_CURRENT_DESKTOP" = 'Hyprland' ]; then
+ current_theme=$(readlink "$XDG_CONFIG_HOME/hypr/theme.conf" 2>/dev/null | awk -F/ '{print $NF}' | sed 's/\.conf$//')
+ if [ "$current_theme" != "$theme" ]; then
+ ln -sf "$XDG_CONFIG_HOME/hypr/themes/$theme.conf" "$XDG_CONFIG_HOME/hypr/theme.conf"
+ ln -sf "$XDG_CONFIG_HOME/waybar/themes/$theme.css" "$XDG_CONFIG_HOME/waybar/themes/theme.css"
+ hyprctl reload
+ pkill -USR1 waybar
+ pkill -USR1 waybar
+ fi
+ elif [ "$XDG_CURRENT_DESKTOP" = 'sway' ]; then
+ current_theme=$(readlink "$XDG_CONFIG_HOME/sway/themes/theme" 2>/dev/null | sed 's|.*/||')
+ if [ "$current_theme" != "$theme" ]; then
+ test -f "$XDG_CONFIG_HOME/sway/themes/theme" && ln -sf "$XDG_CONFIG_HOME/sway/themes/$theme" "$XDG_CONFIG_HOME/sway/themes/theme"
+ swaymsg reload
+ fi
+ fi
+ }
+
+ test -d "$XDG_CONFIG_HOME/rofi/themes" && ln -sf "$XDG_CONFIG_HOME/rofi/themes/$theme.rasi" "$XDG_CONFIG_HOME/rofi/themes/theme.rasi"
+ ;;
+Darwin)
+ case "$theme" in
+ daylight)
+ osascript -e 'tell app "System Events" to tell appearance preferences to set dark mode to false' 2>/dev/null || true
+ ;;
+ midnight)
+ osascript -e 'tell app "System Events" to tell appearance preferences to set dark mode to true' 2>/dev/null || true
+ ;;
+ esac
+ ;;
+esac
+
+test -d "$XDG_CONFIG_HOME/fzf/themes" && ln -sf "$XDG_CONFIG_HOME/fzf/themes/$theme" "$XDG_CONFIG_HOME/fzf/themes/theme"
+test -d "$XDG_CONFIG_HOME/rg/themes" && ln -sf "$XDG_CONFIG_HOME/rg/themes/$theme" "$XDG_CONFIG_HOME/rg/themes/theme"
+test -d "$XDG_CONFIG_HOME/sioyek/themes" && ln -sf "$XDG_CONFIG_HOME/sioyek/themes/$theme.config" "$XDG_CONFIG_HOME/sioyek/themes/theme.config"
+test -d "$XDG_CONFIG_HOME/task/themes" && ln -sf "$XDG_CONFIG_HOME/task/themes/$theme.theme" "$XDG_CONFIG_HOME/task/themes/theme.theme"
+
+if command -v claude >/dev/null 2>&1; then
+ CLAUDE_CONFIG="${CLAUDE_CONFIG_DIR:-$HOME}/.claude.json"
+ claude_theme='light'
+ case "$theme" in
+ daylight)
+ claude_theme='light'
+ ;;
+ midnight)
+ claude_theme='dark'
+ ;;
+ esac
+ test -f "$CLAUDE_CONFIG" && jq ".theme=\"$claude_theme\"" "$CLAUDE_CONFIG" >"$CLAUDE_CONFIG.tmp" && mv "$CLAUDE_CONFIG.tmp" "$CLAUDE_CONFIG"
+fi
+
+test -f ~/.zshenv && sed -i "s|^\(export THEME=\).*|\1$theme|" ~/.zshenv
+[ -n "$TMUX" ] && tmux setenv -g THEME "$theme"
+
+for socket in /tmp/nvim-*.sock; do
+ test -S "$socket" && nvim --server "$socket" --remote-expr "luaeval('require(\"config.fzf_reload\").reload()')" 2>/dev/null || true
+done
diff --git a/scripts/wp b/scripts/wp
deleted file mode 120000
index e8a7196..0000000
--- a/scripts/wp
+++ /dev/null
@@ -1 +0,0 @@
-/nix/store/i53klf9mw9p2k5nfawr6r2b0v3jyq1si-home-manager-files/.local/bin/scripts/wp
\ No newline at end of file
diff --git a/scripts/wp b/scripts/wp
new file mode 100755
index 0000000..40ade30
--- /dev/null
+++ b/scripts/wp
@@ -0,0 +1,148 @@
+#!/usr/bin/env python3
+
+import os
+import random
+import subprocess
+import sys
+
+try:
+ from PIL import Image, ImageDraw
+except ImportError:
+ print("wp: missing dependency: pillow (pip install pillow)", file=sys.stderr)
+ sys.exit(1)
+
+HOME = os.environ["HOME"]
+DIR = f"{HOME}/img/screen"
+os.makedirs(DIR, exist_ok=True)
+
+
+def get_resolution():
+ output = "1920x1080"
+ if "WAYLAND_DISPLAY" in os.environ:
+ desktop = os.environ.get("XDG_CURRENT_DESKTOP", "")
+ if desktop == "Hyprland":
+ output = (
+ subprocess.check_output(
+ "hyprctl monitors -j | jq -r '.[] | select(.focused==true) | .width, .height' | paste -sd x -",
+ shell=True,
+ )
+ .decode()
+ .strip()
+ )
+ elif desktop == "sway":
+ output = (
+ subprocess.check_output(
+ "swaymsg -t get_outputs -r | jq -r '.[] | select(.active==true) | .current_mode.width,.current_mode.height' | paste -sd x -",
+ shell=True,
+ )
+ .decode()
+ .strip()
+ )
+ else:
+ output = (
+ subprocess.check_output(
+ "xrandr | grep '*' | awk '{print $1}'", shell=True
+ )
+ .decode()
+ .strip()
+ )
+ return map(int, output.split("x"))
+
+
+def lock():
+ W, H = get_resolution()
+ UNIT = 16
+ grid_w = W // UNIT
+ grid_h = H // UNIT
+
+ bg_color = tuple(random.randint(0, 255) for _ in range(3))
+ img = Image.new("RGB", (W, H), bg_color)
+ draw = ImageDraw.Draw(img)
+
+ MU = 0.8
+ SIGMA = 0.7
+ SCALE_FACTOR = 1.8
+
+ S = {(x, y) for x in range(grid_w) for y in range(grid_h)}
+ N = len(S)
+
+ while S:
+ if random.random() < 1 / N:
+ break
+ gx, gy = S.pop()
+ w_units = max(
+ 1,
+ min(
+ grid_w - gx,
+ int(round(random.lognormvariate(MU, SIGMA) * SCALE_FACTOR)),
+ ),
+ )
+ h_units = max(
+ 1,
+ min(
+ grid_h - gy,
+ int(round(random.lognormvariate(MU, SIGMA) * SCALE_FACTOR)),
+ ),
+ )
+ color = (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255))
+
+ dx = random.choice([1, -1])
+ dy = random.choice([1, -1])
+
+ gx_end = gx + dx * (w_units - 1)
+ gy_end = gy + dy * (h_units - 1)
+
+ x0, x1 = sorted([gx, gx_end])
+ y0, y1 = sorted([gy, gy_end])
+
+ draw.rectangle(
+ [x0 * UNIT, y0 * UNIT, (x1 + 1) * UNIT - 1, (y1 + 1) * UNIT - 1],
+ fill=color,
+ )
+
+ img.save(f"{DIR}/lock.jpg", quality=95)
+
+
+def wall():
+ W, H = get_resolution()
+
+ colors = [
+ "#aa0000",
+ "#ff3333",
+ "#ff7777",
+ "#ffbb55",
+ "#ffcc88",
+ "#ff88ff",
+ "#aaaaff",
+ "#77ddbb",
+ "#77ff77",
+ "#cccccc",
+ ]
+
+ colors.reverse()
+
+ img = Image.new("RGB", (W, H), "white")
+ draw = ImageDraw.Draw(img)
+
+ num_bars = len(colors)
+
+ for i, color in enumerate(reversed(colors)):
+ top = round(i * H / num_bars)
+ bottom = round((i + 1) * H / num_bars)
+ draw.rectangle([0, top, W, bottom], fill=color)
+
+ img.save(f"{DIR}/wallpaper.jpg", quality=95)
+
+
+if len(sys.argv) < 2:
+ print("Usage: wp {lock|wall}", file=sys.stderr)
+ sys.exit(1)
+
+cmd = sys.argv[1]
+if cmd == "lock":
+ lock()
+elif cmd == "wall":
+ wall()
+else:
+ print("Usage: wp {lock|wall}", file=sys.stderr)
+ sys.exit(1)
diff --git a/scripts/x b/scripts/x
deleted file mode 120000
index 5839a0b..0000000
--- a/scripts/x
+++ /dev/null
@@ -1 +0,0 @@
-/nix/store/i53klf9mw9p2k5nfawr6r2b0v3jyq1si-home-manager-files/.local/bin/scripts/x
\ No newline at end of file
diff --git a/scripts/x b/scripts/x
new file mode 100755
index 0000000..2e74ad7
--- /dev/null
+++ b/scripts/x
@@ -0,0 +1,68 @@
+#!/bin/sh
+
+require() {
+ for cmd in "$@"; do
+ command -v "$cmd" >/dev/null 2>&1 || {
+ echo "x: missing dependency: $cmd" >&2
+ exit 1
+ }
+ done
+}
+
+cmd="$1"; shift
+
+case "$cmd" in
+setup)
+ require xrdb xset xmodmap xss-lock slock
+ xrdb -merge "$XDG_CONFIG_HOME"/X11/xresources."$THEME"
+ xset b off
+ xset m 0
+ xset r rate 300 50
+ xset s 300
+ xset dpms 420 540 720
+ xmodmap "$XDG_CONFIG_HOME"/X11/xmodmap
+ xss-lock -- slock &
+ ;;
+bg)
+ require xrandr feh
+ randr="$(xrandr | rg ' connected ')"
+ mons="$(echo "$randr" | wc -l)"
+ wpdir="$HOME"/img/wp
+
+ [ "$1" ] && bgone="$1" || bgone="$wpdir"/one/cliff.jpg
+ cmd="feh --no-fehbg --bg-fill $bgone"
+
+ if [ "$mons" = 2 ]; then
+ [ "$2" ] && bgtwo="$2" || bgtwo="$wpdir"/two/lilies.jpg
+ cmd="$cmd --bg-fill $bgtwo"
+ else
+ cmd="feh --no-fehbg --bg-fill $bgone"
+ fi
+
+ eval "$cmd"
+ ;;
+mon)
+ require xrandr
+ mons="$(xrandr | rg --no-config ' connected ' | awk '{ print $1 }')"
+
+ one="$(echo "$mons" | head -n 1)"
+ two="$(echo "$mons" | tail -n 1)"
+ case "$two" in
+ *None* | "$one")
+ unset two
+ ;;
+ esac
+
+ xrandr --auto
+
+ xrandr --output "$one" --primary --mode 1920x1200 --scale 1x1 --set TearFree on --dpi 161.73
+
+ [ -z "$two" ] && exit
+
+ xrandr --output "$one" --pos 960x2160 --output "$two" --scale 1.5x1.5 --pos 0x0
+ ;;
+*)
+ echo "Usage: x {setup|bg|mon}" >&2
+ exit 1
+ ;;
+esac