Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "hatchling.build"

[project]
name = "pyqt-reactive"
version = "0.1.25"
version = "0.1.26"
description = "React-quality reactive form generation framework for PyQt6"
authors = [{name = "Tristan Simas", email = "tristan.simas@mail.mcgill.ca"}]
license = {text = "MIT"}
Expand Down
2 changes: 1 addition & 1 deletion src/pyqt_reactive/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
- Cross-window reactive updates
"""

__version__ = "0.1.25"
__version__ = "0.1.26"

# Public API will be populated as modules are added
__all__ = [
Expand Down
35 changes: 35 additions & 0 deletions src/pyqt_reactive/utils/log_highlighter.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,41 @@ def parse_line(text: str) -> list[dict]:
return segments


def _escape_html(text: str) -> str:
return text.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")


def build_log_line_html(text: str) -> str:
"""Render one line from the authoritative parsed highlight segments."""
segments = sorted(parse_line(text), key=lambda segment: segment["start"])
cursor = 0
parts: list[str] = ['<span style="white-space: pre-wrap;">']
text_len = len(text)

for segment in segments:
start = max(0, segment["start"])
end = min(text_len, start + segment["length"])
if end <= start or start < cursor:
continue
if start > cursor:
parts.append(_escape_html(text[cursor:start]))

red, green, blue = segment["color"]
style = f"color: rgb({red},{green},{blue});"
if segment.get("bold"):
style += " font-weight: 700;"
parts.append(
f'<span style="{style}">{_escape_html(text[start:end])}</span>'
)
cursor = end

if cursor < text_len:
parts.append(_escape_html(text[cursor:]))

parts.append("</span>")
return "".join(parts)


def main() -> int:
for line in sys.stdin:
try:
Expand Down
80 changes: 2 additions & 78 deletions src/pyqt_reactive/utils/log_streamer.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
import sys
from collections import deque
from pathlib import Path
import re

from pyqt_reactive.utils.log_highlighter import build_log_line_html

def emit(obj: dict) -> None:
sys.stdout.write(json.dumps(obj, ensure_ascii=True) + "\n")
Expand All @@ -24,82 +24,6 @@ def tail_lines(path: Path, max_lines: int) -> list[str]:
return list(buf)


_TIMESTAMP_RE = re.compile(r"^\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2},\d{3}")
_LOG_LEVEL_RE = re.compile(r"\b(ERROR|WARNING|INFO|DEBUG|CRITICAL)\b")
_LOGGER_NAME_RE = re.compile(r" - ([\w\.]+) - ")
_FILE_PATH_RE = re.compile(r"(?:/[\w\-\.]+)+\.py")
_PYTHON_STRING_RE = re.compile(r"[\"'](?:[^\"'\\]|\\.)*[\"']")
_NUMBER_RE = re.compile(r"\b\d+(?:\.\d+)?\b")


def _escape_html(text: str) -> str:
return (
text.replace("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
)


def _segments_for_line(text: str) -> list[dict]:
segments = []

match = _TIMESTAMP_RE.match(text)
if match:
segments.append({"start": match.start(), "length": match.end() - match.start(), "color": (105, 105, 105)})

for match in _LOG_LEVEL_RE.finditer(text):
level = match.group(1)
if level in ("ERROR", "CRITICAL"):
color = (255, 85, 85)
elif level == "WARNING":
color = (255, 140, 0)
else:
color = (100, 160, 210)
segments.append({"start": match.start(), "length": match.end() - match.start(), "color": color, "bold": True})

for match in _LOGGER_NAME_RE.finditer(text):
segments.append({"start": match.start(1), "length": match.end(1) - match.start(1), "color": (147, 112, 219)})

for match in _FILE_PATH_RE.finditer(text):
segments.append({"start": match.start(), "length": match.end() - match.start(), "color": (34, 139, 34)})

for match in _PYTHON_STRING_RE.finditer(text):
segments.append({"start": match.start(), "length": match.end() - match.start(), "color": (206, 145, 120)})

for match in _NUMBER_RE.finditer(text):
segments.append({"start": match.start(), "length": match.end() - match.start(), "color": (181, 206, 168)})

return segments


def _build_html(text: str) -> str:
segments = sorted(_segments_for_line(text), key=lambda s: s["start"])
cursor = 0
parts: list[str] = ["<span style=\"white-space: pre-wrap;\">"]
text_len = len(text)

for seg in segments:
start = max(0, seg["start"])
end = min(text_len, start + seg["length"])
if end <= start or start < cursor:
continue
if start > cursor:
parts.append(_escape_html(text[cursor:start]))

r, g, b = seg["color"]
style = f"color: rgb({r},{g},{b});"
if seg.get("bold"):
style += " font-weight: 700;"
parts.append(f"<span style=\"{style}\">{_escape_html(text[start:end])}</span>")
cursor = end

if cursor < text_len:
parts.append(_escape_html(text[cursor:]))

parts.append("</span>")
return "".join(parts)


def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("path")
Expand All @@ -114,7 +38,7 @@ def main() -> int:
chunk: list = []
for line in lines:
if args.html:
chunk.append({"text": line, "html": _build_html(line)})
chunk.append({"text": line, "html": build_log_line_html(line)})
else:
chunk.append(line)
if len(chunk) >= args.chunk_lines:
Expand Down
Loading
Loading