# How Global Shortcuts and System Notifications Are Implemented in Clash Nyanpasu

> Discover how Clash Nyanpasu implements global shortcuts with Tauri v2 and Rust, and handles system notifications via TypeScript and Tauri's plugins. Learn about the feat.rs integration and fallback mechanisms.

- Repository: [Nyanpasu/clash-nyanpasu](https://github.com/libnyanpasu/clash-nyanpasu)
- Tags: internals
- Published: 2026-03-06

---

**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`](https://github.com/libnyanpasu/clash-nyanpasu/blob/main/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`](https://github.com/libnyanpasu/clash-nyanpasu/blob/main/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`](https://github.com/libnyanpasu/clash-nyanpasu/blob/main/backend/tauri/src/lib.rs) creates the hotkey singleton and triggers registration:

```rust
// 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`](https://github.com/libnyanpasu/clash-nyanpasu/blob/main/backend/tauri/src/core/hotkey.rs), the `register` method implements the binding logic:

```rust
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`](https://github.com/libnyanpasu/clash-nyanpasu/blob/main/backend/tauri/src/feat.rs):

```rust
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:

```rust
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`](https://github.com/libnyanpasu/clash-nyanpasu/blob/main/backend/tauri/Cargo.toml) with platform-specific targeting:

```toml
[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`](https://github.com/libnyanpasu/clash-nyanpasu/blob/main/frontend/nyanpasu/src/utils/notification.ts). The `notification()` function provides a unified interface for displaying alerts:

```typescript
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:

```typescript
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`](https://github.com/libnyanpasu/clash-nyanpasu/blob/main/backend/tauri/Cargo.toml)):

```toml
tauri-plugin-notification = "2.2"

```

**Frontend TypeScript** ([`frontend/nyanpasu/package.json`](https://github.com/libnyanpasu/clash-nyanpasu/blob/main/frontend/nyanpasu/package.json)):

```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`](https://github.com/libnyanpasu/clash-nyanpasu/blob/main/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`](https://github.com/libnyanpasu/clash-nyanpasu/blob/main/backend/tauri/src/core/hotkey.rs) that parses user configuration, binds shortcuts to functions in [`feat.rs`](https://github.com/libnyanpasu/clash-nyanpasu/blob/main/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`](https://github.com/libnyanpasu/clash-nyanpasu/blob/main/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`](https://github.com/libnyanpasu/clash-nyanpasu/blob/main/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`](https://github.com/libnyanpasu/clash-nyanpasu/blob/main/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`](https://github.com/libnyanpasu/clash-nyanpasu/blob/main/hotkey.rs). The callback checks if the key state is `ShortcutState::Pressed`, then executes the mapped function from [`feat.rs`](https://github.com/libnyanpasu/clash-nyanpasu/blob/main/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`](https://github.com/libnyanpasu/clash-nyanpasu/blob/main/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.