Skip to content

feat: port dry TUI to Bubbletea v2#239

Merged
moncho merged 41 commits intomasterfrom
port-to-bubbletea2
Feb 26, 2026
Merged

feat: port dry TUI to Bubbletea v2#239
moncho merged 41 commits intomasterfrom
port-to-bubbletea2

Conversation

@moncho
Copy link
Owner

@moncho moncho commented Feb 25, 2026

Replaces the entire tcell/gizak-termui rendering stack with Bubbletea v2, Bubbles v2, and Lipgloss v2, adopting the Elm architecture with a single top-level model that delegates to sub-models for each view. A shared TableModel provides cursor navigation, column sorting, text filtering, and scroll windowing across all list views. An overlay system handles the less viewer, confirmation prompts, text input, and menu. Docker events stream in with a 250ms debounce for live refresh. The old ui/termui/ package and legacy event-loop code have been removed entirely, going from ~16k lines down to ~6.5k.

moncho and others added 26 commits February 25, 2026 00:39
Replace the entire UI layer with Bubbletea v2 (charm.land/bubbletea/v2),
Bubbles v2, and Lipgloss v2. This is a complete rewrite of the TUI
framework while preserving all Docker functionality and key bindings.

Key changes:
- Replace tcell/termui render loop with Bubbletea's Elm architecture
- New top-level model (app/model.go) with Init/Update/View lifecycle
- Shared TableModel component for all list views (containers, images,
  networks, volumes, nodes, services, stacks)
- Overlay system: less viewer, prompt, input prompt, container menu
- Docker event throttling with 250ms debounce
- Monitor view with channel-based live stats
- All swarm views (nodes, services, stacks, tasks)
- Disk usage view with prune support
- 52 new tests across app/, appui/, appui/swarm/

Removed old dependencies: gdamore/tcell, gizak/termui, nsf/termbox-go,
olekukonko/tablewriter, mitchellh/go-wordwrap

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Remove ui/color.go (550 lines of unused Color type and constants)
- Remove ui/screen_dimension.go (unused Dimensions type)
- Remove appui/appui.go (unused invalidRow error type)
- Remove unused overlayStream constant from app/model.go
- Remove unused constants from appui/ui.go (DownArrowLength,
  RightArrow, CalcItemWidth)
- Update CLAUDE.md to reflect Bubbletea v2 architecture

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The viewport was rendering empty because SetSize only set a lipgloss
Style on the viewport instead of calling SetWidth/SetHeight. The
bubbles v2 viewport uses internal width/height fields to determine
visible content, not the Style dimensions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Set viewport dimensions before content so soft-wrap calculates correctly
- Re-apply content when SetSize is called to recalculate wrapping
- Enable AltScreen for proper full-screen terminal redraws
- Add g/G keybindings for go-to-top/bottom in less viewer
- Forward mouse wheel events to less overlay for scroll support
- Add TestModel_LessScrolling test

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Forward WindowSizeMsg to overlay models (less, prompt, input, menu)
  and message bar so they adapt when terminal is resized
- Remove "f" from viewport PageDown binding to avoid conflict with
  less viewer's follow toggle (space/pgdown still work for page down)
- Add "f" key (follow mode) to help text for logs/inspect buffers
- Fix "s" key help text: says "snapshot" instead of "live stream"
- Add TestModel_ResizeWithOverlay test

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Remove unimplemented [s]:Set refresh rate from monitor help text
- Remove unused ListItem field from Theme struct
- Remove BUBBLETEA_MIGRATION_SPEC.md (migration is complete)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix darkgrey/grey colors: 232/233 were nearly black on dark
  backgrounds, changed to 242/244 for readable medium grey
- Fix markup style stacking: <b><darkgrey> now produces bold+grey
  instead of replacing bold with grey (use lipgloss Inherit)
- Add background color to footer bar using theme Footer color
- Add markup rendering tests

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Color-code container rows: green for running, red for stopped
  (add StyledRow interface for row-level styling)
- Expand header to show all Docker info: hostname, swarm status,
  cert path, API version, CPU, memory, OS/arch/kernel
- Add column spacing in table rendering (PaddingRight)
- Add CursorLineBg to Black256 theme (was missing)
- Constrain message bar width to terminal width

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Set tea.View.BackgroundColor to DryTheme.Bg (Color234) for uniform
  dark grey terminal background matching the old screen fill behavior
