diff --git a/bin/README.md b/bin/README.md index b19ea05..602392e 100644 --- a/bin/README.md +++ b/bin/README.md @@ -4,8 +4,8 @@ This directory contains pre-compiled binaries for the DB Backup Tool across mult ## Build Information - **Version**: 3.42.10 -- **Build Time**: 2026-01-08_09:40:57_UTC -- **Git Commit**: 55d34be +- **Build Time**: 2026-01-08_09:54:02_UTC +- **Git Commit**: 83ad62b ## Recent Updates (v1.1.0) - ✅ Fixed TUI progress display with line-by-line output diff --git a/internal/tui/backup_manager.go b/internal/tui/backup_manager.go index e9d26b9..1b8d9c4 100755 --- a/internal/tui/backup_manager.go +++ b/internal/tui/backup_manager.go @@ -9,7 +9,6 @@ import ( "time" tea "github.com/charmbracelet/bubbletea" - "github.com/mattn/go-runewidth" "dbbackup/internal/config" "dbbackup/internal/logger" @@ -229,72 +228,66 @@ func (m BackupManagerModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { func (m BackupManagerModel) View() string { 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 - s.WriteString(titleStyle.Render("[DB] Backup Archive Manager")) + s.WriteString(TitleStyle.Render("[DB] Backup Archive Manager")) s.WriteString("\n\n") - // Operation Status Box (always visible) - s.WriteString("+--[ STATUS ]" + strings.Repeat("-", boxWidth-13) + "+\n") + // Status line (no box, bold+color accents) switch m.opState { case OpVerifying: spinner := spinnerFrames[m.spinnerFrame] - statusText := fmt.Sprintf(" %s Verifying: %s", spinner, m.opTarget) - s.WriteString("|" + padToWidth(statusText, boxWidth) + "|\n") + s.WriteString(StatusActiveStyle.Render(fmt.Sprintf("%s Verifying: %s", spinner, m.opTarget))) + s.WriteString("\n\n") case OpDeleting: spinner := spinnerFrames[m.spinnerFrame] - statusText := fmt.Sprintf(" %s Deleting: %s", spinner, m.opTarget) - s.WriteString("|" + padToWidth(statusText, boxWidth) + "|\n") + s.WriteString(StatusActiveStyle.Render(fmt.Sprintf("%s Deleting: %s", spinner, m.opTarget))) + s.WriteString("\n\n") default: if m.loading { spinner := spinnerFrames[m.spinnerFrame] - statusText := fmt.Sprintf(" %s Loading archives...", spinner) - s.WriteString("|" + padToWidth(statusText, boxWidth) + "|\n") + s.WriteString(StatusActiveStyle.Render(fmt.Sprintf("%s Loading archives...", spinner))) + s.WriteString("\n\n") } else if m.message != "" { - msgText := " " + m.message - s.WriteString("|" + padToWidth(msgText, boxWidth) + "|\n") - } else { - s.WriteString("|" + padToWidth(" Ready", boxWidth) + "|\n") + // Color based on message content + 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 { + 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 { return s.String() } 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(infoStyle.Render("Press Esc to go back")) + s.WriteString(ShortcutStyle.Render("Press Esc to go back")) return s.String() } // 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)))) s.WriteString("\n\n") // Archives list 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(infoStyle.Render("Press Esc to go back")) + s.WriteString(ShortcutStyle.Render("Press Esc to go back")) return s.String() } // 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"))) s.WriteString("\n") s.WriteString(strings.Repeat("-", 90)) @@ -313,18 +306,18 @@ func (m BackupManagerModel) View() string { for i := start; i < end; i++ { archive := m.archives[i] cursor := " " - style := archiveNormalStyle + style := ListNormalStyle if i == m.cursor { cursor = "> " - style = archiveSelectedStyle + style = ListSelectedStyle } // Status icon - consistent 4-char width statusIcon := " [+]" if !archive.Valid { statusIcon = " [-]" - style = archiveInvalidStyle + style = ItemInvalidStyle } else if time.Since(archive.Modified) > 30*24*time.Hour { statusIcon = " [!]" } @@ -347,11 +340,11 @@ func (m BackupManagerModel) View() string { // Footer 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") - // Grouped keyboard shortcuts - simple aligned format - s.WriteString("SHORTCUTS: Up/Down=Move | r=Restore | v=Verify | d=Delete | i=Info | R=Refresh | Esc=Back | q=Quit") + // Grouped keyboard shortcuts + 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() } diff --git a/internal/tui/styles.go b/internal/tui/styles.go new file mode 100644 index 0000000..9fbb995 --- /dev/null +++ b/internal/tui/styles.go @@ -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 +)