How Global Shortcuts and System Notifications Are Implemented in Clash Nyanpasu

Clash Nyanpasu implements global shortcuts using Tauri v2's global-shortcut plugin with a Rust-based hotkey manager that binds keyboard combinations to functions in feat.rs, while system notifications are handled by a TypeScript wrapper around Tauri's notification plugin that falls back to in-app toasts when permissions are denied.

Clash Nyanpasu is a modern GUI client for Clash built with Tauri v2, combining a Rust backend with a TypeScript frontend. The application provides native desktop integration through two key features: global shortcuts that allow users to trigger actions from anywhere using keyboard combinations, and system notifications that deliver native desktop alerts for important events. Both features leverage Tauri's official plugins while implementing custom logic for configuration management and cross-platform compatibility.

Global Shortcuts Implementation

Configuration Storage and Format

User-configurable shortcuts are stored in the verge configuration under verge.latest().hotkeys. Each entry follows a specific string format: "function_name,key_combination", such as "toggle_dashboard,Ctrl+Shift+D" or "clash_mode_rule,Ctrl+Shift+R".

The hotkey manager parses these strings to extract the function identifier and the keyboard shortcut. This configuration lives in backend/tauri/src/core/hotkey.rs, where the Hotkey struct manages the lifecycle of all registered shortcuts.

Initialization Flow

When the application starts, the backend initialization sequence in backend/tauri/src/lib.rs creates the hotkey singleton and triggers registration:

// In Backend::init
Hotkey::global().init(app_handle)?;

The Hotkey::init method retrieves the saved hotkey configuration from verge settings, iterates through each entry, and calls self.register(key, func) to bind the keyboard combination to its corresponding action. This process establishes the connection between user-defined shortcuts and the Rust functions that execute the desired operations.

Registration and Callback Binding

The actual registration uses Tauri's global-shortcut plugin through the GlobalShortcut manager. In backend/tauri/src/core/hotkey.rs, the register method implements the binding logic:

let manager = app_handle.global_shortcut();
manager.on_shortcut(hotkey, move |_app_handle, hotkey, ev| {
    if let ShortcutState::Pressed = ev.state {
        tracing::info!("hotkey pressed: {}", hotkey);
        f();  // Invoke the matched function
    }
})?;

The closure f is determined by matching the function name from the configuration against concrete implementations in backend/tauri/src/feat.rs:

let f = match func.trim() {
    "open_or_close_dashboard" => feat::toggle_dashboard,
    "clash_mode_rule"       => || feat::change_clash_mode("rule".into()),
    "clash_mode_global"     => || feat::change_clash_mode("global".into()),
    "clash_mode_direct"     => || feat::change_clash_mode("direct".into()),
    "toggle_system_proxy"   => feat::toggle_system_proxy,
    _ => bail!("invalid function \"{func}\""),
};

This architecture decouples the shortcut configuration from the implementation, allowing users to customize key combinations without modifying the underlying Rust code.

Runtime Updates

When users modify shortcuts through the UI, the changes propagate through feat::update_hotkeys, which forwards the new configuration to Hotkey::update(new_hotkeys). This method performs a diff calculation between the old and new shortcut maps to minimize re-registrations:

  1. Validation: Each new key combination is parsed using Shortcut::parse to ensure validity
  2. Diffing: The get_ops function identifies added, changed, and removed shortcuts
  3. Atomic updates: The method registers new shortcuts before unregistering old ones to prevent gaps in functionality

This approach ensures that users can reconfigure hotkeys without restarting the application.

Cleanup and Lifecycle Management

The Hotkey struct implements the Drop trait to ensure proper cleanup when the application exits:

impl Drop for Hotkey {
    fn drop(&mut self) {
        // Unregister all shortcuts to avoid dangling listeners
        if let Some(app_handle) = self.app_handle.get() {
            let manager = app_handle.global_shortcut();
            for (key, _) in self.hotkeys.iter() {
                let _ = manager.unregister(key);
            }
        }
    }
}

This prevents system-level shortcut leaks that could interfere with other applications after Clash Nyanpasu closes.

Dependencies

The global shortcut functionality relies on the Tauri global-shortcut plugin, declared in backend/tauri/Cargo.toml with platform-specific targeting:

[target."cfg(not(any(target_os = \"android\", target_os = \"ios\")))".dependencies]
tauri-plugin-global-shortcut = "2.2.0"

This ensures the plugin is only compiled for desktop platforms where global hotkeys are supported.

System Notifications Implementation

Frontend API Wrapper

System notifications are managed through a TypeScript utility in frontend/nyanpasu/src/utils/notification.ts. The notification() function provides a unified interface for displaying alerts:

