feat(native): Linux parity + autostart + tray DND (P6-1; no macOS)
Build Lotus Chat Desktop / prepare (push) Successful in 3s
Build Lotus Chat Desktop / build-windows (push) Successful in 23m43s
Build Lotus Chat Desktop / build-linux (push) Successful in 23m57s
Build Lotus Chat Desktop / update-manifest (push) Successful in 4s

Rounds out the native app on Linux (Windows features kept; macOS stays no-op):
- power.rs: no-sleep during calls on Linux via a zbus org.freedesktop.ScreenSaver
  Inhibit/UnInhibit (cookie held in ScreenSaverInhibit managed state).
- set_badge_count: Linux launcher badge via the Unity
  com.canonical.Unity.LauncherEntry.Update D-Bus signal (best-effort; app_uri
  = cinny.desktop per Tauri's mainBinaryName naming).
- tauri-plugin-autostart registered (+ autostart:allow-enable/disable/is-enabled
  capabilities); web toggles it from Settings.
- Tray "Do Not Disturb" CheckMenuItem → emits lotus-dnd-changed to the web,
  which ORs it into the notification quiet-gate.

zbus 5 (Linux target dep; blocking-api default). CI-compile-verified
(windows+linux); reviewer confirmed no build-breakers. Runtime to check on
Linux: DND toggle polarity, badge .desktop id.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-02 15:18:28 -04:00
co-authored by Claude Opus 4.8
parent 213f79d2a2
commit 22f8e1566c
5 changed files with 171 additions and 10 deletions
+73 -3
View File
@@ -4,7 +4,7 @@
)]
use tauri::{
menu::{Menu, MenuItem, PredefinedMenuItem},
menu::{CheckMenuItem, Menu, MenuItem, PredefinedMenuItem},
tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent},
webview::{NewWindowResponse, PageLoadEvent, WebviewWindowBuilder},
Manager, WebviewUrl,
@@ -145,6 +145,11 @@ async fn install_update(app: tauri::AppHandle) -> Result<(), String> {
#[tauri::command]
fn set_badge_count(count: u32, window: tauri::Window) -> Result<(), String> {
// `window` is only consulted on Windows (needs the HWND for the taskbar
// overlay). Bind it elsewhere so cross-platform builds don't warn.
#[cfg(not(target_os = "windows"))]
let _ = &window;
#[cfg(target_os = "windows")]
{
use windows::{
@@ -313,6 +318,44 @@ fn set_badge_count(count: u32, window: tauri::Window) -> Result<(), String> {
}
}
}
// Linux (P6-1): emit the Unity `LauncherEntry.Update` broadcast signal so
// launchers/docks that speak the com.canonical.Unity.LauncherEntry protocol
// (GNOME "Dash to Dock", KDE task manager, Unity, etc.) render a count
// badge on the app's launcher icon. Best-effort: any D-Bus failure is
// logged and swallowed so a headless/unsupported environment never breaks
// the badge call.
#[cfg(target_os = "linux")]
{
use std::collections::HashMap;
use zbus::zvariant::Value;
// application://<desktop-file-id>.desktop — the installed .desktop
// basename. Tauri v2's Linux bundler names it after mainBinaryName
// ("cinny"), NOT the identifier, so the file is `cinny.desktop`. If the
// badge doesn't attach at runtime, verify against
// /usr/share/applications/ and adjust.
let app_uri = "application://cinny.desktop";
let mut props: HashMap<&str, Value> = HashMap::new();
props.insert("count", Value::from(count as i64));
props.insert("count-visible", Value::from(count > 0));
match zbus::blocking::Connection::session() {
Ok(conn) => {
if let Err(e) = conn.emit_signal(
None::<&str>,
"/com/canonical/unity/launcherentry/lotuschat",
"com.canonical.Unity.LauncherEntry",
"Update",
&(app_uri, props),
) {
eprintln!("badge: Unity LauncherEntry emit failed: {e}");
}
}
Err(e) => eprintln!("badge: D-Bus session connection failed: {e}"),
}
}
Ok(())
}
@@ -441,6 +484,14 @@ pub fn run() {
#[cfg(not(any(target_os = "android", target_os = "ios")))]
{
builder = builder.plugin(tauri_plugin_updater::Builder::new().build());
// P6-1 launch-on-login. The web side drives it via the plugin's own
// `plugin:autostart|enable`/`disable`/`is-enabled` commands (no wrapper
// command). The MacosLauncher arg is mandatory by the plugin API even
// though macOS is out of scope; `None` = no extra launch args.
builder = builder.plugin(tauri_plugin_autostart::init(
tauri_plugin_autostart::MacosLauncher::LaunchAgent,
None,
));
}
builder
@@ -451,17 +502,36 @@ pub fn run() {
// panicking at startup (the tray simply isn't created).
if let Some(base_icon) = app.default_window_icon().cloned() {
let open_item = MenuItem::with_id(app, "open", "Open Lotus Chat", true, None::<&str>)?;
// P6-1: Do Not Disturb toggle. The CheckMenuItem auto-flips its
// own checkstate on click; we read the new state and push it to
// the web client, which owns the actual notification-muting.
let dnd_item =
CheckMenuItem::with_id(app, "dnd", "Do Not Disturb", true, false, None::<&str>)?;
let quit_item = MenuItem::with_id(app, "quit", "Quit Lotus Chat", true, None::<&str>)?;
let separator = PredefinedMenuItem::separator(app)?;
let tray_menu = Menu::with_items(app, &[&open_item, &separator, &quit_item])?;
let tray_menu =
Menu::with_items(app, &[&open_item, &dnd_item, &separator, &quit_item])?;
// Clone the handle into the menu-event closure so we can query
// is_checked() after the auto-toggle. CheckMenuItem is a cheap
// clonable handle to the same underlying menu item.
let dnd_for_event = dnd_item.clone();
let tray = TrayIconBuilder::with_id("main-tray")
.icon(base_icon.clone())
.tooltip("Lotus Chat")
.menu(&tray_menu)
.show_menu_on_left_click(false)
.on_menu_event(|app, event| match event.id.as_ref() {
.on_menu_event(move |app, event| match event.id.as_ref() {
"open" => show_main(app),
"quit" => app.exit(0),
"dnd" => {
let checked = dnd_for_event.is_checked().unwrap_or(false);
native::emit_to_web(
app,
"lotus-dnd-changed",
&serde_json::to_string(&serde_json::json!({ "active": checked }))
.unwrap_or_default(),
);
}
_ => {}
})
.on_tray_icon_event(|tray, event| {