diff --git a/client.go b/client.go index 7f28419..296acfa 100644 --- a/client.go +++ b/client.go @@ -11,6 +11,7 @@ import ( "os" "strings" + "github.com/joshuarubin/lifecycle" "go.uber.org/multierr" ) @@ -19,23 +20,49 @@ type client struct { path string } +// A Client provides simple communication with the sway IPC type Client interface { - GetTree(context.Context) (*Node, error) + // Runs the payload as sway commands RunCommand(context.Context, string) ([]RunCommandReply, error) + + // Get the list of current workspaces GetWorkspaces(context.Context) ([]Workspace, error) - GetMarks(context.Context) ([]string, error) + + // Get the list of current outputs GetOutputs(context.Context) ([]Output, error) - GetBarIDs(context.Context) ([]BarID, error) - GetBarConfig(context.Context, BarID) (*BarConfig, error) + + // 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 GetVersion(context.Context) (*Version, error) + + // Get the list of binding mode names GetBindingModes(context.Context) ([]string, error) + + // Returns the config that was last loaded GetConfig(context.Context) (*Config, error) + + // Sends a tick event with the specified payload SendTick(context.Context, string) (*TickReply, error) + + // Get the list of input devices GetInputs(context.Context) ([]Input, error) + + // Get the list of seats GetSeats(context.Context) ([]Seat, error) - Close() error } +// New returns a Client configured to connect to $SWAYSOCK func New(ctx context.Context) (_ Client, err error) { c := &client{} @@ -44,11 +71,17 @@ func New(ctx context.Context) (_ Client, err error) { } c.conn, err = (&net.Dialer{}).DialContext(ctx, "unix", c.path) - return c, err -} -func (c *client) Close() error { - return c.conn.Close() + if lifecycle.Exists(ctx) { + lifecycle.DeferErr(ctx, c.conn.Close) + } else { + go func() { + <-ctx.Done() + _ = c.conn.Close() + }() + } + + return c, err } type payloadReader struct { @@ -182,17 +215,17 @@ func (c *client) GetMarks(ctx context.Context) ([]string, error) { return ret, msg.Decode(&ret) } -func (c *client) GetBarIDs(ctx context.Context) ([]BarID, error) { +func (c *client) GetBarIDs(ctx context.Context) ([]string, error) { msg, err := c.roundTrip(ctx, messageTypeGetBarConfig, nil) if err != nil { return nil, err } - var ret []BarID + var ret []string return ret, msg.Decode(&ret) } -func (c *client) GetBarConfig(ctx context.Context, id BarID) (*BarConfig, error) { +func (c *client) GetBarConfig(ctx context.Context, id string) (*BarConfig, error) { msg, err := c.roundTrip(ctx, messageTypeGetBarConfig, []byte(id)) if err != nil { return nil, err diff --git a/cmd/sway-focused/main.go b/cmd/sway-focused/main.go deleted file mode 100644 index 181bb27..0000000 --- a/cmd/sway-focused/main.go +++ /dev/null @@ -1,82 +0,0 @@ -package main - -import ( - "context" - "fmt" - "log" - "os" - "syscall" - - sway "github.com/joshuarubin/go-sway" - "github.com/joshuarubin/lifecycle" -) - -func main() { - if err := run(); err != nil && !isSignal(err) { - fmt.Fprintf(os.Stderr, "%+v\n", err) - os.Exit(1) - } -} - -func isSignal(err error, sigs ...os.Signal) bool { - serr, ok := err.(lifecycle.ErrSignal) - if !ok { - return false - } - switch serr.Signal { - case syscall.SIGINT, syscall.SIGTERM: - return true - } - return false -} - -func run() error { - ctx := lifecycle.New(context.Background()) - - client, err := sway.New(ctx) - if err != nil { - return err - } - defer client.Close() - - n, err := client.GetTree(ctx) - if err != nil { - return err - } - - processFocus(ctx, client, n.FocusedNode()) - - lifecycle.GoErr(ctx, func() error { - return sway.Subscribe(ctx, handler{client: client}, sway.EventTypeWindow) - }) - - return lifecycle.Wait(ctx) -} - -type handler struct { - sway.NoOpEventHandler - client sway.Client -} - -func (h handler) Window(ctx context.Context, e sway.WindowEvent) { - if e.Change != "focus" { - return - } - - processFocus(ctx, h.client, e.Container.FocusedNode()) -} - -func processFocus(ctx context.Context, client sway.Client, 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) - } -} diff --git a/go.mod b/go.mod index de8960f..d53ea75 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/joshuarubin/go-sway go 1.12 require ( - github.com/joshuarubin/lifecycle v0.0.3 + github.com/joshuarubin/lifecycle v1.0.0 github.com/stretchr/testify v1.3.0 // indirect go.uber.org/atomic v1.3.2 // indirect go.uber.org/multierr v1.1.0 diff --git a/go.sum b/go.sum index 1d1d19e..956635b 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,7 @@ 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/joshuarubin/lifecycle v0.0.3 h1:CpBhY0kS+tln6u7/p3I84cu8sDzluVIF/lCO9BpodjQ= -github.com/joshuarubin/lifecycle v0.0.3/go.mod h1:uOdzFKF/2+tKiVSF2lLCuI1dQZo35yXr93rPOd7Hsio= +github.com/joshuarubin/lifecycle v1.0.0 h1:N/lPEC8f+dBZ1Tn99vShqp36LwB+LI7XNAiNadZeLUQ= +github.com/joshuarubin/lifecycle v1.0.0/go.mod h1:sRy++ATvR9Ee21tkRdFkQeywAWvDsue66V70K0Dnl54= 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= @@ -11,5 +11,5 @@ 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= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190412183630-56d357773e84 h1:IqXQ59gzdXv58Jmm2xn0tSOR9i6HqroaOFRQ3wR/dJQ= +golang.org/x/sync v0.0.0-20190412183630-56d357773e84/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= diff --git a/socket_test.go b/socket_test.go index 9d6739c..de90aa6 100644 --- a/socket_test.go +++ b/socket_test.go @@ -24,7 +24,6 @@ func TestSocket(t *testing.T) { if err != nil { t.Fatal(err) } - defer client.Close() workspaces, err := client.GetWorkspaces(ctx) if err != nil { @@ -111,13 +110,18 @@ func TestSocket(t *testing.T) { processFocus(ctx, client, n.FocusedNode()) - if err = sway.Subscribe(ctx, testHandler{client: client}, sway.EventTypeWindow); err != context.DeadlineExceeded && err != nil { + th := testHandler{ + EventHandler: sway.NoOpEventHandler(), + client: client, + } + + if err = sway.Subscribe(ctx, th, sway.EventTypeWindow); err != context.DeadlineExceeded && err != nil { t.Fatal(err) } } type testHandler struct { - sway.NoOpEventHandler + sway.EventHandler client sway.Client } diff --git a/subscribe.go b/subscribe.go index cd7a96b..dfe0cdd 100644 --- a/subscribe.go +++ b/subscribe.go @@ -4,19 +4,41 @@ import ( "context" ) +// EventType is used to choose which events to Subscribe to type EventType string const ( - EventTypeWorkspace EventType = "workspace" - EventTypeMode EventType = "mode" - EventTypeWindow EventType = "window" + // EventTypeWorkspace is sent whenever an event involving a workspace occurs + // such as initialization of a new workspace or a different workspace gains + // focus + EventTypeWorkspace EventType = "workspace" + + // EventTypeMode is sent whenever the binding mode changes + EventTypeMode EventType = "mode" + + // EventTypeWindow is sent whenever an event involving a view occurs such as + // being reparented, focused, or closed + EventTypeWindow EventType = "window" + + // EventTypeBarConfigUpdate is sent whenever a bar config changes EventTypeBarConfigUpdate EventType = "barconfig_update" - EventTypeBinding EventType = "binding" - EventTypeShutdown EventType = "shutdown" - EventTypeTick EventType = "tick" + + // EventTypeBinding is sent when a configured binding is executed + EventTypeBinding EventType = "binding" + + // EventTypeShutdown is sent when the ipc shuts down because sway is exiting + EventTypeShutdown EventType = "shutdown" + + // EventTypeTick is sent when an ipc client sends a SEND_TICK message + EventTypeTick EventType = "tick" + + //EventTypeBarStatusUpdate send when the visibility of a bar should change + //due to a modifier EventTypeBarStatusUpdate EventType = "bar_status_update" ) +// An EventHandler is passed to Subscribe and its methods are called in response +// to sway events type EventHandler interface { Workspace(context.Context, WorkspaceEvent) Mode(context.Context, ModeEvent) @@ -28,23 +50,47 @@ type EventHandler interface { BarStatusUpdate(context.Context, BarStatusUpdateEvent) } -type NoOpEventHandler struct{} +// NoOpEventHandler is used to help provide empty methods that aren't intended +// to be handled by Subscribe +// +// type handler struct { +// sway.EventHandler +// } +// +// func (h handler) Window(ctx context.Context, e sway.WindowEvent) { +// ... +// } +// +// func main() { +// h := handler{ +// EventHandler: sway.NoOpEventHandler(), +// } +// +// ctx := context.Background() +// +// sway.Subscribe(ctx, h, sway.EventTypeWindow) +// } +func NoOpEventHandler() EventHandler { + return noOpEventHandler{} +} -func (h NoOpEventHandler) Workspace(context.Context, WorkspaceEvent) {} -func (h NoOpEventHandler) Mode(context.Context, ModeEvent) {} -func (h NoOpEventHandler) Window(context.Context, WindowEvent) {} -func (h NoOpEventHandler) BarConfigUpdate(context.Context, BarConfigUpdateEvent) {} -func (h NoOpEventHandler) Binding(context.Context, BindingEvent) {} -func (h NoOpEventHandler) Shutdown(context.Context, ShutdownEvent) {} -func (h NoOpEventHandler) Tick(context.Context, TickEvent) {} -func (h NoOpEventHandler) BarStatusUpdate(context.Context, BarStatusUpdateEvent) {} +type noOpEventHandler struct{} +func (h noOpEventHandler) Workspace(context.Context, WorkspaceEvent) {} +func (h noOpEventHandler) Mode(context.Context, ModeEvent) {} +func (h noOpEventHandler) Window(context.Context, WindowEvent) {} +func (h noOpEventHandler) BarConfigUpdate(context.Context, BarConfigUpdateEvent) {} +func (h noOpEventHandler) Binding(context.Context, BindingEvent) {} +func (h noOpEventHandler) Shutdown(context.Context, ShutdownEvent) {} +func (h noOpEventHandler) Tick(context.Context, TickEvent) {} +func (h noOpEventHandler) BarStatusUpdate(context.Context, BarStatusUpdateEvent) {} + +// Subscribe the IPC connection to the events listed in the payload 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) diff --git a/types.go b/types.go index b39c3e9..73e73bf 100644 --- a/types.go +++ b/types.go @@ -41,6 +41,7 @@ type Node struct { WindowProperties *WindowProperties `json:"window_properties,omitempty"` } +// FocusedNode traverses the node tree and returns the focused node func (n *Node) FocusedNode() *Node { queue := []*Node{n} for len(queue) > 0 { @@ -61,20 +62,54 @@ func (n *Node) FocusedNode() *Node { return nil } -type Event interface{} - +// WorkspaceEvent is sent whenever a change involving a workspace occurs type WorkspaceEvent struct { - Change string `json:"change,omitempty"` - Current *Node `json:"current,omitempty"` - Old *Node `json:"old,omitempty"` + // The type of change that occurred + // The following change types are currently available: + // init: the workspace was created + // empty: the workspace is empty and is being destroyed since it is not + // visible + // focus: the workspace was focused. See the old property for the previous + // focus + // move: the workspace was moved to a different output + // rename: the workspace was renamed + // urgent: a view on the workspace has had their urgency hint set or all + // urgency hints for views on the workspace have been cleared + // reload: The configuration file has been reloaded + Change string `json:"change,omitempty"` + + // An object representing the workspace effected or null for reload changes + Current *Node `json:"current,omitempty"` + + // For a focus change, this is will be an object representing the workspace + // being switched from. Otherwise, it is null + Old *Node `json:"old,omitempty"` } +// WindowEvent is sent whenever a change involving a view occurs type WindowEvent struct { - Change string `json:"change,omitempty"` - Container Node `json:"container,omitempty"` + // The type of change that occurred + // + // The following change types are currently available: + // new: The view was created + // close: The view was closed + // focus: The view was focused + // title: The view's title has changed + // fullscreen_mode: The view's fullscreen mode has changed + // move: The view has been reparented in the tree + // floating: The view has become floating or is no longer floating + // urgent: The view's urgency hint has changed status + // mark: A mark has been added or removed from the view + Change string `json:"change,omitempty"` + + // An object representing the view effected + Container Node `json:"container,omitempty"` } +// ShutdownEvent is sent whenever the IPC is shutting down type ShutdownEvent struct { + // A string containing the reason for the shutdown. Currently, the only + // value for change is exit, which is issued when sway is exiting. Change string `json:"change,omitempty"` } @@ -156,12 +191,12 @@ type BarConfigColors struct { BindingModeBorder string `json:"binding_mode_border,omitempty"` } -type BarID string - +// BarConfigUpdateEvent is sent whenever a config for a bar changes. The event +// is identical to that of GET_BAR_CONFIG when a bar ID is given as a payload. type BarConfigUpdateEvent = BarConfig type BarConfig struct { - ID BarID `json:"id,omitempty"` + ID string `json:"id,omitempty"` Mode string `json:"mode,omitempty"` Position string `json:"position,omitempty"` StatusCommand string `json:"status_command,omitempty"` @@ -226,31 +261,60 @@ type Seat struct { Devices []Input `json:"devices,omitempty"` } +// ModeEvent is sent whenever the binding mode changes type ModeEvent struct { - Change string `json:"change,omitempty"` - PangoMarkup bool `json:"pango_markup,omitempty"` + // The binding mode that became active + Change string `json:"change,omitempty"` + + // Whether the mode should be parsed as pango markup + PangoMarkup bool `json:"pango_markup,omitempty"` } type Binding struct { - Change string `json:"change,omitempty"` - Command string `json:"command,omitempty"` + // The command associated with the binding + Command string `json:"command,omitempty"` + + // An array of strings that correspond to each modifier key for the binding EventStateMask []string `json:"event_state_mask,omitempty"` - InputCode int64 `json:"input_code,omitempty"` - Symbol *string `json:"symbol,omitempty"` - InputType string `json:"input_type,omitempty"` + + // For keyboard bindcodes, this is the key code for the binding. For mouse + // bindings, this is the X11 button number, if there is an equivalent. In + // all other cases, this will be 0. + InputCode int64 `json:"input_code,omitempty"` + + // For keyboard bindsyms, this is the bindsym for the binding. Otherwise, + // this will be null + Symbol *string `json:"symbol,omitempty"` + + // The input type that triggered the binding. This is either keyboard or + // mouse + InputType string `json:"input_type,omitempty"` } +// BindingEvent is sent whenever a binding is executed type BindingEvent struct { + // Currently this will only be run Change string `json:"change,omitempty"` Binding Binding `json:"binding,omitempty"` } +// TickEvent is sent when first subscribing to tick events or by a SEND_TICK +// message type TickEvent struct { - First bool `json:"first,omitempty"` + // Whether this event was triggered by subscribing to the tick events + First bool `json:"first,omitempty"` + + // The payload given with a SEND_TICK message, if any. Otherwise, an empty + // string Payload string `json:"payload,omitempty"` } +// BarStatusUpdateEvent is sent when the visibility of a bar changes due to a +// modifier being pressed type BarStatusUpdateEvent struct { - ID string `json:"id,omitempty"` - VisibleByModifier bool `json:"visible_by_modifier,omitempty"` + // The bar ID effected + ID string `json:"id,omitempty"` + + // Whether the bar should be made visible due to a modifier being pressed + VisibleByModifier bool `json:"visible_by_modifier,omitempty"` }