|
|
|
@ -10,22 +10,33 @@ from .tool import (
|
|
|
|
from .providers import Provider, load_providers, add_provider, delete_provider, get_provider
|
|
|
|
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 = [
|
|
|
|
PALETTE = [
|
|
|
|
('body', 'white', 'dark blue'),
|
|
|
|
('body', 'white', 'dark blue'),
|
|
|
|
('header', 'white', 'dark red', 'bold'),
|
|
|
|
('header', 'white', 'dark red', 'bold'),
|
|
|
|
('footer', 'black', 'light gray'),
|
|
|
|
('footer', 'black', 'light gray'),
|
|
|
|
|
|
|
|
# Button colors - raised 3D effect
|
|
|
|
('button', 'black', 'light gray'),
|
|
|
|
('button', 'black', 'light gray'),
|
|
|
|
('button_focus', 'white', 'dark red', 'bold'),
|
|
|
|
('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', 'black', 'light gray'),
|
|
|
|
('edit_focus', 'black', 'yellow'),
|
|
|
|
('edit_focus', 'black', 'yellow'),
|
|
|
|
|
|
|
|
# List items
|
|
|
|
('listbox', 'black', 'light gray'),
|
|
|
|
('listbox', 'black', 'light gray'),
|
|
|
|
('listbox_focus', 'white', 'dark red'),
|
|
|
|
('listbox_focus', 'white', 'dark red'),
|
|
|
|
|
|
|
|
# Dialog
|
|
|
|
('dialog', 'black', 'light gray'),
|
|
|
|
('dialog', 'black', 'light gray'),
|
|
|
|
('dialog_border', 'white', 'dark blue'),
|
|
|
|
('dialog_border', 'white', 'dark blue'),
|
|
|
|
|
|
|
|
# Text styles
|
|
|
|
('label', 'yellow', 'dark blue', 'bold'),
|
|
|
|
('label', 'yellow', 'dark blue', 'bold'),
|
|
|
|
('error', 'white', 'dark red', 'bold'),
|
|
|
|
('error', 'white', 'dark red', 'bold'),
|
|
|
|
('success', 'light green', 'dark blue', '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
|
|
|
|
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):
|
|
|
|
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):
|
|
|
|
def __init__(self, label, on_press=None, user_data=None):
|
|
|
|
self.on_press = on_press
|
|
|
|
self.on_press = on_press
|
|
|
|
@ -150,7 +302,13 @@ class ToolListBox(urwid.ListBox):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TabCyclePile(urwid.Pile):
|
|
|
|
class TabCyclePile(urwid.Pile):
|
|
|
|
"""A Pile that uses Tab/Shift-Tab to cycle between specific positions."""
|
|
|
|
"""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):
|
|
|
|
def __init__(self, widget_list, tab_positions=None):
|
|
|
|
super().__init__(widget_list)
|
|
|
|
super().__init__(widget_list)
|
|
|
|
@ -173,6 +331,152 @@ class TabCyclePile(urwid.Pile):
|
|
|
|
return super().keypress(size, key)
|
|
|
|
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 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):
|
|
|
|
class ToolBuilderLayout(urwid.WidgetWrap):
|
|
|
|
"""Custom layout for tool builder that handles Tab cycling across all sections."""
|
|
|
|
"""Custom layout for tool builder that handles Tab cycling across all sections."""
|
|
|
|
|
|
|
|
|
|
|
|
@ -314,25 +618,34 @@ class ToolBuilderLayout(urwid.WidgetWrap):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Dialog(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):
|
|
|
|
def __init__(self, title, body, buttons, width=60, height=None):
|
|
|
|
# Title
|
|
|
|
# Title
|
|
|
|
title_widget = urwid.Text(('header', f' {title} '), align='center')
|
|
|
|
title_widget = urwid.Text(('header', f' {title} '), align='center')
|
|
|
|
|
|
|
|
|
|
|
|
# Buttons row
|
|
|
|
# Buttons row - use 3D compact buttons for dialog actions
|
|
|
|
button_widgets = []
|
|
|
|
button_widgets = []
|
|
|
|
for label, callback in buttons:
|
|
|
|
for label, callback in buttons:
|
|
|
|
btn = ClickableButton(label, callback)
|
|
|
|
btn = Button3DCompact(label, callback)
|
|
|
|
button_widgets.append(btn)
|
|
|
|
button_widgets.append(btn)
|
|
|
|
buttons_row = urwid.Columns([('pack', b) for b in button_widgets], dividechars=2)
|
|
|
|
buttons_row = urwid.Columns([('pack', b) for b in button_widgets], dividechars=2)
|
|
|
|
buttons_centered = urwid.Padding(buttons_row, align='center', width='pack')
|
|
|
|
buttons_centered = urwid.Padding(buttons_row, align='center', width='pack')
|
|
|
|
|
|
|
|
|
|
|
|
# Check if body is a box widget (like ListBox)
|
|
|
|
# Check if body is a box widget
|
|
|
|
is_box_widget = isinstance(body, urwid.ListBox)
|
|
|
|
# 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:
|
|
|
|
if is_box_widget:
|
|
|
|
# ListBox is a box widget - use directly with weight
|
|
|
|
# Box widget - use directly with weight
|
|
|
|
pile = urwid.Pile([
|
|
|
|
pile = urwid.Pile([
|
|
|
|
('pack', title_widget),
|
|
|
|
('pack', title_widget),
|
|
|
|
('pack', urwid.Divider('─')),
|
|
|
|
('pack', urwid.Divider('─')),
|
|
|
|
@ -486,22 +799,22 @@ class SmartToolsUI:
|
|
|
|
tool_listbox = ToolListBox(self._tool_walker, on_focus_change=self._on_tool_focus)
|
|
|
|
tool_listbox = ToolListBox(self._tool_walker, on_focus_change=self._on_tool_focus)
|
|
|
|
tool_box = urwid.LineBox(tool_listbox, title='Tools')
|
|
|
|
tool_box = urwid.LineBox(tool_listbox, title='Tools')
|
|
|
|
|
|
|
|
|
|
|
|
# Action buttons - Tab navigates here from tool list
|
|
|
|
# Action buttons - Tab navigates here from tool list (3D style)
|
|
|
|
create_btn = ClickableButton("Create", lambda _: self._create_tool_before_selected())
|
|
|
|
create_btn = Button3DCompact("Create", lambda _: self._create_tool_before_selected())
|
|
|
|
edit_btn = ClickableButton("Edit", lambda _: self._edit_selected_tool())
|
|
|
|
edit_btn = Button3DCompact("Edit", lambda _: self._edit_selected_tool())
|
|
|
|
delete_btn = ClickableButton("Delete", lambda _: self._delete_selected_tool())
|
|
|
|
delete_btn = Button3DCompact("Delete", lambda _: self._delete_selected_tool())
|
|
|
|
test_btn = ClickableButton("Test", lambda _: self._test_selected_tool())
|
|
|
|
test_btn = Button3DCompact("Test", lambda _: self._test_selected_tool())
|
|
|
|
providers_btn = ClickableButton("Providers", lambda _: self.manage_providers())
|
|
|
|
providers_btn = Button3DCompact("Providers", lambda _: self.manage_providers())
|
|
|
|
|
|
|
|
|
|
|
|
buttons_row = urwid.Columns([
|
|
|
|
buttons_row = urwid.Columns([
|
|
|
|
('pack', create_btn),
|
|
|
|
('pack', create_btn),
|
|
|
|
('pack', urwid.Text(" ")),
|
|
|
|
('pack', urwid.Text(" ")),
|
|
|
|
('pack', edit_btn),
|
|
|
|
('pack', edit_btn),
|
|
|
|
('pack', urwid.Text(" ")),
|
|
|
|
('pack', urwid.Text(" ")),
|
|
|
|
('pack', delete_btn),
|
|
|
|
('pack', delete_btn),
|
|
|
|
('pack', urwid.Text(" ")),
|
|
|
|
('pack', urwid.Text(" ")),
|
|
|
|
('pack', test_btn),
|
|
|
|
('pack', test_btn),
|
|
|
|
('pack', urwid.Text(" ")),
|
|
|
|
('pack', urwid.Text(" ")),
|
|
|
|
('pack', providers_btn),
|
|
|
|
('pack', providers_btn),
|
|
|
|
])
|
|
|
|
])
|
|
|
|
buttons_padded = urwid.Padding(buttons_row, align='left', left=1)
|
|
|
|
buttons_padded = urwid.Padding(buttons_row, align='left', left=1)
|
|
|
|
@ -526,9 +839,9 @@ class SmartToolsUI:
|
|
|
|
info_filler = urwid.Filler(info_content, valign='top')
|
|
|
|
info_filler = urwid.Filler(info_content, valign='top')
|
|
|
|
info_box = urwid.LineBox(info_filler, title='Tool Info')
|
|
|
|
info_box = urwid.LineBox(info_filler, title='Tool Info')
|
|
|
|
|
|
|
|
|
|
|
|
# Exit button at bottom
|
|
|
|
# Exit button at bottom (3D style)
|
|
|
|
exit_btn = ClickableButton("EXIT", lambda _: self.exit_app())
|
|
|
|
exit_btn = Button3DCompact("EXIT", lambda _: self.exit_app())
|
|
|
|
exit_centered = urwid.Padding(exit_btn, align='center', width=10)
|
|
|
|
exit_centered = urwid.Padding(exit_btn, align='center', width=12)
|
|
|
|
|
|
|
|
|
|
|
|
# Use a custom Pile that handles Tab to cycle between tool list and buttons
|
|
|
|
# Use a custom Pile that handles Tab to cycle between tool list and buttons
|
|
|
|
self._main_pile = TabCyclePile([
|
|
|
|
self._main_pile = TabCyclePile([
|
|
|
|
@ -894,7 +1207,6 @@ class SmartToolsUI:
|
|
|
|
|
|
|
|
|
|
|
|
def _edit_argument_at(self, idx):
|
|
|
|
def _edit_argument_at(self, idx):
|
|
|
|
"""Edit argument at index."""
|
|
|
|
"""Edit argument at index."""
|
|
|
|
arg = self._current_tool.arguments[idx]
|
|
|
|
|
|
|
|
self._do_edit_argument(idx)
|
|
|
|
self._do_edit_argument(idx)
|
|
|
|
|
|
|
|
|
|
|
|
def _edit_step_at(self, idx):
|
|
|
|
def _edit_step_at(self, idx):
|
|
|
|
@ -947,26 +1259,6 @@ class SmartToolsUI:
|
|
|
|
dialog = Dialog("Add Argument", body, [("OK", on_ok), ("Cancel", on_cancel)])
|
|
|
|
dialog = Dialog("Add Argument", body, [("OK", on_ok), ("Cancel", on_cancel)])
|
|
|
|
self.show_overlay(dialog, width=50, height=14)
|
|
|
|
self.show_overlay(dialog, width=50, height=14)
|
|
|
|
|
|
|
|
|
|
|
|
def _edit_argument_dialog(self, idx):
|
|
|
|
|
|
|
|
"""Show edit/delete argument dialog."""
|
|
|
|
|
|
|
|
arg = self._current_tool.arguments[idx]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def on_edit(_):
|
|
|
|
|
|
|
|
self.close_overlay()
|
|
|
|
|
|
|
|
self._do_edit_argument(idx)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def on_delete(_):
|
|
|
|
|
|
|
|
self._current_tool.arguments.pop(idx)
|
|
|
|
|
|
|
|
self.close_overlay()
|
|
|
|
|
|
|
|
self._show_tool_builder()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def on_cancel(_):
|
|
|
|
|
|
|
|
self.close_overlay()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
body = urwid.Text(f"Argument: {arg.flag} -> {{{arg.variable}}}")
|
|
|
|
|
|
|
|
dialog = Dialog("Edit Argument", body, [("Edit", on_edit), ("Delete", on_delete), ("Cancel", on_cancel)])
|
|
|
|
|
|
|
|
self.show_overlay(dialog, width=50, height=10)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _do_edit_argument(self, idx):
|
|
|
|
def _do_edit_argument(self, idx):
|
|
|
|
"""Edit an existing argument."""
|
|
|
|
"""Edit an existing argument."""
|
|
|
|
# Save current field values before showing dialog
|
|
|
|
# Save current field values before showing dialog
|
|
|
|
@ -1006,11 +1298,19 @@ class SmartToolsUI:
|
|
|
|
|
|
|
|
|
|
|
|
def on_prompt(_):
|
|
|
|
def on_prompt(_):
|
|
|
|
self.close_overlay()
|
|
|
|
self.close_overlay()
|
|
|
|
self._add_prompt_dialog()
|
|
|
|
# Defer dialog opening to avoid overlay rendering issues
|
|
|
|
|
|
|
|
if self.loop:
|
|
|
|
|
|
|
|
self.loop.set_alarm_in(0, lambda loop, data: self._add_prompt_dialog())
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
self._add_prompt_dialog()
|
|
|
|
|
|
|
|
|
|
|
|
def on_code(_):
|
|
|
|
def on_code(_):
|
|
|
|
self.close_overlay()
|
|
|
|
self.close_overlay()
|
|
|
|
self._add_code_dialog()
|
|
|
|
# Defer dialog opening to avoid overlay rendering issues
|
|
|
|
|
|
|
|
if self.loop:
|
|
|
|
|
|
|
|
self.loop.set_alarm_in(0, lambda loop, data: self._add_code_dialog())
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
self._add_code_dialog()
|
|
|
|
|
|
|
|
|
|
|
|
def on_cancel(_):
|
|
|
|
def on_cancel(_):
|
|
|
|
self.close_overlay()
|
|
|
|
self.close_overlay()
|
|
|
|
@ -1119,17 +1419,14 @@ class SmartToolsUI:
|
|
|
|
popup = Dialog("Select Provider", body, [])
|
|
|
|
popup = Dialog("Select Provider", body, [])
|
|
|
|
self.show_overlay(popup, width=50, height=min(len(provider_names) + 6, 16))
|
|
|
|
self.show_overlay(popup, width=50, height=min(len(provider_names) + 6, 16))
|
|
|
|
|
|
|
|
|
|
|
|
provider_select_btn = urwid.AttrMap(
|
|
|
|
provider_select_btn = Button3DCompact("▼", on_press=show_provider_dropdown)
|
|
|
|
urwid.Button("Select", on_press=show_provider_dropdown),
|
|
|
|
|
|
|
|
'button', 'button_focus'
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# File input for external prompt
|
|
|
|
# File input for external prompt
|
|
|
|
default_file = existing.prompt_file if existing and existing.prompt_file else "prompt.txt"
|
|
|
|
default_file = existing.prompt_file if existing and existing.prompt_file else "prompt.txt"
|
|
|
|
file_edit = urwid.Edit(('label', "File: "), default_file)
|
|
|
|
file_edit = urwid.Edit(('label', "File: "), default_file)
|
|
|
|
|
|
|
|
|
|
|
|
# Multiline prompt editor
|
|
|
|
# Multiline prompt editor - use TabPassEdit so Tab passes through for navigation
|
|
|
|
prompt_edit = urwid.Edit(
|
|
|
|
prompt_edit = TabPassEdit(
|
|
|
|
edit_text=existing.prompt if existing else "{input}",
|
|
|
|
edit_text=existing.prompt if existing else "{input}",
|
|
|
|
multiline=True
|
|
|
|
multiline=True
|
|
|
|
)
|
|
|
|
)
|
|
|
|
@ -1209,51 +1506,57 @@ class SmartToolsUI:
|
|
|
|
def on_cancel(_):
|
|
|
|
def on_cancel(_):
|
|
|
|
self.close_overlay()
|
|
|
|
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
|
|
|
|
# Prompt editor in a box - use ListBox for proper focus handling and scrolling
|
|
|
|
prompt_box = urwid.LineBox(
|
|
|
|
# Wrap in DOSScrollBar for DOS-style scrollbar with arrow buttons
|
|
|
|
urwid.Filler(urwid.AttrMap(prompt_edit, 'edit', 'edit_focus'), valign='top'),
|
|
|
|
prompt_edit_styled = urwid.AttrMap(prompt_edit, 'edit', 'edit_focus')
|
|
|
|
title="Prompt"
|
|
|
|
prompt_walker = urwid.SimpleFocusListWalker([prompt_edit_styled])
|
|
|
|
)
|
|
|
|
prompt_listbox = urwid.ListBox(prompt_walker)
|
|
|
|
|
|
|
|
prompt_scrollbar = DOSScrollBar(prompt_listbox)
|
|
|
|
|
|
|
|
prompt_box = urwid.LineBox(prompt_scrollbar, title="Prompt")
|
|
|
|
|
|
|
|
|
|
|
|
body = urwid.Pile([
|
|
|
|
# Use TabCyclePile so Tab cycles between sections
|
|
|
|
vars_text,
|
|
|
|
# Note: All flow widgets must be explicitly wrapped in ('pack', ...) when
|
|
|
|
urwid.Divider(),
|
|
|
|
# the Pile contains weighted items (urwid 3.x requirement)
|
|
|
|
urwid.Columns([
|
|
|
|
body = TabCyclePile([
|
|
|
|
|
|
|
|
('pack', vars_text),
|
|
|
|
|
|
|
|
('pack', urwid.Divider()),
|
|
|
|
|
|
|
|
('pack', urwid.Columns([
|
|
|
|
('pack', urwid.Text(('label', "Provider: "))),
|
|
|
|
('pack', urwid.Text(('label', "Provider: "))),
|
|
|
|
('weight', 1, provider_btn),
|
|
|
|
('weight', 1, provider_btn),
|
|
|
|
('pack', urwid.Text(" ")),
|
|
|
|
('pack', urwid.Text(" ")),
|
|
|
|
('pack', provider_select_btn),
|
|
|
|
('pack', provider_select_btn),
|
|
|
|
]),
|
|
|
|
])),
|
|
|
|
urwid.Divider(),
|
|
|
|
('pack', urwid.Divider()),
|
|
|
|
urwid.Columns([
|
|
|
|
('pack', urwid.Columns([
|
|
|
|
('weight', 1, urwid.AttrMap(file_edit, 'edit', 'edit_focus')),
|
|
|
|
('weight', 1, urwid.AttrMap(file_edit, 'edit', 'edit_focus')),
|
|
|
|
('pack', urwid.Text(" ")),
|
|
|
|
('pack', urwid.Text(" ")),
|
|
|
|
('pack', load_btn),
|
|
|
|
('pack', load_btn),
|
|
|
|
]),
|
|
|
|
])),
|
|
|
|
status_text,
|
|
|
|
('pack', status_text),
|
|
|
|
('weight', 1, prompt_box),
|
|
|
|
('weight', 1, prompt_box),
|
|
|
|
urwid.Divider(),
|
|
|
|
('pack', urwid.Divider()),
|
|
|
|
urwid.AttrMap(output_edit, 'edit', 'edit_focus'),
|
|
|
|
('pack', urwid.AttrMap(output_edit, 'edit', 'edit_focus')),
|
|
|
|
])
|
|
|
|
], tab_positions=[2, 4, 6, 8])
|
|
|
|
|
|
|
|
|
|
|
|
title = "Edit Prompt Step" if existing else "Add Prompt Step"
|
|
|
|
title = "Edit Prompt Step" if existing else "Add Prompt Step"
|
|
|
|
dialog = Dialog(title, body, [("OK", on_ok), ("Cancel", on_cancel)])
|
|
|
|
dialog = Dialog(title, body, [("OK", on_ok), ("Cancel", on_cancel)])
|
|
|
|
self.show_overlay(dialog, width=70, height=22)
|
|
|
|
self.show_overlay(dialog, width=70, height=22)
|
|
|
|
|
|
|
|
|
|
|
|
def _add_code_dialog(self, existing=None, idx=-1):
|
|
|
|
def _add_code_dialog(self, existing=None, idx=-1):
|
|
|
|
"""Add/edit code step with multiline editor and file support."""
|
|
|
|
"""Add/edit code step with multiline editor, file support, and AI auto-adjust."""
|
|
|
|
from .tool import get_tools_dir
|
|
|
|
from .tool import get_tools_dir
|
|
|
|
|
|
|
|
from .providers import load_providers, call_provider
|
|
|
|
|
|
|
|
|
|
|
|
# File name input (default based on output_var)
|
|
|
|
# File name input (default based on output_var)
|
|
|
|
default_output_var = existing.output_var if existing else "processed"
|
|
|
|
default_output_var = existing.output_var if existing else "processed"
|
|
|
|
default_file = existing.code_file if existing and existing.code_file else f"{default_output_var}.py"
|
|
|
|
default_file = existing.code_file if existing and existing.code_file else f"{default_output_var}.py"
|
|
|
|
file_edit = urwid.Edit(('label', "File: "), default_file)
|
|
|
|
file_edit = urwid.Edit(('label', "File: "), default_file)
|
|
|
|
|
|
|
|
|
|
|
|
# Multiline code editor (default uses output_var name)
|
|
|
|
# Multiline code editor - use TabPassEdit so Tab passes through for navigation
|
|
|
|
default_code = existing.code if existing else f"{default_output_var} = input.upper()"
|
|
|
|
default_code = existing.code if existing else f"{default_output_var} = input.upper()"
|
|
|
|
code_edit = urwid.Edit(
|
|
|
|
code_edit = TabPassEdit(
|
|
|
|
edit_text=default_code,
|
|
|
|
edit_text=default_code,
|
|
|
|
multiline=True
|
|
|
|
multiline=True
|
|
|
|
)
|
|
|
|
)
|
|
|
|
@ -1265,6 +1568,151 @@ class SmartToolsUI:
|
|
|
|
|
|
|
|
|
|
|
|
status_text = urwid.Text("")
|
|
|
|
status_text = urwid.Text("")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# --- Auto-adjust AI feature ---
|
|
|
|
|
|
|
|
providers = load_providers()
|
|
|
|
|
|
|
|
provider_names = [p.name for p in providers]
|
|
|
|
|
|
|
|
if not provider_names:
|
|
|
|
|
|
|
|
provider_names = ["mock"]
|
|
|
|
|
|
|
|
selected_ai_provider = [provider_names[0]]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ai_provider_btn_text = urwid.Text(provider_names[0])
|
|
|
|
|
|
|
|
ai_provider_btn = urwid.AttrMap(
|
|
|
|
|
|
|
|
urwid.Padding(ai_provider_btn_text, left=1, right=1),
|
|
|
|
|
|
|
|
'edit', 'edit_focus'
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def show_ai_provider_dropdown(_):
|
|
|
|
|
|
|
|
provider_lookup = {p.name: p.description for p in providers}
|
|
|
|
|
|
|
|
desc_text = urwid.Text("")
|
|
|
|
|
|
|
|
desc_box = urwid.AttrMap(urwid.Padding(desc_text, left=1, right=1), 'label')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def update_description(name):
|
|
|
|
|
|
|
|
desc = provider_lookup.get(name, "")
|
|
|
|
|
|
|
|
desc_text.set_text(('label', desc if desc else "No description"))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def select_provider(name):
|
|
|
|
|
|
|
|
def callback(_):
|
|
|
|
|
|
|
|
selected_ai_provider[0] = name
|
|
|
|
|
|
|
|
ai_provider_btn_text.set_text(name)
|
|
|
|
|
|
|
|
self.close_overlay()
|
|
|
|
|
|
|
|
return callback
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class DescriptiveButton(urwid.Button):
|
|
|
|
|
|
|
|
def __init__(btn_self, name, desc_callback):
|
|
|
|
|
|
|
|
super().__init__(name, on_press=select_provider(name))
|
|
|
|
|
|
|
|
btn_self._name = name
|
|
|
|
|
|
|
|
btn_self._desc_callback = desc_callback
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def render(btn_self, size, focus=False):
|
|
|
|
|
|
|
|
if focus:
|
|
|
|
|
|
|
|
btn_self._desc_callback(btn_self._name)
|
|
|
|
|
|
|
|
return super().render(size, focus)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
items = []
|
|
|
|
|
|
|
|
for name in provider_names:
|
|
|
|
|
|
|
|
short_desc = provider_lookup.get(name, "")
|
|
|
|
|
|
|
|
if "|" in short_desc:
|
|
|
|
|
|
|
|
short_desc = short_desc.split("|", 1)[1].strip()[:20]
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
short_desc = short_desc[:20]
|
|
|
|
|
|
|
|
label = f"{name:<18} {short_desc}"
|
|
|
|
|
|
|
|
btn = DescriptiveButton(name, update_description)
|
|
|
|
|
|
|
|
btn.set_label(label)
|
|
|
|
|
|
|
|
items.append(urwid.AttrMap(btn, 'button', 'button_focus'))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
update_description(provider_names[0])
|
|
|
|
|
|
|
|
listbox = urwid.ListBox(urwid.SimpleFocusListWalker(items))
|
|
|
|
|
|
|
|
popup_body = urwid.Pile([
|
|
|
|
|
|
|
|
('weight', 1, listbox),
|
|
|
|
|
|
|
|
('pack', urwid.Divider('─')),
|
|
|
|
|
|
|
|
('pack', desc_box),
|
|
|
|
|
|
|
|
])
|
|
|
|
|
|
|
|
popup = Dialog("Select Provider", popup_body, [])
|
|
|
|
|
|
|
|
self.show_overlay(popup, width=50, height=min(len(provider_names) + 6, 16))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
INSTRUCTION: [Describe what you want]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
CURRENT CODE:
|
|
|
|
|
|
|
|
```python
|
|
|
|
|
|
|
|
{{code}}
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
AVAILABLE VARIABLES: {', '.join(vars_available)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Return ONLY the Python code, no explanations or markdown fencing."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Multiline editable prompt for AI with DOS-style scrollbar
|
|
|
|
|
|
|
|
ai_prompt_edit = TabPassEdit(edit_text=default_ai_prompt, multiline=True)
|
|
|
|
|
|
|
|
ai_prompt_styled = urwid.AttrMap(ai_prompt_edit, 'edit', 'edit_focus')
|
|
|
|
|
|
|
|
ai_prompt_walker = urwid.SimpleFocusListWalker([ai_prompt_styled])
|
|
|
|
|
|
|
|
ai_prompt_listbox = urwid.ListBox(ai_prompt_walker)
|
|
|
|
|
|
|
|
ai_prompt_scrollbar = DOSScrollBar(ai_prompt_listbox)
|
|
|
|
|
|
|
|
ai_prompt_box = urwid.LineBox(ai_prompt_scrollbar, title="Prompt")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Output/feedback area for AI responses
|
|
|
|
|
|
|
|
ai_output_text = urwid.Text("")
|
|
|
|
|
|
|
|
ai_output_walker = urwid.SimpleFocusListWalker([ai_output_text])
|
|
|
|
|
|
|
|
ai_output_listbox = urwid.ListBox(ai_output_walker)
|
|
|
|
|
|
|
|
ai_output_box = urwid.LineBox(ai_output_listbox, title="Output & Feedback")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def on_auto_adjust(_):
|
|
|
|
|
|
|
|
prompt_template = ai_prompt_edit.edit_text.strip()
|
|
|
|
|
|
|
|
if not prompt_template:
|
|
|
|
|
|
|
|
ai_output_text.set_text(('error', "Enter a prompt for the AI"))
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
current_code = code_edit.edit_text.strip()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Replace {code} placeholder with actual code
|
|
|
|
|
|
|
|
prompt = prompt_template.replace("{code}", current_code)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
provider_name = selected_ai_provider[0]
|
|
|
|
|
|
|
|
ai_output_text.set_text(('label', f"Calling {provider_name}...\nPlease wait..."))
|
|
|
|
|
|
|
|
self.refresh()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
result = call_provider(provider_name, prompt)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if result.success:
|
|
|
|
|
|
|
|
new_code = result.text.strip()
|
|
|
|
|
|
|
|
# Strip markdown code fences if present
|
|
|
|
|
|
|
|
if new_code.startswith("```python"):
|
|
|
|
|
|
|
|
new_code = new_code[9:]
|
|
|
|
|
|
|
|
if new_code.startswith("```"):
|
|
|
|
|
|
|
|
new_code = new_code[3:]
|
|
|
|
|
|
|
|
if new_code.endswith("```"):
|
|
|
|
|
|
|
|
new_code = new_code[:-3]
|
|
|
|
|
|
|
|
new_code = new_code.strip()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
code_edit.set_edit_text(new_code)
|
|
|
|
|
|
|
|
ai_output_text.set_text(('success', f"✓ Code updated successfully!\n\nProvider: {provider_name}\nResponse length: {len(result.text)} chars"))
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
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 = 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([
|
|
|
|
|
|
|
|
('pack', urwid.Text(('label', "Provider: "))),
|
|
|
|
|
|
|
|
('pack', ai_provider_btn),
|
|
|
|
|
|
|
|
('pack', ai_provider_select_btn),
|
|
|
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ai_assist_content = urwid.Pile([
|
|
|
|
|
|
|
|
('pack', ai_provider_row),
|
|
|
|
|
|
|
|
('pack', urwid.Divider()),
|
|
|
|
|
|
|
|
('weight', 2, ai_prompt_box),
|
|
|
|
|
|
|
|
('weight', 1, ai_output_box),
|
|
|
|
|
|
|
|
('pack', urwid.Padding(auto_adjust_btn, align='center', width=16)),
|
|
|
|
|
|
|
|
])
|
|
|
|
|
|
|
|
ai_assist_box = urwid.LineBox(ai_assist_content, title="AI Assisted Auto-adjust")
|
|
|
|
|
|
|
|
# --- End Auto-adjust feature ---
|
|
|
|
|
|
|
|
|
|
|
|
def do_load():
|
|
|
|
def do_load():
|
|
|
|
"""Actually load code from file."""
|
|
|
|
"""Actually load code from file."""
|
|
|
|
filename = file_edit.edit_text.strip()
|
|
|
|
filename = file_edit.edit_text.strip()
|
|
|
|
@ -1291,7 +1739,6 @@ class SmartToolsUI:
|
|
|
|
status_text.set_text(('error', f"File not found: {filename}"))
|
|
|
|
status_text.set_text(('error', f"File not found: {filename}"))
|
|
|
|
return
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
# Show confirmation dialog
|
|
|
|
|
|
|
|
def on_yes(_):
|
|
|
|
def on_yes(_):
|
|
|
|
self.close_overlay()
|
|
|
|
self.close_overlay()
|
|
|
|
do_load()
|
|
|
|
do_load()
|
|
|
|
@ -1310,7 +1757,6 @@ class SmartToolsUI:
|
|
|
|
output_var = output_edit.edit_text.strip() or "processed"
|
|
|
|
output_var = output_edit.edit_text.strip() or "processed"
|
|
|
|
code_file = file_edit.edit_text.strip() or None
|
|
|
|
code_file = file_edit.edit_text.strip() or None
|
|
|
|
|
|
|
|
|
|
|
|
# Validate Python syntax
|
|
|
|
|
|
|
|
if code:
|
|
|
|
if code:
|
|
|
|
try:
|
|
|
|
try:
|
|
|
|
ast.parse(code)
|
|
|
|
ast.parse(code)
|
|
|
|
@ -1319,7 +1765,6 @@ class SmartToolsUI:
|
|
|
|
status_text.set_text(('error', f"Syntax error{line_info}: {e.msg}"))
|
|
|
|
status_text.set_text(('error', f"Syntax error{line_info}: {e.msg}"))
|
|
|
|
return
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
# Auto-save to file if filename is set
|
|
|
|
|
|
|
|
if code_file:
|
|
|
|
if code_file:
|
|
|
|
tool_dir = get_tools_dir() / self._current_tool.name
|
|
|
|
tool_dir = get_tools_dir() / self._current_tool.name
|
|
|
|
tool_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
tool_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
@ -1343,55 +1788,42 @@ class SmartToolsUI:
|
|
|
|
def on_cancel(_):
|
|
|
|
def on_cancel(_):
|
|
|
|
self.close_overlay()
|
|
|
|
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 with scrolling
|
|
|
|
# Code editor in a box - use ListBox for proper focus handling and scrolling
|
|
|
|
code_box = urwid.LineBox(
|
|
|
|
# Wrap in DOSScrollBar for DOS-style scrollbar with arrow buttons
|
|
|
|
urwid.Filler(urwid.AttrMap(code_edit, 'edit', 'edit_focus'), valign='top'),
|
|
|
|
code_edit_styled = urwid.AttrMap(code_edit, 'edit', 'edit_focus')
|
|
|
|
title="Code"
|
|
|
|
code_walker = urwid.SimpleFocusListWalker([code_edit_styled])
|
|
|
|
)
|
|
|
|
code_listbox = urwid.ListBox(code_walker)
|
|
|
|
|
|
|
|
code_scrollbar = DOSScrollBar(code_listbox)
|
|
|
|
|
|
|
|
code_box = urwid.LineBox(code_scrollbar, title="Code")
|
|
|
|
|
|
|
|
|
|
|
|
body = urwid.Pile([
|
|
|
|
# Layout: Code editor on left, AI assist box on right
|
|
|
|
vars_text,
|
|
|
|
main_columns = urwid.Columns([
|
|
|
|
urwid.Divider(),
|
|
|
|
('weight', 1, code_box),
|
|
|
|
urwid.Columns([
|
|
|
|
('weight', 1, ai_assist_box),
|
|
|
|
|
|
|
|
], dividechars=1)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Use TabCyclePile so Tab cycles between sections
|
|
|
|
|
|
|
|
# Note: All flow widgets must be explicitly wrapped in ('pack', ...) when
|
|
|
|
|
|
|
|
# the Pile contains weighted items (urwid 3.x requirement)
|
|
|
|
|
|
|
|
body = TabCyclePile([
|
|
|
|
|
|
|
|
('pack', vars_text),
|
|
|
|
|
|
|
|
('pack', urwid.Divider()),
|
|
|
|
|
|
|
|
('pack', urwid.Columns([
|
|
|
|
('weight', 1, urwid.AttrMap(file_edit, 'edit', 'edit_focus')),
|
|
|
|
('weight', 1, urwid.AttrMap(file_edit, 'edit', 'edit_focus')),
|
|
|
|
('pack', urwid.Text(" ")),
|
|
|
|
('pack', urwid.Text(" ")),
|
|
|
|
('pack', load_btn),
|
|
|
|
('pack', load_btn),
|
|
|
|
]),
|
|
|
|
])),
|
|
|
|
status_text,
|
|
|
|
('pack', status_text),
|
|
|
|
('weight', 1, code_box),
|
|
|
|
('weight', 1, main_columns),
|
|
|
|
urwid.Divider(),
|
|
|
|
('pack', urwid.Divider()),
|
|
|
|
urwid.AttrMap(output_edit, 'edit', 'edit_focus'),
|
|
|
|
('pack', urwid.AttrMap(output_edit, 'edit', 'edit_focus')),
|
|
|
|
])
|
|
|
|
], tab_positions=[2, 4, 6])
|
|
|
|
|
|
|
|
|
|
|
|
title = "Edit Code Step" if existing else "Add Code Step"
|
|
|
|
title = "Edit Code Step" if existing else "Add Code Step"
|
|
|
|
dialog = Dialog(title, body, [("OK", on_ok), ("Cancel", on_cancel)])
|
|
|
|
dialog = Dialog(title, body, [("OK", on_ok), ("Cancel", on_cancel)])
|
|
|
|
self.show_overlay(dialog, width=70, height=20)
|
|
|
|
self.show_overlay(dialog, width=90, height=30)
|
|
|
|
|
|
|
|
|
|
|
|
def _edit_step_dialog(self, idx):
|
|
|
|
|
|
|
|
"""Show edit/delete step dialog."""
|
|
|
|
|
|
|
|
step = self._current_tool.steps[idx]
|
|
|
|
|
|
|
|
step_type = "Prompt" if isinstance(step, PromptStep) else "Code"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def on_edit(_):
|
|
|
|
|
|
|
|
self.close_overlay()
|
|
|
|
|
|
|
|
if isinstance(step, PromptStep):
|
|
|
|
|
|
|
|
self._add_prompt_dialog(step, idx)
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
self._add_code_dialog(step, idx)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def on_delete(_):
|
|
|
|
|
|
|
|
self._current_tool.steps.pop(idx)
|
|
|
|
|
|
|
|
self.close_overlay()
|
|
|
|
|
|
|
|
self._show_tool_builder()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def on_cancel(_):
|
|
|
|
|
|
|
|
self.close_overlay()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
body = urwid.Text(f"Step {idx+1}: {step_type}")
|
|
|
|
|
|
|
|
dialog = Dialog("Edit Step", body, [("Edit", on_edit), ("Delete", on_delete), ("Cancel", on_cancel)])
|
|
|
|
|
|
|
|
self.show_overlay(dialog, width=45, height=10)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _on_save_tool(self, _):
|
|
|
|
def _on_save_tool(self, _):
|
|
|
|
"""Save the tool."""
|
|
|
|
"""Save the tool."""
|
|
|
|
@ -1429,84 +1861,6 @@ class SmartToolsUI:
|
|
|
|
"""Cancel tool editing."""
|
|
|
|
"""Cancel tool editing."""
|
|
|
|
self.show_main_menu()
|
|
|
|
self.show_main_menu()
|
|
|
|
|
|
|
|
|
|
|
|
# ==================== Tool Selection ====================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def select_edit_tool(self):
|
|
|
|
|
|
|
|
"""Select a tool to edit."""
|
|
|
|
|
|
|
|
tools = list_tools()
|
|
|
|
|
|
|
|
if not tools:
|
|
|
|
|
|
|
|
self.message_box("Edit Tool", "No tools found.")
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def on_select(name):
|
|
|
|
|
|
|
|
self.close_overlay()
|
|
|
|
|
|
|
|
tool = load_tool(name)
|
|
|
|
|
|
|
|
if tool:
|
|
|
|
|
|
|
|
self.tool_builder(tool)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def on_cancel(_):
|
|
|
|
|
|
|
|
self.close_overlay()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
items = []
|
|
|
|
|
|
|
|
for name in tools:
|
|
|
|
|
|
|
|
item = SelectableText(f" {name} ", value=name, on_select=on_select)
|
|
|
|
|
|
|
|
items.append(item)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
listbox = urwid.ListBox(urwid.SimpleFocusListWalker(items))
|
|
|
|
|
|
|
|
dialog = Dialog("Select Tool to Edit", listbox, [("Cancel", on_cancel)])
|
|
|
|
|
|
|
|
self.show_overlay(dialog, width=50, height=min(len(tools) + 8, 20))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def select_delete_tool(self):
|
|
|
|
|
|
|
|
"""Select a tool to delete."""
|
|
|
|
|
|
|
|
tools = list_tools()
|
|
|
|
|
|
|
|
if not tools:
|
|
|
|
|
|
|
|
self.message_box("Delete Tool", "No tools found.")
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def on_select(name):
|
|
|
|
|
|
|
|
self.close_overlay()
|
|
|
|
|
|
|
|
def do_delete():
|
|
|
|
|
|
|
|
delete_tool(name)
|
|
|
|
|
|
|
|
self.message_box("Deleted", f"Tool '{name}' deleted.")
|
|
|
|
|
|
|
|
self.yes_no("Confirm", f"Delete tool '{name}'?", on_yes=do_delete)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def on_cancel(_):
|
|
|
|
|
|
|
|
self.close_overlay()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
items = []
|
|
|
|
|
|
|
|
for name in tools:
|
|
|
|
|
|
|
|
item = SelectableText(f" {name} ", value=name, on_select=on_select)
|
|
|
|
|
|
|
|
items.append(item)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
listbox = urwid.ListBox(urwid.SimpleFocusListWalker(items))
|
|
|
|
|
|
|
|
dialog = Dialog("Select Tool to Delete", listbox, [("Cancel", on_cancel)])
|
|
|
|
|
|
|
|
self.show_overlay(dialog, width=50, height=min(len(tools) + 8, 20))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def select_test_tool(self):
|
|
|
|
|
|
|
|
"""Select a tool to test."""
|
|
|
|
|
|
|
|
tools = list_tools()
|
|
|
|
|
|
|
|
if not tools:
|
|
|
|
|
|
|
|
self.message_box("Test Tool", "No tools found.")
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def on_select(name):
|
|
|
|
|
|
|
|
self.close_overlay()
|
|
|
|
|
|
|
|
tool = load_tool(name)
|
|
|
|
|
|
|
|
if tool:
|
|
|
|
|
|
|
|
self._test_tool(tool)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def on_cancel(_):
|
|
|
|
|
|
|
|
self.close_overlay()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
items = []
|
|
|
|
|
|
|
|
for name in tools:
|
|
|
|
|
|
|
|
item = SelectableText(f" {name} ", value=name, on_select=on_select)
|
|
|
|
|
|
|
|
items.append(item)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
listbox = urwid.ListBox(urwid.SimpleFocusListWalker(items))
|
|
|
|
|
|
|
|
dialog = Dialog("Select Tool to Test", listbox, [("Cancel", on_cancel)])
|
|
|
|
|
|
|
|
self.show_overlay(dialog, width=50, height=min(len(tools) + 8, 20))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _test_tool(self, tool):
|
|
|
|
def _test_tool(self, tool):
|
|
|
|
"""Test a tool with mock input."""
|
|
|
|
"""Test a tool with mock input."""
|
|
|
|
def on_input(text):
|
|
|
|
def on_input(text):
|
|
|
|
@ -1525,20 +1879,6 @@ class SmartToolsUI:
|
|
|
|
|
|
|
|
|
|
|
|
self.input_dialog("Test Input", "Enter test input", "Hello world", on_input)
|
|
|
|
self.input_dialog("Test Input", "Enter test input", "Hello world", on_input)
|
|
|
|
|
|
|
|
|
|
|
|
def show_tools_list(self):
|
|
|
|
|
|
|
|
"""Show list of all tools."""
|
|
|
|
|
|
|
|
tools = list_tools()
|
|
|
|
|
|
|
|
if not tools:
|
|
|
|
|
|
|
|
self.message_box("Tools", "No tools found.")
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
text = ""
|
|
|
|
|
|
|
|
for name in tools:
|
|
|
|
|
|
|
|
tool = load_tool(name)
|
|
|
|
|
|
|
|
if tool:
|
|
|
|
|
|
|
|
text += f"{name}: {tool.description or 'No description'}\n"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
self.message_box("Available Tools", text.strip())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ==================== Provider Management ====================
|
|
|
|
# ==================== Provider Management ====================
|
|
|
|
|
|
|
|
|
|
|
|
|