detached 8d7d700

fix(packaging): clean-tree dist, wayland and dbus deps, no personal paths, session export before clients

commit
8d7d70018efa2d4cbc226f078355138f21e395ee
author
brice · · 2026-09-26 06:01 UTC
parent
7abacfb
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
8 files · +139 -35
M Makefile +28 -18
@@ -47,25 +47,35 @@ arch:
4747 check: vet test arch fakes-check
4848
4949 # Self-contained source tarball for packaging (packaging/arch/PKGBUILD): the
50-# sibling purego-* modules are vendored, so the package builds offline.
51-# pacman-ordered: 0.0.0.r<commits>.g<hash>, a tag replaces 0.0.0 once there is one.
52-VERSION ?= $(shell t=$$(git describe --tags --abbrev=0 2>/dev/null | sed 's/^v//'); \
53- echo "$${t:-0.0.0}.r$$(git rev-list --count HEAD).g$$(git rev-parse --short HEAD)")
54-DIST := dist/nefertty-$(VERSION)
50+# sibling purego-* modules are vendored, so the package builds offline. It
51+# holds the working trees; the version ends in .dirty when one is not clean.
5552 SIBLINGS := github.com/bnema/purego-libwayland github.com/bnema/purego-vulkan
53+SIBLING_DIRS := $(shell go list -m -f '{{.Dir}}' $(SIBLINGS))
54+dirty = $(shell for d in . $(SIBLING_DIRS); do git -C $$d diff --quiet HEAD && \
55+ test -z "$$(git -C $$d ls-files -o --exclude-standard)" || { echo .dirty; break; }; done)
56+# pacman-ordered: 0.0.0.r<commits>.g<hash>; a tag replaces 0.0.0.
57+VERSION := $(or $(VERSION),$(shell t=$$(git describe --tags --abbrev=0 2>/dev/null | sed 's/^v//; s/-/_/g'); \
58+ echo "$${t:-0.0.0}.r$$(git rev-list --count HEAD).g$$(git rev-parse --short HEAD)")$(dirty))
59+DIST := dist/nefertty-$(VERSION)
60+dist: SHELL := bash
61+dist: .SHELLFLAGS := -eo pipefail -c
5662 dist:
57- @rm -rf $(DIST) && mkdir -p $(DIST)
58- git archive $$(git stash create || echo HEAD) | tar -x -C $(DIST)
59- cd $(DIST) && go mod edit $$(cd $(CURDIR) && go list -m -f '-replace={{.Path}}={{.Dir}}' $(SIBLINGS)) && go mod vendor && \
60- for m in $(SIBLINGS); do d=$$(cd $(CURDIR) && go list -m -f '{{.Dir}}' $$m); \
61- sed -i "s#=> $$d\$$#=> ../$${m##*/}#" go.mod vendor/modules.txt; done && \
62- ! grep -rq "$(HOME)" go.mod vendor/modules.txt && echo '$(VERSION)' > VERSION
63- tar -C dist -czf $(DIST).tar.gz nefertty-$(VERSION)
64- @rm -rf $(DIST) && echo $(DIST).tar.gz
63+ @test -n "$(SIBLING_DIRS)" || { echo "sibling modules not found: $(SIBLINGS)" >&2; exit 1; }
64+ rm -rf $(DIST) && mkdir -p $(DIST)
65+ git ls-files -co --exclude-standard -z | tar --null -T - -c | tar -x -C $(DIST)
66+ cd $(DIST) && go mod edit $(foreach m,$(SIBLINGS),-replace=$(m)=$(shell go list -m -f '{{.Dir}}' $(m))) && go mod vendor
67+ cd $(DIST) && $(foreach m,$(SIBLINGS),sed -i 's#=> $(shell go list -m -f '{{.Dir}}' $(m))$$#=> ../$(notdir $(m))#' go.mod vendor/modules.txt &&) true
68+ @! grep -rqF "$(HOME)" $(DIST) || { echo "home path leaked into $(DIST)" >&2; exit 1; }
69+ echo '$(VERSION)' > $(DIST)/VERSION
70+ tar --owner=0 --group=0 --numeric-owner --sort=name --mtime=@$$(git log -1 --format=%ct) -C dist -czf $(DIST).tar.gz nefertty-$(VERSION)
71+ rm -rf $(DIST)
72+ @echo $(DIST).tar.gz
6573
66-# Arch package from the dist tarball; install it with `sudo pacman -U` on
67-# the printed file.
74+# Arch package from the dist tarball, built outside the home directory so
75+# no path of it lands in the package metadata. Install it with
76+# `sudo pacman -U` on the printed file.
6877 pkg: dist
69- rm -rf dist/pkg && mkdir -p dist/pkg && cp packaging/arch/PKGBUILD $(DIST).tar.gz dist/pkg/
70- cd dist/pkg && sed -i "s/^pkgver=.*/pkgver=$(VERSION)/" PKGBUILD && makepkg -f --noconfirm
71- @ls dist/pkg/*.pkg.tar.zst
78+ d=$$(mktemp -d /tmp/nefertty-pkg.XXXXXX) && cp packaging/arch/PKGBUILD $(DIST).tar.gz $$d/ && \
79+ cd $$d && sed -i "s/^pkgver=.*/pkgver=$(VERSION)/; s/^sha256sums=.*/sha256sums=('$$(sha256sum *.tar.gz | cut -d' ' -f1)')/" PKGBUILD && \
80+ makepkg -f --noconfirm && mv *.pkg.tar.zst $(CURDIR)/dist/ && cd / && rm -rf $$d
81+ @ls dist/*.pkg.tar.zst
@@ -47,25 +47,35 @@ arch:
47 check: vet test arch fakes-check47 check: vet test arch fakes-check
48 48
49 # Self-contained source tarball for packaging (packaging/arch/PKGBUILD): the49 # Self-contained source tarball for packaging (packaging/arch/PKGBUILD): the
50-# sibling purego-* modules are vendored, so the package builds offline.50+# sibling purego-* modules are vendored, so the package builds offline. It
51-# pacman-ordered: 0.0.0.r<commits>.g<hash>, a tag replaces 0.0.0 once there is one.51+# holds the working trees; the version ends in .dirty when one is not clean.
52-VERSION ?= $(shell t=$$(git describe --tags --abbrev=0 2>/dev/null | sed 's/^v//'); \
53- echo "$${t:-0.0.0}.r$$(git rev-list --count HEAD).g$$(git rev-parse --short HEAD)")
54-DIST := dist/nefertty-$(VERSION)
55 SIBLINGS := github.com/bnema/purego-libwayland github.com/bnema/purego-vulkan52 SIBLINGS := github.com/bnema/purego-libwayland github.com/bnema/purego-vulkan
53+SIBLING_DIRS := $(shell go list -m -f '{{.Dir}}' $(SIBLINGS))
54+dirty = $(shell for d in . $(SIBLING_DIRS); do git -C $$d diff --quiet HEAD && \
55+ test -z "$$(git -C $$d ls-files -o --exclude-standard)" || { echo .dirty; break; }; done)
56+# pacman-ordered: 0.0.0.r<commits>.g<hash>; a tag replaces 0.0.0.
57+VERSION := $(or $(VERSION),$(shell t=$$(git describe --tags --abbrev=0 2>/dev/null | sed 's/^v//; s/-/_/g'); \
58+ echo "$${t:-0.0.0}.r$$(git rev-list --count HEAD).g$$(git rev-parse --short HEAD)")$(dirty))
59+DIST := dist/nefertty-$(VERSION)
60+dist: SHELL := bash
61+dist: .SHELLFLAGS := -eo pipefail -c
56 dist:62 dist:
57- @rm -rf $(DIST) && mkdir -p $(DIST)63+ @test -n "$(SIBLING_DIRS)" || { echo "sibling modules not found: $(SIBLINGS)" >&2; exit 1; }
58- git archive $$(git stash create || echo HEAD) | tar -x -C $(DIST)64+ rm -rf $(DIST) && mkdir -p $(DIST)
59- cd $(DIST) && go mod edit $$(cd $(CURDIR) && go list -m -f '-replace={{.Path}}={{.Dir}}' $(SIBLINGS)) && go mod vendor && \65+ git ls-files -co --exclude-standard -z | tar --null -T - -c | tar -x -C $(DIST)
60- for m in $(SIBLINGS); do d=$$(cd $(CURDIR) && go list -m -f '{{.Dir}}' $$m); \66+ cd $(DIST) && go mod edit $(foreach m,$(SIBLINGS),-replace=$(m)=$(shell go list -m -f '{{.Dir}}' $(m))) && go mod vendor
61- sed -i "s#=> $$d\$$#=> ../$${m##*/}#" go.mod vendor/modules.txt; done && \67+ cd $(DIST) && $(foreach m,$(SIBLINGS),sed -i 's#=> $(shell go list -m -f '{{.Dir}}' $(m))$$#=> ../$(notdir $(m))#' go.mod vendor/modules.txt &&) true
62- ! grep -rq "$(HOME)" go.mod vendor/modules.txt && echo '$(VERSION)' > VERSION68+ @! grep -rqF "$(HOME)" $(DIST) || { echo "home path leaked into $(DIST)" >&2; exit 1; }
63- tar -C dist -czf $(DIST).tar.gz nefertty-$(VERSION)69+ echo '$(VERSION)' > $(DIST)/VERSION
64- @rm -rf $(DIST) && echo $(DIST).tar.gz70+ tar --owner=0 --group=0 --numeric-owner --sort=name --mtime=@$$(git log -1 --format=%ct) -C dist -czf $(DIST).tar.gz nefertty-$(VERSION)
71+ rm -rf $(DIST)
72+ @echo $(DIST).tar.gz
65 73
66-# Arch package from the dist tarball; install it with `sudo pacman -U` on74+# Arch package from the dist tarball, built outside the home directory so
67-# the printed file.75+# no path of it lands in the package metadata. Install it with
76+# `sudo pacman -U` on the printed file.
68 pkg: dist77 pkg: dist
69- rm -rf dist/pkg && mkdir -p dist/pkg && cp packaging/arch/PKGBUILD $(DIST).tar.gz dist/pkg/78+ d=$$(mktemp -d /tmp/nefertty-pkg.XXXXXX) && cp packaging/arch/PKGBUILD $(DIST).tar.gz $$d/ && \
70- cd dist/pkg && sed -i "s/^pkgver=.*/pkgver=$(VERSION)/" PKGBUILD && makepkg -f --noconfirm79+ cd $$d && sed -i "s/^pkgver=.*/pkgver=$(VERSION)/; s/^sha256sums=.*/sha256sums=('$$(sha256sum *.tar.gz | cut -d' ' -f1)')/" PKGBUILD && \
71- @ls dist/pkg/*.pkg.tar.zst80+ makepkg -f --noconfirm && mv *.pkg.tar.zst $(CURDIR)/dist/ && cd / && rm -rf $$d
81+ @ls dist/*.pkg.tar.zst
M README.md +1 -1
@@ -21,7 +21,7 @@ make pkg # builds dist/pkg/nefertty-*.pkg.tar.z
2121 sudo pacman -U dist/pkg/nefertty-*.pkg.tar.zst
2222 ```
2323
24-The package installs `nefertty` and a **NeferTTY** session for display managers (Ly, GDM, SDDM). The session runs `nefertty --session`, which shares `WAYLAND_DISPLAY` and `DISPLAY` with D-Bus and systemd user services, so portals and notification daemons reach it. `make dist` alone builds the self-contained source tarball (Go modules vendored) that the package uses.
24+The package installs `nefertty` and a **NeferTTY** session for display managers (Ly, GDM, SDDM). The session runs `nefertty --session`, which shares `WAYLAND_DISPLAY` and `DISPLAY` with D-Bus and systemd user services, so portals and notification daemons reach it. `make dist` alone builds the self-contained source tarball (Go modules vendored) that the package uses; its version ends in `.dirty` when this repo or a sibling module has uncommitted changes.
2525
2626 ## Configuration
2727
@@ -21,7 +21,7 @@ make pkg # builds dist/pkg/nefertty-*.pkg.tar.z
21 sudo pacman -U dist/pkg/nefertty-*.pkg.tar.zst21 sudo pacman -U dist/pkg/nefertty-*.pkg.tar.zst
22 ```22 ```
23 23
24-The package installs `nefertty` and a **NeferTTY** session for display managers (Ly, GDM, SDDM). The session runs `nefertty --session`, which shares `WAYLAND_DISPLAY` and `DISPLAY` with D-Bus and systemd user services, so portals and notification daemons reach it. `make dist` alone builds the self-contained source tarball (Go modules vendored) that the package uses.24+The package installs `nefertty` and a **NeferTTY** session for display managers (Ly, GDM, SDDM). The session runs `nefertty --session`, which shares `WAYLAND_DISPLAY` and `DISPLAY` with D-Bus and systemd user services, so portals and notification daemons reach it. `make dist` alone builds the self-contained source tarball (Go modules vendored) that the package uses; its version ends in `.dirty` when this repo or a sibling module has uncommitted changes.
25 25
26 ## Configuration26 ## Configuration
27 27
M cmd/nefertty/main.go +1 -10
@@ -23,10 +23,6 @@ import (
2323
2424 type usageError struct{ error }
2525
26-// sessionEnvCommand exports the session environment to D-Bus and systemd
27-// user services (--session).
28-var sessionEnvCommand = []string{"dbus-update-activation-environment", "--systemd", "WAYLAND_DISPLAY", "DISPLAY", "XDG_CURRENT_DESKTOP", "XDG_SESSION_TYPE"}
29-
3026 // version is set with -ldflags "-X main.version=..." by packaged builds.
3127 var version string
3228
@@ -190,11 +186,6 @@ func run() error {
190186 fmt.Fprintln(os.Stderr, cfgErr)
191187 return cfgErr
192188 }
193- if *session {
194- // First startup command: portals and other D-Bus or systemd user
195- // services started later find the session's displays.
196- cfg.Startup = append([][]string{sessionEnvCommand}, cfg.Startup...)
197- }
198189 var script *os.File
199190 if *inputPath != "" {
200191 if *inputPath == "-" {
@@ -236,7 +227,7 @@ func run() error {
236227 for _, w := range warnings {
237228 configLog.Warn().Int("line", w.Line).Msg(w.Msg)
238229 }
239- err = app.Run(ctx, app.Options{Backend: *backend, Config: cfg, ConfigPath: path, Timeout: *timeout, NoTerminal: *noTerminal, NoXwayland: *noXwayland, ScreenshotDir: *screenshot, Sizes: sizes, Script: script})
230+ err = app.Run(ctx, app.Options{Backend: *backend, Config: cfg, ConfigPath: path, Timeout: *timeout, NoTerminal: *noTerminal, NoXwayland: *noXwayland, Session: *session, ScreenshotDir: *screenshot, Sizes: sizes, Script: script})
240231 // SIGINT and SIGTERM cancel the context and are clean exits.
241232 if err != nil && ctx.Err() != nil && errors.Is(err, context.Canceled) {
242233 err = nil
@@ -23,10 +23,6 @@ import (
23 23
24 type usageError struct{ error }24 type usageError struct{ error }
25 25
26-// sessionEnvCommand exports the session environment to D-Bus and systemd
27-// user services (--session).
28-var sessionEnvCommand = []string{"dbus-update-activation-environment", "--systemd", "WAYLAND_DISPLAY", "DISPLAY", "XDG_CURRENT_DESKTOP", "XDG_SESSION_TYPE"}
29-
30 // version is set with -ldflags "-X main.version=..." by packaged builds.26 // version is set with -ldflags "-X main.version=..." by packaged builds.
31 var version string27 var version string
32 28
@@ -190,11 +186,6 @@ func run() error {
190 fmt.Fprintln(os.Stderr, cfgErr)186 fmt.Fprintln(os.Stderr, cfgErr)
191 return cfgErr187 return cfgErr
192 }188 }
193- if *session {
194- // First startup command: portals and other D-Bus or systemd user
195- // services started later find the session's displays.
196- cfg.Startup = append([][]string{sessionEnvCommand}, cfg.Startup...)
197- }
198 var script *os.File189 var script *os.File
199 if *inputPath != "" {190 if *inputPath != "" {
200 if *inputPath == "-" {191 if *inputPath == "-" {
@@ -236,7 +227,7 @@ func run() error {
236 for _, w := range warnings {227 for _, w := range warnings {
237 configLog.Warn().Int("line", w.Line).Msg(w.Msg)228 configLog.Warn().Int("line", w.Line).Msg(w.Msg)
238 }229 }
239- err = app.Run(ctx, app.Options{Backend: *backend, Config: cfg, ConfigPath: path, Timeout: *timeout, NoTerminal: *noTerminal, NoXwayland: *noXwayland, ScreenshotDir: *screenshot, Sizes: sizes, Script: script})230+ err = app.Run(ctx, app.Options{Backend: *backend, Config: cfg, ConfigPath: path, Timeout: *timeout, NoTerminal: *noTerminal, NoXwayland: *noXwayland, Session: *session, ScreenshotDir: *screenshot, Sizes: sizes, Script: script})
240 // SIGINT and SIGTERM cancel the context and are clean exits.231 // SIGINT and SIGTERM cancel the context and are clean exits.
241 if err != nil && ctx.Err() != nil && errors.Is(err, context.Canceled) {232 if err != nil && ctx.Err() != nil && errors.Is(err, context.Canceled) {
242 err = nil233 err = nil
A internal/adapters/launcher/session.go +52 -0
@@ -0,0 +1,52 @@
1+package launcher
2+
3+import (
4+ "context"
5+ "os/exec"
6+ "time"
7+
8+ "github.com/bnema/zerowrap"
9+)
10+
11+// sessionVars are the displays D-Bus and systemd user services (portals,
12+// notification daemons) need to reach the session.
13+var sessionVars = []string{"WAYLAND_DISPLAY", "DISPLAY", "XDG_CURRENT_DESKTOP", "XDG_SESSION_TYPE"}
14+
15+// ExportSession shares the session's displays with D-Bus and systemd user
16+// services, and waits for it: clients started next may activate them.
17+func ExportSession(ctx context.Context, env []string, log zerowrap.Logger) {
18+ ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
19+ defer cancel()
20+ runSessionCommand(ctx, env, log, append([]string{"--systemd"}, sessionVars...), "session exported")
21+}
22+
23+// UnexportSession withdraws the displays from the systemd user manager on
24+// exit, so services started after logout do not reach a dead display.
25+func UnexportSession(env []string, log zerowrap.Logger) {
26+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
27+ defer cancel()
28+ path, err := childLookPath("systemctl", env)
29+ if err != nil {
30+ return
31+ }
32+ cmd := exec.CommandContext(ctx, path, append([]string{"--user", "unset-environment"}, sessionVars...)...)
33+ cmd.Env = env
34+ if err := cmd.Run(); err != nil {
35+ log.Warn().Err(err).Msg("session unexport failed")
36+ }
37+}
38+
39+func runSessionCommand(ctx context.Context, env []string, log zerowrap.Logger, args []string, done string) {
40+ path, err := childLookPath("dbus-update-activation-environment", env)
41+ if err != nil {
42+ log.Warn().Err(err).Msg("session export skipped: dbus-update-activation-environment not found")
43+ return
44+ }
45+ cmd := exec.CommandContext(ctx, path, args...)
46+ cmd.Env = env
47+ if err := cmd.Run(); err != nil {
48+ log.Warn().Err(err).Msg("session export failed")
49+ return
50+ }
51+ log.Info().Strs("vars", sessionVars).Msg(done)
52+}
@@ -0,0 +1,52 @@
1+package launcher
2+
3+import (
4+ "context"
5+ "os/exec"
6+ "time"
7+
8+ "github.com/bnema/zerowrap"
9+)
10+
11+// sessionVars are the displays D-Bus and systemd user services (portals,
12+// notification daemons) need to reach the session.
13+var sessionVars = []string{"WAYLAND_DISPLAY", "DISPLAY", "XDG_CURRENT_DESKTOP", "XDG_SESSION_TYPE"}
14+
15+// ExportSession shares the session's displays with D-Bus and systemd user
16+// services, and waits for it: clients started next may activate them.
17+func ExportSession(ctx context.Context, env []string, log zerowrap.Logger) {
18+ ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
19+ defer cancel()
20+ runSessionCommand(ctx, env, log, append([]string{"--systemd"}, sessionVars...), "session exported")
21+}
22+
23+// UnexportSession withdraws the displays from the systemd user manager on
24+// exit, so services started after logout do not reach a dead display.
25+func UnexportSession(env []string, log zerowrap.Logger) {
26+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
27+ defer cancel()
28+ path, err := childLookPath("systemctl", env)
29+ if err != nil {
30+ return
31+ }
32+ cmd := exec.CommandContext(ctx, path, append([]string{"--user", "unset-environment"}, sessionVars...)...)
33+ cmd.Env = env
34+ if err := cmd.Run(); err != nil {
35+ log.Warn().Err(err).Msg("session unexport failed")
36+ }
37+}
38+
39+func runSessionCommand(ctx context.Context, env []string, log zerowrap.Logger, args []string, done string) {
40+ path, err := childLookPath("dbus-update-activation-environment", env)
41+ if err != nil {
42+ log.Warn().Err(err).Msg("session export skipped: dbus-update-activation-environment not found")
43+ return
44+ }
45+ cmd := exec.CommandContext(ctx, path, args...)
46+ cmd.Env = env
47+ if err := cmd.Run(); err != nil {
48+ log.Warn().Err(err).Msg("session export failed")
49+ return
50+ }
51+ log.Info().Strs("vars", sessionVars).Msg(done)
52+}
A internal/adapters/launcher/session_test.go +42 -0
@@ -0,0 +1,42 @@
1+package launcher
2+
3+import (
4+ "context"
5+ "os"
6+ "path/filepath"
7+ "strings"
8+ "testing"
9+
10+ "github.com/bnema/nefertty/internal/logging"
11+)
12+
13+// The session commands run with the child environment and the right
14+// arguments; the scripts on PATH record them.
15+func TestExportSession(t *testing.T) {
16+ dir := t.TempDir()
17+ out := filepath.Join(dir, "calls")
18+ script := "#!/bin/sh\necho \"${0##*/} $* $WAYLAND_DISPLAY\" >> " + out + "\n"
19+ for _, name := range []string{"dbus-update-activation-environment", "systemctl"} {
20+ if err := os.WriteFile(filepath.Join(dir, name), []byte(script), 0o755); err != nil {
21+ t.Fatal(err)
22+ }
23+ }
24+ env := []string{"PATH=" + dir, "WAYLAND_DISPLAY=wayland-9"}
25+ ctx := context.Background()
26+ ExportSession(ctx, env, logging.For(ctx, "launcher"))
27+ UnexportSession(env, logging.For(ctx, "launcher"))
28+ data, err := os.ReadFile(out)
29+ if err != nil {
30+ t.Fatal(err)
31+ }
32+ want := "dbus-update-activation-environment --systemd WAYLAND_DISPLAY DISPLAY XDG_CURRENT_DESKTOP XDG_SESSION_TYPE wayland-9\n" +
33+ "systemctl --user unset-environment WAYLAND_DISPLAY DISPLAY XDG_CURRENT_DESKTOP XDG_SESSION_TYPE wayland-9\n"
34+ if string(data) != want {
35+ t.Fatalf("calls:\n%s", data)
36+ }
37+ // Missing tools are skipped, not fatal.
38+ ExportSession(ctx, []string{"PATH=" + t.TempDir()}, logging.For(ctx, "launcher"))
39+ if strings.Count(string(data), "\n") != 2 {
40+ t.Fatal(string(data))
41+ }
42+}
@@ -0,0 +1,42 @@
1+package launcher
2+
3+import (
4+ "context"
5+ "os"
6+ "path/filepath"
7+ "strings"
8+ "testing"
9+
10+ "github.com/bnema/nefertty/internal/logging"
11+)
12+
13+// The session commands run with the child environment and the right
14+// arguments; the scripts on PATH record them.
15+func TestExportSession(t *testing.T) {
16+ dir := t.TempDir()
17+ out := filepath.Join(dir, "calls")
18+ script := "#!/bin/sh\necho \"${0##*/} $* $WAYLAND_DISPLAY\" >> " + out + "\n"
19+ for _, name := range []string{"dbus-update-activation-environment", "systemctl"} {
20+ if err := os.WriteFile(filepath.Join(dir, name), []byte(script), 0o755); err != nil {
21+ t.Fatal(err)
22+ }
23+ }
24+ env := []string{"PATH=" + dir, "WAYLAND_DISPLAY=wayland-9"}
25+ ctx := context.Background()
26+ ExportSession(ctx, env, logging.For(ctx, "launcher"))
27+ UnexportSession(env, logging.For(ctx, "launcher"))
28+ data, err := os.ReadFile(out)
29+ if err != nil {
30+ t.Fatal(err)
31+ }
32+ want := "dbus-update-activation-environment --systemd WAYLAND_DISPLAY DISPLAY XDG_CURRENT_DESKTOP XDG_SESSION_TYPE wayland-9\n" +
33+ "systemctl --user unset-environment WAYLAND_DISPLAY DISPLAY XDG_CURRENT_DESKTOP XDG_SESSION_TYPE wayland-9\n"
34+ if string(data) != want {
35+ t.Fatalf("calls:\n%s", data)
36+ }
37+ // Missing tools are skipped, not fatal.
38+ ExportSession(ctx, []string{"PATH=" + t.TempDir()}, logging.For(ctx, "launcher"))
39+ if strings.Count(string(data), "\n") != 2 {
40+ t.Fatal(string(data))
41+ }
42+}
M internal/app/app.go +8 -1
@@ -34,7 +34,10 @@ type Options struct {
3434 Timeout time.Duration
3535 NoTerminal bool
3636 // NoXwayland skips the X11 display even when the config enables it.
37- NoXwayland bool
37+ NoXwayland bool
38+ // Session exports the displays to D-Bus and systemd user services
39+ // before any client starts, and withdraws them on exit.
40+ Session bool
3841 ScreenshotDir string
3942 // Sizes are the headless outputs (width, height), left to right; empty
4043 // means one 1920x1080 output. With several outputs, screenshots go to
@@ -116,6 +119,10 @@ func run(ctx context.Context, opts Options, inject func(chan<- ports.InputEvent)
116119 defer xdisplay.Close()
117120 childEnv = append(childEnv, "DISPLAY="+xdisplay.Name())
118121 }
122+ if opts.Session {
123+ launcher.ExportSession(ctx, childEnv, logging.For(ctx, "launcher"))
124+ defer launcher.UnexportSession(childEnv, logging.For(ctx, "launcher"))
125+ }
119126 child := launcher.New(childEnv, logging.For(ctx, "launcher"))
120127 if inject != nil {
121128 inject(input)
@@ -34,7 +34,10 @@ type Options struct {
34 Timeout time.Duration34 Timeout time.Duration
35 NoTerminal bool35 NoTerminal bool
36 // NoXwayland skips the X11 display even when the config enables it.36 // NoXwayland skips the X11 display even when the config enables it.
37- NoXwayland bool37+ NoXwayland bool
38+ // Session exports the displays to D-Bus and systemd user services
39+ // before any client starts, and withdraws them on exit.
40+ Session bool
38 ScreenshotDir string41 ScreenshotDir string
39 // Sizes are the headless outputs (width, height), left to right; empty42 // Sizes are the headless outputs (width, height), left to right; empty
40 // means one 1920x1080 output. With several outputs, screenshots go to43 // means one 1920x1080 output. With several outputs, screenshots go to
@@ -116,6 +119,10 @@ func run(ctx context.Context, opts Options, inject func(chan<- ports.InputEvent)
116 defer xdisplay.Close()119 defer xdisplay.Close()
117 childEnv = append(childEnv, "DISPLAY="+xdisplay.Name())120 childEnv = append(childEnv, "DISPLAY="+xdisplay.Name())
118 }121 }
122+ if opts.Session {
123+ launcher.ExportSession(ctx, childEnv, logging.For(ctx, "launcher"))
124+ defer launcher.UnexportSession(childEnv, logging.For(ctx, "launcher"))
125+ }
119 child := launcher.New(childEnv, logging.For(ctx, "launcher"))126 child := launcher.New(childEnv, logging.For(ctx, "launcher"))
120 if inject != nil {127 if inject != nil {
121 inject(input)128 inject(input)
M packaging/arch/PKGBUILD +5 -5
@@ -8,20 +8,19 @@ url='https://github.com/bnema/nefertty'
88 license=('MIT')
99 # Loaded at runtime (purego, no cgo): not seen by makepkg's library scan.
1010 depends=('glibc' 'libinput' 'libxkbcommon' 'seatd' 'systemd-libs' 'vulkan-icd-loader'
11- 'vulkan-driver')
11+ 'vulkan-driver' 'wayland' 'dbus')
1212 makedepends=('go')
13-optdepends=('xwayland-satellite: X11 apps'
13+optdepends=('xwayland-satellite>=0.7: X11 apps'
1414 'foot: default terminal'
1515 'fuzzel: launcher'
1616 'xdg-desktop-portal-gtk: file pickers and settings for apps')
17-options=('!debug')
1817 source=("$pkgname-$pkgver.tar.gz")
1918 sha256sums=('SKIP')
2019
2120 build() {
2221 cd "$pkgname-$pkgver"
23- export CGO_ENABLED=0 GOFLAGS='-mod=vendor -trimpath -buildvcs=false' GOPROXY=off
24- go build -ldflags "-s -w -X main.version=$pkgver" -o nefertty ./cmd/nefertty
22+ export CGO_ENABLED=0 GOFLAGS='-mod=vendor -trimpath -buildvcs=false -buildmode=pie' GOPROXY=off GOTOOLCHAIN=local
23+ go build -ldflags "-X main.version=$pkgver" -o nefertty ./cmd/nefertty
2524 }
2625
2726 check() {
@@ -34,6 +33,7 @@ package() {
3433 cd "$pkgname-$pkgver"
3534 install -Dm755 nefertty "$pkgdir/usr/bin/nefertty"
3635 install -Dm644 packaging/nefertty.desktop "$pkgdir/usr/share/wayland-sessions/nefertty.desktop"
36+ install -Dm644 packaging/nefertty-portals.conf "$pkgdir/usr/share/xdg-desktop-portal/nefertty-portals.conf"
3737 install -Dm644 examples/config "$pkgdir/usr/share/doc/nefertty/config.example"
3838 install -Dm644 README.md "$pkgdir/usr/share/doc/nefertty/README.md"
3939 install -Dm644 LICENSE "$pkgdir/usr/share/licenses/nefertty/LICENSE"
@@ -8,20 +8,19 @@ url='https://github.com/bnema/nefertty'
8 license=('MIT')8 license=('MIT')
9 # Loaded at runtime (purego, no cgo): not seen by makepkg's library scan.9 # Loaded at runtime (purego, no cgo): not seen by makepkg's library scan.
10 depends=('glibc' 'libinput' 'libxkbcommon' 'seatd' 'systemd-libs' 'vulkan-icd-loader'10 depends=('glibc' 'libinput' 'libxkbcommon' 'seatd' 'systemd-libs' 'vulkan-icd-loader'
11- 'vulkan-driver')11+ 'vulkan-driver' 'wayland' 'dbus')
12 makedepends=('go')12 makedepends=('go')
13-optdepends=('xwayland-satellite: X11 apps'13+optdepends=('xwayland-satellite>=0.7: X11 apps'
14 'foot: default terminal'14 'foot: default terminal'
15 'fuzzel: launcher'15 'fuzzel: launcher'
16 'xdg-desktop-portal-gtk: file pickers and settings for apps')16 'xdg-desktop-portal-gtk: file pickers and settings for apps')
17-options=('!debug')
18 source=("$pkgname-$pkgver.tar.gz")17 source=("$pkgname-$pkgver.tar.gz")
19 sha256sums=('SKIP')18 sha256sums=('SKIP')
20 19
21 build() {20 build() {
22 cd "$pkgname-$pkgver"21 cd "$pkgname-$pkgver"
23- export CGO_ENABLED=0 GOFLAGS='-mod=vendor -trimpath -buildvcs=false' GOPROXY=off22+ export CGO_ENABLED=0 GOFLAGS='-mod=vendor -trimpath -buildvcs=false -buildmode=pie' GOPROXY=off GOTOOLCHAIN=local
24- go build -ldflags "-s -w -X main.version=$pkgver" -o nefertty ./cmd/nefertty23+ go build -ldflags "-X main.version=$pkgver" -o nefertty ./cmd/nefertty
25 }24 }
26 25
27 check() {26 check() {
@@ -34,6 +33,7 @@ package() {
34 cd "$pkgname-$pkgver"33 cd "$pkgname-$pkgver"
35 install -Dm755 nefertty "$pkgdir/usr/bin/nefertty"34 install -Dm755 nefertty "$pkgdir/usr/bin/nefertty"
36 install -Dm644 packaging/nefertty.desktop "$pkgdir/usr/share/wayland-sessions/nefertty.desktop"35 install -Dm644 packaging/nefertty.desktop "$pkgdir/usr/share/wayland-sessions/nefertty.desktop"
36+ install -Dm644 packaging/nefertty-portals.conf "$pkgdir/usr/share/xdg-desktop-portal/nefertty-portals.conf"
37 install -Dm644 examples/config "$pkgdir/usr/share/doc/nefertty/config.example"37 install -Dm644 examples/config "$pkgdir/usr/share/doc/nefertty/config.example"
38 install -Dm644 README.md "$pkgdir/usr/share/doc/nefertty/README.md"38 install -Dm644 README.md "$pkgdir/usr/share/doc/nefertty/README.md"
39 install -Dm644 LICENSE "$pkgdir/usr/share/licenses/nefertty/LICENSE"39 install -Dm644 LICENSE "$pkgdir/usr/share/licenses/nefertty/LICENSE"
A packaging/nefertty-portals.conf +2 -0
@@ -0,0 +1,2 @@
1+[preferred]
2+default=gtk
@@ -0,0 +1,2 @@
1+[preferred]
2+default=gtk

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 25

Tags 10

↑↓ moveenter openesc close