Skip to content

Open and Save dialogs for other apps: folio chooser - #75

Open
28allday wants to merge 10 commits into
omacom:mainfrom
28allday:file-chooser
Open

28allday wants to merge 10 commits into
omacom:mainfrom
28allday:file-chooser

Conversation

@28allday

@28allday 28allday commented Nov 11, 2026 •

Copy link
Copy Markdown

For #40. Folio as the file chooser other apps show: an xdg-desktop-portal FileChooser backend, so Open and Save dialogs are a Folio window rather than the GTK one.

What it is

  • folio chooser serves org.freedesktop.impl.portal.FileChooser. D-Bus starts it for the first dialog and it goes a minute after the last one closes, so nothing runs while no one is using it.
  • A dialog is the ordinary window (Main.qml: same views, sidebar, previews, keys) with a bar along the bottom, ChooserBar.qml: the name to save under, the app's filters and choices, Cancel and the answer.
  • OpenFile (files or folders, one or several), SaveFile and SaveFiles. Filters, the filter to start on, choices, the folder and name to start from, the app's own word for the button.
  • Return or a double click on a file answers; Ctrl+Return answers from anywhere; Escape calls it off once nothing else wants it (a search, a selection, Rename).
  • A save starts with the keyboard in the name. Ctrl+L still goes to type a path from there (the second commit), which is how to reach a server or anywhere the sidebar doesn't list.
  • A save over a file that's there asks "Replace?" first.
  • Opt-in: nothing changes until portals.conf names folio for the FileChooser (README has the three lines).

How it fits

  • FolderModel gets a filter (name patterns and MIME types). Files that fit none aren't listed; folders always are. An ordinary window has no filter and lists as before.
  • Windows::open() takes an optional Chooser; the window's context carries it as chooser, null everywhere else. Backend::open() on a file hands it to the chooser rather than starting an app.
  • share/ gains folio.portal, the D-Bus service file and a hidden folio-chooser.desktop. Windows have the app id folio-chooser, for a rule to float them.

