espilon-source/tools/c2/tui/widgets/command_input.py

216 lines
7.0 KiB
Python

"""
Command input widget with history and zsh-style tab completion.
"""
from textual.widgets import Input
from textual.message import Message
from typing import Callable, Optional
class CommandInput(Input):
"""Command input with history and zsh-style tab completion."""
DEFAULT_CSS = """
CommandInput {
dock: bottom;
height: 1;
border: none;
background: $surface;
padding: 0 1;
}
CommandInput:focus {
border: none;
}
"""
class CommandSubmitted(Message):
"""Posted when a command is submitted."""
def __init__(self, command: str):
self.command = command
super().__init__()
class CompletionsAvailable(Message):
"""Posted when multiple completions are available."""
def __init__(self, completions: list[str], word: str):
self.completions = completions
self.word = word
super().__init__()
def __init__(self, **kwargs):
super().__init__(
placeholder="c2:> Type command here...",
**kwargs
)
self._history: list[str] = []
self._history_index: int = -1
self._current_input: str = ""
self._completer: Optional[Callable[[str, int], Optional[str]]] = None
self._last_completion_text: str = ""
self._last_completions: list[str] = []
self._completion_cycle_index: int = 0
def set_completer(self, completer: Callable[[str, int], Optional[str]]):
"""Set the tab completion function (same signature as readline completer)."""
self._completer = completer
def on_key(self, event) -> None:
"""Handle special keys for history and completion."""
if event.key == "up":
event.prevent_default()
self._navigate_history(-1)
elif event.key == "down":
event.prevent_default()
self._navigate_history(1)
elif event.key == "tab":
event.prevent_default()
self._handle_tab_completion()
def _get_all_completions(self, word: str) -> list[str]:
"""Get all possible completions for a word."""
if not self._completer:
return []
completions = []
state = 0
while True:
completion = self._completer(word, state)
if completion is None:
break
completions.append(completion)
state += 1
return completions
def _find_common_prefix(self, strings: list[str]) -> str:
"""Find the longest common prefix among strings."""
if not strings:
return ""
if len(strings) == 1:
return strings[0]
prefix = strings[0]
for s in strings[1:]:
while not s.startswith(prefix):
prefix = prefix[:-1]
if not prefix:
return ""
return prefix
def _handle_tab_completion(self):
"""Handle zsh-style tab completion."""
if not self._completer:
return
current_text = self.value
cursor_pos = self.cursor_position
# Get the word being completed
text_before_cursor = current_text[:cursor_pos]
parts = text_before_cursor.split()
if not parts:
word_to_complete = ""
elif text_before_cursor.endswith(" "):
word_to_complete = ""
else:
word_to_complete = parts[-1]
# Check if context changed (new completion session)
context_changed = text_before_cursor != self._last_completion_text
if context_changed:
# New completion session - get all completions
self._last_completions = self._get_all_completions(word_to_complete)
self._completion_cycle_index = 0
self._last_completion_text = text_before_cursor
if not self._last_completions:
# No completions
return
if len(self._last_completions) == 1:
# Single match - complete directly
self._apply_completion(self._last_completions[0], word_to_complete, cursor_pos)
self._last_completions = []
return
# Multiple matches - complete to common prefix and show options
common_prefix = self._find_common_prefix(self._last_completions)
if common_prefix and len(common_prefix) > len(word_to_complete):
# Complete to common prefix
self._apply_completion(common_prefix, word_to_complete, cursor_pos)
# Show all completions
self.post_message(self.CompletionsAvailable(
self._last_completions.copy(),
word_to_complete
))
else:
# Same context - cycle through completions
if not self._last_completions:
return
# Get next completion in cycle
completion = self._last_completions[self._completion_cycle_index]
self._apply_completion(completion, word_to_complete, cursor_pos)
# Advance cycle
self._completion_cycle_index = (self._completion_cycle_index + 1) % len(self._last_completions)
def _apply_completion(self, completion: str, word_to_complete: str, cursor_pos: int):
"""Apply a completion to the input."""
current_text = self.value
text_before_cursor = current_text[:cursor_pos]
if word_to_complete:
prefix = text_before_cursor[:-len(word_to_complete)]
else:
prefix = text_before_cursor
new_text = prefix + completion + current_text[cursor_pos:]
new_cursor = len(prefix) + len(completion)
self.value = new_text
self.cursor_position = new_cursor
self._last_completion_text = new_text[:new_cursor]
def _navigate_history(self, direction: int):
"""Navigate through command history."""
if not self._history:
return
if self._history_index == -1:
self._current_input = self.value
new_index = self._history_index + direction
if new_index < -1:
new_index = -1
elif new_index >= len(self._history):
new_index = len(self._history) - 1
self._history_index = new_index
if self._history_index == -1:
self.value = self._current_input
else:
self.value = self._history[-(self._history_index + 1)]
self.cursor_position = len(self.value)
def action_submit(self) -> None:
"""Submit the current command."""
command = self.value.strip()
if command:
self._history.append(command)
if len(self._history) > 100:
self._history.pop(0)
self.post_message(self.CommandSubmitted(command))
self.value = ""
self._history_index = -1
self._current_input = ""
self._last_completions = []
self._completion_cycle_index = 0
self._last_completion_text = ""