ts_terminal/
action.rs

1//! Reporting an action that a CLI is performing.
2
3use alloc::string::{String, ToString};
4use std::io::{Write, stderr};
5
6use ts_ansi::style::*;
7
8/// Extension trait to update an action state based on the value of `self`.
9pub trait ActionResult {
10    /// Bind the final outcome of the action to the state of `self`.
11    fn bind_action(self, action: Action) -> Self;
12
13    /// Update the outcome of the action to error depending if `self` is considered an error.
14    fn error_action(self, action: &mut Action) -> Self;
15}
16
17impl<T, E> ActionResult for Result<T, E> {
18    fn bind_action(self, mut action: Action) -> Self {
19        match &self {
20            Ok(_) => action.report_success(),
21            Err(_) => action.report_fail(),
22        }
23        self
24    }
25
26    fn error_action(self, action: &mut Action) -> Self {
27        if self.is_err() {
28            action.report_fail();
29        }
30        self
31    }
32}
33impl<T> ActionResult for Option<T> {
34    fn bind_action(self, mut action: Action) -> Self {
35        match &self {
36            Some(_) => action.report_success(),
37            None => action.report_fail(),
38        }
39        self
40    }
41
42    fn error_action(self, action: &mut Action) -> Self {
43        if self.is_none() {
44            action.report_fail();
45        }
46        self
47    }
48}
49
50/// Action State
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52#[non_exhaustive]
53enum ActionState {
54    /// The action is in progress.
55    InProgress,
56    /// The action was a success.
57    Success,
58    /// The action was an error.
59    Fail,
60}
61
62/// Action progress reporter.
63#[derive(Debug, Clone)]
64pub struct Action {
65    /// The current state of the action
66    state: ActionState,
67    /// Verb for the in progress action.
68    actioning_verb: String,
69    /// Verb for the completed action.
70    actioned_verb: String,
71    /// Details for the action.
72    detail: String,
73    /// Should the action erase the previous line when printing the next state.
74    should_erase: bool,
75}
76
77impl Action {
78    /// Create and report a new in progress action.
79    ///
80    /// ## Limitations
81    /// * Anything else writing to the `stdout`/`stderr` will cause this to erase them unless
82    ///   [`Self::dont_erase`] is called.
83    /// * If the content is wrapped, this will erase part of it, keep details and verbs short.
84    pub fn new<S1: ToString, S2: ToString, S3: ToString>(
85        actioning_verb: S1,
86        actioned_verb: S2,
87        detail: S3,
88    ) -> Self {
89        let mut progress = Self {
90            state: ActionState::InProgress,
91            actioning_verb: actioning_verb.to_string(),
92            actioned_verb: actioned_verb.to_string(),
93            detail: detail.to_string(),
94            should_erase: false,
95        };
96
97        progress.print();
98        progress
99    }
100
101    /// Report the action as failed.
102    pub fn report_fail(&mut self) {
103        self.state = ActionState::Fail;
104        self.print();
105    }
106
107    /// Report the action as a success.
108    pub fn report_success(&mut self) {
109        self.state = ActionState::Success;
110        self.print();
111    }
112
113    /// Print the message for this action to `stderr`.
114    ///
115    /// All IO errors are ignored.
116    pub fn print(&mut self) {
117        #![expect(
118            unused_must_use,
119            reason = "displaying output is a non-critical part of the program, so this should not 
120            panic, additionally, I don't want to have to think about the errors when calling this"
121        )]
122
123        let mut stderr = stderr().lock();
124
125        if self.should_erase {
126            stderr.write_all(ERASE_LINE_UP.as_bytes());
127        }
128
129        let actioning = &self.actioning_verb;
130        let actioned = &self.actioned_verb;
131        let detail = &self.detail;
132
133        match self.state {
134            ActionState::InProgress => {
135                writeln!(stderr, "{CYAN}{BOLD}{actioning}{RESET} {detail}");
136            }
137            ActionState::Success => {
138                writeln!(stderr, "{GREEN}{BOLD}{actioned}{RESET} {detail}");
139            }
140            ActionState::Fail => {
141                writeln!(
142                    stderr,
143                    "{RED}{BOLD}{actioning}{RESET} {detail} {RED}{BOLD}failed{RESET}"
144                );
145            }
146        };
147
148        stderr.flush();
149
150        self.should_erase = true;
151    }
152
153    /// Disable erasing the previous line on next print.
154    pub fn dont_erase(&mut self) {
155        self.should_erase = false;
156    }
157}