From 6f872e0e02e5fffca1fb8040f608753ccc29d608 Mon Sep 17 00:00:00 2001 From: VariableThe Date: Tue, 23 Jun 2026 14:38:11 +0530 Subject: [PATCH] fix: make window auto-hide and alt keybinds conditional for Hyprland --- AUDIT_LOG.md | 7 ++++++ CHANGELOG.md | 2 ++ package-lock.json | 41 ++++++++++---------------------- src-tauri/src/commands/fs.rs | 18 +++++++++++++- src-tauri/src/commands/system.rs | 5 ++++ src-tauri/src/lib.rs | 6 ++++- src/App.tsx | 3 +++ src/Settings.tsx | 28 ++++++++++++---------- src/api.ts | 1 + src/components/NoteSearch.tsx | 5 ++-- src/hooks/useGlobalHotkey.ts | 23 ++++++++++-------- src/lib/editor/extensions.ts | 16 ++++++++----- src/setupTests.ts | 24 +++++++++++++++++++ src/store/useAppStore.ts | 4 ++++ src/types.d.ts | 1 + 15 files changed, 122 insertions(+), 62 deletions(-) diff --git a/AUDIT_LOG.md b/AUDIT_LOG.md index d944a2f..5750f39 100644 --- a/AUDIT_LOG.md +++ b/AUDIT_LOG.md @@ -2,6 +2,13 @@ This log tracks all significant changes, updates, and versions in the PaperCache project. +## 2026-06-23 - (Uncommitted) +**Change:** fix: shift all keybinds to Alt and disable window auto-hide + +**Details/Why:** +Changed default keybindings across the application from `Ctrl/Cmd` to `Alt` to prevent conflicts. Disabled auto-hide on focus loss in `lib.rs` to allow the app to be used as a persistent window, fixing Wayland shortcut issues. + +--- ## 2026-06-23 - a250dce8a **Change:** feat: enable auto-updates on startup and add manual check button diff --git a/CHANGELOG.md b/CHANGELOG.md index 288ba8f..941e193 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - **Tauri Migration**: PaperCache has been fully migrated from Electron to Tauri, reducing memory usage, startup time, and application size while preserving existing workflows. - The macOS distribution format is now `.tar.gz` and `.app` to circumvent strict macOS 14 runner restrictions with `osascript`. The Homebrew Cask automation pulls the `.tar.gz` bundle. +- **Keybindings**: Changed default global and internal keybindings from `Ctrl/Cmd` to `Alt` to prevent conflicts with terminal emulators and OS shortcuts. +- **Window Behavior**: Disabled the "hide on focus loss" behavior so the application acts as a standard window, fixing global shortcut limitations on Wayland compositors (like Hyprland). ### Fixed - Addressed multiple edge-cases with `CodeMirror` state overwrites causing typed text to disappear or duplicate. diff --git a/package-lock.json b/package-lock.json index b40d595..60e60c2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "papercache", - "version": "0.5.0-beta", + "version": "0.5.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "papercache", - "version": "0.5.0-beta", + "version": "0.5.0", "dependencies": { "@tauri-apps/api": "^2.11.1", "@tauri-apps/plugin-autostart": "^2.5.1", @@ -147,7 +147,6 @@ "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", @@ -548,7 +547,6 @@ "integrity": "sha512-+BIjw/AG3tDQ4pJgTLPYdAW25eDE66YsvM4LKyVPgGzVgZ4a9Wj1SRX8kPVKgBDdPt8oHtZ15F0qx7p0oOHdHw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@codemirror/state": "^6.6.0", "crelt": "^1.0.6", @@ -644,7 +642,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=20.19.0" }, @@ -693,7 +690,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=20.19.0" } @@ -705,7 +701,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" @@ -718,7 +713,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "tslib": "^2.4.0" } @@ -2221,7 +2215,8 @@ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@types/chai": { "version": "5.2.3", @@ -2268,7 +2263,6 @@ "integrity": "sha512-fRa09kZTgu8o71KFcDjUFuc7F+dEbZYZmkI0mg5YBTRs0yMKjYHsq/c0urDKeDb+D5qVgXOdFcuu+DZPKOITwA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.18.0" } @@ -2279,7 +2273,6 @@ "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -2290,7 +2283,6 @@ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "dev": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -2340,7 +2332,6 @@ "integrity": "sha512-PJ5vePq5/ognBbrIcoC5+SHO5dfpeLPzP9FpLkzWrguoYQEeeSjlJpVwOpo1JRSTEi7dRcwNy4h4dzV70PqHcg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.61.1", "@typescript-eslint/types": "8.61.1", @@ -2626,7 +2617,6 @@ "integrity": "sha512-G9/lgqibheLVBDRuya45EbsEXTYcWoSG+TLg7i2axuzx0Eq62eXn+aWXyaVdV5vKvFSWd6ywcX8hA7la9Pvu8g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@bcoe/v8-coverage": "^1.0.2", "@vitest/utils": "4.1.9", @@ -2781,7 +2771,6 @@ "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2838,6 +2827,7 @@ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=8" } @@ -2848,6 +2838,7 @@ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=10" }, @@ -2971,7 +2962,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.38", "caniuse-lite": "^1.0.30001799", @@ -3294,7 +3284,6 @@ "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "dev": true, "license": "ISC", - "peer": true, "engines": { "node": ">=12" } @@ -3443,7 +3432,8 @@ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/electron-to-chromium": { "version": "1.5.376", @@ -3499,7 +3489,6 @@ "dev": true, "hasInstallScript": true, "license": "MIT", - "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -3564,7 +3553,6 @@ "integrity": "sha512-1y+7C+vi12bUK1IpZeaV3gsH9fHLBmPvYmPx42pvT/E9yG0IC8g3PUZZgp0+JLJl7ZDK0flc2gc+Aw9dpCvIsQ==", "dev": true, "license": "MIT", - "peer": true, "workspaces": [ "packages/*" ], @@ -3624,7 +3612,6 @@ "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "license": "MIT", - "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -4826,6 +4813,7 @@ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -5176,7 +5164,6 @@ "integrity": "sha512-N2MylSdi48+5N/6S5j+maeHbUSIzzZ5uOcX5Hm4QpV8Dkb1HFjfAKTKX6yNPJQD9AhcT3ifHNB66tWTTJDi11Q==", "dev": true, "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -5206,6 +5193,7 @@ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -5250,7 +5238,6 @@ "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -5261,7 +5248,6 @@ "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -5292,7 +5278,8 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/react-kapsule": { "version": "2.6.0", @@ -5768,7 +5755,6 @@ "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -5865,7 +5851,6 @@ "integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", @@ -5944,7 +5929,6 @@ "integrity": "sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@vitest/expect": "4.1.9", "@vitest/mocker": "4.1.9", @@ -6218,7 +6202,6 @@ "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", "dev": true, "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/src-tauri/src/commands/fs.rs b/src-tauri/src/commands/fs.rs index 0db0d35..44a20c3 100644 --- a/src-tauri/src/commands/fs.rs +++ b/src-tauri/src/commands/fs.rs @@ -194,10 +194,26 @@ pub fn set_dialog_open(state: tauri::State<'_, crate::DialogState>, open: bool) } pub fn run_onboarding() { + let is_hyprland = std::env::var("HYPRLAND_INSTANCE_SIGNATURE").is_ok() || std::env::var("HYPRLAND_CMD").is_ok(); + let mod_key = if is_hyprland { "Alt" } else { "Command/Ctrl" }; + if let Ok(base) = get_papercache_dir() { let welcome_path = base.join("Welcome.md"); + + let welcome_content = format!( + "# Welcome to PaperCache\n\nThis is your first note. Start typing to edit it, or use {} + Shift + N to create a new one!", + mod_key + ); + if !welcome_path.exists() { - let _ = fs::write(&welcome_path, "# Welcome to PaperCache\n\nThis is your first note. Start typing to edit it, or use shortcuts to create a new one!"); + let _ = fs::write(&welcome_path, &welcome_content); + } else { + // Force update if the file contains the old generic shortcut text + if let Ok(content) = fs::read_to_string(&welcome_path) { + if content.contains("use shortcuts to create a new one!") || content.contains("Command/Ctrl + Shift + N") || content.contains("Alt + Shift + N") { + let _ = fs::write(&welcome_path, &welcome_content); + } + } } let commands_dir = base.join("commands"); diff --git a/src-tauri/src/commands/system.rs b/src-tauri/src/commands/system.rs index cc39f56..f676f2a 100644 --- a/src-tauri/src/commands/system.rs +++ b/src-tauri/src/commands/system.rs @@ -87,3 +87,8 @@ pub async fn check_for_updates(app: tauri::AppHandle) -> Result<(), String> { } Ok(()) } + +#[tauri::command] +pub fn is_hyprland() -> Result { + Ok(std::env::var("HYPRLAND_INSTANCE_SIGNATURE").is_ok() || std::env::var("HYPRLAND_CMD").is_ok()) +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 3dba1c2..d51d614 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -76,7 +76,10 @@ pub fn run() { tauri::WindowEvent::Focused(focused) if !focused && !is_dialog_open.load(Ordering::SeqCst) => { - let _ = w.hide(); + let is_hyprland = std::env::var("HYPRLAND_INSTANCE_SIGNATURE").is_ok() || std::env::var("HYPRLAND_CMD").is_ok(); + if !is_hyprland { + let _ = w.hide(); + } } _ => {} } @@ -107,6 +110,7 @@ pub fn run() { commands::system::open_file, commands::system::set_launch_at_startup, commands::system::check_for_updates, + commands::system::is_hyprland, commands::keychain::set_api_key, commands::keychain::get_api_key_status, commands::keychain::get_api_key, diff --git a/src/App.tsx b/src/App.tsx index c1f573c..4381eeb 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -47,6 +47,9 @@ function App() { useEffect(() => { window.electronAPI.checkForUpdates() + window.electronAPI.isHyprland().then((isHyp) => { + useAppStore.getState().setIsHyprland(isHyp) + }) }, []) useEffect(() => { diff --git a/src/Settings.tsx b/src/Settings.tsx index 337fba8..670d6fd 100644 --- a/src/Settings.tsx +++ b/src/Settings.tsx @@ -1,7 +1,7 @@ import { useState, useEffect } from 'react' import { SETTINGS_KEYS } from './lib/settingsKeys' -import { useSettingsStore } from './store/useSettingsStore' import { useAppStore } from './store/useAppStore' +import { useSettingsStore } from './store/useSettingsStore' import './Settings.css' export default function Settings({ onClose }: { onClose?: () => void }) { @@ -35,11 +35,14 @@ export default function Settings({ onClose }: { onClose?: () => void }) { ) // Shortcuts - const [globalShortcutNewNote, setGlobalShortcutNewNote] = useState( - localStorage.getItem(SETTINGS_KEYS.SHORTCUT_NEWNOTE) || 'CommandOrControl+Shift+N' + const isHyprland = useAppStore((state) => state.isHyprland) + const defaultMod = isHyprland ? 'Alt' : 'CommandOrControl' + + const [shortcutNewNote, setShortcutNewNote] = useState( + localStorage.getItem(SETTINGS_KEYS.SHORTCUT_NEWNOTE) || `${defaultMod}+Shift+N` ) - const [globalShortcutToggle, setGlobalShortcutToggle] = useState( - localStorage.getItem(SETTINGS_KEYS.SHORTCUT_TOGGLE) || 'CommandOrControl+Shift+C' + const [shortcutToggle, setShortcutToggle] = useState( + localStorage.getItem(SETTINGS_KEYS.SHORTCUT_TOGGLE) || `${defaultMod}+Shift+C` ) // Startup @@ -100,17 +103,16 @@ export default function Settings({ onClose }: { onClose?: () => void }) { // Shortcuts const oldShortcut = - localStorage.getItem('papercache-shortcut-newnote') || 'CommandOrControl+Shift+N' - localStorage.setItem('papercache-shortcut-newnote', globalShortcutNewNote) + localStorage.getItem('papercache-shortcut-newnote') || `${defaultMod}+Shift+N` if (window.electronAPI.updateGlobalShortcut) { - window.electronAPI.updateGlobalShortcut('new-note', oldShortcut, globalShortcutNewNote) + window.electronAPI.updateGlobalShortcut('new-note', oldShortcut, shortcutNewNote) } + localStorage.setItem('papercache-shortcut-newnote', shortcutNewNote) const oldToggleShortcut = - localStorage.getItem('papercache-shortcut-toggle') || 'CommandOrControl+Shift+C' - localStorage.setItem('papercache-shortcut-toggle', globalShortcutToggle) + localStorage.getItem('papercache-shortcut-toggle') || `${defaultMod}+Shift+C` if (window.electronAPI.updateGlobalShortcut) { - window.electronAPI.updateGlobalShortcut('toggle', oldToggleShortcut, globalShortcutToggle) + window.electronAPI.updateGlobalShortcut('toggle', oldToggleShortcut, shortcutToggle) } // Dispatch storage event manually for the same window to pick it up immediately @@ -203,11 +205,11 @@ export default function Settings({ onClose }: { onClose?: () => void }) {

