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