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.
@@ -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
+ ![Demo App](./demo.gif)
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
+ ![Demo App](./demo.gif)
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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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