Skip to content

Commit 0f328cc

Browse files
synleclaude
andcommitted
docs: document macOS tray icon pitfall (async commands + mutex contention)
Add warnings to CLAUDE.md, DEV.md (rules 9-10, known limitations, adding new commands), and README.md (known issues) explaining: - Tauri commands with State<AppState> must be async on macOS - write_debug_log() must not be used in frequently-called sync commands - Both patterns starve the macOS main-thread run-loop, breaking tray clicks Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 191eda7 commit 0f328cc

3 files changed

Lines changed: 31 additions & 9 deletions

File tree

CLAUDE.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,16 @@ After making changes to frontend code (`src/`), config files, or docs, always ru
5757
4. **Method comments**: Always add doc comments to all new functions and methods. Rust uses `///` doc comments; TypeScript/React uses `/** */` JSDoc comments. Every public function, Tauri command, React component, and non-trivial helper must have a comment describing what it does.
5858
5. **CLI sidecar version bumps**: When updating `displayDjCliVersion` in `package.json`, always check the [display-dj-cli changelog and commits](https://github.com/synle/display-dj-cli) for upstream changes (new endpoints, changed response formats, removed features). Update our code to use any new APIs and remove usage of deprecated ones. Document the changes in CLAUDE.md and CONTRIBUTING.md.
5959

60+
## macOS Tray Icon Pitfall (Critical)
61+
62+
On macOS, two patterns in Tauri command handlers break the system tray icon — both left-click and right-click stop working entirely:
63+
64+
1. **Sync Tauri commands that access `AppState`**: Declaring a `#[tauri::command]` as `pub fn` (sync) instead of `pub async fn` causes Tauri to run it on a blocking thread that starves the macOS main-thread run-loop, preventing `on_tray_icon_event` from firing. All Tauri commands that access `State<'_, AppState>` must be `async`.
65+
66+
2. **`write_debug_log()` in frequently-called sync commands**: `write_debug_log()` locks `state.preferences` to check `debug_logging`. Using it in `get_preferences` (sync, called on every frontend render) creates enough mutex contention to starve the run-loop. Use `log::info!` instead in sync commands. `write_debug_log()` is safe in async/infrequent commands like `save_preferences`.
67+
68+
These are documented inline in `config.rs` with WARNING comments.
69+
6070
## Key Conventions
6171

6272
- All Rust structs sent to frontend use `#[serde(rename_all = "camelCase")]`

DEV.md

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,10 @@ lib.rs background thread (every 60s)
416416

417417
8. **The sidecar port is global.** Set once at startup in `AtomicU16`, accessed everywhere via `crate::server_port()`. All modules use `base_url()` to build HTTP URLs.
418418

419+
9. **Tauri commands accessing AppState must be `async` on macOS.** Sync `#[tauri::command]` functions that take `State<'_, AppState>` block the macOS main-thread run-loop, preventing tray icon click events from firing. This was the root cause of a hard-to-diagnose bug where both left-click and right-click on the tray icon stopped working. See `config.rs` `save_preferences` for the documented warning.
420+
421+
10. **Do not use `write_debug_log()` in frequently-called sync commands.** `write_debug_log()` locks `state.preferences` to check the `debug_logging` flag. In sync Tauri commands called on every frontend render (like `get_preferences`), this mutex contention starves the macOS run-loop and breaks tray icon events. Use `log::info!` in those paths. `write_debug_log()` is safe in async or infrequently-called commands.
422+
419423
---
420424

421425
## Where to Edit
@@ -443,12 +447,15 @@ Quick reference for common tasks:
443447

444448
### 1. Define the Rust function
445449

446-
In the appropriate module (e.g., `display.rs`):
450+
In the appropriate module (e.g., `display.rs`). **Use `async fn` if the command accesses `State<'_, AppState>`** — sync commands with state access break macOS tray icon events (see rule 9):
447451

448452
```rust
449453
/// Does something useful.
450454
#[tauri::command]
451-
pub async fn my_new_command(some_param: String) -> Result<String, String> {
455+
pub async fn my_new_command(
456+
state: tauri::State<'_, crate::AppState>,
457+
some_param: String,
458+
) -> Result<String, String> {
452459
Ok(format!("Hello {}", some_param))
453460
}
454461
```
@@ -613,13 +620,14 @@ kill %1
613620

614621
## Known Limitations
615622

616-
| Limitation | Details |
617-
| --------------------------- | ------------------------------------------------------------ |
618-
| DDC/CI not universal | Budget monitors and some HDMI connections may not support it |
619-
| Built-in HDMI on base M1/M2 | No DDC/CI support. Use USB-C/DisplayPort |
620-
| Global shortcuts on Wayland | Wayland restricts global hotkey capture. X11 works fine |
621-
| Tray left-click on Linux | AppIndicator doesn't always fire left-click |
622-
| Dark mode on non-GNOME | `gsettings` is GNOME-specific. KDE, XFCE not supported |
623+
| Limitation | Details |
624+
| --------------------------- | --------------------------------------------------------------------------------------------------------- |
625+
| DDC/CI not universal | Budget monitors and some HDMI connections may not support it |
626+
| Built-in HDMI on base M1/M2 | No DDC/CI support. Use USB-C/DisplayPort |
627+
| Global shortcuts on Wayland | Wayland restricts global hotkey capture. X11 works fine |
628+
| Tray clicks dead on macOS | Sync Tauri commands or `write_debug_log()` in hot sync commands starve the run-loop. See rules 9-10 above |
629+
| Tray left-click on Linux | AppIndicator doesn't always fire left-click |
630+
| Dark mode on non-GNOME | `gsettings` is GNOME-specific. KDE, XFCE not supported |
623631

624632
---
625633

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,10 @@ Config files live in `~/Library/Application Support/display-dj/` (macOS), `%APPD
8080
- Built-in HDMI on base M1/M2 Macs doesn't support DDC/CI -- use USB-C/DisplayPort
8181
- Linux global shortcuts may not work under Wayland (X11 works fine)
8282

83+
### Developer Note: macOS Tray Icon
84+
85+
If tray icon clicks stop working on macOS after code changes, the most likely causes are: (1) a Tauri command was changed from `async` to sync, or (2) `write_debug_log()` was added to a frequently-called sync command. Both starve the macOS main-thread run-loop. See `config.rs` inline warnings and [DEV.md](DEV.md) rules 9-10 for details.
86+
8387
## Contributing
8488

8589
See **[CONTRIBUTING.md](CONTRIBUTING.md)** for the full development setup (per-platform), project structure, testing, and platform guides. See **[DEV.md](DEV.md)** for the architecture deep-dive: request lifecycle, layer-by-layer breakdown, data flow diagrams, and "where to edit" reference.

0 commit comments

Comments
 (0)