mirror of
https://github.com/wagoodman/dive
synced 2026-03-14 22:35:50 +01:00
feat: add new UI panes for image details, layer details, and file tree
- Implemented a details pane to display information about a single layer, including tags, ID, size, digest, and command. - Created a file tree pane to manage and display the file structure of the image, allowing for navigation and selection of files and directories. - Added an image pane to show image-level statistics and inefficiencies, including total size and potential wasted space. - Developed a layers pane to list and manage layers, allowing users to navigate through layers and view their details. - Introduced new styles for UI elements, including colors and icons for better visual representation. - Added utility functions for formatting sizes and rendering content in a human-readable format.
This commit is contained in:
parent
821f2d6521
commit
bb7acef312
13 changed files with 324 additions and 265 deletions
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
@ -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
|
||||
}
|
||||
34
cmd/dive/cli/internal/ui/v2/styles/colors.go
Normal file
34
cmd/dive/cli/internal/ui/v2/styles/colors.go
Normal file
|
|
@ -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
|
||||
)
|
||||
18
cmd/dive/cli/internal/ui/v2/styles/icons.go
Normal file
18
cmd/dive/cli/internal/ui/v2/styles/icons.go
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
package styles
|
||||
|
||||
// --- File Icons ---
|
||||
|
||||
var (
|
||||
IconDirOpen = "📂 "
|
||||
IconDirClosed = "📁 "
|
||||
IconFile = "📄 "
|
||||
IconSymlink = "🔗 "
|
||||
)
|
||||
|
||||
// --- Diff Type Icons ---
|
||||
|
||||
var (
|
||||
IconAdded = "✨ "
|
||||
IconRemoved = "❌ "
|
||||
IconModified = "✏️ "
|
||||
)
|
||||
|
|
@ -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, "...")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
BIN
dive-test
BIN
dive-test
Binary file not shown.
Loading…
Add table
Add a link
Reference in a new issue