Skip to main content

cli_ui/prompt/core/
runner.rs

1//! The generic prompt runner — owns raw mode, the key loop, line
2//! bookkeeping, the validate → success transition, the answered redraw,
3//! and Ctrl-C cleanup.
4//!
5//! Every prompt's `run()` method ultimately calls [`run`] with `self`.
6
7use super::super::error::{PromptError, Result};
8use super::{Frame, Prompt, RenderCtx, Step};
9
10/// Run a prompt to completion.
11///
12/// Picks the interactive engine when stderr & stdin are TTYs; otherwise
13/// delegates to [`Prompt::run_fallback`] for piped / CI environments.
14pub fn run<P: Prompt>(p: P) -> Result<P::Output> {
15    if super::super::engine::is_interactive() {
16        run_interactive(p)
17    } else {
18        p.run_fallback()
19    }
20}
21
22fn run_interactive<P: Prompt>(mut p: P) -> Result<P::Output> {
23    use super::super::engine::interactive::{enter_raw, leave_raw, read_key};
24    use super::super::render::{cleanup, clear_lines, hide_cursor, show_cursor};
25
26    let mut out = std::io::stderr();
27    let mut error: Option<String> = None;
28
29    enter_raw()?;
30    hide_cursor(&mut out).ok();
31
32    let mut frame = p.render(RenderCtx {
33        error: error.as_deref(),
34    });
35    write_frame(&mut out, &frame);
36
37    let outcome: Result<P::Output> = loop {
38        let key = match read_key() {
39            Ok(k) => k,
40            Err(_) => {
41                cleanup(&mut out, frame.len());
42                return Err(PromptError::Interrupted);
43            }
44        };
45        match p.handle(key) {
46            Step::Continue => {
47                error = None;
48            }
49            Step::Reject(msg) => {
50                error = Some(msg);
51            }
52            Step::Submit(value) => break Ok(value),
53            Step::Cancel => {
54                cleanup(&mut out, frame.len());
55                return Err(PromptError::Interrupted);
56            }
57        }
58        clear_lines(&mut out, frame.len()).ok();
59        frame = p.render(RenderCtx {
60            error: error.as_deref(),
61        });
62        write_frame(&mut out, &frame);
63    };
64
65    // If the *previous* frame happened to be an error frame but the user
66    // immediately produced a valid Submit, repaint a clean frame so the
67    // line counts the answered renderer is about to clear match reality.
68    if error.is_some() {
69        clear_lines(&mut out, frame.len()).ok();
70        frame = p.render(RenderCtx { error: None });
71        write_frame(&mut out, &frame);
72    }
73
74    show_cursor(&mut out).ok();
75    leave_raw()?;
76
77    let value = outcome?;
78    clear_lines(&mut out, frame.len()).ok();
79    write_answered(&p.render_answered(&value));
80    Ok(value)
81}
82
83fn write_frame(out: &mut std::io::Stderr, frame: &Frame) {
84    use std::io::Write;
85    for line in frame {
86        let _ = out.write_all(line.as_bytes());
87        let _ = out.write_all(b"\r\n");
88    }
89    let _ = out.flush();
90}
91
92fn write_answered(frame: &Frame) {
93    // Called after `leave_raw()` so plain `\n` line endings are correct.
94    for line in frame {
95        eprintln!("{line}");
96    }
97}