From bca2255f5844170bd2faaf005ac90a2a4c7e2d15 Mon Sep 17 00:00:00 2001 From: "n loewen (aider)" Date: Mon, 5 May 2025 09:01:23 +0100 Subject: [PATCH 01/14] feat: Improve divider dragging with real-time mouse tracking and responsive UI --- git_time_machine.py | 56 ++++++++++++++++++--------------------------- 1 file changed, 22 insertions(+), 34 deletions(-) diff --git a/git_time_machine.py b/git_time_machine.py index ea1f503..f9d773e 100644 --- a/git_time_machine.py +++ b/git_time_machine.py @@ -19,6 +19,8 @@ def main(stdscr, filename): curses.mousemask(curses.ALL_MOUSE_EVENTS | curses.REPORT_MOUSE_POSITION) curses.mouseinterval(0) stdscr.keypad(True) + # Default to blocking mode (-1) + stdscr.timeout(-1) commits = get_commits(filename) selected_commit = 0 @@ -78,9 +80,12 @@ def main(stdscr, filename): try: _, mx, my, _, bstate = curses.getmouse() + # Start dragging when mouse is pressed on or near divider if bstate & curses.BUTTON1_PRESSED: - if mx == divider_col: + if abs(mx - divider_col) <= 1: # Allow clicking within 1 column of divider dragging_divider = True + # Set short timeout for responsive updates during dragging + stdscr.timeout(1) # Update divider position while dragging if dragging_divider: @@ -88,57 +93,40 @@ def main(stdscr, filename): max_col = width - 20 # leave space for right pane divider_col = max(min_col, min(mx, max_col)) + # Stop dragging when mouse is released if bstate & curses.BUTTON1_RELEASED: - dragging_divider = False + if dragging_divider: + dragging_divider = False + # Reset to blocking mode when done dragging + stdscr.timeout(-1) elif bstate & curses.BUTTON1_CLICKED and not dragging_divider: focus = "left" if mx < divider_col else "right" except curses.error: pass - # Continue updating divider position even without explicit mouse events - elif dragging_divider: + # When no key is pressed but we're in dragging mode (timeout returns -1) + elif key == -1 and dragging_divider: try: + # Get current mouse position and update divider _, mx, my, _, bstate = curses.getmouse() - # Update divider position + # Update divider position based on current mouse position min_col = 10 max_col = width - 20 # leave space for right pane divider_col = max(min_col, min(mx, max_col)) # Check for mouse button release - if bstate & curses.BUTTON1_RELEASED or not (bstate & curses.BUTTON1_PRESSED): + if not (bstate & curses.BUTTON1_PRESSED): dragging_divider = False + # Reset to blocking mode when done dragging + stdscr.timeout(-1) except curses.error: - # If we can't get mouse state, keep the divider where it is + # If we can't get mouse state, continue pass -# -# # Mouse click changes focus -# if key == curses.KEY_MOUSE: -# try: -# _, mx, my, _, bstate = curses.getmouse() -# -# if bstate & curses.BUTTON1_PRESSED: -# if dragging_divider: -# # Update divider while dragging -# min_col = 10 -# max_col = width - 20 # leave space for right pane -# divider_col = max(min_col, min(mx, max_col)) -# elif mx == divider_col: -# dragging_divider = True -# -# elif bstate & curses.BUTTON1_RELEASED: -# dragging_divider = False -# -# elif bstate & curses.BUTTON1_CLICKED: -# focus = "left" if mx < divider_col else "right" -# -# except curses.error: -# pass -# -# -# elif key in [ord('q'), 27]: -# break + # Exit on 'q' or ESC + elif key in [ord('q'), 27]: + break # Left pane movement elif focus == "left": From 8fa0ed1fc559e60581460976087144ef50ff334a Mon Sep 17 00:00:00 2001 From: "n loewen (aider)" Date: Mon, 5 May 2025 09:03:30 +0100 Subject: [PATCH 02/14] feat: reinstate mouse click to change panel focus --- git_time_machine.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/git_time_machine.py b/git_time_machine.py index f9d773e..dc8c161 100644 --- a/git_time_machine.py +++ b/git_time_machine.py @@ -99,8 +99,9 @@ def main(stdscr, filename): dragging_divider = False # Reset to blocking mode when done dragging stdscr.timeout(-1) - elif bstate & curses.BUTTON1_CLICKED and not dragging_divider: - focus = "left" if mx < divider_col else "right" + else: + # Change focus on mouse click (when not dragging) + focus = "left" if mx < divider_col else "right" except curses.error: pass From 7b4b95102565e176d8992af2147cf06ef43b5eb2 Mon Sep 17 00:00:00 2001 From: "n loewen (aider)" Date: Mon, 5 May 2025 09:08:13 +0100 Subject: [PATCH 03/14] refactor: Simplify mouse event handling and remove real-time divider tracking --- git_time_machine.py | 40 +++++++--------------------------------- 1 file changed, 7 insertions(+), 33 deletions(-) diff --git a/git_time_machine.py b/git_time_machine.py index dc8c161..3c3b732 100644 --- a/git_time_machine.py +++ b/git_time_machine.py @@ -16,11 +16,9 @@ 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) - # Default to blocking mode (-1) - stdscr.timeout(-1) commits = get_commits(filename) selected_commit = 0 @@ -80,51 +78,27 @@ def main(stdscr, filename): try: _, mx, my, _, bstate = curses.getmouse() - # Start dragging when mouse is pressed on or near divider + # Handle divider dragging if bstate & curses.BUTTON1_PRESSED: if abs(mx - divider_col) <= 1: # Allow clicking within 1 column of divider dragging_divider = True - # Set short timeout for responsive updates during dragging - stdscr.timeout(1) - - # Update divider position while dragging - if dragging_divider: + + # Update divider position on drag + elif dragging_divider and (bstate & curses.BUTTON1_PRESSED): min_col = 10 max_col = width - 20 # leave space for right pane divider_col = max(min_col, min(mx, max_col)) - # Stop dragging when mouse is released - if bstate & curses.BUTTON1_RELEASED: + # Handle mouse release + elif bstate & curses.BUTTON1_RELEASED: if dragging_divider: dragging_divider = False - # Reset to blocking mode when done dragging - stdscr.timeout(-1) else: # Change focus on mouse click (when not dragging) focus = "left" if mx < divider_col else "right" except curses.error: pass - # When no key is pressed but we're in dragging mode (timeout returns -1) - elif key == -1 and dragging_divider: - try: - # Get current mouse position and update divider - _, mx, my, _, bstate = curses.getmouse() - - # Update divider position based on current mouse position - min_col = 10 - max_col = width - 20 # leave space for right pane - divider_col = max(min_col, min(mx, max_col)) - - # Check for mouse button release - if not (bstate & curses.BUTTON1_PRESSED): - dragging_divider = False - # Reset to blocking mode when done dragging - stdscr.timeout(-1) - except curses.error: - # If we can't get mouse state, continue - pass - # Exit on 'q' or ESC elif key in [ord('q'), 27]: break From 5d104026dc6ce2c96805c5800ba947a4fda93f72 Mon Sep 17 00:00:00 2001 From: "n loewen (aider)" Date: Mon, 5 May 2025 09:08:49 +0100 Subject: [PATCH 04/14] fix: correct divider dragging logic to enable movement --- git_time_machine.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/git_time_machine.py b/git_time_machine.py index 3c3b732..cf50ee1 100644 --- a/git_time_machine.py +++ b/git_time_machine.py @@ -82,12 +82,11 @@ def main(stdscr, filename): if bstate & curses.BUTTON1_PRESSED: if abs(mx - divider_col) <= 1: # Allow clicking within 1 column of divider dragging_divider = True - - # Update divider position on drag - elif dragging_divider and (bstate & curses.BUTTON1_PRESSED): - min_col = 10 - max_col = width - 20 # leave space for right pane - divider_col = max(min_col, min(mx, max_col)) + elif 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 elif bstate & curses.BUTTON1_RELEASED: From 58173af81b7f94d6e6905bd1d496addd7ac9148f Mon Sep 17 00:00:00 2001 From: "n loewen (aider)" Date: Mon, 5 May 2025 09:09:25 +0100 Subject: [PATCH 05/14] fix: Improve divider dragging logic in mouse event handling --- git_time_machine.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/git_time_machine.py b/git_time_machine.py index cf50ee1..4a0bdd7 100644 --- a/git_time_machine.py +++ b/git_time_machine.py @@ -80,13 +80,16 @@ def main(stdscr, filename): # 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 - elif 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)) + + # 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 elif bstate & curses.BUTTON1_RELEASED: From ac52d157665b824b45ebc5cef3af43eed7d87fde Mon Sep 17 00:00:00 2001 From: "n loewen (aider)" Date: Mon, 5 May 2025 09:10:02 +0100 Subject: [PATCH 06/14] fix: ensure divider stops dragging on mouse button release --- git_time_machine.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/git_time_machine.py b/git_time_machine.py index 4a0bdd7..e932a7a 100644 --- a/git_time_machine.py +++ b/git_time_machine.py @@ -92,7 +92,7 @@ def main(stdscr, filename): divider_col = max(min_col, min(mx, max_col)) # Handle mouse release - elif bstate & curses.BUTTON1_RELEASED: + if bstate & curses.BUTTON1_RELEASED: if dragging_divider: dragging_divider = False else: From df1a0d4ad3cfc241e288962bc621f994a073be60 Mon Sep 17 00:00:00 2001 From: "n loewen (aider)" Date: Mon, 5 May 2025 09:11:17 +0100 Subject: [PATCH 07/14] feat: add terminal default color support in curses initialization --- git_time_machine.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/git_time_machine.py b/git_time_machine.py index e932a7a..17cdce7 100644 --- a/git_time_machine.py +++ b/git_time_machine.py @@ -19,6 +19,10 @@ def main(stdscr, filename): 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 commits = get_commits(filename) selected_commit = 0 From b6ec425d913a6475ef3b1af6cfb626403fa013b7 Mon Sep 17 00:00:00 2001 From: "n loewen (aider)" Date: Mon, 5 May 2025 09:13:00 +0100 Subject: [PATCH 08/14] feat: Optimize scrolling performance and reduce screen flickering --- git_time_machine.py | 33 +++++++++++++++++++++++++++------ 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/git_time_machine.py b/git_time_machine.py index 17cdce7..7a44ed1 100644 --- a/git_time_machine.py +++ b/git_time_machine.py @@ -23,6 +23,9 @@ def main(stdscr, filename): # 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 @@ -32,13 +35,22 @@ def main(stdscr, filename): 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] @@ -57,9 +69,12 @@ def main(stdscr, filename): for y in range(height): stdscr.addch(y, divider_col, divider_char) - # 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 visible_height = height - 1 # Reserve 1 line for the status bar @@ -75,7 +90,13 @@ def main(stdscr, filename): 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: From 07127c1d1bf8b39f98a304a0d706f84ff8c348aa Mon Sep 17 00:00:00 2001 From: "n loewen (aider)" Date: Mon, 5 May 2025 09:14:00 +0100 Subject: [PATCH 09/14] feat: add support for scrolling up with Shift+Space in different terminals --- git_time_machine.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/git_time_machine.py b/git_time_machine.py index 7a44ed1..f36e7fb 100644 --- a/git_time_machine.py +++ b/git_time_machine.py @@ -151,7 +151,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 From bc6ec6d847f1b416ca53796ef0e6eac308fb746a Mon Sep 17 00:00:00 2001 From: "n loewen (aider)" Date: Mon, 5 May 2025 09:18:18 +0100 Subject: [PATCH 10/14] feat: implement vertical scrolling for commit list in left pane --- git_time_machine.py | 62 +++++++++++++++++++++++++++++++++++---------- 1 file changed, 49 insertions(+), 13 deletions(-) diff --git a/git_time_machine.py b/git_time_machine.py index f36e7fb..1ea828a 100644 --- a/git_time_machine.py +++ b/git_time_machine.py @@ -32,6 +32,7 @@ def main(stdscr, filename): divider_col = 40 focus = "left" scroll_offset = 0 + left_scroll_offset = 0 # Scroll position for left pane dragging_divider = False @@ -55,13 +56,24 @@ def main(stdscr, filename): 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 @@ -76,19 +88,31 @@ def main(stdscr, filename): 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 + stdscr.addnstr(height - 1, 1, left_status, len(left_status), curses.A_REVERSE) + x = width - len(right_status) - 1 + stdscr.addnstr(height - 1, x, right_status, len(right_status), curses.A_REVERSE) # Get input with a small timeout for smoother scrolling stdscr.timeout(50) # 50ms timeout @@ -140,6 +164,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": From 0025c4e437956cd121bf5ee7e2676f059bcf863f Mon Sep 17 00:00:00 2001 From: "n loewen (aider)" Date: Mon, 5 May 2025 09:22:39 +0100 Subject: [PATCH 11/14] feat: add colored status bars with focus-based highlighting --- git_time_machine.py | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/git_time_machine.py b/git_time_machine.py index 1ea828a..17df67e 100644 --- a/git_time_machine.py +++ b/git_time_machine.py @@ -23,6 +23,8 @@ def main(stdscr, filename): # Initialize colors if terminal supports them if curses.has_colors(): curses.use_default_colors() # Use terminal's default colors + curses.init_pair(1, curses.COLOR_WHITE, curses.COLOR_BLACK) # Focused status bar + curses.init_pair(2, curses.COLOR_WHITE, curses.COLOR_BLUE) # Unfocused status bar # Initialize key variable key = 0 @@ -109,10 +111,22 @@ def main(stdscr, filename): left_percent = 0 left_status = f"{left_percent}%" - # Draw status bars - stdscr.addnstr(height - 1, 1, left_status, len(left_status), curses.A_REVERSE) - x = width - len(right_status) - 1 - stdscr.addnstr(height - 1, x, right_status, len(right_status), curses.A_REVERSE) + # 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): + 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 From 78ea389c82a51964dbee7042105c7e410450b07e Mon Sep 17 00:00:00 2001 From: "n loewen (aider)" Date: Mon, 5 May 2025 09:23:06 +0100 Subject: [PATCH 12/14] fix: prevent curses error by avoiding writing to last terminal column --- git_time_machine.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/git_time_machine.py b/git_time_machine.py index 17df67e..58aea1c 100644 --- a/git_time_machine.py +++ b/git_time_machine.py @@ -118,7 +118,7 @@ def main(stdscr, filename): # 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): + for x in range(divider_col + 1, width - 1): # Avoid the last column stdscr.addch(height - 1, x, ' ', right_attr) # Add the percentage text From 831cbb1d762b68b84125c5ff80d4deee55280901 Mon Sep 17 00:00:00 2001 From: n loewen Date: Mon, 5 May 2025 09:35:38 +0100 Subject: [PATCH 13/14] Make status bar indicate which pane is focused --- git_time_machine.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/git_time_machine.py b/git_time_machine.py index 58aea1c..e8fbd52 100644 --- a/git_time_machine.py +++ b/git_time_machine.py @@ -23,8 +23,8 @@ def main(stdscr, filename): # Initialize colors if terminal supports them if curses.has_colors(): curses.use_default_colors() # Use terminal's default colors - curses.init_pair(1, curses.COLOR_WHITE, curses.COLOR_BLACK) # Focused status bar - curses.init_pair(2, curses.COLOR_WHITE, curses.COLOR_BLUE) # Unfocused status bar + 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 @@ -100,7 +100,7 @@ def main(stdscr, filename): right_percent = 100 if last_visible_line >= len(file_lines) else right_percent else: right_percent = 0 - right_status = f"{right_percent}%" + right_status = f"{right_percent}% " # Left pane status last_visible_commit = left_scroll_offset + visible_height From f0e5cc0abd5027ee0152d2229529de5a3bf0efc5 Mon Sep 17 00:00:00 2001 From: n loewen Date: Mon, 5 May 2025 10:59:19 +0100 Subject: [PATCH 14/14] Add gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b0ac3ed --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.aider*