Compositor bridge examples
Use gnoblinctl for a one-off action. Use the bridge socket when a shell needs live snapshots, previews or a shortcut session. Its transport is one UTF-8 JSON object per line on a persistent Unix socket. The first line from the server is a hello event. The bridge reference describes the other operations and limits.
The examples below run inside a Gnoblin session. They use GNOBLIN_COMPOSITOR_SOCKET when set, or $XDG_RUNTIME_DIR/gnoblin/compositor-v1.sock otherwise. They send a request ID and wait for the matching reply; unrelated events may arrive first.
Shell script: focus a window
For occasional commands, let gnoblinctl handle the socket protocol. This example requires jq and acts only when exactly one matching window exists:
ids=$(gnoblinctl window list --app-id org.example.Editor.desktop --json |
jq -r '.windows[].id')
count=$(printf '%s\n' "$ids" | sed '/^$/d' | wc -l)
if [ "$count" -eq 1 ]; then
gnoblinctl window focus "$ids"
fiReplace the app ID with the exact value from gnoblinctl window list --json. IDs expire when their windows close. If you need repeated updates, subscribe to snapshots instead of polling the command.
Python: watch windows
The Python watcher subscribes with {"op":"windows"} and prints each full snapshot. Keep its connection open for as long as your dock or switcher needs updates. An empty windows array means no eligible windows, not a request failure.
Node.js: request a snapshot
Save this as windows.mjs and run node windows.mjs:
import net from "node:net";
import path from "node:path";
import readline from "node:readline";
const runtime = process.env.XDG_RUNTIME_DIR;
const socketPath = process.env.GNOBLIN_COMPOSITOR_SOCKET ||
(runtime && path.join(runtime, "gnoblin/compositor-v1.sock"));
if (!socketPath) throw new Error("XDG_RUNTIME_DIR is unset");
const connection = net.createConnection(socketPath);
const lines = readline.createInterface({ input: connection });
connection.setTimeout(5000);
connection.on("connect", () => {
connection.write(JSON.stringify({ op: "command", id: "list-1", command: "windows" }) + "\n");
});
connection.on("timeout", () => connection.destroy(new Error("Gnoblin did not reply")));
connection.on("error", (error) => {
console.error(error.message);
process.exitCode = 1;
});
lines.on("line", (line) => {
const message = JSON.parse(line);
if (message.id !== "list-1") return; // includes the initial hello event
if (message.event === "error") {
console.error(message.message);
process.exitCode = 1;
} else if (message.event === "reply") {
console.log(JSON.stringify(message.result.windows, null, 2));
} else {
return;
}
connection.end();
});command requests return one reply envelope. For live changes, send {"op":"windows"} on the same connection and handle every windows event. Each event replaces the prior array; it is not a delta.
Go: request a snapshot
This version uses only Go's standard library. Save it as windows.go and run go run windows.go:
package main
import (
"bufio"
"encoding/json"
"fmt"
"log"
"net"
"os"
"path/filepath"
"time"
)
func main() {
socket := os.Getenv("GNOBLIN_COMPOSITOR_SOCKET")
if socket == "" {
runtime := os.Getenv("XDG_RUNTIME_DIR")
if runtime == "" { log.Fatal("XDG_RUNTIME_DIR is unset") }
socket = filepath.Join(runtime, "gnoblin", "compositor-v1.sock")
}
conn, err := net.Dial("unix", socket)
if err != nil { log.Fatal(err) }
defer conn.Close()
if err := conn.SetDeadline(time.Now().Add(5 * time.Second)); err != nil { log.Fatal(err) }
if _, err := fmt.Fprintln(conn, `{"op":"command","id":"list-1","command":"windows"}`); err != nil {
log.Fatal(err)
}
scanner := bufio.NewScanner(conn)
scanner.Buffer(make([]byte, 4096), 4*1024*1024)
for scanner.Scan() {
var message struct {
Event string `json:"event"`
ID string `json:"id"`
Result json.RawMessage `json:"result"`
Message string `json:"message"`
}
if err := json.Unmarshal(scanner.Bytes(), &message); err != nil { log.Fatal(err) }
if message.ID != "list-1" { continue }
if message.Event == "error" { log.Fatal(message.Message) }
if message.Event == "reply" {
var pretty []byte
pretty, err = json.MarshalIndent(message.Result, "", " ")
if err != nil { log.Fatal(err) }
fmt.Println(string(pretty))
return
}
}
if err := scanner.Err(); err != nil { log.Fatal(err) }
log.Fatal("connection closed before reply")
}The reply contains a windows object, so Go prints the full result object. The ID lets a client match replies when it has several requests in flight.
Quickshell and other UI processes
A Qt or Quickshell shell can keep a Unix socket open and parse newline-delimited JSON using its normal socket and JSON APIs. Register shortcuts on that same connection, render a popup on activated, and send end when it closes. If the connection drops, reconnect, request a fresh windows snapshot and register bindings again: bindings belong to the connection that created them.
For an example of a Quickshell integration, Bingux is a separate project that uses Gnoblin's bridge. The shell integration guide describes the contract without depending on Bingux's implementation.