| 26 |
26 |
|
//! | lang | `localectl set-locale` | `locale1.set-locale` |
|
| 27 |
27 |
|
//! | keymap | `localectl set-keymap` | `locale1.set-keyboard` |
|
| 28 |
28 |
|
//!
|
|
29 |
+ |
//! The theme row is the exception that proves the shape: it writes
|
|
30 |
+ |
//! `$XDG_CONFIG_HOME/alloy/console.toml` rather than running anything, because
|
|
31 |
+ |
//! which theme a terminal renders in is the person's business and not the
|
|
32 |
+ |
//! machine's. It needs no privilege and grants none, and the console re-themes
|
|
33 |
+ |
//! the moment it is written.
|
|
34 |
+ |
//!
|
| 29 |
35 |
|
//! Every one is in the five `usr/share/polkit-1/rules.d/50-alloy-settings.rules`
|
| 30 |
36 |
|
//! grants, so none prompts, and nothing here can reach an action deliberately
|
| 31 |
37 |
|
//! left prompting. Two consequences that look like quirks and are not:
|
| 69 |
75 |
|
const HOSTNAME: &str = "hostname.static";
|
| 70 |
76 |
|
const LOCALE: &str = "locale.lang";
|
| 71 |
77 |
|
const KEYMAP: &str = "locale.keymap";
|
|
78 |
+ |
/// The one row that writes a file rather than running a command.
|
|
79 |
+ |
pub(crate) const THEME: &str = "appearance.theme";
|
| 72 |
80 |
|
|
| 73 |
81 |
|
/// Live system settings, fronted by `timedatectl`, `hostnamectl`, `localectl`.
|
| 74 |
82 |
|
pub(crate) struct SystemBind {
|
| 124 |
132 |
|
|
| 125 |
133 |
|
let mut bind = Self {
|
| 126 |
134 |
|
sections: sections(),
|
| 127 |
|
- |
fields: rows(zones, locales, keymaps),
|
|
135 |
+ |
fields: rows(zones, locales, keymaps, theme_choices()),
|
| 128 |
136 |
|
time,
|
| 129 |
137 |
|
host: read_host(log),
|
| 130 |
138 |
|
locale: read_locale(log),
|
| 213 |
221 |
|
)
|
| 214 |
222 |
|
};
|
| 215 |
223 |
|
set(self.field_mut(KEYMAP), note, closed);
|
|
224 |
+ |
|
|
225 |
+ |
// Whether the console is rendering what someone chose or what it
|
|
226 |
+ |
// guessed from the terminal background is not visible anywhere else,
|
|
227 |
+ |
// and it is the difference between a setting and an accident.
|
|
228 |
+ |
let (note, closed) = if self.choices(THEME).is_empty() {
|
|
229 |
+ |
("No themes found on the search path.", true)
|
|
230 |
+ |
} else if crate::theme::remembered().is_some() {
|
|
231 |
+ |
("How the console renders. Yours, remembered.", false)
|
|
232 |
+ |
} else {
|
|
233 |
+ |
(
|
|
234 |
+ |
"How the console renders. Guessed from the terminal; pick one to keep it.",
|
|
235 |
+ |
false,
|
|
236 |
+ |
)
|
|
237 |
+ |
};
|
|
238 |
+ |
set(self.field_mut(THEME), note, closed);
|
| 216 |
239 |
|
}
|
| 217 |
240 |
|
}
|
| 218 |
241 |
|
|
| 250 |
273 |
|
path: "locale".to_string(),
|
| 251 |
274 |
|
description: Some("Language and keyboard.".to_string()),
|
| 252 |
275 |
|
},
|
|
276 |
+ |
Section {
|
|
277 |
+ |
path: "appearance".to_string(),
|
|
278 |
+ |
description: Some("How the console renders.".to_string()),
|
|
279 |
+ |
},
|
| 253 |
280 |
|
]
|
| 254 |
281 |
|
}
|
| 255 |
282 |
|
|
| 256 |
283 |
|
/// Every row. Help lines and read-only flags are [`SystemBind::annotate`]'s.
|
| 257 |
|
- |
fn rows(zones: Vec<EnumValue>, locales: Vec<EnumValue>, keymaps: Vec<EnumValue>) -> Vec<Field> {
|
|
284 |
+ |
fn rows(
|
|
285 |
+ |
zones: Vec<EnumValue>,
|
|
286 |
+ |
locales: Vec<EnumValue>,
|
|
287 |
+ |
keymaps: Vec<EnumValue>,
|
|
288 |
+ |
themes: Vec<EnumValue>,
|
|
289 |
+ |
) -> Vec<Field> {
|
| 258 |
290 |
|
let enumerated = |path: &str, values: Vec<EnumValue>| Field {
|
| 259 |
291 |
|
path: path.to_string(),
|
| 260 |
292 |
|
description: None,
|
| 290 |
322 |
|
text(HOSTNAME, false),
|
| 291 |
323 |
|
enumerated(LOCALE, locales),
|
| 292 |
324 |
|
enumerated(KEYMAP, keymaps),
|
|
325 |
+ |
enumerated(THEME, themes),
|
| 293 |
326 |
|
]
|
| 294 |
327 |
|
}
|
| 295 |
328 |
|
|
|
329 |
+ |
/// The themes makeover can find, as choices.
|
|
330 |
+ |
///
|
|
331 |
+ |
/// The id is what is written and the name is what is shown, which is the one
|
|
332 |
+ |
/// place on this screen where those differ: `akari-night` is the file and
|
|
333 |
+ |
/// "Akari Night" is the thing a person picked. The variant rides along as the
|
|
334 |
+ |
/// description, so a list of a dozen themes says which are dark without being
|
|
335 |
+ |
/// opened.
|
|
336 |
+ |
fn theme_choices() -> Vec<EnumValue> {
|
|
337 |
+ |
crate::theme::available()
|
|
338 |
+ |
.into_iter()
|
|
339 |
+ |
.map(|meta| EnumValue {
|
|
340 |
+ |
label: meta.name,
|
|
341 |
+ |
description: Some(if meta.is_custom {
|
|
342 |
+ |
format!("{}, yours", meta.variant)
|
|
343 |
+ |
} else {
|
|
344 |
+ |
meta.variant
|
|
345 |
+ |
}),
|
|
346 |
+ |
value: meta.id,
|
|
347 |
+ |
})
|
|
348 |
+ |
.collect()
|
|
349 |
+ |
}
|
|
350 |
+ |
|
| 296 |
351 |
|
/// Parse `timedatectl show`, which is `Key=value` a line at a time.
|
| 297 |
352 |
|
///
|
| 298 |
353 |
|
/// Values are taken verbatim after the first `=`. `TimeUSec` arrives already
|
| 437 |
492 |
|
HOSTNAME => text(self.host.fixed.as_ref().or(self.host.effective.as_ref())),
|
| 438 |
493 |
|
LOCALE => text(self.locale.lang.as_ref()),
|
| 439 |
494 |
|
KEYMAP => text(self.locale.keymap.as_ref()),
|
|
495 |
+ |
THEME => Some(Value::String(crate::theme::current_id())),
|
| 440 |
496 |
|
_ => None,
|
| 441 |
497 |
|
}
|
| 442 |
498 |
|
}
|
| 483 |
539 |
|
(KEYMAP, Value::String(keymap)) => {
|
| 484 |
540 |
|
Invocation::new("localectl").args(["set-keymap", keymap])
|
| 485 |
541 |
|
}
|
|
542 |
+ |
// A file, not a command, and the only row here that needs no
|
|
543 |
+ |
// privilege at all.
|
|
544 |
+ |
(THEME, Value::String(id)) => {
|
|
545 |
+ |
return crate::theme::remember(id).map(|write| vec![write]);
|
|
546 |
+ |
}
|
| 486 |
547 |
|
_ => anyhow::bail!("`{path}` has no setter"),
|
| 487 |
548 |
|
};
|
| 488 |
549 |
|
Ok(vec![Effect::Run(invocation)])
|
| 523 |
584 |
|
parse_list("America/Los_Angeles\nAmerica/Denver\nUTC\n"),
|
| 524 |
585 |
|
parse_list("en_US.UTF-8\nen_GB.UTF-8\n"),
|
| 525 |
586 |
|
parse_list("us\nuk\n"),
|
|
587 |
+ |
theme_choices(),
|
| 526 |
588 |
|
),
|
| 527 |
589 |
|
time: parse_show(tests::SHOW),
|
| 528 |
590 |
|
host: parse_host(tests::HOST),
|
| 723 |
785 |
|
fn a_machine_that_lists_no_keymaps_does_not_offer_the_row() {
|
| 724 |
786 |
|
let mut bind = SystemBind {
|
| 725 |
787 |
|
sections: sections(),
|
| 726 |
|
- |
fields: rows(Vec::new(), parse_list("en_US.UTF-8\n"), Vec::new()),
|
|
788 |
+ |
fields: rows(
|
|
789 |
+ |
Vec::new(),
|
|
790 |
+ |
parse_list("en_US.UTF-8\n"),
|
|
791 |
+ |
Vec::new(),
|
|
792 |
+ |
theme_choices(),
|
|
793 |
+ |
),
|
| 727 |
794 |
|
time: parse_show(SHOW),
|
| 728 |
795 |
|
host: parse_host(HOST),
|
| 729 |
796 |
|
locale: parse_locale(LOCALE_STATUS),
|