fix: cream background fills entire terminal, bottom bar pinned to bottom

- PadLine now wraps entire line content with Background(ScreenBg) so
  cream color shows behind all text, not just the right-edge padding
- Added PadLineWithBg for active row warm highlight (#f8f0e6)
- Restructured View() to pin bottom bar at terminal bottom with blank
  fill between file list and footer
- Removed 'd remove' from bottom bar keybindings to match web reference
This commit is contained in:
noah
2026-03-09 23:13:32 +01:00
parent c9f1242823
commit ddab9087d8
2 changed files with 87 additions and 63 deletions
+13 -9
View File
@@ -18,7 +18,7 @@ var (
Teal = lipgloss.Color("#2dd4bf") Teal = lipgloss.Color("#2dd4bf")
Cream = lipgloss.Color("#fdf6ef") Cream = lipgloss.Color("#fdf6ef")
Warm = lipgloss.Color("#faf0e6") Warm = lipgloss.Color("#f8f0e6")
Peach = lipgloss.Color("#fce8d5") Peach = lipgloss.Color("#fce8d5")
Dark = lipgloss.Color("#2d1f14") Dark = lipgloss.Color("#2d1f14")
@@ -149,18 +149,22 @@ var (
Bold(true) Bold(true)
) )
// PadLine pads a single rendered line to the given width with the screen // PadLine pads a single rendered line to the full terminal width and applies
// background color. This ensures every line carries the background color // the cream background behind ALL content (not just the padding). This is
// all the way to the right edge of the terminal. // achieved by placing the line inside a full-width style with Background set.
func PadLine(line string, width int) string { func PadLine(line string, width int) string {
return PadLineWithBg(line, width, ScreenBg)
}
// PadLineWithBg pads a line to full width with a specific background color.
func PadLineWithBg(line string, width int, bg color.Color) string {
w := lipgloss.Width(line) w := lipgloss.Width(line)
if w >= width { if w >= width {
return line // Even if the line is already wide enough, wrap with background
return lipgloss.NewStyle().Background(bg).Render(line)
} }
pad := lipgloss.NewStyle(). pad := strings.Repeat(" ", width-w)
Background(ScreenBg). return lipgloss.NewStyle().Background(bg).Render(line + pad)
Render(strings.Repeat(" ", width-w))
return line + pad
} }
// FillBlankLines returns n blank lines fully painted with the screen // FillBlankLines returns n blank lines fully painted with the screen
+74 -54
View File
@@ -31,51 +31,85 @@ func nameWidth(termW int) int {
return w return w
} }
// pad is a shortcut that pads a line to full width with cream background.
func (m Model) pad(line string) string {
return theme.PadLine(line, m.width)
}
// padWarm pads a line to full width with the warm (highlighted) background.
func (m Model) padWarm(line string) string {
return theme.PadLineWithBg(line, m.width, theme.Warm)
}
// blank returns a full-width blank line with cream background.
func (m Model) blank() string {
return theme.PadLine("", m.width)
}
// View renders the entire TUI, filling the full terminal. // View renders the entire TUI, filling the full terminal.
func (m Model) View() string { func (m Model) View() string {
if m.width == 0 || m.height == 0 { if m.width == 0 || m.height == 0 {
return "Loading..." return "Loading..."
} }
var sections []string // We build an array of pre-padded lines (each already full-width with bg).
var lines []string
sections = append(sections, m.renderTitleBar()) // Title bar (1 line)
sections = append(sections, m.renderDivider()) lines = append(lines, m.pad(m.renderTitleBar()))
// Divider (1 line)
lines = append(lines, m.pad(m.renderDivider()))
// State-specific content
var bottomLines []string // lines that go at the very bottom
switch m.state { switch m.state {
case stateFileList: case stateFileList:
sections = append(sections, m.renderColumnHeader()) // Column header
sections = append(sections, m.renderFileList()) lines = append(lines, m.pad(m.renderColumnHeader()))
sections = append(sections, m.renderDivider())
sections = append(sections, m.renderBottomBar()) // File rows
fileLines := m.renderFileRows()
lines = append(lines, fileLines...)
// Bottom section: divider + bottom bar (pinned to bottom)
bottomLines = append(bottomLines, m.pad(m.renderDivider()))
bottomLines = append(bottomLines, m.pad(m.renderBottomBar()))
case stateConverting: case stateConverting:
sections = append(sections, m.renderConverting()) for _, l := range strings.Split(m.renderConverting(), "\n") {
lines = append(lines, m.pad(l))
}
case stateResults: case stateResults:
sections = append(sections, m.renderResults()) for _, l := range strings.Split(m.renderResults(), "\n") {
sections = append(sections, m.renderDivider()) lines = append(lines, m.pad(l))
sections = append(sections, m.renderResultsFooter()) }
bottomLines = append(bottomLines, m.pad(m.renderDivider()))
bottomLines = append(bottomLines, m.pad(m.renderResultsFooter()))
} }
// Help overlay (if visible, goes right after content)
if m.showHelp { if m.showHelp {
sections = append(sections, m.renderHelp()) for _, l := range strings.Split(m.renderHelp(), "\n") {
lines = append(lines, m.pad(l))
}
} }
content := lipgloss.JoinVertical(lipgloss.Left, sections...) // Calculate how many blank lines we need between content and bottom bar
totalUsed := len(lines) + len(bottomLines)
// Split into individual lines and pad each to full width with background remaining := m.height - totalUsed
lines := strings.Split(content, "\n")
for i, line := range lines {
lines[i] = theme.PadLine(line, m.width)
}
// Fill remaining vertical space with background-colored blank lines
remaining := m.height - len(lines)
if remaining > 0 { if remaining > 0 {
fill := theme.FillBlankLines(remaining, m.width) for i := 0; i < remaining; i++ {
return strings.Join(lines, "\n") + "\n" + fill lines = append(lines, m.blank())
}
} }
// Truncate if content exceeds terminal height // Append bottom bar lines
lines = append(lines, bottomLines...)
// Truncate if somehow we exceed terminal height
if len(lines) > m.height { if len(lines) > m.height {
lines = lines[:m.height] lines = lines[:m.height]
} }
@@ -122,7 +156,6 @@ func (m Model) renderDivider() string {
func (m Model) renderColumnHeader() string { func (m Model) renderColumnHeader() string {
nw := nameWidth(m.width) nw := nameWidth(m.width)
// "Name" sits after the cursor+check prefix (6 chars), indented
nameHdr := theme.Breadcrumb.Copy().Width(colCursor + colCheck + nw).Render( nameHdr := theme.Breadcrumb.Copy().Width(colCursor + colCheck + nw).Render(
strings.Repeat(" ", colCursor+colCheck) + "Name") strings.Repeat(" ", colCursor+colCheck) + "Name")
sizeHdr := theme.Breadcrumb.Copy().Width(colSize).Align(lipgloss.Right).Render("Size") sizeHdr := theme.Breadcrumb.Copy().Width(colSize).Align(lipgloss.Right).Render("Size")
@@ -132,16 +165,16 @@ func (m Model) renderColumnHeader() string {
return nameHdr + " " + sizeHdr + " " + fmtHdr + " " + statHdr return nameHdr + " " + sizeHdr + " " + fmtHdr + " " + statHdr
} }
// ─── File list ─────────────────────────────────────────────── // ─── File rows ───────────────────────────────────────────────
func (m Model) renderFileList() string { // renderFileRows returns already-padded lines for the file list area.
func (m Model) renderFileRows() []string {
if len(m.files) == 0 { if len(m.files) == 0 {
empty := lipgloss.NewStyle(). empty := lipgloss.NewStyle().
Foreground(theme.Light). Foreground(theme.Light).
Italic(true). Italic(true).
Padding(2, 4). Render(" No supported files found. Pass file paths or glob patterns as arguments.")
Render("No supported files found. Pass file paths or glob patterns as arguments.") return []string{m.blank(), m.pad(empty), m.blank()}
return empty
} }
maxVisible := m.maxVisibleFiles() maxVisible := m.maxVisibleFiles()
@@ -152,26 +185,23 @@ func (m Model) renderFileList() string {
var rows []string var rows []string
for i := m.scroll; i < end; i++ { for i := m.scroll; i < end; i++ {
rows = append(rows, m.renderFileRow(i)) row := m.renderFileRow(i)
} isCursor := i == m.cursor
if isCursor {
// Pad with empty rows so the file list always fills the available space rows = append(rows, m.padWarm(row))
rendered := len(rows) } else {
if rendered < maxVisible { rows = append(rows, m.pad(row))
emptyRow := strings.Repeat(" ", m.width)
for i := rendered; i < maxVisible; i++ {
rows = append(rows, emptyRow)
} }
} }
// Scrollbar indicator // Scrollbar indicator (if needed)
if len(m.files) > maxVisible { if len(m.files) > maxVisible {
scrollInfo := theme.Help.Render(fmt.Sprintf( scrollInfo := theme.Help.Render(fmt.Sprintf(
" showing %d\u2013%d of %d", m.scroll+1, end, len(m.files))) " showing %d\u2013%d of %d", m.scroll+1, end, len(m.files)))
rows = append(rows, scrollInfo) rows = append(rows, m.pad(scrollInfo))
} }
return lipgloss.JoinVertical(lipgloss.Left, rows...) return rows
} }
func (m Model) renderFileRow(idx int) string { func (m Model) renderFileRow(idx int) string {
@@ -197,7 +227,6 @@ func (m Model) renderFileRow(idx int) string {
extBadge := theme.ExtBadge(catColor).Render(strings.ToUpper(f.ext)) extBadge := theme.ExtBadge(catColor).Render(strings.ToUpper(f.ext))
nameText := f.name nameText := f.name
// Reserve space for icon(2) + space(1) + ext(max 5) + space(1) + ...
maxName := nw - 10 maxName := nw - 10
if maxName < 8 { if maxName < 8 {
maxName = 8 maxName = 8
@@ -213,7 +242,6 @@ func (m Model) renderFileRow(idx int) string {
nameStyle = theme.FileName nameStyle = theme.FileName
} }
// Build the name cell content, then wrap in a fixed-width style
nameContent := icon + " " + extBadge + " " + nameStyle.Render(nameText) nameContent := icon + " " + extBadge + " " + nameStyle.Render(nameText)
nameCell := lipgloss.NewStyle().Width(nw).MaxWidth(nw).Render(nameContent) nameCell := lipgloss.NewStyle().Width(nw).MaxWidth(nw).Render(nameContent)
@@ -238,15 +266,7 @@ func (m Model) renderFileRow(idx int) string {
} }
statusCell := lipgloss.NewStyle().Width(colStatus).Align(lipgloss.Center).Render(statusStr) statusCell := lipgloss.NewStyle().Width(colStatus).Align(lipgloss.Center).Render(statusStr)
// ── Assemble row ── return cursor + check + nameCell + " " + sizeCell + " " + fmtCell + " " + statusCell
row := cursor + check + nameCell + " " + sizeCell + " " + fmtCell + " " + statusCell
// Highlight active row with warm background
if isCursor {
row = lipgloss.NewStyle().Background(theme.Warm).Render(row)
}
return row
} }
func renderFormatSelector(f fileEntry, active bool) string { func renderFormatSelector(f fileEntry, active bool) string {