Add clipboard-∞ tray icon, fix tray click, and Settings gear.
CI / skip-ci-check (pull_request) Successful in 4s
CI / secret-scan (pull_request) Successful in 3s
CI / node-ci (pull_request) Successful in 22s

Ship a macOS template menu-bar glyph, only toggle on left mouse-down so
press/release no longer opens-then-closes, and put Settings next to Search
so it is reachable without the tray menu.
This commit is contained in:
2026-09-04 22:04:37 -04:00
parent 49e8f299d2
commit d8090b251c
33 changed files with 312 additions and 39 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 597 B

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 804 B

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 458 B

After

Width:  |  Height:  |  Size: 996 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 567 B

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 908 B

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 890 B

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 674 B

After

Width:  |  Height:  |  Size: 1.7 KiB

+12
View File
@@ -0,0 +1,12 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22" fill="none">
<!-- Template glyph: black on transparent. macOS tints this in the menu bar. -->
<g stroke="#000000" stroke-width="1.55" stroke-linecap="round" stroke-linejoin="round">
<rect x="4.2" y="4.2" width="13.6" height="15.6" rx="2"/>
<rect x="8.2" y="1.9" width="5.6" height="2.6" rx="1.3"/>
<path d="M6.6 13
C 6.6 10.7, 8.4 10.7, 11 13
C 13.6 15.3, 15.4 15.3, 15.4 13
C 15.4 10.7, 13.6 10.7, 11 13
C 8.4 15.3, 6.6 15.3, 6.6 13"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 579 B

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 361 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 486 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 873 B

+71 -6
View File
@@ -6,11 +6,13 @@ mod redact;
use commands::{DbState, LastHashState, PausedState};
use db::Database;
use std::sync::atomic::AtomicBool;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::Arc;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use tauri::{
image::Image,
menu::{CheckMenuItem, Menu, MenuItem, PredefinedMenuItem},
tray::TrayIconBuilder,
tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent},
Emitter, Manager, WindowEvent,
};
use tauri_plugin_autostart::MacosLauncher;
@@ -161,6 +163,26 @@ fn toggle_window(app: &tauri::AppHandle) {
}
}
/// Tray `Click` fires on both mouse-down and mouse-up. Only the down edge
/// should toggle — otherwise press opens and release immediately closes.
fn is_tray_primary_down(event: &TrayIconEvent) -> bool {
matches!(
event,
TrayIconEvent::Click {
button: MouseButton::Left,
button_state: MouseButtonState::Down,
..
}
)
}
fn now_millis() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_millis() as u64)
.unwrap_or(0)
}
fn position_window(window: &tauri::WebviewWindow, position: &str) {
let get_monitor_info =
|w: &tauri::WebviewWindow| -> Option<(f64, f64, f64)> {
@@ -271,6 +293,7 @@ pub fn run() {
commands::set_hotkey,
])
.setup(move |app| {
let ignore_blur_until = Arc::new(AtomicU64::new(0));
let hotkey_str = db
.get_settings()
.map(|s| s.hotkey)
@@ -290,8 +313,11 @@ pub fn run() {
let quit_i = MenuItem::with_id(app, "quit", "Quit", true, None::<&str>)?;
let menu = Menu::with_items(app, &[&show_i, &pause_i, &sep, &settings_i, &quit_i])?;
let tray_icon = Image::from_bytes(include_bytes!("../icons/menubarTemplate@2x.png"))
.expect("embedded menu-bar template");
let _tray = TrayIconBuilder::with_id("macopy-tray")
.icon(app.default_window_icon().unwrap().clone())
.icon(tray_icon)
.icon_as_template(true)
.menu(&menu)
.show_menu_on_left_click(false)
.tooltip("maCopy — Clipboard Manager")
@@ -316,8 +342,15 @@ pub fn run() {
_ => {}
}
})
.on_tray_icon_event(|tray, event| {
if let tauri::tray::TrayIconEvent::Click { .. } = event {
.on_tray_icon_event({
let ignore_blur_until = ignore_blur_until.clone();
move |tray, event| {
if !is_tray_primary_down(&event) {
return;
}
// Mouse-up lands on the menu bar and steals focus; don't
// treat that as "clicked outside".
ignore_blur_until.store(now_millis() + 500, Ordering::Relaxed);
toggle_window(tray.app_handle());
}
})
@@ -328,13 +361,17 @@ pub fn run() {
if let Some(window) = app.get_webview_window("main") {
let w = window.clone();
let ignore_blur_until = ignore_blur_until.clone();
window.on_window_event(move |event| {
if let WindowEvent::Focused(false) = event {
if now_millis() < ignore_blur_until.load(Ordering::Relaxed) {
return;
}
// Delay hide so left-clicks inside the list finish before
// macOS/webview focus churn can cancel them.
let w2 = w.clone();
std::thread::spawn(move || {
std::thread::sleep(std::time::Duration::from_millis(180));
std::thread::sleep(Duration::from_millis(180));
if !w2.is_focused().unwrap_or(false) {
let _ = w2.hide();
}
@@ -423,4 +460,32 @@ mod tests {
assert!(parse_hotkey("cmd+banana").is_none());
assert!(parse_hotkey("cmd+v+x").is_none());
}
#[test]
fn tray_toggle_is_left_mouse_down_only() {
let down = TrayIconEvent::Click {
id: tauri::tray::TrayIconId::new("t"),
position: tauri::PhysicalPosition::new(0.0, 0.0),
rect: tauri::Rect::default(),
button: MouseButton::Left,
button_state: MouseButtonState::Down,
};
let up = TrayIconEvent::Click {
id: tauri::tray::TrayIconId::new("t"),
position: tauri::PhysicalPosition::new(0.0, 0.0),
rect: tauri::Rect::default(),
button: MouseButton::Left,
button_state: MouseButtonState::Up,
};
let right = TrayIconEvent::Click {
id: tauri::tray::TrayIconId::new("t"),
position: tauri::PhysicalPosition::new(0.0, 0.0),
rect: tauri::Rect::default(),
button: MouseButton::Right,
button_state: MouseButtonState::Down,
};
assert!(is_tray_primary_down(&down));
assert!(!is_tray_primary_down(&up));
assert!(!is_tray_primary_down(&right));
}
}