tui/src/public_widgets/composer_input.rs
135 lines
//! Public wrapper around the internal ChatComposer for simple, reusable text input.//!//! This exposes a minimal interface suitable for other crates (e.g.,//! codex-cloud-tasks) to reuse the mature composer behavior: multi-line input,//! paste heuristics, Enter-to-submit, and Shift+Enter for newline.use crossterm::event::KeyEvent;use ratatui::buffer::Buffer;use ratatui::layout::Rect;use std::time::Duration;use crate::app_event::AppEvent;use crate::app_event_sender::AppEventSender;use crate::bottom_pane::ChatComposer;use crate::bottom_pane::InputResult;use crate::render::renderable::Renderable;/// Action returned from feeding a key event into the ComposerInput.pub enum ComposerAction { /// The user submitted the current text (typically via Enter). Contains the submitted text. Submitted(String), /// No submission occurred; UI may need to redraw if `needs_redraw()` returned true. None,}/// A minimal, public wrapper for the internal `ChatComposer` that behaves as a/// reusable text input field with submit semantics.pub struct ComposerInput { inner: ChatComposer, _tx: tokio::sync::mpsc::UnboundedSender<AppEvent>, rx: tokio::sync::mpsc::UnboundedReceiver<AppEvent>,}impl ComposerInput { /// Create a new composer input with a neutral placeholder. pub fn new() -> Self { let (tx, rx) = tokio::sync::mpsc::unbounded_channel(); let sender = AppEventSender::new(tx.clone()); // `enhanced_keys_supported=true` enables Shift+Enter newline hint/behavior. let inner = ChatComposer::new( /*has_input_focus*/ true, sender, /*enhanced_keys_supported*/ true, "Compose new task".to_string(), /*disable_paste_burst*/ false, ); Self { inner, _tx: tx, rx } } /// Returns true if the input is empty. pub fn is_empty(&self) -> bool { self.inner.is_empty() } /// Clear the input text. pub fn clear(&mut self) { self.inner .set_text_content(String::new(), Vec::new(), Vec::new()); } /// Feed a key event into the composer and return a high-level action. pub fn input(&mut self, key: KeyEvent) -> ComposerAction { let action = match self.inner.handle_key_event(key).0 { InputResult::Submitted { text, .. } => ComposerAction::Submitted(text), _ => ComposerAction::None, }; self.drain_app_events(); action } pub fn handle_paste(&mut self, pasted: String) -> bool { let handled = self.inner.handle_paste(pasted); self.drain_app_events(); handled } /// Override the footer hint items displayed under the composer. /// Each tuple is rendered as "<key> <label>", with keys styled. pub fn set_hint_items(&mut self, items: Vec<(impl Into<String>, impl Into<String>)>) { let mapped: Vec<(String, String)> = items .into_iter() .map(|(k, v)| (k.into(), v.into())) .collect(); self.inner.set_footer_hint_override(Some(mapped)); } /// Clear any previously set custom hint items and restore the default hints. pub fn clear_hint_items(&mut self) { self.inner.set_footer_hint_override(/*items*/ None); } /// Desired height (in rows) for a given width. pub fn desired_height(&self, width: u16) -> u16 { self.inner.desired_height(width) } /// Compute the on-screen cursor position for the given area. pub fn cursor_pos(&self, area: Rect) -> Option<(u16, u16)> { self.inner.cursor_pos(area) } /// Render the input into the provided buffer at `area`. pub fn render_ref(&self, area: Rect, buf: &mut Buffer) { self.inner.render(area, buf); } /// Return true if a paste-burst detection is currently active. pub fn is_in_paste_burst(&self) -> bool { self.inner.is_in_paste_burst() } /// Flush a pending paste-burst if the inter-key timeout has elapsed. /// Returns true if text changed and a redraw is warranted. pub fn flush_paste_burst_if_due(&mut self) -> bool { let flushed = self.inner.flush_paste_burst_if_due(); self.drain_app_events(); flushed } /// Recommended delay to schedule the next micro-flush frame while a /// paste-burst is active. pub fn recommended_flush_delay() -> Duration { crate::bottom_pane::ChatComposer::recommended_paste_flush_delay() } fn drain_app_events(&mut self) { while self.rx.try_recv().is_ok() {} }}impl Default for ComposerInput { fn default() -> Self { Self::new() }}