From c58fe0d332a36663f09f85f7e9b7fa1ee89c1a72 Mon Sep 17 00:00:00 2001 From: mixeme Date: Sun, 26 Jul 2026 15:30:15 +0300 Subject: [PATCH] docs: add implementation plan for compact job list view Plans an opt-in one-line rendering of the Jobs tab list (name left, status right) alongside the current three-line detailed rows, switched from a "View" dropdown beside the Folder filter and persisted as a new Config.JobListView field. Co-Authored-By: Claude Opus 5 --- docs/PLAN-compact-job-list.md | 135 ++++++++++++++++++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 docs/PLAN-compact-job-list.md diff --git a/docs/PLAN-compact-job-list.md b/docs/PLAN-compact-job-list.md new file mode 100644 index 0000000..83b3185 --- /dev/null +++ b/docs/PLAN-compact-job-list.md @@ -0,0 +1,135 @@ +# Implementation plan — compact / detailed job list view + +## Context + +The Jobs tab sidebar renders every job as a three-line block (bold name, a +metadata line with folder + schedule + command, and a status line) — see the +`widget.NewList` call in [jobs_view.go:108](../src/ui/jobs_view.go). That is +informative but tall: with many jobs the sidebar needs constant scrolling. + +Add a second, opt-in rendering of the same list: **Compact** shows one line per +job (name left, status right); **Detailed** is the current three-line block and +stays the default. The choice is switchable from the Jobs tab itself and is +persisted in `gosentry.json`, so it survives a restart like `Config.Theme` does. + +## Design decisions + +- **Control lives in the Jobs sidebar header** — a "View" dropdown beside the + existing "Folder" filter. Switching is one click, next to what it affects. +- **Compact row = name + status** on a single line. Same height as one label, + so per-job health stays visible at a glance. +- **One list, one row template.** `widget.List` caches the row template's + `MinSize` as `itemMin`; `list.Refresh()` re-creates the template and + recomputes it (verified in Fyne 2.7.4 `widget/list.go`, + `listRenderer.Refresh`), so the rows genuinely shrink/grow on a mode switch + without rebuilding the widget. The template therefore always holds the same + four objects, and the mode is expressed by `Show()`/`Hide()`. Both + `compactVBoxLayout` (`src/ui/layout.go`) and Fyne's border layout skip hidden + children in `MinSize`, so a hidden line contributes no height. + +## Changes + +### 1. `src/domain/config.go` — persisted preference + +- New string type `JobListView` with `JobListViewDetailed = "detailed"` and + `JobListViewCompact = "compact"`, documented like `Theme` as a UI-only choice. +- New field `JobListView JobListView \`json:"job_list_view,omitempty"\`` on + `Config`; comment that empty means detailed so pre-existing configs keep the + current look. +- `DefaultConfig()` returns `JobListViewDetailed`. +- Method `func (v JobListView) IsCompact() bool` — only the exact `"compact"` + value is compact — so every consumer normalizes unknown/legacy values the + same way. + +### 2. `src/app/operations.go` — save the choice + +New `func (s *Service) SetJobListView(view domain.JobListView) error`, modelled +on `SetGlobalPause` (`src/app/operations.go:168`) but deliberately lighter: +normalize a non-`compact` value to `JobListViewDetailed`, take `mu`, return +early if unchanged, set `s.store.Config.JobListView`, call +`s.store.SaveConfig()`, unlock. No `SaveJobs` (no job changed), no event emitted +(this is presentational only — the Jobs view refreshes its own list; an event +would trigger a pointless whole-window refresh). A comment records that +reasoning. + +### 3. `src/ui/jobs_view.go` — rendering and the toggle + +- Read the initial mode into a `compactList` local: + `svc.Store().Config.JobListView.IsCompact()`. +- **CreateItem**: build `name` (bold, `Wrapping = fyne.TextTruncate` so a long + name cannot push the status off the row), `inlineStatus` (compact-only, + right-hand side), `meta`, and `status`. Put the first two on one line via + `container.NewBorder(nil, nil, nil, inlineStatus, name)` and keep the outer + `compactVBoxLayout{spacing: jobRowSpacing}` container with + `[nameLine, meta, status]`. Apply the mode's visibility here too — this is + what makes `itemMin` correct for the mode. +- **UpdateItem**: reach `nameLine` through `row.Objects[0].(*fyne.Container)` + (`NewBorder` appends the border slots after the center object, so + `Objects[0]` is `name` and `Objects[1]` is `inlineStatus` — worth a comment, + matching the existing index-based access in this callback). Set all four texts + from the existing helpers (`app.DisplayFolder`, `app.DisplayInvocation`, + `app.StatusText`) and **re-apply visibility on every update**: a full + `Refresh` reuses the already-visible row objects, which were created under the + old mode, so visibility cannot be left to `CreateItem` alone. +- **View dropdown**: `widget.NewSelect([]string{viewLabelDetailed, + viewLabelCompact}, …)`. Its handler maps label → `domain.JobListView`, returns + early when unchanged, updates `compactList`, persists via + `svc.SetJobListView` (on error `dialog.ShowError` and revert the select, per + the error rule in `docs/STANDARDS.md`), then calls `list.Refresh()`. The + selection and the details panel are untouched by the switch. +- **Header layout**: replace the current `"Folder"` caption + `folderSelect` + pair in `sidebarHeader` with + `container.NewGridWithColumns(2, , )`, so the second dropdown costs no extra header height and the list + keeps the same room. The 400px `minJobsSidebarWidth` comfortably fits two + short selects. + +### 4. `src/ui/jobs_view_helpers.go` — label mapping + +`viewLabelDetailed`/`viewLabelCompact` consts plus pure +`viewLabel(domain.JobListView) string` and +`viewFromLabel(string) domain.JobListView`, mirroring +`themeLabel`/`themeFromLabel` (`src/ui/settings_view.go:370`) so the on-disk +strings never reach the user. + +### 5. Tests + +- `src/domain/config_test.go` (new): `JobListView.IsCompact` for `"compact"`, + `"detailed"`, `""`, and a junk value. +- `src/ui/jobs_view_test.go`: `viewLabel`/`viewFromLabel` round-trip, and + unknown/empty label → detailed. +- `src/app/operations_test.go`: `TestSetJobListViewPersistsToConfigFile` + modelled on `TestSetGlobalPausePersistsToConfigFile` + (`src/app/operations_test.go:553`) — switch to compact, unmarshal + `svc.store.Paths.ConfigPath`, assert the field, switch back; plus a case + asserting an unknown value is stored as `detailed`. +- `src/storage/store_test.go`: one added assertion in the defaults test beside + the existing `Theme` check (~line 174) that a fresh config is `detailed`. + +### 6. Docs and version + +- `docs/CHANGELOG.md`: new `## 0.14.0 - 2026-07-26` section (a feature → minor + bump) covering the compact/detailed toggle and the new `job_list_view` field, + noting legacy configs stay detailed. +- `src/app/version.go`: `0.13.0` → `0.14.0`. +- `docs/ARCHITECTURE.md`: add the two new helpers to the `jobs_view_helpers.go` + row of the file-split table (~line 195). + +## Verification + +1. Build, vet, and test the whole module through the cgo-enabled shell (the + default Bash env has CGO off, so GUI packages will not compile there): + +```bash +export PATH="/c/msys64/ucrt64/bin:$PATH"; export CGO_ENABLED=1; go build ./... && go vet ./... && go test -race ./... +``` + +2. Run the app (`go run ./cmd/gosentry`) with several jobs configured and check: + - Default launch is Detailed and looks exactly as before. + - Switching to Compact collapses every row to one line, name left / status + right, and many more jobs fit without scrolling. + - Selection and the details panel keep working after switching, in both + directions, including with a folder filter active and with an empty list. + - A running job's status still updates live in compact rows. + - `gosentry.json` gains `"job_list_view": "compact"`; restarting reopens in + compact, and switching back removes/flips the field.