use std::time::Duration; use tuirealm::{ application::PollStrategy, command::{Cmd, CmdResult, Direction, Position}, event::{Event, Key, KeyEvent, KeyModifiers}, props::{Alignment, AttrValue, Attribute, BorderType, Borders, Color, Style, TextModifiers}, terminal::{CrosstermTerminalAdapter, TerminalBridge}, Application, Component, EventListenerCfg, MockComponent, NoUserEvent, State, Update, }; // tui use tuirealm::ratatui::layout::{Constraint, Direction as LayoutDirection, Layout}; // textarea #[cfg(feature = "clipboard")] use tui_realm_textarea::TEXTAREA_CMD_PASTE; use tui_realm_textarea::{ TextArea, TEXTAREA_CMD_MOVE_WORD_BACK, TEXTAREA_CMD_MOVE_WORD_FORWARD, TEXTAREA_CMD_NEWLINE, TEXTAREA_CMD_REDO, TEXTAREA_CMD_UNDO, }; // -- message #[derive(Debug, PartialEq)] pub enum Msg { AppClose, Submit(Vec), None, } // Let's define the component ids for our application #[derive(Debug, Eq, PartialEq, Clone, Hash)] pub enum Id { Input, } struct Model { app: Application, quit: bool, // Becomes true when the user presses redraw: bool, // Tells whether to refresh the UI; performance optimization terminal: TerminalBridge, } impl Model { fn new() -> Self { // Setup app let mut app: Application = Application::init( EventListenerCfg::default().crossterm_input_listener(Duration::from_millis(10), 10), ); assert!(app .mount(Id::Input, Box::new(Input::default()), vec![]) .is_ok()); assert!(app.active(&Id::Input).is_ok()); Model { app, quit: false, redraw: true, terminal: TerminalBridge::init_crossterm().expect("Could not initialize terminal"), } } fn view(&mut self) { let _ = self.terminal.raw_mut().draw(|f| { // Prepare chunks let chunks = Layout::default() .direction(LayoutDirection::Vertical) .constraints([Constraint::Length(6), Constraint::Min(0)].as_ref()) .split(f.area()); self.app.view(&Id::Input, f, chunks[0]); }); } } fn main() { // Make model let mut model: Model = Model::new(); let _ = model.terminal.enable_raw_mode(); let _ = model.terminal.enter_alternate_screen(); // let's loop until quit is true while !model.quit { // Tick if let Ok(messages) = model.app.tick(PollStrategy::Once) { for msg in messages.into_iter() { let mut msg = Some(msg); while msg.is_some() { msg = model.update(msg); } } } // Redraw if model.redraw { model.view(); model.redraw = false; } } // Terminate terminal let _ = model.terminal.leave_alternate_screen(); let _ = model.terminal.disable_raw_mode(); let _ = model.terminal.clear_screen(); // print content model .app .state(&Id::Input) .unwrap() .unwrap_vec() .into_iter() .for_each(|x| println!("{}", x.unwrap_string())); } // -- update impl Update for Model { fn update(&mut self, msg: Option) -> Option { self.redraw = true; match msg.unwrap_or(Msg::None) { Msg::AppClose => { self.quit = true; None } Msg::Submit(lines) => { println!("Got user text: {:?}", lines); None } Msg::None => None, } } } // -- components pub struct Input<'a> { component: TextArea<'a>, } impl<'a> MockComponent for Input<'a> { fn view(&mut self, frame: &mut tuirealm::Frame, area: tuirealm::ratatui::layout::Rect) { self.component.view(frame, area); } fn query(&self, attr: Attribute) -> Option { self.component.query(attr) } fn attr(&mut self, query: Attribute, attr: AttrValue) { self.component.attr(query, attr) } fn state(&self) -> State { self.component.state() } fn perform(&mut self, cmd: Cmd) -> CmdResult { self.component.perform(cmd) } } impl<'a> Default for Input<'a> { fn default() -> Self { let textarea = TextArea::default().single_line(true); Self { component: textarea .borders( Borders::default() .color(Color::LightYellow) .modifiers(BorderType::Plain), ) .cursor_line_style(Style::default()) .cursor_style(Style::default().add_modifier(TextModifiers::REVERSED)) .footer_bar("Press to quit", Style::default()) .max_histories(64) .tab_length(4) .title("Value", Alignment::Left), } } } impl<'a> Component for Input<'a> { fn on(&mut self, ev: Event) -> Option { match ev { Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => Some(Msg::AppClose), Event::Keyboard(KeyEvent { code: Key::Backspace, .. }) | Event::Keyboard(KeyEvent { code: Key::Char('h'), modifiers: KeyModifiers::CONTROL, }) => { self.perform(Cmd::Delete); Some(Msg::None) } Event::Keyboard(KeyEvent { code: Key::Delete, .. }) => { self.perform(Cmd::Cancel); Some(Msg::None) } Event::Keyboard(KeyEvent { code: Key::PageDown, .. }) | Event::Keyboard(KeyEvent { code: Key::Down, modifiers: KeyModifiers::SHIFT, }) => { self.perform(Cmd::Scroll(Direction::Down)); Some(Msg::None) } Event::Keyboard(KeyEvent { code: Key::PageUp, .. }) | Event::Keyboard(KeyEvent { code: Key::Up, modifiers: KeyModifiers::SHIFT, }) => { self.perform(Cmd::Scroll(Direction::Up)); Some(Msg::None) } Event::Keyboard(KeyEvent { code: Key::Down, .. }) => { self.perform(Cmd::Move(Direction::Down)); Some(Msg::None) } Event::Keyboard(KeyEvent { code: Key::Left, modifiers: KeyModifiers::SHIFT, }) => { self.perform(Cmd::Custom(TEXTAREA_CMD_MOVE_WORD_BACK)); Some(Msg::None) } Event::Keyboard(KeyEvent { code: Key::Left, .. }) => { self.perform(Cmd::Move(Direction::Left)); Some(Msg::None) } Event::Keyboard(KeyEvent { code: Key::Right, modifiers: KeyModifiers::SHIFT, }) => { self.perform(Cmd::Custom(TEXTAREA_CMD_MOVE_WORD_FORWARD)); Some(Msg::None) } Event::Keyboard(KeyEvent { code: Key::Right, .. }) => { self.perform(Cmd::Move(Direction::Right)); Some(Msg::None) } Event::Keyboard(KeyEvent { code: Key::Up, .. }) => { self.perform(Cmd::Move(Direction::Up)); Some(Msg::None) } Event::Keyboard(KeyEvent { code: Key::End, .. }) | Event::Keyboard(KeyEvent { code: Key::Char('e'), modifiers: KeyModifiers::CONTROL, }) => { self.perform(Cmd::GoTo(Position::End)); Some(Msg::None) } Event::Keyboard(KeyEvent { code: Key::Enter, .. }) | Event::Keyboard(KeyEvent { code: Key::Char('m'), modifiers: KeyModifiers::CONTROL, }) => { self.perform(Cmd::Custom(TEXTAREA_CMD_NEWLINE)); Some(Msg::None) } Event::Keyboard(KeyEvent { code: Key::Home, .. }) | Event::Keyboard(KeyEvent { code: Key::Char('a'), modifiers: KeyModifiers::CONTROL, }) => { self.perform(Cmd::GoTo(Position::Begin)); Some(Msg::None) } #[cfg(feature = "clipboard")] Event::Keyboard(KeyEvent { code: Key::Char('v'), modifiers: KeyModifiers::CONTROL, }) => { self.perform(Cmd::Custom(TEXTAREA_CMD_PASTE)); Some(Msg::None) } Event::Keyboard(KeyEvent { code: Key::Char('z'), modifiers: KeyModifiers::CONTROL, }) => { self.perform(Cmd::Custom(TEXTAREA_CMD_UNDO)); Some(Msg::None) } Event::Keyboard(KeyEvent { code: Key::Char('y'), modifiers: KeyModifiers::CONTROL, }) => { self.perform(Cmd::Custom(TEXTAREA_CMD_REDO)); Some(Msg::None) } Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => { self.perform(Cmd::Type('\t')); Some(Msg::None) } Event::Keyboard(KeyEvent { code: Key::Char(ch), .. }) => { self.perform(Cmd::Type(ch)); Some(Msg::None) } _ => None, } } }