1use clap::builder::BoolishValueParser;
2use clap::{value_parser, Arg, ArgAction, Command};
3use clap_complete::shells::Shell;
4use std::io;
5use std::path::PathBuf;
6
7fn leak_str(s: &str) -> &'static str {
8 Box::leak(s.to_owned().into_boxed_str())
10}
11
12pub fn app(binary_name: &str) -> Command {
13 Command::new(leak_str(binary_name))
14 .version(env!("CARGO_PKG_VERSION"))
15 .author(env!("CARGO_PKG_AUTHORS"))
16 .about(env!("CARGO_PKG_DESCRIPTION"))
17 .arg(
19 Arg::new("log")
20 .long("log")
21 .global(true)
22 .num_args(1)
23 .value_name("LEVEL")
24 .value_parser(["trace", "debug", "info", "warn", "error"])
25 .help("Sets the log level, overriding the RUST_LOG environment variable."),
26 )
27 .arg(
28 Arg::new("verbose")
29 .short('v')
30 .global(true)
31 .help("Sets the log level to debug.")
32 .action(clap::ArgAction::SetTrue),
33 )
34 .subcommand(
36 Command::new("record")
37 .about("Record an asciicast via ttyd")
38 .arg(
39 Arg::new("session")
40 .long("session")
41 .num_args(1)
42 .value_name("NAME")
43 .env("SESSION")
44 .default_value("cast")
45 .help("tmux session name"),
46 )
47 .arg(
48 Arg::new("cols")
49 .long("cols")
50 .num_args(1)
51 .value_name("N")
52 .env("TMUX_COLS")
53 .value_parser(value_parser!(u32))
54 .default_value("80")
55 .help("tmux cols"),
56 )
57 .arg(
58 Arg::new("rows")
59 .long("rows")
60 .num_args(1)
61 .value_name("N")
62 .env("TMUX_ROWS")
63 .value_parser(value_parser!(u32))
64 .default_value("24")
65 .help("tmux rows"),
66 )
67 .arg(
68 Arg::new("port")
69 .long("port")
70 .num_args(1)
71 .value_name("PORT")
72 .env("TT_PORT")
73 .value_parser(value_parser!(u16))
74 .default_value("7681")
75 .help("starting port for ttyd"),
76 )
77 .arg(
78 Arg::new("font_size")
79 .long("font-size")
80 .num_args(1)
81 .value_name("PT")
82 .env("FONT_SIZE")
83 .value_parser(value_parser!(u32))
84 .default_value("24")
85 .help("font size for ttyd"),
86 )
87 .arg(
88 Arg::new("out")
89 .long("out")
90 .num_args(1)
91 .value_name("PATH")
92 .env("ASCII_OUT")
93 .value_parser(value_parser!(PathBuf))
94 .help("ascii output path (.cast). Default set dynamically."),
95 )
96 .arg(
97 Arg::new("workdir")
98 .long("workdir")
99 .num_args(1)
100 .value_name("PATH")
101 .env("WORKING_DIRECTORY")
102 .value_parser(value_parser!(PathBuf))
103 .help("working directory for tmux session. Default: $HOME"),
104 )
105 .arg(
106 Arg::new("kill_on_detach")
107 .long("kill-on-detach")
108 .env("TMUX_KILL_ON_DETACH")
109 .num_args(0..=1)
111 .require_equals(false)
112 .default_missing_value("true")
114 .value_parser(BoolishValueParser::new())
116 .action(ArgAction::Set)
118 .help("Kill tmux session after detach (supports true/false/1/0/yes/no/on/off)"),
119 ),
120 )
121 .subcommand(
123 Command::new("record-hook")
124 .about("INTERNAL: tmux/asciinema worker invoked inside ttyd")
125 .hide(true)
126 .arg(
127 Arg::new("child")
128 .long("child")
129 .action(clap::ArgAction::SetTrue),
130 )
131 .arg(
132 Arg::new("session")
133 .long("session")
134 .num_args(1)
135 .value_name("NAME")
136 .env("SESSION")
137 .default_value("cast"),
138 )
139 .arg(
140 Arg::new("cols")
141 .long("cols")
142 .num_args(1)
143 .value_name("N")
144 .env("TMUX_COLS")
145 .value_parser(value_parser!(u32))
146 .default_value("80"),
147 )
148 .arg(
149 Arg::new("rows")
150 .long("rows")
151 .num_args(1)
152 .value_name("N")
153 .env("TMUX_ROWS")
154 .value_parser(value_parser!(u32))
155 .default_value("24"),
156 )
157 .arg(
158 Arg::new("out")
159 .long("out")
160 .num_args(1)
161 .value_name("PATH")
162 .env("ASCII_OUT")
163 .value_parser(value_parser!(PathBuf))
164 .help("ascii output path (.cast). Default set dynamically."),
165 )
166 .arg(
167 Arg::new("workdir")
168 .long("workdir")
169 .num_args(1)
170 .value_name("PATH")
171 .env("WORKING_DIRECTORY")
172 .value_parser(value_parser!(PathBuf))
173 .help("working directory for tmux session. Default: $HOME"),
174 )
175 .arg(
176 Arg::new("kill_on_detach")
177 .long("kill-on-detach")
178 .env("TMUX_KILL_ON_DETACH")
179 .action(clap::ArgAction::SetTrue),
180 ),
181 )
182 .subcommand(
184 Command::new("completions")
185 .about("Generates shell completions script (tab completion)")
186 .arg(
187 Arg::new("shell")
188 .help("The shell to generate completions for")
189 .required(false)
190 .value_parser(["bash", "zsh", "fish"]),
191 ),
192 )
193}
194
195pub fn generate_completion_script(shell: Shell, binary_name: &str) {
196 clap_complete::generate(shell, &mut app(binary_name), binary_name, &mut io::stdout())
197}
198
199pub fn print_completion_instructions(binary_name: &str) {
200 eprintln!("### Instructions to enable tab completion for {binary_name}\n");
201 eprintln!("### Bash (~/.bashrc)\n source <({binary_name} completions bash)\n");
202 eprintln!("### Fish (~/.config/fish/config.fish)\n {binary_name} completions fish | source\n");
203 eprintln!("### Zsh (~/.zshrc)\n autoload -U compinit; compinit; source <({binary_name} completions zsh)");
204}
205
206#[cfg(test)]
207mod tests {
208 use super::*;
209
210 fn test_cmd() -> Command {
212 #[allow(unused_mut)]
214 let mut cmd = { app("shell-scene") };
215 let _ = cmd.render_help();
217 cmd
218 }
219
220 #[test]
221 fn record_kill_flag_true_when_present_without_value() {
222 let cmd = test_cmd();
223 let m = cmd
224 .clone()
225 .try_get_matches_from(["shell-scene", "record", "--kill-on-detach"])
226 .expect("parse should succeed");
227 let sub = m.subcommand().expect("has sub");
228 assert_eq!(sub.0, "record");
229 let kill = *sub.1.get_one::<bool>("kill_on_detach").unwrap_or(&false);
230 assert!(kill, "flag without value should imply true");
231 }
232
233 #[test]
234 fn record_kill_flag_false_when_zero_value() {
235 let cmd = test_cmd();
236 let m = cmd
237 .clone()
238 .try_get_matches_from(["shell-scene", "record", "--kill-on-detach=0"])
239 .expect("parse should succeed");
240 let sub = m.subcommand().expect("has sub");
241 assert_eq!(sub.0, "record");
242 let kill = *sub.1.get_one::<bool>("kill_on_detach").unwrap_or(&true);
243 assert!(!kill, "explicit =0 should parse as false");
244 }
245}