Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 43 additions & 4 deletions KEYS.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ Syntax highlighting only. No completion, no LSP, no snippets, no AI.
| `Ctrl+Y` | redo | normal, insert, visual |
| `Ctrl+Shift+Z` | redo (needs a terminal that speaks CSI-u) | normal, insert, visual |
| `Tab` | indent the selected lines | **selection only** |
| `Shift+Tab` | unindent the selected lines | **selection only** |
| `Shift+Tab` | unindent the selected lines — or, in insert, the line you are on | selection, insert |
| `Ctrl+Q` | visual block (was `Ctrl+V`) | normal, visual |
| `Ctrl+Shift+Q` | **quit, with a confirmation** | normal, insert, **and inside the REPL** — except on a terminal with no CSI-u, where **normal mode is not covered**; see below |
| **Shift+arrows** | **select, like any other editor** | normal, insert |
Expand Down Expand Up @@ -75,6 +75,16 @@ walks into the empty space past it instead of stopping. Delete the
`vim.opt.virtualedit` line to get the old behaviour back, at the price of the
click snapping again.

**Backspace out of the empty space walks you home.** A click past the end of a
line parks the caret where there are no characters under it, and vim's own
backspace out there inched back one ghost column per press with the text
untouched — thirty presses of nothing visible before the first real character
would go, which read as "backspace does not move the cursor back". Now one
press snaps the caret to the end of the real text, and the next one deletes
for real. On a blank line it snaps to column 1 first, then joins upward.
Backspace anywhere else — mid-word, after typing into the padded gap, at a
line start — is vim's own, untouched.

**Option+click is the same click.** (Alt+click on Linux and WSL — same key, same
handling; the explanation below is macOS because that is the habit it
accommodates.) macOS habit — it is how iTerm2 moves the
Expand Down Expand Up @@ -132,6 +142,29 @@ edit, the next one, and the one after, until you happened to run another search.
`Ctrl+A` selects the file in **Select** mode, so the next thing you type —
a letter, `Delete`, `Backspace` — replaces the lot and leaves you typing.

## macOS: the same keys on Cmd

Every chord above lives on Ctrl because a program running in a terminal never
sees the Cmd key — the terminal emulator owns it (Cmd+C *is* the terminal's
Copy, Cmd+Q quits the terminal), which is why every terminal editor is a Ctrl
editor on a Mac. Two habits work regardless, with zero setup: **Cmd+V already
pastes** — the terminal pastes, bracketed, into the file and the REPL alike —
and **Shift+drag then Cmd+C** copies through the terminal's own selection.

A terminal that *forwards* Cmd chords gets the whole set on Cmd too — iTerm2
3.5+ is one setting (scoped so your shell prompt keeps normal Cmd), kitty and
Ghostty unmap theirs per chord, Terminal.app cannot do it at all. Forwarded,
you get `Cmd+S/C/X/V/A/F/Z//` plus **`Cmd+Shift+Z`** for redo, **`Cmd+Q`**
for the quit prompt, **`Cmd+arrows`** for line/file ends (shifted: selecting)
and **`Cmd+Backspace`** to delete to the line start — with two deliberate
mac-isms: insert-mode `Cmd+C` copies the *line* rather than acting as Esc
(that job stays on `Ctrl+C`), and `Cmd+V` also pastes inside the REPL, where
`Ctrl+V` is left to python. Every *other* Cmd chord is deliberately
swallowed: unmapped, a forwarded `Cmd+W` would type the literal text `<D-w>`
into your file (measured). Setup recipes:
[docs/macos-cmd.md](docs/macos-cmd.md), and `tests/keycheck.sh` prints what
your terminal actually sends when you press one.

## Still vim

