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
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ the toast. That's it.
instead of dumping the last 90 days on you.
- **Starts with your session**, minimized to the tray, if you want it to.
- **English and Spanish.** Follows your system language, or pick one in settings.
- **Glass.** The window is translucent and asks the compositor to blur what's
behind it: KDE Plasma (both the KDE and the new `ext_background_effect`
protocols), Windows 11 acrylic, macOS vibrancy. A slider in settings sets
the opacity; 100% is opaque. GNOME shows the tint without blur.
- **Three-step first run.** Token, what to watch (your busiest repos suggested), and a real notification from your own repo. Skippable, replayable from settings.
- **Tiny.** Built with Tauri v2: a few megabytes, not a bundled browser.

Expand Down
6 changes: 6 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,12 @@ <h2 data-i18n="options.title"></h2>
</select>
</label>
</div>
<div class="glass-row">
<label for="glass"><span data-i18n="options.glass"></span></label>
<input id="glass" type="range" min="30" max="100" step="5" />
<output id="glass-value" for="glass"></output>
<p class="hint" data-i18n="options.glassHint"></p>
</div>
<div class="options actions">
<button id="test-toast" class="ghost" data-i18n="options.test"></button>
<button id="replay-onboarding" class="ghost" data-i18n="ob.replay"></button>
Expand Down
133 changes: 131 additions & 2 deletions src-tauri/Cargo.lock

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

11 changes: 11 additions & 0 deletions src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,17 @@ tauri-plugin-autostart = "2"
tauri-plugin-single-instance = "2"

[target.'cfg(target_os = "linux")'.dependencies]
gdk = "0.18"
gdkwayland-sys = "0.18"
glib = "0.18"
gtk = "0.18"
# direct access to freedesktop hints (app name, desktop-entry, click actions)
# that tauri-plugin-notification doesn't expose
notify-rust = "4"
wayland-backend = { version = "0.3", features = ["client_system"] }
wayland-client = "0.31"
wayland-protocols = { version = "0.32", features = ["client", "staging"] }
wayland-protocols-plasma = { version = "0.3", features = ["client"] }

[target.'cfg(any(target_os = "windows", target_os = "macos"))'.dependencies]
window-vibrancy = "0.8"
133 changes: 133 additions & 0 deletions src-tauri/src/glass.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
//! Real glass: ask the compositor to blur whatever is behind our window.
//!
//! Tauri can make the window transparent, but "blur what's behind" is a
//! per-compositor favor an app has to request. On Wayland we speak two
//! protocols: KDE's `org_kde_kwin_blur_manager` (Plasma <= 6.6) and the
//! standard `ext_background_effect_v1` (Plasma 6.7+, Niri). We reuse the
//! wl_display/wl_surface GTK already owns, so no second connection.
//! Windows/macOS use Tauri's native vibrancy crate instead (todo).

