plugins: add orbit.addShortcut() for global keyboard shortcuts
combos like mod+shift+k are matched in the chat keydown handler after the built-in shortcuts. demo plugin binds mod+shift+d.
This commit is contained in:
parent
b0ca4a1b4f
commit
dafc417c61
4 changed files with 46 additions and 0 deletions
|
|
@ -41,4 +41,7 @@ Orbit.plugin('orbit-demo', (orbit, log) => {
|
|||
help: 'Roll a die: /roll [sides]',
|
||||
run: (args) => orbit.irc.say('\u{1F3B2} ' + (1 + Math.floor(Math.random() * Math.max(2, +args[0] || 6)))),
|
||||
});
|
||||
|
||||
// Register a keyboard shortcut: mod+shift+d fires a notification.
|
||||
orbit.addShortcut('mod+shift+d', () => orbit.notify('Demo plugin', 'shortcut fired'));
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { ConnectScreen } from './components/ConnectScreen';
|
|||
import { Chat } from './components/Chat';
|
||||
import { refreshPush } from './services/push';
|
||||
import { getConfig } from './config';
|
||||
import { usePluginRegistry, matchShortcut } from './plugins/registry';
|
||||
|
||||
// Shown while a site handoff connects, so visitors who already chose a pseudo
|
||||
// never see the join form. A failure clears autoConnecting and falls back to it.
|
||||
|
|
@ -72,6 +73,10 @@ export default function App() {
|
|||
if (e.key === '?' && !isTyping()) {
|
||||
e.preventDefault(); st.setModal(st.modal === 'shortcuts' ? '' : 'shortcuts'); return;
|
||||
}
|
||||
// Plugin-registered shortcuts (the built-ins above take priority).
|
||||
for (const sc of usePluginRegistry.getState().shortcuts) {
|
||||
if (matchShortcut(e, sc.combo)) { e.preventDefault(); try { sc.run(e); } catch { /* plugin threw */ } return; }
|
||||
}
|
||||
};
|
||||
window.addEventListener('keydown', onKey);
|
||||
return () => window.removeEventListener('keydown', onKey);
|
||||
|
|
|
|||
|
|
@ -105,6 +105,9 @@ export interface OrbitPluginApi {
|
|||
addCommand: (name: string, spec: { run: (args: string[], rest: string) => void; help?: string }) => () => void;
|
||||
/** Fire a desktop notification with the brand icon (asks permission once). */
|
||||
notify: (title: string, body?: string) => void;
|
||||
/** Register a global keyboard shortcut, e.g. "mod+shift+k" (mod = Cmd/Ctrl).
|
||||
* Runs in the chat view; built-in shortcuts take priority. */
|
||||
addShortcut: (combo: string, run: (e: KeyboardEvent) => void) => () => void;
|
||||
}
|
||||
|
||||
function makeApi(name: string): OrbitPluginApi {
|
||||
|
|
@ -167,6 +170,7 @@ function makeApi(name: string): OrbitPluginApi {
|
|||
addMessageFilter: (fn) => usePluginRegistry.getState().addMessageFilter(name, fn),
|
||||
addCommand: (cmd, spec) => usePluginRegistry.getState().addCommand(name, cmd, spec.run, spec.help),
|
||||
notify: (title, body) => pluginNotify(title, body),
|
||||
addShortcut: (combo, run) => usePluginRegistry.getState().addShortcut(name, combo, run),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -68,6 +68,32 @@ export interface PluginCommand {
|
|||
help?: string;
|
||||
}
|
||||
|
||||
// A plugin-registered keyboard shortcut. `combo` is "+"-joined, e.g. "mod+k" or
|
||||
// "alt+shift+r" (mod = Cmd on macOS, Ctrl elsewhere). Built-in shortcuts win.
|
||||
export interface PluginShortcut {
|
||||
id: string;
|
||||
plugin: string;
|
||||
combo: string;
|
||||
run: (e: KeyboardEvent) => void;
|
||||
}
|
||||
|
||||
const IS_MAC = typeof navigator !== 'undefined' && /mac/i.test(navigator.platform || '');
|
||||
// True when a keydown event matches a "mod+shift+k"-style combo.
|
||||
export function matchShortcut(e: KeyboardEvent, combo: string): boolean {
|
||||
const want = { ctrl: false, meta: false, alt: false, shift: false };
|
||||
let key = '';
|
||||
for (const p of combo.toLowerCase().split('+').map((s) => s.trim()).filter(Boolean)) {
|
||||
if (p === 'mod') { if (IS_MAC) want.meta = true; else want.ctrl = true; }
|
||||
else if (p === 'ctrl' || p === 'control') want.ctrl = true;
|
||||
else if (p === 'meta' || p === 'cmd' || p === 'super') want.meta = true;
|
||||
else if (p === 'alt' || p === 'option') want.alt = true;
|
||||
else if (p === 'shift') want.shift = true;
|
||||
else key = p;
|
||||
}
|
||||
return e.ctrlKey === want.ctrl && e.metaKey === want.meta && e.altKey === want.alt
|
||||
&& e.shiftKey === want.shift && (e.key || '').toLowerCase() === key;
|
||||
}
|
||||
|
||||
interface RegistryState {
|
||||
ui: PluginUi[];
|
||||
decorators: PluginPerMessage[];
|
||||
|
|
@ -75,12 +101,14 @@ interface RegistryState {
|
|||
userActions: PluginPerUser[];
|
||||
messageFilters: PluginFilter[];
|
||||
commands: PluginCommand[];
|
||||
shortcuts: PluginShortcut[];
|
||||
addUi: (slot: UiSlot, plugin: string, render: () => ReactNode, meta?: PluginUi['meta']) => () => void;
|
||||
addDecorator: (plugin: string, render: (m: MessageInfo) => ReactNode) => () => void;
|
||||
addAction: (plugin: string, render: (m: MessageInfo) => ReactNode) => () => void;
|
||||
addUserAction: (plugin: string, render: (ctx: UserActionCtx) => ReactNode) => () => void;
|
||||
addMessageFilter: (plugin: string, fn: (m: FilterableMessage) => boolean) => () => void;
|
||||
addCommand: (plugin: string, name: string, run: PluginCommand['run'], help?: string) => () => void;
|
||||
addShortcut: (plugin: string, combo: string, run: PluginShortcut['run']) => () => void;
|
||||
}
|
||||
|
||||
export const usePluginRegistry = create<RegistryState>((set) => ({
|
||||
|
|
@ -90,6 +118,7 @@ export const usePluginRegistry = create<RegistryState>((set) => ({
|
|||
userActions: [],
|
||||
messageFilters: [],
|
||||
commands: [],
|
||||
shortcuts: [],
|
||||
addUi: (slot, plugin, render, meta) => {
|
||||
const id = `${plugin}:${slot}:${Math.random().toString(36).slice(2, 8)}`;
|
||||
set((s) => ({ ui: [...s.ui, { id, plugin, slot, render, meta }] }));
|
||||
|
|
@ -121,4 +150,9 @@ export const usePluginRegistry = create<RegistryState>((set) => ({
|
|||
set((s) => ({ commands: [...s.commands, { id, plugin, name: clean, run, help }] }));
|
||||
return () => set((s) => ({ commands: s.commands.filter((c) => c.id !== id) }));
|
||||
},
|
||||
addShortcut: (plugin, combo, run) => {
|
||||
const id = `${plugin}:key:${Math.random().toString(36).slice(2, 8)}`;
|
||||
set((s) => ({ shortcuts: [...s.shortcuts, { id, plugin, combo, run }] }));
|
||||
return () => set((s) => ({ shortcuts: s.shortcuts.filter((k) => k.id !== id) }));
|
||||
},
|
||||
}));
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue