/** * The keybinding switchboard, as this plugin reads it. * * A structural mirror rather than an import, for the reason `omdsh-codemode` * mirrors the mode registry: cordis binds services by name at runtime, so * depending on `@omdsh-plugins/omdsh-shortcuts` for its `.d.ts` would buy nothing * but a version to keep in step — and a cross-plugin value import is a * client-bundle purity error besides. The command ids below are string * literals for the same reason: an id is a wire name shared with a document, not * a symbol shared with a package. * * ## Why the panels register instead of binding a key * * This plugin knows what its panels do; it does not know, and should not * decide, which chord a person wants for them. Registering a command says the * first without claiming the second, so the key lives in one settings form * beside every other key rather than hard-coded in whichever plugin owns the * behaviour. A composition with no `shortcut` service simply never runs the * fiber this face is resolved in, and the panels keep working from their * switches — which is why the registration hangs off `ctx.inject` rather than * this package's own `inject` list. * @module @omdsh-plugins/omdsh-sidepanel/src/client/shortcut */ /** Service name the switchboard is published under in the browser. */ export const SHORTCUT_SERVICE = 'shortcut' /** Command id the file panel answers to. */ export const FILE_PANEL_COMMAND = 'panel.files' /** Command id the terminal panel answers to. */ export const TERMINAL_COMMAND = 'panel.terminal' /** As much of the browser-side switchboard as a registrant uses. */ export interface IShortcutClient { /** * Perform one `browser` command in this page. * * Registering claims no key: it says what this plugin can do, and whether a * chord reaches it — and which — is the shortcut document's business. A * command the document never declares registers fine and never fires. * @param command - the item id. * @param handler - what the press runs. * @returns the deregistration. * @throws when something in this page already answers to this command. */ register: (command: string, handler: () => void) => () => void /** * How one command's chord is spelled for a reader here, or undefined when no * chord reaches it on this surface. * @param command - the item id. * @returns the chord as the platform writes it. */ chordLabel: (command: string) => string | undefined /** * Watch for the document changing. * @param listener - called after each revision. * @returns unsubscribe. */ onBindings: (listener: () => void) => () => void } /** * The chords the two switches teach, as the toggles render them. * * Held as its own fact rather than read on every render because a chord is * PUSHED: the document arrives over a stream after this plugin mounts, and a * rebinding in the settings panel republishes it with no reload. A component * that called `chordLabel` during render would show the empty first read * forever. */ export interface PanelChords { /** The chord reaching the file panel, when one does. */ files?: string /** The chord reaching the terminal, when one does. */ terminal?: string } /** * Read both chords out of the switchboard. * @param shortcut - the switchboard. * @returns what the tooltips should teach right now. */ export function readPanelChords(shortcut: IShortcutClient): PanelChords { const files = shortcut.chordLabel(FILE_PANEL_COMMAND) const terminal = shortcut.chordLabel(TERMINAL_COMMAND) return { ...files === undefined ? {} : { files }, ...terminal === undefined ? {} : { terminal }, } } /** * Whether two readings differ in anything a tooltip shows. * @param a - the previous reading. * @param b - the next reading. * @returns true when a tooltip would read differently. */ export function chordsEqual(a: PanelChords, b: PanelChords): boolean { return a.files === b.files && a.terminal === b.terminal } /** * A label with its chord after it, in the separator this deployment already * uses between a name and its key. * @param label - the localized label. * @param chord - the chord, when one reaches this command here. * @returns what the tooltip shows. */ export function withChord(label: string, chord: string | undefined): string { return chord === undefined ? label : `${label} · ${chord}` }