lazyports

command module
v0.0.0-...-539db09 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 16, 2026 License: MIT Imports: 7 Imported by: 0

README ΒΆ

πŸ”Œ lazyports

Visual TUI port manager for Mac and Linux. See what's listening, kill it with one key, tag it by project β€” all from a clean terminal UI. Inspired by lazygit and pgflow.

Go macOS Linux Tests


What it solves

You start a project on :3000, close your terminal, come back tomorrow, and the port is still busy. Or you have three Next.js apps running and want to kill the one on :3000 but not the one on :3030, and you don't remember which PID belongs to which. lazyports answers in one terminal view:

 πŸ”Œ lazyports                                 16 listening  β€’  6 tagged  β€’  view: flat  β€’  next: 8s
  Active Ports (16)                                          Port Details
 β”Œβ”€ PORT  PROCESS    PID    STATE  CAT   PROJECT         β”Œβ”€ :3000  ● LISTEN  fe
 β”‚  3000  node       70595  LISTEN  fe    Amatista-eco   β”‚
 β”‚  3030  node       24986  LISTEN  fe    web            β”‚  Project  Amatista-eco
 β”‚  4000  node       69831  LISTEN  fe    Patines-eco    β”‚  Process  node
 β”‚  5000  ControlCe  676    LISTEN  sys   macOS-control β”‚  PID      70595
 β”‚  5173  node       70534  LISTEN  fe    Amatista-fe    β”‚  User     max
 β”‚  5432  postgres   2826   LISTEN  db    postgres       β”‚
 β”‚  6379  redis      2821   LISTEN  cache redis          β”‚  Tag      frontend-patines  ⚠ stale
 β”‚  11434 ollama     78424  LISTEN  llm   ollama         β”‚            tag doesn't match project
 β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
  x kill   m mark   X kill-marked   t tag   f filter   c copy   tab view   r refresh   q quit

Why lazyports

  • One binary, zero runtime β€” pure Go with Bubble Tea, no Python, no Node, no language runtime to install.
  • Auto-detects your projects β€” reads the process cwd via lsof and infers which project each port belongs to. No config file, no manual tagging required.
  • Smart categorization β€” heuristically classifies each port as frontend, backend, database, cache, LLM, tunnel, dev tools, or system. With colors.
  • Three view modes β€” tab cycles: flat, by-category, by-project.
  • Multi-select + multi-kill β€” mark several ports, then kill them all in one go with a confirmation that lists every target.
  • Stale tag detection β€” when your manual tag (e.g. frontend-patines) no longer matches the auto-detected project (Amatista-eco), it shows a ⚠ stale warning so you can fix it.
  • Crisp performance β€” first scan warms the cache, every subsequent scan is ~130ms regardless of how many ports you have. Refresh interval adapts to scan speed.
  • CLI mode β€” pipe ports as JSON to jq, grep, or your editor.

Install

Quick install (one binary)

You need Go 1.22+ to build (only required once):

git clone https://github.com/ander0code/lazyports.git ~/projects/lazyports
cd ~/projects/lazyports
bash install.sh

install.sh will:

  1. Check for go (complain if missing).
  2. go build with -ldflags="-s -w" (strips debug info β†’ small binary).
  3. Drop the binary at ~/.local/bin/lazyports and chmod +x it.
  4. Print a hint if ~/.local/bin isn't on your PATH.

If ~/.local/bin isn't on your PATH yet, add this to ~/.zshrc (or ~/.bashrc):

export PATH="$HOME/.local/bin:$PATH"

Then just:

lazyports
From source (no install)
git clone https://github.com/ander0code/lazyports.git
cd lazyports
go run .                  # launches the TUI
go run . --scan           # quick text dump
Homebrew (planned)
brew install ander0code/tap/lazyports

Usage

lazyports                  # launch the TUI
lazyports --scan           # print listening ports (text)
lazyports --scan --json    # print as JSON for piping
lazyports --version
lazyports --help
Inside the TUI
Key Action
↑ ↓ / j k move cursor
g / G jump to first / last
x / enter kill selected port (or all marked if any)
t tag / edit / remove project tag
m mark / unmark for multi-kill
M (shift) mark all visible (filtered) ports
X (shift) open multi-kill modal showing every marked port
f / / open filter input
c copy kill -9 $(lsof -ti:PORT) to clipboard
r force a refresh now
tab cycle view: flat β†’ by-category β†’ by-project
space in flat: mark; in grouped: collapse/expand group
esc clear filter, or clear all marks
q / ctrl+c quit
Modal kill confirmation
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃                                                              ┃
┃     ☠  Kill port :3000 ?                                     ┃
┃                                                              ┃
┃     Process  node                                            ┃
┃     Project  Amatista-eco                                    ┃
┃     PID  70595                                               ┃
┃                                                              ┃
┃   Sends SIGKILL to the listening process.                    ┃
┃                                                              ┃
┃   ──────────────────────────────────────────────────────     ┃
┃                                                              ┃
┃     y/enter   yes, kill it     n/esc   cancel               ┃
┃                                                              ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
Multi-kill modal

When you've marked several ports with m:

┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃     ☠  Kill 4 marked ports ?                                 ┃
┃                                                              ┃
┃      fe  :3000  node        Amatista-eco    frontend-…       ┃
┃      fe  :3030  node        web                              ┃
┃      fe  :4000  node        Patines-eco                      ┃
┃      fe  :5173  node        Amatista-fe     frontend-…       ┃
┃                                                              ┃
┃   Sends SIGKILL to 4 processes.                              ┃
┃                                                              ┃
┃   ──────────────────────────────────────────────────────     ┃
┃                                                              ┃
┃     y/enter   yes, kill them all    n/esc   cancel          ┃
┃                                                              ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
CLI mode (no TUI)

