Skip to content

feat(picker): a sort header and s/S sorting for the file chooser - #3

Closed
bandoyer wants to merge 3 commits into
preview/v0.3.1-basefrom
picker-sort-headers
Closed

bandoyer wants to merge 3 commits into
preview/v0.3.1-basefrom
picker-sort-headers

Conversation

@bandoyer

Copy link
Copy Markdown
Owner

Preview only. This PR exists so the upstream description can be read as GitHub renders it. Its base is a throwaway branch at v0.3.1; the change itself is already merged into this fork by #2. Do not merge. Nothing has been sent upstream.

The gap

Since thisisgm#134 the file chooser opens in the window's saved order, which is right. But it draws no column header, so a window sorted by Modified gives a chooser sorted by Modified with nothing on screen to say so and no way to change it.

Everything needed already exists: s/S resolve in the listing context, Header.qml emits sortRequested, and the backend's sort command reorders the listing it holds. The chooser was the one surface connected to none of it. No Rust, protocol or keymap changes.

Before (main) After
before after

Same folder, same saved order, same window size. After two clicks on Size, with a file checked beforehand:

size descending

Behaviour

  • A click on a column sorts it ascending; a click on the sorted column reverses it. s steps Name → Size → Modified, S reverses. The mark moves on the click, per tests/js/sort.js.
  • The chooser offers Picker.SORT_ORDERS (name, size, mtime): Kind is in HIDDEN_COLS, and an order no header can mark has no feedback. A saved kind order is still inherited, and S still reverses it.
  • The choice is the dialog's own. It holds across filter chips, the hidden toggle and watched refreshes, and nothing is written to ui.json.
  • The folder is not read again, so marks and a Save review are untouched. filter_listing runs before ordering in run.rs, so sort reorders the already-filtered listing.
  • Recent draws no mark and cannot be sorted. s/S typed into the Save filename field are text.

Three commits, separable

  1. refactor: footer → ui/PickerFooter.qml. ui/picker.qml sat at exactly its recorded 635 in tools/flea-file-budget, so the header had nowhere to go. The footer is the one block there that only reads the picker. No behaviour change. If you would rather cut that file differently, the feature commit does not depend on how the room was made. The ledger entry is not raised; the file ends at 635.
  2. feat: ui/PickerHeader.qml is Header.qml over the chooser's columns; Header.qml gains leadingSlot and compactDate, the pair Row.qml already takes, defaulting to the window's geometry. The column / next / reverse decisions in ui/js/Sort.js become pure functions the pane and the chooser share, so a new backend sort key still changes one file. requestSort in ui/picker.qml is the one way in.
  3. fix: review repairs: see below.

The one clause worth reading

win.sortable is the single condition the header and the keys both obey. Its listingFailed clause is there because a refused list deliberately leaves the backend holding the previous folder, while the chooser's path has already moved and its state reads empty. With the clause removed I observed s in a mode-000 folder drawing the parent's rows under the refused path, which is the path Picker.rowPath builds marks and returned URIs from. A rescan never had this problem; it is the one question that choosing sort over a re-read opens, so the sort route closes it.

Verification

Run:

  • tests/js.sh: new decision cases in tests/js/picker.js, red first, then green. tests/js/sort.js passes unchanged.
  • ./tests/run-all.sh: the same pass/fail set as v0.3.1 on my box (the suites that fail there need the media fixture, python-gobject under my Python, or a display, and fail identically on pristine main).
  • tools/flea-file-budget exit 0. tools/flea-qmllint-gate: gated categories identical to v0.3.1; only the ungated missing-property context count moved, 1680 → 1683.
  • makepkg check() passes; the package is installed here and was driven through the real portal with omarchy-file-select.
  • Driven on the running chooser, keys by wtype and clicks by a real pointer (ydotool), state read from fleapicker snapshot, against a fixture whose name, size and modified orders are three different orders: every header click, the s cycle and wrap, S, an inherited order, a mark surviving reorders, the hidden toggle, into a subfolder and back, the refused folder, Recent, and typing in the Save field.
  • The window's header compared beside v0.3.1 at the same width, single- and dual-pane: identical.

Not run:

  • tests/picker-native.py sorting has never been executed. It needs omarchy-drive, which I do not have. It is written against the harness's existing helpers and compiles; one bug in it was already found by reading and fixed in the third commit. Please treat it as unproven until it has run on your box, and I am glad to fix whatever it turns up.
  • At the dual-pane width I could test, only Name fits, so the dual Size/Modified header cells were not seen drawn. The arithmetic for them is unchanged.

Known trade-offs

  • The listing blanks for the sort's round trip. Clearing it is what returns the viewport to the top, through the same mechanism navigation already relies on. If that is visible on large folders it is worth revisiting.
  • The footer hints do not name s/S. Those strings are the board's, so I left them alone.
  • Reusing Header.qml means the chooser instantiates its search strip, never visible here, and a right-click signal with no listener.

Heads-up

thisisgm#175 also edits ui/picker.qml. This branch leaves that file at exactly 635, so whichever lands second will need to re-solve the ceiling.

🤖 Generated with Claude Code

bandoyer and others added 3 commits September 21, 2026 14:20
ui/picker.qml sat at exactly its recorded 635-line ceiling, so the sort
header the chooser is missing had nowhere to go. The footer is the one
block in the window that is presentation alone: it reads the picker and
changes nothing. It takes `picker` the way its sibling components do, and
snapshot() reads the hint text from the component instead of the Text.

No behaviour change; the existing picker checks are the net.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The chooser drew no column header, so a user whose window was sorted by
size got a chooser sorted by size with nothing to say so and no way to
change it. Sorting already existed end to end: the s and S bindings
resolve in the listing context, Header.qml emits sortRequested, and the
backend's sort command reorders the listing it holds. The chooser was
the one surface that never connected to any of it.

ui/PickerHeader.qml is Header.qml over the chooser's columns, and
Header.qml takes the leadingSlot and compactDate pair Row.qml already
takes, both defaulting to the window's geometry. The column, next and
reverse decisions in ui/js/Sort.js are now pure functions the pane and
the chooser share; the chooser offers Picker.SORT_ORDERS, which leaves
out the hidden Kind column. requestSort in ui/picker.qml sends sort and
asks for the first window. The folder is not read again, so marks and a
save review are untouched, and the choice rides later listings through
preserveSort without being written to ui.json.

win.sortable is the single condition the header and the keys both read.
Its listingFailed clause matters: a refused scan leaves the backend
holding the previous folder while the chooser's path has moved, and with
the clause removed a sort was observed drawing the previous folder's
rows under the refused path, which is the path marks are built from.

tests/js/picker.js covers the decisions. tests/picker-native.py gains a
sorting group for clicks, keys, marks, refresh, the refused folder,
Recent, the scrolled viewport and Save; it needs omarchy-drive and has
not been run.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…s from name

Review of the sorting change found three things worth repairing.

listingFailed was set in onFailed's catch-all, so a statefile, read or
state error would have disabled sorting over a listing that arrived
fine. It is now set only where a scan or sort failure has left no
listing for this path.

preserveSort is declared on the chooser's Backend, the way ui/shell.qml
declares it, instead of being switched on by the first sort. The saved
order still seeds the first listing; after that the header's mark can
only move when the chooser itself reorders.

tests/picker-native.py sorting left ui.json seeded with kind for its
second and third requests, whose expectations start from name, so both
would have timed out on first run. The seed is removed once the
byte-for-byte check has read it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@bandoyer

Copy link
Copy Markdown
Owner Author

Preview read and approved; closing. The upstream PR carries this description.

@bandoyer bandoyer closed this Sep 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant