Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
146 changes: 145 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ clap_complete = "4.5"
clap_complete_nushell = "4.5"
rand = "0.8"
serde = { version = "1.0.225", features = ["derive"] }
serde_json = "1.0.145"
serde_json = { version = "1.0.145", features = ["preserve_order"] }
sha2 = "0.10"
sha1 = "0.10"
md-5 = "0.10"
Expand Down Expand Up @@ -52,6 +52,7 @@ cron = "0.12.1"
chrono = "0.4.42"
bcrypt = "0.16"
jsonwebtoken = "9.3"
sysinfo = "0.39.0"

# The profile that 'dist' will build with
[profile.dist]
Expand Down
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,12 @@ After setting up completions, restart your shell or source your configuration fi
│ │ └── describe
│ ├── http - HTTP utilities
│ │ └── status
│ ├── proc - Process and port utilities
│ │ ├── list
│ │ ├── name
│ │ ├── pid
│ │ ├── port
│ │ └── ports
│ ├── serve - Local HTTP file server
│ └── qr - Generate QR codes
├── Color & Design
Expand Down Expand Up @@ -421,6 +427,33 @@ ut http status 404
ut http status # List all status codes
```

#### `proc`
Query running processes and find which process owns a given TCP port.
- List all running processes, optionally filtered by name
- Look up a specific process by PID
- Identify which process is listening on a port
- List all open TCP listening ports with their owning process
- Cross-platform: Linux (reads `/proc/net/tcp` directly), macOS (uses `lsof`), Windows (uses `netstat`)
- Processes owned by other users may require elevated privileges

```bash
# List all running processes
ut proc list

# Filter by name (case-insensitive substring match)
ut proc list --name node
ut proc name nginx

# Inspect a process by PID
ut proc pid 1234

# Find what's listening on port 3000
ut proc port 3000

# List every open TCP listening port with its owning process
ut proc ports
```

#### `serve`
Start a local HTTP file server.
- Customizable host and port
Expand Down
1 change: 1 addition & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ fn main() -> anyhow::Result<()> {
(tools::jwt::JwtTool, "jwt",),
(tools::lorem::LoremTool, "lorem",),
(tools::pp::PrettyPrintTool, "pretty-print", "pp"),
(tools::proc::ProcTool, "proc",),
(tools::qr::QRTool, "qr",),
(tools::random::RandomTool, "random",),
(tools::regex::RegexTool, "regex",),
Expand Down
41 changes: 41 additions & 0 deletions src/tool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use serde_json::Value;
use std::io::{self, Write};
use tabled::{
Table, Tabled,
builder::Builder,
settings::{Padding, Remove, Style, object::Rows},
};

Expand All @@ -21,6 +22,9 @@ pub trait Tool {
pub enum Output {
Bytes(Vec<u8>),
JsonValue(serde_json::Value),
/// An array of homogeneous objects rendered as a columnar table in human
/// mode and as a JSON array when `--json` is passed.
Table(serde_json::Value),
Text(String),
}

Expand All @@ -40,6 +44,15 @@ impl Output {
println!("{}", value_to_string(value));
}
}
Output::Table(value) => {
if structured {
println!("{}", value);
} else if let Value::Array(rows) = value {
println!("{}", render_object_table(rows));
} else {
println!("{}", value_to_string(value));
}
}
Output::Text(text) => {
println!("{}", text);
}
Expand All @@ -49,6 +62,34 @@ impl Output {
}
}

/// Renders a slice of JSON objects as a columnar table.
/// Column order follows the key insertion order of the first row.
fn render_object_table(rows: &[Value]) -> String {
let Some(Value::Object(first)) = rows.first() else {
return String::new();
};

let headers: Vec<String> = first.keys().cloned().collect();
let mut builder = Builder::default();
builder.push_record(headers.iter().map(String::as_str));

for row in rows {
if let Value::Object(obj) = row {
let vals: Vec<String> = headers
.iter()
.map(|h| value_to_string(obj.get(h).unwrap_or(&Value::Null)))
.collect();
builder.push_record(vals.iter().map(String::as_str));
}
}

let mut table = builder.build();
table
.with(Style::empty())
.with(Padding::new(0, 2, 0, 0));
table.to_string()
}

fn value_to_string(value: &Value) -> String {
match value {
Value::Object(o) => {
Expand Down
1 change: 1 addition & 0 deletions src/tools/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ pub mod json;
pub mod jwt;
pub mod lorem;
pub mod pp;
pub mod proc;
pub mod qr;
pub mod random;
pub mod regex;
Expand Down
Loading
Loading