@codebend3r/che 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -24
- package/bin/art.py +22 -20
- package/bin/che.py +208 -587
- package/bin/commands.py +220 -231
- package/bin/doctor.py +220 -0
- package/bin/drives/eject-all-drives.py +21 -38
- package/bin/drives/mount-all-drives.py +18 -62
- package/bin/drives/ping-nas.py +77 -105
- package/bin/files/compress-folders.py +7 -11
- package/bin/files/delete-by-ext.py +10 -14
- package/bin/files/delete-empty-folders.py +33 -37
- package/bin/files/delete-smb-files.py +12 -21
- package/bin/files/files-under-size.py +5 -11
- package/bin/files/find-largest-files.py +21 -25
- package/bin/files/list-permission.py +40 -0
- package/bin/files/make-alpha-dir.py +2 -0
- package/bin/git/all-actions.py +350 -253
- package/bin/git/checkout-my-branches.py +42 -63
- package/bin/git/clean-stale-branches.py +23 -48
- package/bin/git/prune-worktrees.py +12 -51
- package/bin/git/sync-all-branches.py +54 -283
- package/bin/git/update-local-branches.py +67 -73
- package/bin/gitutils.py +263 -0
- package/bin/install.py +393 -1182
- package/bin/machine.py +345 -0
- package/bin/media.py +127 -0
- package/bin/menu.py +437 -0
- package/bin/nas.py +102 -0
- package/bin/rcfile.py +380 -0
- package/bin/shellgen.py +503 -431
- package/bin/system/btop-launch.py +17 -13
- package/bin/system/update-brew.py +57 -68
- package/bin/tui.py +156 -153
- package/bin/utils.py +165 -237
- package/bin/video/delete-duplicate-videos.py +64 -62
- package/bin/video/detect-green-magenta-videos.py +105 -95
- package/bin/video/find-movie-by-year.py +3 -0
- package/bin/video/find-video-mkv-issues.py +48 -65
- package/bin/video/fix-codecs.py +25 -30
- package/bin/video/largest-tv-shows.py +60 -57
- package/bin/video/remove-metadata.py +3 -6
- package/bin/video/rename-video-file.py +74 -84
- package/bin/video/scan-videos-audio-language.py +16 -33
- package/bin/video/show-codecs.py +20 -24
- package/bin/video/validate-video-files.py +5 -8
- package/bin/video/video-list.py +17 -20
- package/package.json +4 -10
- package/shell/che.bash +33 -33
- package/shell/che.fish +7 -7
- package/shell/che.zsh +33 -33
- package/shell/completions/_che +19 -19
- package/shell/completions/che.bash +15 -15
- package/shell/completions/che.fish +34 -50
package/bin/shellgen.py
CHANGED
|
@@ -1,49 +1,47 @@
|
|
|
1
1
|
"""Render the shell wrappers and completions from :mod:`commands`.
|
|
2
2
|
|
|
3
|
-
Everything under ``shell/`` is produced here,
|
|
4
|
-
|
|
3
|
+
Everything under ``shell/`` is produced here, along with the README's
|
|
4
|
+
generated islands, and :func:`sync_generated` writes them to disk for both
|
|
5
|
+
``che install`` and ``bun run generate`` (which runs this module). Two rules
|
|
6
|
+
keep the generated files honest:
|
|
5
7
|
|
|
6
8
|
* **No machine-specific data.** These files are committed, so they must work
|
|
7
9
|
from any clone path. The one machine-specific thing - where the repo lives
|
|
8
10
|
and which ``python3`` to use - goes in the small block the installer writes
|
|
9
11
|
into ``~/.zshrc``, not here. As a fallback the wrappers resolve their own
|
|
10
12
|
location, so sourcing ``shell/che.zsh`` directly works with no block at all.
|
|
11
|
-
* **Deterministic.**
|
|
12
|
-
|
|
13
|
-
|
|
13
|
+
* **Deterministic.** A test re-renders and diffs against the files on disk,
|
|
14
|
+
and ``bun run ci`` runs it, so generation may not depend on the environment
|
|
15
|
+
it runs in.
|
|
14
16
|
|
|
15
|
-
Library module: no shebang, not executable.
|
|
17
|
+
Library module: no shebang, not executable. ``python3 bin/shellgen.py``
|
|
18
|
+
rewrites every generated file.
|
|
16
19
|
"""
|
|
17
20
|
|
|
18
21
|
from __future__ import annotations
|
|
19
22
|
|
|
23
|
+
import re
|
|
20
24
|
import shlex
|
|
25
|
+
from collections.abc import Callable, Iterable, Sequence
|
|
26
|
+
from dataclasses import dataclass
|
|
21
27
|
from pathlib import Path
|
|
22
28
|
|
|
23
29
|
import art
|
|
24
30
|
from commands import (
|
|
25
31
|
BUILTINS,
|
|
26
|
-
CATEGORIES,
|
|
27
32
|
COMMANDS,
|
|
28
33
|
MENU_KEYS,
|
|
34
|
+
REPO,
|
|
29
35
|
VERSION,
|
|
30
36
|
Command,
|
|
37
|
+
Resolved,
|
|
38
|
+
all_commands,
|
|
39
|
+
grouped,
|
|
31
40
|
required_binaries,
|
|
41
|
+
variants,
|
|
42
|
+
wrapper_names,
|
|
32
43
|
)
|
|
33
|
-
|
|
34
|
-
SHELLS = ("zsh", "bash", "fish")
|
|
35
|
-
|
|
36
|
-
# Where each shell's generated file lives, relative to the repo root.
|
|
37
|
-
WRAPPER_FILES = {
|
|
38
|
-
"zsh": Path("shell/che.zsh"),
|
|
39
|
-
"bash": Path("shell/che.bash"),
|
|
40
|
-
"fish": Path("shell/che.fish"),
|
|
41
|
-
}
|
|
42
|
-
COMPLETION_FILES = {
|
|
43
|
-
"zsh": Path("shell/completions/_che"),
|
|
44
|
-
"bash": Path("shell/completions/che.bash"),
|
|
45
|
-
"fish": Path("shell/completions/che.fish"),
|
|
46
|
-
}
|
|
44
|
+
from utils import atomic_write_text
|
|
47
45
|
|
|
48
46
|
# The rc-file block is delimited by these. Anything between them belongs to
|
|
49
47
|
# `che install` and is rewritten wholesale on every install; anything outside
|
|
@@ -51,42 +49,10 @@ COMPLETION_FILES = {
|
|
|
51
49
|
BEGIN_MARKER = "# >>> che shell functions >>>"
|
|
52
50
|
END_MARKER = "# <<< che shell functions <<<"
|
|
53
51
|
|
|
54
|
-
# Flags that take a value, so completion can offer `--flag=` without a
|
|
55
|
-
# trailing space and then complete filenames after the `=`.
|
|
56
|
-
VALUE_FLAGS = frozenset(
|
|
57
|
-
{
|
|
58
|
-
"--author",
|
|
59
|
-
"--category",
|
|
60
|
-
"--exts",
|
|
61
|
-
"--ext",
|
|
62
|
-
"--ignore-words",
|
|
63
|
-
"--interval",
|
|
64
|
-
"--length",
|
|
65
|
-
"--limit",
|
|
66
|
-
"--only",
|
|
67
|
-
"--owner",
|
|
68
|
-
"--path",
|
|
69
|
-
"--ping-timeout",
|
|
70
|
-
"--pr-limit",
|
|
71
|
-
"--protect",
|
|
72
|
-
"--samples",
|
|
73
|
-
"--shell",
|
|
74
|
-
"--shells",
|
|
75
|
-
"--size",
|
|
76
|
-
"--sort",
|
|
77
|
-
"--strategy",
|
|
78
|
-
"--threshold",
|
|
79
|
-
"--year",
|
|
80
|
-
}
|
|
81
|
-
)
|
|
82
|
-
|
|
83
|
-
# Flags whose value is a path, so completion offers files rather than nothing.
|
|
84
|
-
PATH_FLAGS = frozenset({"--path"})
|
|
85
|
-
|
|
86
52
|
|
|
87
|
-
def _banner(
|
|
53
|
+
def _banner() -> str:
|
|
88
54
|
return "\n".join(
|
|
89
|
-
f"
|
|
55
|
+
f"# {line}".rstrip()
|
|
90
56
|
for line in (
|
|
91
57
|
f"che {VERSION} - generated file, do not edit.",
|
|
92
58
|
"",
|
|
@@ -98,66 +64,215 @@ def _banner(comment: str = "#") -> str:
|
|
|
98
64
|
|
|
99
65
|
def _flag_words(command: Command) -> list[str]:
|
|
100
66
|
"""Completion words for one command: its flags plus the universal ones."""
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
67
|
+
return [*command.flags, "--help"]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# ---------------------------------------------------------------------------
|
|
71
|
+
# Dialects: how each family of shells spells the same thing
|
|
72
|
+
# ---------------------------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class Dialect:
|
|
76
|
+
"""The syntax one family of shells writes the generated code in."""
|
|
77
|
+
|
|
78
|
+
indent: str
|
|
79
|
+
all_args: str # every argument the function was called with
|
|
80
|
+
status: str # the last command's exit status
|
|
81
|
+
env_runner: str # what has to precede `NAME=value command`
|
|
82
|
+
end: str # closes a function
|
|
83
|
+
|
|
84
|
+
@staticmethod
|
|
85
|
+
def quote(text: str) -> str:
|
|
86
|
+
raise NotImplementedError
|
|
87
|
+
|
|
88
|
+
def header(self, name: str, summary: str) -> list[str]:
|
|
89
|
+
"""The lines that open a function definition."""
|
|
90
|
+
raise NotImplementedError
|
|
91
|
+
|
|
92
|
+
def file_head(self, shell: Shell) -> str:
|
|
93
|
+
"""Everything a wrapper file sets up before its first wrapper."""
|
|
94
|
+
raise NotImplementedError
|
|
95
|
+
|
|
96
|
+
def file_tail(self, shell: Shell) -> str:
|
|
97
|
+
"""Everything a wrapper file does after its last wrapper."""
|
|
98
|
+
raise NotImplementedError
|
|
99
|
+
|
|
100
|
+
def export(self, name: str, word: str) -> str:
|
|
101
|
+
raise NotImplementedError
|
|
102
|
+
|
|
103
|
+
def source_if_readable(self, path: Path) -> str:
|
|
104
|
+
raise NotImplementedError
|
|
105
|
+
|
|
106
|
+
def add_to_path(self, directory: Path) -> str:
|
|
107
|
+
"""Prepend ``directory`` to PATH unless it is already there."""
|
|
108
|
+
raise NotImplementedError
|
|
109
|
+
|
|
110
|
+
def wrapper(self, variant: Resolved) -> list[str]:
|
|
111
|
+
"""One wrapper function.
|
|
112
|
+
|
|
113
|
+
DELIBERATE DIVERGENCE from the hand-written wrappers: every wrapper now
|
|
114
|
+
forwards ``"$@"``. A few of them (clean-stale-branches, prune-worktrees)
|
|
115
|
+
used to swallow their arguments, so ``clean-stale-branches --protect=x``
|
|
116
|
+
silently ignored a flag the script documents.
|
|
117
|
+
"""
|
|
118
|
+
assignments = "".join(
|
|
119
|
+
f"{key}={self.quote(value)} " for key, value in sorted(variant.env.items())
|
|
120
|
+
)
|
|
121
|
+
runner = self.env_runner + assignments if assignments else ""
|
|
122
|
+
argv = "".join(f" {self.quote(arg)}" for arg in variant.args)
|
|
123
|
+
call = f'{runner}"$CHE_PYTHON" "$CHE_BIN/{variant.command.script}"{argv} {self.all_args}'
|
|
124
|
+
|
|
125
|
+
lines = [*self.header(variant.name, variant.summary("preview only")), self.indent + call]
|
|
126
|
+
if variant.sound is not None:
|
|
127
|
+
# `$?` is expanded before che_notify runs, so the wrapper still returns
|
|
128
|
+
# the script's exit status. The old wrappers returned playsound's.
|
|
129
|
+
lines.append(f"{self.indent}che_notify {variant.sound} {self.status}")
|
|
130
|
+
return [*lines, self.end]
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class _Posix(Dialect):
|
|
134
|
+
indent = " "
|
|
135
|
+
all_args = '"$@"'
|
|
136
|
+
status = "$?"
|
|
137
|
+
env_runner = ""
|
|
138
|
+
end = "}"
|
|
139
|
+
|
|
140
|
+
@staticmethod
|
|
141
|
+
def quote(text: str) -> str:
|
|
142
|
+
return shlex.quote(text)
|
|
143
|
+
|
|
144
|
+
def header(self, name: str, summary: str) -> list[str]:
|
|
145
|
+
return [f"# {summary}", f"{name}() {{"]
|
|
146
|
+
|
|
147
|
+
def file_head(self, shell: Shell) -> str:
|
|
148
|
+
return f"""{_banner()}
|
|
149
|
+
#
|
|
150
|
+
# Source this file (or let the `che install` block in your rc file do it):
|
|
151
|
+
#
|
|
152
|
+
# . ~/Developer/git/che/{shell.wrapper_file}
|
|
153
|
+
#
|
|
154
|
+
# Every wrapper is a shell function, so `type <name>` shows what it runs and
|
|
155
|
+
# `che doctor` can tell you when something it needs is missing.
|
|
156
|
+
|
|
157
|
+
# CHE_HOME is normally exported by the installed rc block. Fall back to this
|
|
158
|
+
# file's own location so a bare `source` of it also works.
|
|
159
|
+
if [ -z "${{CHE_HOME:-}}" ]; then
|
|
160
|
+
{shell.find_home}
|
|
161
|
+
fi
|
|
162
|
+
{POSIX_EXPORTS}{shell.alias_guard}
|
|
163
|
+
{_stash_call()}
|
|
164
|
+
{POSIX_FUNCTIONS.rstrip()}"""
|
|
165
|
+
|
|
166
|
+
def file_tail(self, shell: Shell) -> str:
|
|
167
|
+
return f"_che_restore_aliases\n\n{shell.load_completions}"
|
|
168
|
+
|
|
169
|
+
def export(self, name: str, word: str) -> str:
|
|
170
|
+
return f"export {name}={word}"
|
|
171
|
+
|
|
172
|
+
def source_if_readable(self, path: Path) -> str:
|
|
173
|
+
quoted = self.quote(str(path))
|
|
174
|
+
return f"[ -r {quoted} ] && . {quoted}"
|
|
175
|
+
|
|
176
|
+
def add_to_path(self, directory: Path) -> str:
|
|
177
|
+
quoted = self.quote(str(directory))
|
|
178
|
+
return f'case ":$PATH:" in *:{quoted}:*) ;; *) PATH={quoted}:"$PATH" ;; esac'
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
# The characters shlex.quote leaves unquoted.
|
|
182
|
+
_SAFE_WORD = re.compile(r"[\w@%+=:,./-]+", re.ASCII)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
class _Fish(Dialect):
|
|
186
|
+
indent = " "
|
|
187
|
+
all_args = "$argv"
|
|
188
|
+
status = "$status"
|
|
189
|
+
env_runner = "env "
|
|
190
|
+
end = "end"
|
|
191
|
+
|
|
192
|
+
@staticmethod
|
|
193
|
+
def quote(text: str) -> str:
|
|
194
|
+
# Bare when shlex would leave it bare too, so `--dry-run` reads the
|
|
195
|
+
# same in every shell's wrapper.
|
|
196
|
+
if _SAFE_WORD.fullmatch(text):
|
|
197
|
+
return text
|
|
198
|
+
return "'" + text.replace("\\", "\\\\").replace("'", "\\'") + "'"
|
|
104
199
|
|
|
200
|
+
def header(self, name: str, summary: str) -> list[str]:
|
|
201
|
+
return [f"function {name} --description {self.quote(summary)}"]
|
|
105
202
|
|
|
106
|
-
def
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
203
|
+
def file_head(self, shell: Shell) -> str:
|
|
204
|
+
return f"{_banner()}\n{FISH_PRELUDE}"
|
|
205
|
+
|
|
206
|
+
def file_tail(self, shell: Shell) -> str:
|
|
207
|
+
return shell.load_completions
|
|
208
|
+
|
|
209
|
+
def export(self, name: str, word: str) -> str:
|
|
210
|
+
return f"set -gx {name} {word}"
|
|
211
|
+
|
|
212
|
+
def source_if_readable(self, path: Path) -> str:
|
|
213
|
+
quoted = self.quote(str(path))
|
|
214
|
+
return f"if test -r {quoted}\n source {quoted}\nend"
|
|
215
|
+
|
|
216
|
+
def add_to_path(self, directory: Path) -> str:
|
|
217
|
+
quoted = self.quote(str(directory))
|
|
218
|
+
return f"if not contains {quoted} $PATH\n set -gx PATH {quoted} $PATH\nend"
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
POSIX = _Posix()
|
|
222
|
+
FISH = _Fish()
|
|
111
223
|
|
|
112
224
|
|
|
113
225
|
# ---------------------------------------------------------------------------
|
|
114
|
-
#
|
|
226
|
+
# The wrapper files
|
|
115
227
|
# ---------------------------------------------------------------------------
|
|
116
228
|
|
|
229
|
+
# What a POSIX wrapper file sets up once it has found CHE_HOME.
|
|
230
|
+
POSIX_EXPORTS = """
|
|
231
|
+
# Exported, not just set: every wrapper expands "$CHE_BIN", and a shell that
|
|
232
|
+
# replays the *functions* without re-reading this file - a `typeset -f` dump, an
|
|
233
|
+
# environment snapshot, any tool that rebuilds a shell from its exported
|
|
234
|
+
# environment - keeps the functions and drops a plain variable, leaving the
|
|
235
|
+
# wrappers to run "/che.py". CHE_HOME comes in exported by the rc block; this
|
|
236
|
+
# keeps everything derived from it exported too.
|
|
237
|
+
export CHE_BIN="${CHE_HOME}/bin"
|
|
117
238
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
forwards ``"$@"``. A few of them (clean-stale-branches, prune-worktrees)
|
|
123
|
-
used to swallow their arguments, so ``clean-stale-branches --protect=x``
|
|
124
|
-
silently ignored a flag the script documents.
|
|
125
|
-
"""
|
|
126
|
-
env, args = command.invocation(dry=dry)
|
|
127
|
-
prefix = "".join(f"{name}={shlex.quote(value)} " for name, value in sorted(env.items()))
|
|
128
|
-
|
|
129
|
-
if command.body:
|
|
130
|
-
call = command.body
|
|
131
|
-
else:
|
|
132
|
-
parts = ['"$CHE_PYTHON"', f'"$CHE_BIN/{command.script}"']
|
|
133
|
-
parts += [shlex.quote(arg) for arg in args]
|
|
134
|
-
parts.append('"$@"')
|
|
135
|
-
call = prefix + " ".join(parts)
|
|
136
|
-
|
|
137
|
-
sound = command.dry_sound if dry else command.sound
|
|
138
|
-
lines = [f" {call}"]
|
|
139
|
-
if sound is not None:
|
|
140
|
-
# `$?` is expanded before che_notify runs, so the wrapper still returns
|
|
141
|
-
# the script's exit status. The old wrappers returned playsound's.
|
|
142
|
-
lines.append(f" che_notify {sound} $?")
|
|
143
|
-
return lines
|
|
239
|
+
# Any python3 on PATH by default. `che install` records the interpreter it
|
|
240
|
+
# verified (3.12+) in the rc block, which wins over this.
|
|
241
|
+
: "${CHE_PYTHON:=python3}"
|
|
242
|
+
export CHE_PYTHON
|
|
144
243
|
|
|
244
|
+
# Legacy name from the hand-maintained wrappers, kept so an older snippet in
|
|
245
|
+
# ~/.zshrc that still refers to it keeps resolving.
|
|
246
|
+
export SHELL_FUNCTIONS_BIN="${CHE_BIN}"
|
|
247
|
+
"""
|
|
145
248
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
249
|
+
# The two functions every wrapper relies on. They come after the alias guard:
|
|
250
|
+
# an alias named `che` would otherwise break `che() {` exactly the way the
|
|
251
|
+
# guard describes, and take the rest of the file with it.
|
|
252
|
+
POSIX_FUNCTIONS = """
|
|
253
|
+
# Play the completion sound for a wrapper, then return the status it was given.
|
|
254
|
+
#
|
|
255
|
+
# playsound-N is defined outside this repo. Every wrapper used to call it
|
|
256
|
+
# unconditionally, which printed "command not found" on a machine that never
|
|
257
|
+
# had it; here a missing playsound is simply skipped. Set CHE_SOUNDS=0 to
|
|
258
|
+
# silence them all.
|
|
259
|
+
che_notify() {
|
|
260
|
+
if [ "${CHE_SOUNDS:-1}" != "0" ] && command -v "playsound-$1" >/dev/null 2>&1; then
|
|
261
|
+
"playsound-$1" >/dev/null 2>&1
|
|
262
|
+
fi
|
|
263
|
+
return "${2:-0}"
|
|
264
|
+
}
|
|
160
265
|
|
|
266
|
+
# The dispatcher. `che` with no arguments opens the interactive menu.
|
|
267
|
+
che() {
|
|
268
|
+
if [ "$#" -eq 0 ]; then
|
|
269
|
+
"$CHE_PYTHON" "$CHE_BIN/che.py"
|
|
270
|
+
else
|
|
271
|
+
"$CHE_PYTHON" "$CHE_BIN/che.py" "$@"
|
|
272
|
+
che_notify 7 $?
|
|
273
|
+
fi
|
|
274
|
+
}
|
|
275
|
+
"""
|
|
161
276
|
|
|
162
277
|
ZSH_ALIAS_GUARD = """
|
|
163
278
|
# An alias sharing a name with a wrapper below is fatal, not cosmetic: zsh
|
|
@@ -217,19 +332,60 @@ _che_restore_aliases() {
|
|
|
217
332
|
}
|
|
218
333
|
"""
|
|
219
334
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
335
|
+
# fish needs no alias guard, so its whole setup is one block. No `cd` in it:
|
|
336
|
+
# fish runs command substitution in the current process, so `(cd ...; and pwd)`
|
|
337
|
+
# would move the user's shell as a side effect.
|
|
338
|
+
FISH_PRELUDE = """
|
|
339
|
+
if not set -q CHE_HOME
|
|
340
|
+
set -l __che_dir (dirname (status --current-filename))/..
|
|
341
|
+
if type -q path
|
|
342
|
+
set -gx CHE_HOME (path resolve $__che_dir)
|
|
343
|
+
else
|
|
344
|
+
set -gx CHE_HOME $__che_dir
|
|
345
|
+
end
|
|
346
|
+
set -e __che_dir
|
|
347
|
+
end
|
|
348
|
+
set -gx CHE_BIN "$CHE_HOME/bin"
|
|
349
|
+
if not set -q CHE_PYTHON
|
|
350
|
+
set -gx CHE_PYTHON python3
|
|
351
|
+
end
|
|
352
|
+
set -gx SHELL_FUNCTIONS_BIN "$CHE_BIN"
|
|
353
|
+
|
|
354
|
+
# Play a wrapper's completion sound, then return the status it was given.
|
|
355
|
+
function che_notify --description 'che: completion sound'
|
|
356
|
+
set -l code 0
|
|
357
|
+
if set -q argv[2]
|
|
358
|
+
set code $argv[2]
|
|
359
|
+
end
|
|
360
|
+
if test "$CHE_SOUNDS" != "0"
|
|
361
|
+
if type -q "playsound-$argv[1]"
|
|
362
|
+
"playsound-$argv[1]" >/dev/null 2>&1
|
|
363
|
+
end
|
|
364
|
+
end
|
|
365
|
+
return $code
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
function che --description 'che: shell helpers dispatcher'
|
|
369
|
+
if test (count $argv) -eq 0
|
|
370
|
+
"$CHE_PYTHON" "$CHE_BIN/che.py"
|
|
371
|
+
else
|
|
372
|
+
"$CHE_PYTHON" "$CHE_BIN/che.py" $argv
|
|
373
|
+
che_notify 7 $status
|
|
374
|
+
end
|
|
375
|
+
end"""
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def defined_names() -> list[str]:
|
|
379
|
+
"""Every function name a wrapper file defines: the sound helper, the
|
|
380
|
+
dispatcher, then every wrapper."""
|
|
381
|
+
return ["che_notify", "che", *wrapper_names()]
|
|
226
382
|
|
|
227
383
|
|
|
228
384
|
def _stash_call() -> str:
|
|
229
|
-
"""`_che_stash_aliases`
|
|
385
|
+
"""`_che_stash_aliases` for every name the file defines, wrapped to stay readable."""
|
|
230
386
|
lines: list[str] = []
|
|
231
387
|
current = "_che_stash_aliases"
|
|
232
|
-
for name in
|
|
388
|
+
for name in defined_names():
|
|
233
389
|
candidate = f"{current} {name}"
|
|
234
390
|
if len(candidate) > 76:
|
|
235
391
|
lines.append(f"{current} \\")
|
|
@@ -240,197 +396,23 @@ def _stash_call() -> str:
|
|
|
240
396
|
return "\n".join(lines)
|
|
241
397
|
|
|
242
398
|
|
|
243
|
-
def
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
}[shell]
|
|
248
|
-
|
|
249
|
-
head = f"""{_banner()}
|
|
250
|
-
#
|
|
251
|
-
# Source this file (or let the `che install` block in your rc file do it):
|
|
252
|
-
#
|
|
253
|
-
# . {"~/Developer/git/che/shell/che." + shell}
|
|
254
|
-
#
|
|
255
|
-
# Every wrapper is a shell function, so `type <name>` shows what it runs and
|
|
256
|
-
# `che doctor` can tell you when something it needs is missing.
|
|
257
|
-
|
|
258
|
-
# CHE_HOME is normally exported by the installed rc block. Fall back to this
|
|
259
|
-
# file's own location so a bare `source` of it also works.
|
|
260
|
-
if [ -z "${{CHE_HOME:-}}" ]; then
|
|
261
|
-
{resolve_self}
|
|
262
|
-
fi
|
|
263
|
-
|
|
264
|
-
# Exported, not just set: every wrapper expands "$CHE_BIN", and a shell that
|
|
265
|
-
# replays the *functions* without re-reading this file - a `typeset -f` dump, an
|
|
266
|
-
# environment snapshot, any tool that rebuilds a shell from its exported
|
|
267
|
-
# environment - keeps the functions and drops a plain variable, leaving the
|
|
268
|
-
# wrappers to run "/che.py". CHE_HOME comes in exported by the rc block; this
|
|
269
|
-
# keeps everything derived from it exported too.
|
|
270
|
-
export CHE_BIN="${{CHE_HOME}}/bin"
|
|
271
|
-
|
|
272
|
-
# Any python3 on PATH by default. `che install` records the interpreter it
|
|
273
|
-
# verified (3.12+) in the rc block, which wins over this.
|
|
274
|
-
: "${{CHE_PYTHON:=python3}}"
|
|
275
|
-
export CHE_PYTHON
|
|
276
|
-
|
|
277
|
-
# Legacy name from the hand-maintained wrappers, kept so an older snippet in
|
|
278
|
-
# ~/.zshrc that still refers to it keeps resolving.
|
|
279
|
-
export SHELL_FUNCTIONS_BIN="${{CHE_BIN}}"
|
|
280
|
-
|
|
281
|
-
# Play the completion sound for a wrapper, then return the status it was given.
|
|
282
|
-
#
|
|
283
|
-
# playsound-N is defined outside this repo. Every wrapper used to call it
|
|
284
|
-
# unconditionally, which printed "command not found" on a machine that never
|
|
285
|
-
# had it; here a missing playsound is simply skipped. Set CHE_SOUNDS=0 to
|
|
286
|
-
# silence them all.
|
|
287
|
-
che_notify() {{
|
|
288
|
-
if [ "${{CHE_SOUNDS:-1}}" = "1" ] && command -v "playsound-$1" >/dev/null 2>&1; then
|
|
289
|
-
"playsound-$1" >/dev/null 2>&1
|
|
290
|
-
fi
|
|
291
|
-
return "${{2:-0}}"
|
|
292
|
-
}}
|
|
293
|
-
|
|
294
|
-
# The dispatcher. `che` with no arguments opens the interactive menu.
|
|
295
|
-
che() {{
|
|
296
|
-
if [ "$#" -eq 0 ]; then
|
|
297
|
-
"$CHE_PYTHON" "$CHE_BIN/che.py"
|
|
298
|
-
else
|
|
299
|
-
"$CHE_PYTHON" "$CHE_BIN/che.py" "$@"
|
|
300
|
-
che_notify 7 $?
|
|
301
|
-
fi
|
|
302
|
-
}}"""
|
|
303
|
-
|
|
304
|
-
tail_by_shell = {
|
|
305
|
-
"zsh": """
|
|
306
|
-
# Completions. compdef only exists once compinit has run; when it has not,
|
|
307
|
-
# skip quietly rather than erroring at shell start.
|
|
308
|
-
if whence -w compdef >/dev/null 2>&1 && [ -r "${CHE_HOME}/shell/completions/_che" ]; then
|
|
309
|
-
. "${CHE_HOME}/shell/completions/_che"
|
|
310
|
-
fi""",
|
|
311
|
-
"bash": """
|
|
312
|
-
# Completions, when this is bash and the completion builtin is available.
|
|
313
|
-
if [ -n "${BASH_VERSION:-}" ] && [ -r "${CHE_HOME}/shell/completions/che.bash" ]; then
|
|
314
|
-
. "${CHE_HOME}/shell/completions/che.bash"
|
|
315
|
-
fi""",
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
guard = {"zsh": ZSH_ALIAS_GUARD, "bash": BASH_ALIAS_GUARD}[shell]
|
|
319
|
-
|
|
320
|
-
return "\n".join(
|
|
321
|
-
[
|
|
322
|
-
head,
|
|
323
|
-
guard.rstrip("\n"),
|
|
324
|
-
"",
|
|
325
|
-
_stash_call(),
|
|
326
|
-
*_posix_wrappers(),
|
|
327
|
-
"",
|
|
328
|
-
"_che_restore_aliases",
|
|
329
|
-
"",
|
|
330
|
-
tail_by_shell[shell].strip("\n"),
|
|
331
|
-
"",
|
|
332
|
-
]
|
|
333
|
-
)
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
# ---------------------------------------------------------------------------
|
|
337
|
-
# fish
|
|
338
|
-
# ---------------------------------------------------------------------------
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
def _fish_quote(text: str) -> str:
|
|
342
|
-
return "'" + text.replace("\\", "\\\\").replace("'", "\\'") + "'"
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
def _render_fish() -> str:
|
|
346
|
-
lines = [
|
|
347
|
-
_banner(),
|
|
348
|
-
"",
|
|
349
|
-
# No `cd` here: fish runs command substitution in the current process,
|
|
350
|
-
# so `(cd ...; and pwd)` would move the user's shell as a side effect.
|
|
351
|
-
"if not set -q CHE_HOME",
|
|
352
|
-
" set -l __che_dir (dirname (status --current-filename))/..",
|
|
353
|
-
" if type -q path",
|
|
354
|
-
" set -gx CHE_HOME (path resolve $__che_dir)",
|
|
355
|
-
" else",
|
|
356
|
-
" set -gx CHE_HOME $__che_dir",
|
|
357
|
-
" end",
|
|
358
|
-
" set -e __che_dir",
|
|
359
|
-
"end",
|
|
360
|
-
'set -gx CHE_BIN "$CHE_HOME/bin"',
|
|
361
|
-
"if not set -q CHE_PYTHON",
|
|
362
|
-
" set -gx CHE_PYTHON python3",
|
|
363
|
-
"end",
|
|
364
|
-
'set -gx SHELL_FUNCTIONS_BIN "$CHE_BIN"',
|
|
365
|
-
"",
|
|
366
|
-
"# Play a wrapper's completion sound, then return the status it was given.",
|
|
367
|
-
"function che_notify --description 'che: completion sound'",
|
|
368
|
-
" set -l code 0",
|
|
369
|
-
" if set -q argv[2]",
|
|
370
|
-
" set code $argv[2]",
|
|
371
|
-
" end",
|
|
372
|
-
' if test "$CHE_SOUNDS" != "0"',
|
|
373
|
-
' if type -q "playsound-$argv[1]"',
|
|
374
|
-
' "playsound-$argv[1]" >/dev/null 2>&1',
|
|
375
|
-
" end",
|
|
376
|
-
" end",
|
|
377
|
-
" return $code",
|
|
378
|
-
"end",
|
|
379
|
-
"",
|
|
380
|
-
"function che --description 'che: shell helpers dispatcher'",
|
|
381
|
-
" if test (count $argv) -eq 0",
|
|
382
|
-
' "$CHE_PYTHON" "$CHE_BIN/che.py"',
|
|
383
|
-
" else",
|
|
384
|
-
' "$CHE_PYTHON" "$CHE_BIN/che.py" $argv',
|
|
385
|
-
" che_notify 7 $status",
|
|
386
|
-
" end",
|
|
387
|
-
"end",
|
|
388
|
-
]
|
|
389
|
-
|
|
390
|
-
for category in CATEGORIES:
|
|
391
|
-
members = [command for command in COMMANDS if command.category == category.key]
|
|
392
|
-
if not members:
|
|
393
|
-
continue
|
|
399
|
+
def _wrapper_section(dialect: Dialect) -> list[str]:
|
|
400
|
+
"""Every wrapper, grouped under a banner per category."""
|
|
401
|
+
lines: list[str] = []
|
|
402
|
+
for category, members in grouped(COMMANDS):
|
|
394
403
|
lines += ["", f"# {'-' * 74}", f"# {category.title} - {category.blurb}", f"# {'-' * 74}"]
|
|
395
|
-
for
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
summary = command.summary + (" (preview only)" if dry else "")
|
|
399
|
-
lines += ["", f"function {name} --description {_fish_quote(summary)}"]
|
|
400
|
-
|
|
401
|
-
if command.body:
|
|
402
|
-
# fish has no `$1`; the sh body's positional becomes $argv[1].
|
|
403
|
-
lines.append(" " + command.body.replace("$1", "$argv[1]"))
|
|
404
|
-
else:
|
|
405
|
-
prefix = "".join(f"{key}={value} " for key, value in sorted(env.items()))
|
|
406
|
-
argv = "".join(f" {shlex.quote(arg)}" for arg in args)
|
|
407
|
-
runner = "env " + prefix if prefix else ""
|
|
408
|
-
lines.append(
|
|
409
|
-
f' {runner}"$CHE_PYTHON" "$CHE_BIN/{command.script}"{argv} $argv'
|
|
410
|
-
)
|
|
411
|
-
|
|
412
|
-
sound = command.dry_sound if dry else command.sound
|
|
413
|
-
if sound is not None:
|
|
414
|
-
lines.append(f" che_notify {sound} $status")
|
|
415
|
-
lines.append("end")
|
|
416
|
-
|
|
417
|
-
lines += [
|
|
418
|
-
"",
|
|
419
|
-
'if test -r "$CHE_HOME/shell/completions/che.fish"',
|
|
420
|
-
' source "$CHE_HOME/shell/completions/che.fish"',
|
|
421
|
-
"end",
|
|
422
|
-
"",
|
|
423
|
-
]
|
|
424
|
-
return "\n".join(lines)
|
|
404
|
+
for variant in variants(members):
|
|
405
|
+
lines += ["", *dialect.wrapper(variant)]
|
|
406
|
+
return lines
|
|
425
407
|
|
|
426
408
|
|
|
427
409
|
def wrappers(shell: str) -> str:
|
|
428
410
|
"""The generated wrapper file for ``shell``."""
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
411
|
+
spec = _shell(shell)
|
|
412
|
+
dialect = spec.dialect
|
|
413
|
+
return "\n".join(
|
|
414
|
+
[dialect.file_head(spec), *_wrapper_section(dialect), "", dialect.file_tail(spec), ""]
|
|
415
|
+
)
|
|
434
416
|
|
|
435
417
|
|
|
436
418
|
# ---------------------------------------------------------------------------
|
|
@@ -438,25 +420,14 @@ def wrappers(shell: str) -> str:
|
|
|
438
420
|
# ---------------------------------------------------------------------------
|
|
439
421
|
|
|
440
422
|
|
|
441
|
-
def _all_completable() -> list[tuple[str, Command, bool]]:
|
|
442
|
-
entries: list[tuple[str, Command, bool]] = []
|
|
443
|
-
for command in (*COMMANDS, *BUILTINS):
|
|
444
|
-
for name, dry in _dry_variants(command):
|
|
445
|
-
entries.append((name, command, dry))
|
|
446
|
-
return entries
|
|
447
|
-
|
|
448
|
-
|
|
449
423
|
def _zsh_completions() -> str:
|
|
450
|
-
describe = [
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
cases.append(f" {name}) print -r -- {shlex.quote(' '.join(_flag_words(command)))} ;;")
|
|
458
|
-
|
|
459
|
-
wrapper_names = " ".join(name for name, command, _ in _all_completable() if not command.builtin)
|
|
424
|
+
describe = [
|
|
425
|
+
" " + shlex.quote(f"{v.name}:{v.summary('preview')}") for v in variants(all_commands())
|
|
426
|
+
]
|
|
427
|
+
cases = [
|
|
428
|
+
f" {v.name}) print -r -- {shlex.quote(' '.join(_flag_words(v.command)))} ;;"
|
|
429
|
+
for v in variants(all_commands())
|
|
430
|
+
]
|
|
460
431
|
|
|
461
432
|
return f"""#compdef che
|
|
462
433
|
{_banner()}
|
|
@@ -502,17 +473,16 @@ _che_wrapper() {{
|
|
|
502
473
|
}}
|
|
503
474
|
|
|
504
475
|
compdef _che che
|
|
505
|
-
compdef _che_wrapper {wrapper_names}
|
|
476
|
+
compdef _che_wrapper {" ".join(wrapper_names())}
|
|
506
477
|
"""
|
|
507
478
|
|
|
508
479
|
|
|
509
480
|
def _bash_completions() -> str:
|
|
510
|
-
cases = [
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
names = " ".join(name for
|
|
515
|
-
wrapper_names = " ".join(name for name, command, _ in _all_completable() if not command.builtin)
|
|
481
|
+
cases = [
|
|
482
|
+
f' {v.name}) echo "{" ".join(_flag_words(v.command))}" ;;'
|
|
483
|
+
for v in variants(all_commands())
|
|
484
|
+
]
|
|
485
|
+
names = " ".join(v.name for v in variants(all_commands()))
|
|
516
486
|
|
|
517
487
|
return f"""{_banner()}
|
|
518
488
|
# shellcheck shell=bash
|
|
@@ -568,52 +538,122 @@ _che_complete_wrapper() {{
|
|
|
568
538
|
}}
|
|
569
539
|
|
|
570
540
|
complete -o nospace -F _che_complete che
|
|
571
|
-
complete -o nospace -F _che_complete_wrapper {wrapper_names}
|
|
541
|
+
complete -o nospace -F _che_complete_wrapper {" ".join(wrapper_names())}
|
|
572
542
|
"""
|
|
573
543
|
|
|
574
544
|
|
|
545
|
+
def _fish_option(flag: str) -> str:
|
|
546
|
+
"""`-l name`, plus `-r` when the flag takes a value and `-F` when that value is a path."""
|
|
547
|
+
spec = f"-l {flag.removeprefix('--').removesuffix('=')}"
|
|
548
|
+
if flag.endswith("="):
|
|
549
|
+
spec += " -r"
|
|
550
|
+
if flag == "--path=":
|
|
551
|
+
spec += " -F"
|
|
552
|
+
return spec
|
|
553
|
+
|
|
554
|
+
|
|
575
555
|
def _fish_completions() -> str:
|
|
576
|
-
lines = [_banner(), ""]
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
lines.append(
|
|
581
|
-
f"complete -c che -n __fish_use_subcommand -a {name} -d {_fish_quote(summary)}"
|
|
582
|
-
)
|
|
556
|
+
lines = [_banner(), "", "complete -c che -f"]
|
|
557
|
+
for v in variants(all_commands()):
|
|
558
|
+
description = FISH.quote(v.summary("preview"))
|
|
559
|
+
lines.append(f"complete -c che -n __fish_use_subcommand -a {v.name} -d {description}")
|
|
583
560
|
|
|
584
561
|
lines.append("")
|
|
585
|
-
for
|
|
586
|
-
for flag in command.flags:
|
|
587
|
-
option = flag.removeprefix("--")
|
|
588
|
-
takes_value = " -r" if flag in VALUE_FLAGS else ""
|
|
589
|
-
paths = " -F" if flag in PATH_FLAGS else ""
|
|
562
|
+
for v in variants(all_commands()):
|
|
563
|
+
for flag in v.command.flags:
|
|
590
564
|
lines.append(
|
|
591
|
-
f'complete -c che -n "__fish_seen_subcommand_from {name}" '
|
|
592
|
-
f"-l {option}{takes_value}{paths}"
|
|
565
|
+
f'complete -c che -n "__fish_seen_subcommand_from {v.name}" {_fish_option(flag)}'
|
|
593
566
|
)
|
|
594
567
|
|
|
595
568
|
lines.append("")
|
|
596
|
-
for
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
for flag in command.flags:
|
|
600
|
-
option = flag.removeprefix("--")
|
|
601
|
-
takes_value = " -r" if flag in VALUE_FLAGS else ""
|
|
602
|
-
paths = " -F" if flag in PATH_FLAGS else ""
|
|
603
|
-
lines.append(f"complete -c {name} -l {option}{takes_value}{paths}")
|
|
569
|
+
for v in variants(COMMANDS):
|
|
570
|
+
for flag in v.command.flags:
|
|
571
|
+
lines.append(f"complete -c {v.name} {_fish_option(flag)}")
|
|
604
572
|
|
|
605
573
|
lines.append("")
|
|
606
574
|
return "\n".join(lines)
|
|
607
575
|
|
|
608
576
|
|
|
577
|
+
# ---------------------------------------------------------------------------
|
|
578
|
+
# Shells
|
|
579
|
+
# ---------------------------------------------------------------------------
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
@dataclass(frozen=True)
|
|
583
|
+
class Shell:
|
|
584
|
+
"""Everything that differs between the shells che generates files for."""
|
|
585
|
+
|
|
586
|
+
name: str
|
|
587
|
+
dialect: Dialect
|
|
588
|
+
wrapper_file: Path # relative to the repo root
|
|
589
|
+
completion_file: Path
|
|
590
|
+
completions: Callable[[], str]
|
|
591
|
+
load_completions: str # the end of the wrapper file
|
|
592
|
+
# POSIX only: how a sourced file finds the repo, and the alias guard
|
|
593
|
+
# around the definitions.
|
|
594
|
+
find_home: str = ""
|
|
595
|
+
alias_guard: str = ""
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
_SHELLS: dict[str, Shell] = {
|
|
599
|
+
shell.name: shell
|
|
600
|
+
for shell in (
|
|
601
|
+
Shell(
|
|
602
|
+
name="zsh",
|
|
603
|
+
dialect=POSIX,
|
|
604
|
+
wrapper_file=Path("shell/che.zsh"),
|
|
605
|
+
completion_file=Path("shell/completions/_che"),
|
|
606
|
+
completions=_zsh_completions,
|
|
607
|
+
find_home='CHE_HOME="${${(%):-%x}:A:h:h}"',
|
|
608
|
+
alias_guard=ZSH_ALIAS_GUARD,
|
|
609
|
+
load_completions="""\
|
|
610
|
+
# Completions. compdef only exists once compinit has run; when it has not,
|
|
611
|
+
# skip quietly rather than erroring at shell start.
|
|
612
|
+
if whence -w compdef >/dev/null 2>&1 && [ -r "${CHE_HOME}/shell/completions/_che" ]; then
|
|
613
|
+
. "${CHE_HOME}/shell/completions/_che"
|
|
614
|
+
fi""",
|
|
615
|
+
),
|
|
616
|
+
Shell(
|
|
617
|
+
name="bash",
|
|
618
|
+
dialect=POSIX,
|
|
619
|
+
wrapper_file=Path("shell/che.bash"),
|
|
620
|
+
completion_file=Path("shell/completions/che.bash"),
|
|
621
|
+
completions=_bash_completions,
|
|
622
|
+
find_home='CHE_HOME="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"',
|
|
623
|
+
alias_guard=BASH_ALIAS_GUARD,
|
|
624
|
+
load_completions="""\
|
|
625
|
+
# Completions, when this is bash and the completion builtin is available.
|
|
626
|
+
if [ -n "${BASH_VERSION:-}" ] && [ -r "${CHE_HOME}/shell/completions/che.bash" ]; then
|
|
627
|
+
. "${CHE_HOME}/shell/completions/che.bash"
|
|
628
|
+
fi""",
|
|
629
|
+
),
|
|
630
|
+
Shell(
|
|
631
|
+
name="fish",
|
|
632
|
+
dialect=FISH,
|
|
633
|
+
wrapper_file=Path("shell/che.fish"),
|
|
634
|
+
completion_file=Path("shell/completions/che.fish"),
|
|
635
|
+
completions=_fish_completions,
|
|
636
|
+
load_completions="""\
|
|
637
|
+
if test -r "$CHE_HOME/shell/completions/che.fish"
|
|
638
|
+
source "$CHE_HOME/shell/completions/che.fish"
|
|
639
|
+
end""",
|
|
640
|
+
),
|
|
641
|
+
)
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
# The shell flavors there are generated files for: `machine.ShellSpec.flavor`.
|
|
645
|
+
FLAVORS = tuple(_SHELLS)
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
def _shell(name: str) -> Shell:
|
|
649
|
+
try:
|
|
650
|
+
return _SHELLS[name]
|
|
651
|
+
except KeyError:
|
|
652
|
+
raise ValueError(f"unsupported shell: {name}") from None
|
|
653
|
+
|
|
654
|
+
|
|
609
655
|
def completions(shell: str) -> str:
|
|
610
|
-
|
|
611
|
-
return _zsh_completions()
|
|
612
|
-
if shell == "bash":
|
|
613
|
-
return _bash_completions()
|
|
614
|
-
if shell == "fish":
|
|
615
|
-
return _fish_completions()
|
|
616
|
-
raise ValueError(f"unsupported shell: {shell}")
|
|
656
|
+
return _shell(shell).completions()
|
|
617
657
|
|
|
618
658
|
|
|
619
659
|
# ---------------------------------------------------------------------------
|
|
@@ -621,45 +661,31 @@ def completions(shell: str) -> str:
|
|
|
621
661
|
# ---------------------------------------------------------------------------
|
|
622
662
|
|
|
623
663
|
|
|
624
|
-
def rc_block(
|
|
664
|
+
def rc_block(
|
|
665
|
+
shell: str, *, home: Path, python: str, sounds: bool = True, path_dir: Path | None = None
|
|
666
|
+
) -> str:
|
|
625
667
|
"""The machine-specific snippet ``che install`` writes into an rc file.
|
|
626
668
|
|
|
627
|
-
Deliberately tiny: a path, an interpreter and a source line
|
|
628
|
-
that
|
|
629
|
-
|
|
669
|
+
Deliberately tiny: a path, an interpreter and a source line, plus the line
|
|
670
|
+
that puts ``path_dir`` (where the ``che`` shim lives) on PATH when given.
|
|
671
|
+
Everything that can be generic lives in the committed ``shell/che.*`` file
|
|
672
|
+
instead, so upgrading che never means rewriting the user's rc file.
|
|
630
673
|
"""
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
body = [
|
|
635
|
-
BEGIN_MARKER,
|
|
636
|
-
f"# Managed by `che install` (che {VERSION}). Edits here are overwritten.",
|
|
637
|
-
f"set -gx CHE_HOME {_fish_quote(str(home))}",
|
|
638
|
-
f"set -gx CHE_PYTHON {_fish_quote(python)}",
|
|
639
|
-
]
|
|
640
|
-
if not sounds:
|
|
641
|
-
body.append("set -gx CHE_SOUNDS 0")
|
|
642
|
-
body += [
|
|
643
|
-
f"if test -r {_fish_quote(str(wrapper))}",
|
|
644
|
-
f" source {_fish_quote(str(wrapper))}",
|
|
645
|
-
"end",
|
|
646
|
-
END_MARKER,
|
|
647
|
-
]
|
|
648
|
-
return "\n".join(body) + "\n"
|
|
649
|
-
|
|
650
|
-
body = [
|
|
674
|
+
spec = _shell(shell)
|
|
675
|
+
dialect = spec.dialect
|
|
676
|
+
lines = [
|
|
651
677
|
BEGIN_MARKER,
|
|
652
678
|
f"# Managed by `che install` (che {VERSION}). Edits here are overwritten.",
|
|
653
|
-
|
|
654
|
-
|
|
679
|
+
dialect.export("CHE_HOME", dialect.quote(str(home))),
|
|
680
|
+
dialect.export("CHE_PYTHON", dialect.quote(python)),
|
|
655
681
|
]
|
|
656
682
|
if not sounds:
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
return "\n".join(
|
|
683
|
+
lines.append(dialect.export("CHE_SOUNDS", "0"))
|
|
684
|
+
lines.append(dialect.source_if_readable(home / spec.wrapper_file))
|
|
685
|
+
if path_dir is not None:
|
|
686
|
+
lines.append(dialect.add_to_path(path_dir))
|
|
687
|
+
lines.append(END_MARKER)
|
|
688
|
+
return "\n".join(lines) + "\n"
|
|
663
689
|
|
|
664
690
|
|
|
665
691
|
def legacy_zshrc() -> str:
|
|
@@ -670,25 +696,18 @@ def legacy_zshrc() -> str:
|
|
|
670
696
|
sourcing this file keeps working; the wrappers themselves now come from
|
|
671
697
|
``shell/che.zsh``.
|
|
672
698
|
"""
|
|
673
|
-
return
|
|
699
|
+
return (
|
|
700
|
+
_banner()
|
|
701
|
+
+ """
|
|
674
702
|
#
|
|
675
703
|
# The wrappers moved to shell/che.zsh and are generated from bin/commands.py.
|
|
676
704
|
# `che install` writes a block into ~/.zshrc that sources them; this file
|
|
677
705
|
# stays so that an existing `source .../shell-functions/.zshrc` keeps working.
|
|
678
706
|
|
|
679
|
-
CHE_HOME="${
|
|
680
|
-
. "${
|
|
707
|
+
CHE_HOME="${CHE_HOME:-${${(%):-%x}:A:h}}"
|
|
708
|
+
. "${CHE_HOME}/shell/che.zsh"
|
|
681
709
|
"""
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
def generated_files() -> dict[Path, str]:
|
|
685
|
-
"""Every generated path mapped to its expected contents, relative to the repo."""
|
|
686
|
-
files: dict[Path, str] = {}
|
|
687
|
-
for shell in SHELLS:
|
|
688
|
-
files[WRAPPER_FILES[shell]] = wrappers(shell)
|
|
689
|
-
files[COMPLETION_FILES[shell]] = completions(shell)
|
|
690
|
-
files[Path(".zshrc")] = legacy_zshrc()
|
|
691
|
-
return files
|
|
710
|
+
)
|
|
692
711
|
|
|
693
712
|
|
|
694
713
|
# ---------------------------------------------------------------------------
|
|
@@ -707,54 +726,52 @@ def _marker(name: str) -> tuple[str, str]:
|
|
|
707
726
|
return f"<!-- che:{name} -->", f"<!-- /che:{name} -->"
|
|
708
727
|
|
|
709
728
|
|
|
729
|
+
def _md_row(*cells: str) -> str:
|
|
730
|
+
return f"| {' | '.join(cells)} |"
|
|
731
|
+
|
|
732
|
+
|
|
733
|
+
def _md_table(headers: Sequence[str], rows: Iterable[str]) -> str:
|
|
734
|
+
return "\n".join([_md_row(*headers), _md_row(*["---"] * len(headers)), *rows])
|
|
735
|
+
|
|
736
|
+
|
|
710
737
|
def _commands_table() -> str:
|
|
711
|
-
rows = [
|
|
712
|
-
for category in
|
|
713
|
-
members = [command for command in COMMANDS if command.category == category.key]
|
|
714
|
-
if not members:
|
|
715
|
-
continue
|
|
738
|
+
rows = []
|
|
739
|
+
for category, members in grouped(COMMANDS):
|
|
716
740
|
rows.append(f"| **{category.title}** | *{category.blurb}* | |")
|
|
717
741
|
for command in members:
|
|
718
|
-
preview =
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
preview = "`--dry-run`"
|
|
723
|
-
if not preview and command.no_preview_reason:
|
|
724
|
-
preview = "none"
|
|
725
|
-
rows.append(f"| `{command.name}` | {command.summary} | {preview} |")
|
|
726
|
-
return "\n".join(rows)
|
|
742
|
+
preview = command.preview
|
|
743
|
+
cell = f"`{preview}`" if preview else ("none" if command.no_preview_reason else "")
|
|
744
|
+
rows.append(_md_row(f"`{command.name}`", command.summary, cell))
|
|
745
|
+
return _md_table(("Command", "Does", "Preview"), rows)
|
|
727
746
|
|
|
728
747
|
|
|
729
748
|
def _builtins_table() -> str:
|
|
730
|
-
rows =
|
|
731
|
-
|
|
732
|
-
rows.append(f"| `che {command.name}` | {command.summary} |")
|
|
733
|
-
return "\n".join(rows)
|
|
749
|
+
rows = (_md_row(f"`che {command.name}`", command.summary) for command in BUILTINS)
|
|
750
|
+
return _md_table(("Command", "Does"), rows)
|
|
734
751
|
|
|
735
752
|
|
|
736
753
|
def _keys_table() -> str:
|
|
737
|
-
rows =
|
|
738
|
-
|
|
739
|
-
rows.append(f"| `{key}` | {description} |")
|
|
740
|
-
return "\n".join(rows)
|
|
754
|
+
rows = (_md_row(f"`{key}`", description) for key, description in MENU_KEYS)
|
|
755
|
+
return _md_table(("Key", "Does"), rows)
|
|
741
756
|
|
|
742
757
|
|
|
743
758
|
def _tools_table() -> str:
|
|
744
|
-
rows = [
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
759
|
+
rows = [
|
|
760
|
+
_md_row(f"`{binary}`", ", ".join(f"`{name}`" for name in users))
|
|
761
|
+
for binary, users in required_binaries().items()
|
|
762
|
+
]
|
|
763
|
+
rows += [
|
|
764
|
+
_md_row("(not a binary)", f"`{command.name}` {command.setup}")
|
|
765
|
+
for command in COMMANDS
|
|
766
|
+
if command.setup
|
|
767
|
+
]
|
|
768
|
+
return _md_table(("Tool", "Needed by"), rows)
|
|
753
769
|
|
|
754
770
|
|
|
755
771
|
def _banner_island() -> str:
|
|
756
|
-
"""The face beside the wordmark
|
|
757
|
-
|
|
772
|
+
"""The face beside the wordmark: the terminal banner without its colour,
|
|
773
|
+
indent and subtitle."""
|
|
774
|
+
rows = art.compose(art.FACE_LINES, art.LOGO_LINES)
|
|
758
775
|
return "```text\n" + "\n".join(rows) + "\n```"
|
|
759
776
|
|
|
760
777
|
|
|
@@ -788,3 +805,58 @@ def render_readme(current: str) -> str:
|
|
|
788
805
|
_, _, tail = rest.partition(end)
|
|
789
806
|
updated = f"{head}{begin}\n{body}\n{end}{tail}"
|
|
790
807
|
return updated
|
|
808
|
+
|
|
809
|
+
|
|
810
|
+
# ---------------------------------------------------------------------------
|
|
811
|
+
# Writing it all out
|
|
812
|
+
# ---------------------------------------------------------------------------
|
|
813
|
+
|
|
814
|
+
|
|
815
|
+
def expected_files() -> dict[Path, str]:
|
|
816
|
+
"""Every generated path, relative to the repo, mapped to the contents the
|
|
817
|
+
manifest implies.
|
|
818
|
+
|
|
819
|
+
``shell/`` is generated whole. The README is generated in place: only the
|
|
820
|
+
marked islands are replaced, so its prose survives.
|
|
821
|
+
"""
|
|
822
|
+
expected: dict[Path, str] = {}
|
|
823
|
+
for spec in _SHELLS.values():
|
|
824
|
+
expected[spec.wrapper_file] = wrappers(spec.name)
|
|
825
|
+
expected[spec.completion_file] = spec.completions()
|
|
826
|
+
expected[Path(".zshrc")] = legacy_zshrc()
|
|
827
|
+
|
|
828
|
+
readme = REPO / README
|
|
829
|
+
if readme.exists():
|
|
830
|
+
expected[README] = render_readme(readme.read_text(encoding="utf-8"))
|
|
831
|
+
|
|
832
|
+
return expected
|
|
833
|
+
|
|
834
|
+
|
|
835
|
+
def sync_generated(*, dry_run: bool) -> list[Path]:
|
|
836
|
+
"""Bring the generated files up to date; returns the ones that were stale.
|
|
837
|
+
|
|
838
|
+
With ``dry_run`` nothing is written, which makes it the check CI runs.
|
|
839
|
+
Raises ``ValueError`` when the README has lost one of its markers.
|
|
840
|
+
|
|
841
|
+
Each file is replaced atomically: ``che install`` rewrites ``shell/`` in
|
|
842
|
+
place, and a shell starting at that moment must not source half a file.
|
|
843
|
+
"""
|
|
844
|
+
changed = []
|
|
845
|
+
for relative, contents in expected_files().items():
|
|
846
|
+
target = REPO / relative
|
|
847
|
+
if target.exists() and target.read_text(encoding="utf-8") == contents:
|
|
848
|
+
continue
|
|
849
|
+
changed.append(relative)
|
|
850
|
+
if not dry_run:
|
|
851
|
+
atomic_write_text(target, contents)
|
|
852
|
+
return changed
|
|
853
|
+
|
|
854
|
+
|
|
855
|
+
if __name__ == "__main__":
|
|
856
|
+
try:
|
|
857
|
+
written = sync_generated(dry_run=False)
|
|
858
|
+
except ValueError as exc:
|
|
859
|
+
raise SystemExit(f"❌ {exc}") from None
|
|
860
|
+
for path in written:
|
|
861
|
+
print(f"wrote {path}")
|
|
862
|
+
print(f"{len(written)} generated file(s) updated")
|