Global Shortcuts

- +
- +
diff --git a/src/api.ts b/src/api.ts index 3bc1aa3..fe3f717 100644 --- a/src/api.ts +++ b/src/api.ts @@ -28,6 +28,7 @@ export const tauriApi: ElectronAPI = { setApiKey: (key) => invoke('set_api_key', { key }), getApiKeyStatus: () => invoke('get_api_key_status'), checkForUpdates: () => invoke('check_for_updates'), + isHyprland: () => invoke('is_hyprland'), onSwipeGesture: () => { return () => {} }, diff --git a/src/components/NoteSearch.tsx b/src/components/NoteSearch.tsx index 1a3f0d1..6017390 100644 --- a/src/components/NoteSearch.tsx +++ b/src/components/NoteSearch.tsx @@ -17,6 +17,7 @@ export function NoteSearch() { const setShowNoteActionMenu = useAppStore((state) => state.setShowNoteActionMenu) const actionMenuIndex = useAppStore((state) => state.actionMenuIndex) const setActionMenuIndex = useAppStore((state) => state.setActionMenuIndex) + const isHyprland = useAppStore((state) => state.isHyprland) if (!showNoteSearch) return null @@ -102,7 +103,7 @@ export function NoteSearch() { } else if (e.key === 'Escape') { e.preventDefault() setShowNoteActionMenu(false) - } else if (e.key === 'k' && (e.metaKey || e.ctrlKey)) { + } else if (e.key === 'k' && (isHyprland ? e.altKey : e.metaKey || e.ctrlKey)) { e.preventDefault() setShowNoteActionMenu(false) } @@ -124,7 +125,7 @@ export function NoteSearch() { if (idx !== -1) setCurrentNoteIndex(idx) setShowNoteSearch(false) } - } else if (e.key === 'k' && (e.metaKey || e.ctrlKey)) { + } else if (e.key === 'k' && (isHyprland ? e.altKey : e.metaKey || e.ctrlKey)) { e.preventDefault() if (filteredNotes.length > 0) { setShowNoteActionMenu(true) diff --git a/src/hooks/useGlobalHotkey.ts b/src/hooks/useGlobalHotkey.ts index 1def1f0..10ee193 100644 --- a/src/hooks/useGlobalHotkey.ts +++ b/src/hooks/useGlobalHotkey.ts @@ -11,10 +11,12 @@ export function useGlobalHotkey() { const setCurrentNoteIndex = useAppStore((state) => state.setCurrentNoteIndex) const setNoteSearchQuery = useAppStore((state) => state.setNoteSearchQuery) const setSearchSelectedIndex = useAppStore((state) => state.setSearchSelectedIndex) + const isHyprland = useAppStore((state) => state.isHyprland) useEffect(() => { const handleGlobalKeyDown = async (e: KeyboardEvent) => { const state = useAppStore.getState() + const isMod = isHyprland ? e.altKey : e.metaKey || e.ctrlKey if (e.key === 'Escape') { const isRenaming = useAppStore.getState().isRenaming @@ -48,19 +50,19 @@ export function useGlobalHotkey() { } // Settings Shortcut - if (e.key.toLowerCase() === 's' && e.shiftKey && (e.metaKey || e.ctrlKey)) { + if (e.key.toLowerCase() === 's' && e.shiftKey && isMod) { e.preventDefault() useAppStore.getState().setShowSettingsModal(true) } // Graph View Shortcut - if (e.key.toLowerCase() === 'g' && (e.metaKey || e.ctrlKey)) { + if (e.key.toLowerCase() === 'g' && isMod) { e.preventDefault() e.stopPropagation() setShowGraphView((prev) => !prev) } - if (e.key === 'n' && (e.metaKey || e.ctrlKey)) { + if (e.key === 'n' && isMod) { e.preventDefault() const id = Date.now() + '.md' const newNote = { id, content: '', mtime: Date.now() } @@ -69,7 +71,7 @@ export function useGlobalHotkey() { window.electronAPI.saveNote(id, '') } - if (e.key === 'e' && (e.metaKey || e.ctrlKey)) { + if (e.key === 'e' && isMod) { e.preventDefault() e.stopPropagation() const { notes, currentNoteIndex } = useAppStore.getState() @@ -80,7 +82,7 @@ export function useGlobalHotkey() { } } - if (e.key.toLowerCase() === 'p' && (e.metaKey || e.ctrlKey)) { + if (e.key.toLowerCase() === 'p' && isMod) { e.preventDefault() e.stopPropagation() setShowNoteSearch(true) @@ -88,13 +90,13 @@ export function useGlobalHotkey() { setSearchSelectedIndex(0) } - if (e.key.toLowerCase() === 't' && (e.metaKey || e.ctrlKey)) { + if (e.key.toLowerCase() === 't' && isMod) { e.preventDefault() e.stopPropagation() setShowRemindersView(true) } - if (e.key.toLowerCase() === 'k' && (e.metaKey || e.ctrlKey)) { + if (e.key.toLowerCase() === 'k' && isMod) { e.preventDefault() e.stopPropagation() setShowMainActionMenu((prev) => !prev) @@ -102,13 +104,13 @@ export function useGlobalHotkey() { } // Sync global shortcut on load - const shortcut = - localStorage.getItem('papercache-shortcut-newnote') || 'CommandOrControl+Shift+N' + const defaultMod = isHyprland ? 'Alt' : 'CommandOrControl' + const shortcut = localStorage.getItem('papercache-shortcut-newnote') || `${defaultMod}+Shift+N` if (window.electronAPI.updateGlobalShortcut) { window.electronAPI.updateGlobalShortcut('new-note', '', shortcut) } const toggleShortcut = - localStorage.getItem('papercache-shortcut-toggle') || 'CommandOrControl+Shift+C' + localStorage.getItem('papercache-shortcut-toggle') || `${defaultMod}+Shift+C` if (window.electronAPI.updateGlobalShortcut) { window.electronAPI.updateGlobalShortcut('toggle', '', toggleShortcut) } @@ -147,5 +149,6 @@ export function useGlobalHotkey() { setShowRemindersView, setNotes, setCurrentNoteIndex, + isHyprland, ]) } diff --git a/src/lib/editor/extensions.ts b/src/lib/editor/extensions.ts index 11b7075..574d08b 100644 --- a/src/lib/editor/extensions.ts +++ b/src/lib/editor/extensions.ts @@ -14,6 +14,7 @@ import { useAppStore, type Note } from '../../store/useAppStore' export function useEditorExtensions() { const { apiBaseUrl, apiModel, aiSystemPrompt } = useAIStore() + const isHyprland = useAppStore((state) => state.isHyprland) const handleDeleteNote = () => { const state = useAppStore.getState() @@ -50,7 +51,7 @@ export function useEditorExtensions() { { key: 'Tab', preventDefault: true, run: insertTab }, { key: 'Shift-Tab', preventDefault: true, run: indentLess }, { - key: 'Mod-h', + key: isHyprland ? 'Alt-h' : 'Mod-h', run: (view) => { const selection = view.state.selection.main if (!selection.empty) { @@ -69,7 +70,7 @@ export function useEditorExtensions() { }, }, { - key: 'Mod-e', + key: isHyprland ? 'Alt-e' : 'Mod-e', run: () => { const note = useAppStore.getState().notes[useAppStore.getState().currentNoteIndex] if (note) { @@ -80,11 +81,11 @@ export function useEditorExtensions() { }, }, { - key: 'Mod-Backspace', + key: isHyprland ? 'Alt-Backspace' : 'Mod-Backspace', run: () => handleDeleteNote(), }, { - key: 'Mod-Delete', + key: isHyprland ? 'Alt-Delete' : 'Mod-Delete', run: () => handleDeleteNote(), }, { @@ -212,7 +213,10 @@ export function useEditorExtensions() { const webLink = target?.closest('.cm-custom-clickable-link') const fileLink = target?.closest('.cm-custom-file-link') - if ((webLink || fileLink) && (event.metaKey || event.ctrlKey)) { + if ( + (webLink || fileLink) && + (isHyprland ? event.altKey : event.metaKey || event.ctrlKey) + ) { event.preventDefault() if (webLink) { const url = webLink.getAttribute('data-url') @@ -235,6 +239,6 @@ export function useEditorExtensions() { }, }), ], - [apiBaseUrl, apiModel, aiSystemPrompt] + [apiBaseUrl, apiModel, aiSystemPrompt, isHyprland] ) } diff --git a/src/setupTests.ts b/src/setupTests.ts index 773fefa..812a0ee 100644 --- a/src/setupTests.ts +++ b/src/setupTests.ts @@ -19,6 +19,29 @@ if (typeof window !== 'undefined') { }) } +// Mock localStorage +const localStorageMock = (function () { + let store: Record = {} + return { + getItem: vi.fn((key: string) => store[key] || null), + setItem: vi.fn((key: string, value: string) => { + store[key] = value.toString() + }), + removeItem: vi.fn((key: string) => { + delete store[key] + }), + clear: vi.fn(() => { + store = {} + }), + } +})() + +if (typeof window !== 'undefined') { + Object.defineProperty(window, 'localStorage', { + value: localStorageMock, + }) +} + // Mock electronAPI for testing environments if (typeof window !== 'undefined') { window.electronAPI = { @@ -31,6 +54,7 @@ if (typeof window !== 'undefined') { setApiKey: vi.fn().mockResolvedValue(true), getApiKeyStatus: vi.fn().mockResolvedValue(true), checkForUpdates: vi.fn(), + isHyprland: vi.fn().mockResolvedValue(false), readNote: vi.fn().mockResolvedValue(''), exportNote: vi.fn().mockResolvedValue(true), setDialogOpen: vi.fn().mockResolvedValue(undefined), diff --git a/src/store/useAppStore.ts b/src/store/useAppStore.ts index 9e05f85..a93172e 100644 --- a/src/store/useAppStore.ts +++ b/src/store/useAppStore.ts @@ -10,6 +10,7 @@ interface AppState { notes: Note[] currentNoteIndex: number themePreset: string + isHyprland: boolean // UI state showGraphView: boolean @@ -28,6 +29,7 @@ interface AppState { setNotes: (notes: Note[] | ((prev: Note[]) => Note[])) => void setCurrentNoteIndex: (index: number) => void setThemePreset: (preset: string) => void + setIsHyprland: (isHyprland: boolean) => void setShowGraphView: (show: boolean | ((prev: boolean) => boolean)) => void setShowRemindersView: (show: boolean | ((prev: boolean) => boolean)) => void @@ -47,6 +49,7 @@ export const useAppStore = create((set) => ({ notes: [], currentNoteIndex: 0, themePreset: (localStorage.getItem('papercache-theme') as string) || 'grid-light', + isHyprland: false, showGraphView: false, showRemindersView: false, @@ -67,6 +70,7 @@ export const useAppStore = create((set) => ({ })), setCurrentNoteIndex: (currentNoteIndex) => set({ currentNoteIndex }), setThemePreset: (themePreset) => set({ themePreset }), + setIsHyprland: (isHyprland) => set({ isHyprland }), setShowGraphView: (showGraphView) => set((state) => ({ diff --git a/src/types.d.ts b/src/types.d.ts index 320b525..8871de8 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -12,6 +12,7 @@ export interface ElectronAPI { setApiKey: (key: string) => Promise getApiKeyStatus: () => Promise checkForUpdates: () => Promise + isHyprland: () => Promise readNote: (id: string) => Promise exportNote: (filename: string, content: string) => Promise setDialogOpen: (open: boolean) => Promise