From 438c6b8b758de6ab5ea3dc761e02a2b5ea0c7a00 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 3 May 2026 14:53:40 -0700 Subject: [PATCH] add String/FromString for Key, Mods, MouseButton, FocusEvent Add human-friendly string conversion to the input enum types so they can be serialized to and parsed from text formats like JSON or configuration files. Each type gains a String() method returning the canonical snake_case name (e.g. "key_a", "arrow_down", "shift+ctrl", "middle", "gained") and a pointer-receiver FromString method that mutates the receiver. A top-level NewKeyFromString, NewModsFromString, NewMouseButtonFromString, and NewFocusEventFromString constructor is also provided as a more ergonomic alternative. --- focus.go | 44 ++++++++- key_string.go | 259 +++++++++++++++++++++++++++++++++++++++++++++++++ mods_string.go | 115 ++++++++++++++++++++++ mouse_event.go | 59 +++++++++++ string_test.go | 205 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 681 insertions(+), 1 deletion(-) create mode 100644 key_string.go create mode 100644 mods_string.go create mode 100644 string_test.go diff --git a/focus.go b/focus.go index c5e6c33..9dae1cb 100644 --- a/focus.go +++ b/focus.go @@ -9,7 +9,10 @@ package libghostty */ import "C" -import "unsafe" +import ( + "fmt" + "unsafe" +) // FocusEvent represents a focus gained or lost event for focus // reporting mode (mode 1004). @@ -25,6 +28,45 @@ const ( FocusLost FocusEvent = C.GHOSTTY_FOCUS_LOST ) +// String returns a human-friendly name for the focus event: +// "gained" or "lost". Unknown values render as "unknown". +func (f FocusEvent) String() string { + switch f { + case FocusGained: + return "gained" + case FocusLost: + return "lost" + default: + return "unknown" + } +} + +// FromString parses a focus event name ("gained" or "lost") and +// stores the corresponding FocusEvent value in the receiver. +// Returns an error if the name is not recognized. +func (f *FocusEvent) FromString(s string) error { + switch s { + case "gained": + *f = FocusGained + case "lost": + *f = FocusLost + default: + return fmt.Errorf("libghostty: unknown focus event %q", s) + } + return nil +} + +// NewFocusEventFromString returns the FocusEvent value for the +// given name ("gained" or "lost"). Returns an error if the name +// is not recognized. +func NewFocusEventFromString(s string) (FocusEvent, error) { + var f FocusEvent + if err := f.FromString(s); err != nil { + return 0, err + } + return f, nil +} + // FocusEncode encodes a focus event into a terminal escape sequence // and returns the result as a byte slice. func FocusEncode(event FocusEvent) ([]byte, error) { diff --git a/key_string.go b/key_string.go new file mode 100644 index 0000000..9f1a059 --- /dev/null +++ b/key_string.go @@ -0,0 +1,259 @@ +package libghostty + +import "fmt" + +// Human-friendly string conversion for Key values. The names use +// snake_case and match the canonical names used by upstream +// libghostty's Zig source (e.g. "key_a", "arrow_down", "fn"). This +// makes them suitable for serialization in JSON, configuration files, +// or any other text-based format. + +// keyNames is the canonical list mapping each Key constant to its +// human-friendly snake_case string name. It is used as the source of +// truth for both String and FromString and is built into reverse +// lookup maps in init. +var keyNames = []struct { + key Key + name string +}{ + // Writing System Keys (W3C § 3.1.1) + {KeyUnidentified, "unidentified"}, + {KeyBackquote, "backquote"}, + {KeyBackslash, "backslash"}, + {KeyBracketLeft, "bracket_left"}, + {KeyBracketRight, "bracket_right"}, + {KeyComma, "comma"}, + {KeyDigit0, "digit_0"}, + {KeyDigit1, "digit_1"}, + {KeyDigit2, "digit_2"}, + {KeyDigit3, "digit_3"}, + {KeyDigit4, "digit_4"}, + {KeyDigit5, "digit_5"}, + {KeyDigit6, "digit_6"}, + {KeyDigit7, "digit_7"}, + {KeyDigit8, "digit_8"}, + {KeyDigit9, "digit_9"}, + {KeyEqual, "equal"}, + {KeyIntlBackslash, "intl_backslash"}, + {KeyIntlRo, "intl_ro"}, + {KeyIntlYen, "intl_yen"}, + {KeyA, "key_a"}, + {KeyB, "key_b"}, + {KeyC, "key_c"}, + {KeyD, "key_d"}, + {KeyE, "key_e"}, + {KeyF, "key_f"}, + {KeyG, "key_g"}, + {KeyH, "key_h"}, + {KeyI, "key_i"}, + {KeyJ, "key_j"}, + {KeyK, "key_k"}, + {KeyL, "key_l"}, + {KeyM, "key_m"}, + {KeyN, "key_n"}, + {KeyO, "key_o"}, + {KeyP, "key_p"}, + {KeyQ, "key_q"}, + {KeyR, "key_r"}, + {KeyS, "key_s"}, + {KeyT, "key_t"}, + {KeyU, "key_u"}, + {KeyV, "key_v"}, + {KeyW, "key_w"}, + {KeyX, "key_x"}, + {KeyY, "key_y"}, + {KeyZ, "key_z"}, + {KeyMinus, "minus"}, + {KeyPeriod, "period"}, + {KeyQuote, "quote"}, + {KeySemicolon, "semicolon"}, + {KeySlash, "slash"}, + + // Functional Keys (W3C § 3.1.2) + {KeyAltLeft, "alt_left"}, + {KeyAltRight, "alt_right"}, + {KeyBackspace, "backspace"}, + {KeyCapsLock, "caps_lock"}, + {KeyContextMenu, "context_menu"}, + {KeyControlLeft, "control_left"}, + {KeyControlRight, "control_right"}, + {KeyEnter, "enter"}, + {KeyMetaLeft, "meta_left"}, + {KeyMetaRight, "meta_right"}, + {KeyShiftLeft, "shift_left"}, + {KeyShiftRight, "shift_right"}, + {KeySpace, "space"}, + {KeyTab, "tab"}, + {KeyConvert, "convert"}, + {KeyKanaMode, "kana_mode"}, + {KeyNonConvert, "non_convert"}, + + // Control Pad Section (W3C § 3.2) + {KeyDelete, "delete"}, + {KeyEnd, "end"}, + {KeyHelp, "help"}, + {KeyHome, "home"}, + {KeyInsert, "insert"}, + {KeyPageDown, "page_down"}, + {KeyPageUp, "page_up"}, + + // Arrow Pad Section (W3C § 3.3) + {KeyArrowDown, "arrow_down"}, + {KeyArrowLeft, "arrow_left"}, + {KeyArrowRight, "arrow_right"}, + {KeyArrowUp, "arrow_up"}, + + // Numpad Section (W3C § 3.4) + {KeyNumLock, "num_lock"}, + {KeyNumpad0, "numpad_0"}, + {KeyNumpad1, "numpad_1"}, + {KeyNumpad2, "numpad_2"}, + {KeyNumpad3, "numpad_3"}, + {KeyNumpad4, "numpad_4"}, + {KeyNumpad5, "numpad_5"}, + {KeyNumpad6, "numpad_6"}, + {KeyNumpad7, "numpad_7"}, + {KeyNumpad8, "numpad_8"}, + {KeyNumpad9, "numpad_9"}, + {KeyNumpadAdd, "numpad_add"}, + {KeyNumpadBackspace, "numpad_backspace"}, + {KeyNumpadClear, "numpad_clear"}, + {KeyNumpadClearEntry, "numpad_clear_entry"}, + {KeyNumpadComma, "numpad_comma"}, + {KeyNumpadDecimal, "numpad_decimal"}, + {KeyNumpadDivide, "numpad_divide"}, + {KeyNumpadEnter, "numpad_enter"}, + {KeyNumpadEqual, "numpad_equal"}, + {KeyNumpadMemoryAdd, "numpad_memory_add"}, + {KeyNumpadMemoryClear, "numpad_memory_clear"}, + {KeyNumpadMemoryRecall, "numpad_memory_recall"}, + {KeyNumpadMemoryStore, "numpad_memory_store"}, + {KeyNumpadMemorySub, "numpad_memory_subtract"}, + {KeyNumpadMultiply, "numpad_multiply"}, + {KeyNumpadParenLeft, "numpad_paren_left"}, + {KeyNumpadParenRight, "numpad_paren_right"}, + {KeyNumpadSubtract, "numpad_subtract"}, + {KeyNumpadSeparator, "numpad_separator"}, + {KeyNumpadUp, "numpad_up"}, + {KeyNumpadDown, "numpad_down"}, + {KeyNumpadRight, "numpad_right"}, + {KeyNumpadLeft, "numpad_left"}, + {KeyNumpadBegin, "numpad_begin"}, + {KeyNumpadHome, "numpad_home"}, + {KeyNumpadEnd, "numpad_end"}, + {KeyNumpadInsert, "numpad_insert"}, + {KeyNumpadDelete, "numpad_delete"}, + {KeyNumpadPageUp, "numpad_page_up"}, + {KeyNumpadPageDown, "numpad_page_down"}, + + // Function Section (W3C § 3.5) + {KeyEscape, "escape"}, + {KeyF1, "f1"}, + {KeyF2, "f2"}, + {KeyF3, "f3"}, + {KeyF4, "f4"}, + {KeyF5, "f5"}, + {KeyF6, "f6"}, + {KeyF7, "f7"}, + {KeyF8, "f8"}, + {KeyF9, "f9"}, + {KeyF10, "f10"}, + {KeyF11, "f11"}, + {KeyF12, "f12"}, + {KeyF13, "f13"}, + {KeyF14, "f14"}, + {KeyF15, "f15"}, + {KeyF16, "f16"}, + {KeyF17, "f17"}, + {KeyF18, "f18"}, + {KeyF19, "f19"}, + {KeyF20, "f20"}, + {KeyF21, "f21"}, + {KeyF22, "f22"}, + {KeyF23, "f23"}, + {KeyF24, "f24"}, + {KeyF25, "f25"}, + {KeyFn, "fn"}, + {KeyFnLock, "fn_lock"}, + {KeyPrintScreen, "print_screen"}, + {KeyScrollLock, "scroll_lock"}, + {KeyPause, "pause"}, + + // Media Keys (W3C § 3.6) + {KeyBrowserBack, "browser_back"}, + {KeyBrowserFavorites, "browser_favorites"}, + {KeyBrowserForward, "browser_forward"}, + {KeyBrowserHome, "browser_home"}, + {KeyBrowserRefresh, "browser_refresh"}, + {KeyBrowserSearch, "browser_search"}, + {KeyBrowserStop, "browser_stop"}, + {KeyEject, "eject"}, + {KeyLaunchApp1, "launch_app_1"}, + {KeyLaunchApp2, "launch_app_2"}, + {KeyLaunchMail, "launch_mail"}, + {KeyMediaPlayPause, "media_play_pause"}, + {KeyMediaSelect, "media_select"}, + {KeyMediaStop, "media_stop"}, + {KeyMediaTrackNext, "media_track_next"}, + {KeyMediaTrackPrevious, "media_track_previous"}, + {KeyPower, "power"}, + {KeySleep, "sleep"}, + {KeyAudioVolumeDown, "audio_volume_down"}, + {KeyAudioVolumeMute, "audio_volume_mute"}, + {KeyAudioVolumeUp, "audio_volume_up"}, + {KeyWakeUp, "wake_up"}, + + // Legacy, Non-standard, and Special Keys (W3C § 3.7) + {KeyCopy, "copy"}, + {KeyCut, "cut"}, + {KeyPaste, "paste"}, +} + +// keyToName maps a Key value to its canonical string name. Built in +// init from keyNames. +var keyToName map[Key]string + +// nameToKey maps a string name to its Key value. Built in init from +// keyNames. +var nameToKey map[string]Key + +func init() { + keyToName = make(map[Key]string, len(keyNames)) + nameToKey = make(map[string]Key, len(keyNames)) + for _, e := range keyNames { + keyToName[e.key] = e.name + nameToKey[e.name] = e.key + } +} + +// String returns the canonical snake_case name of the Key (e.g. +// "key_a", "arrow_down", "fn"). Returns "unidentified" for unknown +// values, mirroring the behavior of KeyUnidentified. +func (k Key) String() string { + if name, ok := keyToName[k]; ok { + return name + } + return keyToName[KeyUnidentified] +} + +// FromString parses a canonical snake_case key name and stores the +// corresponding Key value in the receiver. Returns an error if the +// name is not recognized. +func (k *Key) FromString(s string) error { + if v, ok := nameToKey[s]; ok { + *k = v + return nil + } + return fmt.Errorf("libghostty: unknown key name %q", s) +} + +// NewKeyFromString returns the Key value for the given canonical +// snake_case key name (e.g. "key_a", "arrow_down"). Returns an +// error if the name is not recognized. +func NewKeyFromString(s string) (Key, error) { + var k Key + if err := k.FromString(s); err != nil { + return 0, err + } + return k, nil +} diff --git a/mods_string.go b/mods_string.go new file mode 100644 index 0000000..f38f64f --- /dev/null +++ b/mods_string.go @@ -0,0 +1,115 @@ +package libghostty + +import ( + "fmt" + "strings" +) + +// Human-friendly string conversion for Mods bitmasks. Each set bit +// is rendered using a snake_case name; multiple modifiers are joined +// with "+". Parsing accepts "+" or "," as separators and supports +// common aliases (cmd/command for super, opt/option for alt, control +// for ctrl), matching the upstream Zig source's modifier alias list. + +// modBitNames is the canonical ordered list of single-bit Mods values +// and their snake_case names. The ordering controls the output of +// String, which always renders bits in this fixed order so that +// equivalent bitmasks produce identical strings. +var modBitNames = []struct { + bit Mods + name string +}{ + {ModShift, "shift"}, + {ModCtrl, "ctrl"}, + {ModAlt, "alt"}, + {ModSuper, "super"}, + {ModCapsLock, "caps_lock"}, + {ModNumLock, "num_lock"}, + {ModShiftSide, "shift_side"}, + {ModCtrlSide, "ctrl_side"}, + {ModAltSide, "alt_side"}, + {ModSuperSide, "super_side"}, +} + +// modAliases lists alternate names accepted by FromString. The +// canonical name for each modifier is in modBitNames. +var modAliases = map[string]Mods{ + "cmd": ModSuper, + "command": ModSuper, + "opt": ModAlt, + "option": ModAlt, + "control": ModCtrl, +} + +// String returns a human-friendly representation of the modifier +// bitmask: each set bit is rendered as its snake_case name and bits +// are joined with "+" in a stable canonical order. Returns "" if no +// bits are set. +// +// Examples: +// +// Mods(0).String() == "" +// (ModShift | ModCtrl).String() == "shift+ctrl" +// (ModShift | ModShiftSide).String() == "shift+shift_side" +func (m Mods) String() string { + if m == 0 { + return "" + } + var parts []string + for _, e := range modBitNames { + if m&e.bit != 0 { + parts = append(parts, e.name) + } + } + return strings.Join(parts, "+") +} + +// FromString parses a "+" or "," separated list of modifier names +// and stores the resulting bitmask in the receiver. Whitespace +// around tokens and empty tokens are ignored. Recognized names are +// the canonical snake_case names returned by String plus the +// upstream aliases (cmd/command, opt/option, control). Returns an +// error if any token is not recognized. +// +// The receiver is overwritten, not OR'd into. +func (m *Mods) FromString(s string) error { + var out Mods + if s != "" { + // Normalize "," separators to "+" so we can split once. + s = strings.ReplaceAll(s, ",", "+") + for _, raw := range strings.Split(s, "+") { + tok := strings.TrimSpace(raw) + if tok == "" { + continue + } + if alias, ok := modAliases[tok]; ok { + out |= alias + continue + } + matched := false + for _, e := range modBitNames { + if e.name == tok { + out |= e.bit + matched = true + break + } + } + if !matched { + return fmt.Errorf("libghostty: unknown modifier %q", tok) + } + } + } + *m = out + return nil +} + +// NewModsFromString returns the Mods value parsed from the given +// "+" or "," separated list of modifier names. See Mods.FromString +// for the accepted syntax and aliases. +func NewModsFromString(s string) (Mods, error) { + var m Mods + if err := m.FromString(s); err != nil { + return 0, err + } + return m, nil +} diff --git a/mouse_event.go b/mouse_event.go index b31bfcf..7692a75 100644 --- a/mouse_event.go +++ b/mouse_event.go @@ -8,6 +8,8 @@ package libghostty */ import "C" +import "fmt" + // MouseEvent is an opaque handle representing a normalized mouse // input event containing action, button, modifiers, and surface-space // position. It is mutable and reusable, but not safe for concurrent @@ -55,6 +57,63 @@ const ( MouseButtonEleven MouseButton = C.GHOSTTY_MOUSE_BUTTON_ELEVEN ) +// mouseButtonNames is the canonical mapping between MouseButton +// values and their snake_case string names. Used as the source of +// truth for both String and FromString. +var mouseButtonNames = []struct { + button MouseButton + name string +}{ + {MouseButtonUnknown, "unknown"}, + {MouseButtonLeft, "left"}, + {MouseButtonRight, "right"}, + {MouseButtonMiddle, "middle"}, + {MouseButtonFour, "four"}, + {MouseButtonFive, "five"}, + {MouseButtonSix, "six"}, + {MouseButtonSeven, "seven"}, + {MouseButtonEight, "eight"}, + {MouseButtonNine, "nine"}, + {MouseButtonTen, "ten"}, + {MouseButtonEleven, "eleven"}, +} + +// String returns the canonical lowercase name of the mouse button +// (e.g. "left", "right", "four"). Unknown values render as +// "unknown". +func (b MouseButton) String() string { + for _, e := range mouseButtonNames { + if e.button == b { + return e.name + } + } + return "unknown" +} + +// FromString parses a canonical mouse button name and stores the +// corresponding MouseButton value in the receiver. Returns an error +// if the name is not recognized. +func (b *MouseButton) FromString(s string) error { + for _, e := range mouseButtonNames { + if e.name == s { + *b = e.button + return nil + } + } + return fmt.Errorf("libghostty: unknown mouse button %q", s) +} + +// NewMouseButtonFromString returns the MouseButton value for the +// given canonical name (e.g. "left", "right", "four"). Returns an +// error if the name is not recognized. +func NewMouseButtonFromString(s string) (MouseButton, error) { + var b MouseButton + if err := b.FromString(s); err != nil { + return MouseButtonUnknown, err + } + return b, nil +} + // MousePosition represents a mouse position in surface-space pixels. // // C: GhosttyMousePosition diff --git a/string_test.go b/string_test.go new file mode 100644 index 0000000..e9ba407 --- /dev/null +++ b/string_test.go @@ -0,0 +1,205 @@ +package libghostty + +import "testing" + +// Tests for String / FromString conversions across enum types. + +func TestKeyString(t *testing.T) { + cases := []struct { + key Key + name string + }{ + {KeyA, "key_a"}, + {KeyZ, "key_z"}, + {KeyArrowUp, "arrow_up"}, + {KeyDigit5, "digit_5"}, + {KeyFn, "fn"}, + {KeyF13, "f13"}, + {KeyNumpadMemorySub, "numpad_memory_subtract"}, + {KeyUnidentified, "unidentified"}, + } + for _, c := range cases { + if got := c.key.String(); got != c.name { + t.Errorf("Key(%d).String() = %q, want %q", c.key, got, c.name) + } + } +} + +func TestKeyFromString(t *testing.T) { + var k Key + if err := k.FromString("arrow_left"); err != nil { + t.Fatal(err) + } + if k != KeyArrowLeft { + t.Fatalf("expected KeyArrowLeft, got %d", k) + } + if err := k.FromString("not_a_real_key"); err == nil { + t.Fatal("expected error for unknown key name") + } +} + +func TestKeyRoundtrip(t *testing.T) { + for _, e := range keyNames { + var k Key + if err := k.FromString(e.name); err != nil { + t.Fatalf("FromString(%q) failed: %v", e.name, err) + } + if k != e.key { + t.Fatalf("FromString(%q) = %d, want %d", e.name, k, e.key) + } + if got := k.String(); got != e.name { + t.Fatalf("String() roundtrip mismatch: %q -> %q", e.name, got) + } + } +} + +func TestModsString(t *testing.T) { + cases := []struct { + mods Mods + want string + }{ + {0, ""}, + {ModShift, "shift"}, + {ModCtrl | ModShift, "shift+ctrl"}, + {ModSuper | ModCapsLock, "super+caps_lock"}, + {ModShift | ModShiftSide, "shift+shift_side"}, + {ModShift | ModCtrl | ModAlt | ModSuper, "shift+ctrl+alt+super"}, + } + for _, c := range cases { + if got := c.mods.String(); got != c.want { + t.Errorf("Mods(%d).String() = %q, want %q", c.mods, got, c.want) + } + } +} + +func TestModsFromString(t *testing.T) { + cases := []struct { + in string + want Mods + }{ + {"", 0}, + {"shift", ModShift}, + {"shift+ctrl", ModShift | ModCtrl}, + {"ctrl,shift", ModShift | ModCtrl}, + {" shift + ctrl ", ModShift | ModCtrl}, + // Aliases + {"cmd+opt", ModSuper | ModAlt}, + {"command+option+control", ModSuper | ModAlt | ModCtrl}, + // Empty tokens are skipped + {"shift++ctrl", ModShift | ModCtrl}, + } + for _, c := range cases { + var m Mods + if err := m.FromString(c.in); err != nil { + t.Fatalf("FromString(%q) failed: %v", c.in, err) + } + if m != c.want { + t.Errorf("FromString(%q) = %d, want %d", c.in, m, c.want) + } + } + + var m Mods + if err := m.FromString("nope"); err == nil { + t.Fatal("expected error for unknown modifier") + } +} + +func TestModsRoundtrip(t *testing.T) { + all := ModShift | ModCtrl | ModAlt | ModSuper | ModCapsLock | + ModNumLock | ModShiftSide | ModCtrlSide | ModAltSide | ModSuperSide + var m Mods + if err := m.FromString(all.String()); err != nil { + t.Fatal(err) + } + if m != all { + t.Fatalf("roundtrip failed: %d -> %q -> %d", all, all.String(), m) + } +} + +func TestMouseButtonString(t *testing.T) { + cases := []struct { + button MouseButton + want string + }{ + {MouseButtonUnknown, "unknown"}, + {MouseButtonLeft, "left"}, + {MouseButtonRight, "right"}, + {MouseButtonMiddle, "middle"}, + {MouseButtonFour, "four"}, + {MouseButtonEleven, "eleven"}, + } + for _, c := range cases { + if got := c.button.String(); got != c.want { + t.Errorf("MouseButton(%d).String() = %q, want %q", c.button, got, c.want) + } + } +} + +func TestMouseButtonFromString(t *testing.T) { + for _, e := range mouseButtonNames { + var b MouseButton + if err := b.FromString(e.name); err != nil { + t.Fatalf("FromString(%q) failed: %v", e.name, err) + } + if b != e.button { + t.Fatalf("FromString(%q) = %d, want %d", e.name, b, e.button) + } + } + + var b MouseButton + if err := b.FromString("nope"); err == nil { + t.Fatal("expected error for unknown mouse button") + } +} + +func TestFocusEventString(t *testing.T) { + if FocusGained.String() != "gained" { + t.Errorf("FocusGained.String() = %q, want %q", FocusGained.String(), "gained") + } + if FocusLost.String() != "lost" { + t.Errorf("FocusLost.String() = %q, want %q", FocusLost.String(), "lost") + } +} + +func TestFocusEventFromString(t *testing.T) { + var f FocusEvent + if err := f.FromString("gained"); err != nil || f != FocusGained { + t.Fatalf("FromString(gained) = %d, %v", f, err) + } + if err := f.FromString("lost"); err != nil || f != FocusLost { + t.Fatalf("FromString(lost) = %d, %v", f, err) + } + if err := f.FromString("nope"); err == nil { + t.Fatal("expected error for unknown focus event") + } +} + +func TestNewFromStringConstructors(t *testing.T) { + if k, err := NewKeyFromString("arrow_up"); err != nil || k != KeyArrowUp { + t.Fatalf("NewKeyFromString(arrow_up) = %d, %v", k, err) + } + if _, err := NewKeyFromString("nope"); err == nil { + t.Fatal("expected error from NewKeyFromString") + } + + if m, err := NewModsFromString("shift+ctrl"); err != nil || m != ModShift|ModCtrl { + t.Fatalf("NewModsFromString = %d, %v", m, err) + } + if _, err := NewModsFromString("nope"); err == nil { + t.Fatal("expected error from NewModsFromString") + } + + if b, err := NewMouseButtonFromString("middle"); err != nil || b != MouseButtonMiddle { + t.Fatalf("NewMouseButtonFromString = %d, %v", b, err) + } + if _, err := NewMouseButtonFromString("nope"); err == nil { + t.Fatal("expected error from NewMouseButtonFromString") + } + + if f, err := NewFocusEventFromString("gained"); err != nil || f != FocusGained { + t.Fatalf("NewFocusEventFromString = %d, %v", f, err) + } + if _, err := NewFocusEventFromString("nope"); err == nil { + t.Fatal("expected error from NewFocusEventFromString") + } +}