first commit
This commit is contained in:
123
internal/hhkb/device.go
Normal file
123
internal/hhkb/device.go
Normal file
@@ -0,0 +1,123 @@
|
||||
// Package hhkb talks to the vendor HID interface of an HHKB Professional
|
||||
// keyboard (PFU, USB 04fe:0020-0022) over /dev/hidraw, the same channel the
|
||||
// official Keymap Tool uses. It needs no cgo: just file I/O on the device node.
|
||||
package hhkb
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const reportLen = 64
|
||||
|
||||
// Device is an open handle to the keyboard's vendor HID interface.
|
||||
type Device struct {
|
||||
f *os.File
|
||||
product uint16
|
||||
}
|
||||
|
||||
// Open finds the HHKB vendor interface and opens it for reading and writing.
|
||||
//
|
||||
// It identifies the interface from sysfs alone: the right hidraw node belongs
|
||||
// to USB 04fe:0020 and carries a report descriptor that begins with
|
||||
// 06 00 ff (Usage Page = Vendor-Defined 0xFF00). The keyboard exposes three
|
||||
// hidraw nodes; only this one accepts keymap commands.
|
||||
func Open() (*Device, error) {
|
||||
nodes, err := filepath.Glob("/sys/class/hidraw/hidraw*")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, node := range nodes {
|
||||
name := filepath.Base(node)
|
||||
|
||||
uevent, err := os.ReadFile(filepath.Join(node, "device/uevent"))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
product, ok := parseProduct(string(uevent))
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
desc, err := os.ReadFile(filepath.Join(node, "device/report_descriptor"))
|
||||
if err != nil || len(desc) < 3 || desc[0] != 0x06 || desc[1] != 0x00 || desc[2] != 0xff {
|
||||
continue
|
||||
}
|
||||
|
||||
f, err := os.OpenFile("/dev/"+name, os.O_RDWR, 0)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("found %s but cannot open it: %w", name, err)
|
||||
}
|
||||
return &Device{f: f, product: product}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("HHKB vendor interface (04fe:0020-0022) not found")
|
||||
}
|
||||
|
||||
// Close releases the device.
|
||||
func (d *Device) Close() error { return d.f.Close() }
|
||||
|
||||
// Product is the USB product id of the open device: 0x0020 Classic,
|
||||
// 0x0021 Hybrid, 0x0022 Hybrid Type-S.
|
||||
func (d *Device) Product() uint16 { return d.product }
|
||||
|
||||
// parseProduct reads the USB product id from a hidraw uevent and reports
|
||||
// whether the node is a supported HHKB (vendor 04fe, product 0020-0022).
|
||||
func parseProduct(uevent string) (uint16, bool) {
|
||||
for _, line := range strings.Split(uevent, "\n") {
|
||||
v, ok := strings.CutPrefix(line, "HID_ID=")
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
parts := strings.Split(v, ":") // bus:vendor:product
|
||||
if len(parts) != 3 || !strings.EqualFold(parts[1], "000004FE") {
|
||||
return 0, false
|
||||
}
|
||||
var p uint32
|
||||
if _, err := fmt.Sscanf(parts[2], "%x", &p); err != nil {
|
||||
return 0, false
|
||||
}
|
||||
if p < 0x0020 || p > 0x0022 {
|
||||
return 0, false
|
||||
}
|
||||
return uint16(p), true
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
// request builds a 64-byte report: AA AA <cmd> followed by args, zero-padded.
|
||||
func request(cmd byte, args ...byte) []byte {
|
||||
b := make([]byte, reportLen)
|
||||
b[0], b[1], b[2] = 0xAA, 0xAA, cmd
|
||||
copy(b[3:], args)
|
||||
return b
|
||||
}
|
||||
|
||||
// send writes one report. hidraw takes a leading report-number byte, which the
|
||||
// kernel discards because this device uses unnumbered reports.
|
||||
func (d *Device) send(report []byte) error {
|
||||
_, err := d.f.Write(append([]byte{0x00}, report...))
|
||||
return err
|
||||
}
|
||||
|
||||
// recv reads one 64-byte report. In a reply, the payload starts at byte 6;
|
||||
// bytes 0..1 are an acknowledgement header (55 55).
|
||||
func (d *Device) recv() ([]byte, error) {
|
||||
b := make([]byte, reportLen)
|
||||
n, err := d.f.Read(b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if n != reportLen {
|
||||
return nil, fmt.Errorf("short read: %d/%d bytes", n, reportLen)
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
// do sends a command and returns its single reply.
|
||||
func (d *Device) do(cmd byte, args ...byte) ([]byte, error) {
|
||||
if err := d.send(request(cmd, args...)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d.recv()
|
||||
}
|
||||
43
internal/hhkb/keymap.go
Normal file
43
internal/hhkb/keymap.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package hhkb
|
||||
|
||||
// Mode is the keyboard personality selected by the DIP switches.
|
||||
type Mode byte
|
||||
|
||||
const (
|
||||
ModeHHK Mode = iota
|
||||
ModeMac
|
||||
ModeLite
|
||||
ModeSecret
|
||||
)
|
||||
|
||||
func (m Mode) String() string {
|
||||
switch m {
|
||||
case ModeHHK:
|
||||
return "HHK"
|
||||
case ModeMac:
|
||||
return "Mac"
|
||||
case ModeLite:
|
||||
return "Lite"
|
||||
case ModeSecret:
|
||||
return "Secret"
|
||||
}
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
// LayerLen is the size of one key layer. A key's number indexes directly into
|
||||
// the layer to give its scancode.
|
||||
const LayerLen = 128
|
||||
|
||||
// Layer holds the scancode assigned to every key on one layer.
|
||||
type Layer [LayerLen]byte
|
||||
|
||||
// Key describes one physical key: its protocol key number, factory default
|
||||
// scancode, visual row, and position/width in key units (as in QMK layouts,
|
||||
// where 1.0 is a normal keycap). The key tables live in models.go.
|
||||
type Key struct {
|
||||
Num int
|
||||
Def byte
|
||||
Row int
|
||||
X float64
|
||||
W float64
|
||||
}
|
||||
110
internal/hhkb/models.go
Normal file
110
internal/hhkb/models.go
Normal file
@@ -0,0 +1,110 @@
|
||||
package hhkb
|
||||
|
||||
import "strings"
|
||||
|
||||
// Variant is the physical key layout of a model.
|
||||
type Variant int
|
||||
|
||||
const (
|
||||
US Variant = iota
|
||||
JIS
|
||||
)
|
||||
|
||||
func (v Variant) String() string {
|
||||
if v == JIS {
|
||||
return "JIS"
|
||||
}
|
||||
return "US"
|
||||
}
|
||||
|
||||
// Model is a keyboard this tool understands. Keys is its physical layout, in
|
||||
// reading order; Hybrid marks the Hybrid/Type-S boards, which reserve a few Fn
|
||||
// keys in firmware.
|
||||
type Model struct {
|
||||
Name string
|
||||
Product uint16
|
||||
Variant Variant
|
||||
Hybrid bool
|
||||
Keys []Key
|
||||
}
|
||||
|
||||
// Models lists every supported model. The US/JIS split mirrors the official
|
||||
// tool; the product id groups the Classic, Hybrid, and Hybrid Type-S families.
|
||||
var Models = []Model{
|
||||
{"HHKB Professional Classic (US)", 0x0020, US, false, ansiKeys},
|
||||
{"HHKB Professional Classic (JIS)", 0x0020, JIS, false, jisKeys},
|
||||
{"HHKB Professional Hybrid (US)", 0x0021, US, true, ansiKeys},
|
||||
{"HHKB Professional Hybrid (JIS)", 0x0021, JIS, true, jisKeys},
|
||||
{"HHKB Professional Hybrid Type-S (US)", 0x0022, US, true, ansiKeys},
|
||||
{"HHKB Professional Hybrid Type-S (JIS)", 0x0022, JIS, true, jisKeys},
|
||||
}
|
||||
|
||||
// MatchModel picks the model for a USB product id and reported type number.
|
||||
// JIS boards carry "20" in their type number (PD-KB420, PD-KB820); the rest
|
||||
// are US. Falls back to any model sharing the product id.
|
||||
func MatchModel(product uint16, typeNumber string) (Model, bool) {
|
||||
want := US
|
||||
if strings.Contains(typeNumber, "20") {
|
||||
want = JIS
|
||||
}
|
||||
for _, m := range Models {
|
||||
if m.Product == product && m.Variant == want {
|
||||
return m, true
|
||||
}
|
||||
}
|
||||
for _, m := range Models {
|
||||
if m.Product == product {
|
||||
return m, true
|
||||
}
|
||||
}
|
||||
return Model{}, false
|
||||
}
|
||||
|
||||
// DetectModel identifies the connected keyboard from its product id and info.
|
||||
func (d *Device) DetectModel() (Model, bool, error) {
|
||||
info, err := d.Info()
|
||||
if err != nil {
|
||||
return Model{}, false, err
|
||||
}
|
||||
m, ok := MatchModel(d.product, info.TypeNumber)
|
||||
return m, ok, nil
|
||||
}
|
||||
|
||||
// ansiKeys is the US (60-key) layout. Geometry matches QMK's LAYOUT_60_hhkb:
|
||||
// {Num, Def, Row, X, W}, in key units.
|
||||
var ansiKeys = []Key{
|
||||
{60, 0x29, 0, 0, 1}, {59, 0x1e, 0, 1, 1}, {58, 0x1f, 0, 2, 1}, {57, 0x20, 0, 3, 1}, {56, 0x21, 0, 4, 1},
|
||||
{55, 0x22, 0, 5, 1}, {54, 0x23, 0, 6, 1}, {53, 0x24, 0, 7, 1}, {52, 0x25, 0, 8, 1}, {51, 0x26, 0, 9, 1},
|
||||
{50, 0x27, 0, 10, 1}, {49, 0x2d, 0, 11, 1}, {48, 0x2e, 0, 12, 1}, {47, 0x31, 0, 13, 1}, {46, 0x35, 0, 14, 1},
|
||||
{45, 0x2b, 1, 0, 1.5}, {44, 0x14, 1, 1.5, 1}, {43, 0x1a, 1, 2.5, 1}, {42, 0x08, 1, 3.5, 1}, {41, 0x15, 1, 4.5, 1},
|
||||
{40, 0x17, 1, 5.5, 1}, {39, 0x1c, 1, 6.5, 1}, {38, 0x18, 1, 7.5, 1}, {37, 0x0c, 1, 8.5, 1}, {36, 0x12, 1, 9.5, 1},
|
||||
{35, 0x13, 1, 10.5, 1}, {34, 0x2f, 1, 11.5, 1}, {33, 0x30, 1, 12.5, 1}, {32, 0x2a, 1, 13.5, 1.5},
|
||||
{31, 0xe0, 2, 0, 1.75}, {30, 0x04, 2, 1.75, 1}, {29, 0x16, 2, 2.75, 1}, {28, 0x07, 2, 3.75, 1}, {27, 0x09, 2, 4.75, 1},
|
||||
{26, 0x0a, 2, 5.75, 1}, {25, 0x0b, 2, 6.75, 1}, {24, 0x0d, 2, 7.75, 1}, {23, 0x0e, 2, 8.75, 1}, {22, 0x0f, 2, 9.75, 1},
|
||||
{21, 0x33, 2, 10.75, 1}, {20, 0x34, 2, 11.75, 1}, {19, 0x28, 2, 12.75, 2.25},
|
||||
{18, 0xe1, 3, 0, 2.25}, {17, 0x1d, 3, 2.25, 1}, {16, 0x1b, 3, 3.25, 1}, {15, 0x06, 3, 4.25, 1}, {14, 0x19, 3, 5.25, 1},
|
||||
{13, 0x05, 3, 6.25, 1}, {12, 0x11, 3, 7.25, 1}, {11, 0x10, 3, 8.25, 1}, {10, 0x36, 3, 9.25, 1}, {9, 0x37, 3, 10.25, 1},
|
||||
{8, 0x38, 3, 11.25, 1}, {7, 0xe5, 3, 12.25, 1.75}, {6, 0x01, 3, 14, 1},
|
||||
// row 4 — key5/key1 are the narrow outer keys, key4/key2 the wide ones next to
|
||||
// Space. Which sends Alt vs GUI(◇) depends on the keyboard's mode (HHK vs Mac),
|
||||
// so the labels come from the live read; the Def values here are the HHK defaults.
|
||||
{5, 0xe3, 4, 1.5, 1}, {4, 0xe2, 4, 2.5, 1.5}, {3, 0x2c, 4, 4, 7}, {2, 0xe6, 4, 11, 1.5}, {1, 0xe7, 4, 12.5, 1},
|
||||
}
|
||||
|
||||
// jisKeys is the Japanese (69-key) layout. Key numbers and rows are fixed; the
|
||||
// geometry is a uniform grid for now (exact JIS widths are a later refinement),
|
||||
// and default scancodes are read from the device when connected.
|
||||
var jisKeys = buildJIS()
|
||||
|
||||
func buildJIS() []Key {
|
||||
rows := [...][2]int{{69, 55}, {54, 41}, {40, 28}, {27, 14}, {13, 1}}
|
||||
var keys []Key
|
||||
for row, span := range rows {
|
||||
x := 0.0
|
||||
for n := span[0]; n >= span[1]; n-- {
|
||||
keys = append(keys, Key{Num: n, Row: row, X: x, W: 1})
|
||||
x++
|
||||
}
|
||||
}
|
||||
return keys
|
||||
}
|
||||
29
internal/hhkb/palette.go
Normal file
29
internal/hhkb/palette.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package hhkb
|
||||
|
||||
// KeycodeGroup is one labelled section of the keycode palette.
|
||||
type KeycodeGroup struct {
|
||||
Name string
|
||||
Codes []byte
|
||||
}
|
||||
|
||||
// Palette groups the assignable scancodes for the editor, roughly the way the
|
||||
// official tool and QMK present them.
|
||||
var Palette = []KeycodeGroup{
|
||||
{"Letters", codeRange(0x04, 0x1d)},
|
||||
{"Numbers", codeRange(0x1e, 0x27)},
|
||||
{"Symbols", []byte{0x2d, 0x2e, 0x2f, 0x30, 0x31, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38}},
|
||||
{"Editing", []byte{0x29, 0x2a, 0x2b, 0x28, 0x2c, 0x39}},
|
||||
{"Navigation", codeRange(0x49, 0x52)},
|
||||
{"Function", codeRange(0x3a, 0x45)},
|
||||
{"Modifiers", []byte{0xe0, 0xe1, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7}},
|
||||
{"Media", []byte{0xe8, 0xe9, 0xea, 0xeb}},
|
||||
{"Special", []byte{0x00, 0x01, 0x46, 0x47, 0x48}},
|
||||
}
|
||||
|
||||
func codeRange(lo, hi byte) []byte {
|
||||
out := make([]byte, 0, int(hi-lo)+1)
|
||||
for c := lo; c <= hi; c++ {
|
||||
out = append(out, c)
|
||||
}
|
||||
return out
|
||||
}
|
||||
168
internal/hhkb/proto.go
Normal file
168
internal/hhkb/proto.go
Normal file
@@ -0,0 +1,168 @@
|
||||
package hhkb
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Command ids — byte 3 of every request.
|
||||
const (
|
||||
cmdNotifyApp = 1
|
||||
cmdGetInfo = 2
|
||||
cmdFactoryReset = 3
|
||||
cmdConfirmKeymap = 4
|
||||
cmdGetMode = 6
|
||||
cmdResetDIP = 7
|
||||
cmdWriteKeymap = 134
|
||||
cmdGetKeymap = 135
|
||||
)
|
||||
|
||||
// Info describes the connected keyboard.
|
||||
type Info struct {
|
||||
TypeNumber string
|
||||
Revision string
|
||||
Serial string
|
||||
AppFirmware string
|
||||
BootFirmware string
|
||||
}
|
||||
|
||||
// Info reads the keyboard's identification.
|
||||
func (d *Device) Info() (Info, error) {
|
||||
r, err := d.do(cmdGetInfo)
|
||||
if err != nil {
|
||||
return Info{}, err
|
||||
}
|
||||
p := r[6:]
|
||||
return Info{
|
||||
TypeNumber: cstr(p[0:20]),
|
||||
Revision: cstr(p[20:24]),
|
||||
Serial: cstr(p[24:40]),
|
||||
AppFirmware: fmt.Sprintf("%X", p[40:48]),
|
||||
BootFirmware: fmt.Sprintf("%X", p[48:56]),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Mode reads the active keyboard mode (set by DIP switches).
|
||||
func (d *Device) Mode() (Mode, error) {
|
||||
r, err := d.do(cmdGetMode)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return Mode(r[6]), nil
|
||||
}
|
||||
|
||||
// ReadLayer reads one full key layer: the base layer, or the Fn layer when
|
||||
// fn is true. The reply arrives as three reports that tile the 128-byte layer.
|
||||
func (d *Device) ReadLayer(fn bool) (Layer, error) {
|
||||
mode, err := d.Mode()
|
||||
if err != nil {
|
||||
return Layer{}, err
|
||||
}
|
||||
return d.readLayer(mode, fn)
|
||||
}
|
||||
|
||||
func (d *Device) readLayer(mode Mode, fn bool) (Layer, error) {
|
||||
var lay Layer
|
||||
if err := d.send(request(cmdGetKeymap, 0, 2, byte(mode), boolByte(fn))); err != nil {
|
||||
return lay, err
|
||||
}
|
||||
for _, seg := range [...]struct{ off, n int }{{0, 58}, {58, 58}, {116, 12}} {
|
||||
r, err := d.recv()
|
||||
if err != nil {
|
||||
return lay, err
|
||||
}
|
||||
copy(lay[seg.off:seg.off+seg.n], r[6:6+seg.n])
|
||||
}
|
||||
return lay, nil
|
||||
}
|
||||
|
||||
// writeLayer uploads a full layer in the three passes the firmware expects.
|
||||
// The leading byte pairs (65,59 / 130,59 / 195,12) are offset and length
|
||||
// markers the controller verifies; the data window is mode+fn followed by the
|
||||
// 128 key bytes.
|
||||
func (d *Device) writeLayer(mode Mode, fn bool, lay Layer) error {
|
||||
passes := []struct {
|
||||
mark [2]byte
|
||||
head []byte
|
||||
data []byte
|
||||
}{
|
||||
{[2]byte{65, 59}, []byte{byte(mode), boolByte(fn)}, lay[0:57]},
|
||||
{[2]byte{130, 59}, nil, lay[57:116]},
|
||||
{[2]byte{195, 12}, nil, lay[116:128]},
|
||||
}
|
||||
for _, p := range passes {
|
||||
args := append([]byte{p.mark[0], p.mark[1]}, p.head...)
|
||||
args = append(args, p.data...)
|
||||
if _, err := d.do(cmdWriteKeymap, args...); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Remap assigns scancode to a key (by its key number) on the base or Fn layer.
|
||||
// It mirrors the official tool's sequence: announce the tool, edit the layer,
|
||||
// write it back, commit, and reset DIP state. The change is reversible with
|
||||
// Reset or by remapping again.
|
||||
func (d *Device) Remap(keyNum int, scancode byte, fn bool) error {
|
||||
if keyNum < 1 || keyNum >= LayerLen {
|
||||
return fmt.Errorf("key number %d out of range", keyNum)
|
||||
}
|
||||
if err := d.notifyApp(true); err != nil {
|
||||
return err
|
||||
}
|
||||
defer d.notifyApp(false)
|
||||
|
||||
mode, err := d.Mode()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
lay, err := d.readLayer(mode, fn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
lay[keyNum] = scancode
|
||||
|
||||
if err := d.writeLayer(mode, fn, lay); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := d.do(cmdConfirmKeymap); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = d.do(cmdResetDIP, 0, 1)
|
||||
return err
|
||||
}
|
||||
|
||||
// Reset restores the factory default keymap.
|
||||
func (d *Device) Reset() error {
|
||||
r, err := d.do(cmdFactoryReset)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !bytes.HasPrefix(r, []byte{0x55, 0x55, 0x03, 0x00}) {
|
||||
return fmt.Errorf("unexpected factory-reset reply: % X", r[:6])
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// notifyApp tells the keyboard whether the configuration tool is active.
|
||||
func (d *Device) notifyApp(open bool) error {
|
||||
_, err := d.do(cmdNotifyApp, 0, 1, boolByte(!open))
|
||||
return err
|
||||
}
|
||||
|
||||
func boolByte(b bool) byte {
|
||||
if b {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// cstr converts a fixed-width, NUL-padded field into a Go string.
|
||||
func cstr(b []byte) string {
|
||||
if i := strings.IndexByte(string(b), 0); i >= 0 {
|
||||
b = b[:i]
|
||||
}
|
||||
return strings.TrimSpace(string(b))
|
||||
}
|
||||
39
internal/hhkb/scancode.go
Normal file
39
internal/hhkb/scancode.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package hhkb
|
||||
|
||||
import "fmt"
|
||||
|
||||
// scancodeName maps USB HID keyboard usage ids to short labels. 0x01 is the
|
||||
// HHKB Fn key, which the firmware handles internally rather than reporting.
|
||||
var scancodeName = map[byte]string{
|
||||
0x00: "--", 0x01: "Fn",
|
||||
0x04: "A", 0x05: "B", 0x06: "C", 0x07: "D", 0x08: "E", 0x09: "F",
|
||||
0x0a: "G", 0x0b: "H", 0x0c: "I", 0x0d: "J", 0x0e: "K", 0x0f: "L",
|
||||
0x10: "M", 0x11: "N", 0x12: "O", 0x13: "P", 0x14: "Q", 0x15: "R",
|
||||
0x16: "S", 0x17: "T", 0x18: "U", 0x19: "V", 0x1a: "W", 0x1b: "X",
|
||||
0x1c: "Y", 0x1d: "Z",
|
||||
0x1e: "1", 0x1f: "2", 0x20: "3", 0x21: "4", 0x22: "5",
|
||||
0x23: "6", 0x24: "7", 0x25: "8", 0x26: "9", 0x27: "0",
|
||||
0x28: "Enter", 0x29: "Esc", 0x2a: "Bksp", 0x2b: "Tab", 0x2c: "Space",
|
||||
0x2d: "-", 0x2e: "=", 0x2f: "[", 0x30: "]", 0x31: "\\",
|
||||
0x33: ";", 0x34: "'", 0x35: "`", 0x36: ",", 0x37: ".", 0x38: "/",
|
||||
0x39: "Caps",
|
||||
0x3a: "F1", 0x3b: "F2", 0x3c: "F3", 0x3d: "F4", 0x3e: "F5", 0x3f: "F6",
|
||||
0x40: "F7", 0x41: "F8", 0x42: "F9", 0x43: "F10", 0x44: "F11", 0x45: "F12",
|
||||
0x46: "PrtSc", 0x47: "ScrLk", 0x48: "Pause",
|
||||
0x49: "Ins", 0x4a: "Home", 0x4b: "PgUp", 0x4c: "Del", 0x4d: "End", 0x4e: "PgDn",
|
||||
0x4f: "Right", 0x50: "Left", 0x51: "Down", 0x52: "Up",
|
||||
0xe0: "LCtrl", 0xe1: "LShift", 0xe2: "LAlt", 0xe3: "LGUI",
|
||||
0xe4: "RCtrl", 0xe5: "RShift", 0xe6: "RAlt", 0xe7: "RGUI",
|
||||
0x53: "Numlk", 0x54: "KP/", 0x55: "KP*", 0x56: "KP-", 0x57: "KP+", 0x58: "KPEnt",
|
||||
0x66: "Power",
|
||||
// HHKB media keys (sent via the consumer interface; values are HHKB-internal).
|
||||
0xe8: "Vol-", 0xe9: "Vol+", 0xea: "Mute", 0xeb: "Play",
|
||||
}
|
||||
|
||||
// KeyName returns a label for a scancode, falling back to hex for unknowns.
|
||||
func KeyName(code byte) string {
|
||||
if name, ok := scancodeName[code]; ok {
|
||||
return name
|
||||
}
|
||||
return fmt.Sprintf("0x%02x", code)
|
||||
}
|
||||
Reference in New Issue
Block a user