textual-image 0.5.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.
- textual_image-0.5.0/PKG-INFO +236 -0
- textual_image-0.5.0/README.md +198 -0
- textual_image-0.5.0/pyproject.toml +139 -0
- textual_image-0.5.0/setup.cfg +4 -0
- textual_image-0.5.0/tests/test_geometry.py +38 -0
- textual_image-0.5.0/tests/test_main.py +45 -0
- textual_image-0.5.0/tests/test_pixeldata.py +64 -0
- textual_image-0.5.0/tests/test_sixel.py +12 -0
- textual_image-0.5.0/tests/test_terminal.py +102 -0
- textual_image-0.5.0/tests/test_utils.py +13 -0
- textual_image-0.5.0/textual_image/__init__.py +4 -0
- textual_image-0.5.0/textual_image/__main__.py +30 -0
- textual_image-0.5.0/textual_image/_geometry.py +133 -0
- textual_image-0.5.0/textual_image/_pixeldata.py +134 -0
- textual_image-0.5.0/textual_image/_sixel.py +67 -0
- textual_image-0.5.0/textual_image/_terminal.py +132 -0
- textual_image-0.5.0/textual_image/_utils.py +44 -0
- textual_image-0.5.0/textual_image.egg-info/PKG-INFO +236 -0
- textual_image-0.5.0/textual_image.egg-info/SOURCES.txt +20 -0
- textual_image-0.5.0/textual_image.egg-info/dependency_links.txt +1 -0
- textual_image-0.5.0/textual_image.egg-info/requires.txt +16 -0
- textual_image-0.5.0/textual_image.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: textual-image
|
|
3
|
+
Version: 0.5.0
|
|
4
|
+
Summary: Render images via Kitty's Terminal Graphics Protocol with Rich and Textual
|
|
5
|
+
Author-email: Simon Hayessen <simon@lnqs.io>
|
|
6
|
+
Project-URL: Homepage, https://github.com/lnqs/textual-image
|
|
7
|
+
Project-URL: Documentation, https://github.com/lnqs/textual-image
|
|
8
|
+
Project-URL: Repository, https://github.com/lnqs/textual-image
|
|
9
|
+
Project-URL: Issues, https://github.com/lnqs/textual-image/issues
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
14
|
+
Classifier: Topic :: Software Development :: User Interfaces
|
|
15
|
+
Classifier: License :: OSI Approved :: GNU General Public License (GPL)
|
|
16
|
+
Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
|
|
17
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
Requires-Dist: pillow>=10.3.0
|
|
25
|
+
Requires-Dist: rich>=13.4.0
|
|
26
|
+
Provides-Extra: textual
|
|
27
|
+
Requires-Dist: textual>=0.68.0; extra == "textual"
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: mypy; extra == "dev"
|
|
30
|
+
Requires-Dist: pytest; extra == "dev"
|
|
31
|
+
Requires-Dist: pytest-asyncio; extra == "dev"
|
|
32
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
33
|
+
Requires-Dist: ruff; extra == "dev"
|
|
34
|
+
Requires-Dist: syrupy; extra == "dev"
|
|
35
|
+
Requires-Dist: tox; extra == "dev"
|
|
36
|
+
Requires-Dist: typing-extensions; extra == "dev"
|
|
37
|
+
Requires-Dist: typos; extra == "dev"
|
|
38
|
+
|
|
39
|
+
# textual-image
|
|
40
|
+
|
|
41
|
+
**Render images directly in your terminal using [Textual](https://www.textualize.io/) and [Rich](https://github.com/Textualize/rich).**
|
|
42
|
+
|
|
43
|
+

|
|
44
|
+
|
|
45
|
+
_textual-image_ offers both Rich renderables and Textual Widgets that leverage the [Terminal Graphics Protocol (TGP)](https://sw.kovidgoyal.net/kitty/graphics-protocol/) and [Sixel](https://en.wikipedia.org/wiki/Sixel) protocols to display images in your terminal. For terminals that don't support these protocols, fallback rendering using Unicode characters is available.
|
|
46
|
+
|
|
47
|
+
## Supported Terminals
|
|
48
|
+
|
|
49
|
+
- **Terminal Graphics Protocol (TGP)**: Initially introduced by the [Kitty](https://sw.kovidgoyal.net/kitty/) terminal emulator, fully supported in Kitty, and largely implemented in [WezTerm](https://wezfurlong.org/wezterm/index.html) and partially supported by [Konsole](https://konsole.kde.org/) and [wayst](https://github.com/91861/wayst).
|
|
50
|
+
- **Sixel Graphics**: Supported by various terminal emulators including [xterm](https://invisible-island.net/xterm/) and others.
|
|
51
|
+
|
|
52
|
+
_Note_: Testing has been conducted primarily using Kitty for TGP and xterm for Sixel on Linux, with some sanity checks on other terminals. Feedback and interoperability testing on other terminal emulators and operating systems would be highly valued.
|
|
53
|
+
|
|
54
|
+
See the Support Matrix below on what was tested already.
|
|
55
|
+
|
|
56
|
+
### Support Matrix [^1]
|
|
57
|
+
|
|
58
|
+
| Terminal | TPG support | Sixel support | Works with textual-image |
|
|
59
|
+
|---------------------|:-----------:|:-------------:|:------------------------:|
|
|
60
|
+
| Alacritty | ❌ | ❌ | |
|
|
61
|
+
| Black Box | ❌ | ✅ | ⚫ |
|
|
62
|
+
| Bobcat | ❌ | ✅ | ⚫ |
|
|
63
|
+
| ConEmu | ❌ | ❌ | |
|
|
64
|
+
| Contour | ❌ | ✅ | ⚫ |
|
|
65
|
+
| ctx terminal | ❌ | ✅ | ⚫ |
|
|
66
|
+
| Darktile | ❌ | ✅ | ⚫ |
|
|
67
|
+
| DomTerm | ❌ | ✅ | ⚫ |
|
|
68
|
+
| Eat | ❌ | ✅ | ⚫ |
|
|
69
|
+
| Elementary Terminal | ❌ | ❌ | |
|
|
70
|
+
| foot | ❌ | ✅ | ✅ |
|
|
71
|
+
| GNOME Terminal | ❌ | ❌ | |
|
|
72
|
+
| guake | ❌ | ❌ | |
|
|
73
|
+
| iTerm2 | ❌ | ✅ | ⚫ |
|
|
74
|
+
| kitty | ✅ | ❌ | ✅ |
|
|
75
|
+
| konsole | ✅ | ✅ | ❓[^2] |
|
|
76
|
+
| LaTerminal | ❌ | ✅ | ⚫ |
|
|
77
|
+
| MacTerm | ❌ | ✅ | ⚫ |
|
|
78
|
+
| mintty | ❌ | ✅ | ⚫ |
|
|
79
|
+
| mlterm | ❌ | ✅ | ⚫ |
|
|
80
|
+
| MobaXterm | ❌ | ❌ | |
|
|
81
|
+
| PuTTY | ❌ | ❌ | |
|
|
82
|
+
| Rio terminal | ❌ | ❌ | |
|
|
83
|
+
| Rlogin | ❌ | ✅ | ⚫ |
|
|
84
|
+
| suckless st | ❌ | ❌ | |
|
|
85
|
+
| SwiftTerm | ❌ | ✅ | ⚫ |
|
|
86
|
+
| SyncTERM | ❌ | ✅ | ⚫ |
|
|
87
|
+
| TeraTerm | ❌ | ❌ | |
|
|
88
|
+
| Terminal.app | ❌ | ❌ | |
|
|
89
|
+
| Terminology | ❌ | ❌ | |
|
|
90
|
+
| termux | ❌ | ❌ | |
|
|
91
|
+
| Tilix | ❌ | ❌ | |
|
|
92
|
+
| tmux | ❌ | ✅ | ✅[^3] |
|
|
93
|
+
| toyterm | ❌ | ✅ | ⚫ |
|
|
94
|
+
| URxvt | ❌ | ❌ | |
|
|
95
|
+
| U++ | ❌ | ✅ | ⚫ |
|
|
96
|
+
| Visual Studio Code | ❌ | ✅ | ✅[^4] |
|
|
97
|
+
| wayst | ✅ | ✅ | ❓[^5] |
|
|
98
|
+
| wezterm | ✅ | ✅ | ❓[^6] |
|
|
99
|
+
| Windows Console | ❌ | ❌ | |
|
|
100
|
+
| Windows Terminal | ❌ | ✅ | ⚫ |
|
|
101
|
+
| xfce-terminal | ❌ | ✅ | ⚫ |
|
|
102
|
+
| xterm | ❌ | ✅ | ✅ |
|
|
103
|
+
| xterm.js | ❌ | ✅ | ⚫ |
|
|
104
|
+
| yaft | ❌ | ✅ | ⚫ |
|
|
105
|
+
| Yakuake | ❌ | ✅ | ⚫ |
|
|
106
|
+
| Zellij | ❌ | ✅ | ❌[^7] |
|
|
107
|
+
|
|
108
|
+
✅ = Supported; ❌ = Not Supported; ⚫ = To Be Tested; ❓ = Works, but with glitches (further investigation needed)
|
|
109
|
+
|
|
110
|
+
[^1]: Based on [Are We Sixel Yet?](https://www.arewesixelyet.com/)
|
|
111
|
+
[^2]: Reports to support TGP but doesn't draw images. If set to Sixel explicitly, it works besides a few minor glitches.
|
|
112
|
+
[^3]: Works only in a Sixel enabled terminal, TGP does not work with tmux.
|
|
113
|
+
[^4]: The `terminal.integrated.enableImages` setting has to be enabled.
|
|
114
|
+
[^5]: Both TGP and Sixel draw graphics, but only with major glitches.
|
|
115
|
+
[^6]: TGP draws graphics, but with major glitches; Sixel works fine but doesn't get auto-selected due to reporting TGP support.
|
|
116
|
+
[^7]: Reports to support Sixel, but doesn't draw anything.
|
|
117
|
+
|
|
118
|
+
### Enabling Sixel Support on xterm
|
|
119
|
+
|
|
120
|
+
Sixel on xterm is disabled by default. To enable it, add `+lc` and `-ti vt340` options when launching xterm:
|
|
121
|
+
|
|
122
|
+
```sh
|
|
123
|
+
xterm +lc -ti vt340
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Alternatively, you can add these options to your xterm configuration file (`~/.Xresources` or `~/.Xdefaults`) to make the change permanent:
|
|
127
|
+
|
|
128
|
+
```sh
|
|
129
|
+
echo 'XTerm*decTerminalID: vt340' >> ~/.Xresources
|
|
130
|
+
xrdb -merge ~/.Xresources
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Installation
|
|
134
|
+
|
|
135
|
+
Install _textual-image_ using pip with the following commands:
|
|
136
|
+
|
|
137
|
+
For the basic installation:
|
|
138
|
+
```sh
|
|
139
|
+
pip install textual-image
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
To include the Textual Widget's dependencies:
|
|
143
|
+
```sh
|
|
144
|
+
pip install textual-image[textual]
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Demonstration
|
|
148
|
+
|
|
149
|
+
Once installed, run the demo application to see the module in action.
|
|
150
|
+
|
|
151
|
+
For a demonstration of the Rich renderable, use:
|
|
152
|
+
```sh
|
|
153
|
+
python -m textual_image rich
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
For a demonstration of the Textual Widget, use:
|
|
157
|
+
```sh
|
|
158
|
+
python -m textual_image textual
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
The module will automatically select the best available rendering option. If you wish to specify a particular rendering method, use the `-p` argument with one of the following values: `tgp`, `sixel`, `halfcell`, or `unicode`.
|
|
162
|
+
|
|
163
|
+
For more information, use:
|
|
164
|
+
```sh
|
|
165
|
+
python -m textual_image --help
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## Usage
|
|
169
|
+
|
|
170
|
+
### Rich Integration
|
|
171
|
+
|
|
172
|
+
To use the Rich renderable, simply pass an instance of `textual_image.renderable.Image` to a Rich function that renders data:
|
|
173
|
+
|
|
174
|
+
```python
|
|
175
|
+
from rich.console import Console
|
|
176
|
+
from textual_image.renderable import Image
|
|
177
|
+
|
|
178
|
+
console = Console()
|
|
179
|
+
console.print(Image("path/to/image.png"))
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
The `Image` constructor accepts either a string, a `pathlib.Path` representing the file path of an image readable by [Pillow](https://python-pillow.org/), or a Pillow `Image` instance directly.
|
|
183
|
+
|
|
184
|
+
By default, the image is rendered in its original dimensions. You can modify this behavior by specifying the `width` and/or `height` parameters. These can be defined as an integer (number of cells), a percentage string (e.g., `50%`), or the literal `auto` to automatically scale while maintaining the aspect ratio.
|
|
185
|
+
|
|
186
|
+
`textual_image.renderable.Image` defaults to the best available rendering method. To specify an explicit rendering method, use one of the following classes: `textual_image.renderable.tgp.Image`, `textual_image.renderable.sixel.Image`, `textual_image.renderable.halfcell.Image`, or `textual_image.renderable.unicode.Image`.
|
|
187
|
+
|
|
188
|
+
### Textual Integration
|
|
189
|
+
|
|
190
|
+
For integration with Textual, _textual-image_ offers a Textual `Widget` to render images:
|
|
191
|
+
|
|
192
|
+
```python
|
|
193
|
+
from textual.app import App, ComposeResult
|
|
194
|
+
from textual_image.widget import Image
|
|
195
|
+
|
|
196
|
+
class ImageApp(App[None]):
|
|
197
|
+
def compose(self) -> ComposeResult:
|
|
198
|
+
yield Image("path/to/image.png")
|
|
199
|
+
|
|
200
|
+
ImageApp().run()
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
The `Image` constructor accepts either a string or a `pathlib.Path` with the file path of an image readable by [Pillow](https://python-pillow.org/), or a Pillow `Image` instance directly.
|
|
204
|
+
|
|
205
|
+
You can also set the image using the `image` property of an `Image` instance:
|
|
206
|
+
|
|
207
|
+
```python
|
|
208
|
+
from textual.app import App, ComposeResult
|
|
209
|
+
from textual_image.textual import Image
|
|
210
|
+
|
|
211
|
+
class ImageApp(App[None]):
|
|
212
|
+
def compose(self) -> ComposeResult:
|
|
213
|
+
image = Image()
|
|
214
|
+
image.image = "path/to/image.png"
|
|
215
|
+
yield image
|
|
216
|
+
|
|
217
|
+
ImageApp().run()
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
If a different image is set, the Widget will update to display the new image.
|
|
221
|
+
|
|
222
|
+
By default, the best available rendering option is used. To override this, you can instantiate `textual_image.widget.TGPImage`, `textual_image.widget.SixelImage`, `textual_image.widget.HalfcellImage`, or `textual_image.widget.UnicodeImage` directly.
|
|
223
|
+
|
|
224
|
+
_*Note*_: The process of determining the best available rendering option involves querying the terminal, which means sending and receiving data. Since Textual starts threads to handle input and output, this query will **not work** once the Textual app has started. Therefore, make sure that `textual_image.renderable` is imported **before** running the Textual app (which is typically the case in most use cases).
|
|
225
|
+
|
|
226
|
+
## Limitations
|
|
227
|
+
|
|
228
|
+
- **Sixel Support in Textual**: Sixel support in Textual is not particularly performant due to the way Textual handles rendering. The Sixel graphics are injected into the rendering process in a somewhat hacky manner, which affects performance. Scrolling and changing styles of images can lead to a lot of flickering. But for mostly static images it should work fine. If not, please file an issue on GitHub.
|
|
229
|
+
|
|
230
|
+
## Contribution
|
|
231
|
+
|
|
232
|
+
If you find this module useful, please consider starring the repository on GitHub.
|
|
233
|
+
|
|
234
|
+
This project began by moving some TGP functionality from a private project to a public GitHub repository and PyPI package, with some additional code added along the way to support Sixel graphics. If you encounter any issues, please file an issue on GitHub.
|
|
235
|
+
|
|
236
|
+
Contributions via pull requests are welcome and encouraged.
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# textual-image
|
|
2
|
+
|
|
3
|
+
**Render images directly in your terminal using [Textual](https://www.textualize.io/) and [Rich](https://github.com/Textualize/rich).**
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
_textual-image_ offers both Rich renderables and Textual Widgets that leverage the [Terminal Graphics Protocol (TGP)](https://sw.kovidgoyal.net/kitty/graphics-protocol/) and [Sixel](https://en.wikipedia.org/wiki/Sixel) protocols to display images in your terminal. For terminals that don't support these protocols, fallback rendering using Unicode characters is available.
|
|
8
|
+
|
|
9
|
+
## Supported Terminals
|
|
10
|
+
|
|
11
|
+
- **Terminal Graphics Protocol (TGP)**: Initially introduced by the [Kitty](https://sw.kovidgoyal.net/kitty/) terminal emulator, fully supported in Kitty, and largely implemented in [WezTerm](https://wezfurlong.org/wezterm/index.html) and partially supported by [Konsole](https://konsole.kde.org/) and [wayst](https://github.com/91861/wayst).
|
|
12
|
+
- **Sixel Graphics**: Supported by various terminal emulators including [xterm](https://invisible-island.net/xterm/) and others.
|
|
13
|
+
|
|
14
|
+
_Note_: Testing has been conducted primarily using Kitty for TGP and xterm for Sixel on Linux, with some sanity checks on other terminals. Feedback and interoperability testing on other terminal emulators and operating systems would be highly valued.
|
|
15
|
+
|
|
16
|
+
See the Support Matrix below on what was tested already.
|
|
17
|
+
|
|
18
|
+
### Support Matrix [^1]
|
|
19
|
+
|
|
20
|
+
| Terminal | TPG support | Sixel support | Works with textual-image |
|
|
21
|
+
|---------------------|:-----------:|:-------------:|:------------------------:|
|
|
22
|
+
| Alacritty | ❌ | ❌ | |
|
|
23
|
+
| Black Box | ❌ | ✅ | ⚫ |
|
|
24
|
+
| Bobcat | ❌ | ✅ | ⚫ |
|
|
25
|
+
| ConEmu | ❌ | ❌ | |
|
|
26
|
+
| Contour | ❌ | ✅ | ⚫ |
|
|
27
|
+
| ctx terminal | ❌ | ✅ | ⚫ |
|
|
28
|
+
| Darktile | ❌ | ✅ | ⚫ |
|
|
29
|
+
| DomTerm | ❌ | ✅ | ⚫ |
|
|
30
|
+
| Eat | ❌ | ✅ | ⚫ |
|
|
31
|
+
| Elementary Terminal | ❌ | ❌ | |
|
|
32
|
+
| foot | ❌ | ✅ | ✅ |
|
|
33
|
+
| GNOME Terminal | ❌ | ❌ | |
|
|
34
|
+
| guake | ❌ | ❌ | |
|
|
35
|
+
| iTerm2 | ❌ | ✅ | ⚫ |
|
|
36
|
+
| kitty | ✅ | ❌ | ✅ |
|
|
37
|
+
| konsole | ✅ | ✅ | ❓[^2] |
|
|
38
|
+
| LaTerminal | ❌ | ✅ | ⚫ |
|
|
39
|
+
| MacTerm | ❌ | ✅ | ⚫ |
|
|
40
|
+
| mintty | ❌ | ✅ | ⚫ |
|
|
41
|
+
| mlterm | ❌ | ✅ | ⚫ |
|
|
42
|
+
| MobaXterm | ❌ | ❌ | |
|
|
43
|
+
| PuTTY | ❌ | ❌ | |
|
|
44
|
+
| Rio terminal | ❌ | ❌ | |
|
|
45
|
+
| Rlogin | ❌ | ✅ | ⚫ |
|
|
46
|
+
| suckless st | ❌ | ❌ | |
|
|
47
|
+
| SwiftTerm | ❌ | ✅ | ⚫ |
|
|
48
|
+
| SyncTERM | ❌ | ✅ | ⚫ |
|
|
49
|
+
| TeraTerm | ❌ | ❌ | |
|
|
50
|
+
| Terminal.app | ❌ | ❌ | |
|
|
51
|
+
| Terminology | ❌ | ❌ | |
|
|
52
|
+
| termux | ❌ | ❌ | |
|
|
53
|
+
| Tilix | ❌ | ❌ | |
|
|
54
|
+
| tmux | ❌ | ✅ | ✅[^3] |
|
|
55
|
+
| toyterm | ❌ | ✅ | ⚫ |
|
|
56
|
+
| URxvt | ❌ | ❌ | |
|
|
57
|
+
| U++ | ❌ | ✅ | ⚫ |
|
|
58
|
+
| Visual Studio Code | ❌ | ✅ | ✅[^4] |
|
|
59
|
+
| wayst | ✅ | ✅ | ❓[^5] |
|
|
60
|
+
| wezterm | ✅ | ✅ | ❓[^6] |
|
|
61
|
+
| Windows Console | ❌ | ❌ | |
|
|
62
|
+
| Windows Terminal | ❌ | ✅ | ⚫ |
|
|
63
|
+
| xfce-terminal | ❌ | ✅ | ⚫ |
|
|
64
|
+
| xterm | ❌ | ✅ | ✅ |
|
|
65
|
+
| xterm.js | ❌ | ✅ | ⚫ |
|
|
66
|
+
| yaft | ❌ | ✅ | ⚫ |
|
|
67
|
+
| Yakuake | ❌ | ✅ | ⚫ |
|
|
68
|
+
| Zellij | ❌ | ✅ | ❌[^7] |
|
|
69
|
+
|
|
70
|
+
✅ = Supported; ❌ = Not Supported; ⚫ = To Be Tested; ❓ = Works, but with glitches (further investigation needed)
|
|
71
|
+
|
|
72
|
+
[^1]: Based on [Are We Sixel Yet?](https://www.arewesixelyet.com/)
|
|
73
|
+
[^2]: Reports to support TGP but doesn't draw images. If set to Sixel explicitly, it works besides a few minor glitches.
|
|
74
|
+
[^3]: Works only in a Sixel enabled terminal, TGP does not work with tmux.
|
|
75
|
+
[^4]: The `terminal.integrated.enableImages` setting has to be enabled.
|
|
76
|
+
[^5]: Both TGP and Sixel draw graphics, but only with major glitches.
|
|
77
|
+
[^6]: TGP draws graphics, but with major glitches; Sixel works fine but doesn't get auto-selected due to reporting TGP support.
|
|
78
|
+
[^7]: Reports to support Sixel, but doesn't draw anything.
|
|
79
|
+
|
|
80
|
+
### Enabling Sixel Support on xterm
|
|
81
|
+
|
|
82
|
+
Sixel on xterm is disabled by default. To enable it, add `+lc` and `-ti vt340` options when launching xterm:
|
|
83
|
+
|
|
84
|
+
```sh
|
|
85
|
+
xterm +lc -ti vt340
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Alternatively, you can add these options to your xterm configuration file (`~/.Xresources` or `~/.Xdefaults`) to make the change permanent:
|
|
89
|
+
|
|
90
|
+
```sh
|
|
91
|
+
echo 'XTerm*decTerminalID: vt340' >> ~/.Xresources
|
|
92
|
+
xrdb -merge ~/.Xresources
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Installation
|
|
96
|
+
|
|
97
|
+
Install _textual-image_ using pip with the following commands:
|
|
98
|
+
|
|
99
|
+
For the basic installation:
|
|
100
|
+
```sh
|
|
101
|
+
pip install textual-image
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
To include the Textual Widget's dependencies:
|
|
105
|
+
```sh
|
|
106
|
+
pip install textual-image[textual]
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Demonstration
|
|
110
|
+
|
|
111
|
+
Once installed, run the demo application to see the module in action.
|
|
112
|
+
|
|
113
|
+
For a demonstration of the Rich renderable, use:
|
|
114
|
+
```sh
|
|
115
|
+
python -m textual_image rich
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
For a demonstration of the Textual Widget, use:
|
|
119
|
+
```sh
|
|
120
|
+
python -m textual_image textual
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
The module will automatically select the best available rendering option. If you wish to specify a particular rendering method, use the `-p` argument with one of the following values: `tgp`, `sixel`, `halfcell`, or `unicode`.
|
|
124
|
+
|
|
125
|
+
For more information, use:
|
|
126
|
+
```sh
|
|
127
|
+
python -m textual_image --help
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Usage
|
|
131
|
+
|
|
132
|
+
### Rich Integration
|
|
133
|
+
|
|
134
|
+
To use the Rich renderable, simply pass an instance of `textual_image.renderable.Image` to a Rich function that renders data:
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
from rich.console import Console
|
|
138
|
+
from textual_image.renderable import Image
|
|
139
|
+
|
|
140
|
+
console = Console()
|
|
141
|
+
console.print(Image("path/to/image.png"))
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
The `Image` constructor accepts either a string, a `pathlib.Path` representing the file path of an image readable by [Pillow](https://python-pillow.org/), or a Pillow `Image` instance directly.
|
|
145
|
+
|
|
146
|
+
By default, the image is rendered in its original dimensions. You can modify this behavior by specifying the `width` and/or `height` parameters. These can be defined as an integer (number of cells), a percentage string (e.g., `50%`), or the literal `auto` to automatically scale while maintaining the aspect ratio.
|
|
147
|
+
|
|
148
|
+
`textual_image.renderable.Image` defaults to the best available rendering method. To specify an explicit rendering method, use one of the following classes: `textual_image.renderable.tgp.Image`, `textual_image.renderable.sixel.Image`, `textual_image.renderable.halfcell.Image`, or `textual_image.renderable.unicode.Image`.
|
|
149
|
+
|
|
150
|
+
### Textual Integration
|
|
151
|
+
|
|
152
|
+
For integration with Textual, _textual-image_ offers a Textual `Widget` to render images:
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
from textual.app import App, ComposeResult
|
|
156
|
+
from textual_image.widget import Image
|
|
157
|
+
|
|
158
|
+
class ImageApp(App[None]):
|
|
159
|
+
def compose(self) -> ComposeResult:
|
|
160
|
+
yield Image("path/to/image.png")
|
|
161
|
+
|
|
162
|
+
ImageApp().run()
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
The `Image` constructor accepts either a string or a `pathlib.Path` with the file path of an image readable by [Pillow](https://python-pillow.org/), or a Pillow `Image` instance directly.
|
|
166
|
+
|
|
167
|
+
You can also set the image using the `image` property of an `Image` instance:
|
|
168
|
+
|
|
169
|
+
```python
|
|
170
|
+
from textual.app import App, ComposeResult
|
|
171
|
+
from textual_image.textual import Image
|
|
172
|
+
|
|
173
|
+
class ImageApp(App[None]):
|
|
174
|
+
def compose(self) -> ComposeResult:
|
|
175
|
+
image = Image()
|
|
176
|
+
image.image = "path/to/image.png"
|
|
177
|
+
yield image
|
|
178
|
+
|
|
179
|
+
ImageApp().run()
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
If a different image is set, the Widget will update to display the new image.
|
|
183
|
+
|
|
184
|
+
By default, the best available rendering option is used. To override this, you can instantiate `textual_image.widget.TGPImage`, `textual_image.widget.SixelImage`, `textual_image.widget.HalfcellImage`, or `textual_image.widget.UnicodeImage` directly.
|
|
185
|
+
|
|
186
|
+
_*Note*_: The process of determining the best available rendering option involves querying the terminal, which means sending and receiving data. Since Textual starts threads to handle input and output, this query will **not work** once the Textual app has started. Therefore, make sure that `textual_image.renderable` is imported **before** running the Textual app (which is typically the case in most use cases).
|
|
187
|
+
|
|
188
|
+
## Limitations
|
|
189
|
+
|
|
190
|
+
- **Sixel Support in Textual**: Sixel support in Textual is not particularly performant due to the way Textual handles rendering. The Sixel graphics are injected into the rendering process in a somewhat hacky manner, which affects performance. Scrolling and changing styles of images can lead to a lot of flickering. But for mostly static images it should work fine. If not, please file an issue on GitHub.
|
|
191
|
+
|
|
192
|
+
## Contribution
|
|
193
|
+
|
|
194
|
+
If you find this module useful, please consider starring the repository on GitHub.
|
|
195
|
+
|
|
196
|
+
This project began by moving some TGP functionality from a private project to a public GitHub repository and PyPI package, with some additional code added along the way to support Sixel graphics. If you encounter any issues, please file an issue on GitHub.
|
|
197
|
+
|
|
198
|
+
Contributions via pull requests are welcome and encouraged.
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "textual-image"
|
|
7
|
+
version = "0.5.0"
|
|
8
|
+
description = "Render images via Kitty's Terminal Graphics Protocol with Rich and Textual"
|
|
9
|
+
authors = [
|
|
10
|
+
{name = "Simon Hayessen", email = "simon@lnqs.io"}
|
|
11
|
+
]
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
|
|
19
|
+
"Environment :: Console",
|
|
20
|
+
|
|
21
|
+
"Topic :: Software Development :: Libraries",
|
|
22
|
+
"Topic :: Software Development :: User Interfaces",
|
|
23
|
+
|
|
24
|
+
"License :: OSI Approved :: GNU General Public License (GPL)",
|
|
25
|
+
"License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
|
|
26
|
+
|
|
27
|
+
"Operating System :: POSIX :: Linux",
|
|
28
|
+
|
|
29
|
+
"Programming Language :: Python :: 3",
|
|
30
|
+
"Programming Language :: Python :: 3.10",
|
|
31
|
+
"Programming Language :: Python :: 3.11",
|
|
32
|
+
"Programming Language :: Python :: 3.12",
|
|
33
|
+
|
|
34
|
+
"Typing :: Typed",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
dependencies = [
|
|
38
|
+
"pillow>=10.3.0",
|
|
39
|
+
"rich>=13.4.0",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
[project.urls]
|
|
43
|
+
Homepage = "https://github.com/lnqs/textual-image"
|
|
44
|
+
Documentation = "https://github.com/lnqs/textual-image"
|
|
45
|
+
Repository = "https://github.com/lnqs/textual-image"
|
|
46
|
+
Issues = "https://github.com/lnqs/textual-image/issues"
|
|
47
|
+
|
|
48
|
+
[project.optional-dependencies]
|
|
49
|
+
textual = [
|
|
50
|
+
"textual>=0.68.0",
|
|
51
|
+
]
|
|
52
|
+
dev = [
|
|
53
|
+
"mypy",
|
|
54
|
+
"pytest",
|
|
55
|
+
"pytest-asyncio",
|
|
56
|
+
"pytest-cov",
|
|
57
|
+
"ruff",
|
|
58
|
+
"syrupy",
|
|
59
|
+
"tox",
|
|
60
|
+
"typing-extensions",
|
|
61
|
+
"typos",
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
[tool.setuptools]
|
|
65
|
+
packages = ["textual_image"]
|
|
66
|
+
|
|
67
|
+
[tool.ruff]
|
|
68
|
+
lint.select = ["B", "D", "F", "I", "T", "Q"]
|
|
69
|
+
line-length = 120
|
|
70
|
+
exclude = [
|
|
71
|
+
".env",
|
|
72
|
+
".git",
|
|
73
|
+
".venv",
|
|
74
|
+
"__pycache__",
|
|
75
|
+
"env",
|
|
76
|
+
"venv",
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
[tool.ruff.lint.pydocstyle]
|
|
80
|
+
convention = "google"
|
|
81
|
+
|
|
82
|
+
[tool.ruff.lint.per-file-ignores]
|
|
83
|
+
"tests/**" = ["D"]
|
|
84
|
+
|
|
85
|
+
[tool.ruff.format]
|
|
86
|
+
docstring-code-format = true
|
|
87
|
+
|
|
88
|
+
[tool.pytest.ini_options]
|
|
89
|
+
asyncio_mode = "auto"
|
|
90
|
+
asyncio_default_fixture_loop_scope = "function"
|
|
91
|
+
|
|
92
|
+
[tool.coverage.report]
|
|
93
|
+
fail_under = 100
|
|
94
|
+
exclude_lines = ["if __name__ == .__main__.:"]
|
|
95
|
+
|
|
96
|
+
[tool.mypy]
|
|
97
|
+
strict = true
|
|
98
|
+
exclude = ['build/']
|
|
99
|
+
|
|
100
|
+
[tool.tox]
|
|
101
|
+
isolated_build = true
|
|
102
|
+
envlist = ["format-check", "lint", "spell-check", "types", "3.10", "3.11", "3.12", "rich-only"]
|
|
103
|
+
|
|
104
|
+
[tool.tox.env.format-check]
|
|
105
|
+
description = "Format check"
|
|
106
|
+
skip_install = true
|
|
107
|
+
deps = "ruff"
|
|
108
|
+
commands = [["ruff", "format", "--check", "{posargs:.}"]]
|
|
109
|
+
|
|
110
|
+
[tool.tox.env.lint]
|
|
111
|
+
basepython = ["3.12"]
|
|
112
|
+
description = "Lint"
|
|
113
|
+
skip_install = true
|
|
114
|
+
deps = "ruff"
|
|
115
|
+
commands = [["ruff", "check", "{posargs:.}"]]
|
|
116
|
+
|
|
117
|
+
[tool.tox.env.spell-check]
|
|
118
|
+
basepython = ["3.12"]
|
|
119
|
+
description = "Spell check"
|
|
120
|
+
skip_install = true
|
|
121
|
+
deps = "typos"
|
|
122
|
+
commands = [["typos", "{posargs:.}"]]
|
|
123
|
+
|
|
124
|
+
[tool.tox.env.types]
|
|
125
|
+
basepython = ["3.12"]
|
|
126
|
+
description = "Type checking"
|
|
127
|
+
deps = ".[textual,dev]"
|
|
128
|
+
commands = [["mypy", "{posargs:.}"]]
|
|
129
|
+
|
|
130
|
+
[tool.tox.env_run_base]
|
|
131
|
+
description = "Run tests under {base_python}"
|
|
132
|
+
deps = ".[textual,dev]"
|
|
133
|
+
commands = [["pytest", "--cov=textual_image", "--cov-report=term-missing", "{tty:--color=yes}", "{posargs}"]]
|
|
134
|
+
|
|
135
|
+
[tool.tox.env.rich-only]
|
|
136
|
+
basepython = ["3.12"]
|
|
137
|
+
description = "Test with Rich only"
|
|
138
|
+
deps = ".[dev]"
|
|
139
|
+
commands = [["pytest", "{tty:--color=yes}", "{posargs}"]]
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from pytest import raises
|
|
2
|
+
|
|
3
|
+
from tests.data import TERMINAL_SIZES
|
|
4
|
+
from textual_image._geometry import ImageSize
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def test_image_size_validation() -> None:
|
|
8
|
+
ImageSize(256, 256, None, None).validate()
|
|
9
|
+
ImageSize(256, 256, 128, 128).validate()
|
|
10
|
+
ImageSize(256, 256, "10%", "10%").validate()
|
|
11
|
+
ImageSize(256, 256, "auto", "auto").validate()
|
|
12
|
+
|
|
13
|
+
with raises(ValueError):
|
|
14
|
+
ImageSize(256, 256, "-10%", "-10%").validate()
|
|
15
|
+
|
|
16
|
+
with raises(ValueError):
|
|
17
|
+
ImageSize(256, 256, "10", "10").validate()
|
|
18
|
+
|
|
19
|
+
with raises(ValueError):
|
|
20
|
+
ImageSize(256, 256, "xx%", "xx%").validate()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_image_size_cell_size_calculation() -> None:
|
|
24
|
+
assert ImageSize(0, 0, None, None).get_cell_size(100, 100, TERMINAL_SIZES) == (0, 0)
|
|
25
|
+
assert ImageSize(256, 256, None, None).get_cell_size(100, 100, TERMINAL_SIZES) == (32, 16)
|
|
26
|
+
assert ImageSize(256, 256, "50%", "50%").get_cell_size(100, 100, TERMINAL_SIZES) == (50, 50)
|
|
27
|
+
assert ImageSize(256, 256, "auto", "auto").get_cell_size(100, 100, TERMINAL_SIZES) == (100, 50)
|
|
28
|
+
assert ImageSize(256, 256, "50%", "auto").get_cell_size(100, 100, TERMINAL_SIZES) == (50, 25)
|
|
29
|
+
assert ImageSize(256, 256, 50, "auto").get_cell_size(100, 100, TERMINAL_SIZES) == (50, 25)
|
|
30
|
+
assert ImageSize(256, 256, "auto", "50%").get_cell_size(100, 100, TERMINAL_SIZES) == (100, 50)
|
|
31
|
+
assert ImageSize(256, 256, "auto", 50).get_cell_size(100, 100, TERMINAL_SIZES) == (100, 50)
|
|
32
|
+
assert ImageSize(256, 256, "auto", None).get_cell_size(32, 32, TERMINAL_SIZES) == (32, 16)
|
|
33
|
+
assert ImageSize(256, 256, None, "auto").get_cell_size(32, 32, TERMINAL_SIZES) == (32, 32)
|
|
34
|
+
assert ImageSize(12, 512, "auto", "auto").get_cell_size(32, 32, TERMINAL_SIZES) == (2, 32)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_image_size_pixel_size_calculation() -> None:
|
|
38
|
+
assert ImageSize(256, 256, None, None).get_pixel_size(100, 100, TERMINAL_SIZES) == (256, 256)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from importlib import import_module, reload
|
|
2
|
+
from unittest import skipIf, skipUnless
|
|
3
|
+
from unittest.mock import patch
|
|
4
|
+
|
|
5
|
+
from tests.data import TEXTUAL_ENABLED
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@skipUnless(TEXTUAL_ENABLED, "Textual support disabled")
|
|
9
|
+
def test_main() -> None:
|
|
10
|
+
with patch("sys.argv", ["unittest", "rich"]):
|
|
11
|
+
with patch("textual_image.demo.renderable.run") as run_rich:
|
|
12
|
+
with patch("textual_image.demo.widget.run") as run_textual:
|
|
13
|
+
main = import_module("textual_image.__main__")
|
|
14
|
+
assert run_rich.called
|
|
15
|
+
assert not run_textual.called
|
|
16
|
+
|
|
17
|
+
with patch("sys.argv", ["unittest", "textual"]):
|
|
18
|
+
with patch("textual_image.demo.renderable.run") as run_rich:
|
|
19
|
+
with patch("textual_image.demo.widget.run") as run_textual:
|
|
20
|
+
reload(main)
|
|
21
|
+
assert not run_rich.called
|
|
22
|
+
assert run_textual.called
|
|
23
|
+
|
|
24
|
+
with patch("sys.argv", ["unittest", "textual"]):
|
|
25
|
+
with patch("textual_image.demo.renderable.run") as run_rich:
|
|
26
|
+
with patch("textual_image.demo.widget.run") as run_textual:
|
|
27
|
+
with patch("importlib.util.find_spec", return_value=None):
|
|
28
|
+
reload(main)
|
|
29
|
+
assert not run_rich.called
|
|
30
|
+
assert not run_textual.called
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@skipIf(TEXTUAL_ENABLED, "Textual support enabled")
|
|
34
|
+
def test_main_rich_only() -> None:
|
|
35
|
+
with patch("sys.argv", ["unittest", "rich"]):
|
|
36
|
+
with patch("textual_image.demo.renderable.run") as run_rich:
|
|
37
|
+
main = import_module("textual_image.__main__")
|
|
38
|
+
assert run_rich.called
|
|
39
|
+
|
|
40
|
+
with patch("sys.argv", ["unittest", "textual"]):
|
|
41
|
+
with patch("textual_image.demo.renderable.run") as run_rich:
|
|
42
|
+
with patch("sys.stderr") as stderr:
|
|
43
|
+
reload(main)
|
|
44
|
+
assert stderr.write.called
|
|
45
|
+
assert not run_rich.called
|