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
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
//! What a dog's `shep.pane` pull left on its config pane.
//!
//! The status tab draws it. The pane is rebuilt on every re-read of the dog's
//! section, so this rides across the rebuild, with the open tab and the
//! cursor, beside the open tables.
use std::time::{Duration, Instant};
use shep_core::dogs::pane::{Pane, Section};
use super::status::{Layout, PaneKey, StatusView, Step, Tab, keyed, layout, row_at};
/// What a dog's manifest says about its pane: the names a pane is checked
/// against.
///
/// The actions and the refresh slices read these, since a pane may only name
/// an action the manifest declares and a section's `refresh_on` only means
/// something for an event kind the manifest lists.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct DeclaredPane {
/// The names of the manifest's `actions`.
pub actions: Vec<String>,
/// The kinds of the manifest's `events`.
pub events: Vec<String>,
}
/// The least time between two pulls of one pane, so a burst of events costs
/// one pull at its start and one at its end.
const PULL_GAP: Duration = Duration::from_secs(1);
/// How the pull stands.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PaneState {
/// Asked, and no answer has landed yet.
Waiting,
/// The dog's answer, read.
Ready(Pane),
/// No answer to hold, and a one-line reason why.
Unavailable(String),
}
/// A dog's pane as the config pane holds it: what the manifest declares and
/// what the last pull came back with.
#[derive(Debug, Clone)]
pub struct DogStatus {
declared: DeclaredPane,
state: PaneState,
/// Why the newest pull failed, while [`Self::state`] still holds an older
/// good pane, so one slow answer does not blank it.
failed: Option<String>,
/// Whether a pull is out and has not landed.
pulling: bool,
/// When the newest pull went out.
last_pull: Option<Instant>,
/// Whether an event asked for a pull that has not gone out yet.
pending: bool,
/// Whether another pull is wanted as soon as the one out lands, because
/// an action finished while it was in flight.
pull_again: bool,
/// Whether a pane action is out and has not been answered.
acting: bool,
/// The open tab and the status tab's cursor.
view: StatusView,
/// What the newest action key or action reply said, for the footer.
said: Option<String>,
}
impl DogStatus {
/// A dog that declares a pane, before any pull went out.
#[must_use]
pub(in crate::lookout) fn new(declared: DeclaredPane) -> Self {
Self {
declared,
state: PaneState::Waiting,
failed: None,
pulling: false,
last_pull: None,
pending: false,
pull_again: false,
acting: false,
view: StatusView::default(),
said: None,
}
}
/// Takes the manifest's newest word on what the pane declares, leaving
/// what was pulled alone.
pub(in crate::lookout) fn redeclare(&mut self, declared: DeclaredPane) {
self.declared = declared;
}
/// What the dog's manifest declares.
#[must_use]
pub fn declared(&self) -> &DeclaredPane {
&self.declared
}
/// How the pull stands.
#[must_use]
pub fn state(&self) -> &PaneState {
&self.state
}
/// Why the newest pull failed, when [`Self::state`] is still an older
/// good pane. [`None`] otherwise: an [`PaneState::Unavailable`] carries
/// its own reason.
#[must_use]
pub fn failed(&self) -> Option<&str> {
self.failed.as_deref()
}
/// Marks a pull as out at `now`, and says whether to send it. `false`
/// while one is already waiting, so a second ask is not sent.
#[must_use]
pub(in crate::lookout) fn begin_pull(&mut self, now: Instant) -> bool {
if core::mem::replace(&mut self.pulling, true) {
return false;
}
self.last_pull = Some(now);
self.pending = false;
true
}
/// Whether the pane is showing a section that names `kind`, an event kind
/// the manifest declares.
#[must_use]
pub(in crate::lookout) fn refreshes_on(&self, kind: &str) -> bool {
self.declared()
.events
.iter()
.any(|declared| declared == kind)
&& self.named_kinds().contains(&kind)
}
/// The kinds the held pane's sections name that the manifest does not
/// declare, each once, in the order the sections first name them.
#[must_use]
pub(in crate::lookout) fn undeclared_kinds(&self) -> Vec<&str> {
let mut kinds: Vec<&str> = Vec::new();
for kind in self.named_kinds() {
let declared = self.declared().events.iter().any(|event| event == kind);
if !declared && !kinds.contains(&kind) {
kinds.push(kind);
}
}
kinds
}
/// Every `refresh_on` kind of the held pane's sections.
fn named_kinds(&self) -> Vec<&str> {
let PaneState::Ready(pane) = &self.state else {
return Vec::new();
};
pane.sections
.iter()
.flat_map(|section| match section {
Section::Fields(fields) => fields.refresh_on.as_slice(),
Section::Table(table) => table.refresh_on.as_slice(),
_unknown => &[],
})
.map(String::as_str)
.collect()
}
/// Marks a pull due, and says whether to send it now: when none is out
/// and the last went out a second or more before `now`. Otherwise the
/// pull waits for [`Self::send_pending`].
#[must_use]
pub(in crate::lookout) fn pull_due(&mut self, now: Instant) -> bool {
self.pending = true;
self.send_pending(now)
}
/// Marks a pull due without sending it: the next tick does.
pub(in crate::lookout) fn mark_due(&mut self) {
self.pending = true;
}
/// Sends the pull that was marked due, once none is out and a second has
/// passed since the last.
#[must_use]
pub(in crate::lookout) fn send_pending(&mut self, now: Instant) -> bool {
let rested = self
.last_pull
.is_none_or(|last| now.saturating_duration_since(last) >= PULL_GAP);
self.pending && rested && self.begin_pull(now)
}
/// Asks for a pull after an action: says whether to send it now. While a
/// pull is out it is queued instead, because that pull may have read the
/// dog before the action ran, and [`Self::land`] says when to send it.
#[must_use]
pub(in crate::lookout) fn pull_after_action(&mut self, now: Instant) -> bool {
if self.begin_pull(now) {
return true;
}
self.pull_again = true;
false
}
/// Whether a pane action is out and has not been answered.
#[must_use]
pub(in crate::lookout) fn acting(&self) -> bool {
self.acting
}
/// Marks an action as out.
pub(in crate::lookout) fn begin_action(&mut self) {
self.acting = true;
}
/// Marks the action out as answered.
pub(in crate::lookout) fn end_action(&mut self) {
self.acting = false;
}
/// Takes a pull's answer: a read pane, or the reason it came to nothing.
/// Says whether a queued pull is now out and must be sent.
///
/// A failure after a good pane keeps that pane and notes the reason beside
/// it; a failure with none held is [`PaneState::Unavailable`].
pub(in crate::lookout) fn land(&mut self, answer: Result<Pane, String>) -> bool {
self.pulling = false;
let landed = answer.is_ok();
match answer {
Ok(pane) => {
self.state = PaneState::Ready(pane);
self.failed = None;
}
Err(reason) if matches!(self.state, PaneState::Ready(_)) => {
self.failed = Some(reason);
}
Err(reason) => self.state = PaneState::Unavailable(reason),
}
if landed || !matches!(self.state, PaneState::Ready(_)) {
self.view.settle(&self.layout());
}
let again = core::mem::take(&mut self.pull_again);
self.pulling = again;
again
}
/// The open tab.
#[must_use]
pub fn tab(&self) -> Tab {
self.view.tab()
}
/// Opens the tab that is not open.
///
/// Leaving the status tab drops a pull that was waiting for the tick.
pub(in crate::lookout) fn switch_tab(&mut self) {
self.view.switch();
self.pending = false;
}
/// The row the status tab's cursor is on, counted over every table's rows
/// in order. [`None`] while the pane holds no row.
#[must_use]
pub fn cursor(&self) -> Option<usize> {
self.view.cursor()
}
/// Moves the status tab's cursor, or scrolls the tab when the cursor is
/// at an end of the rows or there are none.
pub(in crate::lookout) fn step(&mut self, step: Step) {
let layout = self.layout();
self.view.step(step, &layout);
}
/// Tells the status tab how tall the pane's body is, the title included.
///
/// The tab spends a line on its strip and one per footer line, and the
/// rest is what the window shows.
pub(in crate::lookout) fn set_rows(&mut self, rows: usize) {
let footer = self.footer_len();
let room = if rows == 0 {
0
} else {
rows.saturating_sub(2 + footer).max(1)
};
let layout = self.layout();
self.view.set_room(room, &layout);
}
/// The pane's actions with their letters, in declared order. Empty unless
/// the pane is ready.
pub(in crate::lookout) fn keys(&self) -> Vec<PaneKey> {
match &self.state {
PaneState::Ready(pane) => keyed(pane, &self.declared().actions),
PaneState::Waiting | PaneState::Unavailable(_) => Vec::new(),
}
}
/// The names of the pane's actions the manifest does not declare.
pub(in crate::lookout) fn undeclared(&self) -> impl Iterator<Item = String> {
self.keys()
.into_iter()
.map(|key| key.action.action)
.filter(|name| !self.declared().actions.contains(name))
}
/// The key of the row the cursor is on. [`None`] while the pane holds no
/// row.
pub(in crate::lookout) fn row_key(&self) -> Option<&str> {
let PaneState::Ready(pane) = &self.state else {
return None;
};
row_at(pane, self.view.cursor()?).map(|row| row.key.as_str())
}
/// Sets the footer's last result, `text` with the terminal's controls
/// removed.
pub(in crate::lookout) fn say(&mut self, text: &str) {
self.said = Some(crate::terminal_safe::sanitise(text).0);
}
/// The footer's last result: what the newest action key or action reply
/// said.
#[must_use]
pub fn said(&self) -> Option<&str> {
self.said.as_deref()
}
/// How many lines the status tab's footer holds.
pub(in crate::lookout) fn footer_len(&self) -> usize {
usize::from(self.failed.is_some())
+ usize::from(matches!(&self.state, PaneState::Ready(pane) if pane.skipped > 0))
+ self.undeclared_kinds().len()
+ self.undeclared().count()
+ usize::from(self.said.is_some())
+ usize::from(self.keys().iter().any(|key| key.letter.is_some()))
}
/// The first body line the status tab draws, when a key scrolled it away
/// from the cursor. [`None`] while the window follows the cursor.
#[must_use]
pub fn scroll(&self) -> Option<usize> {
self.view.scroll()
}
/// Where the lines of the pane's body fall. Empty unless the pane is
/// ready.
fn layout(&self) -> Layout {
match &self.state {
PaneState::Ready(pane) => layout(pane),
PaneState::Waiting | PaneState::Unavailable(_) => Layout::default(),
}
}
}