diff --git a/gtm b/gtm index 1bfa176..8b11863 100755 --- a/gtm +++ b/gtm @@ -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: