Skip to content

Commit 4d1eaa7

Browse files
committed
fix: address audit findings (ESC logic, IPC types, update restart, mutex panics, pause button, ESLint)
1 parent d618743 commit 4d1eaa7

10 files changed

Lines changed: 72 additions & 33 deletions

File tree

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@
4040
"@types/d3-force": "^3.0.10",
4141
"d3-force": "^3.0.0",
4242
"expr-eval": "^2.0.2",
43+
"react": "^19.2.6",
44+
"react-dom": "^19.2.6",
4345
"react-force-graph-3d": "^1.29.1",
4446
"three": "^0.184.0"
4547
},
@@ -73,8 +75,6 @@
7375
"jsdom": "^29.1.1",
7476
"lint-staged": "^17.0.7",
7577
"prettier": "^3.8.3",
76-
"react": "^19.2.6",
77-
"react-dom": "^19.2.6",
7878
"typescript": "~6.0.2",
7979
"typescript-eslint": "^8.59.2",
8080
"vite": "^8.0.12",

src-tauri/src/commands/shortcuts.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ pub fn update_global_shortcut(
5959

6060
// Update state
6161
let state = app.state::<GlobalShortcutState>();
62-
let mut map = state.shortcuts.lock().unwrap();
62+
let mut map = state.shortcuts.lock().map_err(|e| e.to_string())?;
6363
map.insert(action, new_shortcut);
6464

6565
Ok(())
@@ -75,7 +75,7 @@ pub fn pause_shortcuts(app: AppHandle) -> Result<(), String> {
7575
#[tauri::command]
7676
pub fn resume_shortcuts(app: AppHandle) -> Result<(), String> {
7777
let state = app.state::<GlobalShortcutState>();
78-
let map = state.shortcuts.lock().unwrap();
78+
let map = state.shortcuts.lock().map_err(|e| e.to_string())?;
7979

8080
for (action, shortcut_str) in map.iter() {
8181
if let Ok(shortcut) = shortcut_str.parse::<Shortcut>() {

src-tauri/src/commands/system.rs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -114,11 +114,16 @@ pub async fn check_for_updates(app: tauri::AppHandle) -> Result<(), String> {
114114
use tauri_plugin_updater::UpdaterExt;
115115
let updater = app.updater().map_err(|e| e.to_string())?;
116116

117-
// We handle the update automatically if one is available
118117
if let Some(update) = updater.check().await.map_err(|e| e.to_string())? {
119-
// Here we could emit an event to the frontend or just download and install it
120-
let _ = update.download_and_install(|_, _| {}, || {}).await;
121-
app.restart();
118+
// Run the download + install + restart in the background so the command
119+
// returns immediately. The "update-ready" event gives the frontend 3 seconds
120+
// to show a toast before the process restarts.
121+
tokio::spawn(async move {
122+
let _ = update.download_and_install(|_, _| {}, || {}).await;
123+
let _ = app.emit("update-ready", ());
124+
tokio::time::sleep(tokio::time::Duration::from_secs(3)).await;
125+
app.restart();
126+
});
122127
}
123128
Ok(())
124129
}

src-tauri/src/macos.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,11 @@ pub fn setup_power_monitor(app_handle: AppHandle) {
6565

6666
let delegate: id = msg_send![delegate_class, new];
6767

68+
// SAFETY: We intentionally leak the AppHandle here. The NSNotificationCenter
69+
// observer (delegate) is registered for the lifetime of the process and must
70+
// always have a valid pointer to the handle. Freeing the box would invalidate
71+
// the pointer stored in the Objective-C ivar, causing a use-after-free.
72+
// This is a deliberate, bounded leak (one pointer per process lifetime).
6873
let app_box = Box::new(app_handle);
6974
let ptr = Box::into_raw(app_box) as *mut c_void;
7075
(*delegate).set_ivar("app_handle", ptr);

src/App.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,17 @@ function App() {
5757
window.electronAPI.isHyprland().then((isHyp) => {
5858
useAppStore.getState().setIsHyprland(isHyp)
5959
})
60+
61+
// Show a toast before the app auto-restarts for an update
62+
const disposeUpdateReady = window.electronAPI.onUpdateReady(() => {
63+
useAppStore.getState().addToast({
64+
message: '✨ PaperCache updated — restarting in 3 seconds…',
65+
type: 'info',
66+
})
67+
})
68+
return () => {
69+
disposeUpdateReady()
70+
}
6071
}, [])
6172

6273
// Auto-dismiss toasts after 5 seconds

src/GraphView.tsx

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,17 @@ function buildFolderCentroids(folderNames: string[]): Map<string, { cx: number;
4646
return centroids
4747
}
4848

49+
// Minimal typing for the react-force-graph-3d instance (library ships no declarations)
50+
interface ForceGraphInstance {
51+
controls: () => Record<string, unknown> | null
52+
cameraPosition: (pos: { x: number; y: number; z: number }) => void
53+
zoomToFit: (duration: number, padding: number) => void
54+
graphData: () => { nodes: GraphNode[]; links: GraphLink[] } | null
55+
d3Force: (name: string, force?: unknown) => unknown
56+
scene: () => THREE.Scene
57+
nodeThreeObject: unknown
58+
}
59+
4960
export default function GraphView({
5061
notes,
5162
onClose,
@@ -54,13 +65,13 @@ export default function GraphView({
5465
bgColor,
5566
accentColor,
5667
}: GraphViewProps) {
57-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
58-
const fgRef = useRef<any>(null)
68+
const fgRef = useRef<ForceGraphInstance | null>(null)
5969

6070
const draggedNodesRef = useRef<Set<string>>(new Set())
6171

6272
useEffect(() => {
6373
let raf: number
74+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
6475
let ctrls: any = null
6576
const setup = () => {
6677
const fg = fgRef.current
@@ -89,8 +100,10 @@ export default function GraphView({
89100
}, [])
90101

91102
useEffect(() => {
103+
// Snapshot ref at effect-run time so the cleanup reads a stable value
104+
// (avoids the react-hooks/exhaustive-deps stale-ref warning)
105+
const fg = fgRef.current
92106
return () => {
93-
const fg = fgRef.current
94107
if (!fg) return
95108
const data = fg.graphData()
96109
if (!data || !data.nodes) return

src/api.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,7 @@ export const tauriApi: ElectronAPI = {
3939
return () => {}
4040
},
4141
getLaunchAtStartup: () => invoke('get_launch_at_startup'),
42-
setLaunchAtStartup: (value) => {
43-
invoke('set_launch_at_startup', { enabled: value })
44-
},
42+
setLaunchAtStartup: (value) => invoke('set_launch_at_startup', { enabled: value }),
4543
updateGlobalShortcut: (action, oldShortcut, newShortcut) =>
4644
invoke('update_global_shortcut', { action, oldShortcut, newShortcut }),
4745
onTriggerNewNote: (callback) => {
@@ -84,4 +82,13 @@ export const tauriApi: ElectronAPI = {
8482
},
8583
pauseShortcuts: () => invoke('pause_shortcuts') as unknown as void,
8684
resumeShortcuts: () => invoke('resume_shortcuts') as unknown as void,
85+
onUpdateReady: (callback) => {
86+
let unlisten: (() => void) | undefined
87+
listen('update-ready', () => callback()).then((fn) => {
88+
unlisten = fn
89+
})
90+
return () => {
91+
if (unlisten) unlisten()
92+
}
93+
},
8794
}

src/components/TimersPage.tsx

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import { listen } from '@tauri-apps/api/event'
1818
interface TimerItemProps {
1919
timer: Timer
2020
onRemove: (id: string) => void
21-
onPause: (id: string) => void
2221
}
2322

2423
function formatTime(ms: number): string {
@@ -32,7 +31,7 @@ function formatTime(ms: number): string {
3231
return `${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`
3332
}
3433

35-
function TimerItem({ timer, onRemove, onPause }: TimerItemProps) {
34+
function TimerItem({ timer, onRemove }: TimerItemProps) {
3635
const tickTimer = useTimerStore((s) => s.tickTimer)
3736
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
3837

@@ -75,11 +74,6 @@ function TimerItem({ timer, onRemove, onPause }: TimerItemProps) {
7574
<div className="timer-header">
7675
<span className="timer-label">{timer.label || 'Timer'}</span>
7776
<div className="timer-controls">
78-
{timer.status === 'running' && (
79-
<button className="timer-btn" onClick={() => onPause(timer.id)} title="Pause">
80-
81-
</button>
82-
)}
8377
<button
8478
className="timer-btn timer-btn-remove"
8579
onClick={() => onRemove(timer.id)}
@@ -119,7 +113,6 @@ export function TimersPage({ onClose }: TimersPageProps) {
119113
const timers = useTimerStore((s) => s.timers)
120114
const addTimer = useTimerStore((s) => s.addTimer)
121115
const removeTimer = useTimerStore((s) => s.removeTimer)
122-
const pauseTimer = useTimerStore((s) => s.pauseTimer)
123116
const completeTimer = useTimerStore((s) => s.completeTimer)
124117
const addToast = useAppStore((s) => s.addToast)
125118

@@ -284,9 +277,7 @@ export function TimersPage({ onClose }: TimersPageProps) {
284277
</p>
285278
</div>
286279
) : (
287-
timers.map((t) => (
288-
<TimerItem key={t.id} timer={t} onRemove={handleRemove} onPause={pauseTimer} />
289-
))
280+
timers.map((t) => <TimerItem key={t.id} timer={t} onRemove={handleRemove} />)
290281
)}
291282
</div>
292283
</div>

src/hooks/useGlobalHotkey.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,7 @@ export function useGlobalHotkey() {
2525

2626
if (isRecordingShortcut) return // Do not close app while recording shortcut
2727

28-
// Close the app if nothing else was open
29-
if (!state.showNoteSearch && !isRenaming && actionMenuIndex === 0) {
30-
await getCurrentWindow().hide()
31-
}
28+
// Dismiss overlays in priority order — highest-level first
3229
if (state.showMainActionMenu) {
3330
e.preventDefault()
3431
e.stopPropagation()
@@ -47,6 +44,15 @@ export function useGlobalHotkey() {
4744
setShowGraphView(false)
4845
return
4946
}
47+
// Timers and Reminders pages have their own ESC handlers — let them fire
48+
if (state.showTimersView || state.showRemindersView) {
49+
return
50+
}
51+
52+
// Nothing was open: hide the window
53+
if (!isRenaming && actionMenuIndex === 0) {
54+
await getCurrentWindow().hide()
55+
}
5056
}
5157

5258
// Settings Shortcut

src/types.d.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,12 @@ export interface ElectronAPI {
2929
cancelTimer: (id: string) => Promise<void>
3030

3131
removeOnboardingFiles: () => Promise<void>
32-
quitApp: () => void
33-
openExternal: (url: string) => void
34-
openFile: (path: string) => void
32+
quitApp: () => Promise<void>
33+
openExternal: (url: string) => Promise<void>
34+
openFile: (path: string) => Promise<void>
3535
onSwipeGesture: (callback: (direction: string) => void) => () => void
3636
getLaunchAtStartup: () => Promise<boolean>
37-
setLaunchAtStartup: (value: boolean) => void
37+
setLaunchAtStartup: (value: boolean) => Promise<void>
3838
updateGlobalShortcut: (action: string, oldShortcut: string, newShortcut: string) => void
3939
onTriggerNewNote: (callback: () => void) => () => void
4040
onTriggerTasks: (callback: () => void) => () => void
@@ -44,6 +44,7 @@ export interface ElectronAPI {
4444
onPowerResume: (callback: () => void) => () => void
4545
pauseShortcuts: () => void
4646
resumeShortcuts: () => void
47+
onUpdateReady: (callback: () => void) => () => void
4748
}
4849

4950
declare global {

0 commit comments

Comments
 (0)