Add mdview markdown viewer, reorganize tests/examples and libc layout
- Split tests/ (libc feature tests) and examples/ (real apps); shared
app.mk in repo root, was examples/example.mk
- libc/io/* split into libc/{conio,env,errno,file,mouse,string,sys,
time,video}/ — clearer module boundaries
- New examples/mdview/: markdown viewer (Phases 1-5 + light nested
lists). Headers (H1-H4), HR, ulist/olist/quote with nesting via
leading spaces, fenced code blocks, inline emphasis (bold/italic/
underscore/code), wrap/unwrap mode with soft wrap (F2), horizontal
pan (← →) with '>' truncation indicator
- libc additions: scroll() in conio (ESTEX SCROLL), strlwr/strupr,
gets() test
- Makefile updates across tests/ for the new shared app.mk path
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,527 @@
|
||||
# План: текстовый 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*/fread/fgets — не использовать
|
||||
- Максимум 16 KB на один файл (одна EMM-страница); многостраничный режим — в v2
|
||||
- Подсветка через цвет: **размер заголовка** → цвет шрифта; **bold/italic** → цвет фона (моноширинный фонт без жирного/курсивного начертания)
|
||||
|
||||
---
|
||||
|
||||
## Архитектура
|
||||
|
||||
### Раскладка экрана (80×32, текст mode 0x03)
|
||||
|
||||
```
|
||||
Row 0: ┃ mdview.md L 1-30 / 142 21% ┃ ← status bar (BG=blue, FG=white)
|
||||
Row 1: ┃ ┃
|
||||
... ┃ document viewport (30 rows) ┃
|
||||
Row 30: ┃ ┃
|
||||
Row 31: ┃ F1 Help 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-страницами:
|
||||
├── file page: исходный текст .md файла (16 KB)
|
||||
└── cache page: кэш отформатированных строк (Phase 3+)
|
||||
```
|
||||
|
||||
Почему small + W3:
|
||||
- 32 KB на код+данные с большим запасом → нет банкинга
|
||||
- W3 — стандартный paged window, под него у нас уже есть `bank_io_w3` API
|
||||
- v2-расширение (multi-page файлы >16 KB): просто `mem_alloc_pages(N)` и переключение страниц в W3 через `sprinter_page_w3()`
|
||||
|
||||
Статики (в 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: один активный стиль одновременно (без вложенности); конфликтующий маркер при чужом активном стиле уходит литералом. Состояние сбрасывается на каждой строке.
|
||||
|
||||
> **Кэш отформатированных строк** — отложен в самый конец, см. "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" когда выключен).
|
||||
|
||||
**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: многостраничное хранение через `mem_alloc_pages(N)`, ленивая загрузка страниц в W3 при доступе.
|
||||
- Search (Find / Find next) — F3 / F4.
|
||||
- Toggle highlight on/off — F2.
|
||||
- Word wrap.
|
||||
- Image alt-text rendering.
|
||||
- Tables.
|
||||
Reference in New Issue
Block a user