kb

package
v0.0.0-...-4f53d2e Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 3, 2026 License: GPL-3.0 Imports: 14 Imported by: 0

README

kb

Keyboard layouts as a Go library. Pick a layout, read its keys, ask what sits next to a character or above it on the Shift level, and work out what someone typed with the wrong layout selected.

board := kb.MustGet("kbdus")

board.Adjacent("e")   // [w r d 3 4 s]
board.Shifted("4")    // [$]
board.Unshifted("$")  // [4]

The data is the layout set published by kbdlayout.info: 203 layouts across 110 languages — every keyboard Windows ships, including the historic-script ones.

Every snippet below is taken from a runnable example in example_test.go, so the outputs are checked by go test rather than written from memory.

Selecting a layout

A layout is identified by the driver it ships in, and Get also accepts any of the KLIDs installed against that driver, so all of these are the same board:

kb.Get("kbdus")
kb.Get("KBDUS.DLL")
kb.Get("00000409")   // en-US
kb.Get("409")        // the short form Windows tools print
kb.Get("00000804")   // zh-Hans-CN, which shares the US driver

To search rather than address:

kb.ByLanguage("de")      // German, German (IBM), German Extended ×2, Swiss German
kb.ByLanguage("de-CH")   // just the Swiss one
kb.Find("dvorak")        // by name
kb.List()                // the whole catalogue, without decoding any of it
kb.IDs()                 // just the driver names
kb.Languages()           // the 110 subtags, sorted

A bare subtag matches every layout for the language; a full BCP-47 tag matches only the layouts carrying that exact locale. All of these return nil when nothing matches.

You can also ask which keyboards can actually type something:

kb.ByString("google.com")   // 98 layouts
kb.ByString("münchen")      // 20
kb.ByString("中文")          // 0
kb.ByKeys("a", "b", "c")    // the same question, one text per argument

Both intersect — a layout has to type every text given. Unlike the lookups above these cannot be answered from the index, so the first call decodes the whole catalogue (about 50 ms); afterwards it is a map lookup per layout.

Layouts are decoded on first use and cached, so Get is cheap to call repeatedly and returns the same pointer each time. Nothing it returns may be modified.

Reading keys

Every key carries the scan code of the physical switch it sits on, the virtual key it maps to, its position, and what it types in each modifier state.

k, _ := kb.MustGet("kbdgr").Key("29")

k.SC     // "29"
k.VK     // "VK_OEM_5"
k.Base() // "^"
k.Shift()// "°"
k.AltGr()// "" here; "€" on many European layouts
k.Texts()// every distinct string the key can type

o, _ := k.Text(kb.Base)
o.Dead   // true — the German circumflex arms an accent, it does not type one

Caps Lock inverts Shift, so only the keys that depart from that rule — the digit row of a German keyboard, where Caps Lock does nothing — store it. The rest is derived.

Adjacency

Adjacency is geometric, not tabular. Scan codes describe the physical switch rather than the letter printed on it, so one position table serves every layout, and neighbours are found by distance between the points a finger would strike.

That matters because the rows of a real keyboard are staggered. The e key overhangs d by a quarter of a key and s by three quarters, so a finger slipping down from e is likelier to land on d, and the result is ordered accordingly:

us.Adjacent("e")             // [w r d 3 4 s]  — d ahead of s
us.Adjacent("E")             // [W R D # $ S]  — Shift travels with it
us.AdjacentWithin("e", 1.9)  // a wider net
us.AdjacentKeys(k, 1.3)      // the keys themselves, nearest first

A table of equal-width rows cannot make that distinction. Neither can it tell an ANSI keyboard from an ISO one, where the short left Shift makes room for a key that neighbours a:

uk := kb.MustGet("kbduk")
uk.Adjacent("a")               // [s q \ z w]  — ISO
uk.With(kb.ANSI).Adjacent("a") // [s q z w]    — ANSI has no such key

Nor can it handle a key that is not one unit wide. The space bar is six units across, so measuring to its centre would make b its only neighbour instead of the whole bottom row; distance is measured to the nearest point a hand would actually strike, which is the centre for every ordinary key.

And the answer follows the layout rather than assuming QWERTY:

kb.MustGet("kbdgr").Adjacent("z")  // [t u h 6 7 g]  — QWERTZ
kb.MustGet("kbdfr").Adjacent("a")  // [z q & é]      — AZERTY
kb.MustGet("kbddv").Adjacent("e")  // [o u . q j p]  — Dvorak

Typing on the wrong layout

A Stroke is a key and the modifiers held with it. It says nothing about what gets typed — that depends on the layout the keystroke lands on, which is the whole point:

us.Strokes("abc")            // [{1E base} {30 base} {2E base}]
ru.Type(us.Strokes("abc"))   // "фис"

Translate does both steps at once:

