Skip to content

Add macOS Menu Bar, Recordable Shortcuts, and Always-On-Top Scratchpad#21

Open
tiimmmoooo wants to merge 4 commits intoerictli:mainfrom
tiimmmoooo:feat/sticky-scratch
Open

Add macOS Menu Bar, Recordable Shortcuts, and Always-On-Top Scratchpad#21
tiimmmoooo wants to merge 4 commits intoerictli:mainfrom
tiimmmoooo:feat/sticky-scratch

Conversation

@tiimmmoooo
Copy link

@tiimmmoooo tiimmmoooo commented Feb 12, 2026

Features Added

  • macOS menu bar integration
  • Recordable shortcut settings
  • Always On Top toggle
  • Minimal Always-On-Top Scratchpad mode

Why?

Felt it is a massive QoL change

Summary by CodeRabbit

Release Notes

New Features

  • Added minimal editor window with global shortcut support for quick note access
  • Customizable keyboard shortcuts for all app actions with real-time settings updates
  • Always-on-top window toggle to keep the app visible
  • macOS tray menu integration for convenient app access
  • Automatic restoration of last selected scratchpad on startup

Chores

  • Added tray icon and global shortcut plugin support

@coderabbitai
Copy link

coderabbitai bot commented Feb 12, 2026

📝 Walkthrough

Walkthrough

This PR introduces a comprehensive minimal editor window feature with global keyboard shortcut support. The changes include a new configurable shortcut system across the frontend and backend, window management for a separate minimal scratchpad editor, settings persistence for last-selected scratchpad and shortcuts, and macOS tray integration with global shortcut registration.

Changes

Cohort / File(s) Summary
Global Shortcut System
src/lib/shortcuts.ts
New module implementing full keyboard shortcut infrastructure: parsing, normalization, event matching, display formatting, and default mappings. Supports modifier validation and locale-appropriate rendering of shortcut keys.
Minimal Editor Backend
src-tauri/src/lib.rs
Extends AppState with toggle-in-progress flag; adds Settings fields for shortcuts and last-selected scratchpad; implements seven new Tauri commands (create_note, toggle_minimal_editor, toggle_always_on_top, get_or_create_minimal_scratchpad, set_last_selected_scratchpad, etc.); integrates global shortcut registration and macOS tray menu setup; modifies update_settings to accept AppHandle for persistence and shortcut re-registration.
Type & Settings Definitions
src/types/note.ts, src-tauri/capabilities/default.json
Adds ShortcutAction union type and ShortcutSettings to Settings struct; adds lastSelectedScratchpadId field; expands windows capability array to include "minimal" window label alongside "main".
App Dependencies
src-tauri/Cargo.toml
Adds tauri tray-icon feature and new tauri-plugin-global-shortcut = "2" dependency for OS-level shortcut handling.
Shortcut State Management
src/context/ThemeContext.tsx
Introduces shortcut state with setShortcut and resetShortcuts APIs; wires shortcut loading from backend settings during initialization; persists shortcut changes via saveShortcutSettings helper.
Component Shortcut Integration
src/components/editor/Editor.tsx, src/components/command-palette/CommandPalette.tsx, src/components/settings/SettingsPage.tsx
Replace hardcoded shortcut strings with dynamic theme-based shortcuts; add new shortcut labels and keyboard handlers; update UI hints and tooltips to reflect configurable shortcuts; add new commands (Toggle Always on Top, Toggle Minimal Scratchpad).
Interactive Shortcut Configuration
src/components/settings/ShortcutsSettingsSection.tsx
Introduces typed editable and static shortcut rows; adds interactive keyboard capture mechanism with validation; implements category-based shortcut grouping; adds reset-to-defaults UI and help text.
App Entry Point & Minimal Mode
src/App.tsx
Adds minimalMode prop to AppContent; establishes tray and global shortcut event listeners; implements minimal startup logic to load/create scratchpad and restore last selection; adds rate-limited toggle actions with error handling; conditions update checks and standard flow when in minimal mode.
Service Layer
src/services/notes.ts
Adds four new public command wrappers: toggleAlwaysOnTop, toggleMinimalEditor, getOrCreateMinimalScratchpad, setLastSelectedScratchpad for backend communication.
Styling & Config
src/App.css, CLAUDE.md
Adds minimal-scratchpad-shell CSS class with border radius, border styling, and inset shadow (including dark mode variant); adds minimal config file.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant OS as OS/Global Shortcut
    participant App as App (React)
    participant Backend as Tauri Backend
    participant Settings as Settings Storage

    User->>OS: Press Mod+Shift+M (minimal toggle)
    OS->>App: Global shortcut event (global-toggle-minimal-editor)
    App->>App: Rate-limit check & state validation
    App->>Backend: toggle_minimal_editor()
    Backend->>Backend: Check if minimal window exists
    alt Minimal window not open
        Backend->>Backend: get_or_create_minimal_scratchpad()
        Backend->>Settings: Load/create minimal scratchpad
        Backend->>Backend: Create minimal window
    else Minimal window open
        Backend->>Backend: Close/hide minimal window
    end
    Backend->>App: Return opened note id
    App->>App: Update minimal mode state
    App->>User: Show minimal editor or return focus
