From d63f433296f7a2a413bf0ff8e3395a56625697e6 Mon Sep 17 00:00:00 2001 From: lou Date: Mon, 10 Aug 2026 00:02:17 +0800 Subject: [PATCH] =?UTF-8?q?=E8=BE=93=E5=85=A5=E4=BF=AE=E5=A4=8D:=20useInpu?= =?UTF-8?q?t=20=E4=BA=8B=E4=BB=B6=E5=AF=B9=E8=B1=A1/isActive=20=E6=9A=82?= =?UTF-8?q?=E5=81=9C=E8=BE=93=E5=85=A5/ask.py=20=E7=BB=9D=E5=AF=B9?= =?UTF-8?q?=E8=B7=AF=E5=BE=84=20+=20Python=20TUI=20=E8=BF=87=E6=B8=A1?= =?UTF-8?q?=E6=96=B9=E6=A1=88=20(=E5=85=A8=E6=B5=81=E7=A8=8B=E5=AE=9E?= =?UTF-8?q?=E6=B5=8B=E9=80=9A=E8=BF=87)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/ask.py | 33 +++++++++---- scripts/tui.py | 113 +++++++++++++++++++++++++++++++++++++++++++ src/app.vue | 26 ++++++---- src/lib/pipeline.ts | 64 +++++++++++++++++------- src/views/config.vue | 12 +++-- src/views/tasks.vue | 12 +++-- src/views/upload.vue | 22 ++++++--- 7 files changed, 231 insertions(+), 51 deletions(-) create mode 100644 scripts/tui.py diff --git a/scripts/ask.py b/scripts/ask.py index 1444db7..b94b318 100644 --- a/scripts/ask.py +++ b/scripts/ask.py @@ -1,21 +1,34 @@ """TUI 辅助: 中文输入收集 vue-tui 的 useInput 在 raw mode 下不支持 IME, 中文输入必须走 Python cooked-mode input()。 -用法: python ask.py <提示词> [默认值] -> 最后一行输出 JSON {"answer": "..."} +调用方 (TUI) 会先把 vue-tui 输入监听暂停 (isActive=false), 本脚本独占 tty: + - 终端从 raw 临时切回 cooked+echo 供 input()/IME 使用, 结束后恢复 + - prompt 写 stderr (继承终端直接显示), 答案 JSON 写 stdout (管道回传 TUI) +用法: python ask.py <提示词> [默认值] -> stdout 输出 JSON {"answer": "..."} """ import json import sys +import termios -prompt = sys.argv[1] if len(sys.argv) > 1 else "输入" -default = sys.argv[2] if len(sys.argv) > 2 else "" -sys.stdout.write(f"{prompt} [{default}]: ") -sys.stdout.flush() +fd = sys.stdin.fileno() +old = termios.tcgetattr(fd) +new = termios.tcgetattr(fd) +new[3] |= termios.ICANON | termios.ECHO # 开行缓冲 + 回显 (cooked mode) +termios.tcsetattr(fd, termios.TCSANOW, new) try: - answer = input() -except EOFError: - answer = "" -if not answer: - answer = default + prompt = sys.argv[1] if len(sys.argv) > 1 else "输入" + default = sys.argv[2] if len(sys.argv) > 2 else "" + # prompt 走 stderr: 继承终端直接显示 (stdout 管道是给 TUI 解析 JSON 的) + sys.stderr.write(f"{prompt} [{default}]: ") + sys.stderr.flush() + try: + answer = sys.stdin.readline().rstrip("\n") + except EOFError: + answer = "" + if not answer: + answer = default +finally: + termios.tcsetattr(fd, termios.TCSANOW, old) print() print(json.dumps({"answer": answer}, ensure_ascii=False)) diff --git a/scripts/tui.py b/scripts/tui.py new file mode 100644 index 0000000..7637bd2 --- /dev/null +++ b/scripts/tui.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python3 +"""7z-encrypt 客户端 TUI (Python input() REPL, 中文 IME 友好) + +vue-tui 版输入竞争问题修复前的过渡方案 (mt-translate 同款模式: +vue-tui 的 raw mode 输入无法配合 IME, Python cooked-mode input() 可以)。 + +流程: 直接驱动 7z-encrypt 客户端模块 CLI, 实时透传进度输出。 +""" + +import os +import subprocess +import sys + +CLIENT_DIR = "/home/lou/文档/7z-encrypt" +PYTHON = os.path.join(CLIENT_DIR, ".venv", "bin", "python") + + +def run(args: list[str]) -> int: + """子进程运行 7z-encrypt CLI, 实时透传输出 (进度可见)""" + proc = subprocess.Popen( + [PYTHON] + args, + cwd=CLIENT_DIR, + text=True, + encoding="utf-8", + ) + try: + return proc.wait() + except KeyboardInterrupt: + proc.kill() + print("\n[已中断]") + return 130 + + +def upload() -> None: + print("--- 上传文件 ---") + path = input("文件路径: ").strip() + if not path or not os.path.exists(path): + print(f"[错误] 文件不存在: {path}") + return + server = ( + input("服务器 [http://127.0.0.1:8000]: ").strip() + or "http://127.0.0.1:8000" + ) + chunk_size = input("卷大小 MB [10]: ").strip() or "10" + + print("\n[1/2] 加密 + 分卷 + 元数据 ...") + code = run(["metadata.py", path, "--chunk-size", chunk_size]) + if code != 0: + print("[失败] 加密/分卷出错") + return + + init_json = path + ".init.json" + manifest = path + "_manifest.json" + if not (os.path.exists(init_json) and os.path.exists(manifest)): + print("[失败] 产物缺失 (init.json / manifest)") + return + + print("\n[2/2] 上传 ...") + code = run(["transfer.py", "--server", server, "--init", init_json, "--manifest", manifest]) + print("[完成]" if code == 0 else "[失败] 上传出错") + + +def tasks() -> None: + print("--- 任务续传 ---") + run(["state.py", "pending"]) + + +def config_show() -> None: + print("--- 配置 ---") + run(["config.py"]) + + +def config_edit() -> None: + url = input("服务器地址: ").strip() + if not url: + return + run(["config.py", "--set-server", url]) + config_show() + + +def main() -> int: + print("7z-encrypt 客户端 (Python TUI)") + print("安全文件传输 · 加密 / 分卷 / 上传") + while True: + print() + print("主菜单") + print(" 1. 上传文件") + print(" 2. 任务续传") + print(" 3. 配置") + print(" q. 退出") + choice = input("> ").strip().lower() + if choice == "1": + upload() + elif choice == "2": + tasks() + elif choice == "3": + config_show() + edit = input("修改服务器地址? (y/N): ").strip().lower() + if edit == "y": + config_edit() + elif choice == "q": + print("再见") + return 0 + else: + print("无效输入") + + +if __name__ == "__main__": + try: + sys.exit(main()) + except KeyboardInterrupt: + print("\n再见") + sys.exit(130) diff --git a/src/app.vue b/src/app.vue index 0cd742b..66e6d0f 100644 --- a/src/app.vue +++ b/src/app.vue @@ -4,20 +4,26 @@ import { Box, Text, useInput } from "@vue-tui/runtime"; import Upload from "./views/upload.vue"; import Tasks from "./views/tasks.vue"; import Config from "./views/config.vue"; +import { inputEnabled } from "./lib/pipeline"; type View = "menu" | "upload" | "tasks" | "config"; const view = shallowRef("menu"); -useInput((input) => { - if (view.value !== "menu") { - if (input === "q" || input === "\u001b") view.value = "menu"; - return; - } - if (input === "1") view.value = "upload"; - else if (input === "2") view.value = "tasks"; - else if (input === "3") view.value = "config"; - else if (input === "q") process.exit(0); -}); +useInput( + (event) => { + // 事件对象: {type:"text",text} 或 {type:"key",key:{name}} + const t = event.type === "text" ? event.text : event.type === "key" ? event.key.name : ""; + if (view.value !== "menu") { + if (t === "q" || t === "escape") view.value = "menu"; + return; + } + if (t === "1") view.value = "upload"; + else if (t === "2") view.value = "tasks"; + else if (t === "3") view.value = "config"; + else if (t === "q") process.exit(0); + }, + { isActive: inputEnabled }, +);