1use crate::commands::{
7 ByCommand, ForCommand, FreeCommand, FreezeCommand, InCommand, InfoCommand, KillCommand,
8 ListCommand, OnCommand, OrphansCommand, PortsCommand, StopCommand, StuckCommand, ThawCommand,
9 TreeCommand, UnstickCommand, WaitCommand, WatchCommand, WhyCommand,
10};
11use clap::builder::styling::Styles;
12use clap::builder::StyledStr;
13use clap::{Parser, Subcommand};
14use clap_complete::Shell;
15
16const VERSION_INFO: &str = concat!(
17 env!("PROC_VERSION"),
18 "\nhttps://github.com/yazeed/proc-cli",
19 "\nLicense: MIT"
20);
21
22pub const STYLES: Styles = Styles::styled();
26
27const EXAMPLES: &[(&str, &[(&str, &str)])] = &[
29 (
30 "Port/Process Lookup",
31 &[
32 ("proc on :3000", "What's on port 3000?"),
33 ("proc on :3000 :8080", "What's on several ports?"),
34 (
35 "proc on node",
36 "What ports are the processes named node using?",
37 ),
38 ],
39 ),
40 (
41 "Find by File",
42 &[
43 (
44 "proc for ./server",
45 "Processes running this program (or holding it open)",
46 ),
47 (
48 "proc for /usr/bin/node",
49 "Processes running this executable",
50 ),
51 (
52 "proc for app.log",
53 "What has this file open? (not searched on Windows)",
54 ),
55 ],
56 ),
57 (
58 "Filter by Name",
59 &[
60 ("proc by node", "Processes named 'node' (the whole name)"),
61 (
62 "proc by node --cmd",
63 "Anything with 'node' in its name or command line",
64 ),
65 ("proc by node --in .", "Node processes in current directory"),
66 ("proc by node --min-cpu 5", "Node processes using >5% CPU"),
67 ],
68 ),
69 (
70 "Filter by Directory",
71 &[
72 ("proc in .", "Processes in current directory"),
73 ("proc in . --by node", "Node processes in cwd"),
74 ],
75 ),
76 (
77 "List All",
78 &[
79 ("proc list", "All processes"),
80 ("proc list --min-cpu 10", "Processes using >10% CPU"),
81 ],
82 ),
83 (
84 "Info/Kill/Stop (multi-target)",
85 &[
86 ("proc info :3000 :8080", "Info for several targets"),
87 (
88 "proc kill :3000 node",
89 "Kill port 3000 and the processes named node",
90 ),
91 (
92 "proc kill serve --cmd",
93 "Kill processes with 'serve' in their command line",
94 ),
95 (
96 "proc kill 1234 5678 :8080",
97 "Kill two PIDs and whatever is on 8080",
98 ),
99 ("proc stop :3000 :8080", "Stop several targets gracefully"),
100 ],
101 ),
102 (
103 "Watch (Real-Time)",
104 &[
105 ("proc watch", "Watch all processes (alias: proc top)"),
106 ("proc watch node", "Watch the processes named node"),
107 ("proc watch :3000", "Watch process on port 3000"),
108 ("proc watch :3000 :8080 node", "Watch several targets"),
109 ("proc watch -n 1 --sort mem", "1s refresh, sorted by memory"),
110 (
111 "proc watch --in . --by node",
112 "Node processes in current directory",
113 ),
114 ],
115 ),
116 (
117 "Freeze/Thaw",
118 &[
119 (
120 "proc freeze node",
121 "Pause the processes named node (SIGSTOP)",
122 ),
123 ("proc freeze :3000 --yes", "Freeze by port, skip confirm"),
124 ("proc thaw node", "Resume frozen processes (SIGCONT)"),
125 ],
126 ),
127 (
128 "Free Ports",
129 &[
130 ("proc free :3000", "Kill process, verify port freed"),
131 ("proc free :3000 :8080 --yes", "Free several ports"),
132 ],
133 ),
134 (
135 "Diagnostics",
136 &[
137 ("proc why :3000", "Why is port 3000 busy? (ancestry)"),
138 ("proc orphans", "Find orphaned processes"),
139 ("proc orphans --kill", "Find and kill orphans"),
140 ],
141 ),
142 (
143 "Wait",
144 &[
145 ("proc wait node", "Wait until node processes exit"),
146 ("proc wait 1234 5678", "Wait until both exit"),
147 (
148 "proc wait :3000 --timeout 60",
149 "Wait up to 60s for port to free",
150 ),
151 ("proc wait node -n 10 -q", "Check every 10s, quiet mode"),
152 ],
153 ),
154 (
155 "Other",
156 &[
157 ("proc ports", "List all listening TCP ports"),
158 ("proc tree --min-cpu 5", "Process tree filtered by CPU"),
159 ("proc stuck", "Find hung processes"),
160 (
161 "proc unstick --force",
162 "Recover or terminate stuck processes",
163 ),
164 (
165 "proc stop nginx --signal USR1",
166 "Send a different first signal (force-killed after --timeout if it lives)",
167 ),
168 ],
169 ),
170];
171
172struct Section {
175 header: &'static str,
176 rows: &'static [(&'static str, &'static str)],
177}
178
179const TARGETS: Section = Section {
180 header: "Targets:",
181 rows: &[
182 (":3000", "A port"),
183 ("1234", "A PID"),
184 ("node", "A name"),
185 (":3000 node", "Several: separate arguments"),
186 ],
187};
188
189const NAMES: Section = Section {
190 header: "Names:",
191 rows: &[
192 (
193 "node",
194 "The process name, the file name of its executable or of argv[0],",
195 ),
196 (
197 "",
198 "ignoring case (like pkill -x); 'node' is not 'Claude Helper'",
199 ),
200 (
201 "--cmd",
202 "Any part of the name or the full command line (like pkill -f)",
203 ),
204 ("./server", "Not a name: use --cmd, or proc for <file>"),
205 (
206 "node*",
207 "Not expanded by proc, but by your shell if unquoted: use --cmd",
208 ),
209 ],
210};
211
212const NOTES: Section = Section {
213 header: "Notes:",
214 rows: &[
215 (
216 ":3000,:8080",
217 "Deprecated, to be removed; prints a note on stderr",
218 ),
219 ("", "(not with --json)"),
220 (
221 "--in DIR",
222 "Takes the next word as DIR unless it starts with '-':",
223 ),
224 ("", "put it after the targets, or write --in=DIR"),
225 ],
226};
227
228const REPOSITORY: &str = "https://github.com/yazeed/proc-cli";
229
230fn render_sections(sections: &[&Section]) -> String {
233 let literal_width = sections
234 .iter()
235 .flat_map(|section| section.rows.iter())
236 .map(|(literal, _)| literal.chars().count())
237 .max()
238 .unwrap_or(0);
239 let (header, literal) = (STYLES.get_header(), STYLES.get_literal());
240 let mut out = String::new();
241 for section in sections {
242 out.push_str(&format!("{header}{}{header:#}\n", section.header));
243 for (text, meaning) in section.rows {
244 let pad = " ".repeat(literal_width - text.chars().count());
245 if text.is_empty() {
246 out.push_str(&format!(" {pad} {meaning}\n"));
247 } else {
248 out.push_str(&format!(" {literal}{text}{literal:#}{pad} {meaning}\n"));
249 }
250 }
251 out.push('\n');
252 }
253 out
254}
255
256fn render_examples() -> String {
259 let width = EXAMPLES
260 .iter()
261 .flat_map(|(_, rows)| rows.iter())
262 .map(|(command, _)| command.chars().count())
263 .max()
264 .unwrap_or(0);
265 let (header, literal) = (STYLES.get_header(), STYLES.get_literal());
266 let mut out = format!("{header}Examples:{header:#}\n");
267 for (label, rows) in EXAMPLES {
268 out.push_str(&format!("\n {label}\n"));
269 for (command, meaning) in *rows {
270 let pad = " ".repeat(width - command.chars().count());
271 out.push_str(&format!(
272 " {literal}{command}{literal:#}{pad} {meaning}\n"
273 ));
274 }
275 }
276 out
277}
278
279fn more(what: &str) -> String {
281 let header = STYLES.get_header();
282 format!("{header}More:{header:#} {what}")
283}
284
285fn short_footer() -> StyledStr {
287 let mut out = String::from("\n");
288 out.push_str(&render_sections(&[&TARGETS]));
289 out.push_str(&more(&format!("proc --help for examples, or {REPOSITORY}")));
290 StyledStr::from(out)
291}
292
293fn long_footer() -> StyledStr {
295 let mut out = render_examples();
296 out.push('\n');
297 out.push_str(&render_sections(&[&TARGETS, &NAMES, &NOTES]));
298 out.push_str(&more(REPOSITORY));
299 StyledStr::from(out)
300}
301
302#[derive(Parser)]
304#[command(name = "proc")]
305#[command(author, version = VERSION_INFO, about, long_about = None)]
306#[command(propagate_version = true)]
307#[command(styles = STYLES)]
308#[command(after_help = short_footer())]
309#[command(after_long_help = long_footer())]
310pub struct Cli {
311 #[command(subcommand)]
313 pub command: Option<Commands>,
314}
315
316#[derive(Subcommand)]
318pub enum Commands {
319 #[command(visible_alias = ":")]
321 On(OnCommand),
322
323 #[command(visible_alias = "f")]
325 For(ForCommand),
326
327 #[command(visible_alias = "b")]
329 By(ByCommand),
330
331 In(InCommand),
333
334 #[command(visible_aliases = ["l", "ps"])]
336 List(ListCommand),
337
338 #[command(visible_aliases = ["i", "inspect"])]
340 Info(InfoCommand),
341
342 #[command(visible_alias = "p")]
344 Ports(PortsCommand),
345
346 #[command(visible_alias = "k")]
348 Kill(KillCommand),
349
350 #[command(visible_alias = "s")]
352 Stop(StopCommand),
353
354 #[command(visible_alias = "t")]
356 Tree(TreeCommand),
357
358 #[command(visible_aliases = ["w", "top"])]
360 Watch(WatchCommand),
361
362 #[command(visible_alias = "x")]
364 Stuck(StuckCommand),
365
366 #[command(visible_alias = "u")]
368 Unstick(UnstickCommand),
369
370 Freeze(FreezeCommand),
372
373 Thaw(ThawCommand),
375
376 #[command(visible_alias = "o")]
378 Orphans(OrphansCommand),
379
380 Why(WhyCommand),
382
383 Free(FreeCommand),
385
386 Wait(WaitCommand),
388
389 #[command(hide = true)]
391 Completions {
392 #[arg(value_enum)]
394 shell: Shell,
395 },
396
397 #[command(hide = true)]
399 Manpage,
400}