mdview: speed up rendering via bios/text.h, fix wrap-boundary style bug

Replace per-character wrchar() loops in render_line/fill_row/help screens
with batched BIOS calls (bios_writeattr/bios_fillcharattr), and cache the
per-line index record (idx_get) instead of refetching it ~13 times per
rendered line — each refetch cost two W3 bank switches via bank_read().

Also fixes a pre-existing indexer bug: when word-wrap pushes a token that
starts with an emphasis/code marker (e.g. `_text_`) onto a new line, the
marker got re-scanned a second time during the wrap continuation, flipping
line_style back off and corrupting the rendered attribute of the next
word. Fixed by snapshotting line_style at the last seen space and rolling
back to that snapshot (both the saved continuation style and the live
scan state) when a wrap is taken.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-23 17:06:35 +03:00
parent c9d5ea35a4
commit 016fedd94c
4 changed files with 625 additions and 94 deletions
+252 -94
View File
@@ -21,6 +21,7 @@
#include <stdio.h>
#include <string.h>
#include <conio.h>
#include <bios/text.h>
#include <fcntl.h>
#include <unistd.h>
#include <sprinter.h>
@@ -43,7 +44,7 @@
#define PAGE_MASK ((uint16_t)(PAGE_SIZE - 1u))
#define MAX_PAGES 8 /* 8 страниц × 16 КБ = 128 КБ */
#define MAX_FILE ((uint32_t)MAX_PAGES * PAGE_SIZE) /* максимальный размер файла: 131072 байт */
#define MAX_LINES 2048
// #define MAX_LINES 2048
#define FILE_BUF ((char *)0xC000) /* окно W3, куда мапится текущая EMM-страница */
/* ---- Палитра атрибутов ------------------------------------------- */
@@ -172,21 +173,25 @@ static uint8_t spinner_active = 0;
* ================================================================== */
/* Печатает строку с указанным атрибутом, начиная с позиции (x,y),
* и останавливается на конце строки или правой границе экрана. */
* и останавливается на конце строки или правой границе экрана.
* `s` — литерал/статический буфер (CODE/DATA, окна 1/2), поэтому его
* можно передать в BIOS LP_PRINT_LN напрямую, без промежуточной копии. */
static void put_str_attr(uint8_t x, uint8_t y, const char *s, uint8_t attr)
{
while (*s && x < SCREEN_W) {
wrchar(x++, y, *s++, attr);
}
uint8_t maxlen = (x < SCREEN_W) ? (uint8_t)(SCREEN_W - x) : 0;
uint8_t len = (uint8_t)strlen(s);
if (len > maxlen) len = maxlen;
if (len == 0) return;
bios_set_place(y, x);
bios_writeattr(s, len, attr);
}
/* Полностью очищает указанную строку экрана символами пробела с attr. */
static void fill_row(uint8_t y, uint8_t attr)
{
for (uint8_t c = 0; c < SCREEN_W; c++) {
wrchar(c, y, ' ', attr);
}
bios_set_place(y, 0);
bios_fillcharattr(' ', attr, SCREEN_W);
}
/* Продвигает спиннер на один кадр.
@@ -695,6 +700,7 @@ static uint8_t inline_scan(uint32_t q, uint32_t q_end, uint8_t col,
uint8_t ckind, uint8_t line_style)
{
uint32_t last_space = 0xFFFFFFFFu;
uint8_t last_space_style = line_style; /* снимок line_style на момент last_space */
char prev_ch = ' ';
uint8_t seg_col = col;
@@ -755,16 +761,24 @@ static uint8_t inline_scan(uint32_t q, uint32_t q_end, uint8_t col,
}
if (ch == ' ') {
last_space = q + 1;
last_space_style = line_style;
seg_col++; q++; prev_ch = ' ';
} else {
seg_col++; q++; prev_ch = ch;
}
if (seg_col >= SCREEN_W) {
uint32_t wrap_at = (last_space != 0xFFFFFFFFu && last_space > cur_seg_off)
? last_space : q;
uint8_t use_ls = (uint8_t)(last_space != 0xFFFFFFFFu && last_space > cur_seg_off);
uint32_t wrap_at = use_ls ? last_space : q;
if (wrap_at >= q_end || wrap_at == cur_seg_off) break;
emit_seg(wrap_at, line_style, ckind, 1);
uint8_t wrap_style = use_ls ? last_space_style : line_style;
emit_seg(wrap_at, wrap_style, ckind, 1);
seg_col = col; last_space = 0xFFFFFFFFu; prev_ch = ' '; q = wrap_at;
/* Откатываем line_style к снимку на момент wrap_at: диапазон
* [wrap_at, q_old) будет пересканирован заново, и переключающие
* символы (обратная кавычка, звёздочка, подчёркивание, тильда)
* внутри него иначе применились бы дважды — именно так возникал
* баг: code-маркер на границе переноса ломает цвет следующего слова. */
line_style = wrap_style;
}
}
return line_style;
@@ -783,6 +797,7 @@ static uint32_t scan_join_stream(uint32_t q, uint32_t para_start, uint8_t col,
uint8_t ckind, uint8_t *io_line_style, uint8_t mode)
{
uint32_t last_space = 0xFFFFFFFFu;
uint8_t last_space_style = *io_line_style; /* снимок line_style на момент last_space */
char prev_ch = ' ';
uint8_t seg_col = col;
uint8_t line_style = *io_line_style;
@@ -968,9 +983,11 @@ static uint32_t scan_join_stream(uint32_t q, uint32_t para_start, uint8_t col,
if (ch == ' ') {
if (soft_break) {
last_space = q;
last_space_style = line_style;
seg_col++;
} else {
last_space = q + 1;
last_space_style = line_style;
seg_col++;
q++;
}
@@ -981,14 +998,22 @@ static uint32_t scan_join_stream(uint32_t q, uint32_t para_start, uint8_t col,
prev_ch = ch;
}
if (seg_col >= SCREEN_W) {
uint32_t wrap_at = (last_space != 0xFFFFFFFFu && last_space > cur_seg_off)
? last_space : q;
uint8_t use_ls = (uint8_t)(last_space != 0xFFFFFFFFu && last_space > cur_seg_off);
uint32_t wrap_at = use_ls ? last_space : q;
if (wrap_at >= file_size || wrap_at == cur_seg_off) break;
emit_seg(wrap_at, line_style, ckind, 1);
uint8_t wrap_style = use_ls ? last_space_style : line_style;
emit_seg(wrap_at, wrap_style, ckind, 1);
seg_col = col;
last_space = 0xFFFFFFFFu;
prev_ch = ' ';
q = wrap_at;
/* Откатываем line_style к снимку на момент wrap_at: диапазон
* [wrap_at, q_old) будет пересканирован заново, и переключающие
* символы (обратная кавычка, звёздочка, подчёркивание, тильда)
* внутри него иначе применились бы дважды — именно так возникал
* баг: code-маркер на границе переноса ломает цвет следующего слова (mdview.c, отчёт пользователя
* 2026-06-23). */
line_style = wrap_style;
}
}
@@ -1238,26 +1263,69 @@ static uint8_t handle_inline_marker(char ch, uint32_t *io_p, uint32_t seg_end,
return 0;
}
/* Сбрасывает накопленный "прогон" из *col символов одного атрибута
* одним bios_writeattr() вместо посимвольных wrchar(). *need_place
* лениво ставит BIOS-курсор перед первым выводом в текущей строке —
* далее place продвигается сам (verified), повторный bios_set_place
* не нужен пока *col растёт монотонно. */
static void flush_run(uint8_t row, uint8_t *col, const char *buf,
uint8_t len, uint8_t attr, uint8_t *need_place)
{
if (len == 0) return;
/* *col уже указывает на конец прогона (вызывающий цикл инкрементит
* col на каждый символ ещё во время накопления в буфер — это нужно
* для верных проверок col < SCREEN_W до сброса). Поэтому начало
* прогона на экране = *col - len, а не *col; и *col второй раз
* наращивать НЕ нужно — он уже корректен. */
if (*need_place) {
bios_set_place(row, (uint8_t)(*col - len));
*need_place = 0;
}
bios_writeattr(buf, len, attr);
}
static void render_line(uint16_t line_idx, uint8_t row)
{
/* Один idx_get() на line_idx вместо ~13 отдельных is_blank/is_cont/
* is_nowrap/is_code_body/seg_off/get_init_style* вызовов. Каждый из
* них шёл через seg_flags()/seg_off() -> idx_get() -> bank_read(),
* а bank_read() ДВАЖДЫ переключает банк окна 3 (на страницу индекса
* и обратно) плюс memcpy — это оказалось главным узким местом
* рендера (≈700+ переключений банка на кадр), а не сам вывод на
* экран. Все нужные поля (off/flags/style) лежат в одной 8-байтовой
* записи idx_rec_t — читаем её один раз и используем дальше. */
uint8_t have_rec = (line_idx < n_lines);
idx_rec_t rec;
if (have_rec) idx_get(line_idx, &rec);
uint8_t col = 0;
uint8_t base_attr = ATTR_TEXT;
uint8_t cur_attr = is_code_body(line_idx) ? ATTR_TEXT_CODE : get_init_style(line_idx);
uint8_t emph = is_code_body(line_idx) ? EM_CODE : get_init_style_raw(line_idx);
uint8_t is_code = (uint8_t)(have_rec && (rec.flags & IF_CODE));
uint8_t cur_attr = is_code ? ATTR_TEXT_CODE
: styles_map[(have_rec ? rec.style : INIT_STYLE_PLAIN) & 7u];
uint8_t emph = is_code ? EM_CODE
: (uint8_t)((have_rec ? rec.style : INIT_STYLE_PLAIN) & 7u);
uint8_t parse_inline = 1; /* 1: включён парсер выделений, 0: выводим код как есть */
if (is_blank(line_idx)) {
while (col < SCREEN_W) wrchar(col++, row, ' ', ATTR_TEXT);
if (have_rec && (rec.flags & IF_BLANK)) {
bios_set_place(row, col);
bios_fillcharattr(' ', ATTR_TEXT, (uint8_t)(SCREEN_W - col));
return;
}
if (line_idx < n_lines) {
uint32_t p = seg_off(line_idx);
if (have_rec) {
uint32_t p = rec.off;
uint32_t content_off = p;
uint8_t cont = is_cont(line_idx);
uint8_t cont = (uint8_t)((rec.flags & IF_CONT) != 0);
/* Правая граница сегмента: начало следующего сегмента или конец файла. */
uint32_t seg_end = seg_off((uint16_t)(line_idx + 1));
uint32_t seg_end;
if ((uint16_t)(line_idx + 1) < n_lines) {
idx_rec_t rec_next;
idx_get((uint16_t)(line_idx + 1), &rec_next);
seg_end = rec_next.off;
} else {
seg_end = file_size;
}
if (seg_end > file_size) seg_end = file_size;
uint8_t kind;
uint8_t quote_stream = 0; /* 1: текущий сегмент относится к потоку цитаты */
@@ -1265,11 +1333,16 @@ static void render_line(uint16_t line_idx, uint8_t row)
/* Для сегмента-продолжения повторяем префикс/отступ
* от первого не-CONT сегмента логической строки. */
if (cont) {
uint16_t first = line_idx;
uint16_t first = line_idx;
idx_rec_t rec_first = rec; /* rec.flags здесь уже содержит IF_CONT */
/* Для любого сегмента-продолжения откатываемся к первому
* НЕ-cont сегменту этой логической строки. */
while (first > 0 && is_cont(first)) first--;
uint32_t p_start = seg_off(first);
* НЕ-cont сегменту этой логической строки — один idx_get()
* на шаг вместо двух (is_cont + повторный seg_off). */
while (first > 0 && (rec_first.flags & IF_CONT)) {
first--;
idx_get(first, &rec_first);
}
uint32_t p_start = rec_first.off;
uint32_t content_off = p_start;
uint8_t first_kind = classify_line(p_start, &content_off);
uint8_t indent = marker_visible_col(first_kind, p_start, content_off);
@@ -1278,33 +1351,41 @@ static void render_line(uint16_t line_idx, uint8_t row)
* чтобы далее на newline корректно пропускать сырой `>`. */
quote_stream = 1;
uint8_t q = 0;
while (q + 2 < content_off && col < SCREEN_W && fb(p_start + q) == ' ') {
wrchar(col++, row, ' ', ATTR_TEXT);
q++;
while (q + 2 < content_off && q < SCREEN_W && fb(p_start + q) == ' ') q++;
if (q > (uint8_t)(SCREEN_W - col)) q = (uint8_t)(SCREEN_W - col);
if (q > 0) {
bios_set_place(row, col);
bios_fillcharattr(' ', ATTR_TEXT, q);
col = (uint8_t)(col + q);
}
if (col < SCREEN_W) wrchar(col++, row, 0xB3, ATTR_QUOTE_MARKER);
if (col < SCREEN_W) wrchar(col++, row, ' ', ATTR_TEXT);
} else if (first_kind == LK_ULIST || first_kind == LK_OLIST) {
uint8_t q = 0;
while (q < indent && col < SCREEN_W) {
wrchar(col++, row, ' ', ATTR_TEXT);
q++;
uint8_t q = indent;
if (q > (uint8_t)(SCREEN_W - col)) q = (uint8_t)(SCREEN_W - col);
if (q > 0) {
bios_set_place(row, col);
bios_fillcharattr(' ', ATTR_TEXT, q);
col = (uint8_t)(col + q);
}
}
/* Дальше рендерим контент именно с текущего сегмента-продолжения. */
p = seg_off(line_idx);
content_off = p;
/* Для сегмента-продолжения парсер работает как обычно. */
/* p/content_off уже равны rec.off — внутри cont-блока они не
* менялись (там свои локальные тени с тем же именем),
* пересчитывать через seg_off() заново не нужно. */
}
/* Кодовый блок:
* - строка-разделитель показывается как пустая;
* - тело рисуется «как есть» с ATTR_TEXT_CODE без inline-парсинга. */
if (is_fence_delim(line_idx)) {
while (col < SCREEN_W) wrchar(col++, row, ' ', ATTR_TEXT);
* - тело рисуется «как есть» с ATTR_TEXT_CODE без inline-парсинга.
* is_fence_delim(line_idx) разворачено на месте: cont и p уже
* известны, повторный idx_get()/is_cont() не нужен. */
if (!cont && p + 2 < file_size &&
fb(p) == '`' && fb(p + 1) == '`' && fb(p + 2) == '`') {
bios_set_place(row, col);
bios_fillcharattr(' ', ATTR_TEXT, (uint8_t)(SCREEN_W - col));
return;
}
if (is_code_body(line_idx)) {
if (is_code) {
base_attr = ATTR_TEXT_CODE;
cur_attr = ATTR_TEXT_CODE;
parse_inline = 0;
@@ -1321,7 +1402,8 @@ static void render_line(uint16_t line_idx, uint8_t row)
}
if (kind == LK_HR) {
while (col < SCREEN_W) wrchar(col++, row, 0xC4, ATTR_HR);
bios_set_place(row, col);
bios_fillcharattr(0xC4, ATTR_HR, (uint8_t)(SCREEN_W - col));
return;
}
if (kind >= LK_H1 && kind <= LK_H4) {
@@ -1336,10 +1418,15 @@ static void render_line(uint16_t line_idx, uint8_t row)
} else if (kind == LK_ULIST) {
/* Базовая поддержка вложенности: ведущие пробелы сохраняем,
* затем рисуем маркер списка и пробел перед содержимым. */
uint32_t q = p;
while (q + 2 < content_off && col < SCREEN_W && fb(q) == ' ') {
wrchar(col++, row, ' ', ATTR_TEXT);
q++;
uint32_t q = p;
uint32_t qmax = p + SCREEN_W;
while (q + 2 < content_off && q < qmax && fb(q) == ' ') q++;
uint8_t nsp = (uint8_t)(q - p);
if (nsp > (uint8_t)(SCREEN_W - col)) nsp = (uint8_t)(SCREEN_W - col);
if (nsp > 0) {
bios_set_place(row, col);
bios_fillcharattr(' ', ATTR_TEXT, nsp);
col = (uint8_t)(col + nsp);
}
if (col < SCREEN_W) wrchar(col++, row, 0x07, ATTR_LIST_MARKER); /* символ маркера списка */
if (col < SCREEN_W) wrchar(col++, row, ' ', ATTR_TEXT);
@@ -1347,24 +1434,54 @@ static void render_line(uint16_t line_idx, uint8_t row)
} else if (kind == LK_OLIST) {
/* Отступ -> пробелы; цифры и '.' / ')' рисуем как маркер;
* завершающий пробел — обычным атрибутом текста. */
uint32_t q = p;
while (q < content_off && col < SCREEN_W && fb(q) == ' ') {
wrchar(col++, row, ' ', ATTR_TEXT);
q++;
uint32_t q = p;
uint32_t qmax = p + SCREEN_W;
while (q < content_off && q < qmax && fb(q) == ' ') q++;
uint8_t nsp = (uint8_t)(q - p);
if (nsp > (uint8_t)(SCREEN_W - col)) nsp = (uint8_t)(SCREEN_W - col);
if (nsp > 0) {
bios_set_place(row, col);
bios_fillcharattr(' ', ATTR_TEXT, nsp);
col = (uint8_t)(col + nsp);
}
while (q < content_off - 1 && col < SCREEN_W) {
wrchar(col++, row, fb(q), ATTR_LIST_MARKER);
q++;
/* Маркер (цифры/'.'/')') читается из fb() — это окно 3,
* BIOS принимает указатели только из #4000..#BFFF, поэтому
* сначала копируем во временный буфер на стеке (окно 2),
* чанками по 8 байт (маркер почти всегда короче, но длинный
* корректно не обрезается, просто шлётся несколькими
* bios_writeattr вместо одного). */
{
char mkbuf[8];
uint8_t first_chunk = 1;
while (q < content_off - 1 && col < SCREEN_W) {
uint8_t n = 0;
while (q < content_off - 1 &&
(uint8_t)(col + n) < SCREEN_W &&
n < sizeof(mkbuf)) {
mkbuf[n] = fb(q);
q++;
n++;
}
if (n == 0) break;
if (first_chunk) { bios_set_place(row, col); first_chunk = 0; }
bios_writeattr(mkbuf, n, ATTR_LIST_MARKER);
col = (uint8_t)(col + n);
}
}
if (col < SCREEN_W) wrchar(col++, row, ' ', ATTR_TEXT);
p = content_off;
} else if (kind == LK_QUOTE) {
/* Отступ -> пробелы; затем quote-префикс '│' (0xB3),
* пробел и далее содержимое. */
uint32_t q = p;
while (q + 1 < content_off && col < SCREEN_W && fb(q) == ' ') {
wrchar(col++, row, ' ', ATTR_TEXT);
q++;
uint32_t q = p;
uint32_t qmax = p + SCREEN_W;
while (q + 1 < content_off && q < qmax && fb(q) == ' ') q++;
uint8_t nsp = (uint8_t)(q - p);
if (nsp > (uint8_t)(SCREEN_W - col)) nsp = (uint8_t)(SCREEN_W - col);
if (nsp > 0) {
bios_set_place(row, col);
bios_fillcharattr(' ', ATTR_TEXT, nsp);
col = (uint8_t)(col + nsp);
}
if (col < SCREEN_W) wrchar(col++, row, 0xB3, ATTR_QUOTE_MARKER);
if (col < SCREEN_W) wrchar(col++, row, ' ', ATTR_TEXT);
@@ -1372,11 +1489,25 @@ static void render_line(uint16_t line_idx, uint8_t row)
}
/* Эффективный горизонтальный сдвиг: применяется только к nowrap-строкам. */
uint8_t effective_vx = is_nowrap(line_idx) ? viewport_x : 0;
uint8_t effective_vx = (rec.flags & IF_NOWRAP) ? viewport_x : 0;
/* cc — видимая колонка внутри области контента
* (после фиксированного префикса строки). */
uint8_t cc = 0;
char prev_ch = ' '; /* используется в проверке границ emphasis */
/* Буферизация "ровного" прогона одного атрибута: символ и атрибут
* внутри этого while переменные (emphasis/tab/soft-wrap), поэтому
* весь цикл одним BIOS-вызовом не свернуть — но между сменами
* атрибута почти всегда идёт протяжённый одноцветный текст, и
* именно его стоит копить в runbuf и сбрасывать одним
* bios_writeattr() вместо посимвольных wrchar().
* runbuf вмещает целую строку экрана — переполнение невозможно,
* т.к. весь цикл и так ограничен col < SCREEN_W. */
char runbuf[SCREEN_W];
uint8_t run_len = 0;
uint8_t run_attr = cur_attr;
uint8_t need_place = 1;
while (p < seg_end && col < SCREEN_W) {
uint8_t soft_join = 0; /* 1: вставлен виртуальный пробел мягкой склейки, p уже сдвинут */
char ch = fb(p);
@@ -1438,16 +1569,23 @@ static void render_line(uint16_t line_idx, uint8_t row)
char lit_ch = 0;
uint8_t mk = handle_inline_marker(ch, &p, seg_end, prev_ch, &emph, &lit_ch);
if (mk == 1) {
/* Атрибут меняется -> сбросить накопленный прогон
* со СТАРЫМ атрибутом, прежде чем переключиться. */
flush_run(row, &col, runbuf, run_len, run_attr, &need_place);
run_len = 0;
cur_attr = emph_to_attr(emph, base_attr);
run_attr = cur_attr;
continue;
}
if (mk == 2) {
if (cc >= effective_vx && col < SCREEN_W) {
wrchar(col++, row, lit_ch, cur_attr);
if (cc >= effective_vx && col < SCREEN_W && run_len < sizeof(runbuf)) {
runbuf[run_len++] = lit_ch;
col++;
}
cc++;
if (cc >= effective_vx && col < SCREEN_W) {
wrchar(col++, row, lit_ch, cur_attr);
if (cc >= effective_vx && col < SCREEN_W && run_len < sizeof(runbuf)) {
runbuf[run_len++] = lit_ch;
col++;
}
cc++;
prev_ch = lit_ch;
@@ -1457,28 +1595,44 @@ static void render_line(uint16_t line_idx, uint8_t row)
if (!soft_join) p++;
if (ch == '\t') {
/* Таб расширяется в координатах контента (cc). */
uint8_t tgt = (uint8_t)((cc & (uint8_t)~(TAB_STOP - 1)) + TAB_STOP);
while (cc < tgt && col < SCREEN_W) {
if (cc >= effective_vx) wrchar(col++, row, ' ', cur_attr);
cc++;
/* Таб расширяется в координатах контента (cc).
* Сначала сбросить накопленный текстовый прогон — таб
* выводится отдельным bios_fillcharattr(), не через runbuf. */
flush_run(row, &col, runbuf, run_len, run_attr, &need_place);
run_len = 0;
uint8_t cc_start = cc;
uint8_t tgt = (uint8_t)((cc & (uint8_t)~(TAB_STOP - 1)) + TAB_STOP);
uint8_t n_total = (uint8_t)(tgt - cc_start);
uint8_t skip = (effective_vx > cc_start) ? (uint8_t)(effective_vx - cc_start) : 0;
if (skip > n_total) skip = n_total;
uint8_t want = (uint8_t)(n_total - skip);
if (want > (uint8_t)(SCREEN_W - col)) want = (uint8_t)(SCREEN_W - col);
if (want > 0) {
bios_set_place(row, col);
bios_fillcharattr(' ', cur_attr, want);
col = (uint8_t)(col + want);
need_place = 0;
}
cc = (uint8_t)(cc_start + skip + want);
prev_ch = ' ';
} else {
if (cc >= effective_vx && col < SCREEN_W) {
wrchar(col++, row, ch, cur_attr);
if (cc >= effective_vx && col < SCREEN_W && run_len < sizeof(runbuf)) {
runbuf[run_len++] = ch;
col++;
}
cc++;
prev_ch = ch;
}
}
flush_run(row, &col, runbuf, run_len, run_attr, &need_place);
/* Индикатор обрезки справа для nowrap-строки:
* если после текущей позиции есть ещё видимый контент,
* поверх последней колонки рисуем '>'. */
if (is_nowrap(line_idx) && col >= SCREEN_W) {
if ((rec.flags & IF_NOWRAP) && col >= SCREEN_W) {
uint32_t pp = p;
uint32_t left_bound = seg_off(line_idx);
uint32_t left_bound = rec.off;
while (pp < seg_end) {
char c = fb(pp);
if (c == '\n' || c == '\r') break;
@@ -1499,7 +1653,7 @@ static void render_line(uint16_t line_idx, uint8_t row)
}
/* Индикатор скрытого контента слева при горизонтальном сдвиге. */
if (is_nowrap(line_idx) && viewport_x > 0) {
if ((rec.flags & IF_NOWRAP) && viewport_x > 0) {
wrchar(0, row, '<', ATTR_TRUNC);
}
}
@@ -1507,7 +1661,10 @@ static void render_line(uint16_t line_idx, uint8_t row)
* от предыдущего кадра рендера. */
{
uint8_t pad_attr = (parse_inline ? ATTR_TEXT : ATTR_TEXT_CODE);
while (col < SCREEN_W) wrchar(col++, row, ' ', pad_attr);
if (col < SCREEN_W) {
bios_set_place(row, col);
bios_fillcharattr(' ', pad_attr, (uint8_t)(SCREEN_W - col));
}
}
}
@@ -1539,7 +1696,8 @@ static void render_status_numbers(void)
uint8_t pct = calc_pct();
textattr(ATTR_BAR);
for (uint8_t c = 45; c < SCREEN_W; c++) wrchar(c, 0, ' ', ATTR_BAR);
bios_set_place(0, 45);
bios_fillcharattr(' ', ATTR_BAR, (uint8_t)(SCREEN_W - 45));
wrchar(45, 0, 0xB3, ATTR_BAR); /* вертикальный разделитель */
gotoxy(47, 0);
@@ -1684,16 +1842,16 @@ static void die(const char *msg)
* Строка s в кодировке CP866; остаток до края дополняется пробелами. */
static void help_line(uint8_t r, const char *s, uint8_t attr)
{
uint8_t x = HELP_X + 1u;
uint8_t y = HELP_Y + 1u + r;
uint8_t i = 0;
while (s[i] && i < HELP_W - 2u) {
wrchar((uint8_t)(x + i), y, s[i], attr);
i++;
}
while (i < HELP_W - 2u) {
wrchar((uint8_t)(x + i), y, ' ', ATTR_HELP_BG);
i++;
uint8_t x = HELP_X + 1u;
uint8_t y = HELP_Y + 1u + r;
uint8_t len = (uint8_t)strlen(s);
if (len > HELP_W - 2u) len = HELP_W - 2u;
bios_set_place(y, x);
if (len > 0) bios_writeattr(s, len, attr);
/* place уже продвинут bios_writeattr на len колонок (verified). */
if (len < HELP_W - 2u) {
bios_fillcharattr(' ', ATTR_HELP_BG, (uint8_t)(HELP_W - 2u - len));
}
}
@@ -1704,14 +1862,14 @@ static void show_help(void)
uint8_t tlen = 8u;
uint8_t lft = (uint8_t)((HELP_W - 2u - tlen) / 2u); /* = 27 */
uint8_t rgt = (uint8_t)(HELP_W - 2u - tlen - lft);
uint8_t cx, i;
/* Верхняя граница рамки с заголовком по центру */
/* Верхняя граница рамки с заголовком по центру.
* place продвигается каждым BIOS-вызовом (verified), поэтому
* достаточно одной установки места на всю строку рамки. */
wrchar(HELP_X, HELP_Y, 0xDA, ATTR_HELP_BDR); /* ┌ */
cx = HELP_X + 1u;
for (i = 0; i < lft; i++, cx++) wrchar(cx, HELP_Y, 0xC4, ATTR_HELP_BDR);
for (i = 0; i < tlen; i++, cx++) wrchar(cx, HELP_Y, title[i], ATTR_HELP_TIT);
for (i = 0; i < rgt; i++, cx++) wrchar(cx, HELP_Y, 0xC4, ATTR_HELP_BDR);
bios_set_place(HELP_Y, (uint8_t)(HELP_X + 1u));
if (lft > 0) bios_fillcharattr(0xC4, ATTR_HELP_BDR, lft);
if (tlen > 0) bios_writeattr(title, tlen, ATTR_HELP_TIT);
if (rgt > 0) bios_fillcharattr(0xC4, ATTR_HELP_BDR, rgt);
wrchar(HELP_X + HELP_W - 1u, HELP_Y, 0xBF, ATTR_HELP_BDR); /* ┐ */
/* Боковые границы (левый и правый │ для каждой строки тела) */
@@ -1722,8 +1880,8 @@ static void show_help(void)
/* Нижняя граница рамки */
wrchar(HELP_X, HELP_Y + HELP_H - 1u, 0xC0, ATTR_HELP_BDR); /* └ */
for (i = 1u; i < HELP_W - 1u; i++)
wrchar(HELP_X + i, HELP_Y + HELP_H - 1u, 0xC4, ATTR_HELP_BDR);
bios_set_place((uint8_t)(HELP_Y + HELP_H - 1u), (uint8_t)(HELP_X + 1u));
bios_fillcharattr(0xC4, ATTR_HELP_BDR, (uint8_t)(HELP_W - 2u));
wrchar(HELP_X + HELP_W - 1u, HELP_Y + HELP_H - 1u, 0xD9, ATTR_HELP_BDR); /* ┘ */
/* Содержимое (20 внутренних строк) */
@@ -1793,7 +1951,7 @@ int main(int argc, char **argv)
/* Сначала поднимаем UI, чтобы пользователь видел шапку/меню/спиннер
* уже во время загрузки файла и построения индекса. */
set_videotextmode(TEXT_MODE_80x32);
settextmode(TEXT_MODE_80x32);
clrscr_attr(ATTR_TEXT);
set_pallete();
render_menu();