//! Interactive validation demo for `TuiInputView`. //! //! This is the Step 4 validation from `specs/tui-input-view/TECH.md`: //! a real terminal session that proves the full editor-backed input stack works. //! //! Run: //! ```sh //! cargo run -p warp_tui --example tui_input_demo //! ``` //! //! Keys (full Emacs/readline keybinding table): //! Printable chars insert text //! Shift+Enter insert newline (multi-line) //! Ctrl+J insert newline //! ←→↑↓ cursor movement //! Ctrl+A/E line start/end //! Ctrl+B/F char left/right //! Ctrl+P/N line up/down //! Alt+B/F word left/right //! Backspace delete back //! Delete/Ctrl+D delete forward //! Ctrl+W delete word back //! Alt+D delete word forward //! Ctrl+K kill to line end //! Ctrl+U kill to line start //! Ctrl+Y yank //! Ctrl+Z undo //! Enter submit (prints text and quits) //! Esc quit without submitting use std::cell::Cell; use std::rc::Rc; use warp::appearance::Appearance; use warp::editor::{CodeEditorModel, CodeEditorModelEvent}; use warp_tui::input::{TuiInputView, TuiInputViewEvent}; use warpui_core::elements::tui::{ Modifier, TuiColumn, TuiElement, TuiEventHandler, TuiParentElement, TuiStyle, TuiText, }; use warpui_core::platform::WindowStyle; use warpui_core::runtime::TuiRuntime; use warpui_core::{ AddWindowOptions, App, AppContext, Entity, TuiView, TypedActionView, ViewContext, }; // ───────────────────────────────────────────────────────────────────────────── // Shell view: wraps TuiInputView with a status bar and handles Submit/quit // ───────────────────────────────────────────────────────────────────────────── #[derive(Debug, Clone)] enum ShellAction { Quit, Submitted(String), } struct ShellView { input_view: warpui_core::ViewHandle, quit: Rc>, last_submitted: Option, } impl Entity for ShellView { type Event = (); } impl ShellView { fn new(quit: Rc>, ctx: &mut ViewContext) -> Self { let terminal_width = 80_u16; let input_model = ctx.add_model(|ctx| CodeEditorModel::new_tui(terminal_width, ctx)); // Re-render the shell whenever the input's content changes. The child input // view re-renders itself on edits, but the shell's "(N visual rows)" status // line is computed here, so without this subscription it would go stale. let status_model = input_model.clone(); ctx.subscribe_to_model(&status_model, |_, _, event, ctx| { if matches!(event, CodeEditorModelEvent::ContentChanged { .. }) { ctx.notify(); } }); // Create TuiInputView — subscribe to its Submitted event for submit handling. let input_view = ctx.add_typed_action_tui_view(move |ctx| TuiInputView::new(input_model, ctx)); ctx.subscribe_to_view(&input_view, Self::handle_input_view_event); Self { input_view, quit, last_submitted: None, } } fn handle_input_view_event( &mut self, _view: warpui_core::ViewHandle, event: &TuiInputViewEvent, ctx: &mut ViewContext, ) { match event { TuiInputViewEvent::Submitted(text) => { // Defer the dispatch: this callback runs from inside // `emit_event`, which has temporarily removed `ShellView` from // the window's view map. A synchronous `dispatch_typed_action` // here would fail to find `ShellView` in the responder chain, so // `ShellAction::Submitted` would never be handled. Deferring // queues it as an effect that runs after the view is reinserted. ctx.dispatch_typed_action_deferred(ShellAction::Submitted(text.clone())); } } } } impl TuiView for ShellView { fn ui_name() -> &'static str { "ShellView" } fn render(&self, ctx: &AppContext) -> Box { let input_view = self.input_view.as_ref(ctx); let lines = input_view.visual_line_count(ctx); let dim = TuiStyle::default().add_modifier(Modifier::DIM); let bold = TuiStyle::default().add_modifier(Modifier::BOLD); let mut column = TuiColumn::new(); // ── Header ────────────────────────────────────────────────────────── column = column.with_child(Box::new( TuiText::new("TuiInputView validation demo") .with_style(bold) .truncate(), )); column = column.with_child(Box::new( TuiText::new( "Enter=submit · Esc=quit · Shift+Enter=newline · Ctrl+K/U/Y=kill/yank · Ctrl+Z=undo", ) .with_style(dim) .truncate(), )); if let Some(ref submitted) = self.last_submitted { column = column.with_child(Box::new( TuiText::new(format!("Last submitted: {submitted:?}")) .with_style(dim) .truncate(), )); } column = column.with_child(Box::new(TuiText::new("─".repeat(80)).truncate())); // ── Prompt line ────────────────────────────────────────────────────── column = column.with_child(Box::new( TuiText::new(format!("≫ ({lines} visual rows)")) .with_style(dim) .truncate(), )); // ── TuiInputView (editor-backed) ───────────────────────────────────── column = column.with_child(Box::new(warpui_core::elements::tui::TuiChildView::new( &self.input_view, ))); // ── Escape handler (quit) ───────────────────────────────────────────── Box::new(TuiEventHandler::new(column).on_key("escape", |_, ctx, _| { ctx.dispatch_typed_action(ShellAction::Quit) })) } fn child_view_ids(&self, _ctx: &AppContext) -> Vec { vec![self.input_view.id()] } fn keymap_context(&self, _ctx: &AppContext) -> warpui_core::keymap::Context { // Focus propagates into TuiInputView so keystrokes reach its dispatch_event. let mut ctx = warpui_core::keymap::Context::default(); ctx.set.insert("ShellView"); ctx } } impl TypedActionView for ShellView { type Action = ShellAction; fn handle_action(&mut self, action: &ShellAction, ctx: &mut ViewContext) { match action { ShellAction::Quit => self.quit.set(true), ShellAction::Submitted(text) => { self.last_submitted = Some(text.clone()); ctx.notify(); // Print submitted text after quitting so it's visible in the // regular terminal (not the alternate screen). eprintln!("\n[submitted] {text:?}"); self.quit.set(true); } } } } // ───────────────────────────────────────────────────────────────────────────── // Entry point // ───────────────────────────────────────────────────────────────────────────── fn main() { App::test((), |mut app| async move { // `CodeEditorModel::new_tui` reads syntax colors from the `Appearance` // singleton, so register a mock one before constructing the editor. app.add_singleton_model(|_| Appearance::mock()); let quit = Rc::new(Cell::new(false)); let quit_for_view = quit.clone(); let (window_id, root) = app.update(|ctx| { ctx.add_tui_window( AddWindowOptions { window_style: WindowStyle::NotStealFocus, ..Default::default() }, move |ctx| ShellView::new(quit_for_view, ctx), ) }); let mut runtime = TuiRuntime::enter(&app, window_id, root).expect("enter alternate screen"); let quit_for_loop = quit.clone(); runtime .run_until(&mut app, move |_| quit_for_loop.get()) .expect("run TUI event loop"); }); }