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
//! **The accordion**: which server is open, when each was last opened on this
//! seat, and the workspace last aimed under each (DESIGN §4.39).
//!
//! Split from [`super`] at the design-time budget on the seam that module's own
//! doc draws: [`super`] is the module list, the pieces that are a subject of
//! their own live beside it, and this is the arrangement of the one list the
//! window has.
//!
//! **It is the seat's own and crosses no boundary** (§4.39). §4.25 rules that a
//! pin crosses, because a pin is an assertion about the world; this is the
//! other case, and §4.6 draws the line: a channel is a client-side fact, named
//! by this box and held by no other, so the order this box shows its channels
//! in can be nobody else's and there is no server to assert it into. It goes in
//! the place file ([`crate::place`]).
//!
//! **At most one server is open, so the arrangement is a single fact — WHICH —
//! and not a set of flags to keep consistent.** Opening one closes the other by
//! construction rather than by a rule anybody has to keep.
use std::collections::BTreeMap;
use super::{Model, Server};
/// **How the servers are arranged**, and the whole of what the place file
/// keeps about them.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Servers {
/// **Which server the operator last opened**, where this seat still holds
/// one by that name. `None` is a seat nobody has told, which is the first
/// run — [`Model::server_open`] answers that from the aim.
pub open: Option<String>,
/// **What each server's last opening RANKS as** — a count that rises by
/// one with every opening on this seat.
///
/// A rank and not a clock reading, because ordering is the only question
/// ever asked of it: two servers opened inside one millisecond would tie
/// under a wall clock and be ordered by their names, which is the one
/// thing the tie-break is for. A counter cannot tie, needs no clock to
/// inject, and round-trips through the place file as the integer it is.
pub opened: BTreeMap<String, u64>,
/// **The workspace last aimed under each server**, by the address a gesture
/// carries — what an opening aims at, so a server an operator comes back
/// to is the server they left.
pub aimed: BTreeMap<String, String>,
}
impl Servers {
/// **The servers in the order the pane paints them** — the pure function
/// over which one is open, when each was last opened, and their names.
///
/// The open one first, then most recently opened first, the name breaking
/// a tie. A server nobody has opened here ranks below every server
/// somebody has, which is the honest reading of *never*.
pub fn order(&self, names: &[String], open: Option<&str>) -> Vec<String> {
let mut out = names.to_vec();
out.sort_by_key(|name| self.rank(name, open));
out
}
/// Where one server stands, as a key a sort reads.
fn rank(&self, name: &str, open: Option<&str>) -> (u8, std::cmp::Reverse<u64>, String) {
(
u8::from(open != Some(name)),
std::cmp::Reverse(self.opened.get(name).copied().unwrap_or_default()),
name.to_owned(),
)
}
/// **Record an opening.** The rank is one past the highest anybody holds,
/// so the server just opened is the most recently opened by construction.
pub(super) fn opening(&mut self, name: &str) {
let next = self.opened.values().copied().max().unwrap_or_default() + 1;
self.opened.insert(name.to_owned(), next);
self.open = Some(name.to_owned());
}
}
impl Model {
/// Every server this seat holds, by its lead's name (`super::fold`).
fn server_names(&self) -> Vec<String> {
self.folded()
.iter()
.map(|server| server.name().to_owned())
.collect()
}
/// **Which server is open**, and the first run is a derivation rather than
/// a fix-up: nobody has said, so the aim's own server is open, and with no
/// aim the first by name. A name this seat no longer holds is inert, which
/// is [`crate::place`]'s whole rule about a stale place.
pub fn server_open(&self) -> Option<String> {
let names = self.server_names();
let held = |name: Option<&String>| name.and_then(|name| self.lead_of(name));
held(self.servers.open.as_ref())
.or_else(|| held(self.aim.as_ref().map(|aim| &aim.channel)))
.or_else(|| names.iter().min().cloned())
}
/// **The roster in the order the pane paints it** — [`Servers::order`]
/// read back as the sections themselves, so the paint and the keyboard's
/// cursor track cannot disagree about it.
pub fn server_rows(&self) -> Vec<Server> {
let open = self.server_open();
let mut rows = self.folded();
rows.sort_by_key(|server| self.servers.rank(server.name(), open.as_deref()));
rows
}
/// **Open one server**, which closes whichever was open.
///
/// It aims the workspace last aimed under this server, and its first by
/// `crate::ui::roster::ordered` where this seat has never aimed one — so
/// an open server is never open over nothing. A server holding no workspace
/// this seat can address aims nothing and leaves the aim where it was.
pub fn open_server(&mut self, name: &str) {
self.servers.opening(name);
self.standing = None;
if let Some(aim) = self.opening_aim(name) {
self.aim_at(&aim.channel, &aim.address);
}
}
/// **Fire the row the walk stands on** — Enter's door, the one its click
/// fires (bl-f991): open the server, begin on the workspace, or found.
pub fn fire_stand(&mut self, stand: &crate::ui::Stand) {
match stand {
crate::ui::Stand::Server(name) => return self.open_server(name),
crate::ui::Stand::Begin(aim) => self.begin_at(aim),
crate::ui::Stand::Found(channel) => self.begin_founding(channel),
}
self.chose();
}
/// **Begin a conversation on this workspace** — the whole of what `+ new`
/// under every workspace in the switcher does (§4.42 item 7, which is §4.39's
/// `+` moved under the workspace it begins on: `crate::ui::roster::workspace::NEW`).
///
/// It aims the workspace, which clears the selection, because the composer's
/// start mode is *a workspace and no conversation* (§4.38) and this control is
/// how an operator reaches it without hunting for a way to deselect; and
/// asks for the caret in the box, which is `crate::ui::model::fill`'s own
/// door one box over.
///
/// **The server's opening ranks** even though it is already open — the
/// workspaces stand only under the open server — because a start is a use, and
/// the server an operator just began a conversation on is the one they
/// are using.
///
/// **It crosses no wire and carries no `act:` token** (§4.16): an aim, a
/// selection and a focus are views, and views are out of the parity
/// contract. What the start itself fires is still the composer's own
/// control, still tagged there.
pub fn begin_at(&mut self, aim: &crate::ui::Aim) {
let lead = self.server_of(&aim.channel);
self.servers.opening(&lead);
self.aim_at(&aim.channel, &aim.address);
self.fill = Some(crate::ui::Fill::Goal);
}
/// **Begin founding a workspace** — what the switcher's `+ new workspace`
/// does (§4.42 item 7; bl-673e). The selection goes, because a founding
/// is a start and a start is *no conversation*; the caret is asked for;
/// and the box reads its next Enter as `<name> <goal>`
/// (`crate::ui::composer::found`). Like [`Self::begin_at`] it is a view
/// and crosses no wire.
pub fn begin_founding(&mut self, channel: &str) {
self.deselect();
self.standing = None;
self.founding = Some(channel.to_owned());
self.fill = Some(crate::ui::Fill::Goal);
}
/// **Found it**: the draft's first word is the new workspace's name and
/// the rest its first conversation's goal. The start is the ordinary one
/// aimed at a workspace that does not exist yet — a `prepare` naming an
/// unknown workspace routes to this box's own server (REMOTE §8.2) and
/// founds it there, registering this client (REMOTE §4) — so the receipt
/// selects the conversation exactly as any start's does. A draft with no
/// goal after the name fires nothing, the start's own rule.
///
/// **The aim waits for the `prepare`'s answer** ([`Self::founded`];
/// bl-693d). Aimed at once, the workspace's standing reads went out ahead of
/// the act that registers this client, and the server refused them —
/// *registered in no workspace* — into the bar, where the sentence stood
/// beside the founded workspace until Esc.
pub fn found(&mut self) {
let typed = self.draft.trim().to_owned();
let (Some(_), Some((name, goal))) = (
self.founding.as_ref(),
typed.split_once(char::is_whitespace),
) else {
return;
};
goal.trim().clone_into(&mut self.draft);
self.stage(name);
}
/// **The founding's `prepare` was answered**, however it was answered:
/// the workspace exists and this client is registered in it, so it is aimed
/// now — which puts the founding down — and the start's receipt finds
/// the aim it selects against.
pub(super) fn founded(&mut self, op: &str) {
let (Some(channel), Some(start)) = (self.founding.clone(), self.start.as_ref()) else {
return;
};
if op == crate::verbs::PREPARE {
let address = start.address.clone();
self.aim_at(&channel, &address);
}
}
/// The workspace an opening aims at: the one last aimed under it while this
/// seat can still address it, else its first by `ordered` — down
/// whichever of the server's channels addresses it (`super::fold`).
fn opening_aim(&self, name: &str) -> Option<super::Aim> {
let server = self
.folded()
.into_iter()
.find(|server| server.name() == name)?;
let workspaces: Vec<super::Aim> = crate::ui::roster::ordered(&server.workspaces())
.iter()
.filter_map(|row| server.aim(row))
.collect();
let last = self.servers.aimed.get(name);
workspaces
.iter()
.find(|aim| Some(&aim.address) == last)
.or_else(|| workspaces.first())
.cloned()
}
}
#[cfg(test)]
mod tests;