us.Translate("hello", ru)       // "руддщ"
us.Translate("google.com", ru)  // "пщщпдуюсщь"
us.Translate("google.com", fr)  // "google:co," — AZERTY moves the punctuation

Characters the source layout cannot type, and keys the target leaves bare, are passed through unchanged rather than dropped, so a domain keeps its dots. Two consequences worth knowing: the result is not always the same length, since a few layouts have ligature keys that type two characters at once; and it round-trips only if the source can type everything given, because a character that falls through untouched is an ordinary character on the way back.

Printing

kb.MustGet("kbdgr").Print()
kbdgr — German (iso)
+---+---+---+---+---+---+---+---+---+---+---+---+---+
| ° | ! | " | § | $ | % | & | / | ( | ) | = | ? |[`]|
|[^]| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 0 | ß |[´]|
+-----+---+---+---+---+---+---+---+---+---+---+---+---+
|     | Q | W | E | R | T | Z | U | I | O | P | Ü | * |
|     | q | w | e | r | t | z | u | i | o | p | ü | + |
+------+---+---+---+---+---+---+---+---+---+---+---+---+
|      | A | S | D | F | G | H | J | K | L | Ö | Ä | ' |
|      | a | s | d | f | g | h | j | k | l | ö | ä | # |
+----+---+---+---+---+---+---+---+---+---+---+---+-+---+
|    | > | Y | X | C | V | B | N | M | ; | : | _ |
|    | < | y | x | c | v | b | n | m | , | . | - |
+----+---+---+-+------------------------++---+---+
               |                        |
               |                        |
               +------------------------+

Boxes are placed from the real geometry, so the indentation between rows is the physical stagger rather than decoration. Shift sits above base as on a keycap, and dead keys are bracketed because they type nothing on their own.

Print writes to stdout, Fprint to any writer, and String returns the same drawing — so fmt.Print(layout) agrees with all of them.

Scope and known gaps

The dataset covers the alphanumeric block. The function row and the navigation cluster type nothing, and the numeric keypad only duplicates digits the number row already has — including it would put two physical keys behind 4 and make adjacency ambiguous.

Three things are missing, and they are worth knowing before trusting a negative answer:

  • Dead-key compositions are not stored. The dataset records that a key is dead and what accent it carries, but not what it composes into. So German counts as unable to type é, and ByString("café") leaves it out, even though ´ then e produces it. Translate likewise emits the bare accent where a real keyboard would arm one.
  • kbdjpn is Latin-only here. Its kana sit behind a Kana lock, a modifier this package has no bit for, so 97 of its keystrokes are absent.
  • kbdcan is missing its VK_OEM_8 level — the ¹ ² ³ ¼ row, 78 keystrokes, for the same reason.

Those are the only two layouts affected, and rebuilding the dataset reports any level it cannot represent, so a new one cannot slip in unnoticed.

Storage

The dataset is protocol buffers — one encoded layout per file plus an index — compiled into the binary with go:embed. The schema is internal/kbpb/kb.proto.

That is 257 KB for all 203 layouts, against 1.5 MB for the same data as JSON. Selecting a layout costs only the 12 KB index; the layouts themselves are decoded on first use, so a program that touches one keyboard decodes one keyboard.

Everything that is a number is stored as one. A scan code is the byte the keyboard reports, a virtual key is a Windows constant, and a KLID is a 32-bit identifier — the "10", "VK_Q" and "00000409" spellings the source data uses are presentation, and across 10,446 keys they cost 77 KB. VirtualKey is an enum whose values are the Windows codes (VK_A = 0x41), so protoc's generated name tables convert both ways for free, and a virtual key the schema has not seen fails the build instead of being silently dropped. The API keeps the readable spelling either way.

Key positions are not stored at all. They are recomputed from the scan code and the layout's form when a layout loads, which keeps the geometry in one table instead of copying it into every file.

Layout also marshals to JSON, which spells out everything it carries. Nothing in the package reads that back — it is there for exporting a layout or looking at one:

raw, _ := json.MarshalIndent(kb.MustGet("kbdus"), "", "  ")

Regenerating

go generate ./pkg/kb

That runs two steps: protoc for the Go bindings, then gen, which walks the KLID list on kbdlayout.info, follows each to the driver that serves it, and converts the driver's XML export. Responses are cached under pkg/kb/.cache, so a re-run after a parsing change costs no requests; delete the cache to fetch afresh. Cache entries are written atomically, so an interrupted run cannot leave a truncated one behind to be reused.

Encoding is deterministic, so a rebuild that changed nothing leaves the dataset byte for byte identical. Since the files are opaque to a diff, every layout is decoded and compared before it is written, and the tests check the whole catalogue round-trips.

Regenerating needs protoc and protoc-gen-go on PATH; building and using the package does not.

Documentation

Overview

Package kb provides the keyboard layouts published by kbdlayout.info as a Go library: pick a layout, read its keys, and ask what sits next to a character or above it on the Shift level.

