1use std::fmt;
11use std::io;
12use std::str::FromStr;
13use thiserror::Error as ThisError;
14
15use strum_macros::Display as StrumDisplay;
16
17pub type ReturnCode = u16;
19
20pub type MessageId = u32;
22
23pub type ClientId = u32;
25
26#[derive(Debug, Clone, PartialEq, Eq, Hash)]
28pub enum MessageScope {
29 Last,
31 All,
33 Message(MessageId),
35}
36
37impl fmt::Display for MessageScope {
38 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
39 match self {
40 MessageScope::Last => write!(f, "self"),
41 MessageScope::All => write!(f, "all"),
42 MessageScope::Message(id) => write!(f, "{}", id),
43 }
44 }
45}
46
47#[derive(Debug, Clone, Hash, Eq, PartialEq)]
49pub enum ClientScope {
50 Current,
52 All,
54 Client(ClientId),
56}
57
58impl fmt::Display for ClientScope {
59 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60 match self {
61 ClientScope::Current => write!(f, "self"),
62 ClientScope::All => write!(f, "all"),
63 ClientScope::Client(id) => write!(f, "{}", id),
64 }
65 }
66}
67
68#[derive(StrumDisplay, Debug, Clone, PartialEq, Eq, Hash)]
70pub enum Priority {
71 #[strum(serialize = "progress")]
72 Progress,
73 #[strum(serialize = "notification")]
74 Notification,
75 #[strum(serialize = "message")]
76 Message,
77 #[strum(serialize = "text")]
78 Text,
79 #[strum(serialize = "important")]
80 Important,
81}
82
83#[derive(StrumDisplay, Debug, Clone, PartialEq, Eq, Hash)]
85pub enum PunctuationMode {
86 #[strum(serialize = "none")]
87 None,
88 #[strum(serialize = "some")]
89 Some,
90 #[strum(serialize = "most")]
91 Most,
92 #[strum(serialize = "all")]
93 All,
94}
95
96#[derive(StrumDisplay, Debug, Clone, Hash, Eq, PartialEq)]
98pub enum CapitalLettersRecognitionMode {
99 #[strum(serialize = "none")]
100 None,
101 #[strum(serialize = "spell")]
102 Spell,
103 #[strum(serialize = "icon")]
104 Icon,
105}
106
107#[derive(StrumDisplay, Debug, Clone, PartialEq, Eq, Hash)]
109pub enum KeyName {
110 #[strum(serialize = "space")]
111 Space,
112 #[strum(serialize = "underscore")]
113 Underscore,
114 #[strum(serialize = "double-quote")]
115 DoubleQuote,
116 #[strum(serialize = "alt")]
117 Alt,
118 #[strum(serialize = "control")]
119 Control,
120 #[strum(serialize = "hyper")]
121 Hyper,
122 #[strum(serialize = "meta")]
123 Meta,
124 #[strum(serialize = "shift")]
125 Shift,
126 #[strum(serialize = "super")]
127 Super,
128 #[strum(serialize = "backspace")]
129 Backspace,
130 #[strum(serialize = "break")]
131 Break,
132 #[strum(serialize = "delete")]
133 Delete,
134 #[strum(serialize = "down")]
135 Down,
136 #[strum(serialize = "end")]
137 End,
138 #[strum(serialize = "enter")]
139 Enter,
140 #[strum(serialize = "escape")]
141 Escape,
142 #[strum(serialize = "f1")]
143 F1,
144 #[strum(serialize = "f2")]
145 F2,
146 #[strum(serialize = "f3")]
147 F3,
148 #[strum(serialize = "f4")]
149 F4,
150 #[strum(serialize = "f5")]
151 F5,
152 #[strum(serialize = "f6")]
153 F6,
154 #[strum(serialize = "f7")]
155 F7,
156 #[strum(serialize = "f8")]
157 F8,
158 #[strum(serialize = "f9")]
159 F9,
160 #[strum(serialize = "f10")]
161 F10,
162 #[strum(serialize = "f11")]
163 F11,
164 #[strum(serialize = "f12")]
165 F12,
166 #[strum(serialize = "f13")]
167 F13,
168 #[strum(serialize = "f14")]
169 F14,
170 #[strum(serialize = "f15")]
171 F15,
172 #[strum(serialize = "f16")]
173 F16,
174 #[strum(serialize = "f17")]
175 F17,
176 #[strum(serialize = "f18")]
177 F18,
178 #[strum(serialize = "f19")]
179 F19,
180 #[strum(serialize = "f20")]
181 F20,
182 #[strum(serialize = "f21")]
183 F21,
184 #[strum(serialize = "f22")]
185 F22,
186 #[strum(serialize = "f23")]
187 F23,
188 #[strum(serialize = "f24")]
189 F24,
190 #[strum(serialize = "home")]
191 Home,
192 #[strum(serialize = "insert")]
193 Insert,
194 #[strum(serialize = "kp-*")]
195 KpMultiply,
196 #[strum(serialize = "kp-+")]
197 KpPlus,
198 #[strum(serialize = "kp--")]
199 KpMinus,
200 #[strum(serialize = "kp-.")]
201 KpDot,
202 #[strum(serialize = "kp-/")]
203 KpDivide,
204 #[strum(serialize = "kp-0")]
205 Kp0,
206 #[strum(serialize = "kp-1")]
207 Kp1,
208 #[strum(serialize = "kp-2")]
209 Kp2,
210 #[strum(serialize = "kp-3")]
211 Kp3,
212 #[strum(serialize = "kp-4")]
213 Kp4,
214 #[strum(serialize = "kp-5")]
215 Kp5,
216 #[strum(serialize = "kp-6")]
217 Kp6,
218 #[strum(serialize = "kp-7")]
219 Kp7,
220 #[strum(serialize = "kp-8")]
221 Kp8,
222 #[strum(serialize = "kp-9")]
223 Kp9,
224 #[strum(serialize = "kp-enter")]
225 KpEnter,
226 #[strum(serialize = "left")]
227 Left,
228 #[strum(serialize = "menu")]
229 Menu,
230 #[strum(serialize = "next")]
231 Next,
232 #[strum(serialize = "num-lock")]
233 NumLock,
234 #[strum(serialize = "pause")]
235 Pause,
236 #[strum(serialize = "print")]
237 Print,
238 #[strum(serialize = "prior")]
239 Prior,
240 #[strum(serialize = "return")]
241 Return,
242 #[strum(serialize = "right")]
243 Right,
244 #[strum(serialize = "scroll-lock")]
245 ScrollLock,
246 #[strum(serialize = "tab")]
247 Tab,
248 #[strum(serialize = "up")]
249 Up,
250 #[strum(serialize = "window")]
251 Window,
252}
253
254#[derive(StrumDisplay, Debug, Clone, Hash, Eq, PartialEq)]
256pub enum NotificationType {
257 #[strum(serialize = "begin")]
258 Begin,
259 #[strum(serialize = "end")]
260 End,
261 #[strum(serialize = "cancel")]
262 Cancel,
263 #[strum(serialize = "pause")]
264 Pause,
265 #[strum(serialize = "resume")]
266 Resume,
267 #[strum(serialize = "index_mark")]
268 IndexMark,
269 #[strum(serialize = "all")]
270 All,
271}
272
273#[derive(StrumDisplay, Debug, Clone)]
275pub enum EventType {
276 Begin,
277 End,
278 Cancel,
279 Pause,
280 Resume,
281 IndexMark(String),
282}
283
284#[derive(Debug, Clone, Hash, Eq, PartialEq)]
286pub struct EventId {
287 pub message: String,
289 pub client: String,
291}
292
293impl EventId {
294 pub fn new(message: &str, client: &str) -> Self {
296 Self {
297 message: message.to_string(),
298 client: client.to_string(),
299 }
300 }
301}
302
303#[derive(Debug, Clone)]
305pub struct Event {
306 pub ntype: EventType,
307 pub id: EventId,
308}
309
310impl Event {
311 pub fn new(ntype: EventType, message: &str, client: &str) -> Event {
312 Event {
313 ntype,
314 id: EventId::new(message, client),
315 }
316 }
317
318 pub fn begin(message: &str, client: &str) -> Event {
319 Event::new(EventType::Begin, message, client)
320 }
321
322 pub fn end(message: &str, client: &str) -> Event {
323 Event::new(EventType::End, message, client)
324 }
325
326 pub fn index_mark(mark: String, message: &str, client: &str) -> Event {
327 Event::new(EventType::IndexMark(mark), message, client)
328 }
329
330 pub fn cancel(message: &str, client: &str) -> Event {
331 Event::new(EventType::Cancel, message, client)
332 }
333
334 pub fn pause(message: &str, client: &str) -> Event {
335 Event::new(EventType::Pause, message, client)
336 }
337
338 pub fn resume(message: &str, client: &str) -> Event {
339 Event::new(EventType::Resume, message, client)
340 }
341}
342
343#[derive(Debug, PartialEq, Eq, Clone, Hash)]
345pub struct SynthesisVoice {
346 pub name: String,
347 pub language: Option<String>,
348 pub dialect: Option<String>,
349}
350
351impl SynthesisVoice {
352 pub fn new(name: &str, language: Option<&str>, dialect: Option<&str>) -> SynthesisVoice {
353 SynthesisVoice {
354 name: name.to_string(),
355 language: language.map(|s| s.to_string()),
356 dialect: dialect.map(|s| s.to_string()),
357 }
358 }
359 fn parse_none(token: Option<&str>) -> Option<String> {
361 match token {
362 Some(s) => match s {
363 "none" => None,
364 s => Some(s.to_string()),
365 },
366 None => None,
367 }
368 }
369}
370
371impl FromStr for SynthesisVoice {
372 type Err = ClientError;
373
374 fn from_str(s: &str) -> Result<Self, Self::Err> {
375 let mut iter = s.split('\t');
376 match iter.next() {
377 Some(name) => Ok(SynthesisVoice {
378 name: name.to_string(),
379 language: SynthesisVoice::parse_none(iter.next()),
380 dialect: SynthesisVoice::parse_none(iter.next()),
381 }),
382 None => Err(ClientError::unexpected_eof("missing synthesis voice name")),
383 }
384 }
385}
386
387#[derive(Debug, PartialEq, Eq)]
395pub struct StatusLine {
396 pub code: ReturnCode,
397 pub message: String,
398}
399
400impl fmt::Display for StatusLine {
401 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
402 write!(f, "{} {}", self.code, self.message)
403 }
404}
405#[derive(ThisError, Debug)]
407pub enum ClientError {
408 #[error("I/O: {0}")]
409 Io(io::Error),
410 #[error("Not ready")]
411 NotReady,
412 #[error("SSIP: {0}")]
413 Ssip(StatusLine),
414 #[error("Too few lines")]
415 TooFewLines,
416 #[error("Too many lines")]
417 TooManyLines,
418 #[error("Unexpected status: {0}")]
419 UnexpectedStatus(ReturnCode),
420}
421
422impl ClientError {
423 pub fn io_error(kind: io::ErrorKind, msg: &str) -> Self {
425 Self::Io(io::Error::new(kind, msg))
426 }
427
428 pub fn invalid_data(msg: &str) -> Self {
430 ClientError::io_error(io::ErrorKind::InvalidData, msg)
431 }
432
433 pub fn unexpected_eof(msg: &str) -> Self {
435 ClientError::io_error(io::ErrorKind::UnexpectedEof, msg)
436 }
437}
438
439impl From<io::Error> for ClientError {
440 fn from(err: io::Error) -> Self {
441 if err.kind() == io::ErrorKind::WouldBlock {
442 ClientError::NotReady
443 } else {
444 ClientError::Io(err)
445 }
446 }
447}
448
449pub type ClientResult<T> = Result<T, ClientError>;
451
452pub type ClientStatus = ClientResult<StatusLine>;
454
455#[derive(Debug, Clone, PartialEq, Eq, Hash)]
457pub struct ClientName {
458 pub user: String,
459 pub application: String,
460 pub component: String,
461}
462
463impl ClientName {
464 pub fn new(user: &str, application: &str) -> Self {
465 ClientName::with_component(user, application, "main")
466 }
467
468 pub fn with_component(user: &str, application: &str, component: &str) -> Self {
469 ClientName {
470 user: user.to_string(),
471 application: application.to_string(),
472 component: component.to_string(),
473 }
474 }
475}
476
477#[derive(StrumDisplay, Debug, Clone, PartialEq, Eq, Hash)]
479pub enum CursorDirection {
480 #[strum(serialize = "backward")]
481 Backward,
482 #[strum(serialize = "forward")]
483 Forward,
484}
485
486#[derive(StrumDisplay, Debug, Clone, Eq, PartialEq, Hash)]
488pub enum SortDirection {
489 #[strum(serialize = "asc")]
490 Ascending,
491 #[strum(serialize = "desc")]
492 Descending,
493}
494
495#[derive(StrumDisplay, Debug, Clone, PartialEq, Eq, Hash)]
497pub enum SortKey {
498 #[strum(serialize = "client_name")]
499 ClientName,
500 #[strum(serialize = "priority")]
501 Priority,
502 #[strum(serialize = "message_type")]
503 MessageType,
504 #[strum(serialize = "time")]
505 Time,
506 #[strum(serialize = "user")]
507 User,
508}
509
510#[derive(StrumDisplay, Debug, Clone, PartialEq, Eq, Hash)]
512pub enum Ordering {
513 #[strum(serialize = "text")]
514 Text,
515 #[strum(serialize = "sound_icon")]
516 SoundIcon,
517 #[strum(serialize = "char")]
518 Char,
519 #[strum(serialize = "key")]
520 Key,
521}
522
523#[derive(Debug, Clone, Eq, PartialEq, Hash)]
525pub enum HistoryPosition {
526 First,
527 Last,
528 Pos(u16),
529}
530
531impl fmt::Display for HistoryPosition {
532 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
533 match self {
534 HistoryPosition::First => write!(f, "first"),
535 HistoryPosition::Last => write!(f, "last"),
536 HistoryPosition::Pos(n) => write!(f, "pos {}", n),
537 }
538 }
539}
540
541#[derive(Debug, PartialEq, Eq, Clone, Hash)]
543pub struct HistoryClientStatus {
544 pub id: ClientId,
545 pub name: String,
546 pub connected: bool,
547}
548
549impl HistoryClientStatus {
550 pub fn new(id: ClientId, name: &str, connected: bool) -> Self {
551 Self {
552 id,
553 name: name.to_string(),
554 connected,
555 }
556 }
557}
558
559impl FromStr for HistoryClientStatus {
560 type Err = ClientError;
561
562 fn from_str(s: &str) -> Result<Self, Self::Err> {
563 let mut iter = s.splitn(3, ' ');
564 match iter.next() {
565 Some("") => Err(ClientError::unexpected_eof("expecting client id")),
566 Some(client_id) => match client_id.parse::<u32>() {
567 Ok(id) => match iter.next() {
568 Some(name) => match iter.next() {
569 Some(status) if status == "0" => {
570 Ok(HistoryClientStatus::new(id, name, false))
571 }
572 Some(status) if status == "1" => {
573 Ok(HistoryClientStatus::new(id, name, true))
574 }
575 Some(_) => Err(ClientError::invalid_data("invalid client status")),
576 None => Err(ClientError::unexpected_eof("expecting client status")),
577 },
578 None => Err(ClientError::unexpected_eof("expecting client name")),
579 },
580 Err(_) => Err(ClientError::invalid_data("invalid client id")),
581 },
582 None => Err(ClientError::unexpected_eof("expecting client id")),
583 }
584 }
585}
586
587#[derive(Debug, Clone, Hash, PartialEq, Eq)]
588pub enum Request {
590 SetName(ClientName),
591 Speak,
593 SendLine(String),
594 SendLines(Vec<String>),
595 SpeakChar(char),
596 SpeakKey(KeyName),
597 Stop(MessageScope),
599 Cancel(MessageScope),
600 Pause(MessageScope),
601 Resume(MessageScope),
602 SetPriority(Priority),
604 SetDebug(bool),
605 SetOutputModule(ClientScope, String),
606 GetOutputModule,
607 ListOutputModules,
608 SetLanguage(ClientScope, String),
609 GetLanguage,
610 SetSsmlMode(bool),
611 SetPunctuationMode(ClientScope, PunctuationMode),
612 SetSpelling(ClientScope, bool),
613 SetCapitalLettersRecognitionMode(ClientScope, CapitalLettersRecognitionMode),
614 SetVoiceType(ClientScope, String),
615 GetVoiceType,
616 ListVoiceTypes,
617 SetSynthesisVoice(ClientScope, String),
618 ListSynthesisVoices,
619 SetRate(ClientScope, i8),
620 GetRate,
621 SetPitch(ClientScope, i8),
622 GetPitch,
623 SetVolume(ClientScope, i8),
624 GetVolume,
625 SetPauseContext(ClientScope, u32),
626 SetNotification(NotificationType, bool),
627 Begin,
629 End,
630 SetHistory(ClientScope, bool),
632 HistoryGetClients,
633 HistoryGetClientId,
634 HistoryGetClientMsgs(ClientScope, u32, u32),
635 HistoryGetLastMsgId,
636 HistoryGetMsg(MessageId),
637 HistoryCursorGet,
638 HistoryCursorSet(ClientScope, HistoryPosition),
639 HistoryCursorMove(CursorDirection),
640 HistorySpeak(MessageId),
641 HistorySort(SortDirection, SortKey),
642 HistorySetShortMsgLength(u32),
643 HistorySetMsgTypeOrdering(Vec<Ordering>),
644 HistorySearch(ClientScope, String),
645 Quit,
647}
648
649#[derive(Debug, Clone, Hash, PartialEq, Eq)]
650pub enum Response {
652 LanguageSet, PrioritySet, RateSet, PitchSet, PunctuationSet, CapLetRecognSet, SpellingSet, ClientNameSet, VoiceSet, Stopped, Paused, Resumed, Canceled, TableSet, OutputModuleSet, PauseContextSet, VolumeSet, SsmlModeSet, NotificationSet, PitchRangeSet, DebugSet, HistoryCurSetFirst, HistoryCurSetLast, HistoryCurSetPos, HistoryCurMoveFor, HistoryCurMoveBack, MessageQueued, SoundIconQueued, MessageCanceled, ReceivingData, Bye, HistoryClientListSent(Vec<HistoryClientStatus>), HistoryMsgsListSent(Vec<String>), HistoryLastMsg(String), HistoryCurPosRet(String), TableListSent(Vec<String>), HistoryClientIdSent(ClientId), MessageTextSent, HelpSent(Vec<String>), VoicesListSent(Vec<SynthesisVoice>), OutputModulesListSent(Vec<String>), Get(String), InsideBlock, OutsideBlock, NotImplemented, EventIndexMark(EventId, String), EventBegin(EventId), EventEnd(EventId), EventCanceled(EventId), EventPaused(EventId), EventResumed(EventId), }
704
705#[cfg(test)]
706mod tests {
707
708 use std::io;
709 use std::str::FromStr;
710
711 use super::{ClientError, HistoryClientStatus, HistoryPosition, MessageScope, SynthesisVoice};
712
713 #[test]
714 fn parse_synthesis_voice() {
715 let v1 =
717 SynthesisVoice::from_str("Portuguese (Portugal)+Kaukovalta\tpt\tKaukovalta").unwrap();
718 assert_eq!("Portuguese (Portugal)+Kaukovalta", v1.name);
719 assert_eq!("pt", v1.language.unwrap());
720 assert_eq!("Kaukovalta", v1.dialect.unwrap());
721
722 let v2 = SynthesisVoice::from_str("Esperanto\teo\tnone").unwrap();
724 assert_eq!("Esperanto", v2.name);
725 assert_eq!("eo", v2.language.unwrap());
726 assert!(matches!(v2.dialect, None));
727 }
728
729 #[test]
730 fn format_message_scope() {
731 assert_eq!("self", format!("{}", MessageScope::Last).as_str());
732 assert_eq!("all", format!("{}", MessageScope::All).as_str());
733 assert_eq!("123", format!("{}", MessageScope::Message(123)).as_str());
734 }
735
736 #[test]
737 fn format_history_position() {
738 assert_eq!("first", format!("{}", HistoryPosition::First).as_str());
739 assert_eq!("last", format!("{}", HistoryPosition::Last).as_str());
740 assert_eq!("pos 15", format!("{}", HistoryPosition::Pos(15)).as_str());
741 }
742
743 #[test]
744 fn parse_history_client_status() {
745 assert_eq!(
746 HistoryClientStatus::new(10, "joe:speechd_client:main", false),
747 HistoryClientStatus::from_str("10 joe:speechd_client:main 0").unwrap()
748 );
749 assert_eq!(
750 HistoryClientStatus::new(11, "joe:speechd_client:main", true),
751 HistoryClientStatus::from_str("11 joe:speechd_client:main 1").unwrap()
752 );
753 for line in &[
754 "9 joe:speechd_client:main xxx",
755 "xxx joe:speechd_client:main 1",
756 ] {
757 match HistoryClientStatus::from_str(line) {
758 Ok(_) => panic!("parsing should have failed"),
759 Err(ClientError::Io(err)) if err.kind() == io::ErrorKind::InvalidData => (),
760 Err(_) => panic!("expecting error 'invalid data' parsing \"{}\"", line),
761 }
762 }
763 for line in &["8 joe:speechd_client:main", "8", ""] {
764 match HistoryClientStatus::from_str(line) {
765 Ok(_) => panic!("parsing should have failed"),
766 Err(ClientError::Io(err)) if err.kind() == io::ErrorKind::UnexpectedEof => (),
767 Err(_) => panic!("expecting error 'unexpected EOF' parsing \"{}\"", line),
768 }
769 }
770 }
771}