plotty 1.0.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.
- plotty-1.0.0/LICENSE +21 -0
- plotty-1.0.0/PKG-INFO +268 -0
- plotty-1.0.0/README.md +244 -0
- plotty-1.0.0/pyproject.toml +74 -0
- plotty-1.0.0/setup.cfg +4 -0
- plotty-1.0.0/src/plotty.egg-info/PKG-INFO +268 -0
- plotty-1.0.0/src/plotty.egg-info/SOURCES.txt +13 -0
- plotty-1.0.0/src/plotty.egg-info/dependency_links.txt +1 -0
- plotty-1.0.0/src/plotty.egg-info/entry_points.txt +2 -0
- plotty-1.0.0/src/plotty.egg-info/requires.txt +2 -0
- plotty-1.0.0/src/plotty.egg-info/top_level.txt +1 -0
- plotty-1.0.0/src/plotty.py +731 -0
- plotty-1.0.0/tests/test_backend.py +357 -0
- plotty-1.0.0/tests/test_sixel.py +232 -0
- plotty-1.0.0/tests/test_viewer_integration.py +56 -0
plotty-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 xuesoso
|
|
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.
|
plotty-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: plotty
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Inline matplotlib plots in your terminal via sixel, in a tmux pane, over SSH
|
|
5
|
+
Author-email: xuesoso <xuesoso@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/xuesoso/plotty
|
|
8
|
+
Project-URL: Repository, https://github.com/xuesoso/plotty
|
|
9
|
+
Keywords: matplotlib,sixel,tmux,ssh,terminal,plotting,repl
|
|
10
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Framework :: Matplotlib
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: POSIX
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Topic :: Scientific/Engineering :: Visualization
|
|
18
|
+
Requires-Python: >=3.7
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Requires-Dist: matplotlib>=3.5
|
|
22
|
+
Requires-Dist: numpy>=1.17
|
|
23
|
+
Dynamic: license-file
|
|
24
|
+
|
|
25
|
+
# plotty
|
|
26
|
+
|
|
27
|
+
[](https://github.com/xuesoso/plotty/actions/workflows/ci.yml)
|
|
28
|
+
[](https://github.com/xuesoso/plotty/actions/workflows/ci.yml)
|
|
29
|
+
[](pyproject.toml)
|
|
30
|
+
[](LICENSE)
|
|
31
|
+
|
|
32
|
+
> Inline matplotlib plots in your terminal — rendered as **sixel** in a dedicated
|
|
33
|
+
> tmux pane, **including over SSH**. No browser, no X11, no Jupyter server.
|
|
34
|
+
|
|
35
|
+
<p align="center">
|
|
36
|
+
<img src="images/plotty_1.gif" alt="plotty demo" width="720">
|
|
37
|
+
</p>
|
|
38
|
+
|
|
39
|
+
`plotty` is a matplotlib backend that draws figures directly in your terminal, so
|
|
40
|
+
a `tmux` + `ipython` (+ `nvim`) workflow shows plots the way a Jupyter or VS Code
|
|
41
|
+
notebook does. Activate it once and your figures appear in a tmux pane next to
|
|
42
|
+
your REPL — locally or on a remote machine over SSH. It's inspired by and the Python analogue of
|
|
43
|
+
[MuxDisplay.jl](https://github.com/goerz/MuxDisplay.jl).
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
import plotty
|
|
47
|
+
plotty.enable()
|
|
48
|
+
|
|
49
|
+
import matplotlib.pyplot as plt
|
|
50
|
+
plt.plot([1, 4, 9, 16]) # shows up in the plot pane
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Why / when to use it
|
|
56
|
+
|
|
57
|
+
If you do interactive analysis in a terminal — `ipython` inside `tmux`, editing
|
|
58
|
+
in `nvim`, frequently SSH'd into a remote box — you normally lose inline plots:
|
|
59
|
+
`plt.show()` wants a GUI and Jupyter wants a browser. plotty fills that gap and
|
|
60
|
+
covers three setups:
|
|
61
|
+
|
|
62
|
+
- **Local tmux.** Run your REPL in one pane; plots render in another.
|
|
63
|
+
- **Remote over SSH.** Run everything on the remote inside tmux. Only the
|
|
64
|
+
rendered **sixel bytes** cross the wire (drawn by your local terminal); the
|
|
65
|
+
control plane — signals, pidfile, image hand-off — stays host-local, so it
|
|
66
|
+
behaves exactly like a local session.
|
|
67
|
+
- **Nested tmux** (`local tmux → ssh → remote tmux`). Supported with a small,
|
|
68
|
+
one-time tmux config change — see [Nested tmux](#nested-tmux-local--remote).
|
|
69
|
+
|
|
70
|
+
## Requirements
|
|
71
|
+
|
|
72
|
+
| | |
|
|
73
|
+
|---|---|
|
|
74
|
+
| **Python** | ≥ 3.7 |
|
|
75
|
+
| **tmux** | ≥ **3.4**, built with sixel support (`--enable-sixel`) |
|
|
76
|
+
| **Terminal** | a sixel-capable terminal for display — e.g. WezTerm, foot, Konsole, `xterm -ti vt340` |
|
|
77
|
+
| **Python deps** | `matplotlib` (and `numpy`, which ships with matplotlib) — that's all |
|
|
78
|
+
|
|
79
|
+
Check tmux:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
tmux -V # need >= 3.4
|
|
83
|
+
strings "$(command -v tmux)" | grep -qi sixel && echo "sixel: yes" || echo "sixel: MISSING"
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
> Not in tmux? plotty falls back to writing sixel straight to your terminal's
|
|
87
|
+
> stdout, so it still works in any sixel-capable terminal without tmux.
|
|
88
|
+
|
|
89
|
+
## Install
|
|
90
|
+
|
|
91
|
+
plotty installs with uv (which indexes PyPI) or pip:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
uv add plotty # add to your project (resolved + locked)
|
|
95
|
+
# or
|
|
96
|
+
uv pip install plotty # into the active environment
|
|
97
|
+
# or
|
|
98
|
+
pip install plotty
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
From source:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
git clone https://github.com/xuesoso/plotty && cd plotty
|
|
105
|
+
uv pip install .
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Quick start
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
import plotty
|
|
112
|
+
plotty.enable() # auto-detect a renderer, target the last tmux pane,
|
|
113
|
+
# and spawn a tiny viewer there
|
|
114
|
+
|
|
115
|
+
import matplotlib.pyplot as plt
|
|
116
|
+
plt.plot([1, 4, 9, 16])
|
|
117
|
+
# IPython: the figure appears automatically after each cell.
|
|
118
|
+
# Plain REPL: call plt.show().
|
|
119
|
+
|
|
120
|
+
plotty.disable() # stop the viewer and restore matplotlib
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Inside tmux, plotty draws into the **last pane** of the current window by default,
|
|
124
|
+
so split a pane first (`Ctrl-b "`), then call `enable()`. Target another pane with
|
|
125
|
+
`enable(target_pane=...)`.
|
|
126
|
+
|
|
127
|
+
Public API: `enable()`, `disable()`, `redraw()`, `view()`.
|
|
128
|
+
|
|
129
|
+
### Demo
|
|
130
|
+
|
|
131
|
+
Run the bundled example to see it in action (split off a plot pane first, then
|
|
132
|
+
`python examples/demo.py`). The GIF below is the expected output:
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
python examples/demo.py
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
<p align="center">
|
|
139
|
+
<img src="images/plotty_2.gif" alt="plotty rendering the examples/demo.py plots in a tmux pane" width="720">
|
|
140
|
+
</p>
|
|
141
|
+
|
|
142
|
+
## How it works
|
|
143
|
+
|
|
144
|
+
Two cooperating pieces share state via the filesystem + OS signals:
|
|
145
|
+
|
|
146
|
+
- **Backend** (`module://plotty`, runs in your REPL): on each figure it saves a
|
|
147
|
+
PNG, atomically publishes it to `~/.cache/plotty/last.png`, and signals the
|
|
148
|
+
viewer.
|
|
149
|
+
- **Viewer** (runs in the plot pane): redraws on a new figure (`SIGUSR1`) and on
|
|
150
|
+
pane resize/zoom (`SIGWINCH`). It's event-driven (`signal.pause()`), idle at
|
|
151
|
+
zero CPU, and self-cleaning.
|
|
152
|
+
|
|
153
|
+
Because only sixel bytes cross SSH and everything else is host-local, remote use
|
|
154
|
+
is identical to local.
|
|
155
|
+
|
|
156
|
+
## Display modes
|
|
157
|
+
|
|
158
|
+
- **Viewer mode** (default in tmux) — a small viewer process lives in the target
|
|
159
|
+
pane and redraws on new figures *and* on pane resize/zoom. Recommended; it's
|
|
160
|
+
the mode that survives resizing.
|
|
161
|
+
- **Inline mode** (default outside tmux, or `enable(inline=True)`) — the backend
|
|
162
|
+
renders sixel itself, with no helper process, and writes it to the target
|
|
163
|
+
pane's tty (in tmux) or to your stdout (no tmux). It does **not** auto-redraw
|
|
164
|
+
on resize.
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
plotty.enable(inline=True) # force inline even inside tmux
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## Sixel encoders
|
|
171
|
+
|
|
172
|
+
plotty ships with a **built-in, dependency-free sixel encoder** (pure stdlib +
|
|
173
|
+
numpy), so it works out of the box with no external tools.
|
|
174
|
+
|
|
175
|
+
If one is on your `PATH`, plotty auto-detects an external encoder for
|
|
176
|
+
higher-quality (dithered) output, in priority order:
|
|
177
|
+
|
|
178
|
+
1. [`chafa`](https://github.com/hpjansson/chafa) — recommended
|
|
179
|
+
2. [`img2sixel`](https://github.com/saitoha/libsixel) (libsixel)
|
|
180
|
+
3. ImageMagick (`magick` / `convert`)
|
|
181
|
+
|
|
182
|
+
Force the built-in encoder regardless of what's installed:
|
|
183
|
+
|
|
184
|
+
```python
|
|
185
|
+
plotty.enable(imgcat="builtin") # or: PLOTTY_IMGCAT=builtin
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
> plotty is **sixel-only** by design — sixel is the only path that survives tmux
|
|
189
|
+
> and SSH. Non-sixel terminal-image protocols (kitty / iTerm) are not used. A
|
|
190
|
+
> custom non-sixel `imgcat=` may be passed but will warn that it may not display
|
|
191
|
+
> over SSH.
|
|
192
|
+
|
|
193
|
+
## tmux configuration
|
|
194
|
+
|
|
195
|
+
plotty works with no config on a single tmux as long as tmux is ≥ 3.4 with sixel
|
|
196
|
+
and your terminal supports sixel (i.e. Wezterm, iTerm2, xterm, xfce term, VSCode). Reference [Are We Sixel Yet?](https://www.arewesixelyet.com/) for a complete list. If plots don't appear (or you see raw
|
|
197
|
+
escape-sequence junk instead of an image), tmux hasn't recognized that your
|
|
198
|
+
terminal can render sixel — its auto-detection isn't always reliable, especially
|
|
199
|
+
over SSH. Tell it explicitly in `~/.tmux.conf`:
|
|
200
|
+
|
|
201
|
+
```tmux
|
|
202
|
+
set -as terminal-features ',*:sixel'
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### Nested tmux (local + remote)
|
|
206
|
+
|
|
207
|
+
A common remote setup is a tmux **inside** a tmux:
|
|
208
|
+
|
|
209
|
+
```
|
|
210
|
+
local terminal → local tmux → ssh → remote tmux → REPL + plot pane
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
For the image to flow all the way out, **every** tmux layer must render and
|
|
214
|
+
forward the sixel — which means setting the feature on **both** the local and the
|
|
215
|
+
remote tmux:
|
|
216
|
+
|
|
217
|
+
```tmux
|
|
218
|
+
# add to ~/.tmux.conf on BOTH the local laptop and the remote machine
|
|
219
|
+
set -as terminal-features ',*:sixel'
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Without this, the inner (remote) tmux doesn't know to forward sixel and the raw
|
|
223
|
+
escape sequence leaks through as garbage characters. Verify a layer sees the
|
|
224
|
+
feature with:
|
|
225
|
+
|
|
226
|
+
```bash
|
|
227
|
+
tmux display-message -p '#{client_termfeatures}' # should contain "sixel"
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Both tmux layers must be ≥ 3.4 and built with sixel.
|
|
231
|
+
|
|
232
|
+
## Configuration reference
|
|
233
|
+
|
|
234
|
+
`enable()` arguments (each has an environment-variable default):
|
|
235
|
+
|
|
236
|
+
| argument | env var | default | meaning |
|
|
237
|
+
|---|---|---|---|
|
|
238
|
+
| `target_pane` | `PLOTTY_PANE` | `-1` | tmux pane for the plot; negative indexes from the end (`-1` = last) |
|
|
239
|
+
| `size` | `PLOTTY_SIZE` | `60` | display width in terminal cells |
|
|
240
|
+
| `dpi` | `PLOTTY_DPI` | matplotlib default | `savefig` DPI of the source image (raise it for sharper plots at large `size`) |
|
|
241
|
+
| `imgcat` | `PLOTTY_IMGCAT` | auto | renderer command; `"builtin"` forces the built-in encoder |
|
|
242
|
+
| `inline` | `PLOTTY_INLINE` | auto | `True`/`False` to force inline vs viewer-pane mode |
|
|
243
|
+
| `clear` | `PLOTTY_CLEAR` | `True` | clear the pane before each draw |
|
|
244
|
+
| `close` | `PLOTTY_CLOSE` | `True` | close figures after display |
|
|
245
|
+
| `tmux` | `PLOTTY_TMUX` | `tmux` | tmux binary to use |
|
|
246
|
+
| `viewer` | — | `True` | spawn the viewer process (tmux mode) |
|
|
247
|
+
| `verbose` | — | `1` | print startup health-check warnings |
|
|
248
|
+
| — | `PLOTTY_CACHE` | `~/.cache/plotty` | state directory (`last.png`, pidfile) |
|
|
249
|
+
|
|
250
|
+
`size` and `dpi` are independent: `size` is how wide the image is *displayed*,
|
|
251
|
+
`dpi` is how many pixels the *source* has. For a crisp image at a large `size`,
|
|
252
|
+
raise `dpi` so the source has enough pixels.
|
|
253
|
+
|
|
254
|
+
## Troubleshooting
|
|
255
|
+
|
|
256
|
+
- **Garbage / `+++` instead of an image:** a tmux layer isn't forwarding sixel.
|
|
257
|
+
Add `set -as terminal-features ',*:sixel'` to that layer (both layers if
|
|
258
|
+
nested) and confirm tmux ≥ 3.4 with sixel.
|
|
259
|
+
- **Nothing appears:** check `tmux -V` ≥ 3.4 and sixel support
|
|
260
|
+
(`strings $(command -v tmux) | grep -i sixel`); confirm your terminal supports
|
|
261
|
+
sixel; run `plotty.enable(verbose=1)` to print diagnostics.
|
|
262
|
+
- **Image too large / small:** tune `size`. Blurry when enlarged? raise `dpi`.
|
|
263
|
+
- **Plot doesn't refresh when you resize the pane:** use viewer mode (the default
|
|
264
|
+
in tmux); inline mode doesn't auto-redraw on resize.
|
|
265
|
+
|
|
266
|
+
## License
|
|
267
|
+
|
|
268
|
+
MIT
|
plotty-1.0.0/README.md
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
# plotty
|
|
2
|
+
|
|
3
|
+
[](https://github.com/xuesoso/plotty/actions/workflows/ci.yml)
|
|
4
|
+
[](https://github.com/xuesoso/plotty/actions/workflows/ci.yml)
|
|
5
|
+
[](pyproject.toml)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
8
|
+
> Inline matplotlib plots in your terminal — rendered as **sixel** in a dedicated
|
|
9
|
+
> tmux pane, **including over SSH**. No browser, no X11, no Jupyter server.
|
|
10
|
+
|
|
11
|
+
<p align="center">
|
|
12
|
+
<img src="images/plotty_1.gif" alt="plotty demo" width="720">
|
|
13
|
+
</p>
|
|
14
|
+
|
|
15
|
+
`plotty` is a matplotlib backend that draws figures directly in your terminal, so
|
|
16
|
+
a `tmux` + `ipython` (+ `nvim`) workflow shows plots the way a Jupyter or VS Code
|
|
17
|
+
notebook does. Activate it once and your figures appear in a tmux pane next to
|
|
18
|
+
your REPL — locally or on a remote machine over SSH. It's inspired by and the Python analogue of
|
|
19
|
+
[MuxDisplay.jl](https://github.com/goerz/MuxDisplay.jl).
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
import plotty
|
|
23
|
+
plotty.enable()
|
|
24
|
+
|
|
25
|
+
import matplotlib.pyplot as plt
|
|
26
|
+
plt.plot([1, 4, 9, 16]) # shows up in the plot pane
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Why / when to use it
|
|
32
|
+
|
|
33
|
+
If you do interactive analysis in a terminal — `ipython` inside `tmux`, editing
|
|
34
|
+
in `nvim`, frequently SSH'd into a remote box — you normally lose inline plots:
|
|
35
|
+
`plt.show()` wants a GUI and Jupyter wants a browser. plotty fills that gap and
|
|
36
|
+
covers three setups:
|
|
37
|
+
|
|
38
|
+
- **Local tmux.** Run your REPL in one pane; plots render in another.
|
|
39
|
+
- **Remote over SSH.** Run everything on the remote inside tmux. Only the
|
|
40
|
+
rendered **sixel bytes** cross the wire (drawn by your local terminal); the
|
|
41
|
+
control plane — signals, pidfile, image hand-off — stays host-local, so it
|
|
42
|
+
behaves exactly like a local session.
|
|
43
|
+
- **Nested tmux** (`local tmux → ssh → remote tmux`). Supported with a small,
|
|
44
|
+
one-time tmux config change — see [Nested tmux](#nested-tmux-local--remote).
|
|
45
|
+
|
|
46
|
+
## Requirements
|
|
47
|
+
|
|
48
|
+
| | |
|
|
49
|
+
|---|---|
|
|
50
|
+
| **Python** | ≥ 3.7 |
|
|
51
|
+
| **tmux** | ≥ **3.4**, built with sixel support (`--enable-sixel`) |
|
|
52
|
+
| **Terminal** | a sixel-capable terminal for display — e.g. WezTerm, foot, Konsole, `xterm -ti vt340` |
|
|
53
|
+
| **Python deps** | `matplotlib` (and `numpy`, which ships with matplotlib) — that's all |
|
|
54
|
+
|
|
55
|
+
Check tmux:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
tmux -V # need >= 3.4
|
|
59
|
+
strings "$(command -v tmux)" | grep -qi sixel && echo "sixel: yes" || echo "sixel: MISSING"
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
> Not in tmux? plotty falls back to writing sixel straight to your terminal's
|
|
63
|
+
> stdout, so it still works in any sixel-capable terminal without tmux.
|
|
64
|
+
|
|
65
|
+
## Install
|
|
66
|
+
|
|
67
|
+
plotty installs with uv (which indexes PyPI) or pip:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
uv add plotty # add to your project (resolved + locked)
|
|
71
|
+
# or
|
|
72
|
+
uv pip install plotty # into the active environment
|
|
73
|
+
# or
|
|
74
|
+
pip install plotty
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
From source:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
git clone https://github.com/xuesoso/plotty && cd plotty
|
|
81
|
+
uv pip install .
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Quick start
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
import plotty
|
|
88
|
+
plotty.enable() # auto-detect a renderer, target the last tmux pane,
|
|
89
|
+
# and spawn a tiny viewer there
|
|
90
|
+
|
|
91
|
+
import matplotlib.pyplot as plt
|
|
92
|
+
plt.plot([1, 4, 9, 16])
|
|
93
|
+
# IPython: the figure appears automatically after each cell.
|
|
94
|
+
# Plain REPL: call plt.show().
|
|
95
|
+
|
|
96
|
+
plotty.disable() # stop the viewer and restore matplotlib
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Inside tmux, plotty draws into the **last pane** of the current window by default,
|
|
100
|
+
so split a pane first (`Ctrl-b "`), then call `enable()`. Target another pane with
|
|
101
|
+
`enable(target_pane=...)`.
|
|
102
|
+
|
|
103
|
+
Public API: `enable()`, `disable()`, `redraw()`, `view()`.
|
|
104
|
+
|
|
105
|
+
### Demo
|
|
106
|
+
|
|
107
|
+
Run the bundled example to see it in action (split off a plot pane first, then
|
|
108
|
+
`python examples/demo.py`). The GIF below is the expected output:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
python examples/demo.py
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
<p align="center">
|
|
115
|
+
<img src="images/plotty_2.gif" alt="plotty rendering the examples/demo.py plots in a tmux pane" width="720">
|
|
116
|
+
</p>
|
|
117
|
+
|
|
118
|
+
## How it works
|
|
119
|
+
|
|
120
|
+
Two cooperating pieces share state via the filesystem + OS signals:
|
|
121
|
+
|
|
122
|
+
- **Backend** (`module://plotty`, runs in your REPL): on each figure it saves a
|
|
123
|
+
PNG, atomically publishes it to `~/.cache/plotty/last.png`, and signals the
|
|
124
|
+
viewer.
|
|
125
|
+
- **Viewer** (runs in the plot pane): redraws on a new figure (`SIGUSR1`) and on
|
|
126
|
+
pane resize/zoom (`SIGWINCH`). It's event-driven (`signal.pause()`), idle at
|
|
127
|
+
zero CPU, and self-cleaning.
|
|
128
|
+
|
|
129
|
+
Because only sixel bytes cross SSH and everything else is host-local, remote use
|
|
130
|
+
is identical to local.
|
|
131
|
+
|
|
132
|
+
## Display modes
|
|
133
|
+
|
|
134
|
+
- **Viewer mode** (default in tmux) — a small viewer process lives in the target
|
|
135
|
+
pane and redraws on new figures *and* on pane resize/zoom. Recommended; it's
|
|
136
|
+
the mode that survives resizing.
|
|
137
|
+
- **Inline mode** (default outside tmux, or `enable(inline=True)`) — the backend
|
|
138
|
+
renders sixel itself, with no helper process, and writes it to the target
|
|
139
|
+
pane's tty (in tmux) or to your stdout (no tmux). It does **not** auto-redraw
|
|
140
|
+
on resize.
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
plotty.enable(inline=True) # force inline even inside tmux
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## Sixel encoders
|
|
147
|
+
|
|
148
|
+
plotty ships with a **built-in, dependency-free sixel encoder** (pure stdlib +
|
|
149
|
+
numpy), so it works out of the box with no external tools.
|
|
150
|
+
|
|
151
|
+
If one is on your `PATH`, plotty auto-detects an external encoder for
|
|
152
|
+
higher-quality (dithered) output, in priority order:
|
|
153
|
+
|
|
154
|
+
1. [`chafa`](https://github.com/hpjansson/chafa) — recommended
|
|
155
|
+
2. [`img2sixel`](https://github.com/saitoha/libsixel) (libsixel)
|
|
156
|
+
3. ImageMagick (`magick` / `convert`)
|
|
157
|
+
|
|
158
|
+
Force the built-in encoder regardless of what's installed:
|
|
159
|
+
|
|
160
|
+
```python
|
|
161
|
+
plotty.enable(imgcat="builtin") # or: PLOTTY_IMGCAT=builtin
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
> plotty is **sixel-only** by design — sixel is the only path that survives tmux
|
|
165
|
+
> and SSH. Non-sixel terminal-image protocols (kitty / iTerm) are not used. A
|
|
166
|
+
> custom non-sixel `imgcat=` may be passed but will warn that it may not display
|
|
167
|
+
> over SSH.
|
|
168
|
+
|
|
169
|
+
## tmux configuration
|
|
170
|
+
|
|
171
|
+
plotty works with no config on a single tmux as long as tmux is ≥ 3.4 with sixel
|
|
172
|
+
and your terminal supports sixel (i.e. Wezterm, iTerm2, xterm, xfce term, VSCode). Reference [Are We Sixel Yet?](https://www.arewesixelyet.com/) for a complete list. If plots don't appear (or you see raw
|
|
173
|
+
escape-sequence junk instead of an image), tmux hasn't recognized that your
|
|
174
|
+
terminal can render sixel — its auto-detection isn't always reliable, especially
|
|
175
|
+
over SSH. Tell it explicitly in `~/.tmux.conf`:
|
|
176
|
+
|
|
177
|
+
```tmux
|
|
178
|
+
set -as terminal-features ',*:sixel'
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### Nested tmux (local + remote)
|
|
182
|
+
|
|
183
|
+
A common remote setup is a tmux **inside** a tmux:
|
|
184
|
+
|
|
185
|
+
```
|
|
186
|
+
local terminal → local tmux → ssh → remote tmux → REPL + plot pane
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
For the image to flow all the way out, **every** tmux layer must render and
|
|
190
|
+
forward the sixel — which means setting the feature on **both** the local and the
|
|
191
|
+
remote tmux:
|
|
192
|
+
|
|
193
|
+
```tmux
|
|
194
|
+
# add to ~/.tmux.conf on BOTH the local laptop and the remote machine
|
|
195
|
+
set -as terminal-features ',*:sixel'
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Without this, the inner (remote) tmux doesn't know to forward sixel and the raw
|
|
199
|
+
escape sequence leaks through as garbage characters. Verify a layer sees the
|
|
200
|
+
feature with:
|
|
201
|
+
|
|
202
|
+
```bash
|
|
203
|
+
tmux display-message -p '#{client_termfeatures}' # should contain "sixel"
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Both tmux layers must be ≥ 3.4 and built with sixel.
|
|
207
|
+
|
|
208
|
+
## Configuration reference
|
|
209
|
+
|
|
210
|
+
`enable()` arguments (each has an environment-variable default):
|
|
211
|
+
|
|
212
|
+
| argument | env var | default | meaning |
|
|
213
|
+
|---|---|---|---|
|
|
214
|
+
| `target_pane` | `PLOTTY_PANE` | `-1` | tmux pane for the plot; negative indexes from the end (`-1` = last) |
|
|
215
|
+
| `size` | `PLOTTY_SIZE` | `60` | display width in terminal cells |
|
|
216
|
+
| `dpi` | `PLOTTY_DPI` | matplotlib default | `savefig` DPI of the source image (raise it for sharper plots at large `size`) |
|
|
217
|
+
| `imgcat` | `PLOTTY_IMGCAT` | auto | renderer command; `"builtin"` forces the built-in encoder |
|
|
218
|
+
| `inline` | `PLOTTY_INLINE` | auto | `True`/`False` to force inline vs viewer-pane mode |
|
|
219
|
+
| `clear` | `PLOTTY_CLEAR` | `True` | clear the pane before each draw |
|
|
220
|
+
| `close` | `PLOTTY_CLOSE` | `True` | close figures after display |
|
|
221
|
+
| `tmux` | `PLOTTY_TMUX` | `tmux` | tmux binary to use |
|
|
222
|
+
| `viewer` | — | `True` | spawn the viewer process (tmux mode) |
|
|
223
|
+
| `verbose` | — | `1` | print startup health-check warnings |
|
|
224
|
+
| — | `PLOTTY_CACHE` | `~/.cache/plotty` | state directory (`last.png`, pidfile) |
|
|
225
|
+
|
|
226
|
+
`size` and `dpi` are independent: `size` is how wide the image is *displayed*,
|
|
227
|
+
`dpi` is how many pixels the *source* has. For a crisp image at a large `size`,
|
|
228
|
+
raise `dpi` so the source has enough pixels.
|
|
229
|
+
|
|
230
|
+
## Troubleshooting
|
|
231
|
+
|
|
232
|
+
- **Garbage / `+++` instead of an image:** a tmux layer isn't forwarding sixel.
|
|
233
|
+
Add `set -as terminal-features ',*:sixel'` to that layer (both layers if
|
|
234
|
+
nested) and confirm tmux ≥ 3.4 with sixel.
|
|
235
|
+
- **Nothing appears:** check `tmux -V` ≥ 3.4 and sixel support
|
|
236
|
+
(`strings $(command -v tmux) | grep -i sixel`); confirm your terminal supports
|
|
237
|
+
sixel; run `plotty.enable(verbose=1)` to print diagnostics.
|
|
238
|
+
- **Image too large / small:** tune `size`. Blurry when enlarged? raise `dpi`.
|
|
239
|
+
- **Plot doesn't refresh when you resize the pane:** use viewer mode (the default
|
|
240
|
+
in tmux); inline mode doesn't auto-redraw on resize.
|
|
241
|
+
|
|
242
|
+
## License
|
|
243
|
+
|
|
244
|
+
MIT
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "plotty"
|
|
3
|
+
version = "1.0.0"
|
|
4
|
+
description = "Inline matplotlib plots in your terminal via sixel, in a tmux pane, over SSH"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.7"
|
|
7
|
+
license = { text = "MIT" }
|
|
8
|
+
authors = [{ name = "xuesoso", email = "xuesoso@gmail.com" }]
|
|
9
|
+
keywords = ["matplotlib", "sixel", "tmux", "ssh", "terminal", "plotting", "repl"]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Development Status :: 5 - Production/Stable",
|
|
12
|
+
"Environment :: Console",
|
|
13
|
+
"Framework :: Matplotlib",
|
|
14
|
+
"Intended Audience :: Science/Research",
|
|
15
|
+
"License :: OSI Approved :: MIT License",
|
|
16
|
+
"Operating System :: POSIX",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Topic :: Scientific/Engineering :: Visualization",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
# Runtime deps only. numpy ships with matplotlib but plotty imports it directly,
|
|
22
|
+
# so it's declared. Floors are kept low for Python 3.7 support — the resolver
|
|
23
|
+
# picks newer versions automatically on newer Pythons. Everything else (pandas,
|
|
24
|
+
# jupyter, ...) is dev-only — see below.
|
|
25
|
+
dependencies = [
|
|
26
|
+
"matplotlib>=3.5",
|
|
27
|
+
"numpy>=1.17",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[project.urls]
|
|
31
|
+
Homepage = "https://github.com/xuesoso/plotty"
|
|
32
|
+
Repository = "https://github.com/xuesoso/plotty"
|
|
33
|
+
|
|
34
|
+
[project.scripts]
|
|
35
|
+
plotty-view = "plotty:view"
|
|
36
|
+
|
|
37
|
+
[build-system]
|
|
38
|
+
requires = ["setuptools>=61.0"]
|
|
39
|
+
build-backend = "setuptools.build_meta"
|
|
40
|
+
|
|
41
|
+
# Single-module package living at src/plotty.py -> importable as `plotty`.
|
|
42
|
+
[tool.setuptools]
|
|
43
|
+
package-dir = { "" = "src" }
|
|
44
|
+
py-modules = ["plotty"]
|
|
45
|
+
|
|
46
|
+
# Tools for developing/testing plotty itself. Add your analysis packages here
|
|
47
|
+
# (pandas, seaborn, plotly, jupyter, ...) if you want `uv sync` to install them.
|
|
48
|
+
[dependency-groups]
|
|
49
|
+
dev = [
|
|
50
|
+
"pytest>=7.0",
|
|
51
|
+
"ipython>=7.0",
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
[tool.ruff]
|
|
55
|
+
line-length = 100
|
|
56
|
+
target-version = "py37"
|
|
57
|
+
|
|
58
|
+
[tool.ruff.lint]
|
|
59
|
+
select = ["E", "F", "I", "W", "UP", "B", "SIM"]
|
|
60
|
+
ignore = ["E501"] # line length handled by formatter
|
|
61
|
+
|
|
62
|
+
[tool.ruff.format]
|
|
63
|
+
quote-style = "double"
|
|
64
|
+
indent-style = "space"
|
|
65
|
+
|
|
66
|
+
[tool.mypy]
|
|
67
|
+
python_version = "3.7"
|
|
68
|
+
ignore_missing_imports = true
|
|
69
|
+
warn_unused_ignores = true
|
|
70
|
+
check_untyped_defs = true
|
|
71
|
+
|
|
72
|
+
[tool.pytest.ini_options]
|
|
73
|
+
testpaths = ["tests"]
|
|
74
|
+
addopts = "-ra --strict-markers"
|
plotty-1.0.0/setup.cfg
ADDED