shell_scene/util/
proc.rs

1use std::io;
2use std::path::Path;
3use std::process::{Command, Stdio};
4
5use crate::util::eprintln_err;
6use crate::util::net::wait_for_tcp;
7
8pub fn run_tmux(args: &[&str]) -> io::Result<()> {
9    let status = Command::new("tmux").args(args).status()?;
10    if !status.success() {
11        Err(io::Error::new(
12            io::ErrorKind::Other,
13            format!("tmux {:?} failed: {}", args, status),
14        ))
15    } else {
16        Ok(())
17    }
18}
19
20pub fn record_flow(
21    session: &str,
22    cols: u32,
23    rows: u32,
24    ascii_out: &Path,
25    working_dir: &Path,
26    kill_on_detach: bool,
27) -> i32 {
28    use std::fs;
29    if let Some(p) = ascii_out.parent() {
30        let _ = fs::create_dir_all(p);
31    }
32
33    let sock = format!("ttyd-{session}");
34    let has_session = Command::new("tmux")
35        .args(["-L", &sock, "has-session", "-t", session])
36        .stdout(Stdio::null())
37        .stderr(Stdio::null())
38        .status()
39        .map(|s| s.success())
40        .unwrap_or(false);
41
42    if !has_session {
43        let args = [
44            "-L",
45            &sock,
46            "new-session",
47            "-c",
48            &working_dir.to_string_lossy(),
49            "-d",
50            "-s",
51            session,
52            "-x",
53            &cols.to_string(),
54            "-y",
55            &rows.to_string(),
56            "bash",
57            "-l",
58        ];
59        if let Err(e) = run_tmux(&args) {
60            eprintln_err(&format!("Failed to create tmux session: {e}"));
61            return 2;
62        }
63        let _ = run_tmux(&["-L", &sock, "set", "-g", "status", "off"]);
64    }
65
66    let _ = run_tmux(&["-L", &sock, "set", "-g", "window-size", "manual"]);
67    let _ = run_tmux(&["-L", &sock, "set", "-g", "status", "off"]);
68    let _ = run_tmux(&[
69        "-L",
70        &sock,
71        "resize-window",
72        "-t",
73        &format!("{session}:0"),
74        "-x",
75        &cols.to_string(),
76        "-y",
77        &rows.to_string(),
78    ]);
79
80    eprintln!(
81        "[ttyd] Recording to: {} (size {}x{})",
82        ascii_out.display(),
83        cols,
84        rows
85    );
86
87    let attach_cmd = format!("tmux -L \"{}\" attach -t \"{}\"", sock, session);
88    let mut rec = Command::new("asciinema");
89    rec.arg("rec")
90        .arg("--overwrite")
91        .arg("-q")
92        .arg("--cols")
93        .arg(cols.to_string())
94        .arg("--rows")
95        .arg(rows.to_string())
96        .arg(ascii_out.to_string_lossy().to_string())
97        .arg("-c")
98        .arg(&attach_cmd)
99        .stdin(Stdio::inherit())
100        .stdout(Stdio::inherit())
101        .stderr(Stdio::inherit());
102
103    let status = rec.spawn().and_then(|mut child| child.wait());
104    let rc = match status {
105        Ok(s) => s.code().unwrap_or(1),
106        Err(e) => {
107            eprintln_err(&format!("Failed to run asciinema: {e}"));
108            1
109        }
110    };
111
112    if kill_on_detach {
113        let _ = run_tmux(&["-L", &sock, "kill-session", "-t", session]);
114        let _ = run_tmux(&["-L", &sock, "kill-server"]);
115    }
116
117    rc
118}
119
120pub fn spawn_ttyd_and_wait(
121    port: u16,
122    font_size: u32,
123    title: &str,
124    envs: &[(&str, String)],
125    cmd_and_args: &[String],
126) -> i32 {
127    let mut cmd = Command::new("ttyd");
128    cmd.arg("-p")
129        .arg(port.to_string())
130        .arg("-o")
131        .arg("-W")
132        .arg("-t")
133        .arg(format!("fontSize={font_size}"))
134        .arg("-t")
135        .arg("disableReconnect=true")
136        .arg("-t")
137        .arg(format!("titleFixed={title}"))
138        .arg("env");
139    for (k, v) in envs {
140        cmd.arg(format!("{k}={v}"));
141    }
142    for part in cmd_and_args {
143        cmd.arg(part);
144    }
145    cmd.stdout(Stdio::inherit())
146        .stderr(Stdio::inherit())
147        .stdin(Stdio::null());
148
149    let mut child = match cmd.spawn() {
150        Ok(c) => c,
151        Err(e) => {
152            eprintln_err(&format!("Failed to spawn ttyd: {e}"));
153            return 1;
154        }
155    };
156
157    // ctrl-c -> SIGTERM child
158    let child_id = child.id();
159    ctrlc::set_handler(move || {
160        #[cfg(unix)]
161        {
162            let _ = unsafe { libc::kill(child_id as i32, libc::SIGTERM) };
163        }
164        #[cfg(not(unix))]
165        {
166            let _ = child_id;
167        }
168    })
169    .ok();
170
171    let url = format!("http://127.0.0.1:{port}/");
172    eprintln!("[ttyd] Waiting for {url} ...");
173    wait_for_tcp("127.0.0.1", port, 100, 50);
174
175    // optional browser open
176    if which::which("xdg-open").is_ok() {
177        let _ = Command::new("xdg-open")
178            .arg(&url)
179            .stdout(Stdio::null())
180            .stderr(Stdio::null())
181            .spawn();
182    }
183
184    eprintln!(
185        "[ttyd] Serving at {url} (pid {}). Press Ctrl-C to stop.",
186        child.id()
187    );
188    match child.wait() {
189        Ok(status) => status.code().unwrap_or(1),
190        Err(e) => {
191            eprintln_err(&format!("Failed waiting for ttyd: {e}"));
192            1
193        }
194    }
195}