env.go RawHistoryFolder

1package app
2
3import (
4	"context"
5	"encoding/json"
6	"fmt"
7	"io"
8	"sort"
9	"strings"
10)
11
12const envHelp = `usage: vev env <fish|sh> [-s <session>]
13
14Print shell code that applies the session's current desktop and login
15variables (WAYLAND_DISPLAY, DBUS_SESSION_BUS_ADDRESS, SSH_AUTH_SOCK, ...) to a
16running shell. Inside a pane the current session is used; outside a pane,
17-s <session> is required.
18
19  fish   vev env fish | source
20  sh     eval "$(vev env sh)"   (bash, zsh, and other POSIX shells)
21
22For the same values as data, use vev cmd env [--json].`
23
24// envInvocation is the shell-integration front end for the `env` control
25// command. It only renders shell code; `vev cmd env` owns the data API.
26type envInvocation struct {
27	shell   string
28	session string
29	help    bool
30}
31
32func parseEnvArgs(args []string) (envInvocation, error) {
33	var invocation envInvocation
34	for index := 0; index < len(args); index++ {
35		switch arg := args[index]; arg {
36		case "--help", "-h":
37			invocation.help = true
38		case "-s":
39			if index+1 >= len(args) || args[index+1] == "" {
40				return envInvocation{}, usagef("`env -s` requires a session name")
41			}
42			invocation.session = args[index+1]
43			index++
44		case "fish", "sh":
45			if invocation.shell != "" {
46				return envInvocation{}, usagef("`env` takes one shell: fish or sh")
47			}
48			invocation.shell = arg
49		default:
50			if strings.HasPrefix(arg, "-") {
51				return envInvocation{}, usagef("unknown flag %q for `env`", arg)
52			}
53			return envInvocation{}, usagef("unsupported shell %q for `env`; use fish or sh", arg)
54		}
55	}
56	if invocation.shell == "" && !invocation.help {
57		return envInvocation{}, usagef("`env` requires a shell: fish or sh")
58	}
59	return invocation, nil
60}
61
62func runEnv(ctx context.Context, invocation envInvocation) error {
63	return runEnvWithDeps(ctx, invocation, productionCmdDeps())
64}
65
66func runEnvWithDeps(ctx context.Context, invocation envInvocation, deps cmdDeps) error {
67	if invocation.help {
68		_, err := fmt.Fprintln(deps.stdout, envHelp)
69		return err
70	}
71	// The output is evaluated by a shell; never guess which session it follows.
72	if _, inPane := parseVEVEnv(deps.getenv("VEV")); !inPane && invocation.session == "" {
73		return usagef("`env` requires running inside a vev pane or -s <session>")
74	}
75	var captured strings.Builder
76	dataDeps := deps
77	dataDeps.stdout = &captured
78	if err := runCmdWithDeps(ctx, cmdInvocation{slug: "env", session: invocation.session, jsonOut: true}, dataDeps); err != nil {
79		return err
80	}
81	return writeShellEnvironment(deps.stdout, invocation.shell, captured.String())
82}
83
84// writeShellEnvironment converts the `env --json` result into code that sets
85// or unsets each variable in the given shell. Nothing is written on error.
86func writeShellEnvironment(out io.Writer, shell, encoded string) error {
87	var values map[string]*string
88	if err := json.Unmarshal([]byte(encoded), &values); err != nil {
89		return fmt.Errorf("decoding session environment: %w", err)
90	}
91	keys := make([]string, 0, len(values))
92	for key := range values {
93		if !isEnvironmentName(key) {
94			return fmt.Errorf("invalid session environment name %q", key)
95		}
96		keys = append(keys, key)
97	}
98	sort.Strings(keys)
99	var code strings.Builder
100	for _, key := range keys {
101		value := values[key]
102		switch {
103		case shell == "fish" && value == nil:
104			// -g: without a global, a bare `set -e` erases the user's
105			// universal variable for every fish session.
106			code.WriteString("set -e -g " + key + "\n")
107		case shell == "fish":
108			code.WriteString("set -gx " + key + " " + fishQuote(*value) + "\n")
109		case value == nil:
110			code.WriteString("unset " + key + "\n")
111		default:
112			code.WriteString("export " + key + "=" + shQuote(*value) + "\n")
113		}
114	}
115	_, err := io.WriteString(out, code.String())
116	return err
117}
118
119func isEnvironmentName(name string) bool {
120	if name == "" || name[0] >= '0' && name[0] <= '9' {
121		return false
122	}
123	for i := 0; i < len(name); i++ {
124		c := name[i]
125		if c != '_' && (c < 'A' || c > 'Z') && (c < 'a' || c > 'z') && (c < '0' || c > '9') {
126			return false
127		}
128	}
129	return true
130}
131
132func shQuote(value string) string {
133	return "'" + strings.ReplaceAll(value, "'", `'\''`) + "'"
134}
135
136func fishQuote(value string) string {
137	return "'" + strings.NewReplacer(`\`, `\\`, `'`, `\'`).Replace(value) + "'"
138}

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