/* * mdview2 — просмотрщик Markdown для Sprinter (render-cache версия). * * Этот файл — ЯДРО (инфраструктура + точка входа). Прикладные подсистемы * вынесены в отдельные модули (общий интерфейс — mdview2.h): * mdview2_index.c — парсер/индексатор markdown (построение рендер-кэша) * mdview2_md.c — представление MD-документа (отрисовка из кэша + скролл) * mdview2_raw.c — RAW-просмотр исходного текста (F2/F3) * mdview2_enc.c — кодировки CP866/CP1251/KOI8R + UTF-8 конвертер * mdview2_table.c — выровненная отрисовка таблиц * mdview2_help.c — диалог справки (F1) * * В ядре остаётся: пул рендер-кэша, доступ к EMM/окну W3 (fb/win_rest), * примитивы экрана, загрузка файла, наборы документа (doc-slots), кооператив- * ная loading-loop и диспетчер клавиш, сборка наборов и main(). * * Разметка экрана (80x32): * строка 0 — статус-бар (имя файла / диапазон строк / всего / %) * строки 1..30 — область документа (30 видимых строк текста) * строка 31 — меню (F1 Help / F2 RAW / F3 Wrap / F8 CodePg / F10 Exit) * * Память: * CODE → W1 (режим sprinter-cc --memory small) * STACK/HEAP/DATA → W2 * Буфер файла → страницы EMM (до 8 × 16 КБ = 128 КБ), * отображаемые в W3 по требованию через fb(). */ #include #include #include #include #include #include #include #include #include #include #include #include #include "mdview2.h" /* геометрия, атрибуты, общий интерфейс модулей */ /* ---- Палитра атрибутов ------------------------------------------- */ static const uint8_t md_pallete[16 * 4] = { 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x60, 0x00, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0xC8, 0xC8, 0xC8, 0x00, 0xA0, 0xA0, 0x80, 0x00, 0xFF, 0xAA, 0xAA, 0x00, 0xAA, 0xFF, 0xAA, 0x00, 0xFF, 0xFF, 0xAA, 0x00, 0xAA, 0xAA, 0xFF, 0x00, 0xFF, 0xAA, 0xFF, 0x00, 0xAA, 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0xFF }; /* Атрибуты экрана/справки и геометрия — в mdview2.h (общие со всеми модулями). * Типы записей (idx_rec_t — в mdview2_index.c, cache_rec_t — в mdview2.h), * словари флагов/стилей (IF_, CK_, INIT_STYLE_) и константы индекса * (INDEX_RECS_PER_PAGE, MAX_CACHE_LINE_LEN) — в mdview2.h. * * В SDCC/z80 критично явно инициализировать static-переменные (`= 0`): * неинициализированные static-объекты могут конфликтовать по размещению * и перезаписывать друг друга (см. memory/sdcc_static_storage_gotcha). */ #define MAX_INDEX_PAGES 9u /* максимум 9 * 2048 = 18432 сегментов индекса (256 КБ) */ /* ================================================================== * Рендер-кэш: директория и пул EMM-страниц * ================================================================== * Каждая логическая строка экрана рендерится РОВНО ОДИН РАЗ (Фаза 2) * в готовый буфер и больше никогда не парсится повторно при скролле. * Содержимое живёт в ОТДЕЛЬНОМ пуле EMM-страниц (cache_content_*), * не путать с idx_rec_t/index_phys[] выше — те остаются неизменными, * ими пользуется рендер-воркер ПРИ построении кэша (Фаза 2), и их * нельзя перезатирать, т.к. воркер для одной строки может заглядывать * в idx-записи СОСЕДНИХ строк (откат cont-сегментов). * * ЕДИНЫЙ формат для ВСЕХ строк (решено 2026-06-23 — изначальный план * различал тип 1/тип 2 по экономии памяти, но строки таблиц — IF_NOWRAP, * но НЕ IF_CODE — всё равно проходят inline-парсер и могут содержать * несколько атрибутов, так что "один стиль" для них неверно; бюджет EMM * (см. sprinter_emm_budget) с большим запасом покрывает (char,attr) * для всех строк без исключения): контент — `len` пар (char,attr), * `len` — реальная длина в ЯЧЕЙКАХ без паддинга до SCREEN_W, капается на * 255 (патологически длинные nowrap-строки урезаются; индикатор обрезки * добавляется на этапе вывода из кэша, не при рендере в кэш). */ /* cache_rec_t (РОВНО 8 байт, см. sprinter_winrest_format) — в mdview2.h. */ #define CACHE_DIR_REC_SIZE ((uint16_t)sizeof(cache_rec_t)) #define CACHE_DIR_RECS_PER_PAGE (16384 / CACHE_DIR_REC_SIZE) /* 2048 */ #define MAX_CACHE_DIR_PAGES 9u /* симметрично индексу: 9 * 2048 = 18432 записей */ /* MAX_CACHE_LINE_LEN — в mdview2.h (буфер ячеек g_cells живёт в mdview2_index.c). */ #define MAX_CACHE_CONTENT_PAGES 40u /* кап контента на набор: 40 * 16 КБ = 640 КБ (80 на оба набора) */ static uint8_t cache_dir_blk; static uint8_t cache_dir_pages; static uint8_t cache_dir_phys[MAX_CACHE_DIR_PAGES]; static uint8_t cache_content_blk[MAX_CACHE_CONTENT_PAGES]; /* id блока на каждую выделенную страницу */ static uint8_t cache_content_phys[MAX_CACHE_CONTENT_PAGES]; static uint8_t cache_content_n_pages; /* сколько страниц уже выделено */ static uint8_t cache_cur_page_idx; /* индекс текущей страницы записи в cache_content_phys */ static uint16_t cache_cur_off; /* текущее смещение записи в этой странице */ /* Доступ к директории рендер-кэша — тот же приём, что у idx_get/idx_put. */ void cache_dir_get(uint16_t idx, cache_rec_t *r) { uint8_t page = (uint8_t)(idx >> 11); uint16_t off = (uint16_t)((idx & 0x07FF) << 3); /* 2047 */ bank_read(cache_dir_phys[page], off, r, CACHE_DIR_REC_SIZE); } void cache_dir_put(uint16_t idx, const cache_rec_t *r) { uint8_t page = (uint8_t)(idx >> 11); uint16_t off = (uint16_t)((idx & 0x07FF) << 3); /* 2047 */ bank_write(cache_dir_phys[page], off, r, CACHE_DIR_REC_SIZE); } /* Резервирует nbytes подряд в пуле контента рендер-кэша. * Запись никогда не разбивается через границу страницы: если остаток * текущей страницы недостаточен — переходим на следующую целиком, * жертвуя остатком (проще и безопаснее, чем page-crossing в fb()). * При нехватке текущего пула — лениво доращиваем его на 1 страницу. * Возврат: 1 — успех (out_page и out_off заполнены), 0 — EMM кончилась. */ uint8_t cache_reserve(uint16_t nbytes, uint8_t *out_page, uint16_t *out_off) { if (nbytes == 0 || nbytes > PAGE_SIZE) return 0; if (cache_content_n_pages == 0 || (uint16_t)(cache_cur_off + nbytes) > PAGE_SIZE) { /* Нужна новая страница: либо пул ещё пуст, либо текущая страница * не вмещает запись целиком. */ uint8_t next_idx = (cache_content_n_pages == 0) ? 0 : (uint8_t)(cache_cur_page_idx + 1); if (next_idx >= cache_content_n_pages) { if (cache_content_n_pages >= MAX_CACHE_CONTENT_PAGES) return 0; uint8_t blk = mem_alloc_pages(1); if (blk == 0) return 0; cache_content_blk[cache_content_n_pages] = blk; cache_content_phys[cache_content_n_pages] = mem_get_page(blk, 0); cache_content_n_pages++; } cache_cur_page_idx = next_idx; cache_cur_off = 0; } *out_page = cache_content_phys[cache_cur_page_idx]; *out_off = cache_cur_off; cache_cur_off = (uint16_t)(cache_cur_off + nbytes); return 1; } /* Копирует уже готовый буфер (рендер одной строки, собранный в W1/W2 — * НЕ в W3, чтобы не конфликтовать с fb()/чтением исходника во время * рендера) в зарезервированное место в пуле контента. Один bank_write * на строку, а не по байту. len — РОВНО как у cache_reserve()/bank_write * — uint16_t, не uint8_t: строка до MAX_CACHE_LINE_LEN=255 ячеек даёт * до 510 байт (char,attr), это уже не лезет в uint8_t. */ void cache_commit(uint8_t page, uint16_t off, const void *buf, uint16_t len) { bank_write(page, off, buf, len); } /* emph_to_attr определена ниже (рядом с inline-логикой), нужна в inline_scan. */ /* emph_to_attr — в mdview2_index.c. */ uint16_t n_lines; /* extern: всего строк в индексе текущего набора */ uint16_t max_lines; /* extern: ёмкость индекса: index_pages * 2048 */ uint16_t top_line; /* extern: верхняя видимая строка */ uint32_t file_size; /* extern: размер активного буфера документа */ static uint8_t file_blk; static uint8_t file_pages; static uint8_t file_phys[MAX_PAGES]; uint8_t index_blk; /* extern: дескриптор EMM-блока индекса */ uint8_t index_pages; /* */ uint8_t index_phys[MAX_INDEX_PAGES]; /* extern: физ. страницы индекса */ uint8_t index_truncated; /* выставляется при исчерпании ёмкости индекса */ uint8_t g_file_clamped; /* extern: файл >256 КБ обрезан до MAX_FILE при загрузке */ /* cur_seg_off/cur_rec (near-состояние индексатора) — в mdview2_index.c. */ uint8_t cur_page = 0xFF; /* extern: 0xFF = в W3 ещё не показана ни одна страница (extern: кодек инвалидирует) */ uint8_t viewport_x; /* extern: горизонтальный сдвиг для nowrap-строк */ char filename[64]; /* extern: статус-бар */ /* Прогрессивная загрузка: рисуем первый экран и пускаем скролл, пока * index_lines() ещё досканирует файл (кооперативно, без прерываний). */ uint8_t g_view = VIEW_MD; /* extern: активный режим просмотра (существует даже когда доступен только MD режим) */ #if WITH_RAW uint8_t g_ready; /* extern: 1 когда первичный документ построен — F2 (RAW) рабочий */ static uint8_t g_raw_last = VIEW_RAW_WRAP; /* последний под-режим RAW (возврат по F2) */ #if WITH_HEX static uint8_t g_hex_last = VIEW_MD; /* вид, из которого вошли в HEX (возврат по F4) */ #endif #endif 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 на последнем обновлении статуса */ /* Кодировки (ENC_*) и g_encoding — в mdview2.h / mdview2_enc.c. */ const uint8_t *g_remap; /* extern: активная таблица ремапа (CP1251/KOI8), 0 = без ремапа (CP866)*/ uint8_t g_scratch_phys; /* extern: EMM-страница под ремап-буфер строки для win_rest */ /* UTF-8 набор (utf_blk/utf_pages/utf_phys/utf_size/utf_avail, конвертер и его * состояние) — в mdview2_enc.c; разделяемые символы и API — в mdview2.h. * build_doc() ниже перенаправляет живой file_* на utf_phys и оркеструет * инкрементальную конвертацию (utf_conv_reset/utf_convert_more). */ /* Снимок исходного (8-битного) файлового буфера — набор DOC_8BIT читает его * как есть, набор DOC_UTF8 читает utf_phys. */ static uint8_t orig_file_blk; uint8_t orig_file_pages; /* extern: читает кодек (utf_alloc) */ uint32_t orig_file_size; /* extern: читает кодек (utf_convert_more) */ uint8_t orig_file_phys[MAX_PAGES]; /* extern: читает кодек (cv_read) */ /* ----------------- Два сосуществующих набора индекс/кэш ----------------- * 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]; /* built=0 для обоих: _DATA зануляется в crt0 gsinit * (с 2026-07-04), явный {0}-инициализатор больше * не нужен — он стоил ~260 байт _INITIALIZER */ static uint8_t g_active_doc = DOC_8BIT; /* какой набор сейчас в живых глобалах */ static uint8_t g_disp_doc = DOC_8BIT; /* какой набор показан пользователю */ static void doc_switch(uint8_t target); /* forward (нужна в progress_tick) */ /* Статус-бар, меню и спиннер — в mdview2_status.c. */ /* ================================================================== * Примитивы экрана, доступ к EMM/окну W3 (fb/win_rest), палитра * ================================================================== */ /* Печатает строку с указанным атрибутом, начиная с позиции (x,y), * и останавливается на конце строки или правой границе экрана. * `s` — литерал/статический буфер (CODE/DATA, окна 1/2), поэтому его * можно передать в BIOS LP_PRINT_LN напрямую, без промежуточной копии. */ void put_str_attr(uint8_t x, uint8_t y, const char *s, uint8_t attr) { uint8_t len = (x < SCREEN_W) ? (uint8_t)(SCREEN_W - x) : 0; len = min(len, strlen(s)); if (len == 0) return; bios_set_place(y, x); bios_writeattr_until(s, len, attr, 0); } /* Полностью очищает указанную строку экрана символами пробела с attr. */ void fill_row(uint8_t y, uint8_t attr) { bios_clearwin(y, 0, 1, SCREEN_W, attr); } /* Процент 0..100 = num*100/den на чистой 16-битной арифметике — чтобы НЕ * тянуть 32-битное деление __divulong (~760 Б) ради показа процента в статусе. * Оба вызова (MD/RAW) дают операнды, влезающие в 16 бит (≤18432 и ≤1024), * переполняет только *100 — поэтому масштабируем оба вниз, пока num*100 не * влезёт в uint16, затем считаем долю циклом-вычитанием (без div-хелпера). * Инвариант вызовов: num ≤ den; результат всё равно ограничен 100. */ uint8_t pct16(uint16_t num, uint16_t den) { if (den == 0) return 0; while (num > 655) { num >>= 1; den >>= 1; } /* num*100 ≤ 65500 < 65536 */ if (den == 0) return 100; /* страховка от num>den */ uint16_t n = (uint16_t)(num * 100u); uint8_t p = 0; while (n >= den && p < 100) { n = (uint16_t)(n - den); p++; } return p; } /* Отображает указанную EMM-страницу файла в окно W3. * Если эта страница уже активна, переключение не выполняется. */ // static inline void map_page(uint8_t page) // { // if (page != cur_page) { // /* file_phys[] позволяет не вызывать mem_get_page() на каждом свопе. */ // sprinter_page_w3(file_phys[page]); // cur_page = page; // } // } /* Читает один байт логического файла по смещению p. * При переходе между страницами автоматически переключает W3. * Проверка границ выполняется вызывающей стороной. * * Эквивалент C-кода (оставлен для справки): * uint8_t *pb = (uint8_t *)&p; * uint8_t page = (pb[1] >> 6) | (pb[2] << 2); * uint16_t off = ((pb[1] & 0x3F) << 8) | pb[0]; * map_page(page); // if (page!=cur_page){cur_page=page; OUT W3,file_phys[page];} * return FILE_BUF[off]; // *(0xC000 + off) * * Ручной asm — это горячая примитивная операция (вызывается сотни тысяч раз * при индексации 50КБ-файла); C-версия тратила вызов функции + 32-битную * арифметику на КАЖДЫЙ байт. Раскладка __sdcccall(1): аргумент uint32_t в * HLDE (E=pb[0], D=pb[1], L=pb[2], H=pb[3] — проверено sdcc -S), возврат * char в A. Координация маппинга с bank_read/bank_write держится на том, * что _io_page_w3 — это ПОРТ (__sfr 0xE2): здесь пишем его через OUT, * bank_read читает через IN — общий аппаратный регистр, без теневой RAM. * NB: __naked обязателен (внутри собственный ret); IX/IY не трогаем * (callee-saved), BC/DE/HL/AF — caller-saved, можно клобберить. */ char fb(uint32_t p) __naked { (void)p; /* p = HLDE; доступ только из asm */ __asm ; off -> BC = 0xC000 + ((pb[1]&0x3F)<<8 | pb[0]) ; D=pb[1], E=pb[0] ld a, d or a, #0xC0 ; (D|0xC0) == (D&0x3F)|0xC0 — старший байт окна W3 ld b, a ld c, e ; C = pb[0] ; page = (pb[1]>>6) | (pb[2]<<2) ; L=pb[2] ld a, l add a, a add a, a ; A = pb[2]<<2 ld e, a ; E свободен (pb[0] уже сохранён в C) ld a, d rlca rlca and a, #0x03 ; A = pb[1]>>6 or a, e ; A = page ld e, a ; E = page ; map_page(page) ld a, (_cur_page) cp e jr Z, fb_same ld a, e ld (_cur_page), a ld hl, #_file_phys ld d, #0x00 add hl, de ; hl = &file_phys[page] ld a, (hl) out (#0xE2), a ; OUT PORT_PAGE_W3 (== _io_page_w3 sfr) fb_same: ld a, (bc) ; A = *(0xC000 + off) ret __endasm; } /* ESTEX WINREST (5Ah, RST 0x10) — копирует готовый буфер (char,attr) * из смещения `off` ВНУТРИ EMM-страницы `page` прямо в текстовое окно * экрана, минуя любые BIOS print-вызовы. Формат буфера подтверждён * эмпирически в MAME (см. memory/sprinter_winrest_format.md и * tests/winrest/winrest.c): пары (char,attr) по ячейкам, по строкам, * шаг строки = width*2, без паддинга. row/col — левый верхний угол * вывода на экране, h/w — высота/ширина в символах. * * Регистровая раскладка SDCC __sdcccall(1) для (row,col,h,w,page,off) * проверена через `sdcc -S` (пробный non-naked враппер): A=row, L=col, * стек [SP+2]=h [SP+3]=w [SP+4]=page [SP+5..6]=off (little-endian). * * История: до 2026-06-23 `off` не было — IX всегда указывал на самое * начало страницы (#0xC000), это работало только пока в кэше было * РОВНО ОДНА запись на странице со смещением 0 (Фаза 1 self-test). * Когда Фаза 2 начала пакетировать много строк на одной странице с * РАЗНЫМИ off, всё стало читать с начала страницы (т.е. контент первой * записи) независимо от реальной строки — добавлен off, IX = 0xC000+off. */ void win_rest(uint8_t row, uint8_t col, uint8_t h, uint8_t w, uint8_t page, uint16_t off) __naked { (void)row; (void)col; (void)h; (void)w; (void)page; (void)off; __asm ld iy, #2 add iy, sp ld d, a ; D = row ld e, l ; E = col push ix ; сохранить IX вызывающей функции (callee-saved) ld l, 3 (iy) ; off (младший байт), SP+5 ld h, 4 (iy) ; off (старший байт), SP+6 ld bc, #0xC000 add hl, bc push hl pop ix ; IX = 0xC000 + off ld h, 0 (iy) ; H = height ld a, 1 (iy) ; width -> L ld l, a ld a, 2 (iy) ; page -> B ld b, a ld c, #0x5A ; ESTEX WINREST di rst #0x10 ei pop ix ; восстановить IX вызывающей функции pop hl ; адрес возврата inc sp inc sp inc sp inc sp inc sp ; снять h,w,page,off (1+1+1+2 байт) со стека jp (hl) __endasm; } /* Char-хелперы, idx_get/idx_put/seg_off, set_*_cur — в mdview2_index.c. */ /* Загружает 16 текстовых атрибутов в палитры ink/paper/blink. */ static void set_pallete(void) { uint8_t buff[64]; for(int i = 0; i < 16; i++) { for(int j = 0; j < 16; j++) { buff[j * 4] = md_pallete[i * 4]; buff[j * 4 + 1] = md_pallete[i * 4 + 1]; buff[j * 4 + 2] = md_pallete[i * 4 + 2]; buff[j * 4 + 3] = md_pallete[i * 4 + 3]; } text_pal_load(TEXT_PAL_INK, i * 16, 16, md_pallete); text_pal_load(TEXT_PAL_BLINK_INK, i * 16, 16, md_pallete); text_pal_load(TEXT_PAL_PAPER, i * 16, 16, buff); text_pal_load(TEXT_PAL_BLINK_PAPER, i * 16, 8, buff + 32); text_pal_load(TEXT_PAL_BLINK_PAPER, i * 16 + 8, 8, buff + 32); } } /* ================================================================== * Загрузка/выгрузка файла * ================================================================== */ /* Загружает исходные байты файла в EMM-страницы (без индекса/кэша — те * выделяются отдельно на КАЖДЫЙ набор через alloc_set_storage()). * Возврат: 0 — успех, отрицательное значение — код ошибки. */ static int load_file(const char *path) { int fd = open(path, O_RDONLY); if (fd < 0) return -1; long sz = lseek(fd, 0, SEEK_END); if (sz < 0) { close(fd); return -2; } /* Файл больше MAX_FILE (256 КБ) не отвергаем, а КЛАМПим: читаем первые * 256 КБ, индексатор допишет строку-предупреждение (TRUNC_FILE). */ g_file_clamped = 0; if ((uint32_t)sz > MAX_FILE) { sz = (long)MAX_FILE; g_file_clamped = 1; } file_size = (uint32_t)sz; lseek(fd, 0, SEEK_SET); /* Выделяем ровно столько EMM-страниц, сколько нужно под файл. */ file_pages = (uint8_t)((file_size + PAGE_SIZE - 1u) / PAGE_SIZE); if (file_pages == 0) file_pages = 1; file_blk = mem_alloc_pages(file_pages); if (file_blk == 0) { close(fd); return -3; } /* Читаем файл кусками по 16 КБ, последовательно маппя страницы в W3. * На последней итерации читаем остаток меньше PAGE_SIZE. */ uint32_t remaining = file_size; for (uint8_t page = 0; page < file_pages; page++) { spinner_tick(); uint8_t phys = mem_get_page(file_blk, page); file_phys[page] = phys; sprinter_page_w3(phys); cur_page = page; uint16_t chunk = (remaining > PAGE_SIZE) ? (uint16_t)PAGE_SIZE : (uint16_t)remaining; int n = read(fd, FILE_BUF, chunk); if (n != (int)chunk) { close(fd); return -4; } remaining -= chunk; } close(fd); return 0; } /* Выделяет в живые глобалы блок индекса + директорию рендер-кэша под текущий * набор. file_pages должно быть уже выставлено (размер набора). Сбрасывает * пул контента в пустое состояние (страницы доращиваются лениво при рендере). * Возврат: 0 — успех, -5/-6 — нехватка EMM. */ static int alloc_set_storage(void) { uint8_t want; want = (uint8_t)((file_pages >> 1) + 1u); if (want > MAX_INDEX_PAGES) want = MAX_INDEX_PAGES; index_blk = mem_alloc_pages(want); if (index_blk == 0) return -5; index_pages = want; for (uint8_t i = 0; i < index_pages; i++) index_phys[i] = mem_get_page(index_blk, i); max_lines = (uint16_t)index_pages * INDEX_RECS_PER_PAGE; want = (uint8_t)((file_pages >> 1) + 1u); if (want > MAX_CACHE_DIR_PAGES) want = MAX_CACHE_DIR_PAGES; cache_dir_blk = mem_alloc_pages(want); if (cache_dir_blk == 0) return -6; cache_dir_pages = want; 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; } static void doc_save_live(uint8_t i); /* forward */ /* Освобождает все EMM-блоки обоих наборов (индекс/директория/пул контента) * плюс два файловых буфера (исходный 8-битный и UTF-8). */ static void unload_file(void) { 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; } /* Парсер/индексатор (classify_line, scan_join_stream, inline_scan, index_lines, emph_to_attr) — в mdview2_index.c. */ /* Таблицы ремапа, set_encoding, кодировки и UTF-8 конвертер — в mdview2_enc.c. */ /* ================================================================== * Наборы документа (doc-slots): сохранение/свап * ================================================================== * Два сосуществующих набора (DOC_8BIT / DOC_UTF8) живут как копии всего * состояния (file/index/cache/позиция) в g_doc[]; «живые» глобалы — это * активный набор. 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; } /* Автоопределение/имя кодировки, ремап-вывод и UTF-8→CP866 конвертер — в mdview2_enc.c. */ /* Отрисовка области/статус-бара/меню и прокрутка — в модуле mdview2_md.c. */ /* ================================================================== * Прогрессивная загрузка и диспетчер клавиш (MD) * ================================================================== * index_lines() строит индекс кооперативно и периодически зовёт * progress_tick(): тот рисует первый готовый экран, крутит спиннер и * обрабатывает клавиши на лету (load_key). Диалог справки (F1) — в * mdview2_help.c, RAW-навигация — в mdview2_raw.c. */ /* Обработка клавиши во время прогрессивной загрузки: скролл строго в * пределах готовых строк (drawable_lines); End = последняя готовая страница; * F1 = справка (show_help сам блокирует и перерисовывает). */ static void load_key(uint16_t k) { uint8_t ascii = (uint8_t)(k & 0xFF); uint8_t scan = (uint8_t)((k >> 8) & 0x7F); uint16_t dr = drawable_lines(); uint16_t maxtop = (dr > VIEW_H) ? (uint16_t)(dr - VIEW_H) : 0; uint16_t nt = top_line; if (ascii == 0x1B || scan == KEY_F10) { g_abort = 1; return; } /* Esc/F10 → выход */ if (ascii) return; /* прочие ascii игнорируем */ switch (scan) { case KEY_UP: nt = (top_line >= 1) ? (uint16_t)(top_line - 1) : 0; break; case KEY_DOWN: nt = top_line + 1; break; case KEY_PGUP: nt = (top_line >= VIEW_H) ? (uint16_t)(top_line - VIEW_H) : 0; break; case KEY_PGDN: nt = top_line + VIEW_H; break; case KEY_HOME: top_line = 0; viewport_x = 0; draw_viewport_from_cache(); render_md_status_numbers(); return; case KEY_END: top_line = maxtop; viewport_x = 0; draw_viewport_from_cache(); render_md_status_numbers(); return; case KEY_LEFT: md_scroll_horizon(-(int8_t)HPAN_STEP); render_md_status_numbers(); return; case KEY_RIGHT: md_scroll_horizon(+(int8_t)HPAN_STEP); render_md_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; if (nt != top_line) { top_line = nt; draw_viewport_from_cache(); render_md_status_numbers(); } } /* Кооперативный «тик» прогрессивной загрузки — вызывается из index_lines(). */ void progress_tick(void) { #if WITH_RAW /* В RAW-режиме буфер документа готов сразу после конвертации (до индекса), * поэтому первый экран RAW можно показать немедленно — рисуем raw_draw, а * не MD-вид (иначе мелькал бы MD). Индекс при этом строится дальше (нужен * для MD-вида). */ if (g_view != VIEW_MD) { #if WITH_HEX if (g_view == VIEW_HEX) { /* HEX показывает оригинал — экран уже актуален */ spinner_tick(); return; } #endif if (!g_first_drawn) { raw_draw(); // render_raw_status(); g_first_drawn = 1; cur_page = 0xFF; /* raw_draw сбил W3-маппинг, на который опирается fb() */ } spinner_tick(); return; } #endif /* Прогрессивная отрисовка строящегося (всегда показываемого) набора. */ uint8_t touched = 0; if (!g_first_drawn && drawable_lines() >= VIEW_H) { draw_viewport_from_cache(); render_md_status_numbers(); g_first_drawn = 1; touched = 1; } if (n_lines >= (uint16_t)(g_last_prog + 10)) { g_last_prog = n_lines; spinner_tick(); if (g_first_drawn) render_md_status_numbers(); touched = 1; } if (g_first_drawn && kbhit()) { load_key(getkey()); touched = 1; } /* Отрисовка/WINREST/BIOS могли сбить маппинг W3, на который опирается fb() * при индексации → форсируем ре-маппинг страницы файла на следующем fb(). */ if (touched) cur_page = 0xFF; } /* ================================================================== * Точка входа * ================================================================== */ /* Строит набор `slot` кооперативно (index_step + progress_tick). * visible=1 — первичный (показываемый): прогрессивная отрисовка первого экрана * и скролл по готовым строкам. visible=0 — вторичный: индексация идёт в живых * глобалах, а progress_tick через свап показывает/листает уже готовый * g_disp_doc. Возврат: 0 — успех, <0 — нехватка EMM (-5/-6/-7). * * Если slot прямо сейчас строится ФОНОМ (g_bg_doc, см. bg_step ниже) — * не начинаем заново, а ДОКРУЧИВАЕМ: загружаем частично построенный набор * и продолжаем с сохранённой позиции индексатора (его межшаговое состояние * живёт в статиках модуля index и переживает свапы наборов). * * UTF-8 набор конвертируется ИНКРЕМЕНТАЛЬНО: выделяем страницы, переключаем * живой буфер на utf и запускаем конвертацию первого чанка; дальше index_step * докручивает её впереди позиции чтения (g_utf_building). Конвертер читает * оригинал из orig_file_phys[] через cv_read (не через fb, который теперь * указывает на utf). */ /* Настраивает живые глобалы под сборку набора 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()) return -7; /* выделяет страницы + сбрасывает конвертер */ utf_avail = 1; } file_blk = utf_blk; file_pages = utf_pages; memcpy(file_phys, utf_phys, MAX_PAGES); if (g_utf_building) { file_size = 0; cur_page = 0xFF; utf_convert_more(CONV_MARGIN); /* первый чанк — для первого экрана */ } else { file_size = utf_size; } } 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; 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; viewport_x = 0; g_active_doc = slot; 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; while (!index_step(255)) {} g_loading = 0; /* Зафиксировать конвертацию (на случай обрыва индекса по max_lines до конца * исходника) — иначе g_utf_building остался бы 1 и сломал бы следующую сборку. */ if (g_utf_building) { utf_size = file_size; g_utf_building = 0; } g_doc[slot].built = 1; doc_save_live(slot); 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) * сразу добавляет to/2^k в результат — сумма to/2^k по установленным * битам и есть off*to/from. Точность from/65536 (≤4 байта на максимальном * файле); ляжет на границу строки через line_at_off / raw_reanchor. */ static uint32_t map_off(uint32_t off, uint32_t from, uint32_t to) { if (from == to || from == 0) return off; uint32_t res = 0; for (uint8_t i = 0; i < 16; i++) { from >>= 1; to >>= 1; if (off >= from) { off -= from; res += to; } } return res; } /* Байт-позиция текущего вида (верх экрана) — единая «валюта» для переноса * позиции между видами/наборами. Валюта = offset АКТИВНОГО буфера; HEX живёт * в байтах оригинала, для UTF-набора конвертируем через map_off. */ static uint32_t view_pos(void) { #if WITH_HEX if (g_view == VIEW_HEX) return (g_active_doc == DOC_UTF8) ? map_off(hex_pos(), orig_file_size, file_size) : hex_pos(); #endif #if WITH_RAW if (g_view != VIEW_MD) return raw_pos(); #endif return seg_off(top_line); } /* Поставить текущий вид на байт-позицию off (активного буфера). */ static void view_reanchor(uint32_t off) { #if WITH_HEX if (g_view == VIEW_HEX) { hex_reanchor((g_active_doc == DOC_UTF8) ? map_off(off, file_size, orig_file_size) : off); return; } #endif #if WITH_RAW if (g_view != VIEW_MD) { raw_reanchor(off); return; } #endif top_line = line_at_off(off); } /* Переключает кодировку на `next` (вызывается из главного цикла по F8). * 8-бит↔8-бит внутри одного набора — мгновенный ремап; переход в другой набор * — мгновенный doc_switch, если он построен, иначе ЛЕНИВАЯ сборка по месту * (со спиннером), результат кэшируется. */ /* Меняет кодировку: тот же набор — ремап; другой готовый — doc_switch; * непостроенный — ЛЕНИВАЯ ТИХАЯ сборка (спиннер, без отрисовки документа). * НЕ перерисовывает документ/статус — это делает вызывающий в своём режиме * (MD: draw_viewport_from_cache; RAW: raw_draw), чтобы не мелькал чужой вид. */ 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) { /* набор готов — мгновенный свап */ /* Позицию переносим пропорцией байтов (буферы наборов разного * размера), а не из сохранённой в g_doc копии — иначе каждый набор * скачет по своей старой позиции. */ uint32_t old_off = view_pos(); uint32_t old_size = file_size; doc_switch(target); g_disp_doc = target; set_encoding(next); g_doc[target].enc = next; view_reanchor(map_off(old_off, old_size, file_size)); } else { /* строим набор лениво */ doc_save_live(g_active_doc); /* сохранить текущий (позиция и т.п.) */ #if WITH_RAW uint32_t raw_save = raw_pos(); if (VIEW_IS_RAW(g_view)) /* HEX показывает оригинал — его позиция инвариантна */ raw_reanchor(0); /* прогрессивный показ нового буфера — с начала */ g_ready = 0; render_menu(); /* идёт индексация — прячем F2 */ #endif spinner_show(1); int rc = build_doc(target, next, 1); /* visible: прогрессивный показ первого экрана * (progress_tick рисует по текущему виду — MD/RAW) */ spinner_show(0); #if WITH_RAW g_ready = 1; render_menu(); #endif if (rc < 0) { /* нет EMM — откат на текущий набор */ if (target == DOC_UTF8) g_utf_failed = 1; doc_load_live(g_active_doc); #if WITH_RAW if (VIEW_IS_RAW(g_view)) raw_reanchor(raw_save); /* буфер прежний — вернуть позицию RAW */ #endif return; } g_disp_doc = target; /* build_doc уже выставил active/enc */ } clamp_top(); /* привести top_line к новому набору (для MD) */ } /* ================================================================== * Вывод ошибки * ================================================================== */ static void on_exit(void) { unload_file(); pal_reset(PAL_CGA); } int main(int argc, char **argv) { const char *path; if (argc >= 2) { path = argv[1]; } else { path = "README.MD"; /* Дефолтный файл отсутствует — выводим подсказку без смены режима экрана. */ { int chk = open(path, O_RDONLY); if (chk < 0) { // settextmode(TEXT_MODE_80x32); clrscr_attr(ATTR_TEXT); cputs("MDView v0.3 -- Markdown Viewer for Sprinter\r\n" \ "(c) 2026 Petrov A.G.\r\n\r\n" \ "Usage: mdview2 \r\n" \ " Displays a Markdown document (CP866/CP1251/KOI8R/UTF8, up to 128 KB).\r\n"); exit(-1); } close(chk); } } atexit(on_exit); /* Копируем путь в static filename[] для отображения в статус-баре. */ { uint8_t i = 0; while (i < (uint8_t)(sizeof(filename) - 1) && path[i]) { filename[i] = path[i]; i++; } filename[i] = 0; } /* Сначала поднимаем UI, чтобы пользователь видел шапку/меню/спиннер * уже во время загрузки файла и построения индекса. */ settextmode(TEXT_MODE_80x32); clrscr_attr(ATTR_TEXT); set_pallete(); prerender_status(); render_menu(); render_full_status(); render_percent_progress(0); spinner_show(1); int rc = load_file(path); if (rc < 0) { spinner_show(0); clrscr_attr(ATTR_RESET); textattr(ATTR_RESET); cputs("MDView: cannot load file '"); cputs(path); cputs("'\n\rError: "); switch (rc) { case -1: cputs("open failed"); break; case -2: cputs("seek failed"); break; case -3: cputs("mem_alloc_pages failed"); break; case -4: cputs("short read"); break; case -5: cputs("index alloc failed"); break; case -6: cputs("render-cache dir alloc failed"); break; } cputs("\n\r\n\r"); exit(-1); } /* 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"); exit(-1); } if (g_abort) exit(0); /* F10/Esc во время загрузки — корректный выход */ if (n_lines == 0) { clrscr_attr(ATTR_RESET); textattr(ATTR_RESET); cputs("mdview: empty file"); exit(-1); } /* Первичный готов — гасим спиннер, включаем F8 (второй набор дособерётся * лениво по первому переключению) и рисуем финальный статус с ПОЛНЫМ * счётчиком строк. */ spinner_show(0); g_f8_enabled = 1; #if WITH_RAW g_ready = 1; /* документ построен — теперь доступен F2 (RAW) */ #endif render_menu(); clamp_top(); 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); if (ascii) { if (ascii == 0x1B) break; /* клавиша Esc */ continue; } if (scan == KEY_F10) { break; } if (scan == KEY_F1) { show_help(); continue; } /* show_help сам перерисует */ /* F2 — тумблер RAW ↔ MD (из HEX тоже уводит в MD). * Рабочий только после постройки документа. */ if (scan == KEY_F2) { if (!g_ready) continue; if (g_view == VIEW_MD) { g_view = g_raw_last; /* → RAW (последний под-режим) */ raw_seed_from(top_line); /* позиция из MD */ raw_screen_init(); status_encoding(); render_menu(); render_raw_status(); raw_draw(); } else { uint32_t off = view_pos(); /* позиция из RAW/HEX */ if (VIEW_IS_RAW(g_view)) g_raw_last = g_view; /* запомнить под-режим RAW */ g_view = VIEW_MD; top_line = line_at_off(off); clamp_top(); render_full_status(); render_menu(); draw_viewport_from_cache(); } continue; } #if WITH_HEX /* F4 — тумблер HEX ↔ прежний вид (MD/RAW). */ if (scan == KEY_F4) { if (!g_ready) continue; uint32_t off = view_pos(); /* позиция текущего вида */ if (g_view == VIEW_HEX) { /* HEX → прежний вид */ g_view = g_hex_last; view_reanchor(off); if (g_view == VIEW_MD) { clamp_top(); render_full_status(); render_menu(); draw_viewport_from_cache(); } else { raw_screen_init(); status_encoding(); render_menu(); render_raw_status(); raw_draw(); } } else { /* MD/RAW → HEX */ g_hex_last = g_view; g_view = VIEW_HEX; view_reanchor(off); /* active → байты оригинала */ hex_screen_init(); status_encoding(); render_menu(); render_hex_status(); hex_draw(); } continue; } #endif /* цикл CP866→CP1251→KOI8R→UTF8→CP866; набор дособерётся лениво */ if (scan == KEY_F8) { 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) exit(0); /* Esc/F10 во время ленивой досборки */ /* Перерисовка — по текущему виду (switch_encoding сам не рисует). */ #if WITH_HEX if (g_view == VIEW_HEX) { status_encoding(); render_hex_status(); hex_draw(); /* printable-колонка в новой кодировке */ continue; } #endif #if WITH_RAW if (g_view != VIEW_MD) { status_encoding(); render_raw_status(); raw_draw(); continue; } #endif render_full_status(); draw_viewport_from_cache(); continue; } switch(g_view) { case VIEW_MD: /* MD-режим: F1/F8/F10 здесь, навигация — в md_key() (peer к raw_key). */ md_key(scan); /* навигация MD (сама рисует+статус); незнакомые игнорит */ continue; #if WITH_HEX case VIEW_HEX: hex_key(scan); /* навигация HEX (сама рисует+процент) */ continue; #endif #if WITH_RAW case VIEW_RAW_WRAP: case VIEW_RAW_HSCROLL: /* RAW-режим: F3 Wrap/Unwrap, своя навигация; F8 — кодировка; F1/F10. */ /* Wrap ↔ Unwrap */ if (scan == KEY_F3) { g_view = (g_view == VIEW_RAW_WRAP) ? VIEW_RAW_HSCROLL : VIEW_RAW_WRAP; g_raw_last = g_view; raw_renorm(); raw_screen_init(); render_menu(); render_raw_status(); raw_draw(); continue; } raw_key(scan); continue; #endif /* WITH_RAW */ default: break; } } clrscr_attr(ATTR_TEXT); exit(0); return 0; }