29 Commits
Author SHA1 Message Date
Nicolas De LoofandClaude Fable 5 68d9b510cc fix(display): reset the screen after degraded out-of-operation lines
A plain line printed outside any operation lands below the tracked
block, so the block is no longer directly above the cursor. Reset the
screen so the next operation repaints a fresh block instead of moving
the cursor over rows that shifted — the same reason unsuspend resets
after buildkit output.

Suggested-by: Guillaume Lours <[email protected]>
Co-Authored-By: Claude Fable 5 <[email protected]>
Signed-off-by: Nicolas De Loof <[email protected]>
2026-09-09 16:50:05 +02:00
Nicolas De LoofandClaude Fable 5 1a3bb4f6f5 refactor(display): address review feedback on the term renderer
- layoutFrame clips its output to the terminal height: height==1 no
  longer overflows with header + more-marker
- the two status-to-color switches merge into eventColor(status, def)
- the clear-suspension sequence extracts into unsuspend()
- tests drive a manually-advanced fake clock through the injected
  now(), and the cancel test loses its settling sleep — Done waits for
  the refresh goroutine itself
- roots, per-parent children and the completed count are maintained
  incrementally by apply(), so a 100ms tick with an unchanged tree no
  longer rescans every task
- the frame<0 clamp in spinGlyph drops: time comes from the writer's
  single clock and never regresses; the invariant is documented on the
  model
- Start's retire-previous-cycle comment spells out that cancellation
  only guarantees the old goroutine will exit, not that it already has

