use crate::commands::{
ByCommand, ForCommand, FreeCommand, FreezeCommand, InCommand, InfoCommand, KillCommand,
ListCommand, OnCommand, OrphansCommand, PortsCommand, StopCommand, StuckCommand, ThawCommand,
TreeCommand, UnstickCommand, WaitCommand, WatchCommand, WhyCommand,
};
use clap::builder::styling::Styles;
use clap::builder::StyledStr;
use clap::{Parser, Subcommand};
use clap_complete::Shell;
const VERSION_INFO: &str = concat!(
env!("PROC_VERSION"),
"\nhttps://github.com/yazeed/proc-cli",
"\nLicense: MIT"
);
pub const STYLES: Styles = Styles::styled();
const EXAMPLES: &[(&str, &[(&str, &str)])] = &[
(
"Port/Process Lookup",
&[
("proc on :3000", "What's on port 3000?"),
("proc on :3000 :8080", "What's on several ports?"),
(
"proc on node",
"What ports are the processes named node using?",
),
],
),
(
"Find by File",
&[
(
"proc for ./server",
"Processes running this program (or holding it open)",
),
(
"proc for /usr/bin/node",
"Processes running this executable",
),
(
"proc for app.log",
"What has this file open? (not searched on Windows)",
),
],
),
(
"Filter by Name",
&[
("proc by node", "Processes named 'node' (the whole name)"),
(
"proc by node --cmd",
"Anything with 'node' in its name or command line",
),
("proc by node --in .", "Node processes in current directory"),
("proc by node --min-cpu 5", "Node processes using >5% CPU"),
],
),
(
"Filter by Directory",
&[
("proc in .", "Processes in current directory"),
("proc in . --by node", "Node processes in cwd"),
],
),
(
"List All",
&[
("proc list", "All processes"),
("proc list --min-cpu 10", "Processes using >10% CPU"),
],
),
(
"Info/Kill/Stop (multi-target)",
&[
("proc info :3000 :8080", "Info for several targets"),
(
"proc kill :3000 node",
"Kill port 3000 and the processes named node",
),
(
"proc kill serve --cmd",
"Kill processes with 'serve' in their command line",
),
(
"proc kill 1234 5678 :8080",
"Kill two PIDs and whatever is on 8080",
),
("proc stop :3000 :8080", "Stop several targets gracefully"),
],
),
(
"Watch (Real-Time)",
&[
("proc watch", "Watch all processes (alias: proc top)"),
("proc watch node", "Watch the processes named node"),
("proc watch :3000", "Watch process on port 3000"),
("proc watch :3000 :8080 node", "Watch several targets"),
("proc watch -n 1 --sort mem", "1s refresh, sorted by memory"),
(
"proc watch --in . --by node",
"Node processes in current directory",
),
],
),
(
"Freeze/Thaw",
&[
(
"proc freeze node",
"Pause the processes named node (SIGSTOP)",
),
("proc freeze :3000 --yes", "Freeze by port, skip confirm"),
("proc thaw node", "Resume frozen processes (SIGCONT)"),
],
),
(
"Free Ports",
&[
("proc free :3000", "Kill process, verify port freed"),
("proc free :3000 :8080 --yes", "Free several ports"),
],
),
(
"Diagnostics",
&[
("proc why :3000", "Why is port 3000 busy? (ancestry)"),
("proc orphans", "Find orphaned processes"),
("proc orphans --kill", "Find and kill orphans"),
],
),
(
"Wait",
&[
("proc wait node", "Wait until node processes exit"),
("proc wait 1234 5678", "Wait until both exit"),
(
"proc wait :3000 --timeout 60",
"Wait up to 60s for port to free",
),
("proc wait node -n 10 -q", "Check every 10s, quiet mode"),
],
),
(
"Other",
&[
("proc ports", "List all listening TCP ports"),
("proc tree --min-cpu 5", "Process tree filtered by CPU"),
("proc stuck", "Find hung processes"),
(
"proc unstick --force",
"Recover or terminate stuck processes",
),
(
"proc stop nginx --signal USR1",
"Send a different first signal (force-killed after --timeout if it lives)",
),
],
),
];
struct Section {
header: &'static str,
rows: &'static [(&'static str, &'static str)],
}
const TARGETS: Section = Section {
header: "Targets:",
rows: &[
(":3000", "A port"),
("1234", "A PID"),
("node", "A name"),
(":3000 node", "Several: separate arguments"),
],
};
const NAMES: Section = Section {
header: "Names:",
rows: &[
(
"node",
"The process name, the file name of its executable or of argv[0],",
),
(
"",
"ignoring case (like pkill -x); 'node' is not 'Claude Helper'",
),
(
"--cmd",
"Any part of the name or the full command line (like pkill -f)",
),
("./server", "Not a name: use --cmd, or proc for <file>"),
(
"node*",
"Not expanded by proc, but by your shell if unquoted: use --cmd",
),
],
};
const NOTES: Section = Section {
header: "Notes:",
rows: &[
(
":3000,:8080",
"Deprecated, to be removed; prints a note on stderr",
),
("", "(not with --json)"),
(
"--in DIR",
"Takes the next word as DIR unless it starts with '-':",
),
("", "put it after the targets, or write --in=DIR"),
],
};
const REPOSITORY: &str = "https://github.com/yazeed/proc-cli";
fn render_sections(sections: &[&Section]) -> String {
let literal_width = sections
.iter()
.flat_map(|section| section.rows.iter())
.map(|(literal, _)| literal.chars().count())
.max()
.unwrap_or(0);
let (header, literal) = (STYLES.get_header(), STYLES.get_literal());
let mut out = String::new();
for section in sections {
out.push_str(&format!("{header}{}{header:#}\n", section.header));
for (text, meaning) in section.rows {
let pad = " ".repeat(literal_width - text.chars().count());
if text.is_empty() {
out.push_str(&format!(" {pad} {meaning}\n"));
} else {
out.push_str(&format!(" {literal}{text}{literal:#}{pad} {meaning}\n"));
}
}
out.push('\n');
}
out
}
fn render_examples() -> String {
let width = EXAMPLES
.iter()
.flat_map(|(_, rows)| rows.iter())
.map(|(command, _)| command.chars().count())
.max()
.unwrap_or(0);
let (header, literal) = (STYLES.get_header(), STYLES.get_literal());
let mut out = format!("{header}Examples:{header:#}\n");
for (label, rows) in EXAMPLES {
out.push_str(&format!("\n {label}\n"));
for (command, meaning) in *rows {
let pad = " ".repeat(width - command.chars().count());
out.push_str(&format!(
" {literal}{command}{literal:#}{pad} {meaning}\n"
));
}
}
out
}
fn more(what: &str) -> String {
let header = STYLES.get_header();
format!("{header}More:{header:#} {what}")
}
fn short_footer() -> StyledStr {
let mut out = String::from("\n");
out.push_str(&render_sections(&[&TARGETS]));
out.push_str(&more(&format!("proc --help for examples, or {REPOSITORY}")));
StyledStr::from(out)
}
fn long_footer() -> StyledStr {
let mut out = render_examples();
out.push('\n');
out.push_str(&render_sections(&[&TARGETS, &NAMES, &NOTES]));
out.push_str(&more(REPOSITORY));
StyledStr::from(out)
}
#[derive(Parser)]
#[command(name = "proc")]
#[command(author, version = VERSION_INFO, about, long_about = None)]
#[command(propagate_version = true)]
#[command(styles = STYLES)]
#[command(after_help = short_footer())]
#[command(after_long_help = long_footer())]
pub struct Cli {
#[command(subcommand)]
pub command: Option<Commands>,
}
#[derive(Subcommand)]
pub enum Commands {
#[command(visible_alias = ":")]
On(OnCommand),
#[command(visible_alias = "f")]
For(ForCommand),
#[command(visible_alias = "b")]
By(ByCommand),
In(InCommand),
#[command(visible_aliases = ["l", "ps"])]
List(ListCommand),
#[command(visible_aliases = ["i", "inspect"])]
Info(InfoCommand),
#[command(visible_alias = "p")]
Ports(PortsCommand),
#[command(visible_alias = "k")]
Kill(KillCommand),
#[command(visible_alias = "s")]
Stop(StopCommand),
#[command(visible_alias = "t")]
Tree(TreeCommand),
#[command(visible_aliases = ["w", "top"])]
Watch(WatchCommand),
#[command(visible_alias = "x")]
Stuck(StuckCommand),
#[command(visible_alias = "u")]
Unstick(UnstickCommand),
Freeze(FreezeCommand),
Thaw(ThawCommand),
#[command(visible_alias = "o")]
Orphans(OrphansCommand),
Why(WhyCommand),
Free(FreeCommand),
Wait(WaitCommand),
#[command(hide = true)]
Completions {
#[arg(value_enum)]
shell: Shell,
},
#[command(hide = true)]
Manpage,
}