2019-04-13 00:31:58 +02:00
|
|
|
package sway
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"encoding/binary"
|
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"io/ioutil"
|
|
|
|
"net"
|
|
|
|
"os"
|
|
|
|
"strings"
|
|
|
|
|
2019-04-15 04:21:31 +02:00
|
|
|
"github.com/joshuarubin/lifecycle"
|
2019-04-13 00:31:58 +02:00
|
|
|
"go.uber.org/multierr"
|
|
|
|
)
|
|
|
|
|
|
|
|
type client struct {
|
|
|
|
conn net.Conn
|
|
|
|
path string
|
|
|
|
}
|
|
|
|
|
2019-04-15 04:21:31 +02:00
|
|
|
// A Client provides simple communication with the sway IPC
|
2019-04-13 00:31:58 +02:00
|
|
|
type Client interface {
|
2019-04-15 04:21:31 +02:00
|
|
|
// Runs the payload as sway commands
|
2019-04-13 00:31:58 +02:00
|
|
|
RunCommand(context.Context, string) ([]RunCommandReply, error)
|
2019-04-15 04:21:31 +02:00
|
|
|
|
|
|
|
// Get the list of current workspaces
|
2019-04-14 08:13:12 +02:00
|
|
|
GetWorkspaces(context.Context) ([]Workspace, error)
|
2019-04-15 04:21:31 +02:00
|
|
|
|
|
|
|
// Get the list of current outputs
|
2019-04-14 08:13:12 +02:00
|
|
|
GetOutputs(context.Context) ([]Output, error)
|
2019-04-15 04:21:31 +02:00
|
|
|
|
|
|
|
// Get the node layout tree
|
|
|
|
GetTree(context.Context) (*Node, error)
|
|
|
|
|
|
|
|
// Get the names of all the marks currently set
|
|
|
|
GetMarks(context.Context) ([]string, error)
|
|
|
|
|
|
|
|
// Get the list of configured bar IDs
|
|
|
|
GetBarIDs(context.Context) ([]string, error)
|
|
|
|
|
|
|
|
// Get the specified bar config
|
|
|
|
GetBarConfig(context.Context, string) (*BarConfig, error)
|
|
|
|
|
|
|
|
// Get the version of sway that owns the IPC socket
|
2019-04-14 08:13:12 +02:00
|
|
|
GetVersion(context.Context) (*Version, error)
|
2019-04-15 04:21:31 +02:00
|
|
|
|
|
|
|
// Get the list of binding mode names
|
2019-04-14 08:13:12 +02:00
|
|
|
GetBindingModes(context.Context) ([]string, error)
|
2019-04-15 04:21:31 +02:00
|
|
|
|
|
|
|
// Returns the config that was last loaded
|
2019-04-14 08:13:12 +02:00
|
|
|
GetConfig(context.Context) (*Config, error)
|
2019-04-15 04:21:31 +02:00
|
|
|
|
|
|
|
// Sends a tick event with the specified payload
|
2019-04-14 08:13:12 +02:00
|
|
|
SendTick(context.Context, string) (*TickReply, error)
|
2019-04-15 04:21:31 +02:00
|
|
|
|
|
|
|
// Get the list of input devices
|
2019-04-14 08:13:12 +02:00
|
|
|
GetInputs(context.Context) ([]Input, error)
|
2019-04-15 04:21:31 +02:00
|
|
|
|
|
|
|
// Get the list of seats
|
2019-04-14 08:13:12 +02:00
|
|
|
GetSeats(context.Context) ([]Seat, error)
|
2019-04-13 00:31:58 +02:00
|
|
|
}
|
|
|
|
|
2019-04-15 17:57:48 +02:00
|
|
|
// Option can be passed to New to specify runtime configuration settings
|
|
|
|
type Option func(*client)
|
|
|
|
|
|
|
|
// WithSocketPath explicitly sets the sway socket path so it isn't read from
|
|
|
|
// $SWAYSOCK
|
|
|
|
func WithSocketPath(socketPath string) Option {
|
|
|
|
return func(c *client) {
|
|
|
|
c.path = socketPath
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-04-15 04:21:31 +02:00
|
|
|
// New returns a Client configured to connect to $SWAYSOCK
|
2019-04-15 17:57:48 +02:00
|
|
|
func New(ctx context.Context, opts ...Option) (_ Client, err error) {
|
2019-04-13 00:31:58 +02:00
|
|
|
c := &client{}
|
|
|
|
|
2019-04-15 17:57:48 +02:00
|
|
|
for _, opt := range opts {
|
|
|
|
opt(c)
|
|
|
|
}
|
|
|
|
|
|
|
|
if c.path == "" {
|
|
|
|
c.path = strings.TrimSpace(os.Getenv("SWAYSOCK"))
|
|
|
|
}
|
|
|
|
|
|
|
|
if c.path == "" {
|
2019-04-13 00:31:58 +02:00
|
|
|
return nil, fmt.Errorf("$SWAYSOCK is empty")
|
|
|
|
}
|
|
|
|
|
2019-04-15 18:11:13 +02:00
|
|
|
if c.conn, err = (&net.Dialer{}).DialContext(ctx, "unix", c.path); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2019-04-13 00:31:58 +02:00
|
|
|
|
2019-04-15 04:21:31 +02:00
|
|
|
if lifecycle.Exists(ctx) {
|
|
|
|
lifecycle.DeferErr(ctx, c.conn.Close)
|
|
|
|
} else {
|
|
|
|
go func() {
|
|
|
|
<-ctx.Done()
|
|
|
|
_ = c.conn.Close()
|
|
|
|
}()
|
|
|
|
}
|
|
|
|
|
2019-04-15 18:11:13 +02:00
|
|
|
return c, nil
|
2019-04-13 00:31:58 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|
2019-04-14 08:13:12 +02:00
|
|
|
|
|
|
|
func (c *client) GetWorkspaces(ctx context.Context) ([]Workspace, error) {
|
|
|
|
msg, err := c.roundTrip(ctx, messageTypeGetWorkspaces, nil)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
var ret []Workspace
|
|
|
|
return ret, msg.Decode(&ret)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *client) GetOutputs(ctx context.Context) ([]Output, error) {
|
|
|
|
msg, err := c.roundTrip(ctx, messageTypeGetOutputs, nil)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
var ret []Output
|
|
|
|
return ret, msg.Decode(&ret)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *client) GetMarks(ctx context.Context) ([]string, error) {
|
|
|
|
msg, err := c.roundTrip(ctx, messageTypeGetMarks, nil)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
var ret []string
|
|
|
|
return ret, msg.Decode(&ret)
|
|
|
|
}
|
|
|
|
|
2019-04-15 04:21:31 +02:00
|
|
|
func (c *client) GetBarIDs(ctx context.Context) ([]string, error) {
|
2019-04-14 08:13:12 +02:00
|
|
|
msg, err := c.roundTrip(ctx, messageTypeGetBarConfig, nil)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2019-04-15 04:21:31 +02:00
|
|
|
var ret []string
|
2019-04-14 08:13:12 +02:00
|
|
|
return ret, msg.Decode(&ret)
|
|
|
|
}
|
|
|
|
|
2019-04-15 04:21:31 +02:00
|
|
|
func (c *client) GetBarConfig(ctx context.Context, id string) (*BarConfig, error) {
|
2019-04-14 08:13:12 +02:00
|
|
|
msg, err := c.roundTrip(ctx, messageTypeGetBarConfig, []byte(id))
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
var ret BarConfig
|
|
|
|
return &ret, msg.Decode(&ret)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *client) GetVersion(ctx context.Context) (*Version, error) {
|
|
|
|
msg, err := c.roundTrip(ctx, messageTypeGetVersion, nil)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
var ret Version
|
|
|
|
return &ret, msg.Decode(&ret)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *client) GetBindingModes(ctx context.Context) ([]string, error) {
|
|
|
|
msg, err := c.roundTrip(ctx, messageTypeGetBindingModes, nil)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
var ret []string
|
|
|
|
return ret, msg.Decode(&ret)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *client) GetConfig(ctx context.Context) (*Config, error) {
|
|
|
|
msg, err := c.roundTrip(ctx, messageTypeGetConfig, nil)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
var ret Config
|
|
|
|
return &ret, msg.Decode(&ret)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *client) SendTick(ctx context.Context, payload string) (*TickReply, error) {
|
|
|
|
msg, err := c.roundTrip(ctx, messageTypeSendTick, []byte(payload))
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
var ret TickReply
|
|
|
|
return &ret, msg.Decode(&ret)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *client) GetInputs(ctx context.Context) ([]Input, error) {
|
|
|
|
msg, err := c.roundTrip(ctx, messageTypeGetInputs, nil)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
var ret []Input
|
|
|
|
return ret, msg.Decode(&ret)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *client) GetSeats(ctx context.Context) ([]Seat, error) {
|
|
|
|
msg, err := c.roundTrip(ctx, messageTypeGetSeats, nil)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
var ret []Seat
|
|
|
|
return ret, msg.Decode(&ret)
|
|
|
|
}
|