Add cloud tasks (#3197)
Adds a TUI for managing, applying, and creating cloud tasks
This commit is contained in:
128
codex-rs/tui/src/public_widgets/composer_input.rs
Normal file
128
codex-rs/tui/src/public_widgets/composer_input.rs
Normal file
@@ -0,0 +1,128 @@
|
||||
//! 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 ratatui::widgets::WidgetRef;
|
||||
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;
|
||||
|
||||
/// 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(true, sender, true, "Compose new task".to_string(), 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());
|
||||
}
|
||||
|
||||
/// 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(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) {
|
||||
WidgetRef::render_ref(&self.inner, 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()
|
||||
}
|
||||
}
|
||||
1
codex-rs/tui/src/public_widgets/mod.rs
Normal file
1
codex-rs/tui/src/public_widgets/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod composer_input;
|
||||
Reference in New Issue
Block a user