diff --git a/examples/mdview2/Makefile b/examples/mdview2/Makefile index 19d1614..2bf0769 100644 --- a/examples/mdview2/Makefile +++ b/examples/mdview2/Makefile @@ -35,8 +35,10 @@ $(DISK_TMP): $(README_DISK): README.MD | $(DISK_TMP) iconv -c -f UTF-8 -t CP866 README.MD > $@ || true +# UTF8TEST.MD кладётся на диск КАК ЕСТЬ (в UTF-8, без перекодировки) — +# это тестовый вход для проверки UTF-8 рендеринга (Фаза 2 кодировок). floppy: $(EXAMPLE).exe $(README_DISK) - python3 $(MAKE_DISK) $(FLOPPY_IMG) $(EXAMPLE).exe $(README_DISK) + python3 $(MAKE_DISK) $(FLOPPY_IMG) $(EXAMPLE).exe $(README_DISK) UTF8TEST.MD @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 index d05c97b..4952b65 100644 --- a/examples/mdview2/README.MD +++ b/examples/mdview2/README.MD @@ -33,9 +33,16 @@ Home Начало документа End Конец документа Left Right Горизонтальный сдвиг (только nowrap-строки) F1 Окно справки +F8 Кодировка: CP866 → CP1251 → KOI8-R → UTF-8 → ... F10 / Esc Выход из программы ``` +Кодировка определяется автоматически при открытии (BOM + эвристика по +первым 4 КБ); `F8` переключает её вручную, если детекция ошиблась. +8-битные кодировки (CP866/CP1251/KOI8-R) переключаются мгновенно (ремап на +отрисовке). Второй набор индекс/кэша (например UTF-8) строится лениво — при +первом переключении в него (короткая пауза со спиннером), дальше мгновенно. + ## Синтаксис Markdown ### Заголовки @@ -102,7 +109,7 @@ int main(void) { ## Технические характеристики - **Платформа:** Sprinter, процессор Z80 @ 21 МГц -- **Кодировка:** CP866 (DOS Cyrillic) +- **Кодировки:** CP866 / CP1251 / KOI8-R / UTF-8 (автоопределение, `F8`) - **Максимальный размер файла:** 128 КБ - **Максимальное число строк в индексе:** 16 384 - **Режим памяти:** small @@ -115,15 +122,9 @@ int main(void) { разрешить работать с большим кол-вом страниц памяти, пока оттестированно на работе с 8-мю страницами по 16Кб. -2. **Форматированные таблицы.** Разбирать строки вида `| ячейка | ячейка |` - с автоматическим выравниванием столбцов и отрисовкой разделительных - линий (строки `|---|---|`). На текущий момент таблицы отображаются - как обычные nowrap-строки без выравнивания. - -3. **Поддержка кодировок CP1251 и UTF-8.** Автоопределение кодировки - по BOM, явное указание через аргумент командной строки (`--encoding cp1251`), - возможность переключения кодировки во время просмотра (`F8`). - Нужно прежде всего для документов на русском языке; CP866 кодировака уже поддерживается. +Сделано: форматированные таблицы с рамкой; поддержка кодировок +CP866 / CP1251 / KOI8-R / UTF-8 с автоопределением и переключением по `F8` +(второй набор строится лениво, по первому переключению). 4. **Ускорение рендеринга.** Кэш строк экрана. Оптимизация цикла вывода символов через BIOS WRCHAR (пакетный вывод, DMA). diff --git a/examples/mdview2/UTF8TEST.MD b/examples/mdview2/UTF8TEST.MD new file mode 100644 index 0000000..e9891b2 --- /dev/null +++ b/examples/mdview2/UTF8TEST.MD @@ -0,0 +1,28 @@ +# Тест UTF-8 + +Это **проверка** кодировки _UTF-8_ в `mdview2`. +Кириллица должна читаться: съешь же ещё этих мягких булок. + +## Символы и пунктуация + +Тире — длинное, и – короткое. Кавычки: «ёлочки» и “лапки”. +Многоточие… стрелки → ← ↑ ↓, галочка ✓ и крестик ✗. +Градус 25°, пункт списка • буллет, номер № 7. + +## Список + +- Первый пункт +- Второй пункт с длинным текстом, чтобы проверить перенос строки по словам на границе экрана восемьдесят символов +- Ёжик, ёлка, объём + +> Цитата: «Краткость — сестра таланта». + +## Таблица + +| Язык | Привет | Число | +|----------|---------------|-------| +| Русский | Здравствуйте | 42 | +| English | Hello | 7 | +| Deutsch | Grüße | 100 | + +Конец файла. diff --git a/examples/mdview2/docs/encoding-plan.md b/examples/mdview2/docs/encoding-plan.md index 1a4d632..cf70860 100644 --- a/examples/mdview2/docs/encoding-plan.md +++ b/examples/mdview2/docs/encoding-plan.md @@ -1,6 +1,8 @@ # mdview2 — поддержка кодировок (CP866 / CP1251 / KOI8-R / UTF-8) -Статус: ПЛАН (2026-06-24, ревизия 2). Реализация — отдельными фазами. +Статус: РЕАЛИЗОВАНО (2026-06-25). Все фазы сделаны; Фаза 4 — eager-кооперативным +вариантом (см. ниже), а не idle-build. Осталась только проверка на железе и +возможная оптимизация «не строить заведомо бесполезный вторичный набор». ## Цель @@ -96,15 +98,32 @@ redraw, draw ремапит). Переиндексация только при (если построен; иначе достроить — но при ленивом build обычно уже готов). - Статус-бар: имя кодировки; меню (строка 31): «F8 Codepage». -## Фазы реализации +## Фазы реализации (ИТОГ) -1. **Ядро 8-бит**: ATTR_BOX; кэш хранит исходный байт; таблицы CP1251/KOI8R; - ремап в draw для CP1251/KOI8; кодировка задаётся константой для теста. -2. **UTF-8 набор**: utf_phys + конвертация + отдельный индекс/кэш; ручной - выбор UTF-8. -3. **Детекция** (BOM + эвристика) при загрузке. -4. **Нон-блокирующий main loop + ленивый idle-build** второго набора. -5. **F8** + статус/меню. +1. ✅ **Ядро 8-бит**: ATTR_BOX; кэш хранит исходный байт; таблицы CP1251/KOI8R; + ремап в draw (`win_rest_remap`). +2. ✅ **UTF-8 набор**: `alloc_and_convert_utf8` + `utf8_convert` (декодер 1/2/3- + байт, 4-байтные/битые → `?`); `utf_cyr_to_866[96]` + символьные подстановки + в `conv_emit_cp` (стрелки 0x18-0x1B, галка 0xFB, буллет 0xF9, тире/кавычки/ + box/° и т.п.; «» → `<`/`>`, т.к. в CP866 гильеметов нет). +3. ✅ **Детекция** (BOM + эвристика), сэмпл — первые **4 КБ** (быстро); хвост- + обрезка multibyte на границе сэмпла не штрафуется. +4. ✅ **Сосуществование 2 наборов** (`docset_t g_doc[2]` + `doc_save/load/switch`, + свап «живых» глобалов). Сборка — **ленивая (build-on-demand)**: при старте + строится только первичный (показываемый) набор; UTF-8 конвертация тоже + ленивая (в `build_doc`, не до первого экрана → старт быстрый). Второй набор + достраивается `switch_encoding()` при первом F8-переходе в него (спиннер, + потом кэш). Eager-вариант отвергнут: удваивал старт и блокировал F8 на время + фоновой сборки. +5. ✅ **F8** полный цикл CP866→CP1251→KOI8R→UTF8→CP866 (внутри 8-бит — ремап, + на границе — `doc_switch`/ленивая сборка). Статус: `enc_name` кол.37. Меню + «F8 Codepage» показывается только когда переключение возможно (`g_f8_enabled`): + во время сборки 8-битного первичного F8 разрешён в `load_key` (только цикл + 8-бит); во время сборки UTF-8 первичного метка F8 скрыта. F1-справка: секция + Encoding. + +NB: меню-строка разбита на 10 блоков по 8 колонок, метки Fn кладутся в блок +(n-1)*8 (F1→0, F8→7, F10→9). ## Память - 8-бит: src + scratch-страница для ремапа (1 стр.). Доп. индекса нет. diff --git a/examples/mdview2/mdview2.c b/examples/mdview2/mdview2.c index 8d13db8..ed43d9f 100644 --- a/examples/mdview2/mdview2.c +++ b/examples/mdview2/mdview2.c @@ -30,7 +30,6 @@ #include #include #include -#include /* DEBUG: время обработки в статус-баре прогресса */ /* ---- Геометрия экрана -------------------------------------------- */ @@ -85,7 +84,8 @@ static const uint8_t md_pallete[16 * 4] = { #define ATTR_TEXT_STRIKE COLOR(COLOR_DARKGRAY, COLOR_BLUE) #define ATTR_LIST_MARKER COLOR(COLOR_LIGHTCYAN, COLOR_BLUE) #define ATTR_QUOTE_MARKER COLOR(COLOR_CYAN, COLOR_BLUE) -#define ATTR_HR COLOR(COLOR_LIGHTGRAY, COLOR_BLUE) +#define ATTR_HR COLOR(COLOR_CYAN, COLOR_BLUE) +#define ATTR_BOX COLOR(COLOR_CYAN, COLOR_BLUE) /* рамка таблиц; отдельный (не контентный) attr — чтобы ремап кодировок не трогал box-глифы */ #define ATTR_TRUNC COLOR(COLOR_YELLOW, COLOR_BLUE) #define ATTR_BAR COLOR(COLOR_BLACK, COLOR_LIGHTCYAN) #define ATTR_BAR_SPINNER COLOR(COLOR_WHITE, COLOR_LIGHTCYAN) @@ -104,7 +104,7 @@ static const uint8_t md_pallete[16 * 4] = { #define HELP_X 8u /* левая граница рамки */ #define HELP_Y 4u /* верхняя граница рамки */ #define HELP_W 64u /* ширина рамки (включая │) */ -#define HELP_H 22u /* высота рамки (включая ─) */ +#define HELP_H 26u /* высота рамки (включая ─) */ /* ---- Глобальное состояние ---------------------------------------- */ @@ -287,11 +287,75 @@ static char filename[64] = {0}; * index_lines() ещё досканирует файл (кооперативно, без прерываний). */ static uint8_t g_loading = 0; /* 1 во время index_lines() */ static uint8_t g_first_drawn = 0; /* первый экран уже нарисован */ +static uint8_t g_f8_enabled = 0; /* можно ли сейчас переключать кодировку (метка F8 в меню + F8 в load_key) */ +static uint8_t g_utf_failed = 0; /* UTF-8 набор не удалось построить (нет EMM) — пропускаем UTF-8 в цикле F8 */ static uint8_t g_abort = 0; /* F10/Esc во время загрузки → прервать индексацию */ static uint16_t g_last_prog = 0; /* n_lines на последнем обновлении статуса */ -static datetime_t g_load_t0; /* DEBUG: старт обработки (для времени в статусе) */ static void progress_tick(void); /* определена ниже (нужна в index_lines) */ +/* Кодировки. 8-битные (CP866/CP1251/KOI8R) — общая структура, различаются + * только ремапом глифов [128-255] на этапе отрисовки; UTF-8 — отдельный набор. */ +#define ENC_CP866 0 +#define ENC_CP1251 1 +#define ENC_KOI8R 2 +#define ENC_UTF8 3 +static uint8_t g_encoding = ENC_CP866; /* активная кодировка */ +static const uint8_t *g_remap = 0; /* активная таблица ремапа (CP1251/KOI8), 0 = без ремапа */ +static uint8_t g_scratch_phys = 0; /* EMM-страница под ремап-буфер строки для win_rest */ + +/* UTF-8 набор (Фаза 2): src декодируется в отдельный блок EMM-страниц как + * чистый CP866. После конвертации активный буфер файла (file_phys/file_size/ + * file_pages, который читает fb()) перенаправляется на эти страницы, и весь + * существующий конвейер индексации/кэша работает по ним без изменений — + * конвертированный поток это уже CP866, ремап на отрисовке не нужен. */ +static uint8_t utf_blk = 0; +static uint8_t utf_pages = 0; +static uint8_t utf_phys[MAX_PAGES] = {0}; +static uint32_t utf_size = 0; +static uint8_t utf_avail = 0; /* 1 = UTF-8 набор удалось сконвертировать/построить */ + +/* Снимок исходного (8-битного) файлового буфера — набор DOC_8BIT читает его + * как есть, набор DOC_UTF8 читает utf_phys. */ +static uint8_t orig_file_blk = 0; +static uint8_t orig_file_pages = 0; +static uint32_t orig_file_size = 0; +static uint8_t orig_file_phys[MAX_PAGES] = {0}; + +/* ---------- Два сосуществующих набора индекс/кэш (Фазы 4-5) ---------- + * DOC_8BIT — исходные байты (CP866/CP1251/KOI8R, различаются ремапом на + * отрисовке); DOC_UTF8 — UTF-8, декодированный в CP866. Оба строятся eager + * при старте (кооперативно), поэтому F8 переключает активный набор мгновенно. + * fb()/idx/cache-аксессоры работают по «живым» глобалам; doc_switch() свапает + * их с сохранённой копией нужного набора. */ +typedef struct { + uint8_t built; + uint8_t enc; /* кодировка отображения этого набора */ + uint8_t file_blk, file_pages; + uint32_t file_size; + uint8_t file_phys[MAX_PAGES]; + uint8_t index_blk, index_pages, index_truncated; + uint16_t n_lines, max_lines; + uint8_t index_phys[MAX_INDEX_PAGES]; + uint8_t cache_dir_blk, cache_dir_pages; + uint8_t cache_dir_phys[MAX_CACHE_DIR_PAGES]; + uint8_t cache_content_n_pages, cache_cur_page_idx; + uint16_t cache_cur_off; + uint8_t cache_content_blk[MAX_CACHE_CONTENT_PAGES]; + uint8_t cache_content_phys[MAX_CACHE_CONTENT_PAGES]; + uint16_t top_line; + uint8_t viewport_x; +} docset_t; +#define DOC_8BIT 0 +#define DOC_UTF8 1 +static docset_t g_doc[2] = {{0}, {0}}; /* ОБЯЗАТЕЛЬНО инициализировать: built=0 для обоих + * (SDCC z80 не обнуляет неинициализированные статики + * надёжно — см. memory/sdcc_static_storage_gotcha) */ +static uint8_t g_active_doc = DOC_8BIT; /* какой набор сейчас в живых глобалах */ +static uint8_t g_disp_doc = DOC_8BIT; /* какой набор показан пользователю */ + +static void set_encoding(uint8_t enc); /* forward (нужна в doc_load_live) */ +static void doc_switch(uint8_t target); /* forward (нужна в progress_tick) */ + #define HPAN_STEP 8u /* Спиннер в статус-баре (col=8,row=0). @@ -560,7 +624,8 @@ static void set_pallete(void) { * Загрузка/выгрузка файла * ================================================================== */ -/* Загружает файл в EMM-страницы и подготавливает EMM-индекс. +/* Загружает исходные байты файла в EMM-страницы (без индекса/кэша — те + * выделяются отдельно на КАЖДЫЙ набор через alloc_set_storage()). * Возврат: 0 — успех, отрицательное значение — код ошибки. */ static int load_file(const char *path) { @@ -604,10 +669,16 @@ static int load_file(const char *path) remaining -= chunk; } close(fd); + return 0; +} - /* Выделяем блок индекса в EMM (8 байт на сегмент, 2048 записей/страница). - * Размер берем с запасом относительно файла; если памяти мало — - * уменьшаем количество страниц индекса до доступного значения. */ +/* Выделяет в живые глобалы блок индекса + директорию рендер-кэша под текущий + * набор. file_pages должно быть уже выставлено (размер набора). Сбрасывает + * пул контента в пустое состояние (страницы доращиваются лениво при рендере). + * Возврат: 0 — успех, -5/-6 — нехватка EMM. */ +static int alloc_set_storage(void) +{ + /* Блок индекса (8 байт на сегмент, 2048 записей/страница). */ { uint8_t want = (uint8_t)(file_pages + 1u); if (want > MAX_INDEX_PAGES) want = MAX_INDEX_PAGES; @@ -624,8 +695,7 @@ static int load_file(const char *path) max_lines = (uint16_t)index_pages * INDEX_RECS_PER_PAGE; } - /* Директория рендер-кэша (Фаза 1) — та же ёмкость, что у индекса: - * по одной cache_rec_t на каждую логическую строку. */ + /* Директория рендер-кэша — та же ёмкость, что у индекса. */ { uint8_t want = (uint8_t)(file_pages + 1u); if (want > MAX_CACHE_DIR_PAGES) want = MAX_CACHE_DIR_PAGES; @@ -640,20 +710,32 @@ static int load_file(const char *path) for (uint8_t i = 0; i < cache_dir_pages; i++) cache_dir_phys[i] = mem_get_page(cache_dir_blk, i); } + + /* Свежий пул контента под этот набор. */ + cache_content_n_pages = 0; + cache_cur_page_idx = 0; + cache_cur_off = 0; return 0; } -/* Освобождает выделенные EMM-блоки файла и индекса. */ +static void doc_save_live(uint8_t i); /* forward */ + +/* Освобождает все EMM-блоки обоих наборов (индекс/директория/пул контента) + * плюс два файловых буфера (исходный 8-битный и UTF-8). */ static void unload_file(void) { - if (file_blk) { - mem_free_block(file_blk); - file_blk = 0; - } - if (index_blk) { - mem_free_block(index_blk); - index_blk = 0; + doc_save_live(g_active_doc); /* зафиксировать живой набор в g_doc */ + for (uint8_t s = 0; s < 2; s++) { + docset_t *d = &g_doc[s]; + if (d->index_blk) { mem_free_block(d->index_blk); d->index_blk = 0; } + if (d->cache_dir_blk) { mem_free_block(d->cache_dir_blk); d->cache_dir_blk = 0; } + for (uint8_t i = 0; i < d->cache_content_n_pages; i++) + if (d->cache_content_blk[i]) mem_free_block(d->cache_content_blk[i]); + d->cache_content_n_pages = 0; } + if (orig_file_blk) { mem_free_block(orig_file_blk); orig_file_blk = 0; } + if (utf_blk) { mem_free_block(utf_blk); utf_blk = 0; } + file_blk = 0; } /* ================================================================== @@ -1345,7 +1427,7 @@ static uint32_t scan_join_stream(uint32_t q, uint32_t para_start, uint8_t col, /* ====================== Таблицы: выровненная отрисовка ==================== */ #define TBL_MAX_COLS 16 -#define TBL_ATTR ATTR_TEXT +#define TBL_ATTR ATTR_BOX /* CP866 light box-drawing. */ #define TBL_H 0xC4 /* ─ */ #define TBL_V 0xB3 /* │ */ @@ -1731,6 +1813,319 @@ static uint8_t emph_to_attr(uint8_t emph, uint8_t base_attr) } +/* Таблицы ремапа байтов [128-255] в CP866 (ASCII identity). Сгенерированы + * Python codecs. Кириллица + ходовая пунктуация; неизвестное → '?'. */ +static const uint8_t cp1251_to_866[256] = { + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, + 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x2B, 0x2C, 0x2D, 0x2E, 0x2F, + 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, 0x3F, + 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A, 0x4B, 0x4C, 0x4D, 0x4E, 0x4F, + 0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5A, 0x5B, 0x5C, 0x5D, 0x5E, 0x5F, + 0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, + 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x7B, 0x7C, 0x7D, 0x7E, 0x7F, + 0x3F, 0x3F, 0x27, 0x3F, 0x22, 0x2E, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3C, 0x3F, 0x3F, 0x3F, 0x3F, + 0x3F, 0x27, 0x27, 0x22, 0x22, 0x07, 0x2D, 0x2D, 0x3F, 0x3F, 0x3F, 0x3E, 0x3F, 0x3F, 0x3F, 0x3F, + 0xFF, 0xF6, 0xF7, 0x3F, 0xFD, 0x3F, 0x3F, 0x3F, 0xF0, 0x63, 0xF2, 0x3C, 0x3F, 0x3F, 0x72, 0xF4, + 0xF8, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0xFA, 0xF1, 0xFC, 0xF3, 0x3E, 0x3F, 0x3F, 0x3F, 0xF5, + 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8A, 0x8B, 0x8C, 0x8D, 0x8E, 0x8F, + 0x90, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9A, 0x9B, 0x9C, 0x9D, 0x9E, 0x9F, + 0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xAB, 0xAC, 0xAD, 0xAE, 0xAF, + 0xE0, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA, 0xEB, 0xEC, 0xED, 0xEE, 0xEF, +}; +static const uint8_t koi8r_to_866[256] = { + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, + 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x2B, 0x2C, 0x2D, 0x2E, 0x2F, + 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, 0x3F, + 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A, 0x4B, 0x4C, 0x4D, 0x4E, 0x4F, + 0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5A, 0x5B, 0x5C, 0x5D, 0x5E, 0x5F, + 0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, + 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x7B, 0x7C, 0x7D, 0x7E, 0x7F, + 0xC4, 0xB3, 0xDA, 0xBF, 0xC0, 0xD9, 0xC3, 0xB4, 0xC2, 0xC1, 0xC5, 0xDF, 0xDC, 0xDB, 0xDD, 0xDE, + 0xB0, 0xB1, 0xB2, 0x3F, 0xFE, 0xF9, 0xFB, 0x3F, 0x3F, 0x3F, 0xFF, 0x3F, 0xF8, 0x3F, 0xFA, 0x3F, + 0xCD, 0xBA, 0xD5, 0xF1, 0xD6, 0xC9, 0xB8, 0xB7, 0xBB, 0xD4, 0xD3, 0xC8, 0xBE, 0xBD, 0xBC, 0xC6, + 0xC7, 0xCC, 0xB5, 0xF0, 0xB6, 0xB9, 0xD1, 0xD2, 0xCB, 0xCF, 0xD0, 0xCA, 0xD8, 0xD7, 0xCE, 0x63, + 0xEE, 0xA0, 0xA1, 0xE6, 0xA4, 0xA5, 0xE4, 0xA3, 0xE5, 0xA8, 0xA9, 0xAA, 0xAB, 0xAC, 0xAD, 0xAE, + 0xAF, 0xEF, 0xE0, 0xE1, 0xE2, 0xE3, 0xA6, 0xA2, 0xEC, 0xEB, 0xA7, 0xE8, 0xED, 0xE9, 0xE7, 0xEA, + 0x9E, 0x80, 0x81, 0x96, 0x84, 0x85, 0x94, 0x83, 0x95, 0x88, 0x89, 0x8A, 0x8B, 0x8C, 0x8D, 0x8E, + 0x8F, 0x9F, 0x90, 0x91, 0x92, 0x93, 0x86, 0x82, 0x9C, 0x9B, 0x87, 0x98, 0x9D, 0x99, 0x97, 0x9A, +}; + +/* Устанавливает активную кодировку и таблицу ремапа (для 8-битных). */ +static void set_encoding(uint8_t enc) +{ + g_encoding = enc; + g_remap = (enc == ENC_CP1251) ? cp1251_to_866 : + (enc == ENC_KOI8R) ? koi8r_to_866 : (const uint8_t *)0; +} + +/* ---------- Сохранение/восстановление/переключение наборов ---------- + * doc_save_live: живые глобалы → g_doc[i]. doc_load_live: g_doc[i] → живые + * глобалы (+ инвалидация маппинга fb() и активной кодировки). */ +static void doc_save_live(uint8_t i) +{ + docset_t *d = &g_doc[i]; + d->enc = g_encoding; + d->file_blk = file_blk; d->file_pages = file_pages; d->file_size = file_size; + memcpy(d->file_phys, file_phys, MAX_PAGES); + d->index_blk = index_blk; d->index_pages = index_pages; d->index_truncated = index_truncated; + d->n_lines = n_lines; d->max_lines = max_lines; + memcpy(d->index_phys, index_phys, MAX_INDEX_PAGES); + d->cache_dir_blk = cache_dir_blk; d->cache_dir_pages = cache_dir_pages; + memcpy(d->cache_dir_phys, cache_dir_phys, MAX_CACHE_DIR_PAGES); + d->cache_content_n_pages = cache_content_n_pages; + d->cache_cur_page_idx = cache_cur_page_idx; d->cache_cur_off = cache_cur_off; + memcpy(d->cache_content_blk, cache_content_blk, MAX_CACHE_CONTENT_PAGES); + memcpy(d->cache_content_phys, cache_content_phys, MAX_CACHE_CONTENT_PAGES); + d->top_line = top_line; d->viewport_x = viewport_x; +} + +static void doc_load_live(uint8_t i) +{ + docset_t *d = &g_doc[i]; + file_blk = d->file_blk; file_pages = d->file_pages; file_size = d->file_size; + memcpy(file_phys, d->file_phys, MAX_PAGES); + index_blk = d->index_blk; index_pages = d->index_pages; index_truncated = d->index_truncated; + n_lines = d->n_lines; max_lines = d->max_lines; + memcpy(index_phys, d->index_phys, MAX_INDEX_PAGES); + cache_dir_blk = d->cache_dir_blk; cache_dir_pages = d->cache_dir_pages; + memcpy(cache_dir_phys, d->cache_dir_phys, MAX_CACHE_DIR_PAGES); + cache_content_n_pages = d->cache_content_n_pages; + cache_cur_page_idx = d->cache_cur_page_idx; cache_cur_off = d->cache_cur_off; + memcpy(cache_content_blk, d->cache_content_blk, MAX_CACHE_CONTENT_PAGES); + memcpy(cache_content_phys, d->cache_content_phys, MAX_CACHE_CONTENT_PAGES); + top_line = d->top_line; viewport_x = d->viewport_x; + cur_page = 0xFF; /* следующий fb() пере-смаппит страницу */ + set_encoding(d->enc); +} + +static void doc_switch(uint8_t target) +{ + if (target == g_active_doc) return; + doc_save_live(g_active_doc); + doc_load_live(target); + g_active_doc = target; +} + +/* Самые ходовые строчные русские буквы (о е а и н т с р в л) — их байты + * различают 8-битные кодировки по частоте. */ +static uint8_t common_cp866 (uint8_t b){ return b==0xAE||b==0xA5||b==0xA0||b==0xA8||b==0xAD||b==0xE2||b==0xE1||b==0xE0||b==0xA2||b==0xAB; } +static uint8_t common_cp1251(uint8_t b){ return b==0xEE||b==0xE5||b==0xE0||b==0xE8||b==0xED||b==0xF2||b==0xF1||b==0xF0||b==0xE2||b==0xEB; } +static uint8_t common_koi8r (uint8_t b){ return b==0xCF||b==0xC5||b==0xC1||b==0xC9||b==0xCE||b==0xD4||b==0xD3||b==0xD2||b==0xD7||b==0xCC; } + +/* Автоопределение кодировки дешёвым сканом байтов (до построения индекса). + * BOM → UTF8; иначе валидность UTF-8 (структура multibyte); иначе 8-бит по + * частоте ходовых букв; фолбэк CP866. */ +static uint8_t detect_encoding(void) +{ + if (file_size >= 3 && (uint8_t)fb(0) == 0xEF && (uint8_t)fb(1) == 0xBB && (uint8_t)fb(2) == 0xBF) + return ENC_UTF8; + + uint32_t n = file_size; + if (n > 4096u) n = 4096u; /* сэмпл: первые 4 КБ — детекции хватает */ + uint8_t utf_ok = 1, has_mb = 0, has_high = 0, cont = 0; + uint16_t s866 = 0, s1251 = 0, skoi = 0; + + for (uint32_t p = 0; p < n; p++) { + uint8_t b = (uint8_t)fb(p); + if (b < 0x80) { if (cont) { utf_ok = 0; cont = 0; } continue; } + has_high = 1; + if (common_cp866(b)) s866++; + if (common_cp1251(b)) s1251++; + if (common_koi8r(b)) skoi++; + if (cont) { + if ((b & 0xC0) == 0x80) cont--; + else { utf_ok = 0; cont = 0; } + } else if (b >= 0xC2 && b <= 0xDF) { cont = 1; has_mb = 1; } + else if (b >= 0xE0 && b <= 0xEF) { cont = 2; has_mb = 1; } + else if (b >= 0xF0 && b <= 0xF4) { cont = 3; has_mb = 1; } + else utf_ok = 0; /* битый лид/одиночный континюэйшн */ + } + /* Незавершённая multibyte-последовательность на КОНЦЕ — нарушение только + * если это настоящий EOF; на границе сэмпла (n= s866 && s1251 >= skoi) return ENC_CP1251; + if (skoi >= s866) return ENC_KOI8R; + return ENC_CP866; +} + +static const char *enc_name(uint8_t enc) +{ + return (enc == ENC_CP1251) ? "CP1251" : + (enc == ENC_KOI8R) ? "KOI8-R" : + (enc == ENC_UTF8) ? "UTF-8 " : "CP866 "; +} + +/* Контентный глиф (ремапим при смене кодировки) vs структурный (рамка/HR/ + * маркеры — уже CP866, не трогаем). Различаем по attr. */ +static uint8_t attr_is_content(uint8_t a) +{ + return (uint8_t)(a != ATTR_BOX && a != ATTR_HR && + a != ATTR_LIST_MARKER && a != ATTR_QUOTE_MARKER); +} + +/* Вывод среза строки кэша с ремапом контентных глифов [128-255] через + * g_remap (CP1251/KOI8). Без активной таблицы — прямой win_rest. */ +static void win_rest_remap(uint8_t row, uint8_t w, uint8_t page, uint16_t off) +{ + if (!g_remap || w == 0) { win_rest(row, 0, 1, w, page, off); return; } + uint8_t buf[SCREEN_W * 2]; + bank_read(page, off, buf, (uint16_t)w * 2u); + for (uint8_t i = 0; i < w; i++) { + uint8_t ch = buf[(uint16_t)i * 2u]; + if (ch >= 0x80 && attr_is_content(buf[(uint16_t)i * 2u + 1u])) + buf[(uint16_t)i * 2u] = g_remap[ch]; + } + bank_write(g_scratch_phys, 0, buf, (uint16_t)w * 2u); + win_rest(row, 0, 1, w, g_scratch_phys, 0); +} + +/* ========================= UTF-8 → CP866 (Фаза 2) ======================== */ + +/* Кириллица U+0400..U+045F → CP866. Русский набор + Ё/ё и часть украинских + * (Є є Ї ї Ў ў), которые есть в CP866; пропуски → '?' (0x3F). */ +static const uint8_t utf_cyr_to_866[96] = { + /* 0400 */ 0x3F, 0xF0, 0x3F, 0x3F, 0xF2, 0x3F, 0x3F, 0xF4, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0xF6, 0x3F, + /* 0410 */ 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8A, 0x8B, 0x8C, 0x8D, 0x8E, 0x8F, + /* 0420 */ 0x90, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9A, 0x9B, 0x9C, 0x9D, 0x9E, 0x9F, + /* 0430 */ 0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xAB, 0xAC, 0xAD, 0xAE, 0xAF, + /* 0440 */ 0xE0, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA, 0xEB, 0xEC, 0xED, 0xEE, 0xEF, + /* 0450 */ 0x3F, 0xF1, 0x3F, 0x3F, 0xF3, 0x3F, 0x3F, 0xF5, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0xF7, 0x3F, +}; + +/* Поток вывода конвертера: пишем в utf_phys чанками по странице, буфер в + * near-памяти, флаш через один bank_write (не побайтовый swap W3). */ +static uint8_t conv_buf[256]; +static uint16_t conv_n = 0; /* байт в буфере, ещё не сброшено */ +static uint8_t conv_page = 0; /* индекс текущей страницы в utf_phys */ +static uint16_t conv_off = 0; /* уже сброшенное смещение в этой странице */ + +static void conv_flush(void) +{ + if (conv_n) { + bank_write(utf_phys[conv_page], conv_off, conv_buf, conv_n); + conv_off = (uint16_t)(conv_off + conv_n); + conv_n = 0; + } +} + +/* Добавляет один CP866-байт в выходной поток. Запись никогда не пересекает + * границу страницы (256 делит 16384 нацело). */ +static void conv_put(uint8_t b) +{ + if ((uint16_t)(conv_off + conv_n) == PAGE_SIZE) { /* страница заполнена */ + conv_flush(); + conv_page++; + conv_off = 0; + } + conv_buf[conv_n++] = b; + if (conv_n == (uint16_t)sizeof(conv_buf)) conv_flush(); +} + +/* Кодпойнт → CP866-глиф(ы). Кириллица по таблице; ходовые символы (тире, + * кавычки, буллет, стрелки, галочки, box-drawing) — подстановкой; BOM + * проглатывается; прочее → '?'. */ +static void conv_emit_cp(uint32_t cp) +{ + if (cp < 0x80) { conv_put((uint8_t)cp); return; } + if (cp >= 0x0400 && cp <= 0x045F) { conv_put(utf_cyr_to_866[cp - 0x0400]); return; } + switch (cp) { + case 0x00A0: conv_put(' '); break; /* nbsp */ + case 0x00AB: conv_put('<'); break; /* « (в CP866 гильеметов нет — как в cp1251_to_866) */ + case 0x00BB: conv_put('>'); break; /* » */ + case 0x00B0: conv_put(0xF8); break; /* ° */ + case 0x00B7: conv_put(0xFA); break; /* · */ + case 0x2013: case 0x2014: conv_put('-'); break; /* – — */ + case 0x2018: case 0x2019: conv_put('\''); break; /* ‘ ’ */ + case 0x201C: case 0x201D: conv_put('"'); break; /* “ ” */ + case 0x2022: conv_put(0xF9); break; /* • → ∙ (пункт списка) */ + case 0x2026: conv_put('.'); conv_put('.'); conv_put('.'); break; /* … */ + case 0x2190: conv_put(0x1B); break; /* ← */ + case 0x2192: conv_put(0x1A); break; /* → */ + case 0x2191: conv_put(0x18); break; /* ↑ */ + case 0x2193: conv_put(0x19); break; /* ↓ */ + case 0x2713: case 0x2714: conv_put(0xFB); break; /* ✓ ✔ → галка */ + case 0x2715: case 0x2717: case 0x2718: conv_put('x'); break; /* ✕ ✗ ✘ */ + case 0x2500: conv_put(0xC4); break; /* ─ */ + case 0x2502: conv_put(0xB3); break; /* │ */ + case 0x250C: conv_put(0xDA); break; /* ┌ */ + case 0x2510: conv_put(0xBF); break; /* ┐ */ + case 0x2514: conv_put(0xC0); break; /* └ */ + case 0x2518: conv_put(0xD9); break; /* ┘ */ + case 0x251C: conv_put(0xC3); break; /* ├ */ + case 0x2524: conv_put(0xB4); break; /* ┤ */ + case 0x252C: conv_put(0xC2); break; /* ┬ */ + case 0x2534: conv_put(0xC1); break; /* ┴ */ + case 0x253C: conv_put(0xC5); break; /* ┼ */ + case 0x2580: conv_put(0xDF); break; /* ▀ */ + case 0x2584: conv_put(0xDC); break; /* ▄ */ + case 0x2588: conv_put(0xDB); break; /* █ */ + case 0x2591: conv_put(0xB0); break; /* ░ */ + case 0x2592: conv_put(0xB1); break; /* ▒ */ + case 0x2593: conv_put(0xB2); break; /* ▓ */ + case 0x25A0: conv_put(0xFE); break; /* ■ */ + case 0x2116: conv_put(0xFC); break; /* № */ + case 0xFEFF: break; /* BOM / ZWNBSP — выкинуть */ + default: conv_put('?'); break; + } +} + +/* Декодирует активный (исходный UTF-8) буфер в utf_phys как CP866. + * Битые/обрезанные последовательности → '?'; 4-байтные (эмодзи и т.п.) → '?'. */ +static void utf8_convert(void) +{ + conv_n = 0; conv_page = 0; conv_off = 0; + uint32_t n = file_size; + uint32_t p = 0; + while (p < n) { + if ((uint16_t)((uint16_t)p & 0x0FFFu) == 0) spinner_tick(); + uint8_t b = (uint8_t)fb(p++); + if (b < 0x80) { conv_emit_cp(b); continue; } + + uint32_t cp; + uint8_t need; + if ((b & 0xE0) == 0xC0) { cp = (uint32_t)(b & 0x1F); need = 1; } + else if ((b & 0xF0) == 0xE0) { cp = (uint32_t)(b & 0x0F); need = 2; } + else if ((b & 0xF8) == 0xF0) { cp = (uint32_t)(b & 0x07); need = 3; } + else { conv_put('?'); continue; } /* одиночный континюэйшн/битый лид */ + + uint8_t ok = 1; + for (uint8_t k = 0; k < need; k++) { + if (p >= n) { ok = 0; break; } + uint8_t cb = (uint8_t)fb(p); + if ((cb & 0xC0) != 0x80) { ok = 0; break; } + cp = (cp << 6) | (uint32_t)(cb & 0x3F); + p++; + } + if (!ok || need == 3) { conv_put('?'); continue; } /* битое / 4-байтное */ + conv_emit_cp(cp); + } + conv_flush(); + utf_size = (uint32_t)conv_page * PAGE_SIZE + conv_off; +} + +/* Выделяет страницы под UTF-8 набор и конвертирует туда исходник (читает + * оригинал — вызывать, когда живой буфер = исходные байты). НЕ перенаправляет + * активный буфер: это делает build_doc() при сборке набора DOC_UTF8. + * Конвертированный поток ≤ исходного (кириллица 2 байта → 1), хватает того же + * числа страниц. Возврат: 1 — успех, 0 — нет EMM. */ +static uint8_t alloc_and_convert_utf8(void) +{ + utf_pages = file_pages; + utf_blk = mem_alloc_pages(utf_pages); + if (utf_blk == 0) return 0; + for (uint8_t i = 0; i < utf_pages; i++) + utf_phys[i] = mem_get_page(utf_blk, i); + + utf8_convert(); /* читает оригинал (active) → utf_phys */ + return 1; +} + /* Фаза 4-5 — cache-only draw path: рисует одну строку ИЗ КЭША, без * единого обращения к fb()/исходному файлу. Горизонтальный скролл для * nowrap-строк (Фаза 5) — это просто смещение начала среза на vx*2 @@ -1757,7 +2152,7 @@ static void draw_line_from_cache(uint16_t line_idx, uint8_t row) uint16_t remaining = (uint16_t)(rec.len - vx); uint8_t w = (remaining > SCREEN_W) ? SCREEN_W : (uint8_t)remaining; uint16_t off = (uint16_t)(rec.off + (uint16_t)vx * 2u); - win_rest(row, 0, 1, w, rec.page, off); + win_rest_remap(row, w, rec.page, off); if (remaining > SCREEN_W) { wrchar(SCREEN_W - 1, row, '>', ATTR_TRUNC); } @@ -1823,18 +2218,6 @@ static void render_status_numbers(void) gotoxy( 73, 0); dec8(pct); cputs("%"); - - if (g_loading) { /* DEBUG: время обработки (зона имени файла) */ - datetime_t now; - getdatetime(&now); - int16_t el = (int16_t)now.minute*60 + now.second - - ((int16_t)g_load_t0.minute*60 + g_load_t0.second); - if (el < 0) el += 3600; - gotoxy(31, 0); - cputs("t="); - dec16((uint16_t)el); - cputs("s "); /* трейлинг-пробелы затирают прошлую ширину */ - } } /* Полностью перерисовывает статус-бар (заголовок + числа). */ @@ -1845,6 +2228,7 @@ static void render_full_status(void) /* Размещение в шапке: "MDVIEW", затем слот спиннера и имя файла. */ put_str_attr(1, 0, "MDVIEW", ATTR_BAR); put_str_attr(10, 0, filename, ATTR_BAR); + put_str_attr(37, 0, enc_name(g_encoding), ATTR_BAR); /* текущая кодировка */ render_status_numbers(); } @@ -1857,12 +2241,18 @@ static void render_updated_status(void) /* Отрисовывает нижнюю строку меню. */ static void render_menu(void) { + /* Строка меню разбита на 10 блоков по 8 колонок: метку Fn кладём в блок + * (n-1)*8, чтобы позиции соответствовали номерам клавиш (F1→0, F8→7, + * F10→9). Пока заполнены только F1/F8/F10. */ fill_row(MENU_ROW, ATTR_MENU_T); - put_str_attr(1, MENU_ROW, "F1", ATTR_MENU_K); - put_str_attr(4, MENU_ROW, "Help", ATTR_MENU_T); - /* F2 зарезервирован: режимов wrap/unwrap больше нет. */ - put_str_attr(SCREEN_W - 10, MENU_ROW, "F10", ATTR_MENU_K); - put_str_attr(SCREEN_W - 5, MENU_ROW, "Exit", ATTR_MENU_T); + put_str_attr(0 * 8 + 1, MENU_ROW, "F1", ATTR_MENU_K); + put_str_attr(0 * 8 + 4, MENU_ROW, "Help", ATTR_MENU_T); + if (g_f8_enabled) { /* F8 показываем только когда переключение кодировки возможно */ + put_str_attr(7 * 8 + 1, MENU_ROW, "F8", ATTR_MENU_K); + put_str_attr(7 * 8 + 4, MENU_ROW, "Codepage", ATTR_MENU_T); + } + put_str_attr(9 * 8 + 0, MENU_ROW, "F10", ATTR_MENU_K); + put_str_attr(9 * 8 + 4, MENU_ROW, "Exit", ATTR_MENU_T); } /* ================================================================== @@ -2039,6 +2429,10 @@ static void show_help(void) help_line(r++, " - * + item 1. 2. Ordered list", ATTR_HELP_BG); help_line(r++, " |----|----| Tables", ATTR_HELP_BG); help_line(r++, "", ATTR_HELP_BG); + help_line(r++, " Encoding:", ATTR_HELP_HDR); + help_line(r++, " F8 Cycle CP866 / CP1251 / KOI8-R / UTF-8", ATTR_HELP_BG); + help_line(r++, " Auto-detected on open; F8 to override", ATTR_HELP_BG); + help_line(r++, "", ATTR_HELP_BG); help_line(r++, " File size: up to 128 KB (EMM). Lines: up to 16384.", ATTR_HELP_BG); help_line(r++, "", ATTR_HELP_BG); @@ -2073,6 +2467,16 @@ static void load_key(uint16_t k) case KEY_LEFT: scroll_h(-(int8_t)HPAN_STEP); render_status_numbers(); return; case KEY_RIGHT: scroll_h(+(int8_t)HPAN_STEP); render_status_numbers(); return; case KEY_F1: show_help(); return; + case KEY_F8: /* во время сборки крутим ТОЛЬКО 8-бит и только если набор 8-битный + * (ремап на отрисовке; индексация не зависит от кодировки) */ + if (g_f8_enabled && g_active_doc == DOC_8BIT) { + uint8_t nx = (g_encoding == ENC_CP866) ? ENC_CP1251 : + (g_encoding == ENC_CP1251) ? ENC_KOI8R : ENC_CP866; + set_encoding(nx); g_doc[DOC_8BIT].enc = nx; + draw_viewport_from_cache(); + render_full_status(); + } + return; default: return; } if (nt > maxtop) nt = maxtop; @@ -2086,8 +2490,8 @@ static void load_key(uint16_t k) /* Кооперативный «тик» прогрессивной загрузки — вызывается из index_lines(). */ static void progress_tick(void) { + /* Прогрессивная отрисовка строящегося (всегда показываемого) набора. */ uint8_t touched = 0; - if (!g_first_drawn && drawable_lines() >= VIEW_H) { draw_viewport_from_cache(); render_status_numbers(); @@ -2113,6 +2517,81 @@ static void progress_tick(void) * Точка входа * ================================================================== */ +/* Строит набор `slot` кооперативно (index_lines + progress_tick). + * visible=1 — первичный (показываемый): прогрессивная отрисовка первого экрана + * и скролл по готовым строкам. visible=0 — вторичный: index_lines строит его в + * живых глобалах, а progress_tick через свап показывает/листает уже готовый + * g_disp_doc. Возврат: 0 — успех, <0 — нехватка EMM (-5/-6/-7). + * + * UTF-8 конвертация делается ЛЕНИВО здесь, а не до первого экрана: при входе + * (для DOC_UTF8 ещё не сконвертированного) живой буфер обязан быть = оригинал + * (8-битные байты) — utf8_convert читает его через fb(). Это так и в случае + * первичного UTF-8 (сразу после load_file), и вторичного (после сборки + * первичного 8-битного, чей буфер = оригинал). */ +static int build_doc(uint8_t slot, uint8_t enc, uint8_t visible) +{ + if (slot == DOC_UTF8) { + if (!utf_avail) { /* конвертируем сейчас (live = оригинал) */ + if (!alloc_and_convert_utf8()) return -7; + utf_avail = 1; + } + file_blk = utf_blk; file_pages = utf_pages; file_size = utf_size; + memcpy(file_phys, utf_phys, MAX_PAGES); + } else { + 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) return rc; + + set_encoding(enc); + top_line = 0; viewport_x = 0; + g_active_doc = slot; + if (visible) { g_disp_doc = slot; g_first_drawn = 0; } + + g_loading = 1; + g_last_prog = 0; + index_lines(); + g_loading = 0; + + g_doc[slot].built = 1; + doc_save_live(slot); + return 0; +} + +/* Переключает кодировку на `next` (вызывается из главного цикла по F8). + * 8-бит↔8-бит внутри одного набора — мгновенный ремап; переход в другой набор + * — мгновенный doc_switch, если он построен, иначе ЛЕНИВАЯ сборка по месту + * (со спиннером), результат кэшируется. */ +static void switch_encoding(uint8_t next) +{ + uint8_t target = (next == ENC_UTF8) ? DOC_UTF8 : DOC_8BIT; + + if (target == g_active_doc) { /* тот же набор — только ремап */ + set_encoding(next); g_doc[target].enc = next; + } else if (g_doc[target].built) { /* набор готов — мгновенный свап */ + doc_switch(target); g_disp_doc = target; + set_encoding(next); g_doc[target].enc = next; + } else { /* строим набор лениво */ + doc_save_live(g_active_doc); /* сохранить текущий (позиция и т.п.) */ + spinner_show(1); + int rc = build_doc(target, next, 1); /* visible: рисует прогрессивно */ + spinner_show(0); + if (rc < 0) { /* нет EMM — откат на текущий набор */ + if (target == DOC_UTF8) g_utf_failed = 1; + doc_load_live(g_active_doc); + clamp_top(); draw_viewport_from_cache(); render_full_status(); + return; + } + g_disp_doc = target; /* build_doc уже выставил active/enc */ + } + clamp_top(); + draw_viewport_from_cache(); + render_full_status(); +} + int main(int argc, char **argv) { const char *path; @@ -2172,15 +2651,44 @@ int main(int argc, char **argv) cputs("\n\r\n\r"); return 1; } - /* Прогрессивная загрузка: index_lines() сама рисует первый экран и пускает - * скролл по готовым данным через progress_tick(). */ - g_loading = 1; - g_first_drawn = 0; - g_last_prog = 0; - getdatetime(&g_load_t0); /* DEBUG: старт обработки */ - index_lines(); - g_loading = 0; - spinner_show(0); + /* Scratch-страница под ремап-буфер строки (win_rest_remap). */ + g_scratch_phys = mem_get_page(mem_alloc_pages(1), 0); + + /* Оба набора пока не построены (страховка к инициализатору g_doc). */ + g_doc[DOC_8BIT].built = 0; + g_doc[DOC_UTF8].built = 0; + + /* Снимок исходного 8-битного буфера (его читает набор DOC_8BIT). */ + orig_file_blk = file_blk; orig_file_pages = file_pages; orig_file_size = file_size; + memcpy(orig_file_phys, file_phys, MAX_PAGES); + + /* Детекция кодировки по оригиналу. UTF-8 конвертацию НЕ делаем здесь — + * она дорогая (полный проход) и нужна только для UTF-8 набора, который + * строится через build_doc лениво. Так первый экран рисуется по индексации + * первичного набора без лишнего прохода. */ + uint8_t det = detect_encoding(); + + /* Первичный (показываемый) набор — по детекции; второй строится ЛЕНИВО по + * первому F8 (switch_encoding). Во время сборки 8-битного первичного F8 + * разрешён (ремап 8-бит, см. load_key) → показываем метку F8; для UTF-8 + * первичного переключаться пока некуда → метку прячем. */ + uint8_t prim_enc = det; + uint8_t prim_doc = (prim_enc == ENC_UTF8) ? DOC_UTF8 : DOC_8BIT; + g_f8_enabled = (prim_doc == DOC_8BIT); + render_menu(); + + rc = build_doc(prim_doc, prim_enc, 1); /* первичный — видимый */ + if (rc < 0 && prim_doc == DOC_UTF8) { /* UTF-8 не вышел (нет EMM) → 8-бит */ + prim_doc = DOC_8BIT; prim_enc = ENC_CP866; + rc = build_doc(DOC_8BIT, ENC_CP866, 1); + } + if (rc < 0) { + spinner_show(0); + clrscr_attr(ATTR_RESET); + textattr(ATTR_RESET); + cputs("MDView: index/cache alloc failed\n\r\n\r"); + return 1; + } if (g_abort) goto exit_loop; /* F10/Esc во время загрузки — корректный выход */ if (n_lines == 0) { die("mdview: empty file"); @@ -2188,7 +2696,12 @@ int main(int argc, char **argv) return 1; } - /* Финальная отрисовка: все строки готовы, top_line мог уехать при скролле. */ + /* Первичный готов — гасим спиннер, включаем F8 (второй набор дособерётся + * лениво по первому переключению) и рисуем финальный статус с ПОЛНЫМ + * счётчиком строк. */ + spinner_show(0); + g_f8_enabled = 1; + render_menu(); clamp_top(); render_full_status(); draw_viewport_from_cache(); @@ -2204,6 +2717,15 @@ int main(int argc, char **argv) } switch (scan) { case KEY_F1: show_help(); break; + case KEY_F8: { /* цикл CP866→CP1251→KOI8R→UTF8→CP866; нужный набор дособерётся лениво */ + uint8_t next = (g_encoding == ENC_CP866) ? ENC_CP1251 : + (g_encoding == ENC_CP1251) ? ENC_KOI8R : + (g_encoding == ENC_KOI8R) ? ENC_UTF8 : ENC_CP866; + if (next == ENC_UTF8 && g_utf_failed) next = ENC_CP866; /* UTF-8 недоступен */ + switch_encoding(next); + if (g_abort) goto exit_loop; /* Esc/F10 во время ленивой досборки */ + continue; + } case KEY_F10: goto exit_loop; case KEY_UP: scroll_up(1); break; case KEY_DOWN: scroll_down(1); break;