Skip to content
14 changes: 14 additions & 0 deletions apps/desktop/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
use ext::*;
use store::*;

use tauri::Manager;

Check warning on line 11 in apps/desktop/src-tauri/src/lib.rs

View workflow job for this annotation

GitHub Actions / desktop_ci (linux, depot-ubuntu-22.04-8)

unused import: `tauri::Manager`

Check warning on line 11 in apps/desktop/src-tauri/src/lib.rs

View workflow job for this annotation

GitHub Actions / desktop_ci (linux, depot-ubuntu-22.04-8)

unused import: `tauri::Manager`
use tauri_plugin_permissions::{Permission, PermissionsPluginExt};
use tauri_plugin_windows::{AppWindow, WindowsPluginExt};

Expand Down Expand Up @@ -177,6 +177,15 @@
app_handle.tray().create_app_menu().unwrap();
}

{
use tauri_plugin_tray::HyprMenuItem;
app_handle.on_menu_event(|app, event| {
if let Ok(item) = HyprMenuItem::try_from(event.id().clone()) {
item.handle(app);
}
});
}

{
use tauri_plugin_settings::SettingsPluginExt;
if let Ok(base) = app_handle.settings().global_base()
Expand Down Expand Up @@ -272,6 +281,11 @@
}

api.prevent_exit();

for (_, window) in app.webview_windows() {
let _ = window.close();
}

let _ = app.set_activation_policy(tauri::ActivationPolicy::Accessory);
}
tauri::RunEvent::Exit => {
Expand Down
152 changes: 69 additions & 83 deletions crates/intercept/swift-lib/src/lib.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,6 @@ private final class QuitInterceptor {
case holding
}

private enum Event {
case cmdQPressed
case keyReleased
case dismissTimerFired
case quitTimerFired
}

private var keyMonitor: Any?
private var panel: NSPanel?
private var state: State = .idle
Expand All @@ -63,12 +56,13 @@ private final class QuitInterceptor {
return self.handleKeyDown(event)
case .keyUp:
self.handleKeyUp(event)
return event
case .flagsChanged:
self.handleFlagsChanged(event)
return event
default:
break
return event
}
return event
}
}

