Submenus returned into a case that then fell out of the script, so escape closed everything. The panel now loops, and only escape on the main list (or an action that hands the screen to something else) closes it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cn3dHfbgNYdbZvzb1b3GQt
315 lines
13 KiB
Bash
Executable file
315 lines
13 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
# Sakura Line - control centre
|
|
#
|
|
# One centred panel for network, bluetooth, audio, vpn, session and desktop
|
|
# toggles. Submenus reuse the same window, so it reads as a single panel
|
|
# rather than a pile of separate menus.
|
|
|
|
set -uo pipefail
|
|
|
|
RASI="$HOME/.config/rofi/control.rasi"
|
|
# 1 = close the panel entirely, 0 = fall back to the main list
|
|
CC_EXIT=0
|
|
menu() { # menu <prompt> [rofi args...]
|
|
local prompt="$1"; shift
|
|
rofi -dmenu -i -theme "$RASI" -p "$prompt" "$@"
|
|
}
|
|
notify() { notify-send -a "Control" "$@" >/dev/null 2>&1; }
|
|
have() { command -v "$1" >/dev/null 2>&1; }
|
|
# sxhkd inherits a bare PATH, so ~/.local/bin needs adding back by hand
|
|
export PATH="$HOME/.local/bin:$PATH"
|
|
|
|
# ------------------------------------------------------------------ status
|
|
wifi_status() {
|
|
have nmcli || { echo "unavailable"; return; }
|
|
if [ "$(nmcli -t -f WIFI radio 2>/dev/null)" != "enabled" ]; then echo "off"; return; fi
|
|
local eth ssid
|
|
eth=$(nmcli -t -f TYPE,STATE device status 2>/dev/null | grep -c '^ethernet:connected')
|
|
[ "$eth" -gt 0 ] && { echo "ethernet"; return; }
|
|
ssid=$(nmcli -t -f NAME,TYPE connection show --active 2>/dev/null | awk -F: '$2 ~ /wireless/ {print $1; exit}')
|
|
[ -n "$ssid" ] && echo "$ssid" || echo "on, not connected"
|
|
}
|
|
bt_status() {
|
|
have bluetoothctl || { echo "unavailable"; return; }
|
|
bluetoothctl show 2>/dev/null | grep -q "Powered: yes" || { echo "off"; return; }
|
|
local n
|
|
n=$(bluetoothctl devices Connected 2>/dev/null | grep -c '^Device')
|
|
[ "${n:-0}" -gt 0 ] && echo "$n connected" || echo "on"
|
|
}
|
|
vol_status() {
|
|
have wpctl || { echo "unavailable"; return; }
|
|
local out
|
|
out=$(wpctl get-volume @DEFAULT_AUDIO_SINK@ 2>/dev/null) || { echo "unavailable"; return; }
|
|
grep -q MUTED <<<"$out" && { echo "muted"; return; }
|
|
awk '{printf "%d%%", $2*100}' <<<"$out"
|
|
}
|
|
mic_status() {
|
|
have wpctl || { echo "unavailable"; return; }
|
|
local out
|
|
out=$(wpctl get-volume @DEFAULT_AUDIO_SOURCE@ 2>/dev/null) || { echo "unavailable"; return; }
|
|
grep -q MUTED <<<"$out" && echo "muted" || awk '{printf "%d%%", $2*100}' <<<"$out"
|
|
}
|
|
vpn_status() {
|
|
local ifs
|
|
ifs=$(ip -br link show type wireguard 2>/dev/null | awk '{print $1}' | paste -sd, -)
|
|
[ -n "$ifs" ] && echo "$ifs" || echo "off"
|
|
}
|
|
dnd_status() {
|
|
have dunstctl || { echo "unavailable"; return; }
|
|
[ "$(dunstctl is-paused 2>/dev/null)" = true ] && echo "paused" || echo "on"
|
|
}
|
|
display_status() {
|
|
have xrandr || { echo "unavailable"; return; }
|
|
xrandr --current 2>/dev/null | awk '/\*/{print $1; exit}'
|
|
}
|
|
bar_status() { pgrep -x polybar >/dev/null 2>&1 && echo "shown" || echo "hidden"; }
|
|
particles_status() {
|
|
have particles || { echo "not installed"; return; }
|
|
particles --status 2>/dev/null | grep -q running && echo "on" || echo "off"
|
|
}
|
|
|
|
# ------------------------------------------------------------- submenus
|
|
wifi_menu() {
|
|
local radio choice ssid pass
|
|
radio=$(nmcli -t -f WIFI radio 2>/dev/null)
|
|
{
|
|
[ "$radio" = enabled ] && echo " Turn Wi-Fi off" || echo " Turn Wi-Fi on"
|
|
echo " Rescan"
|
|
if [ "$radio" = enabled ]; then
|
|
nmcli -t -f IN-USE,SIGNAL,SECURITY,SSID device wifi list 2>/dev/null |
|
|
awk -F: 'length($4){
|
|
icon = ($1=="*") ? "" : (($3=="" || $3=="--") ? "" : "")
|
|
printf "%s %s\t%s%%\n", icon, $4, $2 }' | sort -u -k1,1
|
|
fi
|
|
} > /tmp/.cc-wifi.$$
|
|
choice=$(menu "Wi-Fi" < /tmp/.cc-wifi.$$); rm -f /tmp/.cc-wifi.$$
|
|
[ -z "$choice" ] && return
|
|
case "$choice" in
|
|
*"Turn Wi-Fi off") nmcli radio wifi off; notify "Wi-Fi" "Turned off" ;;
|
|
*"Turn Wi-Fi on") nmcli radio wifi on; notify "Wi-Fi" "Turned on" ;;
|
|
*Rescan) nmcli device wifi rescan >/dev/null 2>&1; notify "Wi-Fi" "Rescanning"; wifi_menu ;;
|
|
*) ssid=$(sed 's/^[^ ]* //; s/\t.*$//' <<<"$choice")
|
|
if nmcli -t -f NAME connection show 2>/dev/null | grep -qx "$ssid"; then
|
|
nmcli connection up id "$ssid" >/dev/null 2>&1 \
|
|
&& notify "Wi-Fi" "Connected to $ssid" || notify -u critical "Wi-Fi" "Failed: $ssid"
|
|
else
|
|
pass=$(menu "Password for $ssid" -password)
|
|
[ -z "$pass" ] && return
|
|
nmcli device wifi connect "$ssid" password "$pass" >/dev/null 2>&1 \
|
|
&& notify "Wi-Fi" "Connected to $ssid" || notify -u critical "Wi-Fi" "Failed: $ssid"
|
|
fi ;;
|
|
esac
|
|
}
|
|
|
|
bt_menu() {
|
|
local powered choice mac
|
|
powered=$(bluetoothctl show 2>/dev/null | grep -q "Powered: yes" && echo yes || echo no)
|
|
{
|
|
[ "$powered" = yes ] && echo " Turn Bluetooth off" || echo " Turn Bluetooth on"
|
|
if [ "$powered" = yes ]; then
|
|
echo " Scan for devices"
|
|
bluetoothctl devices 2>/dev/null | while read -r _ mac name; do
|
|
if bluetoothctl info "$mac" 2>/dev/null | grep -q "Connected: yes"; then
|
|
printf " %s\t%s\n" "$name" "$mac"
|
|
else
|
|
printf " %s\t%s\n" "$name" "$mac"
|
|
fi
|
|
done
|
|
fi
|
|
} > /tmp/.cc-bt.$$
|
|
choice=$(menu "Bluetooth" < /tmp/.cc-bt.$$); rm -f /tmp/.cc-bt.$$
|
|
[ -z "$choice" ] && return
|
|
case "$choice" in
|
|
*"Turn Bluetooth off") bluetoothctl power off >/dev/null; notify "Bluetooth" "Turned off" ;;
|
|
*"Turn Bluetooth on") bluetoothctl power on >/dev/null; notify "Bluetooth" "Turned on" ;;
|
|
*"Scan for devices") notify "Bluetooth" "Scanning for 10s"
|
|
bluetoothctl --timeout 10 scan on >/dev/null 2>&1; bt_menu ;;
|
|
*) mac=$(awk -F'\t' '{print $2}' <<<"$choice")
|
|
[ -z "$mac" ] && return
|
|
if bluetoothctl info "$mac" 2>/dev/null | grep -q "Connected: yes"; then
|
|
bluetoothctl disconnect "$mac" >/dev/null && notify "Bluetooth" "Disconnected"
|
|
else
|
|
notify "Bluetooth" "Connecting..."
|
|
bluetoothctl connect "$mac" >/dev/null 2>&1 \
|
|
&& notify "Bluetooth" "Connected" || notify -u critical "Bluetooth" "Connection failed"
|
|
fi ;;
|
|
esac
|
|
}
|
|
|
|
audio_menu() {
|
|
local choice sink
|
|
{
|
|
echo " Toggle mute $(vol_status)"
|
|
echo " Volume down -5%"
|
|
echo " Volume up +5%"
|
|
echo " Toggle mic mute $(mic_status)"
|
|
pactl list short sinks 2>/dev/null | while read -r id name _; do
|
|
printf " %s\t%s\n" "$(pactl list sinks 2>/dev/null | grep -A2 "Name: $name" | awk -F': ' '/Description/{print $2; exit}')" "$name"
|
|
done
|
|
} > /tmp/.cc-audio.$$
|
|
choice=$(menu "Audio" < /tmp/.cc-audio.$$); rm -f /tmp/.cc-audio.$$
|
|
[ -z "$choice" ] && return
|
|
case "$choice" in
|
|
*"Toggle mute") wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle; audio_menu ;;
|
|
*"Volume down"*) wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%-; audio_menu ;;
|
|
*"Volume up"*) wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%+ -l 1.0; audio_menu ;;
|
|
*"Toggle mic mute") wpctl set-mute @DEFAULT_AUDIO_SOURCE@ toggle; audio_menu ;;
|
|
*) sink=$(awk -F'\t' '{print $2}' <<<"$choice")
|
|
[ -n "$sink" ] && pactl set-default-sink "$sink" && notify "Audio" "Output switched" ;;
|
|
esac
|
|
}
|
|
|
|
vpn_menu() {
|
|
local choice iface up
|
|
{
|
|
for iface in $(ip -br link show type wireguard 2>/dev/null | awk '{print $1}'); do
|
|
printf " %s\tdown\n" "$iface"
|
|
done
|
|
# wg-quick@<name> units systemd knows about, minus the ones already up
|
|
systemctl list-units --all --type=service --no-legend 'wg-quick@*' 2>/dev/null |
|
|
awk '{print $1}' | sed 's/^wg-quick@//; s/\.service$//' | while read -r n; do
|
|
[ -z "$n" ] && continue
|
|
ip link show "$n" >/dev/null 2>&1 || printf " %s\tup\n" "$n"
|
|
done
|
|
} > /tmp/.cc-vpn.$$
|
|
if [ ! -s /tmp/.cc-vpn.$$ ]; then
|
|
echo " No WireGuard interfaces found" > /tmp/.cc-vpn.$$
|
|
echo " Start one by name…" >> /tmp/.cc-vpn.$$
|
|
fi
|
|
choice=$(menu "VPN" < /tmp/.cc-vpn.$$); rm -f /tmp/.cc-vpn.$$
|
|
[ -z "$choice" ] && return
|
|
case "$choice" in
|
|
*"No WireGuard"*) return ;;
|
|
*"Start one by name"*)
|
|
iface=$(menu "Interface name (e.g. wg0)")
|
|
[ -z "$iface" ] && return
|
|
systemctl start "wg-quick@${iface}" && notify "VPN" "$iface up" \
|
|
|| notify -u critical "VPN" "Could not start $iface" ;;
|
|
*) iface=$(sed 's/^[^ ]* //; s/\t.*$//' <<<"$choice")
|
|
up=$(awk -F'\t' '{print $2}' <<<"$choice")
|
|
if [ "$up" = down ]; then
|
|
systemctl stop "wg-quick@${iface}" && notify "VPN" "$iface down" \
|
|
|| notify -u critical "VPN" "Could not stop $iface"
|
|
else
|
|
systemctl start "wg-quick@${iface}" && notify "VPN" "$iface up" \
|
|
|| notify -u critical "VPN" "Could not start $iface"
|
|
fi ;;
|
|
esac
|
|
}
|
|
|
|
lock_screen() {
|
|
if have betterlockscreen; then betterlockscreen -l blur
|
|
elif have i3lock; then i3lock -c 0f0d17
|
|
elif have xsecurelock; then xsecurelock &
|
|
elif have slock; then slock
|
|
else notify -u critical "Lock" "No screen locker installed (try i3lock)"; return 1
|
|
fi
|
|
}
|
|
|
|
power_menu() {
|
|
local choice
|
|
choice=$(printf '%s\n' \
|
|
" Lock" \
|
|
" Log out" \
|
|
" Suspend" \
|
|
" Reboot" \
|
|
" Shut down" | menu "Power")
|
|
[ -z "$choice" ] && return # escape here goes back to the panel
|
|
CC_EXIT=1
|
|
case "$choice" in
|
|
*Lock) lock_screen ;;
|
|
*"Log out") bspc quit ;;
|
|
*Suspend) lock_screen; systemctl suspend ;;
|
|
*Reboot) systemctl reboot ;;
|
|
*"Shut down") systemctl poweroff ;;
|
|
esac
|
|
}
|
|
|
|
notif_menu() {
|
|
local choice paused
|
|
paused=$(dunstctl is-paused 2>/dev/null)
|
|
choice=$(printf '%s\n' \
|
|
"$([ "$paused" = true ] && echo " Resume notifications" || echo " Do not disturb")" \
|
|
" Show last notification" \
|
|
" Show all history" \
|
|
" Clear all" | menu "Notifications")
|
|
case "$choice" in
|
|
*"Do not disturb") dunstctl set-paused true; notify "Notifications" "Paused" ;;
|
|
*"Resume notifications") dunstctl set-paused false; notify "Notifications" "Resumed" ;;
|
|
*"Show last notification") dunstctl history-pop ;;
|
|
*"Show all history") for _ in $(seq 6); do dunstctl history-pop; done ;;
|
|
*"Clear all") dunstctl close-all ;;
|
|
esac
|
|
}
|
|
|
|
display_menu() {
|
|
local out choice mode
|
|
out=$(xrandr --current 2>/dev/null | awk '/ connected/{print $1; exit}')
|
|
[ -z "$out" ] && { notify -u critical "Display" "No output found"; return; }
|
|
choice=$(xrandr --current 2>/dev/null | sed -n "/^$out connected/,/^[^ ]/p" |
|
|
awk '/^ /{printf " %s\t%s\n", $1, $2}' | head -12 | menu "Display ($out)")
|
|
[ -z "$choice" ] && return
|
|
mode=$(sed 's/^[^ ]* //; s/\t.*$//' <<<"$choice")
|
|
xrandr --output "$out" --mode "$mode" && notify "Display" "$out set to $mode"
|
|
}
|
|
|
|
shot_menu() {
|
|
local choice
|
|
have flameshot || { notify -u critical "Screenshot" "flameshot is not installed"; return; }
|
|
choice=$(printf '%s\n' \
|
|
" Region" \
|
|
" Full screen" \
|
|
" Region, delayed 3s" | menu "Screenshot")
|
|
[ -z "$choice" ] && return
|
|
CC_EXIT=1
|
|
case "$choice" in
|
|
*Region) flameshot gui ;;
|
|
*"Full screen") flameshot full -c -p "$HOME/pictures" 2>/dev/null || flameshot full -c ;;
|
|
*"delayed 3s") flameshot gui -d 3000 ;;
|
|
esac
|
|
}
|
|
|
|
# ---------------------------------------------------------------- main panel
|
|
main_menu() {
|
|
local choice
|
|
choice=$(printf '%s\n' \
|
|
" Network $(wifi_status)" \
|
|
" Bluetooth $(bt_status)" \
|
|
" Audio $(vol_status)" \
|
|
" Microphone $(mic_status)" \
|
|
" VPN $(vpn_status)" \
|
|
" Display $(display_status)" \
|
|
" Notifications $(dnd_status)" \
|
|
" Clipboard" \
|
|
" Emoji" \
|
|
" Screenshot" \
|
|
" Wallpaper shuffle" \
|
|
" Particles $(particles_status)" \
|
|
" Bar $(bar_status)" \
|
|
" Power" | menu "Control")
|
|
[ -z "$choice" ] && { CC_EXIT=1; return; }
|
|
case "$choice" in
|
|
*) wifi_menu ;;
|
|
*) bt_menu ;;
|
|
*) audio_menu ;;
|
|
*) have wpctl && wpctl set-mute @DEFAULT_AUDIO_SOURCE@ toggle
|
|
notify "Microphone" "Now $(mic_status)" ;;
|
|
*) vpn_menu ;;
|
|
*) display_menu ;;
|
|
*) notif_menu ;;
|
|
*) CC_EXIT=1; sh "$HOME/.config/scripts/rofi-menus/clipboard-manager.sh" ;;
|
|
*) CC_EXIT=1; sh "$HOME/.config/scripts/rofi-menus/rofimoji.sh" ;;
|
|
*) shot_menu ;;
|
|
*) sh "$HOME/.config/scripts/set-wallpaper.sh" >/dev/null 2>&1
|
|
notify "Wallpaper" "Shuffled" ;;
|
|
*) if have particles; then particles >/dev/null 2>&1
|
|
notify "Particles" "Now $(particles_status)"
|
|
else notify -u critical "Particles" "Not installed"; fi ;;
|
|
*) sh "$HOME/.config/scripts/toggle-bar.sh" ;;
|
|
*) power_menu ;;
|
|
esac
|
|
}
|
|
|
|
while [ "$CC_EXIT" -eq 0 ]; do
|
|
main_menu
|
|
done
|