orthodox-spinner 0.1.1__tar.gz
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.
- orthodox_spinner-0.1.1/.github/workflows/publish.yml +54 -0
- orthodox_spinner-0.1.1/.gitignore +6 -0
- orthodox_spinner-0.1.1/PKG-INFO +116 -0
- orthodox_spinner-0.1.1/README.md +99 -0
- orthodox_spinner-0.1.1/demo.py +159 -0
- orthodox_spinner-0.1.1/pyproject.toml +30 -0
- orthodox_spinner-0.1.1/shell/orthodox_spinner.zsh +73 -0
- orthodox_spinner-0.1.1/src/orthodox_spinner/__init__.py +3 -0
- orthodox_spinner-0.1.1/src/orthodox_spinner/core.py +107 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published] # only fires when you publish a GitHub Release
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
contents: read
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
# ── 1. Build & verify ────────────────────────────────────────────────────
|
|
12
|
+
build:
|
|
13
|
+
name: Build & verify
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
|
|
18
|
+
- uses: actions/setup-python@v5
|
|
19
|
+
with:
|
|
20
|
+
python-version: "3.12"
|
|
21
|
+
|
|
22
|
+
- run: pip install build twine
|
|
23
|
+
|
|
24
|
+
- name: Build wheel and sdist
|
|
25
|
+
run: python -m build
|
|
26
|
+
|
|
27
|
+
- name: Check PyPI metadata
|
|
28
|
+
run: twine check --strict dist/*
|
|
29
|
+
|
|
30
|
+
- name: Smoke-test the wheel
|
|
31
|
+
run: |
|
|
32
|
+
pip install dist/*.whl
|
|
33
|
+
spin --version
|
|
34
|
+
spin echo "orthodox cross spinner works"
|
|
35
|
+
|
|
36
|
+
- uses: actions/upload-artifact@v4
|
|
37
|
+
with:
|
|
38
|
+
name: dist
|
|
39
|
+
path: dist/
|
|
40
|
+
|
|
41
|
+
# ── 2. Upload to PyPI (only if build passes) ─────────────────────────────
|
|
42
|
+
publish:
|
|
43
|
+
name: Publish to PyPI
|
|
44
|
+
needs: build
|
|
45
|
+
runs-on: ubuntu-latest
|
|
46
|
+
steps:
|
|
47
|
+
- uses: actions/download-artifact@v4
|
|
48
|
+
with:
|
|
49
|
+
name: dist
|
|
50
|
+
path: dist/
|
|
51
|
+
|
|
52
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
53
|
+
with:
|
|
54
|
+
password: ${{ secrets.PYPI_API_TOKEN }}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: orthodox-spinner
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: A terminal spinner shaped like an orthodox cross ☦
|
|
5
|
+
Project-URL: Homepage, https://github.com/samsonofzorah/orthodox-spinner
|
|
6
|
+
Project-URL: Source, https://github.com/samsonofzorah/orthodox-spinner
|
|
7
|
+
Author-email: Vasily Piccone <vasily.piccone@gmail.com>
|
|
8
|
+
License: MIT
|
|
9
|
+
Keywords: cli,loading,spinner,terminal
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Topic :: Terminals
|
|
15
|
+
Requires-Python: >=3.8
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# orthodox-spinner ☦
|
|
19
|
+
|
|
20
|
+
A terminal spinner shaped like an orthodox cross that breathes in and out.
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
☦ npm install
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The `☦` pulses from bright to dim while any command runs, then clears — replacing whatever spinner the tool would have shown.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
**Recommended (isolated CLI tool):**
|
|
33
|
+
```sh
|
|
34
|
+
pipx install git+https://github.com/samsonofzorah/orthodox-spinner.git
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
> **macOS + Homebrew Python 3.14 note:** if pipx errors with `ImportError: dlopen … pyexpat`, pipx's shared env is on Python 3.14 which has a broken libexpat on macOS. Fix it with:
|
|
38
|
+
> ```sh
|
|
39
|
+
> PIPX_DEFAULT_PYTHON=/opt/homebrew/bin/python3.12 pipx install git+https://github.com/samsonofzorah/orthodox-spinner.git
|
|
40
|
+
> ```
|
|
41
|
+
|
|
42
|
+
**Or with pip (Python 3.11–3.13 all work):**
|
|
43
|
+
```sh
|
|
44
|
+
pip install git+https://github.com/samsonofzorah/orthodox-spinner.git
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## CLI usage
|
|
50
|
+
|
|
51
|
+
Wrap any command — its output is buffered and shown after the spinner clears:
|
|
52
|
+
|
|
53
|
+
```sh
|
|
54
|
+
spin npm install
|
|
55
|
+
spin make build
|
|
56
|
+
spin -m "Deploying…" ./deploy.sh
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
| Flag | Description |
|
|
60
|
+
|------|-------------|
|
|
61
|
+
| `-m TEXT` / `--message TEXT` | Label beside the spinner (default: the command itself) |
|
|
62
|
+
| `--version` | Print version and exit |
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Python library
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
from orthodox_spinner import Spinner
|
|
70
|
+
|
|
71
|
+
# context manager
|
|
72
|
+
with Spinner("installing"):
|
|
73
|
+
subprocess.run(["npm", "install"])
|
|
74
|
+
|
|
75
|
+
# manual control
|
|
76
|
+
s = Spinner("building").start()
|
|
77
|
+
do_work()
|
|
78
|
+
s.stop()
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## Shell integrations
|
|
84
|
+
|
|
85
|
+
To replace the built-in spinners of common tools system-wide, source the
|
|
86
|
+
provided shell file in your `~/.zshrc`:
|
|
87
|
+
|
|
88
|
+
```sh
|
|
89
|
+
echo 'source /path/to/orthodox-spinner/shell/orthodox_spinner.zsh' >> ~/.zshrc
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Or copy the relevant functions directly into your `~/.zshrc`.
|
|
93
|
+
|
|
94
|
+
**Covered tools:** `npm`, `pip`, `pip3`, `brew`, `yarn`, `cargo`
|
|
95
|
+
|
|
96
|
+
Each wrapper only activates for slow subcommands (install, build, update, etc.)
|
|
97
|
+
and falls through to the real binary for everything else.
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## How it works
|
|
102
|
+
|
|
103
|
+
`spin <cmd>` runs the command with its stdout/stderr captured, shows the
|
|
104
|
+
breathing `☦` spinner on stderr, then flushes the buffered output once the
|
|
105
|
+
command exits. The spinner is written to stderr so it does not pollute piped
|
|
106
|
+
output.
|
|
107
|
+
|
|
108
|
+
The animation uses ANSI 256-color grayscale codes (no dependencies beyond the
|
|
109
|
+
Python standard library).
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## Requirements
|
|
114
|
+
|
|
115
|
+
- Python ≥ 3.8
|
|
116
|
+
- A 256-color terminal (iTerm2, Terminal.app on macOS, most modern terminals)
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# orthodox-spinner ☦
|
|
2
|
+
|
|
3
|
+
A terminal spinner shaped like an orthodox cross that breathes in and out.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
☦ npm install
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
The `☦` pulses from bright to dim while any command runs, then clears — replacing whatever spinner the tool would have shown.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
**Recommended (isolated CLI tool):**
|
|
16
|
+
```sh
|
|
17
|
+
pipx install git+https://github.com/samsonofzorah/orthodox-spinner.git
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
> **macOS + Homebrew Python 3.14 note:** if pipx errors with `ImportError: dlopen … pyexpat`, pipx's shared env is on Python 3.14 which has a broken libexpat on macOS. Fix it with:
|
|
21
|
+
> ```sh
|
|
22
|
+
> PIPX_DEFAULT_PYTHON=/opt/homebrew/bin/python3.12 pipx install git+https://github.com/samsonofzorah/orthodox-spinner.git
|
|
23
|
+
> ```
|
|
24
|
+
|
|
25
|
+
**Or with pip (Python 3.11–3.13 all work):**
|
|
26
|
+
```sh
|
|
27
|
+
pip install git+https://github.com/samsonofzorah/orthodox-spinner.git
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## CLI usage
|
|
33
|
+
|
|
34
|
+
Wrap any command — its output is buffered and shown after the spinner clears:
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
spin npm install
|
|
38
|
+
spin make build
|
|
39
|
+
spin -m "Deploying…" ./deploy.sh
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
| Flag | Description |
|
|
43
|
+
|------|-------------|
|
|
44
|
+
| `-m TEXT` / `--message TEXT` | Label beside the spinner (default: the command itself) |
|
|
45
|
+
| `--version` | Print version and exit |
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Python library
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from orthodox_spinner import Spinner
|
|
53
|
+
|
|
54
|
+
# context manager
|
|
55
|
+
with Spinner("installing"):
|
|
56
|
+
subprocess.run(["npm", "install"])
|
|
57
|
+
|
|
58
|
+
# manual control
|
|
59
|
+
s = Spinner("building").start()
|
|
60
|
+
do_work()
|
|
61
|
+
s.stop()
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Shell integrations
|
|
67
|
+
|
|
68
|
+
To replace the built-in spinners of common tools system-wide, source the
|
|
69
|
+
provided shell file in your `~/.zshrc`:
|
|
70
|
+
|
|
71
|
+
```sh
|
|
72
|
+
echo 'source /path/to/orthodox-spinner/shell/orthodox_spinner.zsh' >> ~/.zshrc
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Or copy the relevant functions directly into your `~/.zshrc`.
|
|
76
|
+
|
|
77
|
+
**Covered tools:** `npm`, `pip`, `pip3`, `brew`, `yarn`, `cargo`
|
|
78
|
+
|
|
79
|
+
Each wrapper only activates for slow subcommands (install, build, update, etc.)
|
|
80
|
+
and falls through to the real binary for everything else.
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## How it works
|
|
85
|
+
|
|
86
|
+
`spin <cmd>` runs the command with its stdout/stderr captured, shows the
|
|
87
|
+
breathing `☦` spinner on stderr, then flushes the buffered output once the
|
|
88
|
+
command exits. The spinner is written to stderr so it does not pollute piped
|
|
89
|
+
output.
|
|
90
|
+
|
|
91
|
+
The animation uses ANSI 256-color grayscale codes (no dependencies beyond the
|
|
92
|
+
Python standard library).
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## Requirements
|
|
97
|
+
|
|
98
|
+
- Python ≥ 3.8
|
|
99
|
+
- A 256-color terminal (iTerm2, Terminal.app on macOS, most modern terminals)
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Orthodox Cross Spinner — demo of 3 animation styles.
|
|
4
|
+
Run with: python3 demo.py
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import sys
|
|
8
|
+
import time
|
|
9
|
+
|
|
10
|
+
def w(s): sys.stdout.write(s); sys.stdout.flush()
|
|
11
|
+
|
|
12
|
+
HIDE = '\033[?25l'
|
|
13
|
+
SHOW = '\033[?25h'
|
|
14
|
+
GOLD = '\033[38;5;220m'
|
|
15
|
+
DIM_G = '\033[38;5;136m'
|
|
16
|
+
DIM_W = '\033[38;5;242m'
|
|
17
|
+
WHITE = '\033[97m'
|
|
18
|
+
RST = '\033[0m'
|
|
19
|
+
|
|
20
|
+
# ── 1. Breathing — single ☦ pulsing in brightness ────────────────────────────
|
|
21
|
+
|
|
22
|
+
def breathing_frames():
|
|
23
|
+
cross = '☦'
|
|
24
|
+
lo, hi = 238, 255
|
|
25
|
+
ramp = list(range(hi, lo - 1, -1)) + list(range(lo + 1, hi + 1, 1))
|
|
26
|
+
return [f'\033[38;5;{n}m{cross}{RST}' for n in ramp]
|
|
27
|
+
|
|
28
|
+
def run_breathing(seconds=7, fps=18):
|
|
29
|
+
frames = breathing_frames()
|
|
30
|
+
n = len(frames)
|
|
31
|
+
end = time.time() + seconds
|
|
32
|
+
i = 0
|
|
33
|
+
while time.time() < end:
|
|
34
|
+
w(f'\r\033[K {frames[i % n]} loading...')
|
|
35
|
+
time.sleep(1 / fps)
|
|
36
|
+
i += 1
|
|
37
|
+
w('\r\033[K')
|
|
38
|
+
|
|
39
|
+
# ── 2. Dots drawing the cross segment-by-segment, then erasing ───────────────
|
|
40
|
+
|
|
41
|
+
W, H = 7, 5
|
|
42
|
+
|
|
43
|
+
# Each list is one segment of the orthodox cross, drawn in order:
|
|
44
|
+
# top small bar → upper shaft → main crossbar → lower shaft → diagonal
|
|
45
|
+
SEGS = [
|
|
46
|
+
[(0, 3)],
|
|
47
|
+
[(1, 3)],
|
|
48
|
+
[(2, 0),(2,1),(2,2),(2,3),(2,4),(2,5),(2,6)],
|
|
49
|
+
[(3, 3)],
|
|
50
|
+
[(4, 2),(4, 1)],
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
def dot_grid(lit, char='●', color=GOLD):
|
|
54
|
+
g = [[' '] * W for _ in range(H)]
|
|
55
|
+
for r, c in lit:
|
|
56
|
+
g[r][c] = f'{color}{char}{RST}'
|
|
57
|
+
return [''.join(row) for row in g]
|
|
58
|
+
|
|
59
|
+
def dot_frames():
|
|
60
|
+
fs = []
|
|
61
|
+
lit = []
|
|
62
|
+
for seg in SEGS:
|
|
63
|
+
lit = lit + seg
|
|
64
|
+
fs += [dot_grid(lit)] * 4 # draw each segment
|
|
65
|
+
all_pts = [p for s in SEGS for p in s]
|
|
66
|
+
for k in range(8): # glow: alternate filled / hollow
|
|
67
|
+
char = '●' if k % 2 == 0 else '○'
|
|
68
|
+
color = GOLD if k % 2 == 0 else DIM_G
|
|
69
|
+
fs += [dot_grid(all_pts, char, color)] * 3
|
|
70
|
+
lit = list(all_pts)
|
|
71
|
+
for seg in reversed(SEGS): # erase in reverse order
|
|
72
|
+
for p in seg:
|
|
73
|
+
lit.remove(p)
|
|
74
|
+
fs += [dot_grid(lit)] * 4
|
|
75
|
+
fs += [dot_grid([])] * 4 # pause on empty
|
|
76
|
+
return fs
|
|
77
|
+
|
|
78
|
+
# ── 3. Bright dot travels along the arms of the cross ────────────────────────
|
|
79
|
+
|
|
80
|
+
def base_grid():
|
|
81
|
+
g = [[' '] * W for _ in range(H)]
|
|
82
|
+
g[0][3] = f'{DIM_W}─{RST}'
|
|
83
|
+
g[1][3] = f'{DIM_W}│{RST}'
|
|
84
|
+
for c in range(W):
|
|
85
|
+
g[2][c] = f'{DIM_W}─{RST}'
|
|
86
|
+
g[2][3] = f'{DIM_W}┼{RST}'
|
|
87
|
+
g[3][3] = f'{DIM_W}│{RST}'
|
|
88
|
+
g[4][1] = f'{DIM_W}╲{RST}'
|
|
89
|
+
g[4][2] = f'{DIM_W}╲{RST}'
|
|
90
|
+
return g
|
|
91
|
+
|
|
92
|
+
def travel_frames():
|
|
93
|
+
path = (
|
|
94
|
+
[(2,3)]
|
|
95
|
+
+ [(1,3),(0,3)] # up to top bar
|
|
96
|
+
+ [(1,3),(2,3)] # back to center
|
|
97
|
+
+ [(2,4),(2,5),(2,6)] # right arm
|
|
98
|
+
+ [(2,5),(2,4),(2,3)] # back
|
|
99
|
+
+ [(2,2),(2,1),(2,0)] # left arm
|
|
100
|
+
+ [(2,1),(2,2),(2,3)] # back
|
|
101
|
+
+ [(3,3),(4,2),(4,1)] # down + diagonal
|
|
102
|
+
+ [(4,2),(3,3),(2,3)] # back to center
|
|
103
|
+
)
|
|
104
|
+
fs = []
|
|
105
|
+
for r, c in path:
|
|
106
|
+
g = base_grid()
|
|
107
|
+
g[r][c] = f'{WHITE}●{RST}'
|
|
108
|
+
rows = [''.join(row) for row in g]
|
|
109
|
+
fs += [rows, rows] # hold each position 2 frames
|
|
110
|
+
return fs
|
|
111
|
+
|
|
112
|
+
# ── Shared multi-line runner ──────────────────────────────────────────────────
|
|
113
|
+
|
|
114
|
+
def run_ml(frames, fps=10, loops=1):
|
|
115
|
+
total = len(frames) * loops
|
|
116
|
+
first = True
|
|
117
|
+
for i in range(total):
|
|
118
|
+
f = frames[i % len(frames)]
|
|
119
|
+
if not first:
|
|
120
|
+
w(f'\033[{H}A')
|
|
121
|
+
for row in f:
|
|
122
|
+
w(f'\r\033[2K {row}\n')
|
|
123
|
+
first = False
|
|
124
|
+
time.sleep(1 / fps)
|
|
125
|
+
|
|
126
|
+
# ── Entry point ───────────────────────────────────────────────────────────────
|
|
127
|
+
|
|
128
|
+
def hdr(n, title):
|
|
129
|
+
bar = '─' * (len(title) + 6)
|
|
130
|
+
print(f'\n [{n}/3] {title}')
|
|
131
|
+
print(f' {bar}\n')
|
|
132
|
+
|
|
133
|
+
def main():
|
|
134
|
+
try:
|
|
135
|
+
w(HIDE)
|
|
136
|
+
print('\n ☦ Orthodox Cross Spinner — Animation Demo')
|
|
137
|
+
print(' Ctrl-C to exit early.\n')
|
|
138
|
+
|
|
139
|
+
hdr(1, 'Breathing — single ☦ oscillating in brightness')
|
|
140
|
+
run_breathing(seconds=7, fps=18)
|
|
141
|
+
print()
|
|
142
|
+
time.sleep(0.6)
|
|
143
|
+
|
|
144
|
+
hdr(2, 'Dot cross — draws then erases the cross with dots')
|
|
145
|
+
run_ml(dot_frames(), fps=10, loops=2)
|
|
146
|
+
time.sleep(0.6)
|
|
147
|
+
|
|
148
|
+
hdr(3, 'Traveler — dot traces each arm of the cross')
|
|
149
|
+
run_ml(travel_frames(), fps=10, loops=3)
|
|
150
|
+
|
|
151
|
+
print('\n\n Done — let me know which style (or combo) you want!\n')
|
|
152
|
+
|
|
153
|
+
except KeyboardInterrupt:
|
|
154
|
+
print('\n\n (exited early)\n')
|
|
155
|
+
finally:
|
|
156
|
+
w(SHOW)
|
|
157
|
+
|
|
158
|
+
if __name__ == '__main__':
|
|
159
|
+
main()
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "orthodox-spinner"
|
|
7
|
+
version = "0.1.1"
|
|
8
|
+
description = "A terminal spinner shaped like an orthodox cross ☦"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "Vasily Piccone", email = "vasily.piccone@gmail.com" }]
|
|
13
|
+
keywords = ["spinner", "terminal", "cli", "loading"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Environment :: Console",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Topic :: Terminals",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
[project.urls]
|
|
23
|
+
Homepage = "https://github.com/samsonofzorah/orthodox-spinner"
|
|
24
|
+
Source = "https://github.com/samsonofzorah/orthodox-spinner"
|
|
25
|
+
|
|
26
|
+
[project.scripts]
|
|
27
|
+
spin = "orthodox_spinner.core:cli"
|
|
28
|
+
|
|
29
|
+
[tool.hatch.build.targets.wheel]
|
|
30
|
+
packages = ["src/orthodox_spinner"]
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# orthodox_spinner.zsh
|
|
2
|
+
# Source this file in ~/.zshrc to replace default spinners in common tools
|
|
3
|
+
# with the breathing orthodox cross ☦.
|
|
4
|
+
#
|
|
5
|
+
# Usage: add to ~/.zshrc:
|
|
6
|
+
# source /path/to/orthodox-spinner/shell/orthodox_spinner.zsh
|
|
7
|
+
|
|
8
|
+
# Internal helper — finds the real binary in PATH (bypasses these wrappers)
|
|
9
|
+
# and hands off to `spin`.
|
|
10
|
+
_spin_wrap() {
|
|
11
|
+
local cmd="$1" msg="$2"
|
|
12
|
+
shift 2
|
|
13
|
+
spin -m "$msg" /usr/bin/env "$cmd" "$@"
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
# npm — disable its own progress bar and replace with ☦
|
|
17
|
+
npm() {
|
|
18
|
+
case "$1" in
|
|
19
|
+
install|i|ci|update|up|add|remove|uninstall|run)
|
|
20
|
+
_spin_wrap npm "npm $*" --no-progress "$@" ;;
|
|
21
|
+
*)
|
|
22
|
+
/usr/bin/env npm "$@" ;;
|
|
23
|
+
esac
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
# pip / pip3 — suppress pip's own progress output
|
|
27
|
+
pip() {
|
|
28
|
+
case "$1" in
|
|
29
|
+
install|download|uninstall|wheel)
|
|
30
|
+
_spin_wrap pip "pip $*" "$@" ;;
|
|
31
|
+
*)
|
|
32
|
+
/usr/bin/env pip "$@" ;;
|
|
33
|
+
esac
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
pip3() {
|
|
37
|
+
case "$1" in
|
|
38
|
+
install|download|uninstall|wheel)
|
|
39
|
+
_spin_wrap pip3 "pip3 $*" "$@" ;;
|
|
40
|
+
*)
|
|
41
|
+
/usr/bin/env pip3 "$@" ;;
|
|
42
|
+
esac
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
# brew — wrap slow subcommands only
|
|
46
|
+
brew() {
|
|
47
|
+
case "$1" in
|
|
48
|
+
install|reinstall|upgrade|uninstall|remove|fetch)
|
|
49
|
+
_spin_wrap brew "brew $*" "$@" ;;
|
|
50
|
+
*)
|
|
51
|
+
/usr/bin/env brew "$@" ;;
|
|
52
|
+
esac
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
# yarn
|
|
56
|
+
yarn() {
|
|
57
|
+
case "$1" in
|
|
58
|
+
add|remove|install|upgrade|up)
|
|
59
|
+
_spin_wrap yarn "yarn $*" "$@" ;;
|
|
60
|
+
*)
|
|
61
|
+
/usr/bin/env yarn "$@" ;;
|
|
62
|
+
esac
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
# cargo
|
|
66
|
+
cargo() {
|
|
67
|
+
case "$1" in
|
|
68
|
+
build|check|test|run|install|fetch)
|
|
69
|
+
_spin_wrap cargo "cargo $*" "$@" ;;
|
|
70
|
+
*)
|
|
71
|
+
/usr/bin/env cargo "$@" ;;
|
|
72
|
+
esac
|
|
73
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import time
|
|
3
|
+
import threading
|
|
4
|
+
import subprocess
|
|
5
|
+
import argparse
|
|
6
|
+
from importlib.metadata import version as _pkg_version
|
|
7
|
+
|
|
8
|
+
HIDE = '\033[?25l'
|
|
9
|
+
SHOW = '\033[?25h'
|
|
10
|
+
RST = '\033[0m'
|
|
11
|
+
|
|
12
|
+
def _frames():
|
|
13
|
+
lo, hi = 238, 255
|
|
14
|
+
ramp = list(range(hi, lo - 1, -1)) + list(range(lo + 1, hi + 1, 1))
|
|
15
|
+
return [f'\033[38;5;{n}m☦{RST}' for n in ramp]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Spinner:
|
|
19
|
+
"""
|
|
20
|
+
Breathing orthodox cross spinner ☦.
|
|
21
|
+
|
|
22
|
+
Usage as context manager:
|
|
23
|
+
with Spinner("installing"):
|
|
24
|
+
...
|
|
25
|
+
|
|
26
|
+
Usage manual:
|
|
27
|
+
s = Spinner("building").start()
|
|
28
|
+
...
|
|
29
|
+
s.stop()
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(self, text='', fps=18):
|
|
33
|
+
self.text = text
|
|
34
|
+
self.fps = fps
|
|
35
|
+
self._stop = threading.Event()
|
|
36
|
+
self._thread = None
|
|
37
|
+
|
|
38
|
+
def start(self):
|
|
39
|
+
sys.stderr.write(HIDE)
|
|
40
|
+
sys.stderr.flush()
|
|
41
|
+
self._stop.clear()
|
|
42
|
+
self._thread = threading.Thread(target=self._spin, daemon=True)
|
|
43
|
+
self._thread.start()
|
|
44
|
+
return self
|
|
45
|
+
|
|
46
|
+
def stop(self, success=True):
|
|
47
|
+
self._stop.set()
|
|
48
|
+
if self._thread:
|
|
49
|
+
self._thread.join()
|
|
50
|
+
sys.stderr.write(f'\r\033[K{SHOW}')
|
|
51
|
+
sys.stderr.flush()
|
|
52
|
+
|
|
53
|
+
def _spin(self):
|
|
54
|
+
frames = _frames()
|
|
55
|
+
n = len(frames)
|
|
56
|
+
i = 0
|
|
57
|
+
while not self._stop.is_set():
|
|
58
|
+
suffix = f' {self.text}' if self.text else ''
|
|
59
|
+
sys.stderr.write(f'\r\033[K{frames[i % n]}{suffix}')
|
|
60
|
+
sys.stderr.flush()
|
|
61
|
+
time.sleep(1 / self.fps)
|
|
62
|
+
i += 1
|
|
63
|
+
|
|
64
|
+
def __enter__(self):
|
|
65
|
+
return self.start()
|
|
66
|
+
|
|
67
|
+
def __exit__(self, exc_type, *_):
|
|
68
|
+
self.stop(success=exc_type is None)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def cli():
|
|
72
|
+
parser = argparse.ArgumentParser(
|
|
73
|
+
prog='spin',
|
|
74
|
+
description='Run a command with an orthodox cross spinner ☦',
|
|
75
|
+
)
|
|
76
|
+
parser.add_argument(
|
|
77
|
+
'--version',
|
|
78
|
+
action='version',
|
|
79
|
+
version=f'%(prog)s {_pkg_version("orthodox-spinner")}',
|
|
80
|
+
)
|
|
81
|
+
parser.add_argument(
|
|
82
|
+
'-m', '--message',
|
|
83
|
+
default='',
|
|
84
|
+
metavar='TEXT',
|
|
85
|
+
help='label shown beside the spinner (defaults to the command)',
|
|
86
|
+
)
|
|
87
|
+
parser.add_argument('command', nargs=argparse.REMAINDER)
|
|
88
|
+
args = parser.parse_args()
|
|
89
|
+
|
|
90
|
+
if not args.command:
|
|
91
|
+
parser.print_help()
|
|
92
|
+
sys.exit(0)
|
|
93
|
+
|
|
94
|
+
label = args.message or ' '.join(args.command)
|
|
95
|
+
|
|
96
|
+
with Spinner(text=label):
|
|
97
|
+
result = subprocess.run(args.command, capture_output=True)
|
|
98
|
+
|
|
99
|
+
# flush buffered output after spinner clears
|
|
100
|
+
if result.stdout:
|
|
101
|
+
sys.stdout.buffer.write(result.stdout)
|
|
102
|
+
sys.stdout.buffer.flush()
|
|
103
|
+
if result.stderr:
|
|
104
|
+
sys.stderr.buffer.write(result.stderr)
|
|
105
|
+
sys.stderr.buffer.flush()
|
|
106
|
+
|
|
107
|
+
sys.exit(result.returncode)
|