@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/README.md
CHANGED
|
@@ -5,14 +5,39 @@
|
|
|
5
5
|
**A collection of helper scripts shared amongst computers, called by name from
|
|
6
6
|
your shell, and browsable through one menu.**
|
|
7
7
|
|
|
8
|
-
[](https://github.com/codebend3r/che/actions/workflows/sanity-check.yml)
|
|
9
9
|
[](https://www.npmjs.com/package/@codebend3r/che)
|
|
10
10
|

|
|
11
11
|

|
|
12
12
|

|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
<!-- che:banner -->
|
|
17
|
+
```text
|
|
18
|
+
**#*-
|
|
19
|
+
#*-. .::---:::.
|
|
20
|
+
= .:+#%@@@@@@@@@%#*+-.
|
|
21
|
+
.-*%@@@@@@@@@*==----+*-
|
|
22
|
+
.=-====*%@@@@+:..:===--+:
|
|
23
|
+
. .-:. .=%@%+=-+:...=+==
|
|
24
|
+
.=:-.:=:.+#***+***+++**+. ██████╗██╗ ██╗███████╗
|
|
25
|
+
-.:++**+-#@%##%%####%##*- ██╔════╝██║ ██║██╔════╝
|
|
26
|
+
.=+**#%#-:-+=--+%%@@####+. ██║ ███████║█████╗
|
|
27
|
+
.+*#%@@*...=+::+##%%#***-. ██║ ██╔══██║██╔══╝
|
|
28
|
+
-*##%@*=:-#*=-:--==##*+:. ╚██████╗██║ ██║███████╗
|
|
29
|
+
.=+#+-::--::-:.:.: :++=:. ╚═════╝╚═╝ ╚═╝╚══════╝
|
|
30
|
+
:++...::::---==++:---...
|
|
31
|
+
:..=**+++-=*#%@%=:::..
|
|
32
|
+
.:-*%@@@@@@@%#*. :::
|
|
33
|
+
: . . -+**#*=-.
|
|
34
|
+
++-. .::
|
|
35
|
+
+*+=: .-===.
|
|
36
|
+
```
|
|
37
|
+
<!-- /che:banner -->
|
|
15
38
|
|
|
39
|
+
<div align="center">
|
|
40
|
+
<img src="menu.png" alt="The che menu: commands grouped by category on the left, details for the selected command on the right" width="850">
|
|
16
41
|
</div>
|
|
17
42
|
|
|
18
43
|
The scripts are Python, standard library only. The names you type
|
|
@@ -54,8 +79,8 @@ git clone https://github.com/codebend3r/che.git ~/Developer/git/che
|
|
|
54
79
|
~/Developer/git/che/install.sh
|
|
55
80
|
```
|
|
56
81
|
|
|
57
|
-
Or in one line, which clones for you (into `~/Developer/git/
|
|
58
|
-
|
|
82
|
+
Or in one line, which clones for you (into `~/Developer/git/che`, or wherever
|
|
83
|
+
`CHE_HOME` points):
|
|
59
84
|
|
|
60
85
|
```sh
|
|
61
86
|
curl -fsSL https://raw.githubusercontent.com/codebend3r/che/main/install.sh | bash
|
|
@@ -78,7 +103,9 @@ which opens a first-run wizard: it lists the shells it found and the rc files
|
|
|
78
103
|
it would touch, asks whether to add a `che` on `PATH` and whether to keep the
|
|
79
104
|
completion chimes, offers to remove hand-written wrappers left over from before
|
|
80
105
|
the installer existed, and reports any missing external tool (offering to
|
|
81
|
-
`brew install` them when Homebrew is present).
|
|
106
|
+
`brew install` them when Homebrew is present). A question already answered on
|
|
107
|
+
the command line (`--no-sounds`, `--no-path-shim`, `--replace-legacy`,
|
|
108
|
+
`--install-deps`) is not asked again.
|
|
82
109
|
|
|
83
110
|
<div align="center">
|
|
84
111
|
<img src="first-run.png" alt="The first run screen: che is not installed in this shell yet, offering to install, open the menu, or quit" width="700">
|
|
@@ -93,7 +120,7 @@ Skip the wizard:
|
|
|
93
120
|
./install.sh --yes --shells=zsh,fish # or the ones you name
|
|
94
121
|
./install.sh --dry-run # report what would change, write nothing
|
|
95
122
|
./install.sh --yes --replace-legacy # also remove pre-che hand-written wrappers
|
|
96
|
-
./install.sh --print # print the rc block, install nothing
|
|
123
|
+
./install.sh --print # print the exact rc block, install nothing
|
|
97
124
|
```
|
|
98
125
|
|
|
99
126
|
Every rc file is copied to `~/.config/che/backups/` before it is edited.
|
|
@@ -114,9 +141,9 @@ dotfiles:
|
|
|
114
141
|
```zsh
|
|
115
142
|
# >>> che shell functions >>>
|
|
116
143
|
# Managed by `che install` (che 1.0.0). Edits here are overwritten.
|
|
117
|
-
export CHE_HOME=/Users/you/Developer/git/
|
|
144
|
+
export CHE_HOME=/Users/you/Developer/git/che
|
|
118
145
|
export CHE_PYTHON=/opt/homebrew/bin/python3
|
|
119
|
-
[ -r /Users/you/Developer/git/
|
|
146
|
+
[ -r /Users/you/Developer/git/che/shell/che.zsh ] && . /Users/you/Developer/git/che/shell/che.zsh
|
|
120
147
|
case ":$PATH:" in *":/Users/you/.local/bin:"*) ;; *) PATH="/Users/you/.local/bin:$PATH" ;; esac
|
|
121
148
|
# <<< che shell functions <<<
|
|
122
149
|
```
|
|
@@ -139,7 +166,7 @@ python3 exists. `brew install python@3.13` if none does.
|
|
|
139
166
|
|
|
140
167
|
```sh
|
|
141
168
|
che uninstall # remove the block, the shim and the completions
|
|
142
|
-
che uninstall --purge # also delete ~/.config/che
|
|
169
|
+
che uninstall --purge # also delete ~/.config/che and the menu's input history
|
|
143
170
|
```
|
|
144
171
|
|
|
145
172
|
The repo itself is left alone; delete the directory to finish.
|
|
@@ -189,13 +216,13 @@ have used, and `che help <command>` prints that script's own `--help`.
|
|
|
189
216
|
| Command | Does | Preview |
|
|
190
217
|
| --- | --- | --- |
|
|
191
218
|
| **Git** | *Branch hygiene, worktrees and GitHub Actions* | |
|
|
192
|
-
| `all-actions` | GitHub Actions status for every open PR
|
|
219
|
+
| `all-actions` | GitHub Actions status for every open PR and every default branch | |
|
|
193
220
|
| `all-actions-watch` | Same as all-actions, refreshed on an interval | |
|
|
194
221
|
| `checkout-my-branches` | Check out recent remote branches you authored that aren't local yet | |
|
|
195
222
|
| `clean-stale-branches` | Delete local branches whose upstream is gone | `clean-stale-branches-dr` |
|
|
196
223
|
| `prune-worktrees` | Remove every linked worktree, then prune stale admin records | `prune-worktrees-dr` |
|
|
197
|
-
| `sync-all-branches` |
|
|
198
|
-
| `update-from-origin` |
|
|
224
|
+
| `sync-all-branches` | Delete merged branches, rebase the rest, check out new ones; branches only | `sync-all-branches-dr` |
|
|
225
|
+
| `update-from-origin` | Alias of sync-all-branches | `--dry-run` |
|
|
199
226
|
| `update-local-branches` | Rebase every local branch that has an upstream onto origin | `--dry-run` |
|
|
200
227
|
| **Video** | *Codecs, renaming, dedupe and metadata* | |
|
|
201
228
|
| `show-codecs` | Report media files outside the Direct Play codec/container set | |
|
|
@@ -290,9 +317,17 @@ where you copy one machine's rc file to another.
|
|
|
290
317
|
|
|
291
318
|
```
|
|
292
319
|
bin/
|
|
293
|
-
|
|
320
|
+
art.py the wordmark and the face beside it, for every screen
|
|
321
|
+
che.py the dispatcher, and the loop that runs what the menu picks
|
|
294
322
|
commands.py the command manifest: the source of truth for everything
|
|
295
|
-
|
|
323
|
+
doctor.py `che doctor`: every check on the install
|
|
324
|
+
gitutils.py shared git helpers: worktrees, main branch, hook skipping
|
|
325
|
+
install.py installer, first-run wizard, self-update, uninstall
|
|
326
|
+
machine.py shells, rc file paths, python, config: facts about the machine
|
|
327
|
+
media.py shared ffprobe helpers for the video scripts
|
|
328
|
+
menu.py the menu's list, keys and drawing
|
|
329
|
+
nas.py the NAS drive table and SMB mounting, for the drive scripts
|
|
330
|
+
rcfile.py edits to rc files: the che block and old hand-written wrappers
|
|
296
331
|
shellgen.py renders the wrappers, completions and README tables
|
|
297
332
|
tui.py terminal primitives: raw input, frame buffer, prompts
|
|
298
333
|
utils.py shared library: logging, arg parsing, subprocess, files
|
|
@@ -325,22 +360,27 @@ Every script follows the same shape, described in full at the top of
|
|
|
325
360
|
point, so pointing a delete tool at `~/media -> /Volumes/...` used to be a
|
|
326
361
|
silent no-op. Pass the real path to act on it.
|
|
327
362
|
|
|
328
|
-
## Syncing branches
|
|
363
|
+
## Syncing branches
|
|
329
364
|
|
|
330
365
|
```sh
|
|
331
|
-
sync-all-branches #
|
|
366
|
+
sync-all-branches # delete merged branches, rebase the rest, check out new ones
|
|
332
367
|
sync-all-branches-dr # preview only
|
|
333
|
-
update-from-origin #
|
|
368
|
+
update-from-origin # alias of sync-all-branches
|
|
334
369
|
```
|
|
335
370
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
otherwise makes `main` un-checkoutable and stale branches un-deletable.
|
|
371
|
+
Only branches change. The sync deletes local branches whose upstream is gone,
|
|
372
|
+
`pull --rebase`s every branch with an upstream, and creates local tracking
|
|
373
|
+
branches for the remote branches you authored that aren't local yet, without
|
|
374
|
+
switching to them. It ends with the main clone on `main`.
|
|
341
375
|
|
|
342
|
-
|
|
343
|
-
|
|
376
|
+
It **never pushes**, **never touches a worktree** and **never runs the repo's
|
|
377
|
+
git hooks**. A branch checked out in a linked worktree is rebased in place
|
|
378
|
+
there when that worktree is clean; a stale one is reported and kept, since git
|
|
379
|
+
won't delete a branch a worktree has checked out. The branch scripts
|
|
380
|
+
(`clean-stale-branches`, `update-local-branches`, `checkout-my-branches`,
|
|
381
|
+
`prune-worktrees`, `sync-all-branches`) point `core.hooksPath` at `/dev/null`
|
|
382
|
+
for every git they run, so a lefthook or husky pre-commit, pre-push or
|
|
383
|
+
post-checkout never fires on a checkout or a rebase.
|
|
344
384
|
|
|
345
385
|
## External tools
|
|
346
386
|
|
|
@@ -382,7 +422,7 @@ This repo uses [Bun](https://bun.sh) as its task runner and
|
|
|
382
422
|
```sh
|
|
383
423
|
bun run test # pytest
|
|
384
424
|
bun run generate # rewrite shell/ and this README's tables from the manifest
|
|
385
|
-
bun run ci # ruff +
|
|
425
|
+
bun run ci # ruff + exec bits + shebangs + pytest (stale generated files fail it)
|
|
386
426
|
```
|
|
387
427
|
|
|
388
428
|
Neither `pytest` nor `ruff` needs to be installed globally: the scripts shell
|
package/bin/art.py
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""The che wordmark and the face it shares a name with, as text.
|
|
2
|
+
|
|
3
|
+
``LOGO`` is the block-letter wordmark that ``che install``, ``che doctor`` and
|
|
4
|
+
the first-run screen print. ``FACE`` is Che Guevara, rendered from a photograph
|
|
5
|
+
to 32 columns by 18 rows so the two fit side by side in an 80-column terminal.
|
|
6
|
+
Dense characters are the bright parts of the photo, which reads correctly on a
|
|
7
|
+
dark background.
|
|
8
|
+
|
|
9
|
+
``banner()`` lays the face beside whatever a screen wants next to it, normally
|
|
10
|
+
the wordmark with a subtitle underneath, centred vertically on the face. Lines
|
|
11
|
+
may carry colour escapes; widths are measured with ``tui.display_width`` so the
|
|
12
|
+
escapes do not push the right-hand column about.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from collections.abc import Sequence
|
|
18
|
+
|
|
19
|
+
import tui
|
|
20
|
+
|
|
21
|
+
LOGO = r"""
|
|
22
|
+
██████╗██╗ ██╗███████╗
|
|
23
|
+
██╔════╝██║ ██║██╔════╝
|
|
24
|
+
██║ ███████║█████╗
|
|
25
|
+
██║ ██╔══██║██╔══╝
|
|
26
|
+
╚██████╗██║ ██║███████╗
|
|
27
|
+
╚═════╝╚═╝ ╚═╝╚══════╝
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
FACE = """
|
|
31
|
+
**#*-
|
|
32
|
+
#*-. .::---:::.
|
|
33
|
+
= .:+#%@@@@@@@@@%#*+-.
|
|
34
|
+
.-*%@@@@@@@@@*==----+*-
|
|
35
|
+
.=-====*%@@@@+:..:===--+:
|
|
36
|
+
. .-:. .=%@%+=-+:...=+==
|
|
37
|
+
.=:-.:=:.+#***+***+++**+.
|
|
38
|
+
-.:++**+-#@%##%%####%##*-
|
|
39
|
+
.=+**#%#-:-+=--+%%@@####+.
|
|
40
|
+
.+*#%@@*...=+::+##%%#***-.
|
|
41
|
+
-*##%@*=:-#*=-:--==##*+:.
|
|
42
|
+
.=+#+-::--::-:.:.: :++=:.
|
|
43
|
+
:++...::::---==++:---...
|
|
44
|
+
:..=**+++-=*#%@%=:::..
|
|
45
|
+
.:-*%@@@@@@@%#*. :::
|
|
46
|
+
: . . -+**#*=-.
|
|
47
|
+
++-. .::
|
|
48
|
+
+*+=: .-===.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
# What the face gets on its left, so it lines up with the two-space indent the
|
|
52
|
+
# wordmark and every subtitle carry.
|
|
53
|
+
INDENT = " "
|
|
54
|
+
# Columns between the face and the block beside it. The wordmark lines start
|
|
55
|
+
# with two spaces of their own, so the visible gap is wider than this.
|
|
56
|
+
GAP = 1
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
LOGO_LINES: tuple[str, ...] = tuple(LOGO.strip("\n").splitlines())
|
|
60
|
+
FACE_LINES: tuple[str, ...] = tuple(FACE.strip("\n").splitlines())
|
|
61
|
+
|
|
62
|
+
HEIGHT = len(FACE_LINES)
|
|
63
|
+
WIDTH = len(INDENT) + max(len(line) for line in FACE_LINES)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _paint(lines: Sequence[str], colors: tui.Palette, indent: str = "") -> list[str]:
|
|
67
|
+
return [f"{colors.bright_magenta}{indent}{line}{colors.reset}" for line in lines]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def wordmark(colors: tui.Palette) -> list[str]:
|
|
71
|
+
"""The block letters, coloured the way every screen prints them."""
|
|
72
|
+
return _paint(LOGO_LINES, colors)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def compose(left: Sequence[str], right: Sequence[str], *, gap: int = GAP) -> list[str]:
|
|
76
|
+
"""Lay ``right`` beside ``left``, the shorter block centred on the taller."""
|
|
77
|
+
left = list(left)
|
|
78
|
+
right = list(right)
|
|
79
|
+
left_width = max((tui.display_width(line) for line in left), default=0)
|
|
80
|
+
height = max(len(left), len(right))
|
|
81
|
+
left_top = (height - len(left)) // 2
|
|
82
|
+
right_top = (height - len(right)) // 2
|
|
83
|
+
|
|
84
|
+
rows = []
|
|
85
|
+
for index in range(height):
|
|
86
|
+
a = left[index - left_top] if 0 <= index - left_top < len(left) else ""
|
|
87
|
+
b = right[index - right_top] if 0 <= index - right_top < len(right) else ""
|
|
88
|
+
if b:
|
|
89
|
+
a += " " * (left_width + gap - tui.display_width(a))
|
|
90
|
+
rows.append((a + b).rstrip())
|
|
91
|
+
return rows
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def fits(right: Sequence[str], columns: int) -> bool:
|
|
95
|
+
"""Whether the face and ``right`` sit side by side in ``columns``."""
|
|
96
|
+
right_width = max((tui.display_width(line) for line in right), default=0)
|
|
97
|
+
return WIDTH + GAP + right_width <= columns
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def banner(right: Sequence[str], *, colors: tui.Palette, columns: int | None = None) -> list[str]:
|
|
101
|
+
"""The face with ``right`` beside it, or ``right`` alone when it would not fit.
|
|
102
|
+
|
|
103
|
+
``right`` is normally the wordmark plus whatever a screen wants under it (a
|
|
104
|
+
subtitle, the command count). It comes back untouched, indent and all, on a
|
|
105
|
+
terminal too narrow for both, so every caller keeps working at 40 columns.
|
|
106
|
+
The face is indented to line up with the wordmark and painted in its colour.
|
|
107
|
+
"""
|
|
108
|
+
if columns is None:
|
|
109
|
+
columns = tui.size()[0]
|
|
110
|
+
if not fits(right, columns):
|
|
111
|
+
return list(right)
|
|
112
|
+
return compose(_paint(FACE_LINES, colors, INDENT), right)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def print_logo(subtitle: str = "") -> None:
|
|
116
|
+
"""The face beside the wordmark, with ``subtitle`` under the letters."""
|
|
117
|
+
colors = tui.palette()
|
|
118
|
+
right = wordmark(colors)
|
|
119
|
+
if subtitle:
|
|
120
|
+
right.append(f" {colors.grey}{subtitle}{colors.reset}")
|
|
121
|
+
for line in banner(right, colors=colors):
|
|
122
|
+
print(line)
|
|
123
|
+
print()
|