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
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
//! Bombadil's iced front end.
//!
//! A library with one public item, [`run`]. The binary users install is the
//! `bombadil` crate, which exists to be a name worth typing and does nothing
//! but call it: `cargo install` only installs binaries defined *in* the
//! package being installed, never in its dependencies, so a meta crate has to
//! carry its own `main` and something to put in it.
//!
//! # Conventions these modules follow
//!
//! - `view` is deliberately dumb and untested. Logic worth a test lives in a
//! free function that `view` calls.
//! - Runners, secret stores, config persisters and filesystem probes are
//! **parameters**, never constructed inline, so tests stay hermetic.
//! - Everything that spawns a process or blocks on I/O goes through
//! [`job::run`], never `update` directly.
//! - A secret must never enter a `Message`, which derives `Debug`.
//!
//! ## Never compare a `display()`ed path against a string literal
//!
//! `PathBuf::join` uses `\` on Windows, so `"/p/api/.venv"` never equals a
//! joined `/p/api\.venv`. Derive the expected value through the same function
//! production uses, or compare `Path`s — `Path` comparison is component-based
//! and accepts either separator.
//!
//! Two CI failures on this project came from a platform assumption hiding in an
//! assertion rather than in the code, invisible on Linux and macOS both times.
use Config;
use ;
use ;
use Arc;
/// The icon's raw pixels, 64x64 sRGB RGBA.
///
/// Raw rather than a PNG because `iced::window::icon::from_rgba` wants exactly
/// this, and decoding a PNG for it would mean pulling an image decoder into a
/// binary that otherwise renders every pixel itself. `just icon` regenerates
/// it, and every PNG in the repo's own `assets/`, from `assets/icon.svg` --
/// the file to edit; this one is a build product that happens to be committed.
///
/// Inside this crate rather than beside the SVG it comes from, because
/// `cargo package` only takes files under the crate root: an
/// `include_bytes!("../../../assets/...")` packages cleanly and then fails to
/// compile for whoever installs it. The repo's `assets/` keeps the sources and
/// the PNGs that only the repo and the desktop entry need.
const ICON_RGBA: & = include_bytes!;
const ICON_SIDE: u32 = 64;
/// The window icon, or `None` if the bytes will not make one.
///
/// A missing icon is not worth refusing to start over: the window manager
/// falls back to its own placeholder and everything else works. `expect` here
/// would turn a bad asset into an application that cannot open at all.
/// The name the desktop expects this application to answer to: the basename of
/// `assets/bombadil.desktop`, the `Icon=` key inside it, and the file installed
/// into the hicolor icon theme all have to be this same word.
///
/// `cfg`-gated because its only reader is, and a constant that exists on a
/// platform nothing reads it from is dead code -- which this repo denies. The
/// same class of mistake as comparing a `display()`ed path to a literal:
/// invisible on the machine it was written on, a CI failure on the other two.
const APP_ID: &str = "bombadil";
/// The window, with an icon set two different ways because desktops disagree
/// about where an icon comes from.
///
/// `icon` is the one a window carries itself. X11, Windows and macOS use it.
///
/// **GNOME on Wayland ignores it entirely.** There is no protocol request for
/// "here is my icon" that it honours; the shell resolves a window's icon by
/// matching its `app_id` against the basename of an installed `.desktop` file
/// and reading that file's `Icon=` key. So a Wayland session shows the
/// generic placeholder no matter what bytes this binary holds, until
/// `just install-desktop` puts the entry and the themed PNGs in place. Setting
/// `application_id` is the half that has to live in the binary; the other half
/// cannot.
/// The tray's command channel, parked where a plain `fn` can reach it.
///
/// `Subscription::run_with` takes a function *pointer*, not a closure, so the
/// receiver cannot be captured and has to live somewhere global. `Option`
/// because a `Receiver` can only be consumed once: the first subscription
/// takes it, and any later one gets an empty stream rather than a panic --
/// a tray that stops working beats an application that stops running.
static TRAY_COMMANDS: OnceLock = new;
/// Turns the tray's commands into application messages.
///
/// A `std::sync::mpsc::Receiver` is not a `Stream`, and blocking on one would
/// stall the executor -- the same problem `job::stream` already has, solved
/// the same way: a thread blocks on the receiver and forwards into a channel
/// iced can await.
/// Runs the application. Returns when the last window closes.
///
/// Everything this needs -- the config, the persister, the runner, the tray --
/// is built in here rather than taken as a parameter, because there is exactly
/// one caller and it is a `main` that should hold no opinions.
/// Loads the persisted config, starting from defaults rather than refusing to
/// open when loading does not go cleanly, and returns the store to save
/// through -- both come from opening the same file, so they belong together.
///
/// Returns the whole `Loaded`, not just its `Config`: `Unreadable` and
/// `ReadOnly` both carry defaults, so a caller that keeps only the config
/// cannot tell a corrupt file from an empty one and neither can the user.
/// `app::boot` turns the variant into a banner.
///
/// `Store::open` never returns an error: a missing config file already loads
/// as `Loaded::Fresh(Config::default())` and a corrupt one already loads as
/// `Loaded::Unreadable`, both carrying usable defaults. The only genuine
/// failure at this stage is not being able to locate a config directory at
/// all (no resolvable home directory), reported below as `Unreadable` --
/// which is what it is, from the user's point of view, and gets the same
/// banner rather than the same silence. There is no file to open a `Store`
/// on in that case, so confirming the add-project dialog gets
/// `app::NullPersist` instead, which reports the same failure again rather
/// than pretending to have saved.