@codebend3r/che 1.0.0 → 1.1.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 CHANGED
@@ -11,8 +11,33 @@ your shell, and browsable through one menu.**
11
11
  ![Dependencies: stdlib only](https://img.shields.io/badge/dependencies-stdlib%20only-brightgreen)
12
12
  ![Shells: zsh, bash, fish](https://img.shields.io/badge/shells-zsh%20%7C%20bash%20%7C%20fish-8A2BE2)
13
13
 
14
- <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">
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
@@ -290,6 +315,7 @@ where you copy one machine's rc file to another.
290
315
 
291
316
  ```
292
317
  bin/
318
+ art.py the wordmark and the face beside it, for every screen
293
319
  che.py the dispatcher and the interactive menu
294
320
  commands.py the command manifest: the source of truth for everything
295
321
  install.py installer, first-run wizard, doctor, self-update
package/bin/art.py ADDED
@@ -0,0 +1,121 @@
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
+ def logo_lines() -> list[str]:
60
+ return LOGO.strip("\n").splitlines()
61
+
62
+
63
+ def face_lines() -> list[str]:
64
+ return FACE.strip("\n").splitlines()
65
+
66
+
67
+ HEIGHT = len(face_lines())
68
+ WIDTH = len(INDENT) + max(len(line) for line in face_lines())
69
+
70
+
71
+ def wordmark(colors: tui.Palette | None = None) -> list[str]:
72
+ """The block letters, coloured the way every screen prints them."""
73
+ colors = colors or tui.palette()
74
+ return [f"{colors.bright_magenta}{line}{colors.reset}" for line in logo_lines()]
75
+
76
+
77
+ def face(colors: tui.Palette | None = None) -> list[str]:
78
+ """The face, indented to line up with the wordmark and in the same colour."""
79
+ colors = colors or tui.palette()
80
+ return [f"{colors.bright_magenta}{INDENT}{line}{colors.reset}" for line in face_lines()]
81
+
82
+
83
+ def compose(left: Sequence[str], right: Sequence[str], *, gap: int = GAP) -> list[str]:
84
+ """Lay ``right`` beside ``left``, the shorter block centred on the taller."""
85
+ left = list(left)
86
+ right = list(right)
87
+ left_width = max((tui.display_width(line) for line in left), default=0)
88
+ height = max(len(left), len(right))
89
+ left_top = (height - len(left)) // 2
90
+ right_top = (height - len(right)) // 2
91
+
92
+ rows = []
93
+ for index in range(height):
94
+ a = left[index - left_top] if 0 <= index - left_top < len(left) else ""
95
+ b = right[index - right_top] if 0 <= index - right_top < len(right) else ""
96
+ if b:
97
+ a += " " * (left_width + gap - tui.display_width(a))
98
+ rows.append((a + b).rstrip())
99
+ return rows
100
+
101
+
102
+ def fits(right: Sequence[str], columns: int) -> bool:
103
+ """Whether the face and ``right`` sit side by side in ``columns``."""
104
+ right_width = max((tui.display_width(line) for line in right), default=0)
105
+ return WIDTH + GAP + right_width <= columns
106
+
107
+
108
+ def banner(
109
+ right: Sequence[str], *, columns: int | None = None, colors: tui.Palette | None = None
110
+ ) -> list[str]:
111
+ """The face with ``right`` beside it, or ``right`` alone when it would not fit.
112
+
113
+ ``right`` is normally the wordmark plus whatever a screen wants under it (a
114
+ subtitle, the command count). It comes back untouched, indent and all, on a
115
+ terminal too narrow for both, so every caller keeps working at 40 columns.
116
+ """
117
+ if columns is None:
118
+ columns = tui.size()[0]
119
+ if not fits(right, columns):
120
+ return list(right)
121
+ return compose(face(colors), right)
package/bin/che.py CHANGED
@@ -30,6 +30,7 @@ from pathlib import Path
30
30
 
31
31
  sys.path.insert(0, str(Path(__file__).resolve().parent))
32
32
 
33
+ import art
33
34
  import install as installer
34
35
  import shellgen
35
36
  import tui
@@ -427,33 +428,52 @@ def handle_key(state: Menu, key: str) -> str:
427
428
  return ""
428
429
 
429
430
 
431
+ # The face heads the menu only while the list under it keeps at least this
432
+ # many rows. Below that the one-line header takes over, as it always did.
433
+ MIN_LIST_ROWS = 8
434
+
435
+
436
+ def banner_fits(screen: tui.Screen, right: list[str]) -> bool:
437
+ """Whether the face and wordmark can head the menu and still leave a list."""
438
+ spare = screen.rows - (art.HEIGHT + 1) - 4
439
+ return spare >= MIN_LIST_ROWS and art.fits(right, screen.columns)
440
+
441
+
430
442
  def draw(screen: tui.Screen, state: Menu) -> None:
431
443
  colors = tui.palette()
432
444
  width = screen.columns
433
445
 
434
446
  total = len(all_commands())
435
447
  shown = len(state.matches())
436
- title = f"{colors.bright_magenta}{colors.bold}che{colors.reset}"
437
448
  counter = f"{shown}/{total}" if state.query else str(total)
438
- header = (
439
- f" {title} {colors.grey}v{VERSION}{colors.reset} "
440
- f"{colors.grey}{counter} commands{colors.reset}"
441
- )
442
- screen.line()
443
- screen.line(header)
444
-
445
449
  if state.searching or state.query:
446
450
  caret = f"{colors.cyan}▏{colors.reset}" if state.searching else " "
447
- screen.line(f" {colors.grey}search{colors.reset} {caret}{state.query}")
451
+ prompt = f" {colors.grey}search{colors.reset} {caret}{state.query}"
452
+ else:
453
+ prompt = f" {colors.grey}type to search ↑↓ move ⏎ run ? keys{colors.reset}"
454
+
455
+ right = art.wordmark(colors)
456
+ right += ["", f" {colors.grey}v{VERSION} {counter} commands{colors.reset}", prompt]
457
+ if banner_fits(screen, right) and not state.show_help:
458
+ # The face beside the wordmark, with the version, the count and the
459
+ # search line tucked under the letters.
460
+ screen.lines(art.banner(right, columns=width, colors=colors))
448
461
  else:
449
- screen.line(f" {colors.grey}type to search ↑↓ move ⏎ run ? keys{colors.reset}")
462
+ title = f"{colors.bright_magenta}{colors.bold}che{colors.reset}"
463
+ screen.line()
464
+ screen.line(
465
+ f" {title} {colors.grey}v{VERSION}{colors.reset} "
466
+ f"{colors.grey}{counter} commands{colors.reset}"
467
+ )
468
+ screen.line(prompt)
450
469
  screen.line(f" {colors.grey}{'─' * max(0, width - 4)}{colors.reset}")
470
+ header_rows = screen.used
451
471
 
452
472
  if state.show_help:
453
473
  draw_help(screen)
454
474
  return
455
475
 
456
- body_rows = max(3, screen.rows - 8)
476
+ body_rows = max(3, screen.rows - header_rows - 4)
457
477
  rows = state.rows()
458
478
  if not rows:
459
479
  screen.line()
package/bin/install.py CHANGED
@@ -39,6 +39,7 @@ from pathlib import Path
39
39
  # `parent`, where the scripts under bin/<category>/ use `parent.parent`.
40
40
  sys.path.insert(0, str(Path(__file__).resolve().parent))
41
41
 
42
+ import art
42
43
  import shellgen
43
44
  from commands import PACKAGE_NAME, REPO, VERSION, all_commands, required_binaries
44
45
  from tui import choose, confirm, is_interactive, palette
@@ -818,22 +819,15 @@ def _with_fish_path_line(block: str) -> str:
818
819
  # The first-run wizard
819
820
  # ---------------------------------------------------------------------------
820
821
 
821
- LOGO = r"""
822
- ██████╗██╗ ██╗███████╗
823
- ██╔════╝██║ ██║██╔════╝
824
- ██║ ███████║█████╗
825
- ██║ ██╔══██║██╔══╝
826
- ╚██████╗██║ ██║███████╗
827
- ╚═════╝╚═╝ ╚═╝╚══════╝
828
- """
829
-
830
822
 
831
823
  def print_logo(subtitle: str = "") -> None:
824
+ """The face beside the wordmark, with ``subtitle`` under the letters."""
832
825
  colors = palette()
833
- for line in LOGO.strip("\n").splitlines():
834
- print(f"{colors.bright_magenta}{line}{colors.reset}")
826
+ right = art.wordmark(colors)
835
827
  if subtitle:
836
- print(f" {colors.grey}{subtitle}{colors.reset}")
828
+ right.append(f" {colors.grey}{subtitle}{colors.reset}")
829
+ for line in art.banner(right, colors=colors):
830
+ print(line)
837
831
  print()
838
832
 
839
833
 
package/bin/shellgen.py CHANGED
@@ -20,6 +20,7 @@ from __future__ import annotations
20
20
  import shlex
21
21
  from pathlib import Path
22
22
 
23
+ import art
23
24
  from commands import (
24
25
  BUILTINS,
25
26
  CATEGORIES,
@@ -751,8 +752,15 @@ def _tools_table() -> str:
751
752
  return "\n".join(rows)
752
753
 
753
754
 
755
+ def _banner_island() -> str:
756
+ """The face beside the wordmark, exactly as ``che`` prints them."""
757
+ rows = art.compose(art.face_lines(), art.logo_lines())
758
+ return "```text\n" + "\n".join(rows) + "\n```"
759
+
760
+
754
761
  def readme_islands() -> dict[str, str]:
755
762
  return {
763
+ "banner": _banner_island(),
756
764
  "commands": _commands_table(),
757
765
  "builtins": _builtins_table(),
758
766
  "keys": _keys_table(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codebend3r/che",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Helper scripts shared across machines, called by name from your shell and browsable through one menu",
5
5
  "keywords": [
6
6
  "cli",
package/shell/che.bash CHANGED
@@ -1,4 +1,4 @@
1
- # che 1.0.0 - generated file, do not edit.
1
+ # che 1.1.0 - generated file, do not edit.
2
2
  #
3
3
  # Generated from bin/commands.py by `che install`.
4
4
  # To change a wrapper, edit that manifest and run `che install` again.
package/shell/che.fish CHANGED
@@ -1,4 +1,4 @@
1
- # che 1.0.0 - generated file, do not edit.
1
+ # che 1.1.0 - generated file, do not edit.
2
2
  #
3
3
  # Generated from bin/commands.py by `che install`.
4
4
  # To change a wrapper, edit that manifest and run `che install` again.
package/shell/che.zsh CHANGED
@@ -1,4 +1,4 @@
1
- # che 1.0.0 - generated file, do not edit.
1
+ # che 1.1.0 - generated file, do not edit.
2
2
  #
3
3
  # Generated from bin/commands.py by `che install`.
4
4
  # To change a wrapper, edit that manifest and run `che install` again.
@@ -1,5 +1,5 @@
1
1
  #compdef che
2
- # che 1.0.0 - generated file, do not edit.
2
+ # che 1.1.0 - generated file, do not edit.
3
3
  #
4
4
  # Generated from bin/commands.py by `che install`.
5
5
  # To change a wrapper, edit that manifest and run `che install` again.
@@ -1,4 +1,4 @@
1
- # che 1.0.0 - generated file, do not edit.
1
+ # che 1.1.0 - generated file, do not edit.
2
2
  #
3
3
  # Generated from bin/commands.py by `che install`.
4
4
  # To change a wrapper, edit that manifest and run `che install` again.
@@ -1,4 +1,4 @@
1
- # che 1.0.0 - generated file, do not edit.
1
+ # che 1.1.0 - generated file, do not edit.
2
2
  #
3
3
  # Generated from bin/commands.py by `che install`.
4
4
  # To change a wrapper, edit that manifest and run `che install` again.