#!/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="${XDG_PICTURES_DIR:-$HOME/Pictures}/Screenshots" 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 ;; keyboard) case "$2" in next) if [ "${XDG_SESSION_TYPE:-}" = "wayland" ]; then if [ "$XDG_CURRENT_DESKTOP" = "Hyprland" ]; then hyprctl switchxkblayout current next elif [ "$XDG_CURRENT_DESKTOP" = "sway" ]; then swaymsg input type:keyboard xkb_switch_layout next fi else current="$(setxkbmap -query | awk '/variant/{print $2}')" case "$current" in "") setxkbmap -layout us -variant dvorak ;; dvorak) setxkbmap -layout us -variant colemak ;; *) setxkbmap -layout us ;; esac fi ;; pick) require fuzzel variant=$(hyprctl getoption input:kb_variant 2>/dev/null | awk '/^str:/{print $2}' | cut -d, -f2) case "$variant" in dvorak) current="Dvorak" ;; colemak) current="Colemak" ;; *) current="QWERTY" ;; esac choice=$(printf 'QWERTY\nDvorak\nColemak' | awk -v cur="$current" '{ prefix = ($0 == cur) ? " > " : " " printf "%s%s\t%s\n", prefix, $0, $0 }' | fuzzel --dmenu --prompt="kbd: " --no-icons --lines=3 \ --with-nth=1 --accept-nth=2 --font="monospace:size=12" --width=23) [ -z "$choice" ] && exit 0 case "$choice" in QWERTY) variant="" ;; Dvorak) variant="dvorak" ;; Colemak) variant="colemak" ;; esac if [ "$XDG_CURRENT_DESKTOP" = "Hyprland" ]; then hyprctl keyword input:kb_layout "us,us,us" hyprctl keyword input:kb_variant ",$variant" hyprctl switchxkblayout current 1 fi ;; *) echo "Usage: ctl keyboard {next|pick}" >&2 exit 1 ;; esac ;; audio) require wpctl pw-dump jq fuzzel case "$2" in sink | source) if [ "$2" = sink ]; then class="Audio/Sink" prompt="sink: " default_meta="default.audio.sink" node="@DEFAULT_AUDIO_SINK@" pad=7 else class="Audio/Source" prompt="source: " default_meta="default.audio.source" node="@DEFAULT_AUDIO_SOURCE@" pad=8 fi while :; do dump=$(pw-dump) devices=$(printf '%s' "$dump" | jq -r --arg class "$class" \ '.[] | select(.info.props."media.class" == $class) | "\(.id)\t\(.info.props."node.description" // .info.props."node.name" // "unknown")\t\(.info.props."node.name" // "")"') [ -z "$devices" ] && exit 0 default_name=$(printf '%s' "$dump" | jq -r --arg key "$default_meta" \ '.[] | select(.type == "PipeWire:Interface:Metadata" and .info.props."metadata.name" == "default") | .info.metadata[] | select(.key == $key) | .value | fromjson | .name') rows=$(printf '%s\n' "$devices" | while IFS="$(printf '\t')" read -r id name node_name; do active=$([ "$node_name" = "$default_name" ] && echo ">" || echo "") vol=$(wpctl get-volume "$id" 2>/dev/null | awk '{printf "%d", $2 * 100}') printf '%s\t%s\t%s\t%s\n' "$id" "$name" "$active" "$vol" done) [ -z "$rows" ] && exit 0 w1=$(printf '%s\n' "$rows" | awk -F'\t' 'BEGIN{m=11}{l=length($2);if(l>m)m=l}END{print m}') fw=$((w1 + pad + 16)) sep=$(awk -v n="$((w1 + 10))" 'BEGIN{for(i=0;i%$((pad - 3))s" "") header=$(printf "%s%-*s %s\n%s%s" "$indent" "$w1" "Device name" "Volume" "$indent" "$sep") count=$(printf '%s\n' "$rows" | wc -l) choice=$(printf '%s\n' "$rows" | awk -F'\t' -v w1="$w1" -v indent="$indent" -v active_prefix="$active_prefix" '{ prefix = ($3 == ">") ? active_prefix : indent printf "%s%-*s %3d%%\t%s\n", prefix, w1, $2, $4, $1 }' | fuzzel --dmenu --prompt="$prompt" --no-icons --lines="$count" \ --with-nth=1 --accept-nth=2 \ --mesg="$header" --mesg-mode=expand \ --font="monospace:size=12" --width="$fw") rc=$? [ "$rc" = 11 ] && { wpctl set-volume "$node" 5%+ --limit 1.0; continue; } [ "$rc" = 12 ] && { wpctl set-volume "$node" 5%-; continue; } [ -z "$choice" ] && exit 0 wpctl set-default "$choice" break done ;; *) echo "Usage: ctl audio {sink|source}" >&2 exit 1 ;; esac ;; brightness) require brightnessctl notify-send BRIGHT_STEP=5 case "$2" in up) brightnessctl set "$BRIGHT_STEP"%+ ;; down) brightnessctl set "$BRIGHT_STEP"%- ;; *) echo "Usage: ctl brightness {up|down}" >&2 exit 1 ;; esac pct=$(awk -v cur="$(brightnessctl get)" -v max="$(brightnessctl max)" 'BEGIN { printf "%d", cur * 100 / max }') filled=$((pct / 5)) empty=$((20 - filled)) bar="" i=0 while [ "$i" -lt "$filled" ]; do bar="${bar}━" i=$((i + 1)) done i=0 while [ "$i" -lt "$empty" ]; do bar="${bar}─" i=$((i + 1)) done notify-send -a ctl -t 2500 -r 5555 "󰃠 $bar" ;; volume) require wpctl notify-send SINK="@DEFAULT_AUDIO_SINK@" VOL_STEP=5 get_vol() { wpctl get-volume "$SINK" | awk '{printf "%d", $2 * 100}'; } case "$2" in up) wpctl set-volume "$SINK" "${VOL_STEP}%+" --limit 1.0 ;; down) wpctl set-volume "$SINK" "${VOL_STEP}%-" ;; toggle) wpctl set-mute "$SINK" toggle ;; *) echo "Usage: ctl volume {up|down|toggle}" >&2 exit 1 ;; esac vol=$(get_vol) filled=$((vol / 5)) empty=$((20 - filled)) bar="" i=0 while [ "$i" -lt "$filled" ]; do bar="${bar}━" i=$((i + 1)) done i=0 while [ "$i" -lt "$empty" ]; do bar="${bar}─" i=$((i + 1)) done if wpctl get-volume "$SINK" | grep -q MUTED; then icon="󰖁" elif [ "$vol" -le 33 ]; then icon="󰕿" elif [ "$vol" -le 66 ]; then icon="󰖀" else icon="󰕾" fi notify-send -a ctl -t 2500 -r 5556 "$icon $bar" ;; wifi) case "$2" in pick) require fuzzel station=$(iwctl device list 2>/dev/null | sed 's/\x1b\[[0-9;]*[a-zA-Z]//g' | awk '/station/{print $1}') [ -z "$station" ] && { echo "ctl: no wifi device found" >&2 exit 1 } while :; do networks=$(iwctl station "$station" get-networks 2>/dev/null | sed 's/\x1b\[[0-9;]*[a-zA-Z]//g' | awk 'NR>4 && /\S/ && !/^[[:space:]]*-/' | awk '{ active = ($0 ~ /^[[:space:]]*>/) ? ">" : "" sub(/^[[:space:]>]*/, "") sub(/[[:space:]]*$/, "") i = index($0, " ") if (i > 0) { ssid = substr($0, 1, i - 1) rest = substr($0, i) sub(/^[[:space:]]*/, "", rest) j = index(rest, " ") if (j > 0) { security = substr(rest, 1, j - 1) signal = substr(rest, j) sub(/^[[:space:]]*/, "", signal) sub(/[[:space:]]*$/, "", signal) } else { security = rest signal = "" } sub(/[[:space:]]*$/, "", ssid) sub(/[[:space:]]*$/, "", security) printf "%s\t%s\t%s\t%s\n", ssid, security, signal, active } }') [ -z "$networks" ] && exit 0 w1=$(printf '%s\n' "$networks" | awk -F'\t' 'BEGIN{m=12}{l=length($1);if(l>m)m=l}END{print m}') w2=$(printf '%s\n' "$networks" | awk -F'\t' 'BEGIN{m=8}{l=length($2);if(l>m)m=l}END{print m}') w3=$(printf '%s\n' "$networks" | awk -F'\t' 'BEGIN{m=6}{l=length($3);if(l>m)m=l}END{print m}') fw=$((w1 + 4 + w2 + 4 + w3 + 17)) sep=$(awk -v n="$((w1 + w2 + w3 + 8))" 'BEGIN{for(i=0;i " : " " printf "%s%-*s %-*s %s\t%s\n", prefix, w1, $1, w2, $2, $3, $1 }' | fuzzel --dmenu --prompt="wifi: " --no-icons --lines="$count" \ --with-nth=1 --accept-nth=2 \ --mesg="$header" --mesg-mode=expand \ --font="monospace:size=12" --width="$fw") rc=$? [ "$rc" = 10 ] && continue [ -z "$ssid" ] && exit 0 break done known=$(iwctl known-networks list 2>/dev/null | sed 's/\x1b\[[0-9;]*[a-zA-Z]//g') if printf '%s' "$known" | grep -qF "$ssid"; then iwctl station "$station" connect "$ssid" elif iwctl station "$station" get-networks 2>/dev/null | sed 's/\x1b\[[0-9;]*[a-zA-Z]//g' | grep -F "$ssid" | grep -qi "open"; then iwctl station "$station" connect "$ssid" else pass=$(printf '' | fuzzel --dmenu --prompt="passphrase: " --no-icons --password --lines=0) [ -z "$pass" ] && exit 0 iwctl station "$station" connect "$ssid" --passphrase "$pass" fi ;; *) echo "Usage: ctl wifi {pick}" >&2 exit 1 ;; esac ;; media) if pgrep -x wf-recorder >/dev/null 2>&1; then pkill -INT wf-recorder file=$(cat "${XDG_RUNTIME_DIR:-/tmp}/ctl-recording" 2>/dev/null) rm -f "${XDG_RUNTIME_DIR:-/tmp}/ctl-recording" printf '%s' "$file" | wl-copy notify-send -a ctl -t 2500 "Recording saved to ~${file#"$HOME"}" exit 0 fi pkill -x fuzzel 2>/dev/null && exit 0 require fuzzel grim slurp wl-copy wf-recorder hyprctl jq tesseract ss_dir="${XDG_PICTURES_DIR:-$HOME/Pictures}/Screenshots" rec_dir="${XDG_VIDEOS_DIR:-$HOME/Videos}" mkdir -p "$ss_dir" "$rec_dir" cap_desktop=" Capture Screen" cap_area=" Capture Area" cap_window=" Capture Window" rec_desktop=" Record Screen" rec_area=" Record Area" rec_window=" Record Window" ocr_area=" OCR Area" chosen="$(printf '%s\n' "$cap_desktop" "$cap_area" "$cap_window" "$rec_desktop" "$rec_area" "$rec_window" "$ocr_area" | fuzzel --dmenu --hide-prompt --lines=7 --width=25 --no-icons 2>/dev/null)" case "$chosen" in "$cap_desktop") file="$ss_dir/$(date +'%Y-%m-%d_%H-%M-%S').png" grim "$file" && wl-copy <"$file" && echo "$file" && notify-send -a ctl -t 2500 "Screenshot saved to ~${file#"$HOME"}" ;; "$cap_area") file="$ss_dir/$(date +'%Y-%m-%d_%H-%M-%S').png" grim -g "$(slurp)" "$file" && wl-copy <"$file" && echo "$file" && notify-send -a ctl -t 2500 "Screenshot saved to ~${file#"$HOME"}" ;; "$cap_window") geom=$(hyprctl activewindow -j | jq -r '"\(.at[0]),\(.at[1]) \(.size[0])x\(.size[1])"') file="$ss_dir/$(date +'%Y-%m-%d_%H-%M-%S').png" grim -g "$geom" "$file" && wl-copy <"$file" && echo "$file" && notify-send -a ctl -t 2500 "Screenshot saved to ~${file#"$HOME"}" ;; "$rec_desktop") file="$rec_dir/$(date +'%Y-%m-%d_%H-%M-%S').mp4" echo "$file" >"${XDG_RUNTIME_DIR:-/tmp}/ctl-recording" notify-send -a ctl -t 2500 "Recording started" wf-recorder -f "$file" & ;; "$rec_area") file="$rec_dir/$(date +'%Y-%m-%d_%H-%M-%S').mp4" echo "$file" >"${XDG_RUNTIME_DIR:-/tmp}/ctl-recording" notify-send -a ctl -t 2500 "Recording started" wf-recorder -g "$(slurp)" -f "$file" & ;; "$rec_window") geom=$(hyprctl activewindow -j | jq -r '"\(.at[0]),\(.at[1]) \(.size[0])x\(.size[1])"') file="$rec_dir/$(date +'%Y-%m-%d_%H-%M-%S').mp4" echo "$file" >"${XDG_RUNTIME_DIR:-/tmp}/ctl-recording" notify-send -a ctl -t 2500 "Recording started" wf-recorder -g "$geom" -f "$file" & ;; "$ocr_area") file="$ss_dir/$(date +'%Y-%m-%d_%H-%M-%S').png" region="$(slurp)" || exit 0 [ -n "$region" ] || exit 0 grim -g "$region" "$file" && tesseract -l eng "$file" - 2>/dev/null | wl-copy ;; esac ;; wallpaper) require python python - "$2" <<'PYTHON' import os import random import subprocess import sys try: from PIL import Image, ImageDraw except ImportError: print("ctl: missing dependency: pillow", file=sys.stderr) sys.exit(1) HOME = os.environ["HOME"] DIR = os.path.join(os.environ.get("XDG_PICTURES_DIR", os.path.join(HOME, "Pictures")), "Screensavers") 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) cmd = sys.argv[1] if len(sys.argv) > 1 else None if cmd == "lock": lock() elif cmd == "wall": wall() else: print("Usage: ctl wallpaper {wall|lock}", file=sys.stderr) sys.exit(1) PYTHON ;; clip) require fuzzel cliphist wl-copy entries=$(cliphist list) [ -z "$entries" ] && exit 0 count=$(printf '%s\n' "$entries" | wc -l) lines=$((count < 15 ? count : 15)) chosen=$(printf '%s\n' "$entries" | awk -F'\t' '{ content = substr($0, index($0, "\t") + 1) display = (length(content) > 63) ? substr(content, 1, 60) "..." : content printf " %s\t%s\n", display, $1 }' | fuzzel --dmenu --no-icons --prompt="clip: " \ --with-nth=1 --accept-nth=2 --font="monospace:size=12" \ --lines="$lines" --width=75) [ -z "$chosen" ] && exit 0 original=$(printf '%s\n' "$entries" | awk -F'\t' -v id="$chosen" '$1==id{print;exit}') printf '%s' "$original" | cliphist decode | wl-copy ;; power) require fuzzel lock="󰌾 Lock" suspend="󰒲 Suspend" logout="󰍃 Logout" reboot="󰜉 Reboot" shutdown="󰐥 Shutdown" chosen="$(printf '%s\n' "$lock" "$suspend" "$logout" "$reboot" "$shutdown" | fuzzel --dmenu --hide-prompt --lines=5 --width=20 --no-icons)" case "$chosen" in "$lock") hyprlock ;; "$suspend") systemctl suspend ;; "$logout") hyprctl dispatch exit ;; "$reboot") systemctl reboot ;; "$shutdown") systemctl poweroff ;; esac ;; idle) require notify-send if systemctl --user is-active --quiet hypridle.service; then systemctl --user stop hypridle.service tmux set -g lock-after-time 0 2>/dev/null notify-send -a ctl -t 2500 'idle off' else systemctl --user start hypridle.service tmux set -g lock-after-time 300 2>/dev/null notify-send -a ctl -t 2500 'idle on' fi ;; *) echo "Usage: ctl {screenshot|keyboard|audio|wifi|brightness|volume|media|wallpaper|power|idle|clip}" >&2 exit 1 ;; esac