From 32217776225de8697da801230d96f4f110ae411d Mon Sep 17 00:00:00 2001 From: Thomas von Dein Date: Thu, 14 Aug 2025 14:16:05 +0200 Subject: [PATCH] first api code --- go.mod | 5 ++++ go.sum | 2 ++ i3ipc.go | 80 +++++++++++++++++++++++++++++++++++++++++++++++++++ io.go | 13 +++++++++ net.go | 88 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ node.go | 68 +++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 256 insertions(+) create mode 100644 go.mod create mode 100644 go.sum create mode 100644 i3ipc.go create mode 100644 io.go create mode 100644 net.go create mode 100644 node.go diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..e0c8045 --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module github.com/tlinden/i3ipc + +go 1.22 + +require github.com/alecthomas/repr v0.5.1 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..ef53432 --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/alecthomas/repr v0.5.1 h1:E3G4t2QbHTSNpPKBgMTln5KLkZHLOcU7r37J4pXBuIg= +github.com/alecthomas/repr v0.5.1/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= diff --git a/i3ipc.go b/i3ipc.go new file mode 100644 index 0000000..b03fabc --- /dev/null +++ b/i3ipc.go @@ -0,0 +1,80 @@ +package i3ipc + +import ( + "encoding/json" + "fmt" + "net" +) + +const ( + VERSION = "v0.1.0" + + IPC_HEADER_SIZE = 14 + IPC_MAGIC = "i3-ipc" + IPC_MAGIC_LEN = 6 +) + +const ( + // message types + RUN_COMMAND = iota + GET_WORKSPACES + SUBSCRIBE + GET_OUTPUTS + GET_TREE + GET_MARKS + GET_BAR_CONFIG + GET_VERSION + GET_BINDING_MODES + GET_CONFIG + SEND_TICK + SYNC + GET_BINDING_STATE + GET_INPUTS + GET_SEATS +) + +// module struct +type I3ipc struct { + socket net.Conn + SocketFile string +} + +// i3-ipc structs +type Rect struct { + X int `json:"x"` + Y int `json:"y"` + Width int `json:"width"` + Height int `json:"height"` +} + +type Response struct { + Success bool `json:"success"` + ParseError bool `json:"parse_error"` + Error string `json:"error"` +} + +func NewI3ipc(file string) *I3ipc { + if file == "" { + file = "SWAYSOCK" + } + return &I3ipc{SocketFile: file} +} + +func (ipc *I3ipc) GetTree() (*Node, error) { + err := ipc.sendHeader(GET_TREE, 0) + if err != nil { + return nil, err + } + + payload, err := ipc.readResponse() + if err != nil { + return nil, err + } + + node := &Node{} + if err := json.Unmarshal(payload, &node); err != nil { + return nil, fmt.Errorf("failed to unmarshal json: %w", err) + } + + return node, nil +} diff --git a/io.go b/io.go new file mode 100644 index 0000000..afa9c2a --- /dev/null +++ b/io.go @@ -0,0 +1,13 @@ +package i3ipc + +import "os" + +func fileExists(filename string) bool { + info, err := os.Stat(filename) + + if err != nil { + return false + } + + return !info.IsDir() +} diff --git a/net.go b/net.go new file mode 100644 index 0000000..fd0a836 --- /dev/null +++ b/net.go @@ -0,0 +1,88 @@ +package i3ipc + +import ( + "encoding/binary" + "fmt" + "net" + "os" +) + +func (ipc *I3ipc) Connect() error { + if !fileExists(ipc.SocketFile) { + ipc.SocketFile = os.Getenv(ipc.SocketFile) + if ipc.SocketFile == "" { + return fmt.Errorf("socket file %s doesn't exist", ipc.SocketFile) + } + } + + conn, err := net.Dial("unix", ipc.SocketFile) + if err != nil { + return err + } + + ipc.socket = conn + + return nil +} + +func (ipc *I3ipc) Close() { + ipc.socket.Close() +} + +func (ipc *I3ipc) sendHeader(messageType uint32, len uint32) error { + sendPayload := make([]byte, IPC_HEADER_SIZE) + + copy(sendPayload, []byte(IPC_MAGIC)) + binary.LittleEndian.PutUint32(sendPayload[IPC_MAGIC_LEN:], len) + binary.LittleEndian.PutUint32(sendPayload[IPC_MAGIC_LEN+4:], messageType) + + _, err := ipc.socket.Write(sendPayload) + + if err != nil { + return fmt.Errorf("failed to send header to IPC socket %w", err) + } + + return nil +} + +func (ipc *I3ipc) sendPayload(payload []byte) error { + _, err := ipc.socket.Write(payload) + + if err != nil { + return fmt.Errorf("failed to send payload to IPC socket %w", err) + } + + return nil +} + +func (ipc *I3ipc) readResponse() ([]byte, error) { + // read header + buf := make([]byte, IPC_HEADER_SIZE) + + _, err := ipc.socket.Read(buf) + if err != nil { + return nil, fmt.Errorf("failed to read header from ipc socket: %s", err) + } + + // slog.Debug("got IPC header", "header", hex.EncodeToString(buf)) + + if string(buf[:6]) != IPC_MAGIC { + return nil, fmt.Errorf("got invalid response from IPC socket") + } + + payloadLen := binary.LittleEndian.Uint32(buf[6:10]) + + if payloadLen == 0 { + return nil, fmt.Errorf("got empty payload response from IPC socket") + } + + // read payload + payload := make([]byte, payloadLen) + + _, err = ipc.socket.Read(payload) + if err != nil { + return nil, fmt.Errorf("failed to read payload from IPC socket: %s", err) + } + + return payload, nil +} diff --git a/node.go b/node.go new file mode 100644 index 0000000..55af93d --- /dev/null +++ b/node.go @@ -0,0 +1,68 @@ +package i3ipc + +type Node struct { + Id int `json:"id"` + Type string `json:"type"` // output, workspace or container + Name string `json:"name"` // workspace number or app name + Nodes []*Node `json:"nodes"` + FloatingNodes []*Node `json:"floating_nodes"` + Focused bool `json:"focused"` + Urgent bool `json:"urgent"` + Sticky bool `json:"sticky"` + Border string `json:"border"` + Layout string `json:"layout"` + Orientation string `json:"orientation"` + CurrentBorderWidth int `json:"current_border_width"` + Percent float32 `json:"percent"` + Focus []int `json:"focus"` + Window int `json:"window"` // wayland native + X11Window string `json:"app_id"` // x11 compat + Current_workspace string `json:"current_workspace"` + Rect Rect `json:"rect"` + WindowRect Rect `json:"window_rect"` + DecoRect Rect `json:"deco_rect"` + Geometry Rect `json:"geometry"` +} + +var __focused *Node +var __currentworkspace string + +func (node *Node) FindFocused() *Node { + searchFocused(node.Nodes) + if __focused == nil { + searchFocused(node.FloatingNodes) + } + + return __focused +} + +func searchFocused(nodes []*Node) { + for _, node := range nodes { + if node.Focused { + __focused = node + return + } else { + searchFocused(node.Nodes) + if __focused == nil { + searchFocused(node.FloatingNodes) + } + } + + } +} + +func (node *Node) FindCurrentWorkspace() string { + searchCurrentWorkspace(node.Nodes) + return __currentworkspace +} + +func searchCurrentWorkspace(nodes []*Node) { + for _, node := range nodes { + if node.Current_workspace != "" { + __currentworkspace = node.Current_workspace + return + } else { + searchCurrentWorkspace(node.Nodes) + } + } +}