Expand Down Expand Up @@ -131,27 +125,16 @@ private final class QuitInterceptor {
container.layer?.cornerRadius = QuitOverlay.cornerRadius
container.layer?.masksToBounds = true

let font = QuitOverlay.font

let pressLabel = NSTextField(labelWithString: QuitOverlay.pressText)
pressLabel.font = font
pressLabel.textColor = QuitOverlay.primaryTextColor
pressLabel.alignment = .left
pressLabel.sizeToFit()

let holdLabel = NSTextField(labelWithString: QuitOverlay.holdText)
holdLabel.font = font
holdLabel.textColor = QuitOverlay.secondaryTextColor
holdLabel.alignment = .left
holdLabel.sizeToFit()
let pressLabel = makeLabel(QuitOverlay.pressText, color: QuitOverlay.primaryTextColor)
let holdLabel = makeLabel(QuitOverlay.holdText, color: QuitOverlay.secondaryTextColor)

let pressPrefixWidth = NSAttributedString(
string: "Press ", attributes: [.font: font]
).size().width
let holdPrefixWidth = NSAttributedString(
string: "Hold ", attributes: [.font: font]
).size().width
let prefixDelta = pressPrefixWidth - holdPrefixWidth
let prefixDelta =
NSAttributedString(
string: "Press ", attributes: [.font: QuitOverlay.font]
).size().width
- NSAttributedString(
string: "Hold ", attributes: [.font: QuitOverlay.font]
).size().width

let spacing: CGFloat = 10
let totalHeight = pressLabel.frame.height + spacing + holdLabel.frame.height
Expand All @@ -176,13 +159,18 @@ private final class QuitInterceptor {
return container
}

// MARK: - Show / Hide

func showOverlay() {
showPanel()
private func makeLabel(_ text: String, color: NSColor) -> NSTextField {
let label = NSTextField(labelWithString: text)
label.font = QuitOverlay.font
label.textColor = color
label.alignment = .left
label.sizeToFit()
return label
}

private func showPanel() {
// MARK: - Panel Visibility

func showOverlay() {
if panel == nil {
panel = makePanel()
}
Expand Down Expand Up @@ -212,66 +200,55 @@ private final class QuitInterceptor {

// MARK: - State Machine

private func transition(_ event: Event) {
switch (state, event) {
case (.idle, .cmdQPressed):
private func onCmdQPressed() {
switch state {
case .idle:
state = .awaiting
showPanel()
startDismissTimer()
showOverlay()
scheduleTimer(&dismissTimer, delay: QuitOverlay.overlayDuration) { [weak self] in
guard let self, self.state == .awaiting else { return }
self.state = .idle
self.hidePanel()
}

case (.awaiting, .cmdQPressed):
case .awaiting:
state = .holding
cancelDismissTimer()
startQuitTimer()

case (.awaiting, .keyReleased):
break

case (.awaiting, .dismissTimerFired):
state = .idle
hidePanel()

case (.holding, .keyReleased):
state = .idle
cancelQuitTimer()
performClose()

case (.holding, .quitTimerFired):
performQuit()
cancelTimer(&dismissTimer)
scheduleTimer(&quitTimer, delay: QuitOverlay.holdDuration) { [weak self] in
self?.performQuit()
}

default:
case .holding:
break
}
}

// MARK: - Timers
private func onKeyReleased() {
switch state {
case .idle, .awaiting:
break

private func startDismissTimer() {
dismissTimer?.cancel()
let timer = DispatchWorkItem { [weak self] in
self?.transition(.dismissTimerFired)
case .holding:
state = .idle
cancelTimer(&quitTimer)
performClose()
}
dismissTimer = timer
DispatchQueue.main.asyncAfter(deadline: .now() + QuitOverlay.overlayDuration, execute: timer)
}

private func cancelDismissTimer() {
dismissTimer?.cancel()
dismissTimer = nil
}
// MARK: - Timer Helpers

private func startQuitTimer() {
quitTimer?.cancel()
let timer = DispatchWorkItem { [weak self] in
self?.transition(.quitTimerFired)
}
quitTimer = timer
DispatchQueue.main.asyncAfter(deadline: .now() + QuitOverlay.holdDuration, execute: timer)
private func scheduleTimer(
_ timer: inout DispatchWorkItem?, delay: TimeInterval, action: @escaping () -> Void
) {
timer?.cancel()
let workItem = DispatchWorkItem(block: action)
timer = workItem
DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: workItem)
}

private func cancelQuitTimer() {
quitTimer?.cancel()
quitTimer = nil
private func cancelTimer(_ timer: inout DispatchWorkItem?) {
timer?.cancel()
timer = nil
}

// MARK: - Event Handlers
Expand All @@ -280,19 +257,28 @@ private final class QuitInterceptor {
let flags = event.modifierFlags.intersection(.deviceIndependentFlagsMask)
let isQ = event.charactersIgnoringModifiers?.lowercased() == "q"
guard flags.contains(.command), isQ else { return event }

if flags.contains(.shift) {
performQuit()
return nil
}

if event.isARepeat { return nil }
transition(.cmdQPressed)
onCmdQPressed()
return nil
}

private func handleKeyUp(_ event: NSEvent) {
let isQ = event.charactersIgnoringModifiers?.lowercased() == "q"
if isQ { transition(.keyReleased) }
if event.charactersIgnoringModifiers?.lowercased() == "q" {
onKeyReleased()
}
}

private func handleFlagsChanged(_ event: NSEvent) {
let flags = event.modifierFlags.intersection(.deviceIndependentFlagsMask)
if !flags.contains(.command) { transition(.keyReleased) }
if !flags.contains(.command) {
onKeyReleased()
}
}
}

Expand Down
7 changes: 0 additions & 7 deletions plugins/tray/src/ext.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
use tauri::{
AppHandle, Result,

Check warning on line 2 in plugins/tray/src/ext.rs

View workflow job for this annotation

GitHub Actions / desktop_ci (linux, depot-ubuntu-22.04-8)

unused import: `AppHandle`

Check warning on line 2 in plugins/tray/src/ext.rs

View workflow job for this annotation

GitHub Actions / desktop_ci (linux, depot-ubuntu-22.04-8)

unused import: `AppHandle`

Check warning on line 2 in plugins/tray/src/ext.rs

View workflow job for this annotation

GitHub Actions / desktop_ci (macos, depot-macos-14)

unused import: `AppHandle`

Check warning on line 2 in plugins/tray/src/ext.rs

View workflow job for this annotation

GitHub Actions / desktop_ci (macos, depot-macos-14)

unused import: `AppHandle`
image::Image,
menu::{Menu, MenuItemKind, PredefinedMenuItem, Submenu},
tray::TrayIconBuilder,
};

use crate::menu_items::{
AppInfo, AppNew, HelpReportBug, HelpSuggestFeature, HyprMenuItem, MenuItemHandler,

Check warning on line 9 in plugins/tray/src/ext.rs

View workflow job for this annotation

GitHub Actions / desktop_ci (linux, depot-ubuntu-22.04-8)

unused import: `HyprMenuItem`

Check warning on line 9 in plugins/tray/src/ext.rs

View workflow job for this annotation

GitHub Actions / desktop_ci (linux, depot-ubuntu-22.04-8)

unused import: `HyprMenuItem`

Check warning on line 9 in plugins/tray/src/ext.rs

View workflow job for this annotation

GitHub Actions / desktop_ci (macos, depot-macos-14)

unused import: `HyprMenuItem`

Check warning on line 9 in plugins/tray/src/ext.rs

View workflow job for this annotation

GitHub Actions / desktop_ci (macos, depot-macos-14)

unused import: `HyprMenuItem`
TrayCheckUpdate, TrayOpen, TrayQuit, TraySettings, TrayStart,
};

Expand Down Expand Up @@ -110,13 +110,6 @@
.icon_as_template(true)
.menu(&menu)
.show_menu_on_left_click(true)
.on_menu_event(move |app: &AppHandle, event| {
// Tauri dispatches menu events globally, so we receive events from context menus
// created elsewhere. TryFrom gracefully ignores unknown menu IDs that don't belong to the tray menu.
if let Ok(item) = HyprMenuItem::try_from(event.id.clone()) {
item.handle(app);
}
})
.build(app)?;

Ok(())
Expand Down
2 changes: 1 addition & 1 deletion plugins/tray/src/menu_items/tray_quit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ impl MenuItemHandler for TrayQuit {
const ID: &'static str = "hypr_tray_quit";

fn build(app: &AppHandle<tauri::Wry>) -> Result<MenuItemKind<tauri::Wry>> {
let item = MenuItem::with_id(app, Self::ID, "Quit Completely", true, Some("cmd+q"))?;
let item = MenuItem::with_id(app, Self::ID, "Quit Completely", true, Some("cmd+shift+q"))?;
Ok(MenuItemKind::MenuItem(item))
}

Expand Down
Loading