|
| 1 | +import { Contract } from "../di/contract"; |
| 2 | +import type { Injector } from "../di/injector"; |
| 3 | + |
| 4 | +/** |
| 5 | + * What every shortcut can count on. The context carries state; capabilities |
| 6 | + * come from the injector. Callers extend it with the dimensions their own |
| 7 | + * tables ask about — nothing in the engine inspects the context beyond handing |
| 8 | + * it to `when` and `action`. |
| 9 | + */ |
| 10 | +export interface KeyContextBase { |
| 11 | + injector: Injector; |
| 12 | +} |
| 13 | + |
| 14 | +/** The half of a context its caller owns; the service provides the rest. */ |
| 15 | +export type KeyContextExtras<TContext extends KeyContextBase> = Omit< |
| 16 | + TContext, |
| 17 | + keyof KeyContextBase |
| 18 | +>; |
| 19 | + |
| 20 | +export interface KeyShortcut<TContext extends KeyContextBase = KeyContextBase> { |
| 21 | + key: string; |
| 22 | + description: string; |
| 23 | + group?: string; |
| 24 | + /** Availability AND help visibility — one verdict feeds both. */ |
| 25 | + when?(ctx: TContext): boolean; |
| 26 | + action?(ctx: TContext): void | Promise<void>; |
| 27 | + /** |
| 28 | + * Suppresses the keypress banner. Set by shortcuts that hand the key to a |
| 29 | + * child process, which announces and runs it itself. |
| 30 | + */ |
| 31 | + quiet?: boolean; |
| 32 | +} |
| 33 | + |
| 34 | +export interface IKeyShortcutService { |
| 35 | + /** Returns false when the terminal cannot take raw mode. */ |
| 36 | + attach<TContext extends KeyContextBase = KeyContextBase>(options: { |
| 37 | + context?: KeyContextExtras<TContext>; |
| 38 | + shortcuts: KeyShortcut<TContext>[]; |
| 39 | + }): boolean; |
| 40 | + detach(): void; |
| 41 | + printHelp(): void; |
| 42 | + printHint(): void; |
| 43 | +} |
| 44 | + |
| 45 | +/** What `add` hands back; the only way to take a registration out again. */ |
| 46 | +export interface KeyShortcutRegistration { |
| 47 | + dispose(): void; |
| 48 | +} |
| 49 | + |
| 50 | +/** |
| 51 | + * The shortcuts the running process answers to. Registrations are owned by |
| 52 | + * whoever made them: attaching and detaching the engine disposes only the |
| 53 | + * batch attach itself registered, so entries a lifecycle registered on its own |
| 54 | + * survive until that lifecycle disposes them. |
| 55 | + */ |
| 56 | +@Contract({ name: "keyShortcutRegistry" }) |
| 57 | +export abstract class KeyShortcutRegistry { |
| 58 | + /** Later registrations shadow earlier ones per key; disposing restores what was shadowed. */ |
| 59 | + abstract add(...shortcuts: KeyShortcut[]): KeyShortcutRegistration; |
| 60 | + /** |
| 61 | + * Every entry in registration order. The dedupe by key is the reader's, so |
| 62 | + * that a disposal exposes what it shadowed without the registry tracking it. |
| 63 | + */ |
| 64 | + abstract entries(): KeyShortcut[]; |
| 65 | +} |
| 66 | + |
| 67 | +const OFF_VALUES = ["0", "false", "off", "no"]; |
| 68 | + |
| 69 | +/** Reads an env switch by the convention `NS_KEY_SHORTCUTS` established. */ |
| 70 | +export function envSwitchIsOn(value: string): boolean { |
| 71 | + return value !== undefined && !OFF_VALUES.includes(value.toLowerCase()); |
| 72 | +} |
| 73 | + |
| 74 | +/** |
| 75 | + * Whether a command's declared `shortcuts` are attached when it runs. Off |
| 76 | + * unless `NS_COMMAND_SHORTCUTS` says otherwise: a command that takes the |
| 77 | + * terminal into raw mode and stays resident is not what a plain `ns run` or |
| 78 | + * `ns debug` has ever done. |
| 79 | + */ |
| 80 | +export function commandShortcutsEnabled(): boolean { |
| 81 | + return envSwitchIsOn(process.env.NS_COMMAND_SHORTCUTS); |
| 82 | +} |
0 commit comments