From 46553f4e07dde85a223f2bf3a4ce5e1addb95027 Mon Sep 17 00:00:00 2001 From: Alexander Petrov Date: Sun, 5 Jul 2026 19:33:34 +0300 Subject: [PATCH] =?UTF-8?q?mdview2:=20=D1=84=D0=BE=D0=BD=D0=BE=D0=B2=D0=B0?= =?UTF-8?q?=D1=8F=20=D1=81=D0=B1=D0=BE=D1=80=D0=BA=D0=B0=20=D0=B2=D1=82?= =?UTF-8?q?=D0=BE=D1=80=D0=BE=D0=B3=D0=BE=20=D0=BD=D0=B0=D0=B1=D0=BE=D1=80?= =?UTF-8?q?=D0=B0=20=D0=BA=D0=BE=D0=B4=D0=B8=D1=80=D0=BE=D0=B2=D0=BA=D0=B8?= =?UTF-8?q?=20=D0=B2=20=D0=BF=D0=B0=D1=83=D0=B7=D0=B0=D1=85=20=D0=BC=D0=B5?= =?UTF-8?q?=D0=B6=D0=B4=D1=83=20=D0=BA=D0=BB=D0=B0=D0=B2=D0=B8=D1=88=D0=B0?= =?UTF-8?q?=D0=BC=D0=B8;=20=D0=B2=D0=B5=D1=80=D1=81=D0=B8=D1=8F=20v1.0=20(?= =?UTF-8?q?b3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - индексатор порезан на резюмируемые шаги index_begin/index_step/index_finish; межшаговое состояние в статиках модуля, в docset_t не входит - bg_build_start/bg_step: второй набор (UTF-8 при 8-битном первичном и наоборот) строится в idle главного цикла; холдаун после клавиш, спиннер погашен (g_bg_building) — фон незаметен - F8 до готовности докручивает начатое фоном (ветка resume в build_doc), а не строит заново; общий setup вынесен в doc_setup - кодировка в статус-баре показывается сразу (детект/F8), не дожидаясь конца индексации - побочный фикс: UTF-конвертация впереди проверки останова — >4КБ абзац больше не обрывает конвертацию остатка - README.md (новый, v1.0 b3), CHANGELOG.md; дискета: README/DEMO/CHANGES в трёх кодировках (пути автодетекта) Co-Authored-By: Claude Fable 5 --- examples/mdview2/CHANGELOG.md | 79 ++++++++++++ examples/mdview2/{README.MD => DEMO.MD} | 0 examples/mdview2/Makefile | 39 +++--- examples/mdview2/README.md | 153 ++++++++++++++++++++++++ examples/mdview2/mdview2.c | 141 +++++++++++++++++++--- examples/mdview2/mdview2.h | 9 +- examples/mdview2/mdview2_help.c | 2 +- examples/mdview2/mdview2_index.c | 150 +++++++++++++++-------- 8 files changed, 484 insertions(+), 89 deletions(-) create mode 100644 examples/mdview2/CHANGELOG.md rename examples/mdview2/{README.MD => DEMO.MD} (100%) create mode 100644 examples/mdview2/README.md diff --git a/examples/mdview2/CHANGELOG.md b/examples/mdview2/CHANGELOG.md new file mode 100644 index 0000000..377c447 --- /dev/null +++ b/examples/mdview2/CHANGELOG.md @@ -0,0 +1,79 @@ +# MDView - CHANGELOG + +ଠ: ᨨ git-ਨ ஥. ࠭ CP866 - + 㤮 ᠬ MDView ( ⮤⥪ ஢). + +## v1.0 (b3) - 2026-07-05 + +- ⮢ ண ஢: 짮⥫ ⠥ + 㬥, 蠬 ண + (UTF-8 8-⭮ ࢨ筮 ). ࢮ F8 筮 + 㦥 ⮢ - ४祭 . F8 ⮢ ந + , 稢 ⮥ 䮭 ( ). +- : ࠭ ண, ᫥ + 뤥ন 宫 - ஫ ⮯஬ 䮭 . +- ॢ १㥬 蠣 (index_begin/index_step); + 筮 ࠩ: UTF-樨 ᢥ夫 (>4 ) + १ ⮪ 㬥. +- ஢ - 뢠 ࠧ: ⥪஢ - + 砫 ࢮ 樨, F8 - ४祭, + ᡮન . + +## v1.0 (b1) - 2026-07-05 + +- HEX-० (F4): 䠩 ଠ + "0x012340 : 16 hex : 16 ᨬ". + ⭠ 뢠 ⥪ ஢; UTF-8 + ⠢ 樨 -, continuation- - 窮. + ஢ (F8) HEX ⮫쪮 . +- ப⪠ HEX ப - ஫ ᮢ + 鸞 ( MD/RAW). +- ࠢ: F4, v1.0 (b1). + +## v1.0 (a4) - 2026-07-04 + +- ४祭 ஢: F2 (MD/RAW), + F8 (8- / UTF-8) ࠭ ⥪饥 㬥. + ࠬ ࠧ ࠧ ७ ய樮쭮 + ⠬ 筮 ப. +- ࠢ: F8 RAW-० ᮢ MD- RAW. +- ⨬ ࠧ: exe 28215 -> 25100 (-11%): + ᨢ ॣ஢ SDCC (--max-allocs), 㫥 + _DATA crt0 ( "= 0" ⨪), 㯫 , + ᫨ﭨ ᪠஢ inline-ࠧ⪨ scan_stream. + +## v1.0 (a3) - 2026-06-30 + +- 䠩 256 : 訩 䠩 १ ।०, + ⠭ 㯭. +- ௠ / 蠥 ப-ᮮ饭 + 稭 ( 宩 ப). +- ஢ - ࠢ; 業 ப⪨ 16-⭮ + 䬥⨪ ( 32-⭮ ). +- ⮢ 䠩 ࠭ 砥 (testfiles/, ). + +## v1.0 (a2) - 2026-06-26 + +- 饯 㫨: + index/md/raw/enc/table/status/help + (ࠧ-ࠫ쭮, ⠥). +- ࠢ 㡫 ப RAW ஫ 䠩. + +## v1.0 (a1) - 2026-06-25 + +- প ஢ CP866/CP1251/KOI8-R/UTF-8 (F8): + ⮮। ⨨, ६ 8-, + UTF-8 ⤥ (६⠫쭮). +- RAW-ᬮ 室 (F2): ७ ⭮ 80 ࠬ (F3). +- ⨬ ࠧ (-1.9 ). + +## v1.0 (a0) - 2026-06-23..24 + +- : render-cache ⥪ - 室 , + ⮢ ப (ᨬ+ਡ) EMM, ப⪠. +- Markdown: , ᯨ᪨, , ࠧ⥫, fenced-, + ⠡ ࠬ -ਭ , inline-ࠧ⪠ + (/ᨢ/ભ/ભ/, ࠭஢). +- ணᨢ 㧪: ࠭ ࠧ, 䮭, + ⮢ , Esc/F10 뢠. +- ਧ⠫ ஫ ⠡. +- ᪮७ ९ᨭ (22 -> 10 讬 䠩). diff --git a/examples/mdview2/README.MD b/examples/mdview2/DEMO.MD similarity index 100% rename from examples/mdview2/README.MD rename to examples/mdview2/DEMO.MD diff --git a/examples/mdview2/Makefile b/examples/mdview2/Makefile index 5569dce..bae8bfa 100644 --- a/examples/mdview2/Makefile +++ b/examples/mdview2/Makefile @@ -17,35 +17,36 @@ EXTRA_FLAGS := --max-allocs 100000 include $(PROJ_ROOT)/app.mk # ------------------------------------------------------------------ -# Образ дискеты: только mdview2.exe + README.MD (перекодированный -# из UTF-8 в CP866 — рабочую кодировку Sprinter). -# -# README.MD хранится в репозитории в UTF-8; iconv -c конвертирует -# его в CP866, отбрасывая символы без аналога в целевой кодировке. -# Результат кладётся в .disk_tmp/README.MD, чтобы make_disk.py -# использовал правильное имя файла на диске. +# Образ дискеты: mdview2.exe + документация. Каждый документ — в СВОЕЙ +# кодировке, чтобы дискета заодно покрывала все пути автодетекта: +# README.MD — документация, ОРИГИНАЛЬНЫЙ UTF-8 (как в репозитории); +# CHANGES.MD — история версий, CP866 (хранится так, кладётся как есть; +# имя укорочено: "CHANGELOG" не влезает в FAT 8.3); +# DEMO.MD — демонстрация разметки, конвертируется UTF-8 → CP1251. # # iconv -c возвращает ненулевой код, если хоть один символ отброшен -# (даже с -c) — это ОЖИДАЕМО при потере символов без аналога в CP866, -# не ошибка конвертации; вывод при этом всё равно корректно записан. +# (даже с -c) — это ОЖИДАЕМО при потере символов без аналога в целевой +# кодировке, не ошибка конвертации; вывод при этом корректно записан. # Поэтому код возврата iconv игнорируется (|| true). +# +# Тестовые файлы лимита 256 КБ лежат в testfiles/ (генератор gen_testfiles.py); +# на диск НЕ кладутся — при необходимости добавить вручную в список ниже. # ------------------------------------------------------------------ DISK_TMP := .disk_tmp -README_DISK := $(DISK_TMP)/README.MD +DEMO_DISK := $(DISK_TMP)/DEMO.MD +CHLOG_DISK := $(DISK_TMP)/CHANGES.MD $(DISK_TMP): mkdir -p $@ -$(README_DISK): README.MD | $(DISK_TMP) - iconv -c -f UTF-8 -t CP866 README.MD > $@ || true +$(CHLOG_DISK): CHANGELOG.md | $(DISK_TMP) + cp CHANGELOG.md $@ -# UTF8TEST.MD кладётся на диск КАК ЕСТЬ (в UTF-8, без перекодировки) — -# это тестовый вход для проверки UTF-8 рендеринга (Фаза 2 кодировок). -# -# Тестовые файлы лимита 256 КБ лежат в testfiles/ (генератор gen_testfiles.py); -# на диск НЕ кладутся — при необходимости добавить вручную в список ниже. -floppy: $(EXAMPLE).exe $(README_DISK) - python3 $(MAKE_DISK) $(FLOPPY_IMG) $(EXAMPLE).exe $(README_DISK) UTF8TEST.MD +$(DEMO_DISK): DEMO.MD | $(DISK_TMP) + iconv -c -f UTF-8 -t CP1251 DEMO.MD > $@ || true + +floppy: $(EXAMPLE).exe $(DEMO_DISK) $(CHLOG_DISK) + python3 $(MAKE_DISK) $(FLOPPY_IMG) $(EXAMPLE).exe README.md $(DEMO_DISK) $(CHLOG_DISK) @echo @echo "Floppy ready: $(FLOPPY_IMG)" @echo "Run: cd $(MAME_DIR) && ./run_mame.sh" diff --git a/examples/mdview2/README.md b/examples/mdview2/README.md new file mode 100644 index 0000000..1fde9d7 --- /dev/null +++ b/examples/mdview2/README.md @@ -0,0 +1,153 @@ +# MDView v1.0 (b3) — просмотрщик Markdown для Sprinter + +**MDView** — программа просмотра документов Markdown для компьютера +**Sprinter-2000** (Z80, ОС ESTEX). Файл загружается в расширенную память (EMM) +и один раз «прогоняется» через парсер: готовые к показу строки (пары +символ+атрибут) складываются в **рендер-кэш** в EMM, после чего прокрутка в +любую сторону — это просто копирование готовых строк на экран, без повторного +парсинга. Даже на файлах в сотни килобайт листание остаётся мгновенным. + +Текстовый режим 80×32, цветное оформление элементов разметки, три режима +просмотра (MD / RAW / HEX) и четыре кодировки с автоопределением. + +--- + +## Возможности + +* **Markdown-рендеринг** с цветовым оформлением: + * заголовки `#`…`######` (H5/H6 отображаются как H4); + * **жирный** (`**текст**`), *курсив* (`*текст*`), подчёркнутый (`_текст_`), + ~~зачёркнутый~~ (`~~текст~~`), `встроенный код` (`` `текст` ``); + * экранирование `\*`, `\_`, `` \` `` и любой ASCII-пунктуации; + * чекбоксы `[x]` / `[ ]` в списках; + * ненумерованные (`-`, `*`, `+`) и нумерованные (`1.`, `1)`) списки + с базовой вложенностью по отступам; + * цитаты `>` (склейка многострочных, маркер │ на переносах); + * fenced-блоки кода ` ``` ` (без переносов, горизонтальный скролл); + * таблицы `| … | … |` — рисуются псевдографической рамкой, ширины колонок + вычисляются по содержимому (до 16 колонок); + * горизонтальные разделители `---` / `***` / `___`; + * жёсткие переносы (два пробела или `\` в конце строки); + * мягкая склейка абзацев с переносом по словам под ширину экрана. +* **Кодировки: CP866, CP1251, KOI8-R, UTF-8.** + * автоопределение при открытии (BOM → UTF-8; валидность multibyte-структуры; + частотный анализ ходовых русских букв для 8-битных); + * переключение по кругу клавишей **F8** в любой момент; + * 8-битные кодировки отличаются только перекодировкой глифов на отрисовке — + переключение мгновенно; + * UTF-8 декодируется в CP866 в **отдельный набор** (файл + индекс + кэш); + второй набор готовится **в фоне**, пока вы читаете документ, — обычно + к первому нажатию F8 он уже построен и переключение мгновенно, + с сохранением позиции. Если фон не успел, F8 докручивает начатую + сборку (со спиннером), а не начинает её заново. +* **Три режима просмотра:** + * **MD** — форматированный Markdown (по умолчанию); + * **RAW** (**F2**) — исходный текст без разметки: перенос строк кратно 80 + (**F3** — режим панорамы с горизонтальным скроллом); + * **HEX** (**F4**) — дамп *оригинального* файла: + `0x012340 │ 16 байт hex │ 16 печатных символов`. Печатная колонка + интерпретируется текущей кодировкой; для UTF-8 глиф ставится на позиции + лид-байта, continuation-байты показываются точкой. +* **Единая позиция** при любых переключениях: MD ↔ RAW ↔ HEX и смена + кодировки сохраняют текущее место в документе (между наборами разного + размера — пропорционально, с точностью до строки). +* **Прогрессивная загрузка**: первый экран показывается сразу, индексация + продолжается в фоне; по готовой части документа уже можно листать, + **Esc**/**F10** прерывают загрузку. +* **Фоновая работа незаметна**: второй набор кодировки строится только в + паузах между клавишами (после нажатия выдерживается пауза), поэтому + скролл — в том числе с автоповтором — не теряет плавности. + +--- + +## Запуск + +``` +MDVIEW2.EXE <файл.md> +``` + +Без аргумента открывается `README.MD` из текущего каталога. + +## Клавиши + +| Клавиша | Действие | +|--------------|-------------------------------------------------------------| +| ↑ / ↓ | прокрутка на одну строку | +| PgUp / PgDn | прокрутка на экран (30 строк) | +| Home / End | в начало / в конец документа | +| ← / → | горизонтальный сдвиг: код/таблицы в MD, панорама в RAW | +| F1 | справка | +| F2 | RAW-режим ↔ MD | +| F3 | в RAW: перенос строк ↔ панорама | +| F4 | HEX-режим ↔ прежний вид | +| F8 | кодировка: CP866 → CP1251 → KOI8-R → UTF-8 → … | +| Esc / F10 | выход (во время загрузки — прервать её) | + +Статус-бар (верхняя строка): имя файла, кодировка, диапазон видимых строк +и процент прокрутки. Нижняя строка — меню доступных F-клавиш. + +--- + +## Ограничения + +| Параметр | Значение | +|---------------------------------|-------------------------------------------| +| Размер файла | до 256 КБ (больший — обрезается с предупреждением) | +| Логических строк (после переносов) | до 18 432 | +| Длина строки в рендер-кэше | 255 ячеек (RAW/HEX ограничения не имеют) | +| Колонок в таблице | до 16 | +| Шаг табуляции | 4 | + +При исчерпании любого лимита документ завершается строкой-сообщением +с указанием причины обрыва; всё, что вошло, доступно для просмотра. + +## Требования + +* Sprinter-2000 с ОС ESTEX; +* расширенная память (EMM): в худшем случае (файл 256 КБ + оба набора + кодировок) — до ~150 страниц по 16 КБ (~2,4 МБ). Для типичных файлов + в десятки килобайт достаточно нескольких десятков страниц. + +Код, данные, стек и куча программы занимают окна W1+W2 (32 КБ, режим +памяти `small`); окно W3 используется только для доступа к EMM-страницам. + +--- + +## Сборка + +Требуется тулчейн этого репозитория (обёртка `sprinter-cc` над SDCC 4.5). +Из каталога `examples/mdview2`: + +``` +make # собрать mdview2.exe +make floppy # собрать и упаковать дискету для MAME (mc.img) +make run # floppy + запуск MAME +``` + +На дискету кладутся: `MDVIEW2.EXE` и три документа, каждый в своей кодировке +(заодно покрывают все пути автодетекта): `README.MD` — этот файл, как есть +(UTF-8); `DEMO.MD` — демонстрация всех элементов разметки (CP1251); +`CHANGES.MD` — история версий (CP866). + +## Структура исходников + +| Файл | Назначение | +|------------------|--------------------------------------------------------------| +| `mdview2.c` | ядро: EMM-аллокации, рендер-кэш, наборы кодировок, загрузка файла, главный цикл | +| `mdview2_index.c`| парсер/индексатор Markdown — единственный проход по файлу | +| `mdview2_md.c` | MD-вид: отрисовка из кэша, прокрутка | +| `mdview2_raw.c` | RAW-вид (F2/F3) | +| `mdview2_hex.c` | HEX-вид (F4) | +| `mdview2_enc.c` | кодировки: детект, ремап-таблицы, конвертер UTF-8 → CP866 | +| `mdview2_table.c`| отрисовка таблиц | +| `mdview2_status.c`| статус-бар, меню, спиннер | +| `mdview2_help.c` | справка (F1) | +| `mdview2_conf.h` | конфигурация: `WITH_RAW` / `WITH_HEX` (модули отключаемы) | +| `mdview2.h` | общие константы, атрибуты, межмодульный API | + +Подробности архитектуры — в `docs/mdview2-plan.md`. + +## Лицензия и авторы + +© 2026 Петров А.Г. Часть проекта Sprinter C Compiler +(см. LICENSE в корне репозитория). diff --git a/examples/mdview2/mdview2.c b/examples/mdview2/mdview2.c index a94cd4c..586b3a7 100644 --- a/examples/mdview2/mdview2.c +++ b/examples/mdview2/mdview2.c @@ -203,10 +203,12 @@ static uint8_t g_raw_last = VIEW_RAW_WRAP; /* последний под static uint8_t g_hex_last = VIEW_MD; /* вид, из которого вошли в HEX (возврат по F4) */ #endif #endif -uint8_t g_loading; /* extern: 1 во время index_lines() */ +uint8_t g_loading; /* extern: 1 во время видимой индексации (build_doc) */ static uint8_t g_first_drawn; /* первый экран уже нарисован */ uint8_t g_f8_enabled; /* extern: можно ли сейчас переключать кодировку */ static uint8_t g_utf_failed; /* UTF-8 набор не удалось построить (нет EMM) — пропускаем UTF-8 в цикле F8 */ +uint8_t g_bg_building; /* extern: 1 во время фонового index_step — гасит спиннер индексатора */ +static uint8_t g_bg_doc = 0xFF; /* слот, строящийся в фоне (bg_step); 0xFF = фоновой сборки нет */ uint8_t g_abort; /* extern: F10/Esc во время загрузки → прервать индексацию */ static uint16_t g_last_prog; /* n_lines на последнем обновлении статуса */ @@ -768,26 +770,34 @@ void progress_tick(void) * Точка входа * ================================================================== */ -/* Строит набор `slot` кооперативно (index_lines + progress_tick). +/* Строит набор `slot` кооперативно (index_step + progress_tick). * visible=1 — первичный (показываемый): прогрессивная отрисовка первого экрана - * и скролл по готовым строкам. visible=0 — вторичный: index_lines строит его в - * живых глобалах, а progress_tick через свап показывает/листает уже готовый + * и скролл по готовым строкам. visible=0 — вторичный: индексация идёт в живых + * глобалах, а progress_tick через свап показывает/листает уже готовый * g_disp_doc. Возврат: 0 — успех, <0 — нехватка EMM (-5/-6/-7). * + * Если slot прямо сейчас строится ФОНОМ (g_bg_doc, см. bg_step ниже) — + * не начинаем заново, а ДОКРУЧИВАЕМ: загружаем частично построенный набор + * и продолжаем с сохранённой позиции индексатора (его межшаговое состояние + * живёт в статиках модуля index и переживает свапы наборов). + * * UTF-8 набор конвертируется ИНКРЕМЕНТАЛЬНО: выделяем страницы, переключаем - * живой буфер на utf и запускаем конвертацию первого чанка; дальше index_lines + * живой буфер на utf и запускаем конвертацию первого чанка; дальше index_step * докручивает её впереди позиции чтения (g_utf_building). Конвертер читает * оригинал из orig_file_phys[] через cv_read (не через fb, который теперь * указывает на utf). */ -static int build_doc(uint8_t slot, uint8_t enc, uint8_t visible) +/* Настраивает живые глобалы под сборку набора slot: файловый буфер (utf/orig), + * индекс + директория кэша, сброс индексатора. Общая часть build_doc и + * bg_build_start. Возврат: 0 — успех, <0 — нехватка EMM (-5/-6/-7). */ +static int doc_setup(uint8_t slot) { if (slot == DOC_UTF8) { if (!utf_avail) { /* первый вход: выделить страницы */ - if (!utf_alloc()) + if (!utf_alloc()) return -7; /* выделяет страницы + сбрасывает конвертер */ utf_avail = 1; } - file_blk = utf_blk; + file_blk = utf_blk; file_pages = utf_pages; memcpy(file_phys, utf_phys, MAX_PAGES); if (g_utf_building) { @@ -798,29 +808,45 @@ static int build_doc(uint8_t slot, uint8_t enc, uint8_t visible) file_size = utf_size; } } else { - file_blk = orig_file_blk; - file_pages = orig_file_pages; + file_blk = orig_file_blk; + file_pages = orig_file_pages; file_size = orig_file_size; memcpy(file_phys, orig_file_phys, MAX_PAGES); } cur_page = 0xFF; int rc = alloc_set_storage(); - if (rc < 0) + if (rc < 0) return rc; + index_begin(); + return 0; +} + +static int build_doc(uint8_t slot, uint8_t enc, uint8_t visible) +{ + if (g_bg_doc == slot) { /* фон уже строит его — продолжить */ + doc_load_live(slot); + g_bg_doc = 0xFF; + } else { + int rc = doc_setup(slot); + if (rc < 0) + return rc; + } set_encoding(enc); - top_line = 0; + top_line = 0; viewport_x = 0; g_active_doc = slot; - if (visible) { - g_disp_doc = slot; - g_first_drawn = 0; + if (visible) { + g_disp_doc = slot; + g_first_drawn = 0; + status_encoding(); /* показать кодировку сразу (детект/F8), не дожидаясь индексации */ + cur_page = 0xFF; /* BIOS-вывод мог сбить W3-маппинг, на который опирается fb() */ } g_loading = 1; g_last_prog = 0; - index_lines(); + while (!index_step(255)) {} g_loading = 0; /* Зафиксировать конвертацию (на случай обрыва индекса по max_lines до конца @@ -835,6 +861,69 @@ static int build_doc(uint8_t slot, uint8_t enc, uint8_t visible) return 0; } +/* ================================================================== + * Фоновая сборка второго набора (idle-time, между клавишами) + * ================================================================== + * Пока пользователь читает документ, главный цикл в паузах между клавишами + * докручивает индексацию второго набора (UTF-8 при 8-битном первичном и + * наоборот) порциями index_step: свап на bg-набор → несколько итераций → + * свап обратно. Экран при этом не трогается (g_bg_building гасит спиннер), + * задержка реакции на клавишу — не больше одного шага. По готовности F8 + * переключает наборы мгновенно; F8 ДО готовности докручивает сборку видимо + * (ветка resume в build_doc), не начиная её заново. */ + +#define BG_STEP_BUDGET 2u /* итераций индексатора на idle-шаг: латентность клавиши + * = длительность шага (~единицы мс на обычном абзаце) */ +#define BG_KEY_HOLDOFF 300u /* пустых опросов kbhit после клавиши до возобновления + * фона — при автоповторе (скролл) фон не вклинивается */ + +/* Готовит фоновую сборку второго набора: настраивает живые глобалы (буфер, + * индекс, кэш, index_begin) так же, как build_doc, но БЕЗ индексации, и + * возвращает живым прежний набор. При нехватке EMM фон просто не стартует — + * остаётся сегодняшний ленивый путь по F8. */ +static void bg_build_start(void) +{ + uint8_t slot = (uint8_t)((g_active_doc == DOC_8BIT) ? DOC_UTF8 : DOC_8BIT); + uint8_t vis = g_active_doc; + if (g_doc[slot].built || (slot == DOC_UTF8 && g_utf_failed)) + return; /* строить нечего */ + + doc_save_live(vis); + if (doc_setup(slot) < 0) { /* нет EMM — как и в ленивом пути */ + if (slot == DOC_UTF8) g_utf_failed = 1; + doc_load_live(vis); + return; + } + top_line = 0; + viewport_x = 0; + + g_active_doc = slot; /* живые глобалы = bg-набор... */ + doc_switch(vis); /* ...сохранить его и вернуть видимый */ + g_bg_doc = slot; +} + +/* Один idle-шаг фона: свап на строящийся набор, порция индексации, свап + * обратно. Межшаговое состояние индексатора/UTF-конвертера — статики их + * модулей, обработчики клавиш видимого набора их не трогают. По завершении + * финализирует набор (дальше F8 переключается мгновенно). */ +static void bg_step(void) +{ + uint8_t vis = g_active_doc; + doc_switch(g_bg_doc); + g_bg_building = 1; + uint8_t done = index_step(BG_STEP_BUDGET); + g_bg_building = 0; + if (done) { + if (g_utf_building) { /* как build_doc: обрыв индекса до конца исходника */ + utf_size = file_size; + g_utf_building = 0; + } + g_doc[g_bg_doc].built = 1; /* doc_save_live поле built не трогает */ + g_bg_doc = 0xFF; + } + doc_switch(vis); +} + /* Пропорциональный перенос байт-позиции между буферами разного размера * (8bit ↔ UTF-8 при F8): off * to / from БЕЗ __mullong/__divulong. Один * цикл restoring-деления: бит k дроби off/from (сравнение с from/2^k) @@ -1079,7 +1168,27 @@ int main(int argc, char **argv) render_full_status(); draw_viewport_from_cache(); + /* Второй набор (UTF-8 при 8-битном первичном и наоборот) готовим фоном + * в паузах между клавишами — к первому F8 он обычно уже построен. */ + bg_build_start(); + for (;;) { + /* Idle-фон: пока второй набор не достроен, вместо блокирующего getkey + * опрашиваем клавиатуру и тратим паузы на bg_step. Холдаун после + * каждой клавиши: при активном скролле (автоповтор) фон молчит и + * плавность не страдает. После достройки — снова блокирующий getkey. */ + if (g_bg_doc != 0xFF) { + uint16_t idle = 0; + while (!kbhit()) { + if (idle < BG_KEY_HOLDOFF) { + idle++; + continue; + } + bg_step(); + if (g_bg_doc == 0xFF) + break; + } + } uint16_t k = getkey(); uint8_t ascii = (uint8_t)(k & 0xFF); uint8_t scan = (uint8_t)((k >> 8) & 0x7F); diff --git a/examples/mdview2/mdview2.h b/examples/mdview2/mdview2.h index 8db10f1..1c53d6f 100644 --- a/examples/mdview2/mdview2.h +++ b/examples/mdview2/mdview2.h @@ -245,7 +245,14 @@ uint8_t cache_reserve(uint16_t nbytes, uint8_t *out_page, uint16_t *out_off); void cache_commit(uint8_t page, uint16_t off, const void *buf, uint16_t len); void cache_dir_put(uint16_t idx, const cache_rec_t *r); void progress_tick(void); /* кооперативный шаг loading-loop (рисует/листает) */ -void index_lines(void); /* построить индекс по загруженному файлу */ + +/* Резюмируемая индексация (для фоновой сборки второго набора): begin сбрасывает + * проход, step выполняет до budget итераций (блоков исходника) и возвращает 1, + * когда индекс финализирован. Межшаговое состояние — статики модуля index; + * оно не входит в docset_t (см. bg_step ядра). */ +void index_begin(void); +uint8_t index_step(uint8_t budget); +extern uint8_t g_bg_building; /* 1 = фоновый шаг индексации: не трогать экран (спиннер) */ /* ================================================================== * * Буфер ячеек рендера (mdview2.c) — общий с модулем таблиц. diff --git a/examples/mdview2/mdview2_help.c b/examples/mdview2/mdview2_help.c index cbb3184..3ea3b5c 100644 --- a/examples/mdview2/mdview2_help.c +++ b/examples/mdview2/mdview2_help.c @@ -73,7 +73,7 @@ void show_help(void) /* Содержимое (20 внутренних строк) */ uint8_t r = 0; help_line(r++, "", ATTR_HELP_BG); - help_line(r++, " MDView v1.0 (b1) -- Markdown Viewer for Sprinter", ATTR_HELP_HDR); + help_line(r++, " MDView v1.0 (b3) -- Markdown Viewer for Sprinter", ATTR_HELP_HDR); help_line(r++, " (c) 2026 \x8F\xA5\xE2\xE0\xAE\xA2 \x80\x2E\x83\x2E", ATTR_HELP_BG); help_line(r++, "", ATTR_HELP_BG); diff --git a/examples/mdview2/mdview2_index.c b/examples/mdview2/mdview2_index.c index 96db5a3..2740fbb 100644 --- a/examples/mdview2/mdview2_index.c +++ b/examples/mdview2/mdview2_index.c @@ -1,7 +1,7 @@ /* * mdview2_index.c — парсер/индексатор markdown (сердце приложения). * - * Единственный проход по исходному файлу (index_lines): классификация строк + * Единственный проход по исходному файлу (index_begin/index_step): классификация строк * (classify_line, is_*_raw), сборка абзацев с переносом (scan_join_stream), * inline-разметка (inline_marker/inline_scan) и эмиссия готовых (char,attr)- * ячеек в рендер-кэш (gc_put → seg_commit_cells → cache_*). Ведёт индекс @@ -669,31 +669,88 @@ static uint32_t scan_join_stream(uint32_t q, uint32_t para_start, uint8_t col, /* Отрисовка таблиц (row_end/table_*) — в модуле mdview2_table.c. */ -/* Строит индекс сегментов рендера по загруженному файлу. */ -void index_lines(void) +/* ================== Резюмируемый проход индексации ======================= + * Бывший монолит index_lines() порезан на begin/step/finish, чтобы фоновая + * сборка второго набора (bg_step ядра) могла выполнять индексацию порциями + * в паузах между клавишами. Всё межшаговое состояние — в статиках модуля + * (idx_p/idx_in_block ниже + g_cells/cur_rec/g_trunc_* выше); оно НЕ входит + * в docset_t и переживает свапы наборов, потому что обработчики клавиш + * видимого набора индексатор не трогают. */ +static uint32_t idx_p; /* позиция чтения исходника между шагами */ +static uint8_t idx_in_block; /* внутри fenced code-блока */ +static uint8_t idx_active; /* 1 = проход начат и ещё не финализирован */ + +/* Начинает новый проход (сброс позиции/буферов/причин обрыва). */ +void index_begin(void) { - uint8_t in_block = 0; + idx_p = 0; + idx_in_block = 0; n_lines = 0; index_truncated = 0; g_trunc_cause = 0; g_trunc_line = 0; cur_seg_off = 0xFFFFFFFFu; /* пока не эмитили ни одного сегмента */ - if (file_size == 0) return; + g_ncells = 0; + g_ncells_at_space = 0; + g_skip_flush = 0; + idx_active = 1; +} - uint32_t p = 0; - while (p < file_size && n_lines < max_lines && !g_abort && !g_trunc_cause) { +/* Хвост прохода: финальный флаш ячеек + строка-обрыв (бывший финал index_lines). */ +static void index_finish(void) +{ + idx_active = 0; + /* emit_seg() флашит ячейки с отставанием на одну строку (см. её + * комментарий) — последняя эмитированная строка файла так и остаётся + * незафлашенной, её ячейки ещё в g_cells; коммитим здесь. */ + if (n_lines > 0) seg_commit_cells((uint16_t)(n_lines - 1)); - /* UTF-8: держим конвертацию впереди позиции чтения (включая look-ahead - * склейки/классификации ~ один абзац). file_size при этом = доступная - * сконвертированная часть и растёт по ходу. */ - if (g_utf_building && (uint32_t)(p + CONV_MARGIN) > file_size) - utf_convert_more((uint32_t)(p + CONV_MARGIN)); + /* Лимит строк ловим ЗДЕСЬ, а не в emit_seg: главный цикл выходит по + * `n_lines < max_lines` ДО того, как emit_seg будет вызван в переполненном + * состоянии (для code-block — один emit_seg на строку, эта ветка не + * достигается). Признак — остановились, а файл не кончился; g_abort и + * исчерпание контента (TRUNC_CONTENT) уже обработаны/исключены. */ + if (!g_trunc_cause && !g_abort && idx_p < file_size) { + g_trunc_cause = TRUNC_LINES; + g_trunc_line = (uint16_t)(max_lines - 1); + } - if (g_loading) - progress_tick(); /* первый экран + статус + скролл по готовым данным */ - else if ((n_lines & 15) == 0) - spinner_tick(); + /* Файл был обрезан до 256 КБ при загрузке, а индексация дошла до конца + * клампнутых данных без исчерпания контента/строк → ДОПИСЫВАЕМ строку + * (место есть: n_lines < max_lines). Приоритет ниже content/lines — + * те означают, что обрезали ещё раньше, их сообщение точнее. */ + if (!g_trunc_cause && !g_abort && g_file_clamped) { + g_trunc_cause = TRUNC_FILE; + g_trunc_line = n_lines; + } + /* Обрыв по контенту/лимиту строк (не путать с пользовательским g_abort): + * документ заканчиваем строкой-сообщением на g_trunc_line. Сообщение + * рисуется вживую (IF_TRUNC_MSG) — место в контент-кэше ему не нужно, что + * и снимает тупик «кэш полон, а сообщение тоже надо закэшировать». */ + if (g_trunc_cause && !g_abort) { + cache_rec_t r; + r.page = 0; r.off = 0; r.len = 0; + r.flags = IF_TRUNC_MSG; r.reserved = g_trunc_cause; + r.pad[0] = 0; r.pad[1] = 0; + cache_dir_put(g_trunc_line, &r); + n_lines = (uint16_t)(g_trunc_line + 1); /* документ кончается на строке-обрыве */ + } +} + +/* Одна итерация бывшего главного цикла index_lines(): классифицирует и + * эмитит очередной блок исходника (параграф/строку кода/таблицу целиком). + * do{}while(0) сохраняет исходные `continue` тела как выход из итерации + * с записью локальной p обратно в idx_p. */ +static void index_iter(void) +{ + if (g_loading) + progress_tick(); /* первый экран + статус + скролл по готовым данным */ + else if (!g_bg_building && (n_lines & 15) == 0) + spinner_tick(); + + uint32_t p = idx_p; + do { /* Пустая строка — разделитель параграфов. * В индекс добавляем не более одной пустой экранной строки подряд. */ if (is_line_blank(p)) { @@ -708,13 +765,13 @@ void index_lines(void) /* ---- fenced code-блок (граница или тело) ---- */ if (is_fence_raw(p)) { - in_block = !in_block; + idx_in_block = !idx_in_block; emit_seg(p, INIT_STYLE_PLAIN, CK_OTHER, 0); set_cur_flags(IF_NOWRAP); p = next_line(p); continue; } - if (in_block) { + if (idx_in_block) { emit_seg(p, INIT_STYLE_PLAIN, CK_OTHER, 0); set_cur_flags(IF_NOWRAP | IF_CODE | IF_HSCROLL); /* код скроллится целым блоком */ { @@ -926,43 +983,32 @@ void index_lines(void) p = next_line(q); } } - } - /* emit_seg() флашит ячейки с отставанием на одну строку (см. её - * комментарий) — последняя эмитированная строка файла так и остаётся - * незафлашенной, её ячейки ещё в g_cells; коммитим здесь. */ - if (n_lines > 0) seg_commit_cells((uint16_t)(n_lines - 1)); + } while (0); + idx_p = p; +} - /* Лимит строк ловим ЗДЕСЬ, а не в emit_seg: главный цикл выходит по - * `n_lines < max_lines` ДО того, как emit_seg будет вызван в переполненном - * состоянии (для code-block — один emit_seg на строку, эта ветка не - * достигается). Признак — остановились, а файл не кончился; g_abort и - * исчерпание контента (TRUNC_CONTENT) уже обработаны/исключены. */ - if (!g_trunc_cause && !g_abort && p < file_size) { - g_trunc_cause = TRUNC_LINES; - g_trunc_line = (uint16_t)(max_lines - 1); - } +/* Выполняет до budget итераций индексации. Возврат: 1 — проход завершён + * (индекс финализирован index_finish), 0 — есть ещё работа. + * Конвертация UTF-8 держится впереди позиции чтения ЗДЕСЬ, до проверки + * условия останова: пока исходник не сконвертирован до конца, idx_p не + * может «догнать» file_size и оборвать проход преждевременно. */ +uint8_t index_step(uint8_t budget) +{ + if (!idx_active) return 1; + while (budget-- > 0) { + /* UTF-8: держим конвертацию впереди позиции чтения (включая look-ahead + * склейки/классификации ~ один абзац). file_size при этом = доступная + * сконвертированная часть и растёт по ходу. */ + if (g_utf_building && (uint32_t)(idx_p + CONV_MARGIN) > file_size) + utf_convert_more((uint32_t)(idx_p + CONV_MARGIN)); - /* Файл был обрезан до 256 КБ при загрузке, а индексация дошла до конца - * клампнутых данных без исчерпания контента/строк → ДОПИСЫВАЕМ строку - * (место есть: n_lines < max_lines). Приоритет ниже content/lines — - * те означают, что обрезали ещё раньше, их сообщение точнее. */ - if (!g_trunc_cause && !g_abort && g_file_clamped) { - g_trunc_cause = TRUNC_FILE; - g_trunc_line = n_lines; - } - - /* Обрыв по контенту/лимиту строк (не путать с пользовательским g_abort): - * документ заканчиваем строкой-сообщением на g_trunc_line. Сообщение - * рисуется вживую (IF_TRUNC_MSG) — место в контент-кэше ему не нужно, что - * и снимает тупик «кэш полон, а сообщение тоже надо закэшировать». */ - if (g_trunc_cause && !g_abort) { - cache_rec_t r; - r.page = 0; r.off = 0; r.len = 0; - r.flags = IF_TRUNC_MSG; r.reserved = g_trunc_cause; - r.pad[0] = 0; r.pad[1] = 0; - cache_dir_put(g_trunc_line, &r); - n_lines = (uint16_t)(g_trunc_line + 1); /* документ кончается на строке-обрыве */ + if (!(idx_p < file_size && n_lines < max_lines && !g_abort && !g_trunc_cause)) { + index_finish(); + return 1; + } + index_iter(); } + return 0; } #define EM_NONE 0 #define EM_BOLD 1