Skip to main content

rig_tui/
input.rs

1//! Terminal input, read on a thread that wakes the loop, as in Bevy's
2//! `examples/async_tasks/external_source_external_thread.rs`.
3
4use std::collections::VecDeque;
5use std::sync::Arc;
6use std::sync::atomic::{AtomicBool, Ordering};
7use std::thread::JoinHandle;
8use std::time::Duration;
9
10use bevy_ecs::prelude::*;
11use bevy_ecs::schedule::ScheduleLabel;
12use bevy_ecs::system::SystemParam;
13use bevy_log::error;
14use crossbeam_channel::Receiver;
15use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind};
16use ratatui::layout::Rect;
17
18use super::clipboard;
19use super::complete::{self, Completion, CompletionKind, CompletionSource};
20use super::editor::{Editor, MainEditor};
21use super::history::{PromptHistory, Sent};
22use super::keys::{self, Found, InputAction, KeyBinding, KeyFocus, KeyPress, KeyPressed, KeyScope};
23use super::pane::{MainPane, PaneState};
24use super::panel::{TerminalSize, TuiPanel};
25use super::pick::{self, Choices, PickerInput, Showing};
26use super::render::selectable;
27use super::terminal::TerminalFocus;
28use super::view::{ItemRow, TranscriptItem, TranscriptView, TuiView};
29use rig_ecs::agent::{ActiveTurn, Notice, StopTurn};
30use rig_ecs::inbox::{Deliver, DeliveryMode};
31use rig_ecs::tasks::Wake;
32use rig_harness::main_agent::ShownAgent;
33use rig_harness::pick::{Chosen, Pick};
34use rig_harness::prelude::{Processes, Scratch};
35
36/// Lines a page key scrolls.
37const PAGE: usize = 10;
38
39/// How long the input thread waits for an event before it checks whether
40/// the view is gone.
41const POLL: Duration = Duration::from_millis(100);
42
43/// The items of [`PendingInput`] a frame does at most: the rest, such as
44/// those of a macro that never ends, wait for the next frame, so the view
45/// keeps drawing.
46const KEYS_PER_FRAME: usize = 1024;
47
48/// The frame's input, in order: what the terminal reported and the
49/// actions plugins pushed. The view does one item at a time, running
50/// [`TuiKeys`] for each: [`InputSystems::Filter`] systems may change, drop
51/// or replace it (with the main [`Editor`](crate::Editor) and
52/// [`InputContext`] as the keys before it left them), and
53/// [`InputSystems::Dispatch`] does it. The picker or question shown takes
54/// it first, then a [`KeyBinding`](crate::KeyBinding), then the input.
55/// Items left by a run are done next; a push outside [`TuiKeys`] waits for
56/// the next frame.
57///
58/// ```no_run
59/// use rig_harness::prelude::*;
60/// use rig_tui::crossterm::event::{Event, KeyCode};
61/// use rig_tui::{Editor, InputSystems, KeyInput, MainEditor, PendingInput, InputContext, TuiKeys};
62///
63/// /// `/` on an empty input opens a search instead of a slash command.
64/// fn slash_searches(
65///     mut events: ResMut<PendingInput>,
66///     input: Single<&Editor, With<MainEditor>>,
67///     state: Res<InputContext>,
68/// ) {
69///     let slash = |item: &KeyInput| {
70///         matches!(item, KeyInput::Event(Event::Key(key)) if key.code == KeyCode::Char('/'))
71///     };
72///     if input.is_empty() && state.picking().is_none() && events.0.first().is_some_and(slash) {
73///         events.0.remove(0);
74///         // open the search
75///     }
76/// }
77///
78/// fn build(app: &mut App) {
79///     app.add_systems(TuiKeys, slash_searches.in_set(InputSystems::Filter));
80/// }
81/// ```
82#[derive(Resource, Clone, Debug, Default, PartialEq, Eq)]
83pub struct PendingInput(pub Vec<KeyInput>);
84
85/// An item of the [`PendingInput`].
86#[derive(Clone, Debug, PartialEq, Eq)]
87pub enum KeyInput {
88    /// What the terminal reported: a key, a paste, a resize, a change of
89    /// focus.
90    Event(Event),
91    /// A built-in action of the input, done where it stands among the keys.
92    Action(InputAction),
93}
94
95/// The schedule the view runs once for each item of the frame's
96/// [`PendingInput`], in order, in `PreUpdate`, with that one item in
97/// them: filters see one key (and what earlier filters made of it) with
98/// the input and the [`InputContext`] as the keys before it left them.
99/// Its sets, in this order, are [`InputSystems`]; the commands of a run
100/// are applied before the next, so a panel a key opens takes the next.
101#[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)]
102pub struct TuiKeys;
103
104/// Where the terminal view handles each item of the [`PendingInput`],
105/// in [`TuiKeys`], in this order.
106#[derive(SystemSet, Clone, Copy, Debug, PartialEq, Eq, Hash)]
107pub enum InputSystems {
108    /// Plugins read and change the item in the [`PendingInput`], with
109    /// the [`InputContext`] and the input as the keys before it left them.
110    Filter,
111    /// The view does what is left in the [`PendingInput`].
112    Dispatch,
113}
114
115/// What the view does with the keys, as the keys before the one being done
116/// left it, in [`TuiKeys`], and as the frame's keys left it, after them:
117/// for a filter that acts only in some states, such as not while a
118/// completion is open. The view writes it; it is read-only to plugins.
119#[derive(Resource, Clone, Debug, Default, PartialEq, Eq)]
120pub struct InputContext {
121    scope: KeyScope,
122    editor: Option<Entity>,
123    completion: Option<Completion>,
124    typed: Option<(CompletionKind, String)>,
125    answering: Option<Entity>,
126    picking: Option<Entity>,
127    selection: Option<usize>,
128}
129
130impl InputContext {
131    /// Where the keys go that no picker, question or global binding takes:
132    /// the input, the transcript while the person reads it
133    /// ([`KeyScope::Transcript`], whose keys the input's back), or the panel
134    /// with the [`KeyFocus`].
135    pub fn scope(&self) -> KeyScope {
136        self.scope
137    }
138
139    /// The [`Editor`] the keys type into: the one of the panel with the
140    /// [`KeyFocus`], else the [`MainEditor`]; `None` without either.
141    pub fn editor(&self) -> Option<Entity> {
142        self.editor
143    }
144
145    /// The open completion, which Up, Down, Tab, Enter and Esc act on.
146    pub fn completion(&self) -> Option<&Completion> {
147        self.completion.as_ref()
148    }
149
150    /// The token at the cursor that completion completes, and what is typed
151    /// of it after its marker, also while nothing matches: for a
152    /// [`CompletionSource`](crate::CompletionSource) that looks its
153    /// candidates up, such as issues matching `#14`.
154    pub fn typed(&self) -> Option<(CompletionKind, &str)> {
155        let (kind, text) = self.typed.as_ref()?;
156        Some((*kind, text))
157    }
158
159    /// The question answered in the input: Enter answers it, Esc refuses
160    /// it.
161    pub fn answering(&self) -> Option<Entity> {
162        self.answering
163    }
164
165    /// The picker or question shown over the transcript, which takes every
166    /// key.
167    pub fn picking(&self) -> Option<Entity> {
168        self.picking
169    }
170
171    /// The item highlighted in the picker or question shown over the
172    /// transcript ([`picking`](Self::picking)), as an index into all of its
173    /// items, whatever typing filtered out; `None` while none is, such as
174    /// when the typed answer is.
175    pub fn selection(&self) -> Option<usize> {
176        self.selection
177    }
178}
179
180/// Terminal events read by the input thread. Dropping it stops the thread.
181#[derive(Resource)]
182pub(crate) struct TerminalInput {
183    events: Receiver<Event>,
184    /// Tells the input thread to stop.
185    stop: Arc<AtomicBool>,
186    thread: Option<JoinHandle<()>>,
187}
188
189impl TerminalInput {
190    /// Starts the input thread.
191    pub(crate) fn start(wake: Wake) -> std::io::Result<Self> {
192        let (sender, events) = crossbeam_channel::unbounded();
193        let stop = Arc::new(AtomicBool::new(false));
194        let shared = Arc::clone(&stop);
195        let thread = std::thread::Builder::new()
196            .name("rig-harness-input".to_owned())
197            .spawn(move || {
198                while !shared.load(Ordering::Relaxed) {
199                    let event = match event::poll(POLL) {
200                        Ok(false) => continue,
201                        Ok(true) => event::read(),
202                        Err(failure) => Err(failure),
203                    };
204                    match event {
205                        Ok(event) => {
206                            if sender.send(event).is_err() {
207                                return;
208                            }
209                            wake.wake();
210                        }
211                        Err(failure) => {
212                            error!("could not read the terminal: {failure}");
213                            return;
214                        }
215                    }
216                }
217            })?;
218        Ok(Self {
219            events,
220            stop,
221            thread: Some(thread),
222        })
223    }
224
225    /// Stops the input thread and waits until it no longer reads the
226    /// terminal, for a program to have it.
227    pub(crate) fn stop(mut self) {
228        self.stop.store(true, Ordering::Relaxed);
229        if let Some(thread) = self.thread.take() {
230            thread.join().ok();
231        }
232    }
233}
234
235impl Drop for TerminalInput {
236    fn drop(&mut self) {
237        self.stop.store(true, Ordering::Relaxed);
238    }
239}
240
241/// Passes on what the input thread read.
242pub(crate) fn receive_events(input: Res<TerminalInput>, mut events: ResMut<PendingInput>) {
243    let mut read = input.events.try_iter().peekable();
244    if read.peek().is_some() {
245        events.0.extend(read.map(KeyInput::Event));
246    }
247}
248
249/// Drops the [`PendingInput`] in a run without the view, which nothing
250/// does.
251pub(crate) fn clear_events(mut events: ResMut<PendingInput>) {
252    if !events.0.is_empty() {
253        events.0.clear();
254    }
255}
256
257/// Runs [`TuiKeys`] for each item of the frame's [`PendingInput`], in
258/// order, with that item alone in them; what a run leaves goes next. A run
259/// that changed the input's text ends the reading of the transcript: the
260/// selection goes. Past [`KEYS_PER_FRAME`] the rest waits for the next
261/// frame, which it wakes.
262pub(crate) fn run_keys(world: &mut World) {
263    if world
264        .get_resource::<PendingInput>()
265        .is_none_or(|events| events.0.is_empty())
266    {
267        return;
268    }
269    let mut events = world.resource_mut::<PendingInput>();
270    let mut queue: VecDeque<KeyInput> = std::mem::take(&mut events.0).into();
271    let mut done = 0;
272    while let Some(item) = queue.pop_front() {
273        if done == KEYS_PER_FRAME {
274            queue.push_front(item);
275            world.resource_mut::<PendingInput>().0 = queue.into();
276            if let Some(wake) = world.get_resource::<Wake>() {
277                wake.wake();
278            }
279            return;
280        }
281        done += 1;
282        world.resource_mut::<PendingInput>().0.push(item);
283        let text = main_text(world);
284        world.run_schedule(TuiKeys);
285        let typed = main_text(world) != text;
286        if typed {
287            let mut main = world.query_filtered::<&mut TranscriptView, With<MainPane>>();
288            for mut transcript in main.iter_mut(world) {
289                if transcript.selected.is_some() {
290                    transcript.selected = None;
291                }
292            }
293        }
294        let left = std::mem::take(&mut world.resource_mut::<PendingInput>().0);
295        for item in left.into_iter().rev() {
296            queue.push_front(item);
297        }
298    }
299}
300
301/// The main editor's text.
302fn main_text(world: &mut World) -> Option<String> {
303    let mut main = world.query_filtered::<&Editor, With<MainEditor>>();
304    let text = main.single(world).ok().map(Editor::text);
305    text.map(str::to_owned)
306}
307
308/// Notes the [`InputContext`] for the next key's filters, and after the
309/// frame's keys.
310pub(crate) fn note_input_context(
311    (view, main): (Res<TuiView>, Query<&TranscriptView, With<MainPane>>),
312    showing: Query<(Entity, &PickerInput)>,
313    focus: Res<KeyFocus>,
314    panels: Query<(), With<TuiPanel>>,
315    choices: Choices,
316    (editors, sources): (
317        Query<(Entity, &Editor, Has<MainEditor>)>,
318        Query<&CompletionSource>,
319    ),
320    mut state: ResMut<InputContext>,
321) {
322    let main_editor = editors.iter().find(|(.., main)| *main);
323    let typed = main_editor.and_then(|(_, input, _)| complete::typed(input, &sources));
324    let focused = focus.0.filter(|panel| editors.contains(*panel));
325    let editor = focused.or(main_editor.map(|(entity, ..)| entity));
326    let scope = match focus.0 {
327        Some(panel) if panels.contains(panel) => KeyScope::Panel(panel),
328        _ if reading(&main) => KeyScope::Transcript,
329        _ => KeyScope::Input,
330    };
331    let choosing = showing.iter().next();
332    let answering = choosing.filter(|(_, c)| c.draft.is_some()).map(|(e, _)| e);
333    let picker = choosing.filter(|(_, c)| c.draft.is_none());
334    let picking = picker.map(|(e, _)| e);
335    let selection = picker.and_then(|(entity, choosing)| {
336        let shown = choices.shown(entity)?;
337        match shown.rows(&choosing.filter).get(choosing.selected) {
338            Some(&pick::Row::Item(index)) => Some(index),
339            _ => None,
340        }
341    });
342    state.set_if_neq(InputContext {
343        scope,
344        editor,
345        completion: view.completion.clone(),
346        typed,
347        answering,
348        picking,
349        selection,
350    });
351}
352
353/// Whether the person reads the main transcript: an item of it is
354/// selected.
355fn reading(main: &Query<&TranscriptView, With<MainPane>>) -> bool {
356    main.iter().any(|view| view.selected.is_some())
357}
358
359/// The key bindings and who has the keys.
360#[derive(SystemParam)]
361pub(crate) struct Keymap<'w, 's> {
362    bindings: Query<'w, 's, (Entity, &'static KeyBinding, Option<&'static InputAction>)>,
363    focus: ResMut<'w, KeyFocus>,
364    panels: Query<'w, 's, (), With<TuiPanel>>,
365}
366
367impl Keymap<'_, '_> {
368    /// Where the keys go besides the global bindings: the focused panel
369    /// while it is one, else the transcript while the person is `reading`
370    /// it, else the input.
371    fn scope(&mut self, reading: bool) -> KeyScope {
372        let unfocused = if reading {
373            KeyScope::Transcript
374        } else {
375            KeyScope::Input
376        };
377        match self.focus.0 {
378            Some(panel) if self.panels.contains(panel) => KeyScope::Panel(panel),
379            Some(_) => {
380                self.focus.0 = None;
381                unfocused
382            }
383            None => unfocused,
384        }
385    }
386}
387
388/// What a built-in key needs besides the view and the input.
389pub(crate) struct Context<'a, 'w, 's> {
390    /// The shown agent.
391    agent: Option<Entity>,
392    busy: bool,
393    scratch: Option<&'a Scratch>,
394    processes: Option<&'a Processes>,
395    wake: &'a Wake,
396    /// The prompts Up and Down browse.
397    history: &'a [String],
398    /// The pickers and questions open.
399    choices: &'a Choices<'w, 's>,
400}
401
402/// What a built-in key changes: the view, the input, and the transcript
403/// pane the keys act on, the one with the [`KeyFocus`] or else the main
404/// one. Its view is marked changed only when a key really changes it, as
405/// plugins rerun on its changes.
406pub(crate) struct Target<'a> {
407    view: &'a mut TuiView,
408    input: &'a mut Editor,
409    pane: Option<(Mut<'a, TranscriptView>, Mut<'a, PaneState>)>,
410}
411
412/// The transcript panes, which the keys scroll and select in.
413type Panes<'w, 's> = Query<
414    'w,
415    's,
416    (
417        Entity,
418        &'static mut TranscriptView,
419        &'static mut PaneState,
420        Has<MainPane>,
421    ),
422>;
423
424/// The editors, which the keys type into.
425type Editors<'w, 's> = Query<'w, 's, (Entity, &'static mut Editor, Has<MainEditor>)>;
426
427/// The editor the keys type into: the panel's with the keys, else the main
428/// one.
429fn keyed_editor(editors: &Editors, focus: Option<Entity>) -> Option<Entity> {
430    let focused = focus.filter(|entity| editors.contains(*entity));
431    let main = || editors.iter().find(|(.., main)| *main);
432    focused.or_else(|| main().map(|(entity, ..)| entity))
433}
434
435/// The pane the keys act on: the one with the keys, else the main one.
436fn keyed_pane(panes: &Panes, focus: Option<Entity>) -> Option<Entity> {
437    let focused = focus.filter(|entity| panes.contains(*entity));
438    let main = || {
439        panes
440            .iter()
441            .find(|(.., main)| *main)
442            .map(|(entity, ..)| entity)
443    };
444    focused.or_else(main)
445}
446
447/// Does the [`PendingInput`] left of the key being done, in order.
448pub(crate) fn read_input(
449    mut events: ResMut<PendingInput>,
450    (mut view, mut panes, shown): (ResMut<TuiView>, Panes, Res<ShownAgent>),
451    (mut editors, mut showing): (Editors, Showing),
452    mut keymap: Keymap,
453    choices: Choices,
454    agents: Query<Has<ActiveTurn>>,
455    (scratch, processes, wake, history): (
456        Option<Res<Scratch>>,
457        Option<Res<Processes>>,
458        Res<Wake>,
459        Res<PromptHistory>,
460    ),
461    (mut screen, mut focus): (ResMut<TerminalSize>, ResMut<TerminalFocus>),
462    mut commands: Commands,
463) {
464    // Taking the resources mutably marks them changed, and a frame with no
465    // input must leave them as they are, or the view redraws every frame.
466    if events.0.is_empty() {
467        return;
468    }
469    let busy = shown
470        .0
471        .and_then(|agent| agents.get(agent).ok())
472        .unwrap_or(false);
473    let context = Context {
474        agent: shown.0,
475        busy,
476        scratch: scratch.as_deref(),
477        processes: processes.as_deref(),
478        wake: &wake,
479        history: &history.entries,
480        choices: &choices,
481    };
482    // Keys typed with no editor to take them go nowhere.
483    let mut none = Editor::default();
484    for item in std::mem::take(&mut events.0) {
485        let editor = keyed_editor(&editors, keymap.focus.0);
486        let in_panel = editor.is_some() && editor == keymap.focus.0;
487        let mut found = editor.and_then(|editor| editors.get_mut(editor).ok());
488        let input: &mut Editor = match &mut found {
489            Some((_, editor, _)) => editor,
490            None => &mut none,
491        };
492        let pane = keyed_pane(&panes, keymap.focus.0);
493        let reading = panes
494            .iter()
495            .any(|(_, view, _, main)| main && view.selected.is_some());
496        let event = match item {
497            KeyInput::Event(event) => event,
498            KeyInput::Action(action) => {
499                let pane = pane.and_then(|pane| panes.get_mut(pane).ok());
500                let mut target = Target {
501                    view: &mut view,
502                    input,
503                    pane: pane.map(|(_, view, state, _)| (view, state)),
504                };
505                act(action, &mut target, (&context, &showing), &mut commands);
506                continue;
507            }
508        };
509        match event {
510            Event::Key(key) if key.kind == KeyEventKind::Release => {
511                released(key, (shown.0, reading), &mut keymap, &mut commands);
512            }
513            Event::Key(key) => {
514                // A question answered in the input leaves the keys to it.
515                let picking = showing.iter_mut().find(|(_, c)| c.draft.is_none());
516                if let Some((entity, mut choosing)) = picking {
517                    let shown = choices.shown(entity);
518                    let chosen =
519                        shown.and_then(|shown| pick::choice_key(key, &mut choosing, &shown));
520                    if let Some(chosen) = chosen {
521                        commands.trigger(Pick::person(entity, chosen));
522                        // The next one takes the keys that follow.
523                        view.queue.retain(|&queued| queued != entity);
524                        pick::show_first(&view, &choices, (&showing, input), &mut commands);
525                    }
526                    continue;
527                }
528                let pane = pane.and_then(|pane| panes.get_mut(pane).ok());
529                let mut target = Target {
530                    view: &mut view,
531                    input,
532                    pane: pane.map(|(_, view, state, _)| (view, state)),
533                };
534                pressed(
535                    key,
536                    (reading, in_panel),
537                    &mut target,
538                    &mut keymap,
539                    (&context, &showing),
540                    &mut commands,
541                );
542            }
543            // A paste arrives whole, newlines included, so it is not sent
544            // line by line. A panel with the keys reads it from the event.
545            Event::Paste(text) => match showing.iter_mut().find(|(_, c)| c.draft.is_none()) {
546                Some((_, mut choosing)) => {
547                    choosing
548                        .filter
549                        .push_str(text.lines().next().unwrap_or_default());
550                    choosing.selected = 0;
551                }
552                None if !in_panel && matches!(keymap.scope(reading), KeyScope::Panel(_)) => {}
553                // A dropped image file becomes `@path`, which attaches it.
554                None => match clipboard::dropped_image(&text) {
555                    Some(path) => clipboard::type_path(input, &path),
556                    None => input.insert(&text),
557                },
558            },
559            Event::Resize(width, height) => {
560                screen.set_if_neq(TerminalSize(Rect::new(0, 0, width, height)));
561            }
562            Event::FocusGained => {
563                focus.set_if_neq(TerminalFocus(Some(true)));
564            }
565            Event::FocusLost => {
566                focus.set_if_neq(TerminalFocus(Some(false)));
567            }
568            Event::Mouse(_) => {}
569        }
570    }
571}
572
573/// Handles a key pressed with no picker or question shown: a binding's,
574/// the next of a binding of several, else a character typed into the
575/// input, or into the editor of the panel with the keys (`in_panel`). With
576/// a panel focused, Esc that it does not bind gives the keys back to the
577/// input.
578fn pressed(
579    key: KeyEvent,
580    (reading, in_panel): (bool, bool),
581    target: &mut Target,
582    keymap: &mut Keymap,
583    (context, showing): (&Context, &Showing),
584    commands: &mut Commands,
585) {
586    let Target { view, input, .. } = target;
587    let press = KeyPress::of(&key);
588    let scope = keymap.scope(reading);
589    let bindings: Vec<(Entity, &KeyBinding)> = keymap
590        .bindings
591        .iter()
592        .map(|(entity, binding, _)| (entity, binding))
593        .collect();
594    view.pending.push(press);
595    let found = keys::find(&bindings, &view.pending, scope);
596    if found == Found::Partial {
597        return;
598    }
599    let sequence = std::mem::take(&mut view.pending).len() > 1;
600    let Found::Binding(entity) = found else {
601        if sequence {
602            return;
603        }
604        match scope {
605            KeyScope::Panel(_) if press.code == KeyCode::Esc => keymap.focus.0 = None,
606            KeyScope::Panel(_) if !in_panel => {}
607            _ => {
608                if let Some(character) = press.typed() {
609                    input.insert_char(character);
610                }
611            }
612        }
613        return;
614    };
615    let Ok((_, binding, action)) = keymap.bindings.get(entity) else {
616        return;
617    };
618    // Holding a key that also reports its release repeats it: one press.
619    if binding.releases && key.kind == KeyEventKind::Repeat {
620        return;
621    }
622    match action {
623        Some(action) => act(*action, target, (context, showing), commands),
624        None => commands.trigger(KeyPressed {
625            entity,
626            agent: context.agent,
627            released: false,
628        }),
629    }
630}
631
632/// Tells a binding that asked for releases that its key was let go, the
633/// person `reading` the transcript or not.
634fn released(
635    key: KeyEvent,
636    (agent, reading): (Option<Entity>, bool),
637    keymap: &mut Keymap,
638    commands: &mut Commands,
639) {
640    let press = KeyPress::of(&key);
641    let scope = keymap.scope(reading);
642    let released = keymap.bindings.iter().find(|(_, binding, _)| {
643        binding.releases
644            && binding.keys.presses() == [press]
645            && scope.applying().any(|applying| applying == binding.scope)
646    });
647    if let Some((entity, ..)) = released {
648        commands.trigger(KeyPressed {
649            entity,
650            agent,
651            released: true,
652        });
653    }
654}
655
656/// Does what a built-in key does. With a turn running, Enter steers it and
657/// Tab queues a follow-up. Esc interrupts the agent: it stops its turn,
658/// or, idle, what a plugin stops on it, such as a rebuild.
659fn act(
660    action: InputAction,
661    target: &mut Target,
662    (context, showing): (&Context, &Showing),
663    commands: &mut Commands,
664) {
665    use InputAction as Do;
666    let Target { view, input, pane } = target;
667    // A question answered in the input: Enter answers it, Esc refuses it.
668    let editing = showing
669        .iter()
670        .find(|(_, choosing)| choosing.draft.is_some());
671    let editing = editing.map(|(entity, _)| entity);
672    match (action, editing) {
673        (Do::Send, Some(ask)) if view.completion.as_ref().is_none_or(|c| c.is_exact()) => {
674            let answer = Chosen::text(input.take());
675            return close(ask, Some(answer), target, (context, showing), commands);
676        }
677        (Do::Stop, Some(ask)) if view.completion.is_none() => {
678            return close(ask, None, target, (context, showing), commands);
679        }
680        _ => {}
681    }
682    match action {
683        Do::Clear => input.clear(),
684        Do::NewLine => input.insert_char('\n'),
685        Do::LineStart => input.home(),
686        Do::LineEnd => input.end(),
687        Do::Left => input.left(),
688        Do::Right => input.right(),
689        Do::WordLeft => input.word_left(),
690        Do::WordRight => input.word_right(),
691        Do::Backspace => input.backspace(),
692        Do::Delete => input.delete(),
693        Do::DeleteWordBefore => input.delete_word_before(),
694        Do::DeleteWordAfter => input.delete_word_after(),
695        Do::DeleteToLineStart => input.delete_to_line_start(),
696        Do::DeleteToLineEnd => input.delete_to_line_end(),
697        Do::LineUp => input.up(context.history),
698        Do::LineDown => input.down(context.history),
699        Do::Up => match &mut view.completion {
700            Some(completion) => completion.select(-1),
701            None => input.up(context.history),
702        },
703        Do::Down => match &mut view.completion {
704            Some(completion) => completion.select(1),
705            None => input.down(context.history),
706        },
707        Do::ScrollUp | Do::ScrollDown | Do::PageUp | Do::PageDown => {
708            if let Some((_, state)) = pane {
709                let page = PAGE.min(state.page());
710                match action {
711                    Do::ScrollUp => state.scroll.up(1),
712                    Do::ScrollDown => state.scroll.down(1),
713                    Do::PageUp => state.scroll.up(page),
714                    _ => state.scroll.down(page),
715                }
716            }
717        }
718        Do::Tab if view.completion.is_none() && context.busy && editing.is_none() => {
719            let state = pane.as_mut().map(|(_, state)| &mut **state);
720            send(view, input, state, context.agent, commands, true);
721        }
722        Do::Tab => accept_completion(view, input),
723        Do::Send => {
724            // Enter completes a partly typed command or path first.
725            if view
726                .completion
727                .as_ref()
728                .is_some_and(|completion| !completion.is_exact())
729            {
730                accept_completion(view, input);
731                return;
732            }
733            if input.continue_line() {
734                return;
735            }
736            let state = pane.as_mut().map(|(_, state)| &mut **state);
737            send(view, input, state, context.agent, commands, false);
738        }
739        Do::Stop => match view.completion.take() {
740            Some(completion) => view.dismissed = Some(completion.start),
741            None => {
742                if let Some(entity) = context.agent {
743                    commands.trigger(StopTurn::new(entity));
744                }
745            }
746        },
747        Do::PasteImage => {
748            let Some(images) = context.scratch.and_then(Scratch::images) else {
749                let why = "This session keeps no files to paste an image into.";
750                commands.write_message(Notice::info(context.agent, why));
751                return;
752            };
753            let launcher = context
754                .processes
755                .map(Processes::launcher)
756                .unwrap_or_default();
757            clipboard::paste_image(images, launcher, context.wake, commands);
758        }
759        Do::SelectUp | Do::SelectDown => {
760            if let Some((transcript, state)) = pane {
761                select(action == Do::SelectUp, transcript, state);
762            }
763        }
764        Do::Deselect => {
765            if let Some((transcript, _)) = pane
766                && transcript.selected.is_some()
767            {
768                transcript.selected = None;
769            }
770        }
771        Do::AnswerInline(number) => {
772            answer_inline(usize::from(number), target, (context, showing), commands);
773        }
774        Do::DismissInline => {
775            if let Some(&ask) = view.inline.first() {
776                view.inline.remove(0);
777                commands.trigger(Pick::person(ask, None));
778            }
779        }
780    }
781}
782
783/// Answers the question taking the input with `chosen`, `None` refusing
784/// it, and puts the draft back, or shows the next question.
785fn close(
786    ask: Entity,
787    chosen: Option<Chosen>,
788    target: &mut Target,
789    (context, showing): (&Context, &Showing),
790    commands: &mut Commands,
791) {
792    commands.trigger(Pick::person(ask, chosen));
793    target.view.queue.retain(|&queued| queued != ask);
794    pick::show_first(
795        target.view,
796        context.choices,
797        (showing, target.input),
798        commands,
799    );
800}
801
802/// Moves the selection up or down the items of the pane laid out last:
803/// up from none selects the newest, down past the newest selects none. The
804/// item selected is brought on screen.
805fn select(up: bool, transcript: &mut Mut<TranscriptView>, state: &mut Mut<PaneState>) {
806    let items = state
807        .transcript
808        .marks_in(&state.parts)
809        .map(|(item, _)| item);
810    let items: Vec<&TranscriptItem> = items.filter(|item| selectable(item)).collect();
811    let at = (transcript.selected.as_ref())
812        .and_then(|selected| items.iter().position(|item| *item == selected));
813    let next = match (at, up) {
814        (None, true) => items.len().checked_sub(1),
815        (None, false) => None,
816        (Some(at), true) => Some(at.saturating_sub(1)),
817        (Some(at), false) => Some(at + 1),
818    };
819    let selected = next
820        .and_then(|next| items.get(next))
821        .map(|item| (*item).clone());
822    if let Some(item) = &selected {
823        state.scroll.to(item.clone(), ItemRow::Reveal);
824    }
825    if transcript.selected != selected {
826        transcript.selected = selected;
827    }
828}
829
830/// Answers the question shown beside the work with its choice `number`,
831/// counted from 1, or after its choices, when it takes text, takes the
832/// input for its answer.
833fn answer_inline(
834    number: usize,
835    target: &mut Target,
836    (context, showing): (&Context, &Showing),
837    commands: &mut Commands,
838) {
839    let Some(&ask) = target.view.inline.first() else {
840        return;
841    };
842    let Some(shown) = context.choices.shown(ask) else {
843        return;
844    };
845    let Some(index) = number.checked_sub(1) else {
846        return;
847    };
848    if index < shown.items.len() {
849        target.view.inline.remove(0);
850        commands.trigger(Pick::person(ask, Some(Chosen::item(index))));
851    } else if index == shown.items.len() && shown.free_text && showing.is_empty() {
852        target.view.inline.remove(0);
853        target.view.queue.insert(0, ask);
854        pick::show_first(
855            target.view,
856            context.choices,
857            (showing, target.input),
858            commands,
859        );
860    }
861}
862
863/// Sends the input to `agent`, the one shown, as the user's [`Deliver`]: a
864/// slash command, or a message that starts a turn, or steers the running
865/// one, or is queued for after it with `queue`. A refused command sent
866/// again unchanged is a message. The prompt history records it, and the
867/// pane the keys act on follows the newest output.
868pub(crate) fn send(
869    view: &mut TuiView,
870    input: &mut Editor,
871    pane: Option<&mut PaneState>,
872    agent: Option<Entity>,
873    commands: &mut Commands,
874    queue: bool,
875) {
876    let Some(entity) = agent else {
877        return;
878    };
879    if input.text().trim().is_empty() {
880        return;
881    }
882    let text = input.take();
883    commands.write_message(Sent(text.clone()));
884    if let Some(pane) = pane {
885        pane.scroll.follow();
886    }
887    let mode = if queue {
888        DeliveryMode::Queue
889    } else {
890        DeliveryMode::Steer
891    };
892    let refused = view.refused.take().as_ref() == Some(&text);
893    let typed = Deliver::new(entity, text, mode);
894    commands.trigger(if refused { typed.literal() } else { typed });
895}
896
897/// Puts the selected completion in place of the token being completed.
898fn accept_completion(view: &mut TuiView, input: &mut Editor) {
899    let Some(completion) = view.completion.take() else {
900        return;
901    };
902    if let Some(text) = completion.chosen() {
903        input.replace_before_cursor(completion.start, &text);
904    }
905}
906
907#[cfg(test)]
908mod tests;