refactor: Decouple clipboard selection from screen rendering using data model

This commit is contained in:
n loewen (aider) 2025-06-08 02:17:21 +01:00
parent 217174c647
commit cfa96a436f
1 changed files with 99 additions and 46 deletions

145
gtm
View File

@ -622,66 +622,119 @@ def copy_selection_to_clipboard(stdscr, state):
start_x, start_y = state.selection_start_coord
end_x, end_y = state.selection_end_coord
# Determine pane boundaries based on sidebar visibility
# Determine pane from where selection started
if state.show_sidebar:
# Determine pane from where selection started
pane = 'left' if start_x < state.divider_col else 'right'
if pane == 'left':
pane_x1, pane_x2 = 0, state.divider_col - 1
else: # right
pane_x1, pane_x2 = state.divider_col + 2, state.width - 1
else:
# When sidebar is hidden, there's only the right pane
pane = 'right'
pane_x1, pane_x2 = 0, state.width - 1
pane = 'right' # When sidebar is hidden, there's only the right pane
# Determine drag direction to handle multi-line selection correctly
if start_y < end_y or (start_y == end_y and start_x <= end_x):
drag_start_x, drag_start_y = start_x, start_y
drag_end_x, drag_end_y = end_x, end_y
else: # upward drag or right-to-left on same line
drag_start_x, drag_start_y = end_x, end_y
drag_end_x, drag_end_y = start_x, start_y
if start_y > end_y or (start_y == end_y and start_x > end_x):
# Swap coordinates if selection is bottom-to-top or right-to-left
start_x, start_y, end_x, end_y = end_x, end_y, start_x, start_y
height, width = stdscr.getmaxyx()
# Get the selected text from the data model
selected_text_parts = []
for y in range(drag_start_y, drag_end_y + 1):
if not (0 <= y < height):
continue
x1, x2 = -1, -1
if drag_start_y == drag_end_y: # single line selection
x1, x2 = drag_start_x, drag_end_x
elif y == drag_start_y: # first line of multi-line selection
x1, x2 = drag_start_x, pane_x2
elif y == drag_end_y: # last line of multi-line selection
x1, x2 = pane_x1, drag_end_x
else: # middle line of multi-line selection
x1, x2 = pane_x1, pane_x2
if pane == 'left' and state.show_sidebar:
# Selection in the commits pane
visible_commits = state.commits[state.left_scroll_offset:state.left_scroll_offset + state.height - 1]
for i in range(start_y, min(end_y + 1, len(visible_commits))):
line_idx = i + state.left_scroll_offset
if 0 <= line_idx < len(state.commits):
line = state.commits[line_idx]
# Calculate character positions
start_char = 0 if i > start_y else start_x
end_char = len(line) if i < end_y else end_x + 1
# Get the substring
if start_char < len(line):
selected_text_parts.append(line[start_char:min(end_char, len(line))])
else:
# Selection in the file content pane
# First, build a list of all visible lines including deleted lines if shown
visible_lines = []
deleted_line_map = {}
# Clamp selection to pane boundaries and screen width
x1 = max(x1, pane_x1)
x2 = min(x2, pane_x2, width - 1)
line_str = ""
if x1 <= x2:
for x in range(x1, x2 + 1):
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)
if state.show_whole_diff or state.show_deletions:
for del_line_num, del_content in state.deleted_lines:
if del_line_num not in deleted_line_map:
deleted_line_map[del_line_num] = []
deleted_line_map[del_line_num].append(del_content)
# Build the list of visible lines with their types
file_lines = state.file_lines[state.right_scroll_offset:state.right_scroll_offset + state.height - 1]
display_lines = []
for i, line in enumerate(file_lines):
line_num = i + state.right_scroll_offset + 1
# Add deleted lines if they should be shown
if (state.show_whole_diff or state.show_deletions) and line_num in deleted_line_map:
for del_content in deleted_line_map[line_num]:
display_lines.append({'type': 'deleted', 'content': del_content})
# Determine if this is an added line
is_added = False
if state.show_whole_diff or state.show_additions:
for added_line_num, _ in state.added_lines:
if added_line_num == line_num:
is_added = True
break
display_lines.append({'type': 'added' if is_added else 'regular', 'content': line})
# Now extract the selected text from these lines
right_start = 0 if not state.show_sidebar else state.divider_col + 2
for i in range(start_y, min(end_y + 1, len(display_lines))):
if i < 0 or i >= len(display_lines):
continue
line_info = display_lines[i]
content = line_info['content']
line_type = line_info['type']
# Add prefix based on line type if in diff mode
prefix = ""
if line_type == 'added' and (state.show_whole_diff or state.show_additions):
prefix = "+ "
elif line_type == 'deleted' and (state.show_whole_diff or state.show_deletions):
prefix = "- "
elif state.show_whole_diff or state.show_additions or state.show_deletions:
prefix = " "
# Calculate character positions, accounting for the prefix
content_with_prefix = prefix + content
# Adjust start_x and end_x to account for the right pane offset
adjusted_start_x = max(0, start_x - right_start) if i == start_y else 0
adjusted_end_x = end_x - right_start if i == end_y else len(content_with_prefix)
# Ensure we don't go out of bounds
adjusted_start_x = min(adjusted_start_x, len(content_with_prefix))
adjusted_end_x = min(adjusted_end_x, len(content_with_prefix))
if adjusted_start_x < adjusted_end_x:
selected_text_parts.append(content_with_prefix[adjusted_start_x:adjusted_end_x])
# Join the selected text parts and copy to clipboard
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
try:
# Try xclip for Linux systems
subprocess.run(['xclip', '-selection', 'clipboard'], input=text_to_copy, text=True, check=True)
except (FileNotFoundError, subprocess.CalledProcessError):
try:
# Try clip.exe for Windows
subprocess.run(['clip.exe'], input=text_to_copy, text=True, check=True)
except (FileNotFoundError, subprocess.CalledProcessError):
pass # Silently fail if no clipboard command is available
def handle_mouse_input(stdscr, state: AppState) -> AppState:
try: