From aef5c0f99f665fe0c8dece9de5bd431faf649c43 Mon Sep 17 00:00:00 2001 From: rob Date: Fri, 5 Dec 2025 01:46:20 -0400 Subject: [PATCH] Add 3D-style buttons with box-drawing characters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Create DOS/BIOS-style 3D button effects using Unicode characters: Button3D (multi-line): ┌──────────┐ │ Label │▄ └──────────┘█ Button3DCompact (single-line): ▐ Label ▌▄ Changes: - Add Button3D class for large standalone buttons - Add Button3DCompact class for inline/dialog buttons - Update palette with shadow colors (shadow_edge, button_highlight, button_shadow) - Replace all ClickableButton/urwid.Button with Button3DCompact throughout: - Main menu action buttons (Create, Edit, Delete, Test, Providers, EXIT) - Dialog OK/Cancel buttons - Load buttons in code/prompt dialogs - Provider dropdown buttons - Auto-adjust button 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/smarttools/ui_urwid.py | 207 +++++++++++++++++++++++++++++++------ 1 file changed, 175 insertions(+), 32 deletions(-) diff --git a/src/smarttools/ui_urwid.py b/src/smarttools/ui_urwid.py index 728dcbe..7d9a8b4 100644 --- a/src/smarttools/ui_urwid.py +++ b/src/smarttools/ui_urwid.py @@ -10,22 +10,33 @@ from .tool import ( from .providers import Provider, load_providers, add_provider, delete_provider, get_provider -# Color palette - BIOS style +# Color palette - BIOS style with 3D button effects PALETTE = [ ('body', 'white', 'dark blue'), ('header', 'white', 'dark red', 'bold'), ('footer', 'black', 'light gray'), + # Button colors - raised 3D effect ('button', 'black', 'light gray'), ('button_focus', 'white', 'dark red', 'bold'), + ('button_highlight', 'white', 'light gray'), # Top/left edge (light) + ('button_shadow', 'dark gray', 'light gray'), # Bottom/right edge (dark) + ('button_pressed', 'black', 'dark gray'), # Pressed state + # Edit fields ('edit', 'black', 'light gray'), ('edit_focus', 'black', 'yellow'), + # List items ('listbox', 'black', 'light gray'), ('listbox_focus', 'white', 'dark red'), + # Dialog ('dialog', 'black', 'light gray'), ('dialog_border', 'white', 'dark blue'), + # Text styles ('label', 'yellow', 'dark blue', 'bold'), ('error', 'white', 'dark red', 'bold'), ('success', 'light green', 'dark blue', 'bold'), + # 3D shadow elements + ('shadow', 'black', 'black'), + ('shadow_edge', 'dark gray', 'dark blue'), ] @@ -55,8 +66,149 @@ class SelectableText(urwid.WidgetWrap): return False +class Button3D(urwid.WidgetWrap): + """A 3D-style button using box-drawing characters for depth. + + Creates a raised button effect like DOS/BIOS interfaces: + ┌──────────┐ + │ Label │▄ + └──────────┘█ + + When focused, colors change to show selection. + """ + + signals = ['click'] + + def __init__(self, label, on_press=None, user_data=None): + self.label = label + self.on_press = on_press + self.user_data = user_data + self._pressed = False + + # Build the 3D button structure + self._build_widget() + super().__init__(self._widget) + + def _build_widget(self): + """Build the 3D button widget structure.""" + label = self.label + width = len(label) + 4 # Padding inside button + + # Button face with border + # Top edge: ┌────┐ + top = '┌' + '─' * (width - 2) + '┐' + # Middle: │ Label │ with shadow + middle_text = '│ ' + label + ' │' + # Bottom edge: └────┘ with shadow + bottom = '└' + '─' * (width - 2) + '┘' + + # Shadow characters (right and bottom) + shadow_right = '▄' + shadow_bottom = '█' + + # Create the rows + top_row = urwid.Text(top + ' ') # Space for shadow alignment + middle_row = urwid.Columns([ + ('pack', urwid.Text(middle_text)), + ('pack', urwid.Text(('shadow_edge', shadow_right))), + ]) + bottom_row = urwid.Columns([ + ('pack', urwid.Text(bottom)), + ('pack', urwid.Text(('shadow_edge', shadow_right))), + ]) + shadow_row = urwid.Text(('shadow_edge', ' ' + shadow_bottom * (width - 1))) + + # Stack them + pile = urwid.Pile([ + top_row, + middle_row, + bottom_row, + shadow_row, + ]) + + self._widget = urwid.AttrMap(pile, 'button', 'button_focus') + + def selectable(self): + return True + + def keypress(self, size, key): + if key == 'enter': + self._activate() + return None + return key + + def mouse_event(self, size, event, button, col, row, focus): + if button == 1: + if event == 'mouse press': + self._pressed = True + return True + elif event == 'mouse release' and self._pressed: + self._pressed = False + self._activate() + return True + return False + + def _activate(self): + """Trigger the button callback.""" + if self.on_press: + self.on_press(self.user_data) + self._emit('click') + + +class Button3DCompact(urwid.WidgetWrap): + """A compact 3D button that fits on a single line with shadow effect. + + Creates a subtle 3D effect: [ Label ]▌ + + Better for inline use where vertical space is limited. + """ + + signals = ['click'] + + def __init__(self, label, on_press=None, user_data=None): + self.label = label + self.on_press = on_press + self.user_data = user_data + + # Build compact button: ▐ Label ▌ with shadow + # Using block characters for edges + button_text = urwid.Text([ + ('button_highlight', '▐'), + ('button', f' {label} '), + ('button_shadow', '▌'), + ('shadow_edge', '▄'), + ]) + + self._widget = urwid.AttrMap(button_text, None, { + 'button': 'button_focus', + 'button_highlight': 'button_focus', + 'button_shadow': 'button_focus', + }) + super().__init__(self._widget) + + def selectable(self): + return True + + def keypress(self, size, key): + if key == 'enter': + self._activate() + return None + return key + + def mouse_event(self, size, event, button, col, row, focus): + if button == 1 and event == 'mouse release': + self._activate() + return True + return False + + def _activate(self): + if self.on_press: + self.on_press(self.user_data) + self._emit('click') + + class ClickableButton(urwid.WidgetWrap): - """A button that responds to mouse clicks.""" + """A button that responds to mouse clicks (legacy wrapper).""" def __init__(self, label, on_press=None, user_data=None): self.on_press = on_press @@ -466,16 +618,16 @@ class ToolBuilderLayout(urwid.WidgetWrap): class Dialog(urwid.WidgetWrap): - """A dialog box overlay.""" + """A dialog box overlay with 3D-style buttons.""" def __init__(self, title, body, buttons, width=60, height=None): # Title title_widget = urwid.Text(('header', f' {title} '), align='center') - # Buttons row + # Buttons row - use 3D compact buttons for dialog actions button_widgets = [] for label, callback in buttons: - btn = ClickableButton(label, callback) + btn = Button3DCompact(label, callback) button_widgets.append(btn) buttons_row = urwid.Columns([('pack', b) for b in button_widgets], dividechars=2) buttons_centered = urwid.Padding(buttons_row, align='center', width='pack') @@ -647,22 +799,22 @@ class SmartToolsUI: tool_listbox = ToolListBox(self._tool_walker, on_focus_change=self._on_tool_focus) tool_box = urwid.LineBox(tool_listbox, title='Tools') - # Action buttons - Tab navigates here from tool list - create_btn = ClickableButton("Create", lambda _: self._create_tool_before_selected()) - edit_btn = ClickableButton("Edit", lambda _: self._edit_selected_tool()) - delete_btn = ClickableButton("Delete", lambda _: self._delete_selected_tool()) - test_btn = ClickableButton("Test", lambda _: self._test_selected_tool()) - providers_btn = ClickableButton("Providers", lambda _: self.manage_providers()) + # Action buttons - Tab navigates here from tool list (3D style) + create_btn = Button3DCompact("Create", lambda _: self._create_tool_before_selected()) + edit_btn = Button3DCompact("Edit", lambda _: self._edit_selected_tool()) + delete_btn = Button3DCompact("Delete", lambda _: self._delete_selected_tool()) + test_btn = Button3DCompact("Test", lambda _: self._test_selected_tool()) + providers_btn = Button3DCompact("Providers", lambda _: self.manage_providers()) buttons_row = urwid.Columns([ ('pack', create_btn), - ('pack', urwid.Text(" ")), + ('pack', urwid.Text(" ")), ('pack', edit_btn), - ('pack', urwid.Text(" ")), + ('pack', urwid.Text(" ")), ('pack', delete_btn), - ('pack', urwid.Text(" ")), + ('pack', urwid.Text(" ")), ('pack', test_btn), - ('pack', urwid.Text(" ")), + ('pack', urwid.Text(" ")), ('pack', providers_btn), ]) buttons_padded = urwid.Padding(buttons_row, align='left', left=1) @@ -687,9 +839,9 @@ class SmartToolsUI: info_filler = urwid.Filler(info_content, valign='top') info_box = urwid.LineBox(info_filler, title='Tool Info') - # Exit button at bottom - exit_btn = ClickableButton("EXIT", lambda _: self.exit_app()) - exit_centered = urwid.Padding(exit_btn, align='center', width=10) + # Exit button at bottom (3D style) + exit_btn = Button3DCompact("EXIT", lambda _: self.exit_app()) + exit_centered = urwid.Padding(exit_btn, align='center', width=12) # Use a custom Pile that handles Tab to cycle between tool list and buttons self._main_pile = TabCyclePile([ @@ -1267,10 +1419,7 @@ class SmartToolsUI: popup = Dialog("Select Provider", body, []) self.show_overlay(popup, width=50, height=min(len(provider_names) + 6, 16)) - provider_select_btn = urwid.AttrMap( - urwid.Button("Select", on_press=show_provider_dropdown), - 'button', 'button_focus' - ) + provider_select_btn = Button3DCompact("▼", on_press=show_provider_dropdown) # File input for external prompt default_file = existing.prompt_file if existing and existing.prompt_file else "prompt.txt" @@ -1357,7 +1506,7 @@ class SmartToolsUI: def on_cancel(_): self.close_overlay() - load_btn = urwid.AttrMap(urwid.Button("Load", on_load), 'button', 'button_focus') + load_btn = Button3DCompact("Load", on_load) # Prompt editor in a box - use ListBox for proper focus handling and scrolling # Wrap in DOSScrollBar for DOS-style scrollbar with arrow buttons @@ -1481,10 +1630,7 @@ class SmartToolsUI: popup = Dialog("Select Provider", popup_body, []) self.show_overlay(popup, width=50, height=min(len(provider_names) + 6, 16)) - ai_provider_select_btn = urwid.AttrMap( - urwid.Button("▼", on_press=show_ai_provider_dropdown), - 'button', 'button_focus' - ) + ai_provider_select_btn = Button3DCompact("▼", on_press=show_ai_provider_dropdown) # Default prompt template for AI code generation/adjustment default_ai_prompt = f"""Modify or generate Python code according to my instruction below. @@ -1547,10 +1693,7 @@ Return ONLY the Python code, no explanations or markdown fencing.""" error_msg = result.error or "Unknown error" ai_output_text.set_text(('error', f"✗ Error from {provider_name}:\n\n{error_msg}")) - auto_adjust_btn = urwid.AttrMap( - urwid.Button("Auto-adjust", on_auto_adjust), - 'button', 'button_focus' - ) + auto_adjust_btn = Button3DCompact("Auto-adjust", on_auto_adjust) # Build the AI assist box with provider selector, prompt editor, output area, and button ai_provider_row = urwid.Columns([ @@ -1644,7 +1787,7 @@ Return ONLY the Python code, no explanations or markdown fencing.""" def on_cancel(_): self.close_overlay() - load_btn = urwid.AttrMap(urwid.Button("Load", on_load), 'button', 'button_focus') + load_btn = Button3DCompact("Load", on_load) # Code editor in a box - use ListBox for proper focus handling and scrolling # Wrap in DOSScrollBar for DOS-style scrollbar with arrow buttons