Stabilize panel layout and background fill

This commit is contained in:
vrubelroman 2026-04-23 00:10:41 +03:00
parent bd67696fb0
commit 2a3b58c44e
2 changed files with 98 additions and 58 deletions

View file

@ -8,7 +8,6 @@ import (
"strings" "strings"
"time" "time"
"github.com/charmbracelet/bubbles/help"
"github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/textinput" "github.com/charmbracelet/bubbles/textinput"
"github.com/charmbracelet/bubbles/viewport" "github.com/charmbracelet/bubbles/viewport"
@ -100,7 +99,6 @@ type Model struct {
infoMode bool infoMode bool
selectMode bool selectMode bool
helpModel help.Model
previewModel viewport.Model previewModel viewport.Model
previewData vfs.Preview previewData vfs.Preview
@ -143,17 +141,6 @@ func NewModel(cfg config.Config, configPath string) (Model, error) {
status: "Ready", status: "Ready",
} }
model.helpModel = help.New()
model.helpModel.ShowAll = false
model.helpModel.Styles = help.Styles{
ShortKey: lipgloss.NewStyle().Foreground(palette.FooterKey).Bold(true),
ShortDesc: lipgloss.NewStyle().Foreground(palette.Text),
ShortSeparator: lipgloss.NewStyle().Foreground(palette.Border),
Ellipsis: lipgloss.NewStyle().Foreground(palette.Muted),
FullKey: lipgloss.NewStyle().Foreground(palette.FooterKey).Bold(true),
FullDesc: lipgloss.NewStyle().Foreground(palette.Text),
FullSeparator: lipgloss.NewStyle().Foreground(palette.Border),
}
model.previewModel = viewport.New(0, 0) model.previewModel = viewport.New(0, 0)
if err := model.reloadPane(PaneLeft, ""); err != nil { if err := model.reloadPane(PaneLeft, ""); err != nil {
return Model{}, err return Model{}, err
@ -308,7 +295,11 @@ func (m Model) View() string {
leftWidth, previewWidth, rightWidth := m.layoutWidths() leftWidth, previewWidth, rightWidth := m.layoutWidths()
bodyHeight := m.bodyHeight() bodyHeight := m.bodyHeight()
gap := strings.Repeat(" ", m.cfg.UI.PaneGap) gap := lipgloss.NewStyle().
Width(m.cfg.UI.PaneGap).
Height(bodyHeight).
Background(m.palette.Panel).
Render("")
var panels string var panels string
if m.selectMode && m.infoMode { if m.selectMode && m.infoMode {
@ -347,7 +338,12 @@ func (m Model) View() string {
parts = append(parts, renderFooter(m)) parts = append(parts, renderFooter(m))
} }
view := lipgloss.JoinVertical(lipgloss.Left, parts...) view := lipgloss.NewStyle().
Width(m.width).
Height(m.height).
Background(m.palette.Background).
Foreground(m.palette.Text).
Render(lipgloss.JoinVertical(lipgloss.Left, parts...))
if m.modal.kind != modalNone { if m.modal.kind != modalNone {
view = overlayCenter(view, renderModal(m.modal, m.palette, min(64, m.width-8)), m.width) view = overlayCenter(view, renderModal(m.modal, m.palette, min(64, m.width-8)), m.width)
} }
@ -920,6 +916,7 @@ func (m *Model) resizePreview() {
func renderPreviewPane(preview vfs.Preview, viewportModel *viewport.Model, cfg config.Config, palette theme.Palette, width int, height int) string { func renderPreviewPane(preview vfs.Preview, viewportModel *viewport.Model, cfg config.Config, palette theme.Palette, width int, height int) string {
innerWidth := max(width-2, 1) innerWidth := max(width-2, 1)
innerHeight := max(height-2, 1) innerHeight := max(height-2, 1)
contentWidth := max(innerWidth-2, 1)
box := lipgloss.NewStyle(). box := lipgloss.NewStyle().
Width(innerWidth). Width(innerWidth).
@ -927,10 +924,11 @@ func renderPreviewPane(preview vfs.Preview, viewportModel *viewport.Model, cfg c
Background(palette.Panel). Background(palette.Panel).
Foreground(palette.Text). Foreground(palette.Text).
BorderStyle(borderStyle(cfg.UI.Border)). BorderStyle(borderStyle(cfg.UI.Border)).
BorderForeground(palette.BorderActive) BorderForeground(palette.BorderActive).
BorderBackground(palette.Panel)
title := lipgloss.NewStyle(). title := lipgloss.NewStyle().
Width(innerWidth). Width(contentWidth).
Padding(0, 1). Padding(0, 1).
Background(palette.Accent). Background(palette.Accent).
Foreground(palette.Background). Foreground(palette.Background).
@ -949,7 +947,12 @@ func renderPreviewPane(preview vfs.Preview, viewportModel *viewport.Model, cfg c
viewportModel.Height = max(contentHeight-3, 1) viewportModel.Height = max(contentHeight-3, 1)
parts = append(parts, renderPreviewContent(viewportModel, palette, innerWidth, contentHeight)) parts = append(parts, renderPreviewContent(viewportModel, palette, innerWidth, contentHeight))
return box.Render(lipgloss.JoinVertical(lipgloss.Left, parts...)) content := lipgloss.NewStyle().
Width(innerWidth).
MaxHeight(innerHeight).
Background(palette.Panel).
Render(lipgloss.JoinVertical(lipgloss.Left, parts...))
return box.Render(content)
} }
func renderSelectionPane(preview vfs.Preview, viewportModel *viewport.Model, palette theme.Palette, width int, height int) string { func renderSelectionPane(preview vfs.Preview, viewportModel *viewport.Model, palette theme.Palette, width int, height int) string {
@ -970,6 +973,8 @@ func renderSelectionPane(preview vfs.Preview, viewportModel *viewport.Model, pal
} }
func renderMetadata(meta vfs.Metadata, palette theme.Palette, width int) string { func renderMetadata(meta vfs.Metadata, palette theme.Palette, width int) string {
outerWidth := max(width-2, 1)
innerWidth := max(outerWidth-2, 1)
leftRows := []string{ leftRows := []string{
fmt.Sprintf("kind: %s", fallback(meta.Kind, "n/a")), fmt.Sprintf("kind: %s", fallback(meta.Kind, "n/a")),
fmt.Sprintf("size: %s", metaSize(meta)), fmt.Sprintf("size: %s", metaSize(meta)),
@ -983,29 +988,36 @@ func renderMetadata(meta vfs.Metadata, palette theme.Palette, width int) string
rightRows = append(rightRows, fmt.Sprintf("image: %s %s", meta.ImageFormat, meta.ImageSize)) rightRows = append(rightRows, fmt.Sprintf("image: %s %s", meta.ImageFormat, meta.ImageSize))
} }
leftWidth := max(width/2, 18) leftWidth := max(innerWidth/2, 18)
rightWidth := max(width-leftWidth, 18) if leftWidth > innerWidth {
leftWidth = innerWidth
}
rightWidth := max(innerWidth-leftWidth, 0)
left := lipgloss.NewStyle(). left := lipgloss.NewStyle().
Width(leftWidth). Width(leftWidth).
Background(palette.PanelElevated).
Foreground(palette.Muted). Foreground(palette.Muted).
Render(strings.Join(leftRows, "\n")) Render(strings.Join(leftRows, "\n"))
right := lipgloss.NewStyle(). right := lipgloss.NewStyle().
Width(rightWidth). Width(rightWidth).
Background(palette.PanelElevated).
Foreground(palette.Text). Foreground(palette.Text).
Render(strings.Join(rightRows, "\n")) Render(strings.Join(rightRows, "\n"))
pathLine := lipgloss.NewStyle(). pathLine := lipgloss.NewStyle().
Width(width). Width(innerWidth).
Background(palette.PanelElevated).
Foreground(palette.Text). Foreground(palette.Text).
Render(fmt.Sprintf("path: %s", truncateMiddle(meta.Path, max(width-8, 16)))) Render(fmt.Sprintf("path: %s", truncateMiddle(meta.Path, max(innerWidth-8, 16))))
return lipgloss.NewStyle(). return lipgloss.NewStyle().
Width(width). Width(outerWidth).
Padding(0, 1). Padding(0, 1).
Background(palette.PanelElevated). Background(palette.PanelElevated).
BorderStyle(lipgloss.NormalBorder()). BorderStyle(lipgloss.NormalBorder()).
BorderBottom(true). BorderBottom(true).
BorderForeground(palette.Border). BorderForeground(palette.Border).
BorderBackground(palette.PanelElevated).
Render(lipgloss.JoinVertical( Render(lipgloss.JoinVertical(
lipgloss.Left, lipgloss.Left,
lipgloss.JoinHorizontal(lipgloss.Top, left, right), lipgloss.JoinHorizontal(lipgloss.Top, left, right),
@ -1073,21 +1085,41 @@ func renderStatus(m Model) string {
} }
func renderFooter(m Model) string { func renderFooter(m Model) string {
helpModel := m.helpModel parts := make([]string, 0, 8)
helpModel.Width = max(m.width-4, 20) for _, binding := range m.keys.ShortHelp() {
helpView := helpModel.View(m.keys) help := binding.Help()
modeLabel := "" if help.Key == "" || help.Desc == "" {
continue
}
keyView := lipgloss.NewStyle().
Background(m.palette.Background).
Foreground(m.palette.FooterKey).
Bold(true).
Render(help.Key)
descView := lipgloss.NewStyle().
Background(m.palette.Background).
Foreground(m.palette.Text).
Render(" " + help.Desc)
parts = append(parts, keyView+descView)
}
line := strings.Join(parts, " ")
if m.selectMode { if m.selectMode {
modeLabel = lipgloss.NewStyle(). modeLabel := lipgloss.NewStyle().
Foreground(m.palette.Accent). Foreground(m.palette.Accent).
Bold(true). Bold(true).
Render(" SELECT TEXT MODE") Render("SELECT TEXT MODE")
if line != "" {
line += " "
}
line += modeLabel
} }
return lipgloss.NewStyle(). line = " " + line
Width(m.width). return lipgloss.PlaceHorizontal(
Padding(0, 1). m.width,
Background(m.palette.Panel). lipgloss.Left,
Render(lipgloss.JoinHorizontal(lipgloss.Top, helpView, modeLabel)) line,
lipgloss.WithWhitespaceBackground(m.palette.Background),
)
} }
func renderModal(modal modalState, palette theme.Palette, width int) string { func renderModal(modal modalState, palette theme.Palette, width int) string {
@ -1123,10 +1155,12 @@ func overlayCenter(base string, overlay string, width int) string {
} }
func renderPreviewContent(viewportModel *viewport.Model, palette theme.Palette, width int, height int) string { func renderPreviewContent(viewportModel *viewport.Model, palette theme.Palette, width int, height int) string {
outerWidth := max(width-2, 1)
innerWidth := max(outerWidth-2, 1)
innerHeight := max(height-1, 1) innerHeight := max(height-1, 1)
header := lipgloss.NewStyle(). header := lipgloss.NewStyle().
Width(width). Width(innerWidth).
Padding(0, 1). Padding(0, 1).
Background(palette.PanelInactive). Background(palette.PanelInactive).
Foreground(palette.FooterKey). Foreground(palette.FooterKey).
@ -1134,18 +1168,20 @@ func renderPreviewContent(viewportModel *viewport.Model, palette theme.Palette,
Render("CONTENT") Render("CONTENT")
body := lipgloss.NewStyle(). body := lipgloss.NewStyle().
Width(width). Width(innerWidth).
Height(max(innerHeight-lipgloss.Height(header), 1)). Height(max(innerHeight-lipgloss.Height(header), 1)).
Padding(0, 1). Padding(0, 1).
Background(palette.Panel). Background(palette.Panel).
Render(viewportModel.View()) Render(viewportModel.View())
return lipgloss.NewStyle(). return lipgloss.NewStyle().
Width(width). Width(outerWidth).
Height(innerHeight). Height(innerHeight).
Background(palette.Panel).
BorderStyle(lipgloss.NormalBorder()). BorderStyle(lipgloss.NormalBorder()).
BorderTop(true). BorderTop(true).
BorderForeground(palette.Border). BorderForeground(palette.Border).
BorderBackground(palette.Panel).
Render(lipgloss.JoinVertical(lipgloss.Left, header, body)) Render(lipgloss.JoinVertical(lipgloss.Left, header, body))
} }

View file

@ -108,6 +108,7 @@ func renderPane(
borderColor := palette.Border borderColor := palette.Border
headerBg := palette.PanelInactive headerBg := palette.PanelInactive
bodyBg := palette.Panel
if active { if active {
borderColor = palette.BorderActive borderColor = palette.BorderActive
headerBg = palette.Selection headerBg = palette.Selection
@ -119,17 +120,18 @@ func renderPane(
box := lipgloss.NewStyle(). box := lipgloss.NewStyle().
Width(innerWidth). Width(innerWidth).
Height(innerHeight). Height(innerHeight).
Background(palette.PanelInactive). Background(bodyBg).
Foreground(palette.Text). Foreground(palette.Text).
BorderStyle(borderStyle(cfg.UI.Border)). BorderStyle(borderStyle(cfg.UI.Border)).
BorderForeground(borderColor) BorderForeground(borderColor).
BorderBackground(bodyBg)
header := lipgloss.NewStyle(). header := lipgloss.NewStyle().
Render(renderPaneHeader(pane, cfg, palette, innerWidth, active, headerBg)) Render(renderPaneHeader(pane, cfg, palette, innerWidth, active, headerBg))
rowsHeight := max(innerHeight-2, 1) rowsHeight := max(innerHeight-2, 1)
headerRow := renderColumnsHeader(cfg, innerWidth, palette) headerRow := renderColumnsHeader(cfg, innerWidth, palette, bodyBg)
rows := renderPaneRows(pane, cfg, palette, innerWidth, rowsHeight, active, hoverIndex) rows := renderPaneRows(pane, cfg, palette, innerWidth, rowsHeight, active, hoverIndex, bodyBg)
content := lipgloss.JoinVertical(lipgloss.Left, header, headerRow, rows) content := lipgloss.JoinVertical(lipgloss.Left, header, headerRow, rows)
return box.Render(content) return box.Render(content)
} }
@ -147,6 +149,10 @@ func renderPaneHeader(pane BrowserPane, cfg config.Config, palette theme.Palette
labelView := labelStyle.Render(label) labelView := labelStyle.Render(label)
pathWidth := max(width-lipgloss.Width(labelView)-1, 4) pathWidth := max(width-lipgloss.Width(labelView)-1, 4)
spacer := lipgloss.NewStyle().
Width(1).
Background(headerBg).
Render(" ")
pathStyle := lipgloss.NewStyle(). pathStyle := lipgloss.NewStyle().
Width(pathWidth). Width(pathWidth).
Background(headerBg). Background(headerBg).
@ -156,37 +162,39 @@ func renderPaneHeader(pane BrowserPane, cfg config.Config, palette theme.Palette
return lipgloss.NewStyle(). return lipgloss.NewStyle().
Width(width). Width(width).
Background(headerBg). Background(headerBg).
Render(labelView + " " + pathStyle.Render(truncateMiddle(compactPath(pane.Path, cfg.UI.PathDisplay), pathWidth))) Render(labelView + spacer + pathStyle.Render(truncateMiddle(compactPath(pane.Path, cfg.UI.PathDisplay), pathWidth)))
} }
func renderColumnsHeader(cfg config.Config, width int, palette theme.Palette) string { func renderColumnsHeader(cfg config.Config, width int, palette theme.Palette, background lipgloss.Color) string {
columns := buildColumns(cfg, width) columns := buildColumns(cfg, width)
parts := make([]string, 0, len(columns)) parts := make([]string, 0, len(columns))
for idx, column := range columns { for idx, column := range columns {
style := lipgloss.NewStyle(). style := lipgloss.NewStyle().
Width(column.Width). Width(column.Width).
Foreground(palette.Muted). Foreground(palette.Muted).
Background(background).
Bold(true) Bold(true)
if column.AlignRight { if column.AlignRight {
style = style.Align(lipgloss.Right) style = style.Align(lipgloss.Right)
} }
parts = append(parts, style.Render(truncateRight(column.Title, column.Width))) parts = append(parts, style.Render(truncateRight(column.Title, column.Width)))
if idx < len(columns)-1 { if idx < len(columns)-1 {
parts = append(parts, columnSeparator(column.Key, palette, lipgloss.Color(""))) parts = append(parts, columnSeparator(column.Key, palette, background))
} }
} }
return lipgloss.NewStyle(). return lipgloss.NewStyle().
Width(width). Width(width).
Background(palette.Panel). Background(background).
Render(strings.Join(parts, "")) Render(strings.Join(parts, ""))
} }
func renderPaneRows(pane BrowserPane, cfg config.Config, palette theme.Palette, width int, height int, active bool, hoverIndex int) string { func renderPaneRows(pane BrowserPane, cfg config.Config, palette theme.Palette, width int, height int, active bool, hoverIndex int, background lipgloss.Color) string {
if len(pane.Entries) == 0 { if len(pane.Entries) == 0 {
return lipgloss.NewStyle(). return lipgloss.NewStyle().
Width(width). Width(width).
Height(height). Height(height).
Padding(1, 1). Padding(1, 1).
Background(background).
Foreground(palette.Muted). Foreground(palette.Muted).
Render("Empty directory") Render("Empty directory")
} }
@ -198,21 +206,22 @@ func renderPaneRows(pane BrowserPane, cfg config.Config, palette theme.Palette,
lines := make([]string, 0, visibleHeight) lines := make([]string, 0, visibleHeight)
for idx := pane.Offset; idx < end; idx++ { for idx := pane.Offset; idx < end; idx++ {
entry := pane.Entries[idx] entry := pane.Entries[idx]
row := renderEntryRow(entry, cfg, width, idx == pane.Cursor, idx == hoverIndex, active, palette) row := renderEntryRow(entry, cfg, width, idx == pane.Cursor, idx == hoverIndex, active, palette, background)
lines = append(lines, row) lines = append(lines, row)
} }
for len(lines) < visibleHeight { for len(lines) < visibleHeight {
lines = append(lines, lipgloss.NewStyle().Width(width).Render("")) lines = append(lines, lipgloss.NewStyle().Width(width).Background(background).Render(""))
} }
return lipgloss.NewStyle(). return lipgloss.NewStyle().
Width(width). Width(width).
Background(background).
Render(strings.Join(lines, "\n")) Render(strings.Join(lines, "\n"))
} }
func renderEntryRow(entry vfs.Entry, cfg config.Config, width int, selected bool, hovered bool, active bool, palette theme.Palette) string { func renderEntryRow(entry vfs.Entry, cfg config.Config, width int, selected bool, hovered bool, active bool, palette theme.Palette, baseBackground lipgloss.Color) string {
columns := buildColumns(cfg, width) columns := buildColumns(cfg, width)
rowBackground := lipgloss.Color("") rowBackground := baseBackground
switch { switch {
case selected: case selected:
rowBackground = palette.Selection rowBackground = palette.Selection
@ -225,10 +234,8 @@ func renderEntryRow(entry vfs.Entry, cfg config.Config, width int, selected bool
value := column.Value(entry, cfg.Browser.HumanReadableSize) value := column.Value(entry, cfg.Browser.HumanReadableSize)
style := lipgloss.NewStyle(). style := lipgloss.NewStyle().
Width(column.Width). Width(column.Width).
Foreground(entryColor(entry, palette)) Foreground(entryColor(entry, palette)).
if rowBackground != lipgloss.Color("") { Background(rowBackground)
style = style.Background(rowBackground)
}
if entry.IsHidden { if entry.IsHidden {
style = style.Foreground(palette.Muted) style = style.Foreground(palette.Muted)
@ -242,10 +249,7 @@ func renderEntryRow(entry vfs.Entry, cfg config.Config, width int, selected bool
} }
} }
rowStyle := lipgloss.NewStyle().Width(width) rowStyle := lipgloss.NewStyle().Width(width).Background(rowBackground)
if rowBackground != lipgloss.Color("") {
rowStyle = rowStyle.Background(rowBackground)
}
if selected && active { if selected && active {
rowStyle = rowStyle.Bold(true) rowStyle = rowStyle.Bold(true)
} }