It requires x11-display and contributes exec i3 to xinitrc's last slot, and XDG_CURRENT_DESKTOP/XDG_SESSION_DESKTOP to the environment. It owns ~/.config/i3/config, ending with the config.d include where other modules drop their files, and /etc/lemurs/wms/i3, which runs the session's start. Over today's identical config it drops the dead lxpolkit, the D-Bus-activated portal, the xrdb merge xorg now does, and the execs that XDG autostart already started. It sets JetBrainsMono Nerd Font, runs i3-sensible-terminal, and declares dex, which the desktop lacked. The tools speak i3's IPC: the seat's reload, workspaces and windows, and focus, move, layout save/restore, exec, kill, bindings, config check, marks and the scratchpad. A watcher in the bundle (ADR 0198) replaces the predecessor's inotify script and user unit. It reloads only a configuration i3 -C accepts, and at its start whatever i3 has not loaded.
118 lines
3.2 KiB
Go
118 lines
3.2 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"i3/internal/desktop"
|
|
)
|
|
|
|
// i3's IPC (https://i3wm.org/docs/ipc.html): "i3-ipc", a little-endian length and type, a JSON
|
|
// payload; the reply carries the same header. Spoken directly, so the tools need no i3-msg and no
|
|
// display — only the socket, which lives under the account's runtime directory.
|
|
const (
|
|
RunCommand = 0
|
|
GetWorkspaces = 1
|
|
GetTree = 4
|
|
GetMarks = 5
|
|
GetVersion = 7
|
|
GetConfig = 9
|
|
)
|
|
|
|
const magic = "i3-ipc"
|
|
|
|
// Ask sends one message to i3 at socket and decodes its reply into out.
|
|
func Ask(socket string, kind uint32, payload string, out any) error {
|
|
conn, err := net.DialTimeout("unix", socket, 2*time.Second)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer conn.Close()
|
|
_ = conn.SetDeadline(time.Now().Add(10 * time.Second))
|
|
msg := make([]byte, 14+len(payload))
|
|
copy(msg, magic)
|
|
binary.LittleEndian.PutUint32(msg[6:], uint32(len(payload)))
|
|
binary.LittleEndian.PutUint32(msg[10:], kind)
|
|
copy(msg[14:], payload)
|
|
if _, err := conn.Write(msg); err != nil {
|
|
return err
|
|
}
|
|
head := make([]byte, 14)
|
|
if _, err := io.ReadFull(conn, head); err != nil {
|
|
return fmt.Errorf("i3 did not answer: %w", err)
|
|
}
|
|
if string(head[:6]) != magic {
|
|
return errors.New("the socket's answer is not i3's")
|
|
}
|
|
n := binary.LittleEndian.Uint32(head[6:])
|
|
if n > 64<<20 {
|
|
return fmt.Errorf("an answer of %d bytes", n)
|
|
}
|
|
body := make([]byte, n)
|
|
if _, err := io.ReadFull(conn, body); err != nil {
|
|
return err
|
|
}
|
|
if got := binary.LittleEndian.Uint32(head[10:]); got != kind {
|
|
return fmt.Errorf("asked %d, answered %d", kind, got)
|
|
}
|
|
return json.Unmarshal(body, out)
|
|
}
|
|
|
|
// FindSocket is the running i3's IPC socket. The session's I3SOCK when its process carries one;
|
|
// else the newest `ipc-socket.<pid>` under the runtime directory whose i3 is alive and answers.
|
|
func FindSocket(runtimeDir, i3sock string) (string, error) {
|
|
if i3sock != "" {
|
|
if _, err := os.Stat(i3sock); err == nil {
|
|
return i3sock, nil
|
|
}
|
|
}
|
|
matches, _ := filepath.Glob(filepath.Join(runtimeDir, "i3", "ipc-socket.*"))
|
|
type cand struct {
|
|
path string
|
|
pid int
|
|
}
|
|
var alive []cand
|
|
for _, m := range matches {
|
|
pid, err := strconv.Atoi(strings.TrimPrefix(filepath.Ext(m), "."))
|
|
if err != nil {
|
|
continue
|
|
}
|
|
if _, err := os.Stat(filepath.Join("/proc", strconv.Itoa(pid))); err != nil {
|
|
continue
|
|
}
|
|
alive = append(alive, cand{m, pid})
|
|
}
|
|
sort.Slice(alive, func(i, j int) bool { return alive[i].pid > alive[j].pid })
|
|
for _, c := range alive {
|
|
var v map[string]any
|
|
if Ask(c.path, GetVersion, "", &v) == nil {
|
|
return c.path, nil
|
|
}
|
|
}
|
|
return "", &desktop.NoSession{
|
|
Reason: "i3 is not running: no live IPC socket",
|
|
Looked: []string{filepath.Join(runtimeDir, "i3", "ipc-socket.*")},
|
|
}
|
|
}
|
|
|
|
// Outcome is one command's result as i3 answers RUN_COMMAND.
|
|
type Outcome struct {
|
|
Success bool `json:"success"`
|
|
Error string `json:"error,omitempty"`
|
|
}
|
|
|
|
// Quote makes a value safe inside an i3 command: double quotes, with backslashes and quotes escaped.
|
|
func Quote(s string) string {
|
|
return `"` + strings.NewReplacer(`\`, `\\`, `"`, `\"`).Replace(s) + `"`
|
|
}
|