detached 32dc121

docs(readme): add README with a compiling example and benchmark table

commit
32dc1215d3be4bd534ea73f4b5827884a3f3b1ad
author
brice · · 2026-10-22 10:36 UTC
parent
bb30cc0
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +261 -0
A README.md +175 -0
@@ -0,0 +1,175 @@
1+# xturbo
2+
3+A pure-Go X11 client runtime with generated bindings. The runtime speaks the
4+X11 wire protocol over a non-blocking Unix socket; the bindings come from
5+xcb-proto 1.17.0. It depends only on the standard library and
6+`golang.org/x/sys`, builds with `CGO_ENABLED=0`, and keeps the steady-state
7+request, reply, event and error paths free of allocations.
8+
9+Module: `bnema.dev/xturbo`. Source: <https://git.bnema.dev/xturbo>.
10+
11+## Packages
12+
13+| Package | Contents |
14+| --- | --- |
15+| `bnema.dev/xturbo` | `Conn`, `Dial`, `Buf`, `Dispatch`, `Expect`, `Sync`, `InternAtoms`, XID allocation |
16+| `xproto` | core protocol |
17+| `bigreq`, `ge`, `xcmisc` | BIG-REQUESTS, Generic Event, XC-MISC |
18+| `composite`, `xfixes`, `render`, `shape`, `res` | Composite, XFIXES, RENDER, SHAPE, X-Resource |
19+
20+The root package never imports the generated packages.
21+
22+## Install
23+
24+```sh
25+go get bnema.dev/xturbo
26+```
27+
28+## Example
29+
30+`Dial` (or `NewConn(fd)` for an already-connected socket) performs the
31+handshake and negotiates the supported extensions. Requests are encoded into
32+the connection's `Buf` and return a `Seq` (0 on failure). `Flush` never
33+blocks, and `Dispatch` reads whatever the server sent and calls the `Handler`.
34+
35+```go
36+package xturbo_test
37+
38+import (
39+ "errors"
40+ "log"
41+
42+ "golang.org/x/sys/unix"
43+
44+ "bnema.dev/xturbo"
45+ "bnema.dev/xturbo/xproto"
46+)
47+
48+// Replies registered with Expect carry this kind (16 and above are free).
49+const kindGeometry uint16 = 16
50+
51+func Example() {
52+ c, err := xturbo.Dial("") // "" means $DISPLAY
53+ if err != nil {
54+ log.Fatal(err)
55+ }
56+ defer c.Close()
57+
58+ // Startup: blocking calls, bounded by a 5 s deadline.
59+ var atoms [1]uint32
60+ if err := c.InternAtoms([]string{"WM_DELETE_WINDOW"}, false, atoms[:]); err != nil {
61+ log.Fatal(err)
62+ }
63+
64+ // Queue requests; they return a Seq (0 on failure, see c.Buf().Err()).
65+ b := c.Buf()
66+ win, err := c.NewXID()
67+ if err != nil {
68+ log.Fatal(err)
69+ }
70+ var v xproto.CreateWindowValues
71+ v.SetEventMask(xproto.EventMaskExposure | xproto.EventMaskStructureNotify)
72+ xproto.CreateWindow(b, 0, xproto.Window(win), xproto.Window(c.Root()),
73+ 0, 0, 640, 480, 0, xproto.WindowClassInputOutput, 0, &v)
74+ xproto.MapWindow(b, xproto.Window(win))
75+ if seq := xproto.GetGeometry(b, xproto.Drawable(win)); seq != 0 {
76+ if err := c.Expect(seq, kindGeometry, win, 0, 0, 0); err != nil {
77+ log.Fatal(err)
78+ }
79+ }
80+
81+ // Callbacks run inside Dispatch. Events, replies and errors are borrowed:
82+ // copy what you keep.
83+ h := &xturbo.Handler{
84+ OnEvent: func(ev *xturbo.Event) {
85+ switch ev.Kind {
86+ case xproto.KindExpose:
87+ e := xproto.ExposeOf(ev)
88+ log.Printf("expose %dx%d", e.Width(), e.Height())
89+ }
90+ },
91+ OnReply: func(r *xturbo.Reply) {
92+ if r.Kind == kindGeometry {
93+ if g, ok := xproto.ParseGetGeometryReply(r.Raw); ok {
94+ log.Printf("window %#x is %dx%d", r.Args[0], g.Width(), g.Height())
95+ }
96+ }
97+ },
98+ OnError: func(e *xturbo.XError) { log.Print(e) },
99+ }
100+
101+ for {
102+ // Flush never blocks. On ErrWouldBlock, also wait for POLLOUT.
103+ events := int16(unix.POLLIN)
104+ if err := c.Flush(); errors.Is(err, xturbo.ErrWouldBlock) {
105+ events |= unix.POLLOUT
106+ } else if err != nil {
107+ log.Fatal(err)
108+ }
109+ fds := []unix.PollFd{{Fd: int32(c.Fd()), Events: events}}
110+ if _, err := unix.Poll(fds, -1); err != nil && err != unix.EINTR {
111+ log.Fatal(err)
112+ }
113+ if err := c.Dispatch(h); err != nil { // io.EOF: server closed
114+ log.Fatal(err)
115+ }
116+ // Keep the XID pool topped up without blocking later.
117+ if c.XIDsLow() {
118+ _ = c.PrefetchXIDs()
119+ }
120+ }
121+}
122+```
123+
124+## Rules
125+
126+- **Lifetimes.** A request's body slice is valid only until the next request is
127+ encoded. Reply views (`Reply.Raw` and the parsed views) and event `Tail` are
128+ valid only during the callback; `Event.Raw` is a copy. Copy what you keep.
129+- **Callbacks.** They may queue requests, call `Expect` and `PrefetchXIDs`.
130+ They must not call `Dispatch`, `Sync`, `InternAtoms`, or `NewXID` when it
131+ needs a refill.
132+- **Blocking.** Only `Sync`, `InternAtoms` and a `NewXID` refill block, for at
133+ most 5 s, and are meant for startup. Use `XIDsLow` and `PrefetchXIDs` to
134+ keep the event loop non-blocking.
135+- **Errors.** A failed `Buf` is sticky: later requests return 0 and `Flush`
136+ returns the error.
137+
138+## Performance
139+
140+Measured with `make bench` on an AMD Ryzen 9 7900X3D (Go 1.27, `CGO_ENABLED=0`).
141+Every benchmark reports 0 allocations per operation.
142+
143+| Benchmark | Result |
144+| --- | --- |
145+| Encode a request (`MapWindow`) | 6.2 ns |
146+| Encode and flush 64 requests over a socketpair | 1.55 µs (about 24 ns per request) |
147+| Decode 10k events | 124 µs (about 12 ns per event) |
148+| Dispatch 10k mixed packets | 251 µs (about 25 ns per packet) |
149+| Parse a `GetProperty` reply view | 1.9 ns |
150+| Parse a `QueryTree` reply view | 3.2 ns |
151+
152+## Regenerating bindings
153+
154+Generated code lives in the extension packages and `xproto`; do not edit it.
155+The inputs are the pinned xcb-proto 1.17.0 files in `spec/`, listed with
156+checksums in `spec/manifest.json`.
157+
158+```sh
159+make gen # go generate ./... (cmd/xtgen)
160+make gen-check # regenerate and fail on any difference
161+go run ./cmd/xtsync # refresh spec/ from upstream and update the manifest
162+```
163+
164+## Tests
165+
166+```sh
167+make check # vet, staticcheck, tests, race, allocation, poison, gen-check
168+make xserver # tests against a real Xvfb (needs xvfb and xtrace)
169+make fuzz # every Fuzz target for FUZZTIME each (default 60s)
170+make bench # benchmarks
171+```
172+
173+## License
174+
175+MIT, see `LICENSE`. The xcb-proto XML and its license are under `LICENSES/`.
@@ -0,0 +1,175 @@
1+# xturbo
2+
3+A pure-Go X11 client runtime with generated bindings. The runtime speaks the
4+X11 wire protocol over a non-blocking Unix socket; the bindings come from
5+xcb-proto 1.17.0. It depends only on the standard library and
6+`golang.org/x/sys`, builds with `CGO_ENABLED=0`, and keeps the steady-state
7+request, reply, event and error paths free of allocations.
8+
9+Module: `bnema.dev/xturbo`. Source: <https://git.bnema.dev/xturbo>.
10+
11+## Packages
12+
13+| Package | Contents |
14+| --- | --- |
15+| `bnema.dev/xturbo` | `Conn`, `Dial`, `Buf`, `Dispatch`, `Expect`, `Sync`, `InternAtoms`, XID allocation |
16+| `xproto` | core protocol |
17+| `bigreq`, `ge`, `xcmisc` | BIG-REQUESTS, Generic Event, XC-MISC |
18+| `composite`, `xfixes`, `render`, `shape`, `res` | Composite, XFIXES, RENDER, SHAPE, X-Resource |
19+
20+The root package never imports the generated packages.
21+
22+## Install
23+
24+```sh
25+go get bnema.dev/xturbo
26+```
27+
28+## Example
29+
30+`Dial` (or `NewConn(fd)` for an already-connected socket) performs the
31+handshake and negotiates the supported extensions. Requests are encoded into
32+the connection's `Buf` and return a `Seq` (0 on failure). `Flush` never
33+blocks, and `Dispatch` reads whatever the server sent and calls the `Handler`.
34+
35+```go
36+package xturbo_test
37+
38+import (
39+ "errors"
40+ "log"
41+
42+ "golang.org/x/sys/unix"
43+
44+ "bnema.dev/xturbo"
45+ "bnema.dev/xturbo/xproto"
46+)
47+
48+// Replies registered with Expect carry this kind (16 and above are free).
49+const kindGeometry uint16 = 16
50+
51+func Example() {
52+ c, err := xturbo.Dial("") // "" means $DISPLAY
53+ if err != nil {
54+ log.Fatal(err)
55+ }
56+ defer c.Close()
57+
58+ // Startup: blocking calls, bounded by a 5 s deadline.
59+ var atoms [1]uint32
60+ if err := c.InternAtoms([]string{"WM_DELETE_WINDOW"}, false, atoms[:]); err != nil {
61+ log.Fatal(err)
62+ }
63+
64+ // Queue requests; they return a Seq (0 on failure, see c.Buf().Err()).
65+ b := c.Buf()
66+ win, err := c.NewXID()
67+ if err != nil {
68+ log.Fatal(err)
69+ }
70+ var v xproto.CreateWindowValues
71+ v.SetEventMask(xproto.EventMaskExposure | xproto.EventMaskStructureNotify)
72+ xproto.CreateWindow(b, 0, xproto.Window(win), xproto.Window(c.Root()),
73+ 0, 0, 640, 480, 0, xproto.WindowClassInputOutput, 0, &v)
74+ xproto.MapWindow(b, xproto.Window(win))
75+ if seq := xproto.GetGeometry(b, xproto.Drawable(win)); seq != 0 {
76+ if err := c.Expect(seq, kindGeometry, win, 0, 0, 0); err != nil {
77+ log.Fatal(err)
78+ }
79+ }
80+
81+ // Callbacks run inside Dispatch. Events, replies and errors are borrowed:
82+ // copy what you keep.
83+ h := &xturbo.Handler{
84+ OnEvent: func(ev *xturbo.Event) {
85+ switch ev.Kind {
86+ case xproto.KindExpose:
87+ e := xproto.ExposeOf(ev)
88+ log.Printf("expose %dx%d", e.Width(), e.Height())
89+ }
90+ },
91+ OnReply: func(r *xturbo.Reply) {
92+ if r.Kind == kindGeometry {
93+ if g, ok := xproto.ParseGetGeometryReply(r.Raw); ok {
94+ log.Printf("window %#x is %dx%d", r.Args[0], g.Width(), g.Height())
95+ }
96+ }
97+ },
98+ OnError: func(e *xturbo.XError) { log.Print(e) },
99+ }
100+
101+ for {
102+ // Flush never blocks. On ErrWouldBlock, also wait for POLLOUT.
103+ events := int16(unix.POLLIN)
104+ if err := c.Flush(); errors.Is(err, xturbo.ErrWouldBlock) {
105+ events |= unix.POLLOUT
106+ } else if err != nil {
107+ log.Fatal(err)
108+ }
109+ fds := []unix.PollFd{{Fd: int32(c.Fd()), Events: events}}
110+ if _, err := unix.Poll(fds, -1); err != nil && err != unix.EINTR {
111+ log.Fatal(err)
112+ }
113+ if err := c.Dispatch(h); err != nil { // io.EOF: server closed
114+ log.Fatal(err)
115+ }
116+ // Keep the XID pool topped up without blocking later.
117+ if c.XIDsLow() {
118+ _ = c.PrefetchXIDs()
119+ }
120+ }
121+}
122+```
123+
124+## Rules
125+
126+- **Lifetimes.** A request's body slice is valid only until the next request is
127+ encoded. Reply views (`Reply.Raw` and the parsed views) and event `Tail` are
128+ valid only during the callback; `Event.Raw` is a copy. Copy what you keep.
129+- **Callbacks.** They may queue requests, call `Expect` and `PrefetchXIDs`.
130+ They must not call `Dispatch`, `Sync`, `InternAtoms`, or `NewXID` when it
131+ needs a refill.
132+- **Blocking.** Only `Sync`, `InternAtoms` and a `NewXID` refill block, for at
133+ most 5 s, and are meant for startup. Use `XIDsLow` and `PrefetchXIDs` to
134+ keep the event loop non-blocking.
135+- **Errors.** A failed `Buf` is sticky: later requests return 0 and `Flush`
136+ returns the error.
137+
138+## Performance
139+
140+Measured with `make bench` on an AMD Ryzen 9 7900X3D (Go 1.27, `CGO_ENABLED=0`).
141+Every benchmark reports 0 allocations per operation.
142+
143+| Benchmark | Result |
144+| --- | --- |
145+| Encode a request (`MapWindow`) | 6.2 ns |
146+| Encode and flush 64 requests over a socketpair | 1.55 µs (about 24 ns per request) |
147+| Decode 10k events | 124 µs (about 12 ns per event) |
148+| Dispatch 10k mixed packets | 251 µs (about 25 ns per packet) |
149+| Parse a `GetProperty` reply view | 1.9 ns |
150+| Parse a `QueryTree` reply view | 3.2 ns |
151+
152+## Regenerating bindings
153+
154+Generated code lives in the extension packages and `xproto`; do not edit it.
155+The inputs are the pinned xcb-proto 1.17.0 files in `spec/`, listed with
156+checksums in `spec/manifest.json`.
157+
158+```sh
159+make gen # go generate ./... (cmd/xtgen)
160+make gen-check # regenerate and fail on any difference
161+go run ./cmd/xtsync # refresh spec/ from upstream and update the manifest
162+```
163+
164+## Tests
165+
166+```sh
167+make check # vet, staticcheck, tests, race, allocation, poison, gen-check
168+make xserver # tests against a real Xvfb (needs xvfb and xtrace)
169+make fuzz # every Fuzz target for FUZZTIME each (default 60s)
170+make bench # benchmarks
171+```
172+
173+## License
174+
175+MIT, see `LICENSE`. The xcb-proto XML and its license are under `LICENSES/`.
A example_test.go +86 -0
@@ -0,0 +1,86 @@
1+package xturbo_test
2+
3+import (
4+ "errors"
5+ "log"
6+
7+ "golang.org/x/sys/unix"
8+
9+ "bnema.dev/xturbo"
10+ "bnema.dev/xturbo/xproto"
11+)
12+
13+// Replies registered with Expect carry this kind (16 and above are free).
14+const kindGeometry uint16 = 16
15+
16+func Example() {
17+ c, err := xturbo.Dial("") // "" means $DISPLAY
18+ if err != nil {
19+ log.Fatal(err)
20+ }
21+ defer c.Close()
22+
23+ // Startup: blocking calls, bounded by a 5 s deadline.
24+ var atoms [1]uint32
25+ if err := c.InternAtoms([]string{"WM_DELETE_WINDOW"}, false, atoms[:]); err != nil {
26+ log.Fatal(err)
27+ }
28+
29+ // Queue requests; they return a Seq (0 on failure, see c.Buf().Err()).
30+ b := c.Buf()
31+ win, err := c.NewXID()
32+ if err != nil {
33+ log.Fatal(err)
34+ }
35+ var v xproto.CreateWindowValues
36+ v.SetEventMask(xproto.EventMaskExposure | xproto.EventMaskStructureNotify)
37+ xproto.CreateWindow(b, 0, xproto.Window(win), xproto.Window(c.Root()),
38+ 0, 0, 640, 480, 0, xproto.WindowClassInputOutput, 0, &v)
39+ xproto.MapWindow(b, xproto.Window(win))
40+ if seq := xproto.GetGeometry(b, xproto.Drawable(win)); seq != 0 {
41+ if err := c.Expect(seq, kindGeometry, win, 0, 0, 0); err != nil {
42+ log.Fatal(err)
43+ }
44+ }
45+
46+ // Callbacks run inside Dispatch. Events, replies and errors are borrowed:
47+ // copy what you keep.
48+ h := &xturbo.Handler{
49+ OnEvent: func(ev *xturbo.Event) {
50+ switch ev.Kind {
51+ case xproto.KindExpose:
52+ e := xproto.ExposeOf(ev)
53+ log.Printf("expose %dx%d", e.Width(), e.Height())
54+ }
55+ },
56+ OnReply: func(r *xturbo.Reply) {
57+ if r.Kind == kindGeometry {
58+ if g, ok := xproto.ParseGetGeometryReply(r.Raw); ok {
59+ log.Printf("window %#x is %dx%d", r.Args[0], g.Width(), g.Height())
60+ }
61+ }
62+ },
63+ OnError: func(e *xturbo.XError) { log.Print(e) },
64+ }
65+
66+ for {
67+ // Flush never blocks. On ErrWouldBlock, also wait for POLLOUT.
68+ events := int16(unix.POLLIN)
69+ if err := c.Flush(); errors.Is(err, xturbo.ErrWouldBlock) {
70+ events |= unix.POLLOUT
71+ } else if err != nil {
72+ log.Fatal(err)
73+ }
74+ fds := []unix.PollFd{{Fd: int32(c.Fd()), Events: events}}
75+ if _, err := unix.Poll(fds, -1); err != nil && err != unix.EINTR {
76+ log.Fatal(err)
77+ }
78+ if err := c.Dispatch(h); err != nil { // io.EOF: server closed
79+ log.Fatal(err)
80+ }
81+ // Keep the XID pool topped up without blocking later.
82+ if c.XIDsLow() {
83+ _ = c.PrefetchXIDs()
84+ }
85+ }
86+}
@@ -0,0 +1,86 @@
1+package xturbo_test
2+
3+import (
4+ "errors"
5+ "log"
6+
7+ "golang.org/x/sys/unix"
8+
9+ "bnema.dev/xturbo"
10+ "bnema.dev/xturbo/xproto"
11+)
12+
13+// Replies registered with Expect carry this kind (16 and above are free).
14+const kindGeometry uint16 = 16
15+
16+func Example() {
17+ c, err := xturbo.Dial("") // "" means $DISPLAY
18+ if err != nil {
19+ log.Fatal(err)
20+ }
21+ defer c.Close()
22+
23+ // Startup: blocking calls, bounded by a 5 s deadline.
24+ var atoms [1]uint32
25+ if err := c.InternAtoms([]string{"WM_DELETE_WINDOW"}, false, atoms[:]); err != nil {
26+ log.Fatal(err)
27+ }
28+
29+ // Queue requests; they return a Seq (0 on failure, see c.Buf().Err()).
30+ b := c.Buf()
31+ win, err := c.NewXID()
32+ if err != nil {
33+ log.Fatal(err)
34+ }
35+ var v xproto.CreateWindowValues
36+ v.SetEventMask(xproto.EventMaskExposure | xproto.EventMaskStructureNotify)
37+ xproto.CreateWindow(b, 0, xproto.Window(win), xproto.Window(c.Root()),
38+ 0, 0, 640, 480, 0, xproto.WindowClassInputOutput, 0, &v)
39+ xproto.MapWindow(b, xproto.Window(win))
40+ if seq := xproto.GetGeometry(b, xproto.Drawable(win)); seq != 0 {
41+ if err := c.Expect(seq, kindGeometry, win, 0, 0, 0); err != nil {
42+ log.Fatal(err)
43+ }
44+ }
45+
46+ // Callbacks run inside Dispatch. Events, replies and errors are borrowed:
47+ // copy what you keep.
48+ h := &xturbo.Handler{
49+ OnEvent: func(ev *xturbo.Event) {
50+ switch ev.Kind {
51+ case xproto.KindExpose:
52+ e := xproto.ExposeOf(ev)
53+ log.Printf("expose %dx%d", e.Width(), e.Height())
54+ }
55+ },
56+ OnReply: func(r *xturbo.Reply) {
57+ if r.Kind == kindGeometry {
58+ if g, ok := xproto.ParseGetGeometryReply(r.Raw); ok {
59+ log.Printf("window %#x is %dx%d", r.Args[0], g.Width(), g.Height())
60+ }
61+ }
62+ },
63+ OnError: func(e *xturbo.XError) { log.Print(e) },
64+ }
65+
66+ for {
67+ // Flush never blocks. On ErrWouldBlock, also wait for POLLOUT.
68+ events := int16(unix.POLLIN)
69+ if err := c.Flush(); errors.Is(err, xturbo.ErrWouldBlock) {
70+ events |= unix.POLLOUT
71+ } else if err != nil {
72+ log.Fatal(err)
73+ }
74+ fds := []unix.PollFd{{Fd: int32(c.Fd()), Events: events}}
75+ if _, err := unix.Poll(fds, -1); err != nil && err != unix.EINTR {
76+ log.Fatal(err)
77+ }
78+ if err := c.Dispatch(h); err != nil { // io.EOF: server closed
79+ log.Fatal(err)
80+ }
81+ // Keep the XID pool topped up without blocking later.
82+ if c.XIDsLow() {
83+ _ = c.PrefetchXIDs()
84+ }
85+ }
86+}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 1

↑↓ moveenter openesc close