From db2639edf28885f6bb6177535d9a48a91e2cf39a Mon Sep 17 00:00:00 2001 From: Aslan Dukaev Date: Sun, 18 Jan 2026 23:58:31 +0300 Subject: [PATCH] =?UTF-8?q?refactor:=20=D1=83=D0=BB=D1=83=D1=87=D1=88?= =?UTF-8?q?=D0=B8=D1=82=D1=8C=20=D0=BE=D0=B1=D1=80=D0=B0=D0=B1=D0=BE=D1=82?= =?UTF-8?q?=D0=BA=D1=83=20=D1=81=D0=BE=D0=BE=D0=B1=D1=89=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D0=B9=20=D0=B8=20=D0=BE=D0=BF=D1=82=D0=B8=D0=BC=D0=B8=D0=B7?= =?UTF-8?q?=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D1=82=D1=8C=20=D0=BA=D0=BE=D0=B4?= =?UTF-8?q?=20=D0=B2=20=D1=80=D0=B0=D0=B7=D0=BB=D0=B8=D1=87=D0=BD=D1=8B?= =?UTF-8?q?=D1=85=20=D0=BA=D0=BE=D0=BC=D0=BF=D0=BE=D0=BD=D0=B5=D0=BD=D1=82?= =?UTF-8?q?=D0=B0=D1=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd/dive/cli/internal/ui/v2/app/model.go | 22 +++++++------------ .../ui/v2/components/copiable_value.go | 16 ++++++++------ .../cli/internal/ui/v2/panes/details/pane.go | 1 + .../filetree/__snapshots__/pane_test.snap | 10 ++++----- .../ui/v2/panes/filetree/node_renderer.go | 7 ++++-- .../cli/internal/ui/v2/panes/filetree/pane.go | 14 +++++++----- .../cli/internal/ui/v2/panes/image/pane.go | 7 +++--- .../cli/internal/ui/v2/panes/layers/pane.go | 13 ++--------- cmd/dive/cli/internal/ui/v2/utils/format.go | 1 + go.mod | 4 ++-- go.sum | 7 +++--- 11 files changed, 48 insertions(+), 54 deletions(-) diff --git a/cmd/dive/cli/internal/ui/v2/app/model.go b/cmd/dive/cli/internal/ui/v2/app/model.go index ce2f97a..a2391d3 100644 --- a/cmd/dive/cli/internal/ui/v2/app/model.go +++ b/cmd/dive/cli/internal/ui/v2/app/model.go @@ -264,7 +264,6 @@ func (m *Model) recalculateLayout() { // Update implements tea.Model func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmds []tea.Cmd - var handled bool // Track if message was already handled switch msg := msg.(type) { case tea.KeyMsg: @@ -290,7 +289,6 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { cmds = append(cmds, cmd) } } - handled = true // Global key bindings switch msg.String() { @@ -392,7 +390,6 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.panes[PaneTree] = newTree } m.updateTreeForCurrentLayer() - handled = true case filetreepane.NodeToggledMsg: // Forward message to tree pane to refresh its visibleNodes cache @@ -403,14 +400,12 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { newPane, cmd := m.panes[PaneTree].Update(msg) m.panes[PaneTree] = newPane cmds = append(cmds, cmd) - handled = true case filetreepane.RefreshTreeContentMsg: // Request to refresh tree content // POLYMORPHISM: Send message through interface, no type assertion newPane, _ := m.panes[PaneTree].Update(filetreepane.UpdateViewModelMsg{TreeVM: m.treeVM}) m.panes[PaneTree] = newPane - handled = true case tea.MouseMsg: // BUBBLEZONE: Check which pane was clicked using zone hit testing @@ -455,7 +450,6 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { break } } - handled = true case tea.WindowSizeMsg: m.width = msg.Width @@ -483,19 +477,15 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { layoutCmds = append(layoutCmds, cmd) } cmds = append(cmds, layoutCmds...) - handled = true default: // Forward all OTHER messages (e.g., tickMsg from CopiableValue timers) // to the active pane. This is critical for component internal timers to work. // Without this, messages from child components are lost. - // IMPORTANT: Only forward if not already handled above to prevent double-processing - if !handled { - if activePane, ok := m.panes[m.activePane]; ok { - updatedPane, cmd := activePane.Update(msg) - m.panes[m.activePane] = updatedPane - cmds = append(cmds, cmd) - } + if activePane, ok := m.panes[m.activePane]; ok { + updatedPane, cmd := activePane.Update(msg) + m.panes[m.activePane] = updatedPane + cmds = append(cmds, cmd) } } @@ -851,6 +841,10 @@ func (m Model) updateSearch(msg tea.Msg) (tea.Model, tea.Cmd) { // Empty pattern - clear filter m.totalMatches = 0 m.currentMatch = -1 + + // CRITICAL: Apply empty filter to reset UI state + // This disables Flat mode, removes layer highlighting, and clears file highlighting + m.applyFilter("") } return m, cmd diff --git a/cmd/dive/cli/internal/ui/v2/components/copiable_value.go b/cmd/dive/cli/internal/ui/v2/components/copiable_value.go index c11db2f..fe8b144 100644 --- a/cmd/dive/cli/internal/ui/v2/components/copiable_value.go +++ b/cmd/dive/cli/internal/ui/v2/components/copiable_value.go @@ -2,6 +2,7 @@ package components import ( "bytes" + "context" "os/exec" "runtime" "strings" @@ -112,7 +113,7 @@ func (c CopiableValue) View() string { // Pad icon to match the original value width paddingNeeded := c.width - visualWidth if paddingNeeded > 0 { - iconText = iconText + strings.Repeat(" ", paddingNeeded) + iconText += strings.Repeat(" ", paddingNeeded) } } return lipgloss.NewStyle(). @@ -137,7 +138,7 @@ func (c CopiableValue) View() string { // Pad with spaces on the right to match exact width paddingNeeded := c.width - textWidth if paddingNeeded > 0 { - text = text + strings.Repeat(" ", paddingNeeded) + text += strings.Repeat(" ", paddingNeeded) } } } @@ -166,21 +167,22 @@ func (c CopiableValue) GetVisualWidth() int { func copyToClipboard(text string) tea.Cmd { return func() tea.Msg { var cmd *exec.Cmd + ctx := context.Background() switch runtime.GOOS { case "darwin": - cmd = exec.Command("pbcopy") + cmd = exec.CommandContext(ctx, "pbcopy") case "linux": // Try xclip first, then wl-copy, then xsel if _, err := exec.LookPath("xclip"); err == nil { - cmd = exec.Command("xclip", "-selection", "clipboard") + cmd = exec.CommandContext(ctx, "xclip", "-selection", "clipboard") } else if _, err := exec.LookPath("wl-copy"); err == nil { - cmd = exec.Command("wl-copy") + cmd = exec.CommandContext(ctx, "wl-copy") } else if _, err := exec.LookPath("xsel"); err == nil { - cmd = exec.Command("xsel", "--clipboard", "--input") + cmd = exec.CommandContext(ctx, "xsel", "--clipboard", "--input") } case "windows": - cmd = exec.Command("clip") + cmd = exec.CommandContext(ctx, "clip") } if cmd != nil { diff --git a/cmd/dive/cli/internal/ui/v2/panes/details/pane.go b/cmd/dive/cli/internal/ui/v2/panes/details/pane.go index 98383f1..979ae44 100644 --- a/cmd/dive/cli/internal/ui/v2/panes/details/pane.go +++ b/cmd/dive/cli/internal/ui/v2/panes/details/pane.go @@ -1,3 +1,4 @@ +// Package details provides the file details pane for displaying information about selected files. package details import ( diff --git a/cmd/dive/cli/internal/ui/v2/panes/filetree/__snapshots__/pane_test.snap b/cmd/dive/cli/internal/ui/v2/panes/filetree/__snapshots__/pane_test.snap index 5468330..f8ceeae 100755 --- a/cmd/dive/cli/internal/ui/v2/panes/filetree/__snapshots__/pane_test.snap +++ b/cmd/dive/cli/internal/ui/v2/panes/filetree/__snapshots__/pane_test.snap @@ -25,7 +25,7 @@ [TestPane_View_WithTree - 1] ┌─Current Layer Contents─────────────────────────┐ │Name Size UID:GID│ -│├─󰝰 bin -│ +│├─󰝰 bin 1.1M -│ ││ ├─󰈔 [ 1.0M -│ ││ ├─󰈔 [[ 0 -│ ││ ├─󰈔 acpid 0 -│ @@ -48,7 +48,7 @@ [TestPane_View_Focused - 1] ┌─Current Layer Contents─────────────────────────┐ │Name Size UID:GID│ -│├─󰝰 bin -│ +│├─󰝰 bin 1.1M -│ ││ ├─󰈔 [ 1.0M -│ ││ ├─󰈔 [[ 0 -│ ││ ├─󰈔 acpid 0 -│ @@ -94,7 +94,7 @@ [TestPane_View_SmallHeight - 1] ┌─Current Layer Contents─────────────────────────┐ │Name Size UID:GID│ -│├─󰝰 bin -│ +│├─󰝰 bin 1.1M -│ ││ ├─󰈔 [ 1.0M -│ ││ ├─󰈔 [[ 0 -│ ││ ├─󰈔 acpid 0 -│ @@ -105,7 +105,7 @@ [TestPane_View_LargeSize - 1] ┌─Current Layer Contents───────────────────────────────────────────────────────────────────────────────────────────────┐ │Name Size UID:GID Permissions│ -│├─󰝰 bin - drwxr-xr-x│ +│├─󰝰 bin 1.1M - drwxr-xr-x│ ││ ├─󰈔 [ 1.0M - -rwxr-xr-x│ ││ ├─󰈔 [[ 0 - -rwxr-xr-x│ ││ ├─󰈔 acpid 0 - -rwxr-xr-x│ @@ -148,7 +148,7 @@ [TestPane_Update_WithLayoutMsg - 1] ┌─Current Layer Contents─────────────────────────┐ │Name Size UID:GID│ -│├─󰝰 bin -│ +│├─󰝰 bin 1.1M -│ ││ ├─󰈔 [ 1.0M -│ ││ ├─󰈔 [[ 0 -│ ││ ├─󰈔 acpid 0 -│ diff --git a/cmd/dive/cli/internal/ui/v2/panes/filetree/node_renderer.go b/cmd/dive/cli/internal/ui/v2/panes/filetree/node_renderer.go index e4cf864..ac7237f 100644 --- a/cmd/dive/cli/internal/ui/v2/panes/filetree/node_renderer.go +++ b/cmd/dive/cli/internal/ui/v2/panes/filetree/node_renderer.go @@ -62,8 +62,11 @@ func RenderRow(node *filetree.FileNode, prefix string, displayName string, isSel var sizeStr, uidGid, perm string // Only compute strings if they will be shown (optimization) - if showSize && !node.Data.FileInfo.IsDir() { - size := node.Data.FileInfo.Size + if showSize { + // Use node.GetSize() for both files and directories + // For files: returns the file size + // For directories: recursively calculates total size of all children + size := node.GetSize() if size >= 0 { sizeStr = utils.FormatSize(uint64(size)) } diff --git a/cmd/dive/cli/internal/ui/v2/panes/filetree/pane.go b/cmd/dive/cli/internal/ui/v2/panes/filetree/pane.go index b2c6542..74de380 100644 --- a/cmd/dive/cli/internal/ui/v2/panes/filetree/pane.go +++ b/cmd/dive/cli/internal/ui/v2/panes/filetree/pane.go @@ -2,6 +2,7 @@ package filetree import ( "bytes" + "context" "fmt" "os/exec" "regexp" @@ -355,7 +356,7 @@ func (p *Pane) Update(msg tea.Msg) (common.Pane, tea.Cmd) { p.copyNoticePath = msg.Path // copiedNodeIndex is already set by the right-click handler // Hide notification after 2 seconds - return p, tea.Tick(2*time.Second, func(t time.Time) tea.Msg { + return p, tea.Tick(2*time.Second, func(_ time.Time) tea.Msg { return HideCopyNoticeMsg{} }) } @@ -811,21 +812,22 @@ func (p *Pane) setExclusiveFilter(filterType string) { func copyPathToClipboard(path string) tea.Cmd { return func() tea.Msg { var cmd *exec.Cmd + ctx := context.Background() switch runtime.GOOS { case "darwin": - cmd = exec.Command("pbcopy") + cmd = exec.CommandContext(ctx, "pbcopy") case "linux": // Try xclip first, then wl-copy, then xsel if _, err := exec.LookPath("xclip"); err == nil { - cmd = exec.Command("xclip", "-selection", "clipboard") + cmd = exec.CommandContext(ctx, "xclip", "-selection", "clipboard") } else if _, err := exec.LookPath("wl-copy"); err == nil { - cmd = exec.Command("wl-copy") + cmd = exec.CommandContext(ctx, "wl-copy") } else if _, err := exec.LookPath("xsel"); err == nil { - cmd = exec.Command("xsel", "--clipboard", "--input") + cmd = exec.CommandContext(ctx, "xsel", "--clipboard", "--input") } case "windows": - cmd = exec.Command("clip") + cmd = exec.CommandContext(ctx, "clip") } if cmd != nil { diff --git a/cmd/dive/cli/internal/ui/v2/panes/image/pane.go b/cmd/dive/cli/internal/ui/v2/panes/image/pane.go index cc8dd7e..08096fe 100644 --- a/cmd/dive/cli/internal/ui/v2/panes/image/pane.go +++ b/cmd/dive/cli/internal/ui/v2/panes/image/pane.go @@ -179,11 +179,12 @@ func (m *Pane) generateContent() string { header.WriteString(styles.LayerHeaderStyle.Render("Image efficiency score:")) header.WriteString(" ") scoreStyle := styles.LayerValueStyle - if stats.EfficiencyScore >= 90 { + switch { + case stats.EfficiencyScore >= 90: scoreStyle = scoreStyle.Foreground(styles.SuccessColor) - } else if stats.EfficiencyScore >= 70 { + case stats.EfficiencyScore >= 70: scoreStyle = scoreStyle.Foreground(styles.HighlightColor) - } else { + default: scoreStyle = scoreStyle.Foreground(styles.WarningColor) } header.WriteString(scoreStyle.Render(fmt.Sprintf("%.0f%%", stats.EfficiencyScore))) diff --git a/cmd/dive/cli/internal/ui/v2/panes/layers/pane.go b/cmd/dive/cli/internal/ui/v2/panes/layers/pane.go index 8d0f4e8..b200721 100644 --- a/cmd/dive/cli/internal/ui/v2/panes/layers/pane.go +++ b/cmd/dive/cli/internal/ui/v2/panes/layers/pane.go @@ -653,17 +653,8 @@ func (m *Pane) generateContent() string { matchStyle := lipgloss.NewStyle(). Foreground(styles.HighlightColor). Bold(true) - - // If this row is also selected, we need to be careful about styling - // to avoid color conflicts with the selection background - if i == m.layerIndex { - // For selected row, keep the highlight color but it may blend with selection - // The selection background (PanelBgColor) should work with yellow text - prefix = matchStyle.Render(prefixStr) - } else { - // Normal row - just apply highlight - prefix = matchStyle.Render(prefixStr) - } + // Apply highlight style (works for both selected and normal rows) + prefix = matchStyle.Render(prefixStr) } else { // No matches - use normal styling if i == m.layerIndex { diff --git a/cmd/dive/cli/internal/ui/v2/utils/format.go b/cmd/dive/cli/internal/ui/v2/utils/format.go index 02a71ce..540c7d3 100644 --- a/cmd/dive/cli/internal/ui/v2/utils/format.go +++ b/cmd/dive/cli/internal/ui/v2/utils/format.go @@ -1,3 +1,4 @@ +// Package utils provides utility functions for formatting values used throughout the UI. package utils import "fmt" diff --git a/go.mod b/go.mod index 0a7ee9b..a9ecf36 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,8 @@ module github.com/wagoodman/dive go 1.24.2 require ( + charm.land/bubbles/v2 v2.0.0-rc.1 + charm.land/bubbletea/v2 v2.0.0-rc.1.0.20251106192006-06c0cda318b3 github.com/anchore/clio v0.0.0-20250401141128-4c1d6bd1e872 github.com/anchore/go-logger v0.0.0-20250318195838-07ae343dd722 github.com/awesome-gocui/gocui v1.1.0 @@ -37,8 +39,6 @@ require ( ) require ( - charm.land/bubbles/v2 v2.0.0-rc.1 // indirect - charm.land/bubbletea/v2 v2.0.0-rc.1.0.20251106192006-06c0cda318b3 // indirect charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251106192539-4b304240aab7 // indirect dario.cat/mergo v1.0.1 // indirect github.com/Microsoft/go-winio v0.4.14 // indirect diff --git a/go.sum b/go.sum index 6b9b2bb..17d1b3f 100644 --- a/go.sum +++ b/go.sum @@ -31,8 +31,8 @@ github.com/awesome-gocui/keybinding v1.0.1-0.20211011072933-86029037a63f/go.mod github.com/awesome-gocui/termbox-go v0.0.0-20190427202837-c0aef3d18bcc/go.mod h1:tOy3o5Nf1bA17mnK4W41gD7PS3u4Cv0P0pqFcoWMy8s= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= -github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= -github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= +github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY= +github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl8ZBcNLgcbrw8E= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= @@ -51,9 +51,8 @@ github.com/charmbracelet/x/ansi v0.11.3 h1:6DcVaqWI82BBVM/atTyq6yBoRLZFBsnoDoX9G github.com/charmbracelet/x/ansi v0.11.3/go.mod h1:yI7Zslym9tCJcedxz5+WBq+eUGMJT0bM06Fqy1/Y4dI= github.com/charmbracelet/x/cellbuf v0.0.14 h1:iUEMryGyFTelKW3THW4+FfPgi4fkmKnnaLOXuc+/Kj4= github.com/charmbracelet/x/cellbuf v0.0.14/go.mod h1:P447lJl49ywBbil/KjCk2HexGh4tEY9LH0/1QrZZ9rA= -github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ= -github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f h1:pk6gmGpCE7F3FcjaOEKYriCvpmIN4+6OS/RD0vm4uIA= +github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f/go.mod h1:IfZAMTHB6XkZSeXUqriemErjAWCCzT0LwjKFYCZyw0I= github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk= github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI= github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY=