diff options
| author | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-09-16 20:23:24 +0200 |
|---|---|---|
| committer | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-09-16 20:23:24 +0200 |
| commit | cace98008d132e48f19b405b5bddc4a6067c0cae (patch) | |
| tree | 8bfc6af8fa55160ae38c80b45b66894db025c1da /gui/internal/ui | |
| parent | 4363c7ad13d5eae1752ea3c36e1cfe7c13707c0d (diff) | |
| download | krino-cace98008d132e48f19b405b5bddc4a6067c0cae.tar.gz krino-cace98008d132e48f19b405b5bddc4a6067c0cae.zip | |
gui: the directory's own settings as a form
Diffstat (limited to 'gui/internal/ui')
| -rw-r--r-- | gui/internal/ui/forms.go | 187 |
1 files changed, 181 insertions, 6 deletions
diff --git a/gui/internal/ui/forms.go b/gui/internal/ui/forms.go index 928c17e..1606903 100644 --- a/gui/internal/ui/forms.go +++ b/gui/internal/ui/forms.go @@ -55,6 +55,8 @@ type formsView struct { forms []model.Form sel int editor *formEditor + settingRows []*settingRow + onSettings bool pending glib.SourceHandle quiet bool commentsAcknowledged map[string]bool @@ -110,9 +112,15 @@ func newFormsView(w *Window, owner *rulesView) *formsView { f.root.Append(right) f.list.ConnectRowSelected(func(row *gtk.ListBoxRow) { - if row != nil && !f.quiet { - f.show(row.Index()) + if row == nil || f.quiet { + return + } + // Row 0 is the directory itself; the forms follow it. + if row.Index() == 0 { + f.showSettings() + return } + f.show(row.Index() - 1) }) f.add.ConnectClicked(f.onAdd) f.del.ConnectClicked(f.onDelete) @@ -133,6 +141,7 @@ func (f *formsView) reload() error { keep := f.sel f.quiet = true clearList(f.list) + f.list.Append(settingsRow()) for _, form := range forms { row := gtk.NewListBoxRow() label := gtk.NewLabel(escape(formLabel(form))) @@ -148,16 +157,181 @@ func (f *formsView) reload() error { f.list.Append(row) } f.quiet = false - if keep >= 0 && keep < len(forms) { - f.list.SelectRow(f.list.RowAtIndex(keep)) + switch { + case keep >= 0 && keep < len(forms): + f.list.SelectRow(f.list.RowAtIndex(keep + 1)) f.show(keep) - } else { + case f.onSettings: + f.list.SelectRow(f.list.RowAtIndex(0)) + f.showSettings() + default: f.sel = -1 - f.clearEditor("Select a rule to edit it, or Add rule.") + f.clearEditor("Select the directory or a rule to edit it, or Add rule.") } return nil } +// settingsRow is the first line of the list: the directory's own settings. +func settingsRow() *gtk.ListBoxRow { + row := gtk.NewListBoxRow() + label := gtk.NewLabel("the directory itself") + label.SetXAlign(0) + label.SetMarginStart(6) + label.SetMarginEnd(6) + label.SetMarginTop(2) + label.SetMarginBottom(2) + row.SetChild(label) + return row +} + +// showSettings builds the form for the directory's own settings: one row +// per setting krino.conf(5) documents, each holding what the file writes, +// and empty when the file leaves it out (GUI design ยง5.1). +func (f *formsView) showSettings() { + f.sel = -1 + f.onSettings = true + f.editor = nil + f.note.SetText("These are the directory's own settings. An empty field is one the file does not set, and krino's default applies.") + box := gtk.NewBox(gtk.OrientationVertical, 6) + box.SetMarginStart(8) + box.SetMarginEnd(8) + box.SetMarginTop(6) + box.SetMarginBottom(6) + f.settingRows = nil + for _, head := range model.DirSettings { + args, _, err := f.owner.rules.Setting(head) + if err != nil { + f.owner.setCheck("settings: " + err.Error()) + return + } + row := newSettingRow(head, args, f.armSettings) + f.settingRows = append(f.settingRows, row) + box.Append(row.root) + } + if child := f.place.FirstChild(); child != nil { + f.place.Remove(child) + } + f.place.Append(box) +} + +// armSettings writes the settings back after the same pause a form edit +// waits. +func (f *formsView) armSettings() { + if f.pending != 0 { + glib.SourceRemove(f.pending) + } + f.pending = glib.TimeoutAdd(checkDelay, func() bool { + f.pending = 0 + f.applySettings() + return false + }) +} + +// applySettings writes every setting whose field has changed. +func (f *formsView) applySettings() { + changed := false + for _, row := range f.settingRows { + args := row.value() + was, _, err := f.owner.rules.Setting(row.head) + if err != nil { + f.owner.setCheck("settings: " + err.Error()) + return + } + if args == was { + continue + } + if err := f.owner.rules.SetSetting(row.head, args); err != nil { + f.owner.setCheck("settings: " + err.Error()) + row.set(was) + continue + } + changed = true + } + if changed { + f.owner.textChangedByForm() + } +} + +// settingRow is one directory setting: its name, and what the file writes +// for it. A setting with fixed choices gets them; the rest are written as +// they are, so no value is out of reach. +type settingRow struct { + root *gtk.Box + head string + entry *gtk.Entry + drop *gtk.DropDown + items []string +} + +// settingChoices are the settings whose values are a fixed few. +var settingChoices = map[string][]string{ + "recursive": {"", "yes", "no"}, + "case": {"", "ignore", "strict"}, + "fold": {"", "yes", "no"}, + "on-conflict": {"", "suffix", "skip", "overwrite"}, +} + +// settingHints is the example shown in an empty field. +var settingHints = map[string]string{ + "path": `"~/downloads"`, + "max-depth": `3`, + "min-age": `2m (0, 30s, 2m, 1h, 1d, 1w)`, + "max-read": `50M (0 reads it all)`, + "max-size": `2G`, + "busy": `".part" ".aria2" ".crdownload"`, + "ignore": `"*.part" ".*"`, +} + +func newSettingRow(head, args string, changed func()) *settingRow { + r := &settingRow{head: head} + r.root = gtk.NewBox(gtk.OrientationHorizontal, 6) + label := gtk.NewLabel(head) + label.SetXAlign(0) + label.SetSizeRequest(110, -1) + r.root.Append(label) + if items, ok := settingChoices[head]; ok { + r.items = items + shown := make([]string, len(items)) + copy(shown, items) + shown[0] = "default" + r.drop = gtk.NewDropDownFromStrings(shown) + r.drop.SetSelected(uint(indexOf(items, args))) + if indexOf(items, args) < 0 { + r.drop.SetSelected(0) + } + r.drop.Connect("notify::selected", func() { changed() }) + r.root.Append(r.drop) + return r + } + r.entry = gtk.NewEntry() + r.entry.SetText(args) + r.entry.SetHExpand(true) + r.entry.SetPlaceholderText(settingHints[head]) + r.entry.SetTooltipText(settingHints[head]) + r.entry.ConnectChanged(func() { changed() }) + r.root.Append(r.entry) + return r +} + +// value is what the row would write; "" means the setting is left out. +func (r *settingRow) value() string { + if r.drop != nil { + return r.items[r.drop.Selected()] + } + return strings.TrimSpace(r.entry.Text()) +} + +// set puts a value back, after a refused edit. +func (r *settingRow) set(args string) { + if r.drop != nil { + if i := indexOf(r.items, args); i >= 0 { + r.drop.SetSelected(uint(i)) + } + return + } + r.entry.SetText(args) +} + // formLabel is one line of the list: what the form is, and what it does. func formLabel(f model.Form) string { if f.Kind == model.ExcludeForm { @@ -179,6 +353,7 @@ func (f *formsView) show(i int) { return } f.sel = i + f.onSettings = false form := f.forms[i] f.note.SetText("") if has, err := f.owner.rules.FormHasComments(i); err == nil && has { |
