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.
@@ -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,6 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ .venv/
@@ -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,3 @@
1
+ from .core import Spinner
2
+
3
+ __all__ = ["Spinner"]
@@ -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)