Skip to main content

sayd_core/
queue.rs

1//! The utterance queue and the policy that governs what a new submission does
2//! to whatever is already speaking.
3//!
4//! `Interrupt` and `Front` differ only in what happens to the *current*
5//! utterance, which this type does not own -- the engine does. Both put the
6//! new utterance at the head; the engine additionally stops the current one
7//! for `Interrupt`.
8
9use std::collections::VecDeque;
10
11#[derive(Copy, Clone, Debug, PartialEq, Eq)]
12pub enum Policy {
13    /// Play after everything already queued.
14    Enqueue,
15    /// Stop the current utterance and play next. Pending entries survive.
16    Interrupt,
17    /// Drop everything, current and pending, and play this alone.
18    Replace,
19    /// Play next, but let the current utterance finish first.
20    Front,
21}
22
23#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
24pub enum Source {
25    /// The default: anything arriving over the bus behaves like agent narration.
26    #[default]
27    DBus,
28    Cli,
29    Hotkey,
30    Notification,
31}
32
33impl Source {
34    /// Per-source default from the spec. Agent narration over D-Bus must play
35    /// in order and never talk over itself; a hotkey means "read *this* now";
36    /// a notification should be timely without destroying what is playing.
37    pub fn default_policy(self) -> Policy {
38        match self {
39            Source::DBus | Source::Cli => Policy::Enqueue,
40            Source::Hotkey => Policy::Replace,
41            Source::Notification => Policy::Front,
42        }
43    }
44}
45
46#[derive(Clone, Debug, PartialEq)]
47pub struct Utterance {
48    pub id: u64,
49    pub text: String,
50    pub voice: String,
51    pub speed: f32,
52    pub source: Source,
53}
54
55pub struct Queue {
56    items: VecDeque<Utterance>,
57    next_id: u64,
58}
59
60impl Queue {
61    pub fn new() -> Self {
62        Queue { items: VecDeque::new(), next_id: 1 }
63    }
64
65    pub fn next_id(&mut self) -> u64 {
66        let id = self.next_id;
67        self.next_id += 1;
68        id
69    }
70
71    /// Apply `policy` and add `u`. Returns the ids of any pending utterances
72    /// dropped as a result; the caller is responsible for the current one.
73    pub fn submit(&mut self, u: Utterance, policy: Policy) -> Vec<u64> {
74        let mut dropped = Vec::new();
75        match policy {
76            Policy::Enqueue => self.items.push_back(u),
77            Policy::Interrupt | Policy::Front => self.items.push_front(u),
78            Policy::Replace => {
79                dropped = self.clear();
80                self.items.push_back(u);
81            }
82        }
83        dropped
84    }
85
86    pub fn pop_front(&mut self) -> Option<Utterance> {
87        self.items.pop_front()
88    }
89
90    pub fn clear(&mut self) -> Vec<u64> {
91        let ids = self.items.iter().map(|u| u.id).collect();
92        self.items.clear();
93        ids
94    }
95
96    pub fn cancel(&mut self, id: u64) -> bool {
97        let before = self.items.len();
98        self.items.retain(|u| u.id != id);
99        self.items.len() != before
100    }
101
102    pub fn len(&self) -> usize {
103        self.items.len()
104    }
105
106    pub fn is_empty(&self) -> bool {
107        self.items.is_empty()
108    }
109
110    pub fn iter(&self) -> impl Iterator<Item = &Utterance> {
111        self.items.iter()
112    }
113}
114
115impl Default for Queue {
116    fn default() -> Self {
117        Self::new()
118    }
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124
125    fn utt(id: u64, text: &str) -> Utterance {
126        Utterance {
127            id,
128            text: text.into(),
129            voice: "af_heart".into(),
130            speed: 1.0,
131            source: Source::DBus,
132        }
133    }
134
135    fn texts(q: &Queue) -> Vec<String> {
136        q.iter().map(|u| u.text.clone()).collect()
137    }
138
139    #[test]
140    fn enqueue_appends_in_order() {
141        let mut q = Queue::new();
142        q.submit(utt(1, "a"), Policy::Enqueue);
143        q.submit(utt(2, "b"), Policy::Enqueue);
144        assert_eq!(texts(&q), vec!["a", "b"]);
145    }
146
147    #[test]
148    fn front_jumps_the_line_without_dropping_anything() {
149        let mut q = Queue::new();
150        q.submit(utt(1, "a"), Policy::Enqueue);
151        q.submit(utt(2, "b"), Policy::Enqueue);
152        let dropped = q.submit(utt(3, "urgent"), Policy::Front);
153        assert!(dropped.is_empty());
154        assert_eq!(texts(&q), vec!["urgent", "a", "b"]);
155    }
156
157    #[test]
158    fn interrupt_jumps_the_line_and_keeps_pending() {
159        let mut q = Queue::new();
160        q.submit(utt(1, "a"), Policy::Enqueue);
161        let dropped = q.submit(utt(2, "now"), Policy::Interrupt);
162        assert!(dropped.is_empty(), "interrupt drops the *current* utterance, not the queue");
163        assert_eq!(texts(&q), vec!["now", "a"]);
164    }
165
166    #[test]
167    fn replace_clears_everything_pending() {
168        let mut q = Queue::new();
169        q.submit(utt(1, "a"), Policy::Enqueue);
170        q.submit(utt(2, "b"), Policy::Enqueue);
171        let dropped = q.submit(utt(3, "only"), Policy::Replace);
172        assert_eq!(dropped, vec![1, 2]);
173        assert_eq!(texts(&q), vec!["only"]);
174    }
175
176    #[test]
177    fn source_defaults_match_the_spec() {
178        assert_eq!(Source::DBus.default_policy(), Policy::Enqueue);
179        assert_eq!(Source::Cli.default_policy(), Policy::Enqueue);
180        assert_eq!(Source::Hotkey.default_policy(), Policy::Replace);
181        assert_eq!(Source::Notification.default_policy(), Policy::Front);
182    }
183
184    #[test]
185    fn cancel_removes_one_by_id() {
186        let mut q = Queue::new();
187        q.submit(utt(1, "a"), Policy::Enqueue);
188        q.submit(utt(2, "b"), Policy::Enqueue);
189        assert!(q.cancel(1));
190        assert_eq!(texts(&q), vec!["b"]);
191        assert!(!q.cancel(99), "cancelling an unknown id reports false");
192    }
193
194    #[test]
195    fn clear_returns_the_dropped_ids() {
196        let mut q = Queue::new();
197        q.submit(utt(1, "a"), Policy::Enqueue);
198        q.submit(utt(2, "b"), Policy::Enqueue);
199        assert_eq!(q.clear(), vec![1, 2]);
200        assert!(q.is_empty());
201    }
202
203    #[test]
204    fn pop_front_drains_in_order() {
205        let mut q = Queue::new();
206        q.submit(utt(1, "a"), Policy::Enqueue);
207        q.submit(utt(2, "b"), Policy::Enqueue);
208        assert_eq!(q.pop_front().map(|u| u.text), Some("a".into()));
209        assert_eq!(q.pop_front().map(|u| u.text), Some("b".into()));
210        assert_eq!(q.pop_front(), None);
211    }
212
213    #[test]
214    fn ids_are_unique_and_increasing() {
215        let mut q = Queue::new();
216        let a = q.next_id();
217        let b = q.next_id();
218        assert!(b > a);
219    }
220
221    #[test]
222    fn default_and_new_agree_and_start_at_one() {
223        let mut q_default = Queue::default();
224        let mut q_new = Queue::new();
225        let id_default = q_default.next_id();
226        let id_new = q_new.next_id();
227        assert_eq!(id_default, id_new, "Queue::default() and Queue::new() should produce same first id");
228        assert_ne!(id_default, 0, "first id must not be 0 (sentinel for no utterance)");
229    }
230}