Refine confirmation dialog actions
This commit is contained in:
parent
3d1c572e16
commit
b5cdb77415
1 changed files with 160 additions and 28 deletions
|
|
@ -93,6 +93,12 @@ type copyProgressMsg struct {
|
||||||
progress vfs.CopyProgress
|
progress vfs.CopyProgress
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type deletePlanMsg struct {
|
||||||
|
sourcePaths []string
|
||||||
|
stats vfs.TransferStats
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
type copyDoneMsg struct {
|
type copyDoneMsg struct {
|
||||||
jobID int
|
jobID int
|
||||||
kind fileOpKind
|
kind fileOpKind
|
||||||
|
|
@ -270,21 +276,12 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
|
|
||||||
verb := operationVerb(msg.kind)
|
verb := operationVerb(msg.kind)
|
||||||
title := fmt.Sprintf("%s selected entry?", strings.Title(verb))
|
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{
|
body := strings.Join([]string{
|
||||||
fmt.Sprintf("From: %s", fromLabel),
|
fmt.Sprintf("Items: %d", len(msg.sourcePaths)),
|
||||||
fmt.Sprintf("To: %s", msg.targetDir),
|
|
||||||
"",
|
|
||||||
fmt.Sprintf("Files: %d", msg.stats.FilesTotal),
|
fmt.Sprintf("Files: %d", msg.stats.FilesTotal),
|
||||||
fmt.Sprintf("Size: %s", formatSize(msg.stats.BytesTotal, true)),
|
fmt.Sprintf("Size: %s", formatSize(msg.stats.BytesTotal, true)),
|
||||||
}, "\n")
|
}, "\n")
|
||||||
if msg.existingTargets > 0 {
|
note := "confirm-actions"
|
||||||
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)
|
|
||||||
m.openConfirmModal(title, body, note, pendingOperation{
|
m.openConfirmModal(title, body, note, pendingOperation{
|
||||||
kind: msg.kind,
|
kind: msg.kind,
|
||||||
sourcePaths: append([]string(nil), msg.sourcePaths...),
|
sourcePaths: append([]string(nil), msg.sourcePaths...),
|
||||||
|
|
@ -295,6 +292,31 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
})
|
})
|
||||||
return m, nil
|
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:
|
case copyProgressMsg:
|
||||||
if m.copyJob == nil || msg.jobID != m.copyJob.id {
|
if m.copyJob == nil || msg.jobID != m.copyJob.id {
|
||||||
return m, nil
|
return m, nil
|
||||||
|
|
@ -830,20 +852,9 @@ func (m *Model) handleDelete() (tea.Model, tea.Cmd) {
|
||||||
return m, deletePathsCmd(sources)
|
return m, deletePathsCmd(sources)
|
||||||
}
|
}
|
||||||
|
|
||||||
body := sources[0]
|
m.busy = true
|
||||||
if len(sources) > 1 {
|
m.status = "Calculating delete size"
|
||||||
body = fmt.Sprintf("%d selected entries", len(sources))
|
return m, deletePlanCmd(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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) handleView() (tea.Model, tea.Cmd) {
|
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
|
modal := m.modal
|
||||||
contentWidth := max(width-4, 1)
|
contentWidth := max(width-4, 1)
|
||||||
titleStyle := lipgloss.NewStyle().Width(contentWidth).Background(palette.Panel).Bold(true).Foreground(palette.Accent)
|
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)
|
noteStyle := lipgloss.NewStyle().Width(contentWidth).Background(palette.Panel).Foreground(palette.Muted)
|
||||||
spacer := lipgloss.NewStyle().Width(contentWidth).Background(palette.Panel).Render(" ")
|
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()).
|
BorderStyle(lipgloss.DoubleBorder()).
|
||||||
BorderForeground(palette.BorderActive)
|
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 {
|
if modal.kind == modalMkdir {
|
||||||
lines = append(lines, spacer, lipgloss.NewStyle().Width(contentWidth).Background(palette.Panel).Render(modal.input.View()))
|
lines = append(lines, spacer, lipgloss.NewStyle().Width(contentWidth).Background(palette.Panel).Render(modal.input.View()))
|
||||||
}
|
}
|
||||||
if modal.note != "" {
|
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"))
|
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 {
|
func renderHelpModal(modal modalState, palette theme.Palette, width int) string {
|
||||||
contentWidth := max(width-4, 1)
|
contentWidth := max(width-4, 1)
|
||||||
titleStyle := lipgloss.NewStyle().Width(contentWidth).Background(palette.Panel).Bold(true).Foreground(palette.Accent)
|
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 {
|
func waitCopyProgressCmd(ch <-chan tea.Msg) tea.Cmd {
|
||||||
return func() tea.Msg {
|
return func() tea.Msg {
|
||||||
return <-ch
|
return <-ch
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue