forge_server/surface/dictate.rs
1//! `dictate()`: models and preflight state.
2
3use std::path::PathBuf;
4
5use forge_primitives::SessionSlot;
6use forge_workspace::DictateSnapshot;
7
8use super::ViewSurface;
9
10/// Dictation's preflight state: what the preflight screen draws.
11pub struct DictateView {
12 /// Whether `[dictate] enabled` is set. **Carried rather than inferred
13 /// from an empty `models` list**: the list is empty for a switched-off
14 /// section AND for a `DictateSnapshot::default()`, so a reader asserting
15 /// the cause from the value reports a healthy configuration as switched
16 /// off - or a switched-off one as healthy - with nothing to say which.
17 pub enabled: bool,
18 /// Per-model fetch and load progress.
19 pub snapshot: DictateSnapshot,
20 /// Where the dictation models land. `None` when the platform has no
21 /// usable cache directory and none was configured.
22 pub models_dir: Option<PathBuf>,
23}
24
25impl ViewSurface {
26 /// The connection that was streaming a take for `key` has gone: drop
27 /// the take it started and free the seat for the next one.
28 ///
29 /// A connection's own lifecycle rather than a user action, which is why
30 /// it is a verb here and not a command: nothing a view dispatches means
31 /// "the client went away".
32 pub fn dictate_close(&self, key: &SessionSlot, initiator: u64) -> bool {
33 self.workspace.dictate_close(key, initiator)
34 }
35
36 /// The axes a client that captures starts on and resets to: the
37 /// `[dictate]` keys over the crate's own defaults.
38 pub fn dictate_axes(&self) -> forge_workspace::DictateAxes {
39 self.workspace.dictate_axes()
40 }
41
42 /// Push one frame of client-captured audio into a seat's live take,
43 /// answering whether the samples were kept. The take must belong to
44 /// `initiator`: a seat's live take can be another connection's, and its
45 /// audio is not the pushing connection's to feed.
46 ///
47 /// A data plane rather than a command: a client sends this ~50 times a
48 /// second for as long as it talks, and the command bus's dispatch is
49 /// for the actions a view takes on the user's behalf, not for the
50 /// audio itself.
51 pub fn dictate_push(&self, key: &SessionSlot, samples: &[f32], initiator: Option<u64>) -> bool {
52 self.workspace.dictate_push(key, samples, initiator)
53 }
54
55 /// [`Self::dictate_push`] for the read-aloud recording, which belongs to
56 /// no seat: the same data plane, kept only where the recording is the
57 /// pushing connection's.
58 pub fn dictate_read_aloud_push(&self, samples: &[f32], initiator: Option<u64>) -> bool {
59 self.workspace.read_aloud_push(samples, initiator)
60 }
61
62 pub fn dictate(&self) -> DictateView {
63 DictateView {
64 enabled: self.workspace.dictate_enabled(),
65 snapshot: self.workspace.dictate_snapshot(),
66 models_dir: self.workspace.dictate_models_dir(),
67 }
68 }
69
70 /// The models page's whole read: the pinned models, the catalogue
71 /// check, what the feed proposes, and every row it carries.
72 ///
73 /// Read when the page subscribes and again from each
74 /// [`forge_workspace::SessionUpdate::DictateModelsChanged`]; the
75 /// check it reports on is started by the core at boot and by
76 /// `Command::DictateCatalogueCheck`.
77 pub fn dictate_models(&self) -> forge_workspace::catalogue::DictateModelsSnapshot {
78 self.workspace.dictate_models()
79 }
80
81 /// The `[dictate] bind` key, as `forge.toml` declares it. A client
82 /// draws the keyboard the record carries rather than hardcoding one.
83 pub fn dictate_bind(&self) -> forge_workspace::DictateBind {
84 self.workspace.dictate_bind()
85 }
86
87 /// How a press maps onto a take, as `forge.toml` declares it.
88 pub fn dictate_mode(&self) -> forge_workspace::DictateMode {
89 self.workspace.dictate_mode()
90 }
91}
92
93#[cfg(test)]
94mod tests {
95 use std::sync::Arc;
96
97 use super::*;
98
99 /// Catches either field being read from a source other than the call
100 /// it replaces - the models directory comes off the config rather
101 /// than the platform default, so bypassing it leaves the first-run
102 /// note naming the wrong path.
103 #[test]
104 fn dictate_agrees_with_the_calls_it_replaces() {
105 let (workspace, _dir) = crate::surface::testing::workspace();
106
107 let view = ViewSurface::new(Arc::clone(&workspace)).dictate();
108
109 assert_eq!(view.snapshot, workspace.dictate_snapshot(), "the snapshot is the call's own");
110 assert_eq!(view.snapshot.models.len(), 2, "dictation is on, so the snapshot is not empty");
111 assert_eq!(
112 view.models_dir,
113 workspace.dictate_models_dir(),
114 "the models directory is the config's answer",
115 );
116 assert_eq!(
117 view.models_dir,
118 Some(PathBuf::from("/tmp/forge-dictate-models")),
119 "the configured directory is the one reported, not the platform default",
120 );
121 }
122}