iTerm2 images live in the cell grid, unlike Kitty placements: repainting the mosaic behind them erases the picture. Re-emit the image whenever the mosaic identity (station + track) changes, even if the artwork bytes are the same. Also send doNotMoveCursor=1 to iTerm2 itself so the terminal does not scroll or jump the cursor out from under Textual's compositor; WezTerm keeps the previous payload since it is pickier about parameters.
78 lines
2.6 KiB
Python
78 lines
2.6 KiB
Python
from __future__ import annotations
|
|
|
|
import pytest
|
|
from PIL import Image
|
|
|
|
from somafm_tui import artwork
|
|
from somafm_tui.app import SomaFMApp
|
|
from somafm_tui.widgets import art as art_widgets
|
|
from somafm_tui.widgets.art import ArtPane, ITerm2Image, KittyImage
|
|
from somafm_tui.widgets.stage import StagePanel
|
|
|
|
|
|
async def _no_fetch(client, url: str) -> None:
|
|
return None
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_art_pane_switches_between_image_and_mosaic(monkeypatch) -> None:
|
|
monkeypatch.setattr(artwork, "PROTOCOL", "tgp")
|
|
monkeypatch.setattr(
|
|
artwork,
|
|
"fit_image",
|
|
lambda data, width, height: Image.new(
|
|
"RGBA", (width * 10, height * 20), (10, 20, 30, 255)
|
|
),
|
|
)
|
|
monkeypatch.setattr(artwork, "load_cached", lambda url: None)
|
|
monkeypatch.setattr(artwork, "fetch", _no_fetch)
|
|
|
|
app = SomaFMApp(autoplay=False, offline=True, audio=False)
|
|
async with app.run_test(size=(110, 40)) as pilot:
|
|
await pilot.pause()
|
|
pane = app.query_one(ArtPane)
|
|
panel = app.query_one(StagePanel)
|
|
|
|
app.artwork_bytes = b"fake-image-bytes"
|
|
panel.refresh_art()
|
|
await pilot.pause()
|
|
assert isinstance(pane._image_widget, KittyImage)
|
|
|
|
app.artwork_bytes = None
|
|
panel.refresh_art()
|
|
await pilot.pause()
|
|
assert pane._image_widget is None
|
|
# The mosaic is always painted by ArtPane.render(), even under images.
|
|
assert pane.render() is not None
|
|
|
|
|
|
def test_iterm2_sequence_uses_cell_units(monkeypatch) -> None:
|
|
monkeypatch.setattr(art_widgets, "_iterm2_native", lambda: False)
|
|
widget = ITerm2Image(Image.new("RGBA", (32, 32), (10, 20, 30, 255)))
|
|
sequence = widget._sequence(13, 8)
|
|
assert sequence.startswith(
|
|
"\x1b]1337;File=inline=1;width=13;height=8;"
|
|
)
|
|
# WezTerm shares the protocol but not every iTerm2-only parameter.
|
|
assert "doNotMoveCursor" not in sequence
|
|
|
|
|
|
def test_iterm2_native_keeps_the_cursor_put(monkeypatch) -> None:
|
|
monkeypatch.setattr(art_widgets, "_iterm2_native", lambda: True)
|
|
widget = ITerm2Image(Image.new("RGBA", (32, 32), (10, 20, 30, 255)))
|
|
sequence = widget._sequence(13, 8)
|
|
assert sequence.startswith("\x1b]1337;File=inline=1;doNotMoveCursor=1;")
|
|
assert "width=13;height=8;" in sequence
|
|
|
|
|
|
def test_kitty_direct_placement_sequences() -> None:
|
|
widget = KittyImage(Image.new("RGBA", (32, 32), (10, 20, 30, 255)))
|
|
commands = widget._sequences(13, 8)
|
|
joined = "".join(commands)
|
|
assert "a=t" in joined
|
|
assert "a=p" in joined
|
|
assert "c=13,r=8" in joined
|
|
assert "U=1" not in joined # no Unicode placeholders
|
|
assert widget._sequences(13, 8) == []
|
|
widget.cleanup()
|