This talks to your actual system LVM via vgs, pvs, lvs, and lsblk, and its action keys run
real, mutating lvm2 commands (lvcreate, lvextend, lvremove, vgremove, snapshots). There is
no mock/demo data in this version.

lvm-tui/
├── go.mod
├── cmd/
│ └── lvmtui/
│ └── main.go — thin entrypoint: builds a ui.App and runs it
└── internal/
├── model/ — plain data types, no I/O
│ ├── model.go — VolumeGroup/PhysicalVolume/LogicalVolume/Job/LogEntry,
│ │ Status()/UsedBytes()/UsedPercent(), HumanBytes(), CapacityBar()
│ └── model_test.go
├── lvm/ — real system I/O (the only package that shells out)
│ ├── lvm.go — LoadVolumeGroups(): vgs/pvs/lvs/lsblk → []model.VolumeGroup
│ ├── actions.go — CreateLV/ExtendLV/RemoveLV/RemoveVG/SnapshotLV/... (mutating)
│ ├── lvm_test.go
│ ├── actions_test.go
│ └── testutil_test.go — shared fake-PATH-binary test helper
└── ui/ — tview layout, panels, forms, key bindings
├── app.go — ui.New() / (*App).Run(); everything you saw rendered
└── app_test.go — tests for the non-widget logic (job/log tracking, selection)
Why this layout, and what depends on what:
modelhas zero dependencies beyond the standard library — it's just data plus pure formatting/derivation logic (Status(),HumanBytes(), etc.). Nothing here can accidentally shell out to a real command or touch a terminal.lvmdepends only onmodel(andos/exec). It's the only package that runs real commands. If you ever want a different frontend (a web UI, a CLI that just prints a table, a Prometheus exporter), this is the package you'd reuse as-is.uidepends onmodelandlvm, and ontview/tcell. It has no idea howlvm.LoadVolumeGroups()gets its data — it just calls the function and renders what comes back. This is what made the earlier construction-order panic easy to isolate and fix, and it's why the job/log/selection logic inapp_test.gocan be tested without spinning up any actual terminal widgets.cmd/lvmtuiis deliberately tiny (10 lines) — all it does is callui.New().Run()and report a fatal error if the event loop exits abnormally. This is the standard Go convention:cmd/holds thin entrypoints,internal/holds everything an external project can't import.
This also means go doc lvmtui/internal/lvm, go doc lvmtui/internal/model, etc. each show a
focused, relevant API instead of one 700-line file's worth of unrelated exported names.
- Linux with
lvm2installed (vgs/pvs/lvs/lvcreate/etc. on PATH) - Root privileges — LVM metadata and device nodes require this. Run with
sudo. - Go 1.21+ to build
cd lvm-tui
go mod tidy
go build -o lvmtui ./cmd/lvmtui
sudo ./lvmtuiRunning it without root will very likely fail to read anything (you'll see the error in the header/status panel and Logs) — LVM commands need root even for read-only queries in most setups.
- Volume Groups / Physical Volumes / Logical Volumes panels — populated by parsing
vgs/pvs/lvs --reportformat json --units b --nosuffix, joined together ininternal/lvm/lvm.go. - Filesystem type / mount point / used space per LV — from
lsblk -J -b -o NAME,KNAME,FSTYPE,MOUNTPOINT,FSUSED,FSSIZE, matched to each LV by resolving its device-mapper symlink (e.g./dev/vg_system/root) down to its kernel device name (e.g.dm-0). - VG status (Healthy/Warning/Critical) — computed from
vg_attr(apflag means a PV is missing → Critical) and free-space percentage (<10% free → Warning), same thresholds as your screenshot. - Actions (
n/e/s/x/X/c) run real commands viaos/exec(internal/lvm/actions.go):n— Create LV:lvcreate -y -n <name> -L <size> <vg>e— Extend LV:lvextend -r -L +<size> <lvpath>(-rgrows the filesystem online)s— Snapshot:lvcreate -y -s -n <name> -L <size> <lvpath>x— Remove LV (confirmation required):lvremove -y <lvpath>X— Remove VG (confirmation required):vgremove -y <vg>c— Check VG:vgck <vg>r— Refresh: re-runsvgs/pvs/lvs/lsblkfrom scratch
- Jobs panel — a real history of the actions you've run in this session and whether they succeeded or failed (not simulated progress bars).
- Logs panel — real log lines: what was loaded, what actions ran, and any command errors (stderr) verbatim.
go test ./... # run all tests
go test -v ./... # verbose
go test -race ./... # with the race detector (relevant since actions run in a goroutine)
go test -cover ./... # coverage summary32 tests across the three packages, all passing, no go vet issues, clean under -race.
How the tests work without a real LVM system: internal/lvm shells out to real binaries
(vgs, pvs, lvs, lsblk, lvcreate, ...). The tests never touch your real system — each test
prepends a temp directory of fake executable scripts onto PATH (see testutil_test.go), so:
- Read-path tests (
lvm_test.go) feed the parser realistic LVM2 JSON (the same schema--reportformat jsonreally emits) and assert VG/PV/LV joins, status thresholds, and error handling (command missing, nonzero exit, invalid JSON). - Action tests (
actions_test.go) use fake binaries that just echo their arguments back, so each test asserts the exact flags/arguments (e.g.CreateLVreally invokeslvcreate -y -n <name> -L <size> <vg>) without ever running a real mutating command. model_test.go/ui/app_test.gocover pure logic: byte formatting, VG status derivation, job history, log buffer, and current-selection helpers — no fake binaries needed, and no tview widgets constructed.
ui/app.go's widget-building code (the actual tview.Flex/tview.Table layout) isn't unit
tested — that's better suited to manual/visual testing than assertions — but everything it calls
into that isn't pure widget plumbing (currentVG, addJob, data prep before SetText, etc.) is
tested where it's decoupled from rendering.
One real bug this test suite caught and fixed: CapacityBar capped the rendered bar width for
percentages over 100%, but printed the raw (uncapped) percentage text — e.g. 150.0% instead of
100.0%. Now both are clamped consistently.
lvextend -rrequiresfsadm(part of lvm2) and support for online resize of your filesystem (works for ext2/3/4 and XFS growth; XFS can't shrink).- There's no
lvreducewired to a key yet (shrinking is riskier — theReduceLVfunction exists ininternal/lvm/actions.goif you want to wire it up, but I intentionally didn't bind it to a shortcut). pvmoveand "change VG tag" functions exist ininternal/lvm/actions.gobut aren't wired into the UI — straightforward to add a form for them the same way Create/Extend/Snapshot are done ininternal/ui/app.go.- Destructive actions (
x,X) require an explicit "Confirm" click/press in a modal first. - I don't have a real LVM system in my own environment, so while the read-path parsing and the exact arguments of every mutating command are verified by the test suite, I haven't run this end-to-end against real disks. Try it on a disposable VG before trusting it with real data.