nix/scripts/ctl

417 lines
13 KiB
Bash
Executable file

#!/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
choice=$(printf 'QWERTY\nDvorak\nColemak' | fuzzel --dmenu --prompt="keyboard: " --lines=3 --no-icons)
[ -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
case "$2" in
out)
sinks="$(pw-dump | jq -r '.[] | select(.info.props."media.class" == "Audio/Sink") | "\(.id)\t\(.info.props."node.description" // .info.props."node.name" // "unknown")"')"
[ -z "$sinks" ] && exit 0
if [ "$XDG_SESSION_TYPE" = x11 ]; then
choice="$(printf "%s\n" "$sinks" | awk -F'\t' '{print $1": "$2}' | dmenu -i -p "select sink:" | cut -d: -f1)"
else
count=$(printf "%s\n" "$sinks" | wc -l)
choice="$(printf "%s\n" "$sinks" | awk -F'\t' '{print $1": "$2}' | fuzzel --dmenu --prompt="sink: " --lines="$count" | cut -d: -f1)"
fi
[ "$choice" ] && wpctl set-default "$choice"
;;
in)
sources="$(pw-dump | jq -r '.[] | select(.info.props."media.class" == "Audio/Source") | "\(.id)\t\(.info.props."node.description" // .info.props."node.name" // "unknown")"')"
[ -z "$sources" ] && exit 0
if [ "$XDG_SESSION_TYPE" = x11 ]; then
choice="$(printf "%s\n" "$sources" | awk -F'\t' '{print $1": "$2}' | dmenu -i -p "select source:" | cut -d: -f1)"
else
count=$(printf "%s\n" "$sources" | wc -l)
choice="$(printf "%s\n" "$sources" | awk -F'\t' '{print $1": "$2}' | fuzzel --dmenu --prompt="source: " --lines="$count" | cut -d: -f1)"
fi
[ "$choice" ] && wpctl set-default "$choice"
;;
*)
echo "Usage: ctl audio {in|out}" >&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; }
iwctl station "$station" scan 2>/dev/null
sleep 1
networks=$(iwctl station "$station" get-networks 2>/dev/null | \
sed 's/\x1b\[[0-9;]*[a-zA-Z]//g' | \
awk 'NR>4 && /\S/' | \
sed 's/^[[:space:]>]*//' | \
sed 's/[[:space:]]\{3,\}.*//' | \
sed 's/[[:space:]]*$//' | \
grep -v '^-')
[ -z "$networks" ] && exit 0
count=$(printf '%s\n' "$networks" | wc -l)
ssid=$(printf '%s\n' "$networks" | fuzzel --dmenu --prompt="wifi: " --no-icons --lines="$count")
[ -z "$ssid" ] && exit 0
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
;;
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}" >&2
exit 1
;;
esac