Skip to main content

rich_interact/
event_loop.rs

1//! The event loop, and the blocking [`run`] built on it.
2//!
3//! An [`EventLoop`] runs one or more mounted components at once: it renders
4//! them stacked, paints only what changed, waits for the next event, tick or
5//! timer, and routes keys to the first component still running and mouse
6//! events to the running component under the pointer. It is kept minimal on
7//! purpose (events, timers, repaint on change) so the intuiTUIve track can
8//! grow a component tree, reactive state and focus routing on top of it
9//! rather than beside it. [`run`] is the loop with one component, so a
10//! component behaves the same under both.
11
12use std::cell::RefCell;
13use std::fmt;
14use std::io;
15use std::process::Command;
16use std::rc::Rc;
17use std::time::Duration;
18
19use rich::{ColorSystem, Console, Segment};
20
21use crate::component::{Component, Context, Flow, View};
22use crate::event::{Button, Event, Key, Mouse, MouseKind};
23use crate::paint::Painter;
24use crate::policy::{Fallback, LineIo, NotInteractive, Policy, StdLineIo};
25use crate::session::{Backend, Session, SessionOptions};
26
27/// How a component ended.
28#[derive(Clone, Debug, PartialEq, Eq)]
29pub enum Outcome<T> {
30    Done(T),
31    /// The component cancelled (Escape, `q`).
32    Cancelled,
33    /// Ctrl+C.
34    Interrupted,
35}
36
37impl<T> Outcome<T> {
38    /// The value, if the component finished with one.
39    pub fn value(self) -> Option<T> {
40        match self {
41            Outcome::Done(value) => Some(value),
42            _ => None,
43        }
44    }
45}
46
47/// A component's outcome and whether it finished. Kept apart so taking the
48/// outcome does not make a finished component look like it is running.
49struct Shared<T> {
50    outcome: Option<Outcome<T>>,
51    finished: bool,
52}
53
54impl<T> Shared<T> {
55    fn finish(&mut self, outcome: Outcome<T>) {
56        if !self.finished {
57            self.outcome = Some(outcome);
58            self.finished = true;
59        }
60    }
61}
62
63/// A component's result, readable once the loop has run.
64pub struct Handle<T>(Rc<RefCell<Shared<T>>>);
65
66impl<T> Handle<T> {
67    /// The outcome, once; `None` while the component is running or after
68    /// it was taken.
69    pub fn take(&self) -> Option<Outcome<T>> {
70        self.0.borrow_mut().outcome.take()
71    }
72
73    /// Whether the component finished, even after its outcome was taken.
74    pub fn is_finished(&self) -> bool {
75        self.0.borrow().finished
76    }
77}
78
79/// How an [`EventLoop`] paints.
80#[derive(Clone, Debug)]
81pub struct LoopOptions {
82    /// Rows the inline region may take (default: the terminal's height).
83    pub height: Option<usize>,
84    /// Clear the region when the loop ends, instead of leaving the last
85    /// view on screen.
86    pub transient: bool,
87    /// The colours to paint with. `None`: the headless driver paints
88    /// truecolor, a terminal what rich detects.
89    pub color_system: Option<Option<ColorSystem>>,
90    pub no_color: bool,
91}
92
93impl Default for LoopOptions {
94    fn default() -> Self {
95        LoopOptions {
96            height: None,
97            transient: false,
98            color_system: None,
99            no_color: std::env::var_os("NO_COLOR").is_some_and(|value| !value.is_empty()),
100        }
101    }
102}
103
104/// What a mounted component did with an event.
105enum Step {
106    Continue,
107    Finished,
108    Handoff(Command),
109}
110
111/// A component with its result slot, behind one object-safe face.
112trait Mounted {
113    /// `None` once started.
114    fn start(&mut self, context: &Context<'_>) -> Option<Step>;
115    fn handle(&mut self, event: &Event, context: &Context<'_>) -> Step;
116    fn render(&self, context: &Context<'_>) -> View;
117    fn tick(&self) -> Option<Duration>;
118    fn finished(&self) -> bool;
119    fn interrupt(&mut self);
120}
121
122struct Slot<C: Component> {
123    component: C,
124    result: Rc<RefCell<Shared<C::Output>>>,
125    started: bool,
126}
127
128impl<C: Component> Slot<C> {
129    fn step(&mut self, flow: Flow<C::Output>) -> Step {
130        let outcome = match flow {
131            Flow::Continue | Flow::Ignored => return Step::Continue,
132            Flow::Handoff(command) => return Step::Handoff(command),
133            Flow::Done(value) => Outcome::Done(value),
134            Flow::Cancel => Outcome::Cancelled,
135        };
136        self.result.borrow_mut().finish(outcome);
137        Step::Finished
138    }
139}
140
141impl<C: Component> Mounted for Slot<C> {
142    fn start(&mut self, context: &Context<'_>) -> Option<Step> {
143        if std::mem::replace(&mut self.started, true) {
144            return None;
145        }
146        let flow = self.component.start(context);
147        Some(self.step(flow))
148    }
149
150    fn handle(&mut self, event: &Event, context: &Context<'_>) -> Step {
151        let flow = self.component.handle(event, context);
152        // A focus key nothing inside used ran off the end of the focus
153        // order: at the top, focus wraps round to the other end.
154        if let (Flow::Ignored, Some(key)) = (&flow, event.key()) {
155            let keymap = self.component.keymap();
156            let forward = keymap.is(key, crate::compose::FOCUS_NEXT);
157            if forward || keymap.is(key, crate::compose::FOCUS_PREVIOUS) {
158                self.component.focus_enter(forward);
159            }
160        }
161        self.step(flow)
162    }
163
164    fn render(&self, context: &Context<'_>) -> View {
165        self.component.render(context)
166    }
167
168    fn tick(&self) -> Option<Duration> {
169        self.component.tick()
170    }
171
172    fn finished(&self) -> bool {
173        self.result.borrow().finished
174    }
175
176    fn interrupt(&mut self) {
177        self.result.borrow_mut().finish(Outcome::Interrupted);
178    }
179}
180
181struct Timer<'a> {
182    interval: Duration,
183    next: Duration,
184    callback: Box<dyn FnMut() -> bool + 'a>,
185}
186
187/// Runs mounted components until every one has finished. See the
188/// [module docs](self).
189pub struct EventLoop<'a> {
190    backend: Box<dyn Backend + 'a>,
191    options: LoopOptions,
192    console: Console,
193    painter: Painter,
194    mounted: Vec<(Box<dyn Mounted + 'a>, Option<Duration>)>,
195    timers: Vec<Timer<'a>>,
196    size: (u16, u16),
197    /// The first row of each mounted component's view in the last paint,
198    /// and the painted lines, so a mouse event can be put in a component's
199    /// own coordinates and a click matched to a link.
200    rows: Vec<usize>,
201    shown: Vec<Vec<Segment>>,
202    /// The component a button press went to: its moves and release follow
203    /// it there (a drag), wherever the pointer goes.
204    grab: Option<usize>,
205    /// Whether the first turn (start and paint) has run: [`run`](Self::run)
206    /// or the first [`step`](Self::step) or [`pump`](Self::pump).
207    primed: bool,
208}
209
210impl<'a> EventLoop<'a> {
211    pub fn new(backend: impl Backend + 'a, options: LoopOptions) -> EventLoop<'a> {
212        let size = backend.size();
213        // Pinned unless the caller says otherwise, so headless output is
214        // byte-exact anywhere; `terminal` passes what rich detects.
215        let system = options.color_system.unwrap_or(Some(ColorSystem::Truecolor));
216        let console = Self::console(size, system, options.no_color);
217        EventLoop {
218            painter: Painter::new(system, options.no_color),
219            backend: Box::new(backend),
220            options,
221            console,
222            mounted: Vec::new(),
223            timers: Vec::new(),
224            size,
225            rows: Vec::new(),
226            shown: Vec::new(),
227            grab: None,
228            primed: false,
229        }
230    }
231
232    /// A loop on the real terminal, starting a [`Session`].
233    pub fn terminal(session: SessionOptions, options: LoopOptions) -> io::Result<EventLoop<'a>> {
234        let session = Session::start(session)?;
235        // The backend's own probe of the terminal, if it has one, else
236        // rich's detection.
237        let system = options
238            .color_system
239            .or_else(|| session.color_system())
240            .unwrap_or_else(|| {
241                Console::builder()
242                    .force_terminal(true)
243                    .build()
244                    .color_system()
245            });
246        Ok(EventLoop::new(
247            session,
248            LoopOptions {
249                color_system: Some(system),
250                ..options
251            },
252        ))
253    }
254
255    fn console(size: (u16, u16), system: Option<ColorSystem>, no_color: bool) -> Console {
256        Console::builder()
257            .width(size.0.max(1) as usize)
258            .height(size.1.max(1) as usize)
259            .force_terminal(true)
260            .color_system(system)
261            .no_color(no_color)
262            .build()
263    }
264
265    /// Add a component. Its outcome is in the handle once it finishes.
266    pub fn mount<C: Component + 'a>(&mut self, component: C) -> Handle<C::Output> {
267        let result = Rc::new(RefCell::new(Shared {
268            outcome: None,
269            finished: false,
270        }));
271        let tick = component.tick();
272        let next = tick.map(|interval| self.backend.elapsed() + interval);
273        self.mounted.push((
274            Box::new(Slot {
275                component,
276                result: Rc::clone(&result),
277                started: false,
278            }),
279            next,
280        ));
281        Handle(result)
282    }
283
284    /// Draw the graphics `source` places on the views' cells (micro assets
285    /// through Kitty, iTerm2 or Sixel). The loop wakes when an animation
286    /// among them shows its next frame. Give it only on a terminal that
287    /// showed it can draw them.
288    pub fn graphics(&mut self, source: std::sync::Arc<dyn rich_ext::frame::PlacementSource>) {
289        self.painter.set_graphics(Some(source));
290    }
291
292    /// Call `callback` every `interval` while the loop runs, until it
293    /// returns false.
294    pub fn every(&mut self, interval: Duration, callback: impl FnMut() -> bool + 'a) {
295        self.timers.push(Timer {
296            interval,
297            next: self.backend.elapsed() + interval,
298            callback: Box::new(callback),
299        });
300    }
301
302    /// (width, height) components render for.
303    fn space(&self) -> (usize, usize) {
304        let rows = self.size.1.max(1) as usize;
305        (
306            self.size.0.max(1) as usize,
307            self.options.height.map_or(rows, |height| height.min(rows)),
308        )
309    }
310
311    fn context(&self) -> Context<'_> {
312        let (width, height) = self.space();
313        Context {
314            console: &self.console,
315            width,
316            height,
317        }
318    }
319
320    fn paint(&mut self) -> io::Result<()> {
321        let context = self.context();
322        let max_rows = context.height;
323        let mut view = View::default();
324        let mut rows = Vec::with_capacity(self.mounted.len());
325        for (component, _) in &self.mounted {
326            rows.push(view.lines.len());
327            view.push(component.render(&context));
328        }
329        // The cursor was placed on the rendered cells; shown controls take
330        // one each, so it moves with them.
331        if let Some((row, column)) = view.cursor
332            && let Some(line) = view.lines.get(row)
333        {
334            let shown = crate::paint::sanitized_column(line, column);
335            let last = context.width.saturating_sub(1).max(column);
336            view.cursor = Some((row, shown.min(last)));
337        }
338        // A line wider than the terminal would wrap and shift every row
339        // below it: crop, once controls are shown as the characters the
340        // painter will write.
341        for line in &mut view.lines {
342            crate::paint::sanitize_line(line);
343            if line.iter().map(Segment::cell_length).sum::<usize>() > context.width {
344                *line = Segment::adjust_line_length(line, context.width, None);
345            }
346        }
347        let out = self.painter.paint(&view, max_rows);
348        view.lines.truncate(max_rows);
349        if !out.is_empty() {
350            let plain = view
351                .lines
352                .iter()
353                .map(|line| line.iter().map(|s| s.text.as_str()).collect::<String>())
354                .collect::<Vec<_>>()
355                .join("\n");
356            self.backend.painted(&plain);
357            self.backend.write_frame(&out)?;
358        }
359        self.rows = rows;
360        self.shown = view.lines;
361        Ok(())
362    }
363
364    /// `mouse`, from terminal cells, in the coordinates of mounted
365    /// component `index`'s view: `None` for a press outside it. A press of
366    /// the left button on a hyperlink is [`Event::Link`] instead.
367    fn locate(&self, index: usize, mouse: Mouse) -> Option<Event> {
368        let region_row = self.region_row(mouse);
369        let (start, end) = self.extent(index);
370        let inside = region_row.filter(|row| (start..end).contains(row));
371        if let (MouseKind::Down(_), None) = (mouse.kind, inside) {
372            return None;
373        }
374        if let (MouseKind::Down(Button::Left), Some(row)) = (mouse.kind, inside)
375            && let Some(url) = link_at(&self.shown[row], mouse.column as usize)
376        {
377            return Some(Event::Link(url));
378        }
379        let row = region_row.map_or(0, |row| row.saturating_sub(start));
380        Some(Event::Mouse(Mouse {
381            row: u16::try_from(row).unwrap_or(u16::MAX),
382            ..mouse
383        }))
384    }
385
386    /// `mouse`'s row in the painted region, `None` above it.
387    fn region_row(&self, mouse: Mouse) -> Option<usize> {
388        let top = if self.backend.alternate_screen() {
389            0
390        } else {
391            // An inline region that grew past the bottom of the screen
392            // scrolled up with it.
393            let rows = self.space().1;
394            (self.backend.origin() as usize).min(rows.saturating_sub(self.painter.extent()))
395        };
396        (mouse.row as usize).checked_sub(top)
397    }
398
399    /// The rows mounted component `index`'s view took in the last paint.
400    fn extent(&self, index: usize) -> (usize, usize) {
401        let start = self.rows.get(index).copied().unwrap_or(0);
402        let end = self
403            .rows
404            .get(index + 1)
405            .copied()
406            .unwrap_or(self.shown.len());
407        (start, end)
408    }
409
410    /// The running component a mouse event goes to. A press goes to the
411    /// one under the pointer (none, outside every view) and holds it until
412    /// the release, so a drag stays where it started; anything else goes
413    /// to the one under the pointer, or the first running.
414    fn mouse_target(&mut self, mouse: Mouse) -> Option<usize> {
415        let running = |index: &usize| !self.mounted[*index].0.finished();
416        let grabbed = self.grab.filter(running);
417        let under = self.region_row(mouse).and_then(|row| {
418            (0..self.mounted.len()).filter(running).find(|&index| {
419                let (start, end) = self.extent(index);
420                (start..end).contains(&row)
421            })
422        });
423        let first = (0..self.mounted.len()).find(running);
424        match mouse.kind {
425            MouseKind::Down(_) => {
426                self.grab = under;
427                under
428            }
429            MouseKind::Drag(_) => grabbed.or(under).or(first),
430            MouseKind::Up(_) => {
431                self.grab = None;
432                grabbed.or(under).or(first)
433            }
434            _ => under.or(first),
435        }
436    }
437
438    fn running(&self) -> bool {
439        self.mounted
440            .iter()
441            .any(|(component, _)| !component.finished())
442    }
443
444    /// The next moment a tick or timer is due.
445    fn deadline(&self) -> Option<Duration> {
446        let ticks = self
447            .mounted
448            .iter()
449            .filter(|(component, _)| !component.finished())
450            .filter_map(|(_, next)| *next);
451        let timers = self.timers.iter().map(|timer| timer.next);
452        let animation = self
453            .painter
454            .graphics()
455            .and_then(|source| source.next_change())
456            .map(|wait| self.backend.elapsed() + wait);
457        ticks.chain(timers).chain(animation).min()
458    }
459
460    fn fire_due(&mut self) -> io::Result<()> {
461        let now = self.backend.elapsed();
462        let mut timers = std::mem::take(&mut self.timers);
463        timers.retain_mut(|timer| {
464            if timer.next > now {
465                return true;
466            }
467            timer.next = now + timer.interval;
468            (timer.callback)()
469        });
470        timers.append(&mut self.timers);
471        self.timers = timers;
472        for index in 0..self.mounted.len() {
473            let due = self.mounted[index].1.is_some_and(|next| next <= now);
474            if due && !self.mounted[index].0.finished() {
475                let interval = self.mounted[index].0.tick();
476                self.mounted[index].1 = interval.map(|interval| now + interval);
477                self.deliver(index, &Event::Tick)?;
478            }
479        }
480        Ok(())
481    }
482
483    /// Start every component not started yet, before it is painted.
484    fn start(&mut self) -> io::Result<()> {
485        for index in 0..self.mounted.len() {
486            if self.mounted[index].0.finished() {
487                continue;
488            }
489            let (width, height) = self.space();
490            let context = Context {
491                console: &self.console,
492                width,
493                height,
494            };
495            if let Some(step) = self.mounted[index].0.start(&context) {
496                self.apply(index, step)?;
497            }
498        }
499        Ok(())
500    }
501
502    fn deliver(&mut self, index: usize, event: &Event) -> io::Result<()> {
503        let (width, height) = self.space();
504        // Field by field, so the context can borrow the console while the
505        // component is borrowed mutably.
506        let context = Context {
507            console: &self.console,
508            width,
509            height,
510        };
511        // Copies made while handling go out once the handler returns
512        // (#488): the component cannot write to the terminal itself.
513        crate::clipboard::open(self.backend.clipboard().err());
514        let step = self.mounted[index].0.handle(event, &context);
515        for text in crate::clipboard::close() {
516            self.backend.copy(&text)?;
517        }
518        self.apply(index, step)
519    }
520
521    /// Carry out what a component asked for: for a hand-off, run the
522    /// command and deliver [`Event::Returned`].
523    fn apply(&mut self, index: usize, step: Step) -> io::Result<()> {
524        if let Step::Handoff(mut command) = step {
525            let finish = self.painter.finish(false);
526            self.backend.write_frame(&finish)?;
527            let code = self.backend.handoff(&mut command)?;
528            self.painter.reset();
529            self.deliver(index, &Event::Returned(code))?;
530        }
531        Ok(())
532    }
533
534    /// The terminal is `columns` x `rows` (again): repaint every row, and
535    /// tell the components.
536    fn resized(&mut self, columns: u16, rows: u16) -> io::Result<()> {
537        self.size = (columns, rows);
538        self.console = Self::console(
539            self.size,
540            self.console.color_system(),
541            self.options.no_color,
542        );
543        self.painter.invalidate();
544        let event = Event::Resize { columns, rows };
545        for index in 0..self.mounted.len() {
546            if !self.mounted[index].0.finished() {
547                self.deliver(index, &event)?;
548            }
549        }
550        Ok(())
551    }
552
553    /// Run until every mounted component has finished. Ctrl+C finishes all
554    /// of them as interrupted.
555    pub fn run(&mut self) -> io::Result<()> {
556        let result = self.run_inner();
557        let written = self.finish();
558        result.and(written)
559    }
560
561    fn run_inner(&mut self) -> io::Result<()> {
562        self.prime()?;
563        while self.running() {
564            let event = self.backend.read(self.timeout())?;
565            self.turn(event)?;
566        }
567        Ok(())
568    }
569
570    /// The first turn: start the components and paint them.
571    fn prime(&mut self) -> io::Result<()> {
572        if !std::mem::replace(&mut self.primed, true) {
573            self.start()?;
574            self.paint()?;
575        }
576        Ok(())
577    }
578
579    /// One turn of the loop after a read: `event` (none when the read
580    /// timed out) to the components, then the ticks and timers due, then
581    /// start what was mounted meanwhile and paint.
582    fn turn(&mut self, event: Option<Event>) -> io::Result<()> {
583        match event {
584            None => {}
585            Some(Event::Key(key)) if key.is_interrupt() => {
586                for (component, _) in &mut self.mounted {
587                    component.interrupt();
588                }
589            }
590            // Raw mode turns off the terminal's own Ctrl+Z, so it arrives
591            // as a key: suspend as the terminal would have. The region is
592            // finished first, as for a hand-off, so the shell's "Stopped"
593            // goes below it.
594            Some(Event::Key(key)) if key == Key::ctrl('z') && self.backend.can_suspend() => {
595                let finish = self.painter.finish(false);
596                self.backend.write_frame(&finish)?;
597                self.backend.suspend()?;
598                self.painter.reset();
599                let (columns, rows) = self.backend.size();
600                self.resized(columns, rows)?;
601            }
602            Some(Event::Resize { columns, rows }) => {
603                // Back from a suspend started outside: what is on the
604                // screen is the shell's, not the region.
605                if self.backend.take_resumed() {
606                    self.painter.reset();
607                }
608                self.resized(columns, rows)?;
609            }
610            Some(Event::Mouse(mouse)) => {
611                if let Some(index) = self.mouse_target(mouse)
612                    && let Some(event) = self.locate(index, mouse)
613                {
614                    self.deliver(index, &event)?;
615                }
616            }
617            Some(event) => {
618                if let Some(index) = self
619                    .mounted
620                    .iter()
621                    .position(|(component, _)| !component.finished())
622                {
623                    self.deliver(index, &event)?;
624                }
625            }
626        }
627        self.fire_due()?;
628        self.start()?;
629        self.paint()
630    }
631
632    /// Push driving, for a host that cannot block in
633    /// [`Backend::read`] (a browser tab, another program's event loop):
634    /// deliver `event`, as [`run`](Self::run) does with one it read, then
635    /// fire the ticks and timers due by the backend's
636    /// [`elapsed`](Backend::elapsed) time and paint. Returns whether a
637    /// component is still running. The first call (of this or
638    /// [`pump`](Self::pump)) starts the components and paints them first;
639    /// once none is running, events are dropped. Call
640    /// [`finish`](Self::finish) at the end.
641    ///
642    /// Steps give the same results as [`run`](Self::run) given the same
643    /// events at the same times. [`PushBackend`](crate::PushBackend) is a
644    /// backend made for it: the host sets its time and takes what was
645    /// painted.
646    ///
647    /// ```
648    /// use rich_interact::{EventLoop, Event, Key, KeyCode, LoopOptions, Outcome, PushBackend, Select};
649    ///
650    /// let backend = PushBackend::new(30, 6);
651    /// let port = backend.port();
652    /// let mut event_loop = EventLoop::new(backend, LoopOptions::default());
653    /// let handle = event_loop.mount(Select::new("Pick", vec!["red".to_string(), "green".to_string()]));
654    /// event_loop.pump().unwrap();
655    /// assert!(port.take_output().contains("red"));
656    /// event_loop.step(Event::Key(Key::new(KeyCode::Down))).unwrap();
657    /// let running = event_loop.step(Event::Key(Key::new(KeyCode::Enter))).unwrap();
658    /// assert!(!running);
659    /// event_loop.finish().unwrap();
660    /// assert_eq!(handle.take(), Some(Outcome::Done("green".to_string())));
661    /// ```
662    pub fn step(&mut self, event: Event) -> io::Result<bool> {
663        self.prime()?;
664        if self.running() {
665            self.turn(Some(event))?;
666        }
667        Ok(self.running())
668    }
669
670    /// Push driving without an event: the turn [`run`](Self::run) takes
671    /// when a read times out. Fires the ticks and timers due by the
672    /// backend's time, starts what was mounted, and paints; returns whether
673    /// a component is still running. Call it when
674    /// [`timeout`](Self::timeout) has passed (after moving the backend's
675    /// clock), and once at the start to paint the first view.
676    pub fn pump(&mut self) -> io::Result<bool> {
677        if self.primed {
678            if self.running() {
679                self.turn(None)?;
680            }
681        } else {
682            self.prime()?;
683        }
684        Ok(self.running())
685    }
686
687    /// How long until a tick or timer is due, by the backend's time:
688    /// when to [`pump`](Self::pump) next. `None`: only an event changes
689    /// anything.
690    pub fn timeout(&self) -> Option<Duration> {
691        self.deadline()
692            .map(|deadline| deadline.saturating_sub(self.backend.elapsed()))
693    }
694
695    /// Whether a mounted component is still running.
696    pub fn is_running(&self) -> bool {
697        self.running()
698    }
699
700    /// End painting: leave the last view (or clear it, when
701    /// [`transient`](LoopOptions::transient)) and give the cursor back.
702    /// [`run`](Self::run) calls it; a host driving the loop with
703    /// [`step`](Self::step) calls it once at the end.
704    pub fn finish(&mut self) -> io::Result<()> {
705        let finish = self.painter.finish(self.options.transient);
706        self.backend.write_frame(&finish)
707    }
708}
709
710/// Why [`run`] returned no outcome.
711#[derive(Debug)]
712pub enum Error {
713    Io(io::Error),
714    NotInteractive(NotInteractive),
715}
716
717impl fmt::Display for Error {
718    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
719        match self {
720            Error::Io(error) => write!(f, "{error}"),
721            Error::NotInteractive(error) => write!(f, "{error}"),
722        }
723    }
724}
725
726impl std::error::Error for Error {}
727
728impl From<io::Error> for Error {
729    fn from(error: io::Error) -> Error {
730        Error::Io(error)
731    }
732}
733
734/// Options for [`run`].
735#[derive(Clone, Debug, Default)]
736pub struct RunOptions {
737    pub policy: Policy,
738    pub session: SessionOptions,
739    pub paint: LoopOptions,
740}
741
742/// Run one component on the terminal until it finishes: the blocking
743/// driver. When the terminal is not interactive (see [`Policy`]), no
744/// session starts and the policy's fallback decides the result.
745pub fn run<C: Component>(component: C, options: &RunOptions) -> Result<Outcome<C::Output>, Error> {
746    run_drawing(component, options, None)
747}
748
749/// [`run`], with `graphics` drawing the images placed on the component's
750/// cells (micro assets through Kitty, iTerm2 or Sixel; see
751/// [`EventLoop::graphics`]). Without a terminal, nothing is drawn and the
752/// policy's fallback decides, as with [`run`].
753pub fn run_with_graphics<C: Component>(
754    component: C,
755    options: &RunOptions,
756    graphics: std::sync::Arc<dyn rich_ext::frame::PlacementSource>,
757) -> Result<Outcome<C::Output>, Error> {
758    run_drawing(component, options, Some(graphics))
759}
760
761fn run_drawing<C: Component>(
762    component: C,
763    options: &RunOptions,
764    graphics: Option<std::sync::Arc<dyn rich_ext::frame::PlacementSource>>,
765) -> Result<Outcome<C::Output>, Error> {
766    let mut component = component;
767    if let Err(reason) = options.policy.detect_for(options.session.output) {
768        return degrade(
769            &mut component,
770            options.policy.fallback,
771            reason,
772            &mut StdLineIo,
773        );
774    }
775    let mut session = options.session;
776    if component.mouse() {
777        session.mouse = true;
778        // Clicks are placed from the top of the region, which is known on
779        // the alternate screen and, inline, only by asking the terminal on
780        // standard output: painting on standard error, take the screen.
781        if session.output == crate::session::Output::Stderr {
782            session.alternate_screen = true;
783        }
784    }
785    let mut event_loop = EventLoop::terminal(session, options.paint.clone())?;
786    if let Some(graphics) = graphics {
787        event_loop.graphics(graphics);
788    }
789    let handle = event_loop.mount(component);
790    event_loop.run()?;
791    drop(event_loop);
792    Ok(handle.take().unwrap_or(Outcome::Interrupted))
793}
794
795/// What [`run`] does without a terminal: follow `fallback`, asking through
796/// `io` when it says to prompt, with terminal controls in what is written
797/// shown as text. A prompt that ends with
798/// [`NotInteractive::Ended`] (input ran out, no default) fails with
799/// [`NotInteractive::NoDefault`] and the reason; one that ends with
800/// [`NotInteractive::Interrupted`] is [`Outcome::Interrupted`].
801pub fn degrade<C: Component>(
802    component: &mut C,
803    fallback: Fallback,
804    reason: crate::policy::Reason,
805    io: &mut dyn LineIo,
806) -> Result<Outcome<C::Output>, Error> {
807    let default = |component: &C| {
808        component
809            .default_value()
810            .map(Outcome::Done)
811            .ok_or(Error::NotInteractive(NotInteractive::NoDefault(reason)))
812    };
813    match fallback {
814        Fallback::Error => Err(Error::NotInteractive(NotInteractive::Terminal(reason))),
815        Fallback::Default => default(component),
816        Fallback::Prompt => match component.prompt(&mut crate::policy::Shown(io)) {
817            Ok(Some(value)) => Ok(Outcome::Done(value)),
818            Ok(None) => Ok(Outcome::Cancelled),
819            Err(NotInteractive::NoPrompt) => default(component),
820            Err(NotInteractive::Ended) => {
821                Err(Error::NotInteractive(NotInteractive::NoDefault(reason)))
822            }
823            Err(NotInteractive::Interrupted) => Ok(Outcome::Interrupted),
824            Err(error) => Err(Error::NotInteractive(error)),
825        },
826    }
827}
828
829/// The URL of the hyperlink at `column` of `line`, if one is there.
830fn link_at(line: &[Segment], column: usize) -> Option<String> {
831    let mut at = 0;
832    for segment in line.iter().filter(|segment| !segment.control) {
833        let width = segment.cell_length();
834        if column < at + width {
835            return segment
836                .style
837                .as_ref()
838                .and_then(|style| style.link())
839                .filter(|url| !url.is_empty())
840                .map(str::to_string);
841        }
842        at += width;
843    }
844    None
845}