118 lines
3.4 KiB
Rust
118 lines
3.4 KiB
Rust
|
|
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);
|
||
|
|
}
|
||
|
|
}
|