fix(wayland): document workspace frame clipping and HUD placement, check gofmt in make check
- commit
- 56b8a1f118e6cb57a528a78b9b73c454cba72b92
- author
- brice · · 2026-10-01 02:55 UTC
- committer
- brice · · 2026-10-01 03:03 UTC
- parent
- a558f49
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
7 files · +21 -8
M Makefile +5 -2
| @@ -1,4 +1,4 @@ | |||
| 1 | - | .PHONY: bin build test vet race mod-check mocks mocks-check fakes-check arch perf-check check | |
| 1 | + | .PHONY: bin build test vet race fmt-check mod-check mocks mocks-check fakes-check arch perf-check check | |
| 2 | 2 | ||
| 3 | 3 | GOBIN_DIR := $(or $(shell go env GOBIN),$(firstword $(subst :, ,$(shell go env GOPATH)))/bin) | |
| 4 | 4 | MOCKERY ?= $(GOBIN_DIR)/mockery | |
| @@ -16,6 +16,9 @@ vet: | |||
| 16 | 16 | race: | |
| 17 | 17 | # cgo is enabled only for the race test binary. | |
| 18 | 18 | NEFERCAP_REQUIRE_WAYLAND=1 CGO_ENABLED=1 go test -race ./... | |
| 19 | + | # Go sources must be gofmt-clean (lists the offending files and fails otherwise). | |
| 20 | + | fmt-check: | |
| 21 | + | test -z "$$(gofmt -l ./internal ./cmd)" || { gofmt -l ./internal ./cmd >&2; exit 1; } | |
| 19 | 22 | # go.mod and go.sum must be tidy (prints the diff and fails otherwise). | |
| 20 | 23 | mod-check: | |
| 21 | 24 | go mod tidy -diff | |
| @@ -56,4 +59,4 @@ perf-check: | |||
| 56 | 59 | printf '%s\n' "$$out" | grep -qx "$$t" || { echo "perf-check: test $$t not found in $(PERF_PKGS)" >&2; exit 1; }; \ | |
| 57 | 60 | done | |
| 58 | 61 | NEFERCAP_REQUIRE_WAYLAND=1 CGO_ENABLED=0 go test $(PERF_PKGS) -run '$(PERF_RE)' -count=1 | |
| 59 | - | check: mod-check vet test arch fakes-check perf-check | |
| 62 | + | check: fmt-check mod-check vet test arch fakes-check perf-check | |
| @@ -1,4 +1,4 @@ | |||||
| 1 | - | .PHONY: bin build test vet race mod-check mocks mocks-check fakes-check arch perf-check check | 1 | + | .PHONY: bin build test vet race fmt-check mod-check mocks mocks-check fakes-check arch perf-check check |
| 2 | 2 | ||||
| 3 | GOBIN_DIR := $(or $(shell go env GOBIN),$(firstword $(subst :, ,$(shell go env GOPATH)))/bin) | 3 | GOBIN_DIR := $(or $(shell go env GOBIN),$(firstword $(subst :, ,$(shell go env GOPATH)))/bin) | ||
| 4 | MOCKERY ?= $(GOBIN_DIR)/mockery | 4 | MOCKERY ?= $(GOBIN_DIR)/mockery | ||
| @@ -16,6 +16,9 @@ vet: | |||||
| 16 | race: | 16 | race: | ||
| 17 | # cgo is enabled only for the race test binary. | 17 | # cgo is enabled only for the race test binary. | ||
| 18 | NEFERCAP_REQUIRE_WAYLAND=1 CGO_ENABLED=1 go test -race ./... | 18 | NEFERCAP_REQUIRE_WAYLAND=1 CGO_ENABLED=1 go test -race ./... | ||
| 19 | + | # Go sources must be gofmt-clean (lists the offending files and fails otherwise). | |||
| 20 | + | fmt-check: | |||
| 21 | + | test -z "$$(gofmt -l ./internal ./cmd)" || { gofmt -l ./internal ./cmd >&2; exit 1; } | |||
| 19 | # go.mod and go.sum must be tidy (prints the diff and fails otherwise). | 22 | # go.mod and go.sum must be tidy (prints the diff and fails otherwise). | ||
| 20 | mod-check: | 23 | mod-check: | ||
| 21 | go mod tidy -diff | 24 | go mod tidy -diff | ||
| @@ -56,4 +59,4 @@ perf-check: | |||||
| 56 | printf '%s\n' "$$out" | grep -qx "$$t" || { echo "perf-check: test $$t not found in $(PERF_PKGS)" >&2; exit 1; }; \ | 59 | printf '%s\n' "$$out" | grep -qx "$$t" || { echo "perf-check: test $$t not found in $(PERF_PKGS)" >&2; exit 1; }; \ | ||
| 57 | done | 60 | done | ||
| 58 | NEFERCAP_REQUIRE_WAYLAND=1 CGO_ENABLED=0 go test $(PERF_PKGS) -run '$(PERF_RE)' -count=1 | 61 | NEFERCAP_REQUIRE_WAYLAND=1 CGO_ENABLED=0 go test $(PERF_PKGS) -run '$(PERF_RE)' -count=1 | ||
| 59 | - | check: mod-check vet test arch fakes-check perf-check | 62 | + | check: fmt-check mod-check vet test arch fakes-check perf-check |
M README.md +1 -1
| @@ -108,7 +108,7 @@ NeferWL's optional capture extension adds workspace and region sources and keeps | |||
| 108 | 108 | | KDE Plasma | assumed | no | no | no | | |
| 109 | 109 | | GNOME | unsupported | no | no | no | | |
| 110 | 110 | ||
| 111 | - | Workspaces need `ext-workspace-v1`; with NeferWL's extension the selector outlines the workspace frame (a workspace smaller than its monitor), and the HUD sits in it. Without the extension `W` outlines the whole monitor and captures the workspace currently shown, as its monitor. Without the extension a region is cropped from the monitor frame. *Assumed* means it follows from the protocols the compositor advertises; only NeferWL is tested. | |
| 111 | + | Workspaces need `ext-workspace-v1`; with NeferWL's extension the selector outlines the workspace frame (a workspace smaller than its monitor), and the HUD sits in it (placed when the recording starts: if the workspace frame changes during the recording, the video follows it and the HUD stays in place). Without the extension `W` outlines the whole monitor and captures the workspace currently shown, as its monitor. Without the extension a region is cropped from the monitor frame. *Assumed* means it follows from the protocols the compositor advertises; only NeferWL is tested. | |
| 112 | 112 | ||
| 113 | 113 | A compositor may refuse a capture client; the capture then ends with an error. | |
| 114 | 114 | ||
| @@ -108,7 +108,7 @@ NeferWL's optional capture extension adds workspace and region sources and keeps | |||||
| 108 | | KDE Plasma | assumed | no | no | no | | 108 | | KDE Plasma | assumed | no | no | no | | ||
| 109 | | GNOME | unsupported | no | no | no | | 109 | | GNOME | unsupported | no | no | no | | ||
| 110 | 110 | ||||
| 111 | - | Workspaces need `ext-workspace-v1`; with NeferWL's extension the selector outlines the workspace frame (a workspace smaller than its monitor), and the HUD sits in it. Without the extension `W` outlines the whole monitor and captures the workspace currently shown, as its monitor. Without the extension a region is cropped from the monitor frame. *Assumed* means it follows from the protocols the compositor advertises; only NeferWL is tested. | 111 | + | Workspaces need `ext-workspace-v1`; with NeferWL's extension the selector outlines the workspace frame (a workspace smaller than its monitor), and the HUD sits in it (placed when the recording starts: if the workspace frame changes during the recording, the video follows it and the HUD stays in place). Without the extension `W` outlines the whole monitor and captures the workspace currently shown, as its monitor. Without the extension a region is cropped from the monitor frame. *Assumed* means it follows from the protocols the compositor advertises; only NeferWL is tested. |
| 112 | 112 | ||||
| 113 | A compositor may refuse a capture client; the capture then ends with an error. | 113 | A compositor may refuse a capture client; the capture then ends with an error. | ||
| 114 | 114 | ||||
M internal/adapters/indicator/indicator.go +3 -1
| @@ -62,7 +62,9 @@ type Indicator struct { | |||
| 62 | 62 | // Frame optionally is the part of the output that is recorded, in | |
| 63 | 63 | // output-local logical pixels (a workspace smaller than its output): the | |
| 64 | 64 | // HUD sits at the top centre of it. The zero value is the whole output. | |
| 65 | - | // Set it before Run. | |
| 65 | + | // The position is fixed when the recording starts: if the workspace frame | |
| 66 | + | // changes during the recording, the video follows it and the HUD stays in | |
| 67 | + | // place. Set it before Run. | |
| 66 | 68 | Frame ports.Region | |
| 67 | 69 | ||
| 68 | 70 | authorize func(context.Context, nefergui.WaylandSurface) error | |
| @@ -62,7 +62,9 @@ type Indicator struct { | |||||
| 62 | // Frame optionally is the part of the output that is recorded, in | 62 | // Frame optionally is the part of the output that is recorded, in | ||
| 63 | // output-local logical pixels (a workspace smaller than its output): the | 63 | // output-local logical pixels (a workspace smaller than its output): the | ||
| 64 | // HUD sits at the top centre of it. The zero value is the whole output. | 64 | // HUD sits at the top centre of it. The zero value is the whole output. | ||
| 65 | - | // Set it before Run. | 65 | + | // The position is fixed when the recording starts: if the workspace frame |
| 66 | + | // changes during the recording, the video follows it and the HUD stays in | |||
| 67 | + | // place. Set it before Run. | |||
| 66 | Frame ports.Region | 68 | Frame ports.Region | ||
| 67 | 69 | ||||
| 68 | authorize func(context.Context, nefergui.WaylandSurface) error | 70 | authorize func(context.Context, nefergui.WaylandSurface) error | ||
M internal/adapters/wayland/extensions.go +1 -1
| @@ -42,7 +42,7 @@ const ( | |||
| 42 | 42 | reqNeferwlWorkspaceSource = 1 | |
| 43 | 43 | reqNeferwlRegionSource = 2 | |
| 44 | 44 | reqGetWorkspaceFrame = 3 | |
| 45 | - | reqWorkspaceFrameDestroy = 0 | |
| 45 | + | reqWorkspaceFrameDestroy = 0 | |
| 46 | 46 | reqGetExclusion = 1 | |
| 47 | 47 | reqAttachSurface = 2 | |
| 48 | 48 | reqManagerDestroy = 0 | |
| @@ -42,7 +42,7 @@ const ( | |||||
| 42 | reqNeferwlWorkspaceSource = 1 | 42 | reqNeferwlWorkspaceSource = 1 | ||
| 43 | reqNeferwlRegionSource = 2 | 43 | reqNeferwlRegionSource = 2 | ||
| 44 | reqGetWorkspaceFrame = 3 | 44 | reqGetWorkspaceFrame = 3 | ||
| 45 | - | reqWorkspaceFrameDestroy = 0 | 45 | + | reqWorkspaceFrameDestroy = 0 |
| 46 | reqGetExclusion = 1 | 46 | reqGetExclusion = 1 | ||
| 47 | reqAttachSurface = 2 | 47 | reqAttachSurface = 2 | ||
| 48 | reqManagerDestroy = 0 | 48 | reqManagerDestroy = 0 | ||
M internal/adapters/wayland/testserver/neferwl/neferwl-image-capture-v1.xml +4 -2
| @@ -67,7 +67,8 @@ | |||
| 67 | 67 | <description summary="learn the frame of a workspace"> | |
| 68 | 68 | ext-workspace-v1 carries no geometry. The frame object reports the | |
| 69 | 69 | rectangle that a workspace source of the same handle captures. For a | |
| 70 | - | handle already removed, the object never sends frame. | |
| 70 | + | handle already removed, the object never sends frame. It needs no | |
| 71 | + | capture authorization: the frame is layout information, not pixels. | |
| 71 | 72 | </description> | |
| 72 | 73 | <arg name="id" type="new_id" interface="neferwl_workspace_frame_v1"/> | |
| 73 | 74 | <arg name="workspace" type="object" interface="ext_workspace_handle_v1"/> | |
| @@ -89,7 +90,8 @@ | |||
| 89 | 90 | <description summary="the workspace frame"> | |
| 90 | 91 | The frame in output-local logical pixels. Sent when the object is | |
| 91 | 92 | created and again whenever the frame changes. Not sent once the | |
| 92 | - | workspace is removed. | |
| 93 | + | workspace is removed. Not sent while the session is locked; the | |
| 94 | + | current frame is sent after unlock if it changed. | |
| 93 | 95 | </description> | |
| 94 | 96 | <arg name="x" type="int"/> | |
| 95 | 97 | <arg name="y" type="int"/> | |
| @@ -67,7 +67,8 @@ | |||||
| 67 | <description summary="learn the frame of a workspace"> | 67 | <description summary="learn the frame of a workspace"> | ||
| 68 | ext-workspace-v1 carries no geometry. The frame object reports the | 68 | ext-workspace-v1 carries no geometry. The frame object reports the | ||
| 69 | rectangle that a workspace source of the same handle captures. For a | 69 | rectangle that a workspace source of the same handle captures. For a | ||
| 70 | - | handle already removed, the object never sends frame. | 70 | + | handle already removed, the object never sends frame. It needs no |
| 71 | + | capture authorization: the frame is layout information, not pixels. | |||
| 71 | </description> | 72 | </description> | ||
| 72 | <arg name="id" type="new_id" interface="neferwl_workspace_frame_v1"/> | 73 | <arg name="id" type="new_id" interface="neferwl_workspace_frame_v1"/> | ||
| 73 | <arg name="workspace" type="object" interface="ext_workspace_handle_v1"/> | 74 | <arg name="workspace" type="object" interface="ext_workspace_handle_v1"/> | ||
| @@ -89,7 +90,8 @@ | |||||
| 89 | <description summary="the workspace frame"> | 90 | <description summary="the workspace frame"> | ||
| 90 | The frame in output-local logical pixels. Sent when the object is | 91 | The frame in output-local logical pixels. Sent when the object is | ||
| 91 | created and again whenever the frame changes. Not sent once the | 92 | created and again whenever the frame changes. Not sent once the | ||
| 92 | - | workspace is removed. | 93 | + | workspace is removed. Not sent while the session is locked; the |
| 94 | + | current frame is sent after unlock if it changed. | |||
| 93 | </description> | 95 | </description> | ||
| 94 | <arg name="x" type="int"/> | 96 | <arg name="x" type="int"/> | ||
| 95 | <arg name="y" type="int"/> | 97 | <arg name="y" type="int"/> | ||
M internal/adapters/wayland/workspaces.go +4 -1
| @@ -274,7 +274,10 @@ func (s *Source) workspaceList() []ports.Workspace { | |||
| 274 | 274 | ||
| 275 | 275 | // workspaceRegion is where the workspace is on o, in output-local logical | |
| 276 | 276 | // pixels: its frame from NeferWL's extension, clipped to the output, else the | |
| 277 | - | // whole output (ext-workspace carries no geometry). | |
| 277 | + | // whole output (ext-workspace carries no geometry). NeferWL computes the frame | |
| 278 | + | // clamped to the output and centred, so it never lies outside it; the clip and | |
| 279 | + | // the whole-output fallback are safeguards only (core's Picker.clip is the | |
| 280 | + | // single clip applied to the selection). | |
| 278 | 281 | func workspaceRegion(w *workspace, o *output) ports.Region { | |
| 279 | 282 | lw, lh := o.logicalSize() | |
| 280 | 283 | whole := ports.Region{Width: lw, Height: lh} | |
| @@ -274,7 +274,10 @@ func (s *Source) workspaceList() []ports.Workspace { | |||||
| 274 | 274 | ||||
| 275 | // workspaceRegion is where the workspace is on o, in output-local logical | 275 | // workspaceRegion is where the workspace is on o, in output-local logical | ||
| 276 | // pixels: its frame from NeferWL's extension, clipped to the output, else the | 276 | // pixels: its frame from NeferWL's extension, clipped to the output, else the | ||
| 277 | - | // whole output (ext-workspace carries no geometry). | 277 | + | // whole output (ext-workspace carries no geometry). NeferWL computes the frame |
| 278 | + | // clamped to the output and centred, so it never lies outside it; the clip and | |||
| 279 | + | // the whole-output fallback are safeguards only (core's Picker.clip is the | |||
| 280 | + | // single clip applied to the selection). | |||
| 278 | func workspaceRegion(w *workspace, o *output) ports.Region { | 281 | func workspaceRegion(w *workspace, o *output) ports.Region { | ||
| 279 | lw, lh := o.logicalSize() | 282 | lw, lh := o.logicalSize() | ||
| 280 | whole := ports.Region{Width: lw, Height: lh} | 283 | whole := ports.Region{Width: lw, Height: lh} | ||
M internal/app/record_interactive.go +3 -0
| @@ -184,6 +184,9 @@ func recordedFrame(ctx context.Context, source *wayland.Source, t ports.Target) | |||
| 184 | 184 | } | |
| 185 | 185 | list, err := source.Workspaces(ctx) | |
| 186 | 186 | if err != nil { | |
| 187 | + | if ctx.Err() != nil { | |
| 188 | + | return ports.Region{} | |
| 189 | + | } | |
| 187 | 190 | log := logging.For(ctx, "app") | |
| 188 | 191 | log.Warn().Err(err).Msg("workspace frame unavailable: HUD on the whole output") | |
| 189 | 192 | return ports.Region{} | |
| @@ -184,6 +184,9 @@ func recordedFrame(ctx context.Context, source *wayland.Source, t ports.Target) | |||||
| 184 | } | 184 | } | ||
| 185 | list, err := source.Workspaces(ctx) | 185 | list, err := source.Workspaces(ctx) | ||
| 186 | if err != nil { | 186 | if err != nil { | ||
| 187 | + | if ctx.Err() != nil { | |||
| 188 | + | return ports.Region{} | |||
| 189 | + | } | |||
| 187 | log := logging.For(ctx, "app") | 190 | log := logging.For(ctx, "app") | ||
| 188 | log.Warn().Err(err).Msg("workspace frame unavailable: HUD on the whole output") | 191 | log.Warn().Err(err).Msg("workspace frame unavailable: HUD on the whole output") | ||
| 189 | return ports.Region{} | 192 | return ports.Region{} | ||