mirror of
https://github.com/joshuarubin/go-sway
synced 2024-11-26 08:43:49 +01:00
starting to work
This commit is contained in:
commit
66e28f3069
142
client.go
Normal file
142
client.go
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
package sway
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/binary"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"go.uber.org/multierr"
|
||||||
|
)
|
||||||
|
|
||||||
|
type client struct {
|
||||||
|
conn net.Conn
|
||||||
|
path string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Client interface {
|
||||||
|
GetTree(context.Context) (*Node, error)
|
||||||
|
RunCommand(context.Context, string) ([]RunCommandReply, error)
|
||||||
|
Close() error
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(ctx context.Context) (_ Client, err error) {
|
||||||
|
c := &client{}
|
||||||
|
|
||||||
|
if c.path = strings.TrimSpace(os.Getenv("SWAYSOCK")); c.path == "" {
|
||||||
|
return nil, fmt.Errorf("$SWAYSOCK is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
c.conn, err = (&net.Dialer{}).DialContext(ctx, "unix", c.path)
|
||||||
|
return c, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *client) Close() error {
|
||||||
|
return c.conn.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
type payloadReader struct {
|
||||||
|
io.Reader
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r payloadReader) Close() error {
|
||||||
|
_, err := ioutil.ReadAll(r)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *client) recvMsg(ctx context.Context) (*message, error) {
|
||||||
|
var h header
|
||||||
|
err := do(ctx, func() error {
|
||||||
|
return binary.Read(c.conn, binary.LittleEndian, &h)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &message{
|
||||||
|
Type: h.Type,
|
||||||
|
Payload: payloadReader{io.LimitReader(c.conn, int64(h.Length))},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *client) roundTrip(ctx context.Context, t messageType, payload []byte) (*message, error) {
|
||||||
|
if c == nil {
|
||||||
|
return nil, fmt.Errorf("not connected")
|
||||||
|
}
|
||||||
|
|
||||||
|
err := do(ctx, func() error {
|
||||||
|
err := binary.Write(c.conn, binary.LittleEndian, &header{magic, uint32(len(payload)), t})
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = c.conn.Write(payload)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.recvMsg(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *client) GetTree(ctx context.Context) (*Node, error) {
|
||||||
|
b, err := c.roundTrip(ctx, messageTypeGetTree, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var n Node
|
||||||
|
return &n, b.Decode(&n)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *client) subscribe(ctx context.Context, events ...EventType) error {
|
||||||
|
payload, err := json.Marshal(events)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
msg, err := c.roundTrip(ctx, messageTypeSubscribe, payload)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var reply struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = msg.Decode(&reply); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !reply.Success {
|
||||||
|
return fmt.Errorf("subscribe unsuccessful")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *client) RunCommand(ctx context.Context, command string) ([]RunCommandReply, error) {
|
||||||
|
msg, err := c.roundTrip(ctx, messageTypeRunCommand, []byte(command))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var replies []RunCommandReply
|
||||||
|
if err = msg.Decode(&replies); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, reply := range replies {
|
||||||
|
if !reply.Success {
|
||||||
|
err = multierr.Append(err, fmt.Errorf("command %q unsuccessful: %v", command, reply.Error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return replies, err
|
||||||
|
}
|
9
go.mod
Normal file
9
go.mod
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
module github.com/joshuarubin/go-sway
|
||||||
|
|
||||||
|
go 1.12
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/stretchr/testify v1.3.0 // indirect
|
||||||
|
go.uber.org/atomic v1.3.2 // indirect
|
||||||
|
go.uber.org/multierr v1.1.0
|
||||||
|
)
|
11
go.sum
Normal file
11
go.sum
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
go.uber.org/atomic v1.3.2 h1:2Oa65PReHzfn29GpvgsYwloV9AVFHPDk8tYxt2c2tr4=
|
||||||
|
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||||
|
go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI=
|
||||||
|
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
69
internal.go
Normal file
69
internal.go
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
package sway
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
type header struct {
|
||||||
|
Magic [6]byte
|
||||||
|
Length uint32
|
||||||
|
Type messageType
|
||||||
|
}
|
||||||
|
|
||||||
|
type message struct {
|
||||||
|
Type messageType
|
||||||
|
Payload io.ReadCloser
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m message) Decode(v interface{}) error {
|
||||||
|
defer m.Payload.Close()
|
||||||
|
return json.NewDecoder(m.Payload).Decode(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
type messageType uint32
|
||||||
|
|
||||||
|
const (
|
||||||
|
messageTypeRunCommand messageType = iota
|
||||||
|
messageTypeGetWorkspaces
|
||||||
|
messageTypeSubscribe
|
||||||
|
messageTypeGetOutputs
|
||||||
|
messageTypeGetTree
|
||||||
|
messageTypeGetMarks
|
||||||
|
messageTypeGetBarConfig
|
||||||
|
messageTypeGetVersion
|
||||||
|
messageTypeGetBindingModes
|
||||||
|
messageTypeGetConfig
|
||||||
|
messageTypeSendTick
|
||||||
|
messageTypeSync
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
eventReplyTypeWorkspace messageType = 0x80000000
|
||||||
|
eventReplyTypeMode messageType = 0x80000002
|
||||||
|
eventReplyTypeWindow messageType = 0x80000003
|
||||||
|
eventReplyTypeBarConfigUpdate messageType = 0x80000004
|
||||||
|
eventReplyTypeBinding messageType = 0x80000005
|
||||||
|
eventReplyTypeShutdown messageType = 0x80000006
|
||||||
|
eventReplyTypeTick messageType = 0x80000007
|
||||||
|
eventReplyTypeBarStatusUpdate messageType = 0x80000014
|
||||||
|
)
|
||||||
|
|
||||||
|
var magic = [6]byte{'i', '3', '-', 'i', 'p', 'c'}
|
||||||
|
|
||||||
|
func do(ctx context.Context, fn func() error) error {
|
||||||
|
done := make(chan struct{})
|
||||||
|
var err error
|
||||||
|
go func() {
|
||||||
|
err = fn()
|
||||||
|
close(done)
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
return err
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
}
|
61
socket_test.go
Normal file
61
socket_test.go
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
package sway_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
sway "github.com/joshuarubin/go-sway"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSocket(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
client, err := sway.New(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
n, err := client.GetTree(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fh := focusHandler(client)
|
||||||
|
|
||||||
|
fh(ctx, n.FocusedNode())
|
||||||
|
|
||||||
|
h := sway.EventHandler{
|
||||||
|
Window: func(ctx context.Context, e sway.WindowEvent) {
|
||||||
|
if e.Change != sway.WindowChangeFocus {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fh(ctx, e.Container.FocusedNode())
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err = sway.Subscribe(ctx, h, sway.EventTypeWindow, sway.EventTypeShutdown)
|
||||||
|
if err != context.DeadlineExceeded && err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func focusHandler(client sway.Client) func(context.Context, *sway.Node) {
|
||||||
|
return func(ctx context.Context, node *sway.Node) {
|
||||||
|
if node == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
opt := "none"
|
||||||
|
if node.AppID == nil || *node.AppID != "kitty" {
|
||||||
|
opt = "altwin:ctrl_win"
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := client.RunCommand(ctx, `input '*' xkb_options `+opt); err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
90
subscribe.go
Normal file
90
subscribe.go
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
package sway
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
type EventType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
EventTypeWorkspace EventType = "workspace"
|
||||||
|
EventTypeMode EventType = "mode"
|
||||||
|
EventTypeWindow EventType = "window"
|
||||||
|
EventTypeBarConfigUpdate EventType = "barconfig_update"
|
||||||
|
EventTypeBinding EventType = "binding"
|
||||||
|
EventTypeShutdown EventType = "shutdown"
|
||||||
|
EventTypeTick EventType = "tick"
|
||||||
|
EventTypeBarStatusUpdate EventType = "bar_status_update"
|
||||||
|
)
|
||||||
|
|
||||||
|
type EventHandler struct {
|
||||||
|
Workspace func(context.Context, WorkspaceEvent)
|
||||||
|
Window func(context.Context, WindowEvent)
|
||||||
|
Shutdown func(context.Context, ShutdownEvent)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Subscribe(ctx context.Context, handler EventHandler, events ...EventType) error {
|
||||||
|
n, err := New(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer n.Close()
|
||||||
|
|
||||||
|
c := n.(*client)
|
||||||
|
|
||||||
|
if err = c.subscribe(ctx, events...); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
reply, err := c.recvMsg(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
handler.process(ctx, reply)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h EventHandler) process(ctx context.Context, reply *message) {
|
||||||
|
switch reply.Type {
|
||||||
|
case eventReplyTypeWorkspace:
|
||||||
|
if h.Workspace == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var e WorkspaceEvent
|
||||||
|
if err := reply.Decode(&e); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
h.Workspace(ctx, e)
|
||||||
|
case eventReplyTypeMode:
|
||||||
|
case eventReplyTypeWindow:
|
||||||
|
if h.Window == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var e WindowEvent
|
||||||
|
if err := reply.Decode(&e); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
h.Window(ctx, e)
|
||||||
|
case eventReplyTypeBarConfigUpdate:
|
||||||
|
case eventReplyTypeBinding:
|
||||||
|
case eventReplyTypeShutdown:
|
||||||
|
if h.Shutdown == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var e ShutdownEvent
|
||||||
|
if err := reply.Decode(&e); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
h.Shutdown(ctx, e)
|
||||||
|
case eventReplyTypeTick:
|
||||||
|
case eventReplyTypeBarStatusUpdate:
|
||||||
|
}
|
||||||
|
}
|
143
types.go
Normal file
143
types.go
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
package sway
|
||||||
|
|
||||||
|
type NodeID int64
|
||||||
|
|
||||||
|
type NodeType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
NodeTypeRoot NodeType = "root"
|
||||||
|
NodeTypeOutput NodeType = "output"
|
||||||
|
NodeTypeCon NodeType = "con"
|
||||||
|
NodeTypeFloatingCon NodeType = "floating_con"
|
||||||
|
NodeTypeWorkspace NodeType = "workspace"
|
||||||
|
)
|
||||||
|
|
||||||
|
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 Rect struct {
|
||||||
|
X int64 `json:"x"`
|
||||||
|
Y int64 `json:"y"`
|
||||||
|
Width int64 `json:"width"`
|
||||||
|
Height int64 `json:"height"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type WindowProperties struct {
|
||||||
|
Title string `json:"title"`
|
||||||
|
Instance string `json:"instance"`
|
||||||
|
Class string `json:"class"`
|
||||||
|
Role string `json:"window_role"`
|
||||||
|
TransientFor NodeID `json:"transient_for"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Node struct {
|
||||||
|
ID NodeID `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type NodeType `json:"type"`
|
||||||
|
Border BorderStyle `json:"border"`
|
||||||
|
CurrentBorderWidth int64 `json:"current_border_width"`
|
||||||
|
Layout Layout `json:"layout"`
|
||||||
|
Percent *float64 `json:"percent"`
|
||||||
|
Rect Rect `json:"rect"`
|
||||||
|
WindowRect Rect `json:"window_rect"`
|
||||||
|
DecoRect Rect `json:"deco_rect"`
|
||||||
|
Geometry Rect `json:"geometry"`
|
||||||
|
Urgent *bool `json:"urgent"`
|
||||||
|
Focused bool `json:"focused"`
|
||||||
|
Focus []NodeID `json:"focus"`
|
||||||
|
Nodes []*Node `json:"nodes"`
|
||||||
|
FloatingNodes []*Node `json:"floating_nodes"`
|
||||||
|
Representation *string `json:"representation"`
|
||||||
|
AppID *string `json:"app_id"`
|
||||||
|
PID *uint32 `json:"pid"`
|
||||||
|
Window *int64 `json:"window"`
|
||||||
|
WindowProperties *WindowProperties `json:"window_properties"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *Node) FocusedNode() *Node {
|
||||||
|
queue := []*Node{n}
|
||||||
|
for len(queue) > 0 {
|
||||||
|
n = queue[0]
|
||||||
|
queue = queue[1:]
|
||||||
|
|
||||||
|
if n == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if n.Focused {
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
queue = append(queue, n.Nodes...)
|
||||||
|
queue = append(queue, n.FloatingNodes...)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type Event interface{}
|
||||||
|
|
||||||
|
type WorkspaceChange string
|
||||||
|
|
||||||
|
const (
|
||||||
|
WorkspaceChangeInit WorkspaceChange = "init"
|
||||||
|
WorkspaceChangeEmpty WorkspaceChange = "empty"
|
||||||
|
WorkspaceChangeFocus WorkspaceChange = "focus"
|
||||||
|
WorkspaceChangeMove WorkspaceChange = "move"
|
||||||
|
WorkspaceChangeRename WorkspaceChange = "rename"
|
||||||
|
WorkspaceChangeUrgent WorkspaceChange = "urgent"
|
||||||
|
WorkspaceChangeReload WorkspaceChange = "reload"
|
||||||
|
)
|
||||||
|
|
||||||
|
type WorkspaceEvent struct {
|
||||||
|
Change WorkspaceChange `json:"change"`
|
||||||
|
Current *Node `json:"current"`
|
||||||
|
Old *Node `json:"old"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type WindowChange string
|
||||||
|
|
||||||
|
const (
|
||||||
|
WindowChangeNew WindowChange = "new"
|
||||||
|
WindowChangeClose WindowChange = "close"
|
||||||
|
WindowChangeFocus WindowChange = "focus"
|
||||||
|
WindowChangeTitle WindowChange = "title"
|
||||||
|
WindowChangeFullscreenMode WindowChange = "fullscreen_mode"
|
||||||
|
WindowChangeMove WindowChange = "move"
|
||||||
|
WindowChangeFloating WindowChange = "floating"
|
||||||
|
WindowChangeUrgent WindowChange = "urgent"
|
||||||
|
WindowChangeMark WindowChange = "mark"
|
||||||
|
)
|
||||||
|
|
||||||
|
type WindowEvent struct {
|
||||||
|
Change WindowChange `json:"change"`
|
||||||
|
Container Node `json:"container"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ShutdownChange string
|
||||||
|
|
||||||
|
const ShutdownChangeExit ShutdownChange = "exit"
|
||||||
|
|
||||||
|
type ShutdownEvent struct {
|
||||||
|
Change ShutdownChange `json:"change"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RunCommandReply struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
Error string `json:"error"`
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user