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 }, +);