Checked

  • Installed on an Omarchy machine as its chooser, from a package built off this branch.
  • bin/test: 201 pass, seven of them new (filter, reading a request, working out an answer, answering once, the bar).
  • Real apps in a headless session, through the real xdg-desktop-portal with Folio started by D-Bus: GTK 4 (GtkFileDialog open, open several, save, folder), zenity, a Qt Quick FileDialog through Qt's portal theme, an upload from a page in Chromium, and an upload and a download's Save As in Firefox.
  • A whole-window run of Main.qml under ASan/UBSan, by mouse and keyboard, in all three views: double click, shift/alt click and the button, the filter and choice menus, typing a name (Q, S, / and ? are typed, not acted on), Replace, two dialogs at once, a split pane, right to left, 30,000 files.
  • Malformed calls straight on the bus (wrong shapes for filters and choices, names that are paths or .., a handle used twice): each gets an answer or an error and the service stays up.
  • Names an app supplies (current_name, SaveFiles' files) are taken as bare names; only the user can type a path.

Not here

  • The package: omarchy-pkgs' PKGBUILD would install share/folio.portal to /usr/share/xdg-desktop-portal/portals/, the service file to /usr/share/dbus-1/services/ and folio-chooser.desktop to /usr/share/applications/. Happy to send that once this shape is settled.
  • Making it the default (a line in Omarchy's hyprland-portals.conf, and a window rule for folio-chooser).
  • Dialogs don't attach to the window that asked (xdg-foreign), so they float on their own.
  • The new strings are in en.json only, for the translation sync.
  • Not tried: a Flatpak app, a save onto an SFTP mount.

One thing found on the way that isn't this PR's: FolderColumns.qml calls backend.showItem(path) for a click on a file in an ancestor column, but Backend::showItem isn't Q_INVOKABLE, so the click logs a TypeError and does nothing. I can send that separately.

A file chooser for xdg-desktop-portal (org.freedesktop.impl.portal.FileChooser),
so other apps' Open and Save dialogs can be Folio: the same window, with a bar
along the bottom for the name to save under, the app's filters and choices,
and the answer.

folio chooser is started by D-Bus for the first dialog and goes a minute after
the last. OpenFile (files or folders, one or several), SaveFile and SaveFiles
are answered, with filters, choices, the folder and name to start from, a
Replace prompt before a save writes over a file, and the portal closing a
request. Opt-in through portals.conf (see the README).
A Save dialog starts with the keyboard in the name, where the keys for the
files stand aside. Going to type a path is the one that shouldn't: it's how
to get to a server, or anywhere the sidebar doesn't list.
@28allday
28allday marked this pull request as ready for review November 11, 2026 20:10
@greptile-apps

greptile-apps Bot commented Nov 11, 2026 •

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 5/5

[Medium impact] The PR appears safe to merge; no actionable issues remain from this review.

Summary

Adds folio chooser for other apps’ Open and Save dialogs, using Folio’s existing window and file views.

  • Other apps can ask Folio to open or save files through the desktop portal.
  • Folio’s file window becomes the open or save dialog an app requested.
  • File lists follow the filter selected by the app.

Diagram

sequenceDiagram
    participant App
    participant Portal as xdg-desktop-portal
    participant Folio as folio chooser
    participant User
    App->>Portal: Ask to open or save
    Portal->>Folio: FileChooser request
    Folio->>User: Show window and choices
    User->>Folio: Choose files or cancel
    Folio-->>Portal: Answer with paths and choices
    Portal-->>App: Return answer
Loading

Reviews (9) · Last reviewed commit: "File chooser: a filter of types looks in..." · Reviewed by Greptile

Comment thread src/chooser.cpp Resolved
Comment thread src/foldermodel.cpp Resolved
Comment thread src/foldermodel.cpp Resolved
Comment thread src/chooser.cpp Resolved
Comment thread src/chooser.cpp Resolved
Comment thread src/chooser.cpp Resolved
… five smaller fixes

From the review of the pull request:

- Picking a folder (or saving files into one), nothing inside is selected on
  arrival, so Choose is the folder shown rather than its first subfolder.
- A filter's patterns match a file's own name in search results too, which
  are named by their path from the folder searched.
- A search counts only what the filter lists toward its limit.
- A choice the app leaves unset starts on its first option, a checkbox
  unticked.
- Files saved into a folder never answer with a folder of the same name: the
  file goes beside it under the next free name.
- Only the caller that asked for a dialog can close it.
Comment thread src/chooser.cpp Resolved
…aved files never share a name

From the second review pass:

- Picking a folder, coming back up out of one no longer selects it, and a
  split's right pane starts with nothing selected too, so Choose is always
  the folder shown until one is picked. The pane does this itself as each
  folder arrives, rather than the bar watching one pane.
- Files saved into a folder that are moved aside from a folder of their name
  take a name no other file in the same save has.
Comment thread src/FolderPane.qml Resolved
From the third review pass: selecting nothing on arrival also dropped what
file work had just made in the folder being shown. Only arriving somewhere
else clears the selection now, so a new folder is still the one chosen.
@dhh

dhh commented Nov 11, 2026

Copy link
Copy Markdown
Contributor

Please do add some screenshots/screenrecordings!

@28allday

28allday commented Nov 11, 2026 •

Copy link
Copy Markdown
Author

Here you go.

On a real site (squoosh.app in Chromium): its picker opens Folio, a photo is picked from the grid, the site compresses it, and the download saves over the original through Folio's Save dialog, with the Replace prompt. (mp4)

A photo picked for squoosh.app through Folio, then saved over the original

The rest of it, from a test page: the sidebar, the grid, the filter menu, picking several, a typed name and Replace, and choosing a folder. (mp4)

Opening several photos, saving over a file and choosing a folder through Folio

Open, three picked, with the app's filter in the bar:

Open Files in the grid with three photos selected

Save As, with the app's filter and its own choice (Encoding) beside the name:

Save As in the columns view

Replace, before a save writes over a file:

The Replace prompt

Choose a Folder: the bar says which folder Choose means, the one picked or else the one shown (nothing is picked on arrival).

Choosing a folder

These are from a headless session, so each window fills the screen; on a desktop the dialog is its own window (app id folio-chooser, for a rule to float it).

… settled

main now hides the window's contents until Hyprland has sized it, and gives
the keyboard to the files when they appear. A Save dialog gives it to the
name instead, at the same moment.
Comment thread src/chooser.cpp Resolved
28allday and others added 3 commits November 11, 2026 22:16
…ps the space

From the review: the name typed is trimmed, which is right for a stray space
but saved a file like " report.txt", opened in the window to write over it,
under another name. A file that exists exactly as the field has it is that
file.
A name is saved as it's typed, for a new file as for one that's there:
spaces at the ends are part of a file's name. The path bar still trims
what it's given.
…eld up

xdg-desktop-portal makes a proxy for every backend portals.conf names as it starts, which D-Bus-activates folio chooser and waits for it to take its name. Omarchy sets QT_QPA_PLATFORMTHEME=qtengine, and that plugin reads org.freedesktop.portal.Settings while QGuiApplication is built, before the name is taken. The portal can't answer until it has finished starting, so both wait until Qt's 25 second timeout lets the chooser go on. Every portal start took 25 seconds instead of 50 ms, holding up every app waiting on any portal, and when Qt's timeout landed after the portal's own, the portal gave up on Folio and left FileChooser out for the whole session.

Qt's generic theme reads nothing from the portal. The chooser still looks like Folio, which sets its own font, Material style and colours; what it gives up is the desktop's icon theme for its window icon and any input timings a theme plugin supplies, such as the double-click interval. With it, the portal starts in about 110 ms.

Also drops the older of two stacked versions of one comment in ChooserPortal::ask.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@omarchybot

Copy link
Copy Markdown
Contributor

Reviewed at 42c13e1, plus one fix pushed on top as dca6263.

What was checked. The whole diff, read against main and the code around it: every caller of what changed (Windows::open, Backend::expandTyped, FolderModel::search, the pane's typing), and the names the change puts into QML scope. Ordinary windows behave as before, because with no bar ownTyping is typing. Greptile's nine inline findings are all answered at this head.

On a disposable Omarchy VM (Qt 6.12, xdg-desktop-portal 1.22.1), not on a machine with credentials:

  • bin/test: 201 passed under en_US.UTF-8. Under a bare POSIX locale, folderListsDirectoriesFirstWithoutHidden and placesListEveryFolderInHome fail, and they fail the same way on main, so they aren't this pull request's.
  • The build installed as the FileChooser backend through portals.conf, then called through the real portal frontend, as an app would:
Call Keys What the app got
SaveFile with a filter and two choices Return 0, new.txt, the filter, choices defaulted to utf8 and false
SaveFile over an existing file Return, Return (Replace) 0, existing.txt
OpenFile Escape 1
SaveFiles dir.txt (a folder exists) and dir (2).txt Ctrl+Return 0, dir (3).txt and dir (2).txt
OpenFile for a folder Ctrl+Return 0, the folder shown
SaveFile, keyboard in the name Escape 1

What was found, and fixed in dca6263. With Folio named as the FileChooser, every start of xdg-desktop-portal took 25 seconds, against 50 ms without it. The portal makes a proxy for every backend as it starts, which D-Bus-activates folio chooser and waits for it to take its name. Omarchy sets QT_QPA_PLATFORMTHEME=qtengine, and that plugin calls org.freedesktop.portal.Settings.ReadAll while QGuiApplication is being built, before the chooser has its name. The portal can't answer that until it has finished starting, so the two wait on each other until Qt's 25-second timeout releases the chooser. A bus capture shows the call at +0.06 s and its timeout at +24.93 s. The stall holds up every app waiting on any portal at login, not only file dialogs. Once, the portal's own timeout fired first, and FileChooser was missing for the rest of the session. So "D-Bus starts it for the first dialog" isn't quite what happens: the portal starts it at login.

The fix starts the chooser on Qt's generic platform theme, which reads nothing from the portal. The chooser still looks like Folio, which sets its own font, Material style and colours; what it gives up is the desktop's icon theme for its window icon and any input timings a theme plugin supplies, such as the double-click interval. Portal start is now 106 to 131 ms over three clean restarts, the suite still passes 201, and every call in the table returns the same result. The same commit drops the older of two stacked copies of one comment in ChooserPortal::ask.

Second opinion. Codex (GPT-6, medium effort) and muse-wonder (thinking high) reviewed the diff independently, reading only. Neither found the startup stall; the VM run did. Codex also read the fix: it found no blocking defect, confirmed Qt's generic theme makes no synchronous portal call, and noted that a hand-typed folio -platform wayland chooser would skip the override, which the D-Bus activation line never does. Neither found a crash, a double reply or a path escape on malformed bus input. Both independently raised one thing worth weighing:

  • FolderModel::fits sniffs the content of a file whose name gives no type, again for each exact-MIME rule (the result isn't remembered when it comes back application/octet-stream). The post-filter in reload() doesn't check overtaken(), and search results are filtered twice. That only costs time, and only in a dialog with a MIME filter, but a large folder of extensionless files or an SFTP mount would feel it.

Other points from the review that hold up but are small: a failed window or a duplicate handle answers with a D-Bus error rather than response 2; the service watcher is armed only after the window opens; Trash is accepted as a place to save into; current_file in a folder that's gone saves under that name in the last folder used. The tests never go through D-Bus itself, which the VM run above covers for now.

Two claims didn't hold: that the backend must expose a version property (the impl FileChooser interface defines none, and the folder pick above went through), and that a folder made while picking a folder loses its selection (letGo runs only on arriving somewhere else).

Open, for the maintainer. writable is true for every answer, OpenFile included. The spec's default is false. KDE's backend also returns true, and GTK's defaults to true behind an "Open files read-only" checkbox. Whether Folio offers that choice is a decision about the feature, not a defect. Also the maintainer's: the packaging, making it Omarchy's default, and xdg-foreign.

🤖 Generated by Opus 5.5 in Claude Code. Reviewed by Codex Medium and Muse-Wonder High.

A file whose name says nothing of its type was read again for each type
a filter names, and what a search found was filtered a second time.
It's read once now, a search's results are left as the search filtered
them, and the filtering stops once a newer listing has replaced the one
being read.
@greptile-apps

greptile-apps Bot commented Nov 12, 2026

Copy link
Copy Markdown

Want your agent to iterate on Greptile's feedback? Try greploops.

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.

3 participants