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
//! **A dropup between frames** (DESIGN §4.42, item 11): which selector on
//! the rule is open, where its cursor stands, and whether the read it posted
//! has been answered.
//!
//! A selector opens on what this seat already holds — the roster's walls,
//! the providers read, one provider's models, the provider row's levels —
//! and posts the read that refreshes that list in the same gesture. The list
//! is the engine's answer and lives where every answer lives; what is held
//! here is only the question of whether the refresh has come back, because
//! that is what the mark on the segment says.
//!
//! **A pick is the door the role tuning fires for the worker role**, or the
//! switcher's own aim for a wall — never a second spelling of either.
use super::{Aim, Model, Posted};
use crate::reply::Reply;
use crate::reply::roles::RoleRow;
/// The role a start runs on, and the one the rule's selectors set.
pub const WORKER: &str = "worker";
/// **One selector on the rule**, left to right.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Selector {
Workspace,
Provider,
Model,
Effort,
Priority,
}
/// **Whether the refresh a dropup posted has been answered.**
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Reload {
/// Posted, and nothing has come back.
Asked,
/// The answer landed; the list is the engine's latest.
Landed,
}
/// **An open dropup.**
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Dropup {
pub selector: Selector,
/// The provider row a model list is for — the worker's, or the one just
/// picked on the provider list, because a provider alone is half an
/// assignment and the write takes the pair.
pub provider: Option<String>,
pub cursor: usize,
/// `None` where nothing could be asked (a model list with no provider).
pub reload: Option<Reload>,
}
/// **What picking a row does.**
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Pick {
Wall(Aim),
/// `+ new workspace` on the named channel (bl-673e; DESIGN §4.44).
Found(String),
Provider(String),
Model(String),
Effort(Option<String>),
Priority(bool),
}
/// **One row of a dropup**: its words, whether it is what is in force, and
/// what picking it does.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Choice {
pub text: String,
pub current: bool,
pub pick: Pick,
}
impl Selector {
/// **The op of the read this selector posts.**
fn op(self) -> &'static str {
match self {
Self::Workspace => crate::verbs::WORKSPACES.word,
Self::Model => crate::verbs::MODELS.word,
Self::Provider | Self::Effort | Self::Priority => crate::verbs::PROVIDERS.word,
}
}
/// Whether `reply` is the answer to the read this selector posts.
fn answered_by(self, reply: &Reply) -> bool {
match self {
Self::Workspace => matches!(reply, Reply::Workspaces(_)),
Self::Model => matches!(reply, Reply::Models(_)),
Self::Provider | Self::Effort | Self::Priority => {
matches!(reply, Reply::Providers(_))
}
}
}
}
impl Model {
/// **The worker role's row**, where the roles read has answered one.
pub fn worker(&self) -> Option<&RoleRow> {
self.roles.as_ref()?.iter().find(|row| row.role == WORKER)
}
/// **Open `selector`'s dropup**, or close it where it is the one open —
/// the toggle a click on its segment is.
///
/// **Opening one puts down whatever stands** (bl-27dd), as the switcher's
/// chord does (`Model::switch`, bl-339d): `/model` and Ctrl+O reach it
/// from under a pane, and a dropup opened under a cover was closed by the
/// cover the same frame — the chord did nothing. The enrollment is the
/// exception it is everywhere.
pub fn drop_up(&mut self, selector: Selector) {
if self.dropup.as_ref().is_some_and(|d| d.selector == selector) {
self.dropup = None;
return;
}
if self.enroll.is_some() {
return;
}
while self.covered() {
self.escape();
}
let provider = self.worker().map(|row| row.provider.clone());
self.open_on(selector, provider);
}
/// Open a dropup on `provider`'s rows, its cursor on what is in force,
/// and post the read that refreshes it.
fn open_on(&mut self, selector: Selector, provider: Option<String>) {
let mut open = Dropup {
selector,
provider,
cursor: 0,
reload: None,
};
let asked = self.ask_for(&open);
open.reload = asked.then_some(Reload::Asked);
self.dropup = Some(open);
let current = self.choices().iter().position(|choice| choice.current);
if let Some(open) = &mut self.dropup {
open.cursor = current.unwrap_or(0);
}
}
/// **Post the read that refreshes `open`'s list**, answering whether one
/// could be asked.
fn ask_for(&mut self, open: &Dropup) -> bool {
let address = self.aim.as_ref().map(|aim| aim.address.clone());
let read = match (open.selector, address) {
(Selector::Workspace, _) => crate::verbs::workspaces(),
(Selector::Model, Some(address)) => match &open.provider {
Some(provider) => crate::verbs::models(address, provider.clone()),
None => return false,
},
(_, Some(address)) => crate::verbs::providers(address),
(_, None) => return false,
};
self.outbox.push(Posted::read(read));
true
}
/// **Close the dropup**, picking nothing.
pub fn close_dropup(&mut self) {
self.dropup = None;
}
/// **Move the open dropup's cursor** by `step`, held to its rows.
pub fn walk_dropup(&mut self, step: isize) {
let last = self.choices().len().saturating_sub(1);
if let Some(open) = &mut self.dropup {
open.cursor = open.cursor.saturating_add_signed(step).min(last);
}
}
/// **Pick the row at `at`**: fire its door and close the dropup — or, for
/// a provider, open the model list on it, since the write takes the pair.
pub fn pick(&mut self, at: usize) {
let Some(choice) = self.choices().into_iter().nth(at) else {
return;
};
let asked = self.dropup.take().and_then(|open| open.provider);
match choice.pick {
Pick::Wall(aim) => {
self.engines.opening(&aim.channel);
self.aim_at(&aim.channel, &aim.address);
}
Pick::Found(channel) => self.begin_founding(&channel),
Pick::Provider(name) => self.open_on(Selector::Model, Some(name)),
Pick::Model(id) => {
if let Some(provider) = asked {
self.post_model(WORKER, provider, id);
}
}
Pick::Effort(level) => self.post_effort(WORKER, level),
Pick::Priority(on) => self.post_priority(WORKER, on),
}
}
/// **A refusal of the read the open dropup posted puts its reload
/// down**: nothing is coming, so the mark goes back to rest and the
/// notice says why — a reload that never ends would say the opposite.
pub(super) fn refused(&mut self, op: &str, read: &crate::reply::Read) {
use crate::reply::Read;
if let Some(open) = &mut self.dropup
&& open.selector.op() == op
&& matches!(read, Read::Refusal(_) | Read::Unreadable(_))
{
open.reload = None;
}
}
/// **Take one answer's news for the open dropup**: the read it posted
/// has come back, and a model listing is filed under the row it asked.
pub(super) fn landing(&mut self, reply: &Reply) {
let Some(open) = &mut self.dropup else {
return;
};
if !open.selector.answered_by(reply) {
return;
}
open.reload = Some(Reload::Landed);
if let (Reply::Models(_), Some(provider)) = (reply, open.provider.clone()) {
self.offering(&provider, crate::reply::Read::Answer(reply.clone()));
}
}
}
pub mod choices;
#[cfg(test)]
mod tests;