Loading
sequenceDiagram
    participant User
    participant Settings as Settings UI
    participant Capture as Keyboard Capture
    participant Theme as ThemeContext
    participant Backend as Backend API
    participant Storage as Settings Storage

    User->>Settings: Click editable shortcut row
    Settings->>Capture: Enable capture mode
    User->>Capture: Press key combination (e.g., Cmd+B)
    Capture->>Capture: Validate modifier requirements
    alt Valid shortcut
        Capture->>Theme: setShortcut(action, normalized)
        Theme->>Backend: saveShortcutSettings()
        Backend->>Storage: Persist shortcuts
        Backend->>App: Re-register global shortcuts
        Capture->>Settings: Display updated shortcut
    else Invalid (missing modifier)
        Capture->>User: Show validation error toast
    end
    User->>Settings: Click reset defaults
    Settings->>Theme: resetShortcuts()
    Theme->>Backend: saveShortcutSettings()
    Backend->>Storage: Restore defaults
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 Hops with glee through shortcuts long and grand,
A minimal window dances at command,
Global keybinds whisper from the sky,
Settings saved, no detail left awry,
This rabbit celebrates the flow so bright!

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 46.03% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the three main changes: macOS menu bar integration, recordable shortcuts, and always-on-top scratchpad functionality.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Tip

Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@AGENTS.md`:
- Line 1: AGENTS.md currently contains only the text "CLAUDE.md" and appears to
be accidental; either remove AGENTS.md from the commit/PR or replace its
contents with the intended agent documentation; if it was meant as a
placeholder, update AGENTS.md to include the proper agent instructions or README
content (title, purpose, usage) instead of the lone "CLAUDE.md" string so the
repository no longer contains this meaningless file.

In `@src/components/command-palette/CommandPalette.tsx`:
- Around line 347-349: The useMemo that builds the command list in
CommandPalette.tsx omits shortcuts.openMinimalEditor from its dependency array,
so getShortcutDisplayText(shortcuts.openMinimalEditor) won't update when the
binding changes; update the dependency array for that useMemo to include
shortcuts.openMinimalEditor (alongside shortcuts.createNote,
shortcuts.toggleAlwaysOnTop, shortcuts.openSettings) so the memo recomputes and
the displayed label updates when the shortcut is remapped.
🧹 Nitpick comments (13)
src-tauri/capabilities/default.json (2)

4-5: Description is stale now that two windows share this capability.

Line 4 says "Capability for the main window" but it now also covers the minimal window. A small wording tweak (e.g., "Capability for application windows") would keep it accurate.

✏️ Suggested fix
-  "description": "Capability for the main window",
+  "description": "Capability for main and minimal windows",

5-25: Consider whether the minimal window needs the full permission set.

The minimal window now inherits every permission granted to main, including updater:default, dialog:allow-open, opener:allow-reveal-item-in-dir, and broad fs access. If the minimal scratchpad only needs to read/write notes, you could define a narrower capability for it, following the principle of least privilege. Not blocking, but worth considering. As per coding guidelines, "Add new permissions to src-tauri/capabilities/default.json following Tauri v2 capability-based permissions."

src/App.css (1)

116-127: The dark-mode override is identical to the base rule and can be removed.

Lines 123–127 (html.dark .minimal-scratchpad-shell) declare the exact same border, background, and box-shadow as lines 116–121. Since both reference the same CSS custom properties (which already switch values via the .dark class on :root), the dark-mode block is redundant.

Additionally, Stylelint flags the rgba(255, 255, 255, 0.04) on lines 120 and 126 — modern CSS notation should use rgb(255 255 255 / 4%).

♻️ Proposed fix
 .minimal-scratchpad-shell {
   border-radius: 1.1rem;
   border: 1px solid var(--color-border);
   background: var(--color-bg);
-  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04);
+  box-shadow: inset 0 1px 0 rgb(255 255 255 / 4%);
 }
 
-html.dark .minimal-scratchpad-shell {
-  border: 1px solid var(--color-border);
-  background: var(--color-bg);
-  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04);
-}
src/context/ThemeContext.tsx (2)

260-285: No duplicate shortcut conflict detection.

setShortcut validates that the shortcut is well-formed and has the required modifiers, but it doesn't check whether the same shortcut string is already assigned to a different action. If a user binds the same key combo to two actions, both handlers will fire on keypress. Consider checking for conflicts and either rejecting the assignment or unassigning the conflicting action.

♻️ Sketch of a conflict check
  const setShortcut = useCallback(
    (action: ShortcutAction, shortcut: string) => {
      const normalized = normalizeShortcut(shortcut);
      if (!normalized) return false;
      const parsed = parseShortcut(normalized);
      if (!parsed) return false;

      const hasAnyModifier = parsed.mod || parsed.alt || parsed.shift;
      const allowWithoutCmdCtrl =
        action === "navigateNoteUp" ||
        action === "navigateNoteDown" ||
        action === "openMinimalEditor";

      if (!allowWithoutCmdCtrl && !parsed.mod) return false;
      if (action === "openMinimalEditor" && !hasAnyModifier) return false;

+     // Check for conflicts with other actions
+     const conflicting = Object.entries(shortcuts).find(
+       ([key, value]) => key !== action && normalizeShortcut(value) === normalized,
+     );
+     if (conflicting) return false;
+
      setShortcuts((prev) => {
        const updated = { ...prev, [action]: normalized };
        saveShortcutSettings(updated);
        return updated;
      });

      return true;
    },
-   [saveShortcutSettings]
+   [saveShortcutSettings, shortcuts]
  );

244-258: Read-modify-write race in saveShortcutSettings.

getSettings()updateSettings({...settings, shortcuts}) is a non-atomic read-modify-write. If a theme or font save runs concurrently, one write can clobber the other. The same pattern exists for saveThemeSettings and saveFontSettings, so this is a pre-existing issue, but it grows more likely now that three independent save paths share the same settings object. Not blocking, but worth noting for future consolidation (e.g., a debounced single-writer or optimistic merge).

src/App.tsx (2)

600-606: getCurrentWindow() is called on every render of App.

isMinimalWindow is computed in the function body, so getCurrentWindow() runs each time App re-renders. The result never changes (a window's label is constant), so this could be hoisted to module scope or memoized. It's unlikely to cause a real performance issue since App re-renders infrequently, but it's slightly cleaner as a constant.

♻️ Hoist to module scope
+let isMinimalWindow = false;
+try {
+  isMinimalWindow = getCurrentWindow().label === "minimal";
+} catch {
+  // Running without Tauri (e.g. plain Vite dev server).
+}
+
 function App() {
-  let isMinimalWindow = false;
-  try {
-    isMinimalWindow = getCurrentWindow().label === "minimal";
-  } catch {
-    // Running without Tauri (e.g. plain Vite dev server).
-  }
-
   // Add platform class for OS-specific styling ...

146-152: Persisting lastSelectedScratchpadId on every note selection.

This fires on every selectedNoteId change in the main window, which means every note switch triggers a backend write. This is fine if the backend write is cheap (just updating a JSON settings file), but could be noisy. Consider debouncing this if it proves to be a performance concern — the coding guidelines specify debouncing for "auto-save (300ms)." For now it's acceptable since note switches are infrequent and user-initiated.

src/components/settings/ShortcutsSettingsSection.tsx (1)

159-161: Potential duplicate React key in ShortcutKeys.

Using key={key} means that if a shortcut display ever contains duplicate strings (e.g., two identical modifier symbols), React will warn. Consider using the index as key or a composite key.

Proposed fix
-      {keys.map((key) => (
-        <KeyboardKey key={key} keyLabel={key} />
+      {keys.map((key, i) => (
+        <KeyboardKey key={`${key}-${i}`} keyLabel={key} />
       ))}
src/components/editor/Editor.tsx (1)

1225-1235: Fragile sidebar detection using CSS class substring match.

target.closest('[class*="sidebar"]') relies on a specific class naming convention. If class names are changed, minified, or use a different naming pattern, this check will silently break, causing find-in-note to trigger while focused in the sidebar.

Consider using a data-* attribute instead for a more robust contract.

Example

In the sidebar container element, add:

<div data-region="sidebar" ...>

Then in the check:

-      if (target.closest('[class*="sidebar"]')) {
+      if (target.closest('[data-region="sidebar"]')) {
         return;
       }
src-tauri/src/lib.rs (3)

638-662: unregister_all() removes all global shortcuts indiscriminately.

register_global_minimal_shortcut calls global_shortcut.unregister_all() on Line 641 before registering the minimal editor shortcut. If additional global shortcuts are registered in the future, this will silently remove them. Consider tracking and unregistering only the previously-registered minimal shortcut instead.

Sketch: track previous shortcut and unregister selectively
+// Store the last registered shortcut string in AppState so we can unregister just that one.
+pub last_registered_shortcut: Mutex<Option<String>>,

Then in register_global_minimal_shortcut:

-    global_shortcut
-        .unregister_all()
-        .map_err(|e| e.to_string())?;
+    // Unregister only the previous shortcut if one was registered
+    if let Some(prev) = state.last_registered_shortcut.lock().expect("mutex").take() {
+        let _ = global_shortcut.unregister(prev.as_str());
+    }

2173-2183: Redundant double pattern match on CloseRequested event.

The outer matches! check and inner if let both match the same variant. You can simplify to a single if let.

Proposed simplification
         .on_window_event(|window, event| {
             #[cfg(target_os = "macos")]
-            if window.label() == "main"
-                && matches!(event, tauri::WindowEvent::CloseRequested { .. })
             {
-                if let tauri::WindowEvent::CloseRequested { api, .. } = event {
+            if window.label() == "main" {
+                if let tauri::WindowEvent::CloseRequested { api, .. } = event {
                     api.prevent_close();
                     let _ = window.hide();
                 }
             }
         })

948-959: Settings write-then-read pattern has a narrow race window.

Between releasing the write lock (line 955) and acquiring the read lock (line 957), another thread could modify settings. In a single-user desktop app this is unlikely, but the pattern could be simplified to save within the write lock scope.

Suggested pattern: save while still holding data
     let mut settings_changed = false;
+    let settings_snapshot;
     if let Some((ref old_id_str, _)) = old_id {
         let mut settings = state.settings.write().expect("settings write lock");
         if settings.last_selected_scratchpad_id.as_deref() == Some(old_id_str) {
             settings.last_selected_scratchpad_id = Some(final_id.clone());
             settings_changed = true;
         }
+        settings_snapshot = settings.clone();
+    } else {
+        settings_snapshot = state.settings.read().expect("settings read lock").clone();
     }
     if settings_changed {
-        let settings = state.settings.read().expect("settings read lock");
-        save_settings(&folder, &settings).map_err(|e| e.to_string())?;
+        save_settings(&folder, &settings_snapshot).map_err(|e| e.to_string())?;
     }
src/lib/shortcuts.ts (1)

84-99: Consider handling the Dead key event for international keyboards.

On some keyboard layouts (e.g., German, French), certain key combinations produce event.key === "Dead" for dead keys (accents). normalizeEventKey would pass "Dead" through to normalizeKeyToken and produce "Dead" as a valid key. This could allow recording a shortcut with "Dead" as the key, which won't match on subsequent presses since the actual composed character will differ.

This is an edge case that may not need immediate attention but is worth noting if international keyboard support matters.

@@ -0,0 +1 @@
CLAUDE.md No newline at end of file
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

This file appears to be committed by mistake.

AGENTS.md contains only the text "CLAUDE.md" with no meaningful content. Was this intentionally included in the PR, or is it an artifact? If it's meant to be an agent/AI instruction file, it likely needs actual content; otherwise, consider removing it.

🤖 Prompt for AI Agents
In `@AGENTS.md` at line 1, AGENTS.md currently contains only the text "CLAUDE.md"
and appears to be accidental; either remove AGENTS.md from the commit/PR or
replace its contents with the intended agent documentation; if it was meant as a
placeholder, update AGENTS.md to include the proper agent instructions or README
content (title, purpose, usage) instead of the lone "CLAUDE.md" string so the
repository no longer contains this meaningless file.

Comment on lines +347 to +349
shortcuts.createNote,
shortcuts.toggleAlwaysOnTop,
shortcuts.openSettings,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Missing shortcuts.openMinimalEditor in useMemo dependency array.

Line 130 uses getShortcutDisplayText(shortcuts.openMinimalEditor) to render the shortcut label, but shortcuts.openMinimalEditor is not listed in the dependency array. If the user remaps this shortcut, the command palette label won't update.

Proposed fix
     shortcuts.createNote,
     shortcuts.toggleAlwaysOnTop,
+    shortcuts.openMinimalEditor,
     shortcuts.openSettings,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
shortcuts.createNote,
shortcuts.toggleAlwaysOnTop,
shortcuts.openSettings,
shortcuts.createNote,
shortcuts.toggleAlwaysOnTop,
shortcuts.openMinimalEditor,
shortcuts.openSettings,
🤖 Prompt for AI Agents
In `@src/components/command-palette/CommandPalette.tsx` around lines 347 - 349,
The useMemo that builds the command list in CommandPalette.tsx omits
shortcuts.openMinimalEditor from its dependency array, so
getShortcutDisplayText(shortcuts.openMinimalEditor) won't update when the
binding changes; update the dependency array for that useMemo to include
shortcuts.openMinimalEditor (alongside shortcuts.createNote,
shortcuts.toggleAlwaysOnTop, shortcuts.openSettings) so the memo recomputes and
the displayed label updates when the shortcut is remapped.

@erictli
Copy link
Owner

erictli commented Feb 12, 2026

@tiimmmoooo lots of good ideas here. May need a bit of time to review this, will try to look over the weekend.

@erictli
Copy link
Owner

erictli commented Feb 18, 2026

Update: this is a big one so haven't gotten to it yet. I'm trying to keep the app slim so I may need to look at the individual features and implement some of them individually. Will credit you if i do.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

Comments