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
//! The rows the settings screen's cursor walks: the schema's tree, then one
//! block per candidate dog, and the viewport that shows a slice of them.
use super::*;
/// The key `Settings::folded` holds for the dog `name`'s `env` table.
fn env_fold_key(name: &str) -> String {
format!("dogs.{name}.env")
}
impl Settings {
/// What the screen reads off disk, and renders every row's value and source
/// from.
///
/// A landed write does not update this in place: it raises a fresh
/// [`Effect::LoadSettings`], so `Set` and `Unset` land the same way and
/// neither can drift from the rest of the document.
#[must_use]
pub fn snapshot(&self) -> &SettingsSnapshot {
&self.snapshot
}
/// Every row the cursor can sit on: the sections and their keys in the
/// order the screen draws them, a map's entries under it, then one row per
/// candidate dog, with that dog's own rows under it while its row is open,
/// then one [`SettingsRow::AvailableDog`] per index dog this machine lacks,
/// and [`SettingsRow::BrowseDogs`] last.
#[must_use]
pub fn rows(&self) -> Vec<SettingsRow> {
let shape = shep_toml_shape();
let mut rows = Vec::new();
self.push_children(&shape.outline.roots, &mut rows);
for index in 0..self.snapshot.dogs.len() {
self.push_dog_rows(index, &mut rows);
}
rows.extend((0..self.available.len()).map(SettingsRow::AvailableDog));
rows.push(SettingsRow::BrowseDogs);
rows
}
/// The index dogs this machine lacks, in the catalogue's order.
#[must_use]
pub(crate) fn available(&self) -> &[CatalogueRow] {
&self.available
}
/// Takes the uninstalled dogs out of `catalogue`: those no receipt or
/// adopted entry names, and whose name is not already a row of the dogs
/// list. The cursor stays on the row it was on, by dog for an
/// [`SettingsRow::AvailableDog`] and at the end for
/// [`SettingsRow::BrowseDogs`], since rows landing after it would
/// otherwise move it onto another one.
pub(in crate::lookout::app) fn set_available(&mut self, catalogue: &[CatalogueRow]) {
let cursor = self.cursor();
let kept = match cursor {
Some(SettingsRow::AvailableDog(index)) => self
.available
.get(index)
.map(|row| row.dog.adopt_as.clone()),
_ => None,
};
self.available = catalogue
.iter()
.filter(|row| row.state == DogState::NotInstalled)
.filter(|row| {
!self
.snapshot
.dogs
.iter()
.any(|dog| dog.name == row.dog.adopt_as)
})
.cloned()
.collect();
let len = self.rows_len();
match cursor {
Some(SettingsRow::BrowseDogs) => self.view.move_to(len.saturating_sub(1), len),
Some(SettingsRow::AvailableDog(_)) => {
let at = kept
.and_then(|name| {
self.available
.iter()
.position(|row| row.dog.adopt_as == name)
})
.map_or(len.saturating_sub(1), |index| {
len - 1 - self.available.len() + index
});
self.view.move_to(at, len);
}
_ => self.view.clamp(len),
}
}
/// One dog's block: its header row, then, while its row is open, the four
/// `[daemon] keys that belong to it, the checklist under `grants`, and the
/// rows of its own config tree.
fn push_dog_rows(&self, index: usize, rows: &mut Vec<SettingsRow>) {
rows.push(SettingsRow::Dog(index));
let Some(dog) = self.snapshot.dogs.get(index) else {
return;
};
let Some(tree) = self.dog_trees.get(&dog.name) else {
return;
};
if !tree.is_open() {
return;
}
for key in DogKey::ALL {
rows.push(SettingsRow::DogKey { dog: index, key });
if key != DogKey::Grants {
continue;
}
rows.extend((0..dog.offered.len()).map(|item| SettingsRow::DogGrant {
dog: index,
index: item,
}));
}
if dog.adopted_path.is_some() {
rows.push(SettingsRow::DogEnv(index));
if !self.folded.contains(&env_fold_key(&dog.name)) {
rows.extend((0..dog.env_keys.len()).map(|key| SettingsRow::DogEnvKey {
dog: index,
index: key,
}));
rows.push(SettingsRow::DogEnvNew(index));
}
}
let Some(pane) = tree.pane() else {
return;
};
// The pane's `env` rows are left to the `DogEnv` rows above, which
// read the keys off the snapshot and so follow a landed write.
rows.extend(
pane.rows()
.iter()
.enumerate()
.filter(|(_, row)| pane.tree_row((*row).to_owned()).is_some())
.map(|(row, _)| SettingsRow::DogField { dog: index, row }),
);
}
fn push_children(&self, children: &[Child], rows: &mut Vec<SettingsRow>) {
let shape = shep_toml_shape();
for child in children {
match *child {
Child::Table(index) => {
rows.push(SettingsRow::Table(index));
let table = &shape.outline.tables[index];
if !self.folded.contains(&table.path) {
self.push_children(&table.children, rows);
}
}
Child::Leaf(index) => {
rows.push(SettingsRow::Field(index));
let field = &shape.fields.fields()[index];
if field.kind == FieldKind::Map && !self.folded.contains(&field.key) {
let entries = self.snapshot.entries(&field.key).len();
rows.extend((0..entries).map(|entry| SettingsRow::Entry {
map: index,
index: entry,
}));
rows.push(SettingsRow::AddEntry(index));
}
}
}
}
}
/// How many rows [`Self::rows`] would build.
#[must_use]
pub fn rows_len(&self) -> usize {
self.rows().len()
}
/// The row the cursor sits on. `None` only if [`Self::rows`] is empty,
/// which the schema's sections make unreachable.
#[must_use]
pub fn cursor(&self) -> Option<SettingsRow> {
let rows = self.rows();
rows.get(self.view.cursor().min(rows.len().saturating_sub(1)))
.copied()
}
/// Folds the table or map under the cursor if it is open, opens it if it
/// is folded. Answers whether the cursor was on one.
pub(in crate::lookout::app) fn toggle_fold(&mut self) -> bool {
let shape = shep_toml_shape();
let key = match self.cursor() {
Some(SettingsRow::Table(index)) => shape.outline.tables[index].path.clone(),
Some(SettingsRow::Field(index))
if shape.fields.fields()[index].kind == FieldKind::Map =>
{
shape.fields.fields()[index].key.clone()
}
Some(SettingsRow::DogEnv(dog)) => match self.snapshot.dogs.get(dog) {
Some(dog) => env_fold_key(&dog.name),
None => return false,
},
_ => return false,
};
if !self.folded.remove(&key) {
self.folded.insert(key);
}
true
}
/// Whether the table or map at the dotted `key` shows what is inside it.
#[must_use]
pub fn is_open(&self, key: &str) -> bool {
!self.folded.contains(key)
}
/// Whether the `env` table under the dog named `name` shows its keys.
#[must_use]
pub fn is_env_open(&self, name: &str) -> bool {
self.is_open(&env_fold_key(name))
}
/// Moves the cursor by `delta` rows, clamped to [`Self::rows`], never
/// wrapping.
pub(in crate::lookout::app) fn move_by(&mut self, delta: isize) {
let len = self.rows_len();
self.view.move_by(delta, len);
}
pub(in crate::lookout::app) fn move_to_first(&mut self) {
let len = self.rows_len();
self.view.move_to(0, len);
}
pub(in crate::lookout::app) fn move_to_last(&mut self) {
let len = self.rows_len();
self.view.move_to(len.saturating_sub(1), len);
}
/// The viewport, for the renderer.
#[must_use]
pub fn view(&self) -> &Viewport {
&self.view
}
/// Records the terminal's height.
pub fn set_rows(&mut self, rows: usize) {
let len = self.rows_len();
self.view.set_rows(rows, len);
}
}