@codebend3r/che 1.0.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 +65 -25
- package/bin/art.py +123 -0
- package/bin/che.py +209 -568
- 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 -1188
- 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 +508 -428
- 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,48 +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
|
|
|
29
|
+
import art
|
|
23
30
|
from commands import (
|
|
24
31
|
BUILTINS,
|
|
25
|
-
CATEGORIES,
|
|
26
32
|
COMMANDS,
|
|
27
33
|
MENU_KEYS,
|
|
34
|
+
REPO,
|
|
28
35
|
VERSION,
|
|
29
36
|
Command,
|
|
37
|
+
Resolved,
|
|
38
|
+
all_commands,
|
|
39
|
+
grouped,
|
|
30
40
|
required_binaries,
|
|
41
|
+
variants,
|
|
42
|
+
wrapper_names,
|
|
31
43
|
)
|
|
32
|
-
|
|
33
|
-
SHELLS = ("zsh", "bash", "fish")
|
|
34
|
-
|
|
35
|
-
# Where each shell's generated file lives, relative to the repo root.
|
|
36
|
-
WRAPPER_FILES = {
|
|
37
|
-
"zsh": Path("shell/che.zsh"),
|
|
38
|
-
"bash": Path("shell/che.bash"),
|
|
39
|
-
"fish": Path("shell/che.fish"),
|
|
40
|
-
}
|
|
41
|
-
COMPLETION_FILES = {
|
|
42
|
-
"zsh": Path("shell/completions/_che"),
|
|
43
|
-
"bash": Path("shell/completions/che.bash"),
|
|
44
|
-
"fish": Path("shell/completions/che.fish"),
|
|
45
|
-
}
|
|
44
|
+
from utils import atomic_write_text
|
|
46
45
|
|
|
47
46
|
# The rc-file block is delimited by these. Anything between them belongs to
|
|
48
47
|
# `che install` and is rewritten wholesale on every install; anything outside
|
|
@@ -50,42 +49,10 @@ COMPLETION_FILES = {
|
|
|
50
49
|
BEGIN_MARKER = "# >>> che shell functions >>>"
|
|
51
50
|
END_MARKER = "# <<< che shell functions <<<"
|
|
52
51
|
|
|
53
|
-
# Flags that take a value, so completion can offer `--flag=` without a
|
|
54
|
-
# trailing space and then complete filenames after the `=`.
|
|
55
|
-
VALUE_FLAGS = frozenset(
|
|
56
|
-
{
|
|
57
|
-
"--author",
|
|
58
|
-
"--category",
|
|
59
|
-
"--exts",
|
|
60
|
-
"--ext",
|
|
61
|
-
"--ignore-words",
|
|
62
|
-
"--interval",
|
|
63
|
-
"--length",
|
|
64
|
-
"--limit",
|
|
65
|
-
"--only",
|
|
66
|
-
"--owner",
|
|
67
|
-
"--path",
|
|
68
|
-
"--ping-timeout",
|
|
69
|
-
"--pr-limit",
|
|
70
|
-
"--protect",
|
|
71
|
-
"--samples",
|
|
72
|
-
"--shell",
|
|
73
|
-
"--shells",
|
|
74
|
-
"--size",
|
|
75
|
-
"--sort",
|
|
76
|
-
"--strategy",
|
|
77
|
-
"--threshold",
|
|
78
|
-
"--year",
|
|
79
|
-
}
|
|
80
|
-
)
|
|
81
|
-
|
|
82
|
-
# Flags whose value is a path, so completion offers files rather than nothing.
|
|
83
|
-
PATH_FLAGS = frozenset({"--path"})
|
|
84
|
-
|
|
85
52
|
|
|
86
|
-
def _banner(
|
|
53
|
+
def _banner() -> str:
|
|
87
54
|
return "\n".join(
|
|
88
|
-
f"
|
|
55
|
+
f"# {line}".rstrip()
|
|
89
56
|
for line in (
|
|
90
57
|
f"che {VERSION} - generated file, do not edit.",
|
|
91
58
|
"",
|
|
@@ -97,66 +64,215 @@ def _banner(comment: str = "#") -> str:
|
|
|
97
64
|
|
|
98
65
|
def _flag_words(command: Command) -> list[str]:
|
|
99
66
|
"""Completion words for one command: its flags plus the universal ones."""
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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("'", "\\'") + "'"
|
|
103
199
|
|
|
200
|
+
def header(self, name: str, summary: str) -> list[str]:
|
|
201
|
+
return [f"function {name} --description {self.quote(summary)}"]
|
|
104
202
|
|
|
105
|
-
def
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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()
|
|
110
223
|
|
|
111
224
|
|
|
112
225
|
# ---------------------------------------------------------------------------
|
|
113
|
-
#
|
|
226
|
+
# The wrapper files
|
|
114
227
|
# ---------------------------------------------------------------------------
|
|
115
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"
|
|
116
238
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
forwards ``"$@"``. A few of them (clean-stale-branches, prune-worktrees)
|
|
122
|
-
used to swallow their arguments, so ``clean-stale-branches --protect=x``
|
|
123
|
-
silently ignored a flag the script documents.
|
|
124
|
-
"""
|
|
125
|
-
env, args = command.invocation(dry=dry)
|
|
126
|
-
prefix = "".join(f"{name}={shlex.quote(value)} " for name, value in sorted(env.items()))
|
|
127
|
-
|
|
128
|
-
if command.body:
|
|
129
|
-
call = command.body
|
|
130
|
-
else:
|
|
131
|
-
parts = ['"$CHE_PYTHON"', f'"$CHE_BIN/{command.script}"']
|
|
132
|
-
parts += [shlex.quote(arg) for arg in args]
|
|
133
|
-
parts.append('"$@"')
|
|
134
|
-
call = prefix + " ".join(parts)
|
|
135
|
-
|
|
136
|
-
sound = command.dry_sound if dry else command.sound
|
|
137
|
-
lines = [f" {call}"]
|
|
138
|
-
if sound is not None:
|
|
139
|
-
# `$?` is expanded before che_notify runs, so the wrapper still returns
|
|
140
|
-
# the script's exit status. The old wrappers returned playsound's.
|
|
141
|
-
lines.append(f" che_notify {sound} $?")
|
|
142
|
-
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
|
|
143
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
|
+
"""
|
|
144
248
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
+
}
|
|
159
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
|
+
"""
|
|
160
276
|
|
|
161
277
|
ZSH_ALIAS_GUARD = """
|
|
162
278
|
# An alias sharing a name with a wrapper below is fatal, not cosmetic: zsh
|
|
@@ -216,19 +332,60 @@ _che_restore_aliases() {
|
|
|
216
332
|
}
|
|
217
333
|
"""
|
|
218
334
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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()]
|
|
225
382
|
|
|
226
383
|
|
|
227
384
|
def _stash_call() -> str:
|
|
228
|
-
"""`_che_stash_aliases`
|
|
385
|
+
"""`_che_stash_aliases` for every name the file defines, wrapped to stay readable."""
|
|
229
386
|
lines: list[str] = []
|
|
230
387
|
current = "_che_stash_aliases"
|
|
231
|
-
for name in
|
|
388
|
+
for name in defined_names():
|
|
232
389
|
candidate = f"{current} {name}"
|
|
233
390
|
if len(candidate) > 76:
|
|
234
391
|
lines.append(f"{current} \\")
|
|
@@ -239,197 +396,23 @@ def _stash_call() -> str:
|
|
|
239
396
|
return "\n".join(lines)
|
|
240
397
|
|
|
241
398
|
|
|
242
|
-
def
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
}[shell]
|
|
247
|
-
|
|
248
|
-
head = f"""{_banner()}
|
|
249
|
-
#
|
|
250
|
-
# Source this file (or let the `che install` block in your rc file do it):
|
|
251
|
-
#
|
|
252
|
-
# . {"~/Developer/git/che/shell/che." + shell}
|
|
253
|
-
#
|
|
254
|
-
# Every wrapper is a shell function, so `type <name>` shows what it runs and
|
|
255
|
-
# `che doctor` can tell you when something it needs is missing.
|
|
256
|
-
|
|
257
|
-
# CHE_HOME is normally exported by the installed rc block. Fall back to this
|
|
258
|
-
# file's own location so a bare `source` of it also works.
|
|
259
|
-
if [ -z "${{CHE_HOME:-}}" ]; then
|
|
260
|
-
{resolve_self}
|
|
261
|
-
fi
|
|
262
|
-
|
|
263
|
-
# Exported, not just set: every wrapper expands "$CHE_BIN", and a shell that
|
|
264
|
-
# replays the *functions* without re-reading this file - a `typeset -f` dump, an
|
|
265
|
-
# environment snapshot, any tool that rebuilds a shell from its exported
|
|
266
|
-
# environment - keeps the functions and drops a plain variable, leaving the
|
|
267
|
-
# wrappers to run "/che.py". CHE_HOME comes in exported by the rc block; this
|
|
268
|
-
# keeps everything derived from it exported too.
|
|
269
|
-
export CHE_BIN="${{CHE_HOME}}/bin"
|
|
270
|
-
|
|
271
|
-
# Any python3 on PATH by default. `che install` records the interpreter it
|
|
272
|
-
# verified (3.12+) in the rc block, which wins over this.
|
|
273
|
-
: "${{CHE_PYTHON:=python3}}"
|
|
274
|
-
export CHE_PYTHON
|
|
275
|
-
|
|
276
|
-
# Legacy name from the hand-maintained wrappers, kept so an older snippet in
|
|
277
|
-
# ~/.zshrc that still refers to it keeps resolving.
|
|
278
|
-
export SHELL_FUNCTIONS_BIN="${{CHE_BIN}}"
|
|
279
|
-
|
|
280
|
-
# Play the completion sound for a wrapper, then return the status it was given.
|
|
281
|
-
#
|
|
282
|
-
# playsound-N is defined outside this repo. Every wrapper used to call it
|
|
283
|
-
# unconditionally, which printed "command not found" on a machine that never
|
|
284
|
-
# had it; here a missing playsound is simply skipped. Set CHE_SOUNDS=0 to
|
|
285
|
-
# silence them all.
|
|
286
|
-
che_notify() {{
|
|
287
|
-
if [ "${{CHE_SOUNDS:-1}}" = "1" ] && command -v "playsound-$1" >/dev/null 2>&1; then
|
|
288
|
-
"playsound-$1" >/dev/null 2>&1
|
|
289
|
-
fi
|
|
290
|
-
return "${{2:-0}}"
|
|
291
|
-
}}
|
|
292
|
-
|
|
293
|
-
# The dispatcher. `che` with no arguments opens the interactive menu.
|
|
294
|
-
che() {{
|
|
295
|
-
if [ "$#" -eq 0 ]; then
|
|
296
|
-
"$CHE_PYTHON" "$CHE_BIN/che.py"
|
|
297
|
-
else
|
|
298
|
-
"$CHE_PYTHON" "$CHE_BIN/che.py" "$@"
|
|
299
|
-
che_notify 7 $?
|
|
300
|
-
fi
|
|
301
|
-
}}"""
|
|
302
|
-
|
|
303
|
-
tail_by_shell = {
|
|
304
|
-
"zsh": """
|
|
305
|
-
# Completions. compdef only exists once compinit has run; when it has not,
|
|
306
|
-
# skip quietly rather than erroring at shell start.
|
|
307
|
-
if whence -w compdef >/dev/null 2>&1 && [ -r "${CHE_HOME}/shell/completions/_che" ]; then
|
|
308
|
-
. "${CHE_HOME}/shell/completions/_che"
|
|
309
|
-
fi""",
|
|
310
|
-
"bash": """
|
|
311
|
-
# Completions, when this is bash and the completion builtin is available.
|
|
312
|
-
if [ -n "${BASH_VERSION:-}" ] && [ -r "${CHE_HOME}/shell/completions/che.bash" ]; then
|
|
313
|
-
. "${CHE_HOME}/shell/completions/che.bash"
|
|
314
|
-
fi""",
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
guard = {"zsh": ZSH_ALIAS_GUARD, "bash": BASH_ALIAS_GUARD}[shell]
|
|
318
|
-
|
|
319
|
-
return "\n".join(
|
|
320
|
-
[
|
|
321
|
-
head,
|
|
322
|
-
guard.rstrip("\n"),
|
|
323
|
-
"",
|
|
324
|
-
_stash_call(),
|
|
325
|
-
*_posix_wrappers(),
|
|
326
|
-
"",
|
|
327
|
-
"_che_restore_aliases",
|
|
328
|
-
"",
|
|
329
|
-
tail_by_shell[shell].strip("\n"),
|
|
330
|
-
"",
|
|
331
|
-
]
|
|
332
|
-
)
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
# ---------------------------------------------------------------------------
|
|
336
|
-
# fish
|
|
337
|
-
# ---------------------------------------------------------------------------
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
def _fish_quote(text: str) -> str:
|
|
341
|
-
return "'" + text.replace("\\", "\\\\").replace("'", "\\'") + "'"
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
def _render_fish() -> str:
|
|
345
|
-
lines = [
|
|
346
|
-
_banner(),
|
|
347
|
-
"",
|
|
348
|
-
# No `cd` here: fish runs command substitution in the current process,
|
|
349
|
-
# so `(cd ...; and pwd)` would move the user's shell as a side effect.
|
|
350
|
-
"if not set -q CHE_HOME",
|
|
351
|
-
" set -l __che_dir (dirname (status --current-filename))/..",
|
|
352
|
-
" if type -q path",
|
|
353
|
-
" set -gx CHE_HOME (path resolve $__che_dir)",
|
|
354
|
-
" else",
|
|
355
|
-
" set -gx CHE_HOME $__che_dir",
|
|
356
|
-
" end",
|
|
357
|
-
" set -e __che_dir",
|
|
358
|
-
"end",
|
|
359
|
-
'set -gx CHE_BIN "$CHE_HOME/bin"',
|
|
360
|
-
"if not set -q CHE_PYTHON",
|
|
361
|
-
" set -gx CHE_PYTHON python3",
|
|
362
|
-
"end",
|
|
363
|
-
'set -gx SHELL_FUNCTIONS_BIN "$CHE_BIN"',
|
|
364
|
-
"",
|
|
365
|
-
"# Play a wrapper's completion sound, then return the status it was given.",
|
|
366
|
-
"function che_notify --description 'che: completion sound'",
|
|
367
|
-
" set -l code 0",
|
|
368
|
-
" if set -q argv[2]",
|
|
369
|
-
" set code $argv[2]",
|
|
370
|
-
" end",
|
|
371
|
-
' if test "$CHE_SOUNDS" != "0"',
|
|
372
|
-
' if type -q "playsound-$argv[1]"',
|
|
373
|
-
' "playsound-$argv[1]" >/dev/null 2>&1',
|
|
374
|
-
" end",
|
|
375
|
-
" end",
|
|
376
|
-
" return $code",
|
|
377
|
-
"end",
|
|
378
|
-
"",
|
|
379
|
-
"function che --description 'che: shell helpers dispatcher'",
|
|
380
|
-
" if test (count $argv) -eq 0",
|
|
381
|
-
' "$CHE_PYTHON" "$CHE_BIN/che.py"',
|
|
382
|
-
" else",
|
|
383
|
-
' "$CHE_PYTHON" "$CHE_BIN/che.py" $argv',
|
|
384
|
-
" che_notify 7 $status",
|
|
385
|
-
" end",
|
|
386
|
-
"end",
|
|
387
|
-
]
|
|
388
|
-
|
|
389
|
-
for category in CATEGORIES:
|
|
390
|
-
members = [command for command in COMMANDS if command.category == category.key]
|
|
391
|
-
if not members:
|
|
392
|
-
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):
|
|
393
403
|
lines += ["", f"# {'-' * 74}", f"# {category.title} - {category.blurb}", f"# {'-' * 74}"]
|
|
394
|
-
for
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
summary = command.summary + (" (preview only)" if dry else "")
|
|
398
|
-
lines += ["", f"function {name} --description {_fish_quote(summary)}"]
|
|
399
|
-
|
|
400
|
-
if command.body:
|
|
401
|
-
# fish has no `$1`; the sh body's positional becomes $argv[1].
|
|
402
|
-
lines.append(" " + command.body.replace("$1", "$argv[1]"))
|
|
403
|
-
else:
|
|
404
|
-
prefix = "".join(f"{key}={value} " for key, value in sorted(env.items()))
|
|
405
|
-
argv = "".join(f" {shlex.quote(arg)}" for arg in args)
|
|
406
|
-
runner = "env " + prefix if prefix else ""
|
|
407
|
-
lines.append(
|
|
408
|
-
f' {runner}"$CHE_PYTHON" "$CHE_BIN/{command.script}"{argv} $argv'
|
|
409
|
-
)
|
|
410
|
-
|
|
411
|
-
sound = command.dry_sound if dry else command.sound
|
|
412
|
-
if sound is not None:
|
|
413
|
-
lines.append(f" che_notify {sound} $status")
|
|
414
|
-
lines.append("end")
|
|
415
|
-
|
|
416
|
-
lines += [
|
|
417
|
-
"",
|
|
418
|
-
'if test -r "$CHE_HOME/shell/completions/che.fish"',
|
|
419
|
-
' source "$CHE_HOME/shell/completions/che.fish"',
|
|
420
|
-
"end",
|
|
421
|
-
"",
|
|
422
|
-
]
|
|
423
|
-
return "\n".join(lines)
|
|
404
|
+
for variant in variants(members):
|
|
405
|
+
lines += ["", *dialect.wrapper(variant)]
|
|
406
|
+
return lines
|
|
424
407
|
|
|
425
408
|
|
|
426
409
|
def wrappers(shell: str) -> str:
|
|
427
410
|
"""The generated wrapper file for ``shell``."""
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
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
|
+
)
|
|
433
416
|
|
|
434
417
|
|
|
435
418
|
# ---------------------------------------------------------------------------
|
|
@@ -437,25 +420,14 @@ def wrappers(shell: str) -> str:
|
|
|
437
420
|
# ---------------------------------------------------------------------------
|
|
438
421
|
|
|
439
422
|
|
|
440
|
-
def _all_completable() -> list[tuple[str, Command, bool]]:
|
|
441
|
-
entries: list[tuple[str, Command, bool]] = []
|
|
442
|
-
for command in (*COMMANDS, *BUILTINS):
|
|
443
|
-
for name, dry in _dry_variants(command):
|
|
444
|
-
entries.append((name, command, dry))
|
|
445
|
-
return entries
|
|
446
|
-
|
|
447
|
-
|
|
448
423
|
def _zsh_completions() -> str:
|
|
449
|
-
describe = [
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
cases.append(f" {name}) print -r -- {shlex.quote(' '.join(_flag_words(command)))} ;;")
|
|
457
|
-
|
|
458
|
-
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
|
+
]
|
|
459
431
|
|
|
460
432
|
return f"""#compdef che
|
|
461
433
|
{_banner()}
|
|
@@ -501,17 +473,16 @@ _che_wrapper() {{
|
|
|
501
473
|
}}
|
|
502
474
|
|
|
503
475
|
compdef _che che
|
|
504
|
-
compdef _che_wrapper {wrapper_names}
|
|
476
|
+
compdef _che_wrapper {" ".join(wrapper_names())}
|
|
505
477
|
"""
|
|
506
478
|
|
|
507
479
|
|
|
508
480
|
def _bash_completions() -> str:
|
|
509
|
-
cases = [
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
names = " ".join(name for
|
|
514
|
-
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()))
|
|
515
486
|
|
|
516
487
|
return f"""{_banner()}
|
|
517
488
|
# shellcheck shell=bash
|
|
@@ -567,52 +538,122 @@ _che_complete_wrapper() {{
|
|
|
567
538
|
}}
|
|
568
539
|
|
|
569
540
|
complete -o nospace -F _che_complete che
|
|
570
|
-
complete -o nospace -F _che_complete_wrapper {wrapper_names}
|
|
541
|
+
complete -o nospace -F _che_complete_wrapper {" ".join(wrapper_names())}
|
|
571
542
|
"""
|
|
572
543
|
|
|
573
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
|
+
|
|
574
555
|
def _fish_completions() -> str:
|
|
575
|
-
lines = [_banner(), ""]
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
lines.append(
|
|
580
|
-
f"complete -c che -n __fish_use_subcommand -a {name} -d {_fish_quote(summary)}"
|
|
581
|
-
)
|
|
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}")
|
|
582
560
|
|
|
583
561
|
lines.append("")
|
|
584
|
-
for
|
|
585
|
-
for flag in command.flags:
|
|
586
|
-
option = flag.removeprefix("--")
|
|
587
|
-
takes_value = " -r" if flag in VALUE_FLAGS else ""
|
|
588
|
-
paths = " -F" if flag in PATH_FLAGS else ""
|
|
562
|
+
for v in variants(all_commands()):
|
|
563
|
+
for flag in v.command.flags:
|
|
589
564
|
lines.append(
|
|
590
|
-
f'complete -c che -n "__fish_seen_subcommand_from {name}" '
|
|
591
|
-
f"-l {option}{takes_value}{paths}"
|
|
565
|
+
f'complete -c che -n "__fish_seen_subcommand_from {v.name}" {_fish_option(flag)}'
|
|
592
566
|
)
|
|
593
567
|
|
|
594
568
|
lines.append("")
|
|
595
|
-
for
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
for flag in command.flags:
|
|
599
|
-
option = flag.removeprefix("--")
|
|
600
|
-
takes_value = " -r" if flag in VALUE_FLAGS else ""
|
|
601
|
-
paths = " -F" if flag in PATH_FLAGS else ""
|
|
602
|
-
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)}")
|
|
603
572
|
|
|
604
573
|
lines.append("")
|
|
605
574
|
return "\n".join(lines)
|
|
606
575
|
|
|
607
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
|
+
|
|
608
655
|
def completions(shell: str) -> str:
|
|
609
|
-
|
|
610
|
-
return _zsh_completions()
|
|
611
|
-
if shell == "bash":
|
|
612
|
-
return _bash_completions()
|
|
613
|
-
if shell == "fish":
|
|
614
|
-
return _fish_completions()
|
|
615
|
-
raise ValueError(f"unsupported shell: {shell}")
|
|
656
|
+
return _shell(shell).completions()
|
|
616
657
|
|
|
617
658
|
|
|
618
659
|
# ---------------------------------------------------------------------------
|
|
@@ -620,45 +661,31 @@ def completions(shell: str) -> str:
|
|
|
620
661
|
# ---------------------------------------------------------------------------
|
|
621
662
|
|
|
622
663
|
|
|
623
|
-
def rc_block(
|
|
664
|
+
def rc_block(
|
|
665
|
+
shell: str, *, home: Path, python: str, sounds: bool = True, path_dir: Path | None = None
|
|
666
|
+
) -> str:
|
|
624
667
|
"""The machine-specific snippet ``che install`` writes into an rc file.
|
|
625
668
|
|
|
626
|
-
Deliberately tiny: a path, an interpreter and a source line
|
|
627
|
-
that
|
|
628
|
-
|
|
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.
|
|
629
673
|
"""
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
body = [
|
|
634
|
-
BEGIN_MARKER,
|
|
635
|
-
f"# Managed by `che install` (che {VERSION}). Edits here are overwritten.",
|
|
636
|
-
f"set -gx CHE_HOME {_fish_quote(str(home))}",
|
|
637
|
-
f"set -gx CHE_PYTHON {_fish_quote(python)}",
|
|
638
|
-
]
|
|
639
|
-
if not sounds:
|
|
640
|
-
body.append("set -gx CHE_SOUNDS 0")
|
|
641
|
-
body += [
|
|
642
|
-
f"if test -r {_fish_quote(str(wrapper))}",
|
|
643
|
-
f" source {_fish_quote(str(wrapper))}",
|
|
644
|
-
"end",
|
|
645
|
-
END_MARKER,
|
|
646
|
-
]
|
|
647
|
-
return "\n".join(body) + "\n"
|
|
648
|
-
|
|
649
|
-
body = [
|
|
674
|
+
spec = _shell(shell)
|
|
675
|
+
dialect = spec.dialect
|
|
676
|
+
lines = [
|
|
650
677
|
BEGIN_MARKER,
|
|
651
678
|
f"# Managed by `che install` (che {VERSION}). Edits here are overwritten.",
|
|
652
|
-
|
|
653
|
-
|
|
679
|
+
dialect.export("CHE_HOME", dialect.quote(str(home))),
|
|
680
|
+
dialect.export("CHE_PYTHON", dialect.quote(python)),
|
|
654
681
|
]
|
|
655
682
|
if not sounds:
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
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"
|
|
662
689
|
|
|
663
690
|
|
|
664
691
|
def legacy_zshrc() -> str:
|
|
@@ -669,25 +696,18 @@ def legacy_zshrc() -> str:
|
|
|
669
696
|
sourcing this file keeps working; the wrappers themselves now come from
|
|
670
697
|
``shell/che.zsh``.
|
|
671
698
|
"""
|
|
672
|
-
return
|
|
699
|
+
return (
|
|
700
|
+
_banner()
|
|
701
|
+
+ """
|
|
673
702
|
#
|
|
674
703
|
# The wrappers moved to shell/che.zsh and are generated from bin/commands.py.
|
|
675
704
|
# `che install` writes a block into ~/.zshrc that sources them; this file
|
|
676
705
|
# stays so that an existing `source .../shell-functions/.zshrc` keeps working.
|
|
677
706
|
|
|
678
|
-
CHE_HOME="${
|
|
679
|
-
. "${
|
|
707
|
+
CHE_HOME="${CHE_HOME:-${${(%):-%x}:A:h}}"
|
|
708
|
+
. "${CHE_HOME}/shell/che.zsh"
|
|
680
709
|
"""
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
def generated_files() -> dict[Path, str]:
|
|
684
|
-
"""Every generated path mapped to its expected contents, relative to the repo."""
|
|
685
|
-
files: dict[Path, str] = {}
|
|
686
|
-
for shell in SHELLS:
|
|
687
|
-
files[WRAPPER_FILES[shell]] = wrappers(shell)
|
|
688
|
-
files[COMPLETION_FILES[shell]] = completions(shell)
|
|
689
|
-
files[Path(".zshrc")] = legacy_zshrc()
|
|
690
|
-
return files
|
|
710
|
+
)
|
|
691
711
|
|
|
692
712
|
|
|
693
713
|
# ---------------------------------------------------------------------------
|
|
@@ -706,53 +726,58 @@ def _marker(name: str) -> tuple[str, str]:
|
|
|
706
726
|
return f"<!-- che:{name} -->", f"<!-- /che:{name} -->"
|
|
707
727
|
|
|
708
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
|
+
|
|
709
737
|
def _commands_table() -> str:
|
|
710
|
-
rows = [
|
|
711
|
-
for category in
|
|
712
|
-
members = [command for command in COMMANDS if command.category == category.key]
|
|
713
|
-
if not members:
|
|
714
|
-
continue
|
|
738
|
+
rows = []
|
|
739
|
+
for category, members in grouped(COMMANDS):
|
|
715
740
|
rows.append(f"| **{category.title}** | *{category.blurb}* | |")
|
|
716
741
|
for command in members:
|
|
717
|
-
preview =
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
preview = "`--dry-run`"
|
|
722
|
-
if not preview and command.no_preview_reason:
|
|
723
|
-
preview = "none"
|
|
724
|
-
rows.append(f"| `{command.name}` | {command.summary} | {preview} |")
|
|
725
|
-
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)
|
|
726
746
|
|
|
727
747
|
|
|
728
748
|
def _builtins_table() -> str:
|
|
729
|
-
rows =
|
|
730
|
-
|
|
731
|
-
rows.append(f"| `che {command.name}` | {command.summary} |")
|
|
732
|
-
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)
|
|
733
751
|
|
|
734
752
|
|
|
735
753
|
def _keys_table() -> str:
|
|
736
|
-
rows =
|
|
737
|
-
|
|
738
|
-
rows.append(f"| `{key}` | {description} |")
|
|
739
|
-
return "\n".join(rows)
|
|
754
|
+
rows = (_md_row(f"`{key}`", description) for key, description in MENU_KEYS)
|
|
755
|
+
return _md_table(("Key", "Does"), rows)
|
|
740
756
|
|
|
741
757
|
|
|
742
758
|
def _tools_table() -> str:
|
|
743
|
-
rows = [
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
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)
|
|
747
769
|
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
770
|
+
|
|
771
|
+
def _banner_island() -> str:
|
|
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)
|
|
775
|
+
return "```text\n" + "\n".join(rows) + "\n```"
|
|
752
776
|
|
|
753
777
|
|
|
754
778
|
def readme_islands() -> dict[str, str]:
|
|
755
779
|
return {
|
|
780
|
+
"banner": _banner_island(),
|
|
756
781
|
"commands": _commands_table(),
|
|
757
782
|
"builtins": _builtins_table(),
|
|
758
783
|
"keys": _keys_table(),
|
|
@@ -780,3 +805,58 @@ def render_readme(current: str) -> str:
|
|
|
780
805
|
_, _, tail = rest.partition(end)
|
|
781
806
|
updated = f"{head}{begin}\n{body}\n{end}{tail}"
|
|
782
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")
|