mirror of
https://github.com/acedanger/shell.git
synced 2025-12-06 00:00:13 -08:00
feat: Remove unused features and enhance backup file browsing functionality
This commit is contained in:
@@ -122,7 +122,6 @@ cd tui && ./backup-manager
|
||||
| `v` | View backup logs |
|
||||
| `s` | View backup status |
|
||||
| `f` | View configuration |
|
||||
| `r` | Refresh current view |
|
||||
| `x` | Return to main view |
|
||||
| `c` | Clear output panel |
|
||||
| `Esc` | Return to main view |
|
||||
|
||||
Binary file not shown.
369
tui/main.go
369
tui/main.go
@@ -1,20 +1,16 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/bubbles/key"
|
||||
"github.com/charmbracelet/bubbles/list"
|
||||
"github.com/charmbracelet/bubbles/spinner"
|
||||
"github.com/charmbracelet/bubbles/viewport"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
@@ -22,7 +18,6 @@ import (
|
||||
|
||||
// Message types
|
||||
type (
|
||||
backupDirLoadedMsg struct{ backupDirs []BackupDirectory }
|
||||
logFilesLoadedMsg struct{ logFiles []LogFile }
|
||||
fileContentLoadedMsg struct{ content string }
|
||||
backupDetailsLoadedMsg struct{ details BackupDetails }
|
||||
@@ -69,33 +64,6 @@ type BackupDetails struct {
|
||||
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
|
||||
type LogEntry struct {
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
@@ -128,9 +96,6 @@ var (
|
||||
Foreground(lipgloss.AdaptiveColor{Light: "#343433", Dark: "#C1C6B2"}).
|
||||
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"))
|
||||
|
||||
// Style for the selected item in the list
|
||||
@@ -156,7 +121,6 @@ type keyMap struct {
|
||||
ViewLogs key.Binding
|
||||
ViewStatus key.Binding
|
||||
ViewConfig key.Binding
|
||||
Stop key.Binding
|
||||
Clear key.Binding
|
||||
}
|
||||
|
||||
@@ -168,7 +132,7 @@ func (k keyMap) FullHelp() [][]key.Binding {
|
||||
return [][]key.Binding{
|
||||
{k.Up, k.Down, k.Enter},
|
||||
{k.Tab, k.ViewLogs, k.ViewStatus, k.ViewConfig},
|
||||
{k.Refresh, k.Stop, k.Clear},
|
||||
{k.Refresh, k.Clear},
|
||||
{k.Help, k.Quit},
|
||||
}
|
||||
}
|
||||
@@ -195,12 +159,12 @@ var keys = keyMap{
|
||||
key.WithHelp("?", "toggle help"),
|
||||
),
|
||||
Quit: key.NewBinding(
|
||||
key.WithKeys("q", "esc", "ctrl+c"),
|
||||
key.WithKeys("q", "ctrl+c"),
|
||||
key.WithHelp("q", "quit"),
|
||||
),
|
||||
Enter: key.NewBinding(
|
||||
key.WithKeys("enter", " "),
|
||||
key.WithHelp("enter", "run backup"),
|
||||
key.WithHelp("enter", "browse directory"),
|
||||
),
|
||||
Tab: key.NewBinding(
|
||||
key.WithKeys("tab"),
|
||||
@@ -226,10 +190,6 @@ var keys = keyMap{
|
||||
key.WithKeys("f"),
|
||||
key.WithHelp("f", "view config"),
|
||||
),
|
||||
Stop: key.NewBinding(
|
||||
key.WithKeys("x"),
|
||||
key.WithHelp("x", "stop backup"),
|
||||
),
|
||||
Clear: key.NewBinding(
|
||||
key.WithKeys("c"),
|
||||
key.WithHelp("c", "clear output"),
|
||||
@@ -240,20 +200,16 @@ var keys = keyMap{
|
||||
type Model struct {
|
||||
list list.Model
|
||||
viewport viewport.Model
|
||||
spinner spinner.Model
|
||||
keys keyMap
|
||||
width int
|
||||
height int
|
||||
activePanel int // 0: list, 1: viewport, 2: status
|
||||
showHelp bool
|
||||
ready bool
|
||||
backupStatus map[string]*BackupStatus
|
||||
logs []LogEntry
|
||||
shellPath string
|
||||
items []BackupItem // Store backup items for reference
|
||||
runningBackups map[string]*RunningProcess // Track running processes with enhanced control
|
||||
currentView string // "main", "logs", "status"
|
||||
selectedIndex int // Track currently selected menu item
|
||||
}
|
||||
|
||||
// Initialize the model
|
||||
@@ -353,28 +309,20 @@ func initialModel() Model {
|
||||
l.Styles.PaginationStyle = list.DefaultStyles().PaginationStyle.PaddingLeft(4)
|
||||
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.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{
|
||||
list: l,
|
||||
viewport: vp,
|
||||
spinner: s,
|
||||
keys: keys,
|
||||
activePanel: 0,
|
||||
showHelp: false,
|
||||
ready: false,
|
||||
backupStatus: make(map[string]*BackupStatus),
|
||||
logs: []LogEntry{},
|
||||
shellPath: shellPath,
|
||||
items: convertToBackupItems(items), // Store the items
|
||||
runningBackups: make(map[string]*RunningProcess),
|
||||
currentView: "main",
|
||||
selectedIndex: 0, // Initialize to first item
|
||||
}
|
||||
}
|
||||
|
||||
@@ -400,14 +348,6 @@ 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
|
||||
func convertToBackupItems(items []list.Item) []BackupItem {
|
||||
backupItems := make([]BackupItem, len(items))
|
||||
@@ -420,7 +360,7 @@ func convertToBackupItems(items []list.Item) []BackupItem {
|
||||
}
|
||||
|
||||
func (m Model) Init() tea.Cmd {
|
||||
return m.spinner.Tick
|
||||
return nil
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
// 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()
|
||||
} 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 {
|
||||
m.viewport.Width = msg.Width - 4
|
||||
@@ -480,28 +420,17 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
m.showConfig()
|
||||
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):
|
||||
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"
|
||||
// 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":
|
||||
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"
|
||||
// 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:
|
||||
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):
|
||||
m.showHelp = !m.showHelp
|
||||
}
|
||||
|
||||
// Update the focused component
|
||||
// Update the focused component (CRITICAL: this must be outside the switch)
|
||||
prevIndex := m.list.Index()
|
||||
if m.activePanel == 0 {
|
||||
m.list, cmd = m.list.Update(msg)
|
||||
// Check if selection changed and we're in main view
|
||||
if m.list.Index() != prevIndex && m.currentView == "main" {
|
||||
m.selectedIndex = m.list.Index()
|
||||
m.showItemDescription()
|
||||
}
|
||||
} else {
|
||||
m.viewport, cmd = m.viewport.Update(msg)
|
||||
}
|
||||
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...)
|
||||
@@ -630,37 +567,8 @@ func (m Model) createStatusBar() string {
|
||||
viewIndicator := fmt.Sprintf("View: %s", strings.ToUpper(m.currentView))
|
||||
parts = append(parts, idleStyle.Render(viewIndicator))
|
||||
|
||||
// Add running backups count
|
||||
runningCount := len(m.runningBackups)
|
||||
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 file browser status
|
||||
parts = append(parts, idleStyle.Render("Mode: Browse Only"))
|
||||
|
||||
// Add navigation help
|
||||
if m.activePanel == 0 {
|
||||
@@ -696,82 +604,30 @@ func (m *Model) showLogs() {
|
||||
|
||||
func (m *Model) showStatus() {
|
||||
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")
|
||||
}
|
||||
|
||||
// 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())
|
||||
}
|
||||
|
||||
@@ -812,7 +668,6 @@ func (m *Model) showConfig() {
|
||||
|
||||
// Performance Tips
|
||||
content.WriteString("💡 TIPS:\n")
|
||||
content.WriteString(" • Use 'r' to refresh current view\n")
|
||||
content.WriteString(" • Browse backup directories with Enter\n")
|
||||
content.WriteString(" • Check 's' for detailed status information\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
|
||||
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
|
||||
}
|
||||
|
||||
item := m.items[m.selectedIndex]
|
||||
item := m.items[selectedIndex]
|
||||
|
||||
var content strings.Builder
|
||||
content.WriteString(fmt.Sprintf("📋 %s\n", item.title))
|
||||
@@ -943,9 +800,9 @@ func (m *Model) showItemDescription() {
|
||||
|
||||
content.WriteString("\n💡 Navigation:\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("• Q: Quit application\n")
|
||||
content.WriteString("• q: Quit application\n")
|
||||
|
||||
// Add path information
|
||||
content.WriteString(fmt.Sprintf("\n📁 Path: %s\n", item.path))
|
||||
@@ -953,7 +810,103 @@ func (m *Model) showItemDescription() {
|
||||
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
|
||||
func formatSize(bytes int64) string {
|
||||
@@ -1097,21 +1050,11 @@ func (m *Model) loadLogFiles() tea.Cmd {
|
||||
}
|
||||
}
|
||||
|
||||
// loadFileContent loads content of a specific file
|
||||
func (m *Model) loadFileContent(filePath string) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
content, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return errorMsg{error: fmt.Sprintf("Failed to read file %s: %v", filePath, err)}
|
||||
}
|
||||
|
||||
// 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)}
|
||||
// main function to initialize and run the TUI application
|
||||
func main() {
|
||||
p := tea.NewProgram(initialModel(), tea.WithAltScreen())
|
||||
if _, err := p.Run(); err != nil {
|
||||
fmt.Printf("Alas, there's been an error: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user