npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@sorrell/ink-command

v1.0.0

Published

Register and dispatch command keybinds in Ink applications.

Downloads

19

Readme

Ink Command

@sorrell/ink-command provides focus-aware keyboard commands for Ink applications. It separates keybinds from the components that handle them, routes commands through nested UI scopes, and can render the commands that are currently available.

Features

  • Define semantic commands independently from their handlers.
  • Route input to the focused scope and optionally bubble it through parent scopes.
  • Enable, disable, or replace subscriptions as components mount and unmount.
  • Normalize printable, modifier, and named keys across supported platforms.
  • Render active keybinds with KeybindsDisplay or the ready-made KeybindsFooter.
  • Resolve shared keybinds with a configurable conflict policy.

Installation

Install the package with its Ink and React peer dependencies:

npm install @sorrell/ink-command ink react

Quick start

Define commands at the application root, subscribe to them inside a focusable scope, and render a footer so users can see what is available.

import * as Ink from "ink";
import * as React from "react";
import {
    CommandScope,
    InkCommandProvider,
    Key,
    Keybind,
    KeybindsFooter,
    useCommandSubscriptions
} from "@sorrell/ink-command";

function Editor(): React.ReactElement
{
    const InkApp = Ink.useApp();
    const [ Status, SetStatus ] = React.useState("Ready");

    useCommandSubscriptions({
        "Editor.Quit": InkApp.exit,
        "Editor.Save": () => SetStatus("Saved")
    });

    return (
        <Ink.Box flexDirection="column">
            <Ink.Text>{ Status }</Ink.Text>
            <KeybindsFooter Focusable />
        </Ink.Box>
    );
}

function App(): React.ReactElement
{
    return (
        <InkCommandProvider
            Commands={ {
                "Editor.Quit": Keybind.Of("q"),
                "Editor.Save": Keybind.Of(Key.Control, "s")
            } }>
            <CommandScope
                AutoFocus
                Focusable
                Id="Editor">
                <Editor />
            </CommandScope>
        </InkCommandProvider>
    );
}

Ink.render(<App />);

Command names are plain strings. Names such as Editor.Save and Document.Copy make ownership clear as an application grows.

Commands and subscriptions

A command definition describes which keybinds invoke a semantic action. A subscription supplies the behavior for that action in the current scope. Keeping the two separate lets the same command use different handlers in different parts of an application.

Defining commands

Pass root definitions to InkCommandProvider, or register definitions from a descendant component with useCommandDefinitions.

The concise form accepts one keybind or an array of alternate keybinds:

<InkCommandProvider
    Commands={ {
        "Editor.Close": [
            Keybind.Of(Key.Escape),
            Keybind.Of(Key.Control, "w")
        ],
        "Editor.Save": Keybind.Of(Key.Control, "s")
    } }>
    { children }
</InkCommandProvider>

Use a full definition when the command also needs metadata or an enabled state:

useCommandDefinitions({
    "Editor.Format": {
        Description: "Format the active document",
        Group: "Editor",
        IsEnabled: canFormat,
        Keybinds: [ Keybind.Of(Key.Control, Key.Shift, "f") ]
    }
});

Definitions registered for the same command name are merged while their components are mounted. Keybinds are deduplicated by their normalized form.

Subscribing to commands

For the common case, map command names directly to callbacks:

useCommandSubscriptions({
    "Editor.Copy": () => copySelection(),
    "Editor.Save": (Event) => saveDocument(Event.TargetScopeId)
});

Use a subscription object to control routing and display behavior:

const Failures = useCommandSubscriptions({
    "Editor.Save": {
        AllowChildren: true,
        Bubble: false,
        Display: SaveCommandLabel,
        IsEnabled: isDirty,
        OnInvoke: saveDocument
    }
});

AllowChildren, Bubble, and IsEnabled default to true. The hook returns a record whose values indicate whether an ancestor scope blocks each requested subscription.

A callback may accept a CommandEvent containing the command name, normalized keybind, raw input, platform, target scope, owning scope, and dispatch phase.

