From 496975294675bb25fe405f112fbb9217528afab2 Mon Sep 17 00:00:00 2001 From: NSPC911 <87571998+NSPC911@users.noreply.github.com> Date: Sat, 28 Feb 2026 14:20:39 +0800 Subject: [PATCH 1/3] feat: add item2 image support --- textual_image/demo/renderable.py | 4 + textual_image/demo/widget.py | 3 +- textual_image/renderable/__init__.py | 13 +- textual_image/renderable/iterm2.py | 137 ++++++++++++++++++ textual_image/widget/__init__.py | 13 +- textual_image/widget/iterm2.py | 199 +++++++++++++++++++++++++++ 6 files changed, 360 insertions(+), 9 deletions(-) create mode 100644 textual_image/renderable/iterm2.py create mode 100644 textual_image/widget/iterm2.py diff --git a/textual_image/demo/renderable.py b/textual_image/demo/renderable.py index 1adeafb..f4a4d85 100755 --- a/textual_image/demo/renderable.py +++ b/textual_image/demo/renderable.py @@ -15,6 +15,9 @@ from textual_image.renderable import ( Image as AutoRenderable, ) +from textual_image.renderable import ( + ITerm2Image as ITerm2Renderable, +) from textual_image.renderable import ( SixelImage as SixelRenderable, ) @@ -31,6 +34,7 @@ "auto": AutoRenderable, "tgp": TGPRenderable, "sixel": SixelRenderable, + "iterm2": ITerm2Renderable, "halfcell": HalfcellRenderable, "unicode": UnicodeRenderable, } diff --git a/textual_image/demo/widget.py b/textual_image/demo/widget.py index 9e63c59..8ae1206 100755 --- a/textual_image/demo/widget.py +++ b/textual_image/demo/widget.py @@ -15,7 +15,7 @@ from textual.widgets import Button, Footer, Header, Input, Label, OptionList, Select, TabbedContent, TabPane from textual.widgets.option_list import Option -from textual_image.widget import HalfcellImage, SixelImage, TGPImage, UnicodeImage +from textual_image.widget import HalfcellImage, ITerm2Image, SixelImage, TGPImage, UnicodeImage from textual_image.widget import Image as AutoImage TEST_IMAGE = Path(__file__).parent / ".." / "gracehopper.jpg" @@ -25,6 +25,7 @@ "auto": AutoImage, "tgp": TGPImage, "sixel": SixelImage, + "iterm2": ITerm2Image, "halfcell": HalfcellImage, "unicode": UnicodeImage, } diff --git a/textual_image/renderable/__init__.py b/textual_image/renderable/__init__.py index 71b3e87..ced3c63 100644 --- a/textual_image/renderable/__init__.py +++ b/textual_image/renderable/__init__.py @@ -4,8 +4,9 @@ import sys from typing import Type -from textual_image.renderable import sixel, tgp +from textual_image.renderable import iterm2, sixel, tgp from textual_image.renderable.halfcell import Image as HalfcellImage +from textual_image.renderable.iterm2 import Image as ITerm2Image from textual_image.renderable.sixel import Image as SixelImage from textual_image.renderable.tgp import Image as TGPImage from textual_image.renderable.unicode import Image as UnicodeImage @@ -14,13 +15,17 @@ is_tty = sys.__stdout__ and sys.__stdout__.isatty() -Image: Type[TGPImage | SixelImage | HalfcellImage | UnicodeImage] +Image: Type[TGPImage | ITerm2Image | SixelImage | HalfcellImage | UnicodeImage] # TGP should be on top, as it performs way better than Sixel. # However, the only terminal with TGP unicode diacritic support I know of is Kitty. # Konsole and wezterm report TGP support, but don't work with our placeholder implementation, but do with Sixel. # As Kitty does *not* support Sixel, this order should be best in terms of compatibility. -if is_tty and sixel.query_terminal_support(): +# but wezterm supports iterm2, so we check that before sixel +if is_tty and iterm2.query_terminal_support(): + logger.debug("iTerm2 support detected") + Image = ITerm2Image +elif is_tty and sixel.query_terminal_support(): logger.debug("Sixel support detected") Image = SixelImage elif is_tty and tgp.query_terminal_support(): @@ -33,4 +38,4 @@ logger.debug("Not connected to a terminal, falling back to unicode") Image = UnicodeImage -__all__ = ["Image", "TGPImage", "SixelImage", "HalfcellImage", "UnicodeImage"] +__all__ = ["Image", "TGPImage", "SixelImage", "ITerm2Image", "HalfcellImage", "UnicodeImage"] diff --git a/textual_image/renderable/iterm2.py b/textual_image/renderable/iterm2.py new file mode 100644 index 0000000..d49acad --- /dev/null +++ b/textual_image/renderable/iterm2.py @@ -0,0 +1,137 @@ +"""Provides a Rich Renderable to render images via the iTerm2 Inline Images Protocol (https://iterm2.com/documentation-images.html).""" + +import logging +import os +import sys +from typing import IO + +from PIL import Image as PILImage +from rich.console import Console, ConsoleOptions, RenderResult +from rich.control import Control +from rich.measure import Measurement +from rich.segment import ControlType, Segment + +from textual_image._geometry import ImageSize +from textual_image._pixeldata import PixelData +from textual_image._terminal import get_cell_size +from textual_image._utils import StrOrBytesPath + +logger = logging.getLogger(__name__) + +# Random no-op control code to prevent Rich from messing with our data +_NULL_CONTROL = [(ControlType.CURSOR_FORWARD, 0)] + + +def _build_iterm2_sequence(image_data_b64: str, pixel_width: int, pixel_height: int) -> str: + """Build an iTerm2 inline image escape sequence. + + Args: + image_data_b64: Base64-encoded image data (PNG). + pixel_width: Width to render in pixels. + pixel_height: Height to render in pixels. + + Returns: + The complete escape sequence string. + """ + return ( + f"\x1b]1337;File=" + f"inline=1;" + f"width={pixel_width}px;" + f"height={pixel_height}px;" + f"preserveAspectRatio=0" + f":{image_data_b64}\x07" + ) + + +class Image: + """Rich Renderable to render images via the iTerm2 Inline Images Protocol ().""" + + def __init__( + self, + image: StrOrBytesPath | IO[bytes] | PILImage.Image, + width: int | str | None = None, + height: int | str | None = None, + ) -> None: + """Initialized the `Image`. + + Args: + image: Path to an image file, a byte stream containing image data, or `PIL.Image.Image` instance with the + image data to render. + width: Width specification to render the image. + See `textual_image.geometry.ImageSize` for details about possible values. + height: height specification to render the image. + See `textual_image.geometry.ImageSize` for details about possible values. + """ + self._image_data = PixelData(image) + self._render_size = ImageSize(self._image_data.width, self._image_data.height, width, height) + + def cleanup(self) -> None: + """No-op.""" + pass + + def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult: + """Called by Rich to render the `Image`. + + Args: + console: The `Console` instance to render to. + options: Options for rendering, i.e. available size information. + + Returns: + `Segment`s to display. + """ + terminal_sizes = get_cell_size() + + cell_width, cell_height = self._render_size.get_cell_size(options.max_width, options.max_height, terminal_sizes) + pixel_width, pixel_height = self._render_size.get_pixel_size( + options.max_width, options.max_height, terminal_sizes + ) + + # Add a text placeholder for the image that'll be overwritten with the actual image. + # This way rich realizes how much space the renderable uses. + for _ in range(cell_height): + yield Segment(" " * cell_width + "\n") + + # Save cursor position to restore after drawing the image + yield Segment("\x1b7", control=_NULL_CONTROL) + yield Control.move(0, -cell_height) + + image_data_b64 = self._image_data.scaled(pixel_width, pixel_height).to_base64() + sequence = _build_iterm2_sequence(image_data_b64, pixel_width, pixel_height) + + # We add a random no-op control code to prevent Rich from messing with our data + yield Segment(sequence, control=_NULL_CONTROL) + yield Segment("\x1b8", control=_NULL_CONTROL) + + def __rich_measure__(self, console: Console, options: ConsoleOptions) -> Measurement: + """Called by Rich to get the render width without actually rendering the object. + + Args: + console: The `Console` instance to render to. + options: Options for rendering, i.e. available size information. + + Returns: + A `Measurement` containing minimum and maximum widths required to render the object + """ + terminal_sizes = get_cell_size() + width, _ = self._render_size.get_cell_size(options.max_width, options.max_height, terminal_sizes) + return Measurement(width, width) + + +def query_terminal_support() -> bool: + """Queries the terminal for iTerm2 Inline Images Protocol support. + + Returns: + True if the iTerm2 Inline Images Protocol is supported, False if not + """ + if not sys.__stdout__: + return False + + # Check TERM_PROGRAM environment variable for quick detection + if os.environ.get("TERM_PROGRAM") in ("iTerm2", "WezTerm"): + return True + + # need some help with feature reporting protocol + # iterm2's documentation says https://iterm2.com/feature-reporting but it + # doesnt work on wezterm, and i dont own a mac, so, help i guess? + + return False diff --git a/textual_image/widget/__init__.py b/textual_image/widget/__init__.py index 8d515c5..f3bbc27 100644 --- a/textual_image/widget/__init__.py +++ b/textual_image/widget/__init__.py @@ -5,11 +5,13 @@ from textual_image._terminal import get_cell_size from textual_image.renderable import Image as AutoRenderable from textual_image.renderable.halfcell import Image as HalfcellRenderable +from textual_image.renderable.iterm2 import Image as ITerm2Renderable from textual_image.renderable.sixel import Image as SixelRenderable from textual_image.renderable.sixel import SixelOptions from textual_image.renderable.tgp import Image as TGPRenderable from textual_image.renderable.unicode import Image as UnicodeRenderable from textual_image.widget._base import Image as BaseImage +from textual_image.widget.iterm2 import Image as ITerm2Image from textual_image.widget.sixel import Image as SixelImage # Run `get_cell_size()` once to fill the cache, @@ -23,10 +25,12 @@ class AutoImage(BaseImage, Renderable=AutoRenderable): pass -# This is bit annoying, but as all renderables but the Sixel one can just be thrown in the base class, while -# we need a dedicated one for Sixel, we have to do this `if`. -if AutoRenderable is SixelRenderable: - Image: Type[AutoImage | SixelImage] = SixelImage +# This is bit annoying, but as all renderables but the Sixel and iTerm2 ones can just be thrown in the base class, +# while we need dedicated ones for Sixel and iTerm2, we have to do this `if`. +if AutoRenderable is ITerm2Renderable: + Image = ITerm2Image +elif AutoRenderable is SixelRenderable: + Image: Type[AutoImage | SixelImage | ITerm2Image] = SixelImage else: Image = AutoImage @@ -54,6 +58,7 @@ class UnicodeImage(BaseImage, Renderable=UnicodeRenderable): "TGPImage", "SixelImage", "SixelOptions", + "ITerm2Image", "HalfcellImage", "UnicodeImage", ] diff --git a/textual_image/widget/iterm2.py b/textual_image/widget/iterm2.py new file mode 100644 index 0000000..da2a140 --- /dev/null +++ b/textual_image/widget/iterm2.py @@ -0,0 +1,199 @@ +"""Provides a Textual `Widget` to render images via the iTerm2 Inline Images Protocol in the terminal.""" + +import logging +from typing import IO, Iterable, NamedTuple + +from PIL import Image as PILImage +from rich.console import Console, ConsoleOptions, RenderResult +from rich.control import Control +from rich.measure import Measurement +from rich.segment import ControlType, Segment +from rich.style import Style +from textual.app import ComposeResult +from textual.dom import NoScreen +from textual.geometry import Region, Size +from textual.strip import Strip +from textual.widget import Widget +from typing_extensions import override + +from textual_image._geometry import ImageSize +from textual_image._pixeldata import PixelData +from textual_image._terminal import CellSize, get_cell_size +from textual_image._utils import StrOrBytesPath +from textual_image.renderable.iterm2 import _build_iterm2_sequence +from textual_image.widget._base import Image as BaseImage + +logger = logging.getLogger(__name__) + + +_NULL_STYLE = Style() + + +class _CachedITerm2Data(NamedTuple): + image: StrOrBytesPath | IO[bytes] | PILImage.Image + content_crop: Region + content_size: Size + terminal_sizes: CellSize + iterm2_data: str + + def is_hit( + self, + image: StrOrBytesPath | IO[bytes] | PILImage.Image, + content_crop: Region, + content_size: Size, + terminal_sizes: CellSize, + ) -> bool: + """Check if cached data matches the current parameters.""" + return ( + image == self.image + and content_crop == self.content_crop + and content_size == self.content_size + and terminal_sizes == self.terminal_sizes + ) + + +class _NoopRenderable: + """Image renderable rendering nothing. + + Used by the iTerm2 image as placeholder. + Rendering the iTerm2 renderable doesn't work with Textual as it relies on printable segments. + Instead, iTerm2 data is injected into the rendering process. To keep our base class happy, we use this class + as renderable passed to it. + """ + + def __init__( + self, + image: StrOrBytesPath | IO[bytes] | PILImage.Image, + width: int | str | None = None, + height: int | str | None = None, + ) -> None: + pass + + def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult: + yield Segment("") + + def __rich_measure__(self, console: Console, options: ConsoleOptions) -> Measurement: + return Measurement(0, 0) + + def cleanup(self) -> None: + pass + + +class Image(BaseImage, Renderable=_NoopRenderable): + """Textual `Widget` to render images via the iTerm2 Inline Images Protocol in the terminal.""" + + @override + @BaseImage.image.setter # type: ignore + def image(self, value: StrOrBytesPath | IO[bytes] | PILImage.Image | None) -> None: + """Set the image to render.""" + super(__class__, type(self)).image.fset(self, value) # type: ignore + self.refresh(recompose=True) + + def compose(self) -> ComposeResult: + """Called by Textual to create child widgets.""" + yield _ImageITerm2Impl(self.image) + + +class _ImageITerm2Impl(Widget, can_focus=False, inherit_css=False): + """Widget implementation injecting iTerm2 image data into the rendering process. + + This class is meant to be used only by `textual_image.widget.iterm2.Image`. + It creates and renders iTerm2 inline image data. + + It is done in this child widget to simplify the process -- this class assumes it never has to render any borders or + spacings, but only the parent will if required by the user. + We assume `self.region == self.content_region` in this class, which lets us use the `crop` parameter + in `render_lines()` directly on our image data without having to deal with gutters, as well as moving + the cursor after rendering the iTerm2 data to an easily determinable position. + """ + + DEFAULT_CSS = """ + _ImageData { + width: 100%; + height: 100%; + } + """ + + @override + def __init__( + self, + image: StrOrBytesPath | IO[bytes] | PILImage.Image | None = None, + ) -> None: + super().__init__() + self.image = image + self._cached_iterm2_data: _CachedITerm2Data | None = None + + @override + def render_lines(self, crop: Region) -> list[Strip]: + # We don't render anything if the screen isn't active. Textual may try to tint the widget which leads to weird + # effects. + try: + if not self.image or not self.screen.is_active: + return [] + except NoScreen: # if no screen, return empty list + return [] + + # Inject the iTerm2 data. We can only do it here because we don't know the crop region before. + terminal_sizes = get_cell_size() + + if self._cached_iterm2_data and self._cached_iterm2_data.is_hit( + self.image, crop, self.content_size, terminal_sizes + ): + logger.debug(f"using iTerm2 data from cache for crop region {crop}") + iterm2_data = self._cached_iterm2_data.iterm2_data + else: + logger.debug(f"encoding iTerm2 data for crop region {crop}") + + image_data = PixelData(self.image) + image_data = self._scale_image(image_data, terminal_sizes) + image_data = self._crop_image(image_data, crop, terminal_sizes) + + iterm2_data = _build_iterm2_sequence( + image_data.to_base64(), + crop.width * terminal_sizes.width, + crop.height * terminal_sizes.height, + ) + self._cached_iterm2_data = _CachedITerm2Data( + self.image, crop, self.content_size, terminal_sizes, iterm2_data + ) + + iterm2_segments = self._get_iterm2_segments(iterm2_data) + clear_style = self._get_clear_style() + clear_segment = Segment(" " * crop.width, style=clear_style) + lines = [Strip([clear_segment], cell_length=crop.width) for _ in range(crop.height - 1)] + lines.append(Strip([clear_segment, *iterm2_segments], cell_length=crop.width)) + return lines + + def _scale_image(self, image_data: PixelData, terminal_sizes: CellSize) -> PixelData: + assert isinstance(self.parent, Image) + + styled_width, styled_height = self.parent._get_styled_size() + image_size = ImageSize(image_data.width, image_data.height, width=styled_width, height=styled_height) + pixel_width, pixel_height = image_size.get_pixel_size( + self.content_size.width, self.content_size.height, terminal_sizes + ) + + return image_data.scaled(pixel_width, pixel_height) + + def _crop_image(self, image: PixelData, crop: Region, terminal_sizes: CellSize) -> PixelData: + crop_pixels_left = crop.x * terminal_sizes.width + crop_pixels_top = crop.y * terminal_sizes.height + crop_pixels_right = crop.right * terminal_sizes.width + crop_pixels_bottom = crop.bottom * terminal_sizes.height + + return image.cropped(crop_pixels_left, crop_pixels_top, crop_pixels_right, crop_pixels_bottom) + + def _get_iterm2_segments(self, iterm2_data: str) -> Iterable[Segment]: + visible_region = self.screen.find_widget(self).visible_region + return [ + Segment( + Control.move_to(visible_region.x, visible_region.y).segment.text, + style=_NULL_STYLE, + ), + Segment(iterm2_data, style=_NULL_STYLE, control=((ControlType.CURSOR_FORWARD, 0),)), + Segment(Control.move_to(visible_region.right, visible_region.bottom - 2).segment.text, style=_NULL_STYLE), + ] + + def _get_clear_style(self) -> Style: + _, color = self.background_colors + return Style(bgcolor=color.rich_color) From a85f3c073efab2e21b9c94611eac1e62b0d3b533 Mon Sep 17 00:00:00 2001 From: NSPC911 <87571998+NSPC911@users.noreply.github.com> Date: Fri, 29 May 2026 21:27:31 +0800 Subject: [PATCH 2/3] fix: update readme for iterm2 support --- README.md | 48 +++++++++++++++++++++++++++++++----------------- 1 file changed, 31 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index ce89abe..9524014 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ _textual-image_ offers both Rich renderables and Textual Widgets that leverage t - **Terminal Graphics Protocol (TGP)**: Initially introduced by the [Kitty](https://sw.kovidgoyal.net/kitty/) terminal emulator. While support is partially available in other terminals, it doesn't seem to be really usable there. - **Sixel Graphics**: Supported by various terminal emulators including [xterm](https://invisible-island.net/xterm/) and a lot of others. +- **iTerm2 Inline Images Protocol**: Originally developed for [iTerm2](https://iterm2.com/), this protocol is now supported by several other terminal emulators for displaying inline images. _Note_: As implementation of these protocols differ a lot feedback on different terminal emulators is very welcome. @@ -23,21 +24,21 @@ See the Support Matrix below on what was tested already. [^1]: Based on [Are We Sixel Yet?](https://www.arewesixelyet.com/) -| Terminal | TGP support | Sixel support | Works with textual-image | -|---------------------|:-----------:|:-------------:|:------------------------:| -| Black Box | ❌ | ✅ | ✅ | -| foot | ❌ | ✅ | ✅ | -| GNOME Terminal | ❌ | ❌ | | -| iTerm2 | ❌ | ✅ | ✅ | -| kitty | ✅ | ❌ | ✅ | -| konsole | ✅ | ✅ | ✅ | -| tmux | ⚠️ | ✅ | ✅ | -| Visual Studio Code | ❌ | ✅ | ✅ | -| Warp | ❌ | ❌ | ❌ | -| wezterm | ✅ | ✅ | ✅ | -| Windows Console | ❌ | ❌ | | -| Windows Terminal | ❌ | ✅ | ✅ | -| xterm | ❌ | ✅ | ✅ | +| Terminal | TGP support | Sixel support | iTerm2 support | Works with textual-image | +|---------------------|:-----------:|:-------------:|:--------------:|:------------------------:| +| Black Box | ❌ | ✅ | ❌ | ✅ | +| foot | ❌ | ✅ | ✅ | ✅ | +| GNOME Terminal | ❌ | ❌ | ❌ | | +| iTerm2 | ❌ | ✅ | ✅ | ✅ | +| kitty | ✅ | ❌ | ❌ | ✅ | +| konsole | ✅ | ✅ | ✅ | ✅ | +| tmux | ⚠️ | ✅ | ⚠️ | ✅ | +| Visual Studio Code | ❌ | ✅ | ✅ | ✅ | +| Warp | ❌ | ❌ | ❌ | ❌ | +| wezterm | ✅ | ✅ | ✅ | ✅ | +| Windows Console | ❌ | ❌ | ❌ | | +| Windows Terminal | ❌ | ✅ | ❌ | ✅ | +| xterm | ❌ | ✅ | ❌ | ✅ | ✅ = Supported; ❌ = Not Supported; ⚠️ = Requires additional terminal/tmux configuration @@ -46,6 +47,7 @@ See the Support Matrix below on what was tested already. **Homepage**: https://gitlab.gnome.org/raggesilver/blackbox **TGP support**: No **Sixel support**: Yes +**iTerm2 support**: No **Works**: Yes **Notes**: @@ -59,6 +61,7 @@ This is the case for the Flatpak version of BlackBox, but not on most Linux dist **Homepage**: https://codeberg.org/dnkl/foot **TGP support**: No **Sixel support**: Yes +**iTerm2 support**: Yes **Works**: Yes **Notes:** @@ -69,6 +72,7 @@ Works out of the box, no known issues. **Homepage**: https://gitlab.gnome.org/GNOME/gnome-terminal **TGP support**: No **Sixel support**: No +**iTerm2 support**: No **Works**: No **Notes:** @@ -79,6 +83,7 @@ Relies on VTE Sixel implementation ( Date: Sun, 28 Jun 2026 21:57:46 +0800 Subject: [PATCH 3/3] tests: add iterm2 tests --- .../renderable/__snapshots__/test_iterm2.ambr | 36 ++++ tests/renderable/test_init.py | 18 +- tests/renderable/test_iterm2.py | 68 +++++++ tests/widget/test_iterm2.py | 182 ++++++++++++++++++ 4 files changed, 297 insertions(+), 7 deletions(-) create mode 100644 tests/renderable/__snapshots__/test_iterm2.ambr create mode 100644 tests/renderable/test_iterm2.py create mode 100644 tests/widget/test_iterm2.py diff --git a/tests/renderable/__snapshots__/test_iterm2.ambr b/tests/renderable/__snapshots__/test_iterm2.ambr new file mode 100644 index 0000000..e79e1ad --- /dev/null +++ b/tests/renderable/__snapshots__/test_iterm2.ambr @@ -0,0 +1,36 @@ +# serializer version: 1 +# name: test_render + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 7]1337;File=inline=1;width=40px;height=600px;preserveAspectRatio=0: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 + ''' +# --- diff --git a/tests/renderable/test_init.py b/tests/renderable/test_init.py index 1d0f8c8..b2f4bce 100644 --- a/tests/renderable/test_init.py +++ b/tests/renderable/test_init.py @@ -4,22 +4,26 @@ def test_determining_best_renderable() -> None: import textual_image.renderable - from textual_image.renderable import halfcell, sixel, tgp, unicode + from textual_image.renderable import halfcell, iterm2, sixel, tgp, unicode with patch("sys.__stdout__.isatty", return_value=True): - with patch("textual_image.renderable.tgp.query_terminal_support", return_value=True): + with patch("textual_image.renderable.iterm2.query_terminal_support", return_value=True): module = reload(textual_image.renderable) - assert module.Image is tgp.Image + assert module.Image is iterm2.Image - with patch("textual_image.renderable.tgp.query_terminal_support", return_value=False): + with patch("textual_image.renderable.iterm2.query_terminal_support", return_value=False): with patch("textual_image.renderable.sixel.query_terminal_support", return_value=True): module = reload(textual_image.renderable) assert module.Image is sixel.Image - with patch("textual_image.renderable.tgp.query_terminal_support", return_value=False): with patch("textual_image.renderable.sixel.query_terminal_support", return_value=False): - module = reload(textual_image.renderable) - assert module.Image is halfcell.Image + with patch("textual_image.renderable.tgp.query_terminal_support", return_value=True): + module = reload(textual_image.renderable) + assert module.Image is tgp.Image + + with patch("textual_image.renderable.tgp.query_terminal_support", return_value=False): + module = reload(textual_image.renderable) + assert module.Image is halfcell.Image with patch("sys.__stdout__.isatty", return_value=False): module = reload(textual_image.renderable) diff --git a/tests/renderable/test_iterm2.py b/tests/renderable/test_iterm2.py new file mode 100644 index 0000000..617a33b --- /dev/null +++ b/tests/renderable/test_iterm2.py @@ -0,0 +1,68 @@ +from unittest.mock import patch + +from rich.console import Console +from rich.measure import Measurement +from syrupy.assertion import SnapshotAssertion + +from tests.data import CONSOLE_OPTIONS, TEST_IMAGE +from tests.utils import render + + +def test_build_iterm2_sequence() -> None: + from textual_image.renderable.iterm2 import _build_iterm2_sequence + + seq = _build_iterm2_sequence("abc123", 100, 200) + assert seq.startswith("\x1b]1337;File=") + assert "width=100px" in seq + assert "height=200px" in seq + assert "inline=1" in seq + assert "preserveAspectRatio=0" in seq + assert ":abc123\x07" in seq + + +def test_render(snapshot: SnapshotAssertion) -> None: + from textual_image.renderable.iterm2 import Image + + renderable = Image(TEST_IMAGE, width=4) + assert render(renderable) == snapshot + + +def test_measure() -> None: + from textual_image.renderable.iterm2 import Image + + renderable = Image(TEST_IMAGE, width=4) + assert renderable.__rich_measure__(Console(), CONSOLE_OPTIONS) == Measurement(4, 4) + + +def test_cleanup() -> None: + from textual_image.renderable.iterm2 import Image + + Image(TEST_IMAGE, width=4).cleanup() + + +def test_query_terminal_support_no_stdout() -> None: + from textual_image.renderable.iterm2 import query_terminal_support + + with patch("sys.__stdout__", None): + assert not query_terminal_support() + + +def test_query_terminal_support_iterm2_env() -> None: + from textual_image.renderable.iterm2 import query_terminal_support + + with patch("sys.__stdout__"), patch.dict("os.environ", {"TERM_PROGRAM": "iTerm2"}): + assert query_terminal_support() + + +def test_query_terminal_support_wezterm_env() -> None: + from textual_image.renderable.iterm2 import query_terminal_support + + with patch("sys.__stdout__"), patch.dict("os.environ", {"TERM_PROGRAM": "WezTerm"}): + assert query_terminal_support() + + +def test_query_terminal_support_unknown_env() -> None: + from textual_image.renderable.iterm2 import query_terminal_support + + with patch("sys.__stdout__"), patch.dict("os.environ", {"TERM_PROGRAM": "xterm-256color"}): + assert not query_terminal_support() diff --git a/tests/widget/test_iterm2.py b/tests/widget/test_iterm2.py new file mode 100644 index 0000000..6c05eeb --- /dev/null +++ b/tests/widget/test_iterm2.py @@ -0,0 +1,182 @@ +from unittest import skipUnless +from unittest.mock import PropertyMock, patch + +from PIL import Image as PILImage +from PIL import ImageOps +from rich.console import Console +from rich.measure import Measurement +from rich.segment import Segment + +from tests.data import CONSOLE_OPTIONS, TEST_IMAGE, TEXTUAL_ENABLED +from tests.utils import load_non_seekable_bytes_io + + +@skipUnless(TEXTUAL_ENABLED, "Textual support disabled") +def test_cached_iterm2_data_is_hit() -> None: + from textual.geometry import Region, Size + + from textual_image._terminal import CellSize + from textual_image.widget.iterm2 import _CachedITerm2Data + + image = TEST_IMAGE + crop = Region(0, 0, 10, 10) + size = Size(10, 10) + terminal_sizes = CellSize(10, 20) + + cached = _CachedITerm2Data(image, crop, size, terminal_sizes, "data") + assert cached.is_hit(image, crop, size, terminal_sizes) + assert not cached.is_hit(image, Region(1, 0, 10, 10), size, terminal_sizes) + assert not cached.is_hit(image, crop, Size(5, 5), terminal_sizes) + assert not cached.is_hit(image, crop, size, CellSize(8, 16)) + + +@skipUnless(TEXTUAL_ENABLED, "Textual support disabled") +def test_noop_renderable() -> None: + from textual_image.widget.iterm2 import _NoopRenderable + + r = _NoopRenderable(TEST_IMAGE) + assert r.__rich_measure__(Console(), CONSOLE_OPTIONS) == Measurement(0, 0) + segments = list(r.__rich_console__(Console(), CONSOLE_OPTIONS)) + assert segments == [Segment("")] + r.cleanup() + + +@skipUnless(TEXTUAL_ENABLED, "Textual support disabled") +async def test_app() -> None: + from textual.app import App, ComposeResult + + from textual_image.widget.iterm2 import Image + + class TestApp(App[None]): + CSS = """ + .auto { width: auto; height: auto; } + .fixed { width: 10; height: 10; } + """ + + def compose(self) -> ComposeResult: + yield Image(TEST_IMAGE) + yield Image(TEST_IMAGE, classes="auto") + yield Image(TEST_IMAGE, classes="fixed") + yield Image() + + app = TestApp() + + async with app.run_test() as pilot: + with PILImage.open(TEST_IMAGE) as test_image: + app.query_one(Image).image = ImageOps.flip(test_image) + assert app.query_one(Image).image != TEST_IMAGE + await pilot.pause() + + +@skipUnless(TEXTUAL_ENABLED, "Textual support disabled") +async def test_unseekable_stream() -> None: + from textual.app import App, ComposeResult + + from textual_image.widget.iterm2 import Image + + image = Image(load_non_seekable_bytes_io(TEST_IMAGE)) + + class TestApp(App[None]): + def compose(self) -> ComposeResult: + yield image + + app = TestApp() + async with app.run_test() as pilot: + await pilot.pause() + + +@skipUnless(TEXTUAL_ENABLED, "Textual support disabled") +async def test_handling_no_screen_on_render() -> None: + from textual.app import App, ComposeResult + from textual.dom import NoScreen + from textual.geometry import Region + + from textual_image.widget.iterm2 import Image, _ImageITerm2Impl + + class TestApp(App[None]): + def compose(self) -> ComposeResult: + yield Image(TEST_IMAGE) + + app = TestApp() + + async with app.run_test(): + impl = app.query_one(_ImageITerm2Impl) + + result = impl.render_lines(Region(0, 0, 10, 10)) + assert result + + with patch.object(_ImageITerm2Impl, "screen", PropertyMock(side_effect=NoScreen)): + result = impl.render_lines(Region(0, 0, 10, 10)) + assert not result + + +@skipUnless(TEXTUAL_ENABLED, "Textual support disabled") +async def test_render_lines_no_image_returns_empty() -> None: + from textual.app import App, ComposeResult + from textual.geometry import Region + + from textual_image.widget.iterm2 import Image, _ImageITerm2Impl + + class TestApp(App[None]): + def compose(self) -> ComposeResult: + yield Image() + + async with TestApp().run_test() as pilot: + impl = pilot.app.query_one(_ImageITerm2Impl) + assert impl.render_lines(Region(0, 0, 10, 10)) == [] + + +@skipUnless(TEXTUAL_ENABLED, "Textual support disabled") +async def test_render_lines_uses_cache_on_second_call() -> None: + from textual.app import App, ComposeResult + from textual.geometry import Region + + from textual_image.widget.iterm2 import Image, _ImageITerm2Impl + + class TestApp(App[None]): + CSS = "Image { width: 10; height: 10; }" + + def compose(self) -> ComposeResult: + yield Image(TEST_IMAGE) + + async with TestApp().run_test() as pilot: + await pilot.pause() + impl = pilot.app.query_one(_ImageITerm2Impl) + + crop = Region(0, 0, impl.content_size.width, impl.content_size.height) + impl.render_lines(crop) + cached = impl._cached_iterm2_data + + impl.render_lines(crop) + # ponytail: same object means cache was reused, not recomputed + assert impl._cached_iterm2_data is cached + + +@skipUnless(TEXTUAL_ENABLED, "Textual support disabled") +async def test_render_lines_clears_widget_area_before_iterm2() -> None: + from textual.app import App, ComposeResult + from textual.geometry import Region + + from textual_image.widget.iterm2 import Image, _ImageITerm2Impl + + class TestApp(App[None]): + CSS = """ + Image { width: 4; height: 3; background: red; } + """ + + def compose(self) -> ComposeResult: + yield Image(PILImage.new("RGBA", (2, 2), (0, 0, 0, 0))) + + async with TestApp().run_test() as pilot: + await pilot.pause() + impl = pilot.app.query_one(_ImageITerm2Impl) + + with patch.object(_ImageITerm2Impl, "_get_iterm2_segments", return_value=[Segment("ITERM2")]): + lines = impl.render_lines(Region(0, 0, 4, 3)) + + assert len(lines) == 3 + for strip in lines: + clear_segment = strip._segments[0] + assert clear_segment.text == " " * 4 + + assert any(seg.text == "ITERM2" for seg in lines[-1]._segments)