From 54740a56ee6534b4ed7d4fbd19e55017fa976fce Mon Sep 17 00:00:00 2001 From: Pavel Kirienko Date: Fri, 27 Mar 2026 02:20:47 +0200 Subject: [PATCH] Remove config file, accept target endpoint directly The --target flag now takes an IP address or hostname directly instead of a named alias from a TOML config file. For friendly names, use mDNS (e.g., my-display.local). Co-Authored-By: Claude Opus 4.6 (1M context) --- README.md | 32 ++++------- pyproject.toml | 3 +- src/matrix_display/cli.py | 12 +--- src/matrix_display/config.py | 105 ----------------------------------- tests/test_cli.py | 100 +++++++++------------------------ tests/test_config.py | 93 ------------------------------- 6 files changed, 39 insertions(+), 306 deletions(-) delete mode 100644 src/matrix_display/config.py delete mode 100644 tests/test_config.py diff --git a/README.md b/README.md index f114bce..39fee16 100644 --- a/README.md +++ b/README.md @@ -40,22 +40,10 @@ ln -s ~/matrix_display/matrix_display ~/.local/bin/matrix_display Make sure `~/.local/bin` is on your `PATH`. -3. Create a `~/.matrix_display` config file: - -```toml -[[display]] -target_display = "maksim" -controller_ip = "192.168.1.201" - -[[display]] -target_display = "meeting_room" -controller_ip = "192.168.1.202" -``` - -4. Send a message: +3. Send a message: ```sh -echo "Some message" | matrix_display --target maksim +echo "Some message" | matrix_display --target 192.168.1.201 ``` ## Usage @@ -73,17 +61,17 @@ If you prefer not to install anything, this also works: PYTHONPATH=src python3 -m matrix_display ``` -Use `--target` or `-t` to select which configured display to send to: +Use `--target` or `-t` to specify the controller IP or hostname: ```sh -some_command | matrix_display --target maksim -some_command | matrix_display -t maksim +some_command | matrix_display --target 192.168.1.201 +some_command | matrix_display -t my-display.local ``` ANSI color sequences in stdin are supported. For example: ```sh -printf '\033[31mRED \033[32mGREEN\033[0m\n' | matrix_display -t maksim +printf '\033[31mRED \033[32mGREEN\033[0m\n' | matrix_display -t 192.168.1.201 ``` ## Example Usage @@ -94,7 +82,7 @@ Display the current time in 24-hour format once a second: #!/bin/sh while true; do - date '+%H:%M:%S' | matrix_display -t maksim + date '+%H:%M:%S' | matrix_display -t 192.168.1.201 sleep 1 done ``` @@ -105,9 +93,9 @@ Watch the latest GitHub Actions run and display a green or red result when it fi #!/usr/bin/env bash if gh run watch --exit-status; then - printf '\033[32mPASSED\033[0m\n' | matrix_display -t maksim + printf '\033[32mPASSED\033[0m\n' | matrix_display -t 192.168.1.201 else - printf '\033[31mFAILED\033[0m\n' | matrix_display -t maksim + printf '\033[31mFAILED\033[0m\n' | matrix_display -t 192.168.1.201 fi ``` @@ -120,7 +108,7 @@ the scroll has finished: text=$(printf 'ZUBAX %.0s' $(seq 1 100)) while true; do - printf '\033[31m%s\033[0m\n' "$text" | matrix_display -t maksim + printf '\033[31m%s\033[0m\n' "$text" | matrix_display -t 192.168.1.201 sleep 2m done ``` diff --git a/pyproject.toml b/pyproject.toml index bcf1c38..e6fd82a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,8 @@ name = "matrix-display" version = "0.1.0" description = "Art-Net controller for a 10x118 LED matrix display" readme = "README.md" -requires-python = ">=3.12" +requires-python = ">=3.10" +dependencies = [] [project.scripts] matrix_display = "matrix_display.cli:main" diff --git a/src/matrix_display/cli.py b/src/matrix_display/cli.py index 35ab8be..0e31b23 100644 --- a/src/matrix_display/cli.py +++ b/src/matrix_display/cli.py @@ -6,10 +6,8 @@ import math import sys import time -from pathlib import Path from typing import Callable, TextIO -from .config import DEFAULT_CONFIG_PATH, resolve_controller_ip from .led_controller import LedController from .rendering import normalize_input_text, render_message @@ -30,7 +28,7 @@ def main( try: message = _read_message(stdin or sys.stdin) - controller_ip = resolve_controller_ip(args.target, config_path=args.config) + controller_ip = args.target rendered = render_message(message) controller = controller_factory(target_ip=controller_ip) frame_interval = 1 / getattr(controller, "fps", 30) @@ -64,13 +62,7 @@ def _build_parser() -> argparse.ArgumentParser: "-t", "--target", required=True, - help="Named target display from ~/.matrix_display.", - ) - parser.add_argument( - "--config", - type=Path, - default=DEFAULT_CONFIG_PATH, - help=f"Path to the TOML config file (default: {DEFAULT_CONFIG_PATH}).", + help="IP address or hostname of the PixLite controller.", ) return parser diff --git a/src/matrix_display/config.py b/src/matrix_display/config.py deleted file mode 100644 index ac479b0..0000000 --- a/src/matrix_display/config.py +++ /dev/null @@ -1,105 +0,0 @@ -"""Configuration loading for the matrix_display CLI.""" - -from __future__ import annotations - -import tomllib -from dataclasses import dataclass -from pathlib import Path - -DEFAULT_CONFIG_PATH = Path.home() / ".matrix_display" - - -@dataclass(frozen=True, slots=True) -class DisplayTarget: - """A named matrix display target.""" - - target_display: str - controller_ip: str - - -@dataclass(frozen=True, slots=True) -class MatrixDisplayConfig: - """User-provided CLI configuration.""" - - displays: tuple[DisplayTarget, ...] = () - - -def load_config(path: Path | None = None) -> MatrixDisplayConfig: - """Load matrix display configuration from TOML.""" - config_path = path or DEFAULT_CONFIG_PATH - if not config_path.exists(): - return MatrixDisplayConfig() - - with config_path.open("rb") as handle: - data = tomllib.load(handle) - - if not isinstance(data, dict): - raise ValueError(f"config at {config_path} must contain a TOML table") - - unknown_keys = sorted(set(data) - {"display"}) - if unknown_keys: - formatted = ", ".join(unknown_keys) - raise ValueError(f"unsupported config keys in {config_path}: {formatted}") - - raw_displays = data.get("display", []) - if not isinstance(raw_displays, list): - raise ValueError(f"display in {config_path} must be an array of tables") - - displays = tuple( - _parse_display_target(entry, config_path) for entry in raw_displays - ) - _validate_unique_target_names(displays, config_path) - return MatrixDisplayConfig(displays=displays) - - -def resolve_controller_ip(target_display: str, config_path: Path | None = None) -> str: - """Resolve the target controller IP from the named display configuration.""" - config = load_config(config_path) - for display in config.displays: - if display.target_display == target_display: - return display.controller_ip - - config_file = config_path or DEFAULT_CONFIG_PATH - raise ValueError( - f"target display {target_display!r} was not found in {config_file}" - ) - - -def _parse_display_target(entry: object, config_path: Path) -> DisplayTarget: - if not isinstance(entry, dict): - raise ValueError(f"each display entry in {config_path} must be a TOML table") - - unknown_keys = sorted(set(entry) - {"target_display", "controller_ip"}) - if unknown_keys: - formatted = ", ".join(unknown_keys) - raise ValueError(f"unsupported display keys in {config_path}: {formatted}") - - target_display = entry.get("target_display") - controller_ip = entry.get("controller_ip") - if not isinstance(target_display, str) or not target_display: - raise ValueError(f"target_display in {config_path} must be a non-empty string") - if not isinstance(controller_ip, str) or not controller_ip: - raise ValueError(f"controller_ip in {config_path} must be a non-empty string") - - return DisplayTarget( - target_display=target_display, - controller_ip=controller_ip, - ) - - -def _validate_unique_target_names( - displays: tuple[DisplayTarget, ...], config_path: Path -) -> None: - seen: set[str] = set() - duplicates: set[str] = set() - for display in displays: - if display.target_display in seen: - duplicates.add(display.target_display) - continue - seen.add(display.target_display) - - if duplicates: - formatted = ", ".join(sorted(duplicates)) - raise ValueError( - f"duplicate target_display values in {config_path}: {formatted}" - ) diff --git a/tests/test_cli.py b/tests/test_cli.py index 5e6375a..83ed868 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -3,9 +3,7 @@ from __future__ import annotations import io -import tempfile import unittest -from pathlib import Path from matrix_display.cli import main @@ -27,7 +25,7 @@ def close(self) -> None: class CliTests(unittest.TestCase): - def test_main_renders_stdin_and_uses_selected_target(self) -> None: + def test_main_renders_stdin_to_target(self) -> None: created: list[FakeLedController] = [] def factory(*, target_ip: str) -> FakeLedController: @@ -35,23 +33,15 @@ def factory(*, target_ip: str) -> FakeLedController: created.append(controller) return controller - with tempfile.TemporaryDirectory() as directory: - config_path = Path(directory) / "matrix_display.toml" - config_path.write_text( - "[[display]]\n" - 'target_display = "maksim"\n' - 'controller_ip = "192.168.1.201"\n', - encoding="utf-8", - ) - stderr = io.StringIO() - - result = main( - ["--config", str(config_path), "--target", "maksim"], - stdin=io.StringIO("Hello\n"), - controller_factory=factory, - sleep=lambda _: None, - stderr=stderr, - ) + stderr = io.StringIO() + + result = main( + ["--target", "192.168.1.201"], + stdin=io.StringIO("Hello\n"), + controller_factory=factory, + sleep=lambda _: None, + stderr=stderr, + ) self.assertEqual(0, result) self.assertEqual("", stderr.getvalue()) @@ -60,66 +50,26 @@ def factory(*, target_ip: str) -> FakeLedController: self.assertTrue(created[0].closed) def test_main_accepts_short_target_flag(self) -> None: - with tempfile.TemporaryDirectory() as directory: - config_path = Path(directory) / "matrix_display.toml" - config_path.write_text( - "[[display]]\n" - 'target_display = "maksim"\n' - 'controller_ip = "192.168.1.201"\n', - encoding="utf-8", - ) - - result = main( - ["--config", str(config_path), "-t", "maksim"], - stdin=io.StringIO("Hello\n"), - controller_factory=lambda **_: FakeLedController("192.168.1.201"), - sleep=lambda _: None, - stderr=io.StringIO(), - ) + result = main( + ["-t", "192.168.1.201"], + stdin=io.StringIO("Hello\n"), + controller_factory=lambda **_: FakeLedController("192.168.1.201"), + sleep=lambda _: None, + stderr=io.StringIO(), + ) self.assertEqual(0, result) - def test_main_rejects_unknown_target(self) -> None: - stderr = io.StringIO() - - with tempfile.TemporaryDirectory() as directory: - config_path = Path(directory) / "matrix_display.toml" - config_path.write_text( - "[[display]]\n" - 'target_display = "maksim"\n' - 'controller_ip = "192.168.1.201"\n', - encoding="utf-8", - ) - - result = main( - ["--config", str(config_path), "--target", "office"], - stdin=io.StringIO("Hello\n"), - controller_factory=lambda **_: FakeLedController("192.168.1.201"), - sleep=lambda _: None, - stderr=stderr, - ) - - self.assertEqual(1, result) - self.assertIn("target display 'office' was not found", stderr.getvalue()) - def test_main_rejects_empty_messages(self) -> None: stderr = io.StringIO() - with tempfile.TemporaryDirectory() as directory: - config_path = Path(directory) / "matrix_display.toml" - config_path.write_text( - "[[display]]\n" - 'target_display = "maksim"\n' - 'controller_ip = "192.168.1.201"\n', - encoding="utf-8", - ) - - result = main( - ["--config", str(config_path), "--target", "maksim"], - stdin=io.StringIO("\n"), - controller_factory=lambda **_: FakeLedController("192.168.1.201"), - sleep=lambda _: None, - stderr=stderr, - ) + + result = main( + ["--target", "192.168.1.201"], + stdin=io.StringIO("\n"), + controller_factory=lambda **_: FakeLedController("192.168.1.201"), + sleep=lambda _: None, + stderr=stderr, + ) self.assertEqual(1, result) self.assertIn("expected a message on stdin", stderr.getvalue()) diff --git a/tests/test_config.py b/tests/test_config.py deleted file mode 100644 index 9c76f6c..0000000 --- a/tests/test_config.py +++ /dev/null @@ -1,93 +0,0 @@ -"""Unit tests for configuration loading.""" - -from __future__ import annotations - -import tempfile -import unittest -from pathlib import Path - -from matrix_display.config import load_config, resolve_controller_ip - - -class ConfigTests(unittest.TestCase): - def test_load_config_reads_named_displays(self) -> None: - with tempfile.TemporaryDirectory() as directory: - path = Path(directory) / "matrix_display.toml" - path.write_text( - "[[display]]\n" - 'target_display = "maksim"\n' - 'controller_ip = "192.168.1.77"\n' - "\n" - "[[display]]\n" - 'target_display = "office"\n' - 'controller_ip = "192.168.1.88"\n', - encoding="utf-8", - ) - - config = load_config(path) - - self.assertEqual(2, len(config.displays)) - self.assertEqual("maksim", config.displays[0].target_display) - self.assertEqual("192.168.1.77", config.displays[0].controller_ip) - - def test_resolve_controller_ip_uses_named_target(self) -> None: - with tempfile.TemporaryDirectory() as directory: - path = Path(directory) / "matrix_display.toml" - path.write_text( - "[[display]]\n" - 'target_display = "maksim"\n' - 'controller_ip = "192.168.1.77"\n', - encoding="utf-8", - ) - - controller_ip = resolve_controller_ip("maksim", config_path=path) - - self.assertEqual("192.168.1.77", controller_ip) - - def test_resolve_controller_ip_rejects_unknown_target(self) -> None: - with tempfile.TemporaryDirectory() as directory: - path = Path(directory) / "matrix_display.toml" - path.write_text( - "[[display]]\n" - 'target_display = "maksim"\n' - 'controller_ip = "192.168.1.77"\n', - encoding="utf-8", - ) - - with self.assertRaisesRegex(ValueError, "target display 'office'"): - resolve_controller_ip("office", config_path=path) - - def test_load_config_rejects_duplicate_targets(self) -> None: - with tempfile.TemporaryDirectory() as directory: - path = Path(directory) / "matrix_display.toml" - path.write_text( - "[[display]]\n" - 'target_display = "maksim"\n' - 'controller_ip = "192.168.1.77"\n' - "\n" - "[[display]]\n" - 'target_display = "maksim"\n' - 'controller_ip = "192.168.1.88"\n', - encoding="utf-8", - ) - - with self.assertRaisesRegex(ValueError, "duplicate target_display"): - load_config(path) - - def test_load_config_rejects_unknown_keys(self) -> None: - with tempfile.TemporaryDirectory() as directory: - path = Path(directory) / "matrix_display.toml" - path.write_text( - "[[display]]\n" - 'target_display = "maksim"\n' - 'controller_ip = "192.168.1.77"\n' - "universes = [1]\n", - encoding="utf-8", - ) - - with self.assertRaisesRegex(ValueError, "unsupported display keys"): - load_config(path) - - -if __name__ == "__main__": - unittest.main()