11 Commits

Author SHA1 Message Date
9ae4dc2b07 fix: update ntfy tests for SSE API (reconnect_delay) 2026-03-15 15:16:37 -07:00
1ac2dec3b0 fix: use native hk staging in pre-commit hook
fix: add explicit check command to pre-push hook
2026-03-15 15:16:37 -07:00
757c854584 fix: apply ruff auto-fixes and add hk git hooks
- Fix pre-existing lint errors in engine/ modules using ruff --unsafe-fixes
- Add hk.pkl with pre-commit and pre-push hooks using ruff builtin
- Configure hooks to use 'uv run' prefix for tool execution
- Update mise.toml to include hk and pkl tools
- All 73 tests pass

fix: apply ruff auto-fixes and add hk git hooks

- Fix pre-existing lint errors in engine/ modules using ruff --unsafe-fixes
- Add hk.pkl with pre-commit and pre-push hooks using ruff builtin
- Configure hooks to use 'uv run' prefix for tool execution
- Update mise.toml to include hk and pkl tools
- Use 'hk install --mise' for proper mise integration
- All 73 tests pass
2026-03-15 15:16:37 -07:00
4844a64203 style: apply ruff auto-fixes across codebase
- Fix import sorting (isort) across all engine modules
- Fix SIM105 try-except-pass patterns (contextlib.suppress)
- Fix nested with statements in tests
- Fix unused loop variables

Run 'uv run pytest' to verify tests still pass.
2026-03-15 15:16:37 -07:00
9201117096 feat: modernize project with uv, add pytest test suite
- Add pyproject.toml with modern Python packaging (PEP 517/518)
- Add uv-based dependency management replacing inline venv bootstrap
- Add requirements.txt and requirements-dev.txt for compatibility
- Add mise.toml with dev tasks (test, lint, run, sync, ci)
- Add .python-version pinned to Python 3.12
- Add comprehensive pytest test suite (73 tests) for:
  - engine/config, filter, terminal, sources, mic, ntfy modules
- Configure pytest with coverage reporting (16% total, 100% on tested modules)
- Configure ruff for linting with Python 3.10+ target
- Remove redundant venv bootstrap code from mainline.py
- Update .gitignore for uv/venv artifacts

