Compare commits
1
Commits
v0.4.3
..
97f66b1f1c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
97f66b1f1c |
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 {
|
||||
@@ -392,4 +417,4 @@ func lookupSS3(seq []byte) (Key, Modifier, bool) {
|
||||
return s.key, s.mod, true
|
||||
}
|
||||
return KeyNone, ModNone, false
|
||||
}
|
||||
}
|
||||
+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