Files
TransPyC/_console.py

195 lines
7.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import tkinter as tk
from tkinter import scrolledtext
import subprocess
import threading
import sys
import os
import queue
import time
class ConsoleApp:
def __init__(self, root, args):
self.root = root
self.args = args
self.proc = None
self.output_queue = queue.Queue()
# 获取系统当前编码,尽量保证原样输出
self.encoding = sys.stdout.encoding or 'utf-8'
# 记录最后一次收到输出的时间
self.last_output_time = time.time()
root.title("AI 交互终端")
root.geometry("900x600")
# GUI 布局
# 输出区域
self.text_area = scrolledtext.ScrolledText(root, wrap=tk.WORD, font=("Consolas", 10))
self.text_area.pack(expand=True, fill='both', padx=5, pady=5)
# 输入区域
input_frame = tk.Frame(root)
input_frame.pack(fill='x', padx=5, pady=5)
self.entry = tk.Entry(input_frame, font=("Consolas", 10))
self.entry.pack(side=tk.LEFT, expand=True, fill='x')
self.entry.bind('<Return>', self.send_input)
# 说话模式复选框
self.speak_mode = tk.BooleanVar(value=False)
self.speak_check = tk.Checkbutton(input_frame, text="提示AI(说话)", variable=self.speak_mode, font=("Consolas", 9))
self.speak_check.pack(side=tk.RIGHT, padx=5)
self.kill_btn = tk.Button(input_frame, text="强制终止进程", command=self.force_terminate, bg="red", fg="white")
self.kill_btn.pack(side=tk.RIGHT, padx=5)
# 状态栏 (用于显示无输出时长)
status_frame = tk.Frame(root)
status_frame.pack(fill='x', padx=5, pady=(0, 5))
self.time_label = tk.Label(status_frame, text="无输出时长: 00:00", font=("Consolas", 10), fg="gray")
self.time_label.pack(side=tk.RIGHT)
# 启动子进程 (合并 stderr 到 stdout)
try:
self.proc = subprocess.Popen(
self.args,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
bufsize=0 # 无缓冲,确保实时性
)
except Exception as e:
print(f"启动进程失败: {e}")
sys.exit(1)
# 启动读取子进程输出的线程
self.output_thread = threading.Thread(target=self.read_output, daemon=True)
self.output_thread.start()
# 启动读取当前脚本控制台输入的线程 (允许直接在终端输入)
self.stdin_thread = threading.Thread(target=self.read_stdin, daemon=True)
self.stdin_thread.start()
# 窗口关闭事件
self.root.protocol("WM_DELETE_WINDOW", self.on_closing)
# 开始轮询队列更新GUI
self.update_gui()
def read_output(self):
"""后台线程:实时读取子进程的输出"""
fd = self.proc.stdout.fileno()
while True:
try:
byte_data = os.read(fd, 1024)
if not byte_data:
break # 进程结束
# 只要收到数据,就更新最后输出时间
self.last_output_time = time.time()
# 1. 实时输出到当前脚本的 stdout (不加任何前缀)
sys.stdout.buffer.write(byte_data)
sys.stdout.buffer.flush()
# 2. 放入队列供 GUI 使用
try:
text = byte_data.decode(self.encoding, errors='replace')
except:
text = byte_data.decode('utf-8', errors='replace')
self.output_queue.put(text)
except OSError:
break # 管道已关闭
def read_stdin(self):
"""后台线程:实时读取宿主控制台的 stdin 并转发给子进程"""
while True:
line = sys.stdin.readline()
if not line:
break
try:
self.proc.stdin.write(line.encode(self.encoding, errors='replace'))
self.proc.stdin.flush()
except:
break
def send_input(self, event):
"""GUI 输入框回车事件"""
text = self.entry.get()
self.entry.delete(0, tk.END)
if not text:
return
# 如果勾选了"提示AI",则直接输出到当前脚本的 stdout不发给子进程
if self.speak_mode.get():
speak_text = text + '\n'
# 输出到当前脚本控制台,供 AI 捕获
sys.stdout.buffer.write(speak_text.encode(self.encoding, errors='replace'))
sys.stdout.buffer.flush()
# 在 GUI 中回显,让用户知道说了什么(使用不同颜色区分)
self.text_area.insert(tk.END, speak_text, 'speak_tag')
self.text_area.tag_config('speak_tag', foreground="blue")
self.text_area.see(tk.END)
else:
# 正常模式:发给子进程
text += '\n'
try:
self.proc.stdin.write(text.encode(self.encoding, errors='replace'))
self.proc.stdin.flush()
except Exception as e:
self.output_queue.put(f"\n[输入发送失败: {e}]\n")
def force_terminate(self):
"""强制终止子进程"""
if self.proc and self.proc.poll() is None:
print("用户终止了应用程序") # 输出到控制台 stdout
self.output_queue.put("\n[用户终止了应用程序]\n")
self.proc.kill()
def on_closing(self):
"""关闭窗口时终止子进程"""
if self.proc and self.proc.poll() is None:
self.proc.kill()
self.root.destroy()
def update_gui(self):
"""主线程从队列获取数据并更新GUI"""
try:
while True:
text = self.output_queue.get_nowait()
self.text_area.insert(tk.END, text)
self.text_area.see(tk.END)
except queue.Empty:
pass
# 计算并更新无输出时长
if self.proc and self.proc.poll() is None:
idle_seconds = int(time.time() - self.last_output_time)
mins = idle_seconds // 60
secs = idle_seconds % 60
self.time_label.config(text=f"无输出时长: {mins:02d}:{secs:02d}")
else:
self.time_label.config(text="进程已结束")
# 检查进程是否已结束
if self.proc and self.proc.poll() is not None:
if self.output_queue.empty():
self.kill_btn.config(state=tk.DISABLED, bg="grey", text="进程已结束")
# 进程结束且输出已读取完毕,自动关闭程序
self.root.destroy()
return # 停止轮询
# 每 200ms 轮询一次 (减少CPU占用时间显示不需要50ms那么频繁)
self.root.after(200, self.update_gui)
if __name__ == "__main__":
if len(sys.argv) < 2:
print("用法: python ai_console.py <command> [args...]")
print("示例: python ai_console.py cmd")
sys.exit(1)
root = tk.Tk()
app = ConsoleApp(root, sys.argv[1:])
root.mainloop()