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 don’t 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 }