mirror of
https://github.com/lukaszraczylo/lolcathost.git
synced 2026-10-12 04:55:13 +00:00
Switch imports to the charm.land v2 modules. Return tea.View from Model.View, set alt screen and window title there, use KeyPressMsg and the "space" key name, and call SetWidth on text inputs.
287 lines
7.0 KiB
Go
287 lines
7.0 KiB
Go
// Package tui provides the backup picker component.
|
|
package tui
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"charm.land/lipgloss/v2"
|
|
"github.com/lukaszraczylo/lolcathost/internal/protocol"
|
|
)
|
|
|
|
// BackupMode represents the backup view mode.
|
|
type BackupMode int
|
|
|
|
const (
|
|
BackupModeSelect BackupMode = iota
|
|
BackupModeConfirmRestore
|
|
)
|
|
|
|
// BackupPicker handles the backup selection and restore UI.
|
|
type BackupPicker struct {
|
|
backups []protocol.BackupInfo
|
|
cursor int
|
|
width int
|
|
height int
|
|
mode BackupMode
|
|
previewContent string
|
|
previewScroll int
|
|
}
|
|
|
|
// NewBackupPicker creates a new backup picker.
|
|
func NewBackupPicker() *BackupPicker {
|
|
return &BackupPicker{
|
|
mode: BackupModeSelect,
|
|
}
|
|
}
|
|
|
|
// SetBackups updates the available backups.
|
|
func (b *BackupPicker) SetBackups(backups []protocol.BackupInfo) {
|
|
b.backups = backups
|
|
if b.cursor >= len(backups) {
|
|
b.cursor = max(0, len(backups)-1)
|
|
}
|
|
}
|
|
|
|
// SetSize sets the picker dimensions.
|
|
func (b *BackupPicker) SetSize(width, height int) {
|
|
b.width = width
|
|
b.height = height
|
|
}
|
|
|
|
// MoveUp moves the cursor up.
|
|
func (b *BackupPicker) MoveUp() {
|
|
if b.cursor > 0 {
|
|
b.cursor--
|
|
b.previewContent = "" // Clear preview to trigger reload
|
|
b.previewScroll = 0
|
|
}
|
|
}
|
|
|
|
// MoveDown moves the cursor down.
|
|
func (b *BackupPicker) MoveDown() {
|
|
if b.cursor < len(b.backups)-1 {
|
|
b.cursor++
|
|
b.previewContent = "" // Clear preview to trigger reload
|
|
b.previewScroll = 0
|
|
}
|
|
}
|
|
|
|
// SetPreviewContent sets the preview content for the current backup.
|
|
func (b *BackupPicker) SetPreviewContent(content string) {
|
|
b.previewContent = content
|
|
b.previewScroll = 0
|
|
}
|
|
|
|
// PreviewContent returns the current preview content.
|
|
func (b *BackupPicker) PreviewContent() string {
|
|
return b.previewContent
|
|
}
|
|
|
|
// ScrollPreviewUp scrolls the preview up.
|
|
func (b *BackupPicker) ScrollPreviewUp() {
|
|
if b.previewScroll > 0 {
|
|
b.previewScroll--
|
|
}
|
|
}
|
|
|
|
// ScrollPreviewDown scrolls the preview down.
|
|
func (b *BackupPicker) ScrollPreviewDown() {
|
|
b.previewScroll++
|
|
}
|
|
|
|
// Selected returns the currently selected backup name.
|
|
func (b *BackupPicker) Selected() string {
|
|
if b.cursor >= 0 && b.cursor < len(b.backups) {
|
|
return b.backups[b.cursor].Name
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// SelectedInfo returns the currently selected backup info.
|
|
func (b *BackupPicker) SelectedInfo() *protocol.BackupInfo {
|
|
if b.cursor >= 0 && b.cursor < len(b.backups) {
|
|
return &b.backups[b.cursor]
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Len returns the number of backups.
|
|
func (b *BackupPicker) Len() int {
|
|
return len(b.backups)
|
|
}
|
|
|
|
// Mode returns the current mode.
|
|
func (b *BackupPicker) Mode() BackupMode {
|
|
return b.mode
|
|
}
|
|
|
|
// InitRestore starts restore confirmation.
|
|
func (b *BackupPicker) InitRestore() {
|
|
if b.SelectedInfo() == nil {
|
|
return
|
|
}
|
|
b.mode = BackupModeConfirmRestore
|
|
}
|
|
|
|
// Cancel cancels the current operation.
|
|
func (b *BackupPicker) Cancel() {
|
|
b.mode = BackupModeSelect
|
|
}
|
|
|
|
// View renders the backup picker.
|
|
func (b *BackupPicker) View() string {
|
|
switch b.mode {
|
|
case BackupModeConfirmRestore:
|
|
return b.restoreView()
|
|
default:
|
|
return b.selectView()
|
|
}
|
|
}
|
|
|
|
func (b *BackupPicker) selectView() string {
|
|
if len(b.backups) == 0 {
|
|
var sb strings.Builder
|
|
sb.WriteString(titleStyle.Render("Backups"))
|
|
sb.WriteString("\n\n")
|
|
sb.WriteString(helpDescStyle.Render("No backups available."))
|
|
sb.WriteString("\n\n")
|
|
sb.WriteString(helpDescStyle.Render("Backups are created automatically when hosts are modified."))
|
|
sb.WriteString("\n\n")
|
|
sb.WriteString(helpDescStyle.Render("Esc cancel"))
|
|
return dialogStyle.Render(sb.String())
|
|
}
|
|
|
|
// Build left panel (backup list)
|
|
var leftSb strings.Builder
|
|
leftSb.WriteString(titleStyle.Render("Backups"))
|
|
leftSb.WriteString("\n\n")
|
|
leftSb.WriteString(helpDescStyle.Render(fmt.Sprintf("%d backup(s)", len(b.backups))))
|
|
leftSb.WriteString("\n\n")
|
|
|
|
for i, backup := range b.backups {
|
|
timestamp := time.Unix(backup.Timestamp, 0).Format("2006-01-02 15:04:05")
|
|
sizeStr := formatSize(backup.Size)
|
|
line := fmt.Sprintf("%s (%s)", timestamp, sizeStr)
|
|
|
|
if i == b.cursor {
|
|
leftSb.WriteString(presetSelectedStyle.Render("▸ " + line))
|
|
} else {
|
|
leftSb.WriteString(presetItemStyle.Render(" " + line))
|
|
}
|
|
leftSb.WriteString("\n")
|
|
}
|
|
|
|
leftSb.WriteString("\n")
|
|
leftSb.WriteString(WrapHelpText("↑↓ navigate • Enter restore • Esc cancel", 40))
|
|
|
|
// Build right panel (preview)
|
|
var rightSb strings.Builder
|
|
rightSb.WriteString(titleStyle.Render("Preview"))
|
|
rightSb.WriteString("\n\n")
|
|
|
|
if b.previewContent == "" {
|
|
rightSb.WriteString(helpDescStyle.Render("Loading..."))
|
|
} else {
|
|
// Show content with scroll support
|
|
lines := strings.Split(b.previewContent, "\n")
|
|
previewHeight := b.height - 12 // Reserve space for title, borders, help
|
|
if previewHeight < 5 {
|
|
previewHeight = 5
|
|
}
|
|
|
|
// Clamp scroll position
|
|
maxScroll := len(lines) - previewHeight
|
|
if maxScroll < 0 {
|
|
maxScroll = 0
|
|
}
|
|
if b.previewScroll > maxScroll {
|
|
b.previewScroll = maxScroll
|
|
}
|
|
|
|
// Get visible lines
|
|
endLine := b.previewScroll + previewHeight
|
|
if endLine > len(lines) {
|
|
endLine = len(lines)
|
|
}
|
|
|
|
visibleLines := lines[b.previewScroll:endLine]
|
|
for _, line := range visibleLines {
|
|
// Truncate long lines
|
|
if len(line) > 50 {
|
|
line = line[:47] + "..."
|
|
}
|
|
rightSb.WriteString(helpDescStyle.Render(line))
|
|
rightSb.WriteString("\n")
|
|
}
|
|
|
|
// Show scroll indicator
|
|
if len(lines) > previewHeight {
|
|
rightSb.WriteString("\n")
|
|
scrollText := fmt.Sprintf("%d-%d of %d (↑↓ scroll)", b.previewScroll+1, endLine, len(lines))
|
|
rightSb.WriteString(helpDescStyle.Render(scrollText))
|
|
}
|
|
}
|
|
|
|
// Style the panels
|
|
leftWidth := 45
|
|
rightWidth := b.width - leftWidth - 10
|
|
if rightWidth < 30 {
|
|
rightWidth = 30
|
|
}
|
|
|
|
leftPanel := lipgloss.NewStyle().
|
|
Width(leftWidth).
|
|
Border(lipgloss.RoundedBorder()).
|
|
BorderForeground(colorAccent).
|
|
Padding(1, 2).
|
|
Render(leftSb.String())
|
|
|
|
rightPanel := lipgloss.NewStyle().
|
|
Width(rightWidth).
|
|
Border(lipgloss.RoundedBorder()).
|
|
BorderForeground(colorMuted).
|
|
Padding(1, 2).
|
|
Render(rightSb.String())
|
|
|
|
return lipgloss.JoinHorizontal(lipgloss.Top, leftPanel, " ", rightPanel)
|
|
}
|
|
|
|
func (b *BackupPicker) restoreView() string {
|
|
var sb strings.Builder
|
|
|
|
backup := b.SelectedInfo()
|
|
timestamp := ""
|
|
if backup != nil {
|
|
timestamp = time.Unix(backup.Timestamp, 0).Format("2006-01-02 15:04:05")
|
|
}
|
|
|
|
sb.WriteString(titleStyle.Render("Restore Backup"))
|
|
sb.WriteString("\n\n")
|
|
sb.WriteString(errorMsgStyle.Render(fmt.Sprintf("Restore /etc/hosts from backup '%s'?", timestamp)))
|
|
sb.WriteString("\n\n")
|
|
sb.WriteString(helpDescStyle.Render("This will replace your current hosts file."))
|
|
sb.WriteString("\n\n")
|
|
sb.WriteString(helpDescStyle.Render("y confirm • n/Esc cancel"))
|
|
|
|
return dialogStyle.Render(sb.String())
|
|
}
|
|
|
|
// formatSize formats bytes to human readable format.
|
|
func formatSize(bytes int64) string {
|
|
const (
|
|
KB = 1024
|
|
MB = KB * 1024
|
|
)
|
|
|
|
switch {
|
|
case bytes >= MB:
|
|
return fmt.Sprintf("%.1f MB", float64(bytes)/float64(MB))
|
|
case bytes >= KB:
|
|
return fmt.Sprintf("%.1f KB", float64(bytes)/float64(KB))
|
|
default:
|
|
return fmt.Sprintf("%d B", bytes)
|
|
}
|
|
}
|