arci_speak_cmd/
lib.rs

1#![doc = include_str!("../README.md")]
2#![warn(missing_docs)]
3
4use std::{io, process::Command};
5
6use arci::{Speaker, WaitFuture};
7
8/// A [`Speaker`] implementation using a local command.
9///
10/// Currently, this uses the following command:
11///
12/// - On macOS, use `say` command.
13/// - On Windows, call [SAPI] via PowerShell.
14/// - On others, use `espeak` command.
15///
16/// **Disclaimer**: These commands might change over time.
17///
18/// [SAPI]: https://en.wikipedia.org/wiki/Microsoft_Speech_API
19#[derive(Debug, Default)]
20#[non_exhaustive]
21pub struct LocalCommand {}
22
23impl LocalCommand {
24    /// Creates a new `LocalCommand`.
25    pub fn new() -> Self {
26        Self::default()
27    }
28}
29
30impl Speaker for LocalCommand {
31    fn speak(&self, message: &str) -> Result<WaitFuture, arci::Error> {
32        let (sender, receiver) = tokio::sync::oneshot::channel();
33        let message = message.to_string();
34
35        std::thread::spawn(move || {
36            let res = run_local_command(&message).map_err(|e| arci::Error::Other(e.into()));
37            let _ = sender.send(res);
38        });
39
40        Ok(WaitFuture::new(async move {
41            receiver.await.map_err(|e| arci::Error::Other(e.into()))?
42        }))
43    }
44}
45
46#[cfg(not(windows))]
47fn run_local_command(message: &str) -> io::Result<()> {
48    #[cfg(not(target_os = "macos"))]
49    const CMD_NAME: &str = "espeak";
50    #[cfg(target_os = "macos")]
51    const CMD_NAME: &str = "say";
52
53    let mut cmd = Command::new(CMD_NAME);
54    let status = cmd.arg(message).status()?;
55
56    if status.success() {
57        Ok(())
58    } else {
59        Err(io::Error::other(format!(
60            "failed to run `{CMD_NAME}` with message {message:?}"
61        )))
62    }
63}
64
65#[cfg(windows)]
66fn run_local_command(message: &str) -> io::Result<()> {
67    // TODO: Ideally, it would be more efficient to use SAPI directly via winapi or something.
68    // https://stackoverflow.com/questions/1040655/ms-speech-from-command-line
69    let cmd = format!("PowerShell -Command \"Add-Type –AssemblyName System.Speech; (New-Object System.Speech.Synthesis.SpeechSynthesizer).Speak('{message}');\"");
70    let status = Command::new("powershell").arg(cmd).status()?;
71
72    if status.success() {
73        Ok(())
74    } else {
75        Err(io::Error::other(format!(
76            "failed to run `powershell` with message {message:?}"
77        )))
78    }
79}
80
81#[cfg(test)]
82mod test {
83    use super::*;
84
85    #[test]
86    fn test_local_command() {
87        let local_command = LocalCommand::new();
88
89        let wait = local_command.speak("message");
90
91        assert!(wait.is_ok());
92    }
93}