diff --git a/gtm b/gtm index aba3b52..0311211 100755 --- a/gtm +++ b/gtm @@ -134,25 +134,47 @@ class AppState: wrap_lines: bool = True # Change navigation - change_positions: List[int] = field(default_factory=list) + change_blocks: List[Tuple[int, int]] = field(default_factory=list) # (start_line, end_line) of each change block current_change_idx: int = -1 # -1 means no change is selected # --- Actions (Controller) --- -def calculate_change_positions(state: AppState) -> AppState: - """Calculate positions of all changes (additions and deletions) in the file.""" - positions = [] - # Add positions of added lines +def calculate_change_blocks(state: AppState) -> AppState: + """Calculate positions of change blocks (consecutive changes) in the file.""" + if not state.added_lines and not state.deleted_lines: + return replace(state, change_blocks=[], current_change_idx=-1) + + # Collect all change positions + all_changes = [] for line_num, _ in state.added_lines: - if line_num not in positions: - positions.append(line_num) - # Add positions of deleted lines + all_changes.append(line_num) for line_num, _ in state.deleted_lines: - if line_num not in positions: - positions.append(line_num) - # Sort positions - positions.sort() - return replace(state, change_positions=positions, current_change_idx=-1) + all_changes.append(line_num) + + # Sort all positions + all_changes.sort() + + if not all_changes: + return replace(state, change_blocks=[], current_change_idx=-1) + + # Group into blocks (changes within 3 lines of each other are considered one block) + blocks = [] + current_block_start = all_changes[0] + current_block_end = all_changes[0] + + for pos in all_changes[1:]: + if pos <= current_block_end + 3: # Within 3 lines of previous change + current_block_end = pos + else: + # Start a new block + blocks.append((current_block_start, current_block_end)) + current_block_start = pos + current_block_end = pos + + # Add the last block + blocks.append((current_block_start, current_block_end)) + + return replace(state, change_blocks=blocks, current_change_idx=-1) def load_commit_content(state: AppState) -> AppState: if not state.commits: @@ -199,8 +221,8 @@ def load_commit_content(state: AppState) -> AppState: right_scroll_offset=right_scroll_offset ) - # Calculate change positions for navigation - return calculate_change_positions(new_state) + # Calculate change blocks for navigation + return calculate_change_blocks(new_state) def update_dimensions(state: AppState, height: int, width: int) -> AppState: return replace(state, height=height, width=width) @@ -263,52 +285,54 @@ def draw_left_pane(stdscr, state): stdscr.attroff(curses.A_REVERSE) def jump_to_next_change(state: AppState) -> AppState: - """Jump to the next change after the current scroll position.""" - if not state.change_positions: + """Jump to the next change block after the current scroll position.""" + if not state.change_blocks: return state - # Find the next change after current position + # Find the next change block after current position current_pos = state.right_scroll_offset next_idx = -1 - for i, pos in enumerate(state.change_positions): - if pos > current_pos: + for i, (start_pos, _) in enumerate(state.change_blocks): + if start_pos > current_pos: next_idx = i break # If no next change found, wrap to the first change - if next_idx == -1 and state.change_positions: + if next_idx == -1 and state.change_blocks: next_idx = 0 if next_idx != -1: - new_scroll = state.change_positions[next_idx] + # Get the start position of the change block + new_scroll = max(0, state.change_blocks[next_idx][0] - 3) # Show 3 lines of context above return replace(state, right_scroll_offset=new_scroll, current_change_idx=next_idx) return state def jump_to_prev_change(state: AppState) -> AppState: - """Jump to the previous change before the current scroll position.""" - if not state.change_positions: + """Jump to the previous change block before the current scroll position.""" + if not state.change_blocks: return state - # Find the previous change before current position + # Find the previous change block before current position current_pos = state.right_scroll_offset prev_idx = -1 - for i in range(len(state.change_positions) - 1, -1, -1): - if state.change_positions[i] < current_pos: + for i in range(len(state.change_blocks) - 1, -1, -1): + if state.change_blocks[i][0] < current_pos: prev_idx = i break # If no previous change found, wrap to the last change - if prev_idx == -1 and state.change_positions: - prev_idx = len(state.change_positions) - 1 + if prev_idx == -1 and state.change_blocks: + prev_idx = len(state.change_blocks) - 1 if prev_idx != -1: - new_scroll = state.change_positions[prev_idx] + # Get the start position of the change block + new_scroll = max(0, state.change_blocks[prev_idx][0] - 3) # Show 3 lines of context above return replace(state, right_scroll_offset=new_scroll, current_change_idx=prev_idx) return state def draw_change_markers(stdscr, state): """Draw markers in the scrollbar area to indicate where changes are located.""" - if not state.change_positions or not (state.show_whole_diff or state.show_additions or state.show_deletions): + if not state.change_blocks or not (state.show_whole_diff or state.show_additions or state.show_deletions): return right_start = 0 if not state.show_sidebar else state.divider_col + 2 @@ -323,14 +347,11 @@ def draw_change_markers(stdscr, state): scale_factor = (state.height - 1) / total_lines # Draw markers - for i, pos in enumerate(state.change_positions): - marker_y = int(pos * scale_factor) + for i, (start_pos, _) in enumerate(state.change_blocks): + marker_y = int(start_pos * scale_factor) if 0 <= marker_y < state.height - 1: - # Determine marker color based on change type - is_addition = any(added_line_num == pos for added_line_num, _ in state.added_lines) - is_deletion = any(del_line_num == pos for del_line_num, _ in state.deleted_lines) - - attr = curses.color_pair(3) if is_addition else curses.color_pair(4) + # Use a single color for change blocks + attr = curses.color_pair(5) # New color pair for change blocks marker_char = '>' if i == state.current_change_idx else '|' try: @@ -516,8 +537,8 @@ def draw_status_bars(stdscr, state): # Add wrap indicator and change position to commit message wrap_indicator = " [W] " if state.wrap_lines else " [NW] " change_indicator = "" - if state.change_positions and state.current_change_idx != -1: - change_indicator = f" [Change {state.current_change_idx + 1}/{len(state.change_positions)}] " + if state.change_blocks and state.current_change_idx != -1: + change_indicator = f" [Block {state.current_change_idx + 1}/{len(state.change_blocks)}] " commit_message = wrap_indicator + change_indicator + commit_message @@ -594,7 +615,9 @@ def draw_ui(stdscr, state): draw_left_pane(stdscr, state) draw_right_pane(stdscr, state) draw_divider(stdscr, state) - draw_change_markers(stdscr, state) + # Only draw change markers in the right pane + if state.focus == "right": + draw_change_markers(stdscr, state) draw_status_bars(stdscr, state) draw_selection(stdscr, state) stdscr.refresh() @@ -795,8 +818,9 @@ def main(stdscr, filename, show_diff, show_add, show_del, mouse, wrap_lines=True # Use a safe background color (7 or less) to avoid "Color number is greater than COLORS-1" error curses.init_pair(1, curses.COLOR_BLACK, 7) # Active pane: black on white curses.init_pair(2, curses.COLOR_WHITE, 0) # Inactive pane: white on black - curses.init_pair(3, curses.COLOR_GREEN, -1) - curses.init_pair(4, curses.COLOR_RED, -1) + curses.init_pair(3, curses.COLOR_GREEN, -1) # Added lines + curses.init_pair(4, curses.COLOR_RED, -1) # Deleted lines + curses.init_pair(5, curses.COLOR_YELLOW, -1) # Change block markers height, width = stdscr.getmaxyx() state = AppState(