ffd179e064
Раунд 1 (−1411): --max-allocs 100000 в Makefile (−848 кода) + снятие
всех нулевых инициализаторов file-scope переменных (_INITIALIZER
583→24; _DATA теперь зануляется в crt0).
Раунд 2 (−1188, индексатор 11019→9839): дедупликации в mdview2_index.c:
- classify_line: копия HR-проверки → вызов is_hr_raw;
- next_line() поверх row_end() вместо 9 копий «домотать до \n»;
- inline_marker: emph_to_attr(ls, base) вычисляется один раз (at);
- set_{nowrap,blank,code,hscroll}_cur → set_cur_flags(mask): строка
code-блока делает один idx_put вместо трёх.
Проверено в MAME (README/UTF8TEST: маркеры, списки, цитаты, таблицы,
code-блоки, F8).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1090 lines
53 KiB
C
1090 lines
53 KiB
C
/*
|
||
* 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 <stdint.h>
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
#include <string.h>
|
||
#include <conio.h>
|
||
#include <bios/text.h>
|
||
#include <fcntl.h>
|
||
#include <unistd.h>
|
||
#include <sprinter.h>
|
||
#include <sprinter_mem.h>
|
||
#include <sprinter_exit.h>
|
||
#include <palette.h>
|
||
#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) */
|
||
#endif
|
||
uint8_t g_loading; /* extern: 1 во время index_lines() */
|
||
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_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 (!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_lines + progress_tick).
|
||
* visible=1 — первичный (показываемый): прогрессивная отрисовка первого экрана
|
||
* и скролл по готовым строкам. visible=0 — вторичный: index_lines строит его в
|
||
* живых глобалах, а progress_tick через свап показывает/листает уже готовый
|
||
* g_disp_doc. Возврат: 0 — успех, <0 — нехватка EMM (-5/-6/-7).
|
||
*
|
||
* UTF-8 набор конвертируется ИНКРЕМЕНТАЛЬНО: выделяем страницы, переключаем
|
||
* живой буфер на utf и запускаем конвертацию первого чанка; дальше index_lines
|
||
* докручивает её впереди позиции чтения (g_utf_building). Конвертер читает
|
||
* оригинал из orig_file_phys[] через cv_read (не через fb, который теперь
|
||
* указывает на utf). */
|
||
static int build_doc(uint8_t slot, uint8_t enc, uint8_t visible)
|
||
{
|
||
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;
|
||
|
||
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;
|
||
|
||
/* Зафиксировать конвертацию (на случай обрыва индекса по 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;
|
||
}
|
||
|
||
/* Переключает кодировку на `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) { /* набор готов — мгновенный свап */
|
||
doc_switch(target);
|
||
g_disp_doc = target;
|
||
set_encoding(next);
|
||
g_doc[target].enc = next;
|
||
} else { /* строим набор лениво */
|
||
doc_save_live(g_active_doc); /* сохранить текущий (позиция и т.п.) */
|
||
#if WITH_RAW
|
||
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);
|
||
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 <file.md>\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();
|
||
|
||
for (;;) {
|
||
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 (рабочий только после постройки документа). */
|
||
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 {
|
||
g_raw_last = g_view; /* запомнить под-режим RAW */
|
||
g_view = VIEW_MD;
|
||
clamp_top();
|
||
render_full_status(); render_menu();
|
||
draw_viewport_from_cache();
|
||
}
|
||
continue;
|
||
}
|
||
/* цикл 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 во время ленивой досборки */
|
||
render_full_status(); /* перерисовка — в MD-режиме */
|
||
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_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;
|
||
}
|