session_env.go RawHistoryFolder

1package daemon
2
3import (
4	"encoding/json"
5	"slices"
6	"strings"
7
8	"github.com/bnema/vev/internal/protocol"
9)
10
11// Display variables describe one graphical session and move as a group: a
12// client with a non-empty WAYLAND_DISPLAY or DISPLAY replaces the whole group and removes
13// members it lacks, so a switch to another compositor or an X11-only desktop
14// never leaves a stale socket behind. A client without a display (console)
15// or reached over SSH leaves the group untouched: an X-forwarded DISPLAY
16// belongs to that one connection and must not replace the local desktop.
17var sessionDisplayEnvironment = []string{
18	"WAYLAND_DISPLAY",
19	"DISPLAY",
20	"XAUTHORITY",
21	"XDG_CURRENT_DESKTOP",
22	"XDG_SESSION_TYPE",
23	"XDG_SESSION_DESKTOP",
24	"XDG_SESSION_CLASS",
25	"XDG_SESSION_ID",
26}
27
28// User variables belong to the user login rather than one display: with
29// systemd, every graphical session of a user shares the runtime dir and user
30// bus. They are set when the client has them and never removed.
31var sessionUserEnvironment = []string{
32	"DBUS_SESSION_BUS_ADDRESS",
33	"XDG_RUNTIME_DIR",
34}
35
36// Login variables follow the latest attach exactly: a value the client lacks is
37// removed, because a stale agent socket or SSH peer is worse than none.
38var sessionLoginEnvironment = []string{
39	"SSH_AUTH_SOCK",
40	"SSH_AGENT_PID",
41	"SSH_CONNECTION",
42	"SSH_CLIENT",
43	"SSH_TTY",
44}
45
46// setClientEnvironment records the attachment's own client-owned environment;
47// nil means the daemon owns it and the attachment never changes session env.
48func (ac *attachedClient) setClientEnvironment(env []string) {
49	if env == nil {
50		ac.clientEnv.Store(nil)
51		return
52	}
53	owned := append(make([]string, 0, len(env)), env...)
54	ac.clientEnv.Store(&owned)
55}
56
57// clientEnvironment returns the attachment's client-owned environment, or
58// ok=false when the daemon owns it. The slice is immutable; do not modify it.
59func (ac *attachedClient) clientEnvironment() ([]string, bool) {
60	env := ac.clientEnv.Load()
61	if env == nil {
62		return nil, false
63	}
64	return *env, true
65}
66
67// helloClientEnvironment returns the Hello environment when the client owns
68// it, or nil for a daemon-owned (remote) attach.
69func helloClientEnvironment(h protocol.Hello) []string {
70	if h.EnvironmentPolicy == protocol.EnvironmentPolicyDaemonOwned {
71		return nil
72	}
73	if h.Env == nil {
74		return []string{}
75	}
76	return h.Env
77}
78
79// environmentSeedLocked returns a copy of the session environment for a new
80// session seeded from this one, and whether it is still the daemon's own
81// (provisional). The caller holds s.mu.
82func (s *session) environmentSeedLocked() ([]string, bool) {
83	return copyEnvironment(s.env), s.envProvisional
84}
85
86// markEnvironmentProvisional records that sess.env came from the daemon's own
87// process, so the first client-owned environment replaces it.
88func markEnvironmentProvisional(sess *session) {
89	sess.mu.Lock()
90	sess.envProvisional = true
91	sess.mu.Unlock()
92}
93
94// adoptClientEnvironmentLocked applies a client-owned environment to the
95// session's future PTY children. The caller holds sess.mu.
96//
97// A provisional environment (the daemon's own, see envProvisional) is replaced
98// wholesale, except that a client without its own display keeps the session's
99// display group when it has one, matching refreshSessionEnvironment.
100func (s *session) adoptClientEnvironmentLocked(client []string) {
101	if !s.envProvisional {
102		s.env = refreshSessionEnvironment(s.env, client)
103		return
104	}
105	next := copyEnvironment(client)
106	if !ownsDisplay(client) && hasDisplay(s.env) {
107		next = replaceEnvironmentGroup(next, s.env, sessionDisplayEnvironment)
108	}
109	s.env = next
110	s.envProvisional = false
111}
112
113// refreshSessionEnvironment returns current with its session-bound variables
114// refreshed from client. Entries keep their position; duplicates of a refreshed
115// variable collapse into its first entry, and new variables are appended.
116func refreshSessionEnvironment(current, client []string) []string {
117	if ownsDisplay(client) {
118		current = replaceEnvironmentGroup(current, client, sessionDisplayEnvironment)
119	}
120	for _, key := range sessionUserEnvironment {
121		if value, ok := lookupEnvironment(client, key); ok {
122			current = setEnvironment(current, key, &value)
123		}
124	}
125	return replaceEnvironmentGroup(current, client, sessionLoginEnvironment)
126}
127
128// replaceEnvironmentGroup sets every key of group in env to its value in
129// source, removing keys source lacks. It returns a new slice.
130func replaceEnvironmentGroup(env, source, group []string) []string {
131	for _, key := range group {
132		if value, ok := lookupEnvironment(source, key); ok {
133			env = setEnvironment(env, key, &value)
134		} else {
135			env = setEnvironment(env, key, nil)
136		}
137	}
138	return env
139}
140
141// setEnvironment returns a copy of env with key set to *value in place of its
142// first entry (appended when absent), or removed when value is nil. Later
143// duplicates of key are dropped. env itself is never modified.
144func setEnvironment(env []string, key string, value *string) []string {
145	out := make([]string, 0, len(env)+1)
146	found := false
147	for _, entry := range env {
148		if name, _, _ := environmentEntry(entry); name == key {
149			if !found && value != nil {
150				out = append(out, key+"="+*value)
151			}
152			found = true
153			continue
154		}
155		out = append(out, entry)
156	}
157	if !found && value != nil {
158		out = append(out, key+"="+*value)
159	}
160	return out
161}
162
163// ownsDisplay reports whether client is a local graphical session whose
164// display group should replace the session's. SSH clients never do, even with
165// X forwarding.
166func ownsDisplay(client []string) bool {
167	if _, ssh := lookupEnvironment(client, "SSH_CONNECTION"); ssh {
168		return false
169	}
170	return hasDisplay(client)
171}
172
173// hasDisplay reports whether env belongs to a graphical session. An empty
174// value names no display (it usually means "no X here"), so it does not count.
175func hasDisplay(env []string) bool {
176	wayland, _ := lookupEnvironment(env, "WAYLAND_DISPLAY")
177	x11, _ := lookupEnvironment(env, "DISPLAY")
178	return wayland != "" || x11 != ""
179}
180
181// sessionBoundEnvironment lists the variables that follow the attaching client.
182// A local client-owned attach refreshes only these; every other variable
183// (SHELL, PATH, HOME, ...) keeps the value the session was created with.
184var sessionBoundEnvironment = slices.Concat(sessionDisplayEnvironment, sessionUserEnvironment, sessionLoginEnvironment)
185
186// lookupEnvironment returns the first value for key, matching getenv(3).
187func lookupEnvironment(env []string, key string) (string, bool) {
188	for _, entry := range env {
189		if name, value, ok := environmentEntry(entry); ok && name == key {
190			return value, true
191		}
192	}
193	return "", false
194}
195
196// sessionEnvironmentExport renders the session-bound variables a running shell
197// should apply. JSON maps each variable to its value, or null when the shell
198// should unset it; it is the machine format. Text output is one KEY=value line
199// per set variable, for people.
200// Missing login variables are always unset. Missing display variables are
201// unset only when the session has a display; otherwise they are omitted, so a
202// shell keeps any value it obtained on its own.
203func sessionEnvironmentExport(env []string, asJSON bool) (string, error) {
204	values := make(map[string]*string)
205	var text strings.Builder
206	for _, key := range sessionBoundEnvironment {
207		if value, ok := lookupEnvironment(env, key); ok {
208			values[key] = &value
209			text.WriteString(key + "=" + value + "\n")
210		}
211	}
212	unset := sessionLoginEnvironment
213	if hasDisplay(env) {
214		unset = slices.Concat(sessionDisplayEnvironment, sessionLoginEnvironment)
215	}
216	for _, key := range unset {
217		if _, ok := values[key]; !ok {
218			values[key] = nil
219		}
220	}
221	if !asJSON {
222		return text.String(), nil
223	}
224	encoded, err := json.Marshal(values)
225	if err != nil {
226		return "", err
227	}
228	return string(encoded) + "\n", nil
229}

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 12

Tags 11

↑↓ moveenter openesc close