Co-Authored-By: Claude Fable 5 <[email protected]>
Signed-off-by: Nicolas De Loof <[email protected]>
2026-09-09 16:50:05 +02:00
Nicolas De Loof d82edca1c2 fix(display): harden termWriter lifecycle against review findings on #14119
Transposes the applicable findings from the review of the ttyWriter fix
(#14119) to the rewritten renderer:

- Done waits for the refresh goroutine to exit (outside the lock), so
  nothing can repaint after Done returns and output printed right after
  an operation cannot be garbled by a stray frame
- a nested Start retires the previous cycle instead of leaking its
  refresh goroutine until the parent context is cancelled
- build suspension no longer leaks across cycles: Start resets it and
  starts a fresh block below whatever buildkit wrote
- drop the dead strings.ToLower in startDependencies: the unexported
  start only reads projectName when no Project is passed

The remaining findings are structural to this rewrite: Done is a single
critical section, and per-cycle context cancellation replaces the
doneSignal handshake.

Signed-off-by: Nicolas De Loof <[email protected]>
2026-09-09 16:50:05 +02:00
Nicolas De LoofandClaude Fable 5 136763181b fix(display): clip the header line to the terminal width like every other row
The "[+] op N/M" header was the one line bypassing renderSegs, so on a
very narrow terminal it could wrap and desync the cursor arithmetic the
rest of the design guarantees against. Route it through the same clip
and cover degenerate widths (8, 12 cells) in the invariant test.

Co-Authored-By: Claude Fable 5 <[email protected]>
Signed-off-by: Nicolas De Loof <[email protected]>
2026-09-09 16:50:05 +02:00
Nicolas De LoofandClaude Fable 5 9fc38cce0e refactor(display): rebuild the TTY progress renderer on a model/layout/screen split
The TTY renderer accumulated display-corruption fixes (truncation of
details, then progress sizes, then task ids; timer alignment; rune-based
measurement) that each patched one symptom of the same structural gap:
nothing guaranteed a rendered line fits the terminal, and once a line
wraps, cursor arithmetic desyncs and the block corrupts.

Replace it with three separable units:

- tty_model.go: pure event reducer with an injected clock, preserving
  first-parent-wins updates, monotonic progress and header counters
- tty_layout.go: pure (model, size, now) -> lines function; all widths
  are measured in terminal cells (go-runewidth, so CJK is correct) on
  plain text before coloring, and every line is clipped to the terminal
  width by construction, status text included
- tty_screen.go: diff-based repaint; unchanged rows are skipped,
  identical frames write nothing, a frame is a single Write; a shrinking
  terminal abandons the block instead of moving the cursor over reflowed
  rows

The writer coordinates them behind a mutex and stops the refresh
goroutine through context cancellation, so Done cannot block when the
operation context was cancelled first (Ctrl-C during pull). The spinner
frame is derived from the clock instead of advancing on every call, and
truncation can no longer split multi-byte runes. Visual output is
unchanged: the snapshot test reproduces the previous renderer's golden
output character for character.

Co-Authored-By: Claude Fable 5 <[email protected]>
Signed-off-by: Nicolas De Loof <[email protected]>
2026-09-09 16:50:05 +02:00
Guillaume Lours 6d3d68c1fa fix(display): stop ttyWriter.Done from hanging after context cancel
Fixes #14114: Done() blocked forever once the render goroutine had
already exited via ctx.Done() (e.g. after SIGTERM), since it sent on an
unbuffered channel with nobody left to receive.

Closing the channel instead of sending never blocks, but a bare close()
panics the moment ttyWriter runs a second Start/Done cycle on the same
shared bus - which already happens today (run's nested create/start,
publish's nested push, rm --stop's sequential stop then remove). Start
now hands out a fresh, once-only-closable signal each cycle instead of
a single shared channel, and run/publish stop opening a redundant
nested bracket around calls already inside one.

Signed-off-by: Guillaume Lours <[email protected]>
2026-08-25 11:38:50 +02:00
Nicolas De Loof cccb9f1f7e cli: display.Mode always resolves to the mode actually rendered
The display.Mode package global was left as "auto" on the most common
path (auto mode with an interactive or redirected terminal), so readers
like `up`'s NavigationMenu gate saw an unresolved value; that gate also
compared against the string literal "plain" instead of the constant.
applyDisplayMode wrote a mode that selectEventProcessor could then
contradict (--ansi always with a redirected stderr rendered Plain while
Mode said tty).

selectEventProcessor becomes the single resolution point: every branch
assigns the mode matching the renderer it returns, so after command
setup the global never holds ModeAuto. applyDisplayMode loses its
redundant writes and its misleading name (now applyAnsiMode); the
contract is documented on the Mode variable, and the existing
selectEventProcessor tests now pin the resolved mode for every branch.

Side effect, deliberate: with stderr redirected (auto -> plain
rendering) the navigation menu is no longer offered; it used to slip
through because Mode still read "auto".

Part of #14074 (section E).

Signed-off-by: Nicolas De Loof <[email protected]>
2026-08-20 15:10:15 +02:00
Nicolas De Loof 24059c688c chore: explicit, unique, greppable names for internal identifiers
Rename-only change, no behavior difference, no exported identifier
renamed (only parameter names of a few exported functions, which are
not part of the Go API).

- names that hide what a value is: parameters holding a *name* or *ID*
  while named like the object — project→projectName, service→serviceName,
  container→containerID — so string vs struct is readable at every call
  site without opening the signature
- misleading receivers: monitor methods used c (reads as
  client/container/CLI) → m; jsonWriter used p while its ttyWriter
  sibling uses w → w
- cryptic abbreviations with non-trivial scope: nw (meant both
  network.Summary and types.NetworkConfig) → network/networkConfig,
  svc → service/serviceName/serviceCopy, oc/ocs → observedContainer(s),
  cnts → serviceContainers, cnx → attachResponse
- container.Summary loop variables unified on ctr (was a c/ctr mix);
  fillBindMounts also dropped its p/s/m single-letters, one of which
  was shadowed by a loop variable of a different type
- option-struct identifiers unified on options (opts remains only for
  variadic functional options); ambiguous ones get a precise name
  (bindOptions, networkCreateOptions, projectOptionsFns)

Signed-off-by: Nicolas De Loof <[email protected]>
2026-08-20 11:17:11 +02:00
Nicolas De Loof 1e80a0906f chore: inline needless single-use helpers
Inline small functions that were extracted from their single call site
without a real boundary to justify it — no reuse, no dedicated test, no
responsibility of their own — so each caller now tells its whole story
top-down:

- getExecTarget, attachContainer, logContainer: one-line trampolines to
  getSpecifiedContainer / doAttachContainer / doLogContainer, which
  other call sites already use directly
- removeImage: single-statement wrapper, unlike its removeVolume
  sibling which has actual logic
- checkSelectedServices: named like a validation, actually a filter;
  the subtle rule (an unknown service is only an error with an explicit
  compose file) now reads where options.Services is rewritten
- prepareLabels: mutated the map it received while looking pure at the
  call site; the label writes are now visible in getCreateConfigs
- setDefaultTarget: mutation-by-pointer of the loop copy, now visible
  in the loop of injectFileReferences
- buildVolumeOptions/buildTmpfsOptions/buildImageOptions: nil-guard +
  field copies; the buildMountOptions switch now shows side by side
  what each mount type propagates (buildBindOption keeps real logic and
  keeps buildMountOptions under the gocyclo limit)
- displayDryRunBuildEvent: was longer than its only caller
- hasMore: read like a predicate, was a one-line spinner restart
- escapeDollarSign: wrapped a single bytes.ReplaceAll
- extractEnvCLIDefined: replaced by the canonical compose-go helper
  types.NewMappingWithEquals().ToMapping(), as run.go already does
- isPullPolicyValid: rebuilt the valid-values slice on every call; now
  a package-level list checked at the call site
- viewFromStackList: projection now sits next to the render closure
  that consumes exactly its three fields

No behavior change; single gocyclo threshold untouched.

Signed-off-by: Nicolas De Loof <[email protected]>
2026-08-14 22:49:53 +02:00
Guillaume Lours ef3238ab1f fix(progress): measure taskID width in runes, not bytes
maxBeforeStatusWidth used len(l.taskID) (bytes) while applyPadding
used utf8.RuneCountInString (runes). For ASCII task IDs the two
agree and no symptom surfaces, but a taskID containing multi-byte
UTF-8 chars (CJK, emoji, accented Latin) reported a width larger
than its visual columns. computeOverflow then triggered truncation
where none was needed, and truncateLongestTaskID's byte-indexed
slice could land mid-multibyte sequence, corrupting the displayed
string.

Align the two measurements on rune count.

Signed-off-by: Guillaume Lours <[email protected]>
2026-06-15 15:00:57 +02:00
Guillaume Lours 1a6212c859 fix(progress): drop size info from progress bar when line overflows
In narrow terminals (e.g. tmux panes), the TTY progress UI emitted
lines wider than terminalWidth because adjustLineWidth could shrink
details and taskID but never the progress field. When progress
carried the "X.XMB / Y.YMB" size suffix, the truncation loop exited
with overflow > 0 and applyPadding's max(timerPad, 1) floor pushed
the line one char over. tmux then wrapped the line visually while
print() kept counting logical lines, desyncing aec.Up() on the next
render and producing the mangled "[+] pull X/Y" header overwriting
prior task lines.

Track the size suffix byte length on lineData and let
adjustLineWidth drop it as an intermediate truncation step before
abbreviating the taskID.

Fixes docker/compose#13595

Signed-off-by: Guillaume Lours <[email protected]>
2026-06-15 15:00:57 +02:00
Samarth Verma 0d8dc09048 cmd/display: left-pad timers for right-aligned column
Signed-off-by: Samarth Verma <[email protected]>
2026-03-20 15:14:05 +01:00
Samarth Verma 02aaf253c8 Fix TTY timer alignment leaving stale suffix
Signed-off-by: Samarth Verma <[email protected]>
2026-03-20 15:14:05 +01:00
maks2134 c7e8896360 Fix forbidigo linting error
- Replace context.Background() with t.Context() in TestDoneDeadlockFix
- Ensures .idea files are not included in commit

Signed-off-by: maks2134 <[email protected]>
2026-03-18 16:23:34 +01:00
maks2134 e3fcdb8238 Fix linting issues in tty_test.go
- Replace context.Background() with context.WithCancel() in test
- Fix formatting issues (remove extra empty line)

Signed-off-by: maks2134 <[email protected]>
2026-03-18 16:23:34 +01:00
maks2134 25b29d776b Fix deadlock in ttyWriter.Done()
Resolves race condition between main thread calling Done() and UI thread
calling printWithDimensions(). The issue was that Done() held the mutex
while sending to the done channel, but the UI thread needed the same
mutex to process the done signal.

Fixed by sending the done signal before acquiring the mutex, allowing
the UI thread to receive the signal and release any held locks.

Fixes #13639

Signed-off-by: maks2134 <[email protected]>
2026-03-18 16:23:34 +01:00
Max Proske 85d6770b7f Fix panic when watch rebuilds without up
Signed-off-by: Max Proske <[email protected]>
2026-02-24 17:21:59 +01:00
Sebastiaan van Stijn f9828dfab9 modernize some code
Results of running the modernize command, with some minor changes
afterwards (removing the `contains` and `hasStatus` helper functions);

    go install golang.org/x/tools/go/analysis/passes/modernize/cmd/modernize@latest
    modernize -fix ./...

Signed-off-by: Sebastiaan van Stijn <[email protected]>
2026-02-12 15:31:35 +01:00
Mahesh Thakur a061c17736 fix: emit container status events after network reconnection
Signed-off-by: Mahesh Thakur <[email protected]>
2026-01-21 09:40:05 +01:00
Nicolas De Loof c8d687599a Fixed progress UI to adapt to terminal width
Signed-off-by: Nicolas De Loof <[email protected]>
2026-01-19 11:14:23 +01:00
Nicolas De Loof d4c1987638 Prevented incorrect progress metrics to break compose display.
Signed-off-by: Nicolas De Loof <[email protected]>
2025-12-15 18:02:31 +01:00
Nicolas De Loof 3853ad3911 prefer *task for memory efficiency updating tasks
Signed-off-by: Nicolas De Loof <[email protected]>
2025-12-12 14:22:02 +01:00
Nicolas De Loof 02008a0097 Restored image layer download progress details on pull.
Signed-off-by: Nicolas De Loof <[email protected]>
2025-12-12 14:22:02 +01:00
Nicolas De Loof b62cbed87c Fixed status alignment in progress UI.
Signed-off-by: Nicolas De Loof <[email protected]>
2025-12-11 15:25:03 +01:00
Nicolas De Loof aa9a71f37a run finalization synchronously
Signed-off-by: Nicolas De Loof <[email protected]>
2025-12-10 22:49:37 +01:00
Sebastiaan van Stijn dc66e6bad1 golangci-lint: use gci formatter instead of goimports
Most files already grouped imports into "stdlib -> other -> local",
but some files didn't. The gci formatter is similar to goimports, but
has better options to make sure imports are grouped in the expected
order (and to make sure no additional groups are present).

This formatter has a 'fix' function, so code can be re-formatted auto-
matically;

    golangci-lint run -v --fix

Signed-off-by: Sebastiaan van Stijn <[email protected]>
2025-12-01 12:21:50 +01:00
Nicolas De Loof 2b4543935c next release will be major version v5.x
Signed-off-by: Nicolas De Loof <[email protected]>
2025-11-13 09:32:43 +01:00
Nicolas De Loof 45def51117 make DRYRUN_PREFIX a display attribute, move DryRunClient out of pkg/api
Signed-off-by: Nicolas De Loof <[email protected]>
2025-11-12 11:17:28 +01:00
Nicolas De Loof aff5c115d6 move progress UI components into cmd
Signed-off-by: Nicolas De Loof <[email protected]>
2025-11-12 11:17:28 +01:00