Compare commits

..

1 Commits

Author SHA1 Message Date
682510d1bc v3.42.16: TUI cleanup - remove STATUS box, add global styles
All checks were successful
CI/CD / Test (push) Successful in 1m19s
CI/CD / Lint (push) Successful in 1m24s
CI/CD / Build & Release (push) Successful in 3m19s
2026-01-08 11:17:46 +01:00
3 changed files with 165 additions and 39 deletions

View File

@@ -4,8 +4,8 @@ This directory contains pre-compiled binaries for the DB Backup Tool across mult
## Build Information ## Build Information
- **Version**: 3.42.10 - **Version**: 3.42.10
- **Build Time**: 2026-01-08_09:40:57_UTC - **Build Time**: 2026-01-08_09:54:02_UTC
- **Git Commit**: 55d34be - **Git Commit**: 83ad62b
## Recent Updates (v1.1.0) ## Recent Updates (v1.1.0)
- ✅ Fixed TUI progress display with line-by-line output - ✅ Fixed TUI progress display with line-by-line output

View File

@@ -9,7 +9,6 @@ import (
"time" "time"
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
"github.com/mattn/go-runewidth"
"dbbackup/internal/config" "dbbackup/internal/config"
"dbbackup/internal/logger" "dbbackup/internal/logger"
@@ -229,72 +228,66 @@ func (m BackupManagerModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
func (m BackupManagerModel) View() string { func (m BackupManagerModel) View() string {
var s strings.Builder var s strings.Builder
const boxWidth = 60
// Helper to pad string to box width (handles UTF-8)
padToWidth := func(text string, width int) string {
textWidth := runewidth.StringWidth(text)
if textWidth >= width {
return runewidth.Truncate(text, width-3, "...")
}
return text + strings.Repeat(" ", width-textWidth)
}
// Title // Title
s.WriteString(titleStyle.Render("[DB] Backup Archive Manager")) s.WriteString(TitleStyle.Render("[DB] Backup Archive Manager"))
s.WriteString("\n\n") s.WriteString("\n\n")
// Operation Status Box (always visible) // Status line (no box, bold+color accents)
s.WriteString("+--[ STATUS ]" + strings.Repeat("-", boxWidth-13) + "+\n")
switch m.opState { switch m.opState {
case OpVerifying: case OpVerifying:
spinner := spinnerFrames[m.spinnerFrame] spinner := spinnerFrames[m.spinnerFrame]
statusText := fmt.Sprintf(" %s Verifying: %s", spinner, m.opTarget) s.WriteString(StatusActiveStyle.Render(fmt.Sprintf("%s Verifying: %s", spinner, m.opTarget)))
s.WriteString("|" + padToWidth(statusText, boxWidth) + "|\n") s.WriteString("\n\n")
case OpDeleting: case OpDeleting:
spinner := spinnerFrames[m.spinnerFrame] spinner := spinnerFrames[m.spinnerFrame]
statusText := fmt.Sprintf(" %s Deleting: %s", spinner, m.opTarget) s.WriteString(StatusActiveStyle.Render(fmt.Sprintf("%s Deleting: %s", spinner, m.opTarget)))
s.WriteString("|" + padToWidth(statusText, boxWidth) + "|\n") s.WriteString("\n\n")
default: default:
if m.loading { if m.loading {
spinner := spinnerFrames[m.spinnerFrame] spinner := spinnerFrames[m.spinnerFrame]
statusText := fmt.Sprintf(" %s Loading archives...", spinner) s.WriteString(StatusActiveStyle.Render(fmt.Sprintf("%s Loading archives...", spinner)))
s.WriteString("|" + padToWidth(statusText, boxWidth) + "|\n") s.WriteString("\n\n")
} else if m.message != "" { } else if m.message != "" {
msgText := " " + m.message // Color based on message content
s.WriteString("|" + padToWidth(msgText, boxWidth) + "|\n") if strings.HasPrefix(m.message, "[+]") || strings.HasPrefix(m.message, "Valid") {
s.WriteString(StatusSuccessStyle.Render(m.message))
} else if strings.HasPrefix(m.message, "[-]") || strings.HasPrefix(m.message, "Error") {
s.WriteString(StatusErrorStyle.Render(m.message))
} else { } else {
s.WriteString("|" + padToWidth(" Ready", boxWidth) + "|\n") s.WriteString(StatusActiveStyle.Render(m.message))
} }
s.WriteString("\n\n")
}
// No "Ready" message when idle - cleaner UI
} }
s.WriteString("+" + strings.Repeat("-", boxWidth) + "+\n\n")
if m.loading { if m.loading {
return s.String() return s.String()
} }
if m.err != nil { if m.err != nil {
s.WriteString(errorStyle.Render(fmt.Sprintf("[FAIL] Error: %v", m.err))) s.WriteString(StatusErrorStyle.Render(fmt.Sprintf("[FAIL] Error: %v", m.err)))
s.WriteString("\n\n") s.WriteString("\n\n")
s.WriteString(infoStyle.Render("Press Esc to go back")) s.WriteString(ShortcutStyle.Render("Press Esc to go back"))
return s.String() return s.String()
} }
// Summary // Summary
s.WriteString(infoStyle.Render(fmt.Sprintf("Total Archives: %d | Total Size: %s", s.WriteString(LabelStyle.Render(fmt.Sprintf("Total Archives: %d | Total Size: %s",
len(m.archives), formatSize(m.totalSize)))) len(m.archives), formatSize(m.totalSize))))
s.WriteString("\n\n") s.WriteString("\n\n")
// Archives list // Archives list
if len(m.archives) == 0 { if len(m.archives) == 0 {
s.WriteString(infoStyle.Render("No backup archives found")) s.WriteString(StatusReadyStyle.Render("No backup archives found"))
s.WriteString("\n\n") s.WriteString("\n\n")
s.WriteString(infoStyle.Render("Press Esc to go back")) s.WriteString(ShortcutStyle.Render("Press Esc to go back"))
return s.String() return s.String()
} }
// Column headers with better alignment // Column headers with better alignment
s.WriteString(archiveHeaderStyle.Render(fmt.Sprintf(" %-32s %-22s %10s %-16s", s.WriteString(ListHeaderStyle.Render(fmt.Sprintf(" %-32s %-22s %10s %-16s",
"FILENAME", "FORMAT", "SIZE", "MODIFIED"))) "FILENAME", "FORMAT", "SIZE", "MODIFIED")))
s.WriteString("\n") s.WriteString("\n")
s.WriteString(strings.Repeat("-", 90)) s.WriteString(strings.Repeat("-", 90))
@@ -313,18 +306,18 @@ func (m BackupManagerModel) View() string {
for i := start; i < end; i++ { for i := start; i < end; i++ {
archive := m.archives[i] archive := m.archives[i]
cursor := " " cursor := " "
style := archiveNormalStyle style := ListNormalStyle
if i == m.cursor { if i == m.cursor {
cursor = "> " cursor = "> "
style = archiveSelectedStyle style = ListSelectedStyle
} }
// Status icon - consistent 4-char width // Status icon - consistent 4-char width
statusIcon := " [+]" statusIcon := " [+]"
if !archive.Valid { if !archive.Valid {
statusIcon = " [-]" statusIcon = " [-]"
style = archiveInvalidStyle style = ItemInvalidStyle
} else if time.Since(archive.Modified) > 30*24*time.Hour { } else if time.Since(archive.Modified) > 30*24*time.Hour {
statusIcon = " [!]" statusIcon = " [!]"
} }
@@ -347,11 +340,11 @@ func (m BackupManagerModel) View() string {
// Footer // Footer
s.WriteString("\n") s.WriteString("\n")
s.WriteString(infoStyle.Render(fmt.Sprintf("Selected: %d/%d", m.cursor+1, len(m.archives)))) s.WriteString(StatusReadyStyle.Render(fmt.Sprintf("Selected: %d/%d", m.cursor+1, len(m.archives))))
s.WriteString("\n\n") s.WriteString("\n\n")
// Grouped keyboard shortcuts - simple aligned format // Grouped keyboard shortcuts
s.WriteString("SHORTCUTS: Up/Down=Move | r=Restore | v=Verify | d=Delete | i=Info | R=Refresh | Esc=Back | q=Quit") s.WriteString(ShortcutStyle.Render("SHORTCUTS: Up/Down=Move | r=Restore | v=Verify | d=Delete | i=Info | R=Refresh | Esc=Back | q=Quit"))
return s.String() return s.String()
} }

133
internal/tui/styles.go Normal file
View File

@@ -0,0 +1,133 @@
package tui
import "github.com/charmbracelet/lipgloss"
// =============================================================================
// GLOBAL TUI STYLE DEFINITIONS
// =============================================================================
// Design Language:
// - Bold text for labels and headers
// - Colors for semantic meaning (green=success, red=error, yellow=warning)
// - No emoticons - use simple text prefixes like [OK], [FAIL], [!]
// - No boxes for inline status - use bold+color accents
// - Consistent color palette across all views
// =============================================================================
// Color Palette (ANSI 256 colors for terminal compatibility)
const (
ColorWhite = lipgloss.Color("15") // Bright white
ColorGray = lipgloss.Color("250") // Light gray
ColorDim = lipgloss.Color("244") // Dim gray
ColorDimmer = lipgloss.Color("240") // Darker gray
ColorSuccess = lipgloss.Color("2") // Green
ColorError = lipgloss.Color("1") // Red
ColorWarning = lipgloss.Color("3") // Yellow
ColorInfo = lipgloss.Color("6") // Cyan
ColorAccent = lipgloss.Color("4") // Blue
)
// =============================================================================
// TITLE & HEADER STYLES
// =============================================================================
// TitleStyle - main view title (bold white on gray background)
var TitleStyle = lipgloss.NewStyle().
Bold(true).
Foreground(ColorWhite).
Background(ColorDimmer).
Padding(0, 1)
// HeaderStyle - section headers (bold gray)
var HeaderStyle = lipgloss.NewStyle().
Bold(true).
Foreground(ColorDim)
// LabelStyle - field labels (bold cyan)
var LabelStyle = lipgloss.NewStyle().
Bold(true).
Foreground(ColorInfo)
// =============================================================================
// STATUS STYLES
// =============================================================================
// StatusReadyStyle - idle/ready state (dim)
var StatusReadyStyle = lipgloss.NewStyle().
Foreground(ColorDim)
// StatusActiveStyle - operation in progress (bold cyan)
var StatusActiveStyle = lipgloss.NewStyle().
Bold(true).
Foreground(ColorInfo)
// StatusSuccessStyle - success messages (bold green)
var StatusSuccessStyle = lipgloss.NewStyle().
Bold(true).
Foreground(ColorSuccess)
// StatusErrorStyle - error messages (bold red)
var StatusErrorStyle = lipgloss.NewStyle().
Bold(true).
Foreground(ColorError)
// StatusWarningStyle - warning messages (bold yellow)
var StatusWarningStyle = lipgloss.NewStyle().
Bold(true).
Foreground(ColorWarning)
// =============================================================================
// LIST & TABLE STYLES
// =============================================================================
// ListNormalStyle - unselected list items
var ListNormalStyle = lipgloss.NewStyle().
Foreground(ColorGray)
// ListSelectedStyle - selected/cursor item (bold white)
var ListSelectedStyle = lipgloss.NewStyle().
Foreground(ColorWhite).
Bold(true)
// ListHeaderStyle - column headers (bold dim)
var ListHeaderStyle = lipgloss.NewStyle().
Bold(true).
Foreground(ColorDim)
// =============================================================================
// ITEM STATUS STYLES
// =============================================================================
// ItemValidStyle - valid/OK items (green)
var ItemValidStyle = lipgloss.NewStyle().
Foreground(ColorSuccess)
// ItemInvalidStyle - invalid/failed items (red)
var ItemInvalidStyle = lipgloss.NewStyle().
Foreground(ColorError)
// ItemOldStyle - old/stale items (yellow)
var ItemOldStyle = lipgloss.NewStyle().
Foreground(ColorWarning)
// =============================================================================
// SHORTCUT STYLE
// =============================================================================
// ShortcutStyle - keyboard shortcuts footer (dim)
var ShortcutStyle = lipgloss.NewStyle().
Foreground(ColorDim)
// =============================================================================
// HELPER PREFIXES (no emoticons)
// =============================================================================
const (
PrefixOK = "[OK]"
PrefixFail = "[FAIL]"
PrefixWarn = "[!]"
PrefixInfo = "[i]"
PrefixPlus = "[+]"
PrefixMinus = "[-]"
PrefixArrow = ">"
PrefixSpinner = "" // Spinner character added dynamically
)