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
3 changes: 3 additions & 0 deletions docs/buzz-help.md
Original file line number Diff line number Diff line change
Expand Up @@ -728,6 +728,9 @@ for 30 days expires.
- **Closing the window** in the macOS app hides it; Buzz and its agents keep
running. Switching back to Buzz, for example with ⌘Tab or its Dock icon,
shows it again. A minimized window stays in the Dock. ⌘Q quits.
- **Closing the window** in the Windows app hides it to the notification area;
Buzz and its agents keep running. Click Buzz's icon there, or open Buzz
again, to show it. Right-click the icon and choose **Quit Buzz** to quit.
- **Sharing a Solo chat** grants access to its conversation history, not only
the next message. Search for a channel in its access dialog and click **Share**
beside the result. This makes a Private chat **View only** and posts a link
Expand Down
2 changes: 2 additions & 0 deletions src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,8 @@ objc2-user-notifications = { version = "=0.3.2", default-features = false, featu
objc2-web-kit = { version = "=0.3.2", default-features = false, features = ["std", "objc2-app-kit", "block2", "WKWebView", "WKUIDelegate", "WKFrameInfo", "WKSecurityOrigin", "WKWebViewConfiguration", "WKWebsiteDataStore", "WKHTTPCookieStore", "WKDownload", "WKDownloadDelegate", "WKNavigation", "WKNavigationDelegate"] }

[target.'cfg(target_os = "windows")'.dependencies]
# The notification-area icon that reopens and quits Buzz (tray.rs).
tauri = { version = "=2.12.4", features = ["tray-icon"] }
webview2-com = "=0.39.1"
windows-core = "=0.62.2"
tauri-winrt-notification = "=0.8.1"
Expand Down
20 changes: 20 additions & 0 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ mod microphone;
mod mouse_nav;
#[cfg(target_os = "macos")]
mod reshow;
#[cfg(windows)]
mod tray;

mod notifications;
mod os_idle;
Expand Down Expand Up @@ -1024,6 +1026,12 @@ pub fn run() {
mouse_nav::install(app.handle());
#[cfg(target_os = "macos")]
reshow::install(app.handle());
// Before any close hides the window: the icon is the way back.
// Without it, closing quits as before (see the close handler).
#[cfg(windows)]
if let Err(error) = tray::install(app.handle()) {
eprintln!("Could not add Buzz to the notification area: {error}");
}
#[cfg(target_os = "macos")]
if let Some(home) = dirs::home_dir() {
if let Err(error) = legacy_cli_shortcut::remove_stale(&home.join(".local/bin")) {
Expand Down Expand Up @@ -1285,6 +1293,18 @@ pub fn run() {
return;
}
}
// As on macOS; the notification-area icon reopens and quits Buzz.
// Hide only when that icon exists, or the window could not come back.
#[cfg(windows)]
if window.label() == "main" && tray::installed(window.app_handle()) {
if let tauri::WindowEvent::CloseRequested { api, .. } = event {
api.prevent_close();
if let Err(error) = window.hide() {
eprintln!("Could not close Buzz window: {error}");
}
return;
}
}
browser::window_event(window, event);
})
.build(context)
Expand Down
125 changes: 125 additions & 0 deletions src-tauri/src/tray.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
//! Windows notification-area icon.
//!
//! Closing the main window on Windows hides it (see the `CloseRequested`
//! handler), so running agents keep working. This icon is the way back: a left
//! click opens Buzz, and its menu offers Open and Quit. Once the window is
//! hidden it is the way to quit, since Alt+F4 and the taskbar's Close only
//! close the window.
use tauri::{
menu::{Menu, MenuItem},
tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent},
AppHandle, Runtime,
};

const TRAY_ID: &str = "buzz-tray";
const OPEN_ID: &str = "tray-open";
const QUIT_ID: &str = "tray-quit";

#[derive(Debug, PartialEq, Eq)]
enum TrayCommand {
Open,
Quit,
}

fn command(id: &str) -> Option<TrayCommand> {
match id {
OPEN_ID => Some(TrayCommand::Open),
QUIT_ID => Some(TrayCommand::Quit),
_ => None,
}
}

/// Only a left-button release opens Buzz: the right button belongs to the
/// menu, and each click also reports a press.
fn opens(event: &TrayIconEvent) -> bool {
matches!(
event,
TrayIconEvent::Click {
button: MouseButton::Left,
button_state: MouseButtonState::Up,
..
}
)
}

/// Adds Buzz's icon, which lasts as long as the app.
pub(crate) fn install<R: Runtime>(app: &AppHandle<R>) -> tauri::Result<()> {
let menu = Menu::with_items(
app,
&[
&MenuItem::with_id(app, OPEN_ID, "Open Buzz", true, None::<&str>)?,
&MenuItem::with_id(app, QUIT_ID, "Quit Buzz", true, None::<&str>)?,
],
)?;
let mut builder = TrayIconBuilder::with_id(TRAY_ID)
.menu(&menu)
.tooltip("Buzz")
.show_menu_on_left_click(false)
.on_menu_event(|app, event| match command(event.id.as_ref()) {
Some(TrayCommand::Open) => crate::deep_links::focus_main(app),
// The same exit as quitting from the app menu: agents stop with Buzz.
Some(TrayCommand::Quit) => app.exit(0),
None => {}
})
.on_tray_icon_event(|tray, event| {
if opens(&event) {
crate::deep_links::focus_main(tray.app_handle());
}
});
if let Some(icon) = app.default_window_icon() {
builder = builder.icon(icon.clone());
}
builder.build(app)?;
Ok(())
}

/// Whether Buzz's icon is in the notification area, so a closed window can
/// come back.
pub(crate) fn installed<R: Runtime>(app: &AppHandle<R>) -> bool {
app.tray_by_id(TRAY_ID).is_some()
}

#[cfg(test)]
mod tests {
use super::*;
use tauri::{tray::TrayIconId, PhysicalPosition, Rect};

fn click(button: MouseButton, button_state: MouseButtonState) -> TrayIconEvent {
TrayIconEvent::Click {
id: TrayIconId::new(TRAY_ID),
position: PhysicalPosition::new(0.0, 0.0),
rect: Rect::default(),
button,
button_state,
}
}

#[test]
fn installed_only_after_install() {
let app = tauri::test::mock_builder()
.build(crate::app_context())
.expect("mock app");
assert!(!installed(app.handle()));
install(app.handle()).expect("tray icon");
assert!(installed(app.handle()));
}

#[test]
fn menu_items_open_and_quit() {
assert_eq!(command(OPEN_ID), Some(TrayCommand::Open));
assert_eq!(command(QUIT_ID), Some(TrayCommand::Quit));
assert_eq!(command(""), None);
}

#[test]
fn only_a_left_release_opens_buzz() {
assert!(opens(&click(MouseButton::Left, MouseButtonState::Up)));
assert!(!opens(&click(MouseButton::Left, MouseButtonState::Down)));
assert!(!opens(&click(MouseButton::Right, MouseButtonState::Up)));
assert!(!opens(&TrayIconEvent::Enter {
id: TrayIconId::new(TRAY_ID),
position: PhysicalPosition::new(0.0, 0.0),
rect: Rect::default(),
}));
}
}