For scripts and your editor:

$ lazyports --scan
8 listening ports
  :3000  next-server    pid=70595  max
  :5432  postgres       pid=2826   max
  :6379  redis          pid=2821   max
  :8000  Python         pid=3392   max
  :5173  node           pid=70534  max
  :11434 ollama         pid=78424  max
  ...

$ lazyports --scan --json | jq '.[] | select(.tag == "frontend")'
[
  { "port": 3000, "process": "node", "tag": "frontend", ... }
]

How it knows where each port comes from

For every listening port, lazyports:

  1. Runs lsof -nP -iTCP -sTCP:LISTEN to get the basics (port, PID, command, user).
  2. Runs lsof -d cwd -F pn once to get every process's cwd (about 100ms on macOS, regardless of how many ports you have).
  3. Walks the cwd to extract the meaningful folder name β€” skipping node_modules, version numbers (3.14.5), system paths (/System, /opt/homebrew, /Library), and umbrella folders.
  4. For SSH tunnels, extracts the alias from the command line (ssh -f -N jsoluciones-db β†’ project is jsoluciones-db).
  5. Caches the result per PID for 60s. Subsequent scans only re-run the cheap lsof call.

Categorization is heuristic but accurate for the common cases:

Process / Command Category
next-server, vite, nuxt, webpack, astro, svelte, gatsby fe (frontend)
express, fastify, nest, koa, flask, django, fastapi, uvicorn, gunicorn be (backend)
postgres, mysqld, mariadbd, mongo, cockroach db (database)
redis-server, memcached, keydb cache
ollama, llama-server, vllm llm
ssh -f -N <alias>, ssh -L … tunnel
adb, ngrok, cloudflared tools
rapportd, ControlCe, Raycast, Spotify, Google Chrome, … sys (system)
anything else other

Project tags (config)

Tags are stored in ~/.lazyports.json β€” a tiny file you can edit by hand:

{
  "tags": {
    "3000": "frontend",
    "3030": "another-frontend",
    "5432": "postgres-dev",
    "6379": "redis-cache"
  }
}

Set them in the TUI (t on a row) or edit the file directly. Reload the TUI or press r to pick up file edits. The TAG column was removed from the default view because it tends to go stale (you set frontend-patines for the Patines project, then six months later that same port is running Amatista-eco). Tags now only show in the Port Details panel, with a ⚠ stale warning when they don't match the auto-detected project.

Performance

Measured on a Mac with 16 listening ports:

Scenario Time
First scan (cold cache) ~440 ms
Subsequent scans (warm cache) ~130 ms
Refresh interval 8-20 s, adaptive
RAM ~15 MB resident
Binary size ~6 MB

The auto-refresh is smart: it skips if a scan is already in flight (lsof hung, system load spike), uses an 80 ms spinner tick that self-cancels when nothing is happening, and only shows the spinner if a scan actually takes longer than 300 ms β€” so cached scans are invisible to the user.

Build for release

Cross-platform binaries via the Makefile:

make build                      # current platform
make build-all                  # mac/linux Γ— amd64/arm64
ls dist/                        # lazyports-darwin-arm64, lazyports-linux-amd64, …

Or with plain Go:

go build -ldflags="-s -w" -o lazyports .

Architecture

main.go                 # CLI flag parsing, tea.NewProgram
internal/
  ports/
    ports.go            # lsof scanner with per-PID cache + bulk cwd fetch
    killer.go           # lsof -ti:PORT | kill -9
    category.go         # Categorize() + ProjectName() heuristics
    *_test.go           # 30+ unit tests
  tags/
    tags.go             # ~/.lazyports.json with mutex
  tui/
    tui.go              # Bubble Tea model, Update, View
    commands.go         # tea.Cmd factories (scan, kill, copy, tick)
    messages.go         # all message types
    styles.go           # lipgloss styles, palette
    *test*.go           # 24 unit tests

Why Bubble Tea: it's the framework lazygit-style tools use, it compiles to a single static binary, and Lipgloss gives us crisp layout without ANSI escape debugging. Why not Textual / Python: a single 6 MB binary is easier to ship and run than asking users to pip install two packages.

Troubleshooting

"Permission denied" killing a port on macOS Some processes are owned by other users (system daemons, root). lazyports will tell you which PID refused; if it's yours but owned by root, run the binary as root:

sudo $(which lazyports)

macOS 12+ ports not showing Grant your terminal (iTerm, Ghostty, …) Full Disk Access under System Settings β†’ Privacy & Security.

Clipboard copy doesn't work on Linux lazyports tries wl-copy (Wayland), then xclip, then xsel. Install one of them:

# Wayland
sudo apt install wl-clipboard
# X11
sudo apt install xclip

Tags not being read Tags are loaded from ~/.lazyports.json on every scan. If you edit the file by hand, press r to refresh.

Auto-detected project is wrong Project name is derived from the process's cwd. If the process is launched from a non-standard directory (e.g., ~/ or /tmp/), the project name will be β€”. Run the process from its project folder for better detection, or set a manual tag with t.

Contributing

Issues and PRs welcome. Keep the binary small, keep the dependencies minimal, keep the keys vim-like.

make test     # run all tests
go vet ./...  # static analysis
make build   # build the binary

License

MIT β€” Β© 2026 ander0code

Documentation ΒΆ

The Go Gopher

There is no documentation for this package.

Directories ΒΆ

Path Synopsis
internal
tui

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL