diff --git a/engine/app.py b/engine/app.py index ec36e11..6c22f25 100644 --- a/engine/app.py +++ b/engine/app.py @@ -272,11 +272,10 @@ def main(): time.sleep(0.07) print() - _subtitle = ( - "literary consciousness stream" - if config.MODE == "poetry" - else "digital consciousness stream" - ) + _subtitle = { + "poetry": "literary consciousness stream", + "code": "source consciousness stream", + }.get(config.MODE, "digital consciousness stream") print(f" {W_DIM}v0.1 · {_subtitle}{RST}") print(f" {W_GHOST}{'─' * (w - 4)}{RST}") print() @@ -297,6 +296,14 @@ def main(): ) print(f" {G_DIM}>{RST} {G_MID}{len(items)} STANZAS ACQUIRED{RST}") save_cache(items) + elif config.MODE == "code": + from engine.fetch_code import fetch_code + slow_print(" > INITIALIZING SOURCE ARRAY...\n") + time.sleep(0.2) + print() + items, line_count, _ = fetch_code() + print() + print(f" {G_DIM}>{RST} {G_MID}{line_count} LINES ACQUIRED{RST}") else: slow_print(" > INITIALIZING FEED ARRAY...\n") time.sleep(0.2) diff --git a/engine/config.py b/engine/config.py index 2174568..1de24e5 100644 --- a/engine/config.py +++ b/engine/config.py @@ -57,7 +57,11 @@ def list_repo_font_files(): HEADLINE_LIMIT = 1000 FEED_TIMEOUT = 10 MIC_THRESHOLD_DB = 50 # dB above which glitches intensify -MODE = "poetry" if "--poetry" in sys.argv or "-p" in sys.argv else "news" +MODE = ( + "poetry" if "--poetry" in sys.argv or "-p" in sys.argv + else "code" if "--code" in sys.argv + else "news" +) FIREHOSE = "--firehose" in sys.argv # ─── NTFY MESSAGE QUEUE ────────────────────────────────── diff --git a/engine/fetch_code.py b/engine/fetch_code.py new file mode 100644 index 0000000..3d1160f --- /dev/null +++ b/engine/fetch_code.py @@ -0,0 +1,67 @@ +""" +Source code feed — reads engine/*.py and emits non-blank, non-comment lines +as scroll items. Used by --code mode. +Depends on: nothing (stdlib only). +""" + +import ast +from pathlib import Path + +_ENGINE_DIR = Path(__file__).resolve().parent + + +def _scope_map(source: str) -> dict[int, str]: + """Return {line_number: scope_label} for every line in source. + + Nodes are sorted by range size descending so inner scopes overwrite + outer ones, guaranteeing the narrowest enclosing scope wins. + """ + try: + tree = ast.parse(source) + except SyntaxError: + return {} + + nodes = [] + for node in ast.walk(tree): + if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)): + end = getattr(node, "end_lineno", node.lineno) + span = end - node.lineno + nodes.append((span, node)) + + # Largest range first → inner scopes overwrite on second pass + nodes.sort(key=lambda x: x[0], reverse=True) + + scope = {} + for _, node in nodes: + end = getattr(node, "end_lineno", node.lineno) + if isinstance(node, ast.ClassDef): + label = node.name + else: + label = f"{node.name}()" + for ln in range(node.lineno, end + 1): + scope[ln] = label + + return scope + + +def fetch_code(): + """Read engine/*.py and return (items, line_count, 0). + + Each item is (text, src, ts) where: + text = the code line (rstripped, indentation preserved) + src = enclosing function/class name, e.g. 'stream()' or '' + ts = dotted module path, e.g. 'engine.scroll' + """ + items = [] + for path in sorted(_ENGINE_DIR.glob("*.py")): + module = f"engine.{path.stem}" + source = path.read_text(encoding="utf-8") + scope = _scope_map(source) + for lineno, raw in enumerate(source.splitlines(), start=1): + stripped = raw.strip() + if not stripped or stripped.startswith("#"): + continue + label = scope.get(lineno, "") + items.append((raw.rstrip(), label, module)) + + return items, len(items), 0 diff --git a/fonts/Kapiler.otf b/fonts/Kapiler.otf new file mode 100644 index 0000000..695140a Binary files /dev/null and b/fonts/Kapiler.otf differ diff --git a/fonts/Kapiler.ttf b/fonts/Kapiler.ttf new file mode 100644 index 0000000..930f77e Binary files /dev/null and b/fonts/Kapiler.ttf differ diff --git a/tests/test_fetch_code.py b/tests/test_fetch_code.py new file mode 100644 index 0000000..5578b1d --- /dev/null +++ b/tests/test_fetch_code.py @@ -0,0 +1,35 @@ +import re + +from engine.fetch_code import fetch_code + + +def test_return_shape(): + items, line_count, ignored = fetch_code() + assert isinstance(items, list) + assert line_count == len(items) + assert ignored == 0 + + +def test_items_are_tuples(): + items, _, _ = fetch_code() + assert items, "expected at least one code line" + for item in items: + assert isinstance(item, tuple) and len(item) == 3 + text, src, ts = item + assert isinstance(text, str) + assert isinstance(src, str) + assert isinstance(ts, str) + + +def test_blank_and_comment_lines_excluded(): + items, _, _ = fetch_code() + for text, _, _ in items: + assert text.strip(), "blank line should have been filtered" + assert not text.strip().startswith("#"), "comment line should have been filtered" + + +def test_module_path_format(): + items, _, _ = fetch_code() + pattern = re.compile(r"^engine\.\w+$") + for _, _, ts in items: + assert pattern.match(ts), f"unexpected module path: {ts!r}"