Refactor CLI and UI into modular packages, add registry scrutiny
Major refactoring:
- Split cli.py (1465 lines) into cli/ package with 6 modules
- Split ui_urwid.py (2313 lines) into ui_urwid/ package with 4 modules
- Maintain backwards compatibility via thin wrapper modules
New features:
- Add tool scrutiny system for registry publishing (honesty, transparency, scope, efficiency checks)
- Add optimization suggestions for AI calls that could be pure code
Bug fixes:
- Fix variable substitution escaping ({{literal}} now works)
- Fix provider command parsing with shlex for quoted paths
- Add error logging even without --verbose flag
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <[email protected]>
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,6 @@
|
||||
"""Allow running the UI as a module."""
|
||||
|
||||
from . import run_ui
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_ui()
|
||||
@@ -0,0 +1,30 @@
|
||||
"""Color palette for the BIOS-style TUI."""
|
||||
|
||||
# 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'),
|
||||
]
|
||||
@@ -0,0 +1,715 @@
|
||||
"""Custom widgets for the BIOS-style TUI."""
|
||||
|
||||
import urwid
|
||||
|
||||
|
||||
class SelectableText(urwid.WidgetWrap):
|
||||
"""A selectable text widget for list items."""
|
||||
|
||||
def __init__(self, text, value=None, on_select=None):
|
||||
self.value = value
|
||||
self.on_select = on_select
|
||||
self.text_widget = urwid.Text(text)
|
||||
display = urwid.AttrMap(self.text_widget, 'listbox', 'listbox_focus')
|
||||
super().__init__(display)
|
||||
|
||||
def selectable(self):
|
||||
return True
|
||||
|
||||
def keypress(self, size, key):
|
||||
if key == 'enter' and self.on_select:
|
||||
self.on_select(self.value)
|
||||
return None
|
||||
return key
|
||||
|
||||
def mouse_event(self, size, event, button, col, row, focus):
|
||||
if event == 'mouse press' and button == 1 and self.on_select:
|
||||
self.on_select(self.value)
|
||||
return True
|
||||
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 (legacy wrapper)."""
|
||||
|
||||
def __init__(self, label, on_press=None, user_data=None):
|
||||
self.on_press = on_press
|
||||
self.user_data = user_data
|
||||
button = urwid.Button(label)
|
||||
if on_press:
|
||||
urwid.connect_signal(button, 'click', self._handle_click)
|
||||
display = urwid.AttrMap(button, 'button', 'button_focus')
|
||||
super().__init__(display)
|
||||
|
||||
def _handle_click(self, button):
|
||||
if self.on_press:
|
||||
self.on_press(self.user_data)
|
||||
|
||||
|
||||
class SelectableToolItem(urwid.WidgetWrap):
|
||||
"""A selectable tool item that maintains selection state."""
|
||||
|
||||
def __init__(self, name, on_select=None):
|
||||
self.name = name
|
||||
self.on_select = on_select
|
||||
self._selected = False
|
||||
self.text_widget = urwid.Text(f" {name} ")
|
||||
self.attr_map = urwid.AttrMap(self.text_widget, 'listbox', 'listbox_focus')
|
||||
super().__init__(self.attr_map)
|
||||
|
||||
def selectable(self):
|
||||
return True
|
||||
|
||||
def set_selected(self, selected):
|
||||
"""Set whether this item is the selected tool."""
|
||||
self._selected = selected
|
||||
if self._selected:
|
||||
self.attr_map.set_attr_map({None: 'listbox_focus'})
|
||||
else:
|
||||
self.attr_map.set_attr_map({None: 'listbox'})
|
||||
|
||||
def keypress(self, size, key):
|
||||
if key == 'enter' and self.on_select:
|
||||
self.on_select(self.name)
|
||||
return None
|
||||
return key
|
||||
|
||||
def mouse_event(self, size, event, button, col, row, focus):
|
||||
if event == 'mouse press' and button == 1:
|
||||
# Single click just selects/focuses - don't call on_select
|
||||
# on_select is only called on Enter key (to edit)
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class ToolListBox(urwid.ListBox):
|
||||
"""A ListBox that keeps arrow keys internal and passes Tab out."""
|
||||
|
||||
def __init__(self, body, on_focus_change=None):
|
||||
super().__init__(body)
|
||||
self.on_focus_change = on_focus_change
|
||||
self._last_focus = None
|
||||
|
||||
def keypress(self, size, key):
|
||||
if key in ('up', 'down'):
|
||||
# Handle arrow keys internally - navigate within list
|
||||
result = super().keypress(size, key)
|
||||
# Check if focus changed
|
||||
self._check_focus_change()
|
||||
return result
|
||||
elif key == 'tab':
|
||||
# Pass tab out to parent for focus cycling
|
||||
return key
|
||||
elif key == 'shift tab':
|
||||
return key
|
||||
else:
|
||||
return super().keypress(size, key)
|
||||
|
||||
def _check_focus_change(self):
|
||||
"""Check if focus changed and notify callback."""
|
||||
try:
|
||||
current = self.focus
|
||||
if current is not self._last_focus:
|
||||
self._last_focus = current
|
||||
if self.on_focus_change and isinstance(current, SelectableToolItem):
|
||||
self.on_focus_change(current.name)
|
||||
except (IndexError, TypeError):
|
||||
pass
|
||||
|
||||
def render(self, size, focus=False):
|
||||
# Check focus on render too (for initial display)
|
||||
if focus:
|
||||
self._check_focus_change()
|
||||
return super().render(size, focus)
|
||||
|
||||
|
||||
class TabCyclePile(urwid.Pile):
|
||||
"""A Pile that uses Tab/Shift-Tab to cycle between specific positions.
|
||||
|
||||
Args:
|
||||
widget_list: List of widgets (same as urwid.Pile)
|
||||
tab_positions: List of indices in the pile that Tab should cycle between.
|
||||
Default is [0] (only first position).
|
||||
"""
|
||||
|
||||
def __init__(self, widget_list, tab_positions=None):
|
||||
super().__init__(widget_list)
|
||||
# Positions in the pile that Tab should cycle between
|
||||
self.tab_positions = tab_positions or [0]
|
||||
self._current_tab_idx = 0
|
||||
|
||||
def keypress(self, size, key):
|
||||
if key == 'tab':
|
||||
# Move to next tab position
|
||||
self._current_tab_idx = (self._current_tab_idx + 1) % len(self.tab_positions)
|
||||
self.focus_position = self.tab_positions[self._current_tab_idx]
|
||||
return None
|
||||
elif key == 'shift tab':
|
||||
# Move to previous tab position
|
||||
self._current_tab_idx = (self._current_tab_idx - 1) % len(self.tab_positions)
|
||||
self.focus_position = self.tab_positions[self._current_tab_idx]
|
||||
return None
|
||||
else:
|
||||
return super().keypress(size, key)
|
||||
|
||||
|
||||
class TabPassEdit(urwid.Edit):
|
||||
"""A multiline Edit that passes Tab through for focus cycling instead of inserting tabs."""
|
||||
|
||||
def keypress(self, size, key):
|
||||
if key in ('tab', 'shift tab'):
|
||||
# Pass Tab through to parent for focus cycling
|
||||
return key
|
||||
return super().keypress(size, key)
|
||||
|
||||
|
||||
class UndoableEdit(urwid.Edit):
|
||||
"""A multiline Edit with undo/redo support.
|
||||
|
||||
Features:
|
||||
- Undo with Alt+U (up to 50 states)
|
||||
- Redo with Alt+R
|
||||
- Tab passes through for focus cycling
|
||||
"""
|
||||
|
||||
MAX_UNDO = 50 # Maximum undo history size
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self._undo_stack = [] # List of (text, cursor_pos) tuples
|
||||
self._redo_stack = []
|
||||
|
||||
def keypress(self, size, key):
|
||||
if key in ('tab', 'shift tab'):
|
||||
return key
|
||||
|
||||
# Handle undo (Alt+U or meta u)
|
||||
if key in ('meta u', 'alt u'):
|
||||
self._undo()
|
||||
return None
|
||||
|
||||
# Handle redo (Alt+R or meta r)
|
||||
if key in ('meta r', 'alt r'):
|
||||
self._redo()
|
||||
return None
|
||||
|
||||
# Save current state BEFORE the edit for undo
|
||||
old_text = self.edit_text
|
||||
old_pos = self.edit_pos
|
||||
|
||||
# Let the parent handle the keypress
|
||||
result = super().keypress(size, key)
|
||||
|
||||
# If text changed, save the old state to undo stack
|
||||
if self.edit_text != old_text:
|
||||
self._save_undo_state(old_text, old_pos)
|
||||
self._redo_stack.clear() # Clear redo on new edit
|
||||
|
||||
return result
|
||||
|
||||
def _save_undo_state(self, text, pos):
|
||||
"""Save state to undo stack."""
|
||||
# Don't save duplicate states
|
||||
if self._undo_stack and self._undo_stack[-1][0] == text:
|
||||
return
|
||||
if len(self._undo_stack) >= self.MAX_UNDO:
|
||||
self._undo_stack.pop(0)
|
||||
self._undo_stack.append((text, pos))
|
||||
|
||||
def _undo(self):
|
||||
"""Restore previous state from undo stack."""
|
||||
if not self._undo_stack:
|
||||
return
|
||||
|
||||
# Save current state to redo stack
|
||||
self._redo_stack.append((self.edit_text, self.edit_pos))
|
||||
|
||||
# Restore previous state
|
||||
text, pos = self._undo_stack.pop()
|
||||
self.set_edit_text(text)
|
||||
self.set_edit_pos(min(pos, len(text)))
|
||||
|
||||
def _redo(self):
|
||||
"""Restore state from redo stack."""
|
||||
if not self._redo_stack:
|
||||
return
|
||||
|
||||
# Save current state to undo stack
|
||||
self._undo_stack.append((self.edit_text, self.edit_pos))
|
||||
|
||||
# Restore redo state
|
||||
text, pos = self._redo_stack.pop()
|
||||
self.set_edit_text(text)
|
||||
self.set_edit_pos(min(pos, len(text)))
|
||||
|
||||
|
||||
class DOSScrollBar(urwid.WidgetWrap):
|
||||
"""A DOS-style scrollbar with arrow buttons at top and bottom.
|
||||
|
||||
Renders a scrollbar on the right side of the wrapped widget with:
|
||||
- ▲ arrow at top (click to scroll up)
|
||||
- ░ track with █ thumb showing scroll position
|
||||
- ▼ arrow at bottom (click to scroll down)
|
||||
|
||||
Click zones (expanded to last 2 columns for easier clicking):
|
||||
- Top 25%: scroll up 3 lines
|
||||
- Bottom 25%: scroll down 3 lines
|
||||
- Middle: page up/down based on which half clicked
|
||||
"""
|
||||
|
||||
def __init__(self, widget):
|
||||
self._wrapped = widget
|
||||
# Create a columns layout: content on left, scrollbar on right
|
||||
super().__init__(widget)
|
||||
|
||||
def render(self, size, focus=False):
|
||||
maxcol, maxrow = size
|
||||
|
||||
# Render the wrapped widget with one less column for scrollbar
|
||||
content_size = (maxcol - 1, maxrow)
|
||||
content_canvas = self._wrapped.render(content_size, focus)
|
||||
|
||||
# Build the scrollbar column
|
||||
scrollbar_chars = []
|
||||
|
||||
# Up arrow at top
|
||||
scrollbar_chars.append('▲')
|
||||
|
||||
# Calculate thumb position
|
||||
if maxrow > 2:
|
||||
track_height = maxrow - 2 # Minus the two arrow buttons
|
||||
|
||||
# Get scroll position info from wrapped widget
|
||||
try:
|
||||
if hasattr(self._wrapped, 'rows_max'):
|
||||
rows_max = self._wrapped.rows_max(content_size)
|
||||
scroll_pos = self._wrapped.get_scrollpos(content_size)
|
||||
else:
|
||||
rows_max = maxrow
|
||||
scroll_pos = 0
|
||||
|
||||
if rows_max > maxrow:
|
||||
# Calculate thumb position within track
|
||||
thumb_pos = int((scroll_pos / (rows_max - maxrow)) * (track_height - 1))
|
||||
thumb_pos = max(0, min(thumb_pos, track_height - 1))
|
||||
else:
|
||||
thumb_pos = 0
|
||||
except (AttributeError, TypeError, ZeroDivisionError):
|
||||
thumb_pos = 0
|
||||
|
||||
# Build track with thumb
|
||||
for i in range(track_height):
|
||||
if i == thumb_pos:
|
||||
scrollbar_chars.append('█') # Thumb
|
||||
else:
|
||||
scrollbar_chars.append('░') # Track
|
||||
|
||||
# Down arrow at bottom
|
||||
scrollbar_chars.append('▼')
|
||||
|
||||
# Create scrollbar canvas
|
||||
scrollbar_text = '\n'.join(scrollbar_chars[:maxrow])
|
||||
scrollbar_canvas = urwid.Text(scrollbar_text).render((1,))
|
||||
|
||||
# Combine canvases
|
||||
combined = urwid.CanvasJoin([
|
||||
(content_canvas, None, focus, content_size[0]),
|
||||
(scrollbar_canvas, None, False, 1),
|
||||
])
|
||||
|
||||
return combined
|
||||
|
||||
def keypress(self, size, key):
|
||||
maxcol, maxrow = size
|
||||
content_size = (maxcol - 1, maxrow)
|
||||
return self._wrapped.keypress(content_size, key)
|
||||
|
||||
def mouse_event(self, size, event, button, col, row, focus):
|
||||
maxcol, maxrow = size
|
||||
content_size = (maxcol - 1, maxrow)
|
||||
|
||||
# Expand clickable area - last 2 columns count as scrollbar
|
||||
if col >= maxcol - 2:
|
||||
if button == 1 and event == 'mouse press':
|
||||
# Top 25% of scrollbar = scroll up
|
||||
if row < maxrow // 4:
|
||||
for _ in range(3):
|
||||
self._wrapped.keypress(content_size, 'up')
|
||||
self._invalidate()
|
||||
return True
|
||||
# Bottom 25% of scrollbar = scroll down
|
||||
elif row >= maxrow - (maxrow // 4):
|
||||
for _ in range(3):
|
||||
self._wrapped.keypress(content_size, 'down')
|
||||
self._invalidate()
|
||||
return True
|
||||
# Middle = page up/down based on which half
|
||||
elif row < maxrow // 2:
|
||||
for _ in range(maxrow // 2):
|
||||
self._wrapped.keypress(content_size, 'up')
|
||||
self._invalidate()
|
||||
return True
|
||||
else:
|
||||
for _ in range(maxrow // 2):
|
||||
self._wrapped.keypress(content_size, 'down')
|
||||
self._invalidate()
|
||||
return True
|
||||
|
||||
# Handle mouse wheel on scrollbar
|
||||
if button == 4: # Scroll up
|
||||
for _ in range(3):
|
||||
self._wrapped.keypress(content_size, 'up')
|
||||
self._invalidate()
|
||||
return True
|
||||
elif button == 5: # Scroll down
|
||||
for _ in range(3):
|
||||
self._wrapped.keypress(content_size, 'down')
|
||||
self._invalidate()
|
||||
return True
|
||||
|
||||
return True # Consume other scrollbar clicks
|
||||
|
||||
# Pass to wrapped widget
|
||||
return self._wrapped.mouse_event(content_size, event, button, col, row, focus)
|
||||
|
||||
def selectable(self):
|
||||
return self._wrapped.selectable()
|
||||
|
||||
def sizing(self):
|
||||
return frozenset([urwid.Sizing.BOX])
|
||||
|
||||
|
||||
class ToolBuilderLayout(urwid.WidgetWrap):
|
||||
"""Custom layout for tool builder that handles Tab cycling across all sections."""
|
||||
|
||||
def __init__(self, left_box, args_box, steps_box, args_section, steps_section, bottom_buttons, on_cancel=None):
|
||||
self._current_section = 0
|
||||
self.on_cancel = on_cancel
|
||||
|
||||
# Store references to LineBoxes for title highlighting
|
||||
self.left_box = left_box
|
||||
self.args_box = args_box
|
||||
self.steps_box = steps_box
|
||||
|
||||
# Build visual layout: left column and right column side by side
|
||||
right_pile = urwid.Pile([
|
||||
('weight', 1, args_section),
|
||||
('pack', urwid.Divider()),
|
||||
('weight', 1, steps_section),
|
||||
])
|
||||
|
||||
columns = urwid.Columns([
|
||||
('weight', 1, left_box),
|
||||
('weight', 1, right_pile),
|
||||
], dividechars=1)
|
||||
|
||||
main_pile = urwid.Pile([
|
||||
('weight', 1, columns),
|
||||
('pack', urwid.Divider()),
|
||||
('pack', bottom_buttons),
|
||||
])
|
||||
|
||||
super().__init__(main_pile)
|
||||
|
||||
# Set initial highlight
|
||||
self._update_section_titles()
|
||||
|
||||
def keypress(self, size, key):
|
||||
if key == 'tab':
|
||||
self._current_section = (self._current_section + 1) % 4
|
||||
self._focus_section(self._current_section)
|
||||
self._update_section_titles()
|
||||
return None
|
||||
elif key == 'shift tab':
|
||||
self._current_section = (self._current_section - 1) % 4
|
||||
self._focus_section(self._current_section)
|
||||
self._update_section_titles()
|
||||
return None
|
||||
elif key == 'esc':
|
||||
# Go back to main menu instead of exiting
|
||||
if self.on_cancel:
|
||||
self.on_cancel(None)
|
||||
return None
|
||||
else:
|
||||
return super().keypress(size, key)
|
||||
|
||||
def mouse_event(self, size, event, button, col, row, focus):
|
||||
# Let the parent handle the mouse event first
|
||||
result = super().mouse_event(size, event, button, col, row, focus)
|
||||
|
||||
# After mouse click, detect which section has focus and update titles
|
||||
if event == 'mouse press':
|
||||
self._detect_current_section()
|
||||
self._update_section_titles()
|
||||
|
||||
return result
|
||||
|
||||
def _detect_current_section(self):
|
||||
"""Detect which section currently has focus based on widget hierarchy."""
|
||||
main_pile = self._w
|
||||
|
||||
# Check if bottom buttons have focus (position 2)
|
||||
if main_pile.focus_position == 2:
|
||||
self._current_section = 3
|
||||
return
|
||||
|
||||
# Focus is on columns (position 0)
|
||||
columns = main_pile.contents[0][0]
|
||||
|
||||
if columns.focus_position == 0:
|
||||
# Left box (Tool Info)
|
||||
self._current_section = 0
|
||||
else:
|
||||
# Right pile
|
||||
right_pile = columns.contents[1][0]
|
||||
if right_pile.focus_position == 0:
|
||||
# Args section
|
||||
self._current_section = 1
|
||||
else:
|
||||
# Steps section
|
||||
self._current_section = 2
|
||||
|
||||
def _update_section_titles(self):
|
||||
"""Update section titles to highlight the current one with markers."""
|
||||
# Section 0 = Tool Info, Section 1 = Arguments, Section 2 = Steps, Section 3 = buttons
|
||||
if self._current_section == 0:
|
||||
self.left_box.set_title('[ Tool Info ]')
|
||||
self.args_box.set_title('Arguments')
|
||||
self.steps_box.set_title('Execution Steps')
|
||||
elif self._current_section == 1:
|
||||
self.left_box.set_title('Tool Info')
|
||||
self.args_box.set_title('[ Arguments ]')
|
||||
self.steps_box.set_title('Execution Steps')
|
||||
elif self._current_section == 2:
|
||||
self.left_box.set_title('Tool Info')
|
||||
self.args_box.set_title('Arguments')
|
||||
self.steps_box.set_title('[ Execution Steps ]')
|
||||
else:
|
||||
# Buttons focused - no section highlighted
|
||||
self.left_box.set_title('Tool Info')
|
||||
self.args_box.set_title('Arguments')
|
||||
self.steps_box.set_title('Execution Steps')
|
||||
|
||||
def _focus_section(self, section_idx):
|
||||
"""Set focus to the specified section."""
|
||||
# Get the main pile
|
||||
main_pile = self._w
|
||||
|
||||
if section_idx == 0:
|
||||
# Tool Info (left box) - focus columns, then left
|
||||
main_pile.focus_position = 0 # columns
|
||||
columns = main_pile.contents[0][0]
|
||||
columns.focus_position = 0 # left box
|
||||
elif section_idx == 1:
|
||||
# Arguments section - focus columns, then right, then args
|
||||
main_pile.focus_position = 0 # columns
|
||||
columns = main_pile.contents[0][0]
|
||||
columns.focus_position = 1 # right pile
|
||||
right_pile = columns.contents[1][0]
|
||||
right_pile.focus_position = 0 # args section
|
||||
elif section_idx == 2:
|
||||
# Steps section - focus columns, then right, then steps
|
||||
main_pile.focus_position = 0 # columns
|
||||
columns = main_pile.contents[0][0]
|
||||
columns.focus_position = 1 # right pile
|
||||
right_pile = columns.contents[1][0]
|
||||
right_pile.focus_position = 2 # steps section (after divider)
|
||||
elif section_idx == 3:
|
||||
# Save/Cancel buttons
|
||||
main_pile.focus_position = 2 # bottom buttons (after divider)
|
||||
|
||||
|
||||
class Dialog(urwid.WidgetWrap):
|
||||
"""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 - use 3D compact buttons for dialog actions
|
||||
button_widgets = []
|
||||
for label, callback in buttons:
|
||||
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')
|
||||
|
||||
# Check if body is a box widget
|
||||
# ListBox is always a box widget. For Piles with weighted items,
|
||||
# check if it ONLY supports BOX sizing (not FLOW).
|
||||
is_box_widget = isinstance(body, (urwid.ListBox, urwid.Scrollable, urwid.ScrollBar))
|
||||
if not is_box_widget:
|
||||
try:
|
||||
sizing = body.sizing()
|
||||
# Box widget if it ONLY supports BOX sizing
|
||||
is_box_widget = sizing == frozenset({urwid.Sizing.BOX})
|
||||
except (AttributeError, TypeError):
|
||||
pass
|
||||
|
||||
if is_box_widget:
|
||||
# Box widget - use directly with weight
|
||||
pile = urwid.Pile([
|
||||
('pack', title_widget),
|
||||
('pack', urwid.Divider('─')),
|
||||
('weight', 1, body),
|
||||
('pack', urwid.Divider('─')),
|
||||
('pack', buttons_centered),
|
||||
])
|
||||
else:
|
||||
# Flow widget - wrap in Filler
|
||||
body_padded = urwid.Padding(body, left=1, right=1)
|
||||
body_filled = urwid.Filler(body_padded, valign='top')
|
||||
pile = urwid.Pile([
|
||||
('pack', title_widget),
|
||||
('pack', urwid.Divider('─')),
|
||||
body_filled,
|
||||
('pack', urwid.Divider('─')),
|
||||
('pack', buttons_centered),
|
||||
])
|
||||
|
||||
# Box it
|
||||
box = urwid.LineBox(pile, title='', title_align='center')
|
||||
box = urwid.AttrMap(box, 'dialog')
|
||||
|
||||
super().__init__(box)
|
||||
Reference in New Issue
Block a user