v0.1.10 — Settings TUI layout polish

Banner, state panel, menu header, and menu items now share a single centred
panel column rather than each block centring itself independently. The TUI
feels visibly aligned in a Walker floating window of any width — no more
drifting elements off to the left while the menu floats to the right.

- Adaptive panel width: min(COLS − 6, 60), floored at 40.
- Terminal-width detection: `stty size </dev/tty` first (kernel-reported,
  always reflects the live window) with `tput cols`/80 fallback. Fixes
  off-centre rendering in freshly-spawned floating terminals whose terminfo
  hasn't caught up to the compositor's actual size yet.
- Config-file path renders as `~/...` instead of `/home/<user>/...` so it
  fits the panel.
- Unset resolution shows `<auto>` instead of `?x?` (matches the other unset
  placeholders).

Internally: `pad_block` replaces `center_block`'s widest-line-centres-block
behaviour with a single shared left margin; `cmenu` left-aligns the menu
at the panel edge so the `> ` cursor sits in the same column as the state
panel's section headers above.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
28allday 2026-05-18 19:01:07 +01:00
parent 3131dcab8f
commit aaa2f3d768
3 changed files with 120 additions and 19 deletions

View file

@ -10,6 +10,14 @@ Lineage: forked from [Super-Shift-S-Omarchy-Deck-Mode](https://git.no-signal.uk/
## What's New ## What's New
### v0.1.10 — Settings TUI layout polish
- Banner, state panel, menu header, and menu items now share a single centred panel column rather than each block centring itself independently. The TUI feels visibly aligned in a Walker floating window of any width — no more drifting elements off to the left while the menu floats to the right.
- Panel width is adaptive (`min(terminal 6, 60)`, floored at 40) so the layout looks right from narrow ttys up to fullscreen.
- Terminal-width detection now reads `stty size </dev/tty` first (kernel-reported, always reflects the live window) and only falls back to `tput cols` / `80`. Fixes off-centre rendering in freshly-spawned floating terminals whose terminfo hasn't caught up yet.
- Config-file path now renders with `~` instead of `/home/<user>/…` so it fits the panel.
- Unset resolution shows `<auto>` (matching the other unset placeholders) instead of `?x?`.
### v0.1.9 — Auto-migrate legacy refresh-rate values ### v0.1.9 — Auto-migrate legacy refresh-rate values
- Installer now detects pre-v0.1.8 scalar `CUSTOM_REFRESH_RATES` values (e.g. `165`) and rewrites them to the v0.1.8 comma format (`60,165`), then imports the new value into the running systemd user environment. Re-running `./deckshift.sh` is enough to fix Gaming Mode for users hit by the 60 Hz bug — no need to re-open the Settings TUI and re-pick the rate. - Installer now detects pre-v0.1.8 scalar `CUSTOM_REFRESH_RATES` values (e.g. `165`) and rewrites them to the v0.1.8 comma format (`60,165`), then imports the new value into the running systemd user environment. Re-running `./deckshift.sh` is enough to fix Gaming Mode for users hit by the 60 Hz bug — no need to re-open the Settings TUI and re-pick the rate.

View file

@ -48,6 +48,80 @@ command -v lspci >/dev/null || die "lspci is required"
command -v jq >/dev/null || die "jq is required (install with: omarchy-pkg-add jq)" command -v jq >/dev/null || die "jq is required (install with: omarchy-pkg-add jq)"
command -v hyprctl >/dev/null || die "hyprctl is required (this TUI is for Hyprland sessions)" command -v hyprctl >/dev/null || die "hyprctl is required (this TUI is for Hyprland sessions)"
# ------------------------------------------------------------------------------
# Layout — everything renders inside a single centred "panel" column so the
# banner, state, menu, and toasts all share a left edge regardless of terminal
# width. PANEL_WIDTH is adaptive: target 60 cols, capped at the terminal width
# minus margin, floored so it stays usable on narrow windows.
# ------------------------------------------------------------------------------
# Live terminal width — refreshed each main loop iteration so a window resize
# is picked up between menus. Reads via `stty size` first (kernel-reported,
# always reflects the live window) because `tput cols` in a freshly-spawned
# floating terminal sometimes returns the terminfo default (80) before the
# compositor has applied its size.
#
# Every `$(...)` here ends with `|| true` because under `set -eo pipefail` a
# failing pipeline inside a command substitution propagates out and trips -e
# on the enclosing assignment.
COLS=80
PANEL_WIDTH=60
LEFT_MARGIN=0
LEFT_PAD=""
refresh_cols() {
local raw
raw=$(stty size 2>/dev/null </dev/tty || true)
if [[ "$raw" =~ ^[0-9]+[[:space:]]+([0-9]+) ]] && (( BASH_REMATCH[1] > 0 )); then
COLS=${BASH_REMATCH[1]}
else
raw=$(tput cols 2>/dev/null || true)
if [[ "$raw" =~ ^[0-9]+$ ]] && (( raw > 0 )); then
COLS=$raw
else
COLS=80
fi
fi
PANEL_WIDTH=60
(( PANEL_WIDTH > COLS - 6 )) && PANEL_WIDTH=$(( COLS - 6 ))
(( PANEL_WIDTH < 40 )) && PANEL_WIDTH=40
LEFT_MARGIN=$(( (COLS - PANEL_WIDTH) / 2 ))
(( LEFT_MARGIN < 0 )) && LEFT_MARGIN=0
LEFT_PAD=""
(( LEFT_MARGIN > 0 )) && printf -v LEFT_PAD '%*s' "$LEFT_MARGIN" ''
}
# Prepend N spaces to every line of stdin. Used to position blocks at the
# panel's left edge so banner, state, menu, and toasts share one column.
pad_block() {
local n="${1:-$LEFT_MARGIN}"
local pad=""
(( n > 0 )) && printf -v pad '%*s' "$n" ''
local line
while IFS= read -r line; do
printf '%s%s\n' "$pad" "$line"
done
}
# Centred wrapper around gum choose: positions the menu horizontally so it
# sits inside the panel column. gum draws at column 0 with no alignment flag,
# but it reserves the cursor string's width as the gutter for unselected rows,
# so baking padding into the cursor prefix shifts the whole rendered block.
# Header is padded separately so it sits above the items at the same offset.
#
# Usage: cmenu "Header text" "Item 1" "Item 2" ...
# Cancellation (Esc/Ctrl-C) returns empty just like gchoose.
cmenu() {
refresh_cols
local header="$1"; shift
# Left-align the menu at the panel's left edge so the "> " cursor and item
# labels sit in the same column as the state panel's section headers and
# rows above. Centering items within the panel would put them in the middle
# of the panel — visually disconnected from the state panel column.
local pad="$LEFT_PAD"
gchoose --cursor "${pad}> " --header "${pad}${header}" "$@"
}
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# Conf helpers — read straight from disk; writes go through pending_* below so # Conf helpers — read straight from disk; writes go through pending_* below so
# nothing hits the file until the user explicitly saves. # nothing hits the file until the user explicitly saves.
@ -272,15 +346,25 @@ show_state() {
monitor_label="${monitor_label} (max ${HYPR_NATIVE} @ ${HYPR_MAX_REFRESH:-?}Hz)" monitor_label="${monitor_label} (max ${HYPR_NATIVE} @ ${HYPR_MAX_REFRESH:-?}Hz)"
fi fi
# Build resolution label so an unset value renders as "<auto>" (matching the
# other unset placeholders) rather than "?x?".
local resolution_label="<auto>"
if [[ -n "$width" && -n "$height" ]]; then
resolution_label="${width}x${height}"
fi
# Replace $HOME with ~ so the config path fits the panel column.
local conf_display="${CONF/#$HOME/~}"
cat <<EOF cat <<EOF
Gaming Mode display settings${pending_label}: Gaming Mode display settings${pending_label}
Monitor : ${monitor_label} Monitor : ${monitor_label}
Resolution : ${width:-?}x${height:-?} Resolution : ${resolution_label}
Refresh rate : ${refresh:-<auto>} Hz Refresh rate : ${refresh:-<auto>} Hz
GPU mode : ${gpu_mode} GPU mode : ${gpu_mode}
Config file : ${CONF} Config file : ${conf_display}
EOF EOF
} }
@ -303,7 +387,7 @@ choose_monitor() {
pending_set OUTPUT_CONNECTOR "$connector" pending_set OUTPUT_CONNECTOR "$connector"
return 0 return 0
fi fi
choice=$(printf '%s\n' "${labels[@]}" | gchoose --header "Select monitor for Gaming Mode") choice=$(cmenu "Select monitor for Gaming Mode" "${labels[@]}")
[[ -z "$choice" ]] && return 0 [[ -z "$choice" ]] && return 0
if [[ "$choice" == "(clear"* ]]; then if [[ "$choice" == "(clear"* ]]; then
pending_unset OUTPUT_CONNECTOR pending_unset OUTPUT_CONNECTOR
@ -341,7 +425,7 @@ choose_resolution() {
options+=("Custom…") options+=("Custom…")
choice=$(printf '%s\n' "${options[@]}" | gchoose --header "Select launch resolution (max: ${HYPR_NATIVE:-unknown})") choice=$(cmenu "Select launch resolution (max: ${HYPR_NATIVE:-unknown})" "${options[@]}")
[[ -z "$choice" ]] && return 0 [[ -z "$choice" ]] && return 0
if [[ "$choice" == "Custom…" ]]; then if [[ "$choice" == "Custom…" ]]; then
w=$(ginput --prompt "Width: " --placeholder "2560") w=$(ginput --prompt "Width: " --placeholder "2560")
@ -396,7 +480,7 @@ choose_refresh_rate() {
options+=("Custom…") options+=("Custom…")
choice=$(printf '%s\n' "${options[@]}" | gchoose --header "Select refresh rate (Hz, max: ${HYPR_MAX_REFRESH:-unknown})") choice=$(cmenu "Select refresh rate (Hz, max: ${HYPR_MAX_REFRESH:-unknown})" "${options[@]}")
[[ -z "$choice" ]] && return 0 [[ -z "$choice" ]] && return 0
if [[ "$choice" == "Custom…" ]]; then if [[ "$choice" == "Custom…" ]]; then
rate=$(ginput --prompt "Rate (Hz): " --placeholder "144") rate=$(ginput --prompt "Rate (Hz): " --placeholder "144")
@ -473,7 +557,7 @@ choose_gpu() {
done done
labels+=("(clear GPU override — let system decide)") labels+=("(clear GPU override — let system decide)")
choice=$(printf '%s\n' "${labels[@]}" | gchoose --header "Select GPU for Gaming Mode") choice=$(cmenu "Select GPU for Gaming Mode" "${labels[@]}")
[[ -z "$choice" ]] && return 0 [[ -z "$choice" ]] && return 0
# Wipe every GPU-mode key first; each branch sets only what it needs. # Wipe every GPU-mode key first; each branch sets only what it needs.
@ -500,7 +584,7 @@ choose_gpu() {
esac esac
done done
local t_choice t_entry="" local t_choice t_entry=""
t_choice=$(printf '%s\n' "${t_labels[@]}" | gchoose --header "Pick dGPU (render target) — usually the discrete one") t_choice=$(cmenu "Pick dGPU (render target) — usually the discrete one" "${t_labels[@]}")
[[ -z "$t_choice" ]] && return 0 [[ -z "$t_choice" ]] && return 0
local i local i
for ((i=0; i<${#t_labels[@]}; i++)); do for ((i=0; i<${#t_labels[@]}; i++)); do
@ -571,12 +655,14 @@ confirm_risky_save() {
(( ${#warnings[@]} == 0 )) && return 0 (( ${#warnings[@]} == 0 )) && return 0
refresh_cols
clear clear
gum style --foreground 196 --bold "Warning — selected values may not work:"
echo "" echo ""
printf ' %s\n' "${warnings[@]}" gum style --foreground 196 --bold "Warning — selected values may not work:" | pad_block
echo "" echo ""
gum style --foreground 244 "If Gaming Mode shows a black screen, press Super+Shift+R to return to desktop." printf ' %s\n' "${warnings[@]}" | pad_block
echo ""
gum style --foreground 244 "If Gaming Mode shows a black screen, press Super+Shift+R to return to desktop." | pad_block
echo "" echo ""
gum confirm "Save anyway?" --default=false gum confirm "Save anyway?" --default=false
} }
@ -587,12 +673,17 @@ confirm_risky_save() {
main() { main() {
while true; do while true; do
refresh_cols
refresh_monitor_data refresh_monitor_data
clear clear
echo ""
gum style \ gum style \
--border double --margin "1" --padding "1 4" --border-foreground 212 \ --border double --padding "1 0" --border-foreground 212 \
"DECKSHIFT — Gaming Mode Settings" --width "$PANEL_WIDTH" --align center \
show_state "DECKSHIFT — Gaming Mode Settings" \
| pad_block
echo ""
show_state | pad_block
echo "" echo ""
local save_label="Save and exit" local save_label="Save and exit"
@ -603,7 +694,7 @@ main() {
fi fi
local action local action
action=$(gchoose --header "What do you want to change?" \ action=$(cmenu "What do you want to change?" \
"Monitor" \ "Monitor" \
"Resolution" \ "Resolution" \
"Refresh rate" \ "Refresh rate" \
@ -621,8 +712,9 @@ main() {
fi fi
flush_pending flush_pending
clear clear
gum style --foreground 212 "Settings saved to $CONF" echo ""
gum style --foreground 244 "Changes apply next time you enter Gaming Mode (Super+Shift+S)." gum style --foreground 212 "Settings saved to ${CONF/#$HOME/~}" | pad_block
gum style --foreground 244 "Changes apply next time you enter Gaming Mode (Super+Shift+S)." | pad_block
sleep 1 sleep 1
return 0 return 0
;; ;;
@ -631,7 +723,8 @@ main() {
gum confirm "Discard unsaved changes?" --default=true || continue gum confirm "Discard unsaved changes?" --default=true || continue
fi fi
clear clear
gum style --foreground 244 "No changes saved." echo ""
gum style --foreground 244 "No changes saved." | pad_block
sleep 1 sleep 1
return 0 return 0
;; ;;

View file

@ -34,7 +34,7 @@ set -Euo pipefail
# -u: Treat unset variables as errors (catches typos in variable names) # -u: Treat unset variables as errors (catches typos in variable names)
# -o pipefail: A pipeline fails if ANY command in it fails, not just the last one # -o pipefail: A pipeline fails if ANY command in it fails, not just the last one
DECKSHIFT_VERSION="0.1.9" DECKSHIFT_VERSION="0.1.10"
# Resolve the directory this script lives in so we can find sibling files like # Resolve the directory this script lives in so we can find sibling files like
# bin/deckshift-settings and applications/deckshift-settings.desktop when # bin/deckshift-settings and applications/deckshift-settings.desktop when