From 7866194e65d1ffaba67853424005522a28815f0a Mon Sep 17 00:00:00 2001 From: consultoria-as Date: Sun, 15 Feb 2026 02:31:54 +0000 Subject: [PATCH] ui(console): center menu in a bordered box with better spacing Both renderers now draw the menu inside a centered box with: - Rounded corners (modern) / box-drawing (VT220) - Title centered inside the box top - Section separators as horizontal lines within the box - Selected item highlighted across the full box width - Vertical and horizontal centering on screen Co-Authored-By: Claude Opus 4.6 --- console/renderers/curses_renderer.py | 46 ++++++++++---- console/renderers/textual_renderer.py | 86 +++++++++++++++++++++------ 2 files changed, 103 insertions(+), 29 deletions(-) diff --git a/console/renderers/curses_renderer.py b/console/renderers/curses_renderer.py index 3e7f360..d62699b 100644 --- a/console/renderers/curses_renderer.py +++ b/console/renderers/curses_renderer.py @@ -167,17 +167,42 @@ class CursesRenderer(BaseRenderer): def draw_menu(self, items, selected_index=0, title=''): h, w = self.get_size() - start_row = 3 + # Calculate menu dimensions for centering + item_count = len(items) + # Find widest label for box sizing + max_label = 0 + for num, label in items: + if num != "---" and num != "\u2500": + max_label = max(max_label, len(f" {num}. {label} ")) + box_w = max(max_label + 8, 44) + box_w = min(box_w, w - 4) + box_h = item_count + 4 # top/bottom border + title + blank line if title: - self._safe_addstr(start_row, 2, title, self._attr("title")) - start_row += 2 + box_h += 2 - visible = h - start_row - 3 # leave room for footer - if visible < 1: - return + # Center the box vertically and horizontally + start_row = max((h - box_h) // 2 - 1, 2) + start_col = max((w - box_w) // 2, 1) - # Scrolling offset + # Draw box + self.draw_box(start_row, start_col, box_h, box_w, "") + + row = start_row + 1 + if title: + # Title centered inside the box + title_col = start_col + max((box_w - len(title)) // 2, 2) + self._safe_addstr(row, title_col, title, + self._attr("title")) + row += 1 + self._hline(row, start_col + 1, box_w - 2) + row += 1 + + # Menu items inside the box + inner_left = start_col + 3 + inner_w = box_w - 6 + + visible = box_h - (row - start_row) - 1 offset = 0 if selected_index >= visible: offset = selected_index - visible + 1 @@ -188,19 +213,20 @@ class CursesRenderer(BaseRenderer): break if idx < offset: continue - row = start_row + drawn # Separator if num == "\u2500" or num == "---": - self._hline(row, 2, w - 4) + self._hline(row, start_col + 1, box_w - 2) + row += 1 drawn += 1 continue marker = "\u25b8 " if idx == selected_index else " " text = f"{marker}{num}. {label}" style = "highlight" if idx == selected_index else "normal" - self._safe_addstr(row, 2, pad_right(text, w - 4), + self._safe_addstr(row, inner_left, pad_right(text, inner_w), self._attr(style)) + row += 1 drawn += 1 def draw_table(self, headers, rows, widths, page_info=None, diff --git a/console/renderers/textual_renderer.py b/console/renderers/textual_renderer.py index ec63cea..ae10b82 100644 --- a/console/renderers/textual_renderer.py +++ b/console/renderers/textual_renderer.py @@ -258,46 +258,94 @@ class TextualRenderer(BaseRenderer): def draw_menu(self, items, selected_index=0, title=''): h, w = self.get_size() - visible_lines = h - 6 + + # Calculate menu box dimensions + max_label = 0 + for num, label in items: + if num != "---" and num != "\u2500": + max_label = max(max_label, len(f" {num}. {label} ")) + box_inner = max(max_label + 4, 40) + box_inner = min(box_inner, w - 8) + + # Vertical centering: blank lines before menu + item_count = len(items) + menu_height = item_count + (3 if title else 0) + top_pad = max((h - menu_height - 6) // 2 - 1, 0) + for _ in range(top_pad): + self._add_line(Text("")) + + # Horizontal padding + pad_left = max((w - box_inner - 4) // 2, 2) + pad_str = " " * pad_left + + # Box top border + self._add_line(Text( + f"{pad_str}\u256d{'─' * (box_inner + 2)}\u256e", + style="blue", + )) if title: - self._add_line(Text(f" {title}", style="bold white")) - self._add_line(Text("")) - visible_lines -= 2 + # Title centered inside box + title_pad = max((box_inner - len(title)) // 2, 0) + line = Text() + line.append(f"{pad_str}\u2502 ", style="blue") + line.append(" " * title_pad, style="") + line.append(title, style="bold white") + line.append( + " " * (box_inner - title_pad - len(title)), + style="", + ) + line.append(" \u2502", style="blue") + self._add_line(line) + # Separator inside box + self._add_line(Text( + f"{pad_str}\u251c{'─' * (box_inner + 2)}\u2524", + style="blue", + )) - if visible_lines < 1: - return + visible = h - self._line_count - 4 + if visible < 1: + visible = item_count offset = 0 - if selected_index >= visible_lines: - offset = selected_index - visible_lines + 1 + if selected_index >= visible: + offset = selected_index - visible + 1 drawn = 0 for idx, (num, label) in enumerate(items): - if drawn >= visible_lines: + if drawn >= visible: break if idx < offset: continue if num == "\u2500" or num == "---": - self._add_line( - Text(" " + "─" * (w - 4), style="dim blue") - ) + self._add_line(Text( + f"{pad_str}\u251c{'─' * (box_inner + 2)}\u2524", + style="dim blue", + )) drawn += 1 continue marker = "\u25b8 " if idx == selected_index else " " + entry = f"{marker}{num}. {label}" + entry = pad_right(entry, box_inner) + + line = Text() + line.append(f"{pad_str}\u2502 ", style="blue") if idx == selected_index: - entry = pad_right(f"{marker}{num}. {label}", w - 4) - self._add_line( - Text(f" {entry}", style="bold white on rgb(30,60,120)") - ) + line.append(f" {entry}", style="bold white on rgb(30,60,120)") else: - self._add_line( - Text(f" {marker}{num}. {label}", style="white") - ) + line.append(f" {entry}", style="white") + line.append(" \u2502", style="blue") + self._add_line(line) drawn += 1 + # Box bottom border + self._add_line(Text( + f"{pad_str}\u2570{'─' * (box_inner + 2)}\u256f", + style="blue", + )) + def draw_table(self, headers, rows, widths, page_info=None, selected_row=-1): h, w = self.get_size()