mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-01-16 14:06:49 +01:00
228 lines
7.1 KiB
TypeScript
228 lines
7.1 KiB
TypeScript
import { type } from '@tauri-apps/plugin-os';
|
|
import { debounce } from '@yaakapp-internal/lib';
|
|
import { useEffect, useRef } from 'react';
|
|
import { capitalize } from '../lib/capitalize';
|
|
import { useOsInfo } from './useOsInfo';
|
|
|
|
const HOLD_KEYS = ['Shift', 'Control', 'Command', 'Alt', 'Meta'];
|
|
|
|
export type HotkeyAction =
|
|
| 'app.zoom_in'
|
|
| 'app.zoom_out'
|
|
| 'app.zoom_reset'
|
|
| 'command_palette.toggle'
|
|
| 'environmentEditor.toggle'
|
|
| 'grpc_request.send'
|
|
| 'hotkeys.showHelp'
|
|
| 'http_request.create'
|
|
| 'http_request.delete'
|
|
| 'http_request.duplicate'
|
|
| 'http_request.send'
|
|
| 'request_switcher.next'
|
|
| 'request_switcher.prev'
|
|
| 'request_switcher.toggle'
|
|
| 'settings.show'
|
|
| 'sidebar.focus'
|
|
| 'url_bar.focus'
|
|
| 'workspace_settings.show';
|
|
|
|
const hotkeys: Record<HotkeyAction, string[]> = {
|
|
'app.zoom_in': ['CmdCtrl+='],
|
|
'app.zoom_out': ['CmdCtrl+-'],
|
|
'app.zoom_reset': ['CmdCtrl+0'],
|
|
'command_palette.toggle': ['CmdCtrl+k'],
|
|
'environmentEditor.toggle': ['CmdCtrl+Shift+E', 'CmdCtrl+Shift+e'],
|
|
'grpc_request.send': ['CmdCtrl+Enter', 'CmdCtrl+r'],
|
|
'hotkeys.showHelp': ['CmdCtrl+Shift+/', 'CmdCtrl+Shift+?'], // when shift is pressed, it might be a question mark
|
|
'http_request.create': ['CmdCtrl+n'],
|
|
'http_request.delete': ['Backspace'],
|
|
'http_request.duplicate': ['CmdCtrl+d'],
|
|
'http_request.send': ['CmdCtrl+Enter', 'CmdCtrl+r'],
|
|
'request_switcher.next': ['Control+Shift+Tab'],
|
|
'request_switcher.prev': ['Control+Tab'],
|
|
'request_switcher.toggle': ['CmdCtrl+p'],
|
|
'settings.show': ['CmdCtrl+,'],
|
|
'sidebar.focus': ['CmdCtrl+b'],
|
|
'url_bar.focus': ['CmdCtrl+l'],
|
|
'workspace_settings.show': ['CmdCtrl+;'],
|
|
};
|
|
|
|
const hotkeyLabels: Record<HotkeyAction, string> = {
|
|
'app.zoom_in': 'Zoom In',
|
|
'app.zoom_out': 'Zoom Out',
|
|
'app.zoom_reset': 'Zoom to Actual Size',
|
|
'command_palette.toggle': 'Toggle Command Palette',
|
|
'environmentEditor.toggle': 'Edit Environments',
|
|
'grpc_request.send': 'Send Message',
|
|
'hotkeys.showHelp': 'Show Keyboard Shortcuts',
|
|
'http_request.create': 'New Request',
|
|
'http_request.delete': 'Delete Request',
|
|
'http_request.duplicate': 'Duplicate Request',
|
|
'http_request.send': 'Send Request',
|
|
'request_switcher.next': 'Go To Previous Request',
|
|
'request_switcher.prev': 'Go To Next Request',
|
|
'request_switcher.toggle': 'Toggle Request Switcher',
|
|
'settings.show': 'Open Settings',
|
|
'sidebar.focus': 'Focus or Toggle Sidebar',
|
|
'url_bar.focus': 'Focus URL',
|
|
'workspace_settings.show': 'Open Workspace Settings',
|
|
};
|
|
|
|
export const hotkeyActions: HotkeyAction[] = Object.keys(hotkeys) as (keyof typeof hotkeys)[];
|
|
|
|
interface Options {
|
|
enable?: boolean;
|
|
}
|
|
|
|
export function useHotKey(
|
|
action: HotkeyAction | null,
|
|
callback: (e: KeyboardEvent) => void,
|
|
options: Options = {},
|
|
) {
|
|
const currentKeys = useRef<Set<string>>(new Set());
|
|
const callbackRef = useRef(callback);
|
|
|
|
useEffect(() => {
|
|
callbackRef.current = callback;
|
|
}, [callback]);
|
|
|
|
useEffect(() => {
|
|
// Sometimes the keyup event doesn't fire (eg, cmd+Tab), so we clear the keys after a timeout
|
|
const clearCurrentKeys = debounce(() => currentKeys.current.clear(), 5000);
|
|
|
|
const down = (e: KeyboardEvent) => {
|
|
if (options.enable === false) {
|
|
return;
|
|
}
|
|
|
|
// Don't add key if not holding modifier
|
|
const isValidKeymapKey =
|
|
e.altKey || e.ctrlKey || e.metaKey || e.shiftKey || e.key === 'Backspace';
|
|
if (!isValidKeymapKey) {
|
|
return;
|
|
}
|
|
|
|
// Don't add hold keys
|
|
if (HOLD_KEYS.includes(e.key)) {
|
|
return;
|
|
}
|
|
|
|
currentKeys.current.add(e.key);
|
|
|
|
const currentKeysWithModifiers = new Set(currentKeys.current);
|
|
if (e.altKey) currentKeysWithModifiers.add('Alt');
|
|
if (e.ctrlKey) currentKeysWithModifiers.add('Control');
|
|
if (e.metaKey) currentKeysWithModifiers.add('Meta');
|
|
if (e.shiftKey) currentKeysWithModifiers.add('Shift');
|
|
|
|
for (const [hkAction, hkKeys] of Object.entries(hotkeys) as [HotkeyAction, string[]][]) {
|
|
if (
|
|
(e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) &&
|
|
currentKeysWithModifiers.size === 1 &&
|
|
currentKeysWithModifiers.has('Backspace')
|
|
) {
|
|
// Don't support Backspace-only modifiers within input fields. This is fairly brittle, so maybe there's a
|
|
// better way to do stuff like this in the future.
|
|
continue;
|
|
}
|
|
|
|
for (const hkKey of hkKeys) {
|
|
if (hkAction !== action) {
|
|
continue;
|
|
}
|
|
|
|
const keys = hkKey.split('+').map(resolveHotkeyKey);
|
|
if (
|
|
keys.length === currentKeysWithModifiers.size &&
|
|
keys.every((key) => currentKeysWithModifiers.has(key))
|
|
) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
callbackRef.current(e);
|
|
currentKeys.current.clear();
|
|
}
|
|
}
|
|
}
|
|
|
|
clearCurrentKeys();
|
|
};
|
|
|
|
const up = (e: KeyboardEvent) => {
|
|
if (options.enable === false) {
|
|
return;
|
|
}
|
|
currentKeys.current.delete(e.key);
|
|
|
|
// Clear all keys if no longer holding modifier
|
|
// HACK: This is to get around the case of DOWN SHIFT -> DOWN : -> UP SHIFT -> UP ;
|
|
// As you see, the ":" is not removed because it turned into ";" when shift was released
|
|
const isHoldingModifier = e.altKey || e.ctrlKey || e.metaKey || e.shiftKey;
|
|
if (!isHoldingModifier) {
|
|
currentKeys.current.clear();
|
|
}
|
|
};
|
|
|
|
document.addEventListener('keyup', up, { capture: true });
|
|
document.addEventListener('keydown', down, { capture: true });
|
|
return () => {
|
|
document.removeEventListener('keydown', down, { capture: true });
|
|
document.removeEventListener('keyup', up, { capture: true });
|
|
};
|
|
}, [action, options.enable]);
|
|
}
|
|
|
|
export function useHotKeyLabel(action: HotkeyAction): string {
|
|
return hotkeyLabels[action];
|
|
}
|
|
|
|
export function useFormattedHotkey(action: HotkeyAction | null): string[] | null {
|
|
const osInfo = useOsInfo();
|
|
const trigger = action != null ? (hotkeys[action]?.[0] ?? null) : null;
|
|
if (trigger == null || osInfo == null) {
|
|
return null;
|
|
}
|
|
|
|
const os = osInfo.osType;
|
|
const parts = trigger.split('+');
|
|
const labelParts: string[] = [];
|
|
|
|
for (const p of parts) {
|
|
if (os === 'macos') {
|
|
if (p === 'CmdCtrl') {
|
|
labelParts.push('⌘');
|
|
} else if (p === 'Shift') {
|
|
labelParts.push('⇧');
|
|
} else if (p === 'Control') {
|
|
labelParts.push('⌃');
|
|
} else if (p === 'Enter') {
|
|
labelParts.push('↩');
|
|
} else if (p === 'Tab') {
|
|
labelParts.push('⇥');
|
|
} else if (p === 'Backspace') {
|
|
labelParts.push('⌫');
|
|
} else {
|
|
labelParts.push(capitalize(p));
|
|
}
|
|
} else {
|
|
if (p === 'CmdCtrl') {
|
|
labelParts.push('Ctrl');
|
|
} else {
|
|
labelParts.push(capitalize(p));
|
|
}
|
|
}
|
|
}
|
|
|
|
if (os === 'macos') {
|
|
return labelParts;
|
|
} else {
|
|
return [labelParts.join('+')];
|
|
}
|
|
}
|
|
|
|
const resolveHotkeyKey = (key: string) => {
|
|
const os = type();
|
|
if (key === 'CmdCtrl' && os === 'macos') return 'Meta';
|
|
else if (key === 'CmdCtrl') return 'Control';
|
|
else return key;
|
|
};
|