输入修复: useInput 事件对象/isActive 暂停输入/ask.py 绝对路径 + Python TUI 过渡方案 (全流程实测通过)
This commit is contained in:
+23
-10
@@ -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))
|
||||
|
||||
+113
@@ -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)
|
||||
+16
-10
@@ -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<View>("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 },
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
+47
-17
@@ -1,8 +1,15 @@
|
||||
// 与 7z-encrypt Python 管线之间的桥 (spawn 子进程, 逐行回传输出)
|
||||
import { spawn } from "node:child_process";
|
||||
import { ref } from "vue";
|
||||
|
||||
const CLIENT_DIR = "/home/lou/文档/7z-encrypt";
|
||||
const PYTHON = `${CLIENT_DIR}/.venv/bin/python`;
|
||||
// ask.py 是 tui 项目的脚本, 必须绝对路径 (runPython 的 cwd 是 7z-encrypt)
|
||||
const ASK_SCRIPT = "/home/lou/文档/tui/scripts/ask.py";
|
||||
|
||||
// 输入暂停开关: ask 期间置 false, 各 useInput 传 isActive 引用 ->
|
||||
// vue-tui 释放 stdin 监听, ask.py 从 tty 读输入无竞争
|
||||
export const inputEnabled = ref(true);
|
||||
|
||||
export interface RunResult {
|
||||
code: number;
|
||||
@@ -13,36 +20,59 @@ export interface RunResult {
|
||||
export function runPython(
|
||||
args: string[],
|
||||
onLine?: (line: string) => void,
|
||||
spawnOptions: { stdio?: ("inherit" | "pipe" | "ignore")[] } = {},
|
||||
): Promise<RunResult> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn(PYTHON, args, {
|
||||
cwd: CLIENT_DIR,
|
||||
env: { ...process.env, PYTHONIOENCODING: "utf-8" },
|
||||
stdio: spawnOptions.stdio,
|
||||
});
|
||||
const lines: string[] = [];
|
||||
child.stdout.on("data", (buf: Buffer) => {
|
||||
for (const line of buf.toString().split("\n")) {
|
||||
const l = line.replace(/\r$/, "");
|
||||
if (!l) continue;
|
||||
lines.push(l);
|
||||
onLine?.(l);
|
||||
}
|
||||
});
|
||||
child.stderr.on("data", (buf: Buffer) => {
|
||||
for (const line of buf.toString().split("\n")) {
|
||||
const l = line.trim();
|
||||
if (!l) continue;
|
||||
lines.push(`[stderr] ${l}`);
|
||||
}
|
||||
});
|
||||
const out = child.stdout;
|
||||
if (out) {
|
||||
out.on("data", (buf: Buffer) => {
|
||||
for (const line of buf.toString().split("\n")) {
|
||||
const l = line.replace(/\r$/, "");
|
||||
if (!l) continue;
|
||||
lines.push(l);
|
||||
onLine?.(l);
|
||||
}
|
||||
});
|
||||
}
|
||||
const err = child.stderr;
|
||||
if (err) {
|
||||
err.on("data", (buf: Buffer) => {
|
||||
for (const line of buf.toString().split("\n")) {
|
||||
const l = line.trim();
|
||||
if (!l) continue;
|
||||
lines.push(`[stderr] ${l}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
child.on("close", (code) => resolve({ code: code ?? -1, lines }));
|
||||
child.on("error", reject);
|
||||
});
|
||||
}
|
||||
|
||||
/** 中文输入 (Python cooked-mode input, 支持 IME) */
|
||||
/** 中文输入 (stdin 忽略 + ask.py 自开 /dev/tty, 支持 IME) */
|
||||
export async function ask(prompt: string, fallback = ""): Promise<string> {
|
||||
const r = await runPython(["scripts/ask.py", prompt, fallback]);
|
||||
let r: RunResult;
|
||||
inputEnabled.value = false; // 暂停 vue-tui 输入, 让 ask.py 独占 tty
|
||||
try {
|
||||
r = await runPython(
|
||||
[ASK_SCRIPT, prompt, fallback],
|
||||
undefined,
|
||||
// stdin 继承终端 (ask.py input() 直接读 tty, 此时 vue-tui 已释放监听);
|
||||
// stderr 继承终端 (prompt 直接显示); stdout 管道回传 JSON 答案
|
||||
{ stdio: ["inherit", "pipe", "inherit"] },
|
||||
);
|
||||
} catch (e) {
|
||||
process.stderr.write(`[ask-error] ${String(e)}\n`);
|
||||
inputEnabled.value = true;
|
||||
return "";
|
||||
}
|
||||
inputEnabled.value = true; // 恢复 vue-tui 输入
|
||||
for (const line of [...r.lines].reverse()) {
|
||||
try {
|
||||
const obj = JSON.parse(line) as { answer: string };
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { shallowRef, onMounted } from "vue";
|
||||
import { Box, Text, useInput } from "@vue-tui/runtime";
|
||||
import { runPython, ask } from "../lib/pipeline";
|
||||
import { runPython, ask, inputEnabled } from "../lib/pipeline";
|
||||
|
||||
const content = shallowRef("(读取中)");
|
||||
|
||||
@@ -21,9 +21,13 @@ async function edit() {
|
||||
|
||||
onMounted(() => void show());
|
||||
|
||||
useInput((input) => {
|
||||
if (input === "e") void edit();
|
||||
});
|
||||
useInput(
|
||||
(event) => {
|
||||
const t = event.type === "text" ? event.text : "";
|
||||
if (t === "e") void edit();
|
||||
},
|
||||
{ isActive: inputEnabled },
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
+8
-4
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { shallowRef, onMounted } from "vue";
|
||||
import { Box, Text, useInput } from "@vue-tui/runtime";
|
||||
import { runPython } from "../lib/pipeline";
|
||||
import { runPython, inputEnabled } from "../lib/pipeline";
|
||||
|
||||
const lines = shallowRef<string[]>([]);
|
||||
const message = shallowRef("查询中...");
|
||||
@@ -15,9 +15,13 @@ async function refresh() {
|
||||
|
||||
onMounted(() => void refresh());
|
||||
|
||||
useInput((input) => {
|
||||
if (input === "r") void refresh();
|
||||
});
|
||||
useInput(
|
||||
(event) => {
|
||||
const t = event.type === "text" ? event.text : event.type === "key" ? event.key.name : "";
|
||||
if (t === "r") void refresh();
|
||||
},
|
||||
{ isActive: inputEnabled },
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
+16
-6
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { shallowRef } from "vue";
|
||||
import { Box, Text, useInput } from "@vue-tui/runtime";
|
||||
import { ask, deriveArtifacts, parseProgress, runPython } from "../lib/pipeline";
|
||||
import { ask, deriveArtifacts, inputEnabled, parseProgress, runPython } from "../lib/pipeline";
|
||||
|
||||
type Phase =
|
||||
| "idle"
|
||||
@@ -25,14 +25,24 @@ const pushLog = (line: string) => {
|
||||
log.value = next;
|
||||
};
|
||||
|
||||
useInput((input) => {
|
||||
if (input !== "1" || phase.value !== "idle") return;
|
||||
void startFlow();
|
||||
});
|
||||
useInput(
|
||||
(event) => {
|
||||
const t = event.type === "text" ? event.text : "";
|
||||
if (t !== "1" || phase.value !== "idle") return;
|
||||
void startFlow();
|
||||
},
|
||||
{ isActive: inputEnabled },
|
||||
);
|
||||
|
||||
async function startFlow() {
|
||||
phase.value = "ask_file";
|
||||
filePath.value = await ask("文件路径", "/tmp/demo.bin");
|
||||
try {
|
||||
filePath.value = await ask("文件路径", "/tmp/demo.bin");
|
||||
} catch (e) {
|
||||
phase.value = "error";
|
||||
message.value = `ask 异常: ${String(e)}`;
|
||||
return;
|
||||
}
|
||||
phase.value = "ask_server";
|
||||
server.value = await ask("服务器", "http://192.168.10.133:8000");
|
||||
if (!filePath.value || !server.value) {
|
||||
|
||||
Reference in New Issue
Block a user