feat: Remove unused features and enhance backup file browsing functionality

This commit is contained in:
Peter Wood
2025-06-01 19:17:36 -04:00
parent 5b249c89bd
commit c38dd066f7
4 changed files with 158 additions and 363 deletions

View File

@@ -122,7 +122,6 @@ cd tui && ./backup-manager
| `v` | View backup logs | | `v` | View backup logs |
| `s` | View backup status | | `s` | View backup status |
| `f` | View configuration | | `f` | View configuration |
| `r` | Refresh current view |
| `x` | Return to main view | | `x` | Return to main view |
| `c` | Clear output panel | | `c` | Clear output panel |
| `Esc` | Return to main view | | `Esc` | Return to main view |

Binary file not shown.

View File

@@ -1,20 +1,16 @@
package main package main
import ( import (
"context"
"fmt" "fmt"
"io" "io"
"os" "os"
"os/exec"
"path/filepath" "path/filepath"
"sort" "sort"
"strings" "strings"
"sync"
"time" "time"
"github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/list" "github.com/charmbracelet/bubbles/list"
"github.com/charmbracelet/bubbles/spinner"
"github.com/charmbracelet/bubbles/viewport" "github.com/charmbracelet/bubbles/viewport"
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss"
@@ -22,7 +18,6 @@ import (
// Message types // Message types
type ( type (
backupDirLoadedMsg struct{ backupDirs []BackupDirectory }
logFilesLoadedMsg struct{ logFiles []LogFile } logFilesLoadedMsg struct{ logFiles []LogFile }
fileContentLoadedMsg struct{ content string } fileContentLoadedMsg struct{ content string }
backupDetailsLoadedMsg struct{ details BackupDetails } backupDetailsLoadedMsg struct{ details BackupDetails }
@@ -69,33 +64,6 @@ type BackupDetails struct {
NewestBackup time.Time `json:"newest_backup"` NewestBackup time.Time `json:"newest_backup"`
} }
// BackupStatus represents the status of a backup operation
type BackupStatus struct {
Service string `json:"service"`
Status string `json:"status"` // running, success, error, idle
LastRun time.Time `json:"last_run"`
Duration string `json:"duration"`
Output string `json:"output"`
Error string `json:"error"`
Progress int `json:"progress"` // 0-100 percentage
StartTime time.Time `json:"start_time"`
EndTime time.Time `json:"end_time"`
BackupSize string `json:"backup_size"`
FilesCount int `json:"files_count"`
PID int `json:"pid"` // Process ID if running
Context context.Context `json:"-"` // Context for cancellation
Cancel context.CancelFunc `json:"-"` // Cancel function
}
// RunningProcess represents a running backup process
type RunningProcess struct {
Cmd *exec.Cmd
Context context.Context
Cancel context.CancelFunc
Output []string
Mutex sync.RWMutex
}
// LogEntry represents a log entry from backup operations // LogEntry represents a log entry from backup operations
type LogEntry struct { type LogEntry struct {
Timestamp time.Time `json:"timestamp"` Timestamp time.Time `json:"timestamp"`
@@ -128,9 +96,6 @@ var (
Foreground(lipgloss.AdaptiveColor{Light: "#343433", Dark: "#C1C6B2"}). Foreground(lipgloss.AdaptiveColor{Light: "#343433", Dark: "#C1C6B2"}).
Background(lipgloss.AdaptiveColor{Light: "#D9DCCF", Dark: "#353533"}) Background(lipgloss.AdaptiveColor{Light: "#D9DCCF", Dark: "#353533"})
successStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#04B575"))
errorStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#EF4444"))
runningStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#F59E0B"))
idleStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#6B7280")) idleStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#6B7280"))
// Style for the selected item in the list // Style for the selected item in the list
@@ -156,7 +121,6 @@ type keyMap struct {
ViewLogs key.Binding ViewLogs key.Binding
ViewStatus key.Binding ViewStatus key.Binding
ViewConfig key.Binding ViewConfig key.Binding
Stop key.Binding
Clear key.Binding Clear key.Binding
} }
@@ -168,7 +132,7 @@ func (k keyMap) FullHelp() [][]key.Binding {
return [][]key.Binding{ return [][]key.Binding{
{k.Up, k.Down, k.Enter}, {k.Up, k.Down, k.Enter},
{k.Tab, k.ViewLogs, k.ViewStatus, k.ViewConfig}, {k.Tab, k.ViewLogs, k.ViewStatus, k.ViewConfig},
{k.Refresh, k.Stop, k.Clear}, {k.Refresh, k.Clear},
{k.Help, k.Quit}, {k.Help, k.Quit},
} }
} }
@@ -195,12 +159,12 @@ var keys = keyMap{
key.WithHelp("?", "toggle help"), key.WithHelp("?", "toggle help"),
), ),
Quit: key.NewBinding( Quit: key.NewBinding(
key.WithKeys("q", "esc", "ctrl+c"), key.WithKeys("q", "ctrl+c"),
key.WithHelp("q", "quit"), key.WithHelp("q", "quit"),
), ),
Enter: key.NewBinding( Enter: key.NewBinding(
key.WithKeys("enter", " "), key.WithKeys("enter", " "),
key.WithHelp("enter", "run backup"), key.WithHelp("enter", "browse directory"),
), ),
Tab: key.NewBinding( Tab: key.NewBinding(
key.WithKeys("tab"), key.WithKeys("tab"),
@@ -226,10 +190,6 @@ var keys = keyMap{
key.WithKeys("f"), key.WithKeys("f"),
key.WithHelp("f", "view config"), key.WithHelp("f", "view config"),
), ),
Stop: key.NewBinding(
key.WithKeys("x"),
key.WithHelp("x", "stop backup"),
),
Clear: key.NewBinding( Clear: key.NewBinding(
key.WithKeys("c"), key.WithKeys("c"),
key.WithHelp("c", "clear output"), key.WithHelp("c", "clear output"),
@@ -240,20 +200,16 @@ var keys = keyMap{
type Model struct { type Model struct {
list list.Model list list.Model
viewport viewport.Model viewport viewport.Model
spinner spinner.Model
keys keyMap keys keyMap
width int width int
height int height int
activePanel int // 0: list, 1: viewport, 2: status activePanel int // 0: list, 1: viewport, 2: status
showHelp bool showHelp bool
ready bool ready bool
backupStatus map[string]*BackupStatus
logs []LogEntry logs []LogEntry
shellPath string shellPath string
items []BackupItem // Store backup items for reference items []BackupItem // Store backup items for reference
runningBackups map[string]*RunningProcess // Track running processes with enhanced control
currentView string // "main", "logs", "status" currentView string // "main", "logs", "status"
selectedIndex int // Track currently selected menu item
} }
// Initialize the model // Initialize the model
@@ -353,28 +309,20 @@ func initialModel() Model {
l.Styles.PaginationStyle = list.DefaultStyles().PaginationStyle.PaddingLeft(4) l.Styles.PaginationStyle = list.DefaultStyles().PaginationStyle.PaddingLeft(4)
l.Styles.HelpStyle = list.DefaultStyles().HelpStyle.PaddingLeft(4).PaddingBottom(1) l.Styles.HelpStyle = list.DefaultStyles().HelpStyle.PaddingLeft(4).PaddingBottom(1)
s := spinner.New()
s.Spinner = spinner.Dot
s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("205"))
vp := viewport.New(78, 20) vp := viewport.New(78, 20)
vp.SetContent("Welcome to Backup Manager!\n\nSelect a backup operation from the list and press Enter to execute.\n\nUse 'v' to view logs, 'r' to refresh status, and 'tab' to switch between panels.") vp.SetContent("Welcome to Backup File Browser!\n\nSelect a backup directory from the list and press Enter to browse backup files.\n\nUse 'v' to view logs, 'r' to refresh status, and 'tab' to switch between panels.")
return Model{ return Model{
list: l, list: l,
viewport: vp, viewport: vp,
spinner: s,
keys: keys, keys: keys,
activePanel: 0, activePanel: 0,
showHelp: false, showHelp: false,
ready: false, ready: false,
backupStatus: make(map[string]*BackupStatus),
logs: []LogEntry{}, logs: []LogEntry{},
shellPath: shellPath, shellPath: shellPath,
items: convertToBackupItems(items), // Store the items items: convertToBackupItems(items), // Store the items
runningBackups: make(map[string]*RunningProcess),
currentView: "main", currentView: "main",
selectedIndex: 0, // Initialize to first item
} }
} }
@@ -400,18 +348,10 @@ func (d BackupItemDelegate) Render(w io.Writer, m list.Model, index int, listIte
} }
} }
// Helper functions
func min(a, b int) int {
if a < b {
return a
}
return b
}
// Helper function to convert list items to BackupItems // Helper function to convert list items to BackupItems
func convertToBackupItems(items []list.Item) []BackupItem { func convertToBackupItems(items []list.Item) []BackupItem {
backupItems := make([]BackupItem, len(items)) backupItems := make([]BackupItem, len(items))
for i, item := range items { for i, item := range items {
if bi, ok := item.(BackupItem); ok { if bi, ok := item.(BackupItem); ok {
backupItems[i] = bi backupItems[i] = bi
} }
@@ -420,7 +360,7 @@ func convertToBackupItems(items []list.Item) []BackupItem {
} }
func (m Model) Init() tea.Cmd { func (m Model) Init() tea.Cmd {
return m.spinner.Tick return nil
} }
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
@@ -441,10 +381,10 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.ready = true m.ready = true
// Show description for the first selected item instead of generic welcome // Show description for the first selected item instead of generic welcome
if len(m.items) > 0 && len(m.runningBackups) == 0 { if len(m.items) > 0 {
m.showItemDescription() m.showItemDescription()
} else { } else {
m.viewport.SetContent("Welcome to Backup Manager!\n\nSelect a backup operation from the list and press Enter to execute.\n\nUse 'v' to view logs, 'r' to refresh status, and 'tab' to switch between panels.") m.viewport.SetContent("Welcome to Backup File Browser!\n\nSelect a backup directory from the list and press Enter to browse backup files.\n\nUse 'v' to view logs, 's' to view status, and 'tab' to switch between panels.")
} }
} else { } else {
m.viewport.Width = msg.Width - 4 m.viewport.Width = msg.Width - 4
@@ -480,28 +420,17 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.showConfig() m.showConfig()
m.currentView = "config" m.currentView = "config"
case key.Matches(msg, m.keys.Stop):
// For backup viewer, Stop key can be used to go back to main view
m.currentView = "main"
if len(m.items) > 0 {
m.showItemDescription()
}
case key.Matches(msg, m.keys.Clear): case key.Matches(msg, m.keys.Clear):
m.viewport.SetContent("Output cleared.\n\nSelect a backup operation from the list and press Enter to execute.") m.viewport.SetContent("Output cleared.\n\nSelect a backup directory from the list and press Enter to browse.")
m.currentView = "main" m.currentView = "main"
// Show description for currently selected item // Show description for currently selected item
if len(m.runningBackups) == 0 { m.showItemDescription()
m.showItemDescription()
}
case key.Matches(msg, m.keys.Escape) && m.currentView != "main": case key.Matches(msg, m.keys.Escape) && m.currentView != "main":
m.viewport.SetContent("Welcome to Backup Manager!\n\nSelect a backup operation from the list and press Enter to execute.\n\nUse 'v' to view logs, 's' to view status, and 'tab' to switch between panels.") m.viewport.SetContent("Welcome to Backup File Browser!\n\nSelect a backup directory from the list and press Enter to browse.\n\nUse 'v' to view logs, 's' for status, and 'tab' to switch between panels.")
m.currentView = "main" m.currentView = "main"
// Show description for currently selected item // Show description for currently selected item
if len(m.runningBackups) == 0 { m.showItemDescription()
m.showItemDescription()
}
case key.Matches(msg, m.keys.Enter) && m.activePanel == 0: case key.Matches(msg, m.keys.Enter) && m.activePanel == 0:
if i, ok := m.list.SelectedItem().(BackupItem); ok { if i, ok := m.list.SelectedItem().(BackupItem); ok {
@@ -518,30 +447,38 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
} }
} }
case key.Matches(msg, m.keys.Stop):
// For backup viewer, Stop key can be used to go back to main view
m.currentView = "main"
if len(m.items) > 0 {
m.showItemDescription()
}
case key.Matches(msg, m.keys.Help): case key.Matches(msg, m.keys.Help):
m.showHelp = !m.showHelp m.showHelp = !m.showHelp
} }
// Update the focused component // Update the focused component (CRITICAL: this must be outside the switch)
prevIndex := m.list.Index() prevIndex := m.list.Index()
if m.activePanel == 0 { if m.activePanel == 0 {
m.list, cmd = m.list.Update(msg) m.list, cmd = m.list.Update(msg)
// Check if selection changed and we're in main view // Check if selection changed and we're in main view
if m.list.Index() != prevIndex && m.currentView == "main" { if m.list.Index() != prevIndex && m.currentView == "main" {
m.selectedIndex = m.list.Index()
m.showItemDescription() m.showItemDescription()
} }
} else { } else {
m.viewport, cmd = m.viewport.Update(msg) m.viewport, cmd = m.viewport.Update(msg)
} }
cmds = append(cmds, cmd) cmds = append(cmds, cmd)
case backupDetailsLoadedMsg:
m.showBackupDetails(msg.details)
m.currentView = "directory"
case logFilesLoadedMsg:
m.showLogFiles(msg.logFiles)
m.currentView = "logs"
case fileContentLoadedMsg:
m.viewport.SetContent(msg.content)
m.currentView = "file"
case errorMsg:
m.viewport.SetContent(fmt.Sprintf("Error: %s", msg.error))
m.currentView = "error"
} }
return m, tea.Batch(cmds...) return m, tea.Batch(cmds...)
@@ -630,37 +567,8 @@ func (m Model) createStatusBar() string {
viewIndicator := fmt.Sprintf("View: %s", strings.ToUpper(m.currentView)) viewIndicator := fmt.Sprintf("View: %s", strings.ToUpper(m.currentView))
parts = append(parts, idleStyle.Render(viewIndicator)) parts = append(parts, idleStyle.Render(viewIndicator))
// Add running backups count // Add file browser status
runningCount := len(m.runningBackups) parts = append(parts, idleStyle.Render("Mode: Browse Only"))
if runningCount > 0 {
parts = append(parts, runningStyle.Render(fmt.Sprintf("Running: %d", runningCount)))
}
// Add service statuses
for service, status := range m.backupStatus {
var statusText string
switch status.Status {
case "running":
progressText := ""
if status.Progress > 0 {
progressText = fmt.Sprintf(" (%d%%)", status.Progress)
}
statusText = runningStyle.Render(fmt.Sprintf("%s: %s%s %s", service, status.Status, progressText, m.spinner.View()))
case "success":
statusText = successStyle.Render(fmt.Sprintf("%s: ✓ %s", service, status.Duration))
case "error":
statusText = errorStyle.Render(fmt.Sprintf("%s: ✗ failed", service))
case "stopped":
statusText = idleStyle.Render(fmt.Sprintf("%s: 🛑 stopped", service))
default:
statusText = idleStyle.Render(fmt.Sprintf("%s: idle", service))
}
parts = append(parts, statusText)
}
if len(m.backupStatus) == 0 {
parts = append(parts, idleStyle.Render("No backups running"))
}
// Add navigation help // Add navigation help
if m.activePanel == 0 { if m.activePanel == 0 {
@@ -696,82 +604,30 @@ func (m *Model) showLogs() {
func (m *Model) showStatus() { func (m *Model) showStatus() {
var content strings.Builder var content strings.Builder
content.WriteString("=== BACKUP STATUS ===\n\n") content.WriteString("=== BACKUP DIRECTORY STATUS ===\n\n")
if len(m.backupStatus) == 0 {
content.WriteString("No backup status available.\n\nRun some backups to see their status here.")
} else {
// Sort services alphabetically
var services []string
for service := range m.backupStatus {
services = append(services, service)
}
sort.Strings(services)
for _, service := range services {
status := m.backupStatus[service]
content.WriteString(fmt.Sprintf("🔧 Service: %s\n", strings.ToUpper(service)))
// Status with emoji
statusIcon := "⭕"
switch status.Status {
case "running":
statusIcon = "🔄"
case "success":
statusIcon = "✅"
case "error":
statusIcon = "❌"
case "stopped":
statusIcon = "🛑"
}
content.WriteString(fmt.Sprintf(" Status: %s %s\n", statusIcon, status.Status))
if !status.LastRun.IsZero() {
content.WriteString(fmt.Sprintf(" Last Run: %s\n", status.LastRun.Format("2006-01-02 15:04:05")))
}
if status.Duration != "" {
content.WriteString(fmt.Sprintf(" Duration: %s\n", status.Duration))
}
if status.Progress > 0 && status.Status == "running" {
content.WriteString(fmt.Sprintf(" Progress: %d%%\n", status.Progress))
}
if status.BackupSize != "" {
content.WriteString(fmt.Sprintf(" Size: %s\n", status.BackupSize))
}
if status.FilesCount > 0 {
content.WriteString(fmt.Sprintf(" Files: %d\n", status.FilesCount))
}
if status.Error != "" {
content.WriteString(fmt.Sprintf(" Error: %s\n", status.Error[:min(100, len(status.Error))]))
}
content.WriteString("📂 Available Backup Directories:\n\n")
for _, item := range m.items {
if item.itemType == "directory" {
content.WriteString(fmt.Sprintf("🔧 Service: %s\n", strings.ToUpper(item.service)))
content.WriteString(fmt.Sprintf(" Path: %s\n", item.path))
content.WriteString(" Type: Backup Directory\n")
content.WriteString(" Status: Available for browsing\n")
content.WriteString("\n") content.WriteString("\n")
} }
// Add summary
running := 0
success := 0
failed := 0
for _, status := range m.backupStatus {
switch status.Status {
case "running":
running++
case "success":
success++
case "error":
failed++
}
}
content.WriteString("📊 SUMMARY:\n")
content.WriteString(fmt.Sprintf(" Running: %d | Success: %d | Failed: %d\n", running, success, failed))
} }
content.WriteString("📊 SUMMARY:\n")
directoryCount := 0
for _, item := range m.items {
if item.itemType == "directory" {
directoryCount++
}
}
content.WriteString(fmt.Sprintf(" Available Directories: %d\n", directoryCount))
content.WriteString(" Mode: Read-Only Browser\n")
m.viewport.SetContent(content.String()) m.viewport.SetContent(content.String())
} }
@@ -812,7 +668,6 @@ func (m *Model) showConfig() {
// Performance Tips // Performance Tips
content.WriteString("💡 TIPS:\n") content.WriteString("💡 TIPS:\n")
content.WriteString(" • Use 'r' to refresh current view\n")
content.WriteString(" • Browse backup directories with Enter\n") content.WriteString(" • Browse backup directories with Enter\n")
content.WriteString(" • Check 's' for detailed status information\n") content.WriteString(" • Check 's' for detailed status information\n")
content.WriteString(" • Use 'x' to go back to main view\n") content.WriteString(" • Use 'x' to go back to main view\n")
@@ -822,11 +677,13 @@ func (m *Model) showConfig() {
// showItemDescription displays detailed information about the currently selected menu item // showItemDescription displays detailed information about the currently selected menu item
func (m *Model) showItemDescription() { func (m *Model) showItemDescription() {
if m.selectedIndex < 0 || m.selectedIndex >= len(m.items) { // Use the list's actual selection index instead of a separate field
selectedIndex := m.list.Index()
if selectedIndex < 0 || selectedIndex >= len(m.items) {
return return
} }
item := m.items[m.selectedIndex] item := m.items[selectedIndex]
var content strings.Builder var content strings.Builder
content.WriteString(fmt.Sprintf("📋 %s\n", item.title)) content.WriteString(fmt.Sprintf("📋 %s\n", item.title))
@@ -943,9 +800,9 @@ func (m *Model) showItemDescription() {
content.WriteString("\n💡 Navigation:\n") content.WriteString("\n💡 Navigation:\n")
content.WriteString("• Tab: Switch between panels\n") content.WriteString("• Tab: Switch between panels\n")
content.WriteString("• R: Refresh current view\n") content.WriteString("• r: Refresh current view\n")
content.WriteString("• ?: Toggle help\n") content.WriteString("• ?: Toggle help\n")
content.WriteString("• Q: Quit application\n") content.WriteString("• q: Quit application\n")
// Add path information // Add path information
content.WriteString(fmt.Sprintf("\n📁 Path: %s\n", item.path)) content.WriteString(fmt.Sprintf("\n📁 Path: %s\n", item.path))
@@ -953,7 +810,103 @@ func (m *Model) showItemDescription() {
m.viewport.SetContent(content.String()) m.viewport.SetContent(content.String())
} }
// showBackupDetails displays detailed information about backup files in a directory
func (m *Model) showBackupDetails(details BackupDetails) {
var content strings.Builder
content.WriteString(fmt.Sprintf("📁 %s Backup Directory\n", details.Directory.Name))
content.WriteString(strings.Repeat("=", len(details.Directory.Name)+20) + "\n\n")
// Directory summary
content.WriteString("📊 Directory Summary:\n")
content.WriteString(fmt.Sprintf(" 📁 Path: %s\n", details.Directory.Path))
content.WriteString(fmt.Sprintf(" 📄 Files: %d\n", details.TotalFiles))
content.WriteString(fmt.Sprintf(" 💾 Total Size: %s\n", formatSize(details.TotalSize)))
if !details.NewestBackup.IsZero() {
content.WriteString(fmt.Sprintf(" 🕒 Newest: %s\n", details.NewestBackup.Format("2006-01-02 15:04:05")))
}
if !details.OldestBackup.IsZero() {
content.WriteString(fmt.Sprintf(" 📅 Oldest: %s\n", details.OldestBackup.Format("2006-01-02 15:04:05")))
}
content.WriteString("\n")
// File listings
if len(details.Files) > 0 {
content.WriteString("📄 Backup Files:\n")
content.WriteString(" Name Size Modified\n")
content.WriteString(" " + strings.Repeat("-", 50) + "\n")
for _, file := range details.Files {
// Truncate filename if too long
displayName := file.Name
if len(displayName) > 25 {
displayName = displayName[:22] + "..."
}
compressionIndicator := ""
if file.IsCompressed {
compressionIndicator = "📦 "
}
content.WriteString(fmt.Sprintf(" %s%-25s %8s %s\n",
compressionIndicator,
displayName,
formatSize(file.Size),
file.ModifiedTime.Format("2006-01-02 15:04")))
}
} else {
content.WriteString("📂 No backup files found in this directory.\n")
}
content.WriteString("\n💡 Navigation:\n")
content.WriteString(" • x: Return to main view\n")
content.WriteString(" • r: Refresh directory listing\n")
content.WriteString(" • ?: Toggle help\n")
m.viewport.SetContent(content.String())
}
// showLogFiles displays log files for browsing
func (m *Model) showLogFiles(logFiles []LogFile) {
var content strings.Builder
content.WriteString("📋 Backup Operation Logs\n")
content.WriteString("=========================\n\n")
if len(logFiles) == 0 {
content.WriteString("📂 No log files found.\n")
content.WriteString("Log files are typically stored in /mnt/share/media/backups/logs/\n")
} else {
content.WriteString("📄 Available Log Files:\n")
content.WriteString(" Filename Size Date\n")
content.WriteString(" " + strings.Repeat("-", 45) + "\n")
for _, logFile := range logFiles {
displayName := logFile.Name
if len(displayName) > 25 {
displayName = displayName[:22] + "..."
}
dateStr := "Unknown"
if !logFile.Date.IsZero() {
dateStr = logFile.Date.Format("2006-01-02 15:04")
}
content.WriteString(fmt.Sprintf(" %-25s %8s %s\n",
displayName,
formatSize(logFile.Size),
dateStr))
}
content.WriteString(fmt.Sprintf("\n📊 Total: %d log files\n", len(logFiles)))
}
content.WriteString("\n💡 Navigation:\n")
content.WriteString(" • Enter: View log content (when a specific log is selected)\n")
content.WriteString(" • x: Return to main view\n")
content.WriteString(" • r: Refresh log listing\n")
m.viewport.SetContent(content.String())
}
// formatSize converts bytes to human readable format // formatSize converts bytes to human readable format
func formatSize(bytes int64) string { func formatSize(bytes int64) string {
@@ -1097,21 +1050,11 @@ func (m *Model) loadLogFiles() tea.Cmd {
} }
} }
// loadFileContent loads content of a specific file // main function to initialize and run the TUI application
func (m *Model) loadFileContent(filePath string) tea.Cmd { func main() {
return func() tea.Msg { p := tea.NewProgram(initialModel(), tea.WithAltScreen())
content, err := os.ReadFile(filePath) if _, err := p.Run(); err != nil {
if err != nil { fmt.Printf("Alas, there's been an error: %v", err)
return errorMsg{error: fmt.Sprintf("Failed to read file %s: %v", filePath, err)} os.Exit(1)
}
// Limit content size for display
maxSize := 50000 // 50KB limit for display
if len(content) > maxSize {
content = content[:maxSize]
content = append(content, []byte("\n\n... (file truncated for display)")...)
}
return fileContentLoadedMsg{content: string(content)}
} }
} }

View File

@@ -1,147 +0,0 @@
#!/bin/bash
# Test script for Enhanced Backup TUI Features
# This script validates all the enhanced functionality
set -e
YELLOW='\033[1;33m'
GREEN='\033[0;32m'
RED='\033[0;31m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
echo -e "${BLUE}=== Enhanced Backup TUI Feature Validation ===${NC}"
echo ""
# Test 1: Check if the TUI compiles and runs
echo -e "${YELLOW}Test 1: Compilation and Basic Startup${NC}"
cd /home/acedanger/shell/tui
if go build -o backup-manager-test main.go; then
echo -e "${GREEN}✓ TUI compiles successfully${NC}"
else
echo -e "${RED}✗ TUI compilation failed${NC}"
exit 1
fi
# Test 2: Check if the TUI starts without errors
echo -e "${YELLOW}Test 2: Basic TUI Startup${NC}"
timeout 3s ./backup-manager-test > /dev/null 2>&1 &
TUI_PID=$!
sleep 1
if kill -0 $TUI_PID 2>/dev/null; then
echo -e "${GREEN}✓ TUI starts successfully${NC}"
kill $TUI_PID 2>/dev/null || true
else
echo -e "${RED}✗ TUI failed to start${NC}"
fi
# Test 3: Check for enhanced features in the code
echo -e "${YELLOW}Test 3: Enhanced Feature Code Validation${NC}"
# Check for progress tracking
if grep -q "backupProgressMsg" main.go; then
echo -e "${GREEN}✓ Real-time progress tracking implemented${NC}"
else
echo -e "${RED}✗ Progress tracking not found${NC}"
fi
# Check for context cancellation
if grep -q "context.WithCancel" main.go; then
echo -e "${GREEN}✓ Context cancellation implemented${NC}"
else
echo -e "${RED}✗ Context cancellation not found${NC}"
fi
# Check for process management
if grep -q "BackupStatus" main.go; then
echo -e "${GREEN}✓ Enhanced process management implemented${NC}"
else
echo -e "${RED}✗ Process management not found${NC}"
fi
# Check for output streaming
if grep -q "streamBackupOutput" main.go; then
echo -e "${GREEN}✓ Output streaming implemented${NC}"
else
echo -e "${RED}✗ Output streaming not found${NC}"
fi
# Check for error handling
if grep -q "analyzeError" main.go; then
echo -e "${GREEN}✓ Intelligent error analysis implemented${NC}"
else
echo -e "${RED}✗ Error analysis not found${NC}"
fi
# Test 4: Check for key bindings and UI enhancements
echo -e "${YELLOW}Test 4: UI Enhancement Validation${NC}"
if grep -q "tea.KeyCancel" main.go; then
echo -e "${GREEN}✓ Cancel key binding implemented${NC}"
else
echo -e "${RED}✗ Cancel key binding not found${NC}"
fi
if grep -q "viewport" main.go; then
echo -e "${GREEN}✓ Enhanced viewport system implemented${NC}"
else
echo -e "${RED}✗ Viewport system not found${NC}"
fi
# Test 5: Validate backup script integration
echo -e "${YELLOW}Test 5: Backup Script Integration${NC}"
# Check if backup scripts exist
BACKUP_SCRIPTS=(
"/home/acedanger/shell/plex/backup-plex.sh"
"/home/acedanger/shell/immich/backup-immich.sh"
"/home/acedanger/shell/backup-media.sh"
)
for script in "${BACKUP_SCRIPTS[@]}"; do
if [[ -f "$script" ]]; then
echo -e "${GREEN}✓ Backup script found: $(basename "$script")${NC}"
else
echo -e "${RED}✗ Backup script missing: $(basename "$script")${NC}"
fi
done
# Test 6: Check dependencies
echo -e "${YELLOW}Test 6: Dependency Validation${NC}"
if go mod verify > /dev/null 2>&1; then
echo -e "${GREEN}✓ Go module dependencies verified${NC}"
else
echo -e "${RED}✗ Go module dependency issues${NC}"
fi
# Test 7: Memory and performance validation
echo -e "${YELLOW}Test 7: Performance Validation${NC}"
# Check for potential memory leaks or inefficiencies
if grep -q "sync.Mutex" main.go; then
echo -e "${GREEN}✓ Thread-safe operations implemented${NC}"
else
echo -e "${YELLOW}⚠ Thread safety not explicitly implemented${NC}"
fi
# Test 8: Documentation validation
echo -e "${YELLOW}Test 8: Documentation Validation${NC}"
if [[ -f "README.md" ]]; then
echo -e "${GREEN}✓ README.md exists${NC}"
else
echo -e "${RED}✗ README.md missing${NC}"
fi
echo ""
echo -e "${BLUE}=== Feature Validation Complete ===${NC}"
echo -e "${GREEN}Enhanced Backup TUI is ready for production use!${NC}"
# Cleanup
rm -f backup-manager-test
exit 0