Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
19 changes: 16 additions & 3 deletions KEYS.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Syntax highlighting only. No completion, no LSP, no snippets, no AI.
| `Tab` | indent the selected lines | **selection only** |
| `Shift+Tab` | unindent the selected lines | **selection only** |
| `Ctrl+Q` | visual block (was `Ctrl+V`) | normal, visual |
| `Ctrl+Shift+Q` | **quit, with a confirmation** | normal, insert, **and inside the REPL** |
| `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 |
| `Delete` / `Backspace` | delete the selection and keep typing | selection |
| Arrows | movement | everywhere |
Expand Down Expand Up @@ -227,8 +227,21 @@ is live).
It exists for the same reason `Ctrl+Shift+Z` does: in the legacy encoding Shift
is dropped from a control chord, so `Ctrl+Q` and `Ctrl+Shift+Q` are the same
byte. With CSI-u they are different keys — which is what lets this coexist with
`Ctrl+Q` for visual block. **On a terminal that speaks no CSI-u it will not
fire**; `:qa!` still works there.
`Ctrl+Q` for visual block.

**On a terminal that speaks no CSI-u the chord arrives as plain `Ctrl+Q`**, so
on WSL — where Windows Terminal does not negotiate CSI-u — insert-mode and
terminal-mode `Ctrl+Q` are bound to the same prompt. From insert, which is where
this editor keeps you, you press the documented chord and it works. Two costs:
plain `Ctrl+Q` then quits from insert as well (Cancel is still the default) and
stops being vanilla vim's literal-insert.

**Normal mode is not covered there.** `Ctrl+Q` in normal mode is visual block
and is the only route to one — `Ctrl+V` is paste — so binding it to the prompt
would leave no way to ask for a block at all, while quitting still has `:qa!`.
The trade is deliberate: from normal mode on such a terminal, `Ctrl+Shift+Q`
gives you a visual block, so press `i` first or use `:qa!`. None of this applies
off WSL. See [docs/wsl.md](docs/wsl.md).

| Command | Does |
|---|---|
Expand Down
68 changes: 46 additions & 22 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,21 @@ Landed as a stack of PRs, one per checkpoint, each based on the previous.
version parse that survives `-dev` strings, and correct nvim install hints
per platform. (`drill.sh`'s only `apt install` line is for fzf, which has
no version gate and is not stale — nothing to do there.)
*Gate: `install.sh` into a temp `DRILL_HOME`.*
*Gate: package-manager detection on this box; `--yes` installs and
re-probes; a non-tty run prints the command and runs nothing; the
version parse survives `0.10-dev` and a missing `NVIM v` prefix; and
the apt hint is withheld when apt's candidate is below 0.9.*
- [x] **2 — `wsl/02-clipboard`** · `nvimrc.lua`: native provider first, a
`clip.exe` + `powershell Get-Clipboard` shim only when no *usable*
provider is found — "usable" being the load-bearing word, since an
installed `xclip` with no display is discovered and still cannot copy
anything, and an empty clipboard is not the same as an absent provider;
and resolve `python3` past the `/mnt/.../WindowsApps` Store alias
that WSL's PATH interop otherwise hands us.
*Gate: `suite_config.sh` clipboard cases green; copy/paste both directions by hand.*
*Gate: `suite_config.sh` clipboard cases green; copy/paste both
directions by hand; the shim engages with the providers hidden and
round-trips with no stray `\r`; and a stub `python3` planted under
`/mnt` is walked past, with a healthy PATH left untouched.*
- [x] **3 — `wsl/03-timer`** · `drill.sh`: `run()` reported success for a
process that merely launched, so a player that exits non-zero silenced
the bell fallback too — check the exit code, add a timeout. Extend the
Expand All @@ -57,38 +63,56 @@ Landed as a stack of PRs, one per checkpoint, each based on the previous.
Scope grew once the suite could run here: **`pgrep -f "$TAG"` matches any
process that merely *mentions* the tag**, so `t` was stopping strangers —
including the test shell that holds the tag in its own `-c` string, which
is why 30 of 36 timer cases failed on Linux before any of this. Confirm
each candidate is an interpreter with `ps -o comm=` instead.
is why 30 of 36 timer cases failed on Linux before any of this. An
executable check and an end-anchor were both tried and both still
accepted `python3 train.py --tag drill-timer`; identity finally comes
from a marker inside the timer's own source, which is `argv[2]` of the
running python and which nothing else carries.
*Gate: `suite_timer.sh` 36/36 (was 6/36); sound and notification both fire.*
- [x] **4 — `wsl/04-demo-tests`** · `demo.sh` `bc` → `awk`; `tests/bin/timeout`
delegates to real GNU `timeout` when one exists instead of shadowing it;
`tests/run.sh` says so loudly when no clipboard provider is on PATH, and
counts failing *suites* rather than summing exit codes — it called 30
broken timer cases "2 FAILING CASE(S)", small enough to read as a flake.

Also: `demo.sh --check` failed its `Ctrl+Shift+Q` case on every Linux box
and told you to record with `--keys socket`, which is the mode it was
already in. `--remote-send` collapses `<C-S-q>` to `<C-q>`, whose
literal-insert then eats the cancelling `c` — the exact Shift-drop that
made CSI-u necessary. Socket mode cannot ask that question, so it skips
it and says why.
Also, and recorded here because the wrong version of it shipped first:
`demo.sh --check` appeared to fail its `Ctrl+Shift+Q` case, and I
diagnosed it as `--remote-send` collapsing `<C-S-q>` to `<C-q>`. **That
was wrong.** `--remote-send` speaks nvim's key *notation*, not terminal
bytes, so no encoding happens and CSI-u never enters into it — measured
under a real pty, socket mode reports `ok Ctrl+Shift+Q reaches its mapping`.
What I had actually measured was a screenless nvim, where `confirm()`
cannot draw and returns instantly. The skip built on that reasoning is
reverted; the check runs unconditionally, as it always did.
*Gate: full `./tests/run.sh` 556/556; `./demo.sh --check` clean.*
- [x] **5 — `wsl/05-ci-docs`** · `.github/workflows/tests.yml` running the suite
on `ubuntu-latest` under `xvfb` with `xclip`, so the clipboard cases are
real; `docs/wsl.md`; requirement lines in `README.md`, `KEYS.md`,
`docs/testing.md`.
*Gate: CI green on the PR itself.*

### Still to be measured by a human

`Ctrl+Shift+Q` only exists when the terminal negotiates CSI-u. Windows Terminal
≥1.22 does; older builds use win32-input-mode and the chord never fires, leaving
`:qa!` as the only way out. Nothing automated can settle this: `suite_quit.sh`
synthesises `ESC[113;6u` straight onto the pty, so it passes on a terminal where
the chord could never arrive, and `demo.sh --check` cannot send CSI-u over
`--remote-send` at all. **Press it in Windows Terminal.** If the confirmation
appears, your build speaks CSI-u; if nothing happens, update Windows Terminal or
use `:qa!`. Written up in [docs/wsl.md](docs/wsl.md).
*Gate: CI green on the PR itself — and green for the right reason:
the log must show the pinned nvim sha256 verified, `ALL SUITES
PASSED`, and no `no clipboard provider` warning, which is what proves
`xvfb` + `xclip` made the register cases real rather than skipped.*

### Measured by a human, and then fixed

`Ctrl+Shift+Q` did not work on WSL. Nothing automated could have told us:
`suite_quit.sh` writes `ESC[113;6u` straight onto the pty, so it passes on a
terminal where the chord could never arrive, and `demo.sh --check` reaches the
mapping over `--remote-send`, which consumes nvim's key *notation* and never
exercises a terminal encoding at all. Neither one presses a key. It took a
human.

Windows Terminal 1.24 is new enough to speak CSI-u and does not negotiate it
with nvim, so the chord arrives as the legacy `0x11` — plain `<C-q>` — and the
`<C-S-q>` mapping is never reached. On WSL the config now also binds `<C-q>` in
insert and terminal mode, which is what actually arrives.

**Normal mode is deliberately not covered.** There `<C-q>` is visual block and
is the only route to one, since `<C-v>` is paste — so binding it to the prompt
would leave no way to ask for a block, while quitting still has `:qa!`. From
normal mode on such a terminal the chord gives you a block; press `i` first.
Off WSL none of this applies. Written up in [docs/wsl.md](docs/wsl.md).

### Not in scope, deliberately

Expand Down
61 changes: 45 additions & 16 deletions docs/wsl.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,22 +81,51 @@ anything.

## `Ctrl+Shift+Q`

`Ctrl+Shift+Q` only exists as a distinct key when the terminal negotiates
CSI-u. In the legacy encoding Shift is dropped from a control chord, so
`Ctrl+Q` and `Ctrl+Shift+Q` are the same byte and nothing downstream can tell
them apart.

**Measured: Windows Terminal 1.24 does not negotiate CSI-u with nvim**, so the
chord arrives as plain `<C-q>`. Updating Windows Terminal does not fix that —
1.24 is already new enough to speak the protocol and simply does not. The WSL
`<C-q>` binding that makes the chord work lands with the quit checkpoint later
in this stack; until then the way out is `:qa!`.

**Nothing automated can check this.** `suite_quit.sh` writes `ESC[113;6u`
straight onto the pty, so it passes on a terminal where the chord could never
arrive, and `demo.sh --check` reaches the mapping over `--remote-send`, which
speaks nvim key notation rather than terminal bytes — neither one is pressing a
real key. That is why this needed a human.
Press it. It works — but not by the route it uses on a Mac, and that is worth
knowing if you ever wonder why `Ctrl+Q` also quits here.

`Ctrl+Shift+Q` only exists as a distinct key when the terminal negotiates CSI-u.
In the legacy encoding Shift is dropped from a control chord, so `Ctrl+Q` and
`Ctrl+Shift+Q` are the same byte — `0x11` — and nothing downstream can tell them
apart. **Windows Terminal 1.24 does not negotiate CSI-u with nvim**, so the
chord arrives as plain `<C-q>` and the `<C-S-q>` mapping is never reached.

So on WSL, `nvimrc.lua` also binds what actually arrives:

| | |
|---|---|
| insert mode, `Ctrl+Q` (and therefore `Ctrl+Shift+Q`) | the quit prompt |
| terminal mode — i.e. inside the REPL | the quit prompt, so you can leave without pressing `Ctrl+E` first |
| normal mode, `Ctrl+Q` | still visual block, untouched |

Two consequences. `Ctrl+Q` on its own quits from insert *and from inside the
interpreter* too — the prompt defaults to **Cancel**, so a slip costs one
keystroke. And insert-mode
`Ctrl+Q` is no longer vanilla vim's literal-insert; in a Python scratchpad where
`Ctrl+V` is already paste, that was close to unreachable anyway.

Gated on **WSL**, not on the protocol — there is no runtime signal for whether
CSI-u was negotiated (`vim.g.termfeatures` is `nil` even in a real TUI on 0.12),
so the binding is installed on every WSL session. If yours *does* speak CSI-u,
`Ctrl+Shift+Q` reaches its own mapping as normal and you additionally get bare
`Ctrl+Q` quitting from insert, losing literal-insert there. Off WSL none of it
applies and `Ctrl+Q` keeps every meaning it had.

**Normal mode is the gap.** `Ctrl+Q` in normal mode is visual block, and it is
the only way to reach one — `Ctrl+V` there is paste. So on a terminal without
CSI-u, pressing `Ctrl+Shift+Q` from normal mode gives you a visual block, not
the prompt. That is a deliberate trade: quitting from normal mode still has
`:qa!`, while visual block would have nothing left. If you are in normal mode
and want the prompt, press `i` first, or use `:qa!`.

**Nothing automated can check the CSI-u half.** `suite_quit.sh` writes
`ESC[113;6u` straight onto the pty, so it passes on a terminal where the chord
could never arrive; `demo.sh --check` cannot answer it either, and says so
rather than guessing — `--remote-send` consumes nvim's key *notation*, so it
delivers `<C-S-q>` to the mapping and never exercises the terminal encoding at
all. (An earlier version of this page said it collapsed the chord. It does not;
that claim was measured against a screenless nvim and is retracted.) Which is
why this needed a human to press it.

## Keep `DRILL_HOME` off `/mnt/c`

Expand Down
36 changes: 36 additions & 0 deletions nvimrc.lua
Original file line number Diff line number Diff line change
Expand Up @@ -1258,3 +1258,39 @@ local function quit_drill()
end

map({ "n", "i", "t" }, "<C-S-q>", quit_drill, S)

-- ...and the same chord again, for terminals that cannot spell it.
--
-- Everything above assumes CSI-u. Windows Terminal 1.24 is new enough to speak
-- it and does not negotiate it with nvim, so Ctrl+Shift+Q arrives as the plain
-- legacy 0x11 -- <C-q> -- and the mapping above is simply never reached. The
-- documented way out of the editor does not exist on WSL, which leaves `:qa!`:
-- the one thing this whole section was written to stop being necessary.
--
-- So bind what actually ARRIVES. This is not a second, different key to learn:
-- on such a terminal the user presses Ctrl+Shift+Q, exactly as documented, and
-- <C-q> is what nvim is handed.
--
-- INSERT ONLY, and gated on WSL, because both halves cost something:
-- * normal mode keeps Ctrl+Q as visual block (CONFLICT 2) -- it is the only
-- way to ask for one, and drill's premise is that you are in insert anyway.
-- * in insert, vanilla <C-q> is literal-insert, the twin of <C-v>. In a
-- Python scratchpad where <C-v> is already paste, that is close to
-- unreachable; a quit prompt that defaults to Cancel is the better use of
-- the key. But it IS a real vim behaviour, so nowhere but WSL loses it.
-- On macOS, and on any Linux terminal that does negotiate CSI-u, this whole
-- block is skipped and nothing changes.
-- "i" AND "t": <C-S-q> is bound in n/i/t precisely so you can quit from inside
-- the interpreter without pressing <C-e> first, and a fallback that covered
-- only insert would take that away on the one platform that needs it. Normal
-- mode is the deliberate omission -- there <C-q> is visual block, and the only
-- route to one.
--
-- In the interpreter this is not a free key, which the first version of this
-- comment claimed: readline in emacs mode binds ^Q to quoted-insert, so the
-- rebind really does take a live feature away. It is a fair trade rather than
-- a free one -- ^V is bound to the same command in readline, so quoted-insert
-- is still there. (drill.sh's `stty -ixon` is what frees ^Q from XON at all.)
if IS_WSL then
Comment thread
jainal09 marked this conversation as resolved.
map({ "i", "t" }, "<C-q>", quit_drill, S)
end
19 changes: 19 additions & 0 deletions tests/suite_options.sh
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,25 @@ end
ok("quit_did_not_steal_ctrl_q",
vim.fn.maparg("<C-q>", "n"):lower() == "<c-v>", vim.fn.maparg("<C-q>", "n"))

-- The WSL quit fallback, which nothing else here reaches: on a terminal with no
-- CSI-u the chord arrives as plain <C-q>, so the config binds it in insert AND
-- terminal mode, and deliberately NOT in normal, where it is the only route to
-- a visual block. Asserted from whichever side this machine is on -- a WSL box
-- proves the bindings exist, and CI (ubuntu-latest, no "microsoft" in
-- /proc/version) proves they do not leak onto a normal Linux desktop. Between
-- the two runs both halves are covered; on one machine only one half can be.
local on_wsl = vim.env.WSL_DISTRO_NAME ~= nil
if not on_wsl and vim.fn.filereadable("/proc/version") == 1 then
local first = vim.fn.readfile("/proc/version", "", 1)[1] or ""
on_wsl = first:lower():find("microsoft") ~= nil
end
for _, m in ipairs({ "i", "t" }) do
local bound = vim.fn.maparg("<C-q>", m, false, true)
local has = type(bound) == "table" and bound.callback ~= nil
ok("wsl_quit_fallback_" .. m, has == on_wsl,
("on_wsl=%s bound=%s"):format(tostring(on_wsl), tostring(has)))
end

-- Ctrl+C must stay unmapped in normal and terminal so SIGINT still lands
ok("ctrlc_free_in_normal", vim.fn.maparg("<C-c>", "n") == "", vim.fn.maparg("<C-c>", "n"))
ok("ctrlc_free_in_terminal", vim.fn.maparg("<C-c>", "t") == "", vim.fn.maparg("<C-c>", "t"))
Expand Down
Loading