input box fixes

This commit is contained in:
Dhanji Prasanna
2025-10-06 14:48:27 +11:00
parent c9487db5e7
commit 5a83e1b7e0
2 changed files with 221 additions and 21 deletions

View File

@@ -211,7 +211,6 @@ async fn run_interactive_retro(config: Config, show_prompt: bool, show_code: boo
// Track multiline input
let mut multiline_buffer = String::new();
let mut in_multiline = false;
let mut input_buffer = String::new();
// Main event loop
loop {
@@ -223,9 +222,6 @@ async fn run_interactive_retro(config: Config, show_prompt: bool, show_code: boo
context.percentage_used(),
);
// Update the displayed input buffer
tui.update_input(&input_buffer);
// Poll for keyboard events
if event::poll(Duration::from_millis(50))? {
if let Event::Key(key) = event::read()? {
@@ -238,8 +234,48 @@ async fn run_interactive_retro(config: Config, show_prompt: bool, show_code: boo
tui.exit();
break;
}
// Emacs/bash-like shortcuts
KeyCode::Char('a') if key.modifiers.contains(KeyModifiers::CONTROL) => {
tui.cursor_home();
}
KeyCode::Char('e') if key.modifiers.contains(KeyModifiers::CONTROL) => {
tui.cursor_end();
}
KeyCode::Char('w') if key.modifiers.contains(KeyModifiers::CONTROL) => {
tui.delete_word();
}
KeyCode::Char('k') if key.modifiers.contains(KeyModifiers::CONTROL) => {
tui.delete_to_end();
}
KeyCode::Char('u') if key.modifiers.contains(KeyModifiers::CONTROL) => {
// Delete from beginning to cursor (similar to Ctrl-K but opposite direction)
let (input_buffer, cursor_pos) = tui.get_input_state();
if cursor_pos > 0 {
let after = input_buffer.chars().skip(cursor_pos).collect::<String>();
tui.update_input(&after);
tui.cursor_home();
}
}
KeyCode::Left => {
tui.cursor_left();
}
KeyCode::Right => {
tui.cursor_right();
}
KeyCode::Home => {
tui.cursor_home();
}
KeyCode::End => {
tui.cursor_end();
}
KeyCode::Delete => {
tui.delete_char();
}
KeyCode::Enter => {
let (input_buffer, _) = tui.get_input_state();
if !input_buffer.is_empty() {
// Clear the input for next command
tui.update_input("");
let trimmed = input_buffer.trim_end();
// Check if line ends with backslash for continuation
@@ -249,7 +285,6 @@ async fn run_interactive_retro(config: Config, show_prompt: bool, show_code: boo
multiline_buffer.push_str(without_backslash);
multiline_buffer.push('\n');
in_multiline = true;
input_buffer.clear();
tui.status("MULTILINE INPUT");
continue;
}
@@ -266,8 +301,6 @@ async fn run_interactive_retro(config: Config, show_prompt: bool, show_code: boo
input_buffer.clone()
};
input_buffer.clear();
let input = final_input.trim().to_string();
if input.is_empty() {
continue;
@@ -305,10 +338,10 @@ async fn run_interactive_retro(config: Config, show_prompt: bool, show_code: boo
}
}
KeyCode::Char(c) => {
input_buffer.push(c);
tui.insert_char(c);
}
KeyCode::Backspace => {
input_buffer.pop();
tui.backspace();
}
KeyCode::Up => {
tui.scroll_up();
@@ -322,11 +355,11 @@ async fn run_interactive_retro(config: Config, show_prompt: bool, show_code: boo
KeyCode::PageDown => {
tui.scroll_page_down();
}
KeyCode::Home => {
tui.scroll_home();
KeyCode::Home if key.modifiers.contains(KeyModifiers::CONTROL) => {
tui.scroll_home(); // Ctrl+Home for scrolling to top
}
KeyCode::End => {
tui.scroll_end();
KeyCode::End if key.modifiers.contains(KeyModifiers::CONTROL) => {
tui.scroll_end(); // Ctrl+End for scrolling to bottom
}
_ => {}
}

View File

@@ -65,6 +65,8 @@ pub enum TuiMessage {
struct TerminalState {
/// Current input buffer
input_buffer: String,
/// Cursor position in input buffer (for editing)
cursor_position: usize,
/// Output history
output_history: Vec<String>,
/// Scroll position in output
@@ -115,6 +117,7 @@ impl TerminalState {
fn new() -> Self {
Self {
input_buffer: String::new(),
cursor_position: 0,
output_history: vec![
"WEYLAND-YUTANI SYSTEMS".to_string(),
"MU/TH/UR 6000 - INTERFACE 2.4.1".to_string(),
@@ -380,6 +383,12 @@ impl RetroTui {
// Set animation target based on processing state
state.activity_animation_target = if state.is_processing { 1.0 } else { 0.0 };
// Clear input buffer when entering PROCESSING mode
if !was_processing && state.is_processing {
state.input_buffer.clear();
state.cursor_position = 0;
}
// Remove cursor when exiting PROCESSING mode
if was_processing && !state.is_processing {
if let Some(last) = state.output_history.last_mut() {
@@ -543,7 +552,7 @@ impl RetroTui {
}
// Draw header/input area
Self::draw_input_area(f, chunks[0], &state.input_buffer, state.cursor_blink);
Self::draw_input_area(f, chunks[0], &state.input_buffer, state.cursor_position, state.cursor_blink, state.is_processing);
// Draw main output area
Self::draw_output_area(f, chunks[1], &state.output_history, state.scroll_offset);
@@ -575,15 +584,63 @@ impl RetroTui {
}
/// Draw the input area with prompt
fn draw_input_area(f: &mut Frame, area: Rect, input_buffer: &str, cursor_blink: bool) {
// Show the actual input buffer content with prompt
let input_text = if cursor_blink {
format!("g3> {}", input_buffer)
} else {
format!("g3> {} ", input_buffer)
};
fn draw_input_area(f: &mut Frame, area: Rect, input_buffer: &str, cursor_position: usize, cursor_blink: bool, is_processing: bool) {
let prompt = "g3> ";
let prompt_len = prompt.len();
let input = Paragraph::new(input_text)
// Calculate available width for text (accounting for borders and prompt)
let available_width = area.width.saturating_sub(2).saturating_sub(prompt_len as u16) as usize;
// Don't show cursor if processing
let show_cursor = !is_processing && cursor_blink;
// Build the display text with cursor at the right position
let mut display_text = String::new();
display_text.push_str(prompt);
if input_buffer.is_empty() {
// Empty buffer - just show cursor if applicable
if show_cursor {
display_text.push('█');
}
} else {
// Calculate which part of the buffer to show (handle wrapping)
let total_cursor_pos = cursor_position;
// Determine the window into the buffer we should show
let window_start = if total_cursor_pos > available_width - 1 {
// Cursor is beyond the visible area, scroll the view
total_cursor_pos - (available_width - 1)
} else {
0
};
// Get the visible portion of the buffer
let visible_buffer: String = input_buffer
.chars()
.skip(window_start)
.take(available_width)
.collect();
// Insert cursor at the appropriate position in the visible text
let visible_cursor_pos = cursor_position.saturating_sub(window_start);
for (i, ch) in visible_buffer.chars().enumerate() {
if i == visible_cursor_pos && show_cursor {
display_text.push('█');
// Don't add the character under the cursor if we're showing the block cursor
} else {
display_text.push(ch);
}
}
// If cursor is at the end and we're showing it
if visible_cursor_pos == visible_buffer.len() && show_cursor {
display_text.push('█');
}
}
let input = Paragraph::new(display_text)
.style(Style::default().fg(TERMINAL_GREEN))
.block(
Block::default()
@@ -1092,6 +1149,116 @@ impl RetroTui {
pub fn update_input(&self, input: &str) {
if let Ok(mut state) = self.state.lock() {
state.input_buffer = input.to_string();
// Keep cursor at end when updating the whole buffer
state.cursor_position = input.len();
}
}
/// Move cursor left
pub fn cursor_left(&self) {
if let Ok(mut state) = self.state.lock() {
if state.cursor_position > 0 {
state.cursor_position -= 1;
}
}
}
/// Move cursor right
pub fn cursor_right(&self) {
if let Ok(mut state) = self.state.lock() {
if state.cursor_position < state.input_buffer.len() {
state.cursor_position += 1;
}
}
}
/// Move cursor to beginning of line (Ctrl-A)
pub fn cursor_home(&self) {
if let Ok(mut state) = self.state.lock() {
state.cursor_position = 0;
}
}
/// Move cursor to end of line (Ctrl-E)
pub fn cursor_end(&self) {
if let Ok(mut state) = self.state.lock() {
state.cursor_position = state.input_buffer.len();
}
}
/// Delete word before cursor (Ctrl-W)
pub fn delete_word(&self) {
if let Ok(mut state) = self.state.lock() {
if state.cursor_position > 0 {
// Find the start of the word to delete
let mut word_start = state.cursor_position;
let chars: Vec<char> = state.input_buffer.chars().collect();
// Skip trailing spaces
while word_start > 0 && chars[word_start - 1].is_whitespace() {
word_start -= 1;
}
// Find word boundary
while word_start > 0 && !chars[word_start - 1].is_whitespace() {
word_start -= 1;
}
// Remove the word
let before = state.input_buffer.chars().take(word_start).collect::<String>();
let after = state.input_buffer.chars().skip(state.cursor_position).collect::<String>();
state.input_buffer = format!("{}{}", before, after);
state.cursor_position = word_start;
}
}
}
/// Delete from cursor to end of line (Ctrl-K)
pub fn delete_to_end(&self) {
if let Ok(mut state) = self.state.lock() {
state.input_buffer = state.input_buffer.chars().take(state.cursor_position).collect();
}
}
/// Get current input buffer and cursor position
pub fn get_input_state(&self) -> (String, usize) {
if let Ok(state) = self.state.lock() {
(state.input_buffer.clone(), state.cursor_position)
} else {
(String::new(), 0)
}
}
/// Insert character at cursor position
pub fn insert_char(&self, ch: char) {
if let Ok(mut state) = self.state.lock() {
let before = state.input_buffer.chars().take(state.cursor_position).collect::<String>();
let after = state.input_buffer.chars().skip(state.cursor_position).collect::<String>();
state.input_buffer = format!("{}{}{}", before, ch, after);
state.cursor_position += 1;
}
}
/// Delete character at cursor position (Delete key)
pub fn delete_char(&self) {
if let Ok(mut state) = self.state.lock() {
if state.cursor_position < state.input_buffer.len() {
let before = state.input_buffer.chars().take(state.cursor_position).collect::<String>();
let after = state.input_buffer.chars().skip(state.cursor_position + 1).collect::<String>();
state.input_buffer = format!("{}{}", before, after);
}
}
}
/// Delete character before cursor (Backspace)
pub fn backspace(&self) {
if let Ok(mut state) = self.state.lock() {
if state.cursor_position > 0 {
let before = state.input_buffer.chars().take(state.cursor_position - 1).collect::<String>();
let after = state.input_buffer.chars().skip(state.cursor_position).collect::<String>();
state.input_buffer = format!("{}{}", before, after);
state.cursor_position -= 1;
}
}
}