Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ec33959e3e | |||
| 92402f0fdb | |||
| 682510d1bc |
21
SYSTEMD.md
21
SYSTEMD.md
@@ -116,8 +116,9 @@ sudo chmod 755 /usr/local/bin/dbbackup
|
|||||||
### Step 2: Create Configuration
|
### Step 2: Create Configuration
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Main configuration
|
# Main configuration in working directory (where service runs from)
|
||||||
sudo tee /etc/dbbackup/dbbackup.conf << 'EOF'
|
# dbbackup reads .dbbackup.conf from WorkingDirectory
|
||||||
|
sudo tee /var/lib/dbbackup/.dbbackup.conf << 'EOF'
|
||||||
# DBBackup Configuration
|
# DBBackup Configuration
|
||||||
db-type=postgres
|
db-type=postgres
|
||||||
host=localhost
|
host=localhost
|
||||||
@@ -128,6 +129,8 @@ compression=6
|
|||||||
retention-days=30
|
retention-days=30
|
||||||
min-backups=7
|
min-backups=7
|
||||||
EOF
|
EOF
|
||||||
|
sudo chown dbbackup:dbbackup /var/lib/dbbackup/.dbbackup.conf
|
||||||
|
sudo chmod 600 /var/lib/dbbackup/.dbbackup.conf
|
||||||
|
|
||||||
# Instance credentials (secure permissions)
|
# Instance credentials (secure permissions)
|
||||||
sudo tee /etc/dbbackup/env.d/cluster.conf << 'EOF'
|
sudo tee /etc/dbbackup/env.d/cluster.conf << 'EOF'
|
||||||
@@ -157,13 +160,15 @@ Group=dbbackup
|
|||||||
# Load configuration
|
# Load configuration
|
||||||
EnvironmentFile=-/etc/dbbackup/env.d/cluster.conf
|
EnvironmentFile=-/etc/dbbackup/env.d/cluster.conf
|
||||||
|
|
||||||
# Working directory
|
# Working directory (config is loaded from .dbbackup.conf here)
|
||||||
WorkingDirectory=/var/lib/dbbackup
|
WorkingDirectory=/var/lib/dbbackup
|
||||||
|
|
||||||
# Execute backup
|
# Execute backup (reads .dbbackup.conf from WorkingDirectory)
|
||||||
ExecStart=/usr/local/bin/dbbackup backup cluster \
|
ExecStart=/usr/local/bin/dbbackup backup cluster \
|
||||||
--config /etc/dbbackup/dbbackup.conf \
|
|
||||||
--backup-dir /var/lib/dbbackup/backups \
|
--backup-dir /var/lib/dbbackup/backups \
|
||||||
|
--host localhost \
|
||||||
|
--port 5432 \
|
||||||
|
--user postgres \
|
||||||
--allow-root
|
--allow-root
|
||||||
|
|
||||||
# Security hardening
|
# Security hardening
|
||||||
@@ -443,12 +448,12 @@ sudo systemctl status dbbackup-cluster.service
|
|||||||
# View detailed error
|
# View detailed error
|
||||||
sudo journalctl -u dbbackup-cluster.service -n 50 --no-pager
|
sudo journalctl -u dbbackup-cluster.service -n 50 --no-pager
|
||||||
|
|
||||||
# Test manually as dbbackup user
|
# Test manually as dbbackup user (run from working directory with .dbbackup.conf)
|
||||||
sudo -u dbbackup /usr/local/bin/dbbackup backup cluster --config /etc/dbbackup/dbbackup.conf
|
cd /var/lib/dbbackup && sudo -u dbbackup /usr/local/bin/dbbackup backup cluster
|
||||||
|
|
||||||
# Check permissions
|
# Check permissions
|
||||||
ls -la /var/lib/dbbackup/
|
ls -la /var/lib/dbbackup/
|
||||||
ls -la /etc/dbbackup/
|
ls -la /var/lib/dbbackup/.dbbackup.conf
|
||||||
```
|
```
|
||||||
|
|
||||||
### Permission Denied
|
### Permission Denied
|
||||||
|
|||||||
@@ -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_10:59:00_UTC
|
||||||
- **Git Commit**: 55d34be
|
- **Git Commit**: 92402f0
|
||||||
|
|
||||||
## 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
|
||||||
|
|||||||
@@ -33,8 +33,11 @@ RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
|
|||||||
# Environment
|
# Environment
|
||||||
EnvironmentFile=-/etc/dbbackup/env.d/cluster.conf
|
EnvironmentFile=-/etc/dbbackup/env.d/cluster.conf
|
||||||
|
|
||||||
|
# Working directory (config is loaded from .dbbackup.conf here)
|
||||||
|
WorkingDirectory=/var/lib/dbbackup
|
||||||
|
|
||||||
# Execution - cluster backup (all databases)
|
# Execution - cluster backup (all databases)
|
||||||
ExecStart={{.BinaryPath}} backup cluster --config {{.ConfigPath}}
|
ExecStart={{.BinaryPath}} backup cluster --backup-dir {{.BackupDir}}
|
||||||
TimeoutStartSec={{.TimeoutSeconds}}
|
TimeoutStartSec={{.TimeoutSeconds}}
|
||||||
|
|
||||||
# Post-backup metrics export
|
# Post-backup metrics export
|
||||||
|
|||||||
@@ -33,8 +33,11 @@ RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
|
|||||||
# Environment
|
# Environment
|
||||||
EnvironmentFile=-/etc/dbbackup/env.d/%i.conf
|
EnvironmentFile=-/etc/dbbackup/env.d/%i.conf
|
||||||
|
|
||||||
|
# Working directory (config is loaded from .dbbackup.conf here)
|
||||||
|
WorkingDirectory=/var/lib/dbbackup
|
||||||
|
|
||||||
# Execution
|
# Execution
|
||||||
ExecStart={{.BinaryPath}} backup {{.BackupType}} %i --config {{.ConfigPath}}
|
ExecStart={{.BinaryPath}} backup {{.BackupType}} %i --backup-dir {{.BackupDir}}
|
||||||
TimeoutStartSec={{.TimeoutSeconds}}
|
TimeoutStartSec={{.TimeoutSeconds}}
|
||||||
|
|
||||||
# Post-backup metrics export
|
# Post-backup metrics export
|
||||||
|
|||||||
@@ -4,15 +4,14 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
|
||||||
"strings"
|
"strings"
|
||||||
"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"
|
||||||
|
"dbbackup/internal/restore"
|
||||||
)
|
)
|
||||||
|
|
||||||
// OperationState represents the current operation state
|
// OperationState represents the current operation state
|
||||||
@@ -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,94 +340,79 @@ 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()
|
||||||
}
|
}
|
||||||
|
|
||||||
// verifyArchiveCmd runs actual archive verification
|
// verifyArchiveCmd runs the SAME verification as restore safety checks
|
||||||
|
// This ensures consistency between backup manager verify and restore preview
|
||||||
func verifyArchiveCmd(archive ArchiveInfo) tea.Cmd {
|
func verifyArchiveCmd(archive ArchiveInfo) tea.Cmd {
|
||||||
return func() tea.Msg {
|
return func() tea.Msg {
|
||||||
// Determine verification method based on format
|
var issues []string
|
||||||
var valid bool
|
|
||||||
var details string
|
|
||||||
var err error
|
|
||||||
|
|
||||||
switch {
|
// 1. Run the same archive integrity check as restore
|
||||||
case strings.HasSuffix(archive.Path, ".tar.gz") || strings.HasSuffix(archive.Path, ".tgz"):
|
safety := restore.NewSafety(nil, nil) // Doesn't need config/log for validation
|
||||||
// Verify tar.gz archive
|
if err := safety.ValidateArchive(archive.Path); err != nil {
|
||||||
cmd := exec.Command("tar", "-tzf", archive.Path)
|
return verifyResultMsg{
|
||||||
output, cmdErr := cmd.CombinedOutput()
|
archive: archive.Name,
|
||||||
if cmdErr != nil {
|
valid: false,
|
||||||
return verifyResultMsg{archive: archive.Name, valid: false, err: nil, details: "Archive corrupt or incomplete"}
|
err: nil,
|
||||||
|
details: fmt.Sprintf("Archive integrity: %v", err),
|
||||||
}
|
}
|
||||||
lines := strings.Split(string(output), "\n")
|
|
||||||
fileCount := 0
|
|
||||||
for _, l := range lines {
|
|
||||||
if l != "" {
|
|
||||||
fileCount++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
valid = true
|
|
||||||
details = fmt.Sprintf("%d files in archive", fileCount)
|
|
||||||
|
|
||||||
case strings.HasSuffix(archive.Path, ".dump") || strings.HasSuffix(archive.Path, ".sql"):
|
|
||||||
// Verify PostgreSQL dump with pg_restore --list
|
|
||||||
cmd := exec.Command("pg_restore", "--list", archive.Path)
|
|
||||||
output, cmdErr := cmd.CombinedOutput()
|
|
||||||
if cmdErr != nil {
|
|
||||||
// Try as plain SQL
|
|
||||||
if strings.HasSuffix(archive.Path, ".sql") {
|
|
||||||
// Just check file is readable and has content
|
|
||||||
fi, statErr := os.Stat(archive.Path)
|
|
||||||
if statErr == nil && fi.Size() > 0 {
|
|
||||||
valid = true
|
|
||||||
details = "Plain SQL file readable"
|
|
||||||
} else {
|
|
||||||
return verifyResultMsg{archive: archive.Name, valid: false, err: nil, details: "File empty or unreadable"}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return verifyResultMsg{archive: archive.Name, valid: false, err: nil, details: "pg_restore cannot read dump"}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
lines := strings.Split(string(output), "\n")
|
|
||||||
objectCount := 0
|
|
||||||
for _, l := range lines {
|
|
||||||
if l != "" && !strings.HasPrefix(l, ";") {
|
|
||||||
objectCount++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
valid = true
|
|
||||||
details = fmt.Sprintf("%d objects in dump", objectCount)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case strings.HasSuffix(archive.Path, ".sql.gz"):
|
// 2. Run the same deep diagnosis as restore
|
||||||
// Verify gzipped SQL
|
diagnoser := restore.NewDiagnoser(nil, false)
|
||||||
cmd := exec.Command("gzip", "-t", archive.Path)
|
diagResult, diagErr := diagnoser.DiagnoseFile(archive.Path)
|
||||||
if cmdErr := cmd.Run(); cmdErr != nil {
|
if diagErr != nil {
|
||||||
return verifyResultMsg{archive: archive.Name, valid: false, err: nil, details: "Gzip archive corrupt"}
|
return verifyResultMsg{
|
||||||
|
archive: archive.Name,
|
||||||
|
valid: false,
|
||||||
|
err: diagErr,
|
||||||
|
details: "Cannot diagnose archive",
|
||||||
}
|
}
|
||||||
valid = true
|
|
||||||
details = "Gzip integrity OK"
|
|
||||||
|
|
||||||
default:
|
|
||||||
// Unknown format - just check file exists and has size
|
|
||||||
fi, statErr := os.Stat(archive.Path)
|
|
||||||
if statErr != nil {
|
|
||||||
return verifyResultMsg{archive: archive.Name, valid: false, err: statErr, details: "Cannot access file"}
|
|
||||||
}
|
|
||||||
if fi.Size() == 0 {
|
|
||||||
return verifyResultMsg{archive: archive.Name, valid: false, err: nil, details: "File is empty"}
|
|
||||||
}
|
|
||||||
valid = true
|
|
||||||
details = "File exists and has content"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return verifyResultMsg{archive: archive.Name, valid: valid, err: err, details: details}
|
if !diagResult.IsValid {
|
||||||
|
// Collect error details
|
||||||
|
if diagResult.IsTruncated {
|
||||||
|
issues = append(issues, "TRUNCATED")
|
||||||
|
}
|
||||||
|
if diagResult.IsCorrupted {
|
||||||
|
issues = append(issues, "CORRUPTED")
|
||||||
|
}
|
||||||
|
if len(diagResult.Errors) > 0 {
|
||||||
|
issues = append(issues, diagResult.Errors[0])
|
||||||
|
}
|
||||||
|
return verifyResultMsg{
|
||||||
|
archive: archive.Name,
|
||||||
|
valid: false,
|
||||||
|
err: nil,
|
||||||
|
details: strings.Join(issues, "; "),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build success details
|
||||||
|
details := "Verified"
|
||||||
|
if diagResult.Details != nil {
|
||||||
|
if diagResult.Details.TableCount > 0 {
|
||||||
|
details = fmt.Sprintf("%d databases in archive", diagResult.Details.TableCount)
|
||||||
|
} else if diagResult.Details.PgRestoreListable {
|
||||||
|
details = "pg_restore verified"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add any warnings
|
||||||
|
if len(diagResult.Warnings) > 0 {
|
||||||
|
details += fmt.Sprintf(" [%d warnings]", len(diagResult.Warnings))
|
||||||
|
}
|
||||||
|
|
||||||
|
return verifyResultMsg{archive: archive.Name, valid: true, err: nil, details: details}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
133
internal/tui/styles.go
Normal file
133
internal/tui/styles.go
Normal 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
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user