@@ -23,6 +23,7 @@ use ratatui::{
23 widgets::{Block, Borders, List, ListItem, ListState, Paragraph, Wrap},
23 widgets::{Block, Borders, List, ListItem, ListState, Paragraph, Wrap},
25use tui_textarea::{Input, Key, TextArea};
25use tui_textarea::{Input, Key, TextArea};
26+use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
27const ASCII_BANNER: [&str; 7] = [
28const ASCII_BANNER: [&str; 7] = [
28 "░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░▒▓███████▓▒░░▒▓████████▓▒░",
29 "░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░▒▓███████▓▒░░▒▓████████▓▒░",
@@ -57,13 +58,12 @@ pub struct AppState {
57 tick: u64,
58 tick: u64,
58 spinner: usize,
59 spinner: usize,
59 terminal_scroll: usize,
60 terminal_scroll: usize,
61+ input_view_width: u16,
62impl AppState {
64impl AppState {
63 pub fn new(cwd: PathBuf, vm_info: VmInfo) -> Self {
65 pub fn new(cwd: PathBuf, vm_info: VmInfo) -> Self {
64− let mut input = TextArea::default();
66+ let input = Self::default_input();
65− input.set_cursor_style(Style::default().fg(Color::Yellow));
66− input.set_block(Block::default().borders(Borders::ALL).title("Input"));
@@ -76,21 +76,92 @@ impl AppState {
77 spinner: 0,
77 spinner: 0,
78 terminal_scroll: 0,
78 terminal_scroll: 0,
82− pub fn input_line_count(&self) -> u16 {
83+ fn default_input() -> TextArea<'static> {
83− self.input.lines().len().max(1) as u16
84+ let mut input = TextArea::default();
85+ input.set_cursor_style(Style::default().fg(Color::Yellow));
86+ input.set_block(Block::default().borders(Borders::ALL).title("Input"));
90+ fn reset_input(&mut self) {
91+ self.input = Self::default_input();
86− pub fn input_height(&self) -> u16 {
94+ fn take_input_text(&mut self) -> String {
87− let mut height = self.input_line_count();
95+ let text = self.input.lines().join("");
100+ pub fn input_height_for_width(&self, available_width: u16) -> u16 {
101+ let inner_width = self.input_inner_width(available_width).max(1) as usize;
102+ let mut visual_lines = 0usize;
103+ for line in self.input.lines() {
104+ let line_width = UnicodeWidthStr::width(line.as_str());
105+ let wrapped = if line_width == 0 {
108+ (line_width + inner_width - 1) / inner_width
110+ visual_lines += wrapped;
113+ let mut height = (visual_lines.max(1) as u16).max(1);
88 if self.input.block().is_some() {
114 if self.input.block().is_some() {
89 height = height.saturating_add(2);
115 height = height.saturating_add(2);
91 height.max(1)
117 height.max(1)
120+ fn input_inner_width(&self, available_width: u16) -> u16 {
121+ if self.input.block().is_some() {
122+ available_width.saturating_sub(2)
128+ fn insert_char_with_wrap(&mut self, c: char) {
129+ let char_width = UnicodeWidthChar::width(c).unwrap_or(0);
130+ let width = self.input_view_width.max(1) as usize;
131+ let mut should_wrap = false;
132+ if char_width > 0 && width > 0 {
133+ let (row, col) = self.input.cursor();
134+ if let Some(line) = self.input.lines().get(row) {
135+ if col == line.chars().count() {
136+ let line_width = UnicodeWidthStr::width(line.as_str());
137+ should_wrap = line_width + char_width > width;
144+ self.input.insert_char(c);
145+ self.input.insert_newline();
147+ self.input.insert_newline();
148+ self.input.insert_char(c);
151+ self.input.insert_char(c);
155+ fn insert_str_with_wrap(&mut self, text: &str) {
156+ for c in text.chars() {
158+ '\n' => self.insert_char_with_wrap(' '),
160+ _ => self.insert_char_with_wrap(c),
94 pub fn push_history(&mut self, line: impl Into<String>) {
165 pub fn push_history(&mut self, line: impl Into<String>) {
95 self.history.push(line.into());
166 self.history.push(line.into());
96 if self.history.len() > 2000 {
167 if self.history.len() > 2000 {
@@ -281,7 +352,7 @@ fn handle_event(event: CrosstermEvent, app: &mut AppState) {
281 CrosstermEvent::Mouse(event) => handle_mouse_event(event, app),
352 CrosstermEvent::Mouse(event) => handle_mouse_event(event, app),
282 CrosstermEvent::FocusGained | CrosstermEvent::FocusLost => {}
353 CrosstermEvent::FocusGained | CrosstermEvent::FocusLost => {}
283 CrosstermEvent::Paste(text) => {
354 CrosstermEvent::Paste(text) => {
284− app.input.insert_str(&text);
355+ app.insert_str_with_wrap(&text);
@@ -325,7 +396,21 @@ fn handle_key_event(key: KeyEvent, app: &mut AppState) {
325 KeyCode::Down if key.modifiers.contains(KeyModifiers::CONTROL) => {
396 KeyCode::Down if key.modifiers.contains(KeyModifiers::CONTROL) => {
326 app.terminal_scroll = app.terminal_scroll.saturating_sub(1);
397 app.terminal_scroll = app.terminal_scroll.saturating_sub(1);
400+ let message = app.take_input_text();
401+ if !message.trim().is_empty() {
402+ app.push_history(format!("> {}", message));
403+ app.terminal_scroll = 0;
328 KeyCode::Tab => app.toggle_completions(default_completions()),
406 KeyCode::Tab => app.toggle_completions(default_completions()),
410+ .contains(KeyModifiers::CONTROL | KeyModifiers::ALT) =>
412+ app.insert_char_with_wrap(c);
330 app.input.input(input_from_key_event(key));
415 app.input.input(input_from_key_event(key));
@@ -407,9 +492,10 @@ fn default_completions() -> Vec<String> {
408fn render(frame: &mut Frame<'_>, app: &mut AppState) {
493fn render(frame: &mut Frame<'_>, app: &mut AppState) {
409 let area = frame.area();
494 let area = frame.area();
495+ app.input_view_width = app.input_inner_width(area.width);
410 let layout = compute_layout(
496 let layout = compute_layout(
412− app.input_height(),
498+ app.input_height_for_width(area.width),
413 app.completions.items().len(),
499 app.completions.items().len(),
414 app.completions.is_active(),
500 app.completions.is_active(),
@@ -491,14 +577,14 @@ fn render_terminal(frame: &mut Frame<'_>, area: Rect, app: &AppState) {
491 let scroll_top = max_top.saturating_sub(terminal_scroll);
577 let scroll_top = max_top.saturating_sub(terminal_scroll);
492 let paragraph = Paragraph::new(Text::from_iter(lines))
578 let paragraph = Paragraph::new(Text::from_iter(lines))
493 .block(block)
579 .block(block)
494− .wrap(Wrap { trim: false })
580+ .wrap(Wrap { trim: true })
495 .scroll((scroll_top.min(u16::MAX as usize) as u16, 0));
581 .scroll((scroll_top.min(u16::MAX as usize) as u16, 0));
497 frame.render_widget(paragraph, area);
583 frame.render_widget(paragraph, area);
500fn render_input(frame: &mut Frame<'_>, area: Rect, app: &mut AppState) {
586fn render_input(frame: &mut Frame<'_>, area: Rect, app: &mut AppState) {
501− frame.render_widget(app.input.widget(), area);
587+ frame.render_widget(&app.input, area);
502 if area.height > 0 && area.width > 0 {
588 if area.height > 0 && area.width > 0 {
503 let cursor = app.input.cursor();
589 let cursor = app.input.cursor();
504 let inner = match app.input.block() {
590 let inner = match app.input.block() {