board := kb.MustGet("kbdus")
board.Adjacent("e")     // [w r d 3 4 s]
board.Shifted("4")      // [$]
board.Translate("hello", kb.MustGet("kbdru"))  // "руддщ"

A layout is identified by the Windows driver it ships in — "kbdus", "kbdfr", "kbdgr" — or by any of the KLIDs installed against that driver, so Get("00000409") and Get("kbdus") return the same board. ByLanguage selects layouts by BCP-47 tag.

Adjacency is geometric rather than tabular. Every key carries the scan code of the physical switch it sits on, and scan codes are a property of the keyboard rather than of the layout printed on it, so one position table serves every layout. Neighbours are then found by distance between key strike points, which respects the stagger between rows: on QWERTY the "e" key overhangs "d" far more than it overhangs "s", and Adjacent reports them in that order. A layout laid out as rows of equal-width columns cannot make that distinction, and neither can it tell an ANSI board from an ISO one, where the short left Shift puts an extra key below "a".

The dataset covers the alphanumeric block only. The function row and the navigation cluster type nothing, and the numeric keypad only duplicates digits the number row already has.

Index

Examples

Constants

View Source
const DefaultRadius = 1.3

DefaultRadius is how far apart two keys may be and still count as adjacent. At 1.3 units a letter key reaches its left and right neighbours (1.0), the two keys above and the two below (1.03 to 1.25), the space bar if it runs beneath it (1.0), and nothing else. Raising it to 1.9 pulls in the diagonal-but-one keys as well.

The stagger is what these numbers are for: on QWERTY the "e" key sits a quarter unit left of "d" and three quarters left of "s", so "d" comes back as the likelier slip. A layout stored as rows of equal-width columns cannot tell the two apart.

Variables

This section is empty.

Functions

func IDs

func IDs() []string

IDs returns the driver name of every available layout, in order.

func Languages

func Languages() []string

Languages returns every language subtag with at least one layout, sorted.

Example
package main

import (
	"fmt"

	"github.com/rangertaha/urlinsane/pkg/kb"
)

func main() {
	langs := kb.Languages()
	fmt.Println(len(langs), "languages")
	fmt.Println(len(kb.List()), "layouts")
}
Output:
110 languages
203 layouts

func MarshalCatalogue

func MarshalCatalogue(entries []Entry) ([]byte, error)

MarshalCatalogue encodes the index of available layouts. The generator uses it; reading the dataset goes through List and Get.

Types

type Entry

type Entry struct {
	// ID is the driver name, lowercased: "kbdus", "kbdfr".
	ID string `json:"id"`

	// Name is a human-readable name: "US", "French".
	Name string `json:"name"`

	// Form is the physical shape of the board.
	Form Form `json:"form"`

	// Locales are the language identities the layout is installed under.
	Locales []Locale `json:"locales,omitempty"`
}

Entry is the summary of a layout in the catalogue, enough to choose one without paying to parse its keys.

func ByKeys

func ByKeys(texts ...string) []Entry

ByKeys returns the layouts that can type every one of the given texts, in catalogue order. It answers "which keyboards is this reachable on" — the characters of a domain, a name, a package identifier:

kb.ByKeys("g", "o", "l", "e", ".", "c", "m")
kb.ByKeys(strings.Split("münchen", "")...)

A text matches if some key on the layout produces exactly it, in any modifier state, so "a" and "A" are different questions and a layout that reaches a character only through AltGr still counts. Passing nothing returns nothing, as does passing a text no layout can type.

Unlike ByLanguage this cannot be answered from the index: it has to look at the keys, so the first call decodes the whole catalogue. They are cached afterwards, and later calls are a map lookup per layout.

func ByLanguage

func ByLanguage(tag string) []Entry

ByLanguage returns the layouts installed for a language, given either a primary subtag ("de") or a full BCP-47 tag ("de-CH"). A full tag matches only the layouts carrying that exact locale; a bare subtag matches every layout for the language.

Like the other lookups here it returns nil when nothing matches, which ranges and counts the same as an empty slice.

Example
package main

import (
	"fmt"

	"github.com/rangertaha/urlinsane/pkg/kb"
)

func main() {
	// A bare subtag matches every layout for the language; a full tag
	// matches only those carrying that exact locale.
	for _, e := range kb.ByLanguage("de") {
		fmt.Println(e.ID, e.Name)
	}
	fmt.Println("---")
	for _, e := range kb.ByLanguage("de-CH") {
		fmt.Println(e.ID, e.Name)
	}
}
Output:
kbdgr German
kbdgr1 German (IBM)
kbdgre1 German Extended (E1)
kbdgre2 German Extended (E2)
kbdsg Swiss German
---
kbdsg Swiss German

func ByString

func ByString(s string) []Entry

ByString returns the layouts that can type every character of s, in catalogue order. It is ByKeys over the runes of s, deduplicated:

kb.ByString("google.com")
kb.ByString("münchen")

The question it answers is whether every character sits on a key of its own. A layout that reaches a character only by dead key — an acute accent on a German keyboard, pressed before "e" to make "é" — does not count, because the dataset records that the accent key is dead but not what it composes into. Characters written as a base letter followed by a combining mark are likewise treated as the two runes they are.

Example
package main

import (
	"fmt"

	"github.com/rangertaha/urlinsane/pkg/kb"
)

func main() {
	// Which keyboards have a key for every character of this string?
	fmt.Println(len(kb.ByString("google.com")))
	fmt.Println(len(kb.ByString("münchen")))
	fmt.Println(len(kb.ByString("中文")))
}
Output:
98
20
0

func Find

func Find(q string) []Entry

Find returns the layouts whose ID, name, or locale names contain q, matched case-insensitively. It is for interactive selection — a "--keyboard french" flag — rather than for programmatic lookup, which should use Get.

func List

func List() []Entry

List returns the catalogue of available layouts, ordered by ID. It does not parse any of them.

func UnmarshalCatalogue

func UnmarshalCatalogue(raw []byte) ([]Entry, error)

UnmarshalCatalogue decodes the index of available layouts.

func (Entry) Languages

func (e Entry) Languages() []string

Languages returns the distinct primary language subtags for the entry.

type Form

type Form string

Form is the physical shape of the alphanumeric block. The three forms differ only in the bottom-left and right-hand edges: ISO and JIS keyboards carry extra keys that ANSI does not, which shifts where the Enter and Shift keys sit. Everything between Q and M is identical across all three.

const (
	// ANSI is the 101/104-key form used in the US and most of Asia. It has a
	// wide left Shift and a backslash key above Enter.
	ANSI Form = "ansi"

	// ISO is the 102/105-key form used across Europe. It splits the left
	// Shift to make room for an extra key, and moves backslash next to Enter.
	ISO Form = "iso"

	// JIS is the 106/109-key Japanese form: ISO's tall Enter plus two extra
	// keys, one on the number row and one on the bottom letter row.
	JIS Form = "jis"
)

type Key

type Key struct {
	// SC is the set 1 scan code, in uppercase hex. It identifies the switch
	// by position and means the same thing on every layout.
	SC string `json:"sc"`

	// VK is the Windows virtual key the switch maps to, such as "VK_Q".
	VK string `json:"vk,omitempty"`

	// Name is the label printed on the key, when the layout gives one.
	Name string `json:"name,omitempty"`

	// Pos is where the key sits, filled in from the layout's form when the
	// layout loads rather than stored in the dataset.
	Pos Pos `json:"-"`
	// contains filtered or unexported fields
}

Key is one physical switch on the board, together with everything it types.

Example
package main

import (
	"fmt"

	"github.com/rangertaha/urlinsane/pkg/kb"
)

func main() {
	// A key knows the switch it sits on and what it types in each state.
	k, _ := kb.MustGet("kbdgr").Key("29")

	fmt.Println(k.SC, k.VK)

	o, _ := k.Text(kb.Base)
	fmt.Printf("%q dead=%v\n", o.Text, o.Dead)
	fmt.Printf("%q\n", k.Shift())
}
Output:
29 VK_OEM_5
"^" dead=true
"°"

func NewKey

func NewKey(sc, vk, name string) Key

NewKey builds a key on a scan code. Use Set to give it outputs. It is what the dataset generator calls, and what a caller building a layout of their own would use.

func (Key) AltGr

func (k Key) AltGr() string

AltGr returns what the key types with AltGr held, or "" if it types nothing.

func (Key) Base

func (k Key) Base() string

Base returns what the key types unmodified, or "" if it types nothing.

func (Key) Blank

func (k Key) Blank() bool

Blank reports whether the key types nothing at all — a modifier, Enter, or a switch the layout leaves unassigned.

func (*Key) Compact

func (k *Key) Compact()

Compact drops the modifier states that Text would reproduce on its own, which is most of them: a letter key carries four states in the source data and needs to store two. It is called when the dataset is generated.

Like Set, it replaces the map rather than writing into it, so compacting one copy of a Key leaves every other copy alone.

func (Key) MarshalJSON

func (k Key) MarshalJSON() ([]byte, error)

MarshalJSON writes the key as JSON.

func (Key) Mods

func (k Key) Mods() []Mod

Mods returns every modifier state this key produces text in, in a stable order.

func (*Key) Set

func (k *Key) Set(m Mod, o Out)

Set records what the key types in a modifier state. Setting a state twice keeps the first value, since the dataset lists the plainest spelling first.

