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
//! A dog's pane actions: the letter pressed on its status tab, the question a
//! destructive one asks first, and what the dog's reply leaves in the footer.
use super::dog_pane_pull::replied_body;
use super::*;
use crate::terminal_safe::sanitise;
use shep_client::dogs::ActionId;
impl App {
/// A letter pressed on the status tab: runs the action it names, if any.
///
/// A row action needs the cursor on a row of its own table. A read-only
/// lookout refuses every action, and so does a pane with one still out. A
/// destructive one asks first and its yes sends it, see
/// [`Self::on_pane_action_answer`].
pub(super) fn on_pane_action_key(&mut self, letter: char) -> Effect {
let Some(pane) = self.config_pane() else {
return Effect::None;
};
let name = pane.target().name().to_owned();
let Some(status) = pane.status() else {
return Effect::None;
};
let Some(key) = status
.keys()
.into_iter()
.find(|key| key.letter == Some(letter))
else {
// A letter no action took keeps the meaning it has everywhere else.
return match letter {
'r' => self.reread_pane(),
'h' => self.open_keymap(),
_unbound => Effect::None,
};
};
let row = match &key.rows {
None => None,
Some(rows) => {
let under_cursor = status.cursor().filter(|at| rows.contains(at));
match under_cursor.and(status.row_key()) {
Some(row) => Some(row.to_owned()),
None => {
self.say_on_dog_pane(&name, "no row selected");
return Effect::None;
}
}
}
};
let acting = status.acting();
if self.authorize_write().is_none() {
return Effect::None;
}
if acting {
self.say_on_dog_pane(&name, "an action is still running");
return Effect::None;
}
let question = key.action.destructive.then(|| {
let label = sanitise(&key.action.label).0;
match &row {
Some(row) => format!("Run {label} on {}?", sanitise(row).0),
None => format!("Run {label}?"),
}
});
let sent = Sent::DogPaneAction {
name,
action: key.action.action,
label: key.action.label,
row,
opening: self.dog_opening,
};
let Some(question) = question else {
return self.send_pane_action(sent);
};
let tag = self.mint_tag();
self.pane_action = Some((tag, sent));
self.open_modal(Modal::confirm(tag, question));
Effect::None
}
/// The answer to the question `tag` asked, or `None` when `tag` is not a
/// pane action's. A yes sends the action and any other answer drops it.
pub(super) fn on_pane_action_answer(&mut self, tag: u64, yes: bool) -> Option<Effect> {
let (asked, sent) = self.pane_action.take()?;
if asked != tag {
self.pane_action = Some((asked, sent));
return None;
}
Some(if yes {
self.send_pane_action(sent)
} else {
Effect::None
})
}
/// Marks `sent`'s pane as having an action out and sends it.
fn send_pane_action(&mut self, sent: Sent) -> Effect {
if let Sent::DogPaneAction { name, opening, .. } = &sent
&& let Some(status) = self
.dog_pane_of(name, *opening)
.and_then(ConfigPane::status_mut)
{
status.begin_action();
}
Effect::Send(sent)
}
/// An action's reply: an id starts a long action, a plain answer is shown
/// in the footer and the pane is pulled again, and a refusal or a timeout
/// is shown and nothing else.
///
/// An id is kept with the pane closed, since the result event comes
/// either way. Anything else is dropped once the operator has left the
/// pane or reopened it. A pull already out is followed by another.
pub(super) fn on_dog_pane_action_reply(
&mut self,
name: &str,
opening: u64,
action: &str,
label: &str,
result: Result<Response, RequestError>,
) -> Effect {
let now = self.now;
let replied = replied_body(name, result);
let id = replied.as_deref().ok().and_then(ActionId::from_reply);
let status = self
.dog_pane_of(name, opening)
.and_then(ConfigPane::status_mut);
if let Some(id) = id {
if let Some(status) = status {
status.end_action();
}
return self.start_long_action(name, label, id);
}
let Some(status) = status else {
return Effect::None;
};
status.end_action();
match replied {
Ok(body) => {
let body = body.trim();
status.say(
if body.is_empty() {
format!("{action} done")
} else {
body.to_owned()
}
.as_str(),
);
if status.pull_after_action(now) {
Effect::Send(Sent::DogPane {
name: name.to_owned(),
opening,
})
} else {
Effect::None
}
}
Err(reason) => {
status.say(&reason);
Effect::None
}
}
}
/// An action that never left: the footer says so and the pane is free to
/// run another.
pub(super) fn on_dog_pane_action_unsent(&mut self, name: &str, opening: u64, action: &str) {
let Some(status) = self
.dog_pane_of(name, opening)
.and_then(ConfigPane::status_mut)
else {
return;
};
status.end_action();
status.say(&format!("{action} was not sent"));
}
/// Puts `text` in the footer of dog `name`'s open status tab, or in the
/// status bar when that pane is not the one open.
pub(super) fn say_on_dog_pane(&mut self, name: &str, text: &str) {
let opening = self.dog_opening;
match self
.dog_pane_of(name, opening)
.and_then(ConfigPane::status_mut)
{
Some(status) => status.say(text),
None => self.notice = Some(Notice::grave(format!("{name}: {text}"))),
}
}
/// Whether a dog's status tab is the open screen: the tab is up and no
/// dialog or list sits over it.
pub(super) fn on_status_tab(&self) -> bool {
self.close_dialog.is_none()
&& self.config_pane().is_some_and(|pane| {
pane.list().is_none()
&& pane
.status()
.is_some_and(|status| status.tab() == crate::lookout::pane::Tab::Status)
})
}
}