Skip to main content

slipcase_open/present/
terminal.rs

1//! The floor beneath the notifications: lines on the terminal.
2//
3// Author: David M. Anderson
4// Built with AI assistance (Claude, Anthropic)
5//
6//! Concept 9 keeps the command line always available, and this is what the
7//! instance narrates through when there is nothing better — no notification
8//! service, no session bus, or a build for a platform whose arm is not written
9//! yet.
10//!
11//! **It writes to the error stream, and the reason is the first invocation.**
12//! Where nobody is holding the front door, the `open` that starts the instance
13//! is also its own client, so one process is both narrating and answering a
14//! request. The answer to the request goes to the output stream and belongs to
15//! whoever ran the command; the narration that follows it belongs to the
16//! session and continues for as long as the session lives. Separating them
17//! keeps a shell pipeline reading the first without collecting the second.
18
19use std::io::Write as _;
20
21use super::{Answer, Channel, Choice, Question, Report, Weight};
22
23/// Reports and questions as lines.
24#[derive(Debug, Default, Clone, Copy)]
25pub struct Terminal;
26
27impl Channel for Terminal {
28    fn report(&self, report: &Report) {
29        let mut err = std::io::stderr().lock();
30        // Concept 9's weights have no terminal equivalent and none is invented:
31        // a bell or a colour would be this tool deciding it knows more about
32        // somebody's terminal than their terminal does. The weight is carried
33        // for the channels that can act on it.
34        let _ = writeln!(err, "{}", report.summary);
35        for line in &report.detail {
36            let _ = writeln!(err, "  {line}");
37        }
38        if report.weight == Weight::Interrupt {
39            let _ = err.flush();
40        }
41    }
42
43    fn ask(&self, question: &Question) {
44        let mut err = std::io::stderr().lock();
45        let _ = writeln!(err, "{}", question.summary);
46        for line in &question.detail {
47            let _ = writeln!(err, "  {line}");
48        }
49        for choice in &question.choices {
50            // Reveal has no verb to name, and inventing one to fill out the
51            // list would be adding a command to the interface so that a
52            // rendering could be symmetrical. The question's own detail names
53            // the directory, which is what reveal-the-folder is for.
54            if let Some(flag) = flag_for(*choice) {
55                let _ = writeln!(
56                    err,
57                    "  slipcase-open recover {} {flag}",
58                    question.about.as_str()
59                );
60            }
61        }
62        let _ = err.flush();
63    }
64
65    fn withdraw(&self, _about: &str) {
66        // A line already read cannot be taken back.
67    }
68
69    fn answers(&self) -> Vec<Answer> {
70        // A terminal has no way to deliver an answer to a question asked
71        // minutes ago. It is answered by running the verb the question named,
72        // which reaches the instance through the front door as a request rather
73        // than arriving here as an answer.
74        Vec::new()
75    }
76}
77
78/// The `recover` flag that performs a choice, where one does.
79fn flag_for(choice: Choice) -> Option<&'static str> {
80    match choice {
81        Choice::WriteBack => Some("--write-back"),
82        Choice::Discard => Some("--discard"),
83        Choice::Reveal => None,
84    }
85}
86
87#[cfg(test)]
88mod tests {
89    use super::{flag_for, Terminal};
90    use crate::present::{Answer, Channel, Choice};
91
92    #[test]
93    fn every_choice_is_either_a_verb_or_deliberately_not_one() {
94        // The point of the assertion is that adding a choice makes somebody
95        // decide, rather than letting it fall silently off the terminal.
96        assert_eq!(flag_for(Choice::WriteBack), Some("--write-back"));
97        assert_eq!(flag_for(Choice::Discard), Some("--discard"));
98        assert_eq!(flag_for(Choice::Reveal), None);
99    }
100
101    #[test]
102    fn a_terminal_never_answers() {
103        assert_eq!(Terminal.answers(), Vec::<Answer>::new());
104    }
105}