From 911299f0134e773e02daa3f4318c73a3ab6c52cb Mon Sep 17 00:00:00 2001 From: Joakim Date: Wed, 14 Oct 2026 21:32:47 +0200 Subject: [PATCH] N/A --- src/ai/__pycache__/tui.cpython-313.pyc | Bin 0 -> 35563 bytes src/ai/tui.py | 142 +++++++++++++++++++++---- 2 files changed, 121 insertions(+), 21 deletions(-) create mode 100644 src/ai/__pycache__/tui.cpython-313.pyc diff --git a/src/ai/__pycache__/tui.cpython-313.pyc b/src/ai/__pycache__/tui.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..662b89a56d317bb2f6288eb8bfa074bace21fcc8 GIT binary patch literal 35563 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 b: + a, b = b, a + for r in range(a[0], min(b[0] + 1, len(chat))): + if r < start or r >= start + chat_available: + continue + lo = a[1] if r == a[0] else 0 + hi = b[1] if r == b[0] else len(chat[r][1]) + if hi > lo: + screen.chgat(r - start + 1, lo, min(width - lo, hi - lo), curses.A_REVERSE) screen.move(row, min(width - 1, col)) except curses.error: pass @@ -267,11 +278,55 @@ class App: def move_input(self, direction): if direction == "left" and self.cursor > 0: self.cursor -= 1 - elif direction == "right" and self.cursor < len(self.input): + return + if direction == "right" and self.cursor < len(self.input): self.cursor += 1 - elif direction in ("up", "down"): - line, col = self.line_col() - self.cursor = self.cursor_from_line_col(line + (1 if direction == "down" else -1), col) + return + if direction not in ("up", "down"): + return + + # Move by *visual* terminal lines, not logical newline-separated + # lines. This makes Up/Down behave like a normal terminal editor: + # one press moves roughly one screen width while keeping the column. + width = max(1, self.s.getmaxyx()[1]) + rendered, (row, col) = self.input_lines(width) + target = row + (-1 if direction == "up" else 1) + if target < 0 or target >= len(rendered): + return + + # All input lines use a two-character prefix ("> " / " "). + # Convert the visual column back to a character offset in that line. + prefix_len = len(rendered[target][0]) + target_text_col = max(0, col - prefix_len) + + # Walk the input using the same wrapping rules as input_lines(). + # Build visual chunks and their character ranges. + chunks = [] + pos = 0 + logical_lines = self.input.split("\n") + first_width = max(1, width - 2) + rest_width = max(1, width - 2) + for li, logical in enumerate(logical_lines): + if logical == "": + chunks.append((li, pos, pos, 2)) + else: + remaining = logical + local = pos + first = True + while remaining: + n = first_width if first else rest_width + piece = remaining[:n] + chunks.append((li, local, local + len(piece), 2)) + local += len(piece) + remaining = remaining[len(piece):] + first = False + pos += len(logical) + 1 + + if target >= len(chunks): + self.cursor = len(self.input) + return + _, start, end, _ = chunks[target] + self.cursor = min(end, start + target_text_col) def insert(self, text): self.input = self.input[:self.cursor] + text + self.input[self.cursor:] @@ -338,19 +393,40 @@ class App: lines = self.chat_lines(self.display_messages(), max(1, self.s.getmaxyx()[1])) if not lines: return + row = self.history_cursor + col = self.history_col if key == curses.KEY_UP: - self.history_cursor = max(0, self.history_cursor - 1) + if row > 0: + row -= 1 + col = min(col, len(lines[row][1])) elif key == curses.KEY_DOWN: - self.history_cursor = min(len(lines) - 1, self.history_cursor + 1) + if row < len(lines) - 1: + row += 1 + col = min(col, len(lines[row][1])) elif key == curses.KEY_LEFT: - self.history_col = max(0, self.history_col - 1) + if col > 0: + col -= 1 + elif row > 0: + row -= 1 + col = len(lines[row][1]) elif key == curses.KEY_RIGHT: - self.history_col = min(len(lines[self.history_cursor][1]), self.history_col + 1) + if col < len(lines[row][1]): + col += 1 + elif row < len(lines) - 1: + row += 1 + col = 0 elif key == curses.KEY_PPAGE: - self.history_cursor = max(0, self.history_cursor - 5) + row = max(0, row - 5) + col = min(col, len(lines[row][1])) elif key == curses.KEY_NPAGE: - self.history_cursor = min(len(lines) - 1, self.history_cursor + 5) - self.scroll = max(0, self.history_cursor - 2) + row = min(len(lines) - 1, row + 5) + col = min(col, len(lines[row][1])) + self.history_cursor = row + self.history_col = col + height, width = self.s.getmaxyx() + input_height = min(max(1, height - 4), self.input_visual_height(max(1, width))) + available = max(1, height - 3 - input_height) + self.scroll = max(0, min(self.history_cursor, max(0, len(lines) - available))) def history_mark_start(self): self.history_mark = (self.history_cursor, self.history_col) @@ -379,12 +455,34 @@ class App: self.send_osc52(self.kill_buffer) def send_osc52(self, text): - # OSC 52 lets compatible terminals copy the selected history text - # to the system clipboard. Unsupported terminals simply ignore it. + # Keep a persistent clipboard for local/embedded terminals and also + # offer the terminal emulator clipboard through OSC 52 when supported. import base64 + import os + import shutil + import subprocess + + try: + clip = Path.home() / ".ai" / "clipboard" + clip.parent.mkdir(parents=True, exist_ok=True) + clip.write_text(text, encoding="utf-8") + except Exception: + pass + + for command in (("wl-copy",), ("xclip", "-selection", "clipboard"), + ("xsel", "--clipboard", "--input")): + if shutil.which(command[0]): + try: + subprocess.run(command, input=text, text=True, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, check=False) + break + except Exception: + pass + data = base64.b64encode(text.encode("utf-8")).decode("ascii") try: - print(f"\033]52;c;{data}\a", end="", flush=True) + os.write(1, f"\033]52;c;{data}\a".encode("ascii")) except Exception: pass @@ -443,11 +541,13 @@ class App: def handle_key(self, key): if self.prefix_ctrl_b: self.prefix_ctrl_b = False - if key == curses.KEY_UP: - self.history_focus("up") - return - if key == curses.KEY_DOWN: - self.history_focus("down") + if key in (curses.KEY_UP, curses.KEY_DOWN): + if self.focus == "history": + # Ctrl-B + arrow returns to the input field. Ctrl-B + # + arrow from the input enters history. + self.return_to_input() + else: + self.history_focus("up" if key == curses.KEY_UP else "down") return if key == "b": self.prefix_ctrl_b = True