brush_core/
results.rs

1//! Encapsulation of execution results.
2
3use crate::{error, processes};
4
5/// Represents the result of executing a command or similar item.
6#[derive(Default)]
7pub struct ExecutionResult {
8    /// The control flow transition to apply after execution.
9    pub next_control_flow: ExecutionControlFlow,
10    /// The exit code resulting from execution.
11    pub exit_code: ExecutionExitCode,
12}
13
14impl ExecutionResult {
15    /// Returns a new `ExecutionResult` with the given exit code.
16    ///
17    /// # Arguments
18    ///
19    /// * `exit_code` - The exit code of the command.
20    pub fn new(exit_code: u8) -> Self {
21        Self {
22            exit_code: exit_code.into(),
23            ..Self::default()
24        }
25    }
26
27    /// Returns a new `ExecutionResult` reflecting a process that was stopped.
28    pub fn stopped() -> Self {
29        // TODO: Decide how to sort this out in a platform-independent way.
30        const SIGTSTP: std::os::raw::c_int = 20;
31
32        #[expect(clippy::cast_possible_truncation)]
33        Self::new(128 + SIGTSTP as u8)
34    }
35
36    /// Returns a new `ExecutionResult` with an exit code of 0.
37    pub const fn success() -> Self {
38        Self {
39            next_control_flow: ExecutionControlFlow::Normal,
40            exit_code: ExecutionExitCode::Success,
41        }
42    }
43
44    /// Returns a new `ExecutionResult` with a general error exit code.
45    pub const fn general_error() -> Self {
46        Self {
47            next_control_flow: ExecutionControlFlow::Normal,
48            exit_code: ExecutionExitCode::GeneralError,
49        }
50    }
51
52    /// Returns whether the command was successful.
53    pub const fn is_success(&self) -> bool {
54        self.exit_code.is_success()
55    }
56
57    /// Returns whether the execution result indicates normal control flow.
58    /// Returns `false` if there is any control flow transition requested.
59    pub const fn is_normal_flow(&self) -> bool {
60        matches!(self.next_control_flow, ExecutionControlFlow::Normal)
61    }
62
63    /// Returns whether the execution result indicates a loop break.
64    pub const fn is_break(&self) -> bool {
65        matches!(
66            self.next_control_flow,
67            ExecutionControlFlow::BreakLoop { .. }
68        )
69    }
70
71    /// Returns whether the execution result indicates a loop continue.
72    pub const fn is_continue(&self) -> bool {
73        matches!(
74            self.next_control_flow,
75            ExecutionControlFlow::ContinueLoop { .. }
76        )
77    }
78
79    /// Returns whether the execution result indicates an early return
80    /// from a function or script, or an exit from the shell. Returns `false`
81    /// otherwise, including loop breaks or continues.
82    pub const fn is_return_or_exit(&self) -> bool {
83        matches!(
84            self.next_control_flow,
85            ExecutionControlFlow::ReturnFromFunctionOrScript | ExecutionControlFlow::ExitShell
86        )
87    }
88}
89
90impl From<ExecutionExitCode> for ExecutionResult {
91    fn from(exit_code: ExecutionExitCode) -> Self {
92        Self {
93            next_control_flow: ExecutionControlFlow::Normal,
94            exit_code,
95        }
96    }
97}
98
99/// Represents an exit code from execution.
100#[derive(Clone, Copy, Default)]
101pub enum ExecutionExitCode {
102    /// Indicates successful execution.
103    #[default]
104    Success,
105    /// Indicates a general error.
106    GeneralError,
107    /// Indicates invalid usage.
108    InvalidUsage,
109    /// Cannot execute the command.
110    CannotExecute,
111    /// Indicates a command or similar item was not found.
112    NotFound,
113    /// Indicates execution was interrupted.
114    Interrupted,
115    /// Indicates unimplemented functionality was encountered.
116    Unimplemented,
117    /// A custom exit code.
118    Custom(u8),
119}
120
121impl ExecutionExitCode {
122    /// Returns whether the exit code indicates success.
123    pub const fn is_success(&self) -> bool {
124        matches!(self, Self::Success)
125    }
126}
127
128impl From<u8> for ExecutionExitCode {
129    fn from(code: u8) -> Self {
130        match code {
131            0 => Self::Success,
132            1 => Self::GeneralError,
133            2 => Self::InvalidUsage,
134            99 => Self::Unimplemented,
135            126 => Self::CannotExecute,
136            127 => Self::NotFound,
137            130 => Self::Interrupted,
138            code => Self::Custom(code),
139        }
140    }
141}
142
143impl From<ExecutionExitCode> for u8 {
144    fn from(code: ExecutionExitCode) -> Self {
145        Self::from(&code)
146    }
147}
148
149impl From<&ExecutionExitCode> for u8 {
150    fn from(code: &ExecutionExitCode) -> Self {
151        match code {
152            ExecutionExitCode::Success => 0,
153            ExecutionExitCode::GeneralError => 1,
154            ExecutionExitCode::InvalidUsage => 2,
155            ExecutionExitCode::Unimplemented => 99,
156            ExecutionExitCode::CannotExecute => 126,
157            ExecutionExitCode::NotFound => 127,
158            ExecutionExitCode::Interrupted => 130,
159            ExecutionExitCode::Custom(code) => *code,
160        }
161    }
162}
163
164/// Represents a control flow transition to apply.
165#[derive(Clone, Copy, Default)]
166pub enum ExecutionControlFlow {
167    /// Continue normal execution.
168    #[default]
169    Normal,
170    /// Break out of an enclosing loop.
171    BreakLoop {
172        /// Identifies which level of nested loops to break out of. 0 indicates the innermost loop,
173        /// 1 indicates the next outer loop, and so on.
174        levels: usize,
175    },
176    /// Continue to the next iteration of an enclosing loop.
177    ContinueLoop {
178        /// Identifies which level of nested loops to continue. 0 indicates the innermost loop,
179        /// 1 indicates the next outer loop, and so on.
180        levels: usize,
181    },
182    /// Return from the current function or script.
183    ReturnFromFunctionOrScript,
184    /// Exit the shell.
185    ExitShell,
186}
187
188impl ExecutionControlFlow {
189    /// Attempts to decrement the loop levels for `BreakLoop` or `ContinueLoop`.
190    /// If the levels reach zero, transitions to `Normal`. If the control flow is not
191    /// a loop break or continue, no changes are made.
192    #[must_use]
193    pub const fn try_decrement_loop_levels(&self) -> Self {
194        match self {
195            Self::BreakLoop { levels: 0 } | Self::ContinueLoop { levels: 0 } => Self::Normal,
196            Self::BreakLoop { levels } => Self::BreakLoop {
197                levels: *levels - 1,
198            },
199            Self::ContinueLoop { levels } => Self::ContinueLoop {
200                levels: *levels - 1,
201            },
202            control_flow => *control_flow,
203        }
204    }
205}
206
207/// Represents the result of spawning an execution; captures both execution
208/// that immediately returns as well as execution that starts a process
209/// asynchronously.
210pub enum ExecutionSpawnResult {
211    /// Indicates that the execution completed.
212    Completed(ExecutionResult),
213    /// Indicates that a process was started and had not yet completed.
214    StartedProcess(processes::ChildProcess),
215}
216
217impl From<ExecutionResult> for ExecutionSpawnResult {
218    fn from(result: ExecutionResult) -> Self {
219        Self::Completed(result)
220    }
221}
222
223impl ExecutionSpawnResult {
224    /// Waits for the command to complete.
225    ///
226    /// # Arguments
227    ///
228    /// * `no_wait` - If true, do not wait for the command to complete; return immediately.
229    pub async fn wait(self, no_wait: bool) -> Result<ExecutionWaitResult, error::Error> {
230        match self {
231            Self::StartedProcess(mut child) => {
232                let process_wait_result = if !no_wait {
233                    // Wait for the process to exit or for a relevant signal, whichever happens
234                    // first.
235                    child.wait().await?
236                } else {
237                    processes::ProcessWaitResult::Stopped
238                };
239
240                let wait_result = match process_wait_result {
241                    processes::ProcessWaitResult::Completed(output) => {
242                        ExecutionWaitResult::Completed(ExecutionResult::from(output))
243                    }
244                    processes::ProcessWaitResult::Stopped => ExecutionWaitResult::Stopped(child),
245                };
246
247                Ok(wait_result)
248            }
249            Self::Completed(result) => Ok(ExecutionWaitResult::Completed(result)),
250        }
251    }
252}
253
254/// Represents the result of waiting for an execution to complete.
255pub enum ExecutionWaitResult {
256    /// Indicates that the execution completed.
257    Completed(ExecutionResult),
258    /// Indicates that the execution was stopped.
259    Stopped(processes::ChildProcess),
260}