CLI 视觉升级:tui.py 真彩色界面

- 配色与 GUI 同源(石墨底/青绿点缀),支持 NO_COLOR 与非 tty 自动降级纯文本
- 向导:对齐横幅、❯ 方向键选择菜单(↑↓/数字/回车,q 取消),彩色表单提示
- 隧道运行时实时面板:状态·运行时长·累计连接·滚动日志,原地刷新
- Ctrl+C 退出时面板干净收起;非交互终端自动退化为编号选择
This commit is contained in:
leefer
2026-09-27 20:40:41 +08:00
parent 199b46b593
commit 21cc1e7d04
3 changed files with 390 additions and 82 deletions
+3
View File
@@ -19,6 +19,9 @@ curl -fsSL https://gitea.xbay.cc/leefer/ssh-e2e/raw/branch/main/install.sh | bas
安装完成会**自动进入中文配置向导**:按提示输入 SSH 地址、密码、远程端口等
(直接回车采用方括号里的默认值),填完隧道立即建立,Ctrl+C 断开。
命令行界面支持真彩色(自动适配终端,`NO_COLOR` 关闭):方向键 `❯` 选择菜单、
隧道运行时的实时状态面板(连接状态 · 运行时长 · 累计连接 · 滚动日志)。
## 典型场景
远程主机上有个服务只监听它自己的 `127.0.0.1:3000`,公网访问不到。
+105 -82
View File
@@ -23,6 +23,7 @@ sys.path.insert(0, str(Path(__file__).parent))
from engine import TunnelManager, probe_host # noqa: E402
from storage import Store, data_root # noqa: E402
import tui # noqa: E402
def _print(msg: str) -> None:
@@ -30,26 +31,32 @@ def _print(msg: str) -> None:
class _QuietEmit:
"""CLI 模式下把引擎事件打到终端。"""
"""引擎事件 → 运行面板(无面板时打印到终端)。"""
def __init__(self):
def __init__(self, dash: "tui.Dashboard | None" = None):
self._last: dict[str, str] = {}
self._dash = dash
def __call__(self, ev: dict) -> None:
if ev.get("type") == "tunnel":
if self._last.get(ev["id"]) == ev["status"]:
return # 状态未变(如连接计数刷新),不重复打印
return # 状态未变(如连接计数刷新)
self._last[ev["id"]] = ev["status"]
t = ev["status"]
if t == "connected":
_print("[隧道] 已连接")
elif t == "error":
_print(f"[隧道] 连接失败: {ev.get('error', '')}")
if ev["status"] == "error" and self._dash:
self._dash.log(tui.err(f"✘ {ev.get('error', '')}"))
elif not self._dash:
if ev["status"] == "connected":
_print(tui.ok("[隧道] 已连接"))
elif ev["status"] == "error":
_print(tui.err(f"[隧道] 连接失败: {ev.get('error', '')}"))
elif ev.get("type") == "log":
e = ev["entry"]
lv = e["level"]
mark = {"info": "·", "success": "✔", "warn": "!", "error": "×"}.get(lv, "·")
_print(f"[{e['t']}] {mark} {e['msg']}")
mark = {"info": "·", "success": "✔", "warn": "!", "error": "×"}.get(e["level"], "·")
line = f"{e['t']} {mark} {e['msg']}"
if self._dash:
self._dash.log(line)
else:
_print(line)
def find_host(store: Store, ref: str):
@@ -113,19 +120,22 @@ def cmd_tunnel_add(store, args) -> int:
def cmd_list(store, _args) -> int:
if store.warn_plaintext:
_print("提示: 当前平台密码以明文保存于配置文件(权限已设为仅本用户可读)。")
_print("主机:")
print(tui.bold("主机"))
if not store.data["hosts"]:
_print(" (无) 添加: ssh-tunnel host add <名称> user@host")
print(tui.faint(" (无) ssh-tunnel 进入向导添加"))
for h in store.data["hosts"]:
_print(f" {h['name']} {h['username']}@{h['hostname']}:{h['port']} "
f"{'私钥' if h.get('auth') == 'key' else '密码'}")
_print("隧道:")
auth = "私钥" if h.get("auth") == "key" else "密码"
print(f" {tui.accent('▣')} {h['name']} "
f"{tui.dim(f'{h["username"]}@{h["hostname"]}:{h["port"]}')} {tui.faint(auth)}")
print(tui.bold("隧道"))
if not store.data["tunnels"]:
_print(" (无) 添加: ssh-tunnel tunnel add <名称> --host <主机> --lp 本地端口 --rp 远程端口")
print(tui.faint(" (无) ssh-tunnel 进入向导添加"))
for t in store.data["tunnels"]:
h = next((x for x in store.data["hosts"] if x["id"] == t["host_id"]), None)
hn = h["name"] if h else "(主机缺失)"
_print(f" {t['name']} 127.0.0.1:{t['local_port']} → {t['remote_host']}:{t['remote_port']} 经 {hn}")
print(f" {tui.accent('⇄')} {t['name']} "
f"{tui.dim(f'127.0.0.1:{t["local_port"]} → {t["remote_host"]}:{t["remote_port"]}')} "
f"{tui.faint(f'经 {hn}')}")
return 0
@@ -165,23 +175,36 @@ def cmd_remove(store, args) -> int:
def run_foreground(store: Store, chosen: list) -> int:
"""前台运行选中的隧道,Ctrl+C 退出。"""
"""前台运行选中的隧道,Ctrl+C 退出。tty 下显示实时状态面板。"""
hosts_by_id = {h["id"]: h for h in store.data["hosts"]}
mgr = TunnelManager(_QuietEmit(), data_root())
dash = tui.Dashboard()
mgr = TunnelManager(_QuietEmit(dash), data_root())
mgr.apply_config(chosen, hosts_by_id)
if store.warn_plaintext:
_print("提示: 当前平台密码以明文保存于配置文件(权限已设为仅本用户可读)。")
names = ", ".join(t["name"] for t in chosen)
_print(f"启动隧道: {names} — Ctrl+C 退出")
print()
print(tui.faint(" " + ", ".join(t["name"] for t in chosen) + " — Ctrl+C 退出"))
stopping = False
def _stats():
return [{
"name": t.cfg.get("name", t.cfg["id"]),
"local_port": t.cfg["local_port"],
"remote": f"{t.cfg['remote_host']}:{t.cfg['remote_port']}",
"status": t.status, "started_at": t.started_at,
"conns": t.conn_count, "error": t.error,
} for t in mgr.tunnels.values()]
def _stop(*_a):
nonlocal stopping
if not stopping:
stopping = True
_print("\n正在断开所有隧道 ...")
print() # 光标移到面板下方行首
dash.close()
_print("正在断开所有隧道 ...")
sys.stdout.flush()
mgr.stop_all()
signal.signal(signal.SIGINT, _stop)
@@ -190,13 +213,15 @@ def run_foreground(store: Store, chosen: list) -> int:
t.start()
try:
while not stopping:
time.sleep(0.3)
alive = [t for t in mgr.tunnels.values() if t.status != "stopped"]
if not alive:
_print("所有隧道已停止。")
time.sleep(0.5)
dash.update(_stats())
if not [t for t in mgr.tunnels.values() if t.status != "stopped"]:
dash.close()
_print(tui.err("所有隧道已停止。"))
return 1
except KeyboardInterrupt:
_stop()
dash.close()
return 0
@@ -269,34 +294,31 @@ def _ask_secret(prompt: str) -> str:
def _wizard_pick_host(store: Store):
"""选择已有主机或新建;返回主机 dict 或 None(取消)。"""
hosts = store.data["hosts"]
_print("\n── 第 1 步:选择 SSH 主机 ──")
for i, h in enumerate(hosts, 1):
_print(f" {i}. {h['name']} ({h['username']}@{h['hostname']}:{h['port']})")
_print(f" {len(hosts) + 1}. 新增主机")
while True:
sel = _ask(f"请选择 [1-{len(hosts) + 1}]", str(len(hosts) + 1 if not hosts else "1"))
if not sel.isdigit() or not (1 <= int(sel) <= len(hosts) + 1):
_print(" 无效选择,请重新输入")
continue
n = int(sel)
if n <= len(hosts):
return hosts[n - 1]
return _wizard_new_host(store)
print()
print(tui.bold("◇ SSH 主机"))
opts = [f"{h['name']} ({h['username']}@{h['hostname']}:{h['port']})" for h in hosts]
opts.append(tui.dim("+ 新增主机") if len(hosts) else "+ 新增主机")
n = tui.select(" 选择要使用的主机:", opts, default=len(hosts) if not hosts else 0)
if n is None:
return None
if n < len(hosts):
return hosts[n]
return _wizard_new_host(store)
def _wizard_new_host(store: Store):
_print("\n新增主机(带 * 的为必填):")
print()
print(tui.bold("◇ 新增主机") + tui.faint(" (带 * 为必填)"))
while True:
target = _ask(" * SSH 登录目标 (格式 用户名@主机,如 root@1.2.3.4): ")
target = _ask(" " + tui.accent("SSH 登录目标") + tui.faint(" (用户名@主机,如 root@1.2.3.4)") + ": ")
if "@" in target and len(target.split("@")) == 2 and target.split("@")[1]:
break
_print(" 格式不对,应类似 root@1.2.3.4")
print(tui.warn(" 格式不对,应类似 root@1.2.3.4"))
user, hostaddr = target.rsplit("@", 1)
port = _ask_port(" SSH 端口 [22]: ", 22)
auth = _ask(" 认证方式 1) 密码 2) 私钥 [1]: ", "1")
port = _ask_port(" " + tui.accent("SSH 端口") + tui.faint(" [22]"), 22)
auth = _ask(" " + tui.accent("认证方式") + tui.faint(" 1)密码 2)私钥 [1]"), "1")
while auth not in ("1", "2"):
auth = _ask(" 请输入 1 或 2: ", "1")
auth = _ask(" 请输入 1 或 2: ", "1")
rec = {
"id": Store.new_id(), "name": "", "hostname": hostaddr, "port": port,
@@ -304,61 +326,59 @@ def _wizard_new_host(store: Store):
"password": "", "key_path": "", "key_passphrase": "", "note": "",
}
if rec["auth"] == "key":
rec["key_path"] = _ask(" * 私钥文件路径 (如 ~/.ssh/id_rsa): ")
rec["key_path"] = _ask(" " + tui.accent("* 私钥文件路径") + tui.faint(" (如 ~/.ssh/id_rsa)") + ": ")
rec["key_passphrase"] = _ask_secret(" 私钥口令 (没有则直接回车): ")
else:
rec["password"] = _ask_secret(" * 登录密码 (输入时不显示): ")
rec["name"] = _ask(" 主机备注名 [默认取地址]: ", hostaddr)
rec["password"] = _ask_secret(" " + tui.accent("* 登录密码") + tui.faint(" (输入时不显示)") + ": ")
rec["name"] = _ask(" " + tui.accent("主机备注名") + tui.faint(f" [{hostaddr}]") + ": ", hostaddr)
# 连通测试,失败可重试输入
while True:
_print(f"正在测试 {user}@{hostaddr}:{port} ...")
print(tui.faint(f" 正在测试 {user}@{hostaddr}:{port} ..."))
ok, msg = probe_host(rec, data_root() / "known_hosts")
_print((" ✔ " if ok else " ✘ ") + msg)
print((" " + tui.ok("✔ ") + tui.ok(msg)) if ok else (" " + tui.err("✘ ") + tui.err(msg)))
if ok:
break
retry = _ask(" 连接失败。重试请回车,修改密码输入 p,放弃输入 q [回车重试]: ", "")
retry = _ask(" " + tui.warn("连接失败") + tui.faint(" — 回车重试 / p 改密码 / q 放弃") + ": ", "")
if retry.lower() == "q":
return None
if retry.lower() == "p" and rec["auth"] == "password":
rec["password"] = _ask_secret(" * 重新输入登录密码: ")
rec["password"] = _ask_secret(" 重新输入登录密码: ")
store.data["hosts"].append(rec)
store.save()
_print(f"已保存主机「{rec['name']}」")
print(" " + tui.ok(f"已保存主机「{rec['name']}」"))
return rec
def _wizard_pick_tunnel(store: Store, host: dict):
"""选择该主机的已有隧道或新建;返回隧道 dict 或 None。"""
ts = [t for t in store.data["tunnels"] if t["host_id"] == host["id"]]
_print("\n── 第 2 步:配置隧道 ──")
for i, t in enumerate(ts, 1):
_print(f" {i}. {t['name']} (127.0.0.1:{t['local_port']} → {t['remote_host']}:{t['remote_port']})")
_print(f" {len(ts) + 1}. 新建隧道")
while True:
sel = _ask(f"请选择 [1-{len(ts) + 1}]", str(len(ts) + 1 if not ts else "1"))
if not sel.isdigit() or not (1 <= int(sel) <= len(ts) + 1):
_print(" 无效选择,请重新输入")
continue
n = int(sel)
if n <= len(ts):
return ts[n - 1]
return _wizard_new_tunnel(store, host)
print()
print(tui.bold("◇ 隧道"))
opts = [f"{t['name']} (127.0.0.1:{t['local_port']} → {t['remote_host']}:{t['remote_port']})"
for t in ts]
opts.append("+ 新建隧道")
n = tui.select(" 选择要启动的隧道:", opts, default=len(ts) if not ts else 0)
if n is None:
return None
if n < len(ts):
return ts[n]
return _wizard_new_tunnel(store, host)
def _wizard_new_tunnel(store: Store, host: dict):
_print("\n新建隧道:把远程主机上的服务映射到本机端口")
_print(" (远程服务只监听 127.0.0.1 时,远程地址保持默认即可)")
rh = _ask(" 远程服务地址 [127.0.0.1]: ", "127.0.0.1")
rp = _ask_port(" * 远程服务端口 (如 3000): ")
lp = _ask_port(f" 本地端口 [{rp}]: ", rp)
print()
print(tui.bold("◇ 新建隧道") + tui.faint(" 把远程主机上的服务映射到本机端口"))
print(tui.faint(" (远程服务只监听 127.0.0.1 时,远程地址保持默认即可)"))
rh = _ask(" " + tui.accent("远程服务地址") + tui.faint(" [127.0.0.1]") + ": ", "127.0.0.1")
rp = _ask_port(" " + tui.accent("* 远程服务端口") + tui.faint(" (如 3000)") + ": ")
lp = _ask_port(" " + tui.accent("本地端口") + tui.faint(f" [{rp}]") + ": ", rp)
for t in store.data["tunnels"]:
if t["local_port"] == lp:
_print(f" 本地端口 {lp} 已被隧道「{t['name']}」占用")
print(tui.warn(f" 本地端口 {lp} 已被隧道「{t['name']}」占用"))
lp = _ask_port(" 换一个本地端口: ")
name = _ask(f" 隧道备注名 [{rh}:{rp}]: ", f"{rh}:{rp}")
name = _ask(" " + tui.accent("隧道备注名") + tui.faint(f" [{rh}:{rp}]") + ": ", f"{rh}:{rp}")
rec = {
"id": Store.new_id(), "name": name, "host_id": host["id"],
"local_port": lp, "remote_host": rh, "remote_port": rp,
@@ -366,17 +386,20 @@ def _wizard_new_tunnel(store: Store, host: dict):
}
store.data["tunnels"].append(rec)
store.save()
_print(f"已保存隧道「{name}」")
print(" " + tui.ok(f"已保存隧道「{name}」"))
return rec
def wizard(store: Store) -> int:
_ensure_tty_stdin()
_print("╭─────────────────────────────╮")
_print("│ SSH 隧道配置向导 │")
_print("╰─────────────────────────────╯")
_print("按中文提示输入,直接回车采用 [方括号] 里的默认值;Ctrl+C 随时退出。")
_print(f"配置文件: {store.path}")
title = "SSH 隧道配置向导"
sub = "回车采用 [方括号] 默认值 · Ctrl+C 随时退出"
w = max(tui.disp_len(title) + 4, tui.disp_len(sub) + 4, 40)
print()
print(tui.accent("╭─ ") + tui.bold(title) + " " + tui.accent("─" * (w - tui.disp_len(title) - 3) + "╮"))
print(tui.accent("│") + " " + tui.faint(sub) + " " * (w - tui.disp_len(sub) - 2) + tui.accent("│"))
print(tui.accent("╰" + "─" * w + "╯"))
print(tui.faint(f" 配置文件: {store.path}"))
if store.warn_plaintext:
_print("提示: 当前平台密码以明文保存于配置文件(权限已设为仅本用户可读)。")
+282
View File
@@ -0,0 +1,282 @@
"""终端界面组件:真彩色样式、方向键选择菜单、隧道运行面板。
非 tty 或设置 NO_COLOR 时自动降级为纯文本,行为不变。
"""
from __future__ import annotations
import os
import sys
import threading
import time
from collections import deque
# ---------- 终端能力 ----------
_COLOR = None
def _use_color() -> bool:
global _COLOR
if _COLOR is not None:
return _COLOR
if os.environ.get("NO_COLOR"):
_COLOR = False
elif os.environ.get("FORCE_COLOR"):
_COLOR = True
else:
_COLOR = sys.stdout.isatty()
if _COLOR and sys.platform == "win32":
_enable_vt()
return _COLOR
def _enable_vt() -> None:
try:
import ctypes
k32 = ctypes.windll.kernel32
h = k32.GetStdHandle(-11) # STD_OUTPUT_HANDLE
mode = ctypes.c_uint32()
if k32.GetConsoleMode(h, ctypes.byref(mode)):
k32.SetConsoleMode(h, mode.value | 0x4) # ENABLE_VIRTUAL_TERMINAL_PROCESSING
except Exception:
pass
# ---------- 调色板(与 GUI 同源:石墨底 + 青绿点缀) ----------
ACCENT = (47, 191, 161) # 青绿
DIM = (151, 161, 173) # 次级文字
FAINT = (110, 120, 131) # 弱化文字
GREEN = (70, 196, 110)
RED = (226, 86, 77)
AMBER = (217, 162, 60)
TEXT = (226, 231, 238)
def _c(s: str, rgb: tuple, bold: bool = False) -> str:
if not _use_color():
return s
r, g, b = rgb
prefix = f"\x1b[1m" if bold else ""
return f"{prefix}\x1b[38;2;{r};{g};{b}m{s}\x1b[0m"
def accent(s: str) -> str: return _c(s, ACCENT)
def dim(s: str) -> str: return _c(s, DIM)
def faint(s: str) -> str: return _c(s, FAINT)
def ok(s: str) -> str: return _c(s, GREEN)
def err(s: str) -> str: return _c(s, RED)
def warn(s: str) -> str: return _c(s, AMBER)
def bold(s: str) -> str: return _c(s, TEXT, bold=True)
# ---------- 显示宽度(CJK 全角按 2 列) ----------
_WIDE = ((0x1100, 0x115F), (0x2E80, 0x303E), (0x3041, 0x33FF), (0x3400, 0x4DBF),
(0x4E00, 0x9FFF), (0xA000, 0xA4CF), (0xAC00, 0xD7A3), (0xF900, 0xFAFF),
(0xFE30, 0xFE4F), (0xFF00, 0xFF60), (0xFFE0, 0xFFE6))
def disp_len(s: str) -> int:
n = 0
for ch in s:
o = ord(ch)
n += 2 if any(a <= o <= b for a, b in _WIDE) else 1
return n
# ---------- 方向键选择菜单 ----------
_UP, _DOWN, _ENTER, _ESC, _OTHER = object(), object(), object(), object(), object()
def _read_key() -> object:
"""读一个键:方向上/下、回车、Esc、其他(返回原字符)。跨平台。"""
try:
if sys.platform == "win32":
import msvcrt
ch = msvcrt.getwch()
if ch in ("\x00", "\xe0"):
ext = msvcrt.getwch()
if ext == "H":
return _UP
if ext == "P":
return _DOWN
return _OTHER
return {"\r": _ENTER, "\n": _ENTER, "\x1b": _ESC}.get(ch, ch)
import termios
import tty
fd = os.open("/dev/tty", os.O_RDONLY)
try:
old = termios.tcgetattr(fd)
tty.setraw(fd)
try:
ch = os.read(fd, 1).decode("utf-8", "ignore")
if ch == "\x1b":
rest = os.read(fd, 2).decode("utf-8", "ignore")
if rest == "[A":
return _UP
if rest == "[B":
return _DOWN
return _ESC
return {"\r": _ENTER, "\n": _ENTER}.get(ch, ch)
finally:
termios.tcsetattr(fd, termios.TCSADRAIN, old)
finally:
os.close(fd)
except Exception:
return _OTHER # 无原始终端:退化为编号输入
def select(title: str, options: list, default: int = 0, cancel: bool = True):
"""交互选择:tty 时 ↑↓ 移动、回车确认、数字直达、q/Esc 取消;
非 tty(管道/自动化)退化为编号输入。返回索引;取消返回 None。"""
if len(options) == 1:
# 只有一个选项:直接展示并采用,不打扰
print(title)
print(f" {accent('❯')} {options[0]}")
return 0
if sys.stdin.isatty():
return _select_keys(title, options, default, cancel)
return _select_numbered(title, options, default)
def _select_numbered(title: str, options: list, default: int) -> int | None:
print(title)
for i, opt in enumerate(options, 1):
mark = bold(opt) if i - 1 == default else dim(opt)
print(f" {faint(str(i) + '.')} {mark}")
while True:
try:
raw = input(faint(f" 选择 [1-{len(options)}] (回车 = {default + 1}): ")).strip()
except EOFError:
return None
if not raw:
return default
if raw.isdigit() and 1 <= int(raw) <= len(options):
return int(raw) - 1
print(warn(f" 无效编号,请输入 1-{len(options)}"))
def _select_keys(title: str, options: list, default: int, cancel: bool):
hint = faint(" ↑↓ 选择 · 回车确认" + (" · q 取消" if cancel else ""))
def _paint(cur: int, chosen: bool = False):
lines = []
for i, opt in enumerate(options):
if i == cur:
mark = ok("❯") if chosen else accent("❯")
lines.append(f" {mark} {bold(opt)}")
else:
lines.append(f" {dim(opt)}")
return lines
print(title)
for ln in _paint(default):
print(ln)
print(hint)
sys.stdout.flush()
drawn = len(options) + 1 # 选项 + 提示行
cur = default
while True:
k = _read_key()
if k is _OTHER or k is None:
sys.stdout.write(f"\x1b[{drawn}A\r" if _use_color() else "")
return _select_numbered(" (此终端不支持方向键,请输入编号)", options, cur)
if k == _UP:
cur = (cur - 1) % len(options)
elif k == _DOWN:
cur = (cur + 1) % len(options)
elif k == _ENTER:
# 定格所选,清掉菜单只留一行
sys.stdout.write(f"\x1b[{drawn + 1}A\r\x1b[J")
print(f" {ok('❯')} {bold(options[cur])}")
return cur
elif k in (_ESC, "q", "Q"):
sys.stdout.write(f"\x1b[{drawn + 1}A\r\x1b[J")
print(f" {faint('已取消')}")
return None
elif isinstance(k, str) and k.isdigit() and 1 <= int(k) <= len(options):
cur = int(k) - 1
sys.stdout.write(f"\x1b[{drawn + 1}A\r\x1b[J")
print(f" {ok('❯')} {bold(options[cur])}")
return cur
# 重绘选项+提示
out = [f"\x1b[{drawn}A\r"]
for ln in _paint(cur):
out.append(ln + "\x1b[K\n")
out.append(hint + "\x1b[K")
sys.stdout.write("".join(out))
sys.stdout.flush()
# ---------- 隧道运行面板 ----------
def fmt_up(sec: float) -> str:
sec = max(0, int(sec))
return f"{sec // 3600:02d}:{sec % 3600 // 60:02d}:{sec % 60:02d}"
_STATUS_STYLE = {
"connected": (GREEN, "● 已连接"),
"connecting": (AMBER, "◐ 连接中"),
"error": (RED, "✘ 失败"),
"stopped": (FAINT, "○ 已停止"),
}
class Dashboard:
"""运行时面板:隧道状态 + 滚动日志,原地刷新。非 tty 时为空操作。"""
def __init__(self, logs: int = 4):
self._rows = 0
self._logs: deque = deque(maxlen=logs)
self._stats: list = []
self._lock = threading.Lock()
self._on = sys.stdout.isatty()
def log(self, msg: str) -> None:
with self._lock:
self._logs.append(msg)
self._render()
def update(self, stats: list) -> None:
with self._lock:
self._stats = stats
self._render()
def close(self) -> None:
with self._lock:
if self._on and self._rows:
sys.stdout.write(f"\x1b[{self._rows}A\r\x1b[J")
sys.stdout.flush()
self._rows = 0
def _render(self) -> None:
if not self._on:
return
lines = []
head = " ssh-e2e · 隧道运行中 "
lines.append(accent("─" + head) + faint("─" * max(0, 46 - disp_len(head))))
for st in self._stats:
rgb, label = _STATUS_STYLE.get(st["status"], (FAINT, "○ 未知"))
dot = _c(label.split()[0], rgb)
text = " ".join(label.split()[1:])
lines.append(f" {dot} {bold(st['name'])}")
m = f"{dim('127.0.0.1:' + str(st['local_port']))} {accent('⇄')} {dim(st['remote'])}"
uptime = fmt_up(time.time() - st["started_at"]) if st.get("started_at") else "--:--:--"
lines.append(f" {m} {_c(text, rgb)} · {faint(uptime)} · {faint('连接 ' + str(st.get('conns', 0)))}")
if st.get("error"):
lines.append(f" {err(st['error'])}")
if self._logs:
lines.append(faint("─" * 46))
for e in self._logs:
lines.append(f" {faint(e)}")
lines.append(faint("─ Ctrl+C 断开 " + "─" * 33))
out = []
if self._rows:
out.append(f"\x1b[{self._rows}A\r\x1b[J")
out.append("\n".join(lines) + "\n")
sys.stdout.write("".join(out))
sys.stdout.flush()
self._rows = len(lines)