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}