A personal multi-project dev juggler — run all your services from a single terminal TUI.
procs keeps each project's logs in its own pane, lets you start/stop/restart individual
processes or named groups with a single keystroke, and gives you a full attach-mode bridge
so you can type directly into any child process.
- Single static binary, zero runtime dependencies
- Detach mode (macOS/Linux): a background daemon supervises your processes; the TUI is a client you can detach from and reattach to (tmux-style). Close the terminal or drop your SSH session — your services keep running. See Detach Mode
- Terminal UI built with Bubble Tea
- Per-project log panes with scroll, filter, and rotation
- Named groups — boot the whole stack with one key
- Auto-restart on failure with configurable exponential backoff
- Attach mode: raw PTY bridge with
Esc Escto detach; OS-native paste (Cmd-V / Ctrl-Shift-V / Shift-Insert) is forwarded to the child as bracketed paste when the child enabled mode 2004. Mouse drag inside the attach pane selects → release auto-copies to the system clipboard; when the attached child enables its own mouse tracking (vim, htop, lazygit,less -R) the mouse is instead forwarded to it (tmux-style) - Mouse drag-select on the log pane → release auto-copies to system clipboard. Wheel still scrolls. Hold Option (macOS Terminal/iTerm) or Shift (most Linux terminals) to fall back to the terminal's native selection across borders/scrollbar
- Git branch display per project + in-TUI branch picker with live filter, plus
:fetch/:pull --ff-only - Active port/socket display (processes that LISTEN show their port)
- Per-process CPU% and memory (RSS) in the status bar (whole-process-tree sum; updates every 2 s). CPU can exceed 100% on multi-core workloads — matches
htopconvention. - Hotkey
Ctrl-Eopens the active config in$VISUAL/$EDITOR; on save, procs auto-reloads (added projects appear, removed projects stop, changed projects toast a "restart to apply" hint).:reloadre-reads from disk without opening an editor. - Goroutine-safe; race-detector clean
go install github.com/lpwanw/procs/cmd/procs@latestBinary lands in $(go env GOBIN) — falls back to $(go env GOPATH)/bin.
Make sure that directory is on your PATH.
git clone https://github.com/lpwanw/procs
cd procs
make install # default PREFIX=$HOME/.local → ~/.local/bin/procs (no sudo)
# system-wide install (requires sudo, shared with other users):
sudo make install PREFIX=/usr/localDownload the archive for your OS/arch from
Releases, extract, and
place procs on your PATH.
Homebrew: tap coming in a future release.
| Installed via | Command |
|---|---|
go install |
go install github.com/lpwanw/procs/cmd/procs@latest |
| source checkout | make update (runs git pull --ff-only && make install) |
| direct binary | re-download the latest release and overwrite the binary |
| Installed via | Command |
|---|---|
go install |
rm "$(command -v procs)" (typically $GOPATH/bin/procs) |
| source checkout | make uninstall (matches the PREFIX you installed with) |
| direct binary | delete the binary you placed on PATH |
Remove your config and cached logs if you don't want them around:
rm -rf ~/.config/procs ~/.cache/procs- Create
~/.config/procs/config.yml:
projects:
api:
path: ~/code/myapi
cmd: go run ./cmd/api
restart: on-failure
web:
path: ~/code/myweb
cmd: ./scripts/dev
restart: on-failure
groups:
fullstack: [api, web]- Run
procs. On macOS/Linux the first run starts a background daemon and attaches a TUI to it; all projects are listed in the sidebar.
On macOS/Linux, procs splits into a headless daemon that owns your
processes and a TUI client that connects to it over a per-config unix
socket. This means your services survive the terminal closing, an SSH
disconnect, or logout — exactly like leaving them in tmux, but with per-project
panes, named-group control, and reattach built in.
- Attach / reattach: just run
procs. If a daemon is already running for that config it reattaches (restoring live state + recent logs); otherwise it spawns one and attaches. - Detach:
:detach(or:q/Ctrl-C) leaves the TUI and keeps the daemon and all processes running. Reattach later withprocs. - Stop everything:
:shutdownfrom inside the TUI, orprocs killfrom a shell, stops the daemon and all its children.
| Command | Action |
|---|---|
procs |
Attach to the daemon, spawning it if needed |
procs status |
Print the daemon's project states (or "no daemon running") |
procs kill |
Stop the daemon and all its children |
procs kill --orphans |
Also stop processes left behind by a crashed daemon |
Per-config state lives under your cache dir (~/.cache/procs on Linux,
~/Library/Caches/procs on macOS), keyed by a hash of the config path, all
owner-only (0600):
| File | Purpose |
|---|---|
<hash>.sock |
daemon control socket |
<hash>.pid |
daemon pidfile + lock |
<hash>.daemon.log |
daemon stdout/stderr |
<hash>.children |
live child PIDs (for orphan recovery) |
The socket is restricted to your user (mode 0600 inside a 0700 directory, with
a peer-uid check), so other local users can't drive your processes.
Windows: there is no daemon — procs runs the TUI in-process (today's
behavior) and :detach is unavailable.
Why not just use tmux/screen? You can, but the daemon gives you procs' per-project log panes, named-group start/stop, auto-restart, and one-key reattach without wrapping the whole UI in a multiplexer.
Full reference in examples/config.yml.
| Key | Type | Default | Description |
|---|---|---|---|
projects.<id>.path |
string | required | Working directory for the process |
projects.<id>.cmd |
string | required | Shell command to run |
projects.<id>.restart |
never / on-failure |
never |
Restart policy |
projects.<id>.env |
map[string]string | {} |
Inline env vars injected into the child. Quote numeric values in YAML (PORT: "8080") |
projects.<id>.env_file |
string | — | Path to a KEY=VALUE env file (supports # comments, export prefix, quoted values). Merged under inline env: — inline wins on conflict |
groups.<name> |
[id, ...] |
— | Named group for batch start |
settings.log_buffer_lines |
int | 1000 | Per-project in-memory ring size |
settings.log_dir |
string | ~/.cache/procs/logs |
Log file directory |
settings.log_rotate_size_mb |
int | 10 | Rotate log file after N MB |
settings.log_rotate_keep |
int | 5 | Rotated log files to keep |
settings.shutdown_grace_ms |
int | 5000 | Grace period before SIGKILL |
settings.group_start_delay_ms |
int | 300 | Delay between group member starts |
settings.restart_backoff_ms |
[int, ...] |
[1000,2000,4000,8000,16000] |
Backoff schedule (ms) |
settings.restart_max_attempts |
int | 5 | Max restart attempts |
settings.pty_cols / pty_rows |
int | 120 / 40 | PTY dimensions |
Press Ctrl-E in the TUI to open the active config in $VISUAL (preferred), $EDITOR, or vi (fallback). Saving and quitting the editor triggers an automatic reload:
- Added projects appear in the sidebar immediately.
- Removed projects are stopped (if running) and dropped from the sidebar.
- Changed projects (path/cmd/env/restart) keep running on the old definition; a toast hints "restart to apply" — press
ron the project to pick up the new command. - Parse errors keep the previous config intact; the TUI shows a
config error: …toast.
:reload re-reads the file from disk without opening an editor (useful when you edit it from another terminal). For GUI editors, set EDITOR so it blocks until exit — e.g. export EDITOR='code -w', export EDITOR='subl -w'.
| Key | Action |
|---|---|
k / ↑ |
Select previous process |
j / ↓ |
Select next process |
s |
Start selected process |
r |
Restart selected process |
x |
Stop selected process |
X |
Stop all processes |
a |
Attach to selected process (raw PTY) |
S |
Open group picker → start group |
c / b |
Open branch picker (type to filter; arrows to navigate; Enter checks out; Esc clears filter then closes) |
f |
git fetch --prune for the selected project (async; toast on result) |
p |
git pull --ff-only for the selected project — never merges |
/ |
Search logs (vim-style; matches highlighted inline) |
n / N |
Jump to next / previous search match |
1–9 |
Quick-jump to project 1–9 in the sidebar |
PgUp / Ctrl-B |
Scroll log up |
PgDn / Ctrl-F |
Scroll log down |
g |
Scroll to top |
G |
Scroll to bottom |
Tab |
Enter log focus — hand the keyboard to the log pane for vim-style nav + yank (double-Esc to return) |
:set nu / :set nonu |
Toggle line-number gutter in the log panel |
:set sev / :set nosev |
Toggle severity colouring (ERROR/WARN/INFO/DEBUG → fg colour). On by default |
:set json / :set nojson |
Toggle pretty-print for single-line JSON log lines |
:set sql / :set nosql |
Toggle keyword-aware SQL formatting (handles Rails/Sequel/GORM prefixes) |
:set wrap / :set nowrap |
Toggle hard-wrap of long lines at viewport width. On by default |
:clear / :c / Ctrl-L |
Empty the in-memory log buffer for the selected project (files on disk untouched) |
:w {path} |
Dump the currently visible log buffer to {path} (supports ~ / $VAR) |
Ctrl-E / :edit |
Open active config in $VISUAL / $EDITOR / vi; auto-reload on save |
:reload |
Re-read config from disk without opening an editor |
:fetch |
git fetch --prune the selected project (async; result toasted) |
:pull |
git pull --ff-only the selected project — never merges; refuses diverged branches |
:detach |
Detach the TUI; the daemon + processes keep running (macOS/Linux) |
:shutdown |
Stop the daemon and all its children |
? |
Toggle help overlay |
: |
Open command bar (:q to quit) |
Ctrl-C |
Detach (daemon mode) / quit — press twice within 2 s to confirm |
Esc |
Cancel / close overlay |
Press Tab to hand the keyboard to the log pane. The sidebar dims, a
cursor appears, line numbers show, and all process-control keys become
inert — only vim motions and yank commands are active. Focus persists
across multiple yanks; to return to process-switching press Esc
twice within 2 seconds (mirrors the double-Ctrl-C quit pattern).
Counts work in front of any motion or operator: 3w, 2yy, y3e,
3f.. The command buffer shows up in the status bar while you type
(3yi …) so multi-key commands are discoverable.
Motions:
| Key | Moves to |
|---|---|
h j k l / arrows |
char / line |
w W |
next word / WORD |
b B |
prev word / WORD |
e E |
end of (WORD-)word forward |
ge gE |
end of (WORD-)word backward |
0 ^ $ |
line start / first-non-blank / end |
+ - |
next / prev line first-non-blank |
gg G |
buffer top / bottom |
Space |
pause / resume sticky auto-scroll |
Ctrl-u / d |
half-page up / down |
Ctrl-b / f |
full-page up / down |
H M L |
viewport top / mid / bottom |
f{c} F{c} |
find char forward / backward (current line) |
t{c} T{c} |
till char (stop one before) |
; , |
repeat last find (same / reverse direction) |
/ n N |
filter bar; cursor-jump to next / prev match |
Visual + yank:
| Key | Action |
|---|---|
v V |
char / line visual |
y (visual) |
yank selection → clipboard |
yy / Y |
yank current line (count: 3yy) |
y{motion} |
yank to motion target (yw, y3e, yf., y$) |
Text objects (current-line scoped):
| Object | Meaning |
|---|---|
iw aw / iW aW |
inner / around word (WORD) |
i" a" / i' a' / i` a` |
inner / around quote pair |
i( a( i) a) |
inner / around parens |
i[ a[ i] a] |
inner / around brackets |
i{ a{ i} a} |
inner / around braces |
i< a< i> a> |
inner / around angle brackets |
Exit: first Esc cancels any pending operator, count, or
selection; second Esc within 2 s returns to sidebar / process
switching. The status label flips NORMAL → LOG → COPY as focus
and selection change. The line-number gutter is always shown while log
focus is active; outside focus, toggle it via :set nu / :set nonu
from the command bar.
Note: Attach currently requires in-process mode. Since macOS/Linux default to daemon mode, run
procs --no-daemonto use attach (this runs the TUI without a daemon, so processes stop when you quit). Remote attach over the daemon socket is planned for a future release.
Press a to attach to a running process. The terminal enters raw mode and all
input goes directly to the child PTY. To return to procs:
Press Esc twice (within 400ms)
This matches the double-Esc exit gesture used in log-focus mode, so the same
key works across the TUI. The detach sequence is 0x1b 0x1b (Esc Esc).
A lone Esc is forwarded to the child after the 400 ms window expires, which
introduces a small latency for vim normal-mode and other Esc-driven UIs.
Paste: use your terminal's native paste (Cmd-V on macOS, Ctrl-Shift-V or
Shift-Insert on Linux). The bracketed-paste sequence is forwarded to the
child PTY; if the child enabled DEC mode 2004 (?2004h), it sees the
content wrapped in \x1b[200~ … \x1b[201~ so shells treat it as data
instead of typed commands. Newlines are normalised to CR (the universal
"Enter").
No config file found:
procs: config file not found at "/Users/you/.config/procs/config.yml"
Create the file at that path (see Quick Start), or pass -c /path/to/config.yml.
Port not shown in sidebar: The port column only shows after the process has opened a listening socket. Wait a moment for the process to bind, then the display updates automatically.
Git branch not shown:
The project's path must be inside a Git repository. If git is not on
$PATH or the directory is not a repo, the branch column is left blank.
Process does not restart:
Check that restart: on-failure is set in your config. restart: never
(the default) means the process stays stopped after it exits.
Daemon won't start / procs hangs on launch:
Check the daemon log at ~/.cache/procs/<hash>.daemon.log (macOS:
~/Library/Caches/procs/...). If a previous daemon crashed, a stale socket is
cleaned up automatically on the next launch.
"N processes from a previously crashed daemon are still running":
A daemon died without stopping its children. Run procs kill --orphans to stop
them before they conflict with newly started processes.
Changes to a running project's command aren't applied after reload:
:reload / Ctrl-E reconcile added/removed projects immediately, but a changed
command keeps running on the old definition — press r on the project to pick
up the new command.
MIT — see LICENSE.