sciglyph 0.1.0__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.
sciglyph-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Guo Cheng
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,187 @@
1
+ Metadata-Version: 2.4
2
+ Name: sciglyph
3
+ Version: 0.1.0
4
+ Summary: Publication-quality scientific illustration in pure matplotlib - no BioRender, no Illustrator
5
+ Author: Guo Cheng
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/GuoCheng24/sciglyph
8
+ Project-URL: Issues, https://github.com/GuoCheng24/sciglyph/issues
9
+ Keywords: matplotlib,scientific-figures,publication,illustration,graphical-abstract,bioinformatics,deep-learning,diagram
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Science/Research
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Topic :: Scientific/Engineering :: Visualization
15
+ Requires-Python: >=3.9
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Requires-Dist: matplotlib>=3.5
19
+ Requires-Dist: numpy>=1.20
20
+ Dynamic: license-file
21
+
22
+ # sciglyph
23
+
24
+ [![test](https://github.com/GuoCheng24/sciglyph/actions/workflows/test.yml/badge.svg)](https://github.com/GuoCheng24/sciglyph/actions/workflows/test.yml) [![python](https://img.shields.io/badge/python-3.9%2B-blue)](https://www.python.org/) [![license](https://img.shields.io/badge/license-MIT-green)](LICENSE)
25
+
26
+ **Publication-quality scientific illustration in pure matplotlib — no BioRender, no Illustrator.**
27
+
28
+ Overview figures and architecture diagrams are usually drawn by hand in a
29
+ subscription tool. That makes them pretty, but also unreproducible: you cannot
30
+ diff them, you cannot regenerate them when the numbers change, and you cannot
31
+ put them under version control.
32
+
33
+ `sciglyph` gives you the primitives to draw the same figures as **code**.
34
+
35
+ <p align="center">
36
+ <img src="gallery/overview_figure.png" width="88%">
37
+ </p>
38
+
39
+ <p align="center">
40
+ <img src="gallery/architecture.png" width="100%">
41
+ </p>
42
+
43
+ <sub>Both figures above are generated by the scripts in
44
+ <a href="examples/">examples/</a> — nothing was touched by hand. The content is
45
+ synthetic; swap in your own numbers and the layout carries over.</sub>
46
+
47
+ ---
48
+
49
+ ## Why
50
+
51
+ | | subscription tools | `sciglyph` |
52
+ |---|---|---|
53
+ | Reproducible | ✗ manual pixel-pushing | ✓ a script |
54
+ | Version control | ✗ binary blobs | ✓ diffable source |
55
+ | Data-driven | ✗ retype every number | ✓ read straight from your results |
56
+ | Vector output | ~ depends on export | ✓ PDF/SVG with editable text |
57
+ | Cost | subscription | free, MIT |
58
+
59
+ ## Install
60
+
61
+ ```bash
62
+ pip install git+https://github.com/GuoCheng24/sciglyph
63
+ ```
64
+
65
+ Only `matplotlib` and `numpy`. Nothing else.
66
+
67
+ <sub>Not on PyPI yet, so the git URL above is the install line that works today.
68
+ When it lands, `pip install sciglyph` will too.</sub>
69
+
70
+ ## Quick start
71
+
72
+ ```python
73
+ import matplotlib.pyplot as plt
74
+ from sciglyph import bio, set_canvas, report, RC
75
+
76
+ plt.rcParams.update(RC)
77
+ fig = plt.figure(figsize=(7.2, 3.0), dpi=300)
78
+ ax = fig.add_axes([0, 0, 1, 1]); ax.set_xlim(0, 1); ax.set_ylim(0, 1); ax.axis("off")
79
+ set_canvas(fig) # required on non-square canvases
80
+
81
+ bio.person(ax, .08, .55, s=.30)
82
+ bio.dna(ax, .25, .55, w=.05, h=.45, n=2)
83
+ bio.cell(ax, .42, .55, r=.06, seed=1)
84
+ bio.seq_logo(ax, .60, .40, [("A", .6), ("C", .9), ("G", .4), ("T", .7)], w=.03)
85
+
86
+ report(fig, ax) # catch text collisions before saving
87
+ fig.savefig("figure.pdf", bbox_inches="tight")
88
+ ```
89
+
90
+ Run the full examples:
91
+
92
+ ```bash
93
+ python examples/overview_figure.py # -> gallery/overview_figure.png
94
+ python examples/architecture.py # -> gallery/architecture.png
95
+ ```
96
+
97
+ ## What's included
98
+
99
+ **`sciglyph.bio`** — glyphs for Nature/Science-style overview figures:
100
+ `person` (cohorts) · `dna` · `cell` · `lipid` · `metabolite` ·
101
+ `nucleosome_chain` · `umap_layer` (the stacked atlas look) ·
102
+ `seq_logo` (information-scaled letters, no logomaker needed) ·
103
+ `stacked_planes` · `rbox` · `arr`
104
+
105
+ **`sciglyph.arch`** — glyphs for architecture diagrams:
106
+ `cuboid` / `feature_stack` (3-D feature blocks) · `trapezoid` (encoders) ·
107
+ `module_stack` (`Conv|BN|ReLU` bars) · `dashed_group` (the `(a)/(b)/(c)`
108
+ language) · `flow` · `op_circle` · `snowflake` (frozen backbone) ·
109
+ `image_thumb` · `embedding_space` (contrastive panels) · `loss_tag` · `bracket`
110
+
111
+ **`sciglyph.layout`** — pre-flight collision detection.
112
+
113
+ ## Catching layout bugs before you save
114
+
115
+ When a figure breaks, it is almost never the artwork — it is the layout.
116
+ `report()` uses the real rendered bounding boxes to find overlapping text, so
117
+ you do not have to hunt for it by eye:
118
+
119
+ ```python
120
+ report(fig, ax)
121
+ # [sciglyph.layout] 36 text objects
122
+ # ! 'CD4 Treg/-FOXP3' x 'SMR' overlap 92%
123
+ ```
124
+
125
+ It also works from the command line on any script that exposes `fig` and `ax`:
126
+
127
+ ```bash
128
+ python -m sciglyph.layout my_figure.py
129
+ ```
130
+
131
+ It checks three things, each of which shipped a broken figure before it existed:
132
+
133
+ | check | what it catches |
134
+ |---|---|
135
+ | text overlap | two labels drawn over each other |
136
+ | **artwork overlap** | a row of boxes laid out slightly too wide, so each one covers its neighbour — the strings may not overlap at all, so text-level checks miss it entirely |
137
+ | **missing glyphs** | a character the font cannot draw, rendered as an empty box. Symbols typed as literals (`✓`, `❄`) are the usual casualty |
138
+
139
+ Two kinds of overlap are deliberately *not* reported, because they are the
140
+ layout working: a panel containing its contents, and an unfilled dashed shape —
141
+ a ring drawn around the thing it annotates.
142
+
143
+ **These are geometric checks.** Whether the figure actually *reads* well still
144
+ needs your eyes.
145
+
146
+ ## Notes from actually shipping these figures
147
+
148
+ - **Call `set_canvas(fig)`.** In `[0,1]` coordinates a "circle" is `r·W` wide
149
+ and `r·H` tall. On a 12×3 canvas, every circle becomes a rugby ball.
150
+ - **Anchor arrows to what `feature_stack` returns**, not to hard-coded
151
+ coordinates — otherwise changing the number of blocks silently breaks them.
152
+ - **Never put symbol codepoints in figure text.** `❄` (U+2744) is missing from
153
+ most sans fonts and renders as a tofu box. Draw it (`arch.snowflake`).
154
+ - **Overlapping translucent fills blend into one muddy colour.** Keep the fill
155
+ under `alpha=0.15`, stroke each curve, *and* offset the peaks. Tuning alpha
156
+ alone will not save you.
157
+ - **Fonts:** Arial/Helvetica are often absent on Linux. `RC` falls back to
158
+ Liberation Sans (metric-compatible with Arial) and sets `pdf.fonttype=42`
159
+ so text stays editable in the PDF — a hard requirement at most journals.
160
+ - **Don't move elements toward whitespace.** Whitespace relocates, it does not
161
+ disappear. Decide which row an element belongs to, move it as a group, then
162
+ verify with the quadrant ink distribution.
163
+
164
+ ## Honest scope
165
+
166
+ This gets you clean flat schematics combined with data panels — the register
167
+ of a Nature/Science overview figure or a TPAMI architecture diagram. It will
168
+ **not** reproduce hand-drawn illustration (shaded organs, textured cells,
169
+ gradients). For that, embed a CC-BY asset and cite it rather than fake it.
170
+
171
+ ## License
172
+
173
+ MIT © Guo Cheng
174
+
175
+ ## 关于那行 star 提示
176
+
177
+ 调用 `report()` 时,`sciglyph` 会在**第 5 次和第 25 次**往 stderr 写一行,提一句这个仓库在哪。**一辈子只有这两次**,此外再不出声。
178
+
179
+ 它不会出现在:管道或重定向里(stderr 不是终端就直接返回,连计数文件都不建)、CI 环境里(`CI` / `GITHUB_ACTIONS`)。它写的是 stderr 而非 stdout,所以不会污染你的数据输出;它包在 `try/finally` 里且吞掉自身所有异常,**不会改变退出码,也不会影响结果**。
180
+
181
+ 永久关掉:
182
+
183
+ ```bash
184
+ export SCIGLYPH_NO_NUDGE=1
185
+ ```
186
+
187
+ 计数存在 `$XDG_STATE_HOME/sciglyph/usage.json`(默认 `~/.local/state/sciglyph/usage.json`),删掉即重置。
@@ -0,0 +1,166 @@
1
+ # sciglyph
2
+
3
+ [![test](https://github.com/GuoCheng24/sciglyph/actions/workflows/test.yml/badge.svg)](https://github.com/GuoCheng24/sciglyph/actions/workflows/test.yml) [![python](https://img.shields.io/badge/python-3.9%2B-blue)](https://www.python.org/) [![license](https://img.shields.io/badge/license-MIT-green)](LICENSE)
4
+
5
+ **Publication-quality scientific illustration in pure matplotlib — no BioRender, no Illustrator.**
6
+
7
+ Overview figures and architecture diagrams are usually drawn by hand in a
8
+ subscription tool. That makes them pretty, but also unreproducible: you cannot
9
+ diff them, you cannot regenerate them when the numbers change, and you cannot
10
+ put them under version control.
11
+
12
+ `sciglyph` gives you the primitives to draw the same figures as **code**.
13
+
14
+ <p align="center">
15
+ <img src="gallery/overview_figure.png" width="88%">
16
+ </p>
17
+
18
+ <p align="center">
19
+ <img src="gallery/architecture.png" width="100%">
20
+ </p>
21
+
22
+ <sub>Both figures above are generated by the scripts in
23
+ <a href="examples/">examples/</a> — nothing was touched by hand. The content is
24
+ synthetic; swap in your own numbers and the layout carries over.</sub>
25
+
26
+ ---
27
+
28
+ ## Why
29
+
30
+ | | subscription tools | `sciglyph` |
31
+ |---|---|---|
32
+ | Reproducible | ✗ manual pixel-pushing | ✓ a script |
33
+ | Version control | ✗ binary blobs | ✓ diffable source |
34
+ | Data-driven | ✗ retype every number | ✓ read straight from your results |
35
+ | Vector output | ~ depends on export | ✓ PDF/SVG with editable text |
36
+ | Cost | subscription | free, MIT |
37
+
38
+ ## Install
39
+
40
+ ```bash
41
+ pip install git+https://github.com/GuoCheng24/sciglyph
42
+ ```
43
+
44
+ Only `matplotlib` and `numpy`. Nothing else.
45
+
46
+ <sub>Not on PyPI yet, so the git URL above is the install line that works today.
47
+ When it lands, `pip install sciglyph` will too.</sub>
48
+
49
+ ## Quick start
50
+
51
+ ```python
52
+ import matplotlib.pyplot as plt
53
+ from sciglyph import bio, set_canvas, report, RC
54
+
55
+ plt.rcParams.update(RC)
56
+ fig = plt.figure(figsize=(7.2, 3.0), dpi=300)
57
+ ax = fig.add_axes([0, 0, 1, 1]); ax.set_xlim(0, 1); ax.set_ylim(0, 1); ax.axis("off")
58
+ set_canvas(fig) # required on non-square canvases
59
+
60
+ bio.person(ax, .08, .55, s=.30)
61
+ bio.dna(ax, .25, .55, w=.05, h=.45, n=2)
62
+ bio.cell(ax, .42, .55, r=.06, seed=1)
63
+ bio.seq_logo(ax, .60, .40, [("A", .6), ("C", .9), ("G", .4), ("T", .7)], w=.03)
64
+
65
+ report(fig, ax) # catch text collisions before saving
66
+ fig.savefig("figure.pdf", bbox_inches="tight")
67
+ ```
68
+
69
+ Run the full examples:
70
+
71
+ ```bash
72
+ python examples/overview_figure.py # -> gallery/overview_figure.png
73
+ python examples/architecture.py # -> gallery/architecture.png
74
+ ```
75
+
76
+ ## What's included
77
+
78
+ **`sciglyph.bio`** — glyphs for Nature/Science-style overview figures:
79
+ `person` (cohorts) · `dna` · `cell` · `lipid` · `metabolite` ·
80
+ `nucleosome_chain` · `umap_layer` (the stacked atlas look) ·
81
+ `seq_logo` (information-scaled letters, no logomaker needed) ·
82
+ `stacked_planes` · `rbox` · `arr`
83
+
84
+ **`sciglyph.arch`** — glyphs for architecture diagrams:
85
+ `cuboid` / `feature_stack` (3-D feature blocks) · `trapezoid` (encoders) ·
86
+ `module_stack` (`Conv|BN|ReLU` bars) · `dashed_group` (the `(a)/(b)/(c)`
87
+ language) · `flow` · `op_circle` · `snowflake` (frozen backbone) ·
88
+ `image_thumb` · `embedding_space` (contrastive panels) · `loss_tag` · `bracket`
89
+
90
+ **`sciglyph.layout`** — pre-flight collision detection.
91
+
92
+ ## Catching layout bugs before you save
93
+
94
+ When a figure breaks, it is almost never the artwork — it is the layout.
95
+ `report()` uses the real rendered bounding boxes to find overlapping text, so
96
+ you do not have to hunt for it by eye:
97
+
98
+ ```python
99
+ report(fig, ax)
100
+ # [sciglyph.layout] 36 text objects
101
+ # ! 'CD4 Treg/-FOXP3' x 'SMR' overlap 92%
102
+ ```
103
+
104
+ It also works from the command line on any script that exposes `fig` and `ax`:
105
+
106
+ ```bash
107
+ python -m sciglyph.layout my_figure.py
108
+ ```
109
+
110
+ It checks three things, each of which shipped a broken figure before it existed:
111
+
112
+ | check | what it catches |
113
+ |---|---|
114
+ | text overlap | two labels drawn over each other |
115
+ | **artwork overlap** | a row of boxes laid out slightly too wide, so each one covers its neighbour — the strings may not overlap at all, so text-level checks miss it entirely |
116
+ | **missing glyphs** | a character the font cannot draw, rendered as an empty box. Symbols typed as literals (`✓`, `❄`) are the usual casualty |
117
+
118
+ Two kinds of overlap are deliberately *not* reported, because they are the
119
+ layout working: a panel containing its contents, and an unfilled dashed shape —
120
+ a ring drawn around the thing it annotates.
121
+
122
+ **These are geometric checks.** Whether the figure actually *reads* well still
123
+ needs your eyes.
124
+
125
+ ## Notes from actually shipping these figures
126
+
127
+ - **Call `set_canvas(fig)`.** In `[0,1]` coordinates a "circle" is `r·W` wide
128
+ and `r·H` tall. On a 12×3 canvas, every circle becomes a rugby ball.
129
+ - **Anchor arrows to what `feature_stack` returns**, not to hard-coded
130
+ coordinates — otherwise changing the number of blocks silently breaks them.
131
+ - **Never put symbol codepoints in figure text.** `❄` (U+2744) is missing from
132
+ most sans fonts and renders as a tofu box. Draw it (`arch.snowflake`).
133
+ - **Overlapping translucent fills blend into one muddy colour.** Keep the fill
134
+ under `alpha=0.15`, stroke each curve, *and* offset the peaks. Tuning alpha
135
+ alone will not save you.
136
+ - **Fonts:** Arial/Helvetica are often absent on Linux. `RC` falls back to
137
+ Liberation Sans (metric-compatible with Arial) and sets `pdf.fonttype=42`
138
+ so text stays editable in the PDF — a hard requirement at most journals.
139
+ - **Don't move elements toward whitespace.** Whitespace relocates, it does not
140
+ disappear. Decide which row an element belongs to, move it as a group, then
141
+ verify with the quadrant ink distribution.
142
+
143
+ ## Honest scope
144
+
145
+ This gets you clean flat schematics combined with data panels — the register
146
+ of a Nature/Science overview figure or a TPAMI architecture diagram. It will
147
+ **not** reproduce hand-drawn illustration (shaded organs, textured cells,
148
+ gradients). For that, embed a CC-BY asset and cite it rather than fake it.
149
+
150
+ ## License
151
+
152
+ MIT © Guo Cheng
153
+
154
+ ## 关于那行 star 提示
155
+
156
+ 调用 `report()` 时,`sciglyph` 会在**第 5 次和第 25 次**往 stderr 写一行,提一句这个仓库在哪。**一辈子只有这两次**,此外再不出声。
157
+
158
+ 它不会出现在:管道或重定向里(stderr 不是终端就直接返回,连计数文件都不建)、CI 环境里(`CI` / `GITHUB_ACTIONS`)。它写的是 stderr 而非 stdout,所以不会污染你的数据输出;它包在 `try/finally` 里且吞掉自身所有异常,**不会改变退出码,也不会影响结果**。
159
+
160
+ 永久关掉:
161
+
162
+ ```bash
163
+ export SCIGLYPH_NO_NUDGE=1
164
+ ```
165
+
166
+ 计数存在 `$XDG_STATE_HOME/sciglyph/usage.json`(默认 `~/.local/state/sciglyph/usage.json`),删掉即重置。
@@ -0,0 +1,29 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "sciglyph"
7
+ version = "0.1.0"
8
+ description = "Publication-quality scientific illustration in pure matplotlib - no BioRender, no Illustrator"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "Guo Cheng" }]
13
+ keywords = ["matplotlib", "scientific-figures", "publication", "illustration",
14
+ "graphical-abstract", "bioinformatics", "deep-learning", "diagram"]
15
+ classifiers = [
16
+ "Development Status :: 4 - Beta",
17
+ "Intended Audience :: Science/Research",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Programming Language :: Python :: 3",
20
+ "Topic :: Scientific/Engineering :: Visualization",
21
+ ]
22
+ dependencies = ["matplotlib>=3.5", "numpy>=1.20"]
23
+
24
+ [project.urls]
25
+ Homepage = "https://github.com/GuoCheng24/sciglyph"
26
+ Issues = "https://github.com/GuoCheng24/sciglyph/issues"
27
+
28
+ [tool.setuptools.packages.find]
29
+ include = ["sciglyph*"]
@@ -0,0 +1,35 @@
1
+ """sciglyph - publication-quality scientific illustration in pure matplotlib.
2
+
3
+ Two glyph families plus a layout checker:
4
+
5
+ * :mod:`sciglyph.bio` biological / omics glyphs for Nature-style overview figures
6
+ * :mod:`sciglyph.arch` neural-network architecture glyphs (TPAMI / CVPR style)
7
+ * :mod:`sciglyph.layout` pre-flight text-collision detection
8
+
9
+ Quick start::
10
+
11
+ import matplotlib.pyplot as plt
12
+ from sciglyph import bio, set_canvas, RC, report
13
+
14
+ plt.rcParams.update(RC)
15
+ fig = plt.figure(figsize=(7.2, 4.0), dpi=300)
16
+ ax = fig.add_axes([0, 0, 1, 1]); ax.set_xlim(0, 1); ax.set_ylim(0, 1); ax.axis("off")
17
+ set_canvas(fig) # required on non-square canvases
18
+
19
+ bio.dna(ax, .2, .5, w=.05, h=.4, n=2)
20
+ bio.cell(ax, .4, .5, r=.05, seed=1)
21
+
22
+ report(fig, ax) # check for text collisions
23
+ fig.savefig("figure.pdf", bbox_inches="tight")
24
+ """
25
+
26
+ from ._canvas import set_canvas, aspect
27
+ from .layout import text_collisions, report
28
+ from . import bio
29
+ from . import arch
30
+
31
+ RC = bio.RC
32
+
33
+ __version__ = "0.1.0"
34
+ __all__ = ["bio", "arch", "layout", "set_canvas", "aspect",
35
+ "text_collisions", "report", "RC", "__version__"]
@@ -0,0 +1,37 @@
1
+ """Shared canvas state: aspect-ratio compensation for round primitives.
2
+
3
+ In a [0,1] x [0,1] coordinate system a "circle" of radius r is physically
4
+ r*W wide and r*H tall, where W and H are the figure size in inches. On any
5
+ non-square canvas circles therefore render as ellipses -- measured on a 6x2
6
+ canvas, human heads and cells came out badly flattened.
7
+
8
+ Every round primitive in sciglyph divides its horizontal extent by this ratio,
9
+ so you get visually round shapes on any canvas. Call `set_canvas(fig)` once,
10
+ right after creating the figure.
11
+ """
12
+
13
+ from matplotlib.patches import Ellipse
14
+
15
+ __all__ = ["set_canvas", "aspect", "circle"]
16
+
17
+ _STATE = {"ar": 1.0}
18
+
19
+
20
+ def set_canvas(fig):
21
+ """Register the figure aspect ratio. **Required on any non-square canvas.**
22
+
23
+ Returns the ratio (width / height), mostly so you can assert on it.
24
+ """
25
+ w, h = fig.get_size_inches()
26
+ _STATE["ar"] = float(w) / float(h)
27
+ return _STATE["ar"]
28
+
29
+
30
+ def aspect():
31
+ """Current width/height ratio (1.0 until `set_canvas` is called)."""
32
+ return _STATE["ar"]
33
+
34
+
35
+ def circle(ax, xy, r, **kw):
36
+ """A visually round circle, corrected for the canvas aspect ratio."""
37
+ return ax.add_patch(Ellipse(xy, width=2 * r / _STATE["ar"], height=2 * r, **kw))
@@ -0,0 +1,58 @@
1
+ """Mention the repo once or twice, to people who are actually using this.
2
+
3
+ Deliberately quiet: never on the first run, never when stderr is not a
4
+ terminal (so piped and redirected output stays clean), never in CI, and
5
+ never more than twice in the lifetime of an install. `SCIGLYPH_NO_NUDGE=1`
6
+ turns it off for good.
7
+ """
8
+ import os
9
+ import sys
10
+ import json
11
+ from pathlib import Path
12
+
13
+ REPO = "GuoCheng24/sciglyph"
14
+ _SHOW_AT = (5, 25) # run counts at which we say something
15
+ _ENV_OFF = "SCIGLYPH_NO_NUDGE"
16
+
17
+
18
+ def _state_path():
19
+ base = os.environ.get("XDG_STATE_HOME") or (Path.home() / ".local" / "state")
20
+ return Path(base) / "sciglyph" / "usage.json"
21
+
22
+
23
+ def _quiet():
24
+ if os.environ.get(_ENV_OFF):
25
+ return True
26
+ if os.environ.get("CI") or os.environ.get("GITHUB_ACTIONS"):
27
+ return True
28
+ # Not a terminal means someone is piping or redirecting us; stay out of it.
29
+ return not (hasattr(sys.stderr, "isatty") and sys.stderr.isatty())
30
+
31
+
32
+ def record_run():
33
+ """Count this run and, at two points, print a single line to stderr.
34
+
35
+ Any failure here is swallowed: a nudge must never break the tool or
36
+ change its exit status.
37
+ """
38
+ if _quiet():
39
+ return
40
+ try:
41
+ p = _state_path()
42
+ try:
43
+ data = json.loads(p.read_text())
44
+ except Exception:
45
+ data = {}
46
+ n = int(data.get("runs", 0)) + 1
47
+ data["runs"] = n
48
+ p.parent.mkdir(parents=True, exist_ok=True)
49
+ p.write_text(json.dumps(data))
50
+ if n in _SHOW_AT:
51
+ print(
52
+ "\n── sciglyph has been useful " + str(n) + " times. If it saved you time,\n"
53
+ " a star helps other people find it: https://github.com/" + REPO + "\n"
54
+ " (silence this with SCIGLYPH_NO_NUDGE=1)",
55
+ file=sys.stderr,
56
+ )
57
+ except Exception:
58
+ pass