Compare commits

..

1 Commits

Author SHA1 Message Date
83ad62b6b5 v3.42.15: TUI - always allow Esc/Cancel during spinner operations
All checks were successful
CI/CD / Test (push) Successful in 1m13s
CI/CD / Lint (push) Successful in 1m20s
CI/CD / Build & Release (push) Successful in 3m7s
2026-01-08 10:53:00 +01:00
3 changed files with 39 additions and 30 deletions

View File

@@ -4,8 +4,8 @@ This directory contains pre-compiled binaries for the DB Backup Tool across mult
## Build Information ## Build Information
- **Version**: 3.42.10 - **Version**: 3.42.10
- **Build Time**: 2026-01-08_09:19:02_UTC - **Build Time**: 2026-01-08_09:40:57_UTC
- **Git Commit**: 1831bd7 - **Git Commit**: 55d34be
## Recent Updates (v1.1.0) ## Recent Updates (v1.1.0)
- ✅ Fixed TUI progress display with line-by-line output - ✅ Fixed TUI progress display with line-by-line output

View File

@@ -9,6 +9,7 @@ import (
"time" "time"
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
"github.com/mattn/go-runewidth"
"dbbackup/internal/config" "dbbackup/internal/config"
"dbbackup/internal/logger" "dbbackup/internal/logger"
@@ -130,15 +131,24 @@ func (m BackupManagerModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, nil return m, nil
case tea.KeyMsg: case tea.KeyMsg:
// Block input during operations // Allow escape/cancel even during operations
if msg.String() == "ctrl+c" || msg.String() == "esc" || msg.String() == "q" {
if m.opState != OpIdle {
// Cancel current operation
m.opState = OpIdle
m.opTarget = ""
m.message = "Operation cancelled"
return m, nil
}
return m.parent, nil
}
// Block other input during operations
if m.opState != OpIdle { if m.opState != OpIdle {
return m, nil return m, nil
} }
switch msg.String() { switch msg.String() {
case "ctrl+c", "q", "esc":
return m.parent, nil
case "up", "k": case "up", "k":
if m.cursor > 0 { if m.cursor > 0 {
m.cursor-- m.cursor--
@@ -219,39 +229,45 @@ func (m BackupManagerModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
func (m BackupManagerModel) View() string { func (m BackupManagerModel) View() string {
var s strings.Builder var s strings.Builder
const boxWidth = 60
// Helper to pad string to box width (handles UTF-8)
padToWidth := func(text string, width int) string {
textWidth := runewidth.StringWidth(text)
if textWidth >= width {
return runewidth.Truncate(text, width-3, "...")
}
return text + strings.Repeat(" ", width-textWidth)
}
// Title // Title
s.WriteString(titleStyle.Render("[DB] Backup Archive Manager")) s.WriteString(titleStyle.Render("[DB] Backup Archive Manager"))
s.WriteString("\n\n") s.WriteString("\n\n")
// Operation Status Box (always visible) // Operation Status Box (always visible)
s.WriteString("+--[ STATUS ]" + strings.Repeat("-", 47) + "+\n") s.WriteString("+--[ STATUS ]" + strings.Repeat("-", boxWidth-13) + "+\n")
switch m.opState { switch m.opState {
case OpVerifying: case OpVerifying:
spinner := spinnerFrames[m.spinnerFrame] spinner := spinnerFrames[m.spinnerFrame]
statusText := fmt.Sprintf(" %s Verifying: %s", spinner, m.opTarget) statusText := fmt.Sprintf(" %s Verifying: %s", spinner, m.opTarget)
s.WriteString("|" + statusText + strings.Repeat(" ", 59-len(statusText)) + "|\n") s.WriteString("|" + padToWidth(statusText, boxWidth) + "|\n")
case OpDeleting: case OpDeleting:
spinner := spinnerFrames[m.spinnerFrame] spinner := spinnerFrames[m.spinnerFrame]
statusText := fmt.Sprintf(" %s Deleting: %s", spinner, m.opTarget) statusText := fmt.Sprintf(" %s Deleting: %s", spinner, m.opTarget)
s.WriteString("|" + statusText + strings.Repeat(" ", 59-len(statusText)) + "|\n") s.WriteString("|" + padToWidth(statusText, boxWidth) + "|\n")
default: default:
if m.loading { if m.loading {
spinner := spinnerFrames[m.spinnerFrame] spinner := spinnerFrames[m.spinnerFrame]
statusText := fmt.Sprintf(" %s Loading archives...", spinner) statusText := fmt.Sprintf(" %s Loading archives...", spinner)
s.WriteString("|" + statusText + strings.Repeat(" ", 59-len(statusText)) + "|\n") s.WriteString("|" + padToWidth(statusText, boxWidth) + "|\n")
} else if m.message != "" { } else if m.message != "" {
msgText := " " + m.message msgText := " " + m.message
if len(msgText) > 58 { s.WriteString("|" + padToWidth(msgText, boxWidth) + "|\n")
msgText = msgText[:55] + "..."
}
s.WriteString("|" + msgText + strings.Repeat(" ", 59-len(msgText)) + "|\n")
} else { } else {
statusText := " Ready" s.WriteString("|" + padToWidth(" Ready", boxWidth) + "|\n")
s.WriteString("|" + statusText + strings.Repeat(" ", 59-len(statusText)) + "|\n")
} }
} }
s.WriteString("+" + strings.Repeat("-", 60) + "+\n\n") s.WriteString("+" + strings.Repeat("-", boxWidth) + "+\n\n")
if m.loading { if m.loading {
return s.String() return s.String()
@@ -334,14 +350,8 @@ func (m BackupManagerModel) View() string {
s.WriteString(infoStyle.Render(fmt.Sprintf("Selected: %d/%d", m.cursor+1, len(m.archives)))) s.WriteString(infoStyle.Render(fmt.Sprintf("Selected: %d/%d", m.cursor+1, len(m.archives))))
s.WriteString("\n\n") s.WriteString("\n\n")
// Grouped keyboard shortcuts for better readability // Grouped keyboard shortcuts - simple aligned format
s.WriteString("+--[ SHORTCUTS ]" + strings.Repeat("-", 44) + "+\n") s.WriteString("SHORTCUTS: Up/Down=Move | r=Restore | v=Verify | d=Delete | i=Info | R=Refresh | Esc=Back | q=Quit")
s.WriteString("| NAVIGATE ACTIONS OTHER |\n")
s.WriteString("| Up/Down: Move r: Restore R: Refresh |\n")
s.WriteString("| v: Verify Esc: Back |\n")
s.WriteString("| d: Delete q: Quit |\n")
s.WriteString("| i: Info |\n")
s.WriteString("+" + strings.Repeat("-", 60) + "+")
return s.String() return s.String()
} }

View File

@@ -146,11 +146,10 @@ func (m StatusViewModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, nil return m, nil
case tea.KeyMsg: case tea.KeyMsg:
if !m.loading { // Always allow escape, even during loading
switch msg.String() { switch msg.String() {
case "ctrl+c", "q", "esc", "enter": case "ctrl+c", "q", "esc", "enter":
return m.parent, nil return m.parent, nil
}
} }
} }