feat: Add second status bar with commit details and resizable height

This commit is contained in:
n loewen (aider) 2025-06-08 08:48:29 +01:00
parent 6c319c1570
commit c945c89b74
1 changed files with 172 additions and 45 deletions

217
gtm
View File

@ -24,6 +24,36 @@ def get_commits(filename):
result = subprocess.run(cmd, capture_output=True, text=True)
return result.stdout.splitlines()
def get_commit_details(commit_hash):
"""Get detailed information about a specific commit."""
# Get commit author
author_cmd = ['git', 'show', '-s', '--format=%an <%ae>', commit_hash]
author_result = subprocess.run(author_cmd, capture_output=True, text=True)
author = author_result.stdout.strip()
# Get branch information
branch_cmd = ['git', 'branch', '--contains', commit_hash]
branch_result = subprocess.run(branch_cmd, capture_output=True, text=True)
branches = [b.strip() for b in branch_result.stdout.splitlines()]
# Find the current branch (marked with *)
current_branch = "detached"
for branch in branches:
if branch.startswith('*'):
current_branch = branch[1:].strip()
break
# Get full commit message
message_cmd = ['git', 'show', '-s', '--format=%B', commit_hash]
message_result = subprocess.run(message_cmd, capture_output=True, text=True)
message = message_result.stdout.strip()
return {
'author': author,
'branch': current_branch,
'message': message
}
def get_file_at_commit(commit_hash, filename):
cmd = ['git', 'show', f'{commit_hash}:{filename}']
result = subprocess.run(cmd, capture_output=True, text=True)
@ -147,6 +177,16 @@ class AppState:
# Help popup
show_help: bool = False
# Commit details
commit_hash: str = ""
commit_message: str = ""
commit_author: str = ""
commit_branch: str = ""
# Status bar settings
status_bar_height: int = 2 # Default height for the status bar (2 lines)
dragging_status_bar: bool = False # Flag to track if user is resizing the status bar
# --- Actions (Controller) ---
@ -202,7 +242,7 @@ def load_commit_content(state: AppState) -> AppState:
reference_line = None
scroll_percentage = 0
if len(state.file_lines) > 0:
max_scroll_old = max(0, len(state.file_lines) - (state.height - 1))
max_scroll_old = max(0, len(state.file_lines) - (state.height - state.status_bar_height))
if max_scroll_old > 0:
scroll_percentage = state.right_scroll_offset / max_scroll_old
if state.right_scroll_offset < len(state.file_lines):
@ -220,7 +260,15 @@ def load_commit_content(state: AppState) -> AppState:
else:
added_lines, deleted_lines = [], []
max_scroll_new = max(0, len(file_lines) - (state.height - 1))
# Get commit details
commit_details = get_commit_details(commit_hash)
# Get commit message from the commit line
commit_line = state.commits[state.selected_commit_idx]
parts = commit_line.split(' ', 3) # Split into hash, date, time, message
short_message = parts[3] if len(parts) >= 4 else ""
max_scroll_new = max(0, len(file_lines) - (state.height - state.status_bar_height))
right_scroll_offset = state.right_scroll_offset
if reference_line:
matching_line_idx = find_best_matching_line(reference_line, file_lines, 1000)
@ -237,7 +285,11 @@ def load_commit_content(state: AppState) -> AppState:
file_lines=file_lines,
added_lines=added_lines,
deleted_lines=deleted_lines,
right_scroll_offset=right_scroll_offset
right_scroll_offset=right_scroll_offset,
commit_hash=commit_hash,
commit_message=commit_details['message'],
commit_author=commit_details['author'],
commit_branch=commit_details['branch']
)
# Calculate change blocks for navigation
@ -291,10 +343,10 @@ def draw_left_pane(stdscr, state):
if state.selected_commit_idx < state.left_scroll_offset:
state.left_scroll_offset = state.selected_commit_idx
elif state.selected_commit_idx >= state.left_scroll_offset + state.height - 1:
state.left_scroll_offset = state.selected_commit_idx - (state.height - 2)
elif state.selected_commit_idx >= state.left_scroll_offset + state.height - state.status_bar_height:
state.left_scroll_offset = state.selected_commit_idx - (state.height - state.status_bar_height - 1)
visible_commits = state.commits[state.left_scroll_offset:state.left_scroll_offset + state.height - 1]
visible_commits = state.commits[state.left_scroll_offset:state.left_scroll_offset + state.height - state.status_bar_height]
for i, line in enumerate(visible_commits):
display_index = i + state.left_scroll_offset
if display_index == state.selected_commit_idx:
@ -429,10 +481,10 @@ def draw_right_pane(stdscr, state):
right_width = state.width - right_start - 1
max_scroll = max(0, len(state.file_lines) - (state.height - 1))
max_scroll = max(0, len(state.file_lines) - (state.height - state.status_bar_height))
state.right_scroll_offset = min(state.right_scroll_offset, max_scroll)
visible_lines = state.file_lines[state.right_scroll_offset:state.right_scroll_offset + state.height - 1]
visible_lines = state.file_lines[state.right_scroll_offset:state.right_scroll_offset + state.height - state.status_bar_height]
display_lines = []
deleted_line_map = {}
@ -591,7 +643,7 @@ def draw_divider(stdscr, state):
return
divider_char = "║" if state.dragging_divider else "│"
for y in range(state.height - 1): # Don't draw through the status bar
for y in range(state.height - state.status_bar_height): # Don't draw through the status bars
try:
stdscr.addch(y, state.divider_col, divider_char)
except curses.error:
@ -755,53 +807,51 @@ def draw_help_popup(stdscr, state):
pass
def draw_status_bars(stdscr, state):
# We'll use height-1 for the single status bar
visible_height = state.height - 1
# Calculate the position of the status bars
status_bar_start = state.height - state.status_bar_height
visible_height = status_bar_start
# If in search mode, draw the search input field instead of the normal status bar
# If in search mode, draw the search input field instead of the normal status bars
if state.search_mode:
# Fill the status bar with spaces
# Fill the top status bar with spaces
for x in range(state.width - 1):
try:
stdscr.addch(state.height - 1, x, ' ')
stdscr.addch(status_bar_start, x, ' ')
except curses.error:
pass
# Draw search prompt
search_prompt = "Search: "
try:
stdscr.addstr(state.height - 1, 0, search_prompt)
stdscr.addstr(status_bar_start, 0, search_prompt)
# Draw the search query
stdscr.addstr(state.height - 1, len(search_prompt), state.search_query)
stdscr.addstr(status_bar_start, len(search_prompt), state.search_query)
# Draw cursor at the end of the query
try:
stdscr.move(state.height - 1, len(search_prompt) + len(state.search_query))
stdscr.move(status_bar_start, len(search_prompt) + len(state.search_query))
except curses.error:
pass
# If we have search results, show count
if state.search_matches:
match_info = f" [{state.current_match_idx + 1}/{len(state.search_matches)}]"
stdscr.addstr(state.height - 1, state.width - len(match_info) - 1, match_info)
stdscr.addstr(status_bar_start, state.width - len(match_info) - 1, match_info)
except curses.error:
pass
# Draw a blank second status bar
for x in range(state.width - 1):
try:
stdscr.addch(state.height - 1, x, ' ', curses.A_REVERSE)
except curses.error:
pass
return
# Normal status bar (not in search mode)
# Get commit message for the selected commit
commit_message = ""
if state.commits and state.selected_commit_idx < len(state.commits):
# Format is now: hash date time message
# We need to split on more than just the first two spaces
commit_line = state.commits[state.selected_commit_idx]
parts = commit_line.split(' ', 3) # Split into hash, date, time, message
if len(parts) >= 4:
commit_message = parts[3]
# Add indicators to commit message
# --- First status bar (top) ---
# Add indicators to status bar
wrap_indicator = "" if state.wrap_lines else "[NW] "
change_indicator = ""
if state.change_blocks and state.current_change_idx != -1:
@ -811,7 +861,7 @@ def draw_status_bars(stdscr, state):
if state.search_matches:
search_indicator = f"[Search {state.current_match_idx + 1}/{len(state.search_matches)}] "
commit_message = wrap_indicator + change_indicator + search_indicator + commit_message
status_indicators = wrap_indicator + change_indicator + search_indicator
# Status bar percentages
if len(state.file_lines) > 0:
@ -833,10 +883,10 @@ def draw_status_bars(stdscr, state):
# Use terminal's default colors for the status bar
status_attr = curses.A_NORMAL # Use terminal's default colors
# Fill the status bar with spaces using reverse video
# Fill the top status bar with spaces using reverse video
for x in range(state.width - 1):
try:
stdscr.addch(state.height - 1, x, ' ', curses.A_REVERSE)
stdscr.addch(status_bar_start, x, ' ', curses.A_REVERSE)
except curses.error:
pass
@ -846,26 +896,23 @@ def draw_status_bars(stdscr, state):
# Use normal video if left pane is active (since status bar is already reverse)
left_attr = status_attr if state.focus == "left" else curses.A_REVERSE
# Add a space before and after the percentage with the same highlighting
stdscr.addstr(state.height - 1, 0, f" {left_status} ", left_attr)
stdscr.addstr(status_bar_start, 0, f" {left_status} ", left_attr)
except curses.error:
pass
# Add commit message in the middle
if commit_message:
# Add status indicators in the middle
if status_indicators:
# Calculate available space
left_margin = len(left_status) + 3 if state.show_sidebar else 1 # +3 for the spaces around percentage
right_margin = len(right_status) + 3 # +3 for the spaces around percentage
available_width = state.width - left_margin - right_margin
# Truncate message if needed
if len(commit_message) > available_width:
commit_message = commit_message[:available_width-3] + "..."
# Center the message in the available space
message_x = left_margin
# Truncate if needed
if len(status_indicators) > available_width:
status_indicators = status_indicators[:available_width-3] + "..."
try:
stdscr.addstr(state.height - 1, message_x, commit_message, curses.A_REVERSE)
stdscr.addstr(status_bar_start, left_margin, status_indicators, curses.A_REVERSE)
except curses.error:
pass
@ -877,7 +924,61 @@ def draw_status_bars(stdscr, state):
if right_x >= 0:
# Use normal video if right pane is active (since status bar is already reverse)
right_attr = status_attr if state.focus == "right" else curses.A_REVERSE
stdscr.addstr(state.height - 1, right_x, padded_right_status, right_attr)
stdscr.addstr(status_bar_start, right_x, padded_right_status, right_attr)
except curses.error:
pass
# --- Second status bar (bottom) ---
# Draw the commit details in the second status bar
# Fill the bottom status bar with spaces using reverse video
for x in range(state.width - 1):
try:
stdscr.addch(state.height - 1, x, ' ', curses.A_REVERSE)
except curses.error:
pass
# Draw the commit hash and message on the left
if state.commit_hash:
commit_info = f" {state.commit_hash} "
# Calculate available space for the commit message
author_branch_info = f" {state.commit_author} [{state.commit_branch}] "
available_width = state.width - len(commit_info) - len(author_branch_info) - 1
# Get the commit message (potentially multi-line)
commit_message = state.commit_message
# For multi-line messages, we'll show as many lines as we can fit in the status bar height
message_lines = commit_message.splitlines()
if not message_lines:
message_lines = [""]
# Show the first line in the status bar
display_message = message_lines[0]
# Truncate message if needed
if len(display_message) > available_width:
display_message = display_message[:available_width-3] + "..."
try:
# Draw the commit hash with bold
stdscr.addstr(state.height - 1, 0, commit_info, curses.A_REVERSE | curses.A_BOLD)
# Draw the commit message
stdscr.addstr(state.height - 1, len(commit_info), display_message, curses.A_REVERSE)
# Draw the author and branch on the right
right_x = state.width - len(author_branch_info)
if right_x >= 0:
stdscr.addstr(state.height - 1, right_x, author_branch_info, curses.A_REVERSE)
except curses.error:
pass
# Draw a handle for resizing the status bar
try:
handle_char = "≡" if state.dragging_status_bar else "="
stdscr.addstr(status_bar_start, state.width // 2 - 1, handle_char * 3, curses.A_REVERSE | curses.A_BOLD)
except curses.error:
pass
@ -1014,15 +1115,37 @@ def copy_selection_to_clipboard(stdscr, state):
except (FileNotFoundError, subprocess.CalledProcessError):
pass # Silently fail if no clipboard command is available
def update_status_bar_height(state: AppState, my: int) -> AppState:
"""Update the status bar height based on mouse position."""
# Calculate the minimum and maximum allowed heights
min_height = 2 # Minimum 2 lines for the status bar
max_height = min(10, state.height // 2) # Maximum 10 lines or half the screen
# Calculate the new height based on how far the user has dragged
status_bar_start = state.height - state.status_bar_height
drag_distance = status_bar_start - my
new_height = state.status_bar_height + drag_distance
# Clamp to allowed range
new_height = max(min_height, min(new_height, max_height))
return replace(state, status_bar_height=new_height)
def handle_mouse_input(stdscr, state: AppState) -> AppState:
try:
_, mx, my, _, bstate = curses.getmouse()
state = replace(state, last_bstate=bstate, mouse_x=mx, mouse_y=my)
# Status bar position
status_bar_start = state.height - state.status_bar_height
# Handle mouse button press
if bstate & curses.BUTTON1_PRESSED:
if state.show_sidebar and abs(mx - state.divider_col) <= 1:
return replace(state, dragging_divider=True)
elif my == status_bar_start and abs(mx - (state.width // 2)) <= 2:
# Clicked on the status bar handle
return replace(state, dragging_status_bar=True)
else:
focus = "right"
if state.show_sidebar:
@ -1039,6 +1162,8 @@ def handle_mouse_input(stdscr, state: AppState) -> AppState:
if bstate & curses.BUTTON1_RELEASED:
if state.dragging_divider:
return replace(state, dragging_divider=False)
elif state.dragging_status_bar:
return replace(state, dragging_status_bar=False)
if state.is_selecting:
# Check if this was a click (press and release at same position)
@ -1054,7 +1179,7 @@ def handle_mouse_input(stdscr, state: AppState) -> AppState:
new_state = replace(state, focus=focus)
# If clicking in the left pane on a commit entry, select that commit
if state.show_sidebar and mx < state.divider_col and my < min(state.height - 1, len(state.commits) - state.left_scroll_offset):
if state.show_sidebar and mx < state.divider_col and my < min(status_bar_start, len(state.commits) - state.left_scroll_offset):
new_commit_idx = my + state.left_scroll_offset
if 0 <= new_commit_idx < len(state.commits):
new_state = replace(new_state, selected_commit_idx=new_commit_idx)
@ -1073,6 +1198,8 @@ def handle_mouse_input(stdscr, state: AppState) -> AppState:
# Handle mouse movement during drag operations
if state.dragging_divider:
return update_divider(state, mx)
elif state.dragging_status_bar:
return update_status_bar_height(state, my)
elif state.is_selecting:
return replace(state, selection_end_coord=(mx, my))