Everything else is untouched: `hjkl`, `w/b/e`, `dd`, `yy`, `p`, `ciw`, `.`,
Expand Down Expand Up @@ -159,9 +192,10 @@ type code -> Ctrl+E -> typing at >>> -> Ctrl+E -> back in the file, in I
You never press `i`, and you never press Esc. Landing in a live interpreter puts
you at the prompt; landing back on the file puts you in insert. `Ctrl+W j` /
`Ctrl+W k` and mouse clicks follow the same rule — *leaving the file*. They do
not work leaving the **interpreter**: nothing but `Ctrl+E` and `Ctrl+Shift+Q` is
bound in terminal mode, deliberately, so `Ctrl+W` goes to python as a word-erase
and you stay put. Out of the REPL it is `Ctrl+E`, `Ctrl+\` `Ctrl+N`, or a click.
not work leaving the **interpreter**: nothing but `Ctrl+E` and `Ctrl+Shift+Q` —
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated
plus `Cmd+V` and `Cmd+Q` where the terminal forwards Cmd (see
[docs/macos-cmd.md](docs/macos-cmd.md)) — is bound in terminal mode,
deliberately, so `Ctrl+W` goes to python as a word-erase and you stay put. Out of the REPL it is `Ctrl+E`, `Ctrl+\` `Ctrl+N`, or a click.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

**The interpreter always has the code you can see.** Edited the file since it
started? `Ctrl+E` restarts python with the new code. Didn't touch it? Same
Expand Down Expand Up @@ -341,6 +375,11 @@ Every one of these was a real collision, not a hypothetical.
Measured — two tabs on a two-line selection indented one line by 8 and the
other by 4. It uses the buffer API instead, same rule as `Ctrl+/`: change the
lines, never the mode. Output is byte-identical to `:>` otherwise.
Insert-mode `Shift+Tab` is the one later addition: it dedents the current
line via vim's own `i_CTRL-D` rather than this engine — the engine's
blank-lines-stay-blank rule (right for a block) would skip the
whitespace-only line the autoindent just gave you, which mid-typing is
exactly the line you want dedented.

11. **Clearing the search highlight from an autocmd silently does nothing.** An
autocmd body runs inside a save/restore of the search state, so writing
Expand Down
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ You're in the editor, in a file that didn't exist a second ago, cursor blinking,
already in insert mode. No path to remember, no project to scaffold, no config
to answer. Just type.

**One optional extra step on macOS:** flip a single iTerm2 setting
(*Settings → Profiles → Keys → General → "Left Command key" → Super*) and
every drill shortcut is on **Cmd** too — Cmd+C, Cmd+V, Cmd+Z, the keys your
Comment thread
jainal09 marked this conversation as resolved.
Outdated
hand already knows — while the shell prompt keeps its normal Cmd. Details and
other terminals: [docs/macos-cmd.md](docs/macos-cmd.md).

## `Ctrl+E` — your code, live

The feature you'll press a hundred times a day. One keystroke drops you into a
Expand Down Expand Up @@ -67,6 +73,11 @@ Every key does what your fingers already expect:
`Ctrl+Z` undo · `Ctrl+Y` redo · `Ctrl+F` find · `Ctrl+/` comment ·
**Shift+arrows select** · `Tab`/`Shift+Tab` indent · `Delete` clears a selection

Ctrl, not Cmd, because a terminal never hands the Cmd key to what runs inside
it — but the same chords are bound on Cmd too, live the moment your terminal
forwards them. iTerm2 does it with one setting:
[docs/macos-cmd.md](docs/macos-cmd.md).

And underneath it is still real Neovim: `hjkl`, `dd`, `ciw`, macros and
`:%s/…` all work the moment you want them.

Expand Down
152 changes: 152 additions & 0 deletions docs/macos-cmd.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
# Cmd on a Mac

drill's chords live on **Ctrl** — `Ctrl+C`, `Ctrl+V`, `Ctrl+Z` — and on a Mac
that looks like a mistake until you know the constraint: **a program running
in a terminal never sees the Cmd key.** The terminal emulator owns it. Cmd+C
is the terminal's own Copy, Cmd+V its Paste, Cmd+A selects the scrollback,
Cmd+Q quits the terminal itself — each one is handled (or swallowed) before
nvim gets a byte, and the classic terminal encoding has no way to spell a Cmd
chord at all. That is why every editor that lives in a terminal — vim, emacs,
nano, micro — is a Ctrl editor, even on a Mac.

Two Cmd habits survive that with **zero setup**, which is why they already
work in drill:

- **Cmd+V pastes.** The terminal pastes your clipboard into the session as
input, drill speaks bracketed paste, so indented Python lands unstaircased —
in the file and at the `>>>` prompt alike.
- **Shift+drag, then Cmd+C** copies with the terminal's own selection (Shift
is the modifier this config deliberately leaves to the terminal).

Everything else — Cmd+Z, Cmd+A, Cmd+/ — dies at the terminal. The Ctrl set is
the one drill can promise everywhere.

## The part that is fixable

The CSI-u / kitty keyboard protocol *can* encode a Cmd chord (Cmd is the
"super" modifier, bit 8 — Cmd+Z is `ESC [ 122;9 u`), and nvim decodes those
sequences unconditionally — measured on this config: feeding the raw bytes
fires a `<D-z>` mapping with no protocol negotiation at all. The only missing
piece is a terminal willing to **send** them instead of eating the chord.

So drill binds every editor chord on Cmd too, same modes, same handlers:

| Forwarded chord | Does |
|---|---|
| `Cmd+S` | save |
| `Cmd+C` | copy the selection — **with no selection, the line** (VS Code habit; insert-mode `Cmd+C` is *not* Esc — that job stays on `Ctrl+C`) |
| `Cmd+X` | cut the selection |
| `Cmd+V` | paste — including in the REPL, where `Ctrl+V` is left to python |
| `Cmd+A` | select all |
| `Cmd+F` | find |
| `Cmd+Z` | undo |
| `Cmd+Shift+Z` | redo (the mac redo; `Ctrl+Y` still works everywhere) |
| `Cmd+/` | comment / uncomment |
| `Cmd+Q` | quit, with the same confirmation as `Ctrl+Shift+Q` |
| `Cmd+←` / `Cmd+→` | start / end of line |
| `Cmd+↑` / `Cmd+↓` | start / end of file |
| `Cmd+Shift+arrows` | the same four motions, selecting |
| `Cmd+Backspace` | delete to the start of the line |

The cursor chords ride nvim's own `<Home>`/`<End>`/`<C-Home>`/`<C-End>`,
which are already in drill's `keymodel`, so the shifted forms select exactly
like Shift+arrows and typing over that selection replaces it.

**Every other Cmd chord is swallowed.** This is load-bearing, not tidiness:
an *unmapped* forwarded chord does not die quietly in nvim — measured, a bare
`<D-w>` typed the literal text `<D-w>` into the buffer in insert, replaced
the selection with it in Select mode, and ate a character in normal mode. So
a reflexive Cmd+W or Cmd+B from a mac hand would spray key notation into
your file. drill floors every printable Cmd chord to a no-op — in the REPL
too, where the junk would have gone to python — and binds the useful ones on
top.

On a terminal that forwards nothing, all of this is inert and costs nothing.
`Ctrl+E` and `Ctrl+R` stay Ctrl-only on purpose: they are drill's own keys,
not system chords, and keeping them in one place keeps the muscle memory
portable.

**Check what your terminal actually sends** at any point with
`~/drill/tests/keycheck.sh` — run it in the window you drill in, press the
chord, read the bytes. A forwarded Cmd+Z reads `^[[122;9u`.

## iTerm2 — one extra step after install

1. Open **Settings → Profiles →** *your profile* **→ Keys → General**.
2. Set **"Left Command key"** to **Super**.
3. Open drill and press `Cmd+Z`. If it does not undo, run
`~/drill/tests/keycheck.sh` in that window, press the chord, and read
what actually arrived.

Per iTerm2's documentation this remap is live **only while the running
program uses the kitty keyboard protocol** — nvim negotiates it, your shell
does not — so inside drill, Cmd chords arrive as `<D-...>` keys, and at the
prompt Cmd is still ordinary macOS Cmd: Cmd+V pastes, Cmd+Q quits, nothing
about the rest of your terminal life changes. Needs iTerm2 3.5+; leave the
*Right* Command key alone and you keep a stock Cmd on one thumb at all times.

Prefer cherry-picking chords instead? **Settings → Profiles → Keys → Key
Mappings → +**, action **"Send Escape Sequence"**, one mapping per chord (the
leading ESC is added for you):

| Chord | Esc+ |
|---|---|
| Cmd+Z | `[122;9u` |
| Cmd+Shift+Z | `[90;9u` |
Comment thread
jainal09 marked this conversation as resolved.
Outdated
| Cmd+/ | `[47;9u` |
| Cmd+A | `[97;9u` |
| Cmd+C | `[99;9u` |
| Cmd+X | `[120;9u` |
| Cmd+V | `[118;9u` |
| Cmd+S | `[115;9u` |
| Cmd+F | `[102;9u` |
| Cmd+← / Cmd+→ | `[1;9D` / `[1;9C` |
| Cmd+↑ / Cmd+↓ | `[1;9A` / `[1;9B` |
| Cmd+Backspace | `[127;9u` |

(For a Cmd+Shift+arrow, the modifier is `10` instead of `9`.)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

The cost of this second route: key mappings are per-profile, not per-program,
so a remapped chord sends those bytes at the shell prompt too. Map Cmd+Z and
Cmd+/ (which the shell never needed) and skip Cmd+C/Cmd+V (whose terminal
versions you probably want to keep) and the trade mostly disappears.

## kitty

kitty owns Cmd chords through its own keybindings; an **unmapped** chord is
reported to a program that speaks the keyboard protocol. So free the ones you
want drill to have, in `~/.config/kitty/kitty.conf`:

```
map cmd+z
map cmd+shift+z
map cmd+/
map cmd+a
```

A bare `map` with no action removes kitty's binding. Recent kitty can also
scope an unmap to particular windows (`map --when-focus-on ...`) if you want
a chord back at the shell — see kitty's mapping documentation.

## Ghostty

Unbind Ghostty's own use of a chord and it can reach the program
(`~/.config/ghostty/config`):

```
keybind = cmd+z=ignore
keybind = cmd+shift+z=ignore
Comment thread
jainal09 marked this conversation as resolved.
Outdated
```

`ignore` consumes Ghostty's default action so the chord is not taken; note
that macOS intercepts *menu* shortcuts before Ghostty sees them, so a few
chords (Cmd+C among them) may need their menu equivalent changed in System
Settings → Keyboard → Keyboard Shortcuts → App Shortcuts. `keycheck.sh` is
the arbiter of what actually got through.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

## Terminal.app

Cannot do this. Apple's Terminal speaks no CSI-u at all — the same reason
`Ctrl+Shift+Z` and `Ctrl+Shift+Q` do not exist there — and its keyboard
mapping UI refuses plain Cmd chords. Use the Ctrl set, or a different
terminal.
Loading
Loading