first pass of merging in warp (doesn't build)
This commit is contained in:
@@ -0,0 +1,225 @@
|
||||
//! 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");
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user