https://github.com/cortesi/hotki
A modal hotkey app for macOS
https://github.com/cortesi/hotki
hotkey hotkeys macos shortcuts
Last synced: 10 months ago
JSON representation
A modal hotkey app for macOS
- Host: GitHub
- URL: https://github.com/cortesi/hotki
- Owner: cortesi
- License: mit
- Created: 2025-08-17T08:24:56.000Z (11 months ago)
- Default Branch: main
- Last Pushed: 2025-09-10T03:22:33.000Z (10 months ago)
- Last Synced: 2025-09-10T07:07:56.646Z (10 months ago)
- Topics: hotkey, hotkeys, macos, shortcuts
- Language: Rust
- Homepage:
- Size: 5.48 MB
- Stars: 5
- Watchers: 0
- Forks: 1
- Open Issues: 1
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README

[](https://opensource.org/licenses/MIT)
# Hotki
A modal hotkey app for macOS.
- Modal hotkeys for macOS
- A customizable HUD (Heads-Up Display) for displaying active mode hotkeys
- Customizable notifications to display hotkey action outcomes
- Hotkeys for any app with key relaying and focus matching
Hotki is now an early alpha - it's stable and my daily driver, but I'm not
cutting binary releases yet. See the [Installation](#installation) section
below for how to build it. Next steps:
- Window management actions (move, resize, etc)
- More sophisticated HUD patterns allowing text entry, selection, etc.
- Tiled window layouts
- Window groups
## Window State Architecture
Hotki relies on the `hotki-world` crate as the single source of truth for
window and focus state. Engines, smoketests, and auxiliary tooling subscribe
to the [`WorldView`](crates/hotki-world/src/view.rs) trait instead of calling
`mac_winops::list_windows` directly. This keeps CoreGraphics and AX access in
one place, simplifies testing via the in-memory `TestWorld`, and ensures that
refresh hints flow through a single channel.
## Configuration
Hotki configuration lives at `~/.hotki.ron`, and uses RON ([Rusty Object
Notation](https://github.com/ron-rs/ron)) for its configuration format.
See [`examples/complete.ron`](examples/complete.ron) for a comprehensive
example demonstrating all available settings and action types.
The high-level structure of the config file is as follows:
```ron
(
// Base theme (optional, "default" if omitted)
base_theme: "solarized-dark",
// Styles applied on top of base_theme (optional).
style: (
hud: (
// HUD style overrides ...
),
notify: (
// Notification style overrides ...
),
),
keys: (
[
// Key bindings ...
],
// Attributes (optional)
( ... )
),
)
```
## Keys
The keys section is the heart of Hotki. A key specification has the following format:
```ron
("key combo", "Description", action)
// OR
("key combo", "Description", action, (attributes))
```
Key combos are specified using a `+`-separated list of keys, with zero or more
modifiers and a single key. The following modifiers are supported:
*cmd*, *ctrl*, *opt*, *shift*.
There is a complete list of supported actions and attributes below.
Let's look some real snippets from the author's own config. First, a simple
mode for controlling music:
```ron
("shift+cmd+m", "music", keys([
(
"k", "vol up",
change_volume(5),
(noexit: true, repeat: true)
),
(
"j", "vol down",
change_volume(-5),
(noexit: true, repeat: true)
),
("l", "next >>", shell("spotify next")),
("h", "<< prev", shell("spotify prev")),
("p", "play/pause", shell("spotify pause")),
("shift+cmd+m", "exit", exit, (global: true, hide: true)),
]), (capture: true)),
```
Here, the global hotkey `shift+cmd+m` activates the "music" mode. The `capture:
true` modifier instructs Hotki to swallow all non-bound keys while the HUD is
visible, preventing accidental input to the focused application. Once the music
mode is active, we can adjust the volume with `j` and `k`, both of which have
`repeat` enabled (letting us hold down the key to repeat) and `noexit` (so the
mode remains active after the action - the default is to exit the mode on
action). We also bind `shift+cmd+m` to exit the mode (but hide it from the HUD
display) so we can toggle music mode on and off with the same key combo.
Next, let's look at a very powerful technique that combines focus matching
with key relaying to produce global hotkeys that work when specific apps have
focus.
```ron
("shift+cmd+j", "obsidian", keys([
("f", "file", keys([
("f", "show in finder", relay("ctrl+shift+cmd+1")),
("n", "rename", relay("ctrl+shift+cmd+2")),
]),
),
("s", "split", keys([
("l", "right", relay("ctrl+shift+cmd+3")),
("j", "down", relay("ctrl+shift+cmd+4")),
]),
),
]), (match_app : "Obsidian")),
```
This example binds `shift+cmd+j` to enter a mode for controlling Obsidian. The
`match_app` modifier means that this binding is only active when Obsidian is
the frontmost application. In fact, we can bind the same binding to different
apps, and only the matching key mode will be activated - so `shift+cmd+j` is my
global shortcut for activating app-specific modes. The trick here is that we
combine the matching with the `relay` action, which sends keystrokes to the
focused app. We can then use Obsidian's own keyboard shortcuts to bind
little-used shortcut combinations like `ctrl+shift+cmd+1` to specific actions,
and then trigger those actions from anywhere using Hotki. Hotki's HUD means
that these shortcuts are instantly available, but they're also discoverable
through the HUD.
### Actions
Below is a table of all supported key binding actions.
exit
Exit from the HUD
pop
Pop up one level
set_volume(50)
Set volume, value is 0-100
change_volume(10)
Change volume by a relative amount (-100 to +100)
mute(toggle)
Enable/disable volume mute explicitly (on
enables, off disables, toggle toggles).
# Simple form
shell("echo 'Hello'")
# With default attributes
shell(
"echo 'Hello'",
(ok_notify: ignore, err_notify: warn)
)
Run a shell command. Attributes over-ride which notification style is
used if the command exits with or without error. Values can be "ignore",
"info", "success", "warn" and "error".
relay("cmd+shift+n")
Relay a keystroke to the focused application
fullscreen(toggle)
fullscreen(on, native)
fullscreen(off, nonnative)
Toggle or set fullscreen on the focused window.
native: Uses the app's native macOS Full Screen state
(AXFullScreen); if unsupported, Hotki falls back to sending the
standard Ctrl+Cmd+F shortcut.
nonnative (default): Maximizes the window to the current
screen's visibleFrame (does not create a new Space). Toggle
stores the prior frame per window and restores it on the next
toggle/off.
place(grid(3, 1), at(0, 0))
place(grid(2, 2), at(1, 0))
Place the focused window into a grid cell on the current screen.
-
grid(x, y)divides the screen into x columns and y rows. Both must be> 0. -
at(ix, iy)selects a zero‑based cell within that grid. The origin(0, 0)is top‑left.
Example:
place(grid(3, 1), at(0, 0)) places the window in the left‑most third of the screen.
place_move(grid(3, 2), left)
place_move(grid(3, 2), right)
place_move(grid(3, 2), up)
place_move(grid(3, 2), down)
Move the focused window by one cell within a grid on the current screen.
-
grid(x, y)defines the grid (both> 0). -
up/down/left/rightmove one cell and clamp at edges (no wrap). - First invocation from a non‑aligned position places at the visual top‑left cell (0, 0).
raise(app: "^Safari$")
raise(title: "Downloads")
raise(app: "Safari", title: "Downloads")
Raise (activate) a window matching the given specification.
-
Regex matching:appandtitleare regular expressions, identical in semantics tomatch_app/match_title. -
AND semantics: when both are provided, both must match. -
Scope: on‑screen layer‑0 windows in the current Space (v1). Does not un‑minimize or switch Spaces. -
Cycling: if the current frontmost window matches and there is another matching window behind it, focus moves to the next match; if there is no other match, it’s a no‑op.
keys([ /* ... */ ])
Enter a nested keys section (sub‑mode)
reload_config
Reload the configuration file
clear_notifications
Clear all on‑screen notifications
show_details(toggle)
Control the details window visibility. Use
show_details(on) to show, show_details(off) to
hide, or
show_details(toggle) to toggle.
show_hud_root
Show the HUD with root‑level key bindings
theme_next
Switch to the next theme
theme_prev
Switch to the previous theme
theme_set("dark-blue")
Set a specific theme by name
user_style(toggle)
Enable/disable user style configuration (
on enables,
off disables, toggle toggles). When off, the base
theme is revealed unmodified.
### Attributes
Per-binding attributes are specified as the optional 4th element of a key tuple:
(
"k", "Description", action_here, (modifier1: value, modifier2: value)
)
Attributes apply to the mode they are defined on, and all nested keys and modes
below them.
noexit: true
Do not exit the current mode after executing this action. Also serves as the default for
repeat when repeat is not set.
global: true
Make this binding available in all descendant sub‑modes.
hide: true
Hide this binding from the HUD while keeping it active.
hud_only: true
Only bind this key when the HUD is visible.
match_app: "Safari|Chrome"
Enable only when the frontmost application name matches this regex.
match_title: ".*\\.md$"
Enable only when the active window title matches this regex.
repeat: true
Enable hold‑to‑repeat for supported actions (shell, relay, change_volume). Defaults to the value of
noexit when omitted.
repeat_delay: 250
Initial delay (ms) before the first repeat tick.
repeat_interval: 33
Interval (ms) between repeat ticks.
style: (hud: (...), notify: (...))
Per‑binding style overlay applied while this binding’s mode is active.
capture: true
While this mode is active and the HUD is visible, swallow all non‑bound keys so they are not delivered to the focused app.
## Themes and Styling
Every aspect of Hotki's UI is customizable. We have a few built-in
[themes](./crates/config/themes) that you can build on, or you can override
everything for complete control.
default
solarized-dark
solarized-light
dark-blue
charcoal
## Fonts
The default bundled font is a [Nerd Font](https://www.nerdfonts.com/)
([0xProto](https://github.com/0xType/0xProto)
Nerd Font Mono). Nerd Fonts include a wide range of glyphs and symbols used
throughout the UI, and which can be used in styling.
# Installation
We don't have binary releases yet. For the moment, the installation process is
to compile the app bundle using the following script from the repo root:
```sh
./scripts/bundle.sh
```
The bundle will be at `./target/bundle/Hotki.app`, ready to copy to your
`/Applications` folder.