Fix what a full test round of today's releases found: a ticking single clock, a switch that works in standby, no taps from a swipe (0.2.78)
A test and code-analysis round of 0.2.72 to 0.2.76 (suites at every tag, sanitizers, ESP32 and GitHub builds, host harnesses on both boards, the editor in a browser, best practice checked against ESPHome, LVGL, Home Assistant and Vue) found these, fixed here with Max's choices for the product questions: - Firmware 0.2.65: the second hand of a single analog clock moves again; on a page that is one full on/off tile the waking push also switches it; on the Guition a flick is never a tap and a finger that slides off a tile doesn't tap it; Previous/Next react to a firm press; the same Go to page tile may sit on several pages; a layout the screen has no memory for is refused instead of crashing; 64-bit redraw mask for 48 tiles; Montserrat 14 no longer built. - App: Save refuses a layout too long for one message or too big for eight pages; an offline screen keeps its firmware version from the device registry; HA refusals reach the editor; 128 KB requests; changelog out of the live stream; cached hashed assets; tini as init and a clean stop; changelog parsing without asterisks or crashes. - Editor: clicking the open screen keeps unsaved edits, edits during a save stay unsaved, a Page row and phone dragging to other pages, repeated page tiles, limits from the server, HA's error messages, a type-checked build. - tools/check.sh + GitHub Actions + a CYD flash budget; docs match 48 tiles and the new editor. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -3,39 +3,73 @@
|
||||
The Dockerfile copies CHANGELOG.md next to this file; a development checkout has it one folder up.
|
||||
Each `## <app> (firmware <fw>)` section gives its bullet lines as plain text, newest first.
|
||||
"""
|
||||
import logging
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
LOG = logging.getLogger('screen_manager')
|
||||
|
||||
HEADER = re.compile(r'^## (\d+\.\d+\.\d+) \(firmware (\d+\.\d+\.\d+)\)')
|
||||
# A heading of level one or two ends a section; a `### ` inside one doesn't.
|
||||
SECTION_END = re.compile(r'^#{1,2}(\s|$)')
|
||||
BULLET = re.compile(r'^[-*+]\s+(.*)$')
|
||||
BOLD = re.compile(r'\*\*(.+?)\*\*')
|
||||
# *Full page*: single asterisks around text. A lone asterisk (5 * 3) or one inside a word stays (app 0.2.78).
|
||||
ITALIC = re.compile(r'(?<![\w*])\*(?=\S)([^*\n]*?\S)\*(?![\w*])')
|
||||
CODE = re.compile(r'`([^`]*)`')
|
||||
LINK = re.compile(r'\[([^\]]+)\]\([^)]+\)')
|
||||
|
||||
|
||||
def load(path=None, limit=20):
|
||||
"""[{app, firmware, lines}] for the latest sections; [] without a changelog."""
|
||||
"""[{app, firmware, lines}] for the latest sections; [] without a changelog, or with one that can't be read (a
|
||||
folder, no permission, not UTF-8): the Update badge then has no notes, and the app still starts (app 0.2.78)."""
|
||||
candidates = [Path(path)] if path else [Path(__file__).with_name('CHANGELOG.md'), Path(__file__).parent.parent / 'CHANGELOG.md']
|
||||
for candidate in candidates:
|
||||
if candidate.exists():
|
||||
return parse(candidate.read_text(), limit)
|
||||
try:
|
||||
return parse(candidate.read_text(encoding='utf-8'), limit)
|
||||
except (OSError, UnicodeDecodeError) as error:
|
||||
LOG.warning("What's new stays empty: %s can't be read (%s)", candidate, type(error).__name__)
|
||||
return []
|
||||
return []
|
||||
|
||||
|
||||
def parse(text, limit=20):
|
||||
sections = []
|
||||
"""The sections of a changelog. A bullet (`- ` or `* `) takes the lines that continue it, indented or wrapped
|
||||
right under it; bullets under any other heading of level one or two belong to no section (app 0.2.78)."""
|
||||
sections, current, item, blank = [], None, None, False
|
||||
|
||||
def close():
|
||||
if item is not None and current is not None:
|
||||
current['lines'].append(plain(' '.join(item)))
|
||||
|
||||
for line in text.splitlines():
|
||||
match = HEADER.match(line)
|
||||
if match:
|
||||
if len(sections) >= limit:
|
||||
break
|
||||
sections.append({'app': match.group(1), 'firmware': match.group(2), 'lines': []})
|
||||
elif sections and line.startswith('- '):
|
||||
sections[-1]['lines'].append(plain(line[2:]))
|
||||
stripped = line.strip()
|
||||
if SECTION_END.match(line):
|
||||
close()
|
||||
item, match = None, HEADER.match(line)
|
||||
if match and len(sections) >= limit:
|
||||
return sections
|
||||
current = {'app': match.group(1), 'firmware': match.group(2), 'lines': []} if match else None
|
||||
if current:
|
||||
sections.append(current)
|
||||
elif (bullet := BULLET.match(line)) and current is not None:
|
||||
close()
|
||||
item = [bullet.group(1).strip()]
|
||||
elif stripped and item is not None and (line[:1].isspace() or not blank) and not line.startswith('#'):
|
||||
# Markdown continues a bullet with an indented line, or with any line right under it.
|
||||
item.append(stripped)
|
||||
elif stripped:
|
||||
close()
|
||||
item = None
|
||||
blank = not stripped
|
||||
close()
|
||||
return sections
|
||||
|
||||
|
||||
def plain(markdown):
|
||||
text = BOLD.sub(r'\1', markdown)
|
||||
text = CODE.sub(r'\1', text)
|
||||
text = LINK.sub(r'\1', text)
|
||||
return text.strip()
|
||||
"""The text of one bullet without Markdown: bold, italics and links outside code, and the code's backticks."""
|
||||
parts = CODE.split(markdown)
|
||||
for index in range(0, len(parts), 2):
|
||||
parts[index] = LINK.sub(r'\1', ITALIC.sub(r'\1', BOLD.sub(r'\1', parts[index])))
|
||||
return ''.join(parts).strip()
|
||||
|
||||
Reference in New Issue
Block a user