diff --git a/gtm b/gtm index ab223f7..8b691e8 100755 --- a/gtm +++ b/gtm @@ -126,6 +126,30 @@ def get_diff_info(current_commit, prev_commit, filename): i += 1 return added_lines, deleted_lines +@dataclass +class StatusBarLayout: + """Encapsulates all status bar layout calculations""" + total_height: int + start_y: int + main_status_y: int + commit_detail_start_y: int + commit_detail_end_y: int + available_commit_lines: int + screen_width: int + + @classmethod + def create(cls, screen_height: int, screen_width: int, status_bar_height: int): + start_y = screen_height - status_bar_height + return cls( + total_height=status_bar_height, + start_y=start_y, + main_status_y=start_y, # Top line is main status + commit_detail_start_y=start_y + 1, # Commit details start below + commit_detail_end_y=screen_height - 1, # Bottom line + available_commit_lines=status_bar_height - 1, + screen_width=screen_width + ) + @dataclass class AppState: filename: str @@ -810,52 +834,19 @@ def draw_help_popup(stdscr, state): except curses.error: pass -def draw_status_bars(stdscr, state): - # 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 bars - if state.search_mode: - # Fill the top status bar with spaces - for x in range(state.width - 1): +def draw_status_bar_background(stdscr, layout: StatusBarLayout): + """Fill the entire status bar area with reverse video background""" + for y in range(layout.start_y, layout.start_y + layout.total_height): + for x in range(layout.screen_width - 1): try: - stdscr.addch(status_bar_start, x, ' ') + stdscr.addch(y, x, ' ', curses.A_REVERSE) except curses.error: pass - - # Draw search prompt - search_prompt = "Search: " - try: - stdscr.addstr(status_bar_start, 0, search_prompt) - - # Draw the search query - stdscr.addstr(status_bar_start, len(search_prompt), state.search_query) - - # Draw cursor at the end of the query - try: - 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(status_bar_start, state.width - len(match_info) - 1, match_info) - except curses.error: - pass - - # Fill all remaining status bar lines with spaces - for y in range(status_bar_start + 1, state.height): - for x in range(state.width - 1): - try: - stdscr.addch(y, x, ' ', curses.A_REVERSE) - except curses.error: - pass - - return + +def draw_main_status_line(stdscr, state: AppState, layout: StatusBarLayout): + """Draw the main status line (percentages, indicators, etc.)""" + visible_height = layout.start_y - # --- First status bar (top) --- # Add indicators to status bar wrap_indicator = "" if state.wrap_lines else "[NW] " change_indicator = "" @@ -886,15 +877,7 @@ def draw_status_bars(stdscr, state): left_status = f"{left_percent}%" # Use terminal's default colors for the status bar - status_attr = curses.A_NORMAL # Use terminal's default colors - - # Fill all status bar lines with spaces using reverse video - for y in range(status_bar_start, state.height): - for x in range(state.width - 1): - try: - stdscr.addch(y, x, ' ', curses.A_REVERSE) - except curses.error: - pass + status_attr = curses.A_NORMAL # Add left percentage indicator (only if sidebar is visible) if state.show_sidebar: @@ -902,7 +885,7 @@ 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(status_bar_start, 0, f" {left_status} ", left_attr) + stdscr.addstr(layout.main_status_y, 0, f" {left_status} ", left_attr) except curses.error: pass @@ -911,14 +894,14 @@ def draw_status_bars(stdscr, state): # 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 + available_width = layout.screen_width - left_margin - right_margin # Truncate if needed if len(status_indicators) > available_width: status_indicators = status_indicators[:available_width-3] + "..." try: - stdscr.addstr(status_bar_start, left_margin, status_indicators, curses.A_REVERSE) + stdscr.addstr(layout.main_status_y, left_margin, status_indicators, curses.A_REVERSE) except curses.error: pass @@ -926,80 +909,108 @@ def draw_status_bars(stdscr, state): try: # Include spaces in the highlighted area padded_right_status = f" {right_status} " - right_x = state.width - len(padded_right_status) + right_x = layout.screen_width - len(padded_right_status) 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(status_bar_start, right_x, padded_right_status, right_attr) + stdscr.addstr(layout.main_status_y, right_x, padded_right_status, right_attr) except curses.error: - pass + pass + +def draw_commit_details(stdscr, state: AppState, layout: StatusBarLayout): + """Draw commit hash, message, author in the available space""" + if not state.commit_hash: + return - # --- Second status bar and additional lines for commit details --- - # Draw the commit details in the status bar area + commit_info = f" {state.commit_hash} " + author_branch_info = f" {state.commit_author} [{state.commit_branch}] " + available_width = layout.screen_width - len(commit_info) - len(author_branch_info) - 1 - # Draw the commit hash and message - if state.commit_hash: - commit_info = f" {state.commit_hash} " + # Get the commit message (potentially multi-line) + message_lines = state.commit_message.splitlines() + if not message_lines: + message_lines = [""] + + try: + # Always draw the commit hash and first line of message on the bottom line + bottom_line = layout.commit_detail_end_y - # 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 + # Draw the commit hash with bold + stdscr.addstr(bottom_line, 0, commit_info, curses.A_REVERSE | curses.A_BOLD) - # Get the commit message (potentially multi-line) - commit_message = state.commit_message + # Draw the author and branch on the right of the bottom line + right_x = layout.screen_width - len(author_branch_info) + if right_x >= 0: + stdscr.addstr(bottom_line, right_x, author_branch_info, curses.A_REVERSE) - # 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 = [""] + # Draw the first line of the commit message on the same line as the hash + first_line = message_lines[0] if message_lines else "" + if len(first_line) > available_width: + first_line = first_line[:available_width-3] + "..." + stdscr.addstr(bottom_line, len(commit_info), first_line, curses.A_REVERSE) - # Calculate how many message lines we can display (reserve 1 line for the main status bar) - available_message_lines = state.status_bar_height - 1 + # Draw additional lines of the commit message if we have space and more lines + for i, line in enumerate(message_lines[1:], 1): + line_y = layout.commit_detail_end_y - i + if line_y < layout.commit_detail_start_y: + break # No more space + + # For continuation lines, we have more space since we don't need to show the hash + line_available_width = layout.screen_width - 4 # Leave some margin + if len(line) > line_available_width: + line = line[:line_available_width-3] + "..." + + stdscr.addstr(line_y, 4, line, curses.A_REVERSE) + except curses.error: + pass + +def draw_search_mode(stdscr, state: AppState, layout: StatusBarLayout): + """Draw search input interface""" + # Draw search prompt on the main status line + search_prompt = "Search: " + try: + stdscr.addstr(layout.main_status_y, 0, search_prompt) + # Draw the search query + stdscr.addstr(layout.main_status_y, len(search_prompt), state.search_query) + + # Draw cursor at the end of the query try: - # Always draw the commit hash and first line of message on the bottom line - bottom_line = state.height - 1 - - # Draw the commit hash with bold - stdscr.addstr(bottom_line, 0, commit_info, curses.A_REVERSE | curses.A_BOLD) - - # Draw the author and branch on the right of the bottom line - right_x = state.width - len(author_branch_info) - if right_x >= 0: - stdscr.addstr(bottom_line, right_x, author_branch_info, curses.A_REVERSE) - - # Draw the first line of the commit message on the same line as the hash - first_line = message_lines[0] if message_lines else "" - if len(first_line) > available_width: - first_line = first_line[:available_width-3] + "..." - stdscr.addstr(bottom_line, len(commit_info), first_line, curses.A_REVERSE) - - # Draw additional lines of the commit message if we have space and more lines - for i in range(1, min(len(message_lines), available_message_lines + 1)): - if i >= len(message_lines): - break - line = message_lines[i] - # For continuation lines, we have more space since we don't need to show the hash - line_available_width = state.width - 4 # Leave some margin - if len(line) > line_available_width: - line = line[:line_available_width-3] + "..." - # Draw the line with some indentation (going upward from the bottom line) - line_y = bottom_line - i - if line_y >= status_bar_start: # Make sure we don't draw above the status bar area - stdscr.addstr(line_y, 4, line, curses.A_REVERSE) + stdscr.move(layout.main_status_y, len(search_prompt) + len(state.search_query)) except curses.error: pass - - # Draw a handle for resizing the status bar + + # 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(layout.main_status_y, layout.screen_width - len(match_info) - 1, match_info) + except curses.error: + pass + +def draw_resize_handle(stdscr, state: AppState, layout: StatusBarLayout): + """Draw the resize handle""" try: handle_char = "≡" if state.dragging_status_bar else "=" handle_text = handle_char * 5 # Make handle wider if state.status_bar_height > 2: handle_text += f" ({state.status_bar_height} lines)" - stdscr.addstr(status_bar_start, state.width // 2 - len(handle_text) // 2, handle_text, curses.A_REVERSE | curses.A_BOLD) + handle_x = layout.screen_width // 2 - len(handle_text) // 2 + stdscr.addstr(layout.main_status_y, handle_x, handle_text, curses.A_REVERSE | curses.A_BOLD) except curses.error: pass +def draw_status_bars(stdscr, state): + layout = StatusBarLayout.create(state.height, state.width, state.status_bar_height) + + draw_status_bar_background(stdscr, layout) + + if state.search_mode: + draw_search_mode(stdscr, state, layout) + else: + draw_main_status_line(stdscr, state, layout) + draw_commit_details(stdscr, state, layout) + draw_resize_handle(stdscr, state, layout) + def draw_ui(stdscr, state): stdscr.erase() draw_left_pane(stdscr, state)