lazycomd

TUI

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
h l ← → Commands fold / unfold a group
P Commands act on the whole group: P then s, S or r
/ 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.

P acts on a whole group instead of one command: it names the selected command’s group and waits for a verb, and s, S or r then applies to every command carrying it. Any other key cancels, so a stray s two moves later still acts on one command. Start pulls dependencies in per command, so a group comes up in the order its depends_on describes.

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 form, centred over the panels, and writes the result into your config. A field shows its value until e starts editing it, so tabbing through the form cannot leave keystrokes in the field you were passing:

Key Does
tab shift+tab, ↑ ↓ move between fields
e edit the field, opening its options where it has any
esc stop editing — again to close the form
← → space change RESTART, AUTOSTART and METRICS directly
enter save

A new command opens with the cursor in NAME, since the first thing to do is name it; an edit opens to be read.

┏━ New command ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃  NAME      api                                                       ┃
┃  COMMAND   go run ./cmd/api                                          ┃
┃  FOLDER    /Users/tphuc/aviron/tools/mdm-console                     ┃
┃› GROUP     backend                                                   ┃
┃  ENV       LOG=debug                                                 ┃
┃  PORT      8080                                                      ┃
┃  HEALTH    http://localhost:8080/healthz                             ┃
┃  RESTART   no · on-failure · always                                  ┃
┃  AUTOSTART yes · no                                                  ┃
┃  METRICS   yes · no                                                  ┃
┃                                                                      ┃
┃  the group this is listed under · e for the others                   ┃
┃  e edit · 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 e opens the matching directories: tab finishes the path, ↑↓ pick one.

GROUP is the label this command is listed under. e shows the groups that exist, ↑ ↓ pick one:

┃› GROUP     (none)                                                  ┃
┃            ▸ (none) — on its own                                   ┃
┃              console  1 command                                    ┃
┃              databases  2 commands                                 ┃
┃              + new group                                           ┃

+ new group asks for one thing, the name:

┃            ▸ + new group                                           ┃
┃                called  backend                                     ┃

COMMAND wraps rather than scrolling sideways, so a long line is readable in full. It splits on spaces; a line containing |, >, <, &, ;, $ or * goes to sh -c whole instead, and the form says so while you type.

ENV is one line of KEY=VALUE pairs. A value holding a space cannot survive that shape, so a command with one keeps its env and the field goes read-only rather than writing back half of it.

RESTART, AUTOSTART and METRICS show every choice with the current one marked: ← → or space changes it, with no e needed — they hold a choice rather than text. METRICS off writes metrics: false, which keeps that command out of the daemon’s history.

The form checks its own work before saving — the port parses, the env is well formed, the health URL has an http scheme — so a typo is caught in the field rather than by the daemon’s reparse. A failing write shows the daemon’s whole error, wrapped over as many lines as it needs.

e edits the selected command, its group included. The edit reads the command’s full spec first, so fields the form does not show — depends_on, size, log — 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.

Commands group by their label

A command with group: backend is drawn under a backend heading, alongside every other command carrying that label, whichever file each of them lives in:

┏━ 2 Commands ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃  NAME                        STATE     MEM    CPU   RS ┃
┃> ▾ console  1 running                                  ┃
┃    toolweb                   running   36M    12%   -  ┃
┃  ▾ databases  1 of 2 running                           ┃
┃    aviron-db-alpha-dev       running   38M    -     -  ┃
┃    redis                     stopped   -      -     -  ┃
┃  ▾ (no group)  0 of 1 running                          ┃
┃    tunnel                    stopped   -      -     -  ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛

A heading says how much of its group is up. h folds it, l unfolds it, and s, S or r on a heading act on every command under it. Commands with no label sit last under one of their own.

A catalog where nothing sets a group is drawn flat: a heading above a single list is a row spent saying nothing.

The group is a label, not a location: which file a command is kept in is a different question, and not one the list asks.

Metrics in the log pane

Above a command’s output sit two charts — cpu and memory over the last two minutes, the window the status poll has filled since the TUI opened:

┏━ toolweb — following · pid 24519 ━━━━━━ cpu 12%  mem 36M  up 5m12s ┓
┃cpu 15%  peak 113%                 mem 33M  peak 34M                ┃
┃▆                                            ▁▁▂▃▃▃▄▄▅▅▆▆▆▇▇▇▇▇█████┃
┃██▃▁                                 ▁▂▃▅▆▆▇████████████████████████┃
┃████▇▆▅▅▄▄▄▅▄▃▄▄▅▄▅▄▄▃▅▄▄▃▄▄▄▄▄▄▃  ▇████████████████████████████████┃
┃                      ╷                                             ┃
┃console on http://127.0.0.1:8787  envs=1                            ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛

Each chart scales to its own window’s peak, which its header names, so a command that never leaves 2% still shows its shape and an autoscaled chart cannot mislead about magnitude. CPU is percent of one core summed across the process tree, so a busy multithreaded command reads above 100%.

A row below the charts appears only when the window holds a restart, ticking ╷ where it happened — a restart is usually the most interesting thing in two minutes.

A command that is not running keeps the window it had, drawn dim: what it was doing before it stopped is the part worth seeing. A command with no samples yet, and a pane too narrow for a chart, draw nothing at all.

The window belongs to the daemon, which records it whether or not a TUI is open: close this one and the chart is still full when you come back. How much it keeps, and whether it records at all, is the metrics: block in the config; with recording off the strip says so rather than drawing an empty chart.

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.