From 8bb43a4263a4f9d146fdcfc756fa2dbc8eb5716b Mon Sep 17 00:00:00 2001 From: milarin Date: Wed, 14 Feb 2024 15:12:39 +0100 Subject: [PATCH] rewrite --- client.go | 43 +++++++++ event.go | 173 ++++++++++++++++++++++++++++++++++-- event_type.go | 9 -- find.go | 31 ------- find_test.go | 18 ---- get_tree.go | 23 ----- get_tree_test.go | 16 ---- get_workspaces.go | 40 +++++++-- get_workspaces_test.go | 18 ---- go.mod | 4 +- go.sum | 6 +- node.go | 166 ----------------------------------- run_command.go | 50 +++++++++-- subscribe.go | 113 +++++++++++++++++++++--- subscribe_test.go | 18 ---- types.go | 195 +++++++++++++++++++++++++++++++++++++++++ 16 files changed, 588 insertions(+), 335 deletions(-) create mode 100644 client.go delete mode 100644 event_type.go delete mode 100644 find.go delete mode 100644 find_test.go delete mode 100644 get_tree.go delete mode 100644 get_tree_test.go delete mode 100644 get_workspaces_test.go delete mode 100644 node.go delete mode 100644 subscribe_test.go create mode 100644 types.go diff --git a/client.go b/client.go new file mode 100644 index 0000000..37cc67f --- /dev/null +++ b/client.go @@ -0,0 +1,43 @@ +package sway + +import ( + "errors" + "net" + "os" + "sync" +) + +type Client struct { + sync.Mutex + + socket string + conn net.Conn +} + +func GetDefaultClient() (*Client, error) { + socket, ok := os.LookupEnv("SWAYSOCK") + if !ok { + return nil, errors.New("could not find sway socket. is $SWAYSOCK set properly?") + } + + return GetClientBySocket(socket) +} + +func GetClientBySocket(socket string) (*Client, error) { + conn, err := net.Dial("unix", socket) + if err != nil { + return nil, err + } + + return &Client{ + socket: socket, + conn: conn, + }, nil +} + +func (c *Client) Close() error { + c.Lock() + defer c.Unlock() + + return c.conn.Close() +} diff --git a/event.go b/event.go index 47cdc22..157f0e0 100644 --- a/event.go +++ b/event.go @@ -1,16 +1,173 @@ package sway -import ( - "encoding/json" +import "encoding/json" - "git.milar.in/milarin/adverr" +type EventType uint32 + +const ( + // i3 events + EventTypeWorkspace EventType = 0x80000000 + iota + _ EventType = 0x80000000 + iota // output event not available in sway + EventTypeMode EventType = 0x80000000 + iota + EventTypeWindow EventType = 0x80000000 + iota + _ EventType = 0x80000000 + iota // TODO implement EventTypeBarConfigUpdate + EventTypeBinding EventType = 0x80000000 + iota + EventTypeShutdown EventType = 0x80000000 + iota + EventTypeTick EventType = 0x80000000 + iota + + // sway-only events + EventTypeBarStateUpdate EventType = 0x80000014 + EventTypeInput EventType = 0x80000015 ) -type Event struct { - Change string `json:"change"` - Container Node `json:"container"` +var eventStringRepresentations = map[EventType]string{ + EventTypeWorkspace: "workspace", + EventTypeMode: "mode", + EventTypeWindow: "window", + //EventTypeBarConfigUpdate: "barconfig_update", + EventTypeBinding: "binding", + EventTypeShutdown: "shutdown", + EventTypeTick: "tick", + EventTypeBarStateUpdate: "bar_state_update", + EventTypeInput: "input", } -func (e Event) String() string { - return string(adverr.Must(json.MarshalIndent(e, "", "\t"))) +func (et EventType) String() string { + return eventStringRepresentations[et] +} + +type Event interface { + Type() EventType +} + +type WorkspaceEvent struct { + Change WorkspaceEventChange `json:"change"` + Current *Workspace `json:"current"` + Old *Workspace `json:"old"` +} + +var _ Event = &WorkspaceEvent{} + +func (e WorkspaceEvent) Type() EventType { + return EventTypeWorkspace +} + +func (e WorkspaceEvent) String() string { + data, _ := json.MarshalIndent(e, "", "\t") + return string(data) +} + +type ModeEvent struct { + Change string `json:"change"` + PangoMarkup bool `json:"pango_markup"` +} + +var _ Event = &ModeEvent{} + +func (e ModeEvent) Type() EventType { + return EventTypeMode +} + +func (e ModeEvent) String() string { + data, _ := json.MarshalIndent(e, "", "\t") + return string(data) +} + +type WindowEvent struct { + Change WindowEventChange `json:"change"` + Container *Node `json:"container"` +} + +var _ Event = &WindowEvent{} + +func (e WindowEvent) Type() EventType { + return EventTypeWindow +} + +func (e WindowEvent) String() string { + data, _ := json.MarshalIndent(e, "", "\t") + return string(data) +} + +type BindingEvent struct { + Change string `json:"change"` + Command string `json:"command"` + EventStateMask []string `json:"event_state_mask"` + InputCode int `json:"input_code"` + Symbol string `json:"symbol"` + InputType InputType `json:"input_type"` +} + +var _ Event = &BindingEvent{} + +func (e BindingEvent) Type() EventType { + return EventTypeBinding +} + +func (e BindingEvent) String() string { + data, _ := json.MarshalIndent(e, "", "\t") + return string(data) +} + +type ShutdownEvent struct { + Change string `json:"change"` +} + +var _ Event = &ShutdownEvent{} + +func (e ShutdownEvent) Type() EventType { + return EventTypeShutdown +} + +func (e ShutdownEvent) String() string { + data, _ := json.MarshalIndent(e, "", "\t") + return string(data) +} + +type TickEvent struct { + First bool `json:"first"` + Payload string `json:"payload"` +} + +var _ Event = &TickEvent{} + +func (e TickEvent) Type() EventType { + return EventTypeTick +} + +func (e TickEvent) String() string { + data, _ := json.MarshalIndent(e, "", "\t") + return string(data) +} + +type BarStateUpdateEvent struct { + ID string `json:"id"` + VisibleByModifier bool `json:"visible_by_modifier"` +} + +var _ Event = &BarStateUpdateEvent{} + +func (e BarStateUpdateEvent) Type() EventType { + return EventTypeBarStateUpdate +} + +func (e BarStateUpdateEvent) String() string { + data, _ := json.MarshalIndent(e, "", "\t") + return string(data) +} + +type InputEvent struct { + Change InputEventChange `json:"change"` + //Input Input `json:"input"` // TODO implement +} + +var _ Event = &InputEvent{} + +func (e InputEvent) Type() EventType { + return EventTypeInput +} + +func (e InputEvent) String() string { + data, _ := json.MarshalIndent(e, "", "\t") + return string(data) } diff --git a/event_type.go b/event_type.go deleted file mode 100644 index 77fb3c3..0000000 --- a/event_type.go +++ /dev/null @@ -1,9 +0,0 @@ -package sway - -type EventType string - -const ( - EventTypeWindow EventType = "window" - EventTypeWorkspace EventType = "workspace" - // TODO -) diff --git a/find.go b/find.go deleted file mode 100644 index 34b20c0..0000000 --- a/find.go +++ /dev/null @@ -1,31 +0,0 @@ -package sway - -func (n Node) FindAll(condition func(n Node) bool) []Node { - nodes := make([]Node, 0, 10) - - if condition(n) { - nodes = append(nodes, n) - } - - for _, child := range n.Nodes { - nodes = append(nodes, child.FindAll(condition)...) - } - - for _, child := range n.FloatingNodes { - nodes = append(nodes, child.FindAll(condition)...) - } - - return nodes -} - -func IsOutput(n Node) bool { - return n.Type == NodeTypeOutput -} - -func IsWorkspace(n Node) bool { - return n.Type == NodeTypeWorkspace -} - -func IsContainer(n Node) bool { - return n.Type == NodeTypeCon -} diff --git a/find_test.go b/find_test.go deleted file mode 100644 index d1fd432..0000000 --- a/find_test.go +++ /dev/null @@ -1,18 +0,0 @@ -package sway - -import ( - "context" - "fmt" - "testing" -) - -func TestFind(t *testing.T) { - root, err := GetTree(context.Background()) - if err != nil { - t.Fatal(err) - } - - for _, node := range root.FindAll(IsWorkspace) { - fmt.Println(node.Type, node.Name) - } -} diff --git a/get_tree.go b/get_tree.go deleted file mode 100644 index c6da1e7..0000000 --- a/get_tree.go +++ /dev/null @@ -1,23 +0,0 @@ -package sway - -import ( - "context" - "encoding/json" - "os/exec" -) - -func GetTree(ctx context.Context) (*Node, error) { - cmd := exec.CommandContext(ctx, "swaymsg", "-t", "get_tree") - - data, err := cmd.Output() - if err != nil { - return nil, err - } - - var root Node - if err := json.Unmarshal(data, &root); err != nil { - return nil, err - } - - return &root, nil -} diff --git a/get_tree_test.go b/get_tree_test.go deleted file mode 100644 index d1a09a0..0000000 --- a/get_tree_test.go +++ /dev/null @@ -1,16 +0,0 @@ -package sway - -import ( - "context" - "fmt" - "testing" -) - -func TestGetTree(t *testing.T) { - root, err := GetTree(context.Background()) - if err != nil { - t.Fatal(err) - } - - fmt.Println(root.ID, root.Name) -} diff --git a/get_workspaces.go b/get_workspaces.go index 764e5b5..4e215ad 100644 --- a/get_workspaces.go +++ b/get_workspaces.go @@ -2,22 +2,46 @@ package sway import ( "context" + "encoding/binary" "encoding/json" - "os/exec" + "fmt" + "io" ) -func GetWorkspaces(ctx context.Context) ([]Node, error) { - cmd := exec.CommandContext(ctx, "swaymsg", "-t", "get_workspaces") +func (c *Client) GetWorkspaces(ctx context.Context) ([]Workspace, error) { + c.Lock() + defer c.Unlock() - data, err := cmd.Output() - if err != nil { + if _, err := fmt.Fprint(c.conn, "i3-ipc"); err != nil { return nil, err } - var workspaces []Node - if err := json.Unmarshal(data, &workspaces); err != nil { + if err := binary.Write(c.conn, binary.LittleEndian, uint32(0)); err != nil { return nil, err } - return workspaces, nil + if err := binary.Write(c.conn, binary.LittleEndian, uint32(1)); err != nil { + return nil, err + } + + if _, err := c.conn.Read(make([]byte, 6)); err != nil { + return nil, err + } + + var length uint32 + if err := binary.Read(c.conn, binary.LittleEndian, &length); err != nil { + return nil, err + } + + var messageType uint32 + if err := binary.Read(c.conn, binary.LittleEndian, &messageType); err != nil { + return nil, err + } + + results := []Workspace{} + if err := json.NewDecoder(io.LimitReader(c.conn, int64(length))).Decode(&results); err != nil { + return nil, err + } + + return results, nil } diff --git a/get_workspaces_test.go b/get_workspaces_test.go deleted file mode 100644 index 776f1ab..0000000 --- a/get_workspaces_test.go +++ /dev/null @@ -1,18 +0,0 @@ -package sway - -import ( - "context" - "fmt" - "testing" -) - -func TestGetWorkspaces(t *testing.T) { - workspaces, err := GetWorkspaces(context.Background()) - if err != nil { - t.Fatal(err) - } - - for _, workspace := range workspaces { - fmt.Println(workspace.Type, workspace.Output, workspace.Name) - } -} diff --git a/go.mod b/go.mod index 3ce9f4b..1a3cbc6 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,6 @@ module git.milar.in/milarin/sway go 1.21.1 -require git.milar.in/milarin/adverr v1.1.1 +require git.milar.in/milarin/slices v0.0.8 + +require git.milar.in/milarin/gmath v0.0.3 // indirect diff --git a/go.sum b/go.sum index 1664c40..3107e78 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,4 @@ -git.milar.in/milarin/adverr v1.1.1 h1:ENtBcqT7CncLsVfaLC3KzX8QSSGiSpsC7I7wDqladu8= -git.milar.in/milarin/adverr v1.1.1/go.mod h1:joU9sBb7ySyNv4SpTXB0Z4o1mjXsArBw4N27wjgzj9E= +git.milar.in/milarin/gmath v0.0.3 h1:ii6rKNItS55O/wtIFhD1cTN2BMwDZjTBmiOocKURvxM= +git.milar.in/milarin/gmath v0.0.3/go.mod h1:HDLftG5RLpiNGKiIWh+O2G1PYkNzyLDADO8Cd/1abiE= +git.milar.in/milarin/slices v0.0.8 h1:qN9TE3tkArdTixMKSnwvNPcApwAjxpLVwA5a9k1rm2s= +git.milar.in/milarin/slices v0.0.8/go.mod h1:qMhdtMnfWswc1rHpwgNw33lB84aNEkdBn5BDiYA+G3k= diff --git a/node.go b/node.go deleted file mode 100644 index 7365f6d..0000000 --- a/node.go +++ /dev/null @@ -1,166 +0,0 @@ -package sway - -type Node struct { - ID NodeID `json:"id"` - Type NodeType `json:"type"` - Orientation Orientation `json:"orientation"` - Percentage *float64 `json:"percent"` - Urgent bool `json:"urgent"` - Marks []string `json:"marks"` - Focused bool `json:"focused"` - Layout string `json:"layout"` - Border BorderStyle `json:"border"` - CurrentBorderWidth int `json:"current_border_width"` - Rect Rect `json:"rect"` - DecoRect Rect `json:"deco_rect"` - WindowRect Rect `json:"window_rect"` - Geometry Rect `json:"geometry"` - Name string `json:"name"` - Nodes []Node `json:"nodes"` - FloatingNodes []Node `json:"floating_nodes"` - Focus []NodeID `json:"focus"` - FullscreenMode FullscreenMode `json:"fullscreen_mode"` - Sticky bool `json:"sticky"` - PID int64 `json:"pid"` - AppID string `json:"app_id"` - Visible bool `json:"visible"` - MaxRenderTimeMilli int64 `json:"max_render_time"` - Shell string `json:"shell"` - InhibitIdle bool `json:"inhibit_idle"` - IdleInhibitors IdleInhibitors `json:"idle_inhibitors"` - - // Xorg compatibility - XorgWindowID *int64 `json:"window"` - XorgProperties *XorgProperties `json:"window_properties"` - - // NodeTypeOutput only - Primary bool `json:"primary"` - Make string `json:"make"` - Model string `json:"model"` - Serial string `json:"serial"` - Modes []OutputMode `json:"modes"` - NonDesktop bool `json:"non_desktop"` - Active bool `json:"active"` - DPMS bool `json:"dpms"` - Power bool `json:"power"` - Scale float64 `json:"scale"` - ScaleFilter ScaleFilter `json:"scale_filter"` - Transform Transform `json:"transform"` - AdaptiveSync string `json:"adaptive_sync_status"` - LayerShellSurfaces []LayerShellSurface `json:"layer_shell_surfaces"` - CurrentWorkspace string `json:"current_workspace"` - CurrentMode OutputMode `json:"current_mode"` - - // NodeTypeWorkspace only - WorkspaceNumber int `json:"num"` - Output string `json:"output"` - Representation string `json:"representation"` -} - -type NodeID int64 - -type NodeType string - -const ( - NodeTypeRoot NodeType = "root" - NodeTypeOutput NodeType = "output" - NodeTypeCon NodeType = "con" - NodeTypeFloatingCon NodeType = "floating_con" - NodeTypeWorkspace NodeType = "workspace" - NodeTypeDockarea NodeType = "dockarea" -) - -type Rect struct { - Y int `json:"y"` - X int `json:"x"` - Width int `json:"width"` - Height int `json:"height"` -} - -type BorderStyle string - -const ( - BorderStyleNormal BorderStyle = "normal" - BorderStyleNo BorderStyle = "none" - BorderStylePixel BorderStyle = "pixel" -) - -type Layout string - -const ( - LayoutSplitH Layout = "splith" - LayoutSplitV Layout = "splitv" - LayoutStacked Layout = "stacked" - LayoutTabbed Layout = "tabbed" - LayoutDockarea Layout = "dockarea" - LayoutOutput Layout = "output" -) - -type FullscreenMode int64 - -const ( - FullscreenNone FullscreenMode = 0 - FullscreenOutput FullscreenMode = 1 - FullscreenGlobal FullscreenMode = 2 -) - -type IdleInhibitors struct { - User string `json:"user"` - Application string `json:"application"` -} - -type OutputMode struct { - Width int `json:"width"` - Height int `json:"height"` - RefreshRate int `json:"refresh"` - AspectRatio string `json:"picture_aspect_ratio"` -} - -type ScaleFilter string - -const ( - ScaleFilterNearest ScaleFilter = "nearest" - ScaleFilterLinear ScaleFilter = "linear" - ScaleFilterSmart ScaleFilter = "smart" -) - -type Transform string - -const ( - TransformNormal Transform = "normal" - Transform90 Transform = "90" - Transform180 Transform = "180" - Transform270 Transform = "270" - TransformFlipped Transform = "flipped" - TransformFlipped90 Transform = "flipped-90" - TransformFlipped180 Transform = "flipped-180" - TransformFlipped270 Transform = "flipped-270" -) - -type LayerShellSurface struct { - Namespace string `json:"namespace"` - Layer string `json:"layer"` - Extent Rect `json:"extenct"` - Effects []Effect `json:"effects"` -} - -type Effect string - -const ( - EffectBlur Effect = "blur" -) - -type Orientation string - -const ( - OrientationHorizontal Orientation = "horizontal" - OrientationVertical Orientation = "vertical" -) - -type XorgProperties struct { - Title string `json:"title"` - Instance string `json:"instance"` - Class string `json:"class"` - Role string `json:"window_role"` - Transient NodeID `json:"transient_for"` -} diff --git a/run_command.go b/run_command.go index 769f33e..1dd61ba 100644 --- a/run_command.go +++ b/run_command.go @@ -2,15 +2,53 @@ package sway import ( "context" - "os/exec" + "encoding/binary" + "encoding/json" + "fmt" + "io" + + "git.milar.in/milarin/slices" ) -func RunCommand(ctx context.Context, command string) error { - cmd := exec.CommandContext(ctx, "swaymsg", command) +func (c *Client) RunCommand(ctx context.Context, cmd string) ([]error, error) { + c.Lock() + defer c.Unlock() - if err := cmd.Start(); err != nil { - return err + if _, err := fmt.Fprint(c.conn, "i3-ipc"); err != nil { + return nil, err } - return cmd.Wait() + if err := binary.Write(c.conn, binary.LittleEndian, uint32(len(cmd))); err != nil { + return nil, err + } + + if err := binary.Write(c.conn, binary.LittleEndian, uint32(0)); err != nil { + return nil, err + } + + if _, err := fmt.Fprint(c.conn, cmd); err != nil { + return nil, err + } + + if _, err := c.conn.Read(make([]byte, 6)); err != nil { + return nil, err + } + + var length uint32 + if err := binary.Read(c.conn, binary.LittleEndian, &length); err != nil { + return nil, err + } + + var messageType uint32 + if err := binary.Read(c.conn, binary.LittleEndian, &messageType); err != nil { + return nil, err + } + + results := []commandResult{} + if err := json.NewDecoder(io.LimitReader(c.conn, int64(length))).Decode(&results); err != nil { + return nil, err + } + + errors := slices.Map(slices.Filter(results, commandResult.HasError), commandResult.GetError) + return errors, nil } diff --git a/subscribe.go b/subscribe.go index f10fb5e..334aca4 100644 --- a/subscribe.go +++ b/subscribe.go @@ -2,37 +2,128 @@ package sway import ( "context" + "encoding/binary" "encoding/json" - "os/exec" + "fmt" + "io" + "net" + + "git.milar.in/milarin/slices" ) -func Subscribe(ctx context.Context, events ...EventType) (<-chan Event, error) { - data, err := json.Marshal(events) +func (c *Client) Subscribe(ctx context.Context, events ...EventType) (<-chan Event, error) { + conn, err := net.Dial("unix", c.socket) if err != nil { return nil, err } - cmd := exec.CommandContext(ctx, "swaymsg", "-t", "subscribe", "-m", string(data)) + if _, err := fmt.Fprint(conn, "i3-ipc"); err != nil { + return nil, err + } - stdout, err := cmd.StdoutPipe() + payload, err := json.Marshal(slices.Map(events, EventType.String)) if err != nil { return nil, err } + if err := binary.Write(conn, binary.LittleEndian, uint32(len(payload))); err != nil { + return nil, err + } + + if err := binary.Write(conn, binary.LittleEndian, uint32(2)); err != nil { + return nil, err + } + + if _, err := conn.Write(payload); err != nil { + return nil, err + } + + if _, err := conn.Read(make([]byte, 6)); err != nil { + return nil, err + } + + var length uint32 + if err := binary.Read(conn, binary.LittleEndian, &length); err != nil { + return nil, err + } + + var messageType uint32 + if err := binary.Read(conn, binary.LittleEndian, &messageType); err != nil { + return nil, err + } + + result := commandResult{} + if err := json.NewDecoder(io.LimitReader(conn, int64(length))).Decode(&result); err != nil { + return nil, err + } + + if result.HasError() { + return nil, result.GetError() + } + + return parseEvents(ctx, conn) +} + +func parseEvents(ctx context.Context, conn net.Conn) (<-chan Event, error) { out := make(chan Event, 10) go func() { - defer cmd.Wait() - dec := json.NewDecoder(stdout) - var event Event - for err := dec.Decode(&event); err == nil; err = dec.Decode(&event) { + defer conn.Close() + defer close(out) + + for ctx.Err() == nil { + if _, err := conn.Read(make([]byte, 6)); err != nil { + panic(err) // TODO debug flag with error printing + } + + var length uint32 + if err := binary.Read(conn, binary.LittleEndian, &length); err != nil { + panic(err) // TODO debug flag with error printing + } + + var messageType EventType + if err := binary.Read(conn, binary.LittleEndian, &messageType); err != nil { + panic(err) // TODO debug flag with error printing + } + + event, err := parseEvent(conn, length, messageType) + if err != nil { + panic(err) // TODO debug flag with error printing + } + out <- event } }() - if err := cmd.Start(); err != nil { + return out, nil +} + +func parseEvent(conn net.Conn, length uint32, messageType EventType) (Event, error) { + var event Event + switch messageType { + case EventTypeWorkspace: + event = &WorkspaceEvent{} + case EventTypeMode: + event = &ModeEvent{} + case EventTypeWindow: + event = &WindowEvent{} + case EventTypeBinding: + event = &BindingEvent{} + case EventTypeShutdown: + event = &ShutdownEvent{} + case EventTypeTick: + event = &TickEvent{} + case EventTypeBarStateUpdate: + event = &BarStateUpdateEvent{} + case EventTypeInput: + event = &InputEvent{} + default: + return nil, fmt.Errorf("invalid event type: %x", messageType) + } + + if err := json.NewDecoder(io.LimitReader(conn, int64(length))).Decode(event); err != nil { return nil, err } - return out, nil + return event, nil } diff --git a/subscribe_test.go b/subscribe_test.go deleted file mode 100644 index 475dbe0..0000000 --- a/subscribe_test.go +++ /dev/null @@ -1,18 +0,0 @@ -package sway - -import ( - "context" - "fmt" - "testing" -) - -func TestSubscribe(t *testing.T) { - events, err := Subscribe(context.Background(), EventTypeWindow, EventTypeWorkspace) - if err != nil { - t.Fatal(err) - } - - for event := range events { - fmt.Println(event) - } -} diff --git a/types.go b/types.go new file mode 100644 index 0000000..8f3e79b --- /dev/null +++ b/types.go @@ -0,0 +1,195 @@ +package sway + +import ( + "encoding/json" + "errors" +) + +type NodeID int64 + +type Rectangle struct { + X int `json:"x"` + Y int `json:"y"` + Width int `json:"width"` + Height int `json:"height"` +} + +func (r Rectangle) String() string { + data, _ := json.MarshalIndent(r, "", "\t") + return string(data) +} + +type Workspace struct { + Num int `json:"num"` + Name string `json:"name"` + Visible bool `json:"visible"` + Focused bool `json:"focused"` + Urgent bool `json:"urgent"` + Rect Rectangle `json:"rect"` + Output string `json:"output"` +} + +func (w Workspace) String() string { + data, _ := json.MarshalIndent(w, "", "\t") + return string(data) +} + +type WindowProperties struct { + Title string `json:"title"` + Instance string `json:"instance"` + Class string `json:"class"` + Role string `json:"window_role"` + Transient NodeID `json:"transient_for"` +} + +func (w WindowProperties) String() string { + data, _ := json.MarshalIndent(w, "", "\t") + return string(data) +} + +type Node struct { + ID NodeID `json:"id"` + Name string `json:"name"` + Type NodeType `json:"type"` + Border BorderStyle `json:"border"` + CurrentBorderWidth int `json:"current_border_width"` + Layout Layout `json:"layout"` + Orientation Orientation `json:"orientation"` + Percent float64 `json:"percent"` + Rect Rectangle `json:"rect"` + WindowRect Rectangle `json:"window_rect"` + DecoRect Rectangle `json:"deco_rect"` + Geometry Rectangle `json:"geometry"` + Urgent bool `json:"urgent"` + Sticky bool `json:"sticky"` + Marks []interface{} `json:"marks"` // TODO type + Focused bool `json:"focused"` + Focus []Node `json:"focus"` + Nodes []Node `json:"nodes"` + FloatingNodes []Node `json:"floating_nodes"` + Representation string `json:"representation"` + FullscreenMode FullscreenMode `json:"fullscreen_mode"` + AppID string `json:"app_id"` + PID int `json:"pid"` + Visible bool `json:"visible"` + Shell string `json:"shell"` + InhibitIdle bool `json:"inhibit_idle"` + Window int `json:"window"` + WindowProperties WindowProperties `json:"window_properties"` + + // Undocumented fields. Use with caution! + Num int `json:"num"` + Output string `json:"output"` +} + +func (n Node) String() string { + data, _ := json.MarshalIndent(n, "", "\t") + return string(data) +} + +type NodeType = string + +const ( + NodeTypeRoot NodeType = "root" + NodeTypeOutput NodeType = "output" + NodeTypeWorkspace NodeType = "workspace" + NodeTypeContainer NodeType = "con" + NodeTypeFloatingContainer NodeType = "floating_con" +) + +type BorderStyle = string + +const ( + BorderStyleNormal BorderStyle = "normal" + BorderStyleNone BorderStyle = "none" + BorderStylePixel BorderStyle = "pixel" + BorderStyleCSD BorderStyle = "csd" +) + +type Layout = string + +const ( + LayoutSplitH Layout = "splith" + LayoutSplitV Layout = "splitv" + LayoutStacked Layout = "stacked" + LayoutTabbed Layout = "tabbed" + LayoutOutput Layout = "output" +) + +type Orientation = string + +const ( + OrientationVertical Orientation = "vertical" + OrientationHorizontal Orientation = "horizontal" + OrientationNone Orientation = "none" +) + +type FullscreenMode = uint8 + +const ( + FullscreenModeNone FullscreenMode = iota + FullscreenModeWorkspace FullscreenMode = iota + FullscreenModeGlobal FullscreenMode = iota +) + +type WorkspaceEventChange = string + +const ( + WorkspaceEventChangeInit WorkspaceEventChange = "init" + WorkspaceEventChangeEmpty WorkspaceEventChange = "empty" + WorkspaceEventChangeFocus WorkspaceEventChange = "focus" + WorkspaceEventChangeMove WorkspaceEventChange = "move" + WorkspaceEventChangeRename WorkspaceEventChange = "rename" + WorkspaceEventChangeUrgent WorkspaceEventChange = "urgent" + WorkspaceEventChangeReload WorkspaceEventChange = "reload" +) + +type WindowEventChange = string + +const ( + WindowEventChangeNew WindowEventChange = "new" + WindowEventChangeClose WindowEventChange = "close" + WindowEventChangeFocus WindowEventChange = "focus" + WindowEventChangeTitle WindowEventChange = "title" + WindowEventChangeFullscreenMode WindowEventChange = "fullscreen_mode" + WindowEventChangeMove WindowEventChange = "move" + WindowEventChangeFloating WindowEventChange = "floating" + WindowEventChangeUrgent WindowEventChange = "urgent" + WindowEventChangeMark WindowEventChange = "mark" +) + +type InputEventChange = string + +const ( + InputEventChangeAdded InputEventChange = "added" + InputEventChangeRemoved InputEventChange = "removed" + InputEventChangeXKBKeymap InputEventChange = "xkb_kemap" + InputEventChangeXKBLayout InputEventChange = "xkb_layout" + InputEventChangeLibInputConfig InputEventChange = "libinput_config" +) + +type InputType = string + +const ( + InputTypeKeyboard InputType = "keyboard" + InputTypeMouse InputType = "mouse" +) + +type commandResult struct { + Success bool `json:"success"` + ParseError bool `json:"parse_error"` + Error string `json:"error"` +} + +func (r commandResult) String() string { + data, _ := json.MarshalIndent(r, "", "\t") + return string(data) +} + +func (r commandResult) GetError() error { + return errors.New(r.Error) +} + +func (r commandResult) HasError() bool { + return !r.Success +}