export const notification = async ({
  title,
  body,
  type = NotificationType.Info,
}: NotificationOptions) => {
  // Permission check and notification logic
};

This wrapper abstracts the underlying Tauri notification API while adding application-specific logic for permission handling and fallback behavior.

Permission Handling Strategy

The notification system implements a robust permission workflow:

  1. Portable mode detection: The function first checks isPortable(). When running in portable mode (e.g., from a USB drive), the system bypasses native notifications entirely to avoid permission dialogs on unfamiliar systems.

  2. Permission verification: For standard installations, checkPermission() verifies isPermissionGranted(). If permissions haven't been requested, it calls requestPermission() and caches the result.

  3. Graceful degradation: When permissions are denied or unavailable, the system falls back to an in-app toast notification using the MUI-based Notice component.

Native Notification Delivery

When permissions are granted, the wrapper constructs an Options object and invokes Tauri's native notification API:

import { sendNotification } from '@tauri-apps/plugin-notification';

const options = {
  title,
  body,
};

sendNotification(options);

This delegates to the operating system's native notification center, ensuring consistent appearance with other desktop applications.

Fallback UI Component

The Notice component, imported from @/components/base, provides visual feedback when native notifications are unavailable. It supports multiple notification types (Success, Info, Error) and integrates with the application's MUI-based design system.

This fallback ensures users always receive feedback regardless of system permissions or portable mode restrictions.

Dependencies

The notification system requires two levels of dependencies:

Rust backend (backend/tauri/Cargo.toml):

tauri-plugin-notification = "2.2"

Frontend TypeScript (frontend/nyanpasu/package.json):

"@tauri-apps/plugin-notification": "^2.0.0"

These provide the bridge between the JavaScript API and the native operating system notification centers.

Interaction Between Shortcuts and Notifications

While global shortcuts and notifications operate independently, they frequently interact in typical user workflows:

  1. Shortcut triggers action: A user presses Ctrl+Shift+D (toggle dashboard), which invokes feat::toggle_dashboard via the hotkey manager in backend/tauri/src/core/hotkey.rs.

  2. State change propagates: The backend function modifies the Clash core configuration and may emit an event through the IPC bridge (nyanpasu-ipc) to synchronize the UI state.

  3. Notification provides feedback: The frontend receives the state update and calls notification({title: "System Proxy", body: "Enabled", type: NotificationType.Success}) to display a native desktop alert confirming the action.

This decoupled architecture allows hotkeys to execute logic without direct UI dependencies, while notifications can originate from either frontend or backend depending on which layer detects the event requiring user attention.

Summary

  • Global shortcuts in Clash Nyanpasu rely on Tauri v2's tauri-plugin-global-shortcut, with a custom Hotkey manager in backend/tauri/src/core/hotkey.rs that parses user configuration, binds shortcuts to functions in feat.rs, and handles runtime updates through diff-based re-registration.

  • System notifications use Tauri's tauri-plugin-notification through a TypeScript wrapper in frontend/nyanpasu/src/utils/notification.ts that checks permissions, respects portable mode restrictions, and falls back to MUI Notice toasts when native notifications are unavailable.

  • Both features are cross-platform (desktop only) and decoupled, allowing shortcuts to trigger backend logic while notifications provide user feedback from either frontend or backend contexts.

Frequently Asked Questions

How are global shortcuts configured in Clash Nyanpasu?

Global shortcuts are stored in the verge configuration under verge.latest().hotkeys as strings formatted as "function_name,key_combination", such as "toggle_dashboard,Ctrl+Shift+D". The Hotkey manager in backend/tauri/src/core/hotkey.rs parses these entries during initialization and binds each key combination to its corresponding function in backend/tauri/src/feat.rs using Tauri's global-shortcut plugin.

What happens when a user triggers a global shortcut?

When a registered shortcut is pressed, Tauri's global-shortcut manager invokes the callback registered in hotkey.rs. The callback checks if the key state is ShortcutState::Pressed, then executes the mapped function from feat.rs—such as feat::toggle_dashboard or feat::change_clash_mode. These functions interact with the Clash core or emit IPC events to update the UI, providing immediate system-level control regardless of which window is focused.

Why do notifications fall back to in-app toasts instead of native notifications?

The notification system in frontend/nyanpasu/src/utils/notification.ts implements a fallback strategy for two scenarios: when the application runs in portable mode (detected via isPortable()), or when the user has denied notification permissions. In these cases, the wrapper displays an in-app toast using the MUI Notice component instead of calling sendNotification() from @tauri-apps/plugin-notification. This ensures users always receive feedback even when native desktop notifications are unavailable or inappropriate.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →