From 26dcf49f414e2a6c3c0cd46e01a1c5cb289cc5f3 Mon Sep 17 00:00:00 2001 From: Joakim Date: Tue, 13 Oct 2026 12:13:00 +0200 Subject: [PATCH] N/A --- CHANGELOG.md | 11 ++++ tui/example.tui | 132 ++++++++++++++++++++++++++++++++++++++---------- tui/tui.py | 23 ++++++++- 3 files changed, 138 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 023f406..752b8df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## 3.2.0 + +- Floating menu is anchored near the current cursor item instead of a fixed screen position. +- Added configurable archive, file, folder, shell and file-information menu actions. +- Added Pack as ZIP and Pack as tar.gz. +- Added Extract archive here for common archive formats. +- Added Rename, New folder, New file, Open shell here, SHA-256 and File information. +- Added generic `archive` menu condition. +- All menu entries remain configurable in `.tui`. + + ## 3.1.0 - Added floating action menu rendered over the browser without clearing the underlying file list. diff --git a/tui/example.tui b/tui/example.tui index 07baa93..046ae5a 100644 --- a/tui/example.tui +++ b/tui/example.tui @@ -1,5 +1,4 @@ -# TUI configuration. The Python program only provides the generic UI engine; -# actions, labels, shortcuts and context-specific menu entries live here. +# TUI configuration. UI behavior, labels, shortcuts and actions belong here. keys: up: up @@ -13,7 +12,6 @@ keys: folder: enter file: - "*.zip": list-zip - - "*.md": view enter: folder: enter @@ -39,14 +37,6 @@ keys: folder: - "*": delete -# M opens this menu. The menu is entirely data-driven. -# -# selection: auto -> marked objects if there are marks, otherwise current object -# selection: current -> always current object -# selection: marked -> only when there is a marked selection -# selection: none -> action runs in the current directory, without a target -# -# when: git means this directory is inside a Git worktree. menu: - label: "View" key: v @@ -58,6 +48,13 @@ menu: action: edit when: file + - label: "List archive" + key: l + action: list-zip + when: + - file + - glob:*.zip + - separator: true - label: "Copy" @@ -75,6 +72,56 @@ menu: action: delete selection: auto + - label: "Rename" + key: r + action: rename + selection: current + + - separator: true + + - label: "Pack as ZIP" + key: z + action: pack-zip + selection: auto + + - label: "Pack as tar.gz" + key: t + action: pack-targz + selection: auto + + - label: "Extract archive here" + key: x + action: extract + when: archive + selection: current + + - separator: true + + - label: "New folder" + key: n + action: new-folder + selection: none + + - label: "New file" + key: f + action: new-file + selection: none + + - label: "Open shell here" + key: s + action: shell + selection: none + + - label: "SHA-256" + key: h + action: sha256 + selection: auto + + - label: "File information" + key: i + action: info + selection: auto + - separator: true - label: "Git: Status" @@ -88,6 +135,11 @@ menu: selection: none when: git + - label: "Git: Add -A" + action: git-add + selection: none + when: git + - label: "Git: Diff" action: git-diff selection: none @@ -98,11 +150,6 @@ menu: selection: none when: git - - label: "Git: Add -A" - action: git-add - selection: none - when: git - - label: "Git: Commit" action: git-commit selection: none @@ -120,21 +167,54 @@ actions: - read -rp "Press Enter to continue..." copy: - command: cp -v copy_ + command: cp -v -- copy_ move: - command: mv -v + command: mv -v -- delete: - command: rm -v + command: rm -v -- - edit: - command: joe + rename: + command: 'read -rp "New name for : " new_name && mv -v -- "$new_name"' view: command: less - # target: none means that the action runs once in the current TUI folder. + edit: + command: joe + + pack-zip: + command: 'zip -r ".zip" ' + + pack-targz: + command: 'tar -czvf ".tar.gz" ' + + extract: + command: + - 'case "" in *.zip) unzip -o -- -d ;; *.tar.gz|*.tgz) tar -xzvf -C ;; *.tar.bz2|*.tbz2) tar -xjvf -C ;; *.tar.xz|*.txz) tar -xJvf -C ;; *.tar) tar -xvf -C ;; *.7z) 7z x -o ;; *.rar) unrar x ;; *) echo "Unsupported archive: "; false ;; esac' + - read -rp "Press Enter to continue..." + + new-folder: + target: none + command: 'read -rp "Folder name: " name && mkdir -pv -- "$name"' + + new-file: + target: none + command: 'read -rp "File name: " name && touch -- "$name"' + + shell: + target: none + command: 'bash' + + sha256: + command: sha256sum + + info: + command: + - 'stat -- ' + - read -rp "Press Enter to continue..." + git-status: target: none command: git status @@ -143,6 +223,10 @@ actions: target: none command: git pull + git-add: + target: none + command: git add -A + git-diff: target: none command: git diff @@ -151,10 +235,6 @@ actions: target: none command: git log --oneline --decorate -20 - git-add: - target: none - command: git add -A - git-commit: target: none command: git commit diff --git a/tui/tui.py b/tui/tui.py index 31cdaf9..c1811ef 100755 --- a/tui/tui.py +++ b/tui/tui.py @@ -6,7 +6,7 @@ import os import subprocess from pathlib import Path -TUI_VERSION = "3.1.0" +TUI_VERSION = "3.2.0" SHELL_HANDOFF = 42 TUI_EXIT = 43 @@ -146,6 +146,14 @@ class TuiConfig: if condition == "folder": target = browser.selected() return bool(target and target.is_dir()) + if condition == "archive": + target = browser.selected() + if not target or not target.is_file(): + return False + return target.suffix.lower() in { + ".zip", ".tar", ".gz", ".tgz", ".bz2", ".tbz2", + ".xz", ".txz", ".7z", ".rar", + } or target.name.lower().endswith((".tar.gz", ".tar.bz2", ".tar.xz")) if condition == "marked": return bool(browser.marked) if condition == "multiple": @@ -579,7 +587,18 @@ def show_menu(stdscr, browser): menu_top = max(1, cursor_screen_row - menu_rows - 1) if menu_top + menu_rows > height - 1: menu_top = max(1, height - 1 - menu_rows) - menu_left = max(1, min((width - menu_width) // 2, width - menu_width - 1)) + # Anchor the floating menu near the current item rather than at a + # fixed screen position. The current row remains visible because the + # menu starts after the item's text when there is enough room. + if browser.cursor == 0: + cursor_text = "../" if browser.path != browser.root else "./" + elif 1 <= browser.cursor <= len(browser.entries): + current = browser.entries[browser.cursor - 1] + cursor_text = current.name + ("/" if current.is_dir() else "") + else: + cursor_text = "" + preferred_left = min(max(2, len(cursor_text) + 3), max(2, width - menu_width - 2)) + menu_left = max(1, min(preferred_left, width - menu_width - 1)) win = curses.newwin(menu_rows, menu_width, menu_top, menu_left) win.keypad(True)