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
//! **What a control does, whichever control did it.**
//!
//! The acts the window affords beyond composing a gesture: aim at a wall,
//! select a conversation, put a notice down, and the enrollment's four — open
//! it, spend it, file what it answered, close it and forget. Each used to live
//! inside the
//! `if …clicked()` that made it, which is fine while a pointer is the only way
//! to make it — and stops being fine the moment a key can make it too. Two
//! spellings of *what aiming means* would drift, and the one that drifted would
//! be the one nobody points at.
//!
//! So a binding names a control that already exists (`crate::ui::keys`), by
//! calling the same door the click calls. **A binding that could fire something
//! a click cannot is a second surface**, which is why the keyboard's own cursor
//! walks only rows the pointer can also reach: an unaddressable roster row is
//! skipped by both, structurally, because both ask the same question.
//!
//! # Each act clears exactly what it invalidated
//!
//! Aiming at another wall retires the conversation list, what that list was an
//! answer to, the selection and everything under it, because none of it is
//! about the new wall. Retiring the *answer* beside the rows is what lets the
//! pane say it has not heard yet rather than reporting an empty wall. Selecting a
//! conversation retires the transcript and the live tail, because they are the
//! old one's. Nothing else is touched — and in particular the draft is not,
//! because what an operator typed is theirs until they send it.
//!
//! The enrollment's own act is the sharpest case of that rule: closing it
//! clears the material and nothing else, and clearing the material IS the
//! product — after it there is no copy of that key on this box.
use super::{Aim, Model};
impl Model {
/// **Aim at a wall**, down the channel that resolved it.
///
/// `address` is what a gesture must carry rather than the name the row
/// wears; the two differ exactly where an entry renames, and the roster is
/// what maps one to the other.
pub fn aim_at(&mut self, channel: &str, address: &str) {
self.aim = Some(Aim {
channel: channel.to_owned(),
address: address.to_owned(),
});
self.convs.clear();
self.answered = None;
self.conversation = None;
self.transcript = crate::reply::transcript::Transcript::default();
self.live = None;
// **The tuning pane goes with the wall it was opened on** (bl-4a2c).
// It holds no aim of its own, deliberately — every gesture it composes
// reads `Model::aim` at the moment of composing — so leaving it open
// over a new wall would leave an operator looking at one wall's rows
// while its controls wrote to another's. Retiring the ROWS beside it is
// the same reading `answered` above gets: `None` is a wall nobody has
// asked yet, and the old wall's answer is not this wall's.
self.tuning = None;
self.roles = None;
// **The login pane goes with the wall too** (bl-e3c5), and for the
// sharper form of the same reason: its act signs a credential into ONE
// wall's own store, so a pane left standing over a new aim would offer
// to sign the operator in somewhere they are no longer looking.
self.retire_login();
// **And the clients pane with them** (bl-e53c), on the login pane's own
// terms: its rows are one workspace's registrations, so a pane left
// standing over a new aim would paint one wall's machines under
// another's name.
self.retire_clients();
// **And the config pane with them** (bl-5c53), sharper again: a
// destination carries the aim's own workspace inside it, so a pane
// left standing over a new aim would read one wall's file and paint it
// under another's name.
self.retire_configuring();
self.retire_records();
// **The ball pane's WALL half goes with the wall, and the pane does
// not** (bl-d2af). Two of its four reads name every channel, so the
// pane's subject survives an aim that moved; the two that name one
// wall do not, and painting the old wall's balls under the new wall's
// name is exactly what the retirement above prevents one level up.
self.retire_wall_balls();
// **The fleet pane goes with the wall too** (bl-a43a), and for the
// login pane's sharper reason: its acts run drones and spend money in
// ONE workspace, so a pane left standing over a new aim would offer to
// start a fleet somewhere the operator is no longer looking.
self.retire_fleet();
}
/// **Select a conversation**, by the id every gesture addresses it with.
pub fn select(&mut self, root_id: &str) {
self.conversation = Some(root_id.to_owned());
self.transcript = crate::reply::transcript::Transcript::default();
self.live = None;
// **The records pane goes with the conversation it was opened on**
// (bl-2cf7) — the same rule the tuning pane keeps for its wall, one
// noun over (`super::records`).
self.retire_records();
}
/// **Assert this wall's place in the strip** — pinned, or not (bl-7782).
///
/// `pinned` is what the operator is asking FOR, not what the row currently
/// is: the wire's two ops are assertions rather than a toggle, and upstream
/// says why — *"unpinning one that is not pinned leaves the list alone,
/// which is what lets two seats send it at once and agree."* So this end
/// composes the act it means and never flips a flag it read a beat ago.
///
/// The aim is the gate, exactly as it is for every other per-wall act, and
/// what says it landed is the next roster answer: both ops reply with the
/// listing carrying the ranks it now has, which is a kind this seat already
/// paints.
pub fn post_pin(&mut self, pinned: bool) {
let Some(Aim { address, .. }) = self.aim.clone() else {
return;
};
let gesture = if pinned {
crate::verbs::pin(address)
} else {
crate::verbs::unpin(address)
};
self.outbox.push(super::Posted::act(gesture));
}
/// **Put the notice down.** An operator who has read a refusal should not
/// have to wait for the next answer to clear it.
pub fn dismiss(&mut self) {
self.notice = None;
}
/// **What Escape means**, in the order an operator means it (bl-7574,
/// extended for the tuning pane in bl-4a2c).
///
/// A ladder from the innermost thing on the glass outwards, which is the
/// order the key is actually reached for. The enrollment covers the window
/// and holds a secret, so it goes first — and closing it is the same act
/// `done — forget it` performs, material and all, because the control that
/// closes that pane is the control that forgets. Then a draft assignment,
/// which is a thing inside a pane rather than the pane: Escape over a
/// half-typed model puts the draft down and leaves the rows standing.
/// Then the tuning pane itself, then the records pane (bl-2cf7), then the
/// decision queue (bl-f0ef), then whichever of the window's own two is
/// standing (bl-40ec — one arm, because one field holds both), then the
/// login pane (bl-e3c5), then the clients pane (bl-e53c), then an
/// unmaking (bl-48fa) — no two of the nine ever stand together, so the
/// order among them is never spent. The
/// unmaking is on the ladder because Escape over a destructive pane means
/// what [`Model::close_unmaking`] means and nothing else: it unmakes
/// nothing, and a key that could arm or spend one would be the second
/// surface `crate::ui::keys` refuses to be. With nothing covering, the notice
/// is the only thing left to put down, and Escape is its × reached without
/// a pointer.
pub fn escape(&mut self) {
if self.enroll.is_some() {
self.close_enrollment();
} else if matches!(self.tuning, Some(super::Tuning::Editing(_))) {
self.cancel_assignment();
} else if self.tuning.is_some() {
self.close_tuning();
} else if self.showing(super::Listing::Records) {
self.close_records();
} else if self.showing(super::Listing::Queue) {
self.close_queue();
} else if self.showing(super::Listing::Clients) {
self.close_clients();
} else if self.configuring.is_some() {
self.close_configuring();
} else if self.lookup.is_some() {
self.close_lookup();
} else if self.login.is_some() {
self.close_login();
} else if self.unmaking.is_some() {
self.close_unmaking();
} else {
self.dismiss();
}
}
/// **Open an enrollment aimed at the wall the window is pointed at**, or do
/// nothing where it is pointed at none.
///
/// The aim is the gate rather than a separate check: the act mints a
/// registration, a registration is the pair `(client, workspace)`, and a
/// workspace is exactly what an aim is. So the control is offered where
/// there is one and nowhere else.
pub fn begin_enrollment(&mut self) {
if let Some(aim) = self.aim.clone() {
self.enroll = Some(super::Enrolling::at(aim));
}
}
/// **Spend the enrollment**: compose the gesture for whoever can send it.
///
/// It composes and does not clear, because the pane has to stand until the
/// answer arrives — there is nothing else on the screen that would tell an
/// operator the act is in flight.
pub fn post_enrollment(&mut self) {
let Some(enrolling) = self.enroll.as_mut().filter(|held| held.ready()) else {
return;
};
enrolling.posted = true;
let gesture = enrolling.gesture();
self.outbox.push(super::Posted::act(gesture));
}
/// **Close the enrollment, and drop the material with it.** The one control
/// whose whole product is a forgetting: after it there is no copy of that
/// key anywhere on this box, which is the property DESIGN §3 states and
/// `crate::seat::enroll`'s tests assert over the tree.
pub fn close_enrollment(&mut self) {
self.enroll = None;
}
/// **File the material** — draw it, and hold the picture beside it.
///
/// Two arms and the second is a real one. Where the pane is still open the
/// symbol is drawn once, here, because a frame may not compute one. Where
/// it is **not** — the operator closed it while the act was in flight — the
/// material has arrived with nowhere to go, and it is dropped *and said so*:
/// the enrollment did happen on the engine, and an operator who is not told
/// would go looking for a box that has a registration and no material. A
/// silent drop is the one thing this model's door does not do.
pub(super) fn enrolled(&mut self, material: &crate::reply::enrolled::Enrolled) {
let Some(enrolling) = self.enroll.as_mut() else {
self.notice = Some(super::Notice::Unreadable(format!(
"the engine enrolled {} while this pane was closed, so the \
material is gone — enroll again to mint another",
material.caption()
)));
return;
};
match super::Shown::of(material) {
Ok(shown) => {
enrolling.shown = Some(shown);
self.notice = None;
}
Err(why) => {
self.enroll = None;
self.notice = Some(super::Notice::Unreadable(why));
}
}
}
}
#[cfg(test)]
mod tests;