Focus and scopes

CommandScope creates a routing boundary. When a focusable scope receives Ink focus, matching commands target that scope and then follow their subscription bubbling rules through its ancestors.

<CommandScope
    AutoFocus
    Focusable
    Id="Sidebar"
    IsActive={ isSidebarOpen }>
    <Sidebar />
</CommandScope>
  • Focusable connects the scope to Ink's focus manager.
  • AutoFocus asks Ink to focus the scope when it mounts.
  • Id gives the scope a stable identity; a React-generated ID is used when omitted.
  • IsActive removes an inactive scope from focus and command routing.

Scopes may be nested. This is useful when a screen provides general commands while a focused panel or modal provides more specific handlers.

Displaying active keybinds

KeybindsDisplay renders commands available on the current focused path. KeybindsFooter wraps the same display in a bordered footer.

<KeybindsFooter
    EmptyFallback="No commands available"
    Focusable
    MaxRows={ 2 }
    ShowDisabled
/>

When a focusable display overflows, it can receive focus and page with the left and right arrow keys. Set Width to override terminal-width pagination, or provide RenderKeybind and RenderCommand to customize individual items. Set Scoped to show commands registered in descendant scopes instead of the focused path.

Subscription objects may also provide a Display component for a command-specific label:

function SaveCommandLabel(): React.ReactElement
{
    return <Ink.Text>Save document</Ink.Text>;
}

useCommandSubscriptions({
    "Editor.Save": {
        Display: SaveCommandLabel,
        OnInvoke: saveDocument
    }
});

Creating and formatting keybinds

Keybind is exported as a namespace containing its model and constructors. Use strings for printable keys and Key for modifiers or named keys.

import { FormatKeybind, Key, Keybind } from "@sorrell/ink-command";

const Save = Keybind.Of(Key.Control, "s");
const PreviousPage = Keybind.Of(Key.PageUp);

FormatKeybind(Save); // "Ctrl+S"
FormatKeybind(Save, { Platform: "macos" });

Printable keys are normalized to lowercase for matching. Available modifier keys are Alt, Control, Hyper, Meta, Shift, and Super. Named keys include arrows, Backspace, Delete, End, Enter, Escape, Home, PageDown, PageUp, and Tab.

The provider's Platform prop accepts "auto", "linux", "macos", or "windows". It affects platform-sensitive normalization and labels such as Option, Cmd, and Win. useKeyboardCapabilities returns the library's conservative terminal capability model when behavior must account for ambiguous or unsupported input.

Keybind conflicts

Use InkCommandProvider's ConflictPolicy prop when multiple command names share a keybind:

| Policy | Behavior | | --- | --- | | ClosestScopeWins | Invokes the command with the subscription closest to the focused scope. This is the default. | | FirstRegisteredWins | Invokes the command whose definition was registered first. | | LastRegisteredWins | Invokes the command whose definition was registered most recently. | | Warn | Retains the conflict and dispatches every matching command. |

API overview

| API | Purpose | | --- | --- | | InkCommandProvider | Owns command state, captures Ink input, and provides root definitions. | | CommandScope | Creates a focus and routing boundary. | | useCommandDefinitions | Registers command definitions for the lifetime of a component. | | useCommandSubscriptions | Registers scoped handlers and reports blocked subscriptions. | | useKeyboardCapabilities | Reads the current terminal keyboard capability model. | | KeybindsDisplay | Renders active commands with customizable item renderers. | | KeybindsFooter | Renders KeybindsDisplay inside a bordered footer. | | Key | Provides modifier and named-key symbols. | | Keybind | Creates, compares, and formats immutable keybind values. | | FormatKeybind | Formats a keybind for a selected platform. |

All public APIs are exported from @sorrell/ink-command.

Development

Install dependencies and run the repository checks with:

npm install
npm run check
npm test

Build the package with npm run build. The included footer example has a separate build and run flow:

npm run build:example
npm run demo

The example source is in Example/KeybindsFooterApp.tsx.

License

MIT © 2026 Gage Sorrell