All widgets
Ports
Comes with Chopstick
The dev servers listening on your Mac's ports (node, python, ssh tunnels), to open or stop; kill whatever is on any port by number.
Already installed? It's in your widget library.










Type it in the notch's search
- Kill port port
- Show ports
What it runs
A widget is a folder of plain files. Here is all of its code.
widget.json
{
"id": "ports",
"name": "Ports",
"icon": "icon.png",
"description": "The dev servers listening on your Mac's ports (node, python, ssh tunnels), to open or stop; kill whatever is on any port by number.",
"anytime": true,
"initial": "list",
"watch": [
{
"script": "ports.sh",
"every": 5,
"as": "ports"
}
],
"commands": {
"kill": {
"description": "Stop whatever is listening on a port.",
"phrase": "Kill port {port}",
"inputs": {
"port": {
"type": "number",
"description": "The port number, e.g. 3000"
}
},
"preview": {
"state": "lookup",
"run": "who.sh"
},
"do": [
{
"set": {
"port": "{{input.port}}"
}
},
{
"set": {
"error": ""
}
},
{
"run": "kill.sh",
"then": "killed",
"else": "failed"
},
{
"refresh": "ports"
}
]
},
"show": {
"description": "Show the dev servers listening on ports.",
"phrase": "Show ports",
"do": [
{
"go": "list"
}
]
}
},
"states": {
"list": {
"compact": {
"leading": {
"symbol": "network",
"tint": "cyan"
},
"trailing": {
"text": "{{ports.label | …}}"
}
},
"expanded": {
"size": "regular",
"header": {
"symbol": "network",
"tint": "cyan",
"title": "Ports"
},
"blocks": [
{
"list": {
"from": "ports.ports",
"limit": 8,
"symbol": "server.rack",
"title": "localhost:{{item.port}}",
"subtitle": "{{item.where}}",
"do": [
{
"set": {
"port": "{{item.port}}",
"where": "{{item.where}}",
"url": "{{item.url}}",
"error": ""
}
},
{
"go": "port"
}
]
}
},
{
"note": "{{ports.none}}"
},
{
"input": {
"placeholder": "Kill a port by number, e.g. 3000",
"do": [
{
"command": "kill",
"with": {
"port": "{{item.text}}"
}
}
]
}
}
]
}
},
"port": {
"task": true,
"open": true,
"compact": {
"leading": {
"symbol": "network",
"tint": "cyan"
},
"trailing": {
"text": ":{{port}}"
}
},
"expanded": {
"size": "regular",
"header": {
"symbol": "server.rack",
"tint": "cyan",
"title": "localhost:{{port}}"
},
"blocks": [
{
"text": "{{where}}",
"style": "caption",
"truncate": "middle"
},
{
"row": [
{
"button": {
"symbol": "safari",
"label": "Open",
"do": [
{
"open_url": "{{url}}"
},
"dismiss"
]
}
},
{
"button": {
"symbol": "xmark.octagon",
"label": "Kill",
"tint": "red",
"do": [
{
"set": {
"error": ""
}
},
{
"run": "kill.sh",
"then": "killed",
"else": "failed"
},
{
"refresh": "ports"
}
]
}
},
"spacer",
{
"button": {
"label": "Back",
"style": "plain",
"do": [
{
"go": "list"
}
]
}
}
],
"spacing": "tight",
"align": "center"
}
]
},
"keys": [
{
"key": "return",
"label": "Open",
"do": [
{
"open_url": "{{url}}"
},
"dismiss"
]
},
{
"key": "cmd+delete",
"label": "Kill",
"do": [
{
"set": {
"error": ""
}
},
{
"run": "kill.sh",
"then": "killed",
"else": "failed"
},
{
"refresh": "ports"
}
]
},
{
"key": "esc",
"do": [
{
"go": "list"
}
]
}
]
},
"lookup": {
"task": true,
"open": true,
"compact": {
"leading": {
"symbol": "xmark.octagon",
"tint": "red"
},
"trailing": {
"text": ":{{input.port}}"
}
},
"expanded": {
"size": "regular",
"header": {
"symbol": "xmark.octagon",
"tint": "red",
"title": "Kill port {{input.port}}"
},
"blocks": [
{
"text": "{{result.where}}",
"style": "body"
},
{
"note": "{{error}}"
},
{
"note": "Return stops it."
}
]
}
},
"killed": {
"task": true,
"dismiss_after": 3,
"compact": {
"leading": {
"symbol": "checkmark.circle.fill",
"tint": "green"
},
"trailing": {
"text": ":{{result.port}} stopped"
}
},
"expanded": {
"size": "compact",
"header": {
"symbol": "checkmark.circle.fill",
"tint": "green",
"title": "Stopped port {{result.port}}"
},
"blocks": [
{
"text": "{{result.where}}",
"style": "caption"
}
]
}
},
"failed": {
"task": true,
"open": true,
"dismiss_after": 5,
"compact": {
"leading": {
"symbol": "exclamationmark.triangle",
"tint": "orange"
}
},
"expanded": {
"size": "compact",
"header": {
"symbol": "exclamationmark.triangle",
"tint": "orange",
"title": "Port {{port}}"
},
"blocks": [
{
"text": "{{error | Couldn't stop it}}",
"style": "body",
"lines": 3
}
]
}
}
},
"preview": {
"states": [
"list",
"port",
"lookup",
"killed",
"failed"
],
"data": {
"ports": {
"count": 3,
"label": "3 ports",
"ports": [
{
"port": 3000,
"pid": 4120,
"name": "node",
"where": "node · ~/code/shop",
"url": "http://localhost:3000"
},
{
"port": 5432,
"pid": 812,
"name": "postgres",
"where": "postgres · /opt/homebrew/var/postgresql@16",
"url": "http://localhost:5432"
},
{
"port": 8000,
"pid": 5210,
"name": "Python",
"where": "Python · ~/code/api",
"url": "http://localhost:8000"
}
],
"none": ""
},
"port": 3000,
"where": "node · ~/code/shop",
"url": "http://localhost:3000",
"input": {
"port": 3000
},
"result": {
"port": 3000,
"pid": 4120,
"name": "node",
"where": "node · ~/code/shop"
},
"error": "Nothing is listening on port 3000"
}
}
}
kill.sh
#!/bin/sh
# Stops whatever listens on $CHOPSTICK_PORT: asks it to quit, and forces it after 3 seconds.
# Prints who.sh's description of what was stopped.
PORT=$CHOPSTICK_PORT
[ "$PORT" -gt 0 ] 2>/dev/null || { echo "\"$PORT\" isn't a port" >&2; exit 1; }
WHO=$(sh who.sh) || exit 1
PIDS=$(lsof -nP -iTCP:"$PORT" -sTCP:LISTEN -t 2>/dev/null | sort -u)
kill $PIDS 2>/dev/null || { echo "Couldn't stop port $PORT's process (it may belong to another user)" >&2; exit 1; }
for _ in 1 2 3 4 5 6; do
lsof -nP -iTCP:"$PORT" -sTCP:LISTEN -t >/dev/null 2>&1 || { echo "$WHO"; exit 0; }
sleep 0.5
done
kill -9 $PIDS 2>/dev/null
sleep 0.3
lsof -nP -iTCP:"$PORT" -sTCP:LISTEN -t >/dev/null 2>&1 && { echo "Port $PORT is still in use" >&2; exit 1; }
echo "$WHO"
ports.sh
#!/bin/sh
# Your listening TCP ports that look like dev servers (node, python, an ssh tunnel, a Homebrew
# service), as one JSON object. Apps (in /Applications, their helpers in Library) and macOS's own
# services are left out: they listen on ports of their own all the time.
LSOF=$(lsof -nP -iTCP -sTCP:LISTEN -F pn 2>/dev/null)
PIDS=$(printf '%s\n' "$LSOF" | sed -n 's/^p//p' | paste -sd, -)
[ -n "$PIDS" ] || { echo '{"count":0,"label":"","none":"No dev servers are listening.","ports":[]}'; exit 0; }
PATHS=$(ps -o pid=,comm= -p "$PIDS" 2>/dev/null)
DIRS=$(lsof -a -d cwd -p "$PIDS" -Fn 2>/dev/null)
{ printf '%s\n' "$PATHS" | sed 's/^ */path /'; printf '%s\n' "$DIRS" | sed 's/^/cwd /'; printf '%s\n' "$LSOF" | sed 's/^/port /'; } |
awk -v home="$HOME" '
function esc(s) { gsub(/["\\]/, "", s); return s }
$1 == "path" { pid = $2; sub(/^path [0-9]+ /, ""); path[pid] = $0; next }
$1 == "cwd" { s = substr($0, 5); if (s ~ /^p/) pid = substr(s, 2); else if (s ~ /^n/) { d = substr(s, 2); if (index(d, home) == 1) d = "~" substr(d, length(home) + 1); dir[pid] = d }; next }
$1 == "port" {
s = substr($0, 6)
if (s ~ /^p/) { pid = substr(s, 2); next }
if (s !~ /^n/) next
p = path[pid]
if (p == "" || index(p, "/Applications/") == 1 || index(p, home "/Applications/") == 1 || index(p, home "/Library/") == 1 ||
index(p, "/Library/Application Support/") == 1 || p ~ /^\/(System|usr\/libexec|usr\/sbin|sbin)\//) next
n = substr(s, 2); port = n; sub(/.*:/, "", port)
if (seen[pid ":" port]++) next
name = p; sub(/.*\//, "", name)
d = (pid in dir && dir[pid] != "/") ? " · " dir[pid] : ""
out = out sprintf("%s{\"port\":%d,\"pid\":%d,\"name\":\"%s\",\"where\":\"%s%s\",\"url\":\"http://localhost:%d\"}", (count++ ? "," : ""), port, pid, esc(name), esc(name), esc(d), port)
}
END { printf "{\"count\":%d,\"label\":\"%s\",\"none\":\"%s\",\"ports\":[%s]}\n", count, count ? count " " (count == 1 ? "port" : "ports") : "", count ? "" : "No dev servers are listening.", out }'
who.sh
#!/bin/sh
# What listens on $CHOPSTICK_PORT: {"port", "pid", "name", "where"}, or fails with nothing there.
PORT=$CHOPSTICK_PORT
PID=$(lsof -nP -iTCP:"$PORT" -sTCP:LISTEN -t 2>/dev/null | head -1)
[ -n "$PID" ] || { echo "Nothing is listening on port $PORT" >&2; exit 1; }
NAME=$(ps -o comm= -p "$PID" | sed 's|.*/||; s/["\\]//g')
DIR=$(lsof -a -d cwd -p "$PID" -Fn 2>/dev/null | sed -n 's/^n//p' | sed "s|^$HOME|~|; s/[\"\\\\]//g")
WHERE=$NAME; [ -n "$DIR" ] && [ "$DIR" != "/" ] && WHERE="$NAME · $DIR"
printf '{"port":%d,"pid":%d,"name":"%s","where":"%s"}\n' "$PORT" "$PID" "$NAME" "$WHERE"