Files
dbbackup/internal/tui/history.go
Renz 5286d94c8b Fix: Add viewport scrolling to operation history
PROBLEM:
- History displayed ALL entries at once
- With many backups, first entries scroll off screen
- Cursor navigation worked but selection was invisible
- User had to "blindly" navigate 5+ entries to see anything

SOLUTION:
- Added viewport with max 15 visible items at once
- Viewport auto-scrolls to follow cursor position
- Scroll indicators show when there are more entries:
  * "▲ More entries above..."
  * "▼ X more entries below..."
- Cursor always visible within viewport

RESULT:
-  Always see current selection
-  Works with any number of history entries
-  Clear visual feedback with scroll indicators
-  Smooth navigation experience
2025-11-07 11:58:46 +00:00

187 lines
3.9 KiB
Go

package tui
import (
"fmt"
"io/ioutil"
"strings"
"time"
tea "github.com/charmbracelet/bubbletea"
"dbbackup/internal/config"
"dbbackup/internal/logger"
)
// HistoryViewModel shows operation history
type HistoryViewModel struct {
config *config.Config
logger logger.Logger
parent tea.Model
history []HistoryEntry
cursor int
viewOffset int // For scrolling large lists
}
type HistoryEntry struct {
Type string
Database string
Timestamp time.Time
Status string
Filename string
}
func NewHistoryView(cfg *config.Config, log logger.Logger, parent tea.Model) HistoryViewModel {
return HistoryViewModel{
config: cfg,
logger: log,
parent: parent,
history: loadHistory(cfg),
cursor: 0,
viewOffset: 0, // Start at top
}
}
func loadHistory(cfg *config.Config) []HistoryEntry {
var entries []HistoryEntry
// Read backup files from backup directory
files, err := ioutil.ReadDir(cfg.BackupDir)
if err != nil {
return entries
}
for _, file := range files {
if file.IsDir() {
continue
}
name := file.Name()
if strings.HasSuffix(name, ".info") {
continue
}
var backupType string
var database string
if strings.Contains(name, "cluster") {
backupType = "Cluster Backup"
database = "All Databases"
} else if strings.Contains(name, "sample") {
backupType = "Sample Backup"
parts := strings.Split(name, "_")
if len(parts) > 2 {
database = parts[2]
}
} else {
backupType = "Single Backup"
parts := strings.Split(name, "_")
if len(parts) > 2 {
database = parts[2]
}
}
entries = append(entries, HistoryEntry{
Type: backupType,
Database: database,
Timestamp: file.ModTime(),
Status: "✅ Completed",
Filename: name,
})
}
return entries
}
func (m HistoryViewModel) Init() tea.Cmd {
return nil
}
func (m HistoryViewModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "ctrl+c", "q", "esc":
return m.parent, nil
case "up", "k":
if m.cursor > 0 {
m.cursor--
// Scroll viewport up if cursor moves above visible area
if m.cursor < m.viewOffset {
m.viewOffset = m.cursor
}
}
case "down", "j":
if m.cursor < len(m.history)-1 {
m.cursor++
// Scroll viewport down if cursor moves below visible area
maxVisible := 15 // Show max 15 items at once
if m.cursor >= m.viewOffset+maxVisible {
m.viewOffset = m.cursor - maxVisible + 1
}
}
}
}
return m, nil
}
func (m HistoryViewModel) View() string {
var s strings.Builder
header := titleStyle.Render("📜 Operation History")
s.WriteString(fmt.Sprintf("\n%s\n\n", header))
if len(m.history) == 0 {
s.WriteString(infoStyle.Render("📭 No backup history found"))
s.WriteString("\n\n")
} else {
maxVisible := 15 // Show max 15 items at once
// Calculate visible range
start := m.viewOffset
end := start + maxVisible
if end > len(m.history) {
end = len(m.history)
}
s.WriteString(fmt.Sprintf("Found %d backup operations (Viewing %d/%d):\n\n",
len(m.history), m.cursor+1, len(m.history)))
// Show scroll indicators
if start > 0 {
s.WriteString(infoStyle.Render(" ▲ More entries above...\n"))
}
// Display only visible entries
for i := start; i < end; i++ {
entry := m.history[i]
line := fmt.Sprintf("[%s] %s - %s (%s)",
entry.Timestamp.Format("2006-01-02 15:04"),
entry.Type,
entry.Database,
entry.Status)
if m.cursor == i {
// Highlighted selection
s.WriteString(selectedStyle.Render("→ " + line))
} else {
s.WriteString(" " + line)
}
s.WriteString("\n")
}
// Show scroll indicator if more entries below
if end < len(m.history) {
s.WriteString(infoStyle.Render(fmt.Sprintf(" ▼ %d more entries below...\n", len(m.history)-end)))
}
s.WriteString("\n")
}
s.WriteString("⌨️ ↑/↓: Navigate • ESC: Back • q: Quit\n")
return s.String()
}