Skip to content
Open
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
93 changes: 92 additions & 1 deletion src-tauri/Cargo.lock

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

5 changes: 3 additions & 2 deletions src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,13 @@ tauri-plugin-global-shortcut = "2"
tauri-plugin-autostart = "2.5.1"
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
regex-lite = "0.1.9"
tauri-plugin-liquid-glass = "0.1.6"
aes-gcm = "0.10.3"

[target.'cfg(target_os = "macos")'.dependencies]
objc2 = "0.6"
objc2-foundation = { version = "0.3", features = ["NSProcessInfo", "NSString"] }
objc2-app-kit = { version = "0.3", features = ["NSEvent", "NSScreen", "NSGraphics"] }
objc2-foundation = { version = "0.3", features = ["NSKeyValueCoding", "NSProcessInfo", "NSString"] }
objc2-app-kit = { version = "0.3", features = ["NSClipView", "NSColor", "NSEvent", "NSGraphics", "NSScreen", "NSScrollView", "NSView", "NSWindow"] }
objc2-web-kit = { version = "0.3", features = ["WKPreferences", "WKWebView", "WKWebViewConfiguration"] }

[dev-dependencies]
Expand Down
1 change: 1 addition & 0 deletions src-tauri/capabilities/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"process:allow-restart",
"global-shortcut:default",
"autostart:default",
"liquid-glass:default",
"core:menu:default"
]
}
32 changes: 31 additions & 1 deletion src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ use std::sync::{Arc, Mutex, OnceLock};
use serde::Serialize;
use tauri::Emitter;
use tauri_plugin_aptabase::EventTracker;
use tauri_plugin_liquid_glass::{GlassMaterialVariant, LiquidGlassConfig, LiquidGlassExt};
use tauri_plugin_log::{Target, TargetKind};
use uuid::Uuid;

Expand Down Expand Up @@ -201,6 +202,33 @@ fn hide_panel(app_handle: tauri::AppHandle) {
}
}

#[tauri::command]
fn set_liquid_glass_enabled(app_handle: tauri::AppHandle, enabled: bool) -> Result<(), String> {
use tauri::Manager;

let Some(window) = app_handle.get_webview_window("main") else {
return Ok(());
};

let config = if enabled {
LiquidGlassConfig {
corner_radius: 22.0,
variant: GlassMaterialVariant::Sidebar,
..Default::default()
}
} else {
LiquidGlassConfig {
enabled: false,
..Default::default()
}
};

app_handle
.liquid_glass()
.set_effect(&window, config)
.map_err(|error| error.to_string())
}

