diff --git a/src/smarttools/ui_urwid.py b/src/smarttools/ui_urwid.py index 18f8aac..7c36783 100644 --- a/src/smarttools/ui_urwid.py +++ b/src/smarttools/ui_urwid.py @@ -345,11 +345,9 @@ class UndoableEdit(urwid.Edit): """A multiline Edit with undo/redo support. Features: - - Undo with Ctrl+U (up to 50 states) - - Redo with Ctrl+R + - Undo with Alt+U (up to 50 states) + - Redo with Alt+R - Tab passes through for focus cycling - - Note: Ctrl+Z cannot be used as it's the Unix suspend signal (SIGTSTP). """ MAX_UNDO = 50 # Maximum undo history size @@ -358,40 +356,40 @@ class UndoableEdit(urwid.Edit): super().__init__(*args, **kwargs) self._undo_stack = [] # List of (text, cursor_pos) tuples self._redo_stack = [] - self._last_saved_text = self.edit_text # Track for change detection def keypress(self, size, key): if key in ('tab', 'shift tab'): return key - # Save state before any editing operation (for undo) - current_text = self.edit_text - if current_text != self._last_saved_text: - self._save_undo_state(self._last_saved_text, self.edit_pos) - self._last_saved_text = current_text - - # Handle undo (Ctrl+U) - can't use Ctrl+Z as it's Unix suspend signal - if key == 'ctrl u': + # Handle undo (Alt+U or meta u) + if key in ('meta u', 'alt u'): self._undo() return None - # Handle redo (Ctrl+R) - if key == 'ctrl r': + # 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) - # After edit, check if text changed and save for potential undo - new_text = self.edit_text - if new_text != self._last_saved_text: + # 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 - self._last_saved_text = new_text return result def _save_undo_state(self, text, pos): - """Save current state to undo stack.""" + """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)) @@ -408,7 +406,6 @@ class UndoableEdit(urwid.Edit): text, pos = self._undo_stack.pop() self.set_edit_text(text) self.set_edit_pos(min(pos, len(text))) - self._last_saved_text = text def _redo(self): """Restore state from redo stack.""" @@ -422,7 +419,6 @@ class UndoableEdit(urwid.Edit): text, pos = self._redo_stack.pop() self.set_edit_text(text) self.set_edit_pos(min(pos, len(text))) - self._last_saved_text = text class DOSScrollBar(urwid.WidgetWrap): @@ -1687,7 +1683,7 @@ class SmartToolsUI: 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) - # Multiline code editor with undo/redo (Ctrl+U / Ctrl+R) + # Multiline code editor with undo/redo (Alt+U / Alt+R) default_code = existing.code if existing else f"{default_output_var} = input.upper()" code_edit = UndoableEdit( edit_text=default_code,