From 63fbf216fdf8a18bf297f9009ac0f29b60f39cc9 Mon Sep 17 00:00:00 2001 From: txoof-bot <337660340+txoof-bot@users.noreply.github.com> Date: Wed, 7 Oct 2026 18:05:02 +0200 Subject: [PATCH 1/3] Splash screen shows the [web] port The built-in splash gets [web] port as its `port` setting; a reload that changes the port gives the splash the new address at once. Co-Authored-By: Claude Opus 5.5 --- src/paperpi/plugins/splash_screen/README.md | 4 ++-- src/paperpi/plugins/splash_screen/__init__.py | 19 ++++++++++------- src/paperpi/scheduler.py | 14 +++++++------ tests/test_scheduler.py | 13 ++++++++++++ tests/test_splash_screen.py | 21 ++++++++++++++----- 5 files changed, 50 insertions(+), 21 deletions(-) diff --git a/src/paperpi/plugins/splash_screen/README.md b/src/paperpi/plugins/splash_screen/README.md index 4dc7621..afa09e3 100644 --- a/src/paperpi/plugins/splash_screen/README.md +++ b/src/paperpi/plugins/splash_screen/README.md @@ -16,7 +16,7 @@ Without a network it says "No network" instead, with no QR code. An address is never cut off: when it doesn't fit on one line, it is broken after a "/" (or, in a long host name, after a "." or "-"), drawn smaller if needed, and as a last step broken between letters (v1 split the GitHub address over two lines for the same reason). Only a very long host name without any "." or "-" (over about 40 letters) can still be too long for a small screen. -The web interface comes in milestone M5; until then the port is fixed at 8080, the port it will use. In Docker (M6), PaperPi may see the container's address instead of the Pi's; M6 deals with that. +The port is the web interface's port, `[web] port` (default 8080); a config reload that changes it gives the splash the new address at once. In Docker (M6), PaperPi may see the container's address instead of the Pi's; M6 deals with that. ## Layouts @@ -26,7 +26,7 @@ The web interface comes in milestone M5; until then the port is fixed at 8080, t ## Settings -None of its own, only the settings every plugin has (see the main [README](../../../../README.md)). It suggests a refresh every hour, which matters while no plugin is switched on and in the rotation: it then picks up a new IP address. +One of its own, `port` (default 8080): the port in the addresses, only for a splash you add to the rotation yourself; the splash at start always uses `[web] port`. Plus the settings every plugin has (see the main [README](../../../../README.md)). It suggests a refresh every hour, which matters while no plugin is switched on and in the rotation: it then picks up a new IP address. To put it in the rotation (not needed for the splash at start): diff --git a/src/paperpi/plugins/splash_screen/__init__.py b/src/paperpi/plugins/splash_screen/__init__.py index 6da9639..8e48897 100644 --- a/src/paperpi/plugins/splash_screen/__init__.py +++ b/src/paperpi/plugins/splash_screen/__init__.py @@ -17,6 +17,7 @@ import segno from epdlib import text from PIL import Image +from pydantic import Field from ... import __version__ from ...plugin import Context, Plugin, PluginSettings, ready @@ -24,9 +25,6 @@ NAME = "PaperPi" URL = "https://github.com/txoof/PaperPi" -#: The port of the web interface (docs/decisions/web-interface.md). M5 replaces this with -#: the port setting of the web interface. -WEB_PORT = 8080 NO_NETWORK = "No network" @@ -44,7 +42,12 @@ class About: class Settings(PluginSettings): - pass + port: int = Field( + 8080, + ge=1, + le=65535, + description="The web interface's port. The splash at start always uses [web] port", + ) def ip_address() -> str | None: @@ -74,15 +77,15 @@ def fetch(context: Context): return ready(About(NAME, __version__, URL, ip_address(), hostname())) -def web_addresses(about: About) -> tuple[str, str] | None: +def web_addresses(about: About, port: int) -> tuple[str, str] | None: """The web interface's address by IP and by host name; ``None`` without a network.""" if about.ip is None: return None host = about.hostname or "" if host and "." not in host: host += ".local" # the name the Pi announces on the home network - by_name = f"http://{host}:{WEB_PORT}" if host else "" - return f"http://{about.ip}:{WEB_PORT}", by_name + by_name = f"http://{host}:{port}" if host else "" + return f"http://{about.ip}:{port}", by_name def line_breaks(address: str) -> list[str]: @@ -159,7 +162,7 @@ def draw(about: About, context: Context) -> dict: prepared = layout.prepare(context.width, context.height, context.mode) short_side = min(context.width, context.height) values = {"name": about.name, "version": about.version} - addresses = web_addresses(about) + addresses = web_addresses(about, context.settings.port) texts = {"github": about.url} if addresses is None: texts["ip"] = NO_NETWORK diff --git a/src/paperpi/scheduler.py b/src/paperpi/scheduler.py index 7a16136..03ba435 100644 --- a/src/paperpi/scheduler.py +++ b/src/paperpi/scheduler.py @@ -249,7 +249,7 @@ def __init__( self._default = _Slot(_default_config(config)) self._fallback = _Slot(_fallback_config()) if config.display.fallback_clock else None """The fallback clock, shown when no plugin has anything to show.""" - self._splash = _Slot(_splash_config()) + self._splash = _Slot(_splash_config(config)) """The splash screen: at start, and while no plugin is switched on.""" self._starting = config.display.splash_time > 0 """The splash screen is still to be shown (or being shown) at start.""" @@ -410,8 +410,9 @@ def _apply(self, config: Config) -> None: default = _default_config(config) if redraw or not _same_plugin(self._default.config, default): self._default = _Slot(default) - if redraw: - new_splash = _Slot(self._splash.config) + splash = _splash_config(config) + if redraw or not _same_plugin(self._splash.config, splash): # e.g. a new [web] port + new_splash = _Slot(splash) if self._current is self._splash: self._current = new_splash # the same turn goes on with the new picture self._splash = new_splash @@ -875,13 +876,14 @@ def _fallback_config() -> PluginConfig: return PluginConfig(entry, plugin.settings(), plugin) -def _splash_config() -> PluginConfig: - """The splash screen shown at start, and while no plugin is switched on.""" +def _splash_config(config: Config) -> PluginConfig: + """The splash screen shown at start, and while no plugin is switched on. It shows the + web interface's address, with its port from ``[web] port``.""" plugin = plugins.load("splash_screen") # Its time on screen comes from [display] splash_time, not from display_time. A short # time limit: while its first picture is drawn, the screen waits for it. entry = PluginEntry(name="built-in splash", type="splash_screen", time_limit=30) - return PluginConfig(entry, plugin.settings(), plugin) + return PluginConfig(entry, plugin.settings(port=config.web.port), plugin) def _same_plugin(a: PluginConfig, b: PluginConfig) -> bool: diff --git a/tests/test_scheduler.py b/tests/test_scheduler.py index f3173a7..f50dd44 100644 --- a/tests/test_scheduler.py +++ b/tests/test_scheduler.py @@ -720,6 +720,19 @@ def test_reload_does_not_show_the_splash_again(tmp_path): assert sim.writes == [(1, "BUILT-IN-SPLASH"), (61, "A")] +def test_splash_gets_the_web_port_also_after_a_reload(tmp_path): + def ports(): + return [c.settings.port for c in sim.updates.contexts if c.storage.name == SPLASH] + + sim = Sim(tmp_path, display="splash_time = 60\n[web]\nport = 9000") # no plugins + sim.run(until=10) + assert ports() == [9000] + sim.next_config = make_config(display="splash_time = 60\n[web]\nport = 9100") + sim.at(20, sim.scheduler.reload) + sim.run(until=40) + assert ports() == [9000, 9100] # a new picture with the new address at once + + def test_reload_changing_splash_time_during_the_splash_applies(tmp_path): sim = Sim(tmp_path, rotation("a", refresh=5000), display="splash_time = 60") sim.next_config = make_config(rotation("a", refresh=5000), display="splash_time = 300") diff --git a/tests/test_splash_screen.py b/tests/test_splash_screen.py index b0d6dda..37c16b8 100644 --- a/tests/test_splash_screen.py +++ b/tests/test_splash_screen.py @@ -108,16 +108,27 @@ def test_sample_is_fixed_and_made_up(): ) def test_web_addresses(hostname, by_name): about = replace(SAMPLE, hostname=hostname) - assert web_addresses(about) == ("http://192.0.2.10:8080", by_name) + assert web_addresses(about, 8080) == ("http://192.0.2.10:8080", by_name) def test_no_web_address_without_a_network(): - assert web_addresses(replace(SAMPLE, ip=None)) is None + assert web_addresses(replace(SAMPLE, ip=None), 8080) is None -def test_the_port_is_the_web_interface_port(): - # docs/decisions/web-interface.md: plain HTTP on port 8080. - assert splash_screen.WEB_PORT == 8080 +def test_the_default_port_is_the_web_interface_default(): + from paperpi.config import WebSettings + + assert Settings().port == WebSettings().port == 8080 + + +def test_addresses_and_qr_code_use_the_port(): + ctx = replace(context(), settings=Settings(port=9000)) + values = draw(SAMPLE, ctx) + assert values["ip"].replace("\n", "") == "http://192.0.2.10:9000" + assert values["host"].replace("\n", "") == "http://paperpi.local:9000" + box = PLUGIN.layout("splash", Settings()).prepare(800, 480, ScreenMode.bw()).boxes["qr"] + expected = qr_code("http://192.0.2.10:9000", box.width, box.height) + assert values["qr"].tobytes() == expected.tobytes() def test_line_breaks_after_a_slash_come_first_most_even_first(): From 03f23c61c9ca6794a245b7ea20f2ac55550452c5 Mon Sep 17 00:00:00 2001 From: txoof-bot <337660340+txoof-bot@users.noreply.github.com> Date: Wed, 7 Oct 2026 18:17:42 +0200 Subject: [PATCH 2/3] Fixes from the review: the splash keeps the start-time port [web] port only changes at the next start (WEB_NEXT_START), so the splash keeps the port the web interface started with; the plugin's port is strict. Co-Authored-By: Claude Opus 5.5 --- src/paperpi/plugins/splash_screen/README.md | 2 +- src/paperpi/plugins/splash_screen/__init__.py | 1 + src/paperpi/scheduler.py | 15 ++++++++------- tests/test_scheduler.py | 9 +++++++-- tests/test_splash_screen.py | 7 +++++++ 5 files changed, 24 insertions(+), 10 deletions(-) diff --git a/src/paperpi/plugins/splash_screen/README.md b/src/paperpi/plugins/splash_screen/README.md index afa09e3..3189da6 100644 --- a/src/paperpi/plugins/splash_screen/README.md +++ b/src/paperpi/plugins/splash_screen/README.md @@ -16,7 +16,7 @@ Without a network it says "No network" instead, with no QR code. An address is never cut off: when it doesn't fit on one line, it is broken after a "/" (or, in a long host name, after a "." or "-"), drawn smaller if needed, and as a last step broken between letters (v1 split the GitHub address over two lines for the same reason). Only a very long host name without any "." or "-" (over about 40 letters) can still be too long for a small screen. -The port is the web interface's port, `[web] port` (default 8080); a config reload that changes it gives the splash the new address at once. In Docker (M6), PaperPi may see the container's address instead of the Pi's; M6 deals with that. +The port is the web interface's port, `[web] port` (default 8080); like the web interface itself, a change applies at the next start of PaperPi. In Docker (M6), PaperPi may see the container's address instead of the Pi's; M6 deals with that. ## Layouts diff --git a/src/paperpi/plugins/splash_screen/__init__.py b/src/paperpi/plugins/splash_screen/__init__.py index 8e48897..f2c4ed1 100644 --- a/src/paperpi/plugins/splash_screen/__init__.py +++ b/src/paperpi/plugins/splash_screen/__init__.py @@ -46,6 +46,7 @@ class Settings(PluginSettings): 8080, ge=1, le=65535, + strict=True, description="The web interface's port. The splash at start always uses [web] port", ) diff --git a/src/paperpi/scheduler.py b/src/paperpi/scheduler.py index 03ba435..eadeb69 100644 --- a/src/paperpi/scheduler.py +++ b/src/paperpi/scheduler.py @@ -249,7 +249,9 @@ def __init__( self._default = _Slot(_default_config(config)) self._fallback = _Slot(_fallback_config()) if config.display.fallback_clock else None """The fallback clock, shown when no plugin has anything to show.""" - self._splash = _Slot(_splash_config(config)) + self._web_port = config.web.port + """The web interface's port: it only changes at the next start (WEB_NEXT_START).""" + self._splash = _Slot(_splash_config(self._web_port)) """The splash screen: at start, and while no plugin is switched on.""" self._starting = config.display.splash_time > 0 """The splash screen is still to be shown (or being shown) at start.""" @@ -410,9 +412,8 @@ def _apply(self, config: Config) -> None: default = _default_config(config) if redraw or not _same_plugin(self._default.config, default): self._default = _Slot(default) - splash = _splash_config(config) - if redraw or not _same_plugin(self._splash.config, splash): # e.g. a new [web] port - new_splash = _Slot(splash) + if redraw: + new_splash = _Slot(self._splash.config) if self._current is self._splash: self._current = new_splash # the same turn goes on with the new picture self._splash = new_splash @@ -876,14 +877,14 @@ def _fallback_config() -> PluginConfig: return PluginConfig(entry, plugin.settings(), plugin) -def _splash_config(config: Config) -> PluginConfig: +def _splash_config(web_port: int) -> PluginConfig: """The splash screen shown at start, and while no plugin is switched on. It shows the - web interface's address, with its port from ``[web] port``.""" + web interface's address, with the port the web interface started with.""" plugin = plugins.load("splash_screen") # Its time on screen comes from [display] splash_time, not from display_time. A short # time limit: while its first picture is drawn, the screen waits for it. entry = PluginEntry(name="built-in splash", type="splash_screen", time_limit=30) - return PluginConfig(entry, plugin.settings(port=config.web.port), plugin) + return PluginConfig(entry, plugin.settings(port=web_port), plugin) def _same_plugin(a: PluginConfig, b: PluginConfig) -> bool: diff --git a/tests/test_scheduler.py b/tests/test_scheduler.py index f50dd44..eb46852 100644 --- a/tests/test_scheduler.py +++ b/tests/test_scheduler.py @@ -720,7 +720,7 @@ def test_reload_does_not_show_the_splash_again(tmp_path): assert sim.writes == [(1, "BUILT-IN-SPLASH"), (61, "A")] -def test_splash_gets_the_web_port_also_after_a_reload(tmp_path): +def test_splash_keeps_the_web_port_until_the_next_start(tmp_path): def ports(): return [c.settings.port for c in sim.updates.contexts if c.storage.name == SPLASH] @@ -730,7 +730,12 @@ def ports(): sim.next_config = make_config(display="splash_time = 60\n[web]\nport = 9100") sim.at(20, sim.scheduler.reload) sim.run(until=40) - assert ports() == [9000, 9100] # a new picture with the new address at once + # The web interface keeps its port until the next start, so the splash does too. + sim.plan(**{SPLASH: lambda t: f"splash {int(t)}"}) + sim.next_config = make_config(display="splash_time = 60\nrotation = 90\n[web]\nport = 9100") + sim.at(50, sim.scheduler.reload) + sim.run(until=70) + assert ports() == [9000, 9000] # drawn again (turned), still with the old port def test_reload_changing_splash_time_during_the_splash_applies(tmp_path): diff --git a/tests/test_splash_screen.py b/tests/test_splash_screen.py index 37c16b8..268b171 100644 --- a/tests/test_splash_screen.py +++ b/tests/test_splash_screen.py @@ -6,6 +6,7 @@ import pytest import segno from epdlib import ScreenMode, text +from pydantic import ValidationError from paperpi import __version__ from paperpi.plugin import Context, State, draw_update @@ -121,6 +122,12 @@ def test_the_default_port_is_the_web_interface_default(): assert Settings().port == WebSettings().port == 8080 +@pytest.mark.parametrize("port", [0, 65536, "9000"]) +def test_port_must_be_a_real_port_number(port): + with pytest.raises(ValidationError): + Settings(port=port) + + def test_addresses_and_qr_code_use_the_port(): ctx = replace(context(), settings=Settings(port=9000)) values = draw(SAMPLE, ctx) From 5f373c4fd79a355110900794cdc2998633fe92bd Mon Sep 17 00:00:00 2001 From: txoof-bot <337660340+txoof-bot@users.noreply.github.com> Date: Wed, 7 Oct 2026 20:08:48 +0200 Subject: [PATCH 3/3] Splash shows only the IP address, not the host name The web interface only accepts IP addresses, so the http://.local line led to an error page. txoof chose to drop it (keep it simple). Co-Authored-By: Claude Opus 5.5 --- src/paperpi/plugins/splash_screen/README.md | 8 +-- src/paperpi/plugins/splash_screen/__init__.py | 49 +++++----------- src/paperpi/plugins/splash_screen/layouts.py | 19 ++---- tests/images/splash_screen-splash-5in65.png | Bin 5367 -> 5617 bytes tests/images/splash_screen-splash-7in5.png | Bin 2943 -> 3390 bytes tests/images/splash_screen-splash-9in7.png | Bin 15851 -> 16824 bytes tests/test_splash_screen.py | 54 +++--------------- 7 files changed, 34 insertions(+), 96 deletions(-) diff --git a/src/paperpi/plugins/splash_screen/README.md b/src/paperpi/plugins/splash_screen/README.md index 3189da6..bf51cf8 100644 --- a/src/paperpi/plugins/splash_screen/README.md +++ b/src/paperpi/plugins/splash_screen/README.md @@ -8,13 +8,13 @@ What it shows: - the name, in large letters; - the version of the PaperPi that is running, for example `2.0.0`; -- the web interface's address in two forms, each on its own line (or two, see below): by IP address (`http://192.0.2.10:8080`) and by host name (`http://paperpi.local:8080`). Some phones can't open `.local` names; the IP address always works, but can change when the router gives the Pi a new one; -- a QR code with the IP address, to open the web interface on a phone; +- the web interface's address by IP address, e.g. `http://192.0.2.10:8080`. Not by name (`paperpi.local`): the web interface only accepts IP addresses (see the main README). The address can change when the router gives the Pi a new one; +- a QR code with the same address, to open the web interface on a phone; - PaperPi's GitHub address. Without a network it says "No network" instead, with no QR code. -An address is never cut off: when it doesn't fit on one line, it is broken after a "/" (or, in a long host name, after a "." or "-"), drawn smaller if needed, and as a last step broken between letters (v1 split the GitHub address over two lines for the same reason). Only a very long host name without any "." or "-" (over about 40 letters) can still be too long for a small screen. +An address is never cut off: when it doesn't fit on one line, it is broken after a "/" (or after a "." or ":"), drawn smaller if needed, and as a last step broken between letters (v1 split the GitHub address over two lines for the same reason). The port is the web interface's port, `[web] port` (default 8080); like the web interface itself, a change applies at the next start of PaperPi. In Docker (M6), PaperPi may see the container's address instead of the Pi's; M6 deals with that. @@ -42,7 +42,7 @@ The name is in [Anton](https://fonts.google.com/specimen/Anton) by The Anton Pro ## Sample images -The sample images show version 2.0.0, the made-up address 192.0.2.10 and the host name `paperpi`. All sample images are in [`tests/images/`](../../../../tests/images/), named `splash_screen-splash-.png`, where `` is `9in7` (9.7", 1200x825, 16 grays), `7in5` (7.5", 800x480, black and white) or `5in65` (5.65", 600x448, 7 colours). +The sample images show version 2.0.0, and the made-up address 192.0.2.10. All sample images are in [`tests/images/`](../../../../tests/images/), named `splash_screen-splash-.png`, where `` is `9in7` (9.7", 1200x825, 16 grays), `7in5` (7.5", 800x480, black and white) or `5in65` (5.65", 600x448, 7 colours). | 9.7" | 7.5" black and white | 5.65" 7 colours | |---|---|---| diff --git a/src/paperpi/plugins/splash_screen/__init__.py b/src/paperpi/plugins/splash_screen/__init__.py index f2c4ed1..1584a50 100644 --- a/src/paperpi/plugins/splash_screen/__init__.py +++ b/src/paperpi/plugins/splash_screen/__init__.py @@ -7,8 +7,9 @@ It is a plugin like any other so start-up needs no special drawing code; a ``[[plugin]]`` block can also put it in the rotation, but that is not what it is meant for. -It shows the address of PaperPi's web interface, as an IP address and as a host name, and -a QR code with the IP address, so a phone can open the web interface at once. +It shows the address of PaperPi's web interface by IP address, and a QR code with that +address, so a phone can open the web interface at once. (Not by host name: the web +interface only accepts IP addresses, see docs/decisions/web-interface.md.) """ import socket @@ -37,8 +38,6 @@ class About: url: str ip: str | None """This Pi's address on the network; ``None`` without a network.""" - hostname: str | None - """This Pi's name on the network (without ``.local``); ``None`` when unknown.""" class Settings(PluginSettings): @@ -66,32 +65,18 @@ def ip_address() -> str | None: return None if address.startswith(("0.", "127.")) else address -def hostname() -> str | None: - """This Pi's name, e.g. ``paperpi``; ``None`` when the system doesn't say.""" - try: - return socket.gethostname() or None - except OSError: - return None - - def fetch(context: Context): - return ready(About(NAME, __version__, URL, ip_address(), hostname())) + return ready(About(NAME, __version__, URL, ip_address())) -def web_addresses(about: About, port: int) -> tuple[str, str] | None: - """The web interface's address by IP and by host name; ``None`` without a network.""" - if about.ip is None: - return None - host = about.hostname or "" - if host and "." not in host: - host += ".local" # the name the Pi announces on the home network - by_name = f"http://{host}:{port}" if host else "" - return f"http://{about.ip}:{port}", by_name +def web_address(about: About, port: int) -> str | None: + """The web interface's address; ``None`` without a network.""" + return None if about.ip is None else f"http://{about.ip}:{port}" def line_breaks(address: str) -> list[str]: - """Ways to write ``address``: on one line, then broken after a "/", then (for a long - host name) also after a ".", "-" or ":". + """Ways to write ``address``: on one line, then broken after a "/", then also after a + ".", "-" or ":". epdlib only breaks lines at spaces (or, in a word that is too long, between letters), so these breaks are put in here. Within each kind the most even split comes first, e.g. @@ -163,15 +148,11 @@ def draw(about: About, context: Context) -> dict: prepared = layout.prepare(context.width, context.height, context.mode) short_side = min(context.width, context.height) values = {"name": about.name, "version": about.version} - addresses = web_addresses(about, context.settings.port) - texts = {"github": about.url} - if addresses is None: - texts["ip"] = NO_NETWORK - else: - texts["ip"], texts["host"] = addresses - box = prepared.boxes["qr"] - if box.width > 0 and box.height > 0: - values["qr"] = qr_code(addresses[0], box.width, box.height) + address = web_address(about, context.settings.port) + texts = {"github": about.url, "ip": address or NO_NETWORK} + box = prepared.boxes["qr"] + if address and box.width > 0 and box.height > 0: + values["qr"] = qr_code(address, box.width, box.height) edge = round(PADDING * short_side) for name in ADDRESS_BLOCKS: value = texts.get(name, "") @@ -197,7 +178,7 @@ def draw(about: About, context: Context) -> dict: draw=draw, # A fixed version and made-up addresses, so the sample images never change by # themselves and never show a real Pi's address. - sample=About(NAME, "2.0.0", URL, "192.0.2.10", "paperpi"), + sample=About(NAME, "2.0.0", URL, "192.0.2.10"), # Nothing changes while PaperPi runs, except perhaps the IP address. refresh=3600, ) diff --git a/src/paperpi/plugins/splash_screen/layouts.py b/src/paperpi/plugins/splash_screen/layouts.py index 01ec129..e056a19 100644 --- a/src/paperpi/plugins/splash_screen/layouts.py +++ b/src/paperpi/plugins/splash_screen/layouts.py @@ -1,8 +1,8 @@ """Layouts for splash_screen. The first one is the default. As in v1, the name is in Anton and the rest in Dosis SemiBold. Below the name and version: -the web interface's address (by IP address and by host name) next to a QR code with the -IP address, and PaperPi's GitHub address at the bottom. +the web interface's address (by IP address) next to a QR code with the same address, and +PaperPi's GitHub address at the bottom. """ from ... import fonts @@ -10,10 +10,10 @@ PADDING = 0.01 #: The text blocks that hold an address; ``draw`` breaks these after a "/" when needed. -ADDRESS_BLOCKS = ("ip", "host", "github") +ADDRESS_BLOCKS = ("ip", "github") -# One sample for both web addresses, so they get the same font size. -WEB_SAMPLE = "http://paperpi-kitchen.local:8080" +# The longest web address: the longest IP address and port. +WEB_SAMPLE = "http://255.255.255.255:65535" def _text(name: str, size: float, font: str, sample: str, max_lines: int = 1) -> dict: @@ -37,14 +37,7 @@ def _text(name: str, size: float, font: str, sample: str, max_lines: int = 1) -> _text("version", 1, fonts.DOSIS_SEMIBOLD, "88.88.88.dev88"), { "row": [ - { - "column": [ - _text("ip", 1, fonts.DOSIS_SEMIBOLD, WEB_SAMPLE, 2), - _text("host", 1, fonts.DOSIS_SEMIBOLD, WEB_SAMPLE, 2), - ], - "size": 3, - "gap": 0, - }, + _text("ip", 3, fonts.DOSIS_SEMIBOLD, WEB_SAMPLE, 2), # Drawn at its exact size in draw(), so it is never scaled again. {"name": "qr", "type": "image", "size": 1, "fit": "none"}, ], diff --git a/tests/images/splash_screen-splash-5in65.png b/tests/images/splash_screen-splash-5in65.png index d0065626692c46eb1c07c789b804caa449890f2f..662f4a1d4e3b57a127ab520eb53a2a347ad97546 100644 GIT binary patch literal 5617 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 IMPH8m51#=QJ^%m! literal 5367 zcmc(jc|6o@_s0i=QE0|4Stb#Rx{+-x*+Mebl0C|j?E8|4k;qmUyD=o)ktt*!ifUvH zAv;ZmF^n*bvHo=T+}%C*{rm5Ey`Jaa>vesv>wM33&iixDi8q1ku^l>k2mk=E8R+Yn z0RZ$@0RXz!Ao_!qm65wj0039BfsUp{NZ!)K7_zvAhtXs^ac|#&psM5xW4bW`j#it~ zzEH}lZxYMHL9dbN911vZu@7sc2l$9i zP(=Q}v>~#m0`OW)4b{64nCVQs%6XIs`I`ZkklFg3Sn4ryuRj#dd=G-S+hoII1W*zT zk&{2#w?xEtkSCqqVDcmZg-TZT^p|ziPPDvBnuF2}Xne@Cl6|#W$J94hbPD z;5t0?#8DZkpC7E8Yr#PyXyciN7FjkNh=41>j1w<4J?atrQYx${nDE>%*%bWzt)Z0T>^;6N5!Tfq;!YupE!&dsyB286z_@4RbM-w(GU+rQ9 z`C;rL1mThUcx6l0_4x+Z;6}M!;}BHsL2-b0bgI+QShCk`eeb4Qyx6(D$1xU_TE?z+`&$hD8ov1J~&dL+{dThXD#%0;P zb=1D(dcIGL-Lw!8DrzfcSK+m)Ek(;p6KSf?&ee_1I=P?6?jdW%)YI25(aR~VVfXYI zLJTy6iLv$+gk)ulAZ+>=V$6Pqcq{C%tRRD*lHh0jO8l6LQ}jJT)B{q%0`$Md)Wzc<{+dzTf1|yq5fVsa~($ZS_c2g2)Xpjk(JJy!w3keCiG? zx>zOAw+{q_#(gwnu!Mq$mw@!c$3e7|g}Wj&+%6w4TPvP(M~rdC=+Jw#o<1i8WScJp z-G5rQmk~XlLB1vtor=RJa@itvZQAm7oG)B=OYf6>n&IfOr_R8EKo91xnWfsS;`osw ztPAP>_SypvL`O35{COgYCA$a=nce+XR#J`cpg%9L9!4A8c+Eni}htv zdKqd@{F&KS#XgDzc}rjCA(CpjJc-$Kd00=FukKnYejwwBsG`4?8Bu?aU;*&Ln z9jO^zuwU|6Z7VlSX*FwnZ&O=cQ~%|L0%ha5&pLauvTUu!o{0|Fthhuhzn+_HR{K8b zbf@1uG>5j?CJBUQEQ}7&Y-Tq2NSBjD3G+RXJC3SnCZ5<6sY13T+!fpEaiSX)or;FG zvoG4L3g8@J2Gzr&`(4&f*Q*MMZ0zbWveceXHX0BbJfH@;8}6U-AJKM7?*Q0g|078M zGdlhfifRp4OMuKG?e?9$<;v58B%bG!l``LEi92^yNb+H{Q25gG5K6;qaqVIgQHXwg z3Ves_bFm)iox14U8tdpbjg>?nP644OEf=_AKU$IK9e~ipHFCpc9m~#!^U)#*ry>yf zEuPavo2S)t9-^gq;mk||5K1wnQd-w&<#!g*&UVvAU@c~$Yf&V>6mU*;lk;;Ynmvi$ zI~u$|1hum99gV$c1}1BQwb&#_J_EJ5Nc1hJjo$+0@Xz&Faq_bSL-=lJ8yK8CQPnkvJ z|4OF6-16Vj&;RdfmU>6H0d>#k)|8(VI$Lqf@4Jqfg zd%+HyCR%gv=3B5f+*k;g2hOma3u2bDoI2d2!&zT{vB6+>e07rl>L-cJA*dXq%|cCW zeqQIpE=2K>8O|KNGNqcG!cUrP0AcxRox39y}*rDDei*#7wA66?a z1v3lB1}AcqjkhHQvxR_Kb0r&otFQl7d0KF6m-@t|&eYm8u33*ED=i#x?AASzY&84X ze&5J7Ttw_ZJaDGsEw-dcOi^Pur>el=3jEx&#u!nyZI{Xqq;DarNaxVO6Th`N?;W3R z0M684n7$OU_+h#$I>Qz(4d&9mlrh+a9V7D|i@ph79qN2z$M=vYh`I zyKO);ZtU_5WhJr)EgDppPnL1|>$pnz=jvjDZ{D7ayaDks4MU7c6@)bESP2fkN=#DZ zQ@~M~hN0MzhK71b! zAwJ4sh_#L;CJpCGx4s3ruG#`Ld+we0#@i*kQOBqIDIo8nu+h#w7yUy1g>M5<*4_1Z z9~I3IPpo)8!5i%?DkuL|?h{>B0=h5s^3*1=?ptXBXmPbD>Vn)SC@yM5LcFPqQ0JiM zq&0+|tx!^aghThuk0S~@Q}GeF@l6!w~GAM_QDc# z>SZnoWqGXZy~>FzG+G)q$yI0&`8_Z)UEHc;0$6ug<996WBiKFj2L;Q=T;P$uYuPou z%1O$Z^zqF2XQb4tB~QIj=V&y2Hz(NOEc+??3f+_!_9}z2a^7FBvo1+n!WHCsOdGoW zbMZ2PWlGOVX4xg^D-2U!eAwbI$968Pmxe!>)$oZ(z!VG&_X^bRjEA0FpRDqV7b7j0 zzYM!G!6AJ4*kJ#GNpFnXn2-a~y;i6vr$UccwfFRq-UMB9X&c=#D5{trz^B)g-MjD$ zVW^0Q)m_I zYt!WLR~?xYkB3F|CvZk3L?yu|^J*}SpRe}key?*@goqZ35T-M92~KF{lJ3f6+4mDK z=U7tNQ-HzSZiWV5GSqaHZVDPv_@tJF>lcgUsy3L@U z$a&(5q+PQm#0UA23PSBa+YCs27{frtZ%sQ#97_<|$axG>N*0<)3}9}&iWF;oLmS8S zK0pTudtrCx^c?bQ4j_GiwWa{(u~?PUBuj5DEazjV4ABJ8Y#l~kN@~82+Ecw#F>vMq zu}&{#TD#c4Ul7P=0PZPq^V3Vs76HU@926`;bK!`1*oLm5*PX3%w^Uoo^W3Y0nNK~- zq}{)|o;=qiv^V$O&~Ul`CV8`CXPrJNF#Ps-pkbHKP5DBQQlDqcHxiecTf*qADpI8o ztTu=_#SwYM8@m<1^*~CmQz?sAP3=iFc8cbeoBC+3k#GA#6K;N{8{A zn1m?K2|3V7N|k1wRXC<3U}{eJ3MogPokM&_wTIYJ^!A-5rZnY%SNMq_w*aQ>>zb}= zu(}F+jDRxEv%Z2d5}|oeI=;Eq4fDH zdT`hm7y8vaGP(++(#n%4tsC zOh$Ipr(rF!ofBm=!Z#NuMbac=n!_4Ox3m^1C+v70;>ad|)!*l7P-Q@xf2;=1;2+** zUz{sV*(F@}|A6Z4-7R#Zy=c=2e1trovw35Y03oZ@|WoclS|y52`ce z3_a6fHTfL)tFEqFs;HD^SXNbsr9l3Oit80Z;Rcyv@Q9nYFAzs7Ma30STzuYj<2Q~^ z>!4%4t&o5{s@P%AFO#8nI%p_!Y`3#|+}^WV+m*FTw73ZW=UuUiDVK>}Pe`^FCEf3& zl6~IwrpVCu$Zi+o1{OB2Z9|gFChKA9G{?9z(nm^#YiIH988T1XEuSYSTyD@KL+Dv$ zJy!K`S*q}cE4qyzug=YD7XyGNe`VhRB9i@vCV4#qAe=|z5Pyh2s5h{Iv(sBqjI%>jFH=QOo2Nt;o#T}M6l-G+Fj1ml!& z_qcp;`PW;AJ1afB_Gd{0&HcJ<`q8N{8^zf;1u{cG`99&vq{i)C{Q4AGGI0JqI$Z>T z!*9`r1}1Bq-0}}%1bM2Rr_$1>EvVi(QVZ&EOE^ca=lrb*3UZ-Qf|dF#mwH`w`s>3 zD7v$*xFR=#dwwtaQ?i&iQwXOztWZ}Vj*|%SyS$99SvR=>5B9l~^0?ZyMzR$4*~|$Z zOpEA#Xuld^x{91U-E|)9$EMMEG>{4+Sub>Vlt*r^f1V=f*%f_xB}!bPO0CSLUGy(d(t&y*NIf%@vI>i#_k!-$tW!&7$G@e|w;r=b&jI2;91sjWn z4y+@N`nF%rs_bI4Fb*6U8@;a4+dfRC(!2J3?*=sWl^m`IZ^?u69XXG` diff --git a/tests/images/splash_screen-splash-7in5.png b/tests/images/splash_screen-splash-7in5.png index 1168faa485a23bd5de1b2a33de6bf95b6ac760b0..8f11c308591acb38a7856e2029791728ecd19682 100644 GIT binary patch literal 3390 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 AVgLXD delta 2937 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 O462G+3gyo&-~3;OdvqEA literal 15851 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 z�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 XE%d*iE7e}S#jT~Lr&^_K^X7j5S1gFC diff --git a/tests/test_splash_screen.py b/tests/test_splash_screen.py index 268b171..6f314b5 100644 --- a/tests/test_splash_screen.py +++ b/tests/test_splash_screen.py @@ -23,7 +23,7 @@ fit_address, line_breaks, qr_code, - web_addresses, + web_address, ) from paperpi.plugins.splash_screen.layouts import ADDRESS_BLOCKS, PADDING @@ -38,10 +38,9 @@ def context(width=800, height=480, mode=None): def test_fetch_shows_this_paperpi(monkeypatch): monkeypatch.setattr(splash_screen, "ip_address", lambda: "192.0.2.20") - monkeypatch.setattr(splash_screen, "hostname", lambda: "kitchen") fetched = fetch(context()) assert fetched.state is State.READY - assert fetched.data == About(NAME, __version__, URL, "192.0.2.20", "kitchen") + assert fetched.data == About(NAME, __version__, URL, "192.0.2.20") class FakeSocket: @@ -79,41 +78,18 @@ def test_ip_address(monkeypatch, fake, expected): assert splash_screen.ip_address() == expected -@pytest.mark.parametrize(("name", "expected"), [("kitchen", "kitchen"), ("", None)]) -def test_hostname(monkeypatch, name, expected): - monkeypatch.setattr(splash_screen.socket, "gethostname", lambda: name) - assert splash_screen.hostname() == expected - - -def test_hostname_error(monkeypatch): - def broken(): - raise OSError("no name") - - monkeypatch.setattr(splash_screen.socket, "gethostname", broken) - assert splash_screen.hostname() is None - - def test_sample_is_fixed_and_made_up(): # A fixed version keeps the sample images the same from one release to the next, and # 192.0.2.x is set aside for examples, so no real Pi's address is ever shown. - assert SAMPLE == About(NAME, "2.0.0", URL, "192.0.2.10", "paperpi") + assert SAMPLE == About(NAME, "2.0.0", URL, "192.0.2.10") -@pytest.mark.parametrize( - ("hostname", "by_name"), - [ - ("paperpi", "http://paperpi.local:8080"), - ("pi.example.org", "http://pi.example.org:8080"), # a name with a dot stays as it is - (None, ""), - ], -) -def test_web_addresses(hostname, by_name): - about = replace(SAMPLE, hostname=hostname) - assert web_addresses(about, 8080) == ("http://192.0.2.10:8080", by_name) +def test_web_address(): + assert web_address(SAMPLE, 8080) == "http://192.0.2.10:8080" def test_no_web_address_without_a_network(): - assert web_addresses(replace(SAMPLE, ip=None), 8080) is None + assert web_address(replace(SAMPLE, ip=None), 8080) is None def test_the_default_port_is_the_web_interface_default(): @@ -132,7 +108,6 @@ def test_addresses_and_qr_code_use_the_port(): ctx = replace(context(), settings=Settings(port=9000)) values = draw(SAMPLE, ctx) assert values["ip"].replace("\n", "") == "http://192.0.2.10:9000" - assert values["host"].replace("\n", "") == "http://paperpi.local:9000" box = PLUGIN.layout("splash", Settings()).prepare(800, 480, ScreenMode.bw()).boxes["qr"] expected = qr_code("http://192.0.2.10:9000", box.width, box.height) assert values["qr"].tobytes() == expected.tobytes() @@ -195,14 +170,6 @@ def test_fit_address_falls_back_to_one_line(): assert fit("http://192.0.2.10:8080", 40, 30, 40) == "http://192.0.2.10:8080" -def test_long_host_name_is_broken_after_a_dash_not_inside_a_word(): - values = draw(replace(SAMPLE, hostname="paperpi-living-room-kitchen-shelf"), context(480, 800)) - host = values["host"] - assert host.replace("\n", "") == "http://paperpi-living-room-kitchen-shelf.local:8080" - assert "\n" in host - assert all(line[-1] in "/.-:" for line in host.split("\n")[:-1]) - - def test_qr_code_holds_the_address_with_whole_pixels_per_square(): image = qr_code("http://192.0.2.10:8080", 200, 150) side = segno.make("http://192.0.2.10:8080", error="m").symbol_size(border=2)[0] @@ -222,10 +189,9 @@ def test_draw_fills_every_block_of_the_layout(): assert values["name"] == "PaperPi" assert values["version"] == "2.0.0" assert values["ip"].replace("\n", "") == "http://192.0.2.10:8080" - assert values["host"].replace("\n", "") == "http://paperpi.local:8080" assert values["github"].replace("\n", "") == URL box = PLUGIN.layout("splash", Settings()).prepare(800, 480, ScreenMode.bw()).boxes["qr"] - # The QR code holds the IP address, never the host name (some phones can't open it). + # The QR code holds the same address as the text. expected = qr_code("http://192.0.2.10:8080", box.width, box.height) assert values["qr"].tobytes() == expected.tobytes() @@ -233,7 +199,6 @@ def test_draw_fills_every_block_of_the_layout(): def test_draw_without_a_network(): values = draw(replace(SAMPLE, ip=None), context()) assert values["ip"] == NO_NETWORK - assert values["host"] == "" assert "qr" not in values @@ -266,10 +231,9 @@ def _complete(values, width, height): "about", [ SAMPLE, - replace(SAMPLE, ip="255.255.255.255", hostname="paperpi-living-room"), - replace(SAMPLE, hostname="paperpi-living-room-kitchen"), + replace(SAMPLE, ip="255.255.255.255"), ], - ids=["sample", "long-names", "longer-name"], + ids=["sample", "longest-ip"], ) def test_addresses_always_appear_whole(about, width, height): values = draw(about, context(width, height))