#[tauri::command]
fn open_devtools(#[allow(unused)] app_handle: tauri::AppHandle) {
#[cfg(debug_assertions)]
Expand Down Expand Up @@ -492,9 +520,11 @@ pub fn run() {
.plugin(tauri_plugin_process::init())
.plugin(tauri_plugin_global_shortcut::Builder::new().build())
.plugin(tauri_plugin_autostart::Builder::new().build())
.plugin(tauri_plugin_liquid_glass::init())
.invoke_handler(tauri::generate_handler![
init_panel,
hide_panel,
set_liquid_glass_enabled,
open_devtools,
start_probe_batch,
list_plugins,
Expand All @@ -508,7 +538,7 @@ pub fn run() {
#[cfg(target_os = "macos")]
{
app_nap::disable_app_nap();
webkit_config::disable_webview_suspension(app.handle());
webkit_config::configure_webview(app.handle());
}

use tauri::Manager;
Expand Down
68 changes: 53 additions & 15 deletions src-tauri/src/webkit_config.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
//! WebKit configuration for disabling background suspension on macOS.
//! WebKit configuration for macOS panel behavior.
//!
//! By default, WebKit suspends JavaScript execution when the webview is not visible.
//! This module disables that behavior so auto-update timers continue to fire.
//! We keep JavaScript active while the panel is hidden and force the WKWebView
//! itself fully transparent so native liquid-glass can show through the app
//! container instead of only around it.

use tauri::Manager;

Expand All @@ -12,27 +13,64 @@ fn macos_at_least(major: u64, minor: u64) -> bool {
(version.majorVersion as u64, version.minorVersion as u64) >= (major, minor)
}

pub fn disable_webview_suspension(app_handle: &tauri::AppHandle) {
pub fn configure_webview(app_handle: &tauri::AppHandle) {
let Some(window) = app_handle.get_webview_window("main") else {
log::warn!("webkit_config: main window not found");
return;
};

if !macos_at_least(14, 0) {
log::info!("WebKit inactiveSchedulingPolicy requires macOS 14.0+; skipping on this system");
return;
let can_disable_inactive_scheduling = macos_at_least(14, 0);
if !can_disable_inactive_scheduling {
log::info!(
"WebKit inactiveSchedulingPolicy requires macOS 14.0+; skipping scheduling override on this system"
);
}

if let Err(e) = window.with_webview(|webview| {
unsafe {
use objc2_web_kit::{WKInactiveSchedulingPolicy, WKWebView};
let wk_webview: &WKWebView = &*webview.inner().cast();
let config = wk_webview.configuration();
let prefs = config.preferences();
if let Err(e) = window.with_webview(move |webview| unsafe {
use objc2::sel;
use objc2_app_kit::NSColor;
use objc2_foundation::{NSNumber, NSObjectNSKeyValueCoding, NSObjectProtocol, ns_string};
use objc2_web_kit::{WKInactiveSchedulingPolicy, WKWebView};

let wk_webview: &WKWebView = &*webview.inner().cast();
let clear = NSColor::clearColor();
let no = NSNumber::numberWithBool(false);
let config = wk_webview.configuration();
let prefs = config.preferences();

if can_disable_inactive_scheduling {
prefs.setInactiveSchedulingPolicy(WKInactiveSchedulingPolicy::None);
log::info!("WebKit inactiveSchedulingPolicy set to None");
}

config.setValue_forKey(Some(&no), ns_string!("drawsBackground"));
wk_webview.setValue_forKey(Some(&no), ns_string!("drawsBackground"));

if wk_webview.respondsToSelector(sel!(setUnderPageBackgroundColor:)) {
wk_webview.setUnderPageBackgroundColor(Some(&clear));
}

if let Some(scroll_view) = wk_webview.enclosingScrollView() {
scroll_view.setDrawsBackground(false);
scroll_view.setBackgroundColor(&clear);

let clip_view = scroll_view.contentView();
clip_view.setDrawsBackground(false);
clip_view.setBackgroundColor(&clear);
}

if let Some(ns_window) = wk_webview.window() {
ns_window.setOpaque(false);
ns_window.setBackgroundColor(Some(&clear));
}

if can_disable_inactive_scheduling {
log::info!("Configured transparent WKWebView and disabled inactive scheduling");
} else {
log::info!(
"Configured transparent WKWebView; inactive scheduling override not applied on this macOS version"
);
}
}) {
log::warn!("Failed to configure WebKit scheduling: {e}");
log::warn!("Failed to configure WKWebView transparency/scheduling: {e}");
}
}
27 changes: 27 additions & 0 deletions src/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -384,18 +384,44 @@ describe("App", () => {
// Dark
await userEvent.click(await screen.findByRole("radio", { name: "Dark" }))
expect(document.documentElement.classList.contains("dark")).toBe(true)
expect(document.documentElement.classList.contains("glass")).toBe(false)

// Light
await userEvent.click(await screen.findByRole("radio", { name: "Light" }))
expect(document.documentElement.classList.contains("dark")).toBe(false)
expect(document.documentElement.classList.contains("glass")).toBe(false)

// Glass
await userEvent.click(await screen.findByRole("radio", { name: "Glass" }))
expect(document.documentElement.classList.contains("dark")).toBe(false)
expect(document.documentElement.classList.contains("glass")).toBe(true)

// Back to system should subscribe to matchMedia changes
await userEvent.click(await screen.findByRole("radio", { name: "System" }))
expect(mq.addEventListener).toHaveBeenCalled()
expect(document.documentElement.classList.contains("glass")).toBe(false)

mmSpy.mockRestore()
})

it("syncs native liquid glass mode when running in tauri", async () => {
state.isTauriMock.mockReturnValue(true)

render(<App />)
const settingsButtons = await screen.findAllByRole("button", { name: "Settings" })
await userEvent.click(settingsButtons[0])

await userEvent.click(await screen.findByRole("radio", { name: "Glass" }))
await waitFor(() =>
expect(state.invokeMock).toHaveBeenCalledWith("set_liquid_glass_enabled", { enabled: true })
)

await userEvent.click(await screen.findByRole("radio", { name: "Light" }))
await waitFor(() =>
expect(state.invokeMock).toHaveBeenCalledWith("set_liquid_glass_enabled", { enabled: false })
)
})

it("loads plugins, normalizes settings, and renders overview", async () => {
state.isTauriMock.mockReturnValue(true)
render(<App />)
Expand Down Expand Up @@ -778,6 +804,7 @@ describe("App", () => {
// because "b" is not in DEFAULT_ENABLED_PLUGINS = ["claude","codex","cursor"])
state.loadPluginSettingsMock.mockResolvedValue({ order: ["a", "b"], disabled: ["b"] })
render(<App />)
await waitFor(() => expect(state.invokeMock).toHaveBeenCalledWith("list_plugins"))
const settingsButtons = await screen.findAllByRole("button", { name: "Settings" })
await userEvent.click(settingsButtons[0])
// Re-query before each click: the Checkbox remounts on each toggle because
Expand Down
Loading
Loading