diff --git a/focus.go b/focus.go index 9dae1cb..23c2ad2 100644 --- a/focus.go +++ b/focus.go @@ -10,10 +10,18 @@ package libghostty import "C" import ( + "encoding" "fmt" "unsafe" ) +// Compile-time assertions that FocusEvent implements the standard +// text marshaling interfaces. +var ( + _ encoding.TextMarshaler = FocusEvent(0) + _ encoding.TextUnmarshaler = (*FocusEvent)(nil) +) + // FocusEvent represents a focus gained or lost event for focus // reporting mode (mode 1004). // @@ -41,27 +49,35 @@ func (f FocusEvent) String() string { } } -// 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 { +// MarshalText implements encoding.TextMarshaler. The output is the +// same as String() so the type integrates with encoding/json and +// other text-based encoders. +func (f FocusEvent) MarshalText() ([]byte, error) { + return []byte(f.String()), nil +} + +// UnmarshalText implements encoding.TextUnmarshaler. It 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) UnmarshalText(text []byte) error { + switch string(text) { case "gained": *f = FocusGained case "lost": *f = FocusLost default: - return fmt.Errorf("libghostty: unknown focus event %q", s) + return fmt.Errorf("libghostty: unknown focus event %q", string(text)) } 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) { +// ParseFocusEvent returns the FocusEvent value for the given name +// ("gained" or "lost"). Returns an error if the name is not +// recognized. +func ParseFocusEvent(s string) (FocusEvent, error) { var f FocusEvent - if err := f.FromString(s); err != nil { + if err := f.UnmarshalText([]byte(s)); err != nil { return 0, err } return f, nil diff --git a/key_string.go b/key_string.go index 9f1a059..e24414b 100644 --- a/key_string.go +++ b/key_string.go @@ -1,6 +1,16 @@ package libghostty -import "fmt" +import ( + "encoding" + "fmt" +) + +// Compile-time assertions that Key implements the standard text +// marshaling interfaces. +var ( + _ encoding.TextMarshaler = Key(0) + _ encoding.TextUnmarshaler = (*Key)(nil) +) // Human-friendly string conversion for Key values. The names use // snake_case and match the canonical names used by upstream @@ -236,23 +246,31 @@ func (k Key) String() string { 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 { +// MarshalText implements encoding.TextMarshaler. The output is the +// same as String() so the type integrates with encoding/json and +// other text-based encoders. +func (k Key) MarshalText() ([]byte, error) { + return []byte(k.String()), nil +} + +// UnmarshalText implements encoding.TextUnmarshaler. It parses a +// canonical snake_case key name (e.g. "key_a", "arrow_down") and +// stores the corresponding Key value in the receiver. Returns an +// error if the name is not recognized. +func (k *Key) UnmarshalText(text []byte) error { + if v, ok := nameToKey[string(text)]; ok { *k = v return nil } - return fmt.Errorf("libghostty: unknown key name %q", s) + return fmt.Errorf("libghostty: unknown key name %q", string(text)) } -// 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) { +// ParseKey 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 ParseKey(s string) (Key, error) { var k Key - if err := k.FromString(s); err != nil { + if err := k.UnmarshalText([]byte(s)); err != nil { return 0, err } return k, nil diff --git a/mods_string.go b/mods_string.go index f38f64f..365f2e7 100644 --- a/mods_string.go +++ b/mods_string.go @@ -1,10 +1,18 @@ package libghostty import ( + "encoding" "fmt" "strings" ) +// Compile-time assertions that Mods implements the standard text +// marshaling interfaces. +var ( + _ encoding.TextMarshaler = Mods(0) + _ encoding.TextUnmarshaler = (*Mods)(nil) +) + // 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 @@ -64,19 +72,29 @@ func (m Mods) String() string { 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. +// MarshalText implements encoding.TextMarshaler. The output is the +// same as String() so the type integrates with encoding/json and +// other text-based encoders. An empty bitmask marshals to an empty +// byte slice (i.e. JSON `""`). +func (m Mods) MarshalText() ([]byte, error) { + return []byte(m.String()), nil +} + +// UnmarshalText implements encoding.TextUnmarshaler. It 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). An empty input is valid and +// produces a zero bitmask. Returns an error if any token is not +// recognized. // // The receiver is overwritten, not OR'd into. -func (m *Mods) FromString(s string) error { +func (m *Mods) UnmarshalText(text []byte) error { var out Mods - if s != "" { + if len(text) > 0 { // Normalize "," separators to "+" so we can split once. - s = strings.ReplaceAll(s, ",", "+") + s := strings.ReplaceAll(string(text), ",", "+") for _, raw := range strings.Split(s, "+") { tok := strings.TrimSpace(raw) if tok == "" { @@ -103,12 +121,12 @@ func (m *Mods) FromString(s string) error { 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) { +// ParseMods returns the Mods value parsed from the given "+" or "," +// separated list of modifier names. See Mods.UnmarshalText for the +// accepted syntax and aliases. +func ParseMods(s string) (Mods, error) { var m Mods - if err := m.FromString(s); err != nil { + if err := m.UnmarshalText([]byte(s)); err != nil { return 0, err } return m, nil diff --git a/mouse_event.go b/mouse_event.go index 7692a75..70d823a 100644 --- a/mouse_event.go +++ b/mouse_event.go @@ -8,7 +8,17 @@ package libghostty */ import "C" -import "fmt" +import ( + "encoding" + "fmt" +) + +// Compile-time assertions that MouseButton implements the standard +// text marshaling interfaces. +var ( + _ encoding.TextMarshaler = MouseButton(0) + _ encoding.TextUnmarshaler = (*MouseButton)(nil) +) // MouseEvent is an opaque handle representing a normalized mouse // input event containing action, button, modifiers, and surface-space @@ -90,10 +100,19 @@ func (b MouseButton) String() string { 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 { +// MarshalText implements encoding.TextMarshaler. The output is the +// same as String() so the type integrates with encoding/json and +// other text-based encoders. +func (b MouseButton) MarshalText() ([]byte, error) { + return []byte(b.String()), nil +} + +// UnmarshalText implements encoding.TextUnmarshaler. It parses a +// canonical mouse button name (e.g. "left", "right", "four") and +// stores the corresponding MouseButton value in the receiver. +// Returns an error if the name is not recognized. +func (b *MouseButton) UnmarshalText(text []byte) error { + s := string(text) for _, e := range mouseButtonNames { if e.name == s { *b = e.button @@ -103,12 +122,12 @@ func (b *MouseButton) FromString(s string) error { 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) { +// ParseMouseButton returns the MouseButton value for the given +// canonical name (e.g. "left", "right", "four"). Returns an error +// if the name is not recognized. +func ParseMouseButton(s string) (MouseButton, error) { var b MouseButton - if err := b.FromString(s); err != nil { + if err := b.UnmarshalText([]byte(s)); err != nil { return MouseButtonUnknown, err } return b, nil diff --git a/string_test.go b/string_test.go index e9ba407..51e6d9c 100644 --- a/string_test.go +++ b/string_test.go @@ -1,8 +1,13 @@ package libghostty -import "testing" +import ( + "encoding/json" + "testing" +) -// Tests for String / FromString conversions across enum types. +// Tests for String, MarshalText, UnmarshalText, and Parse* helpers +// across enum types. The compile-time interface assertions live +// alongside each type's implementation. func TestKeyString(t *testing.T) { cases := []struct { @@ -25,15 +30,15 @@ func TestKeyString(t *testing.T) { } } -func TestKeyFromString(t *testing.T) { +func TestKeyUnmarshalText(t *testing.T) { var k Key - if err := k.FromString("arrow_left"); err != nil { + if err := k.UnmarshalText([]byte("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 { + if err := k.UnmarshalText([]byte("not_a_real_key")); err == nil { t.Fatal("expected error for unknown key name") } } @@ -41,18 +46,45 @@ func TestKeyFromString(t *testing.T) { 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 err := k.UnmarshalText([]byte(e.name)); err != nil { + t.Fatalf("UnmarshalText(%q) failed: %v", e.name, err) } if k != e.key { - t.Fatalf("FromString(%q) = %d, want %d", e.name, k, e.key) + t.Fatalf("UnmarshalText(%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) + got, err := k.MarshalText() + if err != nil { + t.Fatalf("MarshalText failed: %v", err) + } + if string(got) != e.name { + t.Fatalf("MarshalText roundtrip mismatch: %q -> %q", e.name, string(got)) } } } +func TestKeyJSON(t *testing.T) { + // Round-trip through encoding/json to confirm TextMarshaler + // integration. + type wrap struct { + K Key `json:"k"` + } + in := wrap{K: KeyArrowUp} + data, err := json.Marshal(in) + if err != nil { + t.Fatal(err) + } + if string(data) != `{"k":"arrow_up"}` { + t.Fatalf("json.Marshal = %s, want %s", data, `{"k":"arrow_up"}`) + } + var out wrap + if err := json.Unmarshal(data, &out); err != nil { + t.Fatal(err) + } + if out.K != KeyArrowUp { + t.Fatalf("json.Unmarshal = %d, want %d", out.K, KeyArrowUp) + } +} + func TestModsString(t *testing.T) { cases := []struct { mods Mods @@ -72,7 +104,17 @@ func TestModsString(t *testing.T) { } } -func TestModsFromString(t *testing.T) { +func TestModsMarshalEmpty(t *testing.T) { + got, err := Mods(0).MarshalText() + if err != nil { + t.Fatal(err) + } + if string(got) != "" { + t.Fatalf("Mods(0).MarshalText() = %q, want empty", string(got)) + } +} + +func TestModsUnmarshalText(t *testing.T) { cases := []struct { in string want Mods @@ -90,16 +132,16 @@ func TestModsFromString(t *testing.T) { } 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 err := m.UnmarshalText([]byte(c.in)); err != nil { + t.Fatalf("UnmarshalText(%q) failed: %v", c.in, err) } if m != c.want { - t.Errorf("FromString(%q) = %d, want %d", c.in, m, c.want) + t.Errorf("UnmarshalText(%q) = %d, want %d", c.in, m, c.want) } } var m Mods - if err := m.FromString("nope"); err == nil { + if err := m.UnmarshalText([]byte("nope")); err == nil { t.Fatal("expected error for unknown modifier") } } @@ -107,12 +149,16 @@ func TestModsFromString(t *testing.T) { func TestModsRoundtrip(t *testing.T) { all := ModShift | ModCtrl | ModAlt | ModSuper | ModCapsLock | ModNumLock | ModShiftSide | ModCtrlSide | ModAltSide | ModSuperSide + text, err := all.MarshalText() + if err != nil { + t.Fatal(err) + } var m Mods - if err := m.FromString(all.String()); err != nil { + if err := m.UnmarshalText(text); err != nil { t.Fatal(err) } if m != all { - t.Fatalf("roundtrip failed: %d -> %q -> %d", all, all.String(), m) + t.Fatalf("roundtrip failed: %d -> %q -> %d", all, string(text), m) } } @@ -135,19 +181,19 @@ func TestMouseButtonString(t *testing.T) { } } -func TestMouseButtonFromString(t *testing.T) { +func TestMouseButtonUnmarshalText(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 err := b.UnmarshalText([]byte(e.name)); err != nil { + t.Fatalf("UnmarshalText(%q) failed: %v", e.name, err) } if b != e.button { - t.Fatalf("FromString(%q) = %d, want %d", e.name, b, e.button) + t.Fatalf("UnmarshalText(%q) = %d, want %d", e.name, b, e.button) } } var b MouseButton - if err := b.FromString("nope"); err == nil { + if err := b.UnmarshalText([]byte("nope")); err == nil { t.Fatal("expected error for unknown mouse button") } } @@ -161,45 +207,45 @@ func TestFocusEventString(t *testing.T) { } } -func TestFocusEventFromString(t *testing.T) { +func TestFocusEventUnmarshalText(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.UnmarshalText([]byte("gained")); err != nil || f != FocusGained { + t.Fatalf("UnmarshalText(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.UnmarshalText([]byte("lost")); err != nil || f != FocusLost { + t.Fatalf("UnmarshalText(lost) = %d, %v", f, err) } - if err := f.FromString("nope"); err == nil { + if err := f.UnmarshalText([]byte("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) +func TestParseHelpers(t *testing.T) { + if k, err := ParseKey("arrow_up"); err != nil || k != KeyArrowUp { + t.Fatalf("ParseKey(arrow_up) = %d, %v", k, err) } - if _, err := NewKeyFromString("nope"); err == nil { - t.Fatal("expected error from NewKeyFromString") + if _, err := ParseKey("nope"); err == nil { + t.Fatal("expected error from ParseKey") } - if m, err := NewModsFromString("shift+ctrl"); err != nil || m != ModShift|ModCtrl { - t.Fatalf("NewModsFromString = %d, %v", m, err) + if m, err := ParseMods("shift+ctrl"); err != nil || m != ModShift|ModCtrl { + t.Fatalf("ParseMods = %d, %v", m, err) } - if _, err := NewModsFromString("nope"); err == nil { - t.Fatal("expected error from NewModsFromString") + if _, err := ParseMods("nope"); err == nil { + t.Fatal("expected error from ParseMods") } - if b, err := NewMouseButtonFromString("middle"); err != nil || b != MouseButtonMiddle { - t.Fatalf("NewMouseButtonFromString = %d, %v", b, err) + if b, err := ParseMouseButton("middle"); err != nil || b != MouseButtonMiddle { + t.Fatalf("ParseMouseButton = %d, %v", b, err) } - if _, err := NewMouseButtonFromString("nope"); err == nil { - t.Fatal("expected error from NewMouseButtonFromString") + if _, err := ParseMouseButton("nope"); err == nil { + t.Fatal("expected error from ParseMouseButton") } - if f, err := NewFocusEventFromString("gained"); err != nil || f != FocusGained { - t.Fatalf("NewFocusEventFromString = %d, %v", f, err) + if f, err := ParseFocusEvent("gained"); err != nil || f != FocusGained { + t.Fatalf("ParseFocusEvent = %d, %v", f, err) } - if _, err := NewFocusEventFromString("nope"); err == nil { - t.Fatal("expected error from NewFocusEventFromString") + if _, err := ParseFocusEvent("nope"); err == nil { + t.Fatal("expected error from ParseFocusEvent") } }