#[cfg(target_os = "macos")]
use objc::runtime::{Class, Object};
#[cfg(target_os = "macos")]
use objc::{msg_send, sel, sel_impl};
#[derive(Debug, Clone)]
pub struct ServerInfo {
pub name: String,
pub uuid: String,
pub app_name: String,
pub bundle_id: String,
}
impl ServerInfo {
pub fn display_name(&self) -> &str {
if self.name.is_empty() { &self.app_name } else { &self.name }
}
}
pub struct SyphonServerDirectory;
impl SyphonServerDirectory {
#[cfg(target_os = "macos")]
fn shared_directory() -> *mut Object {
unsafe {
let cls = Class::get("SyphonServerDirectory").unwrap();
msg_send![cls, sharedDirectory]
}
}
pub fn servers() -> Vec<ServerInfo> {
#[cfg(target_os = "macos")]
{
unsafe {
objc::rc::autoreleasepool(|| Self::servers_inner())
}
}
#[cfg(not(target_os = "macos"))]
{ Vec::new() }
}
#[cfg(target_os = "macos")]
unsafe fn servers_inner() -> Vec<ServerInfo> {
use objc::rc::autoreleasepool;
let dir = Self::shared_directory();
let servers: *mut Object = msg_send![dir, servers];
let initial_count: usize = msg_send![servers, count];
if initial_count == 0 {
let _: () = msg_send![dir, requestServerAnnounce];
}
let servers: *mut Object = msg_send![dir, servers];
let count: usize = msg_send![servers, count];
let mut result = Vec::with_capacity(count);
for i in 0..count {
autoreleasepool(|| {
let desc: *mut Object = msg_send![servers, objectAtIndex: i];
result.push(ServerInfo {
name: Self::string_for_key(desc, "SyphonServerDescriptionNameKey"),
uuid: Self::string_for_key(desc, "SyphonServerDescriptionUUIDKey"),
app_name: Self::string_for_key(desc, "SyphonServerDescriptionAppNameKey"),
bundle_id: Self::string_for_key(desc, "SyphonServerDescriptionAppBundleIdentifierKey"),
});
});
}
result
}
#[cfg(target_os = "macos")]
unsafe fn string_for_key(dict: *mut Object, key: &str) -> String {
use crate::utils::{to_nsstring, from_nsstring};
let key_obj = match to_nsstring(key) {
Ok(k) => k,
Err(_) => return String::new(),
};
let value: *mut Object = msg_send![dict, objectForKey: key_obj];
if value.is_null() { String::new() } else { from_nsstring(value) }
}
pub fn find_by_uuid(uuid: &str) -> Option<ServerInfo> {
Self::servers().into_iter().find(|s| s.uuid == uuid)
}
pub fn find_server(name: &str) -> Option<ServerInfo> {
let mut matches: Vec<ServerInfo> = Self::servers()
.into_iter()
.filter(|s| s.name == name || s.app_name == name)
.collect();
if matches.len() > 1 {
log::warn!(
"[SyphonServerDirectory] {} servers match name '{}'. \
Returning first match. Use find_by_uuid() for precise selection.",
matches.len(), name
);
}
matches.into_iter().next()
}
pub fn server_exists(name: &str) -> bool {
Self::find_server(name).is_some()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_list_servers() {
let servers = SyphonServerDirectory::servers();
println!("Found {} Syphon servers", servers.len());
for server in &servers {
println!(" - '{}' uuid={} app={}", server.display_name(), server.uuid, server.app_name);
}
}
}