387 lines
10 KiB
Go
387 lines
10 KiB
Go
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
|
||
}
|