| 1 |
|
- |
//! The System tab's bind: live state, fronted by a command.
|
|
1 |
+ |
//! The System tab's bind: live state, fronted by commands.
|
| 2 |
2 |
|
//!
|
| 3 |
3 |
|
//! The other half of the [`Bind`] seam. [`FileBind`](crate::bind::FileBind)
|
| 4 |
|
- |
//! reads a `DocumentMut` and writes at save; this reads `timedatectl show` and
|
|
4 |
+ |
//! reads a `DocumentMut` and writes at save; this reads three `*ctl` tools and
|
| 5 |
5 |
|
//! hands back a setter per row. The form above them cannot tell which it is
|
| 6 |
6 |
|
//! holding, which is the whole point of [[alloy-settings]] putting two tabs
|
| 7 |
7 |
|
//! over one engine.
|
| 9 |
9 |
|
//! ## Why the field list is hand-written
|
| 10 |
10 |
|
//!
|
| 11 |
11 |
|
//! The Applications side parses ten schemas and will parse more. This side is a
|
| 12 |
|
- |
//! dozen rows fronting different CLIs, each with its own vocabulary and its own
|
| 13 |
|
- |
//! setter, and a schema DSL general enough to describe `timedatectl` would be a
|
| 14 |
|
- |
//! second language to maintain for no reader. So the table is Rust, and the
|
| 15 |
|
- |
//! only thing it shares with a schema is the [`Field`] it produces.
|
|
12 |
+ |
//! handful of rows fronting different CLIs, each with its own vocabulary and
|
|
13 |
+ |
//! its own setter, and a schema DSL general enough to describe `timedatectl`
|
|
14 |
+ |
//! would be a second language to maintain for no reader. So the table is Rust,
|
|
15 |
+ |
//! and the only thing it shares with a schema is the [`Field`] it produces.
|
| 16 |
16 |
|
//!
|
| 17 |
17 |
|
//! ## Permissions are narrow on principle
|
| 18 |
18 |
|
//!
|
| 19 |
|
- |
//! This bind runs exactly two writing commands: `timedatectl set-timezone` and
|
| 20 |
|
- |
//! `timedatectl set-ntp`. Both are in the five actions
|
| 21 |
|
- |
//! `usr/share/polkit-1/rules.d/50-alloy-settings.rules` grants, so neither
|
| 22 |
|
- |
//! prompts, and nothing here can reach an action that was deliberately left
|
| 23 |
|
- |
//! prompting. Setting the clock by hand is not offered at all: `set-time` is
|
| 24 |
|
- |
//! not granted, moving a system clock invalidates certificates and reorders
|
| 25 |
|
- |
//! logs, and the row exists to show what the timezone did rather than to be
|
| 26 |
|
- |
//! typed into. See wiki note `alloy-privilege`.
|
|
19 |
+ |
//! Five writing commands, no more:
|
|
20 |
+ |
//!
|
|
21 |
+ |
//! | row | command | granted action |
|
|
22 |
+ |
//! |---|---|---|
|
|
23 |
+ |
//! | zone | `timedatectl set-timezone` | `timedate1.set-timezone` |
|
|
24 |
+ |
//! | ntp | `timedatectl set-ntp` | `timedate1.set-ntp` |
|
|
25 |
+ |
//! | static | `hostnamectl --static hostname` | `hostname1.set-static-hostname` |
|
|
26 |
+ |
//! | lang | `localectl set-locale` | `locale1.set-locale` |
|
|
27 |
+ |
//! | keymap | `localectl set-keymap` | `locale1.set-keyboard` |
|
|
28 |
+ |
//!
|
|
29 |
+ |
//! Every one is in the five `usr/share/polkit-1/rules.d/50-alloy-settings.rules`
|
|
30 |
+ |
//! grants, so none prompts, and nothing here can reach an action deliberately
|
|
31 |
+ |
//! left prompting. Two consequences that look like quirks and are not:
|
|
32 |
+ |
//!
|
|
33 |
+ |
//! - **`--static` is load-bearing.** `hostnamectl hostname NAME` with no scope
|
|
34 |
+ |
//! also sets the pretty name, which needs `set-machine-info`, which is not
|
|
35 |
+ |
//! granted. Widening the grant to save a flag would be the wrong trade, and
|
|
36 |
+ |
//! the rules file says so in its own header.
|
|
37 |
+ |
//! - **The clock is shown and never set.** `set-time` is not granted, moving a
|
|
38 |
+ |
//! system clock invalidates certificates and reorders logs, and the row
|
|
39 |
+ |
//! exists to show what changing the timezone did.
|
|
40 |
+ |
//!
|
|
41 |
+ |
//! ## A front that did not answer
|
|
42 |
+ |
//!
|
|
43 |
+ |
//! One rule, applied the same way everywhere: a row whose front did not answer,
|
|
44 |
+ |
//! or whose vocabulary came back empty, is **shown and not settable, and says
|
|
45 |
+ |
//! why**. A machine with no `timesyncd` still sees the network-time row; a
|
|
46 |
+ |
//! `localectl` built without console keymaps still sees the keymap row. Hiding
|
|
47 |
+ |
//! it would answer "where do I set this" with silence, and offering it would
|
|
48 |
+ |
//! promise a command that is not there.
|
|
49 |
+ |
//!
|
|
50 |
+ |
//! See wiki note `alloy-privilege`.
|
| 27 |
51 |
|
//!
|
| 28 |
52 |
|
//! <!-- wiki: alloy-settings -->
|
| 29 |
53 |
|
|
| 34 |
58 |
|
|
| 35 |
59 |
|
use crate::bind::Bind;
|
| 36 |
60 |
|
use crate::cli::{CommandLog, Effect, Invocation};
|
| 37 |
|
- |
use crate::schema::{EnumValue, Field, FieldKind, Section};
|
|
61 |
+ |
use crate::install::validate_hostname;
|
|
62 |
+ |
use crate::schema::{EnumValue, Field, FieldKind, Section, check_value};
|
| 38 |
63 |
|
|
| 39 |
64 |
|
/// Paths the rows are addressed by. Dotted like a schema's, so the form groups
|
| 40 |
65 |
|
/// them under a section header the same way.
|
| 41 |
66 |
|
const ZONE: &str = "time.zone";
|
| 42 |
67 |
|
const NTP: &str = "time.ntp";
|
| 43 |
68 |
|
const CLOCK: &str = "time.clock";
|
|
69 |
+ |
const HOSTNAME: &str = "hostname.static";
|
|
70 |
+ |
const LOCALE: &str = "locale.lang";
|
|
71 |
+ |
const KEYMAP: &str = "locale.keymap";
|
| 44 |
72 |
|
|
| 45 |
|
- |
/// Live system settings, fronted by `timedatectl`.
|
|
73 |
+ |
/// Live system settings, fronted by `timedatectl`, `hostnamectl`, `localectl`.
|
| 46 |
74 |
|
pub(crate) struct SystemBind {
|
| 47 |
75 |
|
sections: Vec<Section>,
|
| 48 |
76 |
|
fields: Vec<Field>,
|
| 49 |
|
- |
/// Last `timedatectl show`, as key=value.
|
| 50 |
|
- |
state: Vec<(String, String)>,
|
|
77 |
+ |
time: Vec<(String, String)>,
|
|
78 |
+ |
host: Host,
|
|
79 |
+ |
locale: Locale,
|
|
80 |
+ |
}
|
|
81 |
+ |
|
|
82 |
+ |
/// What `hostnamectl` reported.
|
|
83 |
+ |
#[derive(Default)]
|
|
84 |
+ |
struct Host {
|
|
85 |
+ |
/// `/etc/hostname`, or `None` on a machine that has never had one set.
|
|
86 |
+ |
fixed: Option<String>,
|
|
87 |
+ |
/// The name the machine is actually answering to.
|
|
88 |
+ |
effective: Option<String>,
|
|
89 |
+ |
/// `static`, `transient`, or `default`.
|
|
90 |
+ |
source: Option<String>,
|
|
91 |
+ |
answered: bool,
|
|
92 |
+ |
}
|
|
93 |
+ |
|
|
94 |
+ |
/// What `localectl` reported.
|
|
95 |
+ |
#[derive(Default)]
|
|
96 |
+ |
struct Locale {
|
|
97 |
+ |
lang: Option<String>,
|
|
98 |
+ |
keymap: Option<String>,
|
|
99 |
+ |
answered: bool,
|
| 51 |
100 |
|
}
|
| 52 |
101 |
|
|
| 53 |
102 |
|
impl SystemBind {
|
| 54 |
103 |
|
/// Read the machine's state and build the rows, or `None` when there is no
|
| 55 |
104 |
|
/// `timedatectl` answering.
|
| 56 |
105 |
|
///
|
| 57 |
|
- |
/// A probe that runs the real command rather than looking for the binary,
|
| 58 |
|
- |
/// for the reason `net` gives: a `timedatectl` that cannot reach a systemd
|
| 59 |
|
- |
/// is worse than none, and only running it reveals that.
|
|
106 |
+ |
/// `timedatectl` is the anchor: no systemd, no live state, no tab. The
|
|
107 |
+ |
/// other two are read best-effort, and rows whose front stayed quiet are
|
|
108 |
+ |
/// marked in [`annotate`](Self::annotate) rather than dropped.
|
|
109 |
+ |
///
|
|
110 |
+ |
/// The probe runs the real command rather than looking for the binary, for
|
|
111 |
+ |
/// the reason `net` gives: a tool that cannot reach its daemon is worse
|
|
112 |
+ |
/// than one that is absent, and only running it reveals that.
|
| 60 |
113 |
|
pub(crate) fn detect(log: &mut CommandLog) -> Option<Self> {
|
| 61 |
|
- |
let state = parse_show(&show().run(log).ok()?);
|
| 62 |
|
- |
if state.is_empty() {
|
|
114 |
+ |
let time = parse_show(&show().run(log).ok()?);
|
|
115 |
+ |
if time.is_empty() {
|
| 63 |
116 |
|
return None;
|
| 64 |
117 |
|
}
|
| 65 |
118 |
|
|
| 66 |
|
- |
// 598 entries on a current systemd, which is the number that forced the
|
| 67 |
|
- |
// pick overlay to filter as you type.
|
| 68 |
|
- |
let zones: Vec<EnumValue> = Invocation::new("timedatectl")
|
| 69 |
|
- |
.arg("list-timezones")
|
| 70 |
|
- |
.run(log)
|
| 71 |
|
- |
.map(|raw| parse_zones(&raw))
|
| 72 |
|
- |
.unwrap_or_default();
|
|
119 |
+ |
// 598 zones and several hundred locales on a current systemd, which is
|
|
120 |
+ |
// the size that forced the pick overlay to filter as you type.
|
|
121 |
+ |
let zones = list(log, &Invocation::new("timedatectl").arg("list-timezones"));
|
|
122 |
+ |
let locales = list(log, &Invocation::new("localectl").arg("list-locales"));
|
|
123 |
+ |
let keymaps = list(log, &Invocation::new("localectl").arg("list-keymaps"));
|
| 73 |
124 |
|
|
| 74 |
125 |
|
let mut bind = Self {
|
| 75 |
|
- |
sections: vec![Section {
|
| 76 |
|
- |
path: "time".to_string(),
|
| 77 |
|
- |
description: Some("Clock, timezone, and network time.".to_string()),
|
| 78 |
|
- |
}],
|
| 79 |
|
- |
fields: rows(zones),
|
| 80 |
|
- |
state,
|
|
126 |
+ |
sections: sections(),
|
|
127 |
+ |
fields: rows(zones, locales, keymaps),
|
|
128 |
+ |
time,
|
|
129 |
+ |
host: read_host(log),
|
|
130 |
+ |
locale: read_locale(log),
|
| 81 |
131 |
|
};
|
| 82 |
132 |
|
bind.annotate();
|
| 83 |
133 |
|
Some(bind)
|
| 84 |
134 |
|
}
|
| 85 |
135 |
|
|
| 86 |
|
- |
fn get(&self, key: &str) -> Option<&str> {
|
| 87 |
|
- |
self.state
|
|
136 |
+ |
fn time(&self, key: &str) -> Option<&str> {
|
|
137 |
+ |
self.time
|
| 88 |
138 |
|
.iter()
|
| 89 |
139 |
|
.find(|(name, _)| name == key)
|
| 90 |
140 |
|
.map(|(_, value)| value.as_str())
|
| 91 |
141 |
|
}
|
| 92 |
142 |
|
|
| 93 |
|
- |
/// Fold the state that is *about* a row into that row's help line.
|
| 94 |
|
- |
///
|
| 95 |
|
- |
/// `NTPSynchronized` is the case the design named: whether network time is
|
| 96 |
|
- |
/// switched on and whether it has actually agreed with a server yet are two
|
| 97 |
|
- |
/// different facts, and a row that showed only the first would say "on"
|
| 98 |
|
- |
/// beside a clock that is still wrong. `CanNTP` is the other: a machine
|
| 99 |
|
- |
/// with no timesyncd can be shown the row and told why it is not a switch,
|
| 100 |
|
- |
/// which is more use than hiding it.
|
| 101 |
|
- |
fn annotate(&mut self) {
|
| 102 |
|
- |
let can = self.get("CanNTP") != Some("no");
|
| 103 |
|
- |
let synced = self.get("NTPSynchronized") == Some("yes");
|
| 104 |
|
- |
let note = if !can {
|
| 105 |
|
- |
"No network time service on this machine.".to_string()
|
| 106 |
|
- |
} else if synced {
|
| 107 |
|
- |
"Network time. The clock has agreed with a server.".to_string()
|
| 108 |
|
- |
} else {
|
| 109 |
|
- |
"Network time. The clock has not synchronized yet.".to_string()
|
| 110 |
|
- |
};
|
|
143 |
+ |
fn field_mut(&mut self, path: &str) -> Option<&mut Field> {
|
|
144 |
+ |
self.fields.iter_mut().find(|field| field.path == path)
|
|
145 |
+ |
}
|
| 111 |
146 |
|
|
| 112 |
|
- |
if let Some(field) = self.fields.iter_mut().find(|field| field.path == NTP) {
|
| 113 |
|
- |
field.description = Some(note);
|
| 114 |
|
- |
field.readonly = !can;
|
|
147 |
+ |
/// How many choices a row's enum carries.
|
|
148 |
+ |
fn choices(&self, path: &str) -> &[EnumValue] {
|
|
149 |
+ |
match self.fields.iter().find(|field| field.path == path) {
|
|
150 |
+ |
Some(Field {
|
|
151 |
+ |
kind: FieldKind::Enum { values, .. },
|
|
152 |
+ |
..
|
|
153 |
+ |
}) => values,
|
|
154 |
+ |
_ => &[],
|
| 115 |
155 |
|
}
|
| 116 |
156 |
|
}
|
|
157 |
+ |
|
|
158 |
+ |
/// Fold the state that is *about* a row into that row's help line, and
|
|
159 |
+ |
/// close any row whose front cannot take a write.
|
|
160 |
+ |
///
|
|
161 |
+ |
/// Run after every read, because all of it moves: `NTPSynchronized` flips
|
|
162 |
+ |
/// some seconds after boot, and a hostname's source changes the moment this
|
|
163 |
+ |
/// screen sets one.
|
|
164 |
+ |
fn annotate(&mut self) {
|
|
165 |
+ |
// Whether network time is switched on and whether it has actually
|
|
166 |
+ |
// agreed with a server are two different facts. A row showing only the
|
|
167 |
+ |
// first would say "on" beside a clock that is still wrong.
|
|
168 |
+ |
let can_ntp = self.time("CanNTP") != Some("no");
|
|
169 |
+ |
let synced = self.time("NTPSynchronized") == Some("yes");
|
|
170 |
+ |
let note = if can_ntp {
|
|
171 |
+ |
if synced {
|
|
172 |
+ |
"Network time. The clock has agreed with a server."
|
|
173 |
+ |
} else {
|
|
174 |
+ |
"Network time. The clock has not synchronized yet."
|
|
175 |
+ |
}
|
|
176 |
+ |
} else {
|
|
177 |
+ |
"No network time service on this machine."
|
|
178 |
+ |
};
|
|
179 |
+ |
set(self.field_mut(NTP), note, !can_ntp);
|
|
180 |
+ |
|
|
181 |
+ |
// `--static` is what this row writes, so it is what the row is named
|
|
182 |
+ |
// for. Saying which name the machine is currently answering to is the
|
|
183 |
+ |
// part a user cannot see anywhere else on the screen.
|
|
184 |
+ |
let note = match (self.host.answered, self.host.source.as_deref()) {
|
|
185 |
+ |
(false, _) => "hostnamectl did not answer.".to_string(),
|
|
186 |
+ |
(true, Some(source)) if source != "static" => format!(
|
|
187 |
+ |
"The machine's persistent name. Currently {source}; setting it here makes it stick."
|
|
188 |
+ |
),
|
|
189 |
+ |
(true, _) => "The machine's persistent name.".to_string(),
|
|
190 |
+ |
};
|
|
191 |
+ |
let quiet = !self.host.answered;
|
|
192 |
+ |
set(self.field_mut(HOSTNAME), ¬e, quiet);
|
|
193 |
+ |
|
|
194 |
+ |
let (note, closed) = if !self.locale.answered {
|
|
195 |
+ |
("localectl did not answer.", true)
|
|
196 |
+ |
} else if self.choices(LOCALE).is_empty() {
|
|
197 |
+ |
("This machine lists no locales.", true)
|
|
198 |
+ |
} else {
|
|
199 |
+ |
("System locale. Applies to new sessions.", false)
|
|
200 |
+ |
};
|
|
201 |
+ |
set(self.field_mut(LOCALE), note, closed);
|
|
202 |
+ |
|
|
203 |
+ |
let (note, closed) = if !self.locale.answered {
|
|
204 |
+ |
("localectl did not answer.", true)
|
|
205 |
+ |
} else if self.choices(KEYMAP).is_empty() {
|
|
206 |
+ |
// Observed on a systemd built without console keymap support: the
|
|
207 |
+ |
// list is empty and `set-keymap` is not even a verb.
|
|
208 |
+ |
("This machine lists no virtual console keymaps.", true)
|
|
209 |
+ |
} else {
|
|
210 |
+ |
(
|
|
211 |
+ |
"Virtual console keymap. The graphical session has its own.",
|
|
212 |
+ |
false,
|
|
213 |
+ |
)
|
|
214 |
+ |
};
|
|
215 |
+ |
set(self.field_mut(KEYMAP), note, closed);
|
|
216 |
+ |
}
|
|
217 |
+ |
}
|
|
218 |
+ |
|
|
219 |
+ |
/// Apply a help line and a read-only flag to a row.
|
|
220 |
+ |
fn set(field: Option<&mut Field>, note: &str, readonly: bool) {
|
|
221 |
+ |
if let Some(field) = field {
|
|
222 |
+ |
field.description = Some(note.to_string());
|
|
223 |
+ |
field.readonly = readonly;
|
|
224 |
+ |
}
|
| 117 |
225 |
|
}
|
| 118 |
226 |
|
|
| 119 |
227 |
|
fn show() -> Invocation {
|
| 120 |
228 |
|
Invocation::new("timedatectl").arg("show")
|
| 121 |
229 |
|
}
|
| 122 |
230 |
|
|
| 123 |
|
- |
/// The three time rows.
|
| 124 |
|
- |
fn rows(zones: Vec<EnumValue>) -> Vec<Field> {
|
|
231 |
+ |
/// Run a list-the-vocabulary command, or nothing if it will not.
|
|
232 |
+ |
fn list(log: &mut CommandLog, invocation: &Invocation) -> Vec<EnumValue> {
|
|
233 |
+ |
invocation
|
|
234 |
+ |
.run(log)
|
|
235 |
+ |
.map(|raw| parse_list(&raw))
|
|
236 |
+ |
.unwrap_or_default()
|
|
237 |
+ |
}
|
|
238 |
+ |
|
|
239 |
+ |
fn sections() -> Vec<Section> {
|
| 125 |
240 |
|
vec![
|
| 126 |
|
- |
Field {
|
| 127 |
|
- |
path: ZONE.to_string(),
|
| 128 |
|
- |
description: Some("The machine's timezone.".to_string()),
|
| 129 |
|
- |
required: false,
|
| 130 |
|
- |
readonly: false,
|
| 131 |
|
- |
kind: FieldKind::Enum {
|
| 132 |
|
- |
default: None,
|
| 133 |
|
- |
values: zones,
|
| 134 |
|
- |
},
|
|
241 |
+ |
Section {
|
|
242 |
+ |
path: "time".to_string(),
|
|
243 |
+ |
description: Some("Clock, timezone, and network time.".to_string()),
|
| 135 |
244 |
|
},
|
|
245 |
+ |
Section {
|
|
246 |
+ |
path: "hostname".to_string(),
|
|
247 |
+ |
description: Some("What this machine calls itself.".to_string()),
|
|
248 |
+ |
},
|
|
249 |
+ |
Section {
|
|
250 |
+ |
path: "locale".to_string(),
|
|
251 |
+ |
description: Some("Language and keyboard.".to_string()),
|
|
252 |
+ |
},
|
|
253 |
+ |
]
|
|
254 |
+ |
}
|
|
255 |
+ |
|
|
256 |
+ |
/// 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> {
|
|
258 |
+ |
let enumerated = |path: &str, values: Vec<EnumValue>| Field {
|
|
259 |
+ |
path: path.to_string(),
|
|
260 |
+ |
description: None,
|
|
261 |
+ |
required: false,
|
|
262 |
+ |
readonly: false,
|
|
263 |
+ |
kind: FieldKind::Enum {
|
|
264 |
+ |
default: None,
|
|
265 |
+ |
values,
|
|
266 |
+ |
},
|
|
267 |
+ |
};
|
|
268 |
+ |
let text = |path: &str, readonly: bool| Field {
|
|
269 |
+ |
path: path.to_string(),
|
|
270 |
+ |
description: None,
|
|
271 |
+ |
required: false,
|
|
272 |
+ |
readonly,
|
|
273 |
+ |
kind: FieldKind::Str {
|
|
274 |
+ |
default: None,
|
|
275 |
+ |
pattern: None,
|
|
276 |
+ |
},
|
|
277 |
+ |
};
|
|
278 |
+ |
|
|
279 |
+ |
vec![
|
|
280 |
+ |
enumerated(ZONE, zones),
|
| 136 |
281 |
|
Field {
|
| 137 |
282 |
|
path: NTP.to_string(),
|
| 138 |
|
- |
// Replaced by `annotate` with what the machine actually reports.
|
| 139 |
283 |
|
description: None,
|
| 140 |
284 |
|
required: false,
|
| 141 |
285 |
|
readonly: false,
|
| 142 |
286 |
|
kind: FieldKind::Bool { default: None },
|
| 143 |
287 |
|
},
|
| 144 |
|
- |
Field {
|
| 145 |
|
- |
path: CLOCK.to_string(),
|
| 146 |
|
- |
description: Some("Local time, as of the last read.".to_string()),
|
| 147 |
|
- |
required: false,
|
| 148 |
|
- |
// Not a setting. `set-time` is deliberately not granted, and this
|
| 149 |
|
- |
// row exists to show what changing the zone did.
|
| 150 |
|
- |
readonly: true,
|
| 151 |
|
- |
kind: FieldKind::Str {
|
| 152 |
|
- |
default: None,
|
| 153 |
|
- |
pattern: None,
|
| 154 |
|
- |
},
|
| 155 |
|
- |
},
|
|
288 |
+ |
// Not a setting. See the module docs.
|
|
289 |
+ |
text(CLOCK, true),
|
|
290 |
+ |
text(HOSTNAME, false),
|
|
291 |
+ |
enumerated(LOCALE, locales),
|
|
292 |
+ |
enumerated(KEYMAP, keymaps),
|
| 156 |
293 |
|
]
|
| 157 |
294 |
|
}
|
| 158 |
295 |
|
|
| 168 |
305 |
|
.collect()
|
| 169 |
306 |
|
}
|
| 170 |
307 |
|
|
| 171 |
|
- |
/// One zone per line. They label themselves: `America/Denver` is already what a
|
| 172 |
|
- |
/// person would look for, and inventing a prettier label would break the
|
| 173 |
|
- |
/// filter, which matches on what is shown as well as on the raw value.
|
| 174 |
|
- |
fn parse_zones(raw: &str) -> Vec<EnumValue> {
|
|
308 |
+ |
/// One entry per line, labelling itself.
|
|
309 |
+ |
///
|
|
310 |
+ |
/// `America/Denver` and `en_GB.UTF-8` are already what a person would look for,
|
|
311 |
+ |
/// and inventing a prettier label would break the pick overlay's filter, which
|
|
312 |
+ |
/// matches on what is shown as well as on the raw value.
|
|
313 |
+ |
fn parse_list(raw: &str) -> Vec<EnumValue> {
|
| 175 |
314 |
|
raw.lines()
|
| 176 |
315 |
|
.map(str::trim)
|
| 177 |
316 |
|
.filter(|line| !line.is_empty())
|
| 178 |
|
- |
.map(|zone| EnumValue {
|
| 179 |
|
- |
value: zone.to_string(),
|
| 180 |
|
- |
label: zone.to_string(),
|
|
317 |
+ |
.map(|entry| EnumValue {
|
|
318 |
+ |
value: entry.to_string(),
|
|
319 |
+ |
label: entry.to_string(),
|
| 181 |
320 |
|
description: None,
|
| 182 |
321 |
|
})
|
| 183 |
322 |
|
.collect()
|
| 184 |
323 |
|
}
|
| 185 |
324 |
|
|
|
325 |
+ |
fn read_host(log: &mut CommandLog) -> Host {
|
|
326 |
+ |
Invocation::new("hostnamectl")
|
|
327 |
+ |
.arg("--json=short")
|
|
328 |
+ |
.run(log)
|
|
329 |
+ |
.map_or_else(|_| Host::default(), |raw| parse_host(&raw))
|
|
330 |
+ |
}
|
|
331 |
+ |
|
|
332 |
+ |
/// Parse `hostnamectl --json=short`.
|
|
333 |
+ |
///
|
|
334 |
+ |
/// JSON rather than `hostnamectl status`, which is laid out for reading. Both
|
|
335 |
+ |
/// `StaticHostname` and `Hostname` are taken: the first is what this row writes
|
|
336 |
+ |
/// and is null on a machine that has never had one set, and the second is what
|
|
337 |
+ |
/// the machine is answering to meanwhile, which is what the row should show
|
|
338 |
+ |
/// rather than an empty cell on a machine that plainly has a name.
|
|
339 |
+ |
fn parse_host(raw: &str) -> Host {
|
|
340 |
+ |
let Ok(json) = serde_json::from_str::<serde_json::Value>(raw) else {
|
|
341 |
+ |
return Host::default();
|
|
342 |
+ |
};
|
|
343 |
+ |
let text = |key: &str| {
|
|
344 |
+ |
json.get(key)
|
|
345 |
+ |
.and_then(serde_json::Value::as_str)
|
|
346 |
+ |
.map(str::to_string)
|
|
347 |
+ |
};
|
|
348 |
+ |
Host {
|
|
349 |
+ |
fixed: text("StaticHostname"),
|
|
350 |
+ |
effective: text("Hostname"),
|
|
351 |
+ |
source: text("HostnameSource"),
|
|
352 |
+ |
answered: true,
|
|
353 |
+ |
}
|
|
354 |
+ |
}
|
|
355 |
+ |
|
|
356 |
+ |
fn read_locale(log: &mut CommandLog) -> Locale {
|
|
357 |
+ |
Invocation::new("localectl")
|
|
358 |
+ |
.arg("status")
|
|
359 |
+ |
.run(log)
|
|
360 |
+ |
.map_or_else(|_| Locale::default(), |raw| parse_locale(&raw))
|
|
361 |
+ |
}
|
|
362 |
+ |
|
|
363 |
+ |
/// Parse `localectl status`, which has no machine-readable mode.
|
|
364 |
+ |
///
|
|
365 |
+ |
/// `localectl` grew no `--json`, so this reads the labelled block it prints:
|
|
366 |
+ |
///
|
|
367 |
+ |
/// ```text
|
|
368 |
+ |
/// System Locale: LANG=en_US.UTF-8
|
|
369 |
+ |
/// VC Keymap: (unset)
|
|
370 |
+ |
/// X11 Layout: us
|
|
371 |
+ |
/// ```
|
|
372 |
+ |
///
|
|
373 |
+ |
/// Two things keep that honest. A label this does not recognize is skipped
|
|
374 |
+ |
/// rather than guessed at, and `(unset)` is read as nothing rather than as a
|
|
375 |
+ |
/// keymap literally named that. So the failure mode of an unfamiliar build is a
|
|
376 |
+ |
/// row that reads unset, never one that reads wrong.
|
|
377 |
+ |
fn parse_locale(raw: &str) -> Locale {
|
|
378 |
+ |
let mut locale = Locale {
|
|
379 |
+ |
answered: true,
|
|
380 |
+ |
..Locale::default()
|
|
381 |
+ |
};
|
|
382 |
+ |
for line in raw.lines() {
|
|
383 |
+ |
let Some((key, value)) = line.split_once(':') else {
|
|
384 |
+ |
continue;
|
|
385 |
+ |
};
|
|
386 |
+ |
let value = value.trim();
|
|
387 |
+ |
if value.is_empty() || value == "(unset)" {
|
|
388 |
+ |
continue;
|
|
389 |
+ |
}
|
|
390 |
+ |
match key.trim() {
|
|
391 |
+ |
// The block can carry several locale variables; LANG is the one
|
|
392 |
+ |
// this screen offers, so it is the one picked out.
|
|
393 |
+ |
"System Locale" => {
|
|
394 |
+ |
locale.lang = value
|
|
395 |
+ |
.split_whitespace()
|
|
396 |
+ |
.find_map(|token| token.strip_prefix("LANG="))
|
|
397 |
+ |
.map(str::to_string);
|
|
398 |
+ |
}
|
|
399 |
+ |
"VC Keymap" => locale.keymap = Some(value.to_string()),
|
|
400 |
+ |
_ => {}
|
|
401 |
+ |
}
|
|
402 |
+ |
}
|
|
403 |
+ |
locale
|
|
404 |
+ |
}
|
|
405 |
+ |
|
| 186 |
406 |
|
/// systemd spells its booleans `yes` and `no`.
|
| 187 |
407 |
|
fn yes(raw: Option<&str>) -> bool {
|
| 188 |
408 |
|
raw == Some("yes")
|
| 190 |
410 |
|
|
| 191 |
411 |
|
impl Bind for SystemBind {
|
| 192 |
412 |
|
fn origin(&self) -> String {
|
| 193 |
|
- |
"timedatectl".to_string()
|
|
413 |
+ |
"timedatectl, hostnamectl, localectl".to_string()
|
| 194 |
414 |
|
}
|
| 195 |
415 |
|
|
| 196 |
416 |
|
fn sections(&self) -> &[Section] {
|
| 202 |
422 |
|
}
|
| 203 |
423 |
|
|
| 204 |
424 |
|
fn read(&self, path: &str) -> Option<Value> {
|
|
425 |
+ |
let text = |value: Option<&String>| value.map(|text| Value::String(text.clone()));
|
| 205 |
426 |
|
match path {
|
| 206 |
427 |
|
ZONE => self
|
| 207 |
|
- |
.get("Timezone")
|
|
428 |
+ |
.time("Timezone")
|