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
3 changes: 2 additions & 1 deletion src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -114,9 +114,10 @@ webkit2gtk = { version = "2.0", optional = true }
# Fork of tauri's unreleased feat/cef branch, ported onto published crates.
# https://github.com/SableClient/tauri-runtime-cef
[target.'cfg(target_os = "linux")'.dependencies]
tauri-runtime-cef = { git = "https://github.com/SableClient/tauri-runtime-cef", branch = "main", optional = true }
tauri-runtime-cef = { git = "https://github.com/SableClient/tauri-runtime-cef", rev = "6fb4fabcc759c1f5407def192540ce149f666fe8", optional = true }
cef = { version = "=150.2.1", optional = true }
libloading = "0.9"
zbus = "5"

[target.'cfg(any(target_os = "android", target_os = "ios"))'.dependencies]
tauri-plugin-notifications = { git = "https://github.com/SableClient/tauri-plugin-notifications.git", rev = "057d323862d815a0c7459a1f1acd5a3c08750a51", features = [
Expand Down
87 changes: 75 additions & 12 deletions src-tauri/src/desktop/tray.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,11 @@ pub fn setup_close_to_background(webview_window: &WebviewWindow<crate::BrowserEn
return;
};
let state = window.state::<DesktopSettingsState>();
if state.close_to_background_on_close.load(Ordering::Relaxed) {
if state.close_to_background_on_close.load(Ordering::Relaxed)
&& can_restore_from_background(DesktopRuntimeState {
tray_available: state.tray_available.load(Ordering::Relaxed),
})
{
api.prevent_close();
let _ = window.hide();
}
Expand All @@ -59,8 +63,19 @@ enum ExitRequestAction {
CloseWindowsToBackground,
}

fn exit_request_action(settings: DesktopSettings, code: Option<i32>) -> ExitRequestAction {
if code.is_some() || !settings.close_to_background_on_close {
fn can_restore_from_background(runtime: DesktopRuntimeState) -> bool {
cfg!(target_os = "macos") || runtime.tray_available
}

fn exit_request_action(
settings: DesktopSettings,
runtime: DesktopRuntimeState,
code: Option<i32>,
) -> ExitRequestAction {
if code.is_some()
|| !settings.close_to_background_on_close
|| !can_restore_from_background(runtime)
{
ExitRequestAction::AllowExit
} else {
ExitRequestAction::CloseWindowsToBackground
Expand Down Expand Up @@ -216,7 +231,8 @@ fn handle_exit_request(
api: &tauri::ExitRequestApi,
) {
let settings = current_desktop_settings(app);
if exit_request_action(settings, code) == ExitRequestAction::CloseWindowsToBackground {
let runtime = desktop_runtime_state(app);
if exit_request_action(settings, runtime, code) == ExitRequestAction::CloseWindowsToBackground {
api.prevent_exit();
close_all_windows(app);
}
Expand Down Expand Up @@ -283,6 +299,24 @@ fn appindicator_available() -> bool {
.any(|name| unsafe { libloading::Library::new(*name) }.is_ok())
}

// The AppImage bundles libayatana-appindicator, so the library probe passes on
// every desktop. Without a StatusNotifierItem host the icon is created and never
// rendered, which strands the window with no way to restore it.
#[cfg(target_os = "linux")]
fn status_notifier_host_available() -> bool {
const WATCHER: &str = "org.kde.StatusNotifierWatcher";
let Ok(name) = zbus::names::BusName::try_from(WATCHER) else {
return false;
};
let Ok(connection) = zbus::blocking::Connection::session() else {
return false;
};
let Ok(dbus) = zbus::blocking::fdo::DBusProxy::new(&connection) else {
return false;
};
dbus.name_has_owner(name).unwrap_or(false)
}

pub fn create_system_tray(app: &AppHandle<crate::BrowserEngine>) -> tauri::Result<()> {
#[cfg(target_os = "linux")]
if !appindicator_available() {
Expand All @@ -293,6 +327,15 @@ pub fn create_system_tray(app: &AppHandle<crate::BrowserEngine>) -> tauri::Resul
.into());
}

#[cfg(target_os = "linux")]
if !status_notifier_host_available() {
return Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
"no StatusNotifierWatcher on the session bus; skipping system tray",
)
.into());
}

let show_item = MenuItem::with_id(app, TRAY_MENU_SHOW_ID, "Show", true, None::<&str>)?;
let quit_item = MenuItem::with_id(app, TRAY_MENU_QUIT_ID, "Quit", true, None::<&str>)?;
let tray_menu = Menu::with_items(app, &[&show_item, &quit_item])?;
Expand Down Expand Up @@ -335,16 +378,23 @@ mod tests {
desktop_settings_from_values, tray_available_for_session, DesktopSettings,
};

const TRAY_UP: DesktopRuntimeState = DesktopRuntimeState {
tray_available: true,
};
const NO_TRAY: DesktopRuntimeState = DesktopRuntimeState {
tray_available: false,
};

#[test]
fn close_behavior_keeps_sable_running() {
let settings = DesktopSettings {
close_to_background_on_close: true,
show_system_tray_icon: false,
show_system_tray_icon: true,
use_custom_title_bar: false,
};

assert_eq!(
exit_request_action(settings, None),
exit_request_action(settings, TRAY_UP, None),
ExitRequestAction::CloseWindowsToBackground
);
}
Expand All @@ -358,24 +408,37 @@ mod tests {
};

assert_eq!(
exit_request_action(settings, None),
exit_request_action(settings, TRAY_UP, None),
ExitRequestAction::AllowExit
);
}

#[test]
fn tray_failure_still_closes_to_background_when_requested() {
fn tray_failure_allows_exit_instead_of_stranding_the_process() {
let settings = DesktopSettings {
close_to_background_on_close: true,
show_system_tray_icon: true,
use_custom_title_bar: false,
};

assert!(!tray_available_for_session(settings, false));
assert_eq!(
exit_request_action(settings, None),
ExitRequestAction::CloseWindowsToBackground
exit_request_action(settings, NO_TRAY, None),
if cfg!(target_os = "macos") {
ExitRequestAction::CloseWindowsToBackground
} else {
ExitRequestAction::AllowExit
}
);
assert!(!tray_available_for_session(settings, false));
}

#[test]
fn macos_closes_to_background_without_a_tray() {
assert_eq!(
can_restore_from_background(NO_TRAY),
cfg!(target_os = "macos")
);
assert!(can_restore_from_background(TRAY_UP));
}

#[test]
Expand All @@ -387,7 +450,7 @@ mod tests {
};

assert_eq!(
exit_request_action(settings, Some(0)),
exit_request_action(settings, TRAY_UP, Some(0)),
ExitRequestAction::AllowExit
);
}
Expand Down
8 changes: 6 additions & 2 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -384,9 +384,13 @@ pub fn run() {
mobile::set_app_handle(app.handle().clone());

// CEF is initialized during runtime construction; initialize GTK afterward
// on the main thread for the Linux tray menu.
// on the main thread for the Linux tray menu. GTK's X11 backend replaces the
// X error handlers during init, so the runtime's have to go back on after.
#[cfg(all(feature = "cef", target_os = "linux"))]
gtk::init()?;
{
gtk::init()?;
tauri_runtime_cef::install_x_error_handlers();
}

network::native_upload::cleanup_uploads(app.handle());

Expand Down
8 changes: 4 additions & 4 deletions src/app/features/settings/desktop/Desktop.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ describe('Desktop', () => {
).toBeInTheDocument();
expect(
screen.getByText(
'When enabled, closing the window keeps Sable running instead of exiting. If the tray icon is enabled and available, Sable stays in the system tray. Otherwise it continues running in the background.'
'When enabled, closing the window keeps Sable running in the system tray instead of exiting. This needs the tray icon below: without a tray to restore from, closing exits Sable.'
)
).toBeInTheDocument();
expect(screen.getByText('Show system tray icon')).toBeInTheDocument();
Expand All @@ -130,7 +130,7 @@ describe('Desktop', () => {

expect(
screen.getByText(
'System tray is unavailable on this system. Sable can still keep running in the background without it.'
'System tray is unavailable on this system. Without it, closing the window exits Sable.'
)
).toBeInTheDocument();
expect(screen.getByRole('switch', { name: 'show-system-tray-icon' })).toBeDisabled();
Expand All @@ -143,7 +143,7 @@ describe('Desktop', () => {

expect(
screen.queryByText(
'System tray is unavailable on this system. Sable can still keep running in the background without it.'
'System tray is unavailable on this system. Without it, closing the window exits Sable.'
)
).not.toBeInTheDocument();
});
Expand All @@ -157,7 +157,7 @@ describe('Desktop', () => {

expect(
screen.queryByText(
'System tray is unavailable on this system. Sable can still keep running in the background without it.'
'System tray is unavailable on this system. Without it, closing the window exits Sable.'
)
).not.toBeInTheDocument();
});
Expand Down
10 changes: 7 additions & 3 deletions src/app/features/settings/desktop/Desktop.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,11 @@ export function Desktop({ requestBack, requestClose }: DesktopProps) {
<SettingToggle
title="Close button keeps Sable running"
focusId="close-to-background-on-close"
description="When enabled, closing the window keeps Sable running instead of exiting. If the tray icon is enabled and available, Sable stays in the system tray. Otherwise it continues running in the background."
description={
type === 'macos'
? 'When enabled, closing the window keeps Sable running instead of exiting. Reopen it from the Dock.'
: 'When enabled, closing the window keeps Sable running in the system tray instead of exiting. This needs the tray icon below: without a tray to restore from, closing exits Sable.'
}
value={closeToBackgroundOnClose}
onChange={setCloseToBackgroundOnClose}
ariaLabel="close-to-background-on-close"
Expand All @@ -66,8 +70,8 @@ export function Desktop({ requestBack, requestClose }: DesktopProps) {
description={
trayFallback ? (
<Text as="span" style={{ color: color.Warning.Main }} size="T200">
System tray is unavailable on this system. Sable can still keep running in
the background without it.
System tray is unavailable on this system. Without it, closing the window
exits Sable.
</Text>
) : (
'Show a system tray icon while Sable is running. Disable this if you want Sable to stay available without a tray icon.'
Expand Down
Loading