Refine confirmation dialog actions

This commit is contained in:
vrubelroman 2026-04-23 20:37:54 +03:00
parent 3d1c572e16
commit b5cdb77415

View file

@ -93,6 +93,12 @@ type copyProgressMsg struct {
progress vfs.CopyProgress
}
type deletePlanMsg struct {
sourcePaths []string
stats vfs.TransferStats
err error
}
type copyDoneMsg struct {
jobID int
kind fileOpKind
@ -270,21 +276,12 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
verb := operationVerb(msg.kind)
title := fmt.Sprintf("%s selected entry?", strings.Title(verb))
fromLabel := msg.sourcePaths[0]
if len(msg.sourcePaths) > 1 {
fromLabel = fmt.Sprintf("%d selected entries", len(msg.sourcePaths))
}
body := strings.Join([]string{
fmt.Sprintf("From: %s", fromLabel),
fmt.Sprintf("To: %s", msg.targetDir),
"",
fmt.Sprintf("Items: %d", len(msg.sourcePaths)),
fmt.Sprintf("Files: %d", msg.stats.FilesTotal),
fmt.Sprintf("Size: %s", formatSize(msg.stats.BytesTotal, true)),
}, "\n")
if msg.existingTargets > 0 {
body += fmt.Sprintf("\n\nExisting targets: %d (will be overwritten)", msg.existingTargets)
}
note := fmt.Sprintf("Enter/y to start %s, Esc/n to cancel", verb)
note := "confirm-actions"
m.openConfirmModal(title, body, note, pendingOperation{
kind: msg.kind,
sourcePaths: append([]string(nil), msg.sourcePaths...),
@ -295,6 +292,31 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
})
return m, nil
case deletePlanMsg:
m.busy = false
if msg.err != nil {
m.status = msg.err.Error()
return m, nil
}
title := "Delete selected entr" + pluralSuffix(len(msg.sourcePaths), "y", "ies") + "?"
bodyLines := []string{
fmt.Sprintf("Items: %d", len(msg.sourcePaths)),
fmt.Sprintf("Files: %d", msg.stats.FilesTotal),
fmt.Sprintf("Size: %s", formatSize(msg.stats.BytesTotal, true)),
}
m.openConfirmModal(
title,
strings.Join(bodyLines, "\n"),
"confirm-actions",
pendingOperation{
kind: opDelete,
sourcePaths: append([]string(nil), msg.sourcePaths...),
stats: msg.stats,
},
)
return m, nil
case copyProgressMsg:
if m.copyJob == nil || msg.jobID != m.copyJob.id {
return m, nil
@ -830,20 +852,9 @@ func (m *Model) handleDelete() (tea.Model, tea.Cmd) {
return m, deletePathsCmd(sources)
}
body := sources[0]
if len(sources) > 1 {
body = fmt.Sprintf("%d selected entries", len(sources))
}
m.openConfirmModal(
"Delete selected entr"+pluralSuffix(len(sources), "y", "ies")+"?",
body,
"Enter/y to delete permanently, Esc/n to cancel",
pendingOperation{
kind: opDelete,
sourcePaths: append([]string(nil), sources...),
},
)
return m, nil
m.busy = true
m.status = "Calculating delete size"
return m, deletePlanCmd(sources)
}
func (m *Model) handleView() (tea.Model, tea.Cmd) {
@ -1433,7 +1444,6 @@ func renderModal(m Model, palette theme.Palette, width int) string {
modal := m.modal
contentWidth := max(width-4, 1)
titleStyle := lipgloss.NewStyle().Width(contentWidth).Background(palette.Panel).Bold(true).Foreground(palette.Accent)
bodyStyle := lipgloss.NewStyle().Width(contentWidth).Background(palette.Panel).Foreground(palette.Muted)
noteStyle := lipgloss.NewStyle().Width(contentWidth).Background(palette.Panel).Foreground(palette.Muted)
spacer := lipgloss.NewStyle().Width(contentWidth).Background(palette.Panel).Render(" ")
@ -1445,18 +1455,119 @@ func renderModal(m Model, palette theme.Palette, width int) string {
BorderStyle(lipgloss.DoubleBorder()).
BorderForeground(palette.BorderActive)
lines := []string{titleStyle.Render(modal.title), spacer, bodyStyle.Render(modal.body)}
lines := []string{titleStyle.Render(modal.title), spacer}
for _, raw := range strings.Split(modal.body, "\n") {
lines = append(lines, renderModalBodyLine(raw, contentWidth, palette))
}
if modal.kind == modalMkdir {
lines = append(lines, spacer, lipgloss.NewStyle().Width(contentWidth).Background(palette.Panel).Render(modal.input.View()))
}
if modal.note != "" {
lines = append(lines, spacer, noteStyle.Render(modal.note))
lines = append(lines, spacer)
if modal.note == "confirm-actions" {
lines = append(lines, renderConfirmActions(contentWidth, palette))
} else {
for _, raw := range strings.Split(modal.note, "\n") {
lines = append(lines, renderModalNoteLine(raw, contentWidth, palette, noteStyle))
}
}
}
return box.Render(strings.Join(lines, "\n"))
}
func renderModalBodyLine(raw string, width int, palette theme.Palette) string {
base := lipgloss.NewStyle().
Width(width).
Background(palette.Panel).
Foreground(palette.Text)
if strings.TrimSpace(raw) == "" {
return base.Render("")
}
if idx := strings.Index(raw, ":"); idx > 0 {
keyText := strings.TrimSpace(raw[:idx+1])
valueText := strings.TrimLeft(raw[idx+1:], " ")
keyWidth := min(max(idx+2, 8), width)
valueWidth := max(width-keyWidth, 0)
keyStyle := lipgloss.NewStyle().
Width(keyWidth).
Background(palette.Panel).
Foreground(palette.FooterKey).
Bold(true)
valueStyle := lipgloss.NewStyle().
Width(valueWidth).
Background(palette.Panel).
Foreground(palette.Text)
return base.Render(keyStyle.Render(keyText) + valueStyle.Render(valueText))
}
if strings.HasPrefix(strings.TrimSpace(raw), "Existing targets") {
return lipgloss.NewStyle().
Width(width).
Background(palette.Panel).
Foreground(palette.Warning).
Render(strings.TrimSpace(raw))
}
return base.Render(raw)
}
func renderModalNoteLine(raw string, width int, palette theme.Palette, fallback lipgloss.Style) string {
trimmed := strings.TrimSpace(raw)
if trimmed == "" {
return fallback.Render("")
}
for _, sep := range []string{" to ", " (", ","} {
if idx := strings.Index(raw, sep); idx > 0 {
keyLabel := strings.TrimSpace(raw[:idx])
action := strings.TrimLeft(raw[idx:], " ")
keyWidth := min(max(lipgloss.Width(keyLabel)+2, 10), width)
actionWidth := max(width-keyWidth, 0)
keyStyle := lipgloss.NewStyle().
Width(keyWidth).
Background(palette.Panel).
Foreground(palette.FooterKey).
Bold(true)
actionStyle := lipgloss.NewStyle().
Width(actionWidth).
Background(palette.Panel).
Foreground(palette.Muted)
return keyStyle.Render(keyLabel) + actionStyle.Render(action)
}
}
return fallback.Render(raw)
}
func renderConfirmActions(width int, palette theme.Palette) string {
buttonWidth := max((width-2)/2, 10)
confirm := lipgloss.NewStyle().
Width(buttonWidth).
Align(lipgloss.Center).
Background(palette.TextFile).
Foreground(palette.Background).
Bold(true).
Render("Enter / y")
cancel := lipgloss.NewStyle().
Width(buttonWidth).
Align(lipgloss.Center).
Background(palette.Danger).
Foreground(palette.Background).
Bold(true).
Render("Esc / n")
row := lipgloss.JoinHorizontal(lipgloss.Top, confirm, " ", cancel)
return lipgloss.NewStyle().
Width(width).
Background(palette.Panel).
Render(row)
}
func renderHelpModal(modal modalState, palette theme.Palette, width int) string {
contentWidth := max(width-4, 1)
titleStyle := lipgloss.NewStyle().Width(contentWidth).Background(palette.Panel).Bold(true).Foreground(palette.Accent)
@ -1738,6 +1849,27 @@ func copyPlanCmd(kind fileOpKind, sourcePaths []string, targetDir string, overwr
}
}
func deletePlanCmd(sourcePaths []string) tea.Cmd {
return func() tea.Msg {
stats := vfs.TransferStats{}
var err error
for _, sourcePath := range sourcePaths {
part, statErr := vfs.CopyStats(sourcePath)
if statErr != nil {
err = statErr
break
}
stats.FilesTotal += part.FilesTotal
stats.BytesTotal += part.BytesTotal
}
return deletePlanMsg{
sourcePaths: append([]string(nil), sourcePaths...),
stats: stats,
err: err,
}
}
}
func waitCopyProgressCmd(ch <-chan tea.Msg) tea.Cmd {
return func() tea.Msg {
return <-ch