fix: prevent Ctrl+R page reload and show platform-aware shortcuts (#58)
Intercept Ctrl+R/Cmd+R in before-input-event to prevent Electron's default page reload. Replace hardcoded macOS shortcut symbols with platform-aware helpers that show Ctrl+ on Windows/Linux.
This commit is contained in:
parent
ffa94f5e0f
commit
258dc6d82f
7 changed files with 45 additions and 10 deletions
|
|
@ -481,7 +481,16 @@ function createWindow(): void {
|
||||||
const ZOOM_OUT_KEYS = new Set(['-', '_']);
|
const ZOOM_OUT_KEYS = new Set(['-', '_']);
|
||||||
mainWindow.webContents.on('before-input-event', (event, input) => {
|
mainWindow.webContents.on('before-input-event', (event, input) => {
|
||||||
if (!mainWindow || mainWindow.isDestroyed()) return;
|
if (!mainWindow || mainWindow.isDestroyed()) return;
|
||||||
if (!input.meta || input.type !== 'keyDown') return;
|
if (input.type !== 'keyDown') return;
|
||||||
|
|
||||||
|
// Prevent Electron's default Ctrl+R / Cmd+R page reload so the renderer
|
||||||
|
// keyboard handler can use it as "Refresh Session" (fixes #58).
|
||||||
|
if ((input.control || input.meta) && input.key.toLowerCase() === 'r') {
|
||||||
|
event.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!input.meta) return;
|
||||||
|
|
||||||
const currentLevel = mainWindow.webContents.getZoomLevel();
|
const currentLevel = mainWindow.webContents.getZoomLevel();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import React, { useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { api } from '@renderer/api';
|
import { api } from '@renderer/api';
|
||||||
import { useStore } from '@renderer/store';
|
import { useStore } from '@renderer/store';
|
||||||
|
import { formatShortcut } from '@renderer/utils/stringUtils';
|
||||||
import { createLogger } from '@shared/utils/logger';
|
import { createLogger } from '@shared/utils/logger';
|
||||||
import { useShallow } from 'zustand/react/shallow';
|
import { useShallow } from 'zustand/react/shallow';
|
||||||
|
|
||||||
|
|
@ -75,7 +76,7 @@ const CommandSearch = ({ value, onChange }: Readonly<CommandSearchProps>): React
|
||||||
<button
|
<button
|
||||||
onClick={() => openCommandPalette()}
|
onClick={() => openCommandPalette()}
|
||||||
className="flex shrink-0 items-center gap-1 transition-opacity hover:opacity-80"
|
className="flex shrink-0 items-center gap-1 transition-opacity hover:opacity-80"
|
||||||
title={selectedProjectId ? 'Search in sessions (⌘K)' : 'Search projects (⌘K)'}
|
title={selectedProjectId ? `Search in sessions (${formatShortcut('K')})` : `Search projects (${formatShortcut('K')})`}
|
||||||
>
|
>
|
||||||
<kbd className="flex h-5 items-center justify-center rounded border border-border bg-surface-overlay px-1.5 text-[10px] font-medium text-text-muted">
|
<kbd className="flex h-5 items-center justify-center rounded border border-border bg-surface-overlay px-1.5 text-[10px] font-medium text-text-muted">
|
||||||
<Command className="size-2.5" />
|
<Command className="size-2.5" />
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ import { useEffect, useRef, useState } from 'react';
|
||||||
import { isElectronMode } from '@renderer/api';
|
import { isElectronMode } from '@renderer/api';
|
||||||
import { HEADER_ROW1_HEIGHT, HEADER_ROW2_HEIGHT } from '@renderer/constants/layout';
|
import { HEADER_ROW1_HEIGHT, HEADER_ROW2_HEIGHT } from '@renderer/constants/layout';
|
||||||
import { useStore } from '@renderer/store';
|
import { useStore } from '@renderer/store';
|
||||||
import { truncateMiddle } from '@renderer/utils/stringUtils';
|
import { formatShortcut, truncateMiddle } from '@renderer/utils/stringUtils';
|
||||||
import { Check, ChevronDown, GitBranch, PanelLeft } from 'lucide-react';
|
import { Check, ChevronDown, GitBranch, PanelLeft } from 'lucide-react';
|
||||||
import { useShallow } from 'zustand/react/shallow';
|
import { useShallow } from 'zustand/react/shallow';
|
||||||
|
|
||||||
|
|
@ -369,7 +369,7 @@ export const SidebarHeader = (): React.JSX.Element => {
|
||||||
backgroundColor: isCollapseHovered ? 'var(--color-surface-raised)' : 'transparent',
|
backgroundColor: isCollapseHovered ? 'var(--color-surface-raised)' : 'transparent',
|
||||||
} as React.CSSProperties
|
} as React.CSSProperties
|
||||||
}
|
}
|
||||||
title="Collapse sidebar (⌘B)"
|
title={`Collapse sidebar (${formatShortcut('B')})`}
|
||||||
>
|
>
|
||||||
<PanelLeft className="size-4" />
|
<PanelLeft className="size-4" />
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import { horizontalListSortingStrategy, SortableContext } from '@dnd-kit/sortabl
|
||||||
import { isElectronMode } from '@renderer/api';
|
import { isElectronMode } from '@renderer/api';
|
||||||
import { HEADER_ROW1_HEIGHT } from '@renderer/constants/layout';
|
import { HEADER_ROW1_HEIGHT } from '@renderer/constants/layout';
|
||||||
import { useStore } from '@renderer/store';
|
import { useStore } from '@renderer/store';
|
||||||
|
import { formatShortcut } from '@renderer/utils/stringUtils';
|
||||||
import { Bell, PanelLeft, Plus, RefreshCw, Search, Settings } from 'lucide-react';
|
import { Bell, PanelLeft, Plus, RefreshCw, Search, Settings } from 'lucide-react';
|
||||||
import { useShallow } from 'zustand/react/shallow';
|
import { useShallow } from 'zustand/react/shallow';
|
||||||
|
|
||||||
|
|
@ -330,7 +331,7 @@ export const TabBar = ({ paneId }: TabBarProps): React.JSX.Element => {
|
||||||
onMouseEnter={() => setRefreshHover(true)}
|
onMouseEnter={() => setRefreshHover(true)}
|
||||||
onMouseLeave={() => setRefreshHover(false)}
|
onMouseLeave={() => setRefreshHover(false)}
|
||||||
onClick={handleRefresh}
|
onClick={handleRefresh}
|
||||||
title="Refresh Session (Cmd+R)"
|
title={`Refresh Session (${formatShortcut('R')})`}
|
||||||
>
|
>
|
||||||
<RefreshCw className="size-4" />
|
<RefreshCw className="size-4" />
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -367,7 +368,7 @@ export const TabBar = ({ paneId }: TabBarProps): React.JSX.Element => {
|
||||||
color: searchHover ? 'var(--color-text)' : 'var(--color-text-muted)',
|
color: searchHover ? 'var(--color-text)' : 'var(--color-text-muted)',
|
||||||
backgroundColor: searchHover ? 'var(--color-surface-raised)' : 'transparent',
|
backgroundColor: searchHover ? 'var(--color-surface-raised)' : 'transparent',
|
||||||
}}
|
}}
|
||||||
title="Search (Cmd+K)"
|
title={`Search (${formatShortcut('K')})`}
|
||||||
>
|
>
|
||||||
<Search className="size-4" />
|
<Search className="size-4" />
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@
|
||||||
|
|
||||||
import { useEffect, useRef } from 'react';
|
import { useEffect, useRef } from 'react';
|
||||||
|
|
||||||
|
import { formatShortcut } from '@renderer/utils/stringUtils';
|
||||||
|
|
||||||
interface TabContextMenuProps {
|
interface TabContextMenuProps {
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
|
|
@ -100,13 +102,13 @@ export const TabContextMenu = ({
|
||||||
onClick={handleClick(onCloseSelectedTabs)}
|
onClick={handleClick(onCloseSelectedTabs)}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<MenuItem label="Close Tab" shortcut="⌘W" onClick={handleClick(onCloseTab)} />
|
<MenuItem label="Close Tab" shortcut={formatShortcut('W')} onClick={handleClick(onCloseTab)} />
|
||||||
)}
|
)}
|
||||||
<MenuItem label="Close Other Tabs" onClick={handleClick(onCloseOtherTabs)} />
|
<MenuItem label="Close Other Tabs" onClick={handleClick(onCloseOtherTabs)} />
|
||||||
<div className="mx-2 my-1 border-t" style={{ borderColor: 'var(--color-border)' }} />
|
<div className="mx-2 my-1 border-t" style={{ borderColor: 'var(--color-border)' }} />
|
||||||
<MenuItem
|
<MenuItem
|
||||||
label="Split Right"
|
label="Split Right"
|
||||||
shortcut="⌘\"
|
shortcut={formatShortcut('\\')}
|
||||||
onClick={handleClick(onSplitRight)}
|
onClick={handleClick(onSplitRight)}
|
||||||
disabled={disableSplit}
|
disabled={disableSplit}
|
||||||
/>
|
/>
|
||||||
|
|
@ -127,7 +129,7 @@ export const TabContextMenu = ({
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<div className="mx-2 my-1 border-t" style={{ borderColor: 'var(--color-border)' }} />
|
<div className="mx-2 my-1 border-t" style={{ borderColor: 'var(--color-border)' }} />
|
||||||
<MenuItem label="Close All Tabs" shortcut="⇧⌘W" onClick={handleClick(onCloseAllTabs)} />
|
<MenuItem label="Close All Tabs" shortcut={formatShortcut('W', { shift: true })} onClick={handleClick(onCloseAllTabs)} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
import { MAX_PANES } from '@renderer/types/panes';
|
import { MAX_PANES } from '@renderer/types/panes';
|
||||||
|
import { formatShortcut } from '@renderer/utils/stringUtils';
|
||||||
import { Check, ClipboardCopy, Eye, EyeOff, Pin, PinOff, Terminal } from 'lucide-react';
|
import { Check, ClipboardCopy, Eye, EyeOff, Pin, PinOff, Terminal } from 'lucide-react';
|
||||||
|
|
||||||
interface SessionContextMenuProps {
|
interface SessionContextMenuProps {
|
||||||
|
|
@ -98,7 +99,7 @@ export const SessionContextMenu = ({
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<MenuItem label="Open in Current Pane" onClick={handleClick(onOpenInCurrentPane)} />
|
<MenuItem label="Open in Current Pane" onClick={handleClick(onOpenInCurrentPane)} />
|
||||||
<MenuItem label="Open in New Tab" shortcut="⌘ Click" onClick={handleClick(onOpenInNewTab)} />
|
<MenuItem label="Open in New Tab" shortcut={`${formatShortcut('')}Click`} onClick={handleClick(onOpenInNewTab)} />
|
||||||
<div className="mx-2 my-1 border-t" style={{ borderColor: 'var(--color-border)' }} />
|
<div className="mx-2 my-1 border-t" style={{ borderColor: 'var(--color-border)' }} />
|
||||||
<MenuItem
|
<MenuItem
|
||||||
label="Split Right and Open"
|
label="Split Right and Open"
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,27 @@
|
||||||
* String utilities for display formatting.
|
* String utilities for display formatting.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
const isMacPlatform =
|
||||||
|
typeof window !== 'undefined' && window.navigator.userAgent.includes('Macintosh');
|
||||||
|
|
||||||
|
/** Returns '⌘' on macOS, 'Ctrl' on Windows/Linux. */
|
||||||
|
export const modKey = isMacPlatform ? '⌘' : 'Ctrl+';
|
||||||
|
|
||||||
|
/** Returns '⇧' on macOS, 'Shift+' on Windows/Linux. */
|
||||||
|
export const shiftKey = isMacPlatform ? '⇧' : 'Shift+';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats a keyboard shortcut for the current platform.
|
||||||
|
* @example formatShortcut('R') → '⌘R' on Mac, 'Ctrl+R' on Windows/Linux
|
||||||
|
* @example formatShortcut('W', { shift: true }) → '⇧⌘W' on Mac, 'Ctrl+Shift+W' on Windows/Linux
|
||||||
|
*/
|
||||||
|
export function formatShortcut(key: string, opts?: { shift?: boolean }): string {
|
||||||
|
if (opts?.shift) {
|
||||||
|
return isMacPlatform ? `${shiftKey}${modKey}${key}` : `${modKey}${shiftKey}${key}`;
|
||||||
|
}
|
||||||
|
return `${modKey}${key}`;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Truncates a string in the middle to preserve both the beginning and end.
|
* Truncates a string in the middle to preserve both the beginning and end.
|
||||||
* Useful for branch names where the unique identifier is often at the end.
|
* Useful for branch names where the unique identifier is often at the end.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue