diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index c2bd6ca..79092cf 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -35,7 +35,7 @@ uuid = { version = "1", features = ["v4", "serde"] } corebluetooth-rs = "=0.3.6" objc2 = "0.6.4" objc2-app-kit = { version = "0.3.2", features = ["NSBitmapImageRep", "NSColor", "NSControl", "NSEvent", "NSGraphics", "NSImage", "NSImageRep", "NSImageView", "NSPanel", "NSResponder", "NSScreen", "NSView", "NSWindow", "NSWorkspace", "objc2-core-foundation"] } -objc2-foundation = { version = "0.3.2", features = ["NSArray", "NSGeometry", "NSObject", "NSString", "NSThread", "NSURL"] } +objc2-foundation = { version = "0.3.2", features = ["NSArray", "NSGeometry", "NSHost", "NSObject", "NSString", "NSThread", "NSURL"] } [target.'cfg(not(target_os = "macos"))'.dependencies] keyring = "4.1.6" diff --git a/src-tauri/src/macos.rs b/src-tauri/src/macos.rs index a314d7d..a3aebe2 100644 --- a/src-tauri/src/macos.rs +++ b/src-tauri/src/macos.rs @@ -5,7 +5,8 @@ use std::time::Duration; use corebluetooth::prelude::*; use enigo::{Enigo, Settings}; use objc2_app_kit::NSWorkspace; -use objc2_foundation::{NSString, NSURL}; +#[allow(deprecated)] +use objc2_foundation::{NSHost, NSString, NSURL}; use tauri::{AppHandle, Manager}; use crate::input::{DesktopInput, PointerFeedback}; @@ -27,7 +28,7 @@ const SERVICE_UUID: &str = "7a78f7e8-1d6d-4d92-9ef0-1f89d3db21f4"; const RX_UUID: &str = "7a78f7e9-1d6d-4d92-9ef0-1f89d3db21f4"; const TX_UUID: &str = "7a78f7ea-1d6d-4d92-9ef0-1f89d3db21f4"; const STATUS_UUID: &str = "7a78f7eb-1d6d-4d92-9ef0-1f89d3db21f4"; -const DISPLAY_NAME: &str = "Switchify PC"; +const FALLBACK_DISPLAY_NAME: &str = "Switchify PC"; const MAX_QUEUED_NOTIFICATIONS: usize = 512; const ACCESSIBILITY_SETTINGS_URL: &str = "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility"; @@ -93,11 +94,28 @@ fn evaluate_accessibility( } } +fn resolved_display_name(localized_name: Option<&str>) -> String { + localized_name + .map(str::trim) + .filter(|name| !name.is_empty()) + .unwrap_or(FALLBACK_DISPLAY_NAME) + .to_owned() +} + +#[allow(deprecated)] +fn system_display_name() -> String { + let localized_name = NSHost::currentHost() + .localizedName() + .map(|name| name.to_string()); + resolved_display_name(localized_name.as_deref()) +} + thread_local! { static RUNTIME: RefCell> = const { RefCell::new(None) }; } pub fn install(app: AppHandle, shared: SharedModel) -> Result<(), String> { + let display_name = system_display_name(); let state_app = app.clone(); let service_app = app.clone(); let advertising_app = app.clone(); @@ -158,6 +176,7 @@ pub fn install(app: AppHandle, shared: SharedModel) -> Result<(), String> { *slot.borrow_mut() = Some(MacRuntime { app, shared, + display_name, manager, service: None, tx_characteristic: None, @@ -332,6 +351,7 @@ fn with_runtime( struct MacRuntime { app: AppHandle, shared: SharedModel, + display_name: String, manager: PeripheralManager, service: Option, tx_characteristic: Option, @@ -454,7 +474,7 @@ impl MacRuntime { model.state.capabilities.platform.clone(), ) }; - self.status_value = bluetooth_status_payload(DISPLAY_NAME, &desktop_id, &platform)?; + self.status_value = bluetooth_status_payload(&self.display_name, &desktop_id, &platform)?; let status = MutableCharacteristic::new( &status_uuid, CharacteristicProperties::READ, @@ -491,7 +511,7 @@ impl MacRuntime { let service_uuid = BluetoothUuid::from_string(SERVICE_UUID).map_err(|error| error.to_string())?; let advertisement = AdvertisementData::new() - .with_local_name(DISPLAY_NAME) + .with_local_name(FALLBACK_DISPLAY_NAME) .with_service_uuid(service_uuid); self.manager .start_advertising(&advertisement) @@ -1436,6 +1456,31 @@ mod tests { assert_eq!((adapter.prompt_requests, adapter.settings_opens), (1, 1)); } + #[test] + fn localized_computer_name_is_trimmed_and_preserves_unicode() { + assert_eq!( + resolved_display_name(Some(" Owen’s Mac Studio ")), + "Owen’s Mac Studio" + ); + } + + #[test] + fn missing_or_blank_computer_name_uses_product_fallback() { + assert_eq!(resolved_display_name(None), FALLBACK_DISPLAY_NAME); + assert_eq!(resolved_display_name(Some(" \n\t ")), FALLBACK_DISPLAY_NAME); + } + + #[test] + fn bluetooth_status_uses_resolved_computer_name() { + let display_name = resolved_display_name(Some("Owen’s Mac Studio")); + let payload = bluetooth_status_payload(&display_name, "desktop-1", "macos").unwrap(); + let status: serde_json::Value = serde_json::from_slice(&payload).unwrap(); + + assert_eq!(status["displayName"], "Owen’s Mac Studio"); + assert_eq!(status["desktopId"], "desktop-1"); + assert_eq!(status["platform"], "macos"); + } + #[test] fn pointer_profile_uses_logical_bounds_and_reduced_movement_steps() { let profile = pointer_profile_for_display("Retina", 2.0, 0, 0, 3840, 2160);