lazycomd

TUI

Run lazycomd with no arguments:

 lazycomd                                              3 of 5 running
╭─ 1 Status ─────────────────────────╮╭─ web — following · pid 9663 ──────╮
│● 3 of 5 running                    ││127.0.0.1 - - "GET / HTTP/1.1" 200 │
│…/lazycomd/lazycomd.sock            ││127.0.0.1 - - "GET / HTTP/1.1" 200 │
╰────────────────────────────────────╯│                                   │
┏━ 2 Commands ━━━━━━━━━━━━━━━━━━━━━━━┓│                                   │
┃  H NAME           STATE     MEM    ┃│                                   │
┃    flaky          stopped   -      ┃│                                   │
┃  ○ ghost          running   1M     ┃│                                   │
┃    noisy          running   3M     ┃│                                   │
┃> ● web            running   25M    ┃│                                   │
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛│                                   │
╭─ 3 Ports ────────────────── 4, 1 ⚠ ╮│                                   │
│  3283   ARDAgent                   ││                                   │
│ ⚠8099   Python     web             ││                                   │
│  11434  ollama                     ││                                   │
╰────────────────────────────────────╯╰───────────────────────────────────╯
 j/k move  1-3 panel  tab cycle panels  f follow  / filter  ? help  q quit

Three panels in the left column, numbered. The right-hand pane follows whichever panel has focus: the selected command’s logs for 2 Commands, the selected listener’s detail for 3 Ports, and daemon and collector state for 1 Status — which is where a missing lsof or an unreachable daemon explains itself.

Key Where Action
1 2 3 anywhere focus the Status, Commands or Ports panel
tab shift+tab anywhere cycle panels
j k focused panel move the cursor
g G focused panel first / last row
s Commands start, dependencies first
S Commands stop
r Commands restart
/ p anywhere search box: commands, listening ports and the buffered log
i Commands detail for the selected command: spec, state, ports, health
o Commands open the full log, which is where the log filter lives
a Commands add a command, writing it to the config
e Commands edit the selected command
d Commands delete it, after confirming which file changes
ctrl+d ctrl+u anywhere scroll the main pane
f anywhere toggle log follow
/ log view filter the lines, esc clears it
esc anywhere close the search box, the log view or the detail pane
? anywhere help overlay
q ctrl+c anywhere quit (the daemon keeps running)

The Ports panel is view only: it lists every listener on the machine, not just lazycomd’s, and start and stop act on commands in the Commands panel.

The mouse works as well: clicking a panel focuses it, clicking a row selects it, clicking a match in the search box goes to it, and the wheel scrolls whatever is under the pointer.

Enter in the search box goes to the match and nothing else: a command is selected, a port is selected in the Ports panel, and a log line opens the full log view with your query already applied as its filter. The pane beside the panels is a preview — it follows the selection and scrolls, and the filter lives in the full view where there is room to read the result.

Lifecycle keys act on the Commands panel only; pressed elsewhere they say so rather than acting on something you cannot see.

The sidebar shows memory from about 34 columns and adds CPU and restart counts past 46. A command’s pid rides in the main pane’s title, since a sidebar has no column to spare for it. Below 90 columns the two columns become two rows: the focused panel keeps its height, the others collapse to their title bars, and the main pane sits underneath — nothing becomes unreachable.

The filter is a plain substring with smart case: a lowercase query matches case-insensitively. A state shown as running* means the config changed under a reload and the new spec applies on that command’s next start.

Quitting the TUI stops nothing. If the daemon is down when you open the TUI (unix socket only), it asks once: y starts it (user service if installed, otherwise a background serve), n or esc skips. After that it retries every 2s rather than exiting. A TCP LAZYCOMD_ADDR never offers to start a local daemon.

With no terminal — lazycomd | cat, CI — the bare command prints usage instead of launching.

Editing the config from the TUI

a opens a four-field form and writes the result into your config:

╭─ New command ──────────────────────────────────╮
│                                                │
│  NAME     web                                  │
│  COMMAND  npm start                            │
│  FOLDER   ~/coding/app                         │
│  RESTART  ‹ on-failure ›                       │
│                                                │
│  where the command runs · blank = ~            │
│  tab next · enter save · esc cancel            │
╰────────────────────────────────────────────────╯

FOLDER is cwd: in the file — where the command runs. It opens filled with the directory you launched the TUI in, and follows the project once the name is namespaced, as in app:api.

The command line splits on spaces. A line containing |, >, <, &, ;, $ or * goes to sh -c whole instead, and the form says so while you type.

e edits the selected command. It reads the command’s full spec first, so fields the form never shows — env, depends_on, health, port — survive the edit untouched. Renaming is not offered: that is a file edit.

d deletes, after a prompt naming the file that will change.

Only the lines of the command being touched are rewritten. Comments, blank lines and quoting everywhere else come out byte-identical, so git diff shows the one command you changed. If the file changed on disk since the daemon read it — because you have it open in an editor — the write is refused and says so rather than overwriting you.

Dashboard

The Ports panel lists every listener with the lazycomd command that owns it, marking the contested ones. Focus it with 3 and the main pane shows the selected port in full:

 port 8099
   address   *
   process   Python
   pid       9663
   owner     web

 conflict
   ⚠ 8099 wanted by rival — held by Python (pid 9663)

Ownership is by process group, so a command that listens from a grandchild — sh -c "npm start" — is still credited correctly.

The Commands panel carries a health dot: up, down, blank for a command with no health: URL. CPU is percent of one core, summed across the command’s whole process tree, so a busy multithreaded process legitimately reads above 100%.

Ports come from lsof, falling back to ss. On a machine with neither, the panel says so, panel 1 explains why, and the other signals keep working.