0dedc4dac8
- prebuild.
571 lines
33 KiB
Markdown
571 lines
33 KiB
Markdown
# План: текстовый Markdown viewer для Sprinter (`examples/mdview`)
|
||
|
||
## Context
|
||
|
||
Тестовая крупная задача — проверить нашу libc на нетривиальном interactive-приложении (полноэкранный UI, файловый I/O, парсер). Параллельно даст хороший showcase платформы и поможет вытащить недоделки в conio/io. Конечная цель: viewer для `.md` файлов с подсветкой синтаксиса, навигацией по тексту и постраничным скроллингом.
|
||
|
||
**Ограничения v1 (зафиксированы пользователем):**
|
||
- POSIX file API (open/read/lseek/close); FILE pointer / fread / fgets — не использовать
|
||
- Подсветка через цвет: **размер заголовка** → цвет шрифта; **bold/italic** → цвет фона (моноширинный фонт без жирного/курсивного начертания)
|
||
|
||
**Расширения, реализованные после плана:**
|
||
- Файлы до 128 KB (1–8 EMM-страниц, lazy map в W3 через `fb()/map_page()`) — изначально было в v2
|
||
- Анимированный spinner и предварительная отрисовка UI при старте — UX
|
||
- Inline emphasis: `_`/`*`/`**` подчиняются XOR flanking-правилу (whitespace ровно с одной стороны), `COLOR_YELLOW` и `2 * 3` остаются литералами
|
||
|
||
---
|
||
|
||
## Текущий статус (2026-06-05)
|
||
|
||
| Phase | Статус | Комментарий |
|
||
|---|---|---|
|
||
| 1 Plain text + nav | ✓ | загрузка, индексация, status/menu, ↑↓/PgUp/PgDn/Home/End/F1/F10/Esc |
|
||
| 2 Headers + HR | ✓ | H1..H4, `---`/`***`/`___` (с ≥3 marker'ов) |
|
||
| 3 Inline emphasis | ✓ | `**` / `*` / `_` / `` ` ``; XOR flanking (см. выше) |
|
||
| 4 Lists / quote / fenced code | ✓ | `- / * / +`, `N. / N)`, `> `, ``` ``` ```; light nested lists |
|
||
| 4-tables | ✗ | таблицы отложены вместе с Phase 6 |
|
||
| 5 Wrap / Unwrap (F2) | ✓ | wrap-by-default; soft wrap; F2 переключает; hpan ←/→ в truncate-режиме |
|
||
| 6 Полный layout таблиц | ✗ | deferred |
|
||
| 7 Links + search | ✗ | deferred |
|
||
| 8 F8 Raw toggle | ✗ | deferred |
|
||
| Cache рендеренных строк | ✗ | не нужно по скорости |
|
||
|
||
**UX-поправки (отдельно от phase-плана, 2026-06-05):**
|
||
- UI (menu + title bar) отрисовывается ДО `load_file`/`index_lines` — пользователь сразу видит интерфейс, а не чёрный экран
|
||
- Title bar: `MDVIEW <spinner> <filename>` (3 пробела между MDVIEW и filename, slot спиннера — col 8)
|
||
- Spinner крутится во время `load_file` (по странице) и `index_lines` (раз в 32 логических строки), включая `toggle_wrap`
|
||
|
||
---
|
||
|
||
## Архитектура
|
||
|
||
### Раскладка экрана (80×32, текст mode 0x03)
|
||
|
||
```
|
||
Row 0: ┃ MDVIEW │ mdview.md │ L 1-30 / 142 │ 21% ┃ ← status bar (BG=blue, FG=white)
|
||
▲ ▲
|
||
│ └── filename @ col 10
|
||
└────────── spinner slot @ col 8 (anim. while busy)
|
||
Row 1: ┃ ┃
|
||
... ┃ document viewport (30 rows) ┃
|
||
Row 30: ┃ ┃
|
||
Row 31: ┃ F1 Help F2 Wrap F10 Exit ┃ ← menu bar (BG=blue, FG=cyan)
|
||
```
|
||
|
||
- Viewport = 30 строк × 80 столбцов.
|
||
- Status / menu рендерятся через `wrchar()` (без авто-скролла), viewport — через `LOCATE` + посимвольный `wrchar()` (тоже без авто-скролла, ставит и char и attr за один call).
|
||
|
||
### Память
|
||
|
||
Memory mode: **`small`** — DSS отводит под наш образ **два банка (W1 + W2)** = 32 KB суммарно (CODE в W1, DATA + STACK + HEAP в W2). Этого должно хватить чтобы НЕ заводить `__banked` функции. W3 остаётся полностью свободным для маппинга больших буферов:
|
||
|
||
```
|
||
W0 (0x0000-0x3FFF): ESTEX (system, untouchable)
|
||
W1 (0x4000-0x7FFF): CODE (small mode page 1)
|
||
W2 (0x8000-0xBFFF): DATA + STACK + HEAP (small mode page 2)
|
||
W3 (0xC000-0xFFFF): paged window — лениво переключается между EMM-страницами
|
||
файла (до 8 страниц = 128 KB). `cur_page` кэширует
|
||
текущую mapping, `fb(p)` маппит нужную страницу при
|
||
первом обращении.
|
||
```
|
||
|
||
Почему small + W3:
|
||
- 32 KB на код+данные с большим запасом → нет банкинга
|
||
- W3 — стандартный paged window, под него у нас уже есть `bank_io_w3` API
|
||
- Файлы до 128 KB поддерживаются нативно: `mem_alloc_pages(pages_needed)` под весь файл; `map_page()` через `sprinter_page_w3()`; `fb(p)` — единая точка доступа из индексатора и рендера.
|
||
|
||
Статики (в W2):
|
||
- `line_offset[MAX_LINES]` — uint16_t смещение каждой строки в файловой странице (4 KB на 2048 строк)
|
||
- `cache_tag[CACHE_N]` — uint16_t тег слота (200 байт на 100 слотов, появляется в Phase 3)
|
||
- `filename[64]`, `top_line`, `total_lines`, `file_size`, `file_blk`, `cache_blk` — единицы байт
|
||
|
||
**FILE_BUF**: `((char*)0xC000)` — фиксированная адресация в W3 после маппинга нужной страницы.
|
||
|
||
### Поток данных
|
||
|
||
```
|
||
main → open() → read() chunks 1KB → write to W1 (mapped EMM page) → close()
|
||
→ index_lines() (одно сканирование, заполняет line_offset[])
|
||
→ render_viewport() + main loop { getkey(); handle(); render_status(); render_viewport() }
|
||
```
|
||
|
||
**Тонкость с `read()`**: ESTEX READ записывает по dst-указателю в адресном пространстве вызывающего. Поскольку мы маппим EMM-страницу в W3 (0xC000) ДО вызова read(), указатель 0xC000+offset валиден. Если выяснится что BIOS трогает W3 во время read (графический видеобуфер по умолчанию в W3 при графических режимах, но в текстовом — должен быть свободен) — fallback: читать в 1 KB буфер в W2 и копировать в W3 через `bank_write_w3()`.
|
||
|
||
---
|
||
|
||
## Реализация — поэтапная
|
||
|
||
### Phase 1 — Plain text viewer (MVP) ✓
|
||
|
||
**Что работает:**
|
||
- Загрузка файла (`open/read/close`) в W1-страницу
|
||
- Индексация строк (LF / CRLF разделители)
|
||
- Status bar: имя файла, L N-M / Total, процент скроллинга
|
||
- Menu bar: `F1 Help F10 Exit`
|
||
- Навигация: ↑/↓ (1 строка), PgUp/PgDn (30 строк), Home/End (начало/конец), Esc/F10 (выход), F1 (help screen)
|
||
- Обрезка строк длиннее 80 символов (без word-wrap)
|
||
- Цвета: текст белый на чёрном; status/menu — белый на синем
|
||
|
||
**Критичные файлы:**
|
||
- `examples/mdview/mdview.c` — main, key loop, rendering, indexing (one-file MVP)
|
||
- `examples/mdview/Makefile`
|
||
- `examples/mdview/SAMPLE.MD` — тестовый markdown файл
|
||
|
||
### Phase 2 — Headers и горизонтальная линия ✓
|
||
|
||
**MD фичи:**
|
||
- `# H1` → ярко-жёлтый (COLOR_YELLOW = 14) на чёрном
|
||
- `## H2` → ярко-голубой (COLOR_LBLUE = 11)
|
||
- `### H3` → ярко-зелёный (COLOR_LGREEN = 10)
|
||
- `#### H4+` → серый (COLOR_GREY = 8)
|
||
- `---` / `***` на отдельной строке → линия 0xC4 (горизонтальная рамка ASCII) во всю ширину
|
||
|
||
### Phase 3 — Inline emphasis ✓
|
||
|
||
**Парсер inline (per-line, runs в одну строку):**
|
||
- `**bold**` → ATTR_TEXT_BOLD
|
||
- `*italic*` → ATTR_TEXT_ITALIC
|
||
- `_underscore_` → ATTR_TEXT_UNDERSORE
|
||
- `` `code` `` → ATTR_TEXT_CODE
|
||
- Маркеры `**`/`*`/`_`/`` ` `` НЕ рендерятся (съедаются)
|
||
|
||
State machine: один активный стиль одновременно (без вложенности); конфликтующий маркер при чужом активном стиле всё равно консьюмится (zero-width) для синхронизации ширины с index_lines. Состояние сбрасывается на каждой строке.
|
||
|
||
**Flanking-правило (CommonMark intraword, реализовано после изначального плана):**
|
||
- `*` / `**` / `_` считаются markdown-маркером только если whitespace/EOL
|
||
ровно с ОДНОЙ стороны (XOR).
|
||
- Случаи "оба whitespace" (`2 * 3`, `2 ** 3`) → литералы (арифметика).
|
||
- Случаи "ни одного whitespace" (`COLOR_YELLOW`, `FILE*/fread`, `foo*bar*baz`)
|
||
→ литералы (intraword).
|
||
- Backtick (`` ` ``) flanking НЕ требует — `` `code` `` работает без пробелов.
|
||
- Правило применено симметрично в 4 местах (`index_lines`, cont-render,
|
||
основной inline-парсер, truncation peek), иначе wrap-индексатор и
|
||
рендер разъедутся по ширине.
|
||
|
||
> **Кэш отформатированных строк** — отложен в самый конец, см. "Phase ∞: оптимизации".
|
||
> Скорости текущего наивного рендера хватает на 80×30 = 2400 wrchar / кадр; PgUp/PgDn визуально мгновенен.
|
||
|
||
### Phase 4 — Block elements ✓ (без таблиц)
|
||
|
||
- Маркированные списки: `- foo`, `* foo`, `+ foo` → префикс `•` (0x07) + пробел; цвет маркера ярче основного
|
||
- Нумерованные списки: `1. foo`, `2. foo` → как есть (число оставляем)
|
||
- Blockquote: строки с `> ` → префикс `│` (0xB3) серого цвета, остальной текст слегка приглушённый
|
||
- Fenced code blocks: `` ``` `` открывает/закрывает блок; все строки между — bg=серый, моноширинно (без inline-парсинга)
|
||
- Indented code blocks (4+ пробелов): аналогично fenced, но без явного маркера
|
||
|
||
**Light nested lists (v1 — реализовано):**
|
||
- `classify_line()` пропускает leading spaces перед ulist/olist/quote маркером,
|
||
возвращает `content_off` после маркера → `content_off - p_start` = indent + marker
|
||
бит в visible col.
|
||
- `render_line()` рисует leading-spaces в `ATTR_TEXT`, потом маркер на сдвинутой
|
||
позиции (col = indent). Marker всё ещё фиксирован при горизонтальном pan'е.
|
||
- HR / header / fence delim остаются строго col-0 (CommonMark разрешает до 3
|
||
ведущих пробелов для них — упростили).
|
||
- Tab-indent → не распознаётся как nesting (только spaces).
|
||
|
||
**Phase 4-full — полная поддержка вложенности (deferred):**
|
||
- **Tab-indent**: считать tab = 4 пробела для определения уровня.
|
||
- **Quote nesting** (`> > foo`): каждый `>` подряд = +1 уровень, каждый рисуется
|
||
отдельным `│` в `ATTR_QUOTE_MARKER` (визуальная "лестница" слева).
|
||
- **Hanging indent в wrap-continuation**: когда `- some very long bullet text
|
||
that wraps...` — continuation seg должен начинаться от content-col (после
|
||
маркера), а не от col 0. Сейчас continuation идёт от col 0 (v1 simplification).
|
||
Требует хранить `marker_width` per логическая строка (8 бит) или re-classify
|
||
first seg при рендере continuation.
|
||
- **Lazy continuation**: строки без маркера, но с правильным indent под
|
||
предыдущим bullet'ом, должны считаться продолжением того bullet'а
|
||
(визуально — общий attr).
|
||
- **Strict CommonMark indent rules**: вложенный пункт должен быть на indent
|
||
≥ content_col родителя, иначе считается breakout. Нужен мини-stack
|
||
активных списков при индексации.
|
||
|
||
### Phase 5 — Wrap / Unwrap длинных строк ✓
|
||
|
||
Дефолт: **wrap on**. F2 переключает; в меню-баре подпись отражает действие
|
||
("Unwrap" когда wrap включён, "Wrap" когда выключен). Во время реиндексации
|
||
крутится спиннер на title bar.
|
||
|
||
**v1 — реализовано:**
|
||
- Один массив `line_offset[2048]` хранит ВИДИМЫЕ сегменты (а не логические
|
||
строки); биты 0..13 — байтовое смещение, бит 15 — CONT-флаг continuation.
|
||
- Wrap-режим: soft wrap на последнем пробеле ≤ 80; hard fallback если
|
||
пробела нет.
|
||
- Маркеры эмфазиса (`**`/`*`/`_`/`` ` ``) и header-префиксы (`#`/`##`/...) не
|
||
учитываются в visible-col при поиске точки переноса.
|
||
- "Специальные" логические строки не wrap'аются вообще (одна seg-запись на
|
||
логическую строку): fence delim, table row (header/separator/body), HR.
|
||
- F2 toggle сохраняет визуальную позицию через `top_offset` в FILE_BUF.
|
||
- Bitmaps (`in_code`/`in_table`/`is_tab_hdr`) перестраиваются вместе с
|
||
сегментами, индексируются seg-индексом, биты ставятся только на первом
|
||
seg'е логической строки.
|
||
- Continuation-сегменты рендерятся в стиле "v1: плоско" — plain text,
|
||
никаких markdown-классификаций; padding до конца строки `ATTR_TEXT`.
|
||
|
||
**v1.5 — отложено для полной картины wrap:**
|
||
- **Hanging indent**: continuation от ulist/olist/quote должен выравниваться
|
||
под content, а не от col 0. Требует хранить marker_width per логическая
|
||
строка ИЛИ re-classify первого seg'а при рендере continuation.
|
||
- **Наследование base_attr**: continuation от header'а должен сохранять
|
||
цвет; continuation от code body — фон ATTR_TEXT_CODE. Требует хранить
|
||
1 байт `base_attr` per seg ИЛИ lookup первого seg'а.
|
||
- **Inline emphasis через границу**: эмфазис, открытый в первом seg'е и не
|
||
закрытый, должен продолжаться во втором. Требует хранить emph state per
|
||
seg (3 бита).
|
||
- Compact way: добавить параллельный массив `seg_meta[MAX_SEGS]` по 1 байту
|
||
— пакует marker_width (4 бита) + emph_state (3 бита) + base_attr_idx
|
||
(4 бита из таблицы → нужен 2-байтовый seg_meta).
|
||
- **Hpan для длинных строк**: если wrap выключен, добавить ←/→ для
|
||
горизонтального скролла >80 cols. Общий механизм с tables (Phase 6).
|
||
**РЕАЛИЗОВАНО** (light) — `viewport_x` + полный re-render на каждое ←/→.
|
||
- **Ускорение hpan через ESTEX WINCOPY/WINREST** (deferred): сейчас pan
|
||
делает полный `render_viewport()` = 30 строк × 80 wrchar. Можно
|
||
скопировать существующее содержимое viewport'а на N cols влево/вправо
|
||
через win-copy, потом рендерить только узкую полосу справа/слева
|
||
(HPAN_STEP cols × 30 rows ≈ 240 wrchar вместо 2400). ESTEX SCROLL
|
||
горизонталь не поддерживает — нужна именно WINCOPY-операция или
|
||
rdchar/wrchar loop. Активировать когда ощутится тормоз; сейчас на
|
||
типовом markdown'е не заметно.
|
||
|
||
**v2 — отдельная фича, мимо wrap:**
|
||
- Toggle подсветки целиком (F3?)
|
||
- Search по тексту (Ctrl+F / F4)
|
||
- Links `[text](url)` → синий подчёркнутый text, url прячется
|
||
- Images `` → `[IMG: alt]`
|
||
|
||
### Phase 7 — Links и поиск (post-v1)
|
||
|
||
- `[text](url)` → отрисовать только `text` с ярко-синим FG (визуально подчёркнутое)
|
||
- `` → `[IMG: alt]` в скобках
|
||
- Search по тексту (F3 / Ctrl+F): инкрементальный, подсветка совпадений
|
||
|
||
### Phase 8 — F8 Raw / Render toggle
|
||
|
||
Переключатель режима отображения: при включённом Raw показывается исходный
|
||
текст файла как есть — все markdown-маркеры (`#`, `**`, `_`, `` ` ``, `|`,
|
||
`>`, `-`, etc.) рендерятся литералами с `ATTR_TEXT`, без классификации.
|
||
Полезно когда:
|
||
- нужно увидеть точную разметку (отлаживание .md, скриншоты, копирование)
|
||
- markdown-классификатор ошибся и хочется увидеть оригинал
|
||
- хочется быстро сравнить "до/после" рендера
|
||
|
||
**Поведение:**
|
||
- F8 переключает `render_mode` (1=render, 0=raw); меню показывает обратное
|
||
действие ("Raw" когда сейчас render, "Render" когда сейчас raw) — той же
|
||
логикой что F2/Wrap/Unwrap.
|
||
- В Raw режиме: `render_line()` идёт по короткому пути — никакого
|
||
`classify_line`, `is_fence_delim`, `is_code_body`, inline-эмфазиса; просто
|
||
байтовый дамп FILE_BUF от seg-offset до next-seg/EOL с tab-expansion и
|
||
ATTR_TEXT.
|
||
- Раздельно от F2: оба режима независимы (можно Raw+Wrap, Raw+Truncate,
|
||
Render+Wrap, Render+Truncate). Wrap-логика в `index_lines` работает в
|
||
обоих случаях одинаково (опирается на визуальные колонки независимо от
|
||
раскраски).
|
||
- Статус-бар: добавить индикатор `[R]` / `[V]` (Raw / View) или текстом
|
||
`RAW` рядом с именем файла.
|
||
|
||
**Минимальная реализация:**
|
||
- Один новый static `uint8_t render_mode = 1;`
|
||
- В `render_line()`: на самом верху `if (!render_mode) { … raw render … return; }`
|
||
- В `render_menu()`: добавить ярлык F8 рядом с F2.
|
||
- В главном цикле: `case KEY_F8: toggle_render(); break;`
|
||
- `toggle_render()` отличается от `toggle_wrap()` тем, что НЕ перестраивает
|
||
`line_offset[]` (wrap-сегментация не меняется), только перерендерит экран.
|
||
|
||
---
|
||
|
||
## API-новинки в libc (минимальные)
|
||
|
||
### `getkey()` — extended key reader (в libc)
|
||
|
||
Текущий `getch()` теряет scan code расширенных клавиш (возвращает только E=ASCII). Добавляем **сразу в `libc/conio/conio.c`** новую функцию рядом с `getch()`:
|
||
|
||
```c
|
||
// Returns scan in high byte, ASCII in low byte.
|
||
// Extended keys (arrows, F-keys, PgUp/PgDn, Home/End): ASCII=0, scan code в high byte.
|
||
// Plain keys: ASCII в low byte; high byte содержит positional scan (бит 7 = Ctrl/Alt/Shift modifier).
|
||
uint16_t getkey(void) __naked {
|
||
__asm
|
||
ld c, #0x30 ; ESTEX WAITKEY
|
||
rst #0x10 ; A=ASCII, D=scan, E=ASCII
|
||
ld e, a ; ensure E=ASCII even if E clobbered
|
||
ret ; SDCC __sdcccall(1): возврат uint16_t в DE (D=scan, E=ASCII)
|
||
__endasm;
|
||
}
|
||
```
|
||
|
||
Дополнительно — **в `libc/include/conio.h`** прописать прототип и константы scan-кодов:
|
||
|
||
```c
|
||
uint16_t getkey(void);
|
||
|
||
/* Scan codes for getkey() high byte when ASCII=0 (extended keys). */
|
||
#define KEY_F1 0x0E
|
||
#define KEY_F2 0x0F
|
||
#define KEY_F3 0x10
|
||
#define KEY_F4 0x11
|
||
#define KEY_F5 0x12
|
||
#define KEY_F6 0x13
|
||
#define KEY_F7 0x14
|
||
#define KEY_F8 0x15
|
||
#define KEY_F9 0x16
|
||
#define KEY_F10 0x17
|
||
#define KEY_F11 0x18
|
||
#define KEY_F12 0x19
|
||
#define KEY_END 0x24
|
||
#define KEY_DOWN 0x25
|
||
#define KEY_PGDN 0x26
|
||
#define KEY_LEFT 0x27
|
||
#define KEY_RIGHT 0x29
|
||
#define KEY_HOME 0x2A
|
||
#define KEY_UP 0x2B
|
||
#define KEY_PGUP 0x2C
|
||
#define KEY_INS 0x23
|
||
#define KEY_DEL 0x22
|
||
```
|
||
|
||
**Скан-коды (из docs/converted/ProgrammerManual.txt:2143-2323):**
|
||
| Клавиша | scan | Клавиша | scan |
|
||
|---|---|---|---|
|
||
| F1 | 0x0E | Up | 0x2B |
|
||
| F10 | 0x17 | Down | 0x25 |
|
||
| F11 | 0x18 | Left | 0x27 |
|
||
| F12 | 0x19 | Right | 0x29 |
|
||
| | | PgUp | 0x2C |
|
||
| | | PgDn | 0x26 |
|
||
| | | Home | 0x2A |
|
||
| | | End | 0x24 |
|
||
|
||
### Что **переиспользуем** из существующей libc
|
||
|
||
- `open/read/lseek/close` — `libc/io/{open,read,lseek}.c` (POSIX wrappers)
|
||
- `mem_alloc_pages/mem_free_block/mem_get_page` — `libc/mem/mem_alloc.c`
|
||
- `sprinter_page_w3()` — inline `__sfr` write в `libc/include/sprinter.h:113`
|
||
- `bank_read_w3/bank_write_w3` — `libc/mem/bank_io_w3.c` (для fallback или v2 multi-page)
|
||
- `wrchar(x, y, ch, attr)` — `libc/conio/conio.c:476` (без auto-scroll, идеально для viewport)
|
||
- `clrscr_attr(attr)` — `libc/conio/conio.c:395`
|
||
- `gotoxy/wherex/wherey` — `libc/conio/conio.c:412-462` (если нужно)
|
||
- `kbhit()` — `libc/conio/conio.c:22` (для non-blocking опроса, опционально)
|
||
- `dec16/dec8` — `libc/stdio/dec_print.c` (для status bar: текущая строка / total / %)
|
||
- `COLOR(fg, bg)` макрос — `libc/include/conio.h:152`
|
||
- Цветовые константы `COLOR_*` — `libc/include/conio.h:145`
|
||
- `strlen/memcpy/memset` — z80.lib (НЕ переписывать)
|
||
|
||
---
|
||
|
||
## Структура исходников
|
||
|
||
```
|
||
examples/mdview/
|
||
├── Makefile # стандартный pattern (см. examples/cat/Makefile)
|
||
├── mdview.c # Phase 1: всё в одном файле (main, keys, indexing, render)
|
||
├── SAMPLE.MD # тестовый markdown
|
||
└── README.md # описание и controls
|
||
```
|
||
|
||
После Phase 3 раскидать по модулям (если суммарный размер > ~6KB):
|
||
```
|
||
mdview.c — main loop, status/menu bars, key dispatch
|
||
mdrender.c — line rendering with MD inline parser
|
||
mdindex.c — file load + line indexing
|
||
```
|
||
|
||
### Сборка
|
||
|
||
```makefile
|
||
PROJ ?= ../..
|
||
SPRINTER_CC := $(PROJ)/bin/sprinter-cc
|
||
mdview.exe: mdview.c
|
||
$(SPRINTER_CC) --memory small -o $@ mdview.c
|
||
```
|
||
|
||
`--memory small`: код+данные в W2 (DSS даёт нужное число страниц); файл — отдельная EMM-страница в W3.
|
||
|
||
---
|
||
|
||
## Структура mdview.c (Phase 1, эскиз)
|
||
|
||
```c
|
||
#include <stdint.h>
|
||
#include <stdio.h>
|
||
#include <conio.h>
|
||
#include <fcntl.h>
|
||
#include <unistd.h>
|
||
#include <sprinter.h>
|
||
#include <sprinter_mem.h>
|
||
|
||
#define VIEW_TOP 1
|
||
#define VIEW_BOT 30 // inclusive
|
||
#define VIEW_H 30
|
||
#define SCREEN_W 80
|
||
#define MAX_LINES 2048
|
||
#define FILE_BUF ((char*)0xC000) /* W3 — EMM page mapped here */
|
||
#define TAB_STOP 4
|
||
|
||
#define ATTR_TEXT COLOR(COLOR_WHITE, COLOR_BLACK)
|
||
#define ATTR_BAR COLOR(COLOR_WHITE, COLOR_BLUE)
|
||
#define ATTR_MENU_K COLOR(COLOR_YELLOW, COLOR_BLUE)
|
||
#define ATTR_MENU_T COLOR(COLOR_WHITE, COLOR_BLUE)
|
||
|
||
static uint16_t line_off[MAX_LINES];
|
||
static uint16_t n_lines;
|
||
static uint16_t top_line;
|
||
static uint16_t file_size;
|
||
static uint8_t file_blk;
|
||
static char filename[64];
|
||
|
||
static int load_file(const char *path); // open, alloc EMM page, map W3, read, close
|
||
static void index_lines(void); // scan FILE_BUF, fill line_off[]
|
||
static void render_status(void); // row 0
|
||
static void render_menu(void); // row 31
|
||
static void render_line(uint16_t idx, uint8_t row); // one line @ row
|
||
static void render_viewport(void); // VIEW_H lines starting from top_line
|
||
static void scroll_up(uint16_t n);
|
||
static void scroll_down(uint16_t n);
|
||
static void help_screen(void); // F1
|
||
|
||
int main(int argc, char **argv) {
|
||
if (argc < 2) { puts("Usage: mdview <file.md>"); return 1; }
|
||
if (load_file(argv[1]) < 0) { puts("load error"); return 1; }
|
||
index_lines();
|
||
clrscr_attr(ATTR_TEXT);
|
||
render_menu();
|
||
render_status();
|
||
render_viewport();
|
||
for (;;) {
|
||
uint16_t k = getkey();
|
||
uint8_t ascii = k & 0xFF;
|
||
uint8_t scan = (k >> 8) & 0x7F; // strip mod bit
|
||
if (ascii) {
|
||
if (ascii == 0x1B) break; // Esc → exit
|
||
continue;
|
||
}
|
||
switch (scan) {
|
||
case KEY_F10: goto exit;
|
||
case KEY_F1: help_screen(); break;
|
||
case KEY_UP: scroll_up(1); break;
|
||
case KEY_DOWN: scroll_down(1); break;
|
||
case KEY_PGUP: scroll_up(VIEW_H); break;
|
||
case KEY_PGDN: scroll_down(VIEW_H); break;
|
||
case KEY_HOME: top_line = 0;
|
||
render_viewport(); break;
|
||
case KEY_END: /* clamp to last viewport */ break;
|
||
}
|
||
render_status();
|
||
}
|
||
exit:
|
||
mem_free_block(file_blk);
|
||
clrscr_attr(ATTR_TEXT);
|
||
return 0;
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Verification
|
||
|
||
### Phase 1
|
||
1. Подготовить `SAMPLE.MD` ~5 KB (заголовки, абзацы, списки) — рендериться будет plain.
|
||
2. `cd examples/mdview && make`
|
||
3. `python make_disk.py mdview.exe SAMPLE.MD → mc.img && ./run_mame.sh`
|
||
4. Проверить:
|
||
- Status bar показывает `SAMPLE.MD L 1-30 / N X%`
|
||
- Menu bar внизу
|
||
- ↑/↓: 1 строка
|
||
- PgUp/PgDn: 30 строк, корректное clamp на границах
|
||
- Home: top_line=0
|
||
- End: top_line = total_lines - VIEW_H
|
||
- F1: показывает help, любая клавиша возвращает
|
||
- F10 / Esc: выход, экран очищен
|
||
5. Edge cases: пустой файл, файл из одной строки, файл с очень длинной строкой (>80), CRLF и LF mixed.
|
||
|
||
### Phase 2-4
|
||
Расширять `SAMPLE.MD` с фичами по мере добавления, визуально верифицировать в MAME. Скриншот-сравнение опционально.
|
||
|
||
### Регрессии
|
||
- Никаких изменений в libc на Phase 1 кроме (потенциально) добавления `getkey()` в `libc/conio/conio.c` — если так, прогнать `examples/conio2` и `examples/filetest` чтобы убедиться что ничего не сломалось.
|
||
|
||
---
|
||
|
||
## Решения по неоднозначностям
|
||
|
||
1. **Word-wrap vs truncate**: v1 = truncate (просто). v2 — F2 toggle wrap.
|
||
2. **Горизонтальный скроллинг**: v1 — нет; v2 — `←/→` сдвиг viewport по столбцам.
|
||
3. **Tab handling**: преобразование при рендере, **tabstop = 4** (стандарт MD). Оригинал в W3 не трогаем.
|
||
4. **UTF-8**: рендерим байты как есть. Если файл в CP866 — отрисуется кириллицей через системный фонт. UTF-8 — пока не поддерживаем (визуально будет каша на не-ASCII символах; детект и предупреждение — в v2).
|
||
|
||
---
|
||
|
||
### Phase 6 — Полный layout таблиц (deferred)
|
||
|
||
Сейчас (Phase 4-tables) таблицы рисуются "декоративно" — пайпы и тире
|
||
заменяются на box-drawing chars, но ширины колонок берутся как есть из
|
||
исходника. Цель Phase 6 — пересчитать таблицу в нормализованный вид:
|
||
|
||
- **Pre-scan таблицы**: пройти все строки одного table-блока, найти
|
||
максимальную ширину каждой колонки (с учётом съеденных inline-маркеров —
|
||
визуальный размер, не байтовый).
|
||
- **Re-emit в буфере**: при загрузке файла (или при первой встрече таблицы)
|
||
переписать строки в FILE_BUF так, чтобы все ячейки одной колонки имели
|
||
одинаковую ширину; добавить top/bottom рамки (`┌─┬─┐` / `└─┴─┘`) как
|
||
синтетические строки. Это позволит сохранить 1:1 соответствие "логическая
|
||
строка → одна viewport row" без специальной логики при рендере.
|
||
- **Память**: re-emit может УВЕЛИЧИТЬ файл за счёт padding и доп.рамок.
|
||
Если буфер близок к 16KB — отрезать таблицу и пометить её overflow'ом.
|
||
- **Горизонтальный скроллинг**: если итоговая ширина таблицы (или любой
|
||
строки) > SCREEN_W = 80 — добавить ←/→ для horizontal pan. Это будет
|
||
общий механизм для длинных строк (см. также wrap mode), не только таблиц.
|
||
- **Выравнивание из separator-row**: `:-` → left, `-:` → right, `:-:` →
|
||
center; учитывать при padding'е содержимого ячейки.
|
||
- **Шаги реализации**:
|
||
1. Walking pass по фенсам/таблицам прямо в `index_lines()` — собрать
|
||
extents всех таблиц.
|
||
2. Для каждой таблицы — определить ширины колонок.
|
||
3. Решение: rewrite-in-buffer (проще для рендера, но мутирует исходник)
|
||
vs render-time layout (cleaner, но требует отдельной структуры
|
||
описания layout'а на каждую таблицу).
|
||
4. Hpan: общий `viewport_x_offset` для всего экрана, или отдельный
|
||
"широкий режим" только внутри таблиц.
|
||
|
||
> Не блокирующая фича. Запускать когда станет понятен типовой источник
|
||
> markdown-файлов (узкие читалки → достаточно текущего декоратора;
|
||
> широкие README с большими таблицами → нужен полный layout).
|
||
|
||
---
|
||
|
||
### Phase ∞ — Кэш рендеренных строк (low priority)
|
||
|
||
Отложено: текущая скорость более чем достаточна. Активировать если появится
|
||
сценарий, где видна задержка PgUp/PgDn (например, при тяжёлом inline-парсере
|
||
v2 с UTF-8 / linkifier / таблицами).
|
||
|
||
**Кэш отформатированных строк** (W3, отдельная EMM-страница):
|
||
|
||
```
|
||
Cache layout (16 KB EMM page, всего 16000 байт используется):
|
||
slot 0: 80 chars + 80 attrs = 160 bytes @ offset 0
|
||
...
|
||
slot 99: 80 chars + 80 attrs = 160 bytes @ offset 15840
|
||
|
||
Cache tags (W2 static): uint16_t cache_tag[100] = 200 bytes
|
||
cache_tag[i] = line_id, или 0xFFFF = invalid
|
||
```
|
||
|
||
Стратегия — **direct map (no LRU)**: `slot = line_id % 100`. Коллизия → вытеснение.
|
||
|
||
**Batched viewport render**: 2 page-swap'а на ВЕСЬ viewport (cache → file → cache),
|
||
не 60 как при наивной реализации.
|
||
|
||
При сборке: `cache_blk = mem_alloc_pages(1)` после `file_blk`; `mem_free_block` на exit.
|
||
|
||
---
|
||
|
||
## Что отложено в v2
|
||
|
||
- ~~Файлы >16 KB~~ — **сделано в v1.5** (до 128 KB через 1–8 EMM-страниц + lazy map в W3).
|
||
- ~~Word wrap~~ — **сделано** (Phase 5, F2 toggle).
|
||
- Search (Find / Find next) — F3 / F4.
|
||
- F8 Raw / Render toggle — спецификация в Phase 8.
|
||
- Links `[text](url)` + image alt — Phase 7.
|
||
- Tables — Phase 6 (полный layout).
|
||
- Toggle highlight on/off — частный случай F8 Raw.
|