1#![doc = include_str!("../README.md")]
2#![warn(missing_docs, rust_2018_idioms)]
3#![allow(clippy::derive_partial_eq_without_eq)]
5
6use std::{io, process::Command};
7
8use arci::{Speaker, WaitFuture};
9
10#[derive(Debug, Default)]
22#[non_exhaustive]
23pub struct LocalCommand {}
24
25impl LocalCommand {
26 pub fn new() -> Self {
28 Self::default()
29 }
30}
31
32impl Speaker for LocalCommand {
33 fn speak(&self, message: &str) -> Result<WaitFuture, arci::Error> {
34 let (sender, receiver) = tokio::sync::oneshot::channel();
35 let message = message.to_string();
36
37 std::thread::spawn(move || {
38 let res = run_local_command(&message).map_err(|e| arci::Error::Other(e.into()));
39 let _ = sender.send(res);
40 });
41
42 Ok(WaitFuture::new(async move {
43 receiver.await.map_err(|e| arci::Error::Other(e.into()))?
44 }))
45 }
46}
47
48#[cfg(not(windows))]
49fn run_local_command(message: &str) -> io::Result<()> {
50 #[cfg(not(target_os = "macos"))]
51 const CMD_NAME: &str = "espeak";
52 #[cfg(target_os = "macos")]
53 const CMD_NAME: &str = "say";
54
55 let mut cmd = Command::new(CMD_NAME);
56 let status = cmd.arg(message).status()?;
57
58 if status.success() {
59 Ok(())
60 } else {
61 Err(io::Error::new(
62 io::ErrorKind::Other,
63 format!("failed to run `{CMD_NAME}` with message {message:?}"),
64 ))
65 }
66}
67
68#[cfg(windows)]
69fn run_local_command(message: &str) -> io::Result<()> {
70 let cmd = format!("PowerShell -Command \"Add-Type –AssemblyName System.Speech; (New-Object System.Speech.Synthesis.SpeechSynthesizer).Speak('{message}');\"");
73 let status = Command::new("powershell").arg(cmd).status()?;
74
75 if status.success() {
76 Ok(())
77 } else {
78 Err(io::Error::new(
79 io::ErrorKind::Other,
80 format!("failed to run `powershell` with message {message:?}"),
81 ))
82 }
83}
84
85#[cfg(test)]
86mod test {
87 use super::*;
88
89 #[test]
90 fn test_local_command() {
91 let local_command = LocalCommand::new();
92
93 let wait = local_command.speak("message");
94
95 assert!(wait.is_ok());
96 }
97}