Skip to main content

max / alloy

Show dotfiles in the GTK file chooser, through a dconf default yazi shows hidden files and helix's picker shows them; the GTK chooser hid them and waited to be told Ctrl-H. A GTK schema default has no file form a user could edit instead, so this is a keyfile or it is nothing - which makes a compiled binary database a second kind of configuration in a tree whose story was files a person can read. Narrow on purpose: one key, both schemas, and wiki alloy-packaging-policy says what may follow it in and what may not. The profile reads the user database above the system one, so a person who sets it the other way wins and keeps winning. That is why this is a system default rather than something alloy-session writes into the user's own dconf. The build compiles the database and then reads the key back through the profile. dconf update exits 0 having compiled nothing, so anything short of a real read leaves a defaults mechanism that can quietly stop applying while still reporting success.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-08-28 17:59 UTC
Commit: 31260754cc91b8e994018d05a6001c19cbeb78ba
Parent: 40de3a0
4 files changed, +191 insertions, -0 deletions
@@ -1581,6 +1581,15 @@
1581 1581 # Cursor, GTK theme
1582 1582 bibata-cursor-theme \
1583 1583 adw-gtk3-theme \
1584 + # dconf, named rather than left to a transitive pull. It is what
1585 + # compiles and reads /etc/dconf/db/local, which is where Alloy's
1586 + # GTK schema defaults live (the file chooser's hidden files, so
1587 + # far). GTK reads that database through gsettings whether or not
1588 + # the CLI is present, but `dconf update` at build time and the
1589 + # assertion after it both need the binary, and a defaults
1590 + # mechanism that depends on someone else's dependency graph is
1591 + # one release from silently not applying.
1592 + dconf \
1584 1593 # Screenshot capture + region-select (sway has no built-in grab)
1585 1594 grim slurp \
1586 1595 # The browser is not a line here any more: it is the `BROWSER` ARG
@@ -2316,6 +2325,46 @@
2316 2325 COPY usr/ /usr/
2317 2326 COPY --from=rust-build /staged-skel/ /
2318 2327
2328 + # =====================================================================
2329 + # dconf: the GTK schema defaults, compiled and proven to answer.
2330 + # =====================================================================
2331 + # Alloy's configuration story is files in etc/skel a person can read and edit.
2332 + # A compiled binary database is a second kind of thing, and this is the whole
2333 + # of it: GTK/GNOME schema defaults have no file form a user could edit
2334 + # instead, so a machine-wide default has to be a dconf keyfile or it cannot
2335 + # exist. What it holds is argued in `etc/dconf/db/local.d/`; wiki
2336 + # `alloy-packaging-policy` says what may go in and what may not.
2337 + #
2338 + # The profile puts `user-db:user` above `system-db:local`, so a person who
2339 + # sets a key the other way wins and stays winning. That is why a system
2340 + # default is the correct shape here and why writing into the user's own dconf
2341 + # from `alloy-session` was refused: this can be overridden, that would
2342 + # override.
2343 + #
2344 + # THE ASSERTION IS THE POINT. `dconf update` exits 0 when it compiles nothing,
2345 + # so a keyfile in the wrong directory, a bad group name, or a missing profile
2346 + # all fail silently and leave a defaults mechanism that quietly stopped
2347 + # applying. So the check is a real read through the profile rather than a test
2348 + # for the file: `dconf read` consults the databases directly and needs no
2349 + # session bus, which is what makes it available at build time.
2350 + RUN set -eu; \
2351 + if [ "$PROFILE" = client ]; then \
2352 + command -v dconf >/dev/null \
2353 + || { echo "dconf is not in the image; /etc/dconf/db/local cannot be compiled and the GTK defaults would be inert" >&2; exit 1; }; \
2354 + test -f /etc/dconf/profile/user \
2355 + || { echo "/etc/dconf/profile/user did not land; with no profile dconf reads the user database alone and db/local is never consulted" >&2; exit 1; }; \
2356 + dconf update; \
2357 + test -s /etc/dconf/db/local \
2358 + || { echo "dconf update compiled nothing; db/local.d is empty or unreadable" >&2; exit 1; }; \
2359 + for key in /org/gtk/settings/file-chooser/show-hidden /org/gtk/gtk4/settings/file-chooser/show-hidden; do \
2360 + [ "$(dconf read "$key")" = true ] \
2361 + || { echo "$key does not read true out of the compiled database; the GTK file chooser would hide dotfiles" >&2; exit 1; }; \
2362 + done; \
2363 + echo "dconf: $(ls /etc/dconf/db/local.d | wc -l) keyfile(s) compiled, GTK3 and GTK4 file choosers show dotfiles"; \
2364 + else \
2365 + echo "dconf: keyfiles ride along inert on a profile with no session"; \
2366 + fi
2367 +
2319 2368 # =====================================================================
2320 2369 # What has to reach a machine that UPGRADED into this image, not only
2321 2370 # one installed from it: the /var declarations and the greeter account.
@@ -1,0 +1,117 @@
1 + //! Alloy's one compiled-database default, asserted from the tree.
2 + //!
3 + //! Ruled 2026-08-27 (GoingsOn alloy `533d8165`): Alloy adopts dconf as a
4 + //! defaults mechanism, narrowly. The GTK file chooser hid dotfiles while yazi
5 + //! and helix's picker showed them, and a GTK schema default has no file form a
6 + //! user could edit instead, so a keyfile is the only shape the fix has.
7 + //!
8 + //! Three parts have to agree or the mechanism is inert, and the failure is
9 + //! silent in every direction: `dconf update` exits 0 having compiled nothing,
10 + //! and GTK falls back to upstream's value with nothing on screen to say so.
11 + //!
12 + //! - The profile, or dconf reads the user database alone and never consults
13 + //! `db/local`.
14 + //! - The keyfile, under `db/local.d/`, in the directory the profile's
15 + //! `system-db:local` names.
16 + //! - The Containerfile's `dconf update` and the read that proves it took.
17 + //!
18 + //! Read rather than executed: compiling the database here would assert this
19 + //! machine's dconf, and the build does the real check against the image's.
20 +
21 + use std::path::PathBuf;
22 +
23 + fn tree(relative: &str) -> String {
24 + let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
25 + .join("../..")
26 + .join(relative);
27 + std::fs::read_to_string(&path)
28 + .unwrap_or_else(|error| panic!("reading {}: {error}", path.display()))
29 + }
30 +
31 + fn lines(relative: &str) -> Vec<String> {
32 + tree(relative)
33 + .lines()
34 + .map(str::trim)
35 + .filter(|line| !line.is_empty() && !line.starts_with('#'))
36 + .map(str::to_string)
37 + .collect()
38 + }
39 +
40 + /// The user database is read first, so a person's own setting wins.
41 + ///
42 + /// The order is the whole difference between a default and a policy. Reversed,
43 + /// Alloy would be overriding a user who chose the other way, which
44 + /// `alloy-byo-principle` forbids and which is why setting this from
45 + /// `alloy-session` was refused.
46 + #[test]
47 + fn the_profile_puts_the_user_above_the_system_default() {
48 + assert_eq!(
49 + lines("etc/dconf/profile/user"),
50 + ["user-db:user", "system-db:local"]
51 + );
52 + }
53 +
54 + /// Both schemas, because GTK3 and GTK4 keep separate ones.
55 + ///
56 + /// An app is covered by the version it links, so declaring one and not the
57 + /// other leaves half the file choosers on upstream's value — the same
58 + /// inconsistency this closed, one layer down.
59 + #[test]
60 + fn both_gtk_file_choosers_show_hidden_files() {
61 + let keyfile = lines("etc/dconf/db/local.d/00-alloy-file-chooser");
62 + assert_eq!(
63 + keyfile,
64 + [
65 + "[org/gtk/settings/file-chooser]",
66 + "show-hidden=true",
67 + "[org/gtk/gtk4/settings/file-chooser]",
68 + "show-hidden=true",
69 + ]
70 + );
71 + }
72 +
73 + /// The keyfile lives where the profile says to look.
74 + ///
75 + /// `system-db:local` means `/etc/dconf/db/local`, compiled from
76 + /// `/etc/dconf/db/local.d/`. A keyfile one directory out compiles into
77 + /// nothing and reports success.
78 + #[test]
79 + fn the_keyfile_sits_in_the_directory_the_profile_names() {
80 + let profile = lines("etc/dconf/profile/user");
81 + let db = profile
82 + .iter()
83 + .find_map(|line| line.strip_prefix("system-db:"))
84 + .expect("the profile names a system database");
85 + let dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
86 + .join("../..")
87 + .join("etc/dconf/db")
88 + .join(format!("{db}.d"));
89 + let entries: Vec<_> = std::fs::read_dir(&dir)
90 + .unwrap_or_else(|error| panic!("reading {}: {error}", dir.display()))
91 + .map(|entry| entry.expect("a readable entry").file_name())
92 + .collect();
93 + assert!(!entries.is_empty(), "{} is empty", dir.display());
94 + }
95 +
96 + /// The build compiles the database and proves it answers.
97 + ///
98 + /// `dconf update` succeeding is not evidence: it exits 0 having compiled
99 + /// nothing at all. Only a read through the profile distinguishes a database
100 + /// that carries the key from one that was never written.
101 + #[test]
102 + fn the_containerfile_compiles_the_database_and_reads_the_key_back() {
103 + let containerfile = tree("Containerfile");
104 + assert!(
105 + containerfile.contains("dconf update"),
106 + "the build never compiles the database"
107 + );
108 + assert!(
109 + containerfile.contains(r#"dconf read "$key""#),
110 + "the build does not read the key back, so a silent no-op would pass"
111 + );
112 + assert!(
113 + containerfile.contains("/org/gtk/settings/file-chooser/show-hidden")
114 + && containerfile.contains("/org/gtk/gtk4/settings/file-chooser/show-hidden"),
115 + "the build checks only one of the two schemas"
116 + );
117 + }
@@ -1,0 +1,14 @@
1 + # The GTK file chooser shows dotfiles.
2 + #
3 + # The rest of Alloy already does: yazi shows hidden files, and helix's picker
4 + # shows them. The GTK chooser was the one place a developer workstation hid
5 + # them and waited to be told Ctrl-H, which is the inconsistency this closes.
6 + #
7 + # Both schemas, because GTK3 and GTK4 keep separate ones and an app is only
8 + # covered by the version it links.
9 +
10 + [org/gtk/settings/file-chooser]
11 + show-hidden=true
12 +
13 + [org/gtk/gtk4/settings/file-chooser]
14 + show-hidden=true
@@ -1,0 +1,11 @@
1 + # Alloy's dconf profile.
2 + #
3 + # Two lines, and the order is the whole point: the user's own database is read
4 + # first, so anything a person sets wins over anything Alloy declares. The
5 + # system database under it is a default, not a policy — `system-db` rather
6 + # than a lockdown entry, so nothing here is enforced.
7 + #
8 + # Fedora ships no /etc/dconf/profile/user, and with no profile dconf reads the
9 + # user database alone. So this file is what makes db/local.d get read at all.
10 + user-db:user
11 + system-db:local