Compare commits
5 Commits
feat/code-
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 2650f7245e | |||
| b1f2b9d2be | |||
| c08a7d3cb0 | |||
| d5a3edba97 | |||
| fb35458718 |
@@ -1,154 +0,0 @@
|
||||
# Code Scroll Mode — Design Spec
|
||||
|
||||
**Date:** 2026-03-16
|
||||
**Branch:** feat/code-scroll
|
||||
**Status:** Approved
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Add a `--code` CLI flag that puts MAINLINE into "source consciousness" mode. Instead of RSS headlines or poetry stanzas, the program's own source code scrolls upward as large OTF half-block characters with the standard white-hot → deep green gradient. Each scroll item is one non-blank, non-comment line from `engine/*.py`, attributed to its enclosing function/class scope and dotted module path.
|
||||
|
||||
---
|
||||
|
||||
## Goals
|
||||
|
||||
- Mirror the existing `--poetry` mode pattern as closely as possible
|
||||
- Zero new runtime dependencies (stdlib `ast` and `pathlib` only)
|
||||
- No changes to `scroll.py` or the render pipeline
|
||||
- The item tuple shape `(text, src, ts)` is unchanged
|
||||
|
||||
---
|
||||
|
||||
## New Files
|
||||
|
||||
### `engine/fetch_code.py`
|
||||
|
||||
Single public function `fetch_code()` that returns `(items, line_count, 0)`.
|
||||
|
||||
**Algorithm:**
|
||||
|
||||
1. Glob `engine/*.py` in sorted order
|
||||
2. For each file:
|
||||
a. Read source text
|
||||
b. `ast.parse(source)` → build a `{line_number: scope_label}` map by walking all `FunctionDef`, `AsyncFunctionDef`, and `ClassDef` nodes. Each node covers its full line range. Inner scopes override outer ones.
|
||||
c. Iterate source lines (1-indexed). Skip if:
|
||||
- The stripped line is empty
|
||||
- The stripped line starts with `#`
|
||||
d. For each kept line emit:
|
||||
- `text` = `line.rstrip()` (preserve indentation for readability in the big render)
|
||||
- `src` = scope label from the AST map, e.g. `stream()` for functions, `MicMonitor` for classes, `<module>` for top-level lines
|
||||
- `ts` = dotted module path derived from filename, e.g. `engine/scroll.py` → `engine.scroll`
|
||||
3. Return `(items, len(items), 0)`
|
||||
|
||||
**Scope label rules:**
|
||||
- `FunctionDef` / `AsyncFunctionDef` → `name()`
|
||||
- `ClassDef` → `name` (no parens)
|
||||
- No enclosing node → `<module>`
|
||||
|
||||
**Dependencies:** `ast`, `pathlib` — stdlib only.
|
||||
|
||||
---
|
||||
|
||||
## Modified Files
|
||||
|
||||
### `engine/config.py`
|
||||
|
||||
Extend `MODE` detection to recognise `--code`:
|
||||
|
||||
```python
|
||||
MODE = (
|
||||
"poetry" if "--poetry" in sys.argv or "-p" in sys.argv
|
||||
else "code" if "--code" in sys.argv
|
||||
else "news"
|
||||
)
|
||||
```
|
||||
|
||||
### `engine/app.py`
|
||||
|
||||
**Subtitle line** — extend the subtitle dict:
|
||||
|
||||
```python
|
||||
_subtitle = {
|
||||
"poetry": "literary consciousness stream",
|
||||
"code": "source consciousness stream",
|
||||
}.get(config.MODE, "digital consciousness stream")
|
||||
```
|
||||
|
||||
**Boot sequence** — add `elif config.MODE == "code":` branch after the poetry branch:
|
||||
|
||||
```python
|
||||
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}")
|
||||
```
|
||||
|
||||
No cache save/load — local source files are read instantly and change only on disk writes.
|
||||
|
||||
---
|
||||
|
||||
## Data Flow
|
||||
|
||||
```
|
||||
engine/*.py (sorted)
|
||||
│
|
||||
▼
|
||||
fetch_code()
|
||||
│ ast.parse → scope map
|
||||
│ filter blank + comment lines
|
||||
│ emit (line, scope(), engine.module)
|
||||
▼
|
||||
items: List[Tuple[str, str, str]]
|
||||
│
|
||||
▼
|
||||
stream(items, ntfy, mic) ← unchanged
|
||||
│
|
||||
▼
|
||||
next_headline() shuffles + recycles automatically
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error Handling
|
||||
|
||||
- If a file fails to `ast.parse` (malformed source), fall back to `<module>` scope for all lines in that file — do not crash.
|
||||
- If `engine/` contains no `.py` files (shouldn't happen in practice), `fetch_code()` returns an empty list; `app.py`'s existing `if not items:` guard handles this.
|
||||
|
||||
---
|
||||
|
||||
## Testing
|
||||
|
||||
New file: `tests/test_fetch_code.py`
|
||||
|
||||
| Test | Assertion |
|
||||
|------|-----------|
|
||||
| `test_items_are_tuples` | Every item from `fetch_code()` is a 3-tuple of strings |
|
||||
| `test_blank_and_comment_lines_excluded` | No item text is empty; no item text (stripped) starts with `#` |
|
||||
| `test_module_path_format` | Every `ts` field matches pattern `engine\.\w+` |
|
||||
|
||||
No mocking — tests read the real engine source files, keeping them honest against actual content.
|
||||
|
||||
---
|
||||
|
||||
## CLI
|
||||
|
||||
```bash
|
||||
python3 mainline.py --code # source consciousness mode
|
||||
uv run mainline.py --code
|
||||
```
|
||||
|
||||
Compatible with all existing flags (`--no-font-picker`, `--font-file`, `--firehose`, etc.).
|
||||
|
||||
---
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- Syntax highlighting / token-aware coloring (can be added later)
|
||||
- `--code-dir` flag for pointing at arbitrary directories (YAGNI)
|
||||
- Caching code items to disk
|
||||
@@ -272,10 +272,11 @@ def main():
|
||||
time.sleep(0.07)
|
||||
|
||||
print()
|
||||
_subtitle = {
|
||||
"poetry": "literary consciousness stream",
|
||||
"code": "source consciousness stream",
|
||||
}.get(config.MODE, "digital consciousness stream")
|
||||
_subtitle = (
|
||||
"literary consciousness stream"
|
||||
if config.MODE == "poetry"
|
||||
else "digital consciousness stream"
|
||||
)
|
||||
print(f" {W_DIM}v0.1 · {_subtitle}{RST}")
|
||||
print(f" {W_GHOST}{'─' * (w - 4)}{RST}")
|
||||
print()
|
||||
@@ -296,14 +297,6 @@ 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)
|
||||
|
||||
@@ -188,11 +188,7 @@ def set_config(config: Config) -> None:
|
||||
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 "code" if "--code" in sys.argv
|
||||
else "news"
|
||||
)
|
||||
MODE = "poetry" if "--poetry" in sys.argv or "-p" in sys.argv else "news"
|
||||
FIREHOSE = "--firehose" in sys.argv
|
||||
|
||||
# ─── NTFY MESSAGE QUEUE ──────────────────────────────────
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
"""
|
||||
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 '<module>'
|
||||
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, "<module>")
|
||||
items.append((raw.rstrip(), label, module))
|
||||
|
||||
return items, len(items), 0
|
||||
Binary file not shown.
Binary file not shown.
@@ -1,35 +0,0 @@
|
||||
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}"
|
||||
Reference in New Issue
Block a user