mdview2: фоновая сборка второго набора кодировки в паузах между клавишами; версия v1.0 (b3)

- индексатор порезан на резюмируемые шаги 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 <noreply@anthropic.com>
This commit is contained in:
2026-07-05 19:33:34 +03:00
parent ae23d2dea2
commit 46553f4e07
8 changed files with 484 additions and 89 deletions
+79
View File
@@ -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á ­  ¡®«ì讬 ä ©«¥).
+20 -19
View File
@@ -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"
+153
View File
@@ -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 в корне репозитория).
+116 -7
View File
@@ -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,18 +770,26 @@ 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) { /* первый вход: выделить страницы */
@@ -808,6 +818,20 @@ static int build_doc(uint8_t slot, uint8_t enc, uint8_t visible)
int rc = alloc_set_storage();
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;
@@ -816,11 +840,13 @@ static int build_doc(uint8_t slot, uint8_t enc, uint8_t visible)
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);
+8 -1
View File
@@ -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) — общий с модулем таблиц.
+1 -1
View File
@@ -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);
+94 -48
View File
@@ -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);
}
/* Файл был обрезан до 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 ((n_lines & 15) == 0)
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));
/* Лимит строк ловим ЗДЕСЬ, а не в 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);
} while (0);
idx_p = p;
}
/* Файл был обрезан до 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;
}
/* Выполняет до 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));
/* Обрыв по контенту/лимиту строк (не путать с пользовательским 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