# by ChatGPT import curses import subprocess import sys def get_commits(filename): cmd = ['git', 'log', '--pretty=format:%h %ad %s', '--date=short', '--', filename] result = subprocess.run(cmd, capture_output=True, text=True) return result.stdout.splitlines() def get_file_at_commit(commit_hash, filename): cmd = ['git', 'show', f'{commit_hash}:{filename}'] result = subprocess.run(cmd, capture_output=True, text=True) return result.stdout.splitlines() def main(stdscr, filename): curses.curs_set(0) 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 # Initialize key variable key = 0 commits = get_commits(filename) selected_commit = 0 divider_col = 40 focus = "left" scroll_offset = 0 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.erase() # Use erase instead of clear for less flickering height, width = stdscr.getmaxyx() # 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] # Draw commit list (left pane) for i in range(min(len(commits), height - 1)): line = commits[i] if i == selected_commit: stdscr.attron(curses.A_REVERSE) # Highlight selected commit stdscr.addnstr(i, 0, line, divider_col - 1) if i == selected_commit: stdscr.attroff(curses.A_REVERSE) # Vertical divider divider_char = "║" if dragging_divider else "│" for y in range(height): stdscr.addch(y, divider_col, divider_char) # Draw file content (right pane) - more efficiently right_width = width - divider_col - 3 for i, line in enumerate(visible_lines): # 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 visible_height = height - 1 # Reserve 1 line for the status bar 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 else: percent = 0 status = f"{percent}%" x = width - len(status) - 1 stdscr.addnstr(height - 1, x, status, len(status), curses.A_REVERSE) # 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: # Start dragging when clicked near divider if abs(mx - divider_col) <= 1: # Allow clicking within 1 column of divider dragging_divider = True # 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: 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 elif key in [ord('q'), 27]: break # Left pane movement elif focus == "left": if key in [curses.KEY_DOWN, ord('j')]: if selected_commit < len(commits) - 1: selected_commit += 1 scroll_offset = 0 elif key in [curses.KEY_UP, ord('k')]: if selected_commit > 0: selected_commit -= 1 scroll_offset = 0 # Right pane scrolling elif focus == "right": if key in [curses.KEY_DOWN, ord('j')]: if scroll_offset < max_scroll: scroll_offset += 1 elif key in [curses.KEY_UP, ord('k')]: if scroll_offset > 0: 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, 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 if key in [curses.KEY_LEFT, ord('h')]: focus = "left" elif key in [curses.KEY_RIGHT, ord('l')]: focus = "right" stdscr.refresh() if __name__ == "__main__": if len(sys.argv) != 2: print("Usage: python git_time_machine.py path/to/file") sys.exit(1) filename = sys.argv[1] curses.wrapper(main, filename)