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
//! The keyboard over an open config pane: the one dispatch that decides which
//! handler answers a key.
//!
//! Moved whole out of `config_pane` under IR-48's 1000-line ceiling: the arms,
//! their order and their comments are what they were, and the handlers this
//! dispatches to live where they already did, in [`crate::lookout::app::close_dialog`],
//! [`crate::lookout::app::pane_list`] and [`crate::lookout::app::pane_dogs`].
use super::*;
use crate::lookout::pane::{Step, Tab};
impl App {
/// The config pane's own keymap, in force for as long as
/// [`Self::config_pane`] is `Some`.
///
/// Movement walks fields, `r` re-reads, `space` cycles the row under the
/// cursor, `Enter` or `e` edits it, `u` undoes the newest edit, and
/// `Escape` asks the close dialog's question if there is one: see
/// [`Self::reread_before_closing`]. Nothing is armed here.
pub(super) fn on_pane_key(&mut self, key: KeyPress) -> Effect {
self.notice = None;
// An armed env delete is cancelled by every key but the confirm and a
// quit, the secrets pane's rule, and its status line promises exactly
// that. The arm is taken before the key is dispatched so `Escape` can
// eat its own press rather than also closing the pane behind it.
let was_armed = !matches!(key, KeyPress::Confirm | KeyPress::Quit)
&& self
.config_pane_mut()
.is_some_and(ConfigPane::disarm_env_delete);
if self.close_dialog.is_some() {
return self.on_close_dialog_key(key);
}
if self.config_pane().is_some_and(|pane| pane.list().is_some()) {
return self.on_list_key(key);
}
if let Some(effect) = self.on_pane_dog_key(key) {
return effect;
}
if let Some(effect) = self.on_dogs_tab_key(key) {
return effect;
}
if key == KeyPress::Quit {
return Effect::Quit;
}
match key {
// Unreachable: the guard above already returned. Kept rather
// than a wildcard, because every other arm here is named on
// purpose, and a wildcard would defeat that for every variant,
// not just this one.
KeyPress::Quit => return Effect::Quit,
// Backs out one level at a time: the close dialog's own
// question first, if there is one, else the pane. `Escape`
// closes rather than cascading to a filter clear or a quit.
// Nothing leaves the pane until the question is answered.
KeyPress::Escape => {
// An armed delete eats the first `Escape`, as it does on the
// secrets pane: a delete waiting on a confirm is a state the
// operator should see cleared before anything else moves.
if was_armed {
return Effect::None;
}
if let Some(text) = self.config_pane().and_then(ConfigPane::incomplete_union) {
self.notice = Some(Notice::grave(text));
return Effect::None;
}
// Tables are read again first: the dialog's `c`, `R` and `L`
// write them too. See [`Self::on_close_reread`].
if let Some(effect) = self.reread_before_closing() {
return effect;
}
if let Some(dialog) = self.close_offer() {
self.close_dialog = Some(dialog);
return Effect::None;
}
return self.write_and_close();
}
KeyPress::SelectUp
| KeyPress::SelectDown
| KeyPress::SelectFirst
| KeyPress::SelectLast => {
if let Some(pane) = self.config_pane_mut() {
match key {
KeyPress::SelectUp => pane.move_by(-1),
KeyPress::SelectDown => pane.move_by(1),
KeyPress::SelectFirst => pane.move_to_first(),
KeyPress::SelectLast => pane.move_to_last(),
_ => unreachable!(),
}
}
}
// Re-reads the same sheep, so an override applied from another
// window shows up. The cursor survives it: see
// `Self::on_sheep_config`.
KeyPress::Refresh => return self.reread_pane(),
KeyPress::Cycle => return self.cycle_field(),
KeyPress::Confirm => return self.confirm_field(),
// `e` does exactly what `Enter` does here: an operator who
// opened the pane with `e` should not have to learn a second
// key to use it. Except on a table, which `Enter` opens and
// closes and which holds nothing for `e` to edit.
KeyPress::Edit => {
if self.config_pane().is_some_and(|pane| !pane.cursor_on_table()) {
return self.confirm_field();
}
}
// On a dog's pane `←` and `→` close and open the table under the
// cursor; a sheep pane has no tables and they do nothing.
KeyPress::TabPrev => {
if let Some(pane) = self.config_pane_mut() {
pane.close_table();
}
}
KeyPress::TabNext => {
if let Some(pane) = self.config_pane_mut() {
pane.open_table();
}
}
KeyPress::Help => {
self.open_keymap();
}
// `d` restores the field under the cursor to its default. Does
// nothing on an env row or `+ add a key`: unsetting a key
// entirely is a different act from restoring a default, and
// the spec does not ask for it.
KeyPress::Remove => return self.restore_default(),
// `D` arms the removal of the env key under the cursor, `Enter`
// confirms it, and every other key cancels. The cursor is asked
// before the gate is: a field row names no key to remove, so `D`
// is inert there. The gate is checked on the arm, not the confirm.
KeyPress::ArmDelete => {
let names_a_key = self
.config_pane()
.is_some_and(|pane| pane.cursor_env_key_name().is_some());
if names_a_key {
let now = self.now;
if self.authorize_write().is_some()
&& let Some(pane) = self.config_pane_mut()
{
pane.arm_env_delete(now);
}
}
}
KeyPress::Action(_)
| KeyPress::Settings
| KeyPress::Catalogue
| KeyPress::FilterStart
| KeyPress::TextChar(_)
| KeyPress::TextBackspace
| KeyPress::TextEdit(_)
| KeyPress::TextApply
| KeyPress::TextAbandon
| KeyPress::StepUp
| KeyPress::StepDown
| KeyPress::FoldView
| KeyPress::Secrets
| KeyPress::Reveal
| KeyPress::Copy
| KeyPress::Collapse
// Bound only in the close dialog; with none up, `c` is a stray
// key the same way an action key is.
| KeyPress::Continue => {}
// The key/value editor's two field steps. `map_key` only raises
// them in `InputMode::Text`, which `on_key` branches on before
// this match is reached, so nothing here can see them.
KeyPress::NextField | KeyPress::PrevField => {}
KeyPress::StreamCycle
| KeyPress::LevelCycle
| KeyPress::PageDown
| KeyPress::PageUp
| KeyPress::FollowToggle
| KeyPress::WrapToggle
| KeyPress::MatchNext
| KeyPress::MatchPrev
| KeyPress::Bleats => {}
// Drops the newest edit and nothing else. No control gate: a
// key that unfiles something cannot write, and refusing it
// behind a closed gate would leave an edit the gate already
// refused to file.
KeyPress::Undo => {
if let Some(pane) = self.config_pane_mut() {
pane.undo_edit();
}
}
// `tab` and `shift-tab` walk the groups; a digit jumps straight
// to one. All reset the cursor to the group's first field, so
// `j`/`k` never start on a row the new tab does not draw. The
// `dogs` tab lists its dogs the first time it comes up.
KeyPress::NextGroup => {
if let Some(pane) = self.config_pane_mut() {
pane.next_group();
pane.move_to_first();
}
return self.dogs_tab_effect();
}
KeyPress::PrevGroup => {
if let Some(pane) = self.config_pane_mut() {
pane.prev_group();
pane.move_to_first();
}
return self.dogs_tab_effect();
}
KeyPress::Group(digit) => {
if let Some(pane) = self.config_pane_mut() {
pane.set_group(digit);
pane.move_to_first();
}
return self.dogs_tab_effect();
}
}
Effect::None
}
}
impl App {
/// A pane dog's tab keys, and its status tab's keymap.
///
/// `tab` and `shift-tab` switch tabs. On the status tab the select keys
/// move its cursor and a key that only means something on the settings
/// form does nothing, so it cannot edit a field the operator cannot see.
/// [`None`] for a dog with no pane, for the settings tab's own keys, and
/// for the keys both tabs share.
fn on_pane_dog_key(&mut self, key: KeyPress) -> Option<Effect> {
let status = self.config_pane_mut()?.status_mut()?;
if matches!(key, KeyPress::NextGroup | KeyPress::PrevGroup) {
status.switch_tab();
return Some(self.pull_on_return());
}
if status.tab() != Tab::Status {
return None;
}
match key {
KeyPress::SelectUp => status.step(Step::Up),
KeyPress::SelectDown => status.step(Step::Down),
KeyPress::SelectFirst => status.step(Step::First),
KeyPress::SelectLast => status.step(Step::Last),
// A dog's action letter. `r` is one when an action's label gives
// it, and the pane pulls again on its own after one.
KeyPress::TextChar(letter) => return Some(self.on_pane_action_key(letter)),
KeyPress::Quit
| KeyPress::Escape
| KeyPress::Refresh
| KeyPress::Help
| KeyPress::Continue => return None,
_other => {}
}
Some(Effect::None)
}
}
#[cfg(test)]
mod tests;