- prebuild.
33 KiB
План: текстовый 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_w3API - Файлы до 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/Makefileexamples/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_widthper логическая строка (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_attrper 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():
// 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-кодов:
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.csprinter_page_w3()— inline__sfrwrite вlibc/include/sprinter.h:113bank_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:395gotoxy/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
Сборка
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, эскиз)
#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
- Подготовить
SAMPLE.MD~5 KB (заголовки, абзацы, списки) — рендериться будет plain. cd examples/mdview && makepython make_disk.py mdview.exe SAMPLE.MD → mc.img && ./run_mame.sh- Проверить:
- 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: выход, экран очищен
- Status bar показывает
- Edge cases: пустой файл, файл из одной строки, файл с очень длинной строкой (>80), CRLF и LF mixed.
Phase 2-4
Расширять SAMPLE.MD с фичами по мере добавления, визуально верифицировать в MAME. Скриншот-сравнение опционально.
Регрессии
- Никаких изменений в libc на Phase 1 кроме (потенциально) добавления
getkey()вlibc/conio/conio.c— если так, прогнатьexamples/conio2иexamples/filetestчтобы убедиться что ничего не сломалось.
Решения по неоднозначностям
- Word-wrap vs truncate: v1 = truncate (просто). v2 — F2 toggle wrap.
- Горизонтальный скроллинг: v1 — нет; v2 —
←/→сдвиг viewport по столбцам. - Tab handling: преобразование при рендере, tabstop = 4 (стандарт MD). Оригинал в W3 не трогаем.
- 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'е содержимого ячейки. - Шаги реализации:
- Walking pass по фенсам/таблицам прямо в
index_lines()— собрать extents всех таблиц. - Для каждой таблицы — определить ширины колонок.
- Решение: rewrite-in-buffer (проще для рендера, но мутирует исходник) vs render-time layout (cleaner, но требует отдельной структуры описания layout'а на каждую таблицу).
- Hpan: общий
viewport_x_offsetдля всего экрана, или отдельный "широкий режим" только внутри таблиц.
- Walking pass по фенсам/таблицам прямо в
Не блокирующая фича. Запускать когда станет понятен типовой источник 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.