#[cfg(target_os = "linux")]
mod linux {
use std::sync::Mutex;

use gtk::prelude::*;
use glib::translate::ToGlibPtr;
use wayland_client::{
backend::{Backend, ObjectId},
globals::registry_queue_init,
protocol::{wl_registry, wl_surface::WlSurface},
Connection, Dispatch, Proxy, QueueHandle,
};
use wayland_protocols::ext::background_effect::v1::client::{
ext_background_effect_manager_v1::ExtBackgroundEffectManagerV1,
ext_background_effect_surface_v1::ExtBackgroundEffectSurfaceV1,
};
use wayland_protocols_plasma::blur::client::{
org_kde_kwin_blur::OrgKdeKwinBlur, org_kde_kwin_blur_manager::OrgKdeKwinBlurManager,
};

/// One wrapper around GTK's display for the whole process. Wrapping the
/// same foreign display twice is asking for trouble.
static CONN: Mutex<Option<Connection>> = Mutex::new(None);
/// The effect object must stay alive or the compositor drops it. GTK
/// destroys the wl_surface when the window hides, so we make a fresh one
/// on every show and let the old proxy go.
static EFFECT: Mutex<Option<Effect>> = Mutex::new(None);
#[allow(dead_code)] // held only to keep the compositor object alive
enum Effect {
Ext(ExtBackgroundEffectSurfaceV1),
Kde(OrgKdeKwinBlur),
}

struct State;
impl Dispatch<wl_registry::WlRegistry, wayland_client::globals::GlobalListContents> for State {
fn event(
_: &mut Self,
_: &wl_registry::WlRegistry,
_: wl_registry::Event,
_: &wayland_client::globals::GlobalListContents,
_: &Connection,
_: &QueueHandle<Self>,
) {
}
}
wayland_client::delegate_noop!(State: ignore ExtBackgroundEffectManagerV1);
wayland_client::delegate_noop!(State: ignore ExtBackgroundEffectSurfaceV1);
wayland_client::delegate_noop!(State: ignore OrgKdeKwinBlurManager);
wayland_client::delegate_noop!(State: ignore OrgKdeKwinBlur);
wayland_client::delegate_noop!(State: ignore WlSurface);

/// Must run on the main thread after the window is shown (realized).
/// Safe to call on every show.
pub fn enable(window: &tauri::WebviewWindow) -> Result<&'static str, String> {
let gtk_win = window.gtk_window().map_err(|e| e.to_string())?;
let gdk_win = gtk_win.window().ok_or("window not realized yet")?;
let display = gdk_win.display();

// GTK hands us raw libwayland pointers; null means X11/XWayland
let display_ptr: *mut gdk::ffi::GdkDisplay = display.to_glib_none().0;
let window_ptr: *mut gdk::ffi::GdkWindow = gdk_win.to_glib_none().0;
let wl_display = unsafe {
gdk_wayland_sys::gdk_wayland_display_get_wl_display(display_ptr as *mut _)
};
let wl_surface = unsafe {
gdk_wayland_sys::gdk_wayland_window_get_wl_surface(window_ptr as *mut _)
};
if wl_display.is_null() || wl_surface.is_null() {
return Err("not a native Wayland window".into());
}

let conn = {
let mut slot = CONN.lock().unwrap();
if slot.is_none() {
let backend = unsafe { Backend::from_foreign_display(wl_display as *mut _) };
*slot = Some(Connection::from_backend(backend));
}
slot.as_ref().unwrap().clone()
};
let (globals, queue) = registry_queue_init::<State>(&conn).map_err(|e| e.to_string())?;
let qh = queue.handle();

let surface_id = unsafe { ObjectId::from_ptr(WlSurface::interface(), wl_surface as *mut _) }
.map_err(|e| e.to_string())?;
let surface = WlSurface::from_id(&conn, surface_id).map_err(|e| e.to_string())?;

// standard protocol first, KDE's legacy one as fallback
let (effect, which) = if let Ok(mgr) = globals.bind::<ExtBackgroundEffectManagerV1, _, _>(&qh, 1..=1, ()) {
let eff = mgr.get_background_effect(&surface, &qh, ());
eff.set_blur_region(None); // null region = the whole surface
(Effect::Ext(eff), "ext_background_effect_v1")
} else if let Ok(mgr) = globals.bind::<OrgKdeKwinBlurManager, _, _>(&qh, 1..=1, ()) {
let blur = mgr.create(&surface, &qh, ());
blur.commit();
(Effect::Kde(blur), "org_kde_kwin_blur_manager")
} else {
return Err("compositor offers no blur protocol".into());
};
// GTK commits the surface on its next frame; nudge one now so the
// effect shows without waiting for a repaint
surface.commit();
conn.flush().map_err(|e| e.to_string())?;

*EFFECT.lock().unwrap() = Some(effect);
Ok(which)
}
}

#[cfg(target_os = "linux")]
pub use linux::enable;

#[cfg(target_os = "windows")]
pub fn enable(window: &tauri::WebviewWindow) -> Result<&'static str, String> {
// acrylic on 10/11; the tint comes from CSS, keep the native one faint
window_vibrancy::apply_acrylic(window, Some((0, 0, 0, 10))).map_err(|e| e.to_string())?;
Ok("acrylic")
}

#[cfg(target_os = "macos")]
pub fn enable(window: &tauri::WebviewWindow) -> Result<&'static str, String> {
use window_vibrancy::{apply_vibrancy, NSVisualEffectMaterial};
apply_vibrancy(window, NSVisualEffectMaterial::HudWindow, None, None).map_err(|e| e.to_string())?;
Ok("vibrancy")
}
1 change: 1 addition & 0 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
//! sounds - import user audio files into the app data dir

mod cli;
mod glass;
mod i18n;
mod notify;
mod secrets;
Expand Down
Loading
Loading