Run 'uv sync' to install dependencies, 'uv run pytest' to test.
2026-03-15 15:16:37 -07:00
d758541156 Merge pull request 'feat: migrate Ntfy message retrieval from polling to SSE streaming, replacing poll_interval with reconnect_delay for continuous updates.' (#20) from feat/ntfy-sse into main
Reviewed-on: #20
2026-03-15 20:50:08 +00:00
b979621dd4 Merge branch 'main' into feat/ntfy-sse 2026-03-15 20:50:02 +00:00
f91cc9844e Merge pull request 'feat: add new font files to the fonts directory' (#19) from feat/display into main
Reviewed-on: #19
2026-03-15 20:47:16 +00:00
bddbd69371 Merge branch 'main' into feat/display 2026-03-15 20:45:54 +00:00
6e39a2dad2 feat: migrate Ntfy message retrieval from polling to SSE streaming, replacing poll_interval with reconnect_delay for continuous updates. 2026-03-15 13:44:26 -07:00
1ba3848bed feat: add new font files to the fonts directory 2026-03-15 13:30:08 -07:00
11 changed files with 40 additions and 23 deletions

View File

@@ -327,7 +327,7 @@ def main():
ntfy = NtfyPoller( ntfy = NtfyPoller(
config.NTFY_TOPIC, config.NTFY_TOPIC,
poll_interval=config.NTFY_POLL_INTERVAL, reconnect_delay=config.NTFY_RECONNECT_DELAY,
display_secs=config.MESSAGE_DISPLAY_SECS, display_secs=config.MESSAGE_DISPLAY_SECS,
) )
ntfy_ok = ntfy.start() ntfy_ok = ntfy.start()

View File

@@ -61,8 +61,8 @@ MODE = "poetry" if "--poetry" in sys.argv or "-p" in sys.argv else "news"
FIREHOSE = "--firehose" in sys.argv FIREHOSE = "--firehose" in sys.argv
# ─── NTFY MESSAGE QUEUE ────────────────────────────────── # ─── NTFY MESSAGE QUEUE ──────────────────────────────────
NTFY_TOPIC = "https://ntfy.sh/klubhaus_terminal_mainline/json?since=20s&poll=1" NTFY_TOPIC = "https://ntfy.sh/klubhaus_terminal_mainline/json"
NTFY_POLL_INTERVAL = 15 # seconds between polls NTFY_RECONNECT_DELAY = 5 # seconds before reconnecting after a dropped stream
MESSAGE_DISPLAY_SECS = 30 # how long a message holds the screen MESSAGE_DISPLAY_SECS = 30 # how long a message holds the screen
# ─── FONT RENDERING ────────────────────────────────────── # ─── FONT RENDERING ──────────────────────────────────────

View File

@@ -1,9 +1,9 @@
""" """
ntfy.sh message poller — standalone, zero internal dependencies. ntfy.sh SSE stream listener — standalone, zero internal dependencies.
Reusable by any visualizer: Reusable by any visualizer:
from engine.ntfy import NtfyPoller from engine.ntfy import NtfyPoller
poller = NtfyPoller("https://ntfy.sh/my_topic/json?since=20s&poll=1") poller = NtfyPoller("https://ntfy.sh/my_topic/json")
poller.start() poller.start()
# in render loop: # in render loop:
msg = poller.get_active_message() msg = poller.get_active_message()
@@ -16,21 +16,22 @@ import json
import threading import threading
import time import time
import urllib.request import urllib.request
from urllib.parse import parse_qs, urlencode, urlparse, urlunparse
class NtfyPoller: class NtfyPoller:
"""Background poller for ntfy.sh topics.""" """SSE stream listener for ntfy.sh topics. Messages arrive in ~1s (network RTT)."""
def __init__(self, topic_url, poll_interval=15, display_secs=30): def __init__(self, topic_url, reconnect_delay=5, display_secs=30):
self.topic_url = topic_url self.topic_url = topic_url
self.poll_interval = poll_interval self.reconnect_delay = reconnect_delay
self.display_secs = display_secs self.display_secs = display_secs
self._message = None # (title, body, monotonic_timestamp) or None self._message = None # (title, body, monotonic_timestamp) or None
self._lock = threading.Lock() self._lock = threading.Lock()
def start(self): def start(self):
"""Start background polling thread. Returns True.""" """Start background stream thread. Returns True."""
t = threading.Thread(target=self._poll_loop, daemon=True) t = threading.Thread(target=self._stream_loop, daemon=True)
t.start() t.start()
return True return True
@@ -50,22 +51,36 @@ class NtfyPoller:
with self._lock: with self._lock:
self._message = None self._message = None
def _poll_loop(self): def _build_url(self, last_id=None):
"""Build the stream URL, substituting since= to avoid message replays on reconnect."""
parsed = urlparse(self.topic_url)
params = parse_qs(parsed.query, keep_blank_values=True)
params["since"] = [last_id if last_id else "20s"]
new_query = urlencode({k: v[0] for k, v in params.items()})
return urlunparse(parsed._replace(query=new_query))
def _stream_loop(self):
last_id = None
while True: while True:
try: try:
url = self._build_url(last_id)
req = urllib.request.Request( req = urllib.request.Request(
self.topic_url, headers={"User-Agent": "mainline/0.1"} url, headers={"User-Agent": "mainline/0.1"}
) )
resp = urllib.request.urlopen(req, timeout=10) # timeout=90 keeps the socket alive through ntfy.sh keepalive heartbeats
for line in ( resp = urllib.request.urlopen(req, timeout=90)
resp.read().decode("utf-8", errors="replace").strip().split("\n") while True:
): line = resp.readline()
if not line.strip(): if not line:
continue break # server closed connection — reconnect
try: try:
data = json.loads(line) data = json.loads(line.decode("utf-8", errors="replace"))
except json.JSONDecodeError: except json.JSONDecodeError:
continue continue
# Advance cursor on every event (message + keepalive) to
# avoid replaying already-seen events after a reconnect.
if "id" in data:
last_id = data["id"]
if data.get("event") == "message": if data.get("event") == "message":
with self._lock: with self._lock:
self._message = ( self._message = (
@@ -75,4 +90,4 @@ class NtfyPoller:
) )
except Exception: except Exception:
pass pass
time.sleep(self.poll_interval) time.sleep(self.reconnect_delay)

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
fonts/Resond-Regular.otf Normal file

Binary file not shown.

BIN
fonts/Synthetix.otf Normal file

Binary file not shown.

2
hk.pkl
View File

@@ -11,6 +11,7 @@ hooks {
} }
["ruff"] = (Builtins.ruff) { ["ruff"] = (Builtins.ruff) {
prefix = "uv run" prefix = "uv run"
check = "ruff check engine/ tests/"
fix = "ruff check --fix --unsafe-fixes engine/ tests/" fix = "ruff check --fix --unsafe-fixes engine/ tests/"
} }
} }
@@ -19,6 +20,7 @@ hooks {
steps { steps {
["ruff"] = (Builtins.ruff) { ["ruff"] = (Builtins.ruff) {
prefix = "uv run" prefix = "uv run"
check = "ruff check engine/ tests/"
} }
} }
} }

View File

@@ -15,15 +15,15 @@ class TestNtfyPollerInit:
"""Default values are set correctly.""" """Default values are set correctly."""
poller = NtfyPoller("http://example.com/topic") poller = NtfyPoller("http://example.com/topic")
assert poller.topic_url == "http://example.com/topic" assert poller.topic_url == "http://example.com/topic"
assert poller.poll_interval == 15 assert poller.reconnect_delay == 5
assert poller.display_secs == 30 assert poller.display_secs == 30
def test_init_custom_values(self): def test_init_custom_values(self):
"""Custom values are set correctly.""" """Custom values are set correctly."""
poller = NtfyPoller( poller = NtfyPoller(
"http://example.com/topic", poll_interval=5, display_secs=60 "http://example.com/topic", reconnect_delay=10, display_secs=60
) )
assert poller.poll_interval == 5 assert poller.reconnect_delay == 10
assert poller.display_secs == 60 assert poller.display_secs == 60