go-i3/subscribe.go

387 lines
10 KiB
Go
Raw Normal View History

2023-10-22 16:19:10 +02:00
package i3
import (
"encoding/json"
"fmt"
"log"
"math/rand"
"net"
"time"
)
// Event is an event received from i3.
//
// Type-assert or type-switch on Event to obtain a more specific type.
type Event interface{}
// WorkspaceEvent contains details about various workspace-related changes.
//
// See https://i3wm.org/docs/ipc.html#_workspace_event for more details.
type WorkspaceEvent struct {
Change string `json:"change"`
Current Node `json:"current"`
Old Node `json:"old"`
}
// OutputEvent contains details about various output-related changes.
//
// See https://i3wm.org/docs/ipc.html#_output_event for more details.
type OutputEvent struct {
Change string `json:"change"`
}
// ModeEvent contains details about various mode-related changes.
//
// See https://i3wm.org/docs/ipc.html#_mode_event for more details.
type ModeEvent struct {
Change string `json:"change"`
PangoMarkup bool `json:"pango_markup"`
}
// WindowEvent contains details about various window-related changes.
//
// See https://i3wm.org/docs/ipc.html#_window_event for more details.
type WindowEvent struct {
Change string `json:"change"`
Container Node `json:"container"`
}
// BarconfigUpdateEvent contains details about various bar config-related changes.
//
// See https://i3wm.org/docs/ipc.html#_barconfig_update_event for more details.
type BarconfigUpdateEvent BarConfig
// BindingEvent contains details about various binding-related changes.
//
// See https://i3wm.org/docs/ipc.html#_binding_event for more details.
type BindingEvent struct {
Change string `json:"change"`
Binding struct {
Command string `json:"command"`
EventStateMask []string `json:"event_state_mask"`
InputCode int64 `json:"input_code"`
Symbol string `json:"symbol"`
InputType string `json:"input_type"`
} `json:"binding"`
}
// ShutdownEvent contains the reason for which the IPC connection is about to be
// shut down.
//
// See https://i3wm.org/docs/ipc.html#_shutdown_event for more details.
type ShutdownEvent struct {
Change string `json:"change"`
}
// TickEvent contains the payload of the last tick command.
//
// See https://i3wm.org/docs/ipc.html#_tick_event for more details.
type TickEvent struct {
First bool `json:"first"`
Payload string `json:"payload"`
}
type eventReplyType int
const (
eventReplyTypeWorkspace eventReplyType = iota
eventReplyTypeOutput
eventReplyTypeMode
eventReplyTypeWindow
eventReplyTypeBarconfigUpdate
eventReplyTypeBinding
eventReplyTypeShutdown
eventReplyTypeTick
)
const (
eventFlagMask = uint32(0x80000000)
eventTypeMask = ^eventFlagMask
)
// EventReceiver is not safe for concurrent use.
type EventReceiver struct {
types []EventType // for re-subscribing on io.EOF
sock *socket
conn net.Conn
ev Event
err error
reconnect bool
closed bool
}
// Event returns the most recent event received from i3 by a call to Next.
func (r *EventReceiver) Event() Event {
return r.ev
}
func (r *EventReceiver) subscribe() error {
var err error
if r.conn != nil {
r.conn.Close()
}
if wasRestart {
r.reconnect = false
}
r.sock, r.conn, err = getIPCSocket(r.reconnect)
r.reconnect = true
if err != nil {
return err
}
payload, err := json.Marshal(r.types)
if err != nil {
return err
}
b, err := r.sock.roundTrip(messageTypeSubscribe, payload)
if err != nil {
return err
}
var reply struct {
Success bool `json:"success"`
}
if err := json.Unmarshal(b.Payload, &reply); err != nil {
return err
}
if !reply.Success {
return fmt.Errorf("could not subscribe, check the i3 log")
}
r.err = nil
return nil
}
func (r *EventReceiver) next() (Event, error) {
reply, err := r.sock.recvMsg()
if err != nil {
return nil, err
}
if (uint32(reply.Type) & eventFlagMask) == 0 {
return nil, fmt.Errorf("unexpectedly did not receive an event")
}
t := uint32(reply.Type) & eventTypeMask
switch eventReplyType(t) {
case eventReplyTypeWorkspace:
var e WorkspaceEvent
return &e, json.Unmarshal(reply.Payload, &e)
case eventReplyTypeOutput:
var e OutputEvent
return &e, json.Unmarshal(reply.Payload, &e)
case eventReplyTypeMode:
var e ModeEvent
return &e, json.Unmarshal(reply.Payload, &e)
case eventReplyTypeWindow:
var e WindowEvent
return &e, json.Unmarshal(reply.Payload, &e)
case eventReplyTypeBarconfigUpdate:
var e BarconfigUpdateEvent
return &e, json.Unmarshal(reply.Payload, &e)
case eventReplyTypeBinding:
var e BindingEvent
return &e, json.Unmarshal(reply.Payload, &e)
case eventReplyTypeShutdown:
var e ShutdownEvent
return &e, json.Unmarshal(reply.Payload, &e)
case eventReplyTypeTick:
var e TickEvent
return &e, json.Unmarshal(reply.Payload, &e)
}
return nil, fmt.Errorf("BUG: event reply type %d not implemented yet", t)
}
// Next advances the EventReceiver to the next event, which will then be
// available through the Event method. It returns false when reaching an
// error. After Next returns false, the Close method will return the first
// error.
//
// Until you call Close, you must call Next in a loop for every EventReceiver
// (usually in a separate goroutine), otherwise i3 will deadlock as soon as the
// UNIX socket buffer is full of unprocessed events.
func (r *EventReceiver) Next() bool {
Outer:
for r.err == nil {
r.ev, r.err = r.next()
if r.err == nil {
return true // happy path
}
if r.closed {
return false
}
// reconnect
start := time.Now()
for time.Since(start) < reconnectTimeout && (r.sock == nil || i3Running()) {
if err := r.subscribe(); err == nil {
continue Outer
} else {
r.err = err
}
// Reconnect within [10, 20) ms to prevent CPU-starving i3.
time.Sleep(time.Duration(10+rand.Int63n(10)) * time.Millisecond)
}
}
return r.err == nil
}
// Close closes the connection to i3. If you dont ever call Close, you must
// consume events via Next to prevent i3 from deadlocking.
func (r *EventReceiver) Close() error {
r.closed = true
if r.conn != nil {
if r.err == nil {
r.err = r.conn.Close()
} else {
// Retain the original error.
r.conn.Close()
}
r.conn = nil
r.sock = nil
}
return r.err
}
// EventType indicates the specific kind of event to subscribe to.
type EventType string
// i3 currently implements the following event types:
const (
WorkspaceEventType EventType = "workspace" // since 4.0
OutputEventType EventType = "output" // since 4.0
ModeEventType EventType = "mode" // since 4.4
WindowEventType EventType = "window" // since 4.5
BarconfigUpdateEventType EventType = "barconfig_update" // since 4.6
BindingEventType EventType = "binding" // since 4.9
ShutdownEventType EventType = "shutdown" // since 4.14
TickEventType EventType = "tick" // since 4.15
)
type majorMinor struct {
major int64
minor int64
}
var eventAtLeast = map[EventType]majorMinor{
WorkspaceEventType: {4, 0},
OutputEventType: {4, 0},
ModeEventType: {4, 4},
WindowEventType: {4, 5},
BarconfigUpdateEventType: {4, 6},
BindingEventType: {4, 9},
ShutdownEventType: {4, 14},
TickEventType: {4, 15},
}
// Subscribe returns an EventReceiver for receiving events of the specified
// types from i3.
//
// Unless the ordering of events matters to your use-case, you are encouraged to
// call Subscribe once per event type, so that you can use type assertions
// instead of type switches.
//
// Subscribe is supported in i3 ≥ v4.0 (2011-07-31).
func Subscribe(eventTypes ...EventType) *EventReceiver {
// Error out early in case any requested event type is not yet supported by
// the running i3 version.
for _, t := range eventTypes {
if err := AtLeast(eventAtLeast[t].major, eventAtLeast[t].minor); err != nil {
return &EventReceiver{err: err}
}
}
return &EventReceiver{types: eventTypes}
}
// restart runs the restart i3 command without entering an infinite loop: as
// RUN_COMMAND with payload "restart" does not result in a reply, we subscribe
// to the shutdown event beforehand (on a dedicated connection), which we can
// receive instead of a reply.
func restart(firstAttempt bool) error {
sock, conn, err := getIPCSocket(!firstAttempt)
if err != nil {
return err
}
defer conn.Close()
payload, err := json.Marshal([]EventType{ShutdownEventType})
if err != nil {
return err
}
b, err := sock.roundTrip(messageTypeSubscribe, payload)
if err != nil {
return err
}
var sreply struct {
Success bool `json:"success"`
}
if err := json.Unmarshal(b.Payload, &sreply); err != nil {
return err
}
if !sreply.Success {
return fmt.Errorf("could not subscribe, check the i3 log")
}
rreply, err := sock.roundTrip(messageTypeRunCommand, []byte("restart"))
if err != nil {
return err
}
if (uint32(rreply.Type) & eventFlagMask) == 0 {
var crs []CommandResult
err = json.Unmarshal(rreply.Payload, &crs)
if err == nil {
for _, cr := range crs {
if !cr.Success {
return &CommandUnsuccessfulError{
command: "restart",
cr: cr,
}
}
}
}
return nil // restart command successful
}
t := uint32(rreply.Type) & eventTypeMask
if got, want := eventReplyType(t), eventReplyTypeShutdown; got != want {
return fmt.Errorf("unexpected reply type: got %d, want %d", got, want)
}
return nil // shutdown event received
}
var wasRestart = false
// Restart sends the restart command to i3. Sending restart via RunCommand will
// result in a deadlock: since i3 restarts before it sends the reply to the
// restart command, RunCommand will retry the command indefinitely.
//
// Restart is supported in i3 ≥ v4.14 (2017-09-04).
func Restart() error {
if err := AtLeast(eventAtLeast[ShutdownEventType].major, eventAtLeast[ShutdownEventType].minor); err != nil {
return err
}
if AtLeast(4, 17) == nil {
_, err := roundTrip(messageTypeRunCommand, []byte("restart"))
return err
}
log.Println("preventing any further X11 connections to work around issue #3")
wasRestart = true
var (
firstAttempt = true
start = time.Now()
lastErr error
)
for time.Since(start) < reconnectTimeout && (firstAttempt || i3Running()) {
lastErr = restart(firstAttempt)
if lastErr == nil {
return nil // success
}
firstAttempt = false
}
return lastErr
}