v0.5.0 key releases: key model, protocol source, console sources (#12)
* key model for releases: KeyAction, SetKeyReport, focus and key report events, hold pairing A key event now carries its Action: KeyPress, KeyRepeat or KeyRelease. Every terminal still reports presses only, so Action stays KeyPress and every event is what v0.4.3 sent, until a game asks with SetKeyReport. The ask names the sources releases may come from (KeySourceConsole, KeySourceProtocol); an EventKeyReport answers each ask with the level reached and its source. No source exists yet, so every ask answers presses and nothing is written. KeyHolds pairs a release with its press by physical key, so a release carries what its press carried, and turns a source's lost reports into Lost releases. It is exported for hosts that read native key events (KeySourceHost). The reader sends keys through the level in force: at presses a repeat is a press and a release is dropped; at releases a key with no physical key behind it is a tap. Focus reports end every hold, Lost, before EventFocus; they are read only once a source turns them on. Mouse events gain a Touch bit for hosts. Terminal gains SetKeyReport, so this is v0.5.0. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B3zCVjspRDuEuMrDTumVhp * protocol source: releases from the terminal's key event protocol, Alacritty's SetKeyReport with KeySourceProtocol asks the terminal whether it has the key event protocol (CSI ? u, fenced by a DA1 request), pushes its flags (all five, CSI > 31 u) and asks what they came to before reporting releases. A terminal must confirm event types, every key as an escape code and the text keys type; one that answers DA1 alone, takes the push without them, or answers nothing within two seconds stays at presses, and is not asked again. Detection goes by these answers only, never by TERM or the terminal's name. While the flags are pushed, key reports are decoded into the events a legacy terminal sends for the same keys, so a game can keep the protocol on for its whole run: Shift goes into the character, Ctrl with a character into its control key, Alt stays a modifier; Super, the locks, lone modifiers, media keys and F13 and up report nothing. Ctrl with a character that has no control key keeps ModCtrl. Releases pair with their presses through KeyHolds; a key the terminal names none for (text from an input method) is a tap. Focus reports come on with the protocol and end every hold, Lost, as focus leaves. Going back to presses releases what is held, pops the flags and stops focus reports, decoding until the terminal confirms the pop. Fini pops the flags before the alternate screen goes, and EmergencyReset when they are pushed. Fini now stops the input reader without holding the terminal's lock, since the reader writes its requests through it. Checked against Alacritty 0.13.2 on Xvfb with xdotool: 58 key combinations read identically with the protocol on and off (Ctrl+1 keeps ModCtrl), every press got its release, Shift released first still released 'A', focus loss and a release swallowed by Alacritty's Vi mode ended holds Lost. Inside tmux 3.4 the ask came to presses. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B3zCVjspRDuEuMrDTumVhp --------- Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
1 parent
a8805d9be1
commit
97f66b1f1c
10 files changed
+1310
-17
No files matched your search
@@ -224,7 +224,8 @@ division approximation; `SoftLight` uses init-time LUTs replacing `math.Sqrt`.
|
||||
|
||||
- `EventKey` — `Key` for named keys (`KeyEnter`, `KeyUp`, `KeyCtrlC`, ...),
|
||||
`Key == KeyRune` with `Rune` set for printable input, `Modifiers` bitmask
|
||||
(`ModShift`, `ModAlt`, `ModCtrl`)
|
||||
(`ModShift`, `ModAlt`, `ModCtrl`), `Action` (`KeyPress`, and `KeyRepeat` and
|
||||
`KeyRelease` while releases are reported; see below)
|
||||
- `EventMouse` — 0-indexed `MouseX/Y`, `MouseBtn` (buttons, wheel),
|
||||
`MouseAction` (press/release/move/drag), modifiers. Enable via
|
||||
`SetMouseMode(MouseModeClick | MouseModeDrag)`; SGR protocol only.
|
||||
@@ -232,6 +233,8 @@ division approximation; `SoftLight` uses init-time LUTs replacing `math.Sqrt`.
|
||||
- `EventPaste` — `Text` holds a whole paste, as sent, control characters
|
||||
included. Enable via `SetPasteMode(true)` (bracketed paste); off, a paste
|
||||
arrives as keys, its newlines pressing Enter.
|
||||
- `EventKeyReport` — the outcome of a `SetKeyReport` ask: `Report` and `Source`
|
||||
- `EventFocus` — `Focused`, while releases come through the terminal's protocol
|
||||
- `EventError`, `EventClosed`
|
||||
|
||||
A standalone ESC press is disambiguated from escape sequences by a short input-idle timeout (one ~10ms poll cycle).
|
||||
@@ -244,6 +247,69 @@ On Unix, a redirected stdin or stdout is replaced by `/dev/tty`, so
|
||||
draws and reads keys on the terminal.
|
||||
`PostEvent` injects synthetic events (used for clean shutdown of blocked `PollEvent`).
|
||||
|
||||
### Key releases
|
||||
|
||||
A terminal reports presses: a key held down arrives as more presses at the
|
||||
keyboard's repeat rate, and nothing says when it comes up. A game that needs
|
||||
holds asks for releases:
|
||||
|
||||
```go
|
||||
term.SetKeyReport(terminal.KeyReportReleases, terminal.KeySourceConsole|terminal.KeySourceProtocol)
|
||||
```
|
||||
|
||||
The ask returns at once. An `EventKeyReport` follows with what the terminal
|
||||
reports now (`Report`) and where releases come from (`Source`), so a game
|
||||
learns the outcome of every ask and can fall back, to taps for one, where
|
||||
there are none. `SetKeyReport(terminal.KeyReportPresses, 0)` gives the source
|
||||
back. Until a game asks for releases, nothing is sent to the terminal or
|
||||
switched, and every key event is a press, as before.
|
||||
|
||||
While releases are reported:
|
||||
|
||||
- Each key event carries its `Action`: `KeyPress`, `KeyRepeat` while held, and
|
||||
`KeyRelease`. A release carries what its press carried, paired by physical
|
||||
key, so letting go of Shift first still releases `'A'`, not `'a'`.
|
||||
- A key event the terminal names no physical key for (text from an input
|
||||
method, a key its protocol leaves out) can't be held: its release follows
|
||||
at once.
|
||||
- Every hold ends. Focus leaving, the console switching away, a change of
|
||||
source and `Fini` release the keys held with `Lost` set, since their own
|
||||
releases won't arrive. A press of a key already held means its release was
|
||||
lost (a terminal's own mode can swallow one): the old hold ends, `Lost`,
|
||||
before the new press.
|
||||
|
||||
Releases come from the first of these sources a game allows that the
|
||||
terminal has:
|
||||
|
||||
- `KeySourceProtocol`: the terminal's key event protocol, CSI u with event
|
||||
types. The package asks the terminal whether it has the protocol (`CSI ? u`,
|
||||
fenced by a DA1 request every terminal answers), pushes its flags and
|
||||
confirms what they came to before reporting releases; it goes by the
|
||||
terminal's answers, never by `TERM` or the terminal's name. Alacritty 0.13
|
||||
and later is the reference (Enter, Tab and Backspace release from 0.15).
|
||||
Other terminals that answer the same way (foot, Ghostty, WezTerm with its
|
||||
opt-in setting, recent Konsole) should work but are untested. Works over ssh.
|
||||
tmux, screen and mosh don't pass the protocol through, and VTE terminals and
|
||||
xterm don't have it; there the answer is presses after one round trip. The
|
||||
flags are popped when the game goes back to presses, at `Fini`, and in
|
||||
`EmergencyReset`.
|
||||
|
||||
With the protocol on, every key arrives as an escape code, and each press
|
||||
reads as the key a legacy terminal sends for it reads, so a game can keep
|
||||
the protocol on for its whole run: Shift goes into the character, Ctrl with
|
||||
a character becomes its control key, and Alt stays a modifier. Ctrl with a
|
||||
character that has no control key (Ctrl+1) keeps `ModCtrl`, which a legacy
|
||||
terminal can't send. Escape no longer waits out the ESC timeout. Lone
|
||||
modifier keys, media keys and F13 and up report nothing. Alacritty reports no
|
||||
keys while its own Vi mode, search or hints run; a key held then ends on its
|
||||
next press, or as focus leaves.
|
||||
|
||||
`KeyHolds` is the pairing on its own, for a host that reads its platform's key
|
||||
events and builds `Event`s itself (`KeySourceHost`): it gives each release its
|
||||
press's key and turns the contradictions above into `Lost` releases.
|
||||
|
||||
A Terminal over a caller's backend (`NewWithBackend`) reports presses.
|
||||
|
||||
## WASM
|
||||
|
||||
WASM builds bridge to xterm.js via JS globals:
|
||||
|
||||
@@ -160,6 +160,9 @@ func (b *unixBackend) Read(stopCh <-chan struct{}) ([]byte, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// keySources: the key protocol, which the terminal is asked about
|
||||
func (b *unixBackend) keySources() KeySource { return KeySourceProtocol }
|
||||
|
||||
func (b *unixBackend) SetResizeHandler(handler func(width, height int)) {
|
||||
b.resizeStopCh = make(chan struct{})
|
||||
b.resizeDoneCh = make(chan struct{})
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
// the terminal's own default colors
|
||||
// - Double-buffered output with cell-level diffing
|
||||
// - Raw input parsing with escape sequence handling and bracketed paste
|
||||
// - Key releases on request, through the terminal's key event protocol
|
||||
// (Alacritty's), and presses on terminals without it
|
||||
// - Drawing on /dev/tty when stdin or stdout is redirected (Unix)
|
||||
// - Resize detection (SIGWINCH on Unix, callback on WASM)
|
||||
// - Clean terminal restoration on exit/panic
|
||||
|
||||
@@ -16,10 +16,12 @@ type EventType uint8
|
||||
const (
|
||||
EventKey EventType = iota
|
||||
EventResize
|
||||
EventPaste // Bracketed paste, Text holds it
|
||||
EventMouse // SGR mouse reporting
|
||||
EventError // Read error
|
||||
EventClosed // Input closed
|
||||
EventPaste // Bracketed paste, Text holds it
|
||||
EventMouse // SGR mouse reporting
|
||||
EventError // Read error
|
||||
EventClosed // Input closed
|
||||
EventFocus // The terminal gained or lost focus, while it reports releases through its protocol
|
||||
EventKeyReport // What a SetKeyReport ask came to: Report through Source
|
||||
)
|
||||
|
||||
// Event represents a terminal input event
|
||||
@@ -28,16 +30,22 @@ type Event struct {
|
||||
Key Key
|
||||
Rune rune
|
||||
Modifiers Modifier
|
||||
Width int // For EventResize
|
||||
Height int // For EventResize
|
||||
Err error // For EventError
|
||||
Text string // For EventPaste: the pasted bytes as sent, controls included
|
||||
Action KeyAction // For EventKey: KeyPress unless the terminal reports releases
|
||||
Lost bool // For a KeyRelease the package made for a hold whose own release can't arrive
|
||||
Width int // For EventResize
|
||||
Height int // For EventResize
|
||||
Err error // For EventError
|
||||
Text string // For EventPaste: the pasted bytes as sent, controls included
|
||||
Focused bool // For EventFocus: whether the terminal now has focus
|
||||
Report KeyReport // For EventKeyReport: what the terminal now reports of each key
|
||||
Source KeySource // For EventKeyReport: the source releases come from, zero for presses
|
||||
|
||||
// Mouse event fields
|
||||
MouseX int
|
||||
MouseY int
|
||||
MouseBtn MouseButton
|
||||
MouseAction MouseAction
|
||||
Touch bool // A touch, from a host that reads one; terminals report none
|
||||
}
|
||||
|
||||
// inputReader handles raw stdin parsing
|
||||
@@ -60,6 +68,34 @@ type inputReader struct {
|
||||
// An over-long CSI sequence is being dropped up to its final byte
|
||||
dropCSI bool
|
||||
|
||||
// Key reporting asks: the sources the backend has, the latest ask waiting
|
||||
// for the reader, and how the reader writes to the terminal, false once it
|
||||
// is closing; pushed is whether the protocol's flags are pushed after seq
|
||||
sources KeySource
|
||||
asks chan keyAsk
|
||||
write func(seq []byte, pushed bool) bool
|
||||
|
||||
// Key reporting, the reader's own: the ask it works toward and whether it
|
||||
// is still to be answered, what the terminal reports of each key and the
|
||||
// source releases come from, whether focus reports are parsed, and the
|
||||
// keys a source reports held down
|
||||
want keyAsk
|
||||
answer bool
|
||||
report KeyReport
|
||||
source KeySource
|
||||
focus bool
|
||||
holds KeyHolds
|
||||
|
||||
// The protocol: an exchange in flight and when it is given up, the flags
|
||||
// the terminal reported in it (-1 for none), whether the terminal has the
|
||||
// protocol (-1 no, 0 not known, 1 yes), and whether key reports are
|
||||
// decoded, from the push until the terminal has the pop
|
||||
exchange protoExchange
|
||||
deadline time.Time
|
||||
protoReported int
|
||||
protoHas int8
|
||||
decoding bool
|
||||
|
||||
mu sync.Mutex
|
||||
running bool
|
||||
}
|
||||
@@ -69,6 +105,7 @@ func newInputReader(backend Backend) *inputReader {
|
||||
return &inputReader{
|
||||
backend: backend,
|
||||
eventCh: make(chan Event, 256),
|
||||
asks: make(chan keyAsk, 1),
|
||||
stopCh: make(chan struct{}),
|
||||
doneCh: make(chan struct{}),
|
||||
buf: make([]byte, 0, 256),
|
||||
@@ -128,9 +165,12 @@ func (r *inputReader) readLoop() {
|
||||
}()
|
||||
|
||||
for {
|
||||
r.serviceKeys()
|
||||
|
||||
// Blocking read from backend
|
||||
data, err := r.backend.Read(r.stopCh)
|
||||
if err != nil {
|
||||
r.holds.ReleaseAll(r.sendEvent)
|
||||
r.sendEvent(Event{Type: EventError, Err: err})
|
||||
return
|
||||
}
|
||||
@@ -147,11 +187,12 @@ func (r *inputReader) readLoop() {
|
||||
// Timeout (Unix poll) or empty read: a pending ESC, or two, was
|
||||
// pressed alone; inside a paste it is text
|
||||
if ev, ok := loneEscape[string(r.buf)]; ok && !r.pasting {
|
||||
r.sendEvent(ev)
|
||||
r.sendKey(0, ev)
|
||||
r.buf = r.buf[:0]
|
||||
}
|
||||
select {
|
||||
case <-r.stopCh:
|
||||
r.holds.ReleaseAll(r.sendEvent)
|
||||
r.sendEvent(Event{Type: EventClosed})
|
||||
return
|
||||
default:
|
||||
@@ -226,7 +267,7 @@ func (r *inputReader) parseInput(data []byte) int {
|
||||
|
||||
// Fast path: printable ASCII
|
||||
if b >= 0x20 && b < 0x7f {
|
||||
r.sendEvent(Event{Type: EventKey, Key: KeyRune, Rune: rune(b)})
|
||||
r.sendKey(0, Event{Type: EventKey, Key: KeyRune, Rune: rune(b)})
|
||||
i++
|
||||
continue
|
||||
}
|
||||
@@ -245,8 +286,10 @@ func (r *inputReader) parseInput(data []byte) int {
|
||||
}
|
||||
|
||||
// Only emit if not a swallowed unknown sequence
|
||||
if ev.Key != KeyNone || ev.Type != EventKey {
|
||||
if ev.Type != EventKey {
|
||||
r.sendEvent(ev)
|
||||
} else if ev.Key != KeyNone {
|
||||
r.sendKey(0, ev)
|
||||
}
|
||||
i += consumed
|
||||
continue
|
||||
@@ -254,14 +297,14 @@ func (r *inputReader) parseInput(data []byte) int {
|
||||
|
||||
// Control characters
|
||||
if b < 0x20 {
|
||||
r.sendEvent(r.parseControl(b))
|
||||
r.sendKey(0, r.parseControl(b))
|
||||
i++
|
||||
continue
|
||||
}
|
||||
|
||||
// DEL
|
||||
if b == 0x7f {
|
||||
r.sendEvent(Event{Type: EventKey, Key: KeyBackspace})
|
||||
r.sendKey(0, Event{Type: EventKey, Key: KeyBackspace})
|
||||
i++
|
||||
continue
|
||||
}
|
||||
@@ -281,7 +324,7 @@ func (r *inputReader) parseInput(data []byte) int {
|
||||
}
|
||||
|
||||
rn, size := decodeRune(data[i:])
|
||||
r.sendEvent(Event{Type: EventKey, Key: KeyRune, Rune: rn})
|
||||
r.sendKey(0, Event{Type: EventKey, Key: KeyRune, Rune: rn})
|
||||
i += size
|
||||
continue
|
||||
}
|
||||
@@ -432,6 +475,25 @@ func (r *inputReader) parseCSI(data []byte) (int, Event) {
|
||||
return csiMaxLen, Event{Type: EventKey, Key: KeyNone}
|
||||
case data[2] == '<' && (data[n-1] == 'M' || data[n-1] == 'm'):
|
||||
return n, parseSGRMouse(data[3:n-1], data[n-1])
|
||||
case r.focus && n == 3 && (data[2] == 'I' || data[2] == 'O'):
|
||||
// Holds end as focus leaves, since their releases go elsewhere
|
||||
if data[2] == 'O' {
|
||||
r.holds.ReleaseAll(r.sendEvent)
|
||||
}
|
||||
return n, Event{Type: EventFocus, Focused: data[2] == 'I'}
|
||||
case data[2] == '?' && data[n-1] == 'u':
|
||||
// The protocol's flags in force, answering a query
|
||||
if v, ok := parseKeyParams(data[3 : n-1]); ok {
|
||||
r.protoReply(int(max(v[0][0], 0)))
|
||||
}
|
||||
return n, Event{Type: EventKey, Key: KeyNone}
|
||||
case data[2] == '?' && data[n-1] == 'c':
|
||||
// Device attributes, which close an exchange: the terminal has
|
||||
// answered all asked before them
|
||||
r.exchangeDone(true)
|
||||
return n, Event{Type: EventKey, Key: KeyNone}
|
||||
case r.decoding && r.protoKey(data[2:n-1], data[n-1]):
|
||||
return n, Event{Type: EventKey, Key: KeyNone}
|
||||
}
|
||||
|
||||
if key, mod, ok := lookupCSI(data[2:n]); ok {
|
||||
@@ -448,7 +510,13 @@ func (r *inputReader) parseSS3(data []byte) (int, Event) {
|
||||
return 0, Event{}
|
||||
}
|
||||
if key, mod, ok := lookupSS3(data[2:3]); ok {
|
||||
return 3, Event{Type: EventKey, Key: key, Modifiers: mod}
|
||||
ev := Event{Type: EventKey, Key: key, Modifiers: mod}
|
||||
if r.decoding && key != KeyRune {
|
||||
// A terminal reporting by the protocol names this key too
|
||||
r.sendKey(protoNamed+uint32(key), ev)
|
||||
return 3, Event{Type: EventKey, Key: KeyNone}
|
||||
}
|
||||
return 3, ev
|
||||
}
|
||||
// Unknown SS3 - consume to prevent garbage
|
||||
return 3, Event{Type: EventKey, Key: KeyNone}
|
||||
@@ -626,6 +694,26 @@ func parseSGRParams(data []byte) (btn, x, y int, ok bool) {
|
||||
return btn, x, y, true
|
||||
}
|
||||
|
||||
// sendKey sends a key event at the level the terminal reports. code names the
|
||||
// physical key the event came from, zero for one the terminal names none for:
|
||||
// at releases such a key can't be held, so its release follows at once.
|
||||
func (r *inputReader) sendKey(code uint32, ev Event) {
|
||||
switch {
|
||||
case r.report == KeyReportPresses:
|
||||
if ev.Action != KeyRelease {
|
||||
ev.Action = KeyPress
|
||||
r.sendEvent(ev)
|
||||
}
|
||||
case code == 0:
|
||||
ev.Action = KeyPress
|
||||
r.sendEvent(ev)
|
||||
ev.Action = KeyRelease
|
||||
r.sendEvent(ev)
|
||||
default:
|
||||
r.holds.Report(code, ev, r.sendEvent)
|
||||
}
|
||||
}
|
||||
|
||||
// sendEvent sends an event to the channel, non-blocking
|
||||
func (r *inputReader) sendEvent(ev Event) {
|
||||
select {
|
||||
|
||||
@@ -90,6 +90,31 @@ const (
|
||||
ModCtrl Modifier = 1 << 2
|
||||
)
|
||||
|
||||
// KeyAction is what happened to a key. Only a terminal reporting releases (see
|
||||
// SetKeyReport) tells them apart; elsewhere every key event is a press, and a
|
||||
// key held down arrives as more presses at the keyboard's repeat rate.
|
||||
type KeyAction uint8
|
||||
|
||||
const (
|
||||
KeyPress KeyAction = iota // the key went down
|
||||
KeyRepeat // the keyboard repeated a key held down
|
||||
KeyRelease // the key came up
|
||||
)
|
||||
|
||||
// String returns the action's name
|
||||
func (a KeyAction) String() string {
|
||||
switch a {
|
||||
case KeyPress:
|
||||
return "Press"
|
||||
case KeyRepeat:
|
||||
return "Repeat"
|
||||
case KeyRelease:
|
||||
return "Release"
|
||||
default:
|
||||
return "None"
|
||||
}
|
||||
}
|
||||
|
||||
// escapeSequence maps escape sequences to keys
|
||||
// Key: sequence after ESC [ (e.g., "A" for up arrow)
|
||||
type escapeSequence struct {
|
||||
|
||||
+407
@@ -0,0 +1,407 @@
|
||||
package terminal
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// The terminal's key event protocol (CSI u with progressive enhancement, as
|
||||
// Alacritty 0.13 and later implement it): asked for flags, the terminal
|
||||
// reports every key as an escape code with its event type, press, repeat or
|
||||
// release, and the text it types.
|
||||
const (
|
||||
protoDisambiguate = 1 << iota // Escape, Alt and Ctrl keys as escape codes
|
||||
protoEventTypes // repeats and releases
|
||||
protoAlternates // the shifted key beside the key
|
||||
protoAllKeys // text keys as escape codes too, so they release
|
||||
protoText // the text a key types, in its code
|
||||
|
||||
// protoFlags are the flags pushed, and protoNeed those a terminal must
|
||||
// confirm before the reader decodes by them: the text keeps every layout's
|
||||
// characters as the terminal types them
|
||||
protoFlags = protoDisambiguate | protoEventTypes | protoAlternates | protoAllKeys | protoText
|
||||
protoNeed = protoEventTypes | protoAllKeys | protoText
|
||||
)
|
||||
|
||||
var (
|
||||
csiKeyQuery = []byte("\x1b[?u") // the flags in force, answered CSI ? flags u
|
||||
csiKeyPush = []byte("\x1b[>" + strconv.Itoa(protoFlags) + "u") // over the flags in force
|
||||
csiKeyPop = []byte("\x1b[<u") // back to the flags before the push
|
||||
csiDA1 = []byte("\x1b[c") // primary device attributes, which every terminal answers
|
||||
csiFocusOn = []byte("\x1b[?1004h")
|
||||
csiFocusOff = []byte("\x1b[?1004l")
|
||||
)
|
||||
|
||||
// protoTimeout bounds an exchange with the terminal; one that answers nothing
|
||||
// by then has no protocol to use
|
||||
var protoTimeout = 2 * time.Second
|
||||
|
||||
// protoExchange is a request to the terminal awaiting its answer, which the
|
||||
// DA1 reply after it closes: terminals answer in order
|
||||
type protoExchange uint8
|
||||
|
||||
const (
|
||||
exchangeNone protoExchange = iota
|
||||
exchangeProbe // does the terminal have the protocol?
|
||||
exchangePush // pushed protoFlags: what did they come to?
|
||||
exchangePop // popped: reports go on until the terminal has it
|
||||
)
|
||||
|
||||
// keyAsk is what a SetKeyReport call asked for
|
||||
type keyAsk struct {
|
||||
report KeyReport
|
||||
allow KeySource
|
||||
}
|
||||
|
||||
// keySourcer is a backend that has sources of key releases. Only the
|
||||
// package's own are: a Terminal over a caller's backend reports presses.
|
||||
type keySourcer interface {
|
||||
keySources() KeySource
|
||||
}
|
||||
|
||||
// backendKeySources is what b offers beyond presses
|
||||
func backendKeySources(b Backend) KeySource {
|
||||
if s, ok := b.(keySourcer); ok {
|
||||
return s.keySources()
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// askKeys hands an ask to the reader; with no source, every ask comes to
|
||||
// presses at once
|
||||
func (r *inputReader) askKeys(report KeyReport, allow KeySource) {
|
||||
if r.sources == 0 {
|
||||
r.sendEvent(Event{Type: EventKeyReport, Report: KeyReportPresses})
|
||||
return
|
||||
}
|
||||
a := keyAsk{report, allow & r.sources}
|
||||
for {
|
||||
select {
|
||||
case r.asks <- a:
|
||||
return
|
||||
default:
|
||||
}
|
||||
select { // an ask the reader has not taken yet gives way to this one
|
||||
case <-r.asks:
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// serviceKeys takes the latest ask, gives up on an exchange the terminal has
|
||||
// not answered in time, and moves toward the ask; the reader calls it between
|
||||
// reads
|
||||
func (r *inputReader) serviceKeys() {
|
||||
select {
|
||||
case a := <-r.asks:
|
||||
r.want, r.answer = a, true
|
||||
default:
|
||||
}
|
||||
if r.exchange != exchangeNone && time.Now().After(r.deadline) {
|
||||
r.exchangeDone(false)
|
||||
}
|
||||
r.reconcile()
|
||||
}
|
||||
|
||||
// target is the source the ask wants of those to be had, the one in use while
|
||||
// it is still allowed
|
||||
func (r *inputReader) target() KeySource {
|
||||
a := r.want
|
||||
switch {
|
||||
case a.report == KeyReportPresses:
|
||||
return 0
|
||||
case r.source != 0 && a.allow&r.source != 0:
|
||||
return r.source
|
||||
case a.allow&KeySourceProtocol != 0 && r.protoHas >= 0:
|
||||
return KeySourceProtocol
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// reconcile moves toward the ask while no exchange is in flight, and answers
|
||||
// it once there
|
||||
func (r *inputReader) reconcile() {
|
||||
for r.answer && r.exchange == exchangeNone {
|
||||
switch target := r.target(); {
|
||||
case target == r.source:
|
||||
r.answer = false
|
||||
r.sendEvent(Event{Type: EventKeyReport, Report: r.report, Source: r.source})
|
||||
case r.source != 0:
|
||||
r.leave()
|
||||
case r.protoHas == 0:
|
||||
r.request(exchangeProbe, false, csiKeyQuery, csiDA1)
|
||||
default:
|
||||
r.decoding = true
|
||||
r.request(exchangePush, true, csiKeyPush, csiKeyQuery, csiDA1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// request writes an exchange's requests and waits for the answer; pushed is
|
||||
// whether the protocol's flags are pushed once they are written. A terminal
|
||||
// closing takes no requests, and the protocol is then none.
|
||||
func (r *inputReader) request(x protoExchange, pushed bool, seqs ...[]byte) {
|
||||
var seq []byte
|
||||
for _, s := range seqs {
|
||||
seq = append(seq, s...)
|
||||
}
|
||||
if r.write == nil || !r.write(seq, pushed) {
|
||||
r.protoHas, r.decoding = -1, false
|
||||
return
|
||||
}
|
||||
r.exchange, r.deadline, r.protoReported = x, time.Now().Add(protoTimeout), -1
|
||||
}
|
||||
|
||||
// protoReply takes the flags a terminal reports in force
|
||||
func (r *inputReader) protoReply(flags int) {
|
||||
if r.exchange != exchangeNone {
|
||||
r.protoReported = flags
|
||||
}
|
||||
}
|
||||
|
||||
// exchangeDone closes the exchange in flight: answered by the DA1 reply after
|
||||
// it, or not in time
|
||||
func (r *inputReader) exchangeDone(answered bool) {
|
||||
x := r.exchange
|
||||
if x == exchangeNone {
|
||||
return
|
||||
}
|
||||
r.exchange = exchangeNone
|
||||
switch x {
|
||||
case exchangeProbe:
|
||||
r.protoHas = -1
|
||||
if answered && r.protoReported >= 0 {
|
||||
r.protoHas = 1
|
||||
}
|
||||
case exchangePush:
|
||||
if answered && r.protoReported&protoNeed == protoNeed {
|
||||
// Keys held across the switch were reported as presses; their next
|
||||
// repeat starts their hold
|
||||
r.holds = KeyHolds{}
|
||||
r.source, r.report, r.focus = KeySourceProtocol, KeyReportReleases, true
|
||||
r.write(csiFocusOn, true)
|
||||
} else {
|
||||
r.protoHas = -1
|
||||
r.request(exchangePop, false, csiKeyPop, csiDA1)
|
||||
}
|
||||
case exchangePop:
|
||||
r.decoding = false
|
||||
}
|
||||
r.reconcile()
|
||||
}
|
||||
|
||||
// leave gives up the source in use, ending its holds first
|
||||
func (r *inputReader) leave() {
|
||||
r.holds.ReleaseAll(r.sendEvent)
|
||||
source := r.source
|
||||
r.source, r.report, r.focus = 0, KeyReportPresses, false
|
||||
if source == KeySourceProtocol {
|
||||
r.request(exchangePop, false, csiKeyPop, csiFocusOff, csiDA1)
|
||||
}
|
||||
}
|
||||
|
||||
// protoNamed sets the codes of keys the protocol reports in their legacy form
|
||||
// (arrows, Home, F1, ...) apart from Unicode code points
|
||||
const protoNamed = 0x110000
|
||||
|
||||
// tildeKeys are the keys reported as CSI number ; modifiers ~
|
||||
var tildeKeys = map[int32]Key{
|
||||
1: KeyHome, 2: KeyInsert, 3: KeyDelete, 4: KeyEnd, 5: KeyPageUp, 6: KeyPageDown, 7: KeyHome, 8: KeyEnd,
|
||||
11: KeyF1, 12: KeyF2, 13: KeyF3, 14: KeyF4, 15: KeyF5, 17: KeyF6, 18: KeyF7, 19: KeyF8,
|
||||
20: KeyF9, 21: KeyF10, 23: KeyF11, 24: KeyF12,
|
||||
}
|
||||
|
||||
// letterKeys are the keys reported as CSI 1 ; modifiers letter
|
||||
var letterKeys = map[byte]Key{
|
||||
'A': KeyUp, 'B': KeyDown, 'C': KeyRight, 'D': KeyLeft, 'H': KeyHome, 'F': KeyEnd,
|
||||
'P': KeyF1, 'Q': KeyF2, 'S': KeyF4, // F3 as CSI R would read as a cursor position report
|
||||
}
|
||||
|
||||
// Keypad keys by protocol code: those that type a character, and those that
|
||||
// move as their main-block twins with Num Lock off
|
||||
var (
|
||||
keypadRunes = map[int32]rune{
|
||||
57399: '0', 57400: '1', 57401: '2', 57402: '3', 57403: '4', 57404: '5', 57405: '6', 57406: '7',
|
||||
57407: '8', 57408: '9', 57409: '.', 57410: '/', 57411: '*', 57412: '-', 57413: '+', 57415: '=',
|
||||
57416: ',',
|
||||
}
|
||||
keypadKeys = map[int32]Key{
|
||||
57414: KeyEnter, 57417: KeyLeft, 57418: KeyRight, 57419: KeyUp, 57420: KeyDown, 57421: KeyPageUp,
|
||||
57422: KeyPageDown, 57423: KeyHome, 57424: KeyEnd, 57425: KeyInsert, 57426: KeyDelete,
|
||||
}
|
||||
)
|
||||
|
||||
// keyParams are a key report's parameters: up to three fields split by ';',
|
||||
// each of subfields split by ':', an absent one -1
|
||||
type keyParams [3][8]int32
|
||||
|
||||
// parseKeyParams reads a key report's parameters; ok is false for anything
|
||||
// else, a private marker or a value past Unicode among them
|
||||
func parseKeyParams(p []byte) (v keyParams, ok bool) {
|
||||
for i := range v {
|
||||
for j := range v[i] {
|
||||
v[i][j] = -1
|
||||
}
|
||||
}
|
||||
f, s := 0, 0
|
||||
for _, b := range p {
|
||||
switch {
|
||||
case b >= '0' && b <= '9':
|
||||
n := max(v[f][s], 0)*10 + int32(b-'0')
|
||||
if n > 0x10ffff {
|
||||
return v, false
|
||||
}
|
||||
v[f][s] = n
|
||||
case b == ':':
|
||||
if s++; s == len(v[f]) {
|
||||
return v, false
|
||||
}
|
||||
case b == ';':
|
||||
if f, s = f+1, 0; f == len(v) {
|
||||
return v, false
|
||||
}
|
||||
default:
|
||||
return v, false
|
||||
}
|
||||
}
|
||||
return v, true
|
||||
}
|
||||
|
||||
// protoKey decodes a key the protocol reports, CSI params final, and sends what
|
||||
// it means at the level in force. It is false for a sequence that is no key
|
||||
// report, which the legacy tables then read.
|
||||
//
|
||||
// Each press reads as the key a legacy terminal sends for it reads (Alacritty's
|
||||
// encoding is the reference), so a game sees the same keys with the protocol
|
||||
// on: Shift goes into the character, Ctrl with a character becomes its control
|
||||
// key, Alt stays a modifier, and Super and the locks are not reported. Keys a
|
||||
// legacy terminal sends nothing for (lone modifiers, media keys, F13 and up)
|
||||
// are dropped.
|
||||
func (r *inputReader) protoKey(params []byte, final byte) bool {
|
||||
v, ok := parseKeyParams(params)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
num, mods := v[0][0], max(v[1][0], 1)-1
|
||||
action := KeyPress
|
||||
switch v[1][1] {
|
||||
case 2:
|
||||
action = KeyRepeat
|
||||
case 3:
|
||||
action = KeyRelease
|
||||
}
|
||||
ev := Event{Type: EventKey, Action: action}
|
||||
var code uint32
|
||||
switch final {
|
||||
case 'u':
|
||||
if num < 0 {
|
||||
return false
|
||||
}
|
||||
var text []rune
|
||||
for _, c := range v[2] {
|
||||
if c >= 0 && printable(rune(c)) {
|
||||
text = append(text, rune(c))
|
||||
}
|
||||
}
|
||||
if !protoCodeKey(num, v[0][1], mods, text, &ev) {
|
||||
// A key with no legacy key, or text with no key: its text is taps
|
||||
for _, t := range text {
|
||||
r.sendKey(0, Event{Type: EventKey, Key: KeyRune, Rune: t})
|
||||
}
|
||||
return true
|
||||
}
|
||||
code = uint32(num)
|
||||
if ev.Key == KeyRune && len(text) > 1 {
|
||||
defer func() {
|
||||
for _, t := range text[1:] {
|
||||
r.sendKey(0, Event{Type: EventKey, Key: KeyRune, Rune: t})
|
||||
}
|
||||
}()
|
||||
}
|
||||
case '~':
|
||||
if ev.Key = tildeKeys[num]; ev.Key == KeyNone {
|
||||
return true // F13 and up, keypad Begin: no legacy key
|
||||
}
|
||||
ev.Modifiers = keyModifiers(mods)
|
||||
code = protoNamed + uint32(ev.Key)
|
||||
default:
|
||||
if num > 1 {
|
||||
return false
|
||||
}
|
||||
if ev.Key = letterKeys[final]; ev.Key == KeyNone {
|
||||
return final == 'E' // keypad Begin
|
||||
}
|
||||
ev.Modifiers = keyModifiers(mods)
|
||||
code = protoNamed + uint32(ev.Key)
|
||||
}
|
||||
r.sendKey(code, ev)
|
||||
return true
|
||||
}
|
||||
|
||||
// protoCodeKey reads the key a CSI u report's code names into ev, as a legacy
|
||||
// terminal reports it: false for a code no legacy key has
|
||||
func protoCodeKey(num, shifted, mods int32, text []rune, ev *Event) bool {
|
||||
alt := Modifier(0)
|
||||
if mods&2 != 0 {
|
||||
alt = ModAlt
|
||||
}
|
||||
shift, ctrl := mods&1 != 0, mods&4 != 0
|
||||
switch {
|
||||
case num == 27:
|
||||
ev.Key, ev.Modifiers = KeyEscape, alt
|
||||
case num == 13:
|
||||
ev.Key, ev.Modifiers = KeyEnter, alt
|
||||
case num == 9 && shift:
|
||||
ev.Key, ev.Modifiers = KeyBacktab, ModShift|alt
|
||||
case num == 9:
|
||||
ev.Key, ev.Modifiers = KeyTab, alt
|
||||
case num == 127, num == 8:
|
||||
ev.Key, ev.Modifiers = KeyBackspace, alt
|
||||
case keypadKeys[num] != KeyNone:
|
||||
ev.Key = keypadKeys[num]
|
||||
if ev.Key == KeyEnter {
|
||||
ev.Modifiers = alt
|
||||
} else {
|
||||
ev.Modifiers = keyModifiers(mods)
|
||||
}
|
||||
default:
|
||||
ch, ok := keypadRunes[num]
|
||||
if !ok {
|
||||
ch = rune(num)
|
||||
}
|
||||
if !ok && (num >= 0xe000 && num <= 0xf8ff || !printable(ch)) {
|
||||
return false // the protocol's own keys, and codes no key types
|
||||
}
|
||||
if shift && shifted > 0 {
|
||||
ch = rune(shifted)
|
||||
}
|
||||
if ctrl {
|
||||
if k, ok := ControlKey(ch); ok {
|
||||
ev.Key, ev.Modifiers = k, alt
|
||||
return true
|
||||
}
|
||||
if k, ok := ControlKey(rune(num)); ok {
|
||||
ev.Key, ev.Modifiers = k, alt
|
||||
return true
|
||||
}
|
||||
}
|
||||
if len(text) > 0 {
|
||||
ch = text[0]
|
||||
}
|
||||
ev.Key, ev.Rune, ev.Modifiers = KeyRune, ch, alt
|
||||
if ctrl {
|
||||
ev.Modifiers |= ModCtrl
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// keyModifiers are the protocol's modifier bits a legacy key reports
|
||||
func keyModifiers(mods int32) Modifier {
|
||||
return Modifier(mods & int32(ModShift|ModAlt|ModCtrl))
|
||||
}
|
||||
|
||||
// printable is whether r types something: no control character, no surrogate
|
||||
func printable(r rune) bool {
|
||||
return r >= 0x20 && (r < 0x7f || r > 0x9f) && (r < 0xd800 || r > 0xdfff) && r <= 0x10ffff
|
||||
}
|
||||
@@ -0,0 +1,360 @@
|
||||
package terminal
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// decoded is what a reader decoding the key protocol sends for input at the
|
||||
// report level given
|
||||
func decoded(report KeyReport, in string) []Event {
|
||||
r := &inputReader{eventCh: make(chan Event, 256), decoding: true, report: report}
|
||||
r.parseInput([]byte(in))
|
||||
close(r.eventCh)
|
||||
var events []Event
|
||||
for ev := range r.eventCh {
|
||||
events = append(events, ev)
|
||||
}
|
||||
return events
|
||||
}
|
||||
|
||||
// Every press the protocol reports reads as the key a legacy terminal sends for
|
||||
// it reads, Alacritty's encoding of each the reference, so a game sees the
|
||||
// same keys with the protocol on
|
||||
func TestProtocolPressesReadAsLegacyKeys(t *testing.T) {
|
||||
for _, c := range []struct{ name, legacy, report string }{
|
||||
{"a", "a", "\x1b[97;1;97u"},
|
||||
{"shift+a", "A", "\x1b[97:65;2;65u"},
|
||||
{"ctrl+a", "\x01", "\x1b[97;5u"},
|
||||
{"alt+a", "\x1ba", "\x1b[97;3;97u"},
|
||||
{"ctrl+alt+a", "\x1b\x01", "\x1b[97;7u"},
|
||||
{"ctrl+shift+a", "\x01", "\x1b[97:65;6u"},
|
||||
{"alt+shift+a", "\x1bA", "\x1b[97:65;4;65u"},
|
||||
{"super+a", "a", "\x1b[97;9;97u"},
|
||||
{"caps lock a", "A", "\x1b[97;65;65u"},
|
||||
{"shift+1", "!", "\x1b[49:33;2;33u"},
|
||||
{"é", "é", "\x1b[233;1;233u"},
|
||||
{"shift+é", "É", "\x1b[233:201;2;201u"},
|
||||
{"altgr+q", "@", "\x1b[64;1;64u"},
|
||||
{"ctrl+[", "\x1b", "\x1b[91;5u"},
|
||||
{"ctrl+h", "\x08", "\x1b[104;5u"},
|
||||
{"ctrl+shift+2", "\x00", "\x1b[50:64;6u"},
|
||||
{"ctrl+/", "\x1f", "\x1b[47;5u"},
|
||||
{"space", " ", "\x1b[32;1;32u"},
|
||||
{"ctrl+space", "\x00", "\x1b[32;5u"},
|
||||
{"alt+space", "\x1b ", "\x1b[32;3;32u"},
|
||||
{"enter", "\r", "\x1b[13u"},
|
||||
{"shift+enter", "\r", "\x1b[13;2u"},
|
||||
{"alt+enter", "\x1b\r", "\x1b[13;3u"},
|
||||
{"tab", "\t", "\x1b[9u"},
|
||||
{"ctrl+tab", "\t", "\x1b[9;5u"},
|
||||
{"shift+tab", "\x1b[Z", "\x1b[9;2u"},
|
||||
{"alt+shift+tab", "\x1b\x1b[Z", "\x1b[9;4u"},
|
||||
{"backspace", "\x7f", "\x1b[127u"},
|
||||
{"ctrl+backspace", "\x08", "\x1b[127;5u"},
|
||||
{"alt+backspace", "\x1b\x7f", "\x1b[127;3u"},
|
||||
{"escape", "\x1b", "\x1b[27u"},
|
||||
{"shift+escape", "\x1b", "\x1b[27;2u"},
|
||||
{"alt+escape", "\x1b\x1b", "\x1b[27;3u"},
|
||||
{"up", "\x1b[A", "\x1b[A"},
|
||||
{"shift+up", "\x1b[1;2A", "\x1b[1;2A"},
|
||||
{"ctrl+alt+left", "\x1b[1;7D", "\x1b[1;7D"},
|
||||
{"home", "\x1b[H", "\x1b[H"},
|
||||
{"end", "\x1b[F", "\x1b[F"},
|
||||
{"f1", "\x1bOP", "\x1b[P"},
|
||||
{"ctrl+f1", "\x1b[1;5P", "\x1b[1;5P"},
|
||||
{"f3", "\x1bOR", "\x1b[13~"},
|
||||
{"f4", "\x1bOS", "\x1b[S"},
|
||||
{"f5", "\x1b[15~", "\x1b[15~"},
|
||||
{"shift+f12", "\x1b[24;2~", "\x1b[24;2~"},
|
||||
{"insert", "\x1b[2~", "\x1b[2~"},
|
||||
{"delete", "\x1b[3~", "\x1b[3~"},
|
||||
{"ctrl+delete", "\x1b[3;5~", "\x1b[3;5~"},
|
||||
{"page up", "\x1b[5~", "\x1b[5~"},
|
||||
{"page down", "\x1b[6~", "\x1b[6~"},
|
||||
{"keypad 1", "1", "\x1b[57400;1;49u"},
|
||||
{"keypad enter", "\r", "\x1b[57414u"},
|
||||
{"keypad left", "\x1b[D", "\x1b[57417u"},
|
||||
{"keypad shift+home", "\x1b[1;2H", "\x1b[57423;2u"},
|
||||
{"left shift alone", "", "\x1b[57441;2u"},
|
||||
{"caps lock alone", "", "\x1b[57358u"},
|
||||
{"f13", "", "\x1b[57376u"},
|
||||
{"volume", "", "\x1b[57439u"},
|
||||
{"keypad begin", "", "\x1b[E\x1b[57427~"},
|
||||
{"compose ü", "ü", "\x1b[0;;252u"},
|
||||
} {
|
||||
// An ESC or two alone read once input pauses
|
||||
want, _ := parsed(c.legacy)
|
||||
if ev, ok := loneEscape[c.legacy]; ok {
|
||||
want = []Event{ev}
|
||||
}
|
||||
if got := decoded(KeyReportPresses, c.report); !slices.Equal(got, want) {
|
||||
t.Errorf("%s: %q reads as %+v, want %+v as %q reads", c.name, c.report, got, want, c.legacy)
|
||||
}
|
||||
}
|
||||
|
||||
// Ctrl with a character no control key has keeps Ctrl, which a legacy
|
||||
// terminal can't send
|
||||
if got, want := decoded(KeyReportPresses, "\x1b[49;5;49u"), (Event{Type: EventKey, Key: KeyRune, Rune: '1', Modifiers: ModCtrl}); len(got) != 1 || got[0] != want {
|
||||
t.Errorf("ctrl+1 reads as %+v, want %+v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
// At releases each key reports its press, repeats and release, paired by the
|
||||
// key: a release carries its press's key whatever was let go first; modifier
|
||||
// keys alone report nothing; text with no key behind it is a tap
|
||||
func TestProtocolReportsHolds(t *testing.T) {
|
||||
ev := func(k Key, r rune, m Modifier, a KeyAction) Event {
|
||||
return Event{Type: EventKey, Key: k, Rune: r, Modifiers: m, Action: a}
|
||||
}
|
||||
got := decoded(KeyReportReleases, strings.Join([]string{
|
||||
"\x1b[57441;2u", "\x1b[97:65;2;65u", // Shift down, a down: 'A'
|
||||
"\x1b[57441;1:3u", "\x1b[97;1:2;97u", // Shift up, a repeats as 'a'
|
||||
"\x1b[97;1:3u", // a up: 'A' comes up
|
||||
"\x1b[A", "\x1b[1;1:2A", "\x1b[1;1:3A", // Up down, repeat, up
|
||||
"\x1b[13u", "\x1b[13;1:3u", // Enter
|
||||
"\x1b[0;;252u", // composed text
|
||||
"\x1b[98;1:3u", // a release with no press
|
||||
"\x1b[3;5~", "\x1b[3;1:3~", // Ctrl+Delete down, Delete up after Ctrl
|
||||
}, ""))
|
||||
want := []Event{
|
||||
ev(KeyRune, 'A', 0, KeyPress), ev(KeyRune, 'a', 0, KeyRepeat), ev(KeyRune, 'A', 0, KeyRelease),
|
||||
ev(KeyUp, 0, 0, KeyPress), ev(KeyUp, 0, 0, KeyRepeat), ev(KeyUp, 0, 0, KeyRelease),
|
||||
ev(KeyEnter, 0, 0, KeyPress), ev(KeyEnter, 0, 0, KeyRelease),
|
||||
ev(KeyRune, 'ü', 0, KeyPress), ev(KeyRune, 'ü', 0, KeyRelease),
|
||||
ev(KeyDelete, 0, ModCtrl, KeyPress), ev(KeyDelete, 0, ModCtrl, KeyRelease),
|
||||
}
|
||||
if !slices.Equal(got, want) {
|
||||
t.Fatalf("got %+v\nwant %+v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
// fakeKeyTerm is a terminal answering the key protocol's requests as Alacritty
|
||||
// does: CSI ? u with the flags in force, which a push and a pop change, and
|
||||
// DA1. Without the protocol it answers DA1 alone; silent, nothing. grant is
|
||||
// the flags it takes of a push, all when zero.
|
||||
type fakeKeyTerm struct {
|
||||
fakeBackend
|
||||
has, silent bool
|
||||
grant int
|
||||
stack []int
|
||||
}
|
||||
|
||||
var keyRequest = regexp.MustCompile(`\x1b\[(\?u|>(\d*)u|<u|c)`)
|
||||
|
||||
func (f *fakeKeyTerm) keySources() KeySource { return KeySourceProtocol }
|
||||
|
||||
func (f *fakeKeyTerm) Write(p []byte) error {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
f.out.Write(p)
|
||||
for _, m := range keyRequest.FindAllSubmatch(p, -1) {
|
||||
switch req := string(m[1]); {
|
||||
case f.silent:
|
||||
case req == "c":
|
||||
f.chunks = append(f.chunks, "\x1b[?62;22c")
|
||||
case !f.has:
|
||||
case req == "?u":
|
||||
flags := 0
|
||||
if len(f.stack) > 0 {
|
||||
flags = f.stack[len(f.stack)-1]
|
||||
}
|
||||
f.chunks = append(f.chunks, "\x1b[?"+strconv.Itoa(flags)+"u")
|
||||
case req == "<u":
|
||||
if len(f.stack) > 0 {
|
||||
f.stack = f.stack[:len(f.stack)-1]
|
||||
}
|
||||
default:
|
||||
flags, _ := strconv.Atoi(string(m[2]))
|
||||
if f.grant != 0 {
|
||||
flags &= f.grant
|
||||
}
|
||||
f.stack = append(f.stack, flags)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// typed hands the reader input as though the terminal sent it
|
||||
func (f *fakeKeyTerm) typed(in string) {
|
||||
f.mu.Lock()
|
||||
f.chunks = append(f.chunks, in)
|
||||
f.mu.Unlock()
|
||||
}
|
||||
|
||||
// pushed is how many pushes the terminal holds
|
||||
func (f *fakeKeyTerm) pushed() int {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
return len(f.stack)
|
||||
}
|
||||
|
||||
// poll is the terminal's next event, failing after two seconds
|
||||
func poll(t *testing.T, term Terminal) Event {
|
||||
t.Helper()
|
||||
got := make(chan Event, 1)
|
||||
go func() { got <- term.PollEvent() }()
|
||||
select {
|
||||
case ev := <-got:
|
||||
return ev
|
||||
case <-time.After(3 * time.Second):
|
||||
t.Fatal("no event")
|
||||
return Event{}
|
||||
}
|
||||
}
|
||||
|
||||
// A terminal with the protocol is asked about it, pushed the flags and asked
|
||||
// what they came to before releases are reported; focus is reported with them,
|
||||
// and they end, Lost, as focus leaves. Going back to presses releases what is
|
||||
// held, pops the flags and stops focus reports, and Fini pops them too.
|
||||
func TestProtocolSourceComesAndGoes(t *testing.T) {
|
||||
f := &fakeKeyTerm{has: true}
|
||||
term := newTerm(f, ColorMode256)
|
||||
if err := term.Init(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
term.SetKeyReport(KeyReportReleases, KeySourceConsole|KeySourceProtocol)
|
||||
if ev := poll(t, term); ev != (Event{Type: EventKeyReport, Report: KeyReportReleases, Source: KeySourceProtocol}) {
|
||||
t.Fatalf("asked for releases: %+v", ev)
|
||||
}
|
||||
waitFor(t, "focus reports", func() bool { return strings.Contains(f.written(), "\x1b[?1004h") })
|
||||
out := f.written()
|
||||
if i := strings.Index(out, "\x1b[?u\x1b[c\x1b[>31u\x1b[?u\x1b[c\x1b[?1004h"); i < 0 {
|
||||
t.Fatalf("requests %q", out)
|
||||
}
|
||||
|
||||
w := Event{Type: EventKey, Key: KeyRune, Rune: 'w'}
|
||||
f.typed("\x1b[119;1;119u\x1b[O\x1b[I\x1b[119;1;119u")
|
||||
lost := w
|
||||
lost.Action, lost.Lost = KeyRelease, true
|
||||
for _, want := range []Event{w, lost, {Type: EventFocus}, {Type: EventFocus, Focused: true}, w} {
|
||||
if ev := poll(t, term); ev != want {
|
||||
t.Fatalf("got %+v, want %+v", ev, want)
|
||||
}
|
||||
}
|
||||
|
||||
term.SetKeyReport(KeyReportPresses, 0)
|
||||
for _, want := range []Event{lost, {Type: EventKeyReport, Report: KeyReportPresses}} {
|
||||
if ev := poll(t, term); ev != want {
|
||||
t.Fatalf("back to presses: %+v, want %+v", ev, want)
|
||||
}
|
||||
}
|
||||
waitFor(t, "the pop", func() bool { return strings.Contains(f.written(), "\x1b[<u\x1b[?1004l\x1b[c") })
|
||||
if n := f.pushed(); n != 0 {
|
||||
t.Fatalf("%d pushes left", n)
|
||||
}
|
||||
|
||||
// Asked again, the protocol is pushed without asking whether the terminal
|
||||
// has it, and Fini pops it before the screen it was pushed on goes
|
||||
term.SetKeyReport(KeyReportReleases, KeySourceProtocol)
|
||||
if ev := poll(t, term); ev.Source != KeySourceProtocol {
|
||||
t.Fatalf("asked again: %+v", ev)
|
||||
}
|
||||
term.Fini()
|
||||
out = f.written()
|
||||
if n := strings.Count(out, "\x1b[?u"); n != 3 {
|
||||
t.Fatalf("asked %d times, want a probe and two pushes: %q", n, out)
|
||||
}
|
||||
if i := strings.LastIndex(out, "\x1b[<u\x1b[?1004l"); i < 0 || i > strings.LastIndex(out, "\x1b[?1049l") {
|
||||
t.Fatalf("Fini left the flags pushed: %q", out)
|
||||
}
|
||||
}
|
||||
|
||||
// A terminal without the protocol, one that takes the push without the flags
|
||||
// releases need, and one that answers nothing all come to presses; the first
|
||||
// two are asked once only
|
||||
func TestProtocolMissingIsPresses(t *testing.T) {
|
||||
defer func(d time.Duration) { protoTimeout = d }(protoTimeout)
|
||||
protoTimeout = 100 * time.Millisecond
|
||||
for _, c := range []struct {
|
||||
name string
|
||||
term *fakeKeyTerm
|
||||
probes int
|
||||
}{
|
||||
{"legacy", &fakeKeyTerm{}, 1},
|
||||
{"partial", &fakeKeyTerm{has: true, grant: protoDisambiguate | protoEventTypes}, 2},
|
||||
{"silent", &fakeKeyTerm{silent: true}, 1},
|
||||
} {
|
||||
term := newTerm(c.term, ColorMode256)
|
||||
if err := term.Init(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for range 2 {
|
||||
term.SetKeyReport(KeyReportReleases, KeySourceProtocol)
|
||||
if ev := poll(t, term); ev != (Event{Type: EventKeyReport, Report: KeyReportPresses}) {
|
||||
t.Fatalf("%s: %+v", c.name, ev)
|
||||
}
|
||||
}
|
||||
term.Fini()
|
||||
out := c.term.written()
|
||||
if n := strings.Count(out, "\x1b[?u"); n != c.probes {
|
||||
t.Errorf("%s: asked %d times, want %d: %q", c.name, n, c.probes, out)
|
||||
}
|
||||
if strings.Count(out, "\x1b[>") != strings.Count(out, "\x1b[<u") || c.term.pushed() != 0 {
|
||||
t.Errorf("%s: flags left pushed: %q", c.name, out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// decodedIn is what a reader decoding the protocol at releases sends for input
|
||||
// arriving in chunks, and how many bytes it holds back
|
||||
func decodedIn(chunks ...string) (events []Event, held int) {
|
||||
r := &inputReader{eventCh: make(chan Event, 4096), decoding: true, report: KeyReportReleases}
|
||||
r.pasteMode.Store(true)
|
||||
var buf []byte
|
||||
for _, c := range chunks {
|
||||
buf = append(buf, c...)
|
||||
buf = buf[r.parseInput(buf):]
|
||||
}
|
||||
close(r.eventCh)
|
||||
for ev := range r.eventCh {
|
||||
events = append(events, ev)
|
||||
}
|
||||
return events, len(buf)
|
||||
}
|
||||
|
||||
// Whatever a terminal reporting by the protocol sends, however reads split it,
|
||||
// the reader sends the same events, holds back no more than a sequence, and
|
||||
// never sends a release before a press of the key it carries
|
||||
func FuzzProtocolNeverStalls(f *testing.F) {
|
||||
for _, s := range []string{"\x1b[97:65;2;65u\x1b[97;1:3u", "\x1b[A\x1b[1;1:3A", "\x1b[0;;252:253u", "\x1b[57441;2u\x1b[3;5:2~", "\x1b[O\x1b[13;1:3u", "\x1b[99;5:3u"} {
|
||||
f.Add(s, uint(len(s)/2))
|
||||
}
|
||||
f.Fuzz(func(t *testing.T, in string, cut uint) {
|
||||
cut %= uint(len(in)) + 1
|
||||
whole, held := decodedIn(in)
|
||||
if split, _ := decodedIn(in[:cut], in[cut:]); !slices.Equal(whole, split) {
|
||||
t.Fatalf("%q: whole %+v, split at %d %+v", in, whole, cut, split)
|
||||
}
|
||||
if held > csiMaxLen+1 {
|
||||
t.Fatalf("%q: holds %d bytes", in, held)
|
||||
}
|
||||
if _, held := decodedIn(in, "\x7f\x7f\x7f\x7f\x1b[201~"); held != 0 {
|
||||
t.Fatalf("%q: stalled holding %d bytes", in, held)
|
||||
}
|
||||
type id struct {
|
||||
k Key
|
||||
r rune
|
||||
m Modifier
|
||||
}
|
||||
down := map[id]int{}
|
||||
for _, ev := range whole {
|
||||
if ev.Type != EventKey {
|
||||
continue
|
||||
}
|
||||
k := id{ev.Key, ev.Rune, ev.Modifiers}
|
||||
switch ev.Action {
|
||||
case KeyPress:
|
||||
down[k]++
|
||||
case KeyRelease:
|
||||
if down[k]--; down[k] < 0 {
|
||||
t.Fatalf("%q: %+v released before its press", in, ev)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
package terminal
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// KeyReport is how much a terminal reports of each key
|
||||
type KeyReport uint8
|
||||
|
||||
const (
|
||||
KeyReportPresses KeyReport = iota // presses; a key held down repeats as more presses
|
||||
KeyReportReleases // presses, repeats and releases, each with its KeyAction
|
||||
)
|
||||
|
||||
// String returns the report's name
|
||||
func (r KeyReport) String() string {
|
||||
if r == KeyReportReleases {
|
||||
return "Releases"
|
||||
}
|
||||
return "Presses"
|
||||
}
|
||||
|
||||
// KeySource is where key releases come from (bitmask). SetKeyReport tries the
|
||||
// sources a game allows in the order below; a terminal with none of them
|
||||
// reports presses, as every terminal does until a game asks.
|
||||
type KeySource uint8
|
||||
|
||||
const (
|
||||
// KeySourceConsole is the kernel console's own keyboard, switched to raw
|
||||
// keycodes while releases are asked for: the Linux console on the VT the
|
||||
// game runs on, and FreeBSD's vt(4). Never over ssh, where the keyboard is
|
||||
// the far terminal's.
|
||||
KeySourceConsole KeySource = 1 << iota
|
||||
|
||||
// KeySourceProtocol is the terminal's key event protocol (CSI u with event
|
||||
// types), found by asking the terminal whether it has one: Alacritty 0.13
|
||||
// and later, and any terminal that answers the same. Over ssh too.
|
||||
KeySourceProtocol
|
||||
|
||||
// KeySourceHost is a host that reads its platform's own key events, such as
|
||||
// a window or a browser, and builds Events itself; no Terminal reports it.
|
||||
KeySourceHost
|
||||
)
|
||||
|
||||
// String returns the sources' names, joined by |
|
||||
func (s KeySource) String() string {
|
||||
var names []string
|
||||
for _, n := range []struct {
|
||||
s KeySource
|
||||
name string
|
||||
}{{KeySourceConsole, "Console"}, {KeySourceProtocol, "Protocol"}, {KeySourceHost, "Host"}} {
|
||||
if s&n.s != 0 {
|
||||
names = append(names, n.name)
|
||||
}
|
||||
}
|
||||
if len(names) == 0 {
|
||||
return "None"
|
||||
}
|
||||
return strings.Join(names, "|")
|
||||
}
|
||||
|
||||
// maxHolds bounds the keys KeyHolds keeps down at once, past any keyboard's
|
||||
// rollover, so a source that never releases can't grow it without end
|
||||
const maxHolds = 32
|
||||
|
||||
// KeyHolds pairs key releases with their presses. A source names each physical
|
||||
// key by a code of its own (a console keycode, a protocol key number, a host's
|
||||
// keycode) and reports presses, repeats and releases of it; KeyHolds hands each
|
||||
// on as an Event, a release carrying what its press carried, so letting go of
|
||||
// Shift first still releases 'A', not 'a'. The zero value is ready to use. It
|
||||
// is not safe for concurrent use.
|
||||
type KeyHolds struct {
|
||||
held []heldKey // oldest press first
|
||||
}
|
||||
|
||||
type heldKey struct {
|
||||
code uint32
|
||||
press Event
|
||||
}
|
||||
|
||||
// Held reports whether the key code names is down
|
||||
func (h *KeyHolds) Held(code uint32) bool {
|
||||
return h.find(code) >= 0
|
||||
}
|
||||
|
||||
func (h *KeyHolds) find(code uint32) int {
|
||||
return slices.IndexFunc(h.held, func(k heldKey) bool { return k.code == code })
|
||||
}
|
||||
|
||||
// Report hands on one report of the key code names through send: ev is the key
|
||||
// as the source reads it, its Action set. A press starts a hold and a release
|
||||
// ends it, carrying the press's Key, Rune and Modifiers; a repeat passes as it
|
||||
// is. A report the holds contradict means the source lost one, as a terminal's
|
||||
// own mode swallows keys: a press of a key held first ends that hold with a
|
||||
// Lost release, a repeat of a key not held is a press, and a release of a key
|
||||
// not held is dropped. A source that repeats a held key as more presses, as the
|
||||
// consoles do, asks Held to tell its repeats apart.
|
||||
func (h *KeyHolds) Report(code uint32, ev Event, send func(Event)) {
|
||||
i := h.find(code)
|
||||
switch {
|
||||
case ev.Action == KeyRelease:
|
||||
if i >= 0 {
|
||||
send(h.end(i, false))
|
||||
}
|
||||
return
|
||||
case ev.Action == KeyRepeat && i >= 0:
|
||||
send(ev)
|
||||
return
|
||||
case i >= 0:
|
||||
send(h.end(i, true))
|
||||
case len(h.held) == maxHolds:
|
||||
send(h.end(0, true))
|
||||
}
|
||||
ev.Action = KeyPress
|
||||
h.held = append(h.held, heldKey{code, ev})
|
||||
send(ev)
|
||||
}
|
||||
|
||||
// ReleaseAll ends every hold, sending a Lost release for each, oldest first:
|
||||
// focus left, the console switched away, or releases stopped
|
||||
func (h *KeyHolds) ReleaseAll(send func(Event)) {
|
||||
for len(h.held) > 0 {
|
||||
send(h.end(0, true))
|
||||
}
|
||||
}
|
||||
|
||||
// end removes hold i and returns its release
|
||||
func (h *KeyHolds) end(i int, lost bool) Event {
|
||||
ev := h.held[i].press
|
||||
h.held = slices.Delete(h.held, i, i+1)
|
||||
ev.Action, ev.Lost = KeyRelease, lost
|
||||
return ev
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
package terminal
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// A release carries what its press carried, whatever the source read when the
|
||||
// key came up; a report the holds contradict is taken as one the source lost
|
||||
func TestKeyHoldsPairReleasesWithPresses(t *testing.T) {
|
||||
press := func(r rune) Event { return Event{Type: EventKey, Key: KeyRune, Rune: r} }
|
||||
as := func(ev Event, a KeyAction, lost bool) Event { ev.Action, ev.Lost = a, lost; return ev }
|
||||
var h KeyHolds
|
||||
var got []Event
|
||||
send := func(ev Event) { got = append(got, ev) }
|
||||
report := func(code uint32, ev Event, a KeyAction) { h.Report(code, as(ev, a, false), send) }
|
||||
|
||||
report(30, press('A'), KeyPress) // Shift+a
|
||||
report(30, press('A'), KeyRepeat) // held
|
||||
report(30, press('a'), KeyRelease) // Shift came up first
|
||||
report(31, press('s'), KeyRelease) // never pressed
|
||||
report(32, press('d'), KeyRepeat) // its press was lost
|
||||
report(32, press('d'), KeyPress) // and so was its release
|
||||
report(33, press('f'), KeyPress)
|
||||
h.ReleaseAll(send) // focus left
|
||||
want := []Event{
|
||||
press('A'), as(press('A'), KeyRepeat, false), as(press('A'), KeyRelease, false),
|
||||
press('d'), as(press('d'), KeyRelease, true), press('d'),
|
||||
press('f'), as(press('d'), KeyRelease, true), as(press('f'), KeyRelease, true),
|
||||
}
|
||||
if !slices.Equal(got, want) {
|
||||
t.Fatalf("got %+v\nwant %+v", got, want)
|
||||
}
|
||||
if h.Held(32) || h.Held(33) || len(h.held) != 0 {
|
||||
t.Fatalf("holds left after ReleaseAll: %+v", h.held)
|
||||
}
|
||||
}
|
||||
|
||||
// A source that never releases can't hold more keys down than maxHolds: the
|
||||
// oldest hold ends to make room
|
||||
func TestKeyHoldsStayBounded(t *testing.T) {
|
||||
var h KeyHolds
|
||||
var lost []rune
|
||||
for code := range uint32(maxHolds + 2) {
|
||||
h.Report(code, Event{Type: EventKey, Key: KeyRune, Rune: 'a' + rune(code)}, func(ev Event) {
|
||||
if ev.Action == KeyRelease && ev.Lost {
|
||||
lost = append(lost, ev.Rune)
|
||||
}
|
||||
})
|
||||
}
|
||||
if len(h.held) != maxHolds || !slices.Equal(lost, []rune{'a', 'b'}) || !h.Held(maxHolds+1) {
|
||||
t.Fatalf("%d held, lost %q", len(h.held), lost)
|
||||
}
|
||||
}
|
||||
|
||||
// Focus reports are read only while asked for; as focus leaves, the keys held
|
||||
// are released first, marked Lost, since their own releases go elsewhere
|
||||
func TestFocusEndsHolds(t *testing.T) {
|
||||
r := &inputReader{eventCh: make(chan Event, 16)}
|
||||
if r.parseInput([]byte("\x1b[O\x1b[Ix")); len(r.eventCh) != 1 || (<-r.eventCh) != key('x') {
|
||||
t.Fatal("focus reports read while not asked for")
|
||||
}
|
||||
|
||||
r.focus = true
|
||||
w := Event{Type: EventKey, Key: KeyRune, Rune: 'w'}
|
||||
r.holds.Report(17, w, func(Event) {})
|
||||
r.parseInput([]byte("\x1b[O\x1b[I"))
|
||||
close(r.eventCh)
|
||||
var got []Event
|
||||
for ev := range r.eventCh {
|
||||
got = append(got, ev)
|
||||
}
|
||||
w.Action, w.Lost = KeyRelease, true
|
||||
if want := []Event{w, {Type: EventFocus}, {Type: EventFocus, Focused: true}}; !slices.Equal(got, want) {
|
||||
t.Fatalf("got %+v, want %+v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
// A terminal with no source of releases answers every ask with presses and
|
||||
// sends the terminal nothing for it
|
||||
func TestKeyReportWithoutSourceIsPresses(t *testing.T) {
|
||||
f := &fakeBackend{}
|
||||
term := newTerm(f, ColorMode256)
|
||||
if err := term.Init(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer term.Fini()
|
||||
waitFor(t, "Init's output", func() bool { return f.written() != "" })
|
||||
before := f.written()
|
||||
|
||||
if err := term.SetKeyReport(KeyReportReleases+1, KeySourceProtocol); err == nil {
|
||||
t.Fatal("an unknown report was taken")
|
||||
}
|
||||
for _, report := range []KeyReport{KeyReportReleases, KeyReportPresses} {
|
||||
if err := term.SetKeyReport(report, KeySourceConsole|KeySourceProtocol); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got := make(chan Event, 1)
|
||||
go func() { got <- term.PollEvent() }()
|
||||
select {
|
||||
case ev := <-got:
|
||||
if ev != (Event{Type: EventKeyReport, Report: KeyReportPresses}) {
|
||||
t.Fatalf("asked for %v: %+v", report, ev)
|
||||
}
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatalf("asked for %v: no answer", report)
|
||||
}
|
||||
}
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
if got := f.written(); got != before {
|
||||
t.Fatalf("asks wrote %q", got[len(before):])
|
||||
}
|
||||
}
|
||||
|
||||
// At presses a repeat is one more press and a release is dropped, as on any
|
||||
// terminal; at releases a key the terminal names no physical key for is a tap,
|
||||
// its release at once, and a named one is held until its own release
|
||||
func TestKeysAtEachLevel(t *testing.T) {
|
||||
as := func(ev Event, a KeyAction) Event { ev.Action = a; return ev }
|
||||
for _, c := range []struct {
|
||||
report KeyReport
|
||||
want []Event
|
||||
}{
|
||||
{KeyReportPresses, []Event{key('x'), key('w'), key('w')}},
|
||||
{KeyReportReleases, []Event{key('x'), as(key('x'), KeyRelease), key('w'), as(key('w'), KeyRepeat), as(key('w'), KeyRelease)}},
|
||||
} {
|
||||
r := &inputReader{eventCh: make(chan Event, 16), report: c.report}
|
||||
r.sendKey(0, key('x'))
|
||||
r.sendKey(17, key('w'))
|
||||
r.sendKey(17, as(key('w'), KeyRepeat))
|
||||
r.sendKey(17, as(key('w'), KeyRelease))
|
||||
close(r.eventCh)
|
||||
var got []Event
|
||||
for ev := range r.eventCh {
|
||||
got = append(got, ev)
|
||||
}
|
||||
if !slices.Equal(got, c.want) {
|
||||
t.Errorf("%v: got %+v, want %+v", c.report, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
+67
-1
@@ -1,6 +1,7 @@
|
||||
package terminal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"sync"
|
||||
@@ -97,6 +98,16 @@ type Terminal interface {
|
||||
// SetPasteMode turns bracketed paste on or off: on, a paste arrives as
|
||||
// one EventPaste rather than as keys, so its newlines don't press Enter
|
||||
SetPasteMode(on bool) error
|
||||
|
||||
// SetKeyReport asks for keys to be reported at level report. Releases come
|
||||
// through the first source in allow the terminal has, the console before
|
||||
// the protocol; KeyReportPresses gives up the source in use. It returns at
|
||||
// once, and an EventKeyReport follows with what the terminal reports now,
|
||||
// so a game learns the outcome of every ask; asks the reader has not yet
|
||||
// acted on are answered together, by the latest. A change of source ends
|
||||
// the holds it leaves with Lost releases. Until an ask for releases, nothing
|
||||
// is sent to the terminal or switched.
|
||||
SetKeyReport(report KeyReport, allow KeySource) error
|
||||
}
|
||||
|
||||
// ResizeEvent represents a terminal resize
|
||||
@@ -121,6 +132,7 @@ type termImpl struct {
|
||||
finalized bool
|
||||
mouseMode MouseMode
|
||||
paste bool
|
||||
keyProto bool // the key protocol's flags are pushed
|
||||
|
||||
// Output reaches the terminal on writeLoop's goroutine, so a terminal that
|
||||
// stops reading holds that goroutine rather than whoever draws. queued is what
|
||||
@@ -198,6 +210,8 @@ func (t *termImpl) Init() error {
|
||||
|
||||
// Create input reader wrapping backend
|
||||
t.input = newInputReader(t.backend)
|
||||
t.input.sources = backendKeySources(t.backend)
|
||||
t.input.write = t.keyWrite
|
||||
|
||||
// Set resize handler on backend
|
||||
t.backend.SetResizeHandler(func(w, h int) {
|
||||
@@ -271,10 +285,20 @@ func (t *termImpl) Fini() {
|
||||
w.Write(csiPasteOff)
|
||||
}
|
||||
|
||||
// Stop handlers
|
||||
// Key reports as the terminal had them, before the screen they were pushed on goes
|
||||
if t.keyProto {
|
||||
w.Write(csiKeyPop)
|
||||
w.Write(csiFocusOff)
|
||||
t.keyProto = false
|
||||
keyProtoPushed.Store(false)
|
||||
}
|
||||
t.mu.Unlock()
|
||||
|
||||
// Stop handlers; the reader may be waiting on mu to write, and finds Fini begun
|
||||
if t.input != nil {
|
||||
t.input.stop()
|
||||
}
|
||||
t.mu.Lock()
|
||||
|
||||
// Show cursor
|
||||
w.Write(csiCursorShow)
|
||||
@@ -576,6 +600,44 @@ func (t *termImpl) SetPasteMode(on bool) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// keyProtoPushed is whether a Terminal has the key protocol's flags pushed, for
|
||||
// EmergencyReset to pop
|
||||
var keyProtoPushed atomic.Bool
|
||||
|
||||
// keyWrite queues seq, the input reader's request, for the terminal unless Fini
|
||||
// has begun; pushed is whether the protocol's flags are pushed once it is
|
||||
// written, which Fini and EmergencyReset undo
|
||||
func (t *termImpl) keyWrite(seq []byte, pushed bool) bool {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
if !t.initialized || t.finalized {
|
||||
return false
|
||||
}
|
||||
t.releaseLocked()
|
||||
t.output.buf.Write(seq)
|
||||
t.sendLocked()
|
||||
// A terminal that takes a request for its own (an old st restores the
|
||||
// cursor on CSI ? u) has moved it as far as the next frame knows
|
||||
t.output.invalidateCursor()
|
||||
t.keyProto = pushed
|
||||
keyProtoPushed.Store(pushed)
|
||||
return true
|
||||
}
|
||||
|
||||
// SetKeyReport hands the ask to the input reader, which answers it
|
||||
func (t *termImpl) SetKeyReport(report KeyReport, allow KeySource) error {
|
||||
if report > KeyReportReleases {
|
||||
return fmt.Errorf("terminal: unknown key report %d", report)
|
||||
}
|
||||
t.mu.Lock()
|
||||
ready := t.initialized && !t.finalized
|
||||
t.mu.Unlock()
|
||||
if ready {
|
||||
t.input.askKeys(report, allow)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// EmergencyReset attempts to restore terminal to sane state
|
||||
// Call this from panic recovery if Fini() cannot be called normally. Given a
|
||||
// redirected stdout, it writes to the controlling terminal instead.
|
||||
@@ -593,6 +655,10 @@ func EmergencyReset(w io.Writer) {
|
||||
w.Write(csiMouseClickOff)
|
||||
w.Write(csiMouseSGROff)
|
||||
w.Write(csiPasteOff)
|
||||
if keyProtoPushed.Load() {
|
||||
w.Write(csiKeyPop)
|
||||
w.Write(csiFocusOff)
|
||||
}
|
||||
|
||||
// Write sequences to provided writer
|
||||
w.Write(csiCursorShow)
|
||||
|
||||
Reference in new issue
Block a user