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)]
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)]
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)]
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)]
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)]
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)]
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)]
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)]
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)]
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)]
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(crate) fn io_error(kind: io::ErrorKind, msg: &str) -> Self {
425 Self::Io(io::Error::new(kind, msg))
426 }
427
428 pub(crate) fn invalid_data(msg: &str) -> Self {
430 ClientError::io_error(io::ErrorKind::InvalidData, msg)
431 }
432
433 pub(crate) 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)]
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)]
479pub enum CursorDirection {
480 #[strum(serialize = "backward")]
481 Backward,
482 #[strum(serialize = "forward")]
483 Forward,
484}
485
486#[derive(StrumDisplay, Debug, Clone)]
488pub enum SortDirection {
489 #[strum(serialize = "asc")]
490 Ascending,
491 #[strum(serialize = "desc")]
492 Descending,
493}
494
495#[derive(StrumDisplay, Debug, Clone)]
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)]
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)]
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)]
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("0") => Ok(HistoryClientStatus::new(id, name, false)),
570 Some("1") => Ok(HistoryClientStatus::new(id, name, true)),
571 Some(_) => Err(ClientError::invalid_data("invalid client status")),
572 None => Err(ClientError::unexpected_eof("expecting client status")),
573 },
574 None => Err(ClientError::unexpected_eof("expecting client name")),
575 },
576 Err(_) => Err(ClientError::invalid_data("invalid client id")),
577 },
578 None => Err(ClientError::unexpected_eof("expecting client id")),
579 }
580 }
581}
582
583#[cfg(test)]
584mod tests {
585
586 use std::io;
587 use std::str::FromStr;
588
589 use super::{ClientError, HistoryClientStatus, HistoryPosition, MessageScope, SynthesisVoice};
590
591 #[test]
592 fn parse_synthesis_voice() {
593 let v1 =
595 SynthesisVoice::from_str("Portuguese (Portugal)+Kaukovalta\tpt\tKaukovalta").unwrap();
596 assert_eq!("Portuguese (Portugal)+Kaukovalta", v1.name);
597 assert_eq!("pt", v1.language.unwrap());
598 assert_eq!("Kaukovalta", v1.dialect.unwrap());
599
600 let v2 = SynthesisVoice::from_str("Esperanto\teo\tnone").unwrap();
602 assert_eq!("Esperanto", v2.name);
603 assert_eq!("eo", v2.language.unwrap());
604 assert!(v2.dialect.is_none());
605 }
606
607 #[test]
608 fn format_message_scope() {
609 assert_eq!("self", format!("{}", MessageScope::Last).as_str());
610 assert_eq!("all", format!("{}", MessageScope::All).as_str());
611 assert_eq!("123", format!("{}", MessageScope::Message(123)).as_str());
612 }
613
614 #[test]
615 fn format_history_position() {
616 assert_eq!("first", format!("{}", HistoryPosition::First).as_str());
617 assert_eq!("last", format!("{}", HistoryPosition::Last).as_str());
618 assert_eq!("pos 15", format!("{}", HistoryPosition::Pos(15)).as_str());
619 }
620
621 #[test]
622 fn parse_history_client_status() {
623 assert_eq!(
624 HistoryClientStatus::new(10, "joe:speechd_client:main", false),
625 HistoryClientStatus::from_str("10 joe:speechd_client:main 0").unwrap()
626 );
627 assert_eq!(
628 HistoryClientStatus::new(11, "joe:speechd_client:main", true),
629 HistoryClientStatus::from_str("11 joe:speechd_client:main 1").unwrap()
630 );
631 for line in &[
632 "9 joe:speechd_client:main xxx",
633 "xxx joe:speechd_client:main 1",
634 ] {
635 match HistoryClientStatus::from_str(line) {
636 Ok(_) => panic!("parsing should have failed"),
637 Err(ClientError::Io(err)) if err.kind() == io::ErrorKind::InvalidData => (),
638 Err(_) => panic!("expecting error 'invalid data' parsing \"{}\"", line),
639 }
640 }
641 for line in &["8 joe:speechd_client:main", "8", ""] {
642 match HistoryClientStatus::from_str(line) {
643 Ok(_) => panic!("parsing should have failed"),
644 Err(ClientError::Io(err)) if err.kind() == io::ErrorKind::UnexpectedEof => (),
645 Err(_) => panic!("expecting error 'unexpected EOF' parsing \"{}\"", line),
646 }
647 }
648 }
649}