diff --git a/cmd/dive/cli/internal/ui/v2/app/messages.go b/cmd/dive/cli/internal/ui/v2/app/messages.go index 0c88a38..eec2f1d 100644 --- a/cmd/dive/cli/internal/ui/v2/app/messages.go +++ b/cmd/dive/cli/internal/ui/v2/app/messages.go @@ -1,37 +1,21 @@ package app +// Note: LayerChangedMsg is now defined in panes/layers package +// Note: NodeToggledMsg, TreeSelectionChangedMsg, RefreshTreeContentMsg are now defined in panes/filetree package -// LayerChangedMsg is sent when the active layer changes -type LayerChangedMsg struct { - LayerIndex int -} +// The following message types are kept for potential future use or for app-level coordination -// NodeToggledMsg is sent when a tree node is collapsed/expanded -type NodeToggledMsg struct { - NodeIndex int -} - -// PaneChangedMsg is sent when the active pane changes +// PaneChangedMsg is sent when the active pane changes (for future use) type PaneChangedMsg struct { Pane Pane } -// LayerSelectionChangedMsg is sent when a layer is selected (via click or keyboard) +// LayerSelectionChangedMsg is sent when a layer is selected (for future use) type LayerSelectionChangedMsg struct { LayerIndex int } -// TreeSelectionChangedMsg is sent when a tree node is selected -type TreeSelectionChangedMsg struct { - NodeIndex int -} - -// PaneFocusRequestMsg requests focus to be moved to a specific pane +// PaneFocusRequestMsg requests focus to be moved to a specific pane (for future use) type PaneFocusRequestMsg struct { Pane Pane } - -// RefreshTreeContentMsg requests tree content to be refreshed -type RefreshTreeContentMsg struct { - LayerIndex int -} diff --git a/cmd/dive/cli/internal/ui/v2/app/model.go b/cmd/dive/cli/internal/ui/v2/app/model.go index 4fde6e2..c3ed604 100644 --- a/cmd/dive/cli/internal/ui/v2/app/model.go +++ b/cmd/dive/cli/internal/ui/v2/app/model.go @@ -11,8 +11,13 @@ import ( v1 "github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v1" "github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v1/viewmodel" "github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v2/app/layout" + "github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v2/keys" + filetree "github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v2/panes/filetree" + imagepane "github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v2/panes/image" + "github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v2/panes/details" + "github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v2/panes/layers" + "github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v2/styles" "github.com/wagoodman/dive/dive/image" - v2styles "github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v2/styles" ) // Pane represents a UI pane @@ -77,10 +82,10 @@ type Model struct { layout LayoutCache // Pane components (independent tea.Models) - layersPane LayersPane - detailsPane DetailsPane - imagePane ImagePane - treePane TreePane + layersPane layers.Pane + detailsPane details.Pane + imagePane imagepane.Pane + treePane filetree.Pane // Active pane state activePane Pane @@ -89,7 +94,7 @@ type Model struct { filter FilterModel // Help and key bindings - keys KeyMap + keys keys.KeyMap help help.Model } @@ -118,17 +123,17 @@ func NewModel(analysis image.Analysis, content image.ContentReader, prefs v1.Pre h := help.New() h.Width = 80 - h.Styles.ShortKey = v2styles.StatusStyle - h.Styles.ShortDesc = v2styles.StatusStyle - h.Styles.Ellipsis = v2styles.StatusStyle + h.Styles.ShortKey = styles.StatusStyle + h.Styles.ShortDesc = styles.StatusStyle + h.Styles.Ellipsis = styles.StatusStyle f := NewFilterModel() // Create pane components - layersPane := NewLayersPane(layerVM) - detailsPane := NewDetailsPane() - imagePane := NewImagePane(&analysis) - treePane := NewTreePane(treeVM) + layersPane := layers.New(layerVM) + detailsPane := details.New() + imagePane := imagepane.New(&analysis) + treePane := filetree.New(treeVM) // Set initial focus layersPane.Focus() @@ -149,7 +154,7 @@ func NewModel(analysis image.Analysis, content image.ContentReader, prefs v1.Pre height: 24, quitting: false, activePane: PaneLayer, - keys: Keys, + keys: keys.Keys, help: h, filter: f, } @@ -220,7 +225,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch m.activePane { case PaneLayer: newPane, cmd := m.layersPane.Update(msg) - m.layersPane = newPane.(LayersPane) + m.layersPane = newPane.(layers.Pane) cmds = append(cmds, cmd) case PaneDetails: @@ -228,12 +233,12 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case PaneImage: newPane, cmd := m.imagePane.Update(msg) - m.imagePane = newPane.(ImagePane) + m.imagePane = newPane.(imagepane.Pane) cmds = append(cmds, cmd) case PaneTree: newPane, cmd := m.treePane.Update(msg) - m.treePane = newPane.(TreePane) + m.treePane = newPane.(filetree.Pane) cmds = append(cmds, cmd) } @@ -250,7 +255,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.filter.Show() } - case LayerChangedMsg: + case layers.LayerChangedMsg: // Layer changed - update details pane and tree if m.layerVM != nil && msg.LayerIndex >= 0 && msg.LayerIndex < len(m.layerVM.Layers) { m.detailsPane.SetLayer(m.layerVM.Layers[msg.LayerIndex]) @@ -263,11 +268,11 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.imagePane.Blur() m.treePane.Blur() - case NodeToggledMsg: + case filetree.NodeToggledMsg: // Tree node was toggled - tree pane already updated its content // Nothing to do here - case RefreshTreeContentMsg: + case filetree.RefreshTreeContentMsg: // Request to refresh tree content m.treePane.SetTreeVM(m.treeVM) @@ -287,7 +292,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if y < layersEndY { // Layers pane newPane, cmd := m.layersPane.Update(msg) - m.layersPane = newPane.(LayersPane) + m.layersPane = newPane.(layers.Pane) cmds = append(cmds, cmd) if m.activePane != PaneLayer { m.activePane = PaneLayer @@ -302,7 +307,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } else { // Image pane newPane, cmd := m.imagePane.Update(msg) - m.imagePane = newPane.(ImagePane) + m.imagePane = newPane.(imagepane.Pane) cmds = append(cmds, cmd) if m.activePane != PaneImage { m.activePane = PaneImage @@ -312,7 +317,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } else if inRightCol { // Tree pane newPane, cmd := m.treePane.Update(msg) - m.treePane = newPane.(TreePane) + m.treePane = newPane.(filetree.Pane) cmds = append(cmds, cmd) if m.activePane != PaneTree { m.activePane = PaneTree @@ -391,7 +396,7 @@ func (m *Model) updateTreeForCurrentLayer() { // View implements tea.Model (PURE FUNCTION - no side effects!) func (m Model) View() string { if m.quitting { - return v2styles.TitleStyle.Foreground(v2styles.SuccessColor).Render("Thanks for using Dive V2UI!") + return styles.TitleStyle.Foreground(styles.SuccessColor).Render("Thanks for using Dive V2UI!") } // Calculate layout if not yet calculated (first run) @@ -403,7 +408,7 @@ func (m Model) View() string { statusBar := m.help.View(m.keys) // Add active pane indicator to status bar - paneName := v2styles.StatusStyle.Render(fmt.Sprintf(" Active: %s ", m.activePane)) + paneName := styles.StatusStyle.Render(fmt.Sprintf(" Active: %s ", m.activePane)) statusBar = lipgloss.JoinHorizontal(lipgloss.Top, statusBar, strings.Repeat(" ", 5), paneName) // Render panes directly using their View() methods diff --git a/cmd/dive/cli/internal/ui/v2/app/keys.go b/cmd/dive/cli/internal/ui/v2/keys/keys.go similarity index 89% rename from cmd/dive/cli/internal/ui/v2/app/keys.go rename to cmd/dive/cli/internal/ui/v2/keys/keys.go index 33088c9..6509720 100644 --- a/cmd/dive/cli/internal/ui/v2/app/keys.go +++ b/cmd/dive/cli/internal/ui/v2/keys/keys.go @@ -1,4 +1,4 @@ -package app +package keys import "github.com/charmbracelet/bubbles/key" @@ -24,9 +24,9 @@ func (k KeyMap) ShortHelp() []key.Binding { // FullHelp returns all keys (for extended help) func (k KeyMap) FullHelp() [][]key.Binding { return [][]key.Binding{ - {k.Up, k.Down}, // Navigation - {k.Enter, k.Space}, // Actions - {k.Tab, k.Filter, k.Quit}, // System + {k.Up, k.Down}, // Navigation + {k.Enter, k.Space}, // Actions + {k.Tab, k.Filter, k.Quit}, // System } } diff --git a/cmd/dive/cli/internal/ui/v2/app/details_pane.go b/cmd/dive/cli/internal/ui/v2/panes/details/pane.go similarity index 64% rename from cmd/dive/cli/internal/ui/v2/app/details_pane.go rename to cmd/dive/cli/internal/ui/v2/panes/details/pane.go index 58ce807..42b9960 100644 --- a/cmd/dive/cli/internal/ui/v2/app/details_pane.go +++ b/cmd/dive/cli/internal/ui/v2/panes/details/pane.go @@ -1,4 +1,4 @@ -package app +package details import ( "fmt" @@ -8,71 +8,72 @@ import ( "github.com/charmbracelet/lipgloss" "github.com/mattn/go-runewidth" + "github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v2/styles" + "github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v2/utils" "github.com/wagoodman/dive/dive/image" - v2styles "github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v2/styles" ) -// DetailsPane displays information about a single layer -type DetailsPane struct { +// Pane displays information about a single layer +type Pane struct { focused bool width int height int layer *image.Layer } -// NewDetailsPane creates a new details pane -func NewDetailsPane() DetailsPane { - return DetailsPane{ +// New creates a new details pane +func New() Pane { + return Pane{ width: 80, height: 10, } } // SetSize updates the pane dimensions -func (m *DetailsPane) SetSize(width, height int) { +func (m *Pane) SetSize(width, height int) { m.width = width m.height = height } // SetLayer updates the layer to display -func (m *DetailsPane) SetLayer(layer *image.Layer) { +func (m *Pane) SetLayer(layer *image.Layer) { m.layer = layer } // Focus sets the pane as active -func (m *DetailsPane) Focus() { +func (m *Pane) Focus() { m.focused = true } // Blur sets the pane as inactive -func (m *DetailsPane) Blur() { +func (m *Pane) Blur() { m.focused = false } // IsFocused returns true if the pane is focused -func (m *DetailsPane) IsFocused() bool { +func (m *Pane) IsFocused() bool { return m.focused } // Init initializes the pane -func (m DetailsPane) Init() tea.Cmd { +func (m Pane) Init() tea.Cmd { return nil } // Update handles messages -func (m DetailsPane) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (m Pane) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // Details pane doesn't handle any messages - it's read-only return m, nil } // View renders the pane -func (m DetailsPane) View() string { +func (m Pane) View() string { content := m.renderContent() - return v2styles.RenderBox("Layer Details", m.width, m.height, content, m.focused) + return styles.RenderBox("Layer Details", m.width, m.height, content, m.focused) } // renderContent generates the details content -func (m DetailsPane) renderContent() string { +func (m Pane) renderContent() string { // Calculate available space: Height - Borders(2) - Header(2) maxLines := m.height - 4 if maxLines < 0 { @@ -101,16 +102,16 @@ func (m DetailsPane) renderContent() string { if lipgloss.Width(tags) > m.width-8 { tags = runewidth.Truncate(tags, m.width-8, "...") } - if !addLine(v2styles.LayerHeaderStyle.Render(fmt.Sprintf("Tags: %s", tags))) { + if !addLine(styles.LayerHeaderStyle.Render(fmt.Sprintf("Tags: %s", tags))) { goto finish } } // ID & Size - if !addLine(v2styles.LayerValueStyle.Render(fmt.Sprintf("Id: %s", layer.Id))) { + if !addLine(styles.LayerValueStyle.Render(fmt.Sprintf("Id: %s", layer.Id))) { goto finish } - if !addLine(v2styles.LayerValueStyle.Render(fmt.Sprintf("Size: %s", formatSize(layer.Size)))) { + if !addLine(styles.LayerValueStyle.Render(fmt.Sprintf("Size: %s", utils.FormatSize(layer.Size)))) { goto finish } @@ -125,18 +126,18 @@ func (m DetailsPane) renderContent() string { if lipgloss.Width(digest) > maxDigestWidth { digest = runewidth.Truncate(digest, maxDigestWidth, "...") } - if !addLine(v2styles.LayerValueStyle.Render(fmt.Sprintf("Digest: %s", digest))) { + if !addLine(styles.LayerValueStyle.Render(fmt.Sprintf("Digest: %s", digest))) { goto finish } } // Command - Maximum 2 lines! - if !addLine(v2styles.LayerHeaderStyle.Render("Command:")) { + if !addLine(styles.LayerHeaderStyle.Render("Command:")) { goto finish } if layer.Command == "" { - addLine(v2styles.LayerValueStyle.Render("(unavailable)")) + addLine(styles.LayerValueStyle.Render("(unavailable)")) } else { maxWidth := m.width - 4 if maxWidth < 10 { @@ -150,14 +151,14 @@ func (m DetailsPane) renderContent() string { // Show max 2 lines: first line + last line (with "..." prefix if long) if len(cmdLines) == 1 { // Short command - fits in 1 line - addLine(v2styles.LayerValueStyle.Render(cmdLines[0])) + addLine(styles.LayerValueStyle.Render(cmdLines[0])) } else if len(cmdLines) == 2 { // Exactly 2 lines - show both - addLine(v2styles.LayerValueStyle.Render(cmdLines[0])) - addLine(v2styles.LayerValueStyle.Render(cmdLines[1])) + addLine(styles.LayerValueStyle.Render(cmdLines[0])) + addLine(styles.LayerValueStyle.Render(cmdLines[1])) } else { // Long command (>2 lines) - show first and last - addLine(v2styles.LayerValueStyle.Render(cmdLines[0])) + addLine(styles.LayerValueStyle.Render(cmdLines[0])) // Last line with "..." prefix lastLine := cmdLines[len(cmdLines)-1] @@ -168,7 +169,7 @@ func (m DetailsPane) renderContent() string { secondLine = runewidth.Truncate(secondLine, maxWidth, "...") } - addLine(v2styles.LayerValueStyle.Render(secondLine)) + addLine(styles.LayerValueStyle.Render(secondLine)) } } diff --git a/cmd/dive/cli/internal/ui/v2/app/tree_pane.go b/cmd/dive/cli/internal/ui/v2/panes/filetree/pane.go similarity index 77% rename from cmd/dive/cli/internal/ui/v2/app/tree_pane.go rename to cmd/dive/cli/internal/ui/v2/panes/filetree/pane.go index dbdcdab..2a3732b 100644 --- a/cmd/dive/cli/internal/ui/v2/app/tree_pane.go +++ b/cmd/dive/cli/internal/ui/v2/panes/filetree/pane.go @@ -1,4 +1,4 @@ -package app +package filetree import ( "strings" @@ -6,11 +6,27 @@ import ( tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/bubbles/viewport" "github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v1/viewmodel" - v2styles "github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v2/styles" + "github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v2/app/layout" + "github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v2/styles" ) -// TreePane manages the file tree -type TreePane struct { +// NodeToggledMsg is sent when a tree node is collapsed/expanded +type NodeToggledMsg struct { + NodeIndex int +} + +// TreeSelectionChangedMsg is sent when a tree node is selected +type TreeSelectionChangedMsg struct { + NodeIndex int +} + +// RefreshTreeContentMsg requests tree content to be refreshed +type RefreshTreeContentMsg struct { + LayerIndex int +} + +// Pane manages the file tree +type Pane struct { focused bool width int height int @@ -19,10 +35,10 @@ type TreePane struct { treeIndex int } -// NewTreePane creates a new tree pane -func NewTreePane(treeVM *viewmodel.FileTreeViewModel) TreePane { +// New creates a new tree pane +func New(treeVM *viewmodel.FileTreeViewModel) Pane { vp := viewport.New(80, 20) - p := TreePane{ + p := Pane{ treeVM: treeVM, viewport: vp, treeIndex: 0, @@ -35,12 +51,12 @@ func NewTreePane(treeVM *viewmodel.FileTreeViewModel) TreePane { } // SetSize updates the pane dimensions -func (m *TreePane) SetSize(width, height int) { +func (m *Pane) SetSize(width, height int) { m.width = width m.height = height viewportWidth := width - 2 - viewportHeight := height - BoxContentPadding + viewportHeight := height - layout.BoxContentPadding if viewportHeight < 0 { viewportHeight = 0 } @@ -54,7 +70,7 @@ func (m *TreePane) SetSize(width, height int) { } // SetTreeVM updates the tree viewmodel -func (m *TreePane) SetTreeVM(treeVM *viewmodel.FileTreeViewModel) { +func (m *Pane) SetTreeVM(treeVM *viewmodel.FileTreeViewModel) { m.treeVM = treeVM m.treeIndex = 0 m.viewport.GotoTop() @@ -62,39 +78,39 @@ func (m *TreePane) SetTreeVM(treeVM *viewmodel.FileTreeViewModel) { } // SetTreeIndex sets the current tree index -func (m *TreePane) SetTreeIndex(index int) { +func (m *Pane) SetTreeIndex(index int) { m.treeIndex = index m.syncScroll() } // GetTreeIndex returns the current tree index -func (m *TreePane) GetTreeIndex() int { +func (m *Pane) GetTreeIndex() int { return m.treeIndex } // Focus sets the pane as active -func (m *TreePane) Focus() { +func (m *Pane) Focus() { m.focused = true } // Blur sets the pane as inactive -func (m *TreePane) Blur() { +func (m *Pane) Blur() { m.focused = false } // IsFocused returns true if the pane is focused -func (m *TreePane) IsFocused() bool { +func (m *Pane) IsFocused() bool { return m.focused } // Init initializes the pane -func (m TreePane) Init() tea.Cmd { +func (m Pane) Init() tea.Cmd { m.updateContent() return nil } // Update handles messages -func (m TreePane) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (m Pane) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmds []tea.Cmd switch msg := msg.(type) { @@ -139,13 +155,13 @@ func (m TreePane) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } // View renders the pane -func (m TreePane) View() string { +func (m Pane) View() string { content := m.viewport.View() - return v2styles.RenderBox("Current Layer Contents", m.width, m.height, content, m.focused) + return styles.RenderBox("Current Layer Contents", m.width, m.height, content, m.focused) } // moveUp moves selection up -func (m *TreePane) moveUp() tea.Cmd { +func (m *Pane) moveUp() tea.Cmd { if m.treeIndex > 0 { m.treeIndex-- m.syncScroll() @@ -154,7 +170,7 @@ func (m *TreePane) moveUp() tea.Cmd { } // moveDown moves selection down -func (m *TreePane) moveDown() tea.Cmd { +func (m *Pane) moveDown() tea.Cmd { if m.treeVM == nil || m.treeVM.ViewTree == nil { return nil } @@ -168,7 +184,7 @@ func (m *TreePane) moveDown() tea.Cmd { } // toggleCollapse toggles the current node's collapse state -func (m *TreePane) toggleCollapse() tea.Cmd { +func (m *Pane) toggleCollapse() tea.Cmd { if m.treeVM == nil || m.treeVM.ViewTree == nil { return nil } @@ -200,12 +216,12 @@ func (m *TreePane) toggleCollapse() tea.Cmd { } // handleClick processes a mouse click -func (m *TreePane) handleClick(x, y int) tea.Cmd { +func (m *Pane) handleClick(x, y int) tea.Cmd { if x < 0 || x >= m.width || y < 0 { return nil } - relativeY := y - ContentVisualOffset + relativeY := y - layout.ContentVisualOffset if relativeY < 0 || relativeY >= m.viewport.Height { return nil } @@ -233,7 +249,7 @@ func (m *TreePane) handleClick(x, y int) tea.Cmd { } // syncScroll ensures the cursor is always visible -func (m *TreePane) syncScroll() { +func (m *Pane) syncScroll() { if m.treeVM == nil || m.treeVM.ViewTree == nil { return } @@ -265,7 +281,7 @@ func (m *TreePane) syncScroll() { } // updateContent regenerates the viewport content -func (m *TreePane) updateContent() { +func (m *Pane) updateContent() { if m.treeVM == nil { m.viewport.SetContent("No tree data") return @@ -279,7 +295,7 @@ func (m *TreePane) updateContent() { } // renderTreeContent generates the tree content -func (m *TreePane) renderTreeContent() string { +func (m *Pane) renderTreeContent() string { if m.treeVM == nil || m.treeVM.ViewTree == nil { return "No tree data" } @@ -296,6 +312,6 @@ func (m *TreePane) renderTreeContent() string { } // GetViewport returns the underlying viewport -func (m *TreePane) GetViewport() *viewport.Model { +func (m *Pane) GetViewport() *viewport.Model { return &m.viewport } diff --git a/cmd/dive/cli/internal/ui/v2/app/tree_renderer.go b/cmd/dive/cli/internal/ui/v2/panes/filetree/renderer.go similarity index 71% rename from cmd/dive/cli/internal/ui/v2/app/tree_renderer.go rename to cmd/dive/cli/internal/ui/v2/panes/filetree/renderer.go index dd2b5b5..ca849a1 100644 --- a/cmd/dive/cli/internal/ui/v2/app/tree_renderer.go +++ b/cmd/dive/cli/internal/ui/v2/panes/filetree/renderer.go @@ -1,4 +1,4 @@ -package app +package filetree import ( "fmt" @@ -7,7 +7,8 @@ import ( "github.com/charmbracelet/lipgloss" "github.com/mattn/go-runewidth" - v2styles "github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v2/styles" + + "github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v2/styles" "github.com/wagoodman/dive/dive/filetree" ) @@ -67,31 +68,31 @@ func renderNodeWithCursor(sb *strings.Builder, node *filetree.FileNode, depth in } // 2. Icon and color - icon := v2styles.IconFile + icon := styles.IconFile diffIcon := "" - color := v2styles.DiffNormalColor + color := styles.DiffNormalColor if node.Data.FileInfo.IsDir() { if node.Data.ViewInfo.Collapsed { - icon = v2styles.IconDirClosed + icon = styles.IconDirClosed } else { - icon = v2styles.IconDirOpen + icon = styles.IconDirOpen } } else if node.Data.FileInfo.TypeFlag == 16 { // Symlink - icon = v2styles.IconSymlink + icon = styles.IconSymlink } // 3. Diff status switch node.Data.DiffType { case filetree.Added: - color = v2styles.DiffAddedColor - diffIcon = v2styles.IconAdded + color = styles.DiffAddedColor + diffIcon = styles.IconAdded case filetree.Removed: - color = v2styles.DiffRemovedColor - diffIcon = v2styles.IconRemoved + color = styles.DiffRemovedColor + diffIcon = styles.IconRemoved case filetree.Modified: - color = v2styles.DiffModifiedColor - diffIcon = v2styles.IconModified + color = styles.DiffModifiedColor + diffIcon = styles.IconModified } // 4. Format name @@ -130,7 +131,7 @@ func renderNodeWithCursor(sb *strings.Builder, node *filetree.FileNode, depth in // For selected items, fill background to full width BUT don't add padding // Using MaxWidth instead of Width to prevent adding extra whitespace style = style. - Background(v2styles.PrimaryColor). + Background(styles.PrimaryColor). Foreground(lipgloss.Color("#000000")). Bold(true). MaxWidth(width) // Prevent exceeding width, but don't add padding @@ -142,15 +143,15 @@ func renderNodeWithCursor(sb *strings.Builder, node *filetree.FileNode, depth in // Note: no recursion here since we're using collectVisibleNodes instead } -// renderNode рекурсивно рендерит узел дерева с иконками и цветами +// renderNode recursively renders a tree node with icons and colors func renderNode(sb *strings.Builder, node *filetree.FileNode, depth int, prefix string) { if node == nil { return } - // Не рендерим корневой элемент (он обычно пустой) + // Don't render root element (it's usually empty) if node.Parent == nil { - // Рендерим детей корня + // Render root's children if !node.Data.ViewInfo.Collapsed { sortedChildren := sortChildren(node.Children) for _, child := range sortedChildren { @@ -160,61 +161,61 @@ func renderNode(sb *strings.Builder, node *filetree.FileNode, depth int, prefix return } - // 1. Определяем иконку - icon := v2styles.IconFile + // 1. Determine icon + icon := styles.IconFile diffIcon := "" - // Определяем тип файла + // Determine file type if node.Data.FileInfo.IsDir() { if node.Data.ViewInfo.Collapsed { - icon = v2styles.IconDirClosed + icon = styles.IconDirClosed } else { - icon = v2styles.IconDirOpen + icon = styles.IconDirOpen } } else if node.Data.FileInfo.TypeFlag == 16 { // tar.TypeSymlink - icon = v2styles.IconSymlink + icon = styles.IconSymlink } - // Определяем Diff (Добавлен/Удален/Изменен) - color := v2styles.DiffNormalColor + // Determine Diff (Added/Removed/Modified) + color := styles.DiffNormalColor switch node.Data.DiffType { case filetree.Added: - color = v2styles.DiffAddedColor - diffIcon = v2styles.IconAdded + color = styles.DiffAddedColor + diffIcon = styles.IconAdded case filetree.Removed: - color = v2styles.DiffRemovedColor - diffIcon = v2styles.IconRemoved + color = styles.DiffRemovedColor + diffIcon = styles.IconRemoved case filetree.Modified: - color = v2styles.DiffModifiedColor - diffIcon = v2styles.IconModified + color = styles.DiffModifiedColor + diffIcon = styles.IconModified } - // 2. Формируем строку + // 2. Build line name := node.Name if name == "" { name = "/" } - // Добавляем symlink target если есть + // Add symlink target if present if node.Data.FileInfo.TypeFlag == 16 && node.Data.FileInfo.Linkname != "" { name += " → " + node.Data.FileInfo.Linkname } - // Собираем строку с префиксом (отступом) + // Build line with prefix (indent) line := prefix + diffIcon + " " + icon + " " + name - // Применяем цвет + // Apply color style := lipgloss.NewStyle().Foreground(color) sb.WriteString(style.Render(line)) sb.WriteString("\n") - // 3. Рекурсия для детей (если папка не свернута) + // 3. Recursion for children (if folder not collapsed) if node.Data.FileInfo.IsDir() && !node.Data.ViewInfo.Collapsed && !node.IsLeaf() { - // Вычисляем префикс для детей + // Calculate prefix for children childPrefix := prefix + " " - // Сортируем и рендерим детей + // Sort and render children sortedChildren := sortChildren(node.Children) for _, child := range sortedChildren { renderNode(sb, child, depth+1, childPrefix) @@ -222,13 +223,13 @@ func renderNode(sb *strings.Builder, node *filetree.FileNode, depth int, prefix } } -// sortChildren сортирует детей узла: сначала папки, потом файлы, все по алфавиту +// sortChildren sorts node children: directories first, then files, all alphabetically func sortChildren(children map[string]*filetree.FileNode) []*filetree.FileNode { if children == nil { return nil } - // Разделяем на папки и файлы + // Split into directories and files var dirs []*filetree.FileNode var files []*filetree.FileNode @@ -240,17 +241,17 @@ func sortChildren(children map[string]*filetree.FileNode) []*filetree.FileNode { } } - // Сортируем папки + // Sort directories sort.Slice(dirs, func(i, j int) bool { return dirs[i].Name < dirs[j].Name }) - // Сортируем файлы + // Sort files sort.Slice(files, func(i, j int) bool { return files[i].Name < files[j].Name }) - // Объединяем: сначала папки, потом файлы + // Combine: directories first, then files result := append(dirs, files...) return result } diff --git a/cmd/dive/cli/internal/ui/v2/app/image_pane.go b/cmd/dive/cli/internal/ui/v2/panes/image/pane.go similarity index 70% rename from cmd/dive/cli/internal/ui/v2/app/image_pane.go rename to cmd/dive/cli/internal/ui/v2/panes/image/pane.go index 79a8e46..3416c04 100644 --- a/cmd/dive/cli/internal/ui/v2/app/image_pane.go +++ b/cmd/dive/cli/internal/ui/v2/panes/image/pane.go @@ -1,4 +1,4 @@ -package app +package image import ( "fmt" @@ -9,12 +9,14 @@ import ( "github.com/charmbracelet/lipgloss" "github.com/mattn/go-runewidth" + "github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v2/app/layout" + "github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v2/styles" + "github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v2/utils" "github.com/wagoodman/dive/dive/image" - v2styles "github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v2/styles" ) -// ImagePane displays image-level statistics and inefficiencies -type ImagePane struct { +// Pane displays image-level statistics and inefficiencies +type Pane struct { focused bool width int height int @@ -22,10 +24,10 @@ type ImagePane struct { viewport viewport.Model } -// NewImagePane creates a new image pane -func NewImagePane(analysis *image.Analysis) ImagePane { +// New creates a new image pane +func New(analysis *image.Analysis) Pane { vp := viewport.New(80, 20) - p := ImagePane{ + p := Pane{ analysis: analysis, viewport: vp, width: 80, @@ -37,12 +39,12 @@ func NewImagePane(analysis *image.Analysis) ImagePane { } // SetSize updates the pane dimensions -func (m *ImagePane) SetSize(width, height int) { +func (m *Pane) SetSize(width, height int) { m.width = width m.height = height viewportWidth := width - 2 - viewportHeight := height - BoxContentPadding + viewportHeight := height - layout.BoxContentPadding if viewportHeight < 0 { viewportHeight = 0 } @@ -55,34 +57,34 @@ func (m *ImagePane) SetSize(width, height int) { } // SetAnalysis updates the analysis data -func (m *ImagePane) SetAnalysis(analysis *image.Analysis) { +func (m *Pane) SetAnalysis(analysis *image.Analysis) { m.analysis = analysis m.updateContent() } // Focus sets the pane as active -func (m *ImagePane) Focus() { +func (m *Pane) Focus() { m.focused = true } // Blur sets the pane as inactive -func (m *ImagePane) Blur() { +func (m *Pane) Blur() { m.focused = false } // IsFocused returns true if the pane is focused -func (m *ImagePane) IsFocused() bool { +func (m *Pane) IsFocused() bool { return m.focused } // Init initializes the pane -func (m ImagePane) Init() tea.Cmd { +func (m Pane) Init() tea.Cmd { m.updateContent() return nil } // Update handles messages -func (m ImagePane) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (m Pane) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmds []tea.Cmd switch msg := msg.(type) { @@ -117,13 +119,13 @@ func (m ImagePane) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } // View renders the pane -func (m ImagePane) View() string { +func (m Pane) View() string { content := m.viewport.View() - return v2styles.RenderBox("Image Details", m.width, m.height, content, m.focused) + return styles.RenderBox("Image Details", m.width, m.height, content, m.focused) } // updateContent regenerates the viewport content -func (m *ImagePane) updateContent() { +func (m *Pane) updateContent() { if m.analysis == nil { m.viewport.SetContent("No image data") return @@ -134,15 +136,15 @@ func (m *ImagePane) updateContent() { } // generateContent creates the image statistics content -func (m *ImagePane) generateContent() string { +func (m *Pane) generateContent() string { width := m.width - 2 // Subtract borders // Header with statistics headerText := fmt.Sprintf( "Image name: %s\nTotal Image size: %s\nPotential wasted space: %s\nImage efficiency score: %.0f%%", m.analysis.Image, - formatSize(m.analysis.SizeBytes), - formatSize(m.analysis.WastedBytes), + utils.FormatSize(m.analysis.SizeBytes), + utils.FormatSize(m.analysis.WastedBytes), m.analysis.Efficiency*100, ) @@ -153,16 +155,16 @@ func (m *ImagePane) generateContent() string { var fullContent strings.Builder fullContent.WriteString(headerText) fullContent.WriteString("\n") - fullContent.WriteString(v2styles.LayerHeaderStyle.Render(tableHeader)) + fullContent.WriteString(styles.LayerHeaderStyle.Render(tableHeader)) fullContent.WriteString("\n") if len(m.analysis.Inefficiencies) > 0 { for _, file := range m.analysis.Inefficiencies { - row := fmt.Sprintf("%-5d %-12s %s", len(file.Nodes), formatSize(uint64(file.CumulativeSize)), file.Path) + row := fmt.Sprintf("%-5d %-12s %s", len(file.Nodes), utils.FormatSize(uint64(file.CumulativeSize)), file.Path) if lipgloss.Width(row) > width { row = runewidth.Truncate(row, width, "...") } - fullContent.WriteString(v2styles.FileTreeModifiedStyle.Render(row)) + fullContent.WriteString(styles.FileTreeModifiedStyle.Render(row)) fullContent.WriteString("\n") } } else { diff --git a/cmd/dive/cli/internal/ui/v2/app/layers_pane.go b/cmd/dive/cli/internal/ui/v2/panes/layers/pane.go similarity index 75% rename from cmd/dive/cli/internal/ui/v2/app/layers_pane.go rename to cmd/dive/cli/internal/ui/v2/panes/layers/pane.go index 930dd4e..06c003a 100644 --- a/cmd/dive/cli/internal/ui/v2/app/layers_pane.go +++ b/cmd/dive/cli/internal/ui/v2/panes/layers/pane.go @@ -1,4 +1,4 @@ -package app +package layers import ( "fmt" @@ -10,28 +10,35 @@ import ( "github.com/mattn/go-runewidth" "github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v1/viewmodel" - v2styles "github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v2/styles" + "github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v2/app/layout" + "github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v2/styles" + "github.com/wagoodman/dive/cmd/dive/cli/internal/ui/v2/utils" ) -// LayersPane manages the layers list -type LayersPane struct { - focused bool - width int - height int - layerVM *viewmodel.LayerSetState - viewport viewport.Model +// LayerChangedMsg is sent when the active layer changes +type LayerChangedMsg struct { + LayerIndex int +} + +// Pane manages the layers list +type Pane struct { + focused bool + width int + height int + layerVM *viewmodel.LayerSetState + viewport viewport.Model layerIndex int } -// NewLayersPane creates a new layers pane -func NewLayersPane(layerVM *viewmodel.LayerSetState) LayersPane { +// New creates a new layers pane +func New(layerVM *viewmodel.LayerSetState) Pane { vp := viewport.New(80, 20) - p := LayersPane{ - layerVM: layerVM, - viewport: vp, + p := Pane{ + layerVM: layerVM, + viewport: vp, layerIndex: 0, - width: 80, - height: 20, + width: 80, + height: 20, } // IMPORTANT: Generate content immediately so viewport is not empty on startup p.updateContent() @@ -39,12 +46,12 @@ func NewLayersPane(layerVM *viewmodel.LayerSetState) LayersPane { } // SetSize updates the pane dimensions -func (m *LayersPane) SetSize(width, height int) { +func (m *Pane) SetSize(width, height int) { m.width = width m.height = height viewportWidth := width - 2 - viewportHeight := height - BoxContentPadding + viewportHeight := height - layout.BoxContentPadding if viewportHeight < 0 { viewportHeight = 0 } @@ -57,7 +64,7 @@ func (m *LayersPane) SetSize(width, height int) { } // SetLayerVM updates the layer viewmodel -func (m *LayersPane) SetLayerVM(layerVM *viewmodel.LayerSetState) { +func (m *Pane) SetLayerVM(layerVM *viewmodel.LayerSetState) { m.layerVM = layerVM if layerVM != nil { m.layerIndex = layerVM.LayerIndex @@ -66,7 +73,7 @@ func (m *LayersPane) SetLayerVM(layerVM *viewmodel.LayerSetState) { } // SetLayerIndex sets the current layer index -func (m *LayersPane) SetLayerIndex(index int) tea.Cmd { +func (m *Pane) SetLayerIndex(index int) tea.Cmd { if m.layerVM == nil || index < 0 || index >= len(m.layerVM.Layers) { return nil } @@ -81,28 +88,28 @@ func (m *LayersPane) SetLayerIndex(index int) tea.Cmd { } // Focus sets the pane as active -func (m *LayersPane) Focus() { +func (m *Pane) Focus() { m.focused = true } // Blur sets the pane as inactive -func (m *LayersPane) Blur() { +func (m *Pane) Blur() { m.focused = false } // IsFocused returns true if the pane is focused -func (m *LayersPane) IsFocused() bool { +func (m *Pane) IsFocused() bool { return m.focused } // Init initializes the pane -func (m LayersPane) Init() tea.Cmd { +func (m Pane) Init() tea.Cmd { m.updateContent() return nil } // Update handles messages -func (m LayersPane) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (m Pane) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmds []tea.Cmd switch msg := msg.(type) { @@ -145,13 +152,13 @@ func (m LayersPane) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } // View renders the pane -func (m LayersPane) View() string { +func (m Pane) View() string { content := m.viewport.View() - return v2styles.RenderBox("Layers", m.width, m.height, content, m.focused) + return styles.RenderBox("Layers", m.width, m.height, content, m.focused) } // moveUp moves selection up -func (m *LayersPane) moveUp() tea.Cmd { +func (m *Pane) moveUp() tea.Cmd { if m.layerVM == nil || m.layerIndex <= 0 { return nil } @@ -166,7 +173,7 @@ func (m *LayersPane) moveUp() tea.Cmd { } // moveDown moves selection down -func (m *LayersPane) moveDown() tea.Cmd { +func (m *Pane) moveDown() tea.Cmd { if m.layerVM == nil || m.layerIndex >= len(m.layerVM.Layers)-1 { return nil } @@ -181,12 +188,12 @@ func (m *LayersPane) moveDown() tea.Cmd { } // handleClick processes a mouse click -func (m *LayersPane) handleClick(x, y int) tea.Cmd { +func (m *Pane) handleClick(x, y int) tea.Cmd { if x < 0 || x >= m.width || y < 0 { return nil } - relativeY := y - ContentVisualOffset + relativeY := y - layout.ContentVisualOffset if relativeY < 0 || relativeY >= m.viewport.Height { return nil } @@ -200,7 +207,7 @@ func (m *LayersPane) handleClick(x, y int) tea.Cmd { } // updateContent regenerates the viewport content -func (m *LayersPane) updateContent() { +func (m *Pane) updateContent() { if m.layerVM == nil || len(m.layerVM.Layers) == 0 { m.viewport.SetContent("No layer data") return @@ -211,7 +218,7 @@ func (m *LayersPane) updateContent() { } // generateContent creates the layers content -func (m *LayersPane) generateContent() string { +func (m *Pane) generateContent() string { width := m.width - 2 const ( @@ -228,7 +235,7 @@ func (m *LayersPane) generateContent() string { if i == m.layerIndex { prefix = "● " - style = v2styles.SelectedLayerStyle + style = styles.SelectedLayerStyle } id := layer.Id @@ -236,7 +243,7 @@ func (m *LayersPane) generateContent() string { id = id[:idWidth] } - size := formatSize(layer.Size) + size := utils.FormatSize(layer.Size) rawCmd := strings.ReplaceAll(layer.Command, "\n", " ") rawCmd = strings.TrimSpace(rawCmd) @@ -266,11 +273,11 @@ func (m *LayersPane) generateContent() string { } // GetLayerIndex returns the current layer index -func (m *LayersPane) GetLayerIndex() int { +func (m *Pane) GetLayerIndex() int { return m.layerIndex } // GetViewport returns the underlying viewport -func (m *LayersPane) GetViewport() *viewport.Model { +func (m *Pane) GetViewport() *viewport.Model { return &m.viewport } diff --git a/cmd/dive/cli/internal/ui/v2/styles/colors.go b/cmd/dive/cli/internal/ui/v2/styles/colors.go new file mode 100644 index 0000000..5ee8560 --- /dev/null +++ b/cmd/dive/cli/internal/ui/v2/styles/colors.go @@ -0,0 +1,34 @@ +package styles + +import "github.com/charmbracelet/lipgloss" + +// --- UI Colors --- + +var ( + // Primary theme colors + PrimaryColor = lipgloss.Color("#007AFF") + SecondaryColor = lipgloss.Color("#5856D6") + + // Status colors + SuccessColor = lipgloss.Color("#34C759") + WarningColor = lipgloss.Color("#FF9500") + ErrorColor = lipgloss.Color("#FF3B30") + + // Gray scale + GrayColor = lipgloss.Color("#8E8E93") + LightGrayColor = lipgloss.Color("#C7C7CC") + DarkGrayColor = lipgloss.Color("#48484A") + + // UI elements + BorderColor = lipgloss.Color("#3A3A3C") +) + +// --- File Tree Diff Colors --- + +var ( + // Diff type colors + DiffAddedColor = lipgloss.Color("#A3BE8C") // Green for added files + DiffRemovedColor = lipgloss.Color("#BF616A") // Red for removed files + DiffModifiedColor = lipgloss.Color("#EBCB8B") // Yellow for modified files + DiffNormalColor = lipgloss.Color("#D8DEE9") // Default color +) diff --git a/cmd/dive/cli/internal/ui/v2/styles/icons.go b/cmd/dive/cli/internal/ui/v2/styles/icons.go new file mode 100644 index 0000000..0f6186f --- /dev/null +++ b/cmd/dive/cli/internal/ui/v2/styles/icons.go @@ -0,0 +1,18 @@ +package styles + +// --- File Icons --- + +var ( + IconDirOpen = "📂 " + IconDirClosed = "📁 " + IconFile = "📄 " + IconSymlink = "🔗 " +) + +// --- Diff Type Icons --- + +var ( + IconAdded = "✨ " + IconRemoved = "❌ " + IconModified = "✏️ " +) diff --git a/cmd/dive/cli/internal/ui/v2/styles/styles.go b/cmd/dive/cli/internal/ui/v2/styles/styles.go index 1cf0782..c75a5ee 100644 --- a/cmd/dive/cli/internal/ui/v2/styles/styles.go +++ b/cmd/dive/cli/internal/ui/v2/styles/styles.go @@ -5,33 +5,24 @@ import ( "github.com/mattn/go-runewidth" ) -// --- Colors --- -var ( - PrimaryColor = lipgloss.Color("#007AFF") - SecondaryColor = lipgloss.Color("#5856D6") - SuccessColor = lipgloss.Color("#34C759") - WarningColor = lipgloss.Color("#FF9500") - ErrorColor = lipgloss.Color("#FF3B30") - GrayColor = lipgloss.Color("#8E8E93") - LightGrayColor = lipgloss.Color("#C7C7CC") - DarkGrayColor = lipgloss.Color("#48484A") - BorderColor = lipgloss.Color("#3A3A3C") -) - // --- Base Styles --- + var ( + // TitleStyle for main titles TitleStyle = lipgloss.NewStyle(). Bold(true). Foreground(PrimaryColor). Background(lipgloss.Color("#1C1C1E")). Padding(0, 1) + // StatusStyle for status bar StatusStyle = lipgloss.NewStyle(). Bold(true). Foreground(lipgloss.Color("#FFFFFF")). Background(SecondaryColor). Padding(0, 1) + // FilterStyle for filter input FilterStyle = lipgloss.NewStyle(). Foreground(lipgloss.Color("#FFFFFF")). Background(DarkGrayColor). @@ -40,10 +31,37 @@ var ( // --- Component Styles --- -// RenderBox создает рамку с заголовком. -// ИСПРАВЛЕНО: Обрезает заголовок чтобы гарантировать одну строку +// SelectedLayerStyle highlights the currently selected layer +var SelectedLayerStyle = lipgloss.NewStyle(). + Bold(true). + Foreground(PrimaryColor). + Background(lipgloss.Color("#1C1C1E")) + +// LayerHeaderStyle for layer field headers +var LayerHeaderStyle = lipgloss.NewStyle(). + Bold(true). + Foreground(SecondaryColor) + +// LayerValueStyle for layer field values +var LayerValueStyle = lipgloss.NewStyle(). + Foreground(LightGrayColor) + +// FileTreeDirStyle for directories in file tree +var FileTreeDirStyle = lipgloss.NewStyle(). + Foreground(SuccessColor). + Bold(true) + +// FileTreeModifiedStyle for modified files in file tree +var FileTreeModifiedStyle = lipgloss.NewStyle(). + Foreground(WarningColor). + Bold(true) + +// --- Rendering Functions --- + +// RenderBox creates a bordered box with title and content +// IMPORTANT: Truncates title to guarantee single line height func RenderBox(title string, width, height int, content string, isSelected bool) string { - // 1. Защита минимальных размеров + // 1. Protect minimum sizes if width < 2 { width = 2 } @@ -69,8 +87,8 @@ func RenderBox(title string, width, height int, content string, isSelected bool) return boxStyle.Render(content) } - // 2. ИСПРАВЛЕНИЕ: Обрезаем заголовок, чтобы он не переносился на 2 строки - // Ширина заголовка: Ширина окна - 2 (рамки) - 2 (запас) + // 2. Truncate title to prevent wrapping to 2 lines + // Title width: Window width - 2 (borders) - 2 (margin) maxTitleWidth := width - 4 if maxTitleWidth < 0 { maxTitleWidth = 0 @@ -78,48 +96,23 @@ func RenderBox(title string, width, height int, content string, isSelected bool) truncatedTitle := runewidth.Truncate(title, maxTitleWidth, "…") - // 3. Рендерим заголовок + // 3. Render title titleStyle := lipgloss.NewStyle(). Foreground(borderColor). Bold(true) titleLine := titleStyle.Render(truncatedTitle) - // 4. Собираем контент: Заголовок + Пробел + Данные - // Используем " " (пробел), чтобы гарантировать высоту отступа в 1 строку + // 4. Assemble content: Title + Space + Data + // Using " " (space) to guarantee 1 line height for padding innerContent := lipgloss.JoinVertical(lipgloss.Left, titleLine, " ", content) return boxStyle.Render(innerContent) } -// --- Specific Styles for content --- +// --- Utility Functions --- -var ( - SelectedLayerStyle = lipgloss.NewStyle().Bold(true).Foreground(PrimaryColor).Background(lipgloss.Color("#1C1C1E")) - LayerHeaderStyle = lipgloss.NewStyle().Bold(true).Foreground(SecondaryColor) - LayerValueStyle = lipgloss.NewStyle().Foreground(LightGrayColor) - - FileTreeDirStyle = lipgloss.NewStyle().Foreground(SuccessColor).Bold(true) - FileTreeModifiedStyle = lipgloss.NewStyle().Foreground(WarningColor).Bold(true) -) - -// --- Icons --- -var ( - IconDirOpen = "📂 " - IconDirClosed = "📁 " - IconFile = "📄 " - IconSymlink = "🔗 " - IconAdded = "✨ " - IconRemoved = "❌ " - IconModified = "✏️ " - - DiffAddedColor = lipgloss.Color("#A3BE8C") - DiffRemovedColor = lipgloss.Color("#BF616A") - DiffModifiedColor = lipgloss.Color("#EBCB8B") - DiffNormalColor = lipgloss.Color("#D8DEE9") -) - -// TruncateString обрезает строку по визуальной ширине +// TruncateString truncates a string by visual width func TruncateString(s string, maxLen int) string { return runewidth.Truncate(s, maxLen, "...") } diff --git a/cmd/dive/cli/internal/ui/v2/app/utils.go b/cmd/dive/cli/internal/ui/v2/utils/format.go similarity index 71% rename from cmd/dive/cli/internal/ui/v2/app/utils.go rename to cmd/dive/cli/internal/ui/v2/utils/format.go index 7ab2cf2..87d08d5 100644 --- a/cmd/dive/cli/internal/ui/v2/app/utils.go +++ b/cmd/dive/cli/internal/ui/v2/utils/format.go @@ -1,12 +1,10 @@ -package app +package utils -import ( - "fmt" -) +import "fmt" -// formatSize formats bytes into human-readable size +// FormatSize formats bytes into human-readable size // This is a shared utility used by all panes -func formatSize(bytes uint64) string { +func FormatSize(bytes uint64) string { const unit = 1024 if bytes < unit { return fmt.Sprintf("%d B", bytes) diff --git a/dive-test b/dive-test index 35d0ed4..429904c 100755 Binary files a/dive-test and b/dive-test differ