1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
//! The single "is `~/.config/edamame` in play at all?" gate.
//!
//! `--no-config` promises a session that neither reads nor writes
//! `~/.config/edamame`. Both halves are one fact about this invocation,
//! so both come from **one** flag that every read and write site asks,
//! rather than a value threaded through the data.
//!
//! It lives in a process-global `AtomicBool` because that is what the
//! fact actually is: a property of *this invocation*, decided once from
//! the command line before the first config file is touched, and true
//! for every site for the rest of the process. An earlier design
//! carried it as a `Config` field, which failed the moment
//! `App::open_config_in_editor` replaced `self.config` with a freshly
//! deserialized one — the flag reverted to its serde default and the
//! guarantee silently lapsed mid-session. A global cannot be
//! overwritten by a reload.
//!
//! **Writes.** Four sites write into the config directory, and all four
//! ask [`config_writes_allowed`]:
//!
//! - [`Config::save`](super::config::Config::save) — `config.toml`
//! - the keybinds overlay's `[ Save ]` — `keybindings.toml`
//! - the export-theme modal — `themes/<name>.toml`
//! - `App::open_config_in_editor`, which both seeds `config.toml` and
//! reloads from it
//!
//! `Config::ensure_default_files` is the one exception, and only because
//! `main` never calls it under `--no-config` in the first place.
//!
//! **Reads.** Skipping the startup load is *not* enough on its own: the
//! config directory is read again mid-session by surfaces that enumerate
//! what the user has dropped into it, and those run long after `main` has
//! made its branch. Three sites ask [`config_reads_allowed`]:
//!
//! - [`list_theme_names`](super::theme::list_theme_names) — the theme
//! picker, the settings overlay's theme cycle, and the export-theme
//! source list all build from it, so a `--no-config` run would
//! otherwise still offer (and load) `themes/*.toml`
//! - [`list_export_stylesheets`](super::readers::list_export_stylesheets)
//! — the HTML-export stylesheet list
//! - `read_theme_named`, the load path behind those names
//!
//! A new reader or writer owes the matching check.
use ;
/// Whether `~/.config/edamame` participates in this run at all. Starts
/// `true`; only [`disable_config_dir`] ever clears it, and nothing sets
/// it back — `--no-config` is a property of the whole invocation.
static CONFIG_DIR_IN_USE: AtomicBool = new;
/// What a "saved" message says instead when the write was suppressed.
///
/// The setting *is* live for the session — only the disk write was
/// skipped — so the wording says exactly that. A single const because
/// three different flashes need to say it identically.
pub const NOT_PERSISTED_NOTE: &str = " (not saved: --no-config)";
/// Take `~/.config/edamame` out of play for the rest of the process, in
/// both directions.
///
/// Called once from `main` when `--no-config` is present, before any
/// config file is read or written. There is deliberately no way to
/// re-enable: a mid-session reversal is the bug this design exists to
/// prevent.
/// The write half of the gate: may the caller write into
/// `~/.config/edamame`?
///
/// `Relaxed` is sufficient — the value is written once at startup on the
/// main thread, long before any other thread exists, and every later
/// access is a read.
/// The read half of the gate: may the caller read from
/// `~/.config/edamame`?
///
/// Separate from [`config_writes_allowed`] for the call site's sake —
/// the two read the same flag, and always will, but a reader guarded by
/// a function with "writes" in its name reads as a mistake.
/// Suffix for a flash that reports a settings change: empty on an
/// ordinary run, [`NOT_PERSISTED_NOTE`] when writes are suppressed.
///
/// Callers phrase the sentence so that both readings are true — the
/// change took effect either way, and only the persistence differs.
/// Test-only scoped suppression.
///
/// The gate is process-global, so a test that flips it must exclude
/// every other test that could touch config *or* read the gate — and
/// that exclusion has to outlast the guard itself, because the usual
/// shape of these tests is "assert nothing was written, then drop the
/// guard and assert the same call *does* write". The second half runs
/// with the gate back at `true` and would fail if another test's guard
/// were live at that moment.
///
/// So the serialization is [`crate::test_env::env_lock`], the crate-wide
/// lock those tests already hold for the whole test body (they point
/// `XDG_CONFIG_HOME` at a tempdir to prove nothing was written), rather
/// than a second mutex of this module's own. One lock also means there
/// is no lock *order* to get wrong.
///
/// **Callers must hold `env_lock` for the whole test**, not just across
/// the guard's lifetime.
pub ;