diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b0ac3ed --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.aider* diff --git a/git_time_machine.py b/git_time_machine.py index 9bedc3c..e18692a 100644 --- a/git_time_machine.py +++ b/git_time_machine.py @@ -14,36 +14,66 @@ def get_file_at_commit(commit_hash, filename): def main(stdscr, filename): curses.curs_set(0) - curses.mousemask(curses.ALL_MOUSE_EVENTS | curses.REPORT_MOUSE_POSITION) + curses.mousemask(curses.ALL_MOUSE_EVENTS) curses.mouseinterval(0) stdscr.keypad(True) + + # Initialize colors if terminal supports them + if curses.has_colors(): + curses.use_default_colors() # Use terminal's default colors + curses.init_pair(1, 5, 7) # Focused status bar + curses.init_pair(2, curses.COLOR_WHITE, 8) # Unfocused status bar + + # Initialize key variable + key = 0 commits = get_commits(filename) selected_commit = 0 divider_col = 40 focus = "left" scroll_offset = 0 + left_scroll_offset = 0 # Scroll position for left pane dragging_divider = False + # Initialize file content + commit_hash = commits[selected_commit].split()[0] + file_lines = get_file_at_commit(commit_hash, filename) + + # Enable nodelay for smoother scrolling + stdscr.nodelay(True) + while True: - stdscr.clear() + stdscr.erase() # Use erase instead of clear for less flickering height, width = stdscr.getmaxyx() - # Fetch file content for selected commit - commit_hash = commits[selected_commit].split()[0] - file_lines = get_file_at_commit(commit_hash, filename) + # Only fetch file content when commit changes + if key in [curses.KEY_DOWN, curses.KEY_UP, ord('j'), ord('k')] and focus == "left": + commit_hash = commits[selected_commit].split()[0] + file_lines = get_file_at_commit(commit_hash, filename) + max_scroll = max(0, len(file_lines) - (height - 1)) scroll_offset = min(scroll_offset, max_scroll) visible_lines = file_lines[scroll_offset:scroll_offset + height - 1] + # Calculate visible commits for left pane + left_max_scroll = max(0, len(commits) - (height - 1)) + left_scroll_offset = min(left_scroll_offset, left_max_scroll) + + # Ensure selected commit is visible + if selected_commit < left_scroll_offset: + left_scroll_offset = selected_commit + elif selected_commit >= left_scroll_offset + height - 1: + left_scroll_offset = selected_commit - (height - 2) + # Draw commit list (left pane) - for i in range(min(len(commits), height - 1)): - line = commits[i] - if i == selected_commit: + visible_commits = commits[left_scroll_offset:left_scroll_offset + height - 1] + for i, line in enumerate(visible_commits): + display_index = i + left_scroll_offset + if display_index == selected_commit: stdscr.attron(curses.A_REVERSE) # Highlight selected commit stdscr.addnstr(i, 0, line, divider_col - 1) - if i == selected_commit: + if display_index == selected_commit: stdscr.attroff(curses.A_REVERSE) # Vertical divider @@ -55,74 +85,89 @@ def main(stdscr, filename): # Avoid errors when drawing at the last column pass - # Draw file content (right pane) + # Draw file content (right pane) - more efficiently + right_width = width - divider_col - 3 for i, line in enumerate(visible_lines): - stdscr.addnstr(i, divider_col + 2, line, width - divider_col - 3) + # Only draw what fits in the window + if i < height - 1: + stdscr.addnstr(i, divider_col + 2, line, right_width) - # Status bar for right pane + # Status bars for both panes visible_height = height - 1 # Reserve 1 line for the status bar + + # Right pane status last_visible_line = scroll_offset + visible_height - if len(file_lines) > 0: - percent = int((last_visible_line / len(file_lines)) * 100) - percent = 100 if last_visible_line >= len(file_lines) else percent + right_percent = int((last_visible_line / len(file_lines)) * 100) + right_percent = 100 if last_visible_line >= len(file_lines) else right_percent else: - percent = 0 - - status = f"{percent}%" - x = width - len(status) - 1 - stdscr.addnstr(height - 1, x, status, len(status), curses.A_REVERSE) + right_percent = 0 + right_status = f"{right_percent}% " + + # Left pane status + last_visible_commit = left_scroll_offset + visible_height + if len(commits) > 0: + left_percent = int((last_visible_commit / len(commits)) * 100) + left_percent = 100 if last_visible_commit >= len(commits) else left_percent + else: + left_percent = 0 + left_status = f"{left_percent}%" + + # Draw status bars - full width with different colors based on focus + left_attr = curses.color_pair(1) if focus == "left" else curses.color_pair(2) + right_attr = curses.color_pair(1) if focus == "right" else curses.color_pair(2) + + # Fill the entire bottom row for each pane + for x in range(divider_col): + stdscr.addch(height - 1, x, ' ', left_attr) + for x in range(divider_col + 1, width - 1): # Avoid the last column + stdscr.addch(height - 1, x, ' ', right_attr) + + # Add the percentage text + stdscr.addstr(height - 1, 1, left_status, left_attr) + stdscr.addstr(height - 1, width - len(right_status) - 1, right_status, right_attr) + + # Add divider character at the bottom row + stdscr.addch(height - 1, divider_col, divider_char) + # Get input with a small timeout for smoother scrolling + stdscr.timeout(50) # 50ms timeout key = stdscr.getch() + + # If no key was pressed, continue the loop + if key == -1: + continue # Mouse interaction if key == curses.KEY_MOUSE: try: _, mx, my, _, bstate = curses.getmouse() + # Handle divider dragging if bstate & curses.BUTTON1_PRESSED: - if mx == divider_col or (mx >= divider_col-1 and mx <= divider_col+1): + # Start dragging when clicked near divider + if abs(mx - divider_col) <= 1: # Allow clicking within 1 column of divider dragging_divider = True - # Always update divider position if dragging, regardless of button state + # If already dragging, update divider position if dragging_divider: + # Update divider position while dragging min_col = 10 max_col = width - 20 # leave space for right pane divider_col = max(min_col, min(mx, max_col)) + # Handle mouse release if bstate & curses.BUTTON1_RELEASED: - dragging_divider = False - elif bstate & curses.BUTTON1_CLICKED and not dragging_divider: - focus = "left" if mx < divider_col else "right" - except curses.error: - pass - - # Set timeout for more responsive updates during dragging - if dragging_divider: - stdscr.timeout(1) # Very short timeout for responsive updates - else: - stdscr.timeout(-1) # Blocking mode when not dragging - - # Handle divider dragging - check mouse position every iteration when dragging - if dragging_divider: - try: - # Get current mouse position - curses.mousemask(curses.ALL_MOUSE_EVENTS | curses.REPORT_MOUSE_POSITION) - _, mx, my, _, bstate = curses.getmouse() - - # Update divider position based on mouse x position - min_col = 10 - max_col = width - 20 # leave space for right pane - divider_col = max(min_col, min(mx, max_col)) - - # Check if mouse button has been released - if not (bstate & curses.BUTTON1_PRESSED): - dragging_divider = False + if dragging_divider: + dragging_divider = False + else: + # Change focus on mouse click (when not dragging) + focus = "left" if mx < divider_col else "right" except curses.error: pass # Exit on 'q' or ESC - if key in [ord('q'), 27]: + elif key in [ord('q'), 27]: break # Left pane movement @@ -135,6 +180,18 @@ def main(stdscr, filename): if selected_commit > 0: selected_commit -= 1 scroll_offset = 0 + elif key in [curses.KEY_NPAGE, ord(' ')]: + # Page down in left pane + new_selected = min(selected_commit + height - 1, len(commits) - 1) + if new_selected != selected_commit: + selected_commit = new_selected + scroll_offset = 0 + elif key in [curses.KEY_PPAGE, 8, 127, curses.KEY_SR]: + # Page up in left pane + new_selected = max(0, selected_commit - (height - 1)) + if new_selected != selected_commit: + selected_commit = new_selected + scroll_offset = 0 # Right pane scrolling elif focus == "right": @@ -146,7 +203,9 @@ def main(stdscr, filename): scroll_offset -= 1 elif key in [curses.KEY_NPAGE, ord(' ')]: scroll_offset = min(scroll_offset + height - 1, max_scroll) - elif key in [curses.KEY_PPAGE, 8, 127]: # Page Up or Shift+Space (some terminals) + elif key in [curses.KEY_PPAGE, 8, 127, curses.KEY_SR]: # Page Up, Backspace, Delete, or Shift+Up + scroll_offset = max(0, scroll_offset - (height - 1)) + elif key == curses.KEY_BACKSPACE: # Another way to detect Shift+Space in some terminals scroll_offset = max(0, scroll_offset - (height - 1)) # Pane switching