diff options
| author | Don Williams <don.e.williams@gmail.com> | 2026-01-15 02:25:36 -0500 |
|---|---|---|
| committer | Don Williams <don.e.williams@gmail.com> | 2026-01-15 02:25:36 -0500 |
| commit | 122f607047b16488c95ea25e842084d88b394e1d (patch) | |
| tree | 9b66d5620293ef77ee58342b34acddb8c0afe752 | |
| parent | b8c823fbab99f857c933e9b45609ab21cee55f31 (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.txt | 52 | ||||
| -rwxr-xr-x | scripts/copy_menu.sh | 73 | ||||
| -rwxr-xr-x | scripts/tui_menu.py | 340 |
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()) |
