diff options
Diffstat (limited to 'gui/internal/ui/window.go')
| -rw-r--r-- | gui/internal/ui/window.go | 105 |
1 files changed, 94 insertions, 11 deletions
diff --git a/gui/internal/ui/window.go b/gui/internal/ui/window.go index 819591c..784fc64 100644 --- a/gui/internal/ui/window.go +++ b/gui/internal/ui/window.go @@ -15,6 +15,7 @@ import ( "krino/gui/internal/model" "krino/internal/engine" + "krino/internal/plan" "krino/internal/xdg" ) @@ -30,6 +31,7 @@ type Window struct { rules *rulesView status *gtk.Label prefs model.Prefs + leaving bool } // NewWindow builds the window for e. Each plan and each undo is its own @@ -91,6 +93,12 @@ func NewWindow(app *gtk.Application, e *engine.Engine) *Window { // Closing the window releases whatever directory lock the open plan // holds, rather than leaving a lock file for the next run to find. w.win.ConnectCloseRequest(func() bool { + // A plan is only a plan until Apply: leaving with one open throws + // it away, which is worth saying out loud (his report, 2026-09-17). + if w.plan.hasUnapplied() && !w.leaving { + w.confirmLeaving() + return true + } w.plan.closeTab() w.plan.closePreview() w.history.closeTab() @@ -118,6 +126,26 @@ func (w *Window) reloadEngine() error { return nil } +// confirmLeaving asks before a window with an unapplied plan closes. +func (w *Window) confirmLeaving() { + n := w.plan.tab.SelectedCount() + d := gtk.NewMessageDialog(&w.win.Window, gtk.DialogModal|gtk.DialogDestroyWithParent, + gtk.MessageQuestion, gtk.ButtonsNone) + d.SetObjectProperty("text", "Close without applying?") + d.SetObjectProperty("secondary-text", fmt.Sprintf( + "%d file(s) are checked but nothing has been moved: a plan lives in this window until Apply, and closing throws it away.", n)) + d.AddButton("Stay", int(gtk.ResponseCancel)) + d.AddButton("Close without applying", int(gtk.ResponseAccept)) + d.ConnectResponse(func(response int) { + d.Destroy() + if response == int(gtk.ResponseAccept) { + w.leaving = true + w.win.Close() + } + }) + d.Show() +} + // applyPrefs takes a change from the settings window: the font of the // editor, whether the configuration is coloured, and whether the file // behind a row is shown. What is already on screen changes at once. @@ -195,27 +223,82 @@ func runInBackground(work func(context.Context) error, done func(error)) context return cancel } -// rowLabel is the middle cell of a row: what would happen to the file and -// where it would land, or - when nothing would - why not. Destinations -// inside root are shown relative to it, as the plan's own output does. -func rowLabel(r model.Row, root string) string { +// actionColours are what each action is painted in, so the eye finds the +// deletions without reading: they are the ones that cannot be undone from +// the window (his request, 2026-09-17). +var actionColours = map[plan.Kind]string{ + plan.Copy: "#2a9d8f", + plan.Move: "#3584e4", + plan.Rename: "#9141ac", + plan.Trash: "#c06014", + plan.DeletePermanent: "#c01c28", +} + +// actionRank decides which action gives a row its colour when a file gets +// several: the one that matters most to the reader. +var actionRank = map[plan.Kind]int{ + plan.Rename: 1, plan.Copy: 2, plan.Move: 3, plan.Trash: 4, plan.DeletePermanent: 5, +} + +// rowAction is a row's actions in capitals - "MOVE", "RENAME+MOVE" - and +// the colour they are shown in. A row that would do nothing has neither. +func rowAction(r model.Row) (text, colour string) { if len(r.Steps) == 0 { - return strings.Join(r.Warnings, "; ") + return "", "" } var parts []string + var worst plan.Kind + rank := -1 + skipped := 0 + for _, s := range r.Steps { + if s.Skip != "" { + skipped++ + continue + } + parts = append(parts, strings.ToUpper(s.Kind.String())) + if actionRank[s.Kind] > rank { + rank, worst = actionRank[s.Kind], s.Kind + } + } + if len(parts) == 0 { + return "SKIPPED", dimColour + } + return strings.Join(parts, "+"), actionColours[worst] +} + +// rowWhere is where a row's file would end up - the last place its steps +// put it - or, when nothing would happen, why not. Destinations inside root +// are shown relative to it, as the plan's own output does. +func rowWhere(r model.Row, root string) string { + if len(r.Steps) == 0 { + return strings.Join(r.Warnings, "; ") + } + where := "" + var notes []string for _, s := range r.Steps { switch { case s.Skip != "": - parts = append(parts, s.Kind.String()+" skipped: "+s.Skip) - case s.Dst == "": - parts = append(parts, s.Kind.String()) - default: - parts = append(parts, s.Kind.String()+" "+shorten(s.Dst, root)) + notes = append(notes, strings.ToLower(s.Kind.String())+" skipped: "+s.Skip) + case s.Kind == plan.Trash: + where = "the Trash" + case s.Kind == plan.DeletePermanent: + where = "gone for good" + case s.Dst != "": + where = shorten(s.Dst, root) } } - return strings.Join(parts, ", ") + if where == "" { + return strings.Join(notes, "; ") + } + if len(notes) > 0 { + return where + " (" + strings.Join(notes, "; ") + ")" + } + return where } +// dimColour is for text that is not an action: a skip, a warning. +const dimColour = "#8b8b8b" + // shorten writes a destination inside root relative to it, and any other // with ~ for the home directory. func shorten(dst, root string) string { |
