rich_interact/component.rs
1//! The component contract: one event in, one view out.
2
3use std::time::Duration;
4
5use rich::{Console, Renderable, Segment};
6
7use crate::event::Event;
8use crate::keymap::Keymap;
9use crate::policy::{LineIo, NotInteractive};
10
11/// What a component wants after handling an event.
12#[derive(Debug)]
13pub enum Flow<T> {
14 /// Keep going. The view is rendered again, and repainted only if it
15 /// changed.
16 Continue,
17 /// Finished with a value.
18 Done(T),
19 /// The user backed out (Escape, `q`): no value.
20 Cancel,
21 /// The event was not for this component: it is left to the container
22 /// the component is in, which may use it itself (a focus key, a
23 /// binding of its own) or pass it on to its own container (0.0.14).
24 /// The event loop, at the top, treats it as [`Continue`](Flow::Continue).
25 Ignored,
26 /// Give the terminal to `command` (`$EDITOR file`, a pager) until it
27 /// exits, then take it back and repaint; the component then gets
28 /// [`Event::Returned`] with the exit code (#489).
29 Handoff(std::process::Command),
30}
31
32/// A rendered view: lines of segments, and where the terminal cursor goes
33/// (a text caret), or `None` to hide it.
34#[derive(Clone, Debug, Default)]
35pub struct View {
36 pub lines: Vec<Vec<Segment>>,
37 /// (row, column) within the view.
38 pub cursor: Option<(usize, usize)>,
39}
40
41impl View {
42 pub fn new(lines: Vec<Vec<Segment>>) -> View {
43 View {
44 lines,
45 cursor: None,
46 }
47 }
48
49 #[must_use]
50 pub fn with_cursor(mut self, row: usize, column: usize) -> View {
51 self.cursor = Some((row, column));
52 self
53 }
54
55 /// Add `other` below this view. Its cursor, if this view has none, moves
56 /// down with it.
57 pub fn push(&mut self, other: View) {
58 if self.cursor.is_none() {
59 self.cursor = other
60 .cursor
61 .map(|(row, column)| (row + self.lines.len(), column));
62 }
63 self.lines.extend(other.lines);
64 }
65}
66
67/// What a component renders for: the console to render with, and the space
68/// it may use.
69pub struct Context<'a> {
70 pub console: &'a Console,
71 /// Columns available.
72 pub width: usize,
73 /// Rows available: the terminal's height, or the inline height the
74 /// caller set. A view taller than this is cut off at the bottom.
75 pub height: usize,
76}
77
78impl Context<'_> {
79 /// Render `renderable` at the context's width into lines.
80 pub fn lines(&self, renderable: &dyn Renderable) -> Vec<Vec<Segment>> {
81 self.lines_at(renderable, self.width)
82 }
83
84 /// Render `renderable` at `width` columns into lines.
85 pub fn lines_at(&self, renderable: &dyn Renderable, width: usize) -> Vec<Vec<Segment>> {
86 let options = self.console.options().update_width(width.max(1));
87 self.console.render_lines(renderable, &options, false)
88 }
89
90 /// Render console markup into lines.
91 pub fn markup(&self, markup: &str) -> Vec<Vec<Segment>> {
92 let text = rich::Text::from_markup(markup).unwrap_or_else(|_| rich::Text::new(markup));
93 self.lines(&text)
94 }
95}
96
97/// An interactive component: a state machine that handles one event at a
98/// time and renders its state. The same component runs under
99/// [`run`](crate::run) and in an [`EventLoop`](crate::EventLoop), and in the
100/// [headless](crate::headless) driver for tests.
101pub trait Component {
102 type Output;
103
104 /// Handle one event. Ctrl+C never arrives here: the event loop ends the
105 /// component with [`Outcome::Interrupted`](crate::Outcome::Interrupted).
106 fn handle(&mut self, event: &Event, context: &Context<'_>) -> Flow<Self::Output>;
107
108 /// Called once, before the component is first painted, with the
109 /// terminal's size: for work that needs it, such as rendering content
110 /// at the width. Returning `Done` or `Cancel` finishes the component
111 /// without waiting for an event. The default does nothing.
112 fn start(&mut self, context: &Context<'_>) -> Flow<Self::Output> {
113 let _ = context;
114 Flow::Continue
115 }
116
117 /// Render the current state.
118 fn render(&self, context: &Context<'_>) -> View;
119
120 /// How often the component wants [`Event::Tick`], for a spinner or a
121 /// countdown. `None` (the default): never.
122 fn tick(&self) -> Option<Duration> {
123 None
124 }
125
126 /// Whether the component wants mouse events (#476): clicks, drags and
127 /// the wheel. Off by default, since reporting the mouse takes text
128 /// selection away from the terminal; a component turns it on when its
129 /// caller opts in, and [`run`](crate::run) then enables mouse reporting
130 /// for the session.
131 fn mouse(&self) -> bool {
132 false
133 }
134
135 /// The bindings this component has right now, for a help overlay, a
136 /// shortcut list or status-bar hints to read (see
137 /// [`keymap`](crate::keymap)). A container's include its focused
138 /// child's, before its own. The default declares none.
139 fn keymap(&self) -> Keymap {
140 Keymap::default()
141 }
142
143 /// Whether the component takes focus in a container: Tab stops on it
144 /// and keys go to it. The default: yes. A label, or a component that
145 /// has finished, says no.
146 fn focusable(&self) -> bool {
147 true
148 }
149
150 /// Move focus to the next (`forward`) or previous focus stop *inside*
151 /// this component, for Tab and Shift+Tab. `false` when there is none
152 /// after the current one, so the container moves on to its next
153 /// child. The default, for a component that is one stop: `false`.
154 fn focus_step(&mut self, forward: bool) -> bool {
155 let _ = forward;
156 false
157 }
158
159 /// Take focus from outside: at the first stop inside when `forward`,
160 /// at the last otherwise. `false` when nothing in it can take focus.
161 /// The default: whether the component is [`focusable`](Self::focusable).
162 fn focus_enter(&mut self, forward: bool) -> bool {
163 let _ = forward;
164 self.focusable()
165 }
166
167 /// The value to return when the terminal is not interactive and the
168 /// policy asks for defaults. `None`: there is none.
169 fn default_value(&self) -> Option<Self::Output> {
170 None
171 }
172
173 /// Ask for the value line by line, for when the terminal is not
174 /// interactive and the policy asks for a prompt. The default says the
175 /// component has no line-based form.
176 ///
177 /// `Ok(None)` means the user backed out ([`Outcome::Cancelled`](crate::Outcome::Cancelled)).
178 /// When input ends before an answer, the built-in components answer
179 /// with their default, and without one return [`NotInteractive::Ended`],
180 /// which [`degrade`](crate::degrade) reports as
181 /// [`NotInteractive::NoDefault`]: end of input is no answer, not a
182 /// refusal.
183 fn prompt(&mut self, io: &mut dyn LineIo) -> Result<Option<Self::Output>, NotInteractive> {
184 let _ = io;
185 Err(NotInteractive::NoPrompt)
186 }
187}