feat: Add second status bar with commit details and resizable height
This commit is contained in:
parent
6c319c1570
commit
c945c89b74
217
gtm
217
gtm
|
|
@ -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)
|
||||
|
|
@ -148,6 +178,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) ---
|
||||
|
||||
def calculate_change_blocks(state: AppState) -> AppState:
|
||||
|
|
@ -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))
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue