226 lines
9.1 KiB
Rust
226 lines
9.1 KiB
Rust
//! 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<TuiInputView>,
|
|
quit: Rc<Cell<bool>>,
|
|
last_submitted: Option<String>,
|
|
}
|
|
|
|
impl Entity for ShellView {
|
|
type Event = ();
|
|
}
|
|
|
|
impl ShellView {
|
|
fn new(quit: Rc<Cell<bool>>, ctx: &mut ViewContext<Self>) -> 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<TuiInputView>,
|
|
event: &TuiInputViewEvent,
|
|
ctx: &mut ViewContext<Self>,
|
|
) {
|
|
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<dyn TuiElement> {
|
|
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<warpui_core::EntityId> {
|
|
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<Self>) {
|
|
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");
|
|
});
|
|
}
|