- Fix table row rendering: per-cell ANSI styling for indicator rows to
  avoid nested reset sequences killing row colors; full-width padding
  for selected row highlight and all data rows
- Add StyledIndicator interface and RunningIndicatorStyle/StoppedIndicatorStyle
  so container status indicator (■) uses green/red while row text uses
  rose(181)/grey(244)
- Column-aligned header grid using fixed-width cells with ansi.Truncate
- Cyan (DryTheme.Key) widget headers across all views
- White (DryTheme.Fg) table column headers instead of blue
- Fix layout math: MainScreenHeaderSize 5→4 to match actual header line
  count; message bar always renders (empty line when no message) for
  consistent height; table pads with empty lines to fill allocated height
  so footer stays at screen bottom
- Fix column width calculation: no double-counting of spacing, last
  proportional column gets remainder to avoid rounding gaps

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add RenderMarkupWithBase() that preserves a base style (including
  background) through tag resets, fixing the ANSI nesting bug where
  inner \e[m resets killed the footer's blue background mid-line
- Footer now uses RenderMarkupWithBase with the footer background as
  base style, ensuring continuous Color25 blue across all text segments
- Add PadLine() helper that pads with styled spaces to extend background
- Render help text markup before passing to less viewer so tags like
  <white>, <yellow>, <blue> display as colored text instead of raw tags

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace the old 256-color themes (Dark256/Black256) with a new
Crush-inspired palette using hex colors from charmbracelet's CharmTone
system. Restyle footer key shortcuts to a cleaner format with cyan-green
keys, muted descriptions, and dot separators.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Rework the theme system so all colors flow through DryTheme:
- Expand ui.Theme with semantic fields (FgMuted, FgSubtle, Primary,
  Secondary, Tertiary, Success, Error, Warning, Border)
- Make CharmTone palette vars unexported in appui/theme.go
- Replace all direct palette references with DryTheme.X fields
- Add rounded border to container menu overlay
- Add progress bars to disk usage view
- Wrap loading screen in a rounded border box

Fix F1 sorting: TableModel.NextSort() now actually sorts rows by the
active column using sort.SliceStable, making sorting work immediately
in all views without requiring a data reload.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add footer key mappings for task views (ServiceTasks, Tasks, StackTasks)
- Add F1 sort handler to MonitorModel and advertise it in footer
- Add F5 handler to TasksModel for consistency with other swarm views

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace custom footer markup rendering with bubbles/help.Model and
per-view KeyMap structs. Replace custom renderBar() in disk usage
with bubbles/progress.ViewAs(). Rewrite TableModel as a wrapper
around bubbles/table while preserving the same public API so all
consumer models need zero changes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Lipgloss v2 uses full SGR reset (\x1b[m) at the end of Render(),
which breaks outer background colors when ANSI is nested. This caused
two visual bugs:

1. Footer: bubbles/help inserts unstyled spaces between key and desc
   text. Replaced help.Model.View() with manual rendering that styles
   every character (including spaces) with the footer background.

2. Selected row: pre-rendered ANSI in table cells (running/stopped
   foreground colors) reset the Selected style's background. Removed
   all ANSI pre-rendering from table cells — toBubblesRows() now
   passes plain text so the bubbles table's Selected style applies
   cleanly.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The prompt and input prompt overlays were appended below the full
screen (main + footer), pushing them past the terminal viewport.
Users could not see confirmation prompts, making commands like
"rm unused" appear broken. Fix by replacing the footer with the
prompt view instead of appending below it.

Also complete all footer key maps with missing action keys:
- Containers: logs, stats, rm, rm stopped, kill, restart, stop
- Images: enter (inspect), % (filter)
- Networks: % (filter)
- Volumes: % (filter)
- Nodes: i (inspect), % (filter)
- Services: enter (tasks), i (inspect)
- Stacks: enter (tasks)

Remove dead containerRow.running field, fix stale help.KeyMap comment.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Use ▶ (green) for running and ■ (red) for stopped containers.
ColorFg helper uses targeted ANSI (SGR 38/39) that only resets
the foreground, preserving the selected row background highlight.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…oter

Replace plain text widget headers with full-width accent bars featuring
per-view icons, styled title/count, and filter info on a charcoal
background. Change selected row color from bright purple to dark teal
(#1E3D3D). Hide swarm navigation keys (5/6/7) from footer and skip
swarm API calls when Docker Swarm is not active.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace CrushBlack with CrushLight (mid-tone warm background, near-black
text, darkened accents). Add RefreshStyles to TableModel and all sub-models
so Ctrl+0 theme rotation updates inner table styles. Use ColorFg with
foreground-only ANSI resets for cell text to preserve uniform selected row
background. Refine dark theme: warm gold Key (#E8A848), dark teal-blue
selection (#182838), themed container indicators (gold ▶ / dim ■).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace electric purple/magenta with copper (#D08850) primary and
warm rose (#E07890) secondary to create a cohesive warm-earthy palette
alongside the gold key and teal-blue selection.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… stats

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@moncho moncho requested a review from Copilot February 25, 2026 21:23
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Ports dry’s TUI to Bubbletea/Bubbles/Lipgloss v2, removing the legacy tcell/termui rendering stack and adopting an Elm-style architecture with view sub-models and shared table behaviors.

Changes:

  • Replaced the old termui/tcell UI widgets + event loop with Bubbletea v2 tea.Program and sub-models per view.
  • Introduced new list/table-based models (containers/images/networks/volumes + swarm views) with filter/sort/navigation, plus overlay models (prompt, input prompt, menu).
  • Updated theming/styling to Lipgloss v2 and removed golden/termui-based rendering tests and helpers.

Reviewed changes

Copilot reviewed 140 out of 175 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
ui/list_test.go Removed legacy termui list unit test.
ui/list.go Removed legacy termui list factory helper.
ui/less_test.go Removed legacy less-scrolling tests tied to old screen implementation.
ui/key.go Removed unused legacy Key type relying on tcell.
ui/focus.go Removed legacy focus interface based on tcell events.
ui/expiring_message.go Removed legacy expiring message widget tied to ActiveScreen.
ui/events.go Removed legacy tcell event source abstraction.
ui/cursor_test.go Removed tests for legacy cursor implementation.
ui/cursor.go Removed legacy cursor implementation.
ui/colorize_test.go Removed tests for markup-tag based colorizers.
ui/colorize.go Replaced markup-tag colorizers with Lipgloss v2 style rendering.
main.go Switched entrypoint to Bubbletea v2 program (tea.NewProgram) and removed loading/whale path.
go.mod Updated Go/tooling + dependencies to Bubbletea/Bubbles/Lipgloss v2 and removed termui/tcell deps.
appui/volumes_test.go Removed termui golden tests for volumes widget.
appui/volumes_model.go Added Bubbletea sub-model for volumes list using shared TableModel and filter input.
appui/volume_row.go Removed legacy termui-based VolumeRow widget.
appui/ui_events.go Removed legacy widget event/filter/sort interfaces tied to termui.
appui/ui.go Updated UI constants (header sizing) and removed legacy width calc helper.
appui/top.go Removed legacy docker top termui renderer.
appui/theme.go Replaced termui color themes with Lipgloss-based ui.Theme palettes.
appui/testing.go Removed legacy golden-test helpers and -update flag.
appui/testdata/TestVolumesWidget_sort_volumes.golden Removed legacy volumes golden output.
appui/testdata/TestVolumesWidget_show_last_4_volumes.golden Removed legacy volumes golden output.
appui/testdata/TestVolumesWidget_show_first_4_volumes.golden Removed legacy volumes golden output.
appui/testdata/TestVolumesWidget_mounted_widget_two_volumes.golden Removed legacy volumes golden output.
appui/testdata/TestVolumesWidget_mounted_widget_no_volumes.golden Removed legacy volumes golden output.
appui/testdata/TestVolumesWidget_filter_volumes.golden Removed legacy volumes golden output.
appui/testdata/TestVolumesWidget_double_sort_volumes.golden Removed legacy volumes golden output.
appui/testdata/DiskUsageTest_noPruneReport.golden Removed legacy disk usage golden output.
appui/testdata/DiskUsageTest.golden Removed legacy disk usage golden output.
appui/swarm/testdata/service_info.golden Removed legacy service info golden output.
appui/swarm/tasks_model.go Added Bubbletea sub-model for swarm tasks list using shared TableModel.
appui/swarm/task_row_test.go Removed legacy termui-based task row test.
appui/swarm/task_row.go Removed legacy termui-based task row widget.
appui/swarm/swarm_test.go Added new Bubbletea-oriented tests for swarm models.
appui/swarm/stacks_model.go Added Bubbletea sub-model for stacks list with filter and table.
appui/swarm/stack_tasks.go Removed legacy termui-based stack tasks widget.
appui/swarm/stack_row_test.go Removed legacy stack row widget test.
appui/swarm/stack_row.go Removed legacy stack row widget.
appui/swarm/services_model.go Added Bubbletea sub-model for services list with filter and table.
appui/swarm/service_tasks.go Removed legacy termui-based service tasks widget.
appui/swarm/service_row.go Removed legacy service row widget.
appui/swarm/service_info_test.go Removed legacy service info golden test.
appui/swarm/service_info.go Removed legacy service info widget/renderer.
appui/swarm/nodes_test.go Removed legacy nodes widget tests.
appui/swarm/nodes_model.go Added Bubbletea sub-model for nodes list with filter and table.
appui/swarm/node_tasks.go Removed legacy termui-based node tasks widget.
appui/swarm/node_row_test.go Removed legacy node row widget test.
appui/swarm/node_row.go Removed legacy node row widget.
appui/styles.go Added Lipgloss-derived styles + widget header rendering + targeted foreground coloring helper.
appui/stream.go Removed legacy stream “less” viewer integration.
appui/stats_row_test.go Removed legacy container stats row tests.
appui/screen.go Removed legacy termui screen abstractions.
appui/run_image.go Removed legacy run-image input widget.
appui/row_filter_test.go Removed legacy row filter tests tied to termui columns.
appui/row_filter.go Removed legacy row filtering helpers tied to termui columns.
appui/row.go Removed legacy base row type for termui widgets.
appui/prompt_model.go Added Bubbletea prompt overlay model (y/N).
appui/prompt.go Removed legacy prompt widget (termui textinput).
appui/networks_model.go Added Bubbletea sub-model for networks list with filter and table.
appui/network_row.go Removed legacy network row widget.
appui/monitor_test.go Removed legacy monitor widget test.
appui/monitor_header.go Removed legacy monitor table header builder.
appui/message_bar.go Added model for timed status messages (auto-expiring).
appui/less.go Removed legacy less viewer helper tied to tcell events.
appui/inspect.go Removed legacy JSON inspect renderer.
appui/input_prompt_model.go Added Bubbletea input prompt overlay model (text input).
appui/input.go Removed legacy no-op input helper.
appui/images_test.go Removed legacy images widget tests (scrolling/visibility).
appui/images_model.go Added Bubbletea sub-model for images list with filter and table.
appui/image_row.go Removed legacy image row widget.
appui/image_history.go Removed legacy image history renderer.
appui/header_test.go Removed legacy widget header test.
appui/header_model.go Added Bubbletea header model rendering Docker daemon info and separator.
appui/header.go Removed legacy widget header implementation.
appui/filter_input.go Added Bubbletea filter input model using Bubbles v2 textinput.
appui/events.go Removed legacy docker events renderer.
appui/docker_info_test.go Removed legacy docker info golden tests.
appui/docker_info.go Removed legacy docker info widget.
appui/disk_usage_test.go Removed legacy disk usage renderer tests.
appui/disk_usage_model.go Added Bubbletea disk usage model with progress bars.
appui/containers_model.go Added Bubbletea sub-model for containers list with sort/show-all/filter and table.
appui/container_row.go Removed legacy container row widget.
appui/container_menu_test.go Removed legacy container menu widget tests.
appui/container_menu_model.go Added Bubbletea container command menu overlay model.
appui/container_menu.go Removed legacy container menu widget.
appui/container_details_test.go Removed legacy container details widget test.
appui/container_details.go Removed legacy container details widget.
appui/container.go Removed legacy container info renderer.
appui/appui.go Removed legacy invalidRow error helper.
app/widget_registry.go Removed legacy widget registry (termui widgets lifecycle).
app/volume_events.go Removed legacy volumes event handler (tcell-based).
app/view.go Updated view mode enum/comments for new architecture.
app/stacktasks_events.go Removed legacy stack tasks event handler.
app/stack_events.go Removed legacy stacks event handler.
app/servicetasks_events.go Removed legacy service tasks event handler.
app/service_events.go Removed legacy services event handler.
app/render.go Removed legacy termui-based renderer + footer rendering.
app/nodetasks_events.go Removed legacy node tasks event handler.
app/node_events.go Removed legacy nodes event handler.
app/network_events.go Removed legacy networks event handler.
app/monitor_events.go Removed legacy monitor event handler.
app/misc_test.go Removed legacy unit test for logs duration curation helper.
app/misc.go Removed legacy misc helpers (events, inspect, less, logs prompt).
app/messages.go Added Bubbletea message types for docker data, refresh, overlays, status messages.
app/loop.go Removed legacy render loop/event loop (tcell + termui).
app/help_texts.go Refactored help text generation into Help() function and updated keybind descriptions.
app/filter_event.go Removed legacy filter prompt flow.
app/events.go Removed legacy event handling/router and forwarder abstractions.
app/dry.go Removed legacy Dry app struct tied to old UI/event loop.
app/df_events.go Removed legacy disk usage event handler.
CLAUDE.md Added repository guidance for Claude Code (architecture, commands, conventions).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.


// SetWidth sets the input width.
func (m *FilterInputModel) SetWidth(w int) {
m.width = w
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SetWidth only updates the outer lipgloss container width, but the underlying textinput.Model keeps its own width for rendering/editing. This can lead to truncation/overflow or awkward cursor behavior. Consider calling m.input.SetWidth(w) (or w - lipgloss.Width(prompt) depending on desired UX) inside SetWidth.

Suggested change
m.width = w
m.width = w
inputWidth := w - lipgloss.Width(m.input.Prompt)
if inputWidth < 0 {
inputWidth = 0
}
m.input.SetWidth(inputWidth)

Copilot uses AI. Check for mistakes.
Comment on lines 144 to 153
func (m NodesModel) widgetHeader() string {
return appui.RenderWidgetHeader(appui.WidgetHeaderOpts{
Icon: "🖥️",
Title: "Nodes",
Total: m.table.TotalRowCount(),
Filtered: m.table.TotalRowCount(),
Width: m.table.Width(),
Accent: appui.DryTheme.Success,
})
}
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When filtering is active, the header won’t reflect it: Filtered is set to TotalRowCount() and Filter isn’t passed. Align this with other models (e.g., containers/images) by using Filtered: m.table.RowCount() and Filter: m.table.FilterText() so the UI correctly displays filtered counts and active filter text.

Copilot uses AI. Check for mistakes.
Comment on lines 142 to 151
func (m ServicesModel) widgetHeader() string {
return appui.RenderWidgetHeader(appui.WidgetHeaderOpts{
Icon: "⚙",
Title: "Services",
Total: m.table.TotalRowCount(),
Filtered: m.table.TotalRowCount(),
Width: m.table.Width(),
Accent: appui.DryTheme.Primary,
})
}
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue as NodesModel: Filtered should be the filtered row count (likely m.table.RowCount()), and the active filter text should be wired into the header (Filter: m.table.FilterText()). Otherwise the header won’t show filter state/count deltas.

Copilot uses AI. Check for mistakes.
Comment on lines 139 to 148
func (m StacksModel) widgetHeader() string {
return appui.RenderWidgetHeader(appui.WidgetHeaderOpts{
Icon: "📚",
Title: "Stacks",
Total: m.table.TotalRowCount(),
Filtered: m.table.TotalRowCount(),
Width: m.table.Width(),
Accent: appui.DryTheme.Error,
})
}
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

StacksModel supports filtering (FilterInputModel + SetFilter), but the header always shows Total → Total and never shows the filter text. Use Filtered: m.table.RowCount() and provide Filter: m.table.FilterText().

Copilot uses AI. Check for mistakes.
Comment on lines +25 to +27
// InitStyles rebuilds all derived styles from DryTheme.
// Call after rotating the color theme.
func InitStyles() {
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Styles are derived from the global DryTheme, but there’s no guarantee they’ll be regenerated when DryTheme is rotated (the existing RotateColorTheme only swaps the pointer per the snippet). Consider updating RotateColorTheme() to call InitStyles() (or returning the new theme and having the app explicitly call InitStyles) to keep the UI consistent after theme changes.

Copilot uses AI. Check for mistakes.
var (
pepper = lipgloss.Color("#201F26")
charcoal = lipgloss.Color("#3A3943")
charple = lipgloss.Color("#D08850")
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The name charple strongly implies a purple hue, but #D08850 is an orange/brown tone. Either adjust the hex value to match the intended palette color, or rename the variable so it reflects the actual color to avoid confusion when theming/tweaking.

Suggested change
charple = lipgloss.Color("#D08850")
charple = lipgloss.Color("#A550DF")

Copilot uses AI. Check for mistakes.
Comment on lines +11 to +23
}

// SetMessage sets a status message that auto-clears after the given duration.
func (m *MessageBarModel) SetMessage(text string, duration time.Duration) {
m.text = text
m.expiry = time.Now().Add(duration)
}

// Message returns the active message text, or "" if expired/unset.
func (m MessageBarModel) Message() string {
if m.text == "" || time.Now().After(m.expiry) {
return ""
}
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This introduces time-based behavior (expiry) but there’s no accompanying unit test. Adding tests for (1) message visible before expiry, (2) cleared after expiry, and (3) empty/unset behavior will help prevent regressions; if you want deterministic tests, consider injecting a clock (e.g., now func() time.Time) into the model.

Suggested change
}
// SetMessage sets a status message that auto-clears after the given duration.
func (m *MessageBarModel) SetMessage(text string, duration time.Duration) {
m.text = text
m.expiry = time.Now().Add(duration)
}
// Message returns the active message text, or "" if expired/unset.
func (m MessageBarModel) Message() string {
if m.text == "" || time.Now().After(m.expiry) {
return ""
}
now func() time.Time
}
// SetMessage sets a status message that auto-clears after the given duration.
func (m *MessageBarModel) SetMessage(text string, duration time.Duration) {
m.text = text
now := time.Now
if m.now != nil {
now = m.now
}
m.expiry = now().Add(duration)
}
// Message returns the active message text, or "" if expired/unset.
func (m MessageBarModel) Message() string {
if m.text == "" {
return ""
}
now := time.Now
if m.now != nil {
now = m.now
}
if now().After(m.expiry) {
return ""
}

Copilot uses AI. Check for mistakes.
moncho and others added 2 commits February 26, 2026 09:10
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
moncho and others added 13 commits February 26, 2026 13:35
Fix race condition in RemoveDanglingImages (local err variable), split
shared context for local/swarm event goroutines, signal eventsDone and
stop monitor on quit, guard nil Replicas pointer in services view, guard
nil ContainerSpec in TaskStringer, and sync sort mode with table column
via new SetSortField method.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…annel

Replace the internal done channel and coordinator goroutine in
DockerDaemon.Events() with a caller-owned context.Context for
cancellation. This simplifies shutdown logic in the app model
(eventsCancel func vs channel send) and is more idiomatic Go.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix timestamp: use time.Unix(0, e.TimeNano) since TimeNano is a full
  nanosecond epoch, not a sub-second offset
- Fix EventLog.Peek() panic on empty log (index at -1)
- Guard against nil EventLog in showDockerEventsCmd
- Remove unused error return from EventCallback signature
- Fall back to Actor.Attributes["name"] when Actor.ID is empty
- Fix flaky TestStreamEvents_NoPanicAfterChannelDrain (use unbuffered
  channel for deterministic select behavior)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The events view (F9) now shows new events in real time as they arrive,
instead of requiring the user to close and reopen it. Uses the existing
LessModel.AppendContent() and following mode pattern already used for
streaming container logs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
HIGH: Events channel now closes on daemon disconnect (innerCtx pattern);
nil UsageData guard in disk usage view.
MEDIUM: Filter input keys no longer intercepted by global handlers;
deterministic monitor row ordering; swarm headers show correct filtered
count; Prune() uses timeout; MessageBar auto-clears via tea.Tick.
LOW: Remove 4 dead message types; O(n) AppendContent; thread-safe
EventLog.Count().

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Monitor goroutine leak: Start() now runs directly in switchView
  (on the returned copy) instead of inside loadViewData (nested copy
  whose cancel funcs were discarded). Goroutines no longer accumulate
  on repeated enter/leave of Monitor view.
- Container store add() no-op: fix slice removal from c.c[pos:] to
  c.c[pos+1:] so updated containers replace the old entry in both
  map and slice. Add test for replacement behavior.
- StackRemove timeout: move context.WithTimeout to just before the
  remove operations so listing calls don't consume the 10s budget.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove Config.dockerEnv(), connection headers var, ContainerFormatter.
fullHeader(), defaultQuietFormat const, unused lines test const, and
commented-out ansiParser.applyEscape().

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove unnecessary fmt.Sprintf for plain string, suppress unused
value assignments in tests with blank identifier.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove redundant embedded Annotations field from swarm Spec selectors
and lowercase error string per Go conventions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix impossible len() < 0 check (SA4024)
- Lowercase error strings (ST1005)
- Replace deprecated certPool.Subjects() (SA1019)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@moncho moncho merged commit 8f76a60 into master Feb 26, 2026
1 check failed
@moncho moncho deleted the port-to-bubbletea2 branch February 26, 2026 19:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants