detached 7e79979

Merge pull request #4 from bnema/ci/release-aur

ci: automated releases to GitHub and the AUR
commit
7e7997921e7dbe2f2e6afb577e655258460268c7
author
bnema · · 2026-10-01 03:15 UTC
committer
GitHub · · 2026-10-01 03:15 UTC
merge of
a558f49 + a40c85a · diff against the first parent
signature
GPG UnverifiedB5690EEEBB952194 The key is unknown
refs
tag: v0.2.1
16 files · +537 -9
A .github/workflows/ci.yml +41 -0
@@ -0,0 +1,41 @@
1+name: CI
2+on:
3+ push:
4+ branches: [main]
5+ pull_request:
6+ # The release workflow runs these checks before it publishes.
7+ workflow_call:
8+concurrency:
9+ group: ci-${{ github.ref }}
10+ cancel-in-progress: true
11+jobs:
12+ # Arch matches the development system: libwayland and Go versions change
13+ # protocol behavior.
14+ check:
15+ runs-on: ubuntu-latest
16+ container:
17+ image: archlinux:latest
18+ # An init process reaps killed children, as on a real session.
19+ options: --init
20+ env:
21+ XDG_RUNTIME_DIR: /tmp/xdg-runtime
22+ steps:
23+ # wayland: libwayland-server for the in-process test compositor
24+ # (NEFERCAP_REQUIRE_WAYLAND=1 fails without it); libxkbcommon is loaded
25+ # at runtime through purego; ffmpeg (libx264) runs the encoder tests;
26+ # gcc is for the race detector.
27+ - run: >-
28+ pacman -Syu --noconfirm --needed
29+ git go make gcc wayland libxkbcommon ffmpeg
30+ - uses: actions/checkout@v4
31+ - run: |
32+ git config --global --add safe.directory "$GITHUB_WORKSPACE"
33+ install -d -m 700 "$XDG_RUNTIME_DIR"
34+ - run: |
35+ go install github.com/bnema/hexcheck/cmd/hexcheck@v0.2.0
36+ go install github.com/vektra/mockery/v3@v3.8.0
37+ go install honnef.co/go/tools/cmd/staticcheck@latest
38+ - run: make check
39+ - run: make race
40+ - run: make mocks-check
41+ - run: ~/go/bin/staticcheck ./...
@@ -0,0 +1,41 @@
1+name: CI
2+on:
3+ push:
4+ branches: [main]
5+ pull_request:
6+ # The release workflow runs these checks before it publishes.
7+ workflow_call:
8+concurrency:
9+ group: ci-${{ github.ref }}
10+ cancel-in-progress: true
11+jobs:
12+ # Arch matches the development system: libwayland and Go versions change
13+ # protocol behavior.
14+ check:
15+ runs-on: ubuntu-latest
16+ container:
17+ image: archlinux:latest
18+ # An init process reaps killed children, as on a real session.
19+ options: --init
20+ env:
21+ XDG_RUNTIME_DIR: /tmp/xdg-runtime
22+ steps:
23+ # wayland: libwayland-server for the in-process test compositor
24+ # (NEFERCAP_REQUIRE_WAYLAND=1 fails without it); libxkbcommon is loaded
25+ # at runtime through purego; ffmpeg (libx264) runs the encoder tests;
26+ # gcc is for the race detector.
27+ - run: >-
28+ pacman -Syu --noconfirm --needed
29+ git go make gcc wayland libxkbcommon ffmpeg
30+ - uses: actions/checkout@v4
31+ - run: |
32+ git config --global --add safe.directory "$GITHUB_WORKSPACE"
33+ install -d -m 700 "$XDG_RUNTIME_DIR"
34+ - run: |
35+ go install github.com/bnema/hexcheck/cmd/hexcheck@v0.2.0
36+ go install github.com/vektra/mockery/v3@v3.8.0
37+ go install honnef.co/go/tools/cmd/staticcheck@latest
38+ - run: make check
39+ - run: make race
40+ - run: make mocks-check
41+ - run: ~/go/bin/staticcheck ./...
A .github/workflows/release.yml +93 -0
@@ -0,0 +1,93 @@
1+name: Release
2+on:
3+ push:
4+ tags: ['v*']
5+permissions:
6+ contents: read
7+concurrency:
8+ group: release-${{ github.ref }}
9+ cancel-in-progress: false
10+jobs:
11+ check:
12+ uses: ./.github/workflows/ci.yml
13+
14+ release:
15+ needs: check
16+ runs-on: ubuntu-latest
17+ permissions:
18+ contents: write # create the release
19+ id-token: write # sign the provenance attestation
20+ attestations: write
21+ steps:
22+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
23+ with:
24+ fetch-depth: 0
25+ persist-credentials: false
26+ - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
27+ with:
28+ go-version-file: go.mod
29+ - uses: goreleaser/goreleaser-action@f06c13b6b1a9625abc9e6e439d9c05a8f2190e94 # v7.2.3
30+ with:
31+ distribution: goreleaser
32+ version: '~> v2'
33+ args: release --clean
34+ env:
35+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
36+ - uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2
37+ with:
38+ subject-path: |
39+ dist/*.tar.gz
40+ dist/checksums.txt
41+ - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
42+ with:
43+ name: aur-bin
44+ path: dist/aur/nefercap-bin.pkgbuild
45+ if-no-files-found: error
46+
47+ # makepkg must run on Arch as a non-root user. publish.sh builds each
48+ # package, verifies every source checksum, then pushes it.
49+ aur:
50+ needs: release
51+ runs-on: ubuntu-latest
52+ container:
53+ image: archlinux:latest
54+ strategy:
55+ fail-fast: false
56+ matrix:
57+ pkg: [nefercap-bin, nefercap-git]
58+ # Pre-releases (v0.2.0-rc1) stay off the AUR: pacman sorts 0.2.0.rc1
59+ # after 0.2.0, so the final release would not look like an upgrade.
60+ env:
61+ SKIP: ${{ contains(github.ref_name, '-') }}
62+ steps:
63+ - run: >-
64+ pacman -Syu --noconfirm --needed
65+ base-devel git openssh sudo
66+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
67+ with:
68+ persist-credentials: false
69+ - if: matrix.pkg == 'nefercap-bin' && env.SKIP != 'true'
70+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
71+ with:
72+ name: aur-bin
73+ path: aur-bin
74+ - name: Create the build user
75+ run: |
76+ useradd -m builder
77+ echo 'builder ALL=(root) NOPASSWD: /usr/bin/pacman' >/etc/sudoers.d/builder
78+ - name: Build, verify and publish ${{ matrix.pkg }}
79+ if: env.SKIP != 'true'
80+ env:
81+ AUR_SSH_PRIVATE_KEY: ${{ secrets.AUR_SSH_PRIVATE_KEY }}
82+ AUR_USERNAME: ${{ secrets.AUR_USERNAME }}
83+ AUR_EMAIL: ${{ secrets.AUR_EMAIL }}
84+ PKG: ${{ matrix.pkg }}
85+ run: |
86+ if [ "$PKG" = nefercap-bin ]; then
87+ pkgbuild=aur-bin/nefercap-bin.pkgbuild
88+ else
89+ pkgbuild=packaging/aur/nefercap-git/PKGBUILD
90+ fi
91+ chmod -R a+rX "$GITHUB_WORKSPACE"
92+ sudo -u builder --preserve-env=AUR_SSH_PRIVATE_KEY,AUR_USERNAME,AUR_EMAIL \
93+ packaging/aur/publish.sh "$PKG" "$pkgbuild"
@@ -0,0 +1,93 @@
1+name: Release
2+on:
3+ push:
4+ tags: ['v*']
5+permissions:
6+ contents: read
7+concurrency:
8+ group: release-${{ github.ref }}
9+ cancel-in-progress: false
10+jobs:
11+ check:
12+ uses: ./.github/workflows/ci.yml
13+
14+ release:
15+ needs: check
16+ runs-on: ubuntu-latest
17+ permissions:
18+ contents: write # create the release
19+ id-token: write # sign the provenance attestation
20+ attestations: write
21+ steps:
22+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
23+ with:
24+ fetch-depth: 0
25+ persist-credentials: false
26+ - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
27+ with:
28+ go-version-file: go.mod
29+ - uses: goreleaser/goreleaser-action@f06c13b6b1a9625abc9e6e439d9c05a8f2190e94 # v7.2.3
30+ with:
31+ distribution: goreleaser
32+ version: '~> v2'
33+ args: release --clean
34+ env:
35+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
36+ - uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2
37+ with:
38+ subject-path: |
39+ dist/*.tar.gz
40+ dist/checksums.txt
41+ - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
42+ with:
43+ name: aur-bin
44+ path: dist/aur/nefercap-bin.pkgbuild
45+ if-no-files-found: error
46+
47+ # makepkg must run on Arch as a non-root user. publish.sh builds each
48+ # package, verifies every source checksum, then pushes it.
49+ aur:
50+ needs: release
51+ runs-on: ubuntu-latest
52+ container:
53+ image: archlinux:latest
54+ strategy:
55+ fail-fast: false
56+ matrix:
57+ pkg: [nefercap-bin, nefercap-git]
58+ # Pre-releases (v0.2.0-rc1) stay off the AUR: pacman sorts 0.2.0.rc1
59+ # after 0.2.0, so the final release would not look like an upgrade.
60+ env:
61+ SKIP: ${{ contains(github.ref_name, '-') }}
62+ steps:
63+ - run: >-
64+ pacman -Syu --noconfirm --needed
65+ base-devel git openssh sudo
66+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
67+ with:
68+ persist-credentials: false
69+ - if: matrix.pkg == 'nefercap-bin' && env.SKIP != 'true'
70+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
71+ with:
72+ name: aur-bin
73+ path: aur-bin
74+ - name: Create the build user
75+ run: |
76+ useradd -m builder
77+ echo 'builder ALL=(root) NOPASSWD: /usr/bin/pacman' >/etc/sudoers.d/builder
78+ - name: Build, verify and publish ${{ matrix.pkg }}
79+ if: env.SKIP != 'true'
80+ env:
81+ AUR_SSH_PRIVATE_KEY: ${{ secrets.AUR_SSH_PRIVATE_KEY }}
82+ AUR_USERNAME: ${{ secrets.AUR_USERNAME }}
83+ AUR_EMAIL: ${{ secrets.AUR_EMAIL }}
84+ PKG: ${{ matrix.pkg }}
85+ run: |
86+ if [ "$PKG" = nefercap-bin ]; then
87+ pkgbuild=aur-bin/nefercap-bin.pkgbuild
88+ else
89+ pkgbuild=packaging/aur/nefercap-git/PKGBUILD
90+ fi
91+ chmod -R a+rX "$GITHUB_WORKSPACE"
92+ sudo -u builder --preserve-env=AUR_SSH_PRIVATE_KEY,AUR_USERNAME,AUR_EMAIL \
93+ packaging/aur/publish.sh "$PKG" "$pkgbuild"
A .goreleaser.yaml +136 -0
@@ -0,0 +1,136 @@
1+# Builds the GitHub release for a pushed v* tag (.github/workflows/release.yml)
2+# and generates the nefercap-bin PKGBUILD. The workflow verifies that PKGBUILD
3+# with makepkg before it pushes it to the AUR, so GoReleaser never uploads it.
4+version: 2
5+
6+project_name: nefercap
7+
8+before:
9+ hooks:
10+ - go mod verify
11+
12+builds:
13+ - id: nefercap
14+ main: ./cmd/nefercap
15+ binary: nefercap
16+ env:
17+ - CGO_ENABLED=0
18+ goos: [linux]
19+ goarch: [amd64, arm64]
20+ flags:
21+ - -trimpath
22+ - -buildmode=pie
23+ ldflags:
24+ - -s -w -X main.version={{ .Version }}
25+ # Same inputs give the same binary.
26+ mod_timestamp: "{{ .CommitTimestamp }}"
27+
28+archives:
29+ - id: nefercap
30+ ids: [nefercap]
31+ name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
32+ formats: [tar.gz]
33+ # Everything the PKGBUILD package() installs, at the paths of the repo.
34+ files:
35+ - LICENSE
36+ - README.md
37+
38+checksum:
39+ name_template: checksums.txt
40+ algorithm: sha256
41+
42+snapshot:
43+ version_template: "{{ incpatch .Version }}-next"
44+
45+git:
46+ tag_sort: -version:refname
47+
48+changelog:
49+ sort: asc
50+ use: github
51+ filters:
52+ exclude:
53+ - "^style(\\([^)]+\\))?:"
54+ - "^test(\\([^)]+\\))?:"
55+ groups:
56+ - title: Added
57+ regexp: "^feat(\\([^)]+\\))?!?:"
58+ order: 0
59+ - title: Fixed
60+ regexp: "^fix(\\([^)]+\\))?!?:"
61+ order: 1
62+ - title: Performance
63+ regexp: "^perf(\\([^)]+\\))?!?:"
64+ order: 2
65+ - title: Changed
66+ regexp: "^(refactor|build)(\\([^)]+\\))?!?:"
67+ order: 3
68+ - title: Documentation
69+ regexp: "^docs(\\([^)]+\\))?!?:"
70+ order: 4
71+ - title: Maintenance
72+ regexp: "^(chore|ci)(\\([^)]+\\))?!?:"
73+ order: 5
74+ - title: Other changes
75+ order: 999
76+
77+release:
78+ github:
79+ owner: bnema
80+ name: nefercap
81+ draft: false
82+ prerelease: auto
83+ mode: replace
84+ header: |
85+ ## nefercap {{ .Tag }}
86+
87+ ### Install on Arch Linux
88+
89+ ```sh
90+ paru -S nefercap-bin # this release, pre-built (x86_64, aarch64)
91+ paru -S nefercap-git # latest main, built from source
92+ ```
93+
94+ ### Install the pre-built archive
95+
96+ Linux amd64 and arm64. Replace `amd64` with `arm64` on ARM.
97+
98+ ```sh
99+ gh release download {{ .Tag }} -R bnema/nefercap -p 'nefercap_{{ .Version }}_linux_amd64.tar.gz' -p checksums.txt
100+ sha256sum -c --ignore-missing checksums.txt
101+ gh attestation verify nefercap_{{ .Version }}_linux_amd64.tar.gz -R bnema/nefercap
102+ ```
103+
104+ The archive holds the `nefercap` binary, the README and the license. Install the binary as `/usr/bin/nefercap`: NeferWL allows that path to capture by default. See the [README](https://github.com/bnema/nefercap#readme) for runtime dependencies.
105+ footer: |
106+ ---
107+
108+ **Full changelog**: https://github.com/bnema/nefercap/compare/{{ .PreviousTag }}...{{ .Tag }}
109+
110+aurs:
111+ - name: nefercap-bin
112+ ids: [nefercap]
113+ homepage: https://github.com/bnema/nefercap
114+ description: Screenshots and silent screen recording for Wayland, from one shortcut
115+ maintainers:
116+ - "bnema <b at bnema dot dev>"
117+ license: GPL-3.0-only
118+ # The release workflow verifies and publishes the generated PKGBUILD.
119+ skip_upload: true
120+ provides: [nefercap]
121+ conflicts: [nefercap, nefercap-git]
122+ # Loaded at runtime (purego, no cgo): not seen by makepkg's library scan.
123+ depends:
124+ - glibc
125+ - wayland
126+ - libxkbcommon
127+ - vulkan-icd-loader
128+ - vulkan-driver
129+ optdepends:
130+ - "ffmpeg: screen recording (libx264)"
131+ - "wl-clipboard: copy screenshots to the clipboard"
132+ - "neferwl: hidden-workspace capture, HUD exclusion and capture indicator"
133+ package: |-
134+ install -Dm755 ./nefercap "${pkgdir}/usr/bin/nefercap"
135+ install -Dm644 ./README.md "${pkgdir}/usr/share/doc/nefercap/README.md"
136+ install -Dm644 ./LICENSE "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE"
@@ -0,0 +1,136 @@
1+# Builds the GitHub release for a pushed v* tag (.github/workflows/release.yml)
2+# and generates the nefercap-bin PKGBUILD. The workflow verifies that PKGBUILD
3+# with makepkg before it pushes it to the AUR, so GoReleaser never uploads it.
4+version: 2
5+
6+project_name: nefercap
7+
8+before:
9+ hooks:
10+ - go mod verify
11+
12+builds:
13+ - id: nefercap
14+ main: ./cmd/nefercap
15+ binary: nefercap
16+ env:
17+ - CGO_ENABLED=0
18+ goos: [linux]
19+ goarch: [amd64, arm64]
20+ flags:
21+ - -trimpath
22+ - -buildmode=pie
23+ ldflags:
24+ - -s -w -X main.version={{ .Version }}
25+ # Same inputs give the same binary.
26+ mod_timestamp: "{{ .CommitTimestamp }}"
27+
28+archives:
29+ - id: nefercap
30+ ids: [nefercap]
31+ name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
32+ formats: [tar.gz]
33+ # Everything the PKGBUILD package() installs, at the paths of the repo.
34+ files:
35+ - LICENSE
36+ - README.md
37+
38+checksum:
39+ name_template: checksums.txt
40+ algorithm: sha256
41+
42+snapshot:
43+ version_template: "{{ incpatch .Version }}-next"
44+
45+git:
46+ tag_sort: -version:refname
47+
48+changelog:
49+ sort: asc
50+ use: github
51+ filters:
52+ exclude:
53+ - "^style(\\([^)]+\\))?:"
54+ - "^test(\\([^)]+\\))?:"
55+ groups:
56+ - title: Added
57+ regexp: "^feat(\\([^)]+\\))?!?:"
58+ order: 0
59+ - title: Fixed
60+ regexp: "^fix(\\([^)]+\\))?!?:"
61+ order: 1
62+ - title: Performance
63+ regexp: "^perf(\\([^)]+\\))?!?:"
64+ order: 2
65+ - title: Changed
66+ regexp: "^(refactor|build)(\\([^)]+\\))?!?:"
67+ order: 3
68+ - title: Documentation
69+ regexp: "^docs(\\([^)]+\\))?!?:"
70+ order: 4
71+ - title: Maintenance
72+ regexp: "^(chore|ci)(\\([^)]+\\))?!?:"
73+ order: 5
74+ - title: Other changes
75+ order: 999
76+
77+release:
78+ github:
79+ owner: bnema
80+ name: nefercap
81+ draft: false
82+ prerelease: auto
83+ mode: replace
84+ header: |
85+ ## nefercap {{ .Tag }}
86+
87+ ### Install on Arch Linux
88+
89+ ```sh
90+ paru -S nefercap-bin # this release, pre-built (x86_64, aarch64)
91+ paru -S nefercap-git # latest main, built from source
92+ ```
93+
94+ ### Install the pre-built archive
95+
96+ Linux amd64 and arm64. Replace `amd64` with `arm64` on ARM.
97+
98+ ```sh
99+ gh release download {{ .Tag }} -R bnema/nefercap -p 'nefercap_{{ .Version }}_linux_amd64.tar.gz' -p checksums.txt
100+ sha256sum -c --ignore-missing checksums.txt
101+ gh attestation verify nefercap_{{ .Version }}_linux_amd64.tar.gz -R bnema/nefercap
102+ ```
103+
104+ The archive holds the `nefercap` binary, the README and the license. Install the binary as `/usr/bin/nefercap`: NeferWL allows that path to capture by default. See the [README](https://github.com/bnema/nefercap#readme) for runtime dependencies.
105+ footer: |
106+ ---
107+
108+ **Full changelog**: https://github.com/bnema/nefercap/compare/{{ .PreviousTag }}...{{ .Tag }}
109+
110+aurs:
111+ - name: nefercap-bin
112+ ids: [nefercap]
113+ homepage: https://github.com/bnema/nefercap
114+ description: Screenshots and silent screen recording for Wayland, from one shortcut
115+ maintainers:
116+ - "bnema <b at bnema dot dev>"
117+ license: GPL-3.0-only
118+ # The release workflow verifies and publishes the generated PKGBUILD.
119+ skip_upload: true
120+ provides: [nefercap]
121+ conflicts: [nefercap, nefercap-git]
122+ # Loaded at runtime (purego, no cgo): not seen by makepkg's library scan.
123+ depends:
124+ - glibc
125+ - wayland
126+ - libxkbcommon
127+ - vulkan-icd-loader
128+ - vulkan-driver
129+ optdepends:
130+ - "ffmpeg: screen recording (libx264)"
131+ - "wl-clipboard: copy screenshots to the clipboard"
132+ - "neferwl: hidden-workspace capture, HUD exclusion and capture indicator"
133+ package: |-
134+ install -Dm755 ./nefercap "${pkgdir}/usr/bin/nefercap"
135+ install -Dm644 ./README.md "${pkgdir}/usr/share/doc/nefercap/README.md"
136+ install -Dm644 ./LICENSE "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE"
M Makefile +25 -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 pkg install
22
33 GOBIN_DIR := $(or $(shell go env GOBIN),$(firstword $(subst :, ,$(shell go env GOPATH)))/bin)
44 MOCKERY ?= $(GOBIN_DIR)/mockery
@@ -16,6 +16,9 @@ vet:
1616 race:
1717 # cgo is enabled only for the race test binary.
1818 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; }
1922 # go.mod and go.sum must be tidy (prints the diff and fails otherwise).
2023 mod-check:
2124 go mod tidy -diff
@@ -56,4 +59,24 @@ perf-check:
5659 printf '%s\n' "$$out" | grep -qx "$$t" || { echo "perf-check: test $$t not found in $(PERF_PKGS)" >&2; exit 1; }; \
5760 done
5861 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
63+
64+# Arch package of the committed HEAD (packaging/arch/PKGBUILD). Go modules
65+# come from the module proxy in prepare(); the build itself runs offline.
66+# pacman-ordered version: 0.0.0.r<commits>.g<hash>; a tag replaces 0.0.0.
67+pkg: SHELL := bash
68+pkg: .SHELLFLAGS := -eo pipefail -c
69+pkg:
70+ @test -z "$$(git status --porcelain)" || echo "warning: uncommitted changes are not packaged" >&2
71+ v=$$(t=$$(git describe --tags --abbrev=0 2>/dev/null | sed 's/^v//; s/-/_/g'); \
72+ echo "$${t:-0.0.0}.r$$(git rev-list --count HEAD).g$$(git rev-parse --short HEAD)"); \
73+ d=$$(mktemp -d /tmp/nefercap-pkg.XXXXXX); trap 'rm -rf "$$d"' EXIT; \
74+ git archive --prefix=nefercap-$$v/ -o "$$d/nefercap-$$v.tar.gz" HEAD; \
75+ cp packaging/arch/PKGBUILD "$$d/"; \
76+ cd "$$d" && sed -i "s/^pkgver=.*/pkgver=$$v/; s/^sha256sums=.*/sha256sums=('$$(sha256sum *.tar.gz | cut -d' ' -f1)')/" PKGBUILD; \
77+ makepkg -f --noconfirm; mkdir -p $(CURDIR)/dist; rm -f $(CURDIR)/dist/nefercap-*.pkg.tar.zst; mv *.pkg.tar.zst $(CURDIR)/dist/
78+ @ls dist/*.pkg.tar.zst
79+
80+# Build the package, then install it with pacman (asks for sudo).
81+install: pkg
82+ sudo pacman -U dist/nefercap-*.pkg.tar.zst
@@ -1,4 +1,4 @@
1-.PHONY: bin build test vet race mod-check mocks mocks-check fakes-check arch perf-check check1+.PHONY: bin build test vet race fmt-check mod-check mocks mocks-check fakes-check arch perf-check check pkg install
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)/mockery4 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 -diff24 go mod tidy -diff
@@ -56,4 +59,24 @@ 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 done60 done
58 NEFERCAP_REQUIRE_WAYLAND=1 CGO_ENABLED=0 go test $(PERF_PKGS) -run '$(PERF_RE)' -count=161 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-check62+check: fmt-check mod-check vet test arch fakes-check perf-check
63+
64+# Arch package of the committed HEAD (packaging/arch/PKGBUILD). Go modules
65+# come from the module proxy in prepare(); the build itself runs offline.
66+# pacman-ordered version: 0.0.0.r<commits>.g<hash>; a tag replaces 0.0.0.
67+pkg: SHELL := bash
68+pkg: .SHELLFLAGS := -eo pipefail -c
69+pkg:
70+ @test -z "$$(git status --porcelain)" || echo "warning: uncommitted changes are not packaged" >&2
71+ v=$$(t=$$(git describe --tags --abbrev=0 2>/dev/null | sed 's/^v//; s/-/_/g'); \
72+ echo "$${t:-0.0.0}.r$$(git rev-list --count HEAD).g$$(git rev-parse --short HEAD)"); \
73+ d=$$(mktemp -d /tmp/nefercap-pkg.XXXXXX); trap 'rm -rf "$$d"' EXIT; \
74+ git archive --prefix=nefercap-$$v/ -o "$$d/nefercap-$$v.tar.gz" HEAD; \
75+ cp packaging/arch/PKGBUILD "$$d/"; \
76+ cd "$$d" && sed -i "s/^pkgver=.*/pkgver=$$v/; s/^sha256sums=.*/sha256sums=('$$(sha256sum *.tar.gz | cut -d' ' -f1)')/" PKGBUILD; \
77+ makepkg -f --noconfirm; mkdir -p $(CURDIR)/dist; rm -f $(CURDIR)/dist/nefercap-*.pkg.tar.zst; mv *.pkg.tar.zst $(CURDIR)/dist/
78+ @ls dist/*.pkg.tar.zst
79+
80+# Build the package, then install it with pacman (asks for sudo).
81+install: pkg
82+ sudo pacman -U dist/nefercap-*.pkg.tar.zst
M README.md +13 -2
@@ -76,6 +76,7 @@ nefercap shot # selector, screenshot only
7676 nefercap rec # selector, recording only
7777 nefercap stop # stop the active recording
7878 nefercap status # print the recording state
79+nefercap version # print the version
7980 ```
8081
8182 Scripted commands never open the selector:
@@ -108,7 +109,7 @@ NeferWL's optional capture extension adds workspace and region sources and keeps
108109 | KDE Plasma | assumed | no | no | no |
109110 | GNOME | unsupported | no | no | no |
110111
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.
112+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.
112113
113114 A compositor may refuse a capture client; the capture then ends with an error.
114115
@@ -116,11 +117,21 @@ Output is SDR, and video is silent H.264 in fragmented MP4. Audio, pause and cur
116117
117118 ## Install
118119
120+On Arch Linux, install from the AUR:
121+
122+```sh
123+paru -S nefercap-bin # latest release, pre-built
124+paru -S nefercap-git # latest main, built from source
125+```
126+
127+Pre-built archives are on the [releases page](https://github.com/bnema/nefercap/releases). To build from a checkout:
128+
119129 ```sh
120130 make bin # builds bin/nefercap
131+make pkg # builds an Arch package of HEAD into dist/
121132 ```
122133
123-Install it as `/usr/bin/nefercap`: NeferWL allows that path to capture by default. Elsewhere (for example `~/.local/bin`, or `go run`), NeferWL refuses capture until root lists the path in `/etc/neferwl/capture-allow`.
134+The packages install `/usr/bin/nefercap`: NeferWL allows that path to capture by default. Elsewhere (for example `~/.local/bin`, or `go run`), NeferWL refuses capture until root lists the path in `/etc/neferwl/capture-allow`.
124135
125136 ## Development
126137
@@ -76,6 +76,7 @@ nefercap shot # selector, screenshot only
76 nefercap rec # selector, recording only76 nefercap rec # selector, recording only
77 nefercap stop # stop the active recording77 nefercap stop # stop the active recording
78 nefercap status # print the recording state78 nefercap status # print the recording state
79+nefercap version # print the version
79 ```80 ```
80 81
81 Scripted commands never open the selector:82 Scripted commands never open the selector:
@@ -108,7 +109,7 @@ NeferWL's optional capture extension adds workspace and region sources and keeps
108 | KDE Plasma | assumed | no | no | no |109 | KDE Plasma | assumed | no | no | no |
109 | GNOME | unsupported | no | no | no |110 | GNOME | unsupported | no | no | no |
110 111
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.112+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 113
113 A compositor may refuse a capture client; the capture then ends with an error.114 A compositor may refuse a capture client; the capture then ends with an error.
114 115
@@ -116,11 +117,21 @@ Output is SDR, and video is silent H.264 in fragmented MP4. Audio, pause and cur
116 117
117 ## Install118 ## Install
118 119
120+On Arch Linux, install from the AUR:
121+
122+```sh
123+paru -S nefercap-bin # latest release, pre-built
124+paru -S nefercap-git # latest main, built from source
125+```
126+
127+Pre-built archives are on the [releases page](https://github.com/bnema/nefercap/releases). To build from a checkout:
128+
119 ```sh129 ```sh
120 make bin # builds bin/nefercap130 make bin # builds bin/nefercap
131+make pkg # builds an Arch package of HEAD into dist/
121 ```132 ```
122 133
123-Install it as `/usr/bin/nefercap`: NeferWL allows that path to capture by default. Elsewhere (for example `~/.local/bin`, or `go run`), NeferWL refuses capture until root lists the path in `/etc/neferwl/capture-allow`.134+The packages install `/usr/bin/nefercap`: NeferWL allows that path to capture by default. Elsewhere (for example `~/.local/bin`, or `go run`), NeferWL refuses capture until root lists the path in `/etc/neferwl/capture-allow`.
124 135
125 ## Development136 ## Development
126 137
M cmd/nefercap/main.go +18 -0
@@ -5,6 +5,7 @@ import (
55 "errors"
66 "flag"
77 "fmt"
8+ "io"
89 "os"
910 "os/signal"
1011 "syscall"
@@ -14,6 +15,9 @@ import (
1415 "github.com/bnema/nefercap/internal/logging"
1516 )
1617
18+// version is set at build time: -ldflags "-X main.version=1.2.3".
19+var version = "dev"
20+
1721 func main() { os.Exit(run()) }
1822
1923 // Only the invocation's bare signal cancellation is a successful interruption.
@@ -22,7 +26,21 @@ func interrupted(ctx context.Context, err error) bool {
2226 return err == context.Canceled && ctx.Err() != nil
2327 }
2428
29+// printVersion implements `nefercap version`. It runs before flag parsing so
30+// it needs no Wayland connection or config.
31+func printVersion(args []string, stdout, stderr io.Writer) int {
32+ if len(args) > 0 {
33+ fmt.Fprintf(stderr, "unexpected argument %q\n", args[0])
34+ return 2
35+ }
36+ fmt.Fprintln(stdout, version)
37+ return 0
38+}
39+
2540 func run() int {
41+ if len(os.Args) > 1 && os.Args[1] == "version" {
42+ return printVersion(os.Args[2:], os.Stdout, os.Stderr)
43+ }
2644 options, err := cli.Parse(os.Args[1:], os.Stderr)
2745 if errors.Is(err, flag.ErrHelp) {
2846 return 0
@@ -5,6 +5,7 @@ import (
5 "errors"5 "errors"
6 "flag"6 "flag"
7 "fmt"7 "fmt"
8+ "io"
8 "os"9 "os"
9 "os/signal"10 "os/signal"
10 "syscall"11 "syscall"
@@ -14,6 +15,9 @@ import (
14 "github.com/bnema/nefercap/internal/logging"15 "github.com/bnema/nefercap/internal/logging"
15 )16 )
16 17
18+// version is set at build time: -ldflags "-X main.version=1.2.3".
19+var version = "dev"
20+
17 func main() { os.Exit(run()) }21 func main() { os.Exit(run()) }
18 22
19 // Only the invocation's bare signal cancellation is a successful interruption.23 // Only the invocation's bare signal cancellation is a successful interruption.
@@ -22,7 +26,21 @@ func interrupted(ctx context.Context, err error) bool {
22 return err == context.Canceled && ctx.Err() != nil26 return err == context.Canceled && ctx.Err() != nil
23 }27 }
24 28
29+// printVersion implements `nefercap version`. It runs before flag parsing so
30+// it needs no Wayland connection or config.
31+func printVersion(args []string, stdout, stderr io.Writer) int {
32+ if len(args) > 0 {
33+ fmt.Fprintf(stderr, "unexpected argument %q\n", args[0])
34+ return 2
35+ }
36+ fmt.Fprintln(stdout, version)
37+ return 0
38+}
39+
25 func run() int {40 func run() int {
41+ if len(os.Args) > 1 && os.Args[1] == "version" {
42+ return printVersion(os.Args[2:], os.Stdout, os.Stderr)
43+ }
26 options, err := cli.Parse(os.Args[1:], os.Stderr)44 options, err := cli.Parse(os.Args[1:], os.Stderr)
27 if errors.Is(err, flag.ErrHelp) {45 if errors.Is(err, flag.ErrHelp) {
28 return 046 return 0
M cmd/nefercap/main_test.go +34 -0
@@ -1,10 +1,12 @@
11 package main
22
33 import (
4+ "bytes"
45 "context"
56 "errors"
67 "fmt"
78 "os"
9+ "strings"
810 "testing"
911 )
1012
@@ -41,3 +43,35 @@ func TestUsageExitCodes(t *testing.T) {
4143 }
4244 }
4345 }
46+
47+func TestVersion(t *testing.T) {
48+ original := version
49+ defer func() { version = original }()
50+
51+ var out, errOut bytes.Buffer
52+ if code := printVersion(nil, &out, &errOut); code != 0 || out.String() != "dev\n" || errOut.Len() != 0 {
53+ t.Fatalf("default: exit %d, stdout %q, stderr %q", code, out.String(), errOut.String())
54+ }
55+ version = "1.2.3"
56+ out.Reset()
57+ if code := printVersion(nil, &out, &errOut); code != 0 || out.String() != "1.2.3\n" {
58+ t.Fatalf("set: exit %d, stdout %q", code, out.String())
59+ }
60+ out.Reset()
61+ if code := printVersion([]string{"extra"}, &out, &errOut); code != 2 || out.Len() != 0 || !strings.Contains(errOut.String(), `"extra"`) {
62+ t.Fatalf("extra: exit %d, stdout %q, stderr %q", code, out.String(), errOut.String())
63+ }
64+}
65+
66+func TestVersionCommandRuns(t *testing.T) {
67+ original := os.Args
68+ defer func() { os.Args = original }()
69+ os.Args = []string{"nefercap", "version"}
70+ if code := run(); code != 0 {
71+ t.Fatalf("version: exit %d", code)
72+ }
73+ os.Args = []string{"nefercap", "version", "x"}
74+ if code := run(); code != 2 {
75+ t.Fatalf("version x: exit %d, want 2", code)
76+ }
77+}
@@ -1,10 +1,12 @@
1 package main1 package main
2 2
3 import (3 import (
4+ "bytes"
4 "context"5 "context"
5 "errors"6 "errors"
6 "fmt"7 "fmt"
7 "os"8 "os"
9+ "strings"
8 "testing"10 "testing"
9 )11 )
10 12
@@ -41,3 +43,35 @@ func TestUsageExitCodes(t *testing.T) {
41 }43 }
42 }44 }
43 }45 }
46+
47+func TestVersion(t *testing.T) {
48+ original := version
49+ defer func() { version = original }()
50+
51+ var out, errOut bytes.Buffer
52+ if code := printVersion(nil, &out, &errOut); code != 0 || out.String() != "dev\n" || errOut.Len() != 0 {
53+ t.Fatalf("default: exit %d, stdout %q, stderr %q", code, out.String(), errOut.String())
54+ }
55+ version = "1.2.3"
56+ out.Reset()
57+ if code := printVersion(nil, &out, &errOut); code != 0 || out.String() != "1.2.3\n" {
58+ t.Fatalf("set: exit %d, stdout %q", code, out.String())
59+ }
60+ out.Reset()
61+ if code := printVersion([]string{"extra"}, &out, &errOut); code != 2 || out.Len() != 0 || !strings.Contains(errOut.String(), `"extra"`) {
62+ t.Fatalf("extra: exit %d, stdout %q, stderr %q", code, out.String(), errOut.String())
63+ }
64+}
65+
66+func TestVersionCommandRuns(t *testing.T) {
67+ original := os.Args
68+ defer func() { os.Args = original }()
69+ os.Args = []string{"nefercap", "version"}
70+ if code := run(); code != 0 {
71+ t.Fatalf("version: exit %d", code)
72+ }
73+ os.Args = []string{"nefercap", "version", "x"}
74+ if code := run(); code != 2 {
75+ t.Fatalf("version x: exit %d, want 2", code)
76+ }
77+}
M internal/adapters/cli/cli.go +3 -0
@@ -92,6 +92,9 @@ Script commands (never open the selector):
9292 screenshot save a PNG screenshot
9393 record record silent video until interrupted or target video length is reached
9494
95+Other:
96+ version print the nefercap version
97+
9598 Flags:
9699 -clipboard copy PNG with wl-copy; no file unless -file is given (shot, screenshot)
97100 -debug enable debug logging (all commands)
@@ -92,6 +92,9 @@ Script commands (never open the selector):
92 screenshot save a PNG screenshot92 screenshot save a PNG screenshot
93 record record silent video until interrupted or target video length is reached93 record record silent video until interrupted or target video length is reached
94 94
95+Other:
96+ version print the nefercap version
97+
95 Flags:98 Flags:
96 -clipboard copy PNG with wl-copy; no file unless -file is given (shot, screenshot)99 -clipboard copy PNG with wl-copy; no file unless -file is given (shot, screenshot)
97 -debug enable debug logging (all commands)100 -debug enable debug logging (all commands)
M internal/adapters/indicator/indicator.go +3 -1
@@ -62,7 +62,9 @@ type Indicator struct {
6262 // Frame optionally is the part of the output that is recorded, in
6363 // output-local logical pixels (a workspace smaller than its output): the
6464 // 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.
6668 Frame ports.Region
6769
6870 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, in62 // Frame optionally is the part of the output that is recorded, in
63 // output-local logical pixels (a workspace smaller than its output): the63 // 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.Region68 Frame ports.Region
67 69
68 authorize func(context.Context, nefergui.WaylandSurface) error70 authorize func(context.Context, nefergui.WaylandSurface) error
M internal/adapters/wayland/extensions.go +1 -1
@@ -42,7 +42,7 @@ const (
4242 reqNeferwlWorkspaceSource = 1
4343 reqNeferwlRegionSource = 2
4444 reqGetWorkspaceFrame = 3
45- reqWorkspaceFrameDestroy = 0
45+ reqWorkspaceFrameDestroy = 0
4646 reqGetExclusion = 1
4747 reqAttachSurface = 2
4848 reqManagerDestroy = 0
@@ -42,7 +42,7 @@ const (
42 reqNeferwlWorkspaceSource = 142 reqNeferwlWorkspaceSource = 1
43 reqNeferwlRegionSource = 243 reqNeferwlRegionSource = 2
44 reqGetWorkspaceFrame = 344 reqGetWorkspaceFrame = 3
45- reqWorkspaceFrameDestroy = 045+ reqWorkspaceFrameDestroy = 0
46 reqGetExclusion = 146 reqGetExclusion = 1
47 reqAttachSurface = 247 reqAttachSurface = 2
48 reqManagerDestroy = 048 reqManagerDestroy = 0
M internal/adapters/wayland/testserver/neferwl/neferwl-image-capture-v1.xml +4 -2
@@ -67,7 +67,8 @@
6767 <description summary="learn the frame of a workspace">
6868 ext-workspace-v1 carries no geometry. The frame object reports the
6969 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.
7172 </description>
7273 <arg name="id" type="new_id" interface="neferwl_workspace_frame_v1"/>
7374 <arg name="workspace" type="object" interface="ext_workspace_handle_v1"/>
@@ -89,7 +90,8 @@
8990 <description summary="the workspace frame">
9091 The frame in output-local logical pixels. Sent when the object is
9192 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.
9395 </description>
9496 <arg name="x" type="int"/>
9597 <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 the68 ext-workspace-v1 carries no geometry. The frame object reports the
69 rectangle that a workspace source of the same handle captures. For a69 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 is91 The frame in output-local logical pixels. Sent when the object is
91 created and again whenever the frame changes. Not sent once the92 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 {
274274
275275 // workspaceRegion is where the workspace is on o, in output-local logical
276276 // 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).
278281 func workspaceRegion(w *workspace, o *output) ports.Region {
279282 lw, lh := o.logicalSize()
280283 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 logical275 // 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 the276 // 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)
184184 }
185185 list, err := source.Workspaces(ctx)
186186 if err != nil {
187+ if ctx.Err() != nil {
188+ return ports.Region{}
189+ }
187190 log := logging.For(ctx, "app")
188191 log.Warn().Err(err).Msg("workspace frame unavailable: HUD on the whole output")
189192 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{}
A packaging/arch/PKGBUILD +41 -0
@@ -0,0 +1,41 @@
1+# Built by `make pkg` at the repo root, from `git archive HEAD`.
2+pkgname=nefercap
3+pkgver=0.0.0
4+pkgrel=1
5+pkgdesc='Screenshots and silent screen recording for Wayland, from one shortcut'
6+arch=('x86_64' 'aarch64')
7+url='https://github.com/bnema/nefercap'
8+license=('GPL-3.0-only')
9+# Loaded at runtime (purego, no cgo): not seen by makepkg's library scan.
10+depends=('glibc' 'wayland' 'libxkbcommon' 'vulkan-icd-loader' 'vulkan-driver')
11+makedepends=('go>=2:1.27')
12+optdepends=('ffmpeg: screen recording (libx264)'
13+ 'wl-clipboard: copy screenshots to the clipboard'
14+ 'neferwl: hidden-workspace capture, HUD exclusion and capture indicator')
15+source=("$pkgname-$pkgver.tar.gz")
16+sha256sums=('SKIP')
17+
18+prepare() {
19+ cd "$pkgname-$pkgver"
20+ export GOPATH="$srcdir/gopath" GOFLAGS='-mod=readonly -modcacherw' GOWORK=off GOTOOLCHAIN=local
21+ go mod download
22+ go mod vendor
23+}
24+
25+build() {
26+ cd "$pkgname-$pkgver"
27+ export CGO_ENABLED=0 GOFLAGS='-mod=vendor -trimpath -buildvcs=false -buildmode=pie' GOPROXY=off GOWORK=off GOTOOLCHAIN=local
28+ go build -ldflags "-X main.version=$pkgver" -o nefercap ./cmd/nefercap
29+}
30+
31+check() {
32+ cd "$pkgname-$pkgver"
33+ ./nefercap version
34+}
35+
36+package() {
37+ cd "$pkgname-$pkgver"
38+ install -Dm755 nefercap "$pkgdir/usr/bin/nefercap"
39+ install -Dm644 README.md "$pkgdir/usr/share/doc/nefercap/README.md"
40+ install -Dm644 LICENSE "$pkgdir/usr/share/licenses/nefercap/LICENSE"
41+}
@@ -0,0 +1,41 @@
1+# Built by `make pkg` at the repo root, from `git archive HEAD`.
2+pkgname=nefercap
3+pkgver=0.0.0
4+pkgrel=1
5+pkgdesc='Screenshots and silent screen recording for Wayland, from one shortcut'
6+arch=('x86_64' 'aarch64')
7+url='https://github.com/bnema/nefercap'
8+license=('GPL-3.0-only')
9+# Loaded at runtime (purego, no cgo): not seen by makepkg's library scan.
10+depends=('glibc' 'wayland' 'libxkbcommon' 'vulkan-icd-loader' 'vulkan-driver')
11+makedepends=('go>=2:1.27')
12+optdepends=('ffmpeg: screen recording (libx264)'
13+ 'wl-clipboard: copy screenshots to the clipboard'
14+ 'neferwl: hidden-workspace capture, HUD exclusion and capture indicator')
15+source=("$pkgname-$pkgver.tar.gz")
16+sha256sums=('SKIP')
17+
18+prepare() {
19+ cd "$pkgname-$pkgver"
20+ export GOPATH="$srcdir/gopath" GOFLAGS='-mod=readonly -modcacherw' GOWORK=off GOTOOLCHAIN=local
21+ go mod download
22+ go mod vendor
23+}
24+
25+build() {
26+ cd "$pkgname-$pkgver"
27+ export CGO_ENABLED=0 GOFLAGS='-mod=vendor -trimpath -buildvcs=false -buildmode=pie' GOPROXY=off GOWORK=off GOTOOLCHAIN=local
28+ go build -ldflags "-X main.version=$pkgver" -o nefercap ./cmd/nefercap
29+}
30+
31+check() {
32+ cd "$pkgname-$pkgver"
33+ ./nefercap version
34+}
35+
36+package() {
37+ cd "$pkgname-$pkgver"
38+ install -Dm755 nefercap "$pkgdir/usr/bin/nefercap"
39+ install -Dm644 README.md "$pkgdir/usr/share/doc/nefercap/README.md"
40+ install -Dm644 LICENSE "$pkgdir/usr/share/licenses/nefercap/LICENSE"
41+}
A packaging/aur/nefercap-git/PKGBUILD +52 -0
@@ -0,0 +1,52 @@
1+# Maintainer: bnema <b at bnema dot dev>
2+# The release workflow sets pkgver and publishes this file to the AUR on every
3+# release tag, so the AUR version never falls behind the latest tag.
4+pkgname=nefercap-git
5+pkgver=0.0.0.r0.g0000000
6+pkgrel=1
7+pkgdesc='Screenshots and silent screen recording for Wayland, from one shortcut (git version)'
8+arch=('x86_64' 'aarch64')
9+url='https://github.com/bnema/nefercap'
10+license=('GPL-3.0-only')
11+# Loaded at runtime (purego, no cgo): not seen by makepkg's library scan.
12+depends=('glibc' 'wayland' 'libxkbcommon' 'vulkan-icd-loader' 'vulkan-driver')
13+makedepends=('go>=2:1.27' 'git')
14+optdepends=('ffmpeg: screen recording (libx264)'
15+ 'wl-clipboard: copy screenshots to the clipboard'
16+ 'neferwl: hidden-workspace capture, HUD exclusion and capture indicator')
17+provides=('nefercap')
18+conflicts=('nefercap' 'nefercap-bin')
19+source=('git+https://github.com/bnema/nefercap.git')
20+sha256sums=('SKIP')
21+
22+pkgver() {
23+ cd nefercap
24+ # v0.2.0-5-gabc1234 -> 0.2.0.r5.gabc1234. Pre-release tags (v0.3.0-rc1)
25+ # are skipped: pacman sorts 0.3.0.rc1 after 0.3.0.
26+ git describe --long --tags --abbrev=7 --exclude='*-*' | sed 's/^v//;s/\([^-]*-g\)/r\1/;s/-/./g'
27+}
28+
29+prepare() {
30+ cd nefercap
31+ export GOPATH="$srcdir/gopath" GOFLAGS='-mod=readonly -modcacherw' GOTOOLCHAIN=local
32+ go mod download
33+}
34+
35+build() {
36+ cd nefercap
37+ export GOPATH="$srcdir/gopath" CGO_ENABLED=0 GOTOOLCHAIN=local GOPROXY=off
38+ export GOFLAGS='-mod=readonly -modcacherw -trimpath -buildvcs=false -buildmode=pie'
39+ go build -ldflags "-X main.version=$pkgver" -o nefercap ./cmd/nefercap
40+}
41+
42+check() {
43+ cd nefercap
44+ ./nefercap version
45+}
46+
47+package() {
48+ cd nefercap
49+ install -Dm755 nefercap "$pkgdir/usr/bin/nefercap"
50+ install -Dm644 README.md "$pkgdir/usr/share/doc/nefercap/README.md"
51+ install -Dm644 LICENSE "$pkgdir/usr/share/licenses/$pkgname/LICENSE"
52+}
@@ -0,0 +1,52 @@
1+# Maintainer: bnema <b at bnema dot dev>
2+# The release workflow sets pkgver and publishes this file to the AUR on every
3+# release tag, so the AUR version never falls behind the latest tag.
4+pkgname=nefercap-git
5+pkgver=0.0.0.r0.g0000000
6+pkgrel=1
7+pkgdesc='Screenshots and silent screen recording for Wayland, from one shortcut (git version)'
8+arch=('x86_64' 'aarch64')
9+url='https://github.com/bnema/nefercap'
10+license=('GPL-3.0-only')
11+# Loaded at runtime (purego, no cgo): not seen by makepkg's library scan.
12+depends=('glibc' 'wayland' 'libxkbcommon' 'vulkan-icd-loader' 'vulkan-driver')
13+makedepends=('go>=2:1.27' 'git')
14+optdepends=('ffmpeg: screen recording (libx264)'
15+ 'wl-clipboard: copy screenshots to the clipboard'
16+ 'neferwl: hidden-workspace capture, HUD exclusion and capture indicator')
17+provides=('nefercap')
18+conflicts=('nefercap' 'nefercap-bin')
19+source=('git+https://github.com/bnema/nefercap.git')
20+sha256sums=('SKIP')
21+
22+pkgver() {
23+ cd nefercap
24+ # v0.2.0-5-gabc1234 -> 0.2.0.r5.gabc1234. Pre-release tags (v0.3.0-rc1)
25+ # are skipped: pacman sorts 0.3.0.rc1 after 0.3.0.
26+ git describe --long --tags --abbrev=7 --exclude='*-*' | sed 's/^v//;s/\([^-]*-g\)/r\1/;s/-/./g'
27+}
28+
29+prepare() {
30+ cd nefercap
31+ export GOPATH="$srcdir/gopath" GOFLAGS='-mod=readonly -modcacherw' GOTOOLCHAIN=local
32+ go mod download
33+}
34+
35+build() {
36+ cd nefercap
37+ export GOPATH="$srcdir/gopath" CGO_ENABLED=0 GOTOOLCHAIN=local GOPROXY=off
38+ export GOFLAGS='-mod=readonly -modcacherw -trimpath -buildvcs=false -buildmode=pie'
39+ go build -ldflags "-X main.version=$pkgver" -o nefercap ./cmd/nefercap
40+}
41+
42+check() {
43+ cd nefercap
44+ ./nefercap version
45+}
46+
47+package() {
48+ cd nefercap
49+ install -Dm755 nefercap "$pkgdir/usr/bin/nefercap"
50+ install -Dm644 README.md "$pkgdir/usr/share/doc/nefercap/README.md"
51+ install -Dm644 LICENSE "$pkgdir/usr/share/licenses/$pkgname/LICENSE"
52+}
A packaging/aur/publish.sh +66 -0
@@ -0,0 +1,66 @@
1+#!/usr/bin/env bash
2+# Builds an AUR package with makepkg, then pushes its PKGBUILD and .SRCINFO.
3+# makepkg downloads every source and checks its sha256sum, so a PKGBUILD that
4+# does not build, or does not match its release assets, is never published.
5+#
6+# Usage: publish.sh <pkgname> <PKGBUILD>
7+# Env: AUR_SSH_PRIVATE_KEY, AUR_USERNAME, AUR_EMAIL
8+# DRY_RUN=1 builds and prints the files without pushing.
9+# Runs as a non-root user that may run pacman through sudo (makepkg -s).
10+set -euo pipefail
11+CARCH=$(uname -m)
12+
13+pkgname=$1
14+work=$(mktemp -d)
15+trap 'rm -rf "$work"' EXIT
16+cp "$2" "$work/PKGBUILD"
17+cd "$work"
18+
19+makepkg --syncdeps --force --noconfirm
20+# A -git pkgver() rewrites pkgver in PKGBUILD; .SRCINFO must match it.
21+makepkg --printsrcinfo >.SRCINFO
22+pkgver=$(sed -n 's/^\tpkgver = //p' .SRCINFO)
23+
24+# makepkg only fetches the sources of this machine's arch: check the others too.
25+while read -r arch; do
26+ if [[ $arch == "$CARCH" ]] || ! grep -q "^source_$arch=" PKGBUILD; then
27+ continue
28+ fi
29+ { cat /etc/makepkg.conf; echo "CARCH=$arch"; } >"makepkg-$arch.conf"
30+ makepkg --config "makepkg-$arch.conf" --verifysource --noconfirm
31+done < <(sed -n 's/^\tarch = //p' .SRCINFO)
32+
33+# pacman versions use _ where the tag has - (0.2.0-rc1 -> 0.2.0_rc1).
34+got=$("pkg/$pkgname/usr/bin/nefercap" version)
35+if [[ $got != "$pkgver" && $got != "${pkgver//_/-}" ]]; then
36+ echo "packaged nefercap reports version $got, want $pkgver" >&2
37+ exit 1
38+fi
39+
40+if [[ ${DRY_RUN:-} == 1 ]]; then
41+ cat PKGBUILD .SRCINFO
42+ exit 0
43+fi
44+
45+ssh_dir=$HOME/.ssh
46+key=$ssh_dir/aur
47+install -d -m 700 "$ssh_dir"
48+(umask 077 && printf '%s\n' "$AUR_SSH_PRIVATE_KEY" >"$key")
49+# Pinned host key. Fingerprint SHA256:RFzBCUItH9LZS0cKB5UE6ceAYhBD5C8GeOBip8Z11+4
50+# is published on https://aur.archlinux.org.
51+echo 'aur.archlinux.org ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEuBKrPzbawxA/k2g6NcyV5jmqwJ2s+zpgZGZ7tpLIcN' >"$ssh_dir/known_hosts"
52+export GIT_SSH_COMMAND="ssh -i $key -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes -o UserKnownHostsFile=$ssh_dir/known_hosts"
53+
54+git clone "ssh://aur@aur.archlinux.org/$pkgname.git" aur
55+cp PKGBUILD .SRCINFO aur/
56+cd aur
57+git add PKGBUILD .SRCINFO
58+if git diff --cached --quiet; then
59+ echo "$pkgname $pkgver is already on the AUR"
60+ exit 0
61+fi
62+# Commits are signed with the AUR SSH key.
63+git -c user.name="$AUR_USERNAME" -c user.email="$AUR_EMAIL" \
64+ -c gpg.format=ssh -c user.signingkey="$key" \
65+ commit -S -m "Update to $pkgver"
66+git push origin HEAD:master
@@ -0,0 +1,66 @@
1+#!/usr/bin/env bash
2+# Builds an AUR package with makepkg, then pushes its PKGBUILD and .SRCINFO.
3+# makepkg downloads every source and checks its sha256sum, so a PKGBUILD that
4+# does not build, or does not match its release assets, is never published.
5+#
6+# Usage: publish.sh <pkgname> <PKGBUILD>
7+# Env: AUR_SSH_PRIVATE_KEY, AUR_USERNAME, AUR_EMAIL
8+# DRY_RUN=1 builds and prints the files without pushing.
9+# Runs as a non-root user that may run pacman through sudo (makepkg -s).
10+set -euo pipefail
11+CARCH=$(uname -m)
12+
13+pkgname=$1
14+work=$(mktemp -d)
15+trap 'rm -rf "$work"' EXIT
16+cp "$2" "$work/PKGBUILD"
17+cd "$work"
18+
19+makepkg --syncdeps --force --noconfirm
20+# A -git pkgver() rewrites pkgver in PKGBUILD; .SRCINFO must match it.
21+makepkg --printsrcinfo >.SRCINFO
22+pkgver=$(sed -n 's/^\tpkgver = //p' .SRCINFO)
23+
24+# makepkg only fetches the sources of this machine's arch: check the others too.
25+while read -r arch; do
26+ if [[ $arch == "$CARCH" ]] || ! grep -q "^source_$arch=" PKGBUILD; then
27+ continue
28+ fi
29+ { cat /etc/makepkg.conf; echo "CARCH=$arch"; } >"makepkg-$arch.conf"
30+ makepkg --config "makepkg-$arch.conf" --verifysource --noconfirm
31+done < <(sed -n 's/^\tarch = //p' .SRCINFO)
32+
33+# pacman versions use _ where the tag has - (0.2.0-rc1 -> 0.2.0_rc1).
34+got=$("pkg/$pkgname/usr/bin/nefercap" version)
35+if [[ $got != "$pkgver" && $got != "${pkgver//_/-}" ]]; then
36+ echo "packaged nefercap reports version $got, want $pkgver" >&2
37+ exit 1
38+fi
39+
40+if [[ ${DRY_RUN:-} == 1 ]]; then
41+ cat PKGBUILD .SRCINFO
42+ exit 0
43+fi
44+
45+ssh_dir=$HOME/.ssh
46+key=$ssh_dir/aur
47+install -d -m 700 "$ssh_dir"
48+(umask 077 && printf '%s\n' "$AUR_SSH_PRIVATE_KEY" >"$key")
49+# Pinned host key. Fingerprint SHA256:RFzBCUItH9LZS0cKB5UE6ceAYhBD5C8GeOBip8Z11+4
50+# is published on https://aur.archlinux.org.
51+echo 'aur.archlinux.org ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEuBKrPzbawxA/k2g6NcyV5jmqwJ2s+zpgZGZ7tpLIcN' >"$ssh_dir/known_hosts"
52+export GIT_SSH_COMMAND="ssh -i $key -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes -o UserKnownHostsFile=$ssh_dir/known_hosts"
53+
54+git clone "ssh://aur@aur.archlinux.org/$pkgname.git" aur
55+cp PKGBUILD .SRCINFO aur/
56+cd aur
57+git add PKGBUILD .SRCINFO
58+if git diff --cached --quiet; then
59+ echo "$pkgname $pkgver is already on the AUR"
60+ exit 0
61+fi
62+# Commits are signed with the AUR SSH key.
63+git -c user.name="$AUR_USERNAME" -c user.email="$AUR_EMAIL" \
64+ -c gpg.format=ssh -c user.signingkey="$key" \
65+ commit -S -m "Update to $pkgver"
66+git push origin HEAD:master

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 2

Tags 9

↑↓ moveenter openesc close