A Key holds its outputs in a map, and copying a Key copies the reference to it, so writing through one copy would otherwise be visible through all of them — including through the layout the copy came from, which Get shares between every caller in the program. Set therefore replaces the map rather than writing into it, which makes a Key behave like the value it looks like.

func (Key) Shift

func (k Key) Shift() string

Shift returns what the key types with Shift held, or "" if it types nothing.

func (Key) String

func (k Key) String() string

String renders the key as its scan code and outputs, for diagnostics.

func (Key) Text

func (k Key) Text(m Mod) (Out, bool)

Text returns what the key types in the given modifier state, and whether it types anything at all.

A Caps Lock state the key does not list falls back to capsFallback, the ordinary behaviour of Caps Lock on a letter. Only the keys that depart from it — the digit row of a German keyboard, where Caps Lock does nothing at all — are stored.

func (Key) Texts

func (k Key) Texts() []string

Texts returns every distinct string the key can type, in modifier order. Dead keys contribute the spacing form of their accent.

func (Key) Types

func (k Key) Types(s string) bool

Types reports whether the key produces s in any modifier state.

func (*Key) UnmarshalJSON

func (k *Key) UnmarshalJSON(b []byte) error

UnmarshalJSON reads a key from JSON. Position is left unset; the layout fills it in when it indexes its keys.

type Layout

type Layout struct {
	// ID is the driver name the layout is published under, lowercased, such
	// as "kbdus" or "kbdfr". It is the layout's canonical identifier.
	ID string `json:"id"`

	// Name is a human-readable name, such as "US" or "French".
	Name string `json:"name"`

	// File is the Windows driver the layout comes from, such as "KBDUS.DLL".
	File string `json:"file,omitempty"`

	// Form is the physical board the layout was drawn for, which decides
	// where the keys around the edges of the alphanumeric block sit. A
	// layout can be typed on a board of another shape — use With to say so.
	Form Form `json:"form"`

	// Locales are the language identities the layout is installed under.
	Locales []Locale `json:"locales,omitempty"`

	// Keys are the keys of the alphanumeric block, ordered top-left to
	// bottom-right.
	Keys []Key `json:"keys"`
	// contains filtered or unexported fields
}

Layout is a keyboard layout: a set of physical keys and the text each one types. Layouts are immutable once loaded and safe for concurrent use.

Get one from Get, New, or by unmarshalling; those position the keys and build the lookup tables. A Layout assembled as a struct literal has neither, and its keys will all sit on top of one another at the origin, so Key, KeysFor and Adjacent will not answer usefully.

func Get

func Get(id string) (*Layout, error)

Get returns the layout for a driver name or KLID. Layouts are parsed on first use and cached, so repeated calls are cheap and return the same pointer. The returned layout must not be modified.

Example
package main

import (
	"fmt"

	"github.com/rangertaha/urlinsane/pkg/kb"
)

func main() {
	// A layout answers to its driver name, to the file that driver ships
	// in, and to any of the KLIDs Windows installs it under.
	for _, id := range []string{"kbdus", "KBDUS.DLL", "00000409", "409", "00000804"} {
		l := kb.MustGet(id)
		fmt.Printf("%-12s %s\n", id, l.ID)
	}
}
Output:
kbdus        kbdus
KBDUS.DLL    kbdus
00000409     kbdus
409          kbdus
00000804     kbdus

func MustGet

func MustGet(id string) *Layout

MustGet is Get for layouts known to exist, such as constants in a program's own source. It panics if the layout is missing.

func New

func New(id, name string, form Form, keys []Key) *Layout

New assembles a layout from a set of keys. Keys outside the alphanumeric block are discarded, and the rest are positioned according to the form. The dataset generator uses it, as would a caller defining a layout of their own.

func (*Layout) Adjacent

func (l *Layout) Adjacent(s string) []string

Adjacent returns the characters typed by the keys physically next to the one that types s, nearest first and without duplicates.

The characters come from the same modifier state s was found in, so Adjacent("e") gives lowercase neighbours and Adjacent("E") gives uppercase ones. This is what separates the result from a naive row-and-column grid: the rows of a real keyboard are staggered, so "e" sits a quarter of a key left of "d" but three quarters left of "s", and "d" comes back first.

Example
package main

import (
	"fmt"

	"github.com/rangertaha/urlinsane/pkg/kb"
)

func main() {
	us := kb.MustGet("kbdus")

	// Nearest first. "d" comes before "s" because the rows of a keyboard
	// are staggered: "e" overhangs "d" by a quarter key and "s" by three
	// quarters.
	fmt.Println(us.Adjacent("e"))

	// Shift travels with the character.
	fmt.Println(us.Adjacent("E"))

}
Output:
[w r d 3 4 s]
[W R D # $ S]
Example (FollowsTheLayout)
package main

import (
	"fmt"

	"github.com/rangertaha/urlinsane/pkg/kb"
)

func main() {
	// The answer is read off the layout rather than assumed from QWERTY.
	fmt.Println(kb.MustGet("kbdgr").Adjacent("z")) // QWERTZ
	fmt.Println(kb.MustGet("kbdfr").Adjacent("a")) // AZERTY
	fmt.Println(kb.MustGet("kbddv").Adjacent("e")) // Dvorak
}
Output:
[t u h 6 7 g]
[z q & é]
[o u . q j p]

func (*Layout) AdjacentKeys

func (l *Layout) AdjacentKeys(k Key, radius float64) []Key

AdjacentKeys returns the keys physically surrounding k, nearest first, within the given radius in key units. Pass DefaultRadius for the eight or so keys a finger could slip onto.

func (*Layout) AdjacentWithin

func (l *Layout) AdjacentWithin(s string, radius float64) []string

AdjacentWithin is Adjacent with an explicit radius in key units. Widening it past about 1.9 starts to include keys two columns away.

func (*Layout) AltGraphed

func (l *Layout) AltGraphed(s string) []string

AltGraphed returns what the keys that type s produce with AltGr held. Most keys on most layouts produce nothing, and the result is then empty.

func (*Layout) Fprint

func (l *Layout) Fprint(w io.Writer) error

Fprint writes the layout to w.

func (*Layout) Key

func (l *Layout) Key(sc string) (Key, bool)

Key returns the key at a scan code.

func (*Layout) KeysFor

func (l *Layout) KeysFor(s string) []Key

KeysFor returns every key that types s in some modifier state. It usually returns one key, but a layout may reach the same character two ways — on many European layouts a digit is on the number row unshifted and again as the shifted form of a letter key.

func (*Layout) Languages

func (l *Layout) Languages() []string

Languages returns the distinct primary language subtags the layout is used for, such as ["en"] for the US layout or ["de"] for German. Layouts shared across locales return one entry per language.

Subtags come back lowercased. BCP-47 tags are case-insensitive and the dataset carries them as Windows spells them, so "ar-SA" and "ar" have to fold together here — otherwise they would count as two languages, and the one that kept its capitals would be unreachable through ByLanguage, which lowercases what it is asked for.

func (*Layout) Marshal

func (l *Layout) Marshal() ([]byte, error)

Marshal encodes the layout in the dataset's storage format.

func (Layout) MarshalJSON

func (l Layout) MarshalJSON() ([]byte, error)

MarshalJSON writes the layout as JSON, which spells out everything it carries. It is for exporting and inspecting a layout, not for the dataset.

func (*Layout) Print

func (l *Layout) Print()

Print writes the layout to standard output. It is the quickest way to see what a layout actually looks like:

kb.MustGet("kbdus").Print()

See String for what the drawing shows.

Example
package main

import (
	"github.com/rangertaha/urlinsane/pkg/kb"
)

func main() {
	// Boxes are placed from the real geometry, so the indentation between
	// rows is the physical stagger. Dead keys are bracketed, since they
	// type nothing on their own.
	kb.MustGet("kbdgr").Print()
}
Output:
kbdgr — German (iso)
+---+---+---+---+---+---+---+---+---+---+---+---+---+
| ° | ! | " | § | $ | % | & | / | ( | ) | = | ? |[`]|
|[^]| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 0 | ß |[´]|
+-----+---+---+---+---+---+---+---+---+---+---+---+---+
|     | Q | W | E | R | T | Z | U | I | O | P | Ü | * |
|     | q | w | e | r | t | z | u | i | o | p | ü | + |
+------+---+---+---+---+---+---+---+---+---+---+---+---+
|      | A | S | D | F | G | H | J | K | L | Ö | Ä | ' |
|      | a | s | d | f | g | h | j | k | l | ö | ä | # |
+----+---+---+---+---+---+---+---+---+---+---+---+-+---+
|    | > | Y | X | C | V | B | N | M | ; | : | _ |
|    | < | y | x | c | v | b | n | m | , | . | - |
+----+---+---+-+------------------------++---+---+
               |                        |
               |                        |
               +------------------------+

func (*Layout) Rows

func (l *Layout) Rows() [][]Key

Rows groups the keys by physical row, top row first and each row ordered left to right.

func (*Layout) Shifted

func (l *Layout) Shifted(s string) []string

Shifted returns what the keys that type s produce with Shift held — the uppercase of a letter, or the symbol printed above a digit. It returns nothing when s is only reachable shifted already.

Example
package main

import (
	"fmt"

	"github.com/rangertaha/urlinsane/pkg/kb"
)

func main() {
	us := kb.MustGet("kbdus")

	fmt.Println(us.Shifted("4"))
	fmt.Println(us.Unshifted("$"))

	// AltGr is where the extra characters of a European layout live.
	fmt.Println(kb.MustGet("kbdfr").AltGraphed("à"))
}
Output:
[$]
[4]
[@]

func (*Layout) String

func (l *Layout) String() string

String renders the layout as a keyboard diagram: one box per key, placed where the key physically sits, with the shifted character above the base one, as on a keycap.

The offset between rows is the real stagger, not decoration — it is what makes "e" nearer to "d" than to "s", and seeing it is usually the quickest way to check that a layout loaded the way you expected.

Keys that type nothing are drawn empty, which is how the modifiers and the space bar show up. A dead key is bracketed, since pressing it produces no character on its own. The drawing assumes every character takes one column, so a layout in a full-width script will not line up.

func (*Layout) Strokes

func (l *Layout) Strokes(s string) []Stroke

Strokes returns the keystrokes that type s on this layout, one per rune. A rune the layout cannot type yields a zero Stroke, so the result always has as many entries as s has runes.

Where a character can be reached more than one way the plainest is chosen: unmodified before shifted, shifted before AltGr.

Example
package main

import (
	"fmt"

	"github.com/rangertaha/urlinsane/pkg/kb"
)

func main() {
	us, ru := kb.MustGet("kbdus"), kb.MustGet("kbdru")

	// Strokes are keys and modifiers, with no text of their own. Reading
	// them on another layout is what Translate does in one step.
	fmt.Println(us.Strokes("abc"))
	fmt.Println(ru.Type(us.Strokes("abc")))
}
Output:
[{1E base} {30 base} {2E base}]
фис

func (*Layout) Translate

func (l *Layout) Translate(s string, as *Layout) string

Translate returns what s becomes when the keystrokes that type it here land on another layout instead — someone typing a familiar word with the wrong layout selected:

us.Translate("hello", ru)   // "руддщ"
us.Translate("google", ru)  // "пщщпду"

Characters this layout cannot type, and keys the other layout leaves bare, are passed through unchanged rather than dropped, so a domain keeps its dots. Use Strokes and Type together when you would rather see exactly what fell through.

Two things follow from that which are easy to assume away:

The result is not always the same length as the input. A handful of layouts have ligature keys that type two characters at once — the Arabic boards reach "لا" on one key — so a rune can come back as two.

And it does not round-trip unless this layout can type every character given. A character that falls through untouched here is still an ordinary character on the way back, and will be translated then: Russian cannot type "abc", so it passes through, and translating the result back through Russian yields "фис" rather than "abc".

A key that is dead on the far layout contributes its accent, since that is what the dataset records; it would really arm the accent instead.

Example
package main

import (
	"fmt"

	"github.com/rangertaha/urlinsane/pkg/kb"
)

func main() {
	us, ru, fr := kb.MustGet("kbdus"), kb.MustGet("kbdru"), kb.MustGet("kbdfr")

	// What someone types with the wrong layout selected.
	fmt.Println(us.Translate("hello", ru))
	fmt.Println(us.Translate("google", ru))
	fmt.Println(us.Translate("google.com", ru))

	// AZERTY moves the punctuation, which is what makes this a squatting
	// vector rather than a curiosity.
	fmt.Println(us.Translate("google.com", fr))
}
Output:
руддщ
пщщпду
пщщпдуюсщь
google:co,

func (*Layout) Type

func (l *Layout) Type(strokes []Stroke) string

Type returns what the given keystrokes produce on this layout. Strokes that this layout has no key for, or whose key types nothing in that modifier state, contribute nothing.

ru.Type(us.Strokes("hello"))  // "руддщ"

That pairing is the interesting one: the keys someone actually pressed, read through a different layout.

func (*Layout) Types

func (l *Layout) Types(s string) bool

Types reports whether the layout can type s at all.

func (*Layout) Unmarshal

func (l *Layout) Unmarshal(raw []byte) error

Unmarshal decodes a layout from the dataset's storage format and indexes it, which positions the keys and discards any that sit outside the alphanumeric block.

func (*Layout) UnmarshalJSON

func (l *Layout) UnmarshalJSON(b []byte) error

UnmarshalJSON reads a layout from JSON and indexes it, which positions the keys and discards any that sit outside the alphanumeric block.

func (*Layout) Unshifted

func (l *Layout) Unshifted(s string) []string

Unshifted returns what the keys that type s produce without Shift, which undoes Shifted.

func (*Layout) With

func (l *Layout) With(form Form) *Layout

With returns the layout as it would sit on a board of a different shape. The characters do not move between keys, but the keys move: an ISO layout typed on an ANSI board loses the extra key below the left Shift, and the backslash beside Enter moves up a row. Callers who know what hardware they are modelling can use this; the rest should take the layout as it comes.

Asking for the shape a layout already has returns the layout itself, not a copy, since neither may be modified in any case.

Example
package main

import (
	"fmt"

	"github.com/rangertaha/urlinsane/pkg/kb"
)

func main() {
	// An ISO board carries a key below the left Shift that an ANSI board
	// does not, and it neighbours "a".
	uk := kb.MustGet("kbduk")

	fmt.Println(uk.Adjacent("a"))
	fmt.Println(uk.With(kb.ANSI).Adjacent("a"))
}
Output:
[s q \ z w]
[s q z w]

type Locale

type Locale struct {
	// KLID is the eight hex digit Windows keyboard layout identifier, such
	// as "00000409".
	KLID string `json:"klid"`

	// Tag is the BCP-47 language tag for the locale, such as "en-US".
	Tag string `json:"tag,omitempty"`

	// Name is how Windows names the layout for this locale.
	Name string `json:"name,omitempty"`
}

Locale is one of the language identities a layout is installed under. Windows ships a single driver for several locales — KBDUS backs the US English layout as well as the US variants of Chinese and Bulgarian — so a layout carries a list of these rather than a single language.

type Mod

type Mod uint8

Mod is a set of held modifiers, as a bitmask. Shift, AltGr and Caps Lock are modelled because between them they cover what almost every layout does.

Two things are left out, and they are not all control codes. Ctrl and Alt on their own do produce control codes rather than text, and go. But a couple of layouts reach real characters through a modifier this package has no bit for: the Japanese board puts its kana on a Kana lock, and the Canadian Multilingual Standard board hangs a level off VK_OEM_8. Those characters are absent from the dataset — kbdjpn is Latin-only here, and kbdcan is missing its ¹ ² ³ ¼ level. The generator reports what it dropped, so a rebuild says so rather than letting it pass unnoticed.

const (
	// Base is the key pressed on its own.
	Base Mod = 0
	// Shift is either Shift key held down.
	Shift Mod = 1 << 0
	// AltGr is the right Alt key on layouts that treat it as AltGr, or the
	// Ctrl+Alt combination that stands in for it elsewhere.
	AltGr Mod = 1 << 1
	// Caps is Caps Lock engaged.
	Caps Mod = 1 << 2
)

func (Mod) String

func (m Mod) String() string

String returns the name this modifier set is stored under.

type Out

type Out struct {
	// Text is the character typed. It is occasionally more than one rune:
	// a few layouts have ligature keys, and dead-key accents are given as
	// the spacing form of the accent.
	Text string `json:"t"`

	// Dead reports that the key does not type Text directly but arms an
	// accent that combines with the next keystroke. The circumflex on a
	// French keyboard is a dead key: it types nothing until "o" follows,
	// at which point "ô" appears.
	Dead bool `json:"d,omitempty"`
}

Out is what a key produces in one modifier state.

type Pos

type Pos struct {
	X float64 `json:"x"`
	Y float64 `json:"y"`
	W float64 `json:"w"`
}

Pos is a key's physical position on the board, measured in key units: one unit is the width of a plain letter key. X grows rightwards from the left edge of the alphanumeric block, Y grows downwards from the number row. The function row is not modelled, since it produces no text.

func Positioned

func Positioned(f Form, sc string) (Pos, bool)

Positioned reports whether a scan code belongs to the alphanumeric block, and returns where it sits on the given form. Scan codes outside the block — function keys, the navigation cluster, the numeric keypad — are not modelled: they either produce no text or duplicate text the block already covers, and including them would put two physical keys behind a digit.

A form this package does not know is read as ANSI, which is the shape the others are variations on. Scan codes are matched however they are spelled, since hex has two spellings and the source data does not agree with itself about which to use.

func (Pos) Center

func (p Pos) Center() (x, y float64)

Center returns the coordinates of the middle of the key. It is where a finger aims, and for every ordinary key it is also where Distance measures from — see strike for the one case where it is not.

func (Pos) Distance

func (p Pos) Distance(q Pos) float64

Distance returns the distance between two keys, in key units. Two side-by-side letter keys are exactly 1.0 apart.

It is measured between the points a finger would plausibly strike rather than between the centers, which for ordinary one-unit keys is the same thing. It stops being the same thing for the space bar: that key is six units wide, so its center sits under "b" and a center-to-center reading would call "b" its only neighbour, when in truth the bar runs the length of the bottom row. A wide key is struck wherever the hand already is, and Distance treats it that way.

The measurement is symmetric — each key's strike point is taken against the other's center, so swapping the arguments only swaps the two terms.

type Stroke

type Stroke struct {
	// SC is the scan code of the key pressed.
	SC string

	// Mod is the set of modifiers held.
	Mod Mod
}

Stroke is a key being pressed: which switch, and what was held down with it. It says nothing about what gets typed — that depends on the layout the keystroke lands on, which is the whole point.

func (Stroke) Zero

func (s Stroke) Zero() bool

Zero reports whether the stroke stands for a character no key could produce.

Directories

Path Synopsis
Command gen rebuilds the pkg/kb dataset from kbdlayout.info.
Command gen rebuilds the pkg/kb dataset from kbdlayout.info.
internal

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL