diff --git a/frontends/tui_v3.py b/frontends/tui_v3.py index d63e2abe..50a166f6 100644 --- a/frontends/tui_v3.py +++ b/frontends/tui_v3.py @@ -807,7 +807,12 @@ def _ptk_keypress_to_bytes(kp) -> bytes: return any(n in a for a in aliases for n in needles) # Enter submits; Ctrl+J / Shift+Enter insert newline when PTK can distinguish. + # Windows terminals send \r for both Enter/Shift+Enter — detect Shift physically. if has('controlm', 'c-m') or key_s in ('\r', '\n'): + if _IS_WINDOWS: + import ctypes + if ctypes.windll.user32.GetAsyncKeyState(0x10) & 0x8000: + return b'\n' return b'\r' if has('controlj', 'c-j', 's-enter', 'shift-enter'): return b'\n' @@ -2542,6 +2547,27 @@ class SB: return i, p - st return len(segs) - 1, len(segs[-1][1]) + def _line_region(self, pos: int | None = None) -> tuple[int, int, int, int]: + """(行号, 总行数, 行起始偏移, 行结束偏移) 基于逻辑行(\\n分割)""" + p = self.pos if pos is None else pos + ls = self.buf.rfind('\n', 0, p) + 1 + le = self.buf.find('\n', p) + if le == -1: + le = len(self.buf) + line_no = self.buf[:p].count('\n') + total = self.buf.count('\n') + 1 + return line_no, total, ls, le + + def _logical_visual_range(self, segs, line_no): + """返回第 line_no 个逻辑行在 segs 中的 (first_vi, last_vi)""" + lines = self.buf.split('\n') + iw = max(1, _term()[0] - 6) + first = 0 + for i in range(line_no): + first += max(1, len(_wrap_cells(lines[i], iw))) + cnt = max(1, len(_wrap_cells(lines[line_no], iw))) + return first, first + cnt - 1 + def _cur_v(self, d: int) -> None: """↑/↓ roam by VISUAL row (a long single-line paste wraps to many rows yet stays one logical line — must still roam). At the top/bottom visual @@ -5157,7 +5183,23 @@ class SB: self._pending_drain_t = 0.0 self._render_live() else: - self._sel = None; self._cur_v(-1) + self._sel = None + line_no, total, ls, le = self._line_region() + segs = self._segs(max(1, _term()[0] - 6)) + vi, _ = self._seg_at(segs) + first_vi, _ = self._logical_visual_range(segs, line_no) + if vi == first_vi: # 在视觉首行 + if line_no == 0: + if self.pos == ls: + self._nav_hist(-1) + else: + self.pos = ls + elif self.pos == ls: + self._cur_v(-1) # 已在行首 → 正常上移 + else: + self.pos = ls # 跳到该行行首 + else: + self._cur_v(-1) elif o == 0x13: # Ctrl+S stash/restore draft self._stash_draft() elif o == 0x0e: # ↓ visual-row down (history at bottom) @@ -5165,7 +5207,23 @@ class SB: n = len(self._cmd_matches(self.buf)) self._palette_sel = min(n - 1, self._palette_sel + 1) if n else 0 else: - self._sel = None; self._cur_v(1) + self._sel = None + line_no, total, ls, le = self._line_region() + segs = self._segs(max(1, _term()[0] - 6)) + vi, _ = self._seg_at(segs) + first_vi, last_vi = self._logical_visual_range(segs, line_no) + if vi == last_vi: # 在视觉末行 + if line_no == total - 1: + if self.pos == le: + self._nav_hist(1) + else: + self.pos = le + elif self.pos == le: + self._cur_v(1) # 已在行末 → 正常下移 + else: + self.pos = le # 跳到该行行末 + else: + self._cur_v(1) elif o == 0x02: # ← caret left self._sel = None; self.pos = max(0, self.pos - 1) elif o == 0x06: # → caret right