first api code

This commit is contained in:
2025-08-14 14:16:05 +02:00
parent f767e697f4
commit 3221777622
6 changed files with 256 additions and 0 deletions

5
go.mod Normal file
View File

@@ -0,0 +1,5 @@
module github.com/tlinden/i3ipc
go 1.22
require github.com/alecthomas/repr v0.5.1 // indirect

2
go.sum Normal file
View File

@@ -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=

80
i3ipc.go Normal file
View File

@@ -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
}

13
io.go Normal file
View File

@@ -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()
}

88
net.go Normal file
View File

@@ -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
}

68
node.go Normal file
View File

@@ -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)
}
}
}