use crate::Theme;
use ratatui::prelude::*;
use ratatui_textarea::{Input as TextAreaInput, Key, TextArea};
#[derive(Debug)]
pub struct InputState {
textarea: TextArea<'static>,
}
impl Default for InputState {
fn default() -> Self {
let mut textarea = TextArea::default();
textarea.remove_line_number();
textarea.set_cursor_line_style(Style::default());
Self { textarea }
}
}
impl InputState {
pub fn new() -> Self {
Self::default()
}
pub fn text(&self) -> String {
self.textarea.lines().join("\n")
}
pub fn set_text(&mut self, text: String) {
self.textarea.clear();
if !text.is_empty() {
self.textarea.insert_str(&text);
}
}
pub fn lines(&self) -> Vec<String> {
self.textarea.lines().to_vec()
}
pub fn clear(&mut self) {
self.textarea.clear();
}
pub fn set_placeholder(&mut self, placeholder: Option<String>) {
if let Some(p) = placeholder {
self.textarea.set_placeholder_text(&p);
} else {
self.textarea.set_placeholder_text("");
}
}
pub fn insert_char(&mut self, c: char) {
self.handle_char(c);
}
pub fn insert_str(&mut self, s: &str) {
self.textarea.insert_str(s);
}
pub fn backspace(&mut self) {
self.textarea.input(TextAreaInput {
key: Key::Backspace,
..Default::default()
});
}
pub fn delete(&mut self) {
self.textarea.input(TextAreaInput {
key: Key::Delete,
..Default::default()
});
}
pub fn move_left(&mut self) {
self.textarea
.move_cursor(ratatui_textarea::CursorMove::Back);
}
pub fn move_right(&mut self) {
self.textarea
.move_cursor(ratatui_textarea::CursorMove::Forward);
}
pub fn move_home(&mut self) {
self.textarea
.move_cursor(ratatui_textarea::CursorMove::Head);
}
pub fn move_end(&mut self) {
self.textarea.move_cursor(ratatui_textarea::CursorMove::End);
}
pub fn move_word_left(&mut self) {
self.textarea
.move_cursor(ratatui_textarea::CursorMove::WordBack);
}
pub fn move_word_right(&mut self) {
self.textarea
.move_cursor(ratatui_textarea::CursorMove::WordForward);
}
pub fn handle_key(&mut self, key: Key) -> bool {
match key {
Key::Enter => true, Key::Tab => true, _ => {
self.textarea.input(TextAreaInput {
key,
..Default::default()
});
false
}
}
}
pub fn handle_char(&mut self, c: char) {
self.textarea.input(TextAreaInput {
key: Key::Char(c),
ctrl: false,
alt: false,
shift: false,
});
}
pub fn handle_input(&mut self, input: TextAreaInput) -> bool {
if input.key == Key::Enter && !input.shift {
true } else {
self.textarea.input(input);
false
}
}
pub fn textarea_mut(&mut self) -> &mut TextArea<'static> {
&mut self.textarea
}
pub fn undo(&mut self) {
self.textarea.undo();
}
pub fn redo(&mut self) {
self.textarea.redo();
}
}
pub struct Input<'a> {
theme: &'a Theme,
placeholder: Option<&'a str>,
}
impl<'a> Input<'a> {
pub fn new(theme: &'a Theme) -> Self {
Self {
theme,
placeholder: None,
}
}
pub fn with_placeholder(mut self, placeholder: &'a str) -> Self {
self.placeholder = Some(placeholder);
self
}
}
impl ratatui::widgets::StatefulWidget for Input<'_> {
type State = InputState;
fn render(self, area: Rect, buf: &mut ratatui::buffer::Buffer, state: &mut Self::State) {
if area.height < 1 || area.width < 4 {
return;
}
let y = area.y;
let textarea = state.textarea_mut();
textarea.set_style(Style::default().fg(self.theme.colors.foreground.to_ratatui()));
textarea.set_cursor_style(
Style::default()
.fg(self.theme.colors.cursor_fg.to_ratatui())
.bg(self.theme.colors.cursor_bg.to_ratatui()),
);
textarea.set_cursor_line_style(Style::default());
textarea.remove_line_number();
textarea.set_placeholder_style(Style::default().fg(self.theme.colors.muted.to_ratatui()));
let content_area = Rect {
x: area.x + 1,
y,
width: area.width.saturating_sub(2), height: area.height,
};
let textarea_clone = textarea.clone();
textarea_clone.render(content_area, buf);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn input_state_empty() {
let state = InputState::default();
assert!(state.text().is_empty());
}
#[test]
fn input_state_insert() {
let mut state = InputState::default();
state.handle_char('a');
assert_eq!(state.text(), "a");
state.handle_char('b');
assert_eq!(state.text(), "ab");
state.handle_char('\u{d55c}'); assert_eq!(state.text(), "ab한");
}
#[test]
fn input_state_insert_str() {
let mut state = InputState::default();
state.insert_str("안녕하세요");
assert_eq!(state.text(), "안녕하세요");
}
#[test]
fn input_state_multiline() {
let mut state = InputState::default();
state.handle_char('a');
state.handle_input(TextAreaInput {
key: Key::Enter,
shift: true, ..Default::default()
});
state.handle_char('b');
assert_eq!(state.text(), "a\nb");
}
#[test]
fn input_state_clear() {
let mut state = InputState::default();
state.insert_str("hello");
state.clear();
assert!(state.text().is_empty());
}
#[test]
fn input_state_undo_redo() {
let mut state = InputState::default();
state.insert_str("hello");
assert_eq!(state.text(), "hello");
state.undo();
assert_eq!(state.text(), "");
state.redo();
assert_eq!(state.text(), "hello");
}
}