mod deserializer;
mod dynamic_image_ext;
use std::{
cmp::Ordering,
fs::{read, write},
io::{Error as IoError, ErrorKind},
process::Command,
};
use std::error::Error;
use image::DynamicImage;
use tempfile::NamedTempFile;
use crate::{deserializer::from_bytes, dynamic_image_ext::DynamicImageExt};
macro_rules! run_apple_script {
($script:expr, $( $args:expr ), * ) => {
Command::new("osascript").args(["-s", "s", "-e", format!($script, $( $args ), *).as_str()]).output()
};
}
const SHORTCUT_EVENTS: &str = "\"Shortcuts Events\"";
#[derive(Clone, Debug)]
pub struct Shortcut {
folder: Option<String>,
icon: DynamicImageExt,
id: String,
index: usize,
name: String,
}
impl Shortcut {
pub fn load() -> Result<Vec<Shortcut>, Box<dyn Error>> {
Shortcut::load_internal(None)
}
pub fn load_folder(folder: &str) -> Result<Vec<Shortcut>, Box<dyn Error>> {
Shortcut::load_internal(Some(folder))
}
fn load_internal(folder: Option<&str>) -> Result<Vec<Shortcut>, Box<dyn Error>> {
let output = run_apple_script!(
"
tell application {}
{{id, name, name of folder, icon}} of every shortcut{}
end tell
",
SHORTCUT_EVENTS,
if folder.is_some() { format!(" in folder \"{}\"", folder.unwrap()) } else { String::new() }
)?;
if !output.status.success() {
Err(IoError::new(ErrorKind::Other, String::from_utf8(output.stderr)?))?;
}
let parsed: (
Vec<String>,
Vec<String>,
Vec<Option<String>>,
Vec<DynamicImageExt>,
) = from_bytes(&output.stdout)?;
let mut shortcuts = Vec::with_capacity(parsed.0.len());
for ((((id, name), folder), icon), index) in parsed
.0
.into_iter()
.zip(parsed.1.into_iter())
.zip(parsed.2.into_iter())
.zip(parsed.3.into_iter())
.zip((1..).into_iter())
{
shortcuts.push(Shortcut {
folder,
icon,
id,
index,
name,
});
}
Ok(shortcuts)
}
pub fn folder(&self) -> Option<&str> {
self.folder.as_deref()
}
pub fn icon(&self) -> &DynamicImage {
&*self.icon
}
pub fn id(&self) -> &str {
&self.id
}
pub fn name(&self) -> &str {
&self.name
}
pub fn run(&self, input: Option<&[u8]>) -> Result<Vec<u8>, Box<dyn Error>> {
let output_file = NamedTempFile::with_prefix("a")?;
let input_file = NamedTempFile::with_prefix("a")?;
let mut args = vec!["run"];
if input.is_some() {
write(&input_file, input.unwrap())?;
args.push("-i");
args.push(input_file.path().to_str().unwrap());
}
args.push("-o");
args.push(output_file.path().to_str().unwrap());
args.push(self.id.as_str());
match Command::new("shortcuts").args(args).status()?.code() {
Some(0) => Ok(read(&output_file)?),
_ => Err(Box::<IoError>::new(ErrorKind::Other.into())),
}
}
}
impl PartialEq for Shortcut {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
impl Eq for Shortcut {}
impl PartialOrd for Shortcut {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Shortcut {
fn cmp(&self, other: &Self) -> Ordering {
self.index.cmp(&other.index)
}
}
#[cfg(target_os = "macos")]
#[test]
fn load() {
assert!(Shortcut::load().is_ok());
}
#[cfg(target_os = "macos")]
#[test]
fn load_folder() {
assert!(Shortcut::load_folder("Streamdeck").is_ok());
}
#[cfg(target_os = "macos")]
#[test]
fn load_folder_fails_for_invalid_folder_name() {
assert!(Shortcut::load_folder("Streamdec").is_err());
}
#[cfg(target_os = "macos")]
#[test]
fn run() {
assert!(Shortcut::load().unwrap().iter().find(|s|s.name == "Get current weather").unwrap().run(None).is_ok());
}