3 Commits
Author SHA1 Message Date
Lixen d4cc9f2407 v0.5.2 tui: titles and labels drawn a column a rune; an unchanged frame writes nothing
* tui: titles, labels and buttons drawn a column a rune

Ten places drew each rune of a title or label at its byte offset, so a
string holding runes of more than one byte drew with holes and ran past the
width it was measured and truncated to: the Pane title, the Modal title and
hint, the Divider label, TabBar titles, Table cells, Form labels, the
TextField placeholder, the ConfirmDialog and AlertDialog buttons, and the
ProgressOverlay title. Each now draws through Region.Text, which counts
columns by rune, within the bound its loop kept; a Form label stays within
its label column, as the byte count meant it to.

* an unchanged frame writes nothing

flush closed every frame with SGR 0, so a frame that changed no cell still
sent four bytes, and an app drawing an idle screen kept writing to the
terminal. The reset now follows only a frame that wrote cells: the next
cell written restates its whole style, and Fini resets on its own.

Each widget, given a title such as "● é · ñ", draws it whole where it drew
it with holes before; a frame flushed twice writes nothing the second time,
where it wrote ESC [ 0 m.
2026-10-24 19:00:37 -04:00
LixenandClaude Opus 5.5 e9aeed0be0 v0.5.1 console key releases: Linux VT and FreeBSD vt(4), with a guard
* linux console source: releases from the console's own keyboard, typed by its keymap

SetKeyReport with KeySourceConsole, on a Linux virtual console the game runs
on, switches the keyboard to raw keycodes (K_MEDIUMRAW) while releases are
reported. The kernel allows the switch on the caller's controlling tty, so no
root is needed; a console that refuses is not asked again and the protocol is
next. Going back to presses, Fini and EmergencyReset switch it back.

Each keycode is typed as the console types it cooked, after
drivers/tty/vt/keyboard.c, by the console's own tables read at the switch
(KDGKBENT, KDGKBSENT, KDGKBDIACRUC, KDGKBMETA): modifiers and their left and
right groups, Caps and Num Lock with their lights kept in step (KDSKBLED),
sticky and locked modifiers, Unicode entries, dead keys and compose, the
keypad by Num Lock, function strings, Meta as ESC prefix or bit 7, console
switching by Alt+Fn and Alt+arrows to consoles in use, and from kernel 7.1
cursor and CSI keys with their modifiers. Translation starts from the
console's modifiers and locks (TIOCLINUX 6, KDGKBLED). Each key is held by
its keycode; a key typing several keys taps all but its last. Switching to
another console ends every hold Lost. Mouse reports are off while the
keyboard is raw, since the console types them into the same input, and come
back after it.

The guard: before the first switch the package runs the binary again, named
console-guard, holding the console and the read end of a pipe only the game
writes. If the pipe ends while the keyboard is raw (kill -9, the OOM killer, a
Go fatal error), the guard switches the keyboard back, restores the termios
the game found and shows the cursor, without a reset that would clear the
game's last words. A game leading its console's session takes the console
with it as it dies; its guard then leads a session of its own and opens the
console again once the kernel has hung it up, to restore it.

Checked in QEMU on a real VT, kernels 6.8 and 7.2, the game as uid 1000:
the same key strokes read identically raw and cooked (Shift released first,
Ctrl, Alt, AltGr, Caps and Num Lock, the keypad, function keys, modified
cursor keys on 7.2; dead grave and tilde, compose, a euro sign and Cyrillic
from a loaded keymap), Alt+F2 away ended the hold Lost and Alt+F1 came back,
and the keyboard came back to K_UNICODE with ICANON and ECHO after Fini, a
panic with EmergencyReset, kill -9, and kill -9 of a game leading its session
(left raw without the guard's own session).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B3zCVjspRDuEuMrDTumVhp

* freebsd console source: releases from vt(4)'s keyboard, typed by its keymap

On FreeBSD's vt(4) console the keyboard switches to keycodes (K_CODE),
which vt allows any process with the console open, no root needed. Each
keycode types what vt types for it cooked: the keyboard's keymap and
accent map (GIO_KEYMAP, GIO_DEADKEYMAP) run through a port of the
kernel's genkbd_keyaction, kbdmux's left Alt keypad codes, and
vt_processkey's xterm sequences, ESC for Alt and console switching. The
locks go back to the keyboard and its lights with KDSKBSTATE. Scroll Lock
is inert while raw; with the console's history mode on (Scroll Lock lit)
the keyboard stays as it is and the ask reports presses.

Shared with the Linux source now: the restore EmergencyReset runs, the
guard, which opens again a console revoked or hung up with its session's
leader, and what a key asks of the console. A console that can't go raw
just now (Scroll Lock on, its guard gone, output not drained) is tried
again at the next ask rather than never; a refusal at prepare still
holds for good.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B3zCVjspRDuEuMrDTumVhp

* example-keys: show each key event, for trying key releases

A viewer for the key events a terminal or console reports: F2 asks for
releases from the console or the terminal's protocol, or gives them
back, and the report each ask came to shows above the events.

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>
2026-10-21 11:16:27 -04:00
LixenandClaude Opus 5.5 97f66b1f1c 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>
2026-10-21 03:59:00 -04:00
39 changed files with 4946 additions and 84 deletions

No files matched your search

+114 -1
View File
@@ -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,116 @@ 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:
- `KeySourceConsole`: the kernel console's own keyboard, on the console the
game runs on: a Linux VT (`/dev/ttyN`) or FreeBSD's vt(4) (`/dev/ttyvN`).
While releases are reported the package switches the keyboard to raw
keycodes, which the kernel allows without root (Linux's `K_MEDIUMRAW` on
the game's controlling tty, FreeBSD's `K_CODE` on a console the game has
open), and switches it back when the game goes back to presses, at `Fini`
and in `EmergencyReset`. Each key types what the console types for it
cooked, by the console's own keymap read at the switch. Switching to
another console ends the holds. The console's mouse reports pause while the
keyboard is raw, and a mouse paste then reads as keys. Never over ssh,
where the keyboard is the far terminal's.
On Linux that is `loadkeys` layouts, dead keys and compose, the locks and
their lights, Alt+F*n* and Alt+arrows switching consoles, and cursor keys
with modifiers from Linux 7.1. Left out while raw: codes typed with Alt on
the keypad, braille, Ctrl+Alt+Del, and Scroll Lock, which only lets stopped
output go on.
On FreeBSD it is `kbdmap` layouts and their accents, the locks and their
lights, Alt+F*n* and Print Screen switching consoles, codes typed with left
Alt on the keypad, and Alt as ESC before a character (right Alt too with
`kern.vt.enable_altgr=0`). Left out while raw: the machine's keys
(Ctrl+Alt+Del, the debugger, suspend), Shift+Insert's paste, and Scroll
Lock. With Scroll Lock on, the console's history mode takes every key, so
the keyboard stays as it is and the ask reports presses; an ask after
Scroll Lock goes off tries again. syscons (`kern.vty=sc`) has no console
source.
Before the first switch the package runs the game's binary again as
`console-guard`, which switches the keyboard back, restores the tty and
shows the cursor if the game dies where nothing of its own runs: `kill -9`,
the OOM killer, a Go fatal error. A game leading its console's session
(on Linux, run with `exec` from the login shell or by an autologin) takes
the console with it as it dies; the guard then opens it again to restore
it. The package's `init` turns the guard's run aside before `main`; the
`init`s of packages initialized before it run in the guard too, so keep
side effects out of them. On FreeBSD the guard goes by the game's name, so
`pkill -9` by that name ends it too: kill the game by its PID. Should the
guard be killed as well, switch the keyboard back from elsewhere: on Linux
`sudo kbd_mode -u -C /dev/ttyN`; FreeBSD has no command for it, so build
one:
```sh
printf '#include <sys/ioctl.h>\n#include <sys/kbio.h>\nint main(void) { return ioctl(0, KDSKBMODE, K_XLATE); }\n' |
cc -x c -o /tmp/kbdxlate - && /tmp/kbdxlate < /dev/ttyvN
```
- `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:
+19
View File
@@ -20,6 +20,7 @@ type unixBackend struct {
outFd int
tty *os.File // the controlling terminal, opened for a redirected end
oldTerm *term.State
console keyConsole // the console input comes from, nil off one
resizeStopCh chan struct{}
resizeDoneCh chan struct{}
@@ -56,12 +57,14 @@ func (b *unixBackend) Init() error {
}
}
cooked, _ := unix.IoctlGetTermios(b.inFd, tcgets)
old, err := term.MakeRaw(b.inFd)
if err != nil {
b.closeTTY()
return err
}
b.oldTerm = old
b.console = openConsole(b.in, cooked)
return nil
}
@@ -79,6 +82,10 @@ func (b *unixBackend) Fini() {
<-b.resizeDoneCh
b.resizeStopCh = nil
}
if b.console != nil {
b.console.close()
b.console = nil
}
if b.oldTerm != nil {
term.Restore(b.inFd, b.oldTerm)
}
@@ -160,6 +167,18 @@ func (b *unixBackend) Read(stopCh <-chan struct{}) ([]byte, error) {
}
}
// keySources: the console's keyboard on a console, and the key protocol,
// which the terminal is asked about
func (b *unixBackend) keySources() KeySource {
if b.console != nil {
return KeySourceConsole | KeySourceProtocol
}
return KeySourceProtocol
}
// keyConsole is the console input comes from, nil off one
func (b *unixBackend) keyConsole() keyConsole { return b.console }
func (b *unixBackend) SetResizeHandler(handler func(width, height int)) {
b.resizeStopCh = make(chan struct{})
b.resizeDoneCh = make(chan struct{})
+148
View File
@@ -0,0 +1,148 @@
// keys — shows each key event the terminal reports, for trying key releases
// where a game runs: a Linux or FreeBSD console, Alacritty, ssh, tmux.
//
// Keys: F2 asks for releases or gives them back · Ctrl+C quits
package main
import (
"fmt"
"os"
"strings"
"github.com/lixenwraith/terminal"
)
func main() {
term := terminal.New()
// Raw-mode safety net: restore the terminal before re-panicking.
defer func() {
if r := recover(); r != nil {
terminal.EmergencyReset(os.Stdout)
panic(r)
}
}()
if err := term.Init(); err != nil {
fmt.Fprintln(os.Stderr, "terminal init:", err)
os.Exit(1)
}
defer term.Fini()
v := &viewer{term: term}
v.ask(true)
for {
v.draw()
ev := term.PollEvent()
switch ev.Type {
case terminal.EventClosed, terminal.EventError:
return
case terminal.EventKeyReport:
v.report = ev.Report.String()
if ev.Source != 0 {
v.report += " from " + ev.Source.String()
}
v.log(" report: " + v.report)
case terminal.EventFocus:
v.log(fmt.Sprintf(" focus: %v", ev.Focused))
case terminal.EventKey:
v.log(describe(ev))
if ev.Action == terminal.KeyRelease {
continue
}
switch ev.Key {
case terminal.KeyCtrlC:
return
case terminal.KeyF2:
if ev.Action == terminal.KeyPress {
v.ask(!v.releases)
}
}
}
}
}
// viewer is the screen: what the terminal reports and the events so far
type viewer struct {
term terminal.Terminal
releases bool
report string
lines []string
}
// ask asks for releases, from the console or the terminal's protocol, or
// gives them back
func (v *viewer) ask(releases bool) {
v.releases, v.report = releases, "asking"
if releases {
v.term.SetKeyReport(terminal.KeyReportReleases, terminal.KeySourceConsole|terminal.KeySourceProtocol)
return
}
v.term.SetKeyReport(terminal.KeyReportPresses, 0)
}
// log adds a line, keeping the last screenful
func (v *viewer) log(line string) {
v.lines = append(v.lines, line)
if len(v.lines) > 500 {
v.lines = v.lines[len(v.lines)-500:]
}
}
// draw shows the report above the events, the latest at the bottom
func (v *viewer) draw() {
w, h := v.term.Size()
if w <= 0 || h <= 0 {
return
}
cells := make([]terminal.Cell, w*h)
text := func(y int, s string, attr terminal.Attr) {
x := 0
for _, r := range s {
if x >= w {
break
}
cells[y*w+x] = terminal.Cell{Rune: r, Attrs: attr | terminal.AttrFgDefault | terminal.AttrBgDefault}
x++
}
for ; x < w; x++ {
cells[y*w+x] = terminal.Cell{Rune: ' ', Attrs: attr | terminal.AttrFgDefault | terminal.AttrBgDefault}
}
}
text(0, "keys: "+v.report, terminal.AttrBold|terminal.AttrReverse)
if h > 1 {
text(1, "F2 asks for releases or gives them back · Ctrl+C quits", terminal.AttrDim)
}
shown := v.lines[max(0, len(v.lines)-(h-2)):]
for y := 2; y < h; y++ {
line := ""
if i := y - 2; i < len(shown) {
line = shown[i]
}
text(y, line, terminal.AttrNone)
}
v.term.Flush(cells, w, h)
}
// describe is a key event in one line: its action, key, modifiers and
// whether its release was made up for a hold whose own can't arrive
func describe(ev terminal.Event) string {
key := terminal.KeyName(ev.Key)
if ev.Key == terminal.KeyRune {
key = fmt.Sprintf("%q", ev.Rune)
}
var mods []string
for _, m := range []struct {
mod terminal.Modifier
name string
}{{terminal.ModCtrl, "ctrl"}, {terminal.ModAlt, "alt"}, {terminal.ModShift, "shift"}} {
if ev.Modifiers&m.mod != 0 {
mods = append(mods, m.name)
}
}
line := fmt.Sprintf("%-8v %s", ev.Action, strings.Join(append(mods, key), "+"))
if ev.Lost {
line += " (lost)"
}
return line
}
+297
View File
@@ -0,0 +1,297 @@
//go:build freebsd
package terminal
import (
"os"
"strconv"
"sync"
"unsafe"
"golang.org/x/sys/unix"
)
// The FreeBSD console's ioctls (sys/kbio.h, sys/consio.h), which
// golang.org/x/sys/unix does not define
const (
kdGetKeyboardMode = 0x40044B06 // KDGKBMODE
kdSetKeyboardMode = 0x20044B07 // KDSKBMODE
kdGetLockState = 0x40044B13 // KDGKBSTATE
kdSetLockState = 0x20044B14 // KDSKBSTATE
gioKeymap = 0x20006B06 // GIO_KEYMAP
gioDeadKeymap = 0x20006B08 // GIO_DEADKEYMAP
vtActivate = 0x20047605 // VT_ACTIVATE
vtGetActive = 0x40047607 // VT_GETACTIVE
vtGetIndex = 0x40047608 // VT_GETINDEX
)
// Keyboard modes
const (
kXlate = 1 // K_XLATE
kCode = 2 // K_CODE
)
// keymapT is GIO_KEYMAP's keymap_t
type keymapT struct {
nKeys uint16
keys [fbsdNumKeys]fbsdKey
}
// accentmapT is GIO_DEADKEYMAP's accentmap_t
type accentmapT struct {
nAccs uint16
accents [fbsdDeadKeys]fbsdAccent
}
// freebsdConsole is the vt(4) console the game runs on, its keyboard switched
// to keycodes (K_CODE) while releases come from it
type freebsdConsole struct {
tty *os.File
fd int
vt int // the console's number, from 1
termios *unix.Termios // the termios the backend found, which the guard restores
mu sync.Mutex
isRaw bool
km fbsdKeymap
kb fbsdKeys
away bool // another console had the keyboard when last asked
guard *keyGuard
}
// openConsole is the console tty is, nil unless it is one: a vt(4) console,
// not a pty, a serial line or syscons
func openConsole(tty *os.File, cooked *unix.Termios) keyConsole {
if vty, err := unix.Sysctl("kern.vty"); cooked == nil || err != nil || vty != "vt" {
return nil
}
fd := int(tty.Fd())
vt, err := unix.IoctlGetInt(fd, vtGetIndex)
if err != nil || vt < 1 {
return nil
}
if _, err := unix.IoctlGetInt(fd, kdGetKeyboardMode); err != nil {
return nil
}
c := &freebsdConsole{tty: tty, fd: fd, vt: vt, termios: cooked}
c.kb.km = &c.km
return c
}
// prepare reads the keymap and starts the guard; a keyboard some other
// program has raw is left alone
func (c *freebsdConsole) prepare() bool {
c.mu.Lock()
defer c.mu.Unlock()
if c.isRaw {
return true
}
mode, err := unix.IoctlGetInt(c.fd, kdGetKeyboardMode)
if err != nil || mode != kXlate || !c.readKeymap() {
return false
}
if c.guard == nil {
if c.guard, err = startGuard(c.tty); err != nil {
return false
}
}
return true
}
// readKeymap reads the keyboard's keymap and accents, and whether vt takes
// right Alt for AltGr
func (c *freebsdConsole) readKeymap() bool {
var km keymapT
if ioctlPtr(c.fd, gioKeymap, unsafe.Pointer(&km)) != nil {
return false
}
c.km.keys = km.keys
var am accentmapT
c.km.nAccents = 0
if ioctlPtr(c.fd, gioDeadKeymap, unsafe.Pointer(&am)) == nil {
c.km.accents, c.km.nAccents = am.accents, min(int(am.nAccs), fbsdDeadKeys)
}
altGr, err := unix.SysctlUint32("kern.vt.enable_altgr")
c.km.altGr = err != nil || altGr != 0
return true
}
// raw arms the guard and switches the keyboard to keycodes, which vt allows
// whoever has the console open. Scroll Lock on is the console's history mode,
// which takes every key but its own: the keyboard stays as it is then.
func (c *freebsdConsole) raw() bool {
c.mu.Lock()
defer c.mu.Unlock()
if c.isRaw {
return true
}
if c.guard == nil || !c.guard.arm(kXlate, c.termios) {
c.dropGuard()
return false
}
locks, err := unix.IoctlGetInt(c.fd, kdGetLockState)
if err != nil || locks&fbsdSLKED != 0 || unix.IoctlSetInt(c.fd, kdSetKeyboardMode, kCode) != nil {
c.guard.disarm()
return false
}
// What was typed cooked would read as keycodes
flushInput(c.fd)
c.kb.seed(uint32(locks))
c.away, c.isRaw = false, true
rawConsoleKeys.Store(&consoleRestore{fd: c.fd, mode: kXlate, guard: c.guard})
return true
}
// cooked switches the keyboard back
func (c *freebsdConsole) cooked() {
c.mu.Lock()
defer c.mu.Unlock()
c.cookedLocked()
}
// cookedLocked switches the keyboard back, dropping keycodes not yet read; a
// console that refuses keeps the guard armed. Caller MUST hold mu.
func (c *freebsdConsole) cookedLocked() {
if !c.isRaw {
return
}
c.isRaw = false
rawConsoleKeys.Store(nil)
if unix.IoctlSetInt(c.fd, kdSetKeyboardMode, kXlate) == nil {
flushInput(c.fd)
c.guard.disarm()
}
}
// keys translates raw keycodes: one byte each, bit 7 set on release
func (c *freebsdConsole) keys(data []byte, key func(code uint32, action KeyAction, typed []byte)) {
c.mu.Lock()
defer c.mu.Unlock()
if !c.isRaw {
return
}
for _, b := range data {
code, down := int(b&0x7f), b&0x80 == 0
if code == 0 {
continue
}
action := KeyRelease
if down {
action = KeyPress
if c.kb.isDown(code) {
action = KeyRepeat
}
}
typed, act := c.kb.key(code, down)
c.act(act)
key(uint32(code), action, typed)
}
}
// act does what a key asks of the console: the locks it toggled kept with the
// keyboard and its lights, and a switch to another console. Caller MUST hold
// mu.
func (c *freebsdConsole) act(a consoleAct) {
if a.leds {
unix.IoctlSetInt(c.fd, kdSetLockState, int(c.kb.state&fbsdLockMask))
}
if a.vt != 0 {
c.switchTo(a.vt)
}
}
// switchTo switches to console vt, or the one before or after the one on
// screen, as vt does for a key cooked; vt itself refuses a console no one has
// open
func (c *freebsdConsole) switchTo(vt int) {
cur, err := unix.IoctlGetInt(c.fd, vtGetActive)
if err != nil {
return
}
switch vt {
case vtNext:
vt = cur%fbsdWindows + 1
case vtPrev:
vt = (cur+fbsdWindows-2)%fbsdWindows + 1
}
if vt != cur {
unix.IoctlSetInt(c.fd, vtActivate, vt)
}
}
// front is whether this console is on screen; back on it, translation starts
// again from the console's locks, its modifiers having moved elsewhere
func (c *freebsdConsole) front() bool {
c.mu.Lock()
defer c.mu.Unlock()
active, err := unix.IoctlGetInt(c.fd, vtGetActive)
if !c.isRaw || err != nil {
return true
}
front := active == c.vt
if front && c.away {
locks, _ := unix.IoctlGetInt(c.fd, kdGetLockState)
c.kb.seed(uint32(locks))
}
c.away = !front
return front
}
// close switches the keyboard back and ends the guard
func (c *freebsdConsole) close() {
c.mu.Lock()
defer c.mu.Unlock()
c.cookedLocked()
c.dropGuard()
}
// dropGuard ends the guard, which the next prepare starts again. Caller MUST
// hold mu.
func (c *freebsdConsole) dropGuard() {
if c.guard != nil {
c.guard.close()
c.guard = nil
}
}
// flushInput drops the console's pending input as its keyboard switches:
// keycodes would read as characters, and characters as keycodes
func flushInput(fd int) {
unix.IoctlSetPointerInt(fd, unix.TIOCFLUSH, unix.TCIFLUSH) // FREAD, which TCIFLUSH equals here
}
// guardExecutable is this program's binary, which runs again as the guard
func guardExecutable() (string, error) { return os.Executable() }
// nameGuard leaves the guard's process name as it is: FreeBSD names a process
// after its binary, and the guard's arguments name it
func nameGuard() {}
// consolePath is the console's device, for the guard to open it again
func consolePath(tty *os.File) string {
vt, err := unix.IoctlGetInt(int(tty.Fd()), vtGetIndex)
if err != nil || vt < 1 {
return ""
}
return "/dev/ttyv" + strconv.FormatInt(int64(vt-1), 32)
}
// guardRestore switches the console keyboard back for the guard, then has
// rest restore the console's tty. A session whose leader ends revokes its
// console, the game's leaving with it, which then takes no ioctl (ENOTTY):
// the guard opens it again, which vt needs no more of to switch the keyboard.
func guardRestore(tty *os.File, path string, mode int, rest func(fd int)) {
fd := int(tty.Fd())
switch err := unix.IoctlSetInt(fd, kdSetKeyboardMode, mode); err {
case unix.ENOTTY, unix.ENXIO, unix.EBADF:
guardReopen(path, mode, func(fd int) {
flushInput(fd)
rest(fd)
})
case nil:
flushInput(fd)
fallthrough
default:
rest(fd)
}
}
+386
View File
@@ -0,0 +1,386 @@
//go:build linux
package terminal
import (
"os"
"strconv"
"strings"
"sync"
"unsafe"
"golang.org/x/sys/unix"
)
// The Linux console's ioctls (linux/kd.h, linux/vt.h, linux/tiocl.h), which
// golang.org/x/sys/unix does not define
const (
kdGetKeyboardMode = 0x4B44 // KDGKBMODE
kdSetKeyboardMode = 0x4B45 // KDSKBMODE
kdGetKeymapEntry = 0x4B46 // KDGKBENT
kdGetFunctionKey = 0x4B48 // KDGKBSENT
kdGetAccents = 0x4BFA // KDGKBDIACRUC
kdGetMetaMode = 0x4B62 // KDGKBMETA
kdGetLockFlags = 0x4B64 // KDGKBLED
kdSetLockFlags = 0x4B65 // KDSKBLED
vtGetState = 0x5603 // VT_GETSTATE
vtActivate = 0x5606 // VT_ACTIVATE
tioclGetShiftState = 6 // TIOCLINUX subcode: the modifiers down
ttyMajor = 4 // the consoles' device major, minors 1 to 63
)
// Keyboard modes, and the meta mode that types Meta keys as ESC prefixes
const (
kXlate = 0x01 // K_XLATE
kMediumRaw = 0x02 // K_MEDIUMRAW
kUnicode = 0x03 // K_UNICODE
kEscPrefix = 0x04 // K_ESCPREFIX
)
type kbEntry struct {
table, index uint8
value uint16
}
type kbSEntry struct {
fn uint8
str [512]byte
}
type kbDiacrsUC struct {
count uint32
entries [256]struct{ diacr, base, result uint32 }
}
type vtStat struct {
active, signal, state uint16
}
// linuxConsole is the Linux console the game runs on, its keyboard switched
// to K_MEDIUMRAW while releases come from it
type linuxConsole struct {
tty *os.File
fd int
vt int // the console's number, from 1
termios *unix.Termios // the termios the backend found, which the guard restores
mu sync.Mutex
mode int // the keyboard mode prepare found, which cooked restores
isRaw bool
km linuxKeymap
kb linuxKeys
defLock uint8 // the default lock flags, beside the flags in KDGKBLED's answer
dec mediumRaw
away bool // another console had the keyboard when last asked
guard *keyGuard
}
// openConsole is the console tty is, nil unless it is one: a Linux virtual
// console, not a pty, a serial line or /dev/console
func openConsole(tty *os.File, cooked *unix.Termios) keyConsole {
fd := int(tty.Fd())
var st unix.Stat_t
if cooked == nil || unix.Fstat(fd, &st) != nil || unix.Major(st.Rdev) != ttyMajor {
return nil
}
vt := int(unix.Minor(st.Rdev))
if vt < 1 || vt > 63 {
return nil
}
if _, err := unix.IoctlGetInt(fd, kdGetKeyboardMode); err != nil {
return nil
}
c := &linuxConsole{tty: tty, fd: fd, vt: vt, termios: cooked}
c.kb.km = &c.km
return c
}
// prepare reads the keymap, starts the guard and lets output a Scroll Lock
// stopped go on; a keyboard some other program has raw is left alone
func (c *linuxConsole) prepare() bool {
c.mu.Lock()
defer c.mu.Unlock()
if c.isRaw {
return true
}
mode, err := unix.IoctlGetInt(c.fd, kdGetKeyboardMode)
if err != nil || mode != kXlate && mode != kUnicode || !c.readKeymap() {
return false
}
if c.guard == nil {
if c.guard, err = startGuard(c.tty); err != nil {
return false
}
}
c.mode = mode
unix.IoctlSetInt(c.fd, unix.TCXONC, unix.TCOON)
return true
}
// readKeymap reads the kernel's keymap: each map's entries, the function key
// strings, the accent table and the meta mode. Entries are read cooked, while
// KDGKBENT still answers Unicode ones.
func (c *linuxConsole) readKeymap() bool {
km := &c.km
for table := range km.maps {
e := kbEntry{table: uint8(table)}
if ioctlPtr(c.fd, kdGetKeymapEntry, unsafe.Pointer(&e)) != nil {
return false
}
if e.value == kNoSuchMap {
km.maps[table] = nil
continue
}
m := km.maps[table]
if m == nil {
m = new([nrKeys]uint16)
km.maps[table] = m
}
m[0] = e.value ^ 0xf000
for i := 1; i < nrKeys; i++ {
e.index = uint8(i)
if ioctlPtr(c.fd, kdGetKeymapEntry, unsafe.Pointer(&e)) != nil {
return false
}
m[i] = e.value ^ 0xf000
}
}
var s kbSEntry
for fn := range km.funcs {
s.fn, km.funcs[fn] = uint8(fn), ""
if ioctlPtr(c.fd, kdGetFunctionKey, unsafe.Pointer(&s)) == nil {
if n := strings.IndexByte(string(s.str[:]), 0); n >= 0 {
km.funcs[fn] = string(s.str[:n])
}
}
}
var acc kbDiacrsUC
km.accents = km.accents[:0]
if ioctlPtr(c.fd, kdGetAccents, unsafe.Pointer(&acc)) == nil {
for _, a := range acc.entries[:min(int(acc.count), len(acc.entries))] {
km.accents = append(km.accents, linuxAccent{rune(a.diacr), rune(a.base), rune(a.result)})
}
}
meta, err := unix.IoctlGetInt(c.fd, kdGetMetaMode)
km.escMeta = err != nil || meta == kEscPrefix
km.csiMods = linuxAtLeast(7, 1)
return true
}
// linuxAtLeast is whether the running kernel is major.minor or later
func linuxAtLeast(major, minor int) bool {
var u unix.Utsname
if unix.Uname(&u) != nil {
return false
}
release := unix.ByteSliceToString(u.Release[:])
parts := strings.SplitN(release, ".", 3)
if len(parts) < 2 {
return false
}
maj, err1 := strconv.Atoi(parts[0])
min, err2 := strconv.Atoi(strings.TrimRightFunc(parts[1], func(r rune) bool { return r < '0' || r > '9' }))
if err1 != nil || err2 != nil {
return false
}
return maj > major || maj == major && min >= minor
}
// state reads the modifiers down and the lock flags translation starts from
func (c *linuxConsole) state() (shift, lock uint8) {
arg := [1]byte{tioclGetShiftState}
if ioctlPtr(c.fd, unix.TIOCLINUX, unsafe.Pointer(&arg[0])) == nil {
shift = arg[0]
}
if flags, err := unix.IoctlGetInt(c.fd, kdGetLockFlags); err == nil {
lock, c.defLock = uint8(flags&0x07), uint8(flags>>4&0x07)
}
return shift, lock
}
// raw arms the guard and switches the keyboard to K_MEDIUMRAW, which the
// kernel allows on the caller's controlling tty
func (c *linuxConsole) raw() bool {
c.mu.Lock()
defer c.mu.Unlock()
if c.isRaw {
return true
}
if c.guard == nil || !c.guard.arm(c.mode, c.termios) {
c.dropGuard()
return false
}
shift, lock := c.state()
if unix.IoctlSetInt(c.fd, kdSetKeyboardMode, kMediumRaw) != nil {
c.guard.disarm()
return false
}
c.kb.seed(shift, lock)
c.dec, c.away, c.isRaw = mediumRaw{}, false, true
rawConsoleKeys.Store(&consoleRestore{fd: c.fd, mode: c.mode, guard: c.guard})
return true
}
// cooked switches the keyboard back
func (c *linuxConsole) cooked() {
c.mu.Lock()
defer c.mu.Unlock()
c.cookedLocked()
}
// cookedLocked switches the keyboard back; a console that refuses keeps the
// guard armed. Caller MUST hold mu.
func (c *linuxConsole) cookedLocked() {
if !c.isRaw {
return
}
c.isRaw = false
rawConsoleKeys.Store(nil)
if unix.IoctlSetInt(c.fd, kdSetKeyboardMode, c.mode) == nil {
c.guard.disarm()
}
}
// keys decodes raw keycodes and translates each key
func (c *linuxConsole) keys(data []byte, key func(code uint32, action KeyAction, typed []byte)) {
c.mu.Lock()
defer c.mu.Unlock()
if !c.isRaw {
return
}
c.dec.decode(data, func(code int, down bool) {
if code >= nrKeys {
return // no keymap maps it
}
action := KeyRelease
if down {
action = KeyPress
if c.kb.isDown(code) {
action = KeyRepeat
}
}
typed, act := c.kb.key(code, down, action == KeyRepeat)
c.act(act)
key(uint32(code), action, typed)
})
}
// act does what a key asks of the console. Caller MUST hold mu.
func (c *linuxConsole) act(a consoleAct) {
if a.leds {
unix.IoctlSetInt(c.fd, kdSetLockFlags, int(c.kb.leds)|int(c.defLock)<<4)
}
if a.start {
unix.IoctlSetInt(c.fd, unix.TCXONC, unix.TCOON)
}
if a.vt != 0 {
c.switchTo(a.vt)
}
}
// switchTo switches to console vt, or the one before or after this one in
// use, as the kernel does for a key cooked: only to a console in use, which
// VT_GETSTATE tells of the first 15
func (c *linuxConsole) switchTo(vt int) {
var st vtStat
if ioctlPtr(c.fd, vtGetState, unsafe.Pointer(&st)) != nil {
return
}
cur, inUse := int(st.active), func(n int) bool { return n > 15 || st.state&(1<<n) != 0 }
if vt < 0 && cur >= 1 && cur <= 15 {
step := 1
if vt == vtPrev {
step = -1
}
vt = 0
for n := (cur+step+14)%15 + 1; n != cur; n = (n+step+14)%15 + 1 {
if inUse(n) {
vt = n
break
}
}
}
if vt > 0 && vt != cur && inUse(vt) {
unix.IoctlSetInt(c.fd, vtActivate, vt)
}
}
// front is whether this console is on screen; back on it, translation starts
// again from the console's state, its modifiers having moved elsewhere
func (c *linuxConsole) front() bool {
c.mu.Lock()
defer c.mu.Unlock()
var st vtStat
if !c.isRaw || ioctlPtr(c.fd, vtGetState, unsafe.Pointer(&st)) != nil {
return true
}
front := int(st.active) == c.vt
if front && c.away {
c.kb.seed(c.state())
c.dec = mediumRaw{}
}
c.away = !front
return front
}
// close switches the keyboard back and ends the guard
func (c *linuxConsole) close() {
c.mu.Lock()
defer c.mu.Unlock()
c.cookedLocked()
c.dropGuard()
}
// dropGuard ends the guard, which the next prepare starts again. Caller MUST
// hold mu.
func (c *linuxConsole) dropGuard() {
if c.guard != nil {
c.guard.close()
c.guard = nil
}
}
// guardExecutable is this program's binary, which runs again as the guard:
// the running one, even replaced or deleted since
func guardExecutable() (string, error) { return "/proc/self/exe", nil }
// nameGuard names the guard's process as its arguments do; init runs on the
// main thread, whose name is the process's
func nameGuard() {
name := append([]byte(guardName), 0)
unix.Prctl(unix.PR_SET_NAME, uintptr(unsafe.Pointer(&name[0])), 0, 0, 0)
}
// consolePath is the console's device, for the guard to open it again
func consolePath(tty *os.File) string {
var st unix.Stat_t
if unix.Fstat(int(tty.Fd()), &st) != nil {
return ""
}
return "/dev/tty" + strconv.Itoa(int(unix.Minor(st.Rdev)))
}
// guardRestore switches the console keyboard back for the guard, then has
// rest restore the console's tty. A game that led the console's session took
// the console with it, the kernel hanging it up as the game died: the guard,
// leading a session of its own then, opens the console again once it is free,
// which makes it the guard's controlling tty and lets it switch the keyboard.
func guardRestore(tty *os.File, path string, mode int, rest func(fd int)) {
fd := int(tty.Fd())
if err := unix.IoctlSetInt(fd, kdSetKeyboardMode, mode); err != unix.EIO && err != unix.EPERM {
rest(fd)
return
}
if sid, _ := unix.Getsid(0); sid != os.Getpid() {
return
}
guardReopen(path, mode, func(fd int) {
rest(fd)
// Given up, the console isn't hung up again as the guard ends
unix.IoctlSetInt(fd, unix.TIOCNOTTY, 0)
})
}
// flushInput drops the console's pending input as its keyboard switches, which
// KDSKBMODE does itself here
func flushInput(int) {}
+6
View File
@@ -0,0 +1,6 @@
//go:build !linux && !freebsd
package terminal
// restoreConsoleKeys has no console keyboard to restore here
func restoreConsoleKeys() {}
+12
View File
@@ -0,0 +1,12 @@
//go:build unix && !linux && !freebsd
package terminal
import (
"os"
"golang.org/x/sys/unix"
)
// openConsole finds no console whose keyboard goes raw here
func openConsole(*os.File, *unix.Termios) keyConsole { return nil }
+39
View File
@@ -0,0 +1,39 @@
//go:build linux || freebsd
package terminal
import (
"sync/atomic"
"unsafe"
"golang.org/x/sys/unix"
)
// What the consoles whose keyboard goes raw share: the Linux console and
// FreeBSD's vt(4)
// ioctlPtr is an ioctl whose argument is a pointer
func ioctlPtr(fd int, req uint, arg unsafe.Pointer) error {
if _, _, e := unix.Syscall(unix.SYS_IOCTL, uintptr(fd), uintptr(req), uintptr(arg)); e != 0 {
return e
}
return nil
}
// consoleRestore is what restores a console keyboard left raw
type consoleRestore struct {
fd, mode int
guard *keyGuard
}
// rawConsoleKeys is the console a Terminal has raw, for EmergencyReset
var rawConsoleKeys atomic.Pointer[consoleRestore]
// restoreConsoleKeys switches back a console keyboard a Terminal has raw. It
// takes no lock, for a reset from a panic that may hold one.
func restoreConsoleKeys() {
if r := rawConsoleKeys.Swap(nil); r != nil && unix.IoctlSetInt(r.fd, kdSetKeyboardMode, r.mode) == nil {
flushInput(r.fd)
r.guard.disarm()
}
}
+3
View File
@@ -6,6 +6,9 @@
// 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, from the Linux or FreeBSD console's keyboard
// or the terminal's key event protocol (Alacritty's), and presses where
// there are neither
// - 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
+498
View File
@@ -0,0 +1,498 @@
package terminal
import "unicode/utf8"
// The FreeBSD console's keymap and its translation of keycodes, after the
// kernel's own (sys/dev/kbd/kbd.c, sys/dev/vt/vt_core.c), so a vt(4)
// keyboard switched to keycodes (K_CODE) types what the console types with it
// cooked: the keyboard's driver makes an action of each keycode by the keymap
// and its modifiers and locks, and vt types the action.
// Actions the keymap's special entries hold (sys/kbio.h)
const (
fbsdNOP = 0x00 // nothing
fbsdLSH = 0x02 // left Shift
fbsdRSH = 0x03 // right Shift
fbsdCLK = 0x04 // Caps Lock
fbsdNLK = 0x05 // Num Lock
fbsdSLK = 0x06 // Scroll Lock
fbsdLALT = 0x07 // left Alt
fbsdBTAB = 0x08 // back tab
fbsdLCTR = 0x09 // left Ctrl
fbsdNEXT = 0x0a // the next console
fbsdFSCR = 0x0b // console 1, the others after it
fbsdFFN = 0x1b // function key 1, the others after it
fbsdLFN = 0x7a // the last function key
fbsdRCTR = 0x80 // right Ctrl
fbsdRALT = 0x81 // right Alt
fbsdALK = 0x82 // AltGr Lock
fbsdASH = 0x83 // AltGr
fbsdMETA = 0x84 // Meta
fbsdRBT = 0x85 // reboot
fbsdDBG = 0x86 // the debugger
fbsdSUSP = 0x87 // suspend
fbsdSPSC = 0x88 // the screen saver
fbsdFACC = 0x89 // the first dead key
fbsdLACC = 0x97 // the last dead key
fbsdSTBY = 0x98 // standby
fbsdPREV = 0x99 // the previous console
fbsdPNC = 0x9a // panic
fbsdLSHA = 0x9b // left Shift, AltGr Lock pressed alone
fbsdRSHA = 0x9c // right Shift, AltGr Lock pressed alone
fbsdLCTRA = 0x9d // left Ctrl, AltGr Lock pressed alone
fbsdRCTRA = 0x9e // right Ctrl, AltGr Lock pressed alone
fbsdLALTA = 0x9f // left Alt, AltGr Lock pressed alone
fbsdRALTA = 0xa0 // right Alt, AltGr Lock pressed alone
fbsdHALT = 0xa1 // halt
fbsdPDWN = 0xa2 // power down
)
// What the keyboard's driver flags an action with (sys/kbio.h)
const (
fbsdNoKey = 0x01000000 // NOKEY: nothing to type
fbsdFKey = 0x02000000 // FKEY: a function key
fbsdMKey = 0x04000000 // MKEY: typed with Meta
fbsdBKey = 0x08000000 // BKEY: back tab
fbsdErrKey = 0x20000000 // ERRKEY: a character a dead key can't accent
fbsdRelKey = 0x40000000 // RELKEY: a special key let go
fbsdSpclKey = 0x80000000 // SPCLKEY: a special action
fbsdFlags = 0xff000000 // KEYFLAGS
)
// The keyboard's state: the locks, as KDGKBSTATE reports them, and the
// modifiers (sys/dev/kbd/kbdreg.h)
const (
fbsdCLKED = 1 << 0 // Caps Lock on
fbsdNLKED = 1 << 1 // Num Lock on
fbsdSLKED = 1 << 2 // Scroll Lock on
fbsdALKED = 1 << 3 // AltGr Lock on
fbsdLockMask = fbsdCLKED | fbsdNLKED | fbsdSLKED | fbsdALKED
fbsdShifts1 = 1 << 16
fbsdShifts2 = 1 << 17
fbsdShifts = fbsdShifts1 | fbsdShifts2
fbsdCtls1 = 1 << 18
fbsdCtls2 = 1 << 19
fbsdCtls = fbsdCtls1 | fbsdCtls2
fbsdAlts1 = 1 << 20
fbsdAlts2 = 1 << 21
fbsdAlts = fbsdAlts1 | fbsdAlts2
fbsdAgrs1 = 1 << 22
fbsdAgrs2 = 1 << 23
fbsdAgrs = fbsdAgrs1 | fbsdAgrs2
fbsdMetas1 = 1 << 24
fbsdMetas2 = 1 << 25
fbsdMetas = fbsdMetas1 | fbsdMetas2
fbsdNLKDown = 1 << 26
fbsdSLKDown = 1 << 27
fbsdCLKDown = 1 << 28
fbsdALKDown = 1 << 29
fbsdShiftAOn = 1 << 30 // a Shift, Ctrl or Alt that locks AltGr pressed alone so far
)
const (
fbsdNumKeys = 256 // NUM_KEYS: entries in a keymap
fbsdAltGrKeys = 128 // ALTGR_OFFSET: where the keys' AltGr entries start
fbsdDeadKeys = 15 // NUM_DEADKEYS
fbsdAccents = 52 // NUM_ACCENTCHARS: the characters a dead key accents
fbsdLockC = 1 // FLAG_LOCK_C: Caps Lock shifts the key
fbsdLockN = 2 // FLAG_LOCK_N: Num Lock shifts the key
fbsdKeycodes = 128 // K_CODE's keycodes, 7 bits
fbsdWindows = 12 // VT_MAXWINDOWS: the consoles vt(4) has
)
// fbsdKeymap is a FreeBSD console keymap as GIO_KEYMAP and GIO_DEADKEYMAP
// read it, and whether vt(4) takes right Alt for AltGr
// (kern.vt.enable_altgr) rather than the Alt that types ESC before a
// character
type fbsdKeymap struct {
keys [fbsdNumKeys]fbsdKey
accents [fbsdDeadKeys]fbsdAccent
nAccents int
altGr bool
}
// fbsdKey is a key's entry (keyent_t), laid out as the kernel's: its action
// in each state of Shift, Ctrl and Alt, the states whose action is special,
// and the locks that shift it
type fbsdKey struct {
actions [8]uint32
spcl uint8
flgs uint8
}
// fbsdAccent is a dead key's accent (acc_t), laid out as the kernel's: its
// character, and the characters it makes of others, in pairs up to a zero
type fbsdAccent struct {
char uint32
pairs [fbsdAccents][2]uint32
}
// fbsdKeys translates keycodes as a vt(4) console does, keeping the
// keyboard's modifiers, locks and dead key, the special action each key down
// began, the code left Alt and the keypad compose, and Alt for vt's ESC. The
// machine's keys (reboot, halt, power down, suspend, standby, the debugger,
// panic), the screen saver and paste type nothing; Scroll Lock does nothing,
// leaving the console's history mode alone. The cursor keys type as they do
// outside application mode, which the package never sets.
type fbsdKeys struct {
km *fbsdKeymap
down [fbsdKeycodes / 64]uint64 // keys down
state uint32 // modifiers and locks
accent int // the dead key pending, from 1; zero for none
lastact [fbsdKeycodes]uint32 // the special action each key down began
composing bool // left Alt down, the keypad's digits composing a code
composed uint32 // the code composed so far
alt bool // Alt down, for vt's ESC before a character
}
// seed starts translation from the console's locks; modifiers already down
// are not known, the keyboard's driver forgetting them as it switches
func (k *fbsdKeys) seed(locks uint32) {
*k = fbsdKeys{km: k.km, state: locks & fbsdLockMask}
}
// isDown is whether a key is down
func (k *fbsdKeys) isDown(code int) bool {
return code >= 0 && code < fbsdKeycodes && k.down[code/64]&(1<<(code%64)) != 0
}
// key translates one keycode event: down for a press, or a repeat, which the
// keyboard sends as another press. It returns what the console types for it
// and what it asks of the console.
func (k *fbsdKeys) key(code int, down bool) (typed []byte, act consoleAct) {
if code <= 0 || code >= fbsdKeycodes {
return nil, act
}
if down {
k.down[code/64] |= 1 << (code % 64)
} else {
k.down[code/64] &^= 1 << (code % 64)
}
if k.compose(code, down) {
return nil, act
}
locks := k.state & fbsdLockMask
c := k.action(code, !down)
act.leds = k.state&fbsdLockMask != locks
typed, act.vt = k.vt(c)
if !k.composing && k.composed > 0 {
// Left Alt up types the code composed
ch, _ := k.vt(k.composed)
typed, k.composed = append(typed, ch...), 0
}
return typed, act
}
// The keycodes of left Alt and the keypad's digits, which the keyboard's
// driver composes a code with
const fbsdKeyLeftAlt = 0x38
var fbsdPadDigits = map[int]uint32{
0x47: 7, 0x48: 8, 0x49: 9, 0x4b: 4, 0x4c: 5, 0x4d: 6, 0x4f: 1, 0x50: 2, 0x51: 3, 0x52: 0,
}
// compose composes a code of the keypad's digits typed with left Alt down, as
// the keyboard's driver does before it makes an action of a keycode
// (kbdmux_read_char): true when it takes the key, which then types nothing.
// Another key ends a code begun, and is lost to it; a code past a byte types
// nothing.
func (k *fbsdKeys) compose(code int, down bool) bool {
if code == fbsdKeyLeftAlt {
if down && !k.composing {
k.composing, k.composed = true, 0
} else if !down && k.composing {
k.composing = false
if k.composed > 0xff {
k.composed = 0
}
}
return false
}
if !k.composing {
return false
}
if digit, ok := fbsdPadDigits[code]; ok {
if down {
k.composed = min(k.composed*10+digit, 0x100)
}
return true
}
if k.composed > 0 {
k.composing, k.composed = false, 0
return true
}
return false
}
// lockKey toggles a lock as its key goes down, not as it repeats
func lockKey(state *uint32, down, locked uint32) {
if *state&down == 0 {
*state |= down
*state ^= locked
}
}
// action makes an action of a keycode by the keymap, as the keyboard's
// driver does (genkbd_keyaction): a character, a special action, or a flag
// for none
func (k *fbsdKeys) action(code int, up bool) uint32 {
state := k.state
i := code
if f := state & (fbsdAgrs | fbsdALKED); f == fbsdAgrs1 || f == fbsdAgrs2 || f == fbsdALKED {
i += fbsdAltGrKeys
}
key := &k.km.keys[i]
col := 0
if state&fbsdShifts != 0 {
col |= 1
}
if state&fbsdCtls != 0 {
col |= 2
}
if state&fbsdAlts != 0 {
col |= 4
}
if key.flgs&fbsdLockC != 0 && state&fbsdCLKED != 0 || key.flgs&fbsdLockN != 0 && state&fbsdNLKED != 0 {
col ^= 1
}
if up {
action := k.lastact[code]
k.lastact[code] = fbsdNOP
if alone, ok := fbsdLockingMods[action]; ok {
// Pressed alone, the key toggles AltGr Lock
if state&fbsdShiftAOn != 0 {
lockKey(&state, fbsdALKDown, fbsdALKED)
state &^= fbsdALKDown
}
action = alone
}
switch action {
case fbsdNOP:
// Regular keys go up unreported
k.state &^= fbsdShiftAOn
return fbsdNoKey
case fbsdLSH:
state &^= fbsdShifts1
case fbsdRSH:
state &^= fbsdShifts2
case fbsdLCTR:
state &^= fbsdCtls1
case fbsdRCTR:
state &^= fbsdCtls2
case fbsdLALT:
state &^= fbsdAlts1
case fbsdRALT:
state &^= fbsdAlts2
case fbsdASH:
state &^= fbsdAgrs1
case fbsdMETA:
state &^= fbsdMetas1
case fbsdNLK:
state &^= fbsdNLKDown
case fbsdCLK:
state &^= fbsdCLKDown
case fbsdSLK:
state &^= fbsdSLKDown
case fbsdALK:
state &^= fbsdALKDown
}
k.state = state &^ fbsdShiftAOn
return fbsdSpclKey | fbsdRelKey | action
}
action := key.actions[col]
state &^= fbsdShiftAOn
if key.spcl&(0x80>>col) == 0 {
// A character, the dead key pending accenting it
k.lastact[code] = fbsdNOP
k.state = state
if k.accent > 0 {
if action = k.accented(action); action == fbsdErrKey {
return action
}
}
if state&fbsdMetas != 0 {
action |= fbsdMKey
}
return action
}
// A special key keeps the action it went down with while it repeats
if k.lastact[code] == fbsdNOP {
k.lastact[code] = action
}
if k.lastact[code] != action {
action = fbsdNOP
}
if alone, ok := fbsdLockingMods[action]; ok {
state |= fbsdShiftAOn
action = alone
}
switch action {
case fbsdNLK:
lockKey(&state, fbsdNLKDown, fbsdNLKED)
case fbsdCLK:
lockKey(&state, fbsdCLKDown, fbsdCLKED)
case fbsdSLK:
// The console's history mode follows Scroll Lock; raw, it stays
case fbsdALK:
lockKey(&state, fbsdALKDown, fbsdALKED)
case fbsdSPSC, fbsdRBT, fbsdSUSP, fbsdSTBY, fbsdDBG, fbsdNEXT, fbsdPREV, fbsdPNC, fbsdHALT, fbsdPDWN:
k.accent = 0
case fbsdBTAB:
k.accent = 0
action |= fbsdBKey
case fbsdLSH:
state |= fbsdShifts1
case fbsdRSH:
state |= fbsdShifts2
case fbsdLCTR:
state |= fbsdCtls1
case fbsdRCTR:
state |= fbsdCtls2
case fbsdLALT:
state |= fbsdAlts1
case fbsdRALT:
state |= fbsdAlts2
case fbsdASH:
state |= fbsdAgrs1
case fbsdMETA:
state |= fbsdMetas1
case fbsdNOP:
k.state = state
return fbsdNoKey
default:
k.state = state
if action >= fbsdFACC && action <= fbsdLACC {
action = k.deadKey(action)
if action == fbsdNoKey || action == fbsdErrKey {
return action
}
if state&fbsdMetas != 0 {
return action | fbsdMKey
}
return action
}
if k.accent > 0 {
k.accent = 0
return fbsdErrKey
}
if action >= fbsdFFN && action <= fbsdLFN {
action |= fbsdFKey
}
return fbsdSpclKey | action
}
k.state = state
return fbsdSpclKey | action
}
// fbsdLockingMods are the modifiers that lock AltGr pressed alone, and the
// modifier each is otherwise
var fbsdLockingMods = map[uint32]uint32{
fbsdLSHA: fbsdLSH, fbsdRSHA: fbsdRSH, fbsdLCTRA: fbsdLCTR,
fbsdRCTRA: fbsdRCTR, fbsdLALTA: fbsdLALT, fbsdRALTA: fbsdRALT,
}
// deadKey is a dead key: the next character takes its accent, and the dead
// key again types the accent itself
func (k *fbsdKeys) deadKey(action uint32) uint32 {
i := int(action-fbsdFACC) + 1
if i > k.km.nAccents || k.km.accents[i-1].char == 0 {
k.accent = 0
return fbsdErrKey
}
if i == k.accent {
k.accent = 0
return k.km.accents[i-1].char
}
k.accent = i
return fbsdNoKey
}
// accented is what the dead key pending makes of ch: its accent for a space,
// the accented character, or an error for a character it can't accent
func (k *fbsdKeys) accented(ch uint32) uint32 {
acc := &k.km.accents[k.accent-1]
k.accent = 0
if ch == ' ' {
return acc.char
}
for _, p := range acc.pairs {
if p[0] == 0 {
break
}
if p[0] == ch {
return p[1]
}
}
return fbsdErrKey
}
// vt types an action as vt(4) does (vt_processkey): a character as UTF-8,
// after an ESC while Alt is down; F1 to F12, the cursor and editing keys and
// back tab as xterm's sequences; and a console to switch to
func (k *fbsdKeys) vt(c uint32) (typed []byte, vt int) {
if c&fbsdRelKey != 0 {
switch c &^ fbsdRelKey {
case fbsdSpclKey | fbsdRALT:
if k.km.altGr {
break
}
fallthrough
case fbsdSpclKey | fbsdLALT:
k.alt = false
}
return nil, 0
}
switch c {
case fbsdSpclKey | fbsdRALT:
if !k.km.altGr {
k.alt = true
}
case fbsdSpclKey | fbsdLALT:
k.alt = true
}
if c&fbsdSpclKey != 0 {
c &^= fbsdSpclKey
switch {
case c >= fbsdFSCR && c < fbsdFSCR+fbsdWindows:
return nil, int(c-fbsdFSCR) + 1
case c == fbsdNEXT:
return nil, vtNext
case c == fbsdPREV:
return nil, vtPrev
case c == fbsdBKey|fbsdBTAB:
return []byte("\x1b[Z"), 0
case c&fbsdFKey != 0:
return fbsdFunctionKey(c &^ fbsdFKey), 0
}
return nil, 0
}
if c&fbsdFlags != 0 {
return nil, 0 // nothing, a character it can't accent, or with Meta, which vt drops
}
if k.alt {
typed = append(typed, 0x1b)
}
return utf8.AppendRune(typed, rune(c)), 0
}
// fbsdFunctionKeys are what vt(4) types for the function keys, by number:
// xterm's sequences for F1 to F12, and for the cursor and editing keys the
// keymap binds as function keys 49 to 61; nothing for the others
var fbsdFunctionKeys = map[uint32]string{
1: "\x1bOP", 2: "\x1bOQ", 3: "\x1bOR", 4: "\x1bOS",
5: "\x1b[15~", 6: "\x1b[17~", 7: "\x1b[18~", 8: "\x1b[19~",
9: "\x1b[20~", 10: "\x1b[21~", 11: "\x1b[23~", 12: "\x1b[24~",
49: "\x1b[H", 50: "\x1b[A", 51: "\x1b[5~", 53: "\x1b[D", 55: "\x1b[C",
57: "\x1b[F", 58: "\x1b[B", 59: "\x1b[6~", 60: "\x1b[2~", 61: "\x1b[3~",
}
// fbsdFunctionKey is what vt(4) types for a function key action
func fbsdFunctionKey(action uint32) []byte {
if s, ok := fbsdFunctionKeys[action-fbsdFFN+1]; ok {
return []byte(s)
}
return nil
}
+274
View File
@@ -0,0 +1,274 @@
package terminal
import (
"bufio"
"os"
"slices"
"strconv"
"strings"
"testing"
)
// testFreeBSDKeymap is the FreeBSD kernel's built-in keymap (US) and accent
// map (sys/dev/kbd/kbdtables.h), as GIO_KEYMAP and GIO_DEADKEYMAP read them,
// with vt's default AltGr
func testFreeBSDKeymap(t *testing.T) *fbsdKeymap {
t.Helper()
f, err := os.Open("testdata/freebsd-keymap.txt")
if err != nil {
t.Fatal(err)
}
defer f.Close()
km := &fbsdKeymap{nAccents: fbsdDeadKeys, altGr: true}
hex := func(s string) uint32 {
x, err := strconv.ParseUint(s, 16, 32)
if err != nil {
t.Fatal(err)
}
return uint32(x)
}
sc := bufio.NewScanner(f)
for sc.Scan() {
fields := strings.Fields(sc.Text())
i, err := strconv.Atoi(fields[1])
if err != nil {
t.Fatal(err)
}
switch fields[0] {
case "key":
key := &km.keys[i]
for j, v := range fields[2:10] {
key.actions[j] = hex(v)
}
key.spcl, key.flgs = uint8(hex(fields[10])), uint8(hex(fields[11]))
case "acc":
acc := &km.accents[i]
acc.char = hex(fields[2])
for j, pair := range fields[3:] {
base, result, _ := strings.Cut(pair, ":")
acc.pairs[j] = [2]uint32{hex(base), hex(result)}
}
}
}
return km
}
// The FreeBSD console's keycodes up to 88 are the Linux console's; its others
// are its own
const (
fbsdKeyKPEnter = 89
fbsdKeyRightCtrl = 90
fbsdKeyPrint = 92
fbsdKeyRightAlt = 93
fbsdKeyHome = 94
fbsdKeyUp = 95
fbsdKeyPageUp = 96
fbsdKeyLeft = 97
fbsdKeyEnd = 99
fbsdKeyInsert = 102
fbsdKeyDelete = 103
fbsdKeyPause = 104
fbsdKeyF11 = 87
fbsdKeyF12 = 88
keyKP9 = 73
keyKP6 = 77
keyKP2 = 80
keyKP3 = 81
)
// fbsdTypedBy is what vt types for strokes, and what they ask of it
func fbsdTypedBy(k *fbsdKeys, strokes []stroke) (string, []consoleAct) {
var out []byte
var acts []consoleAct
for _, s := range strokes {
typed, act := k.key(s.code, s.down)
out = append(out, typed...)
if act != (consoleAct{}) {
acts = append(acts, act)
}
}
return string(out), acts
}
// Keys type what vt types for them cooked, by the keymap and the modifiers
// and locks they move: Shift into the character, Caps Lock on letters only,
// Ctrl as control characters, Alt as an ESC prefix, F1 to F12 and the cursor
// and editing keys as xterm's sequences, and the keypad by Num Lock
func TestFreeBSDConsoleTypesAsCooked(t *testing.T) {
cases := []struct {
name string
strokes []stroke
want string
altGr string // with right Alt as Alt, when it differs
}{
{"letter", press(keyA), "a", ""},
{"shifted letter", hold(keyLeftShift, press(keyA)), "A", ""},
{"shifted digit", hold(keyLeftShift, press(key1)), "!", ""},
{"caps lock", slices.Concat(press(keyCapsLock), press(keyA, key1), press(keyCapsLock), press(keyA)), "A1a", ""},
{"caps lock with shift", slices.Concat(press(keyCapsLock), hold(keyLeftShift, press(keyA))), "a", ""},
{"ctrl letter", hold(keyLeftCtrl, press(keyA)), "\x01", ""},
{"ctrl shift letter", hold(keyLeftCtrl, hold(keyLeftShift, press(keyA))), "\x01", ""},
{"right ctrl letter", hold(fbsdKeyRightCtrl, press(keyA)), "\x01", ""},
{"ctrl space", hold(keyLeftCtrl, press(keySpace)), "\x00", ""},
{"ctrl backspace", hold(keyLeftCtrl, press(keyBackspace)), "\x7f", ""},
{"alt letter", hold(keyLeftAlt, press(keyA)), "\x1ba", ""},
{"alt enter", hold(keyLeftAlt, press(keyEnter)), "\x1b\r", ""},
{"ctrl alt letter", hold(keyLeftCtrl, hold(keyLeftAlt, press(keyA))), "\x1b\x01", ""},
{"right alt letter", hold(fbsdKeyRightAlt, press(keyA)), "a", "\x1ba"},
{"alt released", slices.Concat(hold(keyLeftAlt, press(keyA)), press(keyA)), "\x1baa", ""},
{"enter tab escape backspace", press(keyEnter, keyTab, keyEsc, keyBackspace), "\r\t\x1b\x08", ""},
{"back tab", hold(keyLeftShift, press(keyTab)), "\x1b[Z", ""},
{"alt back tab", hold(keyLeftAlt, hold(keyLeftShift, press(keyTab))), "\x1b[Z", ""},
{"function keys", press(keyF1, keyF6, fbsdKeyF12), "\x1bOP\x1b[17~\x1b[24~", ""},
{"shifted function key, no sequence", hold(keyLeftShift, press(keyF1)), "", ""},
{"editing keys", press(fbsdKeyHome, fbsdKeyEnd, fbsdKeyInsert, fbsdKeyDelete, fbsdKeyPageUp), "\x1b[H\x1b[F\x1b[2~\x1b[3~\x1b[5~", ""},
{"cursor", press(fbsdKeyUp, fbsdKeyLeft), "\x1b[A\x1b[D", ""},
{"shifted cursor", hold(keyLeftShift, press(fbsdKeyUp)), "\x1b[A", ""},
{"keypad, num lock off", press(keyKP8, keyKP5, keyKP0, keyKPDot, fbsdKeyKPEnter), "\x1b[A\x1b[2~\x7f\r", ""},
{"keypad, num lock on", slices.Concat(press(keyNumLock), press(keyKP8, keyKP0, keyKPDot)), "80.", ""},
{"repeat", []stroke{{keyQ, true}, {keyQ, true}, {keyQ, true}, {keyQ, false}}, "qqq", ""},
{"shift released first", []stroke{{keyLeftShift, true}, {keyA, true}, {keyLeftShift, false}, {keyA, true}, {keyA, false}}, "Aa", ""},
{"two shifts", []stroke{{keyLeftShift, true}, {keyRtShift, true}, {keyLeftShift, false}, {keyA, true}, {keyA, false}, {keyRtShift, false}}, "A", ""},
{"alt keypad code", hold(keyLeftAlt, press(keyKP6, keyKP5)), "A", ""},
{"alt keypad code, num lock on", slices.Concat(press(keyNumLock), hold(keyLeftAlt, press(keyKP2, keyKP3, keyKP3))), "é", ""},
{"alt keypad code past a byte", hold(keyLeftAlt, press(keyKP9, keyKP9, keyKP9)), "", ""},
{"another key ends a code", slices.Concat([]stroke{{keyLeftAlt, true}}, press(keyKP6, keyA, keyA), []stroke{{keyLeftAlt, false}}), "\x1ba", ""},
{"right alt keypad", hold(fbsdKeyRightAlt, press(keyKP6)), "6", "\x1b6"},
{"inert keys", slices.Concat(
hold(keyLeftCtrl, hold(keyLeftAlt, press(fbsdKeyDelete))), // reboot
hold(keyLeftCtrl, hold(keyLeftAlt, press(keyEsc))), // the debugger
hold(keyLeftShift, press(fbsdKeyInsert)), // paste
hold(keyLeftAlt, press(fbsdKeyPause)), // suspend
press(keyScrollLk, fbsdKeyPause)), "", ""},
}
for _, altGr := range []bool{true, false} {
km := testFreeBSDKeymap(t)
km.altGr = altGr
for _, c := range cases {
want := c.want
if !altGr && c.altGr != "" {
want = c.altGr
}
k := &fbsdKeys{km: km}
got, _ := fbsdTypedBy(k, c.strokes)
if got != want {
t.Errorf("%s (altGr %v): typed %q, want %q", c.name, altGr, got, want)
}
if k.state&^fbsdLockMask != 0 || k.alt {
t.Errorf("%s (altGr %v): modifiers %#x (alt %v) left down", c.name, altGr, k.state&^fbsdLockMask, k.alt)
}
}
}
}
// Keys that switch consoles and the locks ask the console for what vt would
// do cooked: a console by number or the one before or after, and the locks
// kept with the keyboard. Scroll Lock asks for nothing, the console's history
// mode staying off.
func TestFreeBSDConsoleKeysAskTheConsole(t *testing.T) {
cases := []struct {
name string
strokes []stroke
want []consoleAct
locks uint32
}{
{"alt f2", hold(keyLeftAlt, press(keyF2)), []consoleAct{{vt: 2}}, 0},
{"ctrl alt f1", hold(keyLeftCtrl, hold(keyLeftAlt, press(keyF1))), []consoleAct{{vt: 1}}, 0},
{"alt shift f1", hold(keyLeftAlt, hold(keyLeftShift, press(keyF1))), []consoleAct{{vt: 11}}, 0},
{"alt f12", hold(keyLeftAlt, press(fbsdKeyF12)), []consoleAct{{vt: 12}}, 0},
{"print screen, shifted", slices.Concat(press(fbsdKeyPrint), hold(keyLeftShift, press(fbsdKeyPrint))), []consoleAct{{vt: vtNext}, {vt: vtPrev}}, 0},
{"caps lock twice", press(keyCapsLock, keyCapsLock), []consoleAct{{leds: true}, {leds: true}}, 0},
{"caps lock held", []stroke{{keyCapsLock, true}, {keyCapsLock, true}, {keyCapsLock, false}}, []consoleAct{{leds: true}}, fbsdCLKED},
{"num lock", press(keyNumLock), []consoleAct{{leds: true}}, fbsdNLKED},
{"scroll lock", press(keyScrollLk), nil, 0},
{"a special key repeats as it went down", []stroke{
{keyLeftAlt, true}, {keyF2, true}, {keyLeftAlt, false}, {keyF2, true}, {keyF2, false},
}, []consoleAct{{vt: 2}}, 0},
}
for _, c := range cases {
k := &fbsdKeys{km: testFreeBSDKeymap(t)}
if _, got := fbsdTypedBy(k, c.strokes); !slices.Equal(got, c.want) {
t.Errorf("%s: asked %+v, want %+v", c.name, got, c.want)
}
if k.state != c.locks {
t.Errorf("%s: state %#x, want %#x", c.name, k.state, c.locks)
}
}
}
// Translation starts from the console's locks, with no modifier down
func TestFreeBSDConsoleSeedsFromTheConsole(t *testing.T) {
k := &fbsdKeys{km: testFreeBSDKeymap(t)}
fbsdTypedBy(k, []stroke{{keyLeftShift, true}, {keyLeftAlt, true}})
k.seed(fbsdNLKED | fbsdCLKED | fbsdShifts1)
if got, _ := fbsdTypedBy(k, press(keyKP8, keyA)); got != "8A" {
t.Errorf("typed %q, want %q", got, "8A")
}
if k.isDown(keyLeftShift) || k.state != fbsdNLKED|fbsdCLKED {
t.Errorf("seeded state %#x, left shift down %v", k.state, k.isDown(keyLeftShift))
}
}
// AltGr, from a key some keymaps bind, and AltGr Lock, from a modifier that
// toggles it pressed alone, type the keys' AltGr entries
func TestFreeBSDConsoleAltGr(t *testing.T) {
km := testFreeBSDKeymap(t)
all := func(action uint32) fbsdKey {
return fbsdKey{actions: [8]uint32{action, action, action, action, action, action, action, action}, spcl: 0xff}
}
// AltGr looks every key up in the keymap's AltGr half, modifiers too
km.keys[fbsdKeyRightAlt] = all(fbsdASH)
km.keys[keyLeftShift] = all(fbsdLSHA)
km.keys[fbsdAltGrKeys+keyLeftShift] = all(fbsdLSHA)
km.keys[fbsdAltGrKeys+keyA] = fbsdKey{actions: [8]uint32{'α', 'Α', 1, 1, 'α', 'Α', 1, 1}, flgs: fbsdLockC}
k := &fbsdKeys{km: km}
strokes := slices.Concat(
hold(fbsdKeyRightAlt, press(keyA)),
press(keyA),
press(keyLeftShift), // AltGr Lock on
press(keyA),
hold(keyLeftShift, press(keyA)), // Shift with a key leaves the lock
press(keyLeftShift), // AltGr Lock off
press(keyA))
got, acts := fbsdTypedBy(k, strokes)
if got != "αaαΑa" {
t.Errorf("typed %q, want %q", got, "αaαΑa")
}
if want := []consoleAct{{leds: true}, {leds: true}}; !slices.Equal(acts, want) {
t.Errorf("asked %+v, want %+v", acts, want)
}
if k.state != 0 {
t.Errorf("state %#x left", k.state)
}
}
// Dead keys type what the accent map makes of the character after them, as
// vt does cooked: the accented character, the accent for a space or the dead
// key again, and nothing for a character the accent map has no pair for or a
// special key, which ends the dead key. Modifiers keep it pending.
func TestFreeBSDConsoleDeadKeys(t *testing.T) {
km := testFreeBSDKeymap(t)
km.keys[keyGrave].actions[0], km.keys[keyGrave].actions[1] = fbsdFACC, fbsdFACC+3 // grave, tilde
km.keys[keyGrave].actions[2] = fbsdFACC + 4 // macron, with no accent
km.keys[keyGrave].spcl |= 0x80 | 0x40 | 0x20
cases := []struct {
name string
strokes []stroke
want string
}{
{"dead key", press(keyGrave, keyA), "à"},
{"shifted dead key", slices.Concat(hold(keyLeftShift, press(keyGrave)), press(keyN)), "ñ"},
{"shifted character", slices.Concat(press(keyGrave), hold(keyLeftShift, press(keyA))), "À"},
{"dead key and space", press(keyGrave, keySpace), "`"},
{"dead key twice", press(keyGrave, keyGrave, keyA), "`a"},
{"no pair", press(keyGrave, keyB, keyA), "a"},
{"over a cursor key", press(keyGrave, fbsdKeyUp, keyA), "a"},
{"no accent", slices.Concat(hold(keyLeftCtrl, press(keyGrave)), press(keyA)), "a"},
}
for _, c := range cases {
k := &fbsdKeys{km: km}
if got, _ := fbsdTypedBy(k, c.strokes); got != c.want {
t.Errorf("%s: typed %q, want %q", c.name, got, c.want)
}
}
}
+134 -15
View File
@@ -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,45 @@ 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
// The console the backend runs on, nil for none, and how the reader
// switches its keyboard raw, and back
console keyConsole
keysRaw func() (ok, again bool)
keysCooked func()
// 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
// The console: whether its keyboard refused to go raw, for good (-1) or
// for this ask (-2), and the parser reading what it types for a key
consoleHas int8
typer *inputReader
mu sync.Mutex
running bool
}
@@ -69,6 +116,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,13 +176,28 @@ func (r *inputReader) readLoop() {
}()
for {
r.serviceKeys()
// Blocking read from backend
data, err := r.backend.Read(r.stopCh)
if err != nil {
r.end()
r.sendEvent(Event{Type: EventError, Err: err})
return
}
if r.source == KeySourceConsole {
r.consoleRead(data)
select {
case <-r.stopCh:
r.end()
r.sendEvent(Event{Type: EventClosed})
return
default:
continue
}
}
// A paste whose end marker never comes ends when input pauses, so
// keys are never swallowed for good; a read may block (wasm)
if r.pasting && time.Since(r.pasteSeen) > pasteIdle {
@@ -147,11 +210,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.end()
r.sendEvent(Event{Type: EventClosed})
return
default:
@@ -177,6 +241,14 @@ func (r *inputReader) readLoop() {
}
}
// end releases what is held as input ends, and the console's keyboard
func (r *inputReader) end() {
r.holds.ReleaseAll(r.sendEvent)
if r.source == KeySourceConsole {
r.console.cooked()
}
}
// parseInput parses raw bytes into events and returns bytes consumed (stop on incomplete sequence)
func (r *inputReader) parseInput(data []byte) int {
i := 0
@@ -226,7 +298,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 +317,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 +328,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 +355,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 +506,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 +541,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 +725,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 {
+25
View File
@@ -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 {
+144
View File
@@ -0,0 +1,144 @@
package terminal
// The console source: a kernel console's keyboard switched to raw keycodes.
// Each key types what the console types for it cooked, read as a legacy
// terminal's bytes are, and is held by its keycode.
// keyConsole is a kernel console whose keyboard the reader can switch to raw
// keycodes: the Linux console, FreeBSD's vt(4). The reader calls it from its
// goroutine; close comes from Fini, on another.
type keyConsole interface {
// prepare reads what translating keys needs and readies the switch: the
// keymap, the guard, output a Scroll Lock stopped. False when the console
// can't go raw.
prepare() bool
// raw switches the keyboard to raw keycodes, false when the console refuses
raw() bool
// cooked switches the keyboard back to the mode raw found
cooked()
// keys decodes raw keycodes, keeping a partial one for the next call, and
// reports each key event with what the console types for it
keys(data []byte, key func(code uint32, action KeyAction, typed []byte))
// front is whether the console is the one on screen, the one the
// keyboard types into
front() bool
// close switches the keyboard back if it is raw and ends the guard
close()
}
// consoleAct is what a key asks of the console beside typing
type consoleAct struct {
vt int // a console to switch to, from 1, or vtPrev or vtNext; zero for none
leds bool // the lock flags changed
start bool // restart output stopped with Scroll Lock
}
// Console switches a key asks for beside switching to a console by number
const (
vtPrev = -1 // the console before this one
vtNext = -2 // the console after this one
)
// consoleBackend is a backend on a kernel console
type consoleBackend interface {
keyConsole() keyConsole
}
// backendConsole is the console b runs on, nil for none
func backendConsole(b Backend) keyConsole {
if c, ok := b.(consoleBackend); ok {
return c.keyConsole()
}
return nil
}
// rawConsole switches the console's keyboard raw for releases; a console that
// refuses is not asked again, or not until the next ask when it may not
// refuse then
func (r *inputReader) rawConsole() {
// An ESC waiting for its timeout was Escape, pressed before the switch
if ev, ok := loneEscape[string(r.buf)]; ok {
r.sendKey(0, ev)
}
r.buf = r.buf[:0]
if r.keysRaw == nil {
r.consoleHas = -1
return
}
if ok, again := r.keysRaw(); !ok {
r.consoleHas = -1
if again {
r.consoleHas = -2
}
return
}
r.holds = KeyHolds{}
r.source, r.report = KeySourceConsole, KeyReportReleases
}
// consoleRead reads raw keycodes, and ends every hold while another console
// has the keyboard: their releases go there
func (r *inputReader) consoleRead(data []byte) {
if len(data) > 0 {
r.console.keys(data, r.consoleKey)
}
if !r.console.front() {
r.holds.ReleaseAll(r.sendEvent)
}
}
// consoleKey sends a key event as what the console types for it reads: the
// last key held by the keycode, any before it as taps, as a dead key's
// character the key types before its own
func (r *inputReader) consoleKey(code uint32, action KeyAction, typed []byte) {
if action == KeyRelease {
r.sendKey(code, Event{Type: EventKey, Action: KeyRelease})
return
}
var last Event
have := false
r.typedKeys(typed, func(ev Event) {
if have {
r.sendKey(0, last)
}
last, have = ev, true
})
if have {
last.Action = action
r.sendKey(code, last)
}
}
// typedKeys reads what a console types for one key as a legacy terminal's
// input reads, the key whole: an ESC it ends with is Escape
func (r *inputReader) typedKeys(typed []byte, key func(Event)) {
if len(typed) == 0 {
return
}
p := r.typer
if p == nil {
// A parser of its own: no paste, no protocol, no stop
p = &inputReader{eventCh: make(chan Event, 512)}
r.typer = p
}
p.dropCSI = false
n := p.parseInput(typed)
if ev, ok := loneEscape[string(typed[n:])]; ok {
p.sendKey(0, ev)
}
for {
select {
case ev := <-p.eventCh:
if ev.Type == EventKey && ev.Key != KeyNone {
key(ev)
}
default:
return
}
}
}
+289
View File
@@ -0,0 +1,289 @@
package terminal
import (
"strings"
"testing"
"time"
)
// fakeConsole is a backend on a Linux console: its keyboard goes raw when
// asked, unless it refuses, and raw, its input is K_MEDIUMRAW keycodes typed
// by the kernel's built-in keymap
type fakeConsole struct {
fakeBackend
refuse bool
notNow int // raw refusals left, as a console that can't go raw just now
isRaw bool
away bool
prepares int
rawAt int // how much output the terminal had written when the keyboard went raw
cookedAt int // and when it went back
closed bool
kb linuxKeys
dec mediumRaw
}
func newFakeConsole(t *testing.T) *fakeConsole {
return &fakeConsole{kb: linuxKeys{km: testKeymap(t)}}
}
func (f *fakeConsole) keySources() KeySource { return KeySourceConsole | KeySourceProtocol }
func (f *fakeConsole) keyConsole() keyConsole { return f }
func (f *fakeConsole) Fini() { f.close() } // as unixBackend's
func (f *fakeConsole) prepare() bool {
f.mu.Lock()
defer f.mu.Unlock()
f.prepares++
return !f.refuse
}
func (f *fakeConsole) raw() bool {
f.mu.Lock()
defer f.mu.Unlock()
if f.notNow > 0 {
f.notNow--
return false
}
f.isRaw, f.rawAt = true, f.out.Len()
f.kb.seed(0, 0)
return true
}
func (f *fakeConsole) cooked() {
f.mu.Lock()
defer f.mu.Unlock()
if f.isRaw {
f.isRaw, f.cookedAt = false, f.out.Len()
}
}
func (f *fakeConsole) keys(data []byte, key func(code uint32, action KeyAction, typed []byte)) {
f.dec.decode(data, func(code int, down bool) {
action := KeyRelease
if down {
action = KeyPress
if f.kb.isDown(code) {
action = KeyRepeat
}
}
typed, _ := f.kb.key(code, down, action == KeyRepeat)
key(uint32(code), action, typed)
})
}
func (f *fakeConsole) front() bool {
f.mu.Lock()
defer f.mu.Unlock()
return !f.away
}
func (f *fakeConsole) close() {
f.cooked()
f.mu.Lock()
f.closed = true
f.mu.Unlock()
}
// keyed hands the reader keycodes, as the raw console sends them
func (f *fakeConsole) keyed(codes ...byte) {
f.mu.Lock()
f.chunks = append(f.chunks, string(codes))
f.mu.Unlock()
}
// state reads the fake's fields under its lock
func (f *fakeConsole) state() (isRaw bool, prepares int, out string, rawAt, cookedAt int, closed bool) {
f.mu.Lock()
defer f.mu.Unlock()
return f.isRaw, f.prepares, f.out.String(), f.rawAt, f.cookedAt, f.closed
}
// up is a keycode's release byte
func up(code byte) byte { return code | 0x80 }
// keyEvent is a key event as the console source sends it
func keyEvent(action KeyAction, r rune, lost bool) Event {
return Event{Type: EventKey, Key: KeyRune, Rune: r, Action: action, Lost: lost}
}
// The console's keyboard goes raw when releases are asked of it, before the
// protocol. Raw, each key types as it does cooked and is held by its keycode:
// Shift let go first still releases 'A', a key held repeats, and another
// console taking the keyboard ends the holds Lost. Mouse reports are off while
// the keyboard is raw, the console having taken that first, and come back on
// with it. Going back to presses switches the keyboard back, and so does the
// input ending at Fini, before the backend closes the console.
func TestConsoleSourceComesAndGoes(t *testing.T) {
f := newFakeConsole(t)
term := newTerm(f, ColorModeNone)
if err := term.Init(); err != nil {
t.Fatal(err)
}
term.SetMouseMode(MouseModeClick)
term.SetKeyReport(KeyReportReleases, KeySourceConsole|KeySourceProtocol)
if ev := poll(t, term); ev.Type != EventKeyReport || ev.Report != KeyReportReleases || ev.Source != KeySourceConsole {
t.Fatalf("answered %+v, want releases from the console", ev)
}
isRaw, _, out, rawAt, _, _ := f.state()
if !isRaw {
t.Fatal("keyboard not raw")
}
if !strings.Contains(out[:rawAt], string(csiMouseClickOff)) {
t.Errorf("mouse reports still on as the keyboard went raw: %q", out[:rawAt])
}
f.keyed(keyLeftShift, keyA, up(keyLeftShift), up(keyA))
f.keyed(keyQ, keyQ, up(keyQ))
f.keyed(keyEnter, up(keyEnter))
want := []Event{
keyEvent(KeyPress, 'A', false), keyEvent(KeyRelease, 'A', false),
keyEvent(KeyPress, 'q', false), keyEvent(KeyRepeat, 'q', false), keyEvent(KeyRelease, 'q', false),
{Type: EventKey, Key: KeyEnter, Action: KeyPress}, {Type: EventKey, Key: KeyEnter, Action: KeyRelease},
}
for i, w := range want {
if ev := poll(t, term); ev != w {
t.Fatalf("event %d: %+v, want %+v", i, ev, w)
}
}
f.keyed(keyA)
if ev := poll(t, term); ev != keyEvent(KeyPress, 'a', false) {
t.Fatalf("got %+v, want a press of 'a'", ev)
}
f.mu.Lock()
f.away = true
f.mu.Unlock()
if ev := poll(t, term); ev != keyEvent(KeyRelease, 'a', true) {
t.Fatalf("got %+v, want 'a' released Lost", ev)
}
f.mu.Lock()
f.away = false
f.mu.Unlock()
term.SetKeyReport(KeyReportPresses, 0)
if ev := poll(t, term); ev.Type != EventKeyReport || ev.Report != KeyReportPresses || ev.Source != 0 {
t.Fatalf("answered %+v, want presses", ev)
}
waitFor(t, "mouse reports back on", func() bool {
isRaw, _, out, _, cookedAt, _ := f.state()
return !isRaw && strings.Contains(out[cookedAt:], string(csiMouseClickOn))
})
term.SetKeyReport(KeyReportReleases, KeySourceConsole)
poll(t, term)
f.mu.Lock()
f.closed = false
f.mu.Unlock()
term.input.stop()
if isRaw, _, _, _, _, closed := f.state(); isRaw || closed {
t.Errorf("input ended with the keyboard raw %v, the console closed %v", isRaw, closed)
}
term.Fini()
if _, _, _, _, _, closed := f.state(); !closed {
t.Error("Fini left the console open")
}
}
// A console that can't go raw is not asked again: the protocol is next, and
// the console here doesn't answer its questions
func TestConsoleRefusingIsAskedOnce(t *testing.T) {
f := newFakeConsole(t)
f.refuse = true
old := protoTimeout
protoTimeout = 50 * time.Millisecond
defer func() { protoTimeout = old }()
term := newTerm(f, ColorModeNone)
if err := term.Init(); err != nil {
t.Fatal(err)
}
defer term.Fini()
for range 2 {
term.SetKeyReport(KeyReportReleases, KeySourceConsole|KeySourceProtocol)
if ev := poll(t, term); ev.Type != EventKeyReport || ev.Report != KeyReportPresses {
t.Fatalf("answered %+v, want presses", ev)
}
}
if _, prepares, _, _, _, _ := f.state(); prepares != 1 {
t.Errorf("console asked %d times, want once", prepares)
}
}
// A console that can't go raw just now is asked again at the next ask, and
// mouse reports come back on after the refusal
func TestConsoleRefusingNowIsAskedAgain(t *testing.T) {
f := newFakeConsole(t)
f.notNow = 1
old := protoTimeout
protoTimeout = 50 * time.Millisecond
defer func() { protoTimeout = old }()
term := newTerm(f, ColorModeNone)
if err := term.Init(); err != nil {
t.Fatal(err)
}
defer term.Fini()
term.SetMouseMode(MouseModeClick)
term.SetKeyReport(KeyReportReleases, KeySourceConsole|KeySourceProtocol)
if ev := poll(t, term); ev.Type != EventKeyReport || ev.Report != KeyReportPresses {
t.Fatalf("answered %+v, want presses", ev)
}
waitFor(t, "mouse reports back on", func() bool {
_, _, out, _, _, _ := f.state()
return strings.Count(out, string(csiMouseClickOn)) == 2
})
term.SetKeyReport(KeyReportReleases, KeySourceConsole|KeySourceProtocol)
if ev := poll(t, term); ev.Type != EventKeyReport || ev.Source != KeySourceConsole {
t.Fatalf("answered %+v, want releases from the console", ev)
}
if _, prepares, _, _, _, _ := f.state(); prepares != 2 {
t.Errorf("console prepared %d times, want twice", prepares)
}
}
// Mouse reporting asked for while the keyboard is raw starts once it is
// cooked again
func TestConsoleHoldsMouseReportsWhileRaw(t *testing.T) {
f := newFakeConsole(t)
term := newTerm(f, ColorModeNone)
if err := term.Init(); err != nil {
t.Fatal(err)
}
defer term.Fini()
term.SetKeyReport(KeyReportReleases, KeySourceConsole)
poll(t, term)
term.SetMouseMode(MouseModeClick | MouseModeDrag)
if _, _, out, rawAt, _, _ := f.state(); strings.Contains(out[rawAt:], string(csiMouseSGROn)) {
t.Fatalf("mouse reports on while raw: %q", out[rawAt:])
}
term.SetKeyReport(KeyReportPresses, 0)
poll(t, term)
waitFor(t, "mouse reports on", func() bool {
_, _, out, _, cookedAt, _ := f.state()
return strings.Contains(out[cookedAt:], string(csiMouseDragOn))
})
}
// A key whose string reads as several keys taps all but the last, which it
// holds by its keycode
func TestConsoleKeyTypingSeveralKeys(t *testing.T) {
f := newFakeConsole(t)
f.kb.km.funcs[0] = "ab" // F1
term := newTerm(f, ColorModeNone)
if err := term.Init(); err != nil {
t.Fatal(err)
}
defer term.Fini()
term.SetKeyReport(KeyReportReleases, KeySourceConsole)
poll(t, term)
f.keyed(keyF1, up(keyF1))
want := []Event{
keyEvent(KeyPress, 'a', false), keyEvent(KeyRelease, 'a', false), keyEvent(KeyPress, 'b', false),
keyEvent(KeyRelease, 'b', false),
}
for i, w := range want {
if ev := poll(t, term); ev != w {
t.Fatalf("event %d: %+v, want %+v", i, ev, w)
}
}
}
+170
View File
@@ -0,0 +1,170 @@
//go:build linux || freebsd
package terminal
import (
"io"
"os"
"os/exec"
"os/signal"
"syscall"
"time"
"unsafe"
"golang.org/x/sys/unix"
)
// The guard restores a console keyboard the game leaves raw by dying where no
// handler of its own runs: SIGKILL, the OOM killer, a Go fatal error. Before
// the first raw switch the package runs its program's binary again with
// guardEnv set, which the package's init catches before main. The guard keeps
// the console and the read end of a pipe whose write end only the game holds:
// the pipe's end is the game's, and if the keyboard is raw then, the guard
// switches it back, restores the termios and shows the cursor.
// guardEnv marks a run of the binary as a guard, which ps lists as guardName
const (
guardEnv = "LIXENWRAITH_TERMINAL_GUARD"
guardName = "console-guard"
)
// The guard's messages: armed with the keyboard mode and termios to restore,
// or disarmed
const (
guardArm = 'A'
guardDisarm = 'D'
)
func init() {
if os.Getenv(guardEnv) == "1" {
nameGuard()
os.Exit(runGuard(os.NewFile(3, "guard"), os.NewFile(4, "console")))
}
}
// keyGuard is the game's end of its guard
type keyGuard struct {
w *os.File // the pipe's write end, closed on exec in any other child
}
// startGuard runs the guard over tty
func startGuard(tty *os.File) (*keyGuard, error) {
exe, err := guardExecutable()
if err != nil {
return nil, err
}
r, w, err := os.Pipe()
if err != nil {
return nil, err
}
defer r.Close()
// Named apart from the game, which on Linux spares it a kill by the
// game's name
cmd := exec.Command(exe)
cmd.Args = []string{guardName}
cmd.Env = append(os.Environ(), guardEnv+"=1")
cmd.ExtraFiles = []*os.File{r, tty}
// A group of its own, so a kill of the game's job leaves it to restore.
// A game leading its session takes the console with it as it dies, the
// kernel hanging the console up: its guard leads a session of its own, to
// take the console up again.
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
if sid, err := unix.Getsid(0); err == nil && sid == os.Getpid() {
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
}
if err := cmd.Start(); err != nil {
w.Close()
return nil, err
}
go cmd.Wait()
return &keyGuard{w: w}, nil
}
// termiosBytes is t as the guard reads it, the same binary on both ends
func termiosBytes(t *unix.Termios) []byte {
return unsafe.Slice((*byte)(unsafe.Pointer(t)), unsafe.Sizeof(*t))
}
// arm tells the guard the keyboard is about to go raw, and what restores it;
// false when the guard is gone. One write, under the pipe's atomic size.
func (g *keyGuard) arm(mode int, cooked *unix.Termios) bool {
_, err := g.w.Write(append([]byte{guardArm, byte(mode)}, termiosBytes(cooked)...))
return err == nil
}
// disarm tells the guard the keyboard is cooked again
func (g *keyGuard) disarm() { g.w.Write([]byte{guardDisarm}) }
// close ends the guard, which exits disarmed
func (g *keyGuard) close() { g.w.Close() }
// guardReset is what the guard writes after restoring the keyboard: mouse
// reports off, the cursor shown, attributes reset, but no RIS, which would
// clear the message the game died with
var guardReset = string(csiMouseMotionOff) + string(csiMouseDragOff) + string(csiMouseClickOff) +
string(csiMouseSGROff) + string(csiPasteOff) + string(csiCursorShow) + string(csiSGR0) + string(csiAutoWrapOn)
// runGuard waits on cmds until the game is gone, and restores the console
// tty if the keyboard was raw then
func runGuard(cmds, tty *os.File) int {
signal.Ignore(syscall.SIGHUP, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGTERM,
syscall.SIGTSTP, syscall.SIGTTIN, syscall.SIGTTOU, syscall.SIGPIPE)
path := consolePath(tty)
armed, mode, cooked := guardWait(cmds)
if armed {
guardRestore(tty, path, mode, func(fd int) {
unix.IoctlSetTermios(fd, tcsets, &cooked)
unix.Write(fd, []byte(guardReset))
})
}
return 0
}
// guardReopenFor is how long the guard tries to open again a console its
// game took along
const guardReopenFor = 2 * time.Second
// guardReopen opens the console at path again, until its keyboard switches
// back to mode or guardReopenFor passes, and has rest restore the rest
// through it
func guardReopen(path string, mode int, rest func(fd int)) {
if path == "" {
return
}
for end := time.Now().Add(guardReopenFor); time.Now().Before(end); time.Sleep(10 * time.Millisecond) {
f, err := os.OpenFile(path, os.O_RDWR, 0)
if err != nil {
continue
}
ok := unix.IoctlSetInt(int(f.Fd()), kdSetKeyboardMode, mode) == nil
if ok {
rest(int(f.Fd()))
}
f.Close()
if ok {
return
}
}
}
// guardWait reads the game's messages to their end: whether the guard was
// armed then, and with what
func guardWait(cmds io.Reader) (armed bool, mode int, cooked unix.Termios) {
var msg [1]byte
arg := make([]byte, 1+len(termiosBytes(&cooked)))
for {
if _, err := io.ReadFull(cmds, msg[:]); err != nil {
return armed, mode, cooked
}
switch msg[0] {
case guardArm:
if _, err := io.ReadFull(cmds, arg); err != nil {
return armed, mode, cooked
}
armed, mode = true, int(arg[0])
copy(termiosBytes(&cooked), arg[1:])
case guardDisarm:
armed = false
}
}
}
+171
View File
@@ -0,0 +1,171 @@
//go:build linux
package terminal
import (
"bufio"
"bytes"
"fmt"
"io"
"os"
"os/exec"
"strings"
"syscall"
"testing"
"time"
"golang.org/x/sys/unix"
)
// The guard ends armed with the last arm's mode and termios when the arms
// outnumber the disarms, and disarmed otherwise
func TestGuardFollowsArmsToTheEnd(t *testing.T) {
arm := func(mode byte, iflag uint32) []byte {
tio := unix.Termios{Iflag: iflag}
return append([]byte{guardArm, mode}, termiosBytes(&tio)...)
}
cases := []struct {
name string
msgs [][]byte
armed bool
mode int
iflag uint32
}{
{"never armed", nil, false, 0, 0},
{"armed", [][]byte{arm(3, 7)}, true, 3, 7},
{"disarmed", [][]byte{arm(3, 7), {guardDisarm}}, false, 0, 0},
{"armed again", [][]byte{arm(3, 7), {guardDisarm}, arm(1, 9)}, true, 1, 9},
{"cut off arming", [][]byte{{guardArm, 3, 1, 2}}, false, 0, 0},
}
for _, c := range cases {
armed, mode, tio := guardWait(bytes.NewReader(bytes.Join(c.msgs, nil)))
if armed != c.armed || armed && (mode != c.mode || tio.Iflag != c.iflag) {
t.Errorf("%s: armed %v mode %d iflag %d, want %v %d %d", c.name, armed, mode, tio.Iflag, c.armed, c.mode, c.iflag)
}
}
}
// openPty opens a pty pair, the far end standing in for the console
func openPty(t *testing.T) (master, tty *os.File) {
t.Helper()
master, err := os.OpenFile("/dev/ptmx", os.O_RDWR|syscall.O_NOCTTY, 0)
if err != nil {
t.Skip("no pty:", err)
}
if err := unix.IoctlSetPointerInt(int(master.Fd()), unix.TIOCSPTLCK, 0); err != nil {
t.Fatal(err)
}
n, err := unix.IoctlGetInt(int(master.Fd()), unix.TIOCGPTN)
if err != nil {
t.Fatal(err)
}
tty, err = os.OpenFile(fmt.Sprintf("/dev/pts/%d", n), os.O_RDWR|syscall.O_NOCTTY, 0)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { tty.Close(); master.Close() })
return master, tty
}
// guardGame is a game for TestGuardRestoresWhenTheGameIsKilled: it guards the
// pty it is handed, arms the guard with the pty's termios as cooked, makes
// the pty raw, says so and waits to be killed
func TestGuardGame(t *testing.T) {
path := os.Getenv("TERMINAL_TEST_GUARD_GAME")
if path == "" {
t.Skip("run by TestGuardRestoresWhenTheGameIsKilled")
}
tty, err := os.OpenFile(path, os.O_RDWR|syscall.O_NOCTTY, 0)
if err != nil {
t.Fatal(err)
}
cooked, err := unix.IoctlGetTermios(int(tty.Fd()), tcgets)
if err != nil {
t.Fatal(err)
}
g, err := startGuard(tty)
if err != nil {
t.Fatal(err)
}
if !g.arm(kUnicode, cooked) {
t.Fatal("arming failed")
}
raw := *cooked
raw.Lflag &^= unix.ICANON | unix.ECHO | unix.ISIG
if err := unix.IoctlSetTermios(int(tty.Fd()), tcsets, &raw); err != nil {
t.Fatal(err)
}
if os.Getenv("TERMINAL_TEST_GUARD_DISARM") != "" {
g.disarm()
}
fmt.Println("raw")
time.Sleep(time.Minute)
}
// A game killed with its console raw leaves the guard to restore it: the
// termios the game found, and the cursor shown, with no reset that would
// clear the game's last words; a game that disarmed first leaves it as it was
func TestGuardRestoresWhenTheGameIsKilled(t *testing.T) {
for _, disarm := range []bool{false, true} {
master, tty := openPty(t)
cooked, err := unix.IoctlGetTermios(int(tty.Fd()), tcgets)
if err != nil {
t.Fatal(err)
}
game := exec.Command(os.Args[0], "-test.run=^TestGuardGame$", "-test.v")
game.Env = append(os.Environ(), "TERMINAL_TEST_GUARD_GAME="+tty.Name())
if disarm {
game.Env = append(game.Env, "TERMINAL_TEST_GUARD_DISARM=1")
}
stdout, err := game.StdoutPipe()
if err != nil {
t.Fatal(err)
}
if err := game.Start(); err != nil {
t.Fatal(err)
}
lines := bufio.NewScanner(stdout)
for lines.Scan() && lines.Text() != "raw" {
}
game.Process.Kill()
game.Wait()
restored := func() bool {
now, err := unix.IoctlGetTermios(int(tty.Fd()), tcgets)
return err == nil && now.Lflag == cooked.Lflag
}
if !disarm {
waitFor(t, "the termios restored", restored)
out := readPending(t, master)
if !strings.Contains(out, string(csiCursorShow)) || strings.Contains(out, string(csiRIS)) {
t.Errorf("guard wrote %q, want the cursor shown and no reset", out)
}
continue
}
time.Sleep(200 * time.Millisecond)
if restored() {
t.Error("a disarmed guard restored the termios")
}
if out := readPending(t, master); out != "" {
t.Errorf("a disarmed guard wrote %q", out)
}
}
}
// readPending is what the pty's master has to read now
func readPending(t *testing.T, master *os.File) string {
t.Helper()
fds := []unix.PollFd{{Fd: int32(master.Fd()), Events: unix.POLLIN}}
var out []byte
buf := make([]byte, 4096)
for {
if n, err := unix.Poll(fds, 100); err != nil || n == 0 {
return string(out)
}
n, err := master.Read(buf)
if err != nil && err != io.EOF || n == 0 {
return string(out)
}
out = append(out, buf[:n]...)
}
}
+417
View File
@@ -0,0 +1,417 @@
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
if r.consoleHas == -2 {
r.consoleHas = 0 // refused the last ask only
}
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&KeySourceConsole != 0 && r.consoleHas >= 0:
return KeySourceConsole
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 target == KeySourceConsole:
r.rawConsole()
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
switch source {
case KeySourceProtocol:
r.request(exchangePop, false, csiKeyPop, csiFocusOff, csiDA1)
case KeySourceConsole:
r.keysCooked()
}
}
// 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
}
+360
View File
@@ -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
View File
@@ -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
}
+142
View File
@@ -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)
}
}
}
+485
View File
@@ -0,0 +1,485 @@
package terminal
import (
"strconv"
"unicode/utf8"
)
// The Linux console's keymap and its translation of keycodes, after the
// kernel's own (drivers/tty/vt/keyboard.c), so a keyboard switched to raw
// keycodes types what the console types with it cooked. Keysyms are kept as
// the kernel keeps them: 0xf000 | type<<8 | value for a typed one, anything
// less a Unicode code point.
// Keysym types (linux/keyboard.h)
const (
ktLatin = iota // a Latin-1 character
ktFn // a function key's string
ktSpec // a console function: Enter, the locks, console switching
ktPad // a keypad key
ktDead // a dead key
ktCons // a switch to a console
ktCur // a cursor key
ktShift // a modifier
ktMeta // Alt with a character
ktASCII // a digit of a code typed with Alt held
ktLock // a modifier lock
ktLetter // a letter, which Caps Lock shifts
ktSLock // a sticky modifier
ktDead2 // a dead key by its character
ktBrl // a braille dot
ktCSI // a CSI ~ key that says its modifiers (kernel 7.1 and later)
)
// Modifier groups, the bits of the shift state (linux/keyboard.h)
const (
kgShift = iota
kgAltGr
kgCtrl
kgAlt
kgShiftL
kgShiftR
kgCtrlL
kgCtrlR
kgCapsShift
nrShift
)
// The lock flags, as KDGKBLED reports them
const (
ledScroll = 1 << iota
ledNum
ledCaps
)
const (
nrKeys = 256 // keycodes a keymap maps
kNoSuchMap = 0x027f // KDGKBENT's answer for the first key of a map the keymap lacks
symHole = 0xf200 // K_HOLE as the kernel keeps it: a key that does nothing
)
// linuxKeymap is a Linux console keymap as KDGKBENT, KDGKBSENT and
// KDGKBDIACRUC read it
type linuxKeymap struct {
maps [256]*[nrKeys]uint16 // by shift state, nil where the keymap has none
funcs [256]string // the function keys' strings
accents []linuxAccent // what a dead key makes of the character after it
escMeta bool // Meta keys type ESC and the character (K_ESCPREFIX), else it with bit 7 set
csiMods bool // cursor and CSI keys say their modifiers, as from kernel 7.1
}
// linuxAccent is an accented character: a dead key's character and the
// character typed after it
type linuxAccent struct {
diacr, base, result rune
}
// linuxKeys translates keycodes as the Linux console does in Unicode mode,
// keeping its modifiers, locks, lock flags and dead keys. Codes typed with
// Alt and the keypad and braille type nothing; the keypad and cursor keys
// type as they do outside application mode, which the package never sets.
type linuxKeys struct {
km *linuxKeymap
down [nrKeys / 64]uint64 // keys down
shiftDown [nrShift]uint8 // keys down of each modifier
shift uint16 // modifiers down
lock uint16 // modifiers locked
slock uint16 // modifiers stuck for the next key
leds uint8 // lock flags
diacr rune // a dead key's character, for the next character
deadNext bool // compose: the next character is a dead key's
}
// seed starts translation from the console's state: the modifiers it has
// down and its lock flags; keys already down are not known
func (k *linuxKeys) seed(shift, leds uint8) {
*k = linuxKeys{km: k.km, leds: leds & (ledScroll | ledNum | ledCaps)}
for g := range nrShift {
if shift&(1<<g) != 0 {
k.shiftDown[g], k.shift = 1, k.shift|1<<g
}
}
}
// isDown is whether a key is down
func (k *linuxKeys) isDown(code int) bool {
return code < nrKeys && k.down[code/64]&(1<<(code%64)) != 0
}
// key translates one keycode event: down for a press, rep for a repeat. It
// returns what the console types for it and what it asks of the console.
func (k *linuxKeys) key(code int, down, rep bool) (typed []byte, act consoleAct) {
if code >= nrKeys {
return nil, act // past every keymap
}
if down {
k.down[code/64] |= 1 << (code % 64)
} else {
k.down[code/64] &^= 1 << (code % 64)
}
state := ((k.shift | k.slock) ^ k.lock) & 0xff
km := k.km.maps[state]
// A key that says its modifiers falls back to the plain map when the
// modifiers have no map: the kernel compares the entry with the bare
// K_HOLE, which no kept entry is, so a hole in a map stays a hole
if km == nil && state != 0 && k.km.csiMods && k.km.maps[0] != nil {
if t := symType(k.km.maps[0][code]); t == ktCur || t == ktCSI {
km = k.km.maps[0]
}
}
if km == nil {
k.computeShift()
k.slock = 0
return nil, act
}
sym := km[code]
if sym < 0xf000 {
if down {
typed = k.unicode(typed, rune(sym))
}
return typed, act
}
t, val := symType(sym), byte(sym)
if t == ktLetter {
t = ktLatin
// The kernel takes the shifted entry's value, whatever its type
if k.leds&ledCaps != 0 {
if m := k.km.maps[state^1<<kgShift]; m != nil {
val = byte(m[code])
}
}
}
switch t {
case ktLatin:
if down {
typed = k.unicode(typed, rune(val))
}
case ktFn:
if down {
typed = append(typed, k.km.funcs[val]...)
}
case ktSpec:
if down {
typed, act = k.spec(val, rep)
}
case ktPad:
if down {
typed = k.pad(val)
}
case ktDead:
if down && int(val) < len(deadChars) {
typed = k.dead(typed, rune(deadChars[val]))
}
case ktDead2:
if down {
typed = k.dead(typed, rune(val))
}
case ktCons:
if down {
act.vt = int(val) + 1
}
case ktCur:
if down {
typed = k.cur(val)
}
case ktShift:
k.shiftKey(val, down, rep)
case ktMeta:
if down && k.km.escMeta {
typed = append(typed, 0x1b, val)
} else if down {
typed = append(typed, val|0x80)
}
case ktLock:
if down && !rep && val < 16 {
k.lock ^= 1 << val
}
case ktSLock:
k.shiftKey(val, down, rep)
if down && !rep && val < 16 {
k.slock ^= 1 << val
if k.km.maps[(k.lock^k.slock)&0xff] == nil {
k.slock = 1 << val
}
}
case ktCSI:
if down && k.km.csiMods {
typed = k.csi(val)
}
}
if t != ktSLock {
k.slock = 0
}
return typed, act
}
// deadChars are the characters of the dead keys KT_DEAD names by number, as
// the accent table knows them
const deadChars = "`'^~\",_U.*=cki#o!?+-)(:n;$@"
// unicode types ch, or what a dead key before it makes of it; after compose,
// ch is the dead key
func (k *linuxKeys) unicode(typed []byte, ch rune) []byte {
if k.diacr != 0 {
ch, typed = k.combine(typed, ch)
}
if k.deadNext {
k.deadNext, k.diacr = false, ch
return typed
}
return utf8.AppendRune(typed, ch)
}
// dead is a dead key: the next character takes its accent; one already
// pending combines with it first
func (k *linuxKeys) dead(typed []byte, ch rune) []byte {
if k.diacr != 0 {
ch, typed = k.combine(typed, ch)
}
k.diacr = ch
return typed
}
// combine is what the pending dead key makes of ch: their accented character,
// the dead key's own for space or itself, else ch with the dead key's
// character typed before it
func (k *linuxKeys) combine(typed []byte, ch rune) (rune, []byte) {
d := k.diacr
k.diacr = 0
for _, a := range k.km.accents {
if a.diacr == d && a.base == ch {
return a.result, typed
}
}
if ch == ' ' || ch == d {
return d, typed
}
return ch, utf8.AppendRune(typed, d)
}
// symType is a typed keysym's type, -1 for a Unicode one
func symType(sym uint16) int {
if sym < 0xf000 {
return -1
}
return int(sym>>8) - 0xf0
}
// shiftKey moves a modifier, which a second key of the same group holds down
// until both are up; Caps Shift is Shift that ends Caps Lock
func (k *linuxKeys) shiftKey(val byte, down, rep bool) {
if rep || val >= nrShift {
return
}
if val == kgCapsShift {
val = kgShift
if down {
k.leds &^= ledCaps // the kernel ends Caps Lock itself, raw or not
}
}
switch {
case down && k.shiftDown[val] < 255:
k.shiftDown[val]++
case !down && k.shiftDown[val] > 0:
k.shiftDown[val]--
}
if k.shiftDown[val] > 0 {
k.shift |= 1 << val
} else {
k.shift &^= 1 << val
}
}
// computeShift counts the modifiers again from the keys down, by the plain
// map, as the kernel does when a key finds no map
func (k *linuxKeys) computeShift() {
k.shift, k.shiftDown = 0, [nrShift]uint8{}
plain := k.km.maps[0]
if plain == nil {
return
}
for code := range nrKeys {
if !k.isDown(code) {
continue
}
t, val := symType(plain[code]), byte(plain[code])
if t != ktShift && t != ktSLock {
continue
}
if val == kgCapsShift {
val = kgShift
}
if val < nrShift {
k.shiftDown[val]++
k.shift |= 1 << val
}
}
}
// spec runs a console function key (KT_SPEC) as the console does cooked.
// The register and memory dumps, scrollback, Ctrl+Alt+Del, spawning a console
// and the last console do nothing; SAK the kernel runs itself, raw or not.
// Scroll Lock only ever restarts output, so a game can't be frozen.
func (k *linuxKeys) spec(val byte, rep bool) ([]byte, consoleAct) {
var act consoleAct
switch val {
case 0: // K_HOLE
k.computeShift()
case 1: // K_ENTER, after a dead key pending
var typed []byte
if k.diacr != 0 {
typed, k.diacr = utf8.AppendRune(typed, k.diacr), 0
}
return append(typed, '\r'), act
case 5: // K_BREAK, which a raw tty reads as NUL
return []byte{0}, act
case 7: // K_CAPS
if !rep {
k.leds ^= ledCaps
act.leds = true
}
case 8, 19: // K_NUM, K_BARENUMLOCK
if !rep {
k.leds ^= ledNum
act.leds = true
}
case 9: // K_HOLD
act.start = !rep
case 13: // K_CAPSON
if !rep && k.leds&ledCaps == 0 {
k.leds |= ledCaps
act.leds = true
}
case 14: // K_COMPOSE
k.deadNext = true
case 16: // K_DECRCONSOLE
act.vt = vtPrev
case 17: // K_INCRCONSOLE
act.vt = vtNext
}
return nil, act
}
// padChars are what the keypad keys type with Num Lock on (K_P0 to K_PPARENR)
const padChars = "0123456789+-*/\r,.?()"
// The function strings the keypad types with Num Lock off
const (
fnFind = 20
fnInsert = 21
fnRemove = 22
fnSelect = 23
fnPgUp = 24
fnPgDn = 25
)
// pad is a keypad key: its character with Num Lock on, else the navigation
// key it doubles as
func (k *linuxKeys) pad(val byte) []byte {
if k.leds&ledNum == 0 {
switch val {
case 15, 16: // K_PCOMMA, K_PDOT
return []byte(k.km.funcs[fnRemove])
case 0:
return []byte(k.km.funcs[fnInsert])
case 1:
return []byte(k.km.funcs[fnSelect])
case 2:
return k.cur(0)
case 3:
return []byte(k.km.funcs[fnPgDn])
case 4:
return k.cur(1)
case 5:
return []byte("\x1b[G")
case 6:
return k.cur(2)
case 7:
return []byte(k.km.funcs[fnFind])
case 8:
return k.cur(3)
case 9:
return []byte(k.km.funcs[fnPgUp])
}
}
if int(val) < len(padChars) {
return []byte{padChars[val]}
}
return nil
}
// cur is a cursor key (K_DOWN, K_LEFT, K_RIGHT, K_UP): CSI letter, or with
// modifiers down on a kernel that says them, CSI 1 ; modifiers letter
func (k *linuxKeys) cur(val byte) []byte {
if val > 3 {
return nil
}
ch := "BDCA"[val]
if mod := k.csiMod(); mod > 1 && k.km.csiMods {
return []byte{0x1b, '[', '1', ';', '0' + byte(mod), ch}
}
return []byte{0x1b, '[', ch}
}
// csi is a KT_CSI key: CSI value ~, or CSI value ; modifiers ~
func (k *linuxKeys) csi(val byte) []byte {
if val > 99 {
return nil
}
typed := strconv.AppendInt([]byte{0x1b, '['}, int64(val), 10)
if mod := k.csiMod(); mod > 1 {
typed = append(typed, ';', '0'+byte(mod))
}
return append(typed, '~')
}
// csiMod is xterm's modifier parameter for the modifiers down: Shift, Alt and
// Ctrl, the kernel leaving out their left and right groups and AltGr, which
// keymaps use to pick a layout
func (k *linuxKeys) csiMod() int {
mod := 1
if k.shift&(1<<kgShift) != 0 {
mod++
}
if k.shift&(1<<kgAlt) != 0 {
mod += 2
}
if k.shift&(1<<kgCtrl) != 0 {
mod += 4
}
return mod
}
// mediumRaw decodes the Linux console's K_MEDIUMRAW input: a keycode under
// 128 is one byte, bit 7 set on release; a higher one is a byte of 0, 0x80 on
// release, then its high and low 7 bits, each with bit 7 set
type mediumRaw struct {
up bool
high int
n int // bytes of a long keycode seen
}
// decode reports each keycode event in data, keeping a long keycode begun at
// its end for the next call
func (m *mediumRaw) decode(data []byte, key func(code int, down bool)) {
for _, b := range data {
if m.n > 0 && b&0x80 != 0 {
if m.n++; m.n == 2 {
m.high = int(b & 0x7f)
continue
}
m.n = 0
key(m.high<<7|int(b&0x7f), !m.up)
continue
}
m.n = 0 // a long keycode broken off is dropped
if b&0x7f == 0 {
m.up, m.n = b != 0, 1
continue
}
key(int(b&0x7f), b&0x80 == 0)
}
}
+301
View File
@@ -0,0 +1,301 @@
package terminal
import (
"bufio"
"os"
"slices"
"strconv"
"strings"
"testing"
)
// testKeymap is the kernel's built-in keymap (US), as KDGKBENT and KDGKBSENT
// read it from a Linux 6.18 console
func testKeymap(t *testing.T) *linuxKeymap {
t.Helper()
f, err := os.Open("testdata/linux-keymap.txt")
if err != nil {
t.Fatal(err)
}
defer f.Close()
km := &linuxKeymap{escMeta: true}
sc := bufio.NewScanner(f)
sc.Buffer(nil, 1<<16)
for sc.Scan() {
kind, rest, _ := strings.Cut(sc.Text(), " ")
n, rest, _ := strings.Cut(rest, " ")
i, err := strconv.Atoi(n)
if err != nil {
t.Fatal(err)
}
switch kind {
case "map":
m := new([nrKeys]uint16)
for j, v := range strings.Fields(rest) {
x, err := strconv.ParseUint(v, 16, 16)
if err != nil {
t.Fatal(err)
}
m[j] = uint16(x) ^ 0xf000
}
km.maps[i] = m
case "fn":
if km.funcs[i], err = strconv.Unquote(rest); err != nil {
t.Fatal(err)
}
}
}
return km
}
// Linux input keycodes
const (
keyEsc = 1
key1 = 2
key2 = 3
keyBackspace = 14
keyTab = 15
keyQ = 16
keyO = 24
keyEnter = 28
keyLeftCtrl = 29
keyA = 30
keyS = 31
keyGrave = 41
keyLeftShift = 42
keyB = 48
keyN = 49
keyRtShift = 54
keyLeftAlt = 56
keySpace = 57
keyCapsLock = 58
keyF1 = 59
keyF2 = 60
keyF6 = 64
keyNumLock = 69
keyScrollLk = 70
keyKP8 = 72
keyKP5 = 76
keyKP0 = 82
keyKPDot = 83
keyKPEnter = 96
keyRightAlt = 100
keyHome = 102
keyUp = 103
keyPageUp = 104
keyLeft = 105
keyRight = 106
keyDelete = 111
keyPause = 119
keyCompose = 127
)
// stroke is a key going down (repeating when already down) or up
type stroke struct {
code int
down bool
}
func press(codes ...int) []stroke {
var s []stroke
for _, c := range codes {
s = append(s, stroke{c, true}, stroke{c, false})
}
return s
}
func hold(code int, inner ...[]stroke) []stroke {
s := []stroke{{code, true}}
for _, in := range inner {
s = append(s, in...)
}
return append(s, stroke{code, false})
}
// typedBy is what the console types for strokes, and what they ask of it
func typedBy(k *linuxKeys, strokes []stroke) (string, []consoleAct) {
var out []byte
var acts []consoleAct
for _, s := range strokes {
typed, act := k.key(s.code, s.down, s.down && k.isDown(s.code))
out = append(out, typed...)
if act != (consoleAct{}) {
acts = append(acts, act)
}
}
return string(out), acts
}
// Keys type what the Linux console types for them cooked, by its keymap and
// the modifiers and locks they move: Shift into the character, Caps Lock on
// letters only, Ctrl as control characters, Alt as an ESC prefix, the
// function and editing keys as their strings, the keypad by Num Lock, and
// cursor keys with their modifiers only on a kernel that says them
func TestLinuxConsoleTypesAsCooked(t *testing.T) {
cases := []struct {
name string
strokes []stroke
want string // before kernel 7.1
csiMods string // from 7.1, when it differs
}{
{"letter", press(keyA), "a", ""},
{"shifted letter", hold(keyLeftShift, press(keyA)), "A", ""},
{"shifted digit", hold(keyLeftShift, press(key1)), "!", ""},
{"caps lock", slices.Concat(press(keyCapsLock), press(keyA, key1), press(keyCapsLock), press(keyA)), "A1a", ""},
{"caps lock with shift", slices.Concat(press(keyCapsLock), hold(keyLeftShift, press(keyA))), "a", ""},
{"ctrl letter", hold(keyLeftCtrl, press(keyA)), "\x01", ""},
{"ctrl shift letter", hold(keyLeftCtrl, hold(keyLeftShift, press(keyA))), "\x01", ""},
{"ctrl space", hold(keyLeftCtrl, press(keySpace)), "\x00", ""},
{"ctrl backspace", hold(keyLeftCtrl, press(keyBackspace)), "\x08", ""},
{"alt letter", hold(keyLeftAlt, press(keyA)), "\x1ba", ""},
{"alt enter", hold(keyLeftAlt, press(keyEnter)), "\x1b\r", ""},
{"ctrl alt letter", hold(keyLeftCtrl, hold(keyLeftAlt, press(keyA))), "\x1b\x01", ""},
{"altgr", hold(keyRightAlt, press(key2)), "@", ""},
{"enter tab escape backspace", press(keyEnter, keyTab, keyEsc, keyBackspace), "\r\t\x1b\x7f", ""},
{"function keys", press(keyF1, keyF6), "\x1b[[A\x1b[17~", ""},
{"shifted function key", hold(keyLeftShift, press(keyF1)), "\x1b[23~", ""},
{"editing keys", press(keyHome, keyDelete, keyPageUp, keyPause), "\x1b[1~\x1b[3~\x1b[5~\x1b[P", ""},
{"cursor", press(keyUp, keyLeft), "\x1b[A\x1b[D", ""},
{"shifted cursor", hold(keyLeftShift, press(keyUp)), "\x1b[A", "\x1b[1;2A"},
{"ctrl cursor", hold(keyLeftCtrl, press(keyRight)), "\x1b[C", "\x1b[1;5C"},
{"alt shift cursor, no map", hold(keyLeftAlt, hold(keyLeftShift, press(keyUp))), "", "\x1b[1;4A"},
{"keypad, num lock off", press(keyKP8, keyKP5, keyKP0, keyKPDot, keyKPEnter), "\x1b[A\x1b[G\x1b[2~\x1b[3~\r", ""},
{"keypad, num lock on", slices.Concat(press(keyNumLock), press(keyKP8, keyKP0, keyKPDot)), "80.", ""},
{"repeat", []stroke{{keyQ, true}, {keyQ, true}, {keyQ, true}, {keyQ, false}}, "qqq", ""},
{"shift released first", []stroke{{keyLeftShift, true}, {keyA, true}, {keyLeftShift, false}, {keyA, true}, {keyA, false}}, "Aa", ""},
{"two shifts", []stroke{{keyLeftShift, true}, {keyRtShift, true}, {keyLeftShift, false}, {keyA, true}, {keyA, false}, {keyRtShift, false}}, "A", ""},
{"inert keys", slices.Concat(hold(keyLeftShift, press(keyPageUp)), hold(keyLeftCtrl, hold(keyLeftAlt, press(keyDelete)))), "", ""},
}
for _, csiMods := range []bool{false, true} {
km := testKeymap(t)
km.csiMods = csiMods
for _, c := range cases {
want := c.want
if csiMods && c.csiMods != "" {
want = c.csiMods
}
k := &linuxKeys{km: km}
got, _ := typedBy(k, c.strokes)
if got != want {
t.Errorf("%s (csiMods %v): typed %q, want %q", c.name, csiMods, got, want)
}
if k.shift != 0 {
t.Errorf("%s (csiMods %v): modifiers %#x left down", c.name, csiMods, k.shift)
}
}
}
}
// Keys that switch consoles, the locks and Scroll Lock ask the console for
// what the kernel would do cooked: the locks' flags, a console by number or
// the one before or after, and output going on
func TestLinuxConsoleKeysAskTheConsole(t *testing.T) {
cases := []struct {
name string
strokes []stroke
want []consoleAct
leds uint8
}{
{"alt f2", hold(keyLeftAlt, press(keyF2)), []consoleAct{{vt: 2}}, 0},
{"ctrl alt f1", hold(keyLeftCtrl, hold(keyLeftAlt, press(keyF1))), []consoleAct{{vt: 1}}, 0},
{"altgr f1", hold(keyRightAlt, press(keyF1)), []consoleAct{{vt: 13}}, 0},
{"alt left right", hold(keyLeftAlt, press(keyLeft, keyRight)), []consoleAct{{vt: vtPrev}, {vt: vtNext}}, 0},
{"caps lock twice", press(keyCapsLock, keyCapsLock), []consoleAct{{leds: true}, {leds: true}}, 0},
{"caps lock held", []stroke{{keyCapsLock, true}, {keyCapsLock, true}, {keyCapsLock, false}}, []consoleAct{{leds: true}}, ledCaps},
{"num lock", press(keyNumLock), []consoleAct{{leds: true}}, ledNum},
{"scroll lock", press(keyScrollLk), []consoleAct{{start: true}}, 0},
}
for _, c := range cases {
k := &linuxKeys{km: testKeymap(t)}
if _, got := typedBy(k, c.strokes); !slices.Equal(got, c.want) {
t.Errorf("%s: asked %+v, want %+v", c.name, got, c.want)
}
if k.leds != c.leds {
t.Errorf("%s: lock flags %#x, want %#x", c.name, k.leds, c.leds)
}
}
}
// Translation starts from the console's state: modifiers already down and the
// locks already on
func TestLinuxConsoleSeedsFromTheConsole(t *testing.T) {
k := &linuxKeys{km: testKeymap(t)}
k.seed(1<<kgShift, ledNum)
got, _ := typedBy(k, slices.Concat(press(keyA, keyKP8), []stroke{{keyLeftShift, false}}, press(keyA)))
if got != "A8a" {
t.Errorf("typed %q, want %q", got, "A8a")
}
}
// Sticky and locked modifiers, which some keymaps bind, hold for the next key
// and until locked off; a key no map has types nothing
func TestLinuxConsoleLocks(t *testing.T) {
km := testKeymap(t)
const keyShiftLock, keyStickyShift, keyNoMap = 85, 86, 89
km.maps[0][keyShiftLock] = 0xf000 | ktLock<<8 | kgShift
km.maps[1][keyShiftLock] = 0xf000 | ktLock<<8 | kgShift
km.maps[0][keyStickyShift] = 0xf000 | ktSLock<<8 | kgShift
km.maps[1][keyStickyShift] = 0xf000 | ktSLock<<8 | kgShift
k := &linuxKeys{km: km}
strokes := slices.Concat(press(keyShiftLock), press(keyA, keyA), press(keyShiftLock), press(keyStickyShift), press(keyA, keyA))
if got, _ := typedBy(k, strokes); got != "AAAa" {
t.Errorf("typed %q, want %q", got, "AAAa")
}
if got, _ := typedBy(k, hold(keyLeftAlt, hold(keyLeftShift, press(keyNoMap)))); got != "" {
t.Errorf("a key with no map typed %q", got)
}
}
// Dead keys and compose type what the console's accent table makes of the
// character after them, as cooked: their accented character, the dead key's
// own for a space or itself, or the dead key's character and the next when
// the table has no pair for them. A key typing no character leaves a dead key
// pending, and Enter types it.
func TestLinuxConsoleDeadKeys(t *testing.T) {
km := testKeymap(t)
km.maps[0][keyGrave] = 0xf000 | ktDead<<8 // dead grave, by number
km.maps[1][keyGrave] = 0xf000 | ktDead2<<8 | '~' // dead tilde, by its character
km.maps[0][keyCompose] = 0xf000 | ktSpec<<8 | 14 // K_COMPOSE
km.accents = []linuxAccent{{'`', 'a', 'à'}, {'~', 'n', 'ñ'}, {'o', 'a', 'å'}, {'s', 's', 'ß'}}
cases := []struct {
name string
strokes []stroke
want string
}{
{"dead key", press(keyGrave, keyA), "à"},
{"dead key by its character", slices.Concat(hold(keyLeftShift, press(keyGrave)), press(keyN)), "ñ"},
{"dead key and space", press(keyGrave, keySpace), "`"},
{"dead key twice", press(keyGrave, keyGrave, keyA), "à"},
{"no pair", press(keyGrave, keyB), "`b"},
{"dead key and enter", press(keyGrave, keyEnter), "`\r"},
{"over a cursor key", press(keyGrave, keyUp, keyA), "\x1b[Aà"},
{"compose", press(keyCompose, keyO, keyA), "å"},
{"compose a pair", press(keyCompose, keyS, keyS), "ß"},
{"compose and a dead key", press(keyCompose, keyGrave, keyA, keyB), "àb"},
}
for _, c := range cases {
k := &linuxKeys{km: km}
if got, _ := typedBy(k, c.strokes); got != c.want {
t.Errorf("%s: typed %q, want %q", c.name, got, c.want)
}
}
}
// K_MEDIUMRAW input decodes to keycodes, a long one across reads, and a long
// one broken off is dropped without the key after it
func TestMediumRawDecodes(t *testing.T) {
type ev struct {
code int
down bool
}
var got []ev
var m mediumRaw
key := func(code int, down bool) { got = append(got, ev{code, down}) }
m.decode([]byte{30, 30 | 0x80, 0x00, 0x81}, key)
m.decode([]byte{0x80 | 55, 0x80, 0x82, 0x80 | 3}, key)
m.decode([]byte{0x00, 0x81, 28, 0x80 | 28}, key)
want := []ev{{30, true}, {30, false}, {1<<7 | 55, true}, {2<<7 | 3, false}, {28, true}, {28, false}}
if !slices.Equal(got, want) {
t.Errorf("decoded %v, want %v", got, want)
}
}
+5 -1
View File
@@ -100,6 +100,7 @@ func (o *outputBuffer) flush(cells []Cell, width, height int) {
}
w := &o.buf
start := w.Len()
for y := 0; y < height; y++ {
rowStart := y * width
@@ -194,7 +195,10 @@ func (o *outputBuffer) flush(cells []Cell, width, height int) {
}
}
w.Write(csiSGR0)
// A frame that changed no cell writes nothing, the reset included
if w.Len() > start {
w.Write(csiSGR0)
}
o.lastValid = false
}
+160 -3
View File
@@ -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,8 @@ type termImpl struct {
finalized bool
mouseMode MouseMode
paste bool
keyProto bool // the key protocol's flags are pushed
consoleRaw bool // the console's keyboard is raw: mouse reports wait
// Output reaches the terminal on writeLoop's goroutine, so a terminal that
// stops reading holds that goroutine rather than whoever draws. queued is what
@@ -132,7 +145,8 @@ type termImpl struct {
heldW int
heldH int
holding bool
writeSince atomic.Int64 // unix nanoseconds the write out began; zero when none is
writeSince atomic.Int64 // unix nanoseconds the write out began; zero when none is
drained chan struct{} // a drain left nothing queued
wake chan struct{}
quit chan struct{}
writerDone chan struct{}
@@ -174,6 +188,7 @@ func newTerm(b Backend, c ColorMode) *termImpl {
syntheticCh: make(chan Event, 16),
resizeCh: make(chan ResizeEvent, 1),
wake: make(chan struct{}, 1),
drained: make(chan struct{}, 1),
quit: make(chan struct{}),
writerDone: make(chan struct{}),
}
@@ -198,6 +213,11 @@ 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
if t.input.console = backendConsole(t.backend); t.input.console != nil {
t.input.keysRaw, t.input.keysCooked = t.keysRaw, t.keysCooked
}
// Set resize handler on backend
t.backend.SetResizeHandler(func(w, h int) {
@@ -271,10 +291,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)
@@ -495,7 +525,17 @@ func (t *termImpl) SetMouseMode(mode MouseMode) error {
if !t.initialized || t.finalized {
return nil
}
if t.consoleRaw {
t.mouseMode = mode // reported once the console's keyboard is cooked again
return nil
}
t.setMouseModeLocked(mode)
return nil
}
// setMouseModeLocked writes what moves the terminal's mouse reporting to mode.
// Caller MUST hold mu.
func (t *termImpl) setMouseModeLocked(mode MouseMode) {
oldMode := t.mouseMode
t.mouseMode = mode
@@ -552,7 +592,6 @@ func (t *termImpl) SetMouseMode(mode MouseMode) error {
}
t.sendLocked()
return nil
}
// SetPasteMode turns bracketed paste on or off
@@ -576,10 +615,124 @@ 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
}
// consoleDrainTimeout bounds how long switching a console's keyboard raw waits
// for the console to take what is queued, its mouse reports off among it
const consoleDrainTimeout = 500 * time.Millisecond
// keysRaw switches the console's keyboard raw for the input reader: false
// when it stays cooked, and whether it may not for a later ask. The console
// types its mouse reports into the input the keycodes come through, so they
// are off while it is raw, the console having taken that before the switch,
// which drops reports already sent.
func (t *termImpl) keysRaw() (ok, again bool) {
c := t.input.console
if !c.prepare() {
return false, false
}
t.mu.Lock()
defer t.mu.Unlock()
if !t.initialized || t.finalized {
return false, false
}
mode := t.mouseMode
if mode != MouseModeNone {
t.setMouseModeLocked(MouseModeNone)
t.mouseMode = mode
}
t.consoleRaw = true
if t.idleLocked(consoleDrainTimeout) && !t.finalized && c.raw() {
return true, false
}
t.mouseBackLocked()
return false, !t.finalized
}
// keysCooked switches the console's keyboard back for the input reader, and
// mouse reports on again
func (t *termImpl) keysCooked() {
t.input.console.cooked()
t.mu.Lock()
defer t.mu.Unlock()
if t.initialized {
t.mouseBackLocked()
}
}
// mouseBackLocked ends the hold on mouse reports the console's keyboard put
// while raw, reporting as asked. Caller MUST hold mu.
func (t *termImpl) mouseBackLocked() {
t.consoleRaw = false
if mode := t.mouseMode; mode != MouseModeNone && !t.finalized {
t.mouseMode = MouseModeNone
t.setMouseModeLocked(mode)
}
}
// idleLocked waits, mu released meanwhile, until the terminal has taken all
// that is queued, or for timeout; it returns with mu held, and whether the
// queue emptied. Caller MUST hold mu.
func (t *termImpl) idleLocked(timeout time.Duration) bool {
deadline := time.Now().Add(timeout)
for len(t.queued) > 0 || t.writeSince.Load() != 0 {
left := time.Until(deadline)
if left <= 0 || t.finalized {
return false
}
t.mu.Unlock()
select {
case <-t.drained:
case <-time.After(left):
}
t.mu.Lock()
}
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.
func EmergencyReset(w io.Writer) {
// A console keyboard left raw types nothing a player can use
restoreConsoleKeys()
if f, ok := w.(*os.File); ok && !term.IsTerminal(int(f.Fd())) {
if tty := controllingTTY(); tty != nil {
defer tty.Close()
@@ -593,6 +746,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)
+12
View File
@@ -121,6 +121,18 @@ func TestEachCellIsOneColumn(t *testing.T) {
}
}
// A frame that changes no cell writes nothing, so drawing an idle screen
// again costs the terminal no bytes
func TestUnchangedFrameWritesNothing(t *testing.T) {
o := newOutputBuffer(ColorMode256)
frame := []Cell{{Rune: 'a'}, {Rune: 'é'}}
o.flush(frame, len(frame), 1)
o.buf.Reset()
if o.flush(frame, len(frame), 1); o.buf.Len() != 0 {
t.Fatalf("unchanged frame wrote %q", o.buf.String())
}
}
// A paste arrives as one event, whatever reads split it into: an escape
// inside it is text even when a poll times out on it, and a paste the
// terminal never ends ends when input pauses, whether reads time out or
+116
View File
@@ -0,0 +1,116 @@
key 0 0 0 0 0 0 0 0 0 ff 0
key 1 1b 1b 1b 1b 1b 1b 86 1b 2 0
key 2 31 21 0 0 31 21 0 0 33 0
key 3 32 40 0 0 32 40 0 0 0 0
key 4 33 23 0 0 33 23 0 0 33 0
key 5 34 24 0 0 34 24 0 0 33 0
key 6 35 25 0 0 35 25 0 0 33 0
key 7 36 5e 1e 1e 36 5e 1e 1e 0 0
key 8 37 26 0 0 37 26 0 0 33 0
key 9 38 2a 0 0 38 2a 0 0 33 0
key 10 39 28 0 0 39 28 0 0 33 0
key 11 30 29 0 0 30 29 0 0 33 0
key 12 2d 5f 1f 1f 2d 5f 1f 1f 0 0
key 13 3d 2b 0 0 3d 2b 0 0 33 0
key 14 8 8 7f 7f 8 8 7f 7f 0 0
key 15 9 8 0 0 9 8 0 0 77 0
key 16 71 51 11 11 71 51 11 11 0 1
key 17 77 57 17 17 77 57 17 17 0 1
key 18 65 45 5 5 65 45 5 5 0 1
key 19 72 52 12 12 72 52 12 12 0 1
key 20 74 54 14 14 74 54 14 14 0 1
key 21 79 59 19 19 79 59 19 19 0 1
key 22 75 55 15 15 75 55 15 15 0 1
key 23 69 49 9 9 69 49 9 9 0 1
key 24 6f 4f f f 6f 4f f f 0 1
key 25 70 50 10 10 70 50 10 10 0 1
key 26 5b 7b 1b 1b 5b 7b 1b 1b 0 0
key 27 5d 7d 1d 1d 5d 7d 1d 1d 0 0
key 28 d d a a d d a a 0 0
key 29 9 9 9 9 9 9 9 9 ff 0
key 30 61 41 1 1 61 41 1 1 0 1
key 31 73 53 13 13 73 53 13 13 0 1
key 32 64 44 4 4 64 44 4 4 0 1
key 33 66 46 6 6 66 46 6 6 0 1
key 34 67 47 7 7 67 47 7 7 0 1
key 35 68 48 8 8 68 48 8 8 0 1
key 36 6a 4a a a 6a 4a a a 0 1
key 37 6b 4b b b 6b 4b b b 0 1
key 38 6c 4c c c 6c 4c c c 0 1
key 39 3b 3a 0 0 3b 3a 0 0 33 0
key 40 27 22 0 0 27 22 0 0 33 0
key 41 60 7e 0 0 60 7e 0 0 33 0
key 42 2 2 2 2 2 2 2 2 ff 0
key 43 5c 7c 1c 1c 5c 7c 1c 1c 0 0
key 44 7a 5a 1a 1a 7a 5a 1a 1a 0 1
key 45 78 58 18 18 78 58 18 18 0 1
key 46 63 43 3 3 63 43 3 3 0 1
key 47 76 56 16 16 76 56 16 16 0 1
key 48 62 42 2 2 62 42 2 2 0 1
key 49 6e 4e e e 6e 4e e e 0 1
key 50 6d 4d d d 6d 4d d d 0 1
key 51 2c 3c 0 0 2c 3c 0 0 33 0
key 52 2e 3e 0 0 2e 3e 0 0 33 0
key 53 2f 3f 0 0 2f 3f 0 0 33 0
key 54 3 3 3 3 3 3 3 3 ff 0
key 55 2a 2a 2a 2a 2a 2a 2a 2a 0 0
key 56 7 7 7 7 7 7 7 7 ff 0
key 57 20 20 0 20 20 20 87 20 2 0
key 58 4 4 4 4 4 4 4 4 ff 0
key 59 1b 27 33 3f b 15 b 15 ff 0
key 60 1c 28 34 40 c 16 c 16 ff 0
key 61 1d 29 35 41 d 17 d 17 ff 0
key 62 1e 2a 36 42 e 18 e 18 ff 0
key 63 1f 2b 37 43 f 19 f 19 ff 0
key 64 20 2c 38 44 10 1a 10 1a ff 0
key 65 21 2d 39 45 11 11 11 11 ff 0
key 66 22 2e 3a 46 12 12 12 12 ff 0
key 67 23 2f 3b 47 13 13 13 13 ff 0
key 68 24 30 3c 48 14 14 14 14 ff 0
key 69 5 5 5 5 5 5 5 5 ff 0
key 70 6 6 6 6 6 6 6 6 ff 0
key 71 4b 37 37 37 37 37 37 37 80 2
key 72 4c 38 38 38 38 38 38 38 80 2
key 73 4d 39 39 39 39 39 39 39 80 2
key 74 4e 2d 2d 2d 2d 2d 2d 2d 80 2
key 75 4f 34 34 34 34 34 34 34 80 2
key 76 50 35 35 35 35 35 35 35 80 2
key 77 51 36 36 36 36 36 36 36 80 2
key 78 52 2b 2b 2b 2b 2b 2b 2b 80 2
key 79 53 31 31 31 31 31 31 31 80 2
key 80 54 32 32 32 32 32 32 32 80 2
key 81 55 33 33 33 33 33 33 33 80 2
key 82 56 30 30 30 30 30 30 30 80 2
key 83 7f 2e 2e 2e 2e 2e 85 85 3 2
key 84 0 0 0 0 0 0 0 0 ff 0
key 85 0 0 0 0 0 0 0 0 ff 0
key 86 0 0 0 0 0 0 0 0 ff 0
key 87 25 31 3d 49 15 15 15 15 ff 0
key 88 26 32 3e 4a 16 16 16 16 ff 0
key 89 d d a a d d a a 0 0
key 90 80 80 80 80 80 80 80 80 ff 0
key 91 2f 2f 2f 2f 2f 2f 2f 2f 0 2
key 92 a 99 86 86 0 0 0 0 ff 0
key 93 81 81 81 81 81 81 81 81 ff 0
key 94 4b 4b 4b 4b 4b 4b 4b 4b ff 0
key 95 4c 4c 4c 4c 4c 4c 4c 4c ff 0
key 96 4d 4d 4d 4d 4d 4d 4d 4d ff 0
key 97 4f 4f 4f 4f 4f 4f 4f 4f ff 0
key 98 51 51 51 51 51 51 51 51 ff 0
key 99 53 53 53 53 53 53 53 53 ff 0
key 100 54 54 54 54 54 54 54 54 ff 0
key 101 55 55 55 55 55 55 55 55 ff 0
key 102 56 a3 56 56 56 56 56 56 ff 0
key 103 57 57 57 57 57 57 85 57 ff 0
key 104 6 88 6 88 87 0 87 0 ff 0
key 105 58 58 58 58 58 58 58 58 ff 0
key 106 59 59 59 59 59 59 59 59 ff 0
key 107 5a 5a 5a 5a 5a 5a 5a 5a ff 0
key 108 0 0 0 0 0 0 0 0 ff 0
acc 0 60 61:e0 41:c0 65:e8 45:c8 69:ec 49:cc 6f:f2 4f:d2 75:f9 55:d9
acc 1 b4 61:e1 41:c1 65:e9 45:c9 69:ed 49:cd 6f:f3 4f:d3 75:fa 55:da 79:fd 59:dd
acc 2 5e 61:e2 41:c2 65:ea 45:ca 69:ee 49:ce 6f:f4 4f:d4 75:fb 55:db
acc 3 7e 61:e3 41:c3 6e:f1 4e:d1 6f:f5 4f:d5
acc 7 a8 61:e4 41:c4 65:eb 45:cb 69:ef 49:cf 6f:f6 4f:d6 75:fc 55:dc 79:ff
acc 9 b0 61:e5 41:c5
acc 10 b8 63:e7 43:c7
+35
View File
@@ -0,0 +1,35 @@
map 0 0200 001b 0031 0032 0033 0034 0035 0036 0037 0038 0039 0030 002d 003d 007f 0009 0b71 0b77 0b65 0b72 0b74 0b79 0b75 0b69 0b6f 0b70 005b 005d 0201 0702 0b61 0b73 0b64 0b66 0b67 0b68 0b6a 0b6b 0b6c 003b 0027 0060 0700 005c 0b7a 0b78 0b63 0b76 0b62 0b6e 0b6d 002c 002e 002f 0700 030c 0703 0020 0207 0100 0101 0102 0103 0104 0105 0106 0107 0108 0109 0208 0209 0307 0308 0309 030b 0304 0305 0306 030a 0301 0302 0303 0300 0310 0206 0200 003c 010a 010b 0200 0200 0200 0200 0200 0200 0200 030e 0702 030d 001c 0701 0205 0114 0603 0118 0601 0602 0117 0600 0119 0115 0116 011a 010c 010d 011b 011c 0110 0311 011d 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200
map 1 0200 001b 0021 0040 0023 0024 0025 005e 0026 002a 0028 0029 005f 002b 007f 0009 0b51 0b57 0b45 0b52 0b54 0b59 0b55 0b49 0b4f 0b50 007b 007d 0201 0702 0b41 0b53 0b44 0b46 0b47 0b48 0b4a 0b4b 0b4c 003a 0022 007e 0700 007c 0b5a 0b58 0b43 0b56 0b42 0b4e 0b4d 003c 003e 003f 0700 030c 0703 0020 0207 010a 010b 010c 010d 010e 010f 0110 0111 0112 0113 0213 0203 0307 0308 0309 030b 0304 0305 0306 030a 0301 0302 0303 0300 0310 0206 0200 003e 010a 010b 0200 0200 0200 0200 0200 0200 0200 030e 0702 030d 0200 0701 0205 0114 0603 020b 0601 0602 0117 0600 020a 0115 0116 011a 010c 010d 011b 011c 0110 0311 011d 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200
map 2 0200 0200 0200 0040 0200 0024 0200 0200 007b 005b 005d 007d 005c 0200 0200 0200 0b71 0b77 0918 0b72 0b74 0b79 0b75 0b69 0b6f 0b70 0200 007e 0201 0702 0914 0b73 0917 0919 0b67 0b68 0b6a 0b6b 0b6c 0200 0200 0200 0700 0200 0b7a 0b78 0916 0b76 0915 0b6e 0b6d 0200 0200 0200 0700 030c 0703 0200 0207 050c 050d 050e 050f 0510 0511 0512 0513 0514 0515 0208 0202 0911 0912 0913 030b 090e 090f 0910 030a 090b 090c 090d 090a 0310 0206 0200 007c 0516 0517 0200 0200 0200 0200 0200 0200 0200 030e 0702 030d 0200 0701 0205 0114 0603 0118 0601 0602 0117 0600 0119 0115 0116 011a 010c 010d 011b 011c 0110 0311 011d 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200
map 4 0200 0200 0200 0000 001b 001c 001d 001e 001f 007f 0200 0200 001f 0200 0008 0200 0011 0017 0005 0012 0014 0019 0015 0009 000f 0010 001b 001d 0201 0702 0001 0013 0004 0006 0007 0008 000a 000b 000c 0200 0007 0000 0700 001c 001a 0018 0003 0016 0002 000e 000d 0200 020e 007f 0700 030c 0703 0000 0207 0100 0101 0102 0103 0104 0105 0106 0107 0108 0109 0208 0204 0307 0308 0309 030b 0304 0305 0306 030a 0301 0302 0303 0300 0310 0206 0200 0200 010a 010b 0200 0200 0200 0200 0200 0200 0200 030e 0702 030d 001c 0701 0205 0114 0603 0118 0601 0602 0117 0600 0119 0115 0116 011a 010c 010d 011b 011c 0110 0311 011d 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200
map 5 0200 0200 0200 0000 0200 0200 0200 0200 0200 0200 0200 0200 001f 0200 0200 0200 0011 0017 0005 0012 0014 0019 0015 0009 000f 0010 0200 0200 0201 0702 0001 0013 0004 0006 0007 0008 000a 000b 000c 0200 0200 0200 0700 0200 001a 0018 0003 0016 0002 000e 000d 0200 0200 0200 0700 030c 0703 0200 0207 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0208 0200 0307 0308 0309 030b 0304 0305 0306 030a 0301 0302 0303 0300 0310 0206 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 030e 0702 030d 0200 0701 0205 0114 0603 0118 0601 0602 0117 0600 0119 0115 0116 011a 010c 010d 011b 011c 0110 0311 011d 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200
map 8 0200 081b 0831 0832 0833 0834 0835 0836 0837 0838 0839 0830 082d 083d 087f 0809 0871 0877 0865 0872 0874 0879 0875 0869 086f 0870 085b 085d 080d 0702 0861 0873 0864 0866 0867 0868 086a 086b 086c 083b 0827 0860 0700 085c 087a 0878 0863 0876 0862 086e 086d 082c 082e 082f 0700 030c 0703 0820 0207 0500 0501 0502 0503 0504 0505 0506 0507 0508 0509 0208 0209 0907 0908 0909 030b 0904 0905 0906 030a 0901 0902 0903 0900 0310 0206 0200 083c 050a 050b 0200 0200 0200 0200 0200 0200 0200 030e 0702 030d 001c 0701 0205 0114 0603 0118 0210 0211 0117 0600 0119 0115 0116 011a 010c 010d 011b 011c 0110 0311 011d 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200
map 12 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0811 0817 0805 0812 0814 0819 0815 0809 080f 0810 0200 0200 0201 0702 0801 0813 0804 0806 0807 0808 080a 080b 080c 0200 0200 0200 0700 0200 081a 0818 0803 0816 0802 080e 080d 0200 0200 0200 0700 030c 0703 0200 0207 0500 0501 0502 0503 0504 0505 0506 0507 0508 0509 0208 0200 0307 0308 0309 030b 0304 0305 0306 030a 0301 0302 0303 0300 020c 0206 0200 0200 050a 050b 0200 0200 0200 0200 0200 0200 0200 030e 0702 030d 0200 0701 0205 0114 0603 0118 0601 0602 0117 0600 0119 0115 020c 011a 010c 010d 011b 011c 0110 0311 011d 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200 0200
fn 0 "\x1b[[A"
fn 1 "\x1b[[B"
fn 2 "\x1b[[C"
fn 3 "\x1b[[D"
fn 4 "\x1b[[E"
fn 5 "\x1b[17~"
fn 6 "\x1b[18~"
fn 7 "\x1b[19~"
fn 8 "\x1b[20~"
fn 9 "\x1b[21~"
fn 10 "\x1b[23~"
fn 11 "\x1b[24~"
fn 12 "\x1b[25~"
fn 13 "\x1b[26~"
fn 14 "\x1b[28~"
fn 15 "\x1b[29~"
fn 16 "\x1b[31~"
fn 17 "\x1b[32~"
fn 18 "\x1b[33~"
fn 19 "\x1b[34~"
fn 20 "\x1b[1~"
fn 21 "\x1b[2~"
fn 22 "\x1b[3~"
fn 23 "\x1b[4~"
fn 24 "\x1b[5~"
fn 25 "\x1b[6~"
fn 26 "\x1b[M"
fn 29 "\x1b[P"
+1 -4
View File
@@ -149,10 +149,7 @@ func (r Region) Divider(y int, label string, line LineType, fg color.RGB) {
text = Truncate(text, r.W-2)
textLen = RuneLen(text)
}
startX := (r.W - textLen) / 2
for i, ch := range text {
r.Cell(startX+i, y, ch, fg, color.RGB{}, terminal.AttrBold)
}
r.Text((r.W-textLen)/2, y, text, fg, color.RGB{}, terminal.AttrBold)
}
}
+3 -15
View File
@@ -225,11 +225,7 @@ func (r Region) ConfirmDialog(state *ConfirmState, opts ConfirmOpts) Region {
yesBg = style.ButtonFocusBg
}
}
for i, ch := range yesLabel {
if buttonX+i < content.W {
content.Cell(buttonX+i, buttonY, ch, yesFg, yesBg, terminal.AttrNone)
}
}
content.Text(buttonX, buttonY, yesLabel, yesFg, yesBg, terminal.AttrNone)
// No button
noX := buttonX + yesW + buttonGap
@@ -239,11 +235,7 @@ func (r Region) ConfirmDialog(state *ConfirmState, opts ConfirmOpts) Region {
noFg = style.ButtonFocusFg
noBg = style.ButtonFocusBg
}
for i, ch := range noLabel {
if noX+i < content.W {
content.Cell(noX+i, buttonY, ch, noFg, noBg, terminal.AttrNone)
}
}
content.Text(noX, buttonY, noLabel, noFg, noBg, terminal.AttrNone)
return content.Sub(0, 0, content.W, buttonY-1)
}
@@ -317,11 +309,7 @@ func (r Region) AlertDialog(opts AlertOpts) Region {
buttonW := RuneLen(buttonLabel)
buttonX := (content.W - buttonW) / 2
for i, ch := range buttonLabel {
if buttonX+i < content.W {
content.Cell(buttonX+i, buttonY, ch, style.ButtonFocusFg, style.ButtonFocusBg, terminal.AttrNone)
}
}
content.Text(buttonX, buttonY, buttonLabel, style.ButtonFocusFg, style.ButtonFocusBg, terminal.AttrNone)
return content.Sub(0, 0, content.W, buttonY-1)
}
+1 -6
View File
@@ -164,12 +164,7 @@ func (r Region) Form(state *FormState, opts FormOpts) int {
// Label
label := field.Label + ":"
for j, ch := range label {
if j >= labelW || j >= r.W {
break
}
r.Cell(j, y, ch, style.LabelFg, style.Bg, terminal.AttrNone)
}
r.Sub(0, y, labelW, 1).Text(0, 0, label, style.LabelFg, style.Bg, terminal.AttrNone)
// Field area
fieldX := labelW
+2 -10
View File
@@ -36,10 +36,7 @@ func (r Region) Modal(opts ModalOpts) Region {
title = Truncate(title, r.W-4)
titleLen = RuneLen(title)
}
x := (r.W - titleLen) / 2
for i, ch := range title {
r.Cell(x+i, 0, ch, opts.TitleFg, opts.Bg, terminal.AttrBold)
}
r.Text((r.W-titleLen)/2, 0, title, opts.TitleFg, opts.Bg, terminal.AttrBold)
}
// Hint right-aligned on top edge
@@ -54,12 +51,7 @@ func (r Region) Modal(opts ModalOpts) Region {
if x < r.W/2 {
x = r.W / 2
}
for i, ch := range hint {
if x+i >= r.W-1 {
break
}
r.Cell(x+i, 0, ch, opts.HintFg, opts.Bg, terminal.AttrNone)
}
r.Sub(0, 0, r.W-1, 1).Text(x, 0, hint, opts.HintFg, opts.Bg, terminal.AttrNone)
}
// Return content region
+1 -7
View File
@@ -33,13 +33,7 @@ func (r Region) Pane(opts PaneOpts) Region {
if RuneLen(title) > r.W-4 {
title = Truncate(title, r.W-4)
}
x := 2
for i, ch := range title {
if x+i >= r.W-1 {
break
}
r.Cell(x+i, 0, ch, opts.TitleFg, opts.Bg, terminal.AttrBold)
}
r.Text(2, 0, title, opts.TitleFg, opts.Bg, terminal.AttrBold)
}
// Content region is everything inside the border. The title sits ON the border row.
+1 -4
View File
@@ -194,10 +194,7 @@ func (r Region) ProgressOverlay(opts ProgressOverlayOpts) Region {
title = Truncate(title, overlayW-4)
titleLen = RuneLen(title)
}
titleX := (overlayW - titleLen) / 2
for i, ch := range title {
overlay.Cell(titleX+i, 0, ch, opts.AccentFg, opts.Bg, terminal.AttrBold)
}
overlay.Text((overlayW-titleLen)/2, 0, title, opts.AccentFg, opts.Bg, terminal.AttrBold)
}
// Content area
+1 -7
View File
@@ -64,13 +64,7 @@ func (r Region) TabBar(y int, titles []string, active int, opts TabBarOpts) []Ta
r.Cell(x+j, y, ' ', style.Fg, style.Bg, style.Attr)
}
titleStart := x + opts.Padding
for j, ch := range title {
if titleStart+j >= r.W {
break
}
r.Cell(titleStart+j, y, ch, style.Fg, style.Bg, style.Attr)
}
r.TextStyled(x+opts.Padding, y, title, style)
for j := 0; j < opts.Padding; j++ {
pos := x + opts.Padding + RuneLen(title) + j
+1 -5
View File
@@ -171,11 +171,7 @@ func (r Region) renderTableRow(y int, cells []string, widths []int, aligns []Ali
startX = x
}
for j, ch := range text {
if startX+j < r.W {
r.Cell(startX+j, y, ch, style.Fg, style.Bg, style.Attr)
}
}
r.TextStyled(startX, y, text, style)
x += w
+1 -6
View File
@@ -108,12 +108,7 @@ func (r Region) TextField(state *TextFieldState, opts TextFieldOpts) int {
if RuneLen(placeholder) > viewportW {
placeholder = Truncate(placeholder, viewportW)
}
for i, ch := range placeholder {
if x+i >= contentX+contentW {
break
}
r.Cell(x+i, contentY, ch, style.PlaceholderFg, style.TextBg, terminal.AttrDim)
}
r.Text(x, contentY, placeholder, style.PlaceholderFg, style.TextBg, terminal.AttrDim)
} else {
// Scroll indicators
if state.Scroll > 0 && x > contentX {
+34
View File
@@ -467,3 +467,37 @@ func TestWindowShowsTheRowsInView(t *testing.T) {
}
r.Sub(8, 0, 4, 3).Window(10, &v, MonoTheme.Text, content) // no columns, past the right edge
}
// A label is drawn a column a rune, so one holding runes of more than a byte
// keeps them together: each widget draws s as many times as it is given it
func TestLabelsKeepTheirRunesTogether(t *testing.T) {
const s = "● é · ñ"
for _, c := range []struct {
name string
n int
draw func(r Region)
}{
{"pane", 1, func(r Region) { r.Pane(PaneOpts{Title: s}) }},
{"modal", 2, func(r Region) { r.Modal(ModalOpts{Title: s, Hint: s}) }},
{"divider", 1, func(r Region) { r.Divider(0, s, LineSingle, color.RGB{}) }},
{"tabs", 2, func(r Region) { r.TabBar(0, []string{s, s}, 0, DefaultTabBarOpts()) }},
{"table", 2, func(r Region) { r.Table([]string{s}, [][]string{{s}}, TableOpts{}) }},
{"form", 1, func(r Region) { r.Form(NewFormState(s), FormOpts{}) }},
{"field", 1, func(r Region) { r.TextField(NewTextFieldState(""), TextFieldOpts{Placeholder: s}) }},
{"confirm", 4, func(r Region) {
r.ConfirmDialog(NewConfirmState(true), ConfirmOpts{Title: s, Message: s, YesLabel: s, NoLabel: s})
}},
{"alert", 3, func(r Region) { r.AlertDialog(AlertOpts{Title: s, Message: s, Button: s}) }},
{"progress", 1, func(r Region) { r.ProgressOverlay(DefaultProgressOpts(s, "", ProgressSpinner)) }},
} {
r := canvas(48, 14, MonoTheme)
c.draw(r)
rows := make([]string, r.H)
for y := range rows {
rows[y] = row(r, y)
}
if got := strings.Count(strings.Join(rows, "\n"), s); got != c.n {
t.Errorf("%s: %d of %d, rows %q", c.name, got, c.n, rows)
}
}
}
+4
View File
@@ -52,5 +52,9 @@ func (t *termImpl) drain() {
t.writeSince.Store(0)
t.mu.Lock()
}
select {
case t.drained <- struct{}{}:
default:
}
t.mu.Unlock()
}