working on monitors tui
This commit is contained in:
294
monitors/monitor-tui.sh
Executable file
294
monitors/monitor-tui.sh
Executable file
@@ -0,0 +1,294 @@
|
||||
#! /usr/bin/env nix-shell
|
||||
#! nix-shell -i bash -p gnome-monitor-config dialog newt bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# === Config ===
|
||||
APP_NAME="GNOME Monitor TUI"
|
||||
SNAP_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/monitor-tui"
|
||||
mkdir -p "$SNAP_DIR"
|
||||
|
||||
# === UI helpers (dialog -> whiptail -> plain) ===
|
||||
have() { command -v "$1" >/dev/null 2>&1; }
|
||||
UI=""
|
||||
if have dialog; then UI="dialog"
|
||||
elif have whiptail; then UI="whiptail"
|
||||
else UI="plain"; fi
|
||||
|
||||
# Provide a short, useful description for a connector (status, primary, resolution)
|
||||
describe_connector() {
|
||||
local c="$1"
|
||||
local desc=""
|
||||
if have xrandr; then
|
||||
local line
|
||||
line="$(xrandr --query 2>/dev/null | grep -E "^${c}\\b" | head -n1 || true)"
|
||||
if [ -n "$line" ]; then
|
||||
local status
|
||||
status="$(awk '{print $2}' <<<"$line")"
|
||||
local primary=""
|
||||
grep -q " primary " <<<"$line" && primary=" primary"
|
||||
local respos
|
||||
respos="$(grep -Eo '[0-9]{3,}x[0-9]{3,}\+[0-9]+\+[0-9]+' <<<"$line" | head -n1)"
|
||||
[ -n "$respos" ] && respos=" $respos"
|
||||
desc="[$status$primary$respos]"
|
||||
fi
|
||||
fi
|
||||
if [ -z "$desc" ] && have gnome-monitor-config; then
|
||||
local gline
|
||||
gline="$(gnome-monitor-config list 2>/dev/null | grep -E "\\b${c}\\b" | head -n1 || true)"
|
||||
if [ -n "$gline" ]; then
|
||||
local wh
|
||||
wh="$(echo "$gline" | grep -Eo '[0-9]{3,}x[0-9]{3,}@?[0-9.]*' | head -n1)"
|
||||
if [ -n "$wh" ]; then desc="[present $wh]"; else desc="[present]"; fi
|
||||
fi
|
||||
fi
|
||||
[ -z "$desc" ] && desc="[present]"
|
||||
case "$c" in eDP-*) desc="$desc internal" ;; esac
|
||||
echo "$desc"
|
||||
}
|
||||
|
||||
title() { printf "%s\n" "$1"; }
|
||||
msgbox() {
|
||||
local text="$1"
|
||||
case "$UI" in
|
||||
dialog) dialog --title "$APP_NAME" --msgbox "$text" 10 70 ;;
|
||||
whiptail) whiptail --title "$APP_NAME" --msgbox "$text" 10 70 ;;
|
||||
plain) printf "\n== %s ==\n%s\n\n" "$APP_NAME" "$text"; read -rp "Press Enter..." ;;
|
||||
esac
|
||||
}
|
||||
inputbox() {
|
||||
local prompt="$1" default="${2:-}"
|
||||
case "$UI" in
|
||||
dialog) dialog --title "$APP_NAME" --inputbox "$prompt" 10 70 "$default" 3>&1 1>&2 2>&3 ;;
|
||||
whiptail) whiptail --title "$APP_NAME" --inputbox "$prompt" 10 70 "$default" 3>&1 1>&2 2>&3 ;;
|
||||
plain) read -rp "$prompt [$default]: " ans; echo "${ans:-$default}" ;;
|
||||
esac
|
||||
}
|
||||
menu() {
|
||||
# args: prompt key1 "desc1" key2 "desc2" ...
|
||||
local prompt="$1"; shift
|
||||
case "$UI" in
|
||||
dialog) dialog --title "$APP_NAME" --menu "$prompt" 20 70 12 "$@" 3>&1 1>&2 2>&3 ;;
|
||||
whiptail) whiptail --title "$APP_NAME" --menu "$prompt" 20 70 12 "$@" 3>&1 1>&2 2>&3 ;;
|
||||
plain)
|
||||
echo; echo "$prompt"
|
||||
local i=1 opts=() keys=()
|
||||
while [ "$#" -gt 0 ]; do keys+=("$1"); shift; opts+=("$i) $1"); shift; i=$((i+1)); done
|
||||
for o in "${opts[@]}"; do echo " $o"; done
|
||||
read -rp "Choose 1-$((i-1)): " ix
|
||||
echo "${keys[$((ix-1))]}"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
checklist() {
|
||||
# returns space-separated chosen keys
|
||||
local prompt="$1"; shift
|
||||
case "$UI" in
|
||||
dialog)
|
||||
dialog --title "$APP_NAME" --checklist "$prompt" 20 70 12 "$@" 3>&1 1>&2 2>&3 | tr -d '"'
|
||||
;;
|
||||
whiptail)
|
||||
whiptail --title "$APP_NAME" --checklist "$prompt" 20 70 12 "$@" 3>&1 1>&2 2>&3 | tr -d '"'
|
||||
;;
|
||||
plain)
|
||||
echo; echo "$prompt"
|
||||
local idx=1 keys=() descs=()
|
||||
while [ "$#" -gt 0 ]; do
|
||||
keys+=("$1"); shift
|
||||
[ "$#" -gt 0 ] && descs+=("$1") && shift || break
|
||||
[ "$#" -gt 0 ] && shift || break # skip ON/OFF
|
||||
done
|
||||
for k in "${!keys[@]}"; do echo " [ ] $((k+1)) ${keys[$k]} ${descs[$k]}"; done
|
||||
read -rp "Enter numbers to select (e.g. 1 3): " picks
|
||||
local out=""
|
||||
for n in $picks; do out+="${keys[$((n-1))]} "; done
|
||||
echo "$out"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# === Discover connectors ===
|
||||
discover_connectors() {
|
||||
# Try gnome-monitor-config output first, then fallbacks
|
||||
local out=""
|
||||
if have gnome-monitor-config; then
|
||||
# tolerant scrape for common connector names
|
||||
out="$(gnome-monitor-config list 2>/dev/null | grep -Eo '\b(HDMI|DP|eDP|VGA|DVI|USB-C)(-[0-9]+)\b' | sort -u || true)"
|
||||
fi
|
||||
if [ -z "$out" ] && have xrandr; then
|
||||
out="$(xrandr --listmonitors 2>/dev/null | grep -Eo '\b(HDMI|DP|eDP|VGA|DVI|USB-C)(-[0-9]+)\b' | sort -u || true)"
|
||||
fi
|
||||
if [ -z "$out" ]; then
|
||||
msgbox "Couldn't auto-detect connectors. You'll be asked to type names like: DP-3, DP-10, eDP-1"
|
||||
local manual
|
||||
manual="$(inputbox "Enter connector names (space-separated):" "DP-3 DP-10 eDP-1")"
|
||||
out="$manual"
|
||||
fi
|
||||
echo "$out"
|
||||
}
|
||||
|
||||
# === Build gnome-monitor-config command from choices ===
|
||||
build_command() {
|
||||
local mirror_group="$1"; shift
|
||||
local placement="$1"; shift # below/above/left/right
|
||||
local offset_default_y="$1"; shift
|
||||
local offset_default_x="$1"; shift
|
||||
local others=("$@")
|
||||
|
||||
local cmd=(gnome-monitor-config set)
|
||||
|
||||
# First logical monitor: mirrored group as PRIMARY at 0,0
|
||||
local L1=(-Lp)
|
||||
for m in $mirror_group; do L1+=(-M "$m"); done
|
||||
L1+=(-x 0 -y 0)
|
||||
cmd+=("${L1[@]}")
|
||||
|
||||
# Place others relative to group
|
||||
for o in "${others[@]}"; do
|
||||
[ -z "$o" ] && continue
|
||||
local x=0 y=0
|
||||
case "$placement" in
|
||||
below) y="$offset_default_y"; x="$offset_default_x" ;;
|
||||
above) y="-$offset_default_y"; x="$offset_default_x" ;;
|
||||
right) x="$offset_default_x"; y="$offset_default_y" ;;
|
||||
left) x="-$offset_default_x"; y="$offset_default_y" ;;
|
||||
*) y="$offset_default_y"; x="$offset_default_x" ;;
|
||||
esac
|
||||
cmd+=(-L -M "$o" -x "$x" -y "$y")
|
||||
done
|
||||
|
||||
printf "%q " "${cmd[@]}"
|
||||
}
|
||||
|
||||
# === Snapshots ===
|
||||
snapshot_save() {
|
||||
local label="$1" cmdline="$2"
|
||||
local file="$SNAP_DIR/$(echo "$label" | tr '[:space:]' '_' | tr -cd '[:alnum:]_-.').sh"
|
||||
cat >"$file" <<EOF
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
$cmdline
|
||||
EOF
|
||||
chmod +x "$file"
|
||||
echo "$file"
|
||||
}
|
||||
snapshot_pick() {
|
||||
# List saved snapshot scripts and allow the user to pick one
|
||||
# Avoid invalid array assignment with redirections; just check with compgen/ls
|
||||
if ! ls "$SNAP_DIR"/*.sh >/dev/null 2>&1; then
|
||||
msgbox "No snapshots saved yet."
|
||||
return 1
|
||||
fi
|
||||
|
||||
local menu_args=()
|
||||
for f in "$SNAP_DIR"/*.sh; do
|
||||
local base; base="$(basename "$f")"
|
||||
menu_args+=("$base" "apply/delete")
|
||||
done
|
||||
local pick
|
||||
pick="$(menu "Snapshots:" "${menu_args[@]}")" || return 1
|
||||
echo "$SNAP_DIR/$pick"
|
||||
}
|
||||
|
||||
# === Main flow ===
|
||||
main_menu() {
|
||||
while true; do
|
||||
case "$(menu "Choose an action:" \
|
||||
new "Create & apply a new layout" \
|
||||
apply "Apply a saved snapshot" \
|
||||
delete "Delete a saved snapshot" \
|
||||
quit "Quit")" in
|
||||
new)
|
||||
new_layout
|
||||
;;
|
||||
apply)
|
||||
local f; f="$(snapshot_pick)" || continue
|
||||
if [ -n "${f:-}" ]; then
|
||||
bash "$f" && msgbox "Applied snapshot: $(basename "$f")"
|
||||
fi
|
||||
;;
|
||||
delete)
|
||||
local f; f="$(snapshot_pick)" || continue
|
||||
[ -n "${f:-}" ] && rm -f "$f" && msgbox "Deleted snapshot: $(basename "$f")"
|
||||
;;
|
||||
quit|"")
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
new_layout() {
|
||||
local conns; conns="$(discover_connectors)"
|
||||
# build checklist args: key desc ON/OFF with additional info
|
||||
local cl_args=()
|
||||
for c in $conns; do
|
||||
local info; info="$(describe_connector "$c")"
|
||||
cl_args+=("$c" "$info" OFF)
|
||||
done
|
||||
local picked; picked="$(checklist "Pick one or more connectors to MIRROR as a single logical monitor (these will be your 'big' display):" "${cl_args[@]}")" || return
|
||||
if [ -z "$picked" ]; then msgbox "You must select at least one connector."; return; fi
|
||||
|
||||
# Show a quick summary of current connectors before placement
|
||||
local summary=""
|
||||
for c in $conns; do summary+="$c $(describe_connector "$c")\n"; done
|
||||
msgbox "Detected connectors:\n$summary"
|
||||
|
||||
# Pick placement for the remaining connectors
|
||||
local place; place="$(menu "Where do you want to place the OTHER monitors relative to the mirrored group?" \
|
||||
below "Below (typical laptop-under-desktop)" \
|
||||
above "Above" \
|
||||
left "Left" \
|
||||
right "Right")" || return
|
||||
|
||||
# Default offsets (you can tweak them)
|
||||
local offy offx
|
||||
offy="$(inputbox "Pixel offset for Y (distance from mirrored group). Example: 2160" "2160")"
|
||||
offx="$(inputbox "Pixel offset for X. Example: 0" "0")"
|
||||
|
||||
# Others = conns not in picked
|
||||
read -ra all_arr <<<"$conns"
|
||||
read -ra pick_arr <<<"$picked"
|
||||
# build associative map for fast exclude
|
||||
declare -A is_pick; for p in "${pick_arr[@]}"; do is_pick["$p"]=1; done
|
||||
local others=()
|
||||
for a in "${all_arr[@]}"; do
|
||||
if [ -z "${is_pick[$a]+x}" ]; then others+=("$a"); fi
|
||||
done
|
||||
|
||||
# Confirm which of the remaining to include (maybe none), showing info
|
||||
if [ "${#others[@]}" -gt 0 ]; then
|
||||
local cl2_args=()
|
||||
for o in "${others[@]}"; do
|
||||
local info; info="$(describe_connector "$o")"
|
||||
cl2_args+=("$o" "$info" ON)
|
||||
done
|
||||
local picked2; picked2="$(checklist "Select which of the remaining connectors to include (they will be placed $place the group):" "${cl2_args[@]}")" || picked2=""
|
||||
read -ra others <<<"$picked2"
|
||||
fi
|
||||
|
||||
local cmdline; cmdline="$(build_command "$picked" "$place" "$offy" "$offx" "${others[@]:-}")"
|
||||
# Try to apply
|
||||
if $cmdline; then
|
||||
msgbox "Applied:\n$cmdline"
|
||||
# Offer to snapshot
|
||||
case "$(menu "Snapshot this working layout?" yes "Save snapshot" no "Skip")" in
|
||||
yes)
|
||||
local label; label="$(inputbox "Snapshot label:" "mirrored_group_$(date +%Y%m%d_%H%M%S)")"
|
||||
local file; file="$(snapshot_save "$label" "$cmdline")"
|
||||
msgbox "Saved snapshot: $file"
|
||||
;;
|
||||
*) : ;;
|
||||
esac
|
||||
else
|
||||
msgbox "Failed to apply the layout.\nCommand was:\n$cmdline"
|
||||
fi
|
||||
}
|
||||
|
||||
# === Checks ===
|
||||
if ! have gnome-monitor-config; then
|
||||
msgbox "gnome-monitor-config is required (Wayland). Please install it (part of GNOME).\nAborting."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
main_menu
|
||||
Reference in New Issue
Block a user