aboutsummaryrefslogtreecommitdiff
path: root/gui/internal/ui/window.go
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-09-17 00:51:15 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-09-17 00:51:15 +0200
commit5418ba6bc5c653b6a99572c6ece6abbf57836c43 (patch)
treeae0a430c468890770d20f531c3a026937c9bb948 /gui/internal/ui/window.go
parentca0b703526151149d060d6d38f307e9e38c3dcd2 (diff)
downloadkrino-5418ba6bc5c653b6a99572c6ece6abbf57836c43.tar.gz
krino-5418ba6bc5c653b6a99572c6ece6abbf57836c43.zip
gui: coloured actions with headers, a filter over the plan, bulk trash or delete
Diffstat (limited to 'gui/internal/ui/window.go')
-rw-r--r--gui/internal/ui/window.go105
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 {