Compare commits
5 Commits
effects_pl
...
2229ccdea4
| Author | SHA1 | Date | |
|---|---|---|---|
| 2229ccdea4 | |||
| f13e89f823 | |||
| 19fb4bc4fe | |||
| ae10fd78ca | |||
| 4afab642f7 |
174
README.md
174
README.md
@@ -6,7 +6,9 @@ A full-screen terminal news ticker that renders live global headlines in large O
|
||||
|
||||
---
|
||||
|
||||
## Run
|
||||
## Using
|
||||
|
||||
### Run
|
||||
|
||||
```bash
|
||||
python3 mainline.py # news stream
|
||||
@@ -20,11 +22,15 @@ python3 mainline.py --font-dir ~/fonts # scan a different font folder
|
||||
python3 mainline.py --font-index 1 # select face index within a collection
|
||||
```
|
||||
|
||||
First run bootstraps a local `.mainline_venv/` and installs deps (`feedparser`, `Pillow`, `sounddevice`, `numpy`). Subsequent runs start immediately, loading from cache.
|
||||
Or with uv:
|
||||
|
||||
---
|
||||
```bash
|
||||
uv run mainline.py
|
||||
```
|
||||
|
||||
## Config
|
||||
First run bootstraps a local `.mainline_venv/` and installs deps (`feedparser`, `Pillow`, `sounddevice`, `numpy`). Subsequent runs start immediately, loading from cache. With uv, run `uv sync` or `uv sync --all-extras` (includes mic support) instead.
|
||||
|
||||
### Config
|
||||
|
||||
All constants live in `engine/config.py`:
|
||||
|
||||
@@ -44,23 +50,41 @@ All constants live in `engine/config.py`:
|
||||
| `FRAME_DT` | `0.05` | Frame interval in seconds (20 FPS) |
|
||||
| `GRAD_SPEED` | `0.08` | Gradient sweep speed (cycles/sec, ~12s full sweep) |
|
||||
| `FIREHOSE_H` | `12` | Firehose zone height (terminal rows) |
|
||||
| `NTFY_TOPIC` | klubhaus URL | ntfy.sh JSON endpoint to poll |
|
||||
| `NTFY_POLL_INTERVAL` | `15` | Seconds between ntfy polls |
|
||||
| `NTFY_TOPIC` | klubhaus URL | ntfy.sh JSON stream endpoint |
|
||||
| `NTFY_RECONNECT_DELAY` | `5` | Seconds before reconnecting after a dropped SSE stream |
|
||||
| `MESSAGE_DISPLAY_SECS` | `30` | How long an ntfy message holds the screen |
|
||||
|
||||
---
|
||||
### Feeds
|
||||
|
||||
## Fonts
|
||||
~25 sources across four categories: **Science & Technology**, **Economics & Business**, **World & Politics**, **Culture & Ideas**. Add or swap feeds in `engine/sources.py` → `FEEDS`.
|
||||
|
||||
A `fonts/` directory is bundled with demo faces (AlphatronDemo, CSBishopDrawn, CyberformDemo, KATA, Microbots, Neoform, Pixel Sparta, Robocops, Xeonic, and others). On startup, an interactive picker lists all discovered faces with a live half-block preview rendered at your configured size.
|
||||
**Poetry mode** pulls from Project Gutenberg: Whitman, Dickinson, Thoreau, Emerson. Sources are in `engine/sources.py` → `POETRY_SOURCES`.
|
||||
|
||||
### Fonts
|
||||
|
||||
A `fonts/` directory is bundled with demo faces (AgorTechnoDemo, AlphatronDemo, CSBishopDrawn, CubaTechnologyDemo, CyberformDemo, KATA, Microbots, ModernSpaceDemo, Neoform, Pixel Sparta, RaceHugoDemo, Resond, Robocops, Synthetix, Xeonic, and others). On startup, an interactive picker lists all discovered faces with a live half-block preview rendered at your configured size.
|
||||
|
||||
Navigation: `↑`/`↓` or `j`/`k` to move, `Enter` or `q` to select. The selected face persists for that session.
|
||||
|
||||
To add your own fonts, drop `.otf`, `.ttf`, or `.ttc` files into `fonts/` (or point `--font-dir` at any other folder). Font collections (`.ttc`, multi-face `.otf`) are enumerated face-by-face.
|
||||
|
||||
### ntfy.sh
|
||||
|
||||
Mainline polls a configurable ntfy.sh topic in the background. When a message arrives, the scroll pauses and the message renders full-screen for `MESSAGE_DISPLAY_SECS` seconds, then the stream resumes.
|
||||
|
||||
To push a message:
|
||||
|
||||
```bash
|
||||
curl -d "Body text" -H "Title: Alert title" https://ntfy.sh/your_topic
|
||||
```
|
||||
|
||||
Update `NTFY_TOPIC` in `engine/config.py` to point at your own topic.
|
||||
|
||||
---
|
||||
|
||||
## How it works
|
||||
## Internals
|
||||
|
||||
### How it works
|
||||
|
||||
- On launch, the font picker scans `fonts/` and presents a live-rendered TUI for face selection; `--no-font-picker` skips directly to stream
|
||||
- Feeds are fetched and filtered on startup (sports and vapid content stripped); results are cached to `.mainline_cache_news.json` / `.mainline_cache_poetry.json` for fast restarts
|
||||
@@ -69,11 +93,9 @@ To add your own fonts, drop `.otf`, `.ttf`, or `.ttc` files into `fonts/` (or po
|
||||
- Subject-region detection runs a regex pass on each headline; matches trigger a Google Translate call and font swap to the appropriate script (CJK, Arabic, Devanagari, etc.) using macOS system fonts
|
||||
- The mic stream runs in a background thread, feeding RMS dB into the glitch probability calculation each frame
|
||||
- The viewport scrolls through a virtual canvas of pre-rendered blocks; fade zones at top and bottom dissolve characters probabilistically
|
||||
- An ntfy.sh poller runs in a background thread; incoming messages interrupt the scroll and render full-screen until dismissed or expired
|
||||
- An ntfy.sh SSE stream runs in a background thread; incoming messages interrupt the scroll and render full-screen until dismissed or expired
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
### Architecture
|
||||
|
||||
`mainline.py` is a thin entrypoint (venv bootstrap → `engine.app.main()`). All logic lives in the `engine/` package:
|
||||
|
||||
@@ -91,43 +113,122 @@ engine/
|
||||
mic.py MicMonitor — standalone, graceful fallback
|
||||
scroll.py stream() frame loop + message rendering
|
||||
app.py main(), font picker TUI, boot sequence, signal handler
|
||||
|
||||
tests/
|
||||
test_config.py
|
||||
test_filter.py
|
||||
test_mic.py
|
||||
test_ntfy.py
|
||||
test_sources.py
|
||||
test_terminal.py
|
||||
```
|
||||
|
||||
`ntfy.py` and `mic.py` have zero internal dependencies and can be imported by any other visualizer.
|
||||
|
||||
---
|
||||
|
||||
## Feeds
|
||||
## Extending
|
||||
|
||||
~25 sources across four categories: **Science & Technology**, **Economics & Business**, **World & Politics**, **Culture & Ideas**. Add or swap feeds in `engine/sources.py` → `FEEDS`.
|
||||
`ntfy.py` and `mic.py` are fully standalone and designed to be reused by any terminal visualizer. `engine.render` is the importable rendering pipeline for non-terminal targets.
|
||||
|
||||
**Poetry mode** pulls from Project Gutenberg: Whitman, Dickinson, Thoreau, Emerson. Sources are in `engine/sources.py` → `POETRY_SOURCES`.
|
||||
|
||||
---
|
||||
|
||||
## ntfy.sh Integration
|
||||
|
||||
Mainline polls a configurable ntfy.sh topic in the background. When a message arrives, the scroll pauses and the message renders full-screen for `MESSAGE_DISPLAY_SECS` seconds, then the stream resumes.
|
||||
|
||||
To push a message:
|
||||
|
||||
```bash
|
||||
curl -d "Body text" -H "Title: Alert title" https://ntfy.sh/your_topic
|
||||
```
|
||||
|
||||
Update `NTFY_TOPIC` in `engine/config.py` to point at your own topic. The `NtfyPoller` class is fully standalone and can be reused by other visualizers:
|
||||
### NtfyPoller
|
||||
|
||||
```python
|
||||
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()
|
||||
# in render loop:
|
||||
msg = poller.get_active_message() # returns (title, body, timestamp) or None
|
||||
|
||||
# in your render loop:
|
||||
msg = poller.get_active_message() # → (title, body, timestamp) or None
|
||||
if msg:
|
||||
title, body, ts = msg
|
||||
render_my_message(title, body) # visualizer-specific
|
||||
```
|
||||
|
||||
Dependencies: `urllib.request`, `json`, `threading`, `time` — stdlib only. The `since=` parameter is managed automatically on reconnect.
|
||||
|
||||
### MicMonitor
|
||||
|
||||
```python
|
||||
from engine.mic import MicMonitor
|
||||
|
||||
mic = MicMonitor(threshold_db=50)
|
||||
result = mic.start() # None = sounddevice unavailable; False = stream failed; True = ok
|
||||
if result:
|
||||
excess = mic.excess # dB above threshold, clamped to 0
|
||||
db = mic.db # raw RMS dB level
|
||||
```
|
||||
|
||||
Dependencies: `sounddevice`, `numpy` — both optional; degrades gracefully if unavailable.
|
||||
|
||||
### Render pipeline
|
||||
|
||||
`engine.render` exposes the OTF → raster pipeline independently of the terminal scroll loop. The planned `serve.py` extension will import it directly to pre-render headlines as 1-bit bitmaps for an ESP32 thin client:
|
||||
|
||||
```python
|
||||
# planned — serve.py does not yet exist
|
||||
from engine.render import render_line, big_wrap
|
||||
from engine.fetch import fetch_all
|
||||
|
||||
headlines = fetch_all()
|
||||
for h in headlines:
|
||||
rows = big_wrap(h.text, font, width=800) # list of half-block rows
|
||||
# threshold to 1-bit, pack bytes, serve over HTTP
|
||||
```
|
||||
|
||||
See `Mainline Renderer + ntfy Message Queue for ESP32.md` for the full server + thin client architecture.
|
||||
|
||||
---
|
||||
|
||||
## Ideas / Future
|
||||
## Development
|
||||
|
||||
### Setup
|
||||
|
||||
Requires Python 3.10+ and [uv](https://docs.astral.sh/uv/).
|
||||
|
||||
```bash
|
||||
uv sync # minimal (no mic)
|
||||
uv sync --all-extras # with mic support (sounddevice + numpy)
|
||||
uv sync --all-extras --group dev # full dev environment
|
||||
```
|
||||
|
||||
### Tasks
|
||||
|
||||
With [mise](https://mise.jdx.dev/):
|
||||
|
||||
```bash
|
||||
mise run test # run test suite
|
||||
mise run test-cov # run with coverage report
|
||||
mise run lint # ruff check
|
||||
mise run lint-fix # ruff check --fix
|
||||
mise run format # ruff format
|
||||
mise run run # uv run mainline.py
|
||||
mise run run-poetry # uv run mainline.py --poetry
|
||||
mise run run-firehose # uv run mainline.py --firehose
|
||||
```
|
||||
|
||||
### Testing
|
||||
|
||||
Tests live in `tests/` and cover `config`, `filter`, `mic`, `ntfy`, `sources`, and `terminal`.
|
||||
|
||||
```bash
|
||||
uv run pytest
|
||||
uv run pytest --cov=engine --cov-report=term-missing
|
||||
```
|
||||
|
||||
### Linting
|
||||
|
||||
```bash
|
||||
uv run ruff check engine/ mainline.py
|
||||
uv run ruff format engine/ mainline.py
|
||||
```
|
||||
|
||||
Pre-commit hooks run lint automatically via `hk`.
|
||||
|
||||
---
|
||||
|
||||
## Roadmap
|
||||
|
||||
### Performance
|
||||
- **Concurrent feed fetching** — startup currently blocks sequentially on ~25 HTTP requests; `concurrent.futures.ThreadPoolExecutor` would cut load time to the slowest single feed
|
||||
@@ -154,5 +255,4 @@ msg = poller.get_active_message() # returns (title, body, timestamp) or None
|
||||
|
||||
---
|
||||
|
||||
*macOS only (script/system font paths for translation are hardcoded). Primary display font is user-selectable via the bundled `fonts/` picker. Python 3.9+.*
|
||||
# test
|
||||
*macOS only (script/system font paths for translation are hardcoded). Primary display font is user-selectable via the bundled `fonts/` picker. Python 3.10+.*
|
||||
|
||||
145
docs/superpowers/specs/2026-03-15-readme-update-design.md
Normal file
145
docs/superpowers/specs/2026-03-15-readme-update-design.md
Normal file
@@ -0,0 +1,145 @@
|
||||
# README Update Design — 2026-03-15
|
||||
|
||||
## Goal
|
||||
|
||||
Restructure and expand `README.md` to:
|
||||
1. Align with the current codebase (Python 3.10+, uv/mise/pytest/ruff toolchain, 6 new fonts)
|
||||
2. Add extensibility-focused content (`Extending` section)
|
||||
3. Add developer workflow coverage (`Development` section)
|
||||
4. Improve navigability via top-level grouping (Approach C)
|
||||
|
||||
---
|
||||
|
||||
## Proposed Structure
|
||||
|
||||
```
|
||||
# MAINLINE
|
||||
> tagline + description
|
||||
|
||||
## Using
|
||||
### Run
|
||||
### Config
|
||||
### Feeds
|
||||
### Fonts
|
||||
### ntfy.sh
|
||||
|
||||
## Internals
|
||||
### How it works
|
||||
### Architecture
|
||||
|
||||
## Extending
|
||||
### NtfyPoller
|
||||
### MicMonitor
|
||||
### Render pipeline
|
||||
|
||||
## Development
|
||||
### Setup
|
||||
### Tasks
|
||||
### Testing
|
||||
### Linting
|
||||
|
||||
## Roadmap
|
||||
|
||||
---
|
||||
*footer*
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Section-by-section design
|
||||
|
||||
### Using
|
||||
|
||||
All existing content preserved verbatim. Two changes:
|
||||
- **Run**: add `uv run mainline.py` as an alternative invocation; expand bootstrap note to mention `uv sync` / `uv sync --all-extras`
|
||||
- **ntfy.sh**: remove `NtfyPoller` reuse code example (moves to Extending); keep push instructions and topic config
|
||||
|
||||
Subsections moved into Using (currently standalone):
|
||||
- `Feeds` — it's configuration, not a concept
|
||||
- `ntfy.sh` (usage half)
|
||||
|
||||
### Internals
|
||||
|
||||
All existing content preserved verbatim. One change:
|
||||
- **Architecture**: append `tests/` directory listing to the module tree
|
||||
|
||||
### Extending
|
||||
|
||||
Entirely new section. Three subsections:
|
||||
|
||||
**NtfyPoller**
|
||||
- Minimal working import + usage example
|
||||
- Note: stdlib only dependencies
|
||||
|
||||
```python
|
||||
from engine.ntfy import NtfyPoller
|
||||
|
||||
poller = NtfyPoller("https://ntfy.sh/my_topic/json?since=20s&poll=1")
|
||||
poller.start()
|
||||
|
||||
# in your render loop:
|
||||
msg = poller.get_active_message() # → (title, body, timestamp) or None
|
||||
if msg:
|
||||
title, body, ts = msg
|
||||
render_my_message(title, body) # visualizer-specific
|
||||
```
|
||||
|
||||
**MicMonitor**
|
||||
- Minimal working import + usage example
|
||||
- Note: sounddevice/numpy optional, degrades gracefully
|
||||
|
||||
```python
|
||||
from engine.mic import MicMonitor
|
||||
|
||||
mic = MicMonitor(threshold_db=50)
|
||||
if mic.start(): # returns False if sounddevice unavailable
|
||||
excess = mic.excess # dB above threshold, clamped to 0
|
||||
db = mic.db # raw RMS dB level
|
||||
```
|
||||
|
||||
**Render pipeline**
|
||||
- Brief prose about `engine.render` as importable pipeline
|
||||
- Minimal sketch of serve.py / ESP32 usage pattern
|
||||
- Reference to `Mainline Renderer + ntfy Message Queue for ESP32.md`
|
||||
|
||||
### Development
|
||||
|
||||
Entirely new section. Four subsections:
|
||||
|
||||
**Setup**
|
||||
- Hard requirements: Python 3.10+, uv
|
||||
- `uv sync` / `uv sync --all-extras` / `uv sync --group dev`
|
||||
|
||||
**Tasks** (via mise)
|
||||
- `mise run test`, `test-cov`, `lint`, `lint-fix`, `format`, `run`, `run-poetry`, `run-firehose`
|
||||
|
||||
**Testing**
|
||||
- Tests in `tests/` covering config, filter, mic, ntfy, sources, terminal
|
||||
- `uv run pytest` and `uv run pytest --cov=engine --cov-report=term-missing`
|
||||
|
||||
**Linting**
|
||||
- `uv run ruff check` and `uv run ruff format`
|
||||
- Note: pre-commit hooks run lint via `hk`
|
||||
|
||||
### Roadmap
|
||||
|
||||
Existing `## Ideas / Future` content preserved verbatim. Only change: rename heading to `## Roadmap`.
|
||||
|
||||
### Footer
|
||||
|
||||
Update `Python 3.9+` → `Python 3.10+`.
|
||||
|
||||
---
|
||||
|
||||
## Files changed
|
||||
|
||||
- `README.md` — restructured and expanded as above
|
||||
- No other files
|
||||
|
||||
---
|
||||
|
||||
## What is not changing
|
||||
|
||||
- All existing prose, examples, and config table values — preserved verbatim where retained
|
||||
- The Ideas/Future content — kept intact under the new Roadmap heading
|
||||
- The cyberpunk voice and terse style of the existing README
|
||||
154
docs/superpowers/specs/2026-03-16-code-scroll-design.md
Normal file
154
docs/superpowers/specs/2026-03-16-code-scroll-design.md
Normal file
@@ -0,0 +1,154 @@
|
||||
# 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,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)
|
||||
|
||||
@@ -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 ──────────────────────────────────
|
||||
|
||||
67
engine/fetch_code.py
Normal file
67
engine/fetch_code.py
Normal file
@@ -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 '<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
|
||||
BIN
fonts/Kapiler.otf
Normal file
BIN
fonts/Kapiler.otf
Normal file
Binary file not shown.
BIN
fonts/Kapiler.ttf
Normal file
BIN
fonts/Kapiler.ttf
Normal file
Binary file not shown.
35
tests/test_fetch_code.py
Normal file
35
tests/test_fetch_code.py
Normal file
@@ -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}"
|
||||
Reference in New Issue
Block a user