diff --git a/Cargo.toml b/Cargo.toml index 0ec973a..a9c3722 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,12 +16,17 @@ mio = { version = "0.8", optional = true } thiserror = "1" strum = "0.24" strum_macros = "0.24" +tokio = { version = "^1.21.2", features = ["io-util"] } [features] async-mio = ["mio/net", "mio/os-poll"] +tokio = ["tokio/io-util"] +async-std = ["async-std/default"] [dev-dependencies] mio = { version = "0.8", features = ["os-poll", "os-ext"] } +tokio = { version = "^1.21.2", features = ["io-util"] } +async-std = { version = "1.12.0" } lazy_static = "1" popol = "1" tempfile = "3" diff --git a/src/async_std.rs b/src/async_std.rs new file mode 100644 index 0000000..609d8eb --- /dev/null +++ b/src/async_std.rs @@ -0,0 +1,287 @@ +// ssip-client -- Speech Dispatcher client in Rust +// Copyright (c) 2021-2022 Laurent Pelecq +// +// Licensed under the Apache License, Version 2.0 +// or the MIT +// license , at your +// option. All files in the project carrying such notice may not be copied, +// modified, or distributed except according to those terms. + +use std::io::{self, Read, Write}; + +use crate::constants::*; +use crate::protocol::{ + flush_lines, parse_event_id, parse_single_integer, parse_single_value, parse_typed_lines, + flush_lines_async_std, write_lines_async_std, +}; +use crate::types::*; + +use async_std::io::{AsyncBufRead, AsyncWrite}; + +/// Convert boolean to ON or OFF +fn on_off(value: bool) -> &'static str { + if value { + "on" + } else { + "off" + } +} + +#[derive(Debug, Clone)] +/// Request for SSIP server. +pub enum Request { + SetName(ClientName), + // Speech related requests + Speak, + SendLine(String), + SendLines(Vec), + SpeakChar(char), + SpeakKey(KeyName), + // Flow control + Stop(MessageScope), + Cancel(MessageScope), + Pause(MessageScope), + Resume(MessageScope), + // Setter and getter + SetPriority(Priority), + SetDebug(bool), + SetOutputModule(ClientScope, String), + GetOutputModule, + ListOutputModules, + SetLanguage(ClientScope, String), + GetLanguage, + SetSsmlMode(bool), + SetPunctuationMode(ClientScope, PunctuationMode), + SetSpelling(ClientScope, bool), + SetCapitalLettersRecognitionMode(ClientScope, CapitalLettersRecognitionMode), + SetVoiceType(ClientScope, String), + GetVoiceType, + ListVoiceTypes, + SetSynthesisVoice(ClientScope, String), + ListSynthesisVoices, + SetRate(ClientScope, i8), + GetRate, + SetPitch(ClientScope, i8), + GetPitch, + SetVolume(ClientScope, i8), + GetVolume, + SetPauseContext(ClientScope, u32), + SetNotification(NotificationType, bool), + // Blocks + Begin, + End, + // History + SetHistory(ClientScope, bool), + HistoryGetClients, + HistoryGetClientId, + HistoryGetClientMsgs(ClientScope, u32, u32), + HistoryGetLastMsgId, + HistoryGetMsg(MessageId), + HistoryCursorGet, + HistoryCursorSet(ClientScope, HistoryPosition), + HistoryCursorMove(CursorDirection), + HistorySpeak(MessageId), + HistorySort(SortDirection, SortKey), + HistorySetShortMsgLength(u32), + HistorySetMsgTypeOrdering(Vec), + HistorySearch(ClientScope, String), + // Misc. + Quit, +} + +#[derive(Debug)] +/// Response from SSIP server. +pub enum Response { + LanguageSet, // 201 + PrioritySet, // 202 + RateSet, // 203 + PitchSet, // 204 + PunctuationSet, // 205 + CapLetRecognSet, // 206 + SpellingSet, // 207 + ClientNameSet, // 208 + VoiceSet, // 209 + Stopped, // 210 + Paused, // 211 + Resumed, // 212 + Canceled, // 213 + TableSet, // 215 + OutputModuleSet, // 216 + PauseContextSet, // 217 + VolumeSet, // 218 + SsmlModeSet, // 219 + NotificationSet, // 220 + PitchRangeSet, // 263 + DebugSet, // 262 + HistoryCurSetFirst, // 220 + HistoryCurSetLast, // 221 + HistoryCurSetPos, // 222 + HistoryCurMoveFor, // 223 + HistoryCurMoveBack, // 224 + MessageQueued, // 225, + SoundIconQueued, // 226 + MessageCanceled, // 227 + ReceivingData, // 230 + Bye, // 231 + HistoryClientListSent(Vec), // 240 + HistoryMsgsListSent(Vec), // 241 + HistoryLastMsg(String), // 242 + HistoryCurPosRet(String), // 243 + TableListSent(Vec), // 244 + HistoryClientIdSent(ClientId), // 245 + MessageTextSent, // 246 + HelpSent(Vec), // 248 + VoicesListSent(Vec), // 249 + OutputModulesListSent(Vec), // 250 + Get(String), // 251 + InsideBlock, // 260 + OutsideBlock, // 261 + NotImplemented, // 299 + EventIndexMark(EventId, String), // 700 + EventBegin(EventId), // 701 + EventEnd(EventId), // 702 + EventCanceled(EventId), // 703 + EventPaused(EventId), // 704 + EventResumed(EventId), // 705 +} + +macro_rules! send_one_line { + ($self:expr, $fmt:expr, $( $arg:expr ),+) => { + flush_lines(&mut $self.output, &[format!($fmt, $( $arg ),+).as_str()]) + }; + ($self:expr, $fmt:expr) => { + flush_lines(&mut $self.output, &[$fmt]) + } +} + +macro_rules! send_toggle { + ($output:expr, $fmt:expr, $val:expr) => { + send_one_line!($output, $fmt, on_off($val)) + }; + ($output:expr, $fmt:expr, $arg:expr, $val:expr) => { + send_one_line!($output, $fmt, $arg, on_off($val)) + }; +} + +macro_rules! send_range { + ($output:expr, $fmt:expr, $scope:expr, $val:expr) => { + send_one_line!( + $output, + $fmt, + $scope, + std::cmp::max(-100, std::cmp::min(100, $val)) + ) + }; +} + +/// SSIP client on generic async stream +/// +/// There are two ways to send requests and receive responses: +/// * Either with the generic [`Client::send`] and [`Client::receive`] +/// * Or with the specific methods such as [`Client::set_rate`], ..., [`Client::get_rate`], ... +pub struct AsyncClient { + input: R, + output: W, +} +impl AsyncClient { + pub(crate) fn new(input: R, output: W) -> Self { + Self { input, output } + } + /// Send lines of text (terminated by a single dot). + pub async fn send_lines(&mut self, lines: &[String]) -> ClientResult<&mut Self> { + const END_OF_DATA: [&str; 1] = ["."]; + write_lines_tokio( + &mut self.output, + lines + .iter() + .map(|s| s.as_str()) + .collect::>() + .as_slice(), + ).await?; + flush_lines_tokio(&mut self.output, &END_OF_DATA).await?; + Ok(self) + } + /// Receive answer from server + async fn receive_answer(&mut self, lines: &mut Vec) -> ClientStatus { + crate::protocol::receive_answer_tokio(&mut self.input, Some(lines)).await + } + /// Receive one response. + pub async fn receive(&mut self) -> ClientResult { + const MSG_CURSOR_SET_FIRST: &str = "OK CURSOR SET FIRST"; + let mut lines = Vec::new(); + let status = self.receive_answer(&mut lines).await?; + match status.code { + OK_LANGUAGE_SET => Ok(Response::LanguageSet), + OK_PRIORITY_SET => Ok(Response::PrioritySet), + OK_RATE_SET => Ok(Response::RateSet), + OK_PITCH_SET => Ok(Response::PitchSet), + OK_PUNCTUATION_SET => Ok(Response::PunctuationSet), + OK_CAP_LET_RECOGN_SET => Ok(Response::CapLetRecognSet), + OK_SPELLING_SET => Ok(Response::SpellingSet), + OK_CLIENT_NAME_SET => Ok(Response::ClientNameSet), + OK_VOICE_SET => Ok(Response::VoiceSet), + OK_STOPPED => Ok(Response::Stopped), + OK_PAUSED => Ok(Response::Paused), + OK_RESUMED => Ok(Response::Resumed), + OK_CANCELED => Ok(Response::Canceled), + OK_TABLE_SET => Ok(Response::TableSet), + OK_OUTPUT_MODULE_SET => Ok(Response::OutputModuleSet), + OK_PAUSE_CONTEXT_SET => Ok(Response::PauseContextSet), + OK_VOLUME_SET => Ok(Response::VolumeSet), + OK_SSML_MODE_SET => Ok(Response::SsmlModeSet), + // Warning OK_CUR_SET_FIRST == OK_NOTIFICATION_SET == 220. Matching message to make the difference + OK_NOTIFICATION_SET => { + if status.message == MSG_CURSOR_SET_FIRST { + //OK_CUR_SET_FIRST => Ok(Response::HistoryCurSetFirst) + Ok(Response::HistoryCurSetFirst) + } else { + Ok(Response::NotificationSet) + } + } + OK_CUR_SET_LAST => Ok(Response::HistoryCurSetLast), + OK_CUR_SET_POS => Ok(Response::HistoryCurSetPos), + OK_PITCH_RANGE_SET => Ok(Response::PitchRangeSet), + OK_DEBUG_SET => Ok(Response::DebugSet), + OK_CUR_MOV_FOR => Ok(Response::HistoryCurMoveFor), + OK_CUR_MOV_BACK => Ok(Response::HistoryCurMoveBack), + OK_MESSAGE_QUEUED => Ok(Response::MessageQueued), + OK_SND_ICON_QUEUED => Ok(Response::SoundIconQueued), + OK_MSG_CANCELED => Ok(Response::MessageCanceled), + OK_RECEIVING_DATA => Ok(Response::ReceivingData), + OK_BYE => Ok(Response::Bye), + OK_CLIENTS_LIST_SENT => Ok(Response::HistoryClientListSent(parse_typed_lines::< + HistoryClientStatus, + >(&lines)?)), + OK_MSGS_LIST_SENT => Ok(Response::HistoryMsgsListSent(lines)), + OK_LAST_MSG => Ok(Response::HistoryLastMsg(parse_single_value(&lines)?)), + OK_CUR_POS_RET => Ok(Response::HistoryCurPosRet(parse_single_value(&lines)?)), + OK_TABLE_LIST_SENT => Ok(Response::TableListSent(lines)), + OK_CLIENT_ID_SENT => Ok(Response::HistoryClientIdSent(parse_single_integer(&lines)?)), + OK_MSG_TEXT_SENT => Ok(Response::MessageTextSent), + OK_HELP_SENT => Ok(Response::HelpSent(lines)), + OK_VOICES_LIST_SENT => Ok(Response::VoicesListSent( + parse_typed_lines::(&lines)?, + )), + OK_OUTPUT_MODULES_LIST_SENT => Ok(Response::OutputModulesListSent(lines)), + OK_GET => Ok(Response::Get(parse_single_value(&lines)?)), + OK_INSIDE_BLOCK => Ok(Response::InsideBlock), + OK_OUTSIDE_BLOCK => Ok(Response::OutsideBlock), + OK_NOT_IMPLEMENTED => Ok(Response::NotImplemented), + EVENT_INDEX_MARK => match lines.len() { + 0 | 1 | 2 => Err(ClientError::TooFewLines), + 3 => Ok(Response::EventIndexMark( + parse_event_id(&lines)?, + lines[2].to_owned(), + )), + _ => Err(ClientError::TooManyLines), + }, + EVENT_BEGIN => Ok(Response::EventBegin(parse_event_id(&lines)?)), + EVENT_END => Ok(Response::EventEnd(parse_event_id(&lines)?)), + EVENT_CANCELED => Ok(Response::EventCanceled(parse_event_id(&lines)?)), + EVENT_PAUSED => Ok(Response::EventPaused(parse_event_id(&lines)?)), + EVENT_RESUMED => Ok(Response::EventResumed(parse_event_id(&lines)?)), + _ => panic!("error should have been caught earlier"), + } + } +} + diff --git a/src/client.rs b/src/client.rs index f98ed57..8a83ddf 100644 --- a/src/client.rs +++ b/src/client.rs @@ -12,7 +12,7 @@ use std::io::{self, Read, Write}; use crate::constants::*; use crate::protocol::{ flush_lines, parse_event_id, parse_single_integer, parse_single_value, parse_typed_lines, - write_lines, + write_lines }; use crate::types::*; diff --git a/src/lib.rs b/src/lib.rs index a4383ca..e1f04be 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -42,6 +42,11 @@ pub mod tcp; #[cfg(any(not(feature = "async-mio"), doc))] pub use client::Client; +#[cfg(any(feature = "tokio", doc))] +pub mod tokio; +#[cfg(any(feature = "async-std", doc))] +pub mod async_std; + pub use client::{Request, Response}; pub use constants::*; pub use poll::QueuedClient; diff --git a/src/protocol.rs b/src/protocol.rs index 19d8f0f..457d80a 100644 --- a/src/protocol.rs +++ b/src/protocol.rs @@ -9,6 +9,16 @@ use log::debug; use std::io::{self, BufRead, Write}; + +#[cfg(feature = "tokio")] +use tokio::io::{ + AsyncWrite, AsyncWriteExt, + AsyncBufRead, AsyncBufReadExt, +}; +#[cfg(feature = "async-std")] +use async_std::io::{ + AsyncWrite, AsyncRead, +}; use std::str::FromStr; use crate::types::{ClientError, ClientResult, ClientStatus, EventId, StatusLine}; @@ -73,12 +83,40 @@ pub(crate) fn write_lines(output: &mut dyn Write, lines: &[&str]) -> ClientResul Ok(()) } +/// Write lines (asyncronously) separated by CRLF. +#[cfg(feature = "tokio")] +pub(crate) async fn write_lines_tokio(output: &mut (dyn AsyncWrite + Unpin), lines: &[&str]) -> ClientResult<()> { + for line in lines.iter() { + debug!("SSIP(out): {}", line); + output.write_all(line.as_bytes()).await?; + output.write_all(b"\r\n").await?; + } + Ok(()) +} +/// Write lines (asyncronously) separated by CRLF. +#[cfg(feature = "async-std")] +pub(crate) async fn write_lines_tokio(output: &mut (dyn AsyncWrite + Unpin), lines: &[&str]) -> ClientResult<()> { + for line in lines.iter() { + debug!("SSIP(out): {}", line); + output.write_all(line.as_bytes()).await?; + output.write_all(b"\r\n").await?; + } + Ok(()) +} + /// Write lines separated by CRLF and flush the output. pub(crate) fn flush_lines(output: &mut dyn Write, lines: &[&str]) -> ClientResult<()> { write_lines(output, lines)?; output.flush()?; Ok(()) } +/// Write lines separated by CRLF and flush the output asyncronously. +#[cfg(feature = "tokio")] +pub(crate) async fn flush_lines_tokio(output: &mut (dyn AsyncWrite + Unpin), lines: &[&str]) -> ClientResult<()> { + write_lines_tokio(output, lines).await?; + output.flush().await?; + Ok(()) +} /// Strip prefix if found fn strip_prefix(line: &str, prefix: &str) -> String { @@ -99,6 +137,36 @@ fn parse_status_line(code: u16, line: &str) -> ClientStatus { } /// Read lines from server until a status line is found. +#[cfg(feature = "tokio")] +pub(crate) async fn receive_answer_tokio( + input: &mut (dyn AsyncBufRead + Unpin), + mut lines: Option<&mut Vec>, +) -> ClientStatus { + loop { + let mut line = String::new(); + input.read_line(&mut line).await.map_err(ClientError::Io)?; + debug!("SSIP(in): {}", line.trim_end()); + match line.chars().nth(3) { + Some(ch) => match ch { + ' ' => match line[0..3].parse::() { + Ok(code) => return parse_status_line(code, line[4..].trim_end()), + Err(err) => return Err(invalid_input!(err.to_string())), + }, + '-' => match lines { + Some(ref mut lines) => lines.push(line[4..].trim_end().to_string()), + None => return Err(invalid_input!("unexpected line: {}", line)), + }, + ch => { + return Err(invalid_input!("expecting space or dash, got {}.", ch)); + } + }, + None if line.is_empty() => return Err(invalid_input!("empty line")), + None => return Err(invalid_input!("line too short: {}", line)), + } + } +} + +/// Read lines from server until a status line is found asyncronously. pub(crate) fn receive_answer( input: &mut dyn BufRead, mut lines: Option<&mut Vec>, diff --git a/src/tokio.rs b/src/tokio.rs new file mode 100644 index 0000000..f16bb82 --- /dev/null +++ b/src/tokio.rs @@ -0,0 +1,289 @@ +#[cfg(all(feature = "tokio"))] + +// ssip-client -- Speech Dispatcher client in Rust +// Copyright (c) 2021-2022 Laurent Pelecq +// +// Licensed under the Apache License, Version 2.0 +// or the MIT +// license , at your +// option. All files in the project carrying such notice may not be copied, +// modified, or distributed except according to those terms. + +use std::io::{self, Read, Write}; + +use crate::constants::*; +use crate::protocol::{ + flush_lines, parse_event_id, parse_single_integer, parse_single_value, parse_typed_lines, + flush_lines_tokio, write_lines_tokio, +}; +use crate::types::*; + +use tokio::io::{AsyncBufRead, AsyncWrite}; + +/// Convert boolean to ON or OFF +fn on_off(value: bool) -> &'static str { + if value { + "on" + } else { + "off" + } +} + +#[derive(Debug, Clone)] +/// Request for SSIP server. +pub enum Request { + SetName(ClientName), + // Speech related requests + Speak, + SendLine(String), + SendLines(Vec), + SpeakChar(char), + SpeakKey(KeyName), + // Flow control + Stop(MessageScope), + Cancel(MessageScope), + Pause(MessageScope), + Resume(MessageScope), + // Setter and getter + SetPriority(Priority), + SetDebug(bool), + SetOutputModule(ClientScope, String), + GetOutputModule, + ListOutputModules, + SetLanguage(ClientScope, String), + GetLanguage, + SetSsmlMode(bool), + SetPunctuationMode(ClientScope, PunctuationMode), + SetSpelling(ClientScope, bool), + SetCapitalLettersRecognitionMode(ClientScope, CapitalLettersRecognitionMode), + SetVoiceType(ClientScope, String), + GetVoiceType, + ListVoiceTypes, + SetSynthesisVoice(ClientScope, String), + ListSynthesisVoices, + SetRate(ClientScope, i8), + GetRate, + SetPitch(ClientScope, i8), + GetPitch, + SetVolume(ClientScope, i8), + GetVolume, + SetPauseContext(ClientScope, u32), + SetNotification(NotificationType, bool), + // Blocks + Begin, + End, + // History + SetHistory(ClientScope, bool), + HistoryGetClients, + HistoryGetClientId, + HistoryGetClientMsgs(ClientScope, u32, u32), + HistoryGetLastMsgId, + HistoryGetMsg(MessageId), + HistoryCursorGet, + HistoryCursorSet(ClientScope, HistoryPosition), + HistoryCursorMove(CursorDirection), + HistorySpeak(MessageId), + HistorySort(SortDirection, SortKey), + HistorySetShortMsgLength(u32), + HistorySetMsgTypeOrdering(Vec), + HistorySearch(ClientScope, String), + // Misc. + Quit, +} + +#[derive(Debug)] +/// Response from SSIP server. +pub enum Response { + LanguageSet, // 201 + PrioritySet, // 202 + RateSet, // 203 + PitchSet, // 204 + PunctuationSet, // 205 + CapLetRecognSet, // 206 + SpellingSet, // 207 + ClientNameSet, // 208 + VoiceSet, // 209 + Stopped, // 210 + Paused, // 211 + Resumed, // 212 + Canceled, // 213 + TableSet, // 215 + OutputModuleSet, // 216 + PauseContextSet, // 217 + VolumeSet, // 218 + SsmlModeSet, // 219 + NotificationSet, // 220 + PitchRangeSet, // 263 + DebugSet, // 262 + HistoryCurSetFirst, // 220 + HistoryCurSetLast, // 221 + HistoryCurSetPos, // 222 + HistoryCurMoveFor, // 223 + HistoryCurMoveBack, // 224 + MessageQueued, // 225, + SoundIconQueued, // 226 + MessageCanceled, // 227 + ReceivingData, // 230 + Bye, // 231 + HistoryClientListSent(Vec), // 240 + HistoryMsgsListSent(Vec), // 241 + HistoryLastMsg(String), // 242 + HistoryCurPosRet(String), // 243 + TableListSent(Vec), // 244 + HistoryClientIdSent(ClientId), // 245 + MessageTextSent, // 246 + HelpSent(Vec), // 248 + VoicesListSent(Vec), // 249 + OutputModulesListSent(Vec), // 250 + Get(String), // 251 + InsideBlock, // 260 + OutsideBlock, // 261 + NotImplemented, // 299 + EventIndexMark(EventId, String), // 700 + EventBegin(EventId), // 701 + EventEnd(EventId), // 702 + EventCanceled(EventId), // 703 + EventPaused(EventId), // 704 + EventResumed(EventId), // 705 +} + +macro_rules! send_one_line { + ($self:expr, $fmt:expr, $( $arg:expr ),+) => { + flush_lines(&mut $self.output, &[format!($fmt, $( $arg ),+).as_str()]) + }; + ($self:expr, $fmt:expr) => { + flush_lines(&mut $self.output, &[$fmt]) + } +} + +macro_rules! send_toggle { + ($output:expr, $fmt:expr, $val:expr) => { + send_one_line!($output, $fmt, on_off($val)) + }; + ($output:expr, $fmt:expr, $arg:expr, $val:expr) => { + send_one_line!($output, $fmt, $arg, on_off($val)) + }; +} + +macro_rules! send_range { + ($output:expr, $fmt:expr, $scope:expr, $val:expr) => { + send_one_line!( + $output, + $fmt, + $scope, + std::cmp::max(-100, std::cmp::min(100, $val)) + ) + }; +} + +/// SSIP client on generic async stream +/// +/// There are two ways to send requests and receive responses: +/// * Either with the generic [`Client::send`] and [`Client::receive`] +/// * Or with the specific methods such as [`Client::set_rate`], ..., [`Client::get_rate`], ... +pub struct AsyncClient { + input: R, + output: W, +} +impl AsyncClient { + pub(crate) fn new(input: R, output: W) -> Self { + Self { input, output } + } + /// Send lines of text (terminated by a single dot). + pub async fn send_lines(&mut self, lines: &[String]) -> ClientResult<&mut Self> { + const END_OF_DATA: [&str; 1] = ["."]; + write_lines_tokio( + &mut self.output, + lines + .iter() + .map(|s| s.as_str()) + .collect::>() + .as_slice(), + ).await?; + flush_lines_tokio(&mut self.output, &END_OF_DATA).await?; + Ok(self) + } + /// Receive answer from server + async fn receive_answer(&mut self, lines: &mut Vec) -> ClientStatus { + crate::protocol::receive_answer_tokio(&mut self.input, Some(lines)).await + } + /// Receive one response. + pub async fn receive(&mut self) -> ClientResult { + const MSG_CURSOR_SET_FIRST: &str = "OK CURSOR SET FIRST"; + let mut lines = Vec::new(); + let status = self.receive_answer(&mut lines).await?; + match status.code { + OK_LANGUAGE_SET => Ok(Response::LanguageSet), + OK_PRIORITY_SET => Ok(Response::PrioritySet), + OK_RATE_SET => Ok(Response::RateSet), + OK_PITCH_SET => Ok(Response::PitchSet), + OK_PUNCTUATION_SET => Ok(Response::PunctuationSet), + OK_CAP_LET_RECOGN_SET => Ok(Response::CapLetRecognSet), + OK_SPELLING_SET => Ok(Response::SpellingSet), + OK_CLIENT_NAME_SET => Ok(Response::ClientNameSet), + OK_VOICE_SET => Ok(Response::VoiceSet), + OK_STOPPED => Ok(Response::Stopped), + OK_PAUSED => Ok(Response::Paused), + OK_RESUMED => Ok(Response::Resumed), + OK_CANCELED => Ok(Response::Canceled), + OK_TABLE_SET => Ok(Response::TableSet), + OK_OUTPUT_MODULE_SET => Ok(Response::OutputModuleSet), + OK_PAUSE_CONTEXT_SET => Ok(Response::PauseContextSet), + OK_VOLUME_SET => Ok(Response::VolumeSet), + OK_SSML_MODE_SET => Ok(Response::SsmlModeSet), + // Warning OK_CUR_SET_FIRST == OK_NOTIFICATION_SET == 220. Matching message to make the difference + OK_NOTIFICATION_SET => { + if status.message == MSG_CURSOR_SET_FIRST { + //OK_CUR_SET_FIRST => Ok(Response::HistoryCurSetFirst) + Ok(Response::HistoryCurSetFirst) + } else { + Ok(Response::NotificationSet) + } + } + OK_CUR_SET_LAST => Ok(Response::HistoryCurSetLast), + OK_CUR_SET_POS => Ok(Response::HistoryCurSetPos), + OK_PITCH_RANGE_SET => Ok(Response::PitchRangeSet), + OK_DEBUG_SET => Ok(Response::DebugSet), + OK_CUR_MOV_FOR => Ok(Response::HistoryCurMoveFor), + OK_CUR_MOV_BACK => Ok(Response::HistoryCurMoveBack), + OK_MESSAGE_QUEUED => Ok(Response::MessageQueued), + OK_SND_ICON_QUEUED => Ok(Response::SoundIconQueued), + OK_MSG_CANCELED => Ok(Response::MessageCanceled), + OK_RECEIVING_DATA => Ok(Response::ReceivingData), + OK_BYE => Ok(Response::Bye), + OK_CLIENTS_LIST_SENT => Ok(Response::HistoryClientListSent(parse_typed_lines::< + HistoryClientStatus, + >(&lines)?)), + OK_MSGS_LIST_SENT => Ok(Response::HistoryMsgsListSent(lines)), + OK_LAST_MSG => Ok(Response::HistoryLastMsg(parse_single_value(&lines)?)), + OK_CUR_POS_RET => Ok(Response::HistoryCurPosRet(parse_single_value(&lines)?)), + OK_TABLE_LIST_SENT => Ok(Response::TableListSent(lines)), + OK_CLIENT_ID_SENT => Ok(Response::HistoryClientIdSent(parse_single_integer(&lines)?)), + OK_MSG_TEXT_SENT => Ok(Response::MessageTextSent), + OK_HELP_SENT => Ok(Response::HelpSent(lines)), + OK_VOICES_LIST_SENT => Ok(Response::VoicesListSent( + parse_typed_lines::(&lines)?, + )), + OK_OUTPUT_MODULES_LIST_SENT => Ok(Response::OutputModulesListSent(lines)), + OK_GET => Ok(Response::Get(parse_single_value(&lines)?)), + OK_INSIDE_BLOCK => Ok(Response::InsideBlock), + OK_OUTSIDE_BLOCK => Ok(Response::OutsideBlock), + OK_NOT_IMPLEMENTED => Ok(Response::NotImplemented), + EVENT_INDEX_MARK => match lines.len() { + 0 | 1 | 2 => Err(ClientError::TooFewLines), + 3 => Ok(Response::EventIndexMark( + parse_event_id(&lines)?, + lines[2].to_owned(), + )), + _ => Err(ClientError::TooManyLines), + }, + EVENT_BEGIN => Ok(Response::EventBegin(parse_event_id(&lines)?)), + EVENT_END => Ok(Response::EventEnd(parse_event_id(&lines)?)), + EVENT_CANCELED => Ok(Response::EventCanceled(parse_event_id(&lines)?)), + EVENT_PAUSED => Ok(Response::EventPaused(parse_event_id(&lines)?)), + EVENT_RESUMED => Ok(Response::EventResumed(parse_event_id(&lines)?)), + _ => panic!("error should have been caught earlier"), + } + } +} +