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 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 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}