Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions crates/immersion/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,7 @@ impl RootView {
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
let platform_hooks = PlatformHooks {
launch_at_startup_supported: crate::platform::is_launch_at_startup_supported(),
launch_at_startup_enabled: crate::platform::launch_at_startup_enabled(),
set_launch_at_startup: Box::new(crate::platform::set_launch_at_startup),
..PlatformHooks::default()
};
Expand Down
40 changes: 33 additions & 7 deletions crates/immersion/src/platform/mac.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use std::cell::{Cell, RefCell};
use std::time::{Duration, Instant};

use objc2::rc::Retained;
use objc2::runtime::{AnyObject, Bool, NSObjectProtocol, ProtocolObject};
use objc2::runtime::{AnyClass, AnyObject, Bool, NSObjectProtocol, ProtocolObject};
use objc2::{AllocAnyThread, MainThreadMarker, MainThreadOnly, define_class, msg_send, sel};
use objc2_app_kit::{
NSApplication, NSApplicationActivationPolicy, NSApplicationDidBecomeActiveNotification,
Expand Down Expand Up @@ -407,20 +407,46 @@ pub fn install_status_item(on_open: Box<dyn Fn()>, on_quit: Box<dyn Fn()>) {
// ---- Launch at login (SMAppService) ---------------------------------------

pub fn is_launch_at_startup_supported() -> bool {
is_bundled()
// SMAppService was introduced in macOS 13, while the bundle still runs
// on macOS 12. Looking up the class avoids messaging an unavailable API.
is_bundled() && AnyClass::get(c"SMAppService").is_some()
}

/// Registration can succeed while macOS still waits for the user's approval.
pub fn launch_at_startup_needs_approval() -> bool {
if !is_launch_at_startup_supported() {
return false;
}
use objc2_service_management::{SMAppService, SMAppServiceStatus};
unsafe { SMAppService::mainAppService().status() == SMAppServiceStatus::RequiresApproval }
}

pub fn set_launch_at_startup(enabled: bool) {
if !is_bundled() {
if !is_launch_at_startup_supported() {
return;
}
unsafe {
use objc2_service_management::SMAppService;
use objc2_service_management::{SMAppService, SMAppServiceStatus};
let service = SMAppService::mainAppService();
if enabled {
let _ = service.registerAndReturnError();
let status = service.status();
let result = if enabled
&& matches!(
status,
SMAppServiceStatus::NotRegistered | SMAppServiceStatus::NotFound
) {
service.registerAndReturnError()
} else if !enabled
&& matches!(
status,
SMAppServiceStatus::Enabled | SMAppServiceStatus::RequiresApproval
)
{
service.unregisterAndReturnError()
} else {
let _ = service.unregisterAndReturnError();
return;
};
if let Err(error) = result {
eprintln!("Immersion launch at login could not be updated: {error}");
}
}
}
Expand Down
30 changes: 27 additions & 3 deletions crates/immersion/src/platform/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,9 @@ pub use win::file_type_icon_png;
#[cfg(target_os = "macos")]
pub use mac::{
adopt_main_window, file_type_icon_png, hide_main_window, install_activation_observer,
install_status_item, is_launch_at_startup_supported, launched_at_login,
set_background_agent_mode, set_dock_icon_if_unbundled, set_launch_at_startup, show_main_window,
show_notification, suppress_launch_activation_reveal,
install_status_item, is_launch_at_startup_supported, launch_at_startup_needs_approval,
launched_at_login, set_background_agent_mode, set_dock_icon_if_unbundled,
set_launch_at_startup, show_main_window, show_notification, suppress_launch_activation_reveal,
};

/// OS document icon for a file extension as PNG bytes. macOS, Windows and
Expand All @@ -71,6 +71,11 @@ pub fn file_type_icon_png(_extension: &str, _size_px: usize) -> Option<Vec<u8>>
#[cfg(not(target_os = "macos"))]
pub fn set_background_agent_mode(_enabled: bool) {}

#[cfg(not(target_os = "macos"))]
pub fn launch_at_startup_needs_approval() -> bool {
false
}

#[cfg(not(target_os = "macos"))]
pub fn adopt_main_window(_title: &str) {}

Expand All @@ -87,6 +92,25 @@ pub fn show_main_window() -> bool {
#[cfg(not(target_os = "macos"))]
pub fn install_activation_observer(_on_activate: Box<dyn Fn()>) {}

// Launch at login on Windows: HKCU Run key.
#[cfg(target_os = "windows")]
pub fn launch_at_startup_enabled() -> Option<bool> {
use std::os::windows::process::CommandExt;
const CREATE_NO_WINDOW: u32 = 0x0800_0000;
const RUN_KEY: &str = r"HKCU\Software\Microsoft\Windows\CurrentVersion\Run";
std::process::Command::new("reg")
.args(["query", RUN_KEY, "/v", "Immersion"])
.creation_flags(CREATE_NO_WINDOW)
.status()
.ok()
.map(|status| status.success())
}

#[cfg(not(target_os = "windows"))]
pub fn launch_at_startup_enabled() -> Option<bool> {
None
}

// Launch at login on Windows: HKCU Run key.
#[cfg(target_os = "windows")]
pub fn set_launch_at_startup(enabled: bool) {
Expand Down
11 changes: 10 additions & 1 deletion crates/immersion/src/ui/modals.rs
Original file line number Diff line number Diff line change
Expand Up @@ -327,7 +327,16 @@ impl RootView {
.child(section_label("Launch at login"))
.child(meta_label(
"Start Immersion automatically when you sign in.",
)),
))
.when(
launch_at_startup
&& crate::platform::launch_at_startup_needs_approval(),
|el| {
el.child(meta_label(
"Allow Immersion in System Settings > General > Login Items & Extensions.",
))
},
),
)
.child(themed_switch(
"launch-switch",
Expand Down
12 changes: 11 additions & 1 deletion crates/musit-core/src/backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,9 @@ pub enum BackendEvent {
/// Host-provided platform hooks.
pub struct PlatformHooks {
pub launch_at_startup_supported: bool,
/// Current OS login entry, when the platform exposes one to reconcile
/// with the app setting (the Windows installer can create it first).
pub launch_at_startup_enabled: Option<bool>,
pub set_launch_at_startup: Box<dyn Fn(bool) + Send>,
/// Opens a file or folder with the OS default handler. Returns success.
pub open_path: Box<dyn Fn(&str) -> bool + Send>,
Expand All @@ -226,6 +229,7 @@ impl Default for PlatformHooks {
fn default() -> Self {
Self {
launch_at_startup_supported: false,
launch_at_startup_enabled: None,
set_launch_at_startup: Box::new(|_| {}),
open_path: Box::new(default_open_path),
}
Expand Down Expand Up @@ -1083,10 +1087,16 @@ impl AppBackend {
} else {
&saved_settings.color_scheme_mode
});
backend.launch_at_startup = saved_settings.launch_at_startup;
backend.launch_at_startup = backend
.platform
.launch_at_startup_enabled
.unwrap_or(saved_settings.launch_at_startup);
if backend.platform.launch_at_startup_supported {
(backend.platform.set_launch_at_startup)(backend.launch_at_startup);
}
if backend.launch_at_startup != saved_settings.launch_at_startup {
backend.persist_app_settings();
}

for saved_folder in backend.project_registry.load_projects_folders() {
if folder_settings::has_layout_setting(&saved_folder) {
Expand Down
39 changes: 39 additions & 0 deletions crates/musit-core/tests/autostart.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
//! The installer can set the OS login entry before Immersion has app settings.

use std::sync::{Arc, Mutex};

use musit_core::backend::{AppBackend, PlatformHooks};
use musit_core::project_registry::{self, AppSettings, ProjectRegistry};

#[test]
fn os_login_entry_is_reconciled_with_saved_settings() {
let config = tempfile::tempdir().expect("temp config dir");
project_registry::set_app_config_directory_override(Some(
config.path().to_string_lossy().into_owned(),
));
let registry = ProjectRegistry;
assert!(registry.save_app_settings(&AppSettings::default()));

let applied = Arc::new(Mutex::new(Vec::new()));
let platform = |enabled| {
let applied = applied.clone();
PlatformHooks {
launch_at_startup_supported: true,
launch_at_startup_enabled: Some(enabled),
set_launch_at_startup: Box::new(move |value| {
applied.lock().expect("applied lock").push(value);
}),
..PlatformHooks::default()
}
};

let backend = AppBackend::with_platform(platform(true));
assert!(backend.launch_at_startup());
assert!(registry.load_app_settings().launch_at_startup);
drop(backend);

let backend = AppBackend::with_platform(platform(false));
assert!(!backend.launch_at_startup());
assert!(!registry.load_app_settings().launch_at_startup);
assert_eq!(*applied.lock().expect("applied lock"), [true, false]);
}
1 change: 1 addition & 0 deletions crates/musit-core/tests/e2e_backup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ fn project_index_by_file(backend: &AppBackend, file_name: &str) -> i32 {
fn test_platform() -> PlatformHooks {
PlatformHooks {
launch_at_startup_supported: false,
launch_at_startup_enabled: None,
set_launch_at_startup: Box::new(|_| {}),
// Never actually launch DAWs/Finder from the test suite.
open_path: Box::new(|_| true),
Expand Down
1 change: 1 addition & 0 deletions crates/musit-core/tests/folder_session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use std::time::{Duration, Instant};
fn test_platform() -> PlatformHooks {
PlatformHooks {
launch_at_startup_supported: false,
launch_at_startup_enabled: None,
set_launch_at_startup: Box::new(|_| {}),
open_path: Box::new(|_| true),
}
Expand Down
2 changes: 1 addition & 1 deletion packaging/nsis/Immersion.nsi
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ FunctionEnd
Function StartupPageLeave
${NSD_GetState} $StartupCheckbox $StartupEnabled
${If} $StartupEnabled == ${BST_CHECKED}
WriteRegStr HKCU "${REG_RUN}" "${APP_NAME}" "$INSTDIR\Immersion.exe"
WriteRegStr HKCU "${REG_RUN}" "${APP_NAME}" '"$INSTDIR\Immersion.exe" --autostart'
${Else}
DeleteRegValue HKCU "${REG_RUN}" "${APP_NAME}"
${EndIf}
Expand Down
2 changes: 1 addition & 1 deletion wiki/Settings-and-Storage.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Open **Settings** from the main window. Changes to a folder layout trigger a res
| --- | --- |
| **Projects folder** | Shows the active folder and lets you choose or add another. Multiple folders can be watched at once. |
| **Folder layout** | Choose **Bundles** or **Files** for the active folder. The choice is stored in that folder's `.immersion/settings.json`. |
| **Launch at login** | Starts Immersion when you sign in where the platform supports it. The packaged macOS app is required for this integration. On Linux it writes an XDG autostart entry; it is not available in the Flatpak. |
| **Launch at login** | Starts Immersion when you sign in where the platform supports it. On macOS 13 or later, the packaged app uses Login Items and may need approval in System Settings. On Linux it writes an XDG autostart entry; it is not available in the Flatpak. |
| **Default sort mode** | Start the project list sorted by **Name** or **Last Opened**. |
| **Activity log level** | **Info** shows normal events; **Debug** adds scan, watcher, and storage details. |
| **Snapshot retention** | Number of recent versions with fast uncompressed copies, from 1 to 50; default 5. Older versions remain in compressed storage. |
Expand Down
Loading