1use std::ffi::OsString;
14use std::io;
15use std::path::PathBuf;
16use std::process::{Command as Child, Stdio};
17
18pub struct Handoff<Msg> {
54 program: OsString,
55 args: Vec<OsString>,
56 dir: Option<PathBuf>,
57 env: Vec<(OsString, OsString)>,
58 notice: Option<String>,
59 pause: bool,
60 on_finish: Box<dyn FnOnce(HandoffOutcome) -> Msg + Send>,
61}
62
63impl<Msg: Send + 'static> Handoff<Msg> {
64 pub fn new(program: impl Into<OsString>, on_finish: impl FnOnce(HandoffOutcome) -> Msg + Send + 'static) -> Self {
66 Self {
67 program: program.into(),
68 args: Vec::new(),
69 dir: None,
70 env: Vec::new(),
71 notice: None,
72 pause: false,
73 on_finish: Box::new(on_finish),
74 }
75 }
76
77 #[must_use]
79 pub fn arg(mut self, arg: impl Into<OsString>) -> Self {
80 self.args.push(arg.into());
81 self
82 }
83
84 #[must_use]
86 pub fn args(mut self, args: impl IntoIterator<Item = impl Into<OsString>>) -> Self {
87 self.args.extend(args.into_iter().map(Into::into));
88 self
89 }
90
91 #[must_use]
93 pub fn dir(mut self, dir: impl Into<PathBuf>) -> Self {
94 self.dir = Some(dir.into());
95 self
96 }
97
98 #[must_use]
100 pub fn env(mut self, key: impl Into<OsString>, value: impl Into<OsString>) -> Self {
101 self.env.push((key.into(), value.into()));
102 self
103 }
104
105 #[must_use]
108 pub fn notice(mut self, text: impl Into<String>) -> Self {
109 self.notice = Some(text.into());
110 self
111 }
112
113 #[must_use]
116 pub fn pause(mut self, pause: bool) -> Self {
117 self.pause = pause;
118 self
119 }
120
121 pub(crate) fn request(&self) -> HandoffRequest {
123 HandoffRequest {
124 program: self.program.clone(),
125 args: self.args.clone(),
126 notice: self.notice.clone(),
127 pause: self.pause,
128 }
129 }
130
131 pub(crate) fn finish(self, outcome: HandoffOutcome) -> Msg {
133 (self.on_finish)(outcome)
134 }
135
136 pub(crate) fn map<B>(self, map: impl FnOnce(Msg) -> B + Send + 'static) -> Handoff<B> {
138 let on_finish = self.on_finish;
139 Handoff {
140 program: self.program,
141 args: self.args,
142 dir: self.dir,
143 env: self.env,
144 notice: self.notice,
145 pause: self.pause,
146 on_finish: Box::new(move |outcome| map(on_finish(outcome))),
147 }
148 }
149}
150
151#[derive(Debug, Clone, PartialEq, Eq)]
153pub enum HandoffOutcome {
154 Finished {
156 code: Option<i32>,
158 },
159 Failed(String),
161}
162
163#[derive(Debug, Clone, PartialEq, Eq)]
165pub struct HandoffRequest {
166 pub program: OsString,
168 pub args: Vec<OsString>,
170 pub notice: Option<String>,
172 pub pause: bool,
174}
175
176pub(crate) struct HandoffScreen<'a> {
179 pub(crate) release: &'a mut dyn FnMut(Option<&str>) -> io::Result<()>,
182 pub(crate) take: &'a mut dyn FnMut() -> io::Result<()>,
185 pub(crate) wait_for_key: &'a mut dyn FnMut() -> io::Result<()>,
187}
188
189pub(crate) fn run<Msg: Send + 'static>(handoff: Handoff<Msg>, screen: &mut HandoffScreen<'_>) -> Msg {
196 let outcome = match (screen.release)(handoff.notice.as_deref()) {
197 Ok(()) => {
198 let outcome = spawn(&handoff);
199 if handoff.pause && matches!(outcome, HandoffOutcome::Finished { .. }) {
201 let _ = (screen.wait_for_key)();
202 }
203 match (screen.take)() {
204 Ok(()) => outcome,
205 Err(error) => HandoffOutcome::Failed(error.to_string()),
206 }
207 }
208 Err(error) => {
209 let _ = (screen.take)();
211 HandoffOutcome::Failed(error.to_string())
212 }
213 };
214 handoff.finish(outcome)
215}
216
217fn spawn<Msg>(handoff: &Handoff<Msg>) -> HandoffOutcome {
219 let mut child = Child::new(&handoff.program);
220 child.args(&handoff.args).stdin(Stdio::inherit()).stdout(Stdio::inherit()).stderr(Stdio::inherit());
221 if let Some(dir) = &handoff.dir {
222 child.current_dir(dir);
223 }
224 for (key, value) in &handoff.env {
225 child.env(key, value);
226 }
227 #[cfg(unix)]
228 let status = super::foreground::status(&mut child);
229 #[cfg(not(unix))]
230 let status = child.status();
231 match status {
232 Ok(status) => HandoffOutcome::Finished { code: status.code() },
233 Err(error) => HandoffOutcome::Failed(error.to_string()),
234 }
235}
236
237#[cfg(test)]
238mod tests {
239 use super::*;
240
241 fn run_with(handoff: Handoff<HandoffOutcome>, release_fails: bool) -> (HandoffOutcome, Vec<String>) {
245 let steps = std::cell::RefCell::new(Vec::new());
246 let mut release = |notice: Option<&str>| -> io::Result<()> {
247 steps.borrow_mut().push(match notice {
248 Some(text) => format!("release {text}"),
249 None => "release".to_owned(),
250 });
251 if release_fails { Err(io::Error::other("no terminal")) } else { Ok(()) }
252 };
253 let mut take = || -> io::Result<()> {
254 steps.borrow_mut().push("take".to_owned());
255 Ok(())
256 };
257 let mut wait_for_key = || -> io::Result<()> {
258 steps.borrow_mut().push("key".to_owned());
259 Ok(())
260 };
261 let outcome = run(
262 handoff,
263 &mut HandoffScreen { release: &mut release, take: &mut take, wait_for_key: &mut wait_for_key },
264 );
265 (outcome, steps.into_inner())
266 }
267
268 fn shell(script: &str) -> Handoff<HandoffOutcome> {
270 Handoff::new("sh", |outcome| outcome).arg("-c").arg(script)
271 }
272
273 #[test]
274 fn the_screen_is_released_around_the_program_and_taken_back() {
275 let (outcome, steps) = run_with(shell("exit 0").notice("Installing packages…"), false);
276 assert_eq!(outcome, HandoffOutcome::Finished { code: Some(0) });
277 assert_eq!(steps, ["release Installing packages…", "take"], "the program ran while the screen was released");
278 }
279
280 #[test]
281 fn the_exit_code_reaches_the_message() {
282 let (zero, _) = run_with(shell("exit 0"), false);
283 assert_eq!(zero, HandoffOutcome::Finished { code: Some(0) });
284 let (seven, _) = run_with(shell("exit 7"), false);
285 assert_eq!(seven, HandoffOutcome::Finished { code: Some(7) });
286 let (signal, _) = run_with(shell("kill -TERM $$"), false);
288 assert_eq!(signal, HandoffOutcome::Finished { code: None });
289 }
290
291 #[test]
292 fn arguments_the_directory_and_the_environment_reach_the_program() {
293 let (outcome, _) = run_with(
294 Handoff::new("sh", |outcome| outcome)
295 .args(["-c", r#"test "$(pwd)" = / && test "$QUVYTA_HANDOFF_TEST" = ok"#])
296 .dir("/")
297 .env("QUVYTA_HANDOFF_TEST", "ok"),
298 false,
299 );
300 assert_eq!(outcome, HandoffOutcome::Finished { code: Some(0) });
301 }
302
303 #[test]
304 fn an_unstartable_program_fails_and_the_screen_still_comes_back() {
305 let handoff = Handoff::new("quvyta-no-such-program", |outcome| outcome);
306 let (outcome, steps) = run_with(handoff, false);
307 let HandoffOutcome::Failed(reason) = outcome else {
308 panic!("a program that is not there cannot have finished: {outcome:?}");
309 };
310 assert!(!reason.is_empty(), "the reason names what went wrong");
311 assert_eq!(steps, ["release", "take"], "the terminal is taken back even so");
312 }
313
314 #[test]
315 fn a_terminal_that_cannot_be_released_fails_without_running_the_program() {
316 let handoff = shell("exit 0");
317 let (outcome, steps) = run_with(handoff, true);
318 assert_eq!(outcome, HandoffOutcome::Failed("no terminal".to_owned()));
319 assert_eq!(steps, ["release", "take"], "application mode is put back");
320 }
321
322 #[test]
323 fn pause_waits_for_a_key_only_when_it_is_asked_for() {
324 let (_, waited) = run_with(shell("exit 0").pause(true), false);
325 assert_eq!(waited, ["release", "key", "take"], "the key is awaited before the screen is taken back");
326 let (_, quiet) = run_with(shell("exit 0").pause(false), false);
327 assert_eq!(quiet, ["release", "take"]);
328 let (_, missing) = run_with(Handoff::new("quvyta-no-such-program", |o| o).pause(true), false);
329 assert_eq!(missing, ["release", "take"], "a program that never ran leaves nothing to read");
330 }
331
332 #[test]
333 fn the_request_a_harness_records_carries_the_program_and_its_options() {
334 let handoff = shell("less /etc/hostname").notice("Reading").pause(true);
335 let request = handoff.request();
336 assert_eq!(request.program, OsString::from("sh"));
337 assert_eq!(request.args, ["-c", "less /etc/hostname"].map(OsString::from));
338 assert_eq!(request.notice.as_deref(), Some("Reading"));
339 assert!(request.pause);
340 }
341}