fix: terminal garbled boxes from OSC sequences in PS1
User reported: each prompt line shows a leading '[?]' box. Root cause: Ubuntu's default PS1 uses \\e]0;user@host:dir\\a (OSC 0, set window title) which our ANSI parser did not recognize. It only handled SGR (\\e[...m), so the raw bytes were drawn as tofu boxes. Fix in ui/terminal_panel.py: - Added _OSC_RE for ESC ] ... BEL|ESC\\ sequences (set title, set cwd) - Added _DCS_RE for ESC P/X/^/_ ... ESC\\ (DCS/SOS/PM/APC) - _on_data() branches ESC prefix by next byte: '['->CSI, '] '->OSC, 'P/X/^/_'->DCS, others->skip 2 bytes - Captured OSC 0/1/2 content into self._window_title (so future versions can display it) - _strip_ansi() now also strips OSC/DCS (for clean history recall) Added [4b/5] test: injects a literal Ubuntu PS1 byte stream (OSC 0 + SGR colors) and verifies the rendering is just the plain 'root@ubuntu2204:~# ' with no tofu / no leaked escape chars. All 21 tests pass (core 6 + UI 3 + E2E 6 + terminal 6).
This commit is contained in:
+45
-9
@@ -36,13 +36,22 @@ ANSI_BG = {
|
||||
"100": "#78909c",
|
||||
}
|
||||
|
||||
# ANSI 转义序列正则(CSI ... 结尾)
|
||||
# ANSI CSI 序列:ESC [ ... <final 0x40-0x7e>
|
||||
_ANSI_RE = re.compile(r"\x1b\[([\x30-\x3f]*)([\x20-\x2f]*)([\x40-\x7e])")
|
||||
# OSC 序列:ESC ] ... BEL(0x07) 或 ESC \ 终止(PS1 标题/工作目录等)
|
||||
_OSC_RE = re.compile(r"\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)")
|
||||
# 单字符 ESC 序列:ESC c/=/> 等(RIS、DECSC 等),一般无害;这里跳过 ESC 后跟非 [ 的 1-2 字节
|
||||
_ESC_SINGLE_RE = re.compile(r"\x1b[PX^_](?:[^\x1b]*?\x1b\\)?")
|
||||
# DCS/SOS/PM/APC:ESC P/X/^/_ ... ST 跳过
|
||||
_DCS_RE = re.compile(r"\x1b[PX^_].*?\x1b\\")
|
||||
|
||||
|
||||
def _strip_ansi(text: str) -> str:
|
||||
"""去掉所有 ANSI 转义,得到纯文本(用于历史回放、长度计算)"""
|
||||
return _ANSI_RE.sub("", text)
|
||||
"""去掉所有 ANSI/OSC/DCS 转义,得到纯文本(用于历史回放、长度计算)"""
|
||||
text = _OSC_RE.sub("", text)
|
||||
text = _DCS_RE.sub("", text)
|
||||
text = _ANSI_RE.sub("", text)
|
||||
return text
|
||||
|
||||
|
||||
class _ShellReader(QObject):
|
||||
@@ -122,6 +131,8 @@ class TerminalPanel(QWidget):
|
||||
self._ansi_bold = False
|
||||
self._ansi_underline = False
|
||||
self._ansi_invert = False
|
||||
# OSC 0/2 设置的窗口标题
|
||||
self._window_title = ""
|
||||
|
||||
self._build_ui()
|
||||
self._apply_style()
|
||||
@@ -246,12 +257,37 @@ class TerminalPanel(QWidget):
|
||||
i = 0
|
||||
while i < len(text):
|
||||
ch = text[i]
|
||||
if ch == "\x1b" and i + 1 < len(text) and text[i+1] == "[":
|
||||
# CSI 序列
|
||||
m = _ANSI_RE.match(text, i)
|
||||
if m:
|
||||
self._apply_ansi(m.group(1), m.group(3))
|
||||
i = m.end()
|
||||
if ch == "\x1b" and i + 1 < len(text):
|
||||
nxt = text[i + 1]
|
||||
# CSI: ESC [ ... <final>
|
||||
if nxt == "[":
|
||||
m = _ANSI_RE.match(text, i)
|
||||
if m:
|
||||
self._apply_ansi(m.group(1), m.group(3))
|
||||
i = m.end()
|
||||
continue
|
||||
# OSC: ESC ] ... BEL | ESC \
|
||||
elif nxt == "]":
|
||||
m = _OSC_RE.match(text, i)
|
||||
if m:
|
||||
# OSC 0/1/2 设置标题/工作目录—— GUI 终端里通常更新窗口标题;
|
||||
# 我们这里不维护标题栏,但 PS1 里的 OSC 也照样被吃掉。
|
||||
# 提取 OSC 内容以便调试
|
||||
content = m.group(0)[2:-1] # 去掉 ESC ] 和终止符
|
||||
# 可选:把第一个参数后的内容存到 title
|
||||
if content and content[0:1] in ("0", "1", "2") and ";" in content:
|
||||
self._window_title = content.split(";", 1)[1]
|
||||
i = m.end()
|
||||
continue
|
||||
# DCS / SOS / PM / APC: ESC P/X/^/_ ... ESC \
|
||||
elif nxt in ("P", "X", "^", "_"):
|
||||
m = _DCS_RE.match(text, i)
|
||||
if m:
|
||||
i = m.end()
|
||||
continue
|
||||
# 其他 ESC 序列(ESC = / > / c 等)—— 单字节,吞掉
|
||||
else:
|
||||
i += 2
|
||||
continue
|
||||
elif ch == "\r":
|
||||
# \r 单独:回到行首;\n 真的换行
|
||||
|
||||
Reference in New Issue
Block a user