aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDon Williams <don.e.williams@gmail.com>2026-01-15 02:25:36 -0500
committerDon Williams <don.e.williams@gmail.com>2026-01-15 02:25:36 -0500
commit122f607047b16488c95ea25e842084d88b394e1d (patch)
tree9b66d5620293ef77ee58342b34acddb8c0afe752
parentb8c823fbab99f857c933e9b45609ab21cee55f31 (diff)
Removed python script didn't work
On branch development Your branch is up to date with 'origin/development'. Changes to be committed: deleted: copy-menu-issue.txt modified: scripts/copy_menu.sh deleted: scripts/tui_menu.py
-rw-r--r--copy-menu-issue.txt52
-rwxr-xr-xscripts/copy_menu.sh73
-rwxr-xr-xscripts/tui_menu.py340
3 files changed, 36 insertions, 429 deletions
diff --git a/copy-menu-issue.txt b/copy-menu-issue.txt
deleted file mode 100644
index 94672040..00000000
--- a/copy-menu-issue.txt
+++ /dev/null
@@ -1,52 +0,0 @@
-Summary: copy.sh menu issues on Debian host
-
-Symptoms
-- Running copy.sh shows a black screen with a cursor; no visible menu.
-- Running scripts/tui_menu.py directly renders an ncurses menu, but Express appears disabled.
-- The host has ~/.config/hypr/v2.3.18 present.
-
-Changes already made (in repo)
-- Added Python TUI (scripts/tui_menu.py) with backends: Textual -> curses -> basic; mouse + keyboard; hotkeys; Help panel.
-- Updated scripts/copy_menu.sh to prefer Python TUI and to accept COPY_TUI_BACKEND=auto|textual|curses|basic, passing --express-supported accordingly.
-- Hardened curses drawing to avoid blank screens (use_default_colors, safer bounds).
-- Fixed Express detection in copy.sh to pick the highest version marker (sort -V) under ~/.config/hypr (vX.Y.Z).
-
-Why Express shows disabled when running the TUI directly
-- scripts/tui_menu.py defaults to --express-supported=0 unless told otherwise. When launched by copy.sh, it is passed the correct flag. Running it manually without the flag will show Express as disabled.
-
-Immediate workaround on the Debian host
-1) Ensure you have the latest repo changes
- git --no-pager status
- git pull --ff-only
-
-2) Force the curses backend to avoid the black Textual screen
- # one-shot
- COPY_TUI_BACKEND=curses ./copy.sh
- # or set permanently for the session
- export COPY_TUI_BACKEND=curses
- ./copy.sh
-
-3) If curses ever fails to render, use the basic fallback
- COPY_TUI_BACKEND=basic ./copy.sh
-
-4) To test the TUI directly with Express allowed
- python3 scripts/tui_menu.py --express-supported 1 --backend curses
-
-Verify Express detection on the host
-- Confirm the installed version marker and what copy.sh will see:
- find "$HOME/.config/hypr" -maxdepth 1 -type f -name 'v*.*.*' -printf '%f\n' | sed 's/^v//' | sort -V | tail -n1
-- Expected output: 2.3.18 (or newer). If empty, create the marker:
- : > "$HOME/.config/hypr/v2.3.18"
-
-If black screen persists with copy.sh
-- Dump debug logs for the menu selection phase:
- set -x; COPY_TUI_BACKEND=curses bash -c 'source scripts/copy_menu.sh; show_copy_menu 1; echo "CHOICE=$COPY_MENU_CHOICE"'; set +x
-- Check TERM and Python TUI dependency situations:
- echo "$TERM"
- command -v python3 || true
- python3 -c "import curses; print('curses OK')" || true
-
-Notes for future session
-- copy_menu.sh already honors COPY_TUI_BACKEND and falls back to whiptail/text if Python fails.
-- copy.sh now uses highest version marker to enable Express; the marker must be a file named exactly vX.Y.Z under ~/.config/hypr.
-- If this host lacks Textual (rich) and shows black screen in Textual, forcing curses via COPY_TUI_BACKEND=curses is the recommended run mode on Debian for now.
diff --git a/scripts/copy_menu.sh b/scripts/copy_menu.sh
index 258c2fae..87f9301f 100755
--- a/scripts/copy_menu.sh
+++ b/scripts/copy_menu.sh
@@ -22,47 +22,17 @@ show_copy_menu() {
local quit_desc="Exit without changes"
local choice=""
-
- # Prefer Python TUI if available (mouse + keyboard, styled hotkeys, help)
- # Determine repo dir robustly: prefer SCRIPT_DIR if set by caller (copy.sh); fallback to this file's parent
- local __self_dir __repo_dir
- __self_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
- __repo_dir="${SCRIPT_DIR:-$(cd "${__self_dir}/.." 2>/dev/null && pwd)}"
- local py_menu="${__repo_dir}/scripts/tui_menu.py"
-if command -v python3 >/dev/null 2>&1 && [ -f "$py_menu" ]; then
- # Allow forcing backend via COPY_TUI_BACKEND=auto|textual|curses|basic
- if [ -n "$COPY_TUI_BACKEND" ]; then
- if choice=$(python3 "$py_menu" --express-supported "$express_supported" --backend "$COPY_TUI_BACKEND"); then
- COPY_MENU_CHOICE="$choice"
- return 0
- fi
- else
- if choice=$(python3 "$py_menu" --express-supported "$express_supported"); then
- COPY_MENU_CHOICE="$choice"
- return 0
- fi
- fi
- fi
-
- # Fallback to whiptail if present
- if command -v whiptail >/dev/null 2>&1; then
- if ! choice=$(whiptail --title "$menu_title" --menu "$prompt" 17 60 8 \
- "$install_tag" "$install_desc" \
- "$upgrade_tag" "$upgrade_desc" \
- "$express_tag" "$express_desc" \
- "$update_tag" "$update_desc" \
- "$quit_tag" "$quit_desc" 3>&1 1>&2 2>&3); then
- COPY_MENU_CHOICE="quit"
- return 1
- fi
- else
- # Plain-text fallback
+ run_basic_menu() {
while true; do
printf "\n%s\n" "$menu_title"
printf "%s\n" "$prompt"
printf " 1) Install - %s\n" "$install_desc"
printf " 2) Upgrade - %s\n" "$upgrade_desc"
- printf " 3) Express - %s\n" "$express_desc"
+ if [ "$express_supported" -eq 1 ]; then
+ printf " 3) Express - %s\n" "$express_desc"
+ else
+ printf " 3) Express - %s (disabled)\n" "$express_desc"
+ fi
printf " 4) Update - %s\n" "$update_desc"
printf " 5) Quit - %s\n" "$quit_desc"
printf "Enter choice [1-5]: "
@@ -70,12 +40,41 @@ if command -v python3 >/dev/null 2>&1 && [ -f "$py_menu" ]; then
case "$text_choice" in
1) choice="$install_tag"; break ;;
2) choice="$upgrade_tag"; break ;;
- 3) choice="$express_tag"; break ;;
+ 3)
+ if [ "$express_supported" -eq 1 ]; then
+ choice="$express_tag"
+ break
+ else
+ echo "Express is disabled on this system."
+ fi
+ ;;
4) choice="$update_tag"; break ;;
5) choice="$quit_tag"; break ;;
*) echo "Invalid selection. Please choose 1-5." ;;
esac
done
+ }
+
+ if [ "$COPY_TUI_BACKEND" = "basic" ]; then
+ run_basic_menu
+ COPY_MENU_CHOICE="$choice"
+ return 0
+ fi
+
+ # Fallback to whiptail if present
+ if command -v whiptail >/dev/null 2>&1; then
+ if ! choice=$(whiptail --title "$menu_title" --menu "$prompt" 17 60 8 \
+ "$install_tag" "$install_desc" \
+ "$upgrade_tag" "$upgrade_desc" \
+ "$express_tag" "$express_desc" \
+ "$update_tag" "$update_desc" \
+ "$quit_tag" "$quit_desc" 3>&1 1>&2 2>&3); then
+ COPY_MENU_CHOICE="quit"
+ return 1
+ fi
+ else
+ # Plain-text fallback
+ run_basic_menu
fi
# shellcheck disable=SC2034 # used by parent script after sourcing this file
diff --git a/scripts/tui_menu.py b/scripts/tui_menu.py
deleted file mode 100755
index d57ba1ac..00000000
--- a/scripts/tui_menu.py
+++ /dev/null
@@ -1,340 +0,0 @@
-#!/usr/bin/env python3
-# Simple TUI menu for Hyprland-Dots copy workflow
-# - Prefers Textual (rich) for a nicer UI with mouse + keyboard
-# - Falls back to curses if Textual is unavailable
-# - Prints the chosen action (Install|Upgrade|Express|Update|Quit) to stdout
-
-from __future__ import annotations
-
-import argparse
-import os
-import sys
-
-CHOICES = [
- ("Install", "Fresh copy of the dotfiles into ~/.config"),
- ("Upgrade", "Backups + interactive prompts"),
- ("Express", "Skips restore prompts and large wallpaper download"),
- ("Update", "Update this repo: stash local changes, git pull"),
- ("Help", "Explain the options shown here"),
- ("Quit", "Exit without making changes"),
-]
-
-HELP_TEXT = (
- "Install: Perform a fresh copy of configs into ~/.config.\n"
- "Upgrade: Back up existing configs and prompt to restore what you want.\n"
- "Express: Faster upgrade (requires installed dots >= the minimum version).\n"
- "Update: Safely update this Git repo (stash local changes, then git pull).\n"
- "Quit: Exit without making changes.\n\n"
- "Tips:\n"
- "- Use Up/Down or mouse to select, Enter to confirm.\n"
- "- Press the highlighted first letter (I/U/E/U/Q/H) as a shortcut.\n"
- "- Press h or ? at any time to view this help.\n"
-)
-
-
-def main() -> int:
- parser = argparse.ArgumentParser()
- parser.add_argument("--express-supported", default="0", choices=["0", "1"], help="Whether Express is allowed (1) or not (0)")
- parser.add_argument("--backend", default=os.environ.get("COPY_TUI_BACKEND", "auto"), choices=["auto", "textual", "curses", "basic"], help="Choose UI backend")
- args = parser.parse_args()
- express_supported = args.express_supported == "1"
- backend = args.backend
-
- if backend == "textual":
- return run_textual(express_supported)
- if backend == "curses":
- return run_curses(express_supported)
- if backend == "basic":
- return run_basic(express_supported)
-
- # auto: Try Textual, then curses, then basic
- try:
- return run_textual(express_supported)
- except Exception:
- pass
- try:
- return run_curses(express_supported)
- except Exception:
- return run_basic(express_supported)
-
-
-def stylize_first_letter(label: str) -> tuple[str, str]:
- # returns (styled_label_for_ui, hotkey)
- hotkey = label[0].upper()
- rest = label[1:]
- # Textual Rich markup: bold + accent color for the first letter
- styled = f"[b][cyan]{hotkey}[/cyan][/b]{rest}"
- return styled, hotkey
-
-
-def run_textual(express_supported: bool) -> int:
- from textual.app import App, ComposeResult
- from textual.widgets import Header, Footer, Static, Button
- from textual.containers import Vertical
- from textual.reactive import reactive
- from textual import events
-
- class MenuButton(Button):
- def __init__(self, label: str, choice_key: str, disabled: bool = False) -> None:
- super().__init__(label, disabled=disabled)
- self.choice_key = choice_key
-
- class MenuApp(App):
- CSS = """
- Screen { background: black; }
- #title { content-align: center middle; height: 3; color: white; }
- Vertical { width: 80; max-width: 90; margin: 1 auto; }
- Button { margin: 1 0; padding: 1 2; border: round cornflowerblue; }
- Button:hover { background: rgba(100,100,255,0.1); }
- Button.-disabled { color: grey50; border: round grey35; }
- #help { padding: 1 2; border: round grey42; height: auto; }
- """
-
- selected: reactive[str | None] = reactive(None)
-
- def compose(self) -> ComposeResult: # type: ignore[override]
- yield Header(show_clock=False)
- yield Static("KooL's Hyprland Dotfiles", id="title")
- with Vertical():
- # Build buttons
- for (name, _desc) in CHOICES:
- if name == "Express" and not express_supported:
- styled, _hk = stylize_first_letter(name)
- yield MenuButton(f"{styled} [grey62](requires newer installed dots)\n[/grey62]", name, disabled=True)
- else:
- styled, _hk = stylize_first_letter(name)
- yield MenuButton(styled, name)
- yield Static("Press h or ? for help", id="help")
- yield Footer()
-
- def on_button_pressed(self, event: Button.Pressed) -> None: # type: ignore[override]
- btn = event.button
- if isinstance(btn, MenuButton):
- if btn.choice_key == "Help":
- self.show_help()
- return
- if btn.choice_key == "Express" and not express_supported:
- return
- self.selected = btn.choice_key
- self.exit_app()
-
- def action_quit(self) -> None: # Esc
- self.exit_app("Quit")
-
- BINDINGS = [
- ("escape", "quit", "Quit"),
- ("i", "select('Install')", "Install"),
- ("u", "select('Upgrade')", "Upgrade"),
- ("e", "select('Express')", "Express"),
- ("d", "select('Update')", "Update"),
- ("q", "select('Quit')", "Quit"),
- ("h", "help", "Help"),
- ("?", "help", "Help"),
- ("enter", "activate", "Select"),
- ]
-
- def action_select(self, name: str) -> None:
- if name == "Express" and not express_supported:
- return
- if name == "Help":
- self.show_help()
- return
- self.selected = name
- self.exit_app()
-
- def action_help(self) -> None:
- self.show_help()
-
- def action_activate(self) -> None:
- # Activate focused button
- focused = self.focused
- if isinstance(focused, MenuButton):
- self.on_button_pressed(Button.Pressed(focused))
-
- def show_help(self) -> None:
- self.push_screen(HelpScreen())
-
- def exit_app(self, fallback: str | None = None) -> None:
- result = self.selected or fallback
- if result:
- print(result)
- self.exit(0)
-
- from textual.screen import ModalScreen
-
- class HelpScreen(ModalScreen[None]):
- def compose(self) -> ComposeResult: # type: ignore[override]
- yield Static("[b]Help[/b]\n\n" + HELP_TEXT, id="help")
-
- BINDINGS = [("escape", "dismiss", "Close"), ("q", "dismiss", "Close")]
-
- def action_dismiss(self) -> None:
- self.dismiss(None)
-
- app = MenuApp()
- app.run()
- return 0
-
-
-def run_curses(express_supported: bool) -> int:
- import curses
-
- labels = [name for (name, _d) in CHOICES]
-
- def draw_menu(stdscr, idx: int, show_help: bool) -> None:
- stdscr.clear()
- h, w = stdscr.getmaxyx()
- title = "KooL's Hyprland Dotfiles"
- stdscr.attron(curses.A_BOLD)
- stdscr.addstr(1, max(2, (w - len(title)) // 2), title)
- stdscr.attroff(curses.A_BOLD)
-
- y = 4
- for i, name in enumerate(labels):
- disabled = (name == "Express" and not express_supported)
- hk = name[0].upper()
- rest = name[1:]
- if i == idx:
- stdscr.attron(curses.A_REVERSE)
- # First letter styled
- stdscr.attron(curses.color_pair(1) | curses.A_BOLD)
- stdscr.addstr(y, 4, hk)
- stdscr.attroff(curses.color_pair(1) | curses.A_BOLD)
- if disabled:
- stdscr.attron(curses.A_DIM)
- stdscr.addstr(y, 5, rest)
- if disabled:
- msg = " (requires newer installed dots)"
- stdscr.addstr(y, 5 + len(rest), msg, curses.A_DIM)
- stdscr.attroff(curses.A_DIM)
- if i == idx:
- stdscr.attroff(curses.A_REVERSE)
- y += 2
-
- info = "Enter=Select Up/Down=Move Mouse=Click h/?=Help q=Quit"
- stdscr.addstr(h - 2, 2, info[: max(0, w - 4)])
-
- if show_help:
- box_w = min(w - 6, 76)
- box_h = min(12, h - 6)
- bx = max(2, (w - box_w) // 2)
- by = max(2, (h - box_h) // 2)
- for yy in range(by, by + box_h):
- stdscr.addstr(yy, bx, " " * max(0, box_w), curses.color_pair(3))
- stdscr.attron(curses.A_BOLD)
- stdscr.addstr(by, bx + 2, "Help")
- stdscr.attroff(curses.A_BOLD)
- for i, line in enumerate(HELP_TEXT.splitlines()[: max(0, box_h - 3)]):
- stdscr.addstr(by + 2 + i, bx + 2, line[: max(0, box_w - 4)])
- stdscr.addstr(by + box_h - 2, bx + 2, "Press q or Esc to close help")
-
- stdscr.refresh()
-
- def loop(stdscr) -> str:
- curses.curs_set(0)
- curses.mousemask(1)
- curses.start_color()
- try:
- curses.use_default_colors()
- except Exception:
- pass
- try:
- curses.init_pair(1, curses.COLOR_CYAN, -1)
- except Exception:
- curses.init_pair(1, curses.COLOR_CYAN, 0)
- try:
- curses.init_pair(3, curses.COLOR_WHITE, curses.COLOR_BLUE)
- except Exception:
- curses.init_pair(3, curses.COLOR_WHITE, 0)
- idx = 0
- showing_help = False
-
- while True:
- draw_menu(stdscr, idx, showing_help)
- ch = stdscr.getch()
- if showing_help:
- if ch in (ord('q'), ord('Q'), 27):
- showing_help = False
- continue
-
- if ch in (curses.KEY_UP, ord('k')):
- idx = (idx - 1) % len(labels)
- elif ch in (curses.KEY_DOWN, ord('j')):
- idx = (idx + 1) % len(labels)
- elif ch in (ord('h'), ord('?')):
- showing_help = True
- elif ch in (ord('q'), 27):
- return "Quit"
- elif ch == curses.KEY_MOUSE:
- try:
- _, mx, my, _, _ = curses.getmouse()
- base_y = 4
- if my >= base_y:
- clicked = (my - base_y) // 2
- if 0 <= clicked < len(labels):
- name = labels[clicked]
- if name == "Help":
- showing_help = True
- elif name == "Express" and not express_supported:
- pass
- else:
- return name
- except Exception:
- pass
- elif ch in (curses.KEY_ENTER, 10, 13):
- name = labels[idx]
- if name == "Help":
- showing_help = True
- elif name == "Express" and not express_supported:
- pass
- else:
- return name
- else:
- try:
- key = chr(ch).lower() if 0 <= ch < 256 else ""
- except Exception:
- key = ""
- mapping = {"i": "Install", "u": "Upgrade", "e": "Express", "d": "Update", "q": "Quit", "h": "Help"}
- if key in mapping:
- name = mapping[key]
- if name == "Help":
- showing_help = True
- elif name == "Express" and not express_supported:
- pass
- else:
- return name
-
- choice = curses.wrapper(loop)
- print(choice)
- return 0
-
-
-def run_basic(express_supported: bool) -> int:
- # Minimal stdin-only fallback
- options = [n for (n, _d) in CHOICES]
- while True:
- print("Select:")
- for i, name in enumerate(options, 1):
- if name == "Express" and not express_supported:
- print(f" {i}) {name} (disabled)")
- else:
- print(f" {i}) {name}")
- try:
- sel = input("> ").strip()
- except EOFError:
- print("Quit")
- return 0
- if sel.isdigit():
- idx = int(sel) - 1
- if 0 <= idx < len(options):
- choice = options[idx]
- if choice == "Express" and not express_supported:
- continue
- if choice == "Help":
- print(HELP_TEXT)
- continue
- print(choice)
- return 0
-
-if __name__ == "__main__":
- sys.exit(main())
send patches to the email below
yukais@pinapelz.com
include the subject [PATCH repo_name]
pinapelz.com
homepage