Compare commits
5 Commits
765a53df1c
...
cff3b674ac
| Author | SHA1 | Date |
|---|---|---|
|
|
cff3b674ac | |
|
|
f84e06aa05 | |
|
|
67a83e5379 | |
|
|
6a4e153a62 | |
|
|
f227831feb |
|
|
@ -298,11 +298,15 @@ vnoremap <leader>ec :!explain-code<CR>
|
||||||
| **Select text** | `Shift` + mouse drag |
|
| **Select text** | `Shift` + mouse drag |
|
||||||
| **Copy** | Terminal native (with Shift) |
|
| **Copy** | Terminal native (with Shift) |
|
||||||
| **Paste** | `Ctrl+Shift+V` |
|
| **Paste** | `Ctrl+Shift+V` |
|
||||||
|
| **Undo** (code editor) | `Alt+U` |
|
||||||
|
| **Redo** (code editor) | `Alt+R` |
|
||||||
|
|
||||||
**Tips:**
|
**Tips:**
|
||||||
- Hold `Shift` while using mouse for terminal-native text selection.
|
- Hold `Shift` while using mouse for terminal-native text selection.
|
||||||
- Code/Prompt editors have DOS-style scrollbars with `▲` and `▼` arrow buttons.
|
- Code/Prompt editors have DOS-style scrollbars with `▲` and `▼` arrow buttons.
|
||||||
- In step dialogs, use `Tab` to cycle between File, Editor, and Output fields.
|
- In step dialogs, use `Tab` to cycle between File, Editor, and Output fields.
|
||||||
|
- The code editor supports undo/redo (up to 50 states) with `Alt+U` and `Alt+R`.
|
||||||
|
- Use the `$EDITOR` button to open code or prompts in your external editor.
|
||||||
|
|
||||||
## AI-Assisted Code Generation
|
## AI-Assisted Code Generation
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -341,6 +341,86 @@ class TabPassEdit(urwid.Edit):
|
||||||
return super().keypress(size, 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):
|
class DOSScrollBar(urwid.WidgetWrap):
|
||||||
"""A DOS-style scrollbar with arrow buttons at top and bottom.
|
"""A DOS-style scrollbar with arrow buttons at top and bottom.
|
||||||
|
|
||||||
|
|
@ -1506,7 +1586,54 @@ class SmartToolsUI:
|
||||||
def on_cancel(_):
|
def on_cancel(_):
|
||||||
self.close_overlay()
|
self.close_overlay()
|
||||||
|
|
||||||
|
def on_external_edit(_):
|
||||||
|
"""Open prompt in external editor ($EDITOR)."""
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
current_prompt = prompt_edit.edit_text
|
||||||
|
|
||||||
|
# Stop the urwid loop temporarily
|
||||||
|
if self.loop:
|
||||||
|
self.loop.stop()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Create temp file with current prompt
|
||||||
|
with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False) as f:
|
||||||
|
f.write(current_prompt)
|
||||||
|
temp_path = f.name
|
||||||
|
|
||||||
|
# Get editor from environment
|
||||||
|
editor = os.environ.get('EDITOR', os.environ.get('VISUAL', 'nano'))
|
||||||
|
|
||||||
|
# Run editor
|
||||||
|
subprocess.run([editor, temp_path], check=True)
|
||||||
|
|
||||||
|
# Read back the edited prompt
|
||||||
|
with open(temp_path, 'r') as f:
|
||||||
|
new_prompt = f.read()
|
||||||
|
|
||||||
|
# Update the prompt editor
|
||||||
|
prompt_edit.set_edit_text(new_prompt)
|
||||||
|
status_text.set_text(('success', f"Prompt updated from {editor}"))
|
||||||
|
|
||||||
|
# Clean up temp file
|
||||||
|
os.unlink(temp_path)
|
||||||
|
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
status_text.set_text(('error', "Editor exited with error"))
|
||||||
|
except FileNotFoundError:
|
||||||
|
status_text.set_text(('error', f"Editor '{editor}' not found"))
|
||||||
|
except Exception as e:
|
||||||
|
status_text.set_text(('error', f"Edit error: {e}"))
|
||||||
|
finally:
|
||||||
|
# Restart the urwid loop
|
||||||
|
if self.loop:
|
||||||
|
self.loop.start()
|
||||||
|
|
||||||
load_btn = Button3DCompact("Load", on_load)
|
load_btn = Button3DCompact("Load", on_load)
|
||||||
|
edit_btn = Button3DCompact("$EDITOR", on_external_edit)
|
||||||
|
|
||||||
# Prompt editor in a box - use ListBox for proper focus handling and scrolling
|
# Prompt editor in a box - use ListBox for proper focus handling and scrolling
|
||||||
# Wrap in DOSScrollBar for DOS-style scrollbar with arrow buttons
|
# Wrap in DOSScrollBar for DOS-style scrollbar with arrow buttons
|
||||||
|
|
@ -1533,6 +1660,8 @@ class SmartToolsUI:
|
||||||
('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),
|
||||||
|
('pack', urwid.Text(" ")),
|
||||||
|
('pack', edit_btn),
|
||||||
])),
|
])),
|
||||||
('pack', status_text),
|
('pack', status_text),
|
||||||
('weight', 1, prompt_box),
|
('weight', 1, prompt_box),
|
||||||
|
|
@ -1542,7 +1671,7 @@ class SmartToolsUI:
|
||||||
|
|
||||||
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=75, 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, file support, and AI auto-adjust."""
|
"""Add/edit code step with multiline editor, file support, and AI auto-adjust."""
|
||||||
|
|
@ -1554,9 +1683,9 @@ class SmartToolsUI:
|
||||||
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 - use TabPassEdit so Tab passes through for navigation
|
# Multiline code editor with undo/redo (Alt+U / Alt+R)
|
||||||
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 = TabPassEdit(
|
code_edit = UndoableEdit(
|
||||||
edit_text=default_code,
|
edit_text=default_code,
|
||||||
multiline=True
|
multiline=True
|
||||||
)
|
)
|
||||||
|
|
@ -1788,7 +1917,54 @@ Return ONLY the Python code, no explanations or markdown fencing."""
|
||||||
def on_cancel(_):
|
def on_cancel(_):
|
||||||
self.close_overlay()
|
self.close_overlay()
|
||||||
|
|
||||||
|
def on_external_edit(_):
|
||||||
|
"""Open code in external editor ($EDITOR)."""
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
current_code = code_edit.edit_text
|
||||||
|
|
||||||
|
# Stop the urwid loop temporarily
|
||||||
|
if self.loop:
|
||||||
|
self.loop.stop()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Create temp file with current code
|
||||||
|
with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
|
||||||
|
f.write(current_code)
|
||||||
|
temp_path = f.name
|
||||||
|
|
||||||
|
# Get editor from environment
|
||||||
|
editor = os.environ.get('EDITOR', os.environ.get('VISUAL', 'nano'))
|
||||||
|
|
||||||
|
# Run editor
|
||||||
|
subprocess.run([editor, temp_path], check=True)
|
||||||
|
|
||||||
|
# Read back the edited code
|
||||||
|
with open(temp_path, 'r') as f:
|
||||||
|
new_code = f.read()
|
||||||
|
|
||||||
|
# Update the code editor
|
||||||
|
code_edit.set_edit_text(new_code)
|
||||||
|
status_text.set_text(('success', f"Code updated from {editor}"))
|
||||||
|
|
||||||
|
# Clean up temp file
|
||||||
|
os.unlink(temp_path)
|
||||||
|
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
status_text.set_text(('error', "Editor exited with error"))
|
||||||
|
except FileNotFoundError:
|
||||||
|
status_text.set_text(('error', f"Editor '{editor}' not found"))
|
||||||
|
except Exception as e:
|
||||||
|
status_text.set_text(('error', f"Edit error: {e}"))
|
||||||
|
finally:
|
||||||
|
# Restart the urwid loop
|
||||||
|
if self.loop:
|
||||||
|
self.loop.start()
|
||||||
|
|
||||||
load_btn = Button3DCompact("Load", on_load)
|
load_btn = Button3DCompact("Load", on_load)
|
||||||
|
edit_btn = Button3DCompact("$EDITOR", on_external_edit)
|
||||||
|
|
||||||
# Code editor in a box - use ListBox for proper focus handling and scrolling
|
# Code editor in a box - use ListBox for proper focus handling and scrolling
|
||||||
# Wrap in DOSScrollBar for DOS-style scrollbar with arrow buttons
|
# Wrap in DOSScrollBar for DOS-style scrollbar with arrow buttons
|
||||||
|
|
@ -1814,6 +1990,8 @@ Return ONLY the Python code, no explanations or markdown fencing."""
|
||||||
('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),
|
||||||
|
('pack', urwid.Text(" ")),
|
||||||
|
('pack', edit_btn),
|
||||||
])),
|
])),
|
||||||
('pack', status_text),
|
('pack', status_text),
|
||||||
('weight', 1, main_columns),
|
('weight', 1, main_columns),
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue