Files
llmx/codex-rs/tui/src/bottom_pane/chat_composer.rs

118 lines
3.4 KiB
Rust
Raw Normal View History

use crossterm::event::KeyEvent;
use ratatui::buffer::Buffer;
use ratatui::layout::Alignment;
use ratatui::layout::Rect;
use ratatui::style::Style;
use ratatui::style::Stylize;
use ratatui::text::Line;
use ratatui::widgets::BorderType;
use ratatui::widgets::Borders;
use ratatui::widgets::Widget;
use ratatui::widgets::WidgetRef;
use tui_textarea::Input;
use tui_textarea::Key;
use tui_textarea::TextArea;
/// Minimum number of visible text rows inside the textarea.
const MIN_TEXTAREA_ROWS: usize = 1;
/// Rows consumed by the border.
const BORDER_LINES: u16 = 2;
/// Result returned when the user interacts with the text area.
pub enum InputResult {
Submitted(String),
None,
}
pub(crate) struct ChatComposer<'a> {
textarea: TextArea<'a>,
}
impl ChatComposer<'_> {
pub fn new(has_input_focus: bool) -> Self {
let mut textarea = TextArea::default();
textarea.set_placeholder_text("send a message");
textarea.set_cursor_line_style(ratatui::style::Style::default());
let mut this = Self { textarea };
this.update_border(has_input_focus);
this
}
pub fn set_input_focus(&mut self, has_focus: bool) {
self.update_border(has_focus);
}
/// Handle key event when no overlay is present.
pub fn handle_key_event(&mut self, key_event: KeyEvent) -> (InputResult, bool) {
match key_event.into() {
Input {
key: Key::Enter,
shift: false,
alt: false,
ctrl: false,
} => {
let text = self.textarea.lines().join("\n");
self.textarea.select_all();
self.textarea.cut();
(InputResult::Submitted(text), true)
}
Input {
key: Key::Enter, ..
}
| Input {
key: Key::Char('j'),
ctrl: true,
alt: false,
shift: false,
} => {
self.textarea.insert_newline();
(InputResult::None, true)
}
input => {
self.textarea.input(input);
(InputResult::None, true)
}
}
}
pub fn calculate_required_height(&self, _area: &Rect) -> u16 {
let rows = self.textarea.lines().len().max(MIN_TEXTAREA_ROWS);
rows as u16 + BORDER_LINES
}
fn update_border(&mut self, has_focus: bool) {
struct BlockState {
right_title: Line<'static>,
border_style: Style,
}
let bs = if has_focus {
BlockState {
right_title: Line::from("Enter to send | Ctrl+D to quit | Ctrl+J for newline")
.alignment(Alignment::Right),
border_style: Style::default(),
}
} else {
BlockState {
right_title: Line::from(""),
border_style: Style::default().dim(),
}
};
self.textarea.set_block(
ratatui::widgets::Block::default()
.title_bottom(bs.right_title)
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.border_style(bs.border_style),
);
}
}
impl WidgetRef for &ChatComposer<'_> {
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
self.textarea.render(area, buf);
}
}