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 |
|
| `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.
373
tui/main.go
373
tui/main.go
@@ -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)}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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