refactor: улучшить обработку сообщений и оптимизировать код в различных компонентах

This commit is contained in:
Aslan Dukaev 2026-01-18 23:58:31 +03:00
commit db2639edf2
11 changed files with 48 additions and 54 deletions

View file

@ -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

View file

@ -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 {

View file

@ -1,3 +1,4 @@
// Package details provides the file details pane for displaying information about selected files.
package details
import (

View file

@ -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 -│

View file

@ -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))
}

View file

@ -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 {

View file

@ -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)))

View file

@ -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 {

View file

@ -1,3 +1,4 @@
// Package utils provides utility functions for formatting values used throughout the UI.
package utils
import "fmt"

4
go.mod
View file

@ -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

7
go.sum
View file

@ -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=