From 66940d44f9a0dfea477c8fbe54107137b3584c5e Mon Sep 17 00:00:00 2001 From: "n loewen (aider)" Date: Sat, 7 Jun 2025 21:27:46 +0100 Subject: [PATCH] feat: Implement mouse-based text selection and clipboard copying --- gtm2.py | 84 ++++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 80 insertions(+), 4 deletions(-) diff --git a/gtm2.py b/gtm2.py index 558b082..bdae00a 100755 --- a/gtm2.py +++ b/gtm2.py @@ -118,6 +118,10 @@ class AppState: self.dragging_divider = False self.should_exit = False + self.is_selecting = False + self.selection_start_coord = None + self.selection_end_coord = None + def update_dimensions(self, height, width): self.height = height self.width = width @@ -268,6 +272,28 @@ def draw_divider(stdscr, state): except curses.error: pass +def draw_selection(stdscr, state): + if not state.is_selecting or not state.selection_start_coord: + return + + start_x, start_y = state.selection_start_coord + end_x, end_y = state.selection_end_coord + + x1, x2 = min(start_x, end_x), max(start_x, end_x) + y1, y2 = min(start_y, end_y), max(start_y, end_y) + + for y in range(y1, y2 + 1): + try: + # chgat can fail at the bottom-right corner of the screen + if x1 < state.width: + # `width-1` is the last valid column + end_col = min(x2, state.width - 1) + length = end_col - x1 + 1 + if length > 0: + stdscr.chgat(y, x1, length, curses.A_REVERSE) + except curses.error: + pass + def draw_status_bars(stdscr, state): visible_height = state.height - 1 @@ -308,26 +334,76 @@ def draw_ui(stdscr, state): draw_right_pane(stdscr, state) draw_divider(stdscr, state) draw_status_bars(stdscr, state) + draw_selection(stdscr, state) stdscr.refresh() # --- Input Handling Functions (Controller) --- -def handle_mouse_input(state): +def copy_selection_to_clipboard(stdscr, state): + if not state.selection_start_coord or not state.selection_end_coord: + return + + start_x, start_y = state.selection_start_coord + end_x, end_y = state.selection_end_coord + + x1, x2 = min(start_x, end_x), max(start_x, end_x) + y1, y2 = min(start_y, end_y), max(start_y, end_y) + + height, width = stdscr.getmaxyx() + selected_text_parts = [] + + for y in range(y1, y2 + 1): + if 0 <= y < height: + line_str = "" + for x in range(x1, x2 + 1): + if 0 <= x < width: + try: + char_and_attr = stdscr.inch(y, x) + char = char_and_attr & 0xFF + line_str += chr(char) + except curses.error: + line_str += " " + selected_text_parts.append(line_str.rstrip()) + + if selected_text_parts: + text_to_copy = "\n".join(selected_text_parts) + if text_to_copy.strip(): + try: + subprocess.run(['pbcopy'], input=text_to_copy, text=True, check=True) + except (FileNotFoundError, subprocess.CalledProcessError): + pass + +def handle_mouse_input(stdscr, state): try: _, mx, my, _, bstate = curses.getmouse() if bstate & curses.BUTTON1_PRESSED: if abs(mx - state.divider_col) <= 1: state.dragging_divider = True + else: + state.is_selecting = True + state.selection_start_coord = (mx, my) + state.selection_end_coord = (mx, my) if state.dragging_divider: state.update_divider(mx) + elif state.is_selecting: + state.selection_end_coord = (mx, my) if bstate & curses.BUTTON1_RELEASED: if state.dragging_divider: state.dragging_divider = False - else: - state.focus = "left" if mx < state.divider_col else "right" + elif state.is_selecting: + state.is_selecting = False + start_x, start_y = state.selection_start_coord + + if start_x == mx and start_y == my: + state.focus = "left" if mx < state.divider_col else "right" + else: + copy_selection_to_clipboard(stdscr, state) + + state.selection_start_coord = None + state.selection_end_coord = None except curses.error: pass @@ -397,7 +473,7 @@ def main(stdscr, filename, show_diff, show_add, show_del, mouse): continue if state.enable_mouse and key == curses.KEY_MOUSE: - handle_mouse_input(state) + handle_mouse_input(stdscr, state) else: handle_keyboard_input(key, state)