detached 8e05969

docs!: describe the Wayland-free Renderer and add CI

Rewrite README, runtime and performance docs for Renderer, add ADR 0007
superseding ADR 0002 and 0004, and add the GitHub Actions workflow.
commit
8e059699318a5cd082ef4629a65e3294a3790659
author
brice · · 2026-10-01 17:45 UTC
parent
90277d5
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
8 files · +112 -64
A .github/workflows/ci.yml +33 -0
@@ -0,0 +1,33 @@
1+name: CI
2+on:
3+ push:
4+ branches: [main]
5+ pull_request:
6+concurrency:
7+ group: ci-${{ github.ref }}
8+ cancel-in-progress: true
9+jobs:
10+ # Arch matches the development system.
11+ check:
12+ runs-on: ubuntu-latest
13+ container:
14+ image: archlinux:latest
15+ # An init process reaps killed children, as on a real session.
16+ options: --init
17+ steps:
18+ # The tests run without a compositor or a GPU. gcc is for the race
19+ # detector.
20+ - run: >-
21+ pacman -Syu --noconfirm --needed
22+ git go make gcc
23+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
24+ with:
25+ persist-credentials: false
26+ - run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
27+ - run: |
28+ go install github.com/vektra/mockery/v3@v3.8.0
29+ go install honnef.co/go/tools/cmd/staticcheck@latest
30+ echo "$(go env GOPATH)/bin" >> "$GITHUB_PATH"
31+ - run: make check
32+ - run: make race
33+ - run: make mocks-check
@@ -0,0 +1,33 @@
1+name: CI
2+on:
3+ push:
4+ branches: [main]
5+ pull_request:
6+concurrency:
7+ group: ci-${{ github.ref }}
8+ cancel-in-progress: true
9+jobs:
10+ # Arch matches the development system.
11+ check:
12+ runs-on: ubuntu-latest
13+ container:
14+ image: archlinux:latest
15+ # An init process reaps killed children, as on a real session.
16+ options: --init
17+ steps:
18+ # The tests run without a compositor or a GPU. gcc is for the race
19+ # detector.
20+ - run: >-
21+ pacman -Syu --noconfirm --needed
22+ git go make gcc
23+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
24+ with:
25+ persist-credentials: false
26+ - run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
27+ - run: |
28+ go install github.com/vektra/mockery/v3@v3.8.0
29+ go install honnef.co/go/tools/cmd/staticcheck@latest
30+ echo "$(go env GOPATH)/bin" >> "$GITHUB_PATH"
31+ - run: make check
32+ - run: make race
33+ - run: make mocks-check
M README.md +31 -34
@@ -1,54 +1,52 @@
11 # NeferGUI
22
3-A native Go GUI library for Linux: Wayland windows, Vulkan rendering, and CSS styling. Build your view from the current model; controls return events during that call. No cgo required.
3+A pure-Go GUI renderer for Linux: build your view from the current model, style it with CSS, and draw it with Vulkan into DMA-BUF buffers. Controls return events during the view call. No cgo required.
4+
5+NeferGUI imports no Wayland library. Your application owns the Wayland client (connection, surface, linux-dmabuf feedback, buffer import, seat events) and drives a `Renderer`; [github.com/bnema/neferclient](https://github.com/bnema/neferclient) is a client toolkit built for this. The two do not import each other: the application copies plain fields between them.
46
57 > [!WARNING]
68 > **Early development.** No release yet. Expect bugs and breaking changes. Platform accessibility is not connected.
79
810 ## A small example
911
12+The view is plain Go. The model type is yours.
13+
1014 ```go
11-package main
12-
13-import (
14- "context"
15- "fmt"
16- "log"
17-
18- "github.com/bnema/nefergui"
19-)
20-
21-func main() {
22- count := 0
23- view := func(f *nefergui.Frame, count *int) {
24- root := f.Root().Column()
25- root.Text("Hello, Wayland!")
26- if root.Button("Count", nefergui.Key("count")).Activated() {
27- *count++
28- }
29- root.Text(fmt.Sprintf("Clicked %d times", *count))
30- }
31- if err := nefergui.Run(context.Background(), &count, view,
32- nefergui.Title("Hello"), nefergui.Size(320, 200)); err != nil {
33- log.Fatal(err)
15+type Model struct{ Count int }
16+
17+view := func(f *nefergui.Frame, m *Model) {
18+ root := f.Root().Column()
19+ root.Text("Hello, Wayland!")
20+ if root.Button("Count", nefergui.Key("count")).Activated() {
21+ m.Count++
3422 }
23+ root.Text(fmt.Sprintf("Clicked %d times", m.Count))
3524 }
3625 ```
3726
38-Default styles work without a stylesheet; `nefergui.Styles("app.css")` loads your own CSS.
27+The client loop, on one owner goroutine:
3928
40-## Try it
29+```go
30+r, err := nefergui.NewRenderer(nefergui.RendererConfig{MainDevice: dev, Formats: formats})
31+r.Resize(320, 200, 1) // on configure and scale changes
32+r.Input(&nefergui.Input{...}) // for each pointer, key and focus event
33+
34+var out nefergui.Output
35+if ok, err := r.Render(&out, &model, view); ok && err == nil {
36+ // import out.Planes as a wl_buffer when out.NewBuffer, attach with the
37+ // acquire/release points, commit
38+}
39+r.Released(buffer) // when out.ReleaseFD becomes readable
40+```
4141
42-Requires Go 1.27, `libvulkan.so.1`, `libxkbcommon.so.0`, usable fonts, and a Wayland compositor supporting linux-dmabuf and linux-drm-syncobj. Fractional scaling also requires viewporter. No X11 or software-rendering fallback.
42+`RendererConfig.Styles` loads your own CSS; default styles work without it. See `ExampleRenderer` and [runtime](docs/runtime.md) for the full contract.
4343
44-```sh
45-go get github.com/bnema/nefergui
46-```
44+## Requirements
4745
48-From a checkout, run the [demo](examples/demo):
46+Go 1.27, `libvulkan.so.1` and usable fonts. The GPU must export DMA-BUF and support DRM syncobj timelines (kernel 6.6 or later for eventfd waits). The compositor, reached through your Wayland client, must offer linux-dmabuf and linux-drm-syncobj; fractional scaling also needs viewporter. No X11 or software-rendering fallback.
4947
5048 ```sh
51-CGO_ENABLED=0 go run ./examples/demo
49+go get github.com/bnema/nefergui
5250 ```
5351
5452 ## Documentation
@@ -56,8 +54,7 @@ CGO_ENABLED=0 go run ./examples/demo
5654 - [Controls](docs/controls.md) and [layout](docs/layout.md)
5755 - [CSS properties and limitations](docs/css.md)
5856 - [Runtime requirements and behavior](docs/runtime.md) and [troubleshooting](docs/troubleshooting.md)
59-- [Visual test harness](docs/harness.md) and [performance](docs/performance.md)
60-- [Session lock client](docs/session-lock.md)
57+- [Performance](docs/performance.md)
6158 - [Architecture decisions](docs/adr)
6259
6360 ## License
@@ -1,54 +1,52 @@
1 # NeferGUI1 # NeferGUI
2 2
3-A native Go GUI library for Linux: Wayland windows, Vulkan rendering, and CSS styling. Build your view from the current model; controls return events during that call. No cgo required.3+A pure-Go GUI renderer for Linux: build your view from the current model, style it with CSS, and draw it with Vulkan into DMA-BUF buffers. Controls return events during the view call. No cgo required.
4+
5+NeferGUI imports no Wayland library. Your application owns the Wayland client (connection, surface, linux-dmabuf feedback, buffer import, seat events) and drives a `Renderer`; [github.com/bnema/neferclient](https://github.com/bnema/neferclient) is a client toolkit built for this. The two do not import each other: the application copies plain fields between them.
4 6
5 > [!WARNING]7 > [!WARNING]
6 > **Early development.** No release yet. Expect bugs and breaking changes. Platform accessibility is not connected.8 > **Early development.** No release yet. Expect bugs and breaking changes. Platform accessibility is not connected.
7 9
8 ## A small example10 ## A small example
9 11
12+The view is plain Go. The model type is yours.
13+
10 ```go14 ```go
11-package main15+type Model struct{ Count int }
12-16+
13-import (17+view := func(f *nefergui.Frame, m *Model) {
14- "context"18+ root := f.Root().Column()
15- "fmt"19+ root.Text("Hello, Wayland!")
16- "log"20+ if root.Button("Count", nefergui.Key("count")).Activated() {
17-21+ m.Count++
18- "github.com/bnema/nefergui"
19-)
20-
21-func main() {
22- count := 0
23- view := func(f *nefergui.Frame, count *int) {
24- root := f.Root().Column()
25- root.Text("Hello, Wayland!")
26- if root.Button("Count", nefergui.Key("count")).Activated() {
27- *count++
28- }
29- root.Text(fmt.Sprintf("Clicked %d times", *count))
30- }
31- if err := nefergui.Run(context.Background(), &count, view,
32- nefergui.Title("Hello"), nefergui.Size(320, 200)); err != nil {
33- log.Fatal(err)
34 }22 }
23+ root.Text(fmt.Sprintf("Clicked %d times", m.Count))
35 }24 }
36 ```25 ```
37 26
38-Default styles work without a stylesheet; `nefergui.Styles("app.css")` loads your own CSS.27+The client loop, on one owner goroutine:
39 28
40-## Try it29+```go
30+r, err := nefergui.NewRenderer(nefergui.RendererConfig{MainDevice: dev, Formats: formats})
31+r.Resize(320, 200, 1) // on configure and scale changes
32+r.Input(&nefergui.Input{...}) // for each pointer, key and focus event
33+
34+var out nefergui.Output
35+if ok, err := r.Render(&out, &model, view); ok && err == nil {
36+ // import out.Planes as a wl_buffer when out.NewBuffer, attach with the
37+ // acquire/release points, commit
38+}
39+r.Released(buffer) // when out.ReleaseFD becomes readable
40+```
41 41
42-Requires Go 1.27, `libvulkan.so.1`, `libxkbcommon.so.0`, usable fonts, and a Wayland compositor supporting linux-dmabuf and linux-drm-syncobj. Fractional scaling also requires viewporter. No X11 or software-rendering fallback.42+`RendererConfig.Styles` loads your own CSS; default styles work without it. See `ExampleRenderer` and [runtime](docs/runtime.md) for the full contract.
43 43
44-```sh44+## Requirements
45-go get github.com/bnema/nefergui
46-```
47 45
48-From a checkout, run the [demo](examples/demo):46+Go 1.27, `libvulkan.so.1` and usable fonts. The GPU must export DMA-BUF and support DRM syncobj timelines (kernel 6.6 or later for eventfd waits). The compositor, reached through your Wayland client, must offer linux-dmabuf and linux-drm-syncobj; fractional scaling also needs viewporter. No X11 or software-rendering fallback.
49 47
50 ```sh48 ```sh
51-CGO_ENABLED=0 go run ./examples/demo49+go get github.com/bnema/nefergui
52 ```50 ```
53 51
54 ## Documentation52 ## Documentation
@@ -56,8 +54,7 @@ CGO_ENABLED=0 go run ./examples/demo
56 - [Controls](docs/controls.md) and [layout](docs/layout.md)54 - [Controls](docs/controls.md) and [layout](docs/layout.md)
57 - [CSS properties and limitations](docs/css.md)55 - [CSS properties and limitations](docs/css.md)
58 - [Runtime requirements and behavior](docs/runtime.md) and [troubleshooting](docs/troubleshooting.md)56 - [Runtime requirements and behavior](docs/runtime.md) and [troubleshooting](docs/troubleshooting.md)
59-- [Visual test harness](docs/harness.md) and [performance](docs/performance.md)57+- [Performance](docs/performance.md)
60-- [Session lock client](docs/session-lock.md)
61 - [Architecture decisions](docs/adr)58 - [Architecture decisions](docs/adr)
62 59
63 ## License60 ## License
A docs/adr/0007-wayland-free-renderer.md +24 -0
@@ -0,0 +1,24 @@
1+# ADR 0007 — Wayland-free Renderer
2+
3+Status: accepted. Supersedes ADR 0002 (Wayland bindings and FD ownership) and ADR 0004 (keyboard).
4+
5+## Decision
6+
7+NeferGUI is a rendering library. It builds views, lays them out, styles them with CSS and draws them with Vulkan into DMA-BUF buffers. It does not connect to Wayland, import a Wayland library, link xkbcommon, or own a window, an event loop or a goroutine.
8+
9+The public entry point is `Renderer` (`NewRenderer`, `Resize`, `Input`, `Invalidate`, `Render`, `Released`, `Wake`, `Pending`, `Close`). The application owns the Wayland client and runs one owner goroutine:
10+
11+- it gives the compositor's linux-dmabuf `main_device` and format list to `NewRenderer`;
12+- it forwards size, scale and input as plain `Input` values (keysyms are xkb values; text is UTF-8);
13+- `Render` fills an `Output`: DMA-BUF planes, syncobj timelines, acquire and release points, damage, cursor shape and input region. All file descriptors stay owned by the Renderer; the application duplicates them before a transport consumes them;
14+- an eventfd per buffer becomes readable when the compositor releases it; the application then calls `Released`.
15+
16+Layering is application → Wayland client library → bindings. NeferGUI sits beside the client library, not above it. Applications copy plain fields between the two; neither imports the other. [github.com/bnema/neferclient](https://github.com/bnema/neferclient) is a client toolkit written for this split.
17+
18+## Consequences
19+
20+- `Run`, `RunFrames`, `RunLock`, the window and layer-shell options, `OnInput`, `OnResize`, `OnSurface`, `Wake` as an option, `SecretBuffer` and `WaylandSurface` are removed, together with the Wayland platform, keyboard, harness and example code. This is a breaking change.
21+- The module depends only on purego, purego-vulkan, go-text, x/image and x/sys. `go list -deps` shows no Wayland or xkbcommon module.
22+- Clipboard and input-method ports (`Clipboard`, `IME`) remain interfaces the application implements.
23+- `Password(true)` still masks the displayed value of an in-memory editor value.
24+- ADR 0003 still describes the Vulkan, DMA-BUF and explicit-sync design; the Wayland object handling it mentions now belongs to the application.
@@ -0,0 +1,24 @@
1+# ADR 0007 — Wayland-free Renderer
2+
3+Status: accepted. Supersedes ADR 0002 (Wayland bindings and FD ownership) and ADR 0004 (keyboard).
4+
5+## Decision
6+
7+NeferGUI is a rendering library. It builds views, lays them out, styles them with CSS and draws them with Vulkan into DMA-BUF buffers. It does not connect to Wayland, import a Wayland library, link xkbcommon, or own a window, an event loop or a goroutine.
8+
9+The public entry point is `Renderer` (`NewRenderer`, `Resize`, `Input`, `Invalidate`, `Render`, `Released`, `Wake`, `Pending`, `Close`). The application owns the Wayland client and runs one owner goroutine:
10+
11+- it gives the compositor's linux-dmabuf `main_device` and format list to `NewRenderer`;
12+- it forwards size, scale and input as plain `Input` values (keysyms are xkb values; text is UTF-8);
13+- `Render` fills an `Output`: DMA-BUF planes, syncobj timelines, acquire and release points, damage, cursor shape and input region. All file descriptors stay owned by the Renderer; the application duplicates them before a transport consumes them;
14+- an eventfd per buffer becomes readable when the compositor releases it; the application then calls `Released`.
15+
16+Layering is application → Wayland client library → bindings. NeferGUI sits beside the client library, not above it. Applications copy plain fields between the two; neither imports the other. [github.com/bnema/neferclient](https://github.com/bnema/neferclient) is a client toolkit written for this split.
17+
18+## Consequences
19+
20+- `Run`, `RunFrames`, `RunLock`, the window and layer-shell options, `OnInput`, `OnResize`, `OnSurface`, `Wake` as an option, `SecretBuffer` and `WaylandSurface` are removed, together with the Wayland platform, keyboard, harness and example code. This is a breaking change.
21+- The module depends only on purego, purego-vulkan, go-text, x/image and x/sys. `go list -deps` shows no Wayland or xkbcommon module.
22+- Clipboard and input-method ports (`Clipboard`, `IME`) remain interfaces the application implements.
23+- `Password(true)` still masks the displayed value of an in-memory editor value.
24+- ADR 0003 still describes the Vulkan, DMA-BUF and explicit-sync design; the Wayland object handling it mentions now belongs to the application.
M docs/controls.md +2 -2
@@ -10,11 +10,11 @@ Import `github.com/bnema/nefergui`. Public `Frame`, `Node` and event types alias
1010
1111 Checkbox and radio paint a one-em indicator, centered vertically, in their left padding (UA `padding-left: 1.5em`). Unchecked, it is an outline in `color` at 45% alpha. Checked, it is filled with `accent-color` and carries a white mark: a rounded square for checkboxes, a dot for radios. The slider paints, in its bottom padding (UA `padding-bottom: 1.25em`), a rail across the content width the pointer maps to, filled with `accent-color` up to the value, plus a round `accent-color` thumb. The indicator size follows `font-size`; `accent-color` (UA default `#3584e4`), `color`, `opacity` and `:checked` let author CSS restyle them, for example `checkbox:checked { accent-color: green }`. The painter fixes the rest: shapes and proportions, the white mark and the 45% outline alpha. Indicators stay visible when the control clips its content. The indicator always sits at the left, including for RTL labels. A slider with no bottom padding paints no track; checkbox and radio indicators still paint at the left edge, so keep their left padding. Disabled checkboxes, radios and sliders are dimmed by UA `opacity: 0.5`.
1212
13-The pointer cursor follows the CSS `cursor` of the hovered element (`pointer`, `text`, `not-allowed`; `auto` and `default` use the default arrow). While a primary button is held, the pressed element keeps its cursor. The UA sheet sets `pointer` on buttons, checkboxes, radios and sliders, `text` on inputs and textareas, and `not-allowed` on disabled controls. NeferGUI sets the shape through `wp_cursor_shape_manager_v1` when the compositor offers it; otherwise the compositor's cursor stays unchanged. `Placeholder` paints dimmed text (half opacity) in an empty `Input` or `Textarea` until a value or preedit exists; it never enters the bound value. The `label` argument of `Input` and `Textarea` is not painted; place a visible label with `Text`.
13+The pointer cursor follows the CSS `cursor` of the hovered element (`pointer`, `text`, `not-allowed`; `auto` and `default` use the default arrow). While a primary button is held, the pressed element keeps its cursor. The UA sheet sets `pointer` on buttons, checkboxes, radios and sliders, `text` on inputs and textareas, and `not-allowed` on disabled controls. `Renderer` reports the shape in `Output.Cursor`; the application applies it, for example through `wp_cursor_shape_manager_v1`. `Placeholder` paints dimmed text (half opacity) in an empty `Input` or `Textarea` until a value or preedit exists; it never enters the bound value. The `label` argument of `Input` and `Textarea` is not painted; place a visible label with `Text`.
1414
1515 `Checkbox` toggles `*bool`; `Radio(label, option, *string, ...)` selects an option in a string-valued group; `Slider(label, *float64, min, max, step, ...)` changes its pointer-backed value on arrow, Home/End and PageUp/Down keys. These controls never retain the model pointer. `Disabled(true)` blocks interaction. `Input` and `Textarea` bind `*string`; `Password(true)` masks the displayed string per Unicode grapheme and blocks copy/cut, but not paste. The editor stores cursor and anchor in UTF-8 byte offsets at extended-grapheme boundaries. Left/Right and selections are **logical**, not visual. Primary pointer press places the caret at the nearest shaped grapheme boundary, Shift+press extends from the existing anchor, and captured dragging extends even outside the control. A second press on the same control within 500 ms and 4 logical pixels selects exactly the word-iterator segment under that boundary; at a boundary the segment to the right wins, including whitespace or punctuation segments. An unrelated press resets this sequence. Pointer hit testing requires a measured text layout. Textarea Up/Down traverse wrapped visual lines at a retained preferred x; horizontal movement and edits reset it. The per-identity editor viewport scrolls vertically in Textarea and horizontally in unwrapped Input to keep the caret visible, clamping at the measured extent. Focused editors emit visual cluster highlight rectangles and a one-pixel caret inside the editor clip. At LTR/RTL run boundaries and soft wraps, caret painting, scrolling and preedit positioning use the preceding character's trailing edge. Up/Down navigation treats a shared soft-wrap offset as belonging to the following line. Password geometry maps offsets by grapheme, like the displayed mask. No undo/redo is implemented.
1616
17-`internal/edit` supplies the headless clipboard (`ReadText(limit)`, `WriteText`) and IME (`Enable`, `Disable`, `Surrounding`, `CursorRect`, `ContentType`) ports. A read is limited to 1 MiB and rejected unless UTF-8. IME batches represent preedit, commit and delete-surrounding relative to the UTF-8 cursor, processed at done. The preedit is shaped inline at the model cursor with an underline and its own cursor commands, without changing the model or its normal text run until commit. Password preedit is masked per grapheme before shaping or entering the display list. Ordinary keyboard text is inserted while an IME port is active, except while a preedit is shown; an input method that grabs the keyboard receives its keys instead of the editor. The ports do not negotiate MIME types, own FDs, or provide a Wayland adapter.
17+`internal/edit` supplies the headless clipboard (`ReadText(limit)`, `WriteText`) and IME (`Enable`, `Disable`, `Surrounding`, `CursorRect`, `ContentType`) ports. A read is limited to 1 MiB and rejected unless UTF-8. IME batches represent preedit, commit and delete-surrounding relative to the UTF-8 cursor, processed at done. The preedit is shaped inline at the model cursor with an underline and its own cursor commands, without changing the model or its normal text run until commit. Password preedit is masked per grapheme before shaping or entering the display list. Ordinary keyboard text is inserted while an IME port is active, except while a preedit is shown; an input method that grabs the keyboard receives its keys instead of the editor. The ports do not negotiate MIME types or own FDs; the application implements them and passes them in `RendererConfig`.
1818
1919 Controls are implemented in `internal/ui`; the root package re-exports their types. Display-list JSON snapshots under `internal/ui/testdata/control-display` cover every control's normal and hover states, interactive focus-visible and disabled states, checkbox/radio checked states, and Input LTR/Arabic selections and preedit. They are deterministic command snapshots, not raster screenshots. Platform `Run` uses the same control tree and display list.
2020
@@ -10,11 +10,11 @@ Import `github.com/bnema/nefergui`. Public `Frame`, `Node` and event types alias
10 10
11 Checkbox and radio paint a one-em indicator, centered vertically, in their left padding (UA `padding-left: 1.5em`). Unchecked, it is an outline in `color` at 45% alpha. Checked, it is filled with `accent-color` and carries a white mark: a rounded square for checkboxes, a dot for radios. The slider paints, in its bottom padding (UA `padding-bottom: 1.25em`), a rail across the content width the pointer maps to, filled with `accent-color` up to the value, plus a round `accent-color` thumb. The indicator size follows `font-size`; `accent-color` (UA default `#3584e4`), `color`, `opacity` and `:checked` let author CSS restyle them, for example `checkbox:checked { accent-color: green }`. The painter fixes the rest: shapes and proportions, the white mark and the 45% outline alpha. Indicators stay visible when the control clips its content. The indicator always sits at the left, including for RTL labels. A slider with no bottom padding paints no track; checkbox and radio indicators still paint at the left edge, so keep their left padding. Disabled checkboxes, radios and sliders are dimmed by UA `opacity: 0.5`.11 Checkbox and radio paint a one-em indicator, centered vertically, in their left padding (UA `padding-left: 1.5em`). Unchecked, it is an outline in `color` at 45% alpha. Checked, it is filled with `accent-color` and carries a white mark: a rounded square for checkboxes, a dot for radios. The slider paints, in its bottom padding (UA `padding-bottom: 1.25em`), a rail across the content width the pointer maps to, filled with `accent-color` up to the value, plus a round `accent-color` thumb. The indicator size follows `font-size`; `accent-color` (UA default `#3584e4`), `color`, `opacity` and `:checked` let author CSS restyle them, for example `checkbox:checked { accent-color: green }`. The painter fixes the rest: shapes and proportions, the white mark and the 45% outline alpha. Indicators stay visible when the control clips its content. The indicator always sits at the left, including for RTL labels. A slider with no bottom padding paints no track; checkbox and radio indicators still paint at the left edge, so keep their left padding. Disabled checkboxes, radios and sliders are dimmed by UA `opacity: 0.5`.
12 12
13-The pointer cursor follows the CSS `cursor` of the hovered element (`pointer`, `text`, `not-allowed`; `auto` and `default` use the default arrow). While a primary button is held, the pressed element keeps its cursor. The UA sheet sets `pointer` on buttons, checkboxes, radios and sliders, `text` on inputs and textareas, and `not-allowed` on disabled controls. NeferGUI sets the shape through `wp_cursor_shape_manager_v1` when the compositor offers it; otherwise the compositor's cursor stays unchanged. `Placeholder` paints dimmed text (half opacity) in an empty `Input` or `Textarea` until a value or preedit exists; it never enters the bound value. The `label` argument of `Input` and `Textarea` is not painted; place a visible label with `Text`.13+The pointer cursor follows the CSS `cursor` of the hovered element (`pointer`, `text`, `not-allowed`; `auto` and `default` use the default arrow). While a primary button is held, the pressed element keeps its cursor. The UA sheet sets `pointer` on buttons, checkboxes, radios and sliders, `text` on inputs and textareas, and `not-allowed` on disabled controls. `Renderer` reports the shape in `Output.Cursor`; the application applies it, for example through `wp_cursor_shape_manager_v1`. `Placeholder` paints dimmed text (half opacity) in an empty `Input` or `Textarea` until a value or preedit exists; it never enters the bound value. The `label` argument of `Input` and `Textarea` is not painted; place a visible label with `Text`.
14 14
15 `Checkbox` toggles `*bool`; `Radio(label, option, *string, ...)` selects an option in a string-valued group; `Slider(label, *float64, min, max, step, ...)` changes its pointer-backed value on arrow, Home/End and PageUp/Down keys. These controls never retain the model pointer. `Disabled(true)` blocks interaction. `Input` and `Textarea` bind `*string`; `Password(true)` masks the displayed string per Unicode grapheme and blocks copy/cut, but not paste. The editor stores cursor and anchor in UTF-8 byte offsets at extended-grapheme boundaries. Left/Right and selections are **logical**, not visual. Primary pointer press places the caret at the nearest shaped grapheme boundary, Shift+press extends from the existing anchor, and captured dragging extends even outside the control. A second press on the same control within 500 ms and 4 logical pixels selects exactly the word-iterator segment under that boundary; at a boundary the segment to the right wins, including whitespace or punctuation segments. An unrelated press resets this sequence. Pointer hit testing requires a measured text layout. Textarea Up/Down traverse wrapped visual lines at a retained preferred x; horizontal movement and edits reset it. The per-identity editor viewport scrolls vertically in Textarea and horizontally in unwrapped Input to keep the caret visible, clamping at the measured extent. Focused editors emit visual cluster highlight rectangles and a one-pixel caret inside the editor clip. At LTR/RTL run boundaries and soft wraps, caret painting, scrolling and preedit positioning use the preceding character's trailing edge. Up/Down navigation treats a shared soft-wrap offset as belonging to the following line. Password geometry maps offsets by grapheme, like the displayed mask. No undo/redo is implemented.15 `Checkbox` toggles `*bool`; `Radio(label, option, *string, ...)` selects an option in a string-valued group; `Slider(label, *float64, min, max, step, ...)` changes its pointer-backed value on arrow, Home/End and PageUp/Down keys. These controls never retain the model pointer. `Disabled(true)` blocks interaction. `Input` and `Textarea` bind `*string`; `Password(true)` masks the displayed string per Unicode grapheme and blocks copy/cut, but not paste. The editor stores cursor and anchor in UTF-8 byte offsets at extended-grapheme boundaries. Left/Right and selections are **logical**, not visual. Primary pointer press places the caret at the nearest shaped grapheme boundary, Shift+press extends from the existing anchor, and captured dragging extends even outside the control. A second press on the same control within 500 ms and 4 logical pixels selects exactly the word-iterator segment under that boundary; at a boundary the segment to the right wins, including whitespace or punctuation segments. An unrelated press resets this sequence. Pointer hit testing requires a measured text layout. Textarea Up/Down traverse wrapped visual lines at a retained preferred x; horizontal movement and edits reset it. The per-identity editor viewport scrolls vertically in Textarea and horizontally in unwrapped Input to keep the caret visible, clamping at the measured extent. Focused editors emit visual cluster highlight rectangles and a one-pixel caret inside the editor clip. At LTR/RTL run boundaries and soft wraps, caret painting, scrolling and preedit positioning use the preceding character's trailing edge. Up/Down navigation treats a shared soft-wrap offset as belonging to the following line. Password geometry maps offsets by grapheme, like the displayed mask. No undo/redo is implemented.
16 16
17-`internal/edit` supplies the headless clipboard (`ReadText(limit)`, `WriteText`) and IME (`Enable`, `Disable`, `Surrounding`, `CursorRect`, `ContentType`) ports. A read is limited to 1 MiB and rejected unless UTF-8. IME batches represent preedit, commit and delete-surrounding relative to the UTF-8 cursor, processed at done. The preedit is shaped inline at the model cursor with an underline and its own cursor commands, without changing the model or its normal text run until commit. Password preedit is masked per grapheme before shaping or entering the display list. Ordinary keyboard text is inserted while an IME port is active, except while a preedit is shown; an input method that grabs the keyboard receives its keys instead of the editor. The ports do not negotiate MIME types, own FDs, or provide a Wayland adapter.17+`internal/edit` supplies the headless clipboard (`ReadText(limit)`, `WriteText`) and IME (`Enable`, `Disable`, `Surrounding`, `CursorRect`, `ContentType`) ports. A read is limited to 1 MiB and rejected unless UTF-8. IME batches represent preedit, commit and delete-surrounding relative to the UTF-8 cursor, processed at done. The preedit is shaped inline at the model cursor with an underline and its own cursor commands, without changing the model or its normal text run until commit. Password preedit is masked per grapheme before shaping or entering the display list. Ordinary keyboard text is inserted while an IME port is active, except while a preedit is shown; an input method that grabs the keyboard receives its keys instead of the editor. The ports do not negotiate MIME types or own FDs; the application implements them and passes them in `RendererConfig`.
18 18
19 Controls are implemented in `internal/ui`; the root package re-exports their types. Display-list JSON snapshots under `internal/ui/testdata/control-display` cover every control's normal and hover states, interactive focus-visible and disabled states, checkbox/radio checked states, and Input LTR/Arabic selections and preedit. They are deterministic command snapshots, not raster screenshots. Platform `Run` uses the same control tree and display list.19 Controls are implemented in `internal/ui`; the root package re-exports their types. Display-list JSON snapshots under `internal/ui/testdata/control-display` cover every control's normal and hover states, interactive focus-visible and disabled states, checkbox/radio checked states, and Input LTR/Arabic selections and preedit. They are deterministic command snapshots, not raster screenshots. Platform `Run` uses the same control tree and display list.
20 20
M docs/performance.md +5 -5
@@ -6,7 +6,7 @@ Measured: **2026-09-30, 07:52–07:54 CEST (UTC+02:00)** on commit `7ad3316fd1ad
66
77 AMD Ryzen 9 7900X3D; Go `go1.27.1-X:nodwarf5 linux/amd64`. Command: `CGO_ENABLED=0 GOWORK=off go test -mod=readonly -p=1 -run '^$' -bench . -benchmem -count=5 ./internal/ui ./internal/render ./internal/layout ./internal/text ./internal/css`. Package benchmarks run serially; external profiling runs are separate. Entries are medians of five runs. Budgets allow roughly twice the measured medians; zero-allocation paths stay at zero. DemoFrame has a deliberately tighter 1,100-allocation limit to catch editor-workspace regressions. CPU numbers vary with machine load.
88
9-The demo-frame benchmark builds the light, comfortable document workspace with the example's actual stylesheet, computes layout and produces a display list after warmup. It includes the toolbar, document list, populated multiline editor and properties pane with input, radios and slider; it excludes GPU submission. Its workload is larger than the older unstyled fixture, so those results are not directly comparable. Prepare uses a representative text/box command mix and excludes GPU submission. Paint uses 100 cards with two shadows and text; ListBatches uses 2,000 quads with interleaved images. ListBufferReuse and LayoutPaintTree warm their buffers and text cache before resetting the timer; their figures measure steady state.
9+The demo-frame benchmark builds the light, comfortable document workspace with a demo stylesheet (`internal/ui/testdata/demo.css`), computes layout and produces a display list after warmup. It includes the toolbar, document list, populated multiline editor and properties pane with input, radios and slider; it excludes GPU submission. Its workload is larger than the older unstyled fixture, so those results are not directly comparable. Prepare uses a representative text/box command mix and excludes GPU submission. Paint uses 100 cards with two shadows and text; ListBatches uses 2,000 quads with interleaved images. ListBufferReuse and LayoutPaintTree warm their buffers and text cache before resetting the timer; their figures measure steady state.
1010
1111 | Package / benchmark | Median ns/op | Median B/op | Median allocs/op | Budget ns/op | Budget B/op | Budget allocs/op |
1212 |---|---:|---:|---:|---:|---:|---:|
@@ -29,17 +29,17 @@ The demo-frame benchmark builds the light, comfortable document workspace with t
2929
3030 Natural container measurement prevents nested flex text from collapsing, at a CPU cost that grows with nesting depth. Layout1000 measures about 36% slower than the earlier 656 µs run; allocation count stays at 1,027. A per-layout node/width cache increased time and retained allocation in a trial and is not used. LayoutPaintTree stays near its previous cost. These are correctness/performance tradeoffs, not a claim of universal speed improvement.
3131
32-`TestDemoFrameAllocationBudget` checks the 1,100 allocs/op limit with `testing.AllocsPerRun`. Focused tests also check constant quad allocation, bounded paint allocation, allocation-free warm list-buffer reuse, and allocation-free idle wait results. The real-ioctl allocation test is opt-in via `NEFERGUI_RENDER_NODE`. The other table budgets are reference targets, not automated assertions.
32+`TestAllocDemoFrame` checks the 1,100 allocs/op limit with `testing.AllocsPerRun`. `TestAllocRendererSteadyFrame` measures `Renderer.Render` for a view whose text changes through `TextInt`; it measured 244 allocations per steady Render+Released cycle on 2026-10-01 (the guard fails above that baseline), needs a GPU, and is skipped unless `NEFERGUI_RENDER_NODE` is set. Focused tests also check constant quad allocation, bounded paint allocation, allocation-free warm list-buffer reuse, and allocation-free idle wait results. The real-ioctl allocation test is opt-in via `NEFERGUI_RENDER_NODE`. The other table budgets are reference targets, not automated assertions.
3333
3434 Frame construction limits allocation churn through:
3535
3636 - a detached quad array initially reserved up to 512 quads; larger visible frames grow after culling, so offscreen content cannot force an unbounded speculative allocation;
3737 - detached display-list storage sized once for commands, shadows, runs and glyphs;
38-- a session-owned instance/batch buffer, consumed synchronously before reuse, with image references cleared after use and oversized storage discarded when smaller frames arrive;
38+- a target-owned instance/batch buffer, consumed synchronously before reuse, with image references cleared after use and oversized storage discarded when smaller frames arrive;
3939 - font family normalization at font load and identity path keys computed once per element;
4040 - `text.Engine.Measure` caching by text, request and width. Entries unused for two frames are evicted; the cache holds at most 4,096 entries. Results are immutable; callers use `Layout.Clone` before adjusting positions.
4141
42-`Run` builds a frame only on input or a redraw request. Idle frame construction stops. Expected release-wait timeouts return without constructing errors; real ioctl failures retain their context and wrapped errno.
42+`Renderer.Render` builds a frame only on input or a redraw request. Idle frame construction stops. Expected release-wait timeouts return without constructing errors; real ioctl failures retain their context and wrapped errno.
4343
4444 On commit `9064a73`, measured **2026-09-30, 06:34 CEST**, `BenchmarkWaitPointIdle` on an unsignaled real DRM timeline took a median **21,370,548 ns/op, 0 B/op, 0 allocs/op**. This includes the requested 20 ms timeout, not active CPU time:
4545
@@ -85,6 +85,6 @@ A separate approximately 20-second idle profile recorded about **91 KiB** alloca
8585
8686 The measured commit includes `purego-vulkan` v0.6.0, `wlturbo` v0.3.0 and `typesetting` v0.3.5. The subsequent `2e4532d` commit adds an empty-glyph regression test and raises the minimum compositor version checked at startup; it does not change the measured frame paths on this version-6 compositor. The comparison therefore includes both library optimizations and dependency updates, not isolated effects for each.
8787
88-Prepared quads and layout painting still allocate detached outputs. Editor and indicator command insertion can also reallocate the display list; those UI-owned costs are included in the demo measurements and are not optimized here. Glyph rasterization and glyph-key hashing also appear in cumulative allocation profiles. `NEFERGUI_DEBUG_DIR` enables readbacks and PNG work; leave it unset when measuring ordinary application cost.
88+Prepared quads and layout painting still allocate detached outputs. Editor and indicator command insertion can also reallocate the display list; those UI-owned costs are included in the demo measurements and are not optimized here. Glyph rasterization and glyph-key hashing also appear in cumulative allocation profiles.
8989
9090 Use `pprof`'s `alloc_space` to find cumulative allocation churn and `inuse_space` after GC to inspect retained Go memory. Use `/proc/<pid>/smaps_rollup` for RSS/PSS and DRM fdinfo for GPU counters; heap profiles do not include driver or GPU memory.
@@ -6,7 +6,7 @@ Measured: **2026-09-30, 07:52–07:54 CEST (UTC+02:00)** on commit `7ad3316fd1ad
6 6
7 AMD Ryzen 9 7900X3D; Go `go1.27.1-X:nodwarf5 linux/amd64`. Command: `CGO_ENABLED=0 GOWORK=off go test -mod=readonly -p=1 -run '^$' -bench . -benchmem -count=5 ./internal/ui ./internal/render ./internal/layout ./internal/text ./internal/css`. Package benchmarks run serially; external profiling runs are separate. Entries are medians of five runs. Budgets allow roughly twice the measured medians; zero-allocation paths stay at zero. DemoFrame has a deliberately tighter 1,100-allocation limit to catch editor-workspace regressions. CPU numbers vary with machine load.7 AMD Ryzen 9 7900X3D; Go `go1.27.1-X:nodwarf5 linux/amd64`. Command: `CGO_ENABLED=0 GOWORK=off go test -mod=readonly -p=1 -run '^$' -bench . -benchmem -count=5 ./internal/ui ./internal/render ./internal/layout ./internal/text ./internal/css`. Package benchmarks run serially; external profiling runs are separate. Entries are medians of five runs. Budgets allow roughly twice the measured medians; zero-allocation paths stay at zero. DemoFrame has a deliberately tighter 1,100-allocation limit to catch editor-workspace regressions. CPU numbers vary with machine load.
8 8
9-The demo-frame benchmark builds the light, comfortable document workspace with the example's actual stylesheet, computes layout and produces a display list after warmup. It includes the toolbar, document list, populated multiline editor and properties pane with input, radios and slider; it excludes GPU submission. Its workload is larger than the older unstyled fixture, so those results are not directly comparable. Prepare uses a representative text/box command mix and excludes GPU submission. Paint uses 100 cards with two shadows and text; ListBatches uses 2,000 quads with interleaved images. ListBufferReuse and LayoutPaintTree warm their buffers and text cache before resetting the timer; their figures measure steady state.9+The demo-frame benchmark builds the light, comfortable document workspace with a demo stylesheet (`internal/ui/testdata/demo.css`), computes layout and produces a display list after warmup. It includes the toolbar, document list, populated multiline editor and properties pane with input, radios and slider; it excludes GPU submission. Its workload is larger than the older unstyled fixture, so those results are not directly comparable. Prepare uses a representative text/box command mix and excludes GPU submission. Paint uses 100 cards with two shadows and text; ListBatches uses 2,000 quads with interleaved images. ListBufferReuse and LayoutPaintTree warm their buffers and text cache before resetting the timer; their figures measure steady state.
10 10
11 | Package / benchmark | Median ns/op | Median B/op | Median allocs/op | Budget ns/op | Budget B/op | Budget allocs/op |11 | Package / benchmark | Median ns/op | Median B/op | Median allocs/op | Budget ns/op | Budget B/op | Budget allocs/op |
12 |---|---:|---:|---:|---:|---:|---:|12 |---|---:|---:|---:|---:|---:|---:|
@@ -29,17 +29,17 @@ The demo-frame benchmark builds the light, comfortable document workspace with t
29 29
30 Natural container measurement prevents nested flex text from collapsing, at a CPU cost that grows with nesting depth. Layout1000 measures about 36% slower than the earlier 656 µs run; allocation count stays at 1,027. A per-layout node/width cache increased time and retained allocation in a trial and is not used. LayoutPaintTree stays near its previous cost. These are correctness/performance tradeoffs, not a claim of universal speed improvement.30 Natural container measurement prevents nested flex text from collapsing, at a CPU cost that grows with nesting depth. Layout1000 measures about 36% slower than the earlier 656 µs run; allocation count stays at 1,027. A per-layout node/width cache increased time and retained allocation in a trial and is not used. LayoutPaintTree stays near its previous cost. These are correctness/performance tradeoffs, not a claim of universal speed improvement.
31 31
32-`TestDemoFrameAllocationBudget` checks the 1,100 allocs/op limit with `testing.AllocsPerRun`. Focused tests also check constant quad allocation, bounded paint allocation, allocation-free warm list-buffer reuse, and allocation-free idle wait results. The real-ioctl allocation test is opt-in via `NEFERGUI_RENDER_NODE`. The other table budgets are reference targets, not automated assertions.32+`TestAllocDemoFrame` checks the 1,100 allocs/op limit with `testing.AllocsPerRun`. `TestAllocRendererSteadyFrame` measures `Renderer.Render` for a view whose text changes through `TextInt`; it measured 244 allocations per steady Render+Released cycle on 2026-10-01 (the guard fails above that baseline), needs a GPU, and is skipped unless `NEFERGUI_RENDER_NODE` is set. Focused tests also check constant quad allocation, bounded paint allocation, allocation-free warm list-buffer reuse, and allocation-free idle wait results. The real-ioctl allocation test is opt-in via `NEFERGUI_RENDER_NODE`. The other table budgets are reference targets, not automated assertions.
33 33
34 Frame construction limits allocation churn through:34 Frame construction limits allocation churn through:
35 35
36 - a detached quad array initially reserved up to 512 quads; larger visible frames grow after culling, so offscreen content cannot force an unbounded speculative allocation;36 - a detached quad array initially reserved up to 512 quads; larger visible frames grow after culling, so offscreen content cannot force an unbounded speculative allocation;
37 - detached display-list storage sized once for commands, shadows, runs and glyphs;37 - detached display-list storage sized once for commands, shadows, runs and glyphs;
38-- a session-owned instance/batch buffer, consumed synchronously before reuse, with image references cleared after use and oversized storage discarded when smaller frames arrive;38+- a target-owned instance/batch buffer, consumed synchronously before reuse, with image references cleared after use and oversized storage discarded when smaller frames arrive;
39 - font family normalization at font load and identity path keys computed once per element;39 - font family normalization at font load and identity path keys computed once per element;
40 - `text.Engine.Measure` caching by text, request and width. Entries unused for two frames are evicted; the cache holds at most 4,096 entries. Results are immutable; callers use `Layout.Clone` before adjusting positions.40 - `text.Engine.Measure` caching by text, request and width. Entries unused for two frames are evicted; the cache holds at most 4,096 entries. Results are immutable; callers use `Layout.Clone` before adjusting positions.
41 41
42-`Run` builds a frame only on input or a redraw request. Idle frame construction stops. Expected release-wait timeouts return without constructing errors; real ioctl failures retain their context and wrapped errno.42+`Renderer.Render` builds a frame only on input or a redraw request. Idle frame construction stops. Expected release-wait timeouts return without constructing errors; real ioctl failures retain their context and wrapped errno.
43 43
44 On commit `9064a73`, measured **2026-09-30, 06:34 CEST**, `BenchmarkWaitPointIdle` on an unsignaled real DRM timeline took a median **21,370,548 ns/op, 0 B/op, 0 allocs/op**. This includes the requested 20 ms timeout, not active CPU time:44 On commit `9064a73`, measured **2026-09-30, 06:34 CEST**, `BenchmarkWaitPointIdle` on an unsignaled real DRM timeline took a median **21,370,548 ns/op, 0 B/op, 0 allocs/op**. This includes the requested 20 ms timeout, not active CPU time:
45 45
@@ -85,6 +85,6 @@ A separate approximately 20-second idle profile recorded about **91 KiB** alloca
85 85
86 The measured commit includes `purego-vulkan` v0.6.0, `wlturbo` v0.3.0 and `typesetting` v0.3.5. The subsequent `2e4532d` commit adds an empty-glyph regression test and raises the minimum compositor version checked at startup; it does not change the measured frame paths on this version-6 compositor. The comparison therefore includes both library optimizations and dependency updates, not isolated effects for each.86 The measured commit includes `purego-vulkan` v0.6.0, `wlturbo` v0.3.0 and `typesetting` v0.3.5. The subsequent `2e4532d` commit adds an empty-glyph regression test and raises the minimum compositor version checked at startup; it does not change the measured frame paths on this version-6 compositor. The comparison therefore includes both library optimizations and dependency updates, not isolated effects for each.
87 87
88-Prepared quads and layout painting still allocate detached outputs. Editor and indicator command insertion can also reallocate the display list; those UI-owned costs are included in the demo measurements and are not optimized here. Glyph rasterization and glyph-key hashing also appear in cumulative allocation profiles. `NEFERGUI_DEBUG_DIR` enables readbacks and PNG work; leave it unset when measuring ordinary application cost.88+Prepared quads and layout painting still allocate detached outputs. Editor and indicator command insertion can also reallocate the display list; those UI-owned costs are included in the demo measurements and are not optimized here. Glyph rasterization and glyph-key hashing also appear in cumulative allocation profiles.
89 89
90 Use `pprof`'s `alloc_space` to find cumulative allocation churn and `inuse_space` after GC to inspect retained Go memory. Use `/proc/<pid>/smaps_rollup` for RSS/PSS and DRM fdinfo for GPU counters; heap profiles do not include driver or GPU memory.90 Use `pprof`'s `alloc_space` to find cumulative allocation churn and `inuse_space` after GC to inspect retained Go memory. Use `/proc/<pid>/smaps_rollup` for RSS/PSS and DRM fdinfo for GPU counters; heap profiles do not include driver or GPU memory.
M docs/runtime.md +14 -19
@@ -1,32 +1,27 @@
11 # Immediate runtime
22
3-`Run(ctx, &model, view, options...)` opens a Wayland surface, renders with Vulkan and returns when the surface closes (`nil`) or `ctx` is cancelled. It uses an xdg toplevel by default; `Layer(LayerConfig)` chooses a layer-shell role with an output name, layer, anchors, margins, exclusive zone and keyboard mode. `Title`, `Size` (logical pixels), `Styles` (CSS file path) and `Transparent` configure the surface. Setup and teardown errors are returned; joined errors retain real failures alongside cancellation. Accessibility is not connected.
3+`NewRenderer(RendererConfig)` opens the GPU named by `MainDevice` (the `dev_t` of linux-dmabuf feedback), loads fonts and the optional `Styles` CSS file, and allocates no images. `Formats` is the compositor's flattened format and modifier list in preference order; `Transparent` selects ARGB8888 instead of XRGB8888. `Clipboard` and `IME` are optional ports the application implements. The Renderer imports no Wayland library, starts no goroutine and must be used from one owner goroutine; only `Wake` is safe from any goroutine. Accessibility is not connected.
44
5-`LayerConfig.AllOutputs` puts one layer surface on every output through one Wayland connection and owner loop. Outputs added later gain a surface; removed outputs lose theirs. Every surface runs the same view and model, laid out at its own size and scale; `Wake` and `OnInput` returning true redraw them all. `Run` then returns only on cancellation or failure. It cannot be combined with `Output`, keyboard interactivity, `OnSurface`, `OnResize` or `RunFrames`.
5+A client loop forwards events and presents frames:
66
7-`OnInput(func(InputEvent) bool)` observes input on the owner loop; return true only when application state changed and needs a redraw. `OnResize` runs at startup and on size or scale changes. `Wake` requests an event-driven redraw without a continuous frame loop. `OnSurface(func(context.Context, WaylandSurface) error)` configures extension protocols after the surface role exists and before the first buffer. Its display and surface belong to NeferGUI. Protocol callbacks run on the owner during startup and on the reader after the loop starts; callbacks must not mutate the application's model from the reader.
7+- `Resize(width, height, scale)` sets the logical size and scale; invalid values are ignored. Nothing is drawn before the first `Resize`.
8+- `Input(*Input)` routes one pointer, key or focus event and reports whether a redraw is needed. Coordinates are logical surface pixels. `Keysym` carries xkb keysym values and `Text` the UTF-8 text of the key; `Text` is valid only during the call. `InputReset` drops held state after a lost input queue. `Invalidate` forces a rebuild.
9+- `Render(&out, &model, view)` builds the view when input or a redraw request is queued and draws it into a free buffer. It returns false, without touching the GPU, when nothing changed or no buffer is free. When it returns true, `out` describes the frame in physical pixels: DMA-BUF `Planes`, `FourCC`, `Modifier`, acquire and release timelines and points, `Damage`, `Cursor`, and `InputRects` with `InputRectsChanged`.
10+- `Released(buffer)` returns a buffer to the pool when its `ReleaseFD` eventfd becomes readable.
11+- `Pending()` reports a built frame waiting for the GPU rather than the compositor. No event announces this: arm a short timer (about 2 ms) and call `Render` again. `Wake()` is signaled when another goroutine, for example a finished paste, needs a `Render`.
12+- `Close` cancels pending pastes and frees GPU resources.
813
9-`Frame.SetInputRects` stages the clickable region with the frame's buffer commit. Nil restores the whole surface; a non-nil empty slice makes it click-through. `Node.Rect` places a border-box rectangle relative to a Stack's content box without parsing geometry CSS. It applies only to Stack children; debug builds diagnose other parents.
14+All file descriptors in `Output` stay owned by the Renderer; duplicate them before handing them to a transport that consumes descriptors. `Output.NewBuffer` says to import `Planes` as a new buffer, and `NewTimelines` says the timeline descriptors are new. Descriptors of retired buffers listed in `Output.Retired` stay open until the next `Render` or `Close`, so the client can stop watching and destroy them first. `Output` slices are reused and valid until the next `Render`. Allocation behavior of `Render` is described in [performance](performance.md).
1015
11-The UI loop never blocks on the GPU or the compositor. It applies Wayland events, drains seat input, fires key repeat from a timer, and builds `view` once per input batch or redraw request. A frame is prepared and presented only when the build or the window size/scale changed. Clipboard reads are asynchronous: paste starts a read and the text is inserted by the next frame. A new paste cancels the previous one. Input-method (text-input v3) batches are applied during frame construction.
16+`Frame.SetInputRects` stages the clickable region, reported in `Output.InputRects` only when it changes. Nil restores the whole surface; a non-nil empty slice makes it click-through. `Node.Rect` places a border-box rectangle relative to a Stack's content box without parsing geometry CSS. It applies only to Stack children; debug builds diagnose other parents. The cursor shape follows the CSS `cursor` of the hovered element and is reported in `Output.Cursor`; the client maps it to its own cursor mechanism.
1217
13-The unexported `runtime` constructs views synchronously. A concurrent or re-entrant build never waits: it requests a redraw, posts a wake token and returns false. The in-progress frame commits first; queued input remains for the next build.
14-
15-The unexported UI-loop port `platformInput` accepts logical pointer motion/press/release/axis/leave, interpreted `keyboard.Key` events, focus-in/out and logical viewport resize/scale. `route` hit-tests against the last committed layout (reverse paint order and ancestor clips), resolves the result to a frame identity and wakes the next build only when state changes. `Target` is a committed-child-path helper for tests. A build computes CSS pseudo-state and commits layout, display list and tree together. The headless viewport defaults to 800×600 logical pixels; `Run` sets it from the window. Scale is used by the renderer only and never affects hit testing: Wayland surface coordinates are already logical. A seat input-queue overflow is delivered as a reset (pointer leave, all buttons released, keyboard focus-out). `Run` installs a text engine from system fonts; headless tests install one explicitly, and without one text commands have no measured glyph runs.
16-
17-## Example
18+Clipboard reads are asynchronous: paste starts a read and the text is inserted by the next frame. A new paste cancels the previous one. Input-method batches are applied during frame construction.
1819
19-`examples/demo` is the product example with an embedded stylesheet:
20-
21-```sh
22-GOWORK=off go run ./examples/demo
23-```
24-
25-`--static --frames N` renders N frames and exits (used by the visual harness).
20+The unexported `runtime` constructs views synchronously. A concurrent or re-entrant build never waits: it requests a redraw, posts a wake token and returns false. The in-progress frame commits first; queued input remains for the next build.
2621
27-Tab traverses visible, enabled controls in document order and wraps; Shift+Tab reverses. Keyboard focus sets `:focus-visible`; pointer focus does not. Enter activates focused buttons and checkboxes on initial key press, ignoring repeats. Space sets `:active` on initial press and activates on release only if focus is unchanged; focus loss/change or Escape cancels it, and repeats are ignored. Disabled controls cannot focus or activate. When a focused control is removed or disabled, focus falls back to **none** (the next Tab starts at the first available control). The first pressed pointer button owns capture until its matching release, even outside the target; other button presses/releases cannot steal it or change focus. Only primary-button release on the captured control activates it. Pointer leave clears hover but preserves capture until matching release (an out-of-surface release cannot activate); removal or platform focus loss cancels capture. Scroll wheel deltas apply to the nearest scrollable ancestor and clamp to measured content bounds; offsets are discarded when that identity is no longer a laid-out scroll container. Unchanged state does not redraw. Control callbacks run during frame construction; input, resize and surface hooks run on the session owner at their documented lifecycle points.
22+The unexported input port `platformInput` accepts logical pointer motion/press/release/axis/leave, key events, focus-in/out and logical viewport resize/scale. `route` hit-tests against the last committed layout (reverse paint order and ancestor clips), resolves the result to a frame identity and wakes the next build only when state changes. `Target` is a committed-child-path helper for tests. A build computes CSS pseudo-state and commits layout, display list and tree together. Scale is used by the renderer only and never affects hit testing: surface coordinates are already logical. `NewRenderer` installs a text engine from system fonts; headless tests install one explicitly, and without one text commands have no measured glyph runs.
2823
29-Offline keyboard tests require libxkbcommon.so.0 and a usable XKB data installation; a host without those prerequisites may opt out with `NEFERGUI_SKIP_XKB=1` (other test failures still fail). The valid-FD test fixture is a French Wayland keymap generated with `xkbcli compile-keymap --layout fr --output-format 1` from libxkbcommon 1.13.2; SHA-256 `f6bcde4e4e03293d05c9426d08cdc2e9a78e7b8faef39dc97c5d267ad7355607`.
24+Tab traverses visible, enabled controls in document order and wraps; Shift+Tab reverses. Keyboard focus sets `:focus-visible`; pointer focus does not. Enter activates focused buttons and checkboxes on initial key press, ignoring repeats. Space sets `:active` on initial press and activates on release only if focus is unchanged; focus loss/change or Escape cancels it, and repeats are ignored. Disabled controls cannot focus or activate. When a focused control is removed or disabled, focus falls back to **none** (the next Tab starts at the first available control). The first pressed pointer button owns capture until its matching release, even outside the target; other button presses/releases cannot steal it or change focus. Only primary-button release on the captured control activates it. Pointer leave clears hover but preserves capture until matching release (an out-of-surface release cannot activate); removal or platform focus loss cancels capture. Scroll wheel deltas apply to the nearest scrollable ancestor and clamp to measured content bounds; offsets are discarded when that identity is no longer a laid-out scroll container. Unchanged state does not redraw. Control callbacks run during frame construction; the Renderer calls no application code outside `Render`.
3025
3126 Compile with `-tags nefergui_debug` to report duplicate sibling keys, changed unkeyed child types and stale Node use (stale use panics). Without this tag, stale handles are inert. Do not retain a Node across frames. `ID` is only for CSS, while `Key` identifies an element within its parent. Unkeyed children are identified by their position and type.
3227
@@ -1,32 +1,27 @@
1 # Immediate runtime1 # Immediate runtime
2 2
3-`Run(ctx, &model, view, options...)` opens a Wayland surface, renders with Vulkan and returns when the surface closes (`nil`) or `ctx` is cancelled. It uses an xdg toplevel by default; `Layer(LayerConfig)` chooses a layer-shell role with an output name, layer, anchors, margins, exclusive zone and keyboard mode. `Title`, `Size` (logical pixels), `Styles` (CSS file path) and `Transparent` configure the surface. Setup and teardown errors are returned; joined errors retain real failures alongside cancellation. Accessibility is not connected.3+`NewRenderer(RendererConfig)` opens the GPU named by `MainDevice` (the `dev_t` of linux-dmabuf feedback), loads fonts and the optional `Styles` CSS file, and allocates no images. `Formats` is the compositor's flattened format and modifier list in preference order; `Transparent` selects ARGB8888 instead of XRGB8888. `Clipboard` and `IME` are optional ports the application implements. The Renderer imports no Wayland library, starts no goroutine and must be used from one owner goroutine; only `Wake` is safe from any goroutine. Accessibility is not connected.
4 4
5-`LayerConfig.AllOutputs` puts one layer surface on every output through one Wayland connection and owner loop. Outputs added later gain a surface; removed outputs lose theirs. Every surface runs the same view and model, laid out at its own size and scale; `Wake` and `OnInput` returning true redraw them all. `Run` then returns only on cancellation or failure. It cannot be combined with `Output`, keyboard interactivity, `OnSurface`, `OnResize` or `RunFrames`.5+A client loop forwards events and presents frames:
6 6
7-`OnInput(func(InputEvent) bool)` observes input on the owner loop; return true only when application state changed and needs a redraw. `OnResize` runs at startup and on size or scale changes. `Wake` requests an event-driven redraw without a continuous frame loop. `OnSurface(func(context.Context, WaylandSurface) error)` configures extension protocols after the surface role exists and before the first buffer. Its display and surface belong to NeferGUI. Protocol callbacks run on the owner during startup and on the reader after the loop starts; callbacks must not mutate the application's model from the reader.7+- `Resize(width, height, scale)` sets the logical size and scale; invalid values are ignored. Nothing is drawn before the first `Resize`.
8+- `Input(*Input)` routes one pointer, key or focus event and reports whether a redraw is needed. Coordinates are logical surface pixels. `Keysym` carries xkb keysym values and `Text` the UTF-8 text of the key; `Text` is valid only during the call. `InputReset` drops held state after a lost input queue. `Invalidate` forces a rebuild.
9+- `Render(&out, &model, view)` builds the view when input or a redraw request is queued and draws it into a free buffer. It returns false, without touching the GPU, when nothing changed or no buffer is free. When it returns true, `out` describes the frame in physical pixels: DMA-BUF `Planes`, `FourCC`, `Modifier`, acquire and release timelines and points, `Damage`, `Cursor`, and `InputRects` with `InputRectsChanged`.
10+- `Released(buffer)` returns a buffer to the pool when its `ReleaseFD` eventfd becomes readable.
11+- `Pending()` reports a built frame waiting for the GPU rather than the compositor. No event announces this: arm a short timer (about 2 ms) and call `Render` again. `Wake()` is signaled when another goroutine, for example a finished paste, needs a `Render`.
12+- `Close` cancels pending pastes and frees GPU resources.
8 13
9-`Frame.SetInputRects` stages the clickable region with the frame's buffer commit. Nil restores the whole surface; a non-nil empty slice makes it click-through. `Node.Rect` places a border-box rectangle relative to a Stack's content box without parsing geometry CSS. It applies only to Stack children; debug builds diagnose other parents.14+All file descriptors in `Output` stay owned by the Renderer; duplicate them before handing them to a transport that consumes descriptors. `Output.NewBuffer` says to import `Planes` as a new buffer, and `NewTimelines` says the timeline descriptors are new. Descriptors of retired buffers listed in `Output.Retired` stay open until the next `Render` or `Close`, so the client can stop watching and destroy them first. `Output` slices are reused and valid until the next `Render`. Allocation behavior of `Render` is described in [performance](performance.md).
10 15
11-The UI loop never blocks on the GPU or the compositor. It applies Wayland events, drains seat input, fires key repeat from a timer, and builds `view` once per input batch or redraw request. A frame is prepared and presented only when the build or the window size/scale changed. Clipboard reads are asynchronous: paste starts a read and the text is inserted by the next frame. A new paste cancels the previous one. Input-method (text-input v3) batches are applied during frame construction.16+`Frame.SetInputRects` stages the clickable region, reported in `Output.InputRects` only when it changes. Nil restores the whole surface; a non-nil empty slice makes it click-through. `Node.Rect` places a border-box rectangle relative to a Stack's content box without parsing geometry CSS. It applies only to Stack children; debug builds diagnose other parents. The cursor shape follows the CSS `cursor` of the hovered element and is reported in `Output.Cursor`; the client maps it to its own cursor mechanism.
12 17
13-The unexported `runtime` constructs views synchronously. A concurrent or re-entrant build never waits: it requests a redraw, posts a wake token and returns false. The in-progress frame commits first; queued input remains for the next build.18+Clipboard reads are asynchronous: paste starts a read and the text is inserted by the next frame. A new paste cancels the previous one. Input-method batches are applied during frame construction.
14-
15-The unexported UI-loop port `platformInput` accepts logical pointer motion/press/release/axis/leave, interpreted `keyboard.Key` events, focus-in/out and logical viewport resize/scale. `route` hit-tests against the last committed layout (reverse paint order and ancestor clips), resolves the result to a frame identity and wakes the next build only when state changes. `Target` is a committed-child-path helper for tests. A build computes CSS pseudo-state and commits layout, display list and tree together. The headless viewport defaults to 800×600 logical pixels; `Run` sets it from the window. Scale is used by the renderer only and never affects hit testing: Wayland surface coordinates are already logical. A seat input-queue overflow is delivered as a reset (pointer leave, all buttons released, keyboard focus-out). `Run` installs a text engine from system fonts; headless tests install one explicitly, and without one text commands have no measured glyph runs.
16-
17-## Example
18 19
19-`examples/demo` is the product example with an embedded stylesheet:20+The unexported `runtime` constructs views synchronously. A concurrent or re-entrant build never waits: it requests a redraw, posts a wake token and returns false. The in-progress frame commits first; queued input remains for the next build.
20-
21-```sh
22-GOWORK=off go run ./examples/demo
23-```
24-
25-`--static --frames N` renders N frames and exits (used by the visual harness).
26 21
27-Tab traverses visible, enabled controls in document order and wraps; Shift+Tab reverses. Keyboard focus sets `:focus-visible`; pointer focus does not. Enter activates focused buttons and checkboxes on initial key press, ignoring repeats. Space sets `:active` on initial press and activates on release only if focus is unchanged; focus loss/change or Escape cancels it, and repeats are ignored. Disabled controls cannot focus or activate. When a focused control is removed or disabled, focus falls back to **none** (the next Tab starts at the first available control). The first pressed pointer button owns capture until its matching release, even outside the target; other button presses/releases cannot steal it or change focus. Only primary-button release on the captured control activates it. Pointer leave clears hover but preserves capture until matching release (an out-of-surface release cannot activate); removal or platform focus loss cancels capture. Scroll wheel deltas apply to the nearest scrollable ancestor and clamp to measured content bounds; offsets are discarded when that identity is no longer a laid-out scroll container. Unchanged state does not redraw. Control callbacks run during frame construction; input, resize and surface hooks run on the session owner at their documented lifecycle points.22+The unexported input port `platformInput` accepts logical pointer motion/press/release/axis/leave, key events, focus-in/out and logical viewport resize/scale. `route` hit-tests against the last committed layout (reverse paint order and ancestor clips), resolves the result to a frame identity and wakes the next build only when state changes. `Target` is a committed-child-path helper for tests. A build computes CSS pseudo-state and commits layout, display list and tree together. Scale is used by the renderer only and never affects hit testing: surface coordinates are already logical. `NewRenderer` installs a text engine from system fonts; headless tests install one explicitly, and without one text commands have no measured glyph runs.
28 23
29-Offline keyboard tests require libxkbcommon.so.0 and a usable XKB data installation; a host without those prerequisites may opt out with `NEFERGUI_SKIP_XKB=1` (other test failures still fail). The valid-FD test fixture is a French Wayland keymap generated with `xkbcli compile-keymap --layout fr --output-format 1` from libxkbcommon 1.13.2; SHA-256 `f6bcde4e4e03293d05c9426d08cdc2e9a78e7b8faef39dc97c5d267ad7355607`.24+Tab traverses visible, enabled controls in document order and wraps; Shift+Tab reverses. Keyboard focus sets `:focus-visible`; pointer focus does not. Enter activates focused buttons and checkboxes on initial key press, ignoring repeats. Space sets `:active` on initial press and activates on release only if focus is unchanged; focus loss/change or Escape cancels it, and repeats are ignored. Disabled controls cannot focus or activate. When a focused control is removed or disabled, focus falls back to **none** (the next Tab starts at the first available control). The first pressed pointer button owns capture until its matching release, even outside the target; other button presses/releases cannot steal it or change focus. Only primary-button release on the captured control activates it. Pointer leave clears hover but preserves capture until matching release (an out-of-surface release cannot activate); removal or platform focus loss cancels capture. Scroll wheel deltas apply to the nearest scrollable ancestor and clamp to measured content bounds; offsets are discarded when that identity is no longer a laid-out scroll container. Unchanged state does not redraw. Control callbacks run during frame construction; the Renderer calls no application code outside `Render`.
30 25
31 Compile with `-tags nefergui_debug` to report duplicate sibling keys, changed unkeyed child types and stale Node use (stale use panics). Without this tag, stale handles are inert. Do not retain a Node across frames. `ID` is only for CSS, while `Key` identifies an element within its parent. Unkeyed children are identified by their position and type.26 Compile with `-tags nefergui_debug` to report duplicate sibling keys, changed unkeyed child types and stale Node use (stale use panics). Without this tag, stale handles are inert. Do not retain a Node across frames. `ID` is only for CSS, while `Key` identifies an element within its parent. Unkeyed children are identified by their position and type.
32 27
M docs/troubleshooting.md +2 -3
@@ -1,10 +1,9 @@
11 # Troubleshooting
22
3-- No display: `nefergui: open platform: ...` wraps the Wayland connection error. Check `WAYLAND_DISPLAY` and `XDG_RUNTIME_DIR`, and that a Wayland compositor is running.
4-- Missing protocol: `Wayland capability <interface>: ...` identifies the required compositor global. NeferGUI needs linux-dmabuf and linux-drm-syncobj; switch to a compositor offering both.
3+- Wayland connection and missing globals are reported by the application's Wayland client, not by NeferGUI. The compositor must offer linux-dmabuf and linux-drm-syncobj.
54 - No suitable GPU: `no Vulkan physical devices; need DRM main_device ...`, `no Vulkan DRM device matches linux-dmabuf main_device ...`, or `matching Vulkan device missing ...` means the Vulkan driver cannot serve the compositor's DRM device. `no one-plane exportable B8G8R8A8 modifier shared with compositor ...` means they have no compatible modifier. Check driver and compositor support; there is no cross-GPU fallback.
65 - Fonts: `nefergui: fonts: no usable fonts: ...` or `nefergui: no usable fonts` means system font discovery failed. Install usable TrueType/OpenType system fonts.
76 - `NEFERGUI_VULKAN_VALIDATION=1` enables Vulkan validation; an unavailable validation layer reports `validation layer VK_LAYER_KHRONOS_validation unavailable`. Install the validation layer or unset the variable. Broken implicit Vulkan layers can prevent startup: use `VK_LOADER_LAYERS_DISABLE=<layer>` to disable the offending layer for diagnosis.
8-- `NEFERGUI_DEBUG_DIR` writes diagnostic artifacts, including `frame-timings.jsonl` and `buffer-lifecycle.jsonl`; choose a writable directory. Build with `-tags nefergui_debug` for identity diagnostics (duplicate keys, unkeyed type changes, stale handles).
7+- Build with `-tags nefergui_debug` for identity diagnostics (duplicate keys, unkeyed type changes, stale handles).
98 - To regenerate committed SPIR-V, install `glslc` and run `go generate ./...`; consumers do not need `glslc`.
109 - If a requested font cannot be opened or parsed, text selection skips that face and tries the next matching face. Inspect catalog diagnostics for the failed path; system font discovery uses XDG font directories and does not require fontconfig.
@@ -1,10 +1,9 @@
1 # Troubleshooting1 # Troubleshooting
2 2
3-- No display: `nefergui: open platform: ...` wraps the Wayland connection error. Check `WAYLAND_DISPLAY` and `XDG_RUNTIME_DIR`, and that a Wayland compositor is running.3+- Wayland connection and missing globals are reported by the application's Wayland client, not by NeferGUI. The compositor must offer linux-dmabuf and linux-drm-syncobj.
4-- Missing protocol: `Wayland capability <interface>: ...` identifies the required compositor global. NeferGUI needs linux-dmabuf and linux-drm-syncobj; switch to a compositor offering both.
5 - No suitable GPU: `no Vulkan physical devices; need DRM main_device ...`, `no Vulkan DRM device matches linux-dmabuf main_device ...`, or `matching Vulkan device missing ...` means the Vulkan driver cannot serve the compositor's DRM device. `no one-plane exportable B8G8R8A8 modifier shared with compositor ...` means they have no compatible modifier. Check driver and compositor support; there is no cross-GPU fallback.4 - No suitable GPU: `no Vulkan physical devices; need DRM main_device ...`, `no Vulkan DRM device matches linux-dmabuf main_device ...`, or `matching Vulkan device missing ...` means the Vulkan driver cannot serve the compositor's DRM device. `no one-plane exportable B8G8R8A8 modifier shared with compositor ...` means they have no compatible modifier. Check driver and compositor support; there is no cross-GPU fallback.
6 - Fonts: `nefergui: fonts: no usable fonts: ...` or `nefergui: no usable fonts` means system font discovery failed. Install usable TrueType/OpenType system fonts.5 - Fonts: `nefergui: fonts: no usable fonts: ...` or `nefergui: no usable fonts` means system font discovery failed. Install usable TrueType/OpenType system fonts.
7 - `NEFERGUI_VULKAN_VALIDATION=1` enables Vulkan validation; an unavailable validation layer reports `validation layer VK_LAYER_KHRONOS_validation unavailable`. Install the validation layer or unset the variable. Broken implicit Vulkan layers can prevent startup: use `VK_LOADER_LAYERS_DISABLE=<layer>` to disable the offending layer for diagnosis.6 - `NEFERGUI_VULKAN_VALIDATION=1` enables Vulkan validation; an unavailable validation layer reports `validation layer VK_LAYER_KHRONOS_validation unavailable`. Install the validation layer or unset the variable. Broken implicit Vulkan layers can prevent startup: use `VK_LOADER_LAYERS_DISABLE=<layer>` to disable the offending layer for diagnosis.
8-- `NEFERGUI_DEBUG_DIR` writes diagnostic artifacts, including `frame-timings.jsonl` and `buffer-lifecycle.jsonl`; choose a writable directory. Build with `-tags nefergui_debug` for identity diagnostics (duplicate keys, unkeyed type changes, stale handles).7+- Build with `-tags nefergui_debug` for identity diagnostics (duplicate keys, unkeyed type changes, stale handles).
9 - To regenerate committed SPIR-V, install `glslc` and run `go generate ./...`; consumers do not need `glslc`.8 - To regenerate committed SPIR-V, install `glslc` and run `go generate ./...`; consumers do not need `glslc`.
10 - If a requested font cannot be opened or parsed, text selection skips that face and tries the next matching face. Inspect catalog diagnostics for the failed path; system font discovery uses XDG font directories and does not require fontconfig.9 - If a requested font cannot be opened or parsed, text selection skips that face and tries the next matching face. Inspect catalog diagnostics for the failed path; system font discovery uses XDG font directories and does not require fontconfig.
M internal/presentation/vkdevice/readback.go +1 -1
@@ -11,7 +11,7 @@ import (
1111 "github.com/bnema/purego-vulkan/vulkan"
1212 )
1313
14-// Readback is only allocated when NEFERGUI_DEBUG_DIR is set. Its transfer copy is
14+// Readback is only allocated for tests. Its transfer copy is
1515 // recorded after drawing and before the acquire semaphore signals; mapping
1616 // waits for the submission fence, never for compositor release.
1717 type Readback struct {
@@ -11,7 +11,7 @@ import (
11 "github.com/bnema/purego-vulkan/vulkan"11 "github.com/bnema/purego-vulkan/vulkan"
12 )12 )
13 13
14-// Readback is only allocated when NEFERGUI_DEBUG_DIR is set. Its transfer copy is14+// Readback is only allocated for tests. Its transfer copy is
15 // recorded after drawing and before the acquire semaphore signals; mapping15 // recorded after drawing and before the acquire semaphore signals; mapping
16 // waits for the submission fence, never for compositor release.16 // waits for the submission fence, never for compositor release.
17 type Readback struct {17 type Readback struct {

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 12

↑↓ moveenter openesc close