Skip to content

Keystroke 1.4.0

Choose a tag to compare

@evindor evindor released this 10 Sep 21:00
· 127 commits to main since this release

Released September 11, 2026.

Extensions now live inside Keystroke, off until you turn them on, and the
first two are in the box: Timer, which now rings and counts down in the bar,
and Translate. Every typed command explains itself as you type it. And there
is a usage guide, one page with a screenshot for every feature, reachable from
Settings → Learn Keystroke.

New

  • Extensions ship with Keystroke. Third-party extensions are one folder
    each under extensions/, contributed through pull
    requests the way the Raycast extensions repository works, and reach every
    user with the next update. The marketplace index, the hosted catalog and
    the detached install, update and remove jobs are gone. Every extension
    is off until you turn it on
    , and one that is off is never compiled:
    installing or updating Keystroke runs no third-party code. Turning one on
    asks for confirmation and says what was checked, that it runs in your
    shell with your permissions, and that reading its code first is
    recommended; confirming loads it at once, turning it off destroys its
    service. One that fails to load is listed with the reason under Needs
    attention
    . A folder in ~/.local/share/keystroke/extensions/ is picked
    up on the next open, for an extension you are writing. A declared setup
    script runs only from a Run setup row, in a visible terminal.
    bin/keystroke check-extensions is the review gate and runs on pull
    requests through GitHub Actions.
  • Commands explain themselves. A provider declares its typed triggers
    (prefix, arguments, examples) in extension.json or on the provider
    object, and the palette does the rest for all of them. Type tr, timer
    or : and the line under the search field names the action and the
    argument you are on (Translate · to: a language code or name), the
    arguments still to type appear after the caret as [to] <text> and vanish
    as you fill them, and a sheen sweeps over a recognised prefix. Tab types
    a command's prefix from its name (transl → tr ) and, while typing,
    moves to the next argument. / lists every command with its usage;
    /tr filters it. Each extension's screen starts with its usage and
    examples you can run with ↵, and turning an extension on says what to
    type. Every prefix can be renamed in the provider's settings (tm for
    timer). A typed command is exclusive: only its owner answers, which
    generalises the old ~ special case. Emoji, Files, Timer and Translate
    declare theirs.
  • Translate. Google Translate without an account, ported from the
    Raycast google-translate extension: tr bonjour detects the language and
    translates into your first target, tr fr good morning picks a target by
    code or name, bonjour to english needs no prefix. The translation is the
    answer row (↵ copies, Ctrl+↵ pastes into the app you came from), the
    reverse translation follows as a check, then every other target you
    configured. An editor view shows all of them for longer text, with
    dictation; the selection, when there is one as the palette opens, is
    offered at the root; a picker on the Translate screen chooses the target
    languages. Requests go out 350 ms after you stop typing, are cached for the
    session, and back off for a minute on HTTP 429. Off until you turn it on.
  • Timer rings and counts down in the bar. When a timer ends a sound
    plays (chime by default; drop, alarm, a file of your own, or off), and the
    soonest countdown sits in the bar right after the Keystroke button from the
    moment it starts, with +1 for every timer behind it; pressing it opens
    the Timers screen. Both are settings. This is the answer to the request
    for "a proper alarm and countdown, not a lame notification" (#6). Under
    the hood the host gained host.setBarItem and host.providerSettings, so
    any extension can put an item next to the menu button.
  • Learn Keystroke. Settings gains a Learn Keystroke row (type
    learn or guide anywhere) that opens the new usage guide,
    evindor.github.io/keystroke/guide:
    every feature on one page with a screenshot, a few lines on how it works
    and one thing to try, ending with commands and extensions.

Improvements

  • The bar button goes first on the left when installed from a checkout
    (bin/keystroke install passes left --index 0); omarchy plugin add --enable still asks for a section, and omarchy bar move evindor.keystroke left --index 0 moves it afterwards.
  • Tab is a space while typing a command: after tr fr it moves to the
    text, after timer 10m to the name, and does nothing on the last argument.
  • The website has a usage guide with 35 fresh screenshots, all rendered
    offscreen from the real palette in the current theme by
    tools/showcase/offscreen.py, without touching the desktop.

Fixes

  • Direct routes into a disabled provider (omarchy menu toggle apps with
    Applications off) return to the root with a status line instead of an
    empty palette (#3).
  • The bar widget had no size. It anchored its row to its own height
    while taking its height from the row, so the bar lost the menu button and
    never showed the countdown. It is sized from its buttons now, and the
    extensions check asserts it against the real BarWidget.qml.

Update

omarchy plugin update evindor.keystroke
omarchy restart shell

The restart is needed because Quickshell 0.3.1 keeps cached QML until the
shell restarts. Requires Omarchy 4.0.2 or later.

If you installed the Timer or Calpad extension the old way (as a separate
Omarchy plugin), it is no longer picked up from ~/.config/omarchy/plugins.
Timer ships with Keystroke: open Extensions → Timer and turn it on; its
old settings under the reverse-DNS id were deliberately not migrated. Calpad
still ships from its own repository until it is ported; a symlink to its
checkout in ~/.local/share/keystroke/extensions/calpad keeps it working
meanwhile. omarchy plugin remove <id> removes the old plugin copies.

For extension authors

manifest.json becomes extension.json in a folder under extensions/
whose name is the id; the injected property is extension, not manifest.
Declare your prefixes under commands and read ctx.command.rest instead of
parsing the prefix yourself. CONTRIBUTING.md
and docs/providers.md are current; extensions/timer
is the reference.

Validation: 162 QML tests, 48 time-zone cases, the application, file,
catalog, matching worker, engine and session checks, the palette matching,
shortcut, route, motion, dictation, extensions (now against the real bar
widget) and commands checks, the Translate check with a fake curl, the
extension review checks for Timer and Translate, the voice, clipboard, Codex
and hotkey checks, plugin validation, site checks and qmllint passed
offscreen. See the verification record.

Full changes