Files
Sprinter-SDCC/examples/mdview/mdview.c
T
snark13 0ad0559fc8 mdview: faster file-byte access (fb byte-decode + page cache)
Decode page/offset from the 32-bit offset's bytes to avoid SDCC z80
32-bit shift/mask helpers on the hot path; map_page() uses a cached
file_phys[] table to skip mem_get_page() on every W3 swap.
Behaviour-preserving.

Co-Authored-By: Oz <oz-agent@warp.dev>
2026-06-07 01:00:49 +03:00

1453 lines
52 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* mdview — markdown text viewer for Sprinter.
*
* Layout (80x32):
* row 0 — status bar (filename / line range / total / %)
* rows 1..30 — viewport (30 lines of document text)
* row 31 — menu bar (F1 Help / F2 Wrap / F10 Exit)
*
* Navigation: arrows, PgUp/PgDn, Home/End, F1 help, F2 wrap toggle,
* F10/Esc exit. In Unwrap mode ←/→ pan horizontally.
*
* Memory:
* CODE → W1 (sprinter-cc --memory small)
* STACK/HEAP/DATA → W2
* File buffer → EMM pages (up to 8 × 16 KB = 128 KB) mapped
* into W3 at 0xC000 on demand via fb()/map_page().
*/
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <conio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sprinter.h>
#include <sprinter_mem.h>
#include <palette.h>
/* ---- screen geometry ---------------------------------------------- */
#define SCREEN_W 80
#define SCREEN_H 32
#define VIEW_TOP_ROW 1
#define VIEW_H 30 /* rows 1..30 inclusive */
#define MENU_ROW 31
#define TAB_STOP 4
/* ---- file/memory layout ------------------------------------------- */
#define PAGE_BITS 14u
#define PAGE_SIZE (1u << PAGE_BITS) /* 16 KB */
#define PAGE_MASK ((uint16_t)(PAGE_SIZE - 1u))
#define MAX_PAGES 8 /* 8 × 16 KB = 128 KB */
#define MAX_FILE ((uint32_t)MAX_PAGES * PAGE_SIZE) /* 131072 */
#define MAX_LINES 2048
#define FILE_BUF ((char *)0xC000) /* W3-mapped EMM page */
/* ---- attribute palette -------------------------------------------- */
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
};
// static uint8_t md_pallete[16 * 4] = {
// 0x00,0x00,0x00, 0x00,
// 0x10,0x10,0x10, 0x00,
// 0x20,0x20,0x20, 0x00,
// 0x30,0x30,0x30, 0x00,
// 0x40,0x40,0x40, 0x00,
// 0x50,0x50,0x50, 0x00,
// 0x60,0x60,0x60, 0x00,
// 0x70,0x70,0x70, 0x00,
// 0x80,0x80,0x80, 0x00,
// 0x90,0x90,0x90, 0x00,
// 0xA0,0xA0,0xA0, 0x00,
// 0xB0,0xB0,0xB0, 0x00,
// 0xC0,0xC0,0xC0, 0x00,
// 0xD0,0xD0,0xD0, 0x00,
// 0xE0,0xE0,0xE0, 0x00,
// 0xF0,0xF0,0xF0, 0x00
// };
#define ATTR_TEXT COLOR(COLOR_LIGHTGRAY, COLOR_BLUE)
#define ATTR_TEXT_TITLE1 COLOR(COLOR_YELLOW, COLOR_BLUE)
#define ATTR_TEXT_TITLE2 COLOR(COLOR_LIGHTBLUE, COLOR_BLUE)
#define ATTR_TEXT_TITLE3 COLOR(COLOR_LIGHTGREEN, COLOR_BLUE)
#define ATTR_TEXT_TITLE4 COLOR(COLOR_LIGHTGREEN, COLOR_BLUE)
#define ATTR_TEXT_BOLD COLOR(COLOR_LIGHTRED, COLOR_BLUE)
#define ATTR_TEXT_ITALIC COLOR(COLOR_LIGHTGREEN, COLOR_BLUE)
#define ATTR_TEXT_UNDERSORE COLOR(COLOR_LIGHTMAGENTA, COLOR_BLUE)
#define ATTR_TEXT_CODE COLOR(COLOR_WHITE, COLOR_BLUE)
#define ATTR_LIST_MARKER COLOR(COLOR_LIGHTCYAN, COLOR_BLUE)
#define ATTR_QUOTE_MARKER COLOR(COLOR_CYAN, COLOR_BLUE)
#define ATTR_HR COLOR(COLOR_LIGHTGRAY, COLOR_BLUE)
#define ATTR_TRUNC COLOR(COLOR_YELLOW, COLOR_BLUE)
#define ATTR_BAR COLOR(COLOR_BLACK, COLOR_LIGHTCYAN)
#define ATTR_BAR_SPINNER COLOR(COLOR_WHITE, COLOR_LIGHTCYAN)
#define ATTR_MENU_T COLOR(COLOR_BLACK, COLOR_LIGHTCYAN)
#define ATTR_MENU_K COLOR(COLOR_YELLOW, COLOR_BLACK)
/* ---- global state ------------------------------------------------- */
/* Files up to MAX_FILE = 128 KB are supported by spreading content
* across up to 8 EMM pages. Only one page is mapped into W3 at any
* given time (FILE_BUF); fb() swaps pages on demand and tracks the
* currently-mapped one in cur_page to avoid redundant OUTs.
*
* line_offset[]: one entry per VISIBLE viewport row. Segments created by
* word-wrap store the byte offset of the wrapped segment; continuation
* flag is in cont_flag[]. nowrap_flag[] marks code block / HR / table
* rows that do NOT wrap and support horizontal scrolling.
*/
/* NOTE: explicit `= 0` initialisers are MANDATORY on SDCC z80.
* Uninitialised static storage (`static T x;`) doesn't actually allocate
* space — multiple such declarations end up at the SAME address and
* silently overwrite each other. See memory/sdcc_static_storage_gotcha. */
uint32_t line_offset[MAX_LINES]; // = {0};
/* Bitmap: seg i is a continuation of the previous logical line. */
uint8_t cont_flag[MAX_LINES / 8]; // = {0};
/* Bitmap: seg i (non-CONT) is inside a fenced code block. */
uint8_t in_code[MAX_LINES / 8]; // = {0};
/* Bitmap: seg i should NOT be wrapped (code block, HR, table). */
uint8_t nowrap_flag[MAX_LINES / 8]; // = {0};
/* Bitmap: seg i is a blank (empty visual line). */
uint8_t blank_flag[MAX_LINES / 8]; // = {0};
/* 2-bit continuation kind per seg (4 lines/byte): 0=PLAIN, 1=QUOTE, 2=LIST, 3=OTHER.
* Used by render_line() to repeat prefix/indent on wrapped continuation rows. */
uint8_t line_kind[MAX_LINES / 4]; // = {0};
uint8_t init_style[MAX_LINES]; // = {0};
static uint16_t n_lines = 0;
static uint16_t top_line = 0;
static uint32_t file_size = 0;
static uint8_t file_blk = 0;
static uint8_t file_pages = 0;
static uint8_t file_phys[MAX_PAGES] = {0};
static uint8_t cur_page = 0xFF; /* 0xFF = no page mapped yet */
static uint8_t viewport_x = 0; /* horizontal pan (for nowrap lines) */
static char filename[64] = {0};
#define HPAN_STEP 8u
/* Spinner on the title bar (col=8, row=0): a small animated glyph
* shown while load_file / index_lines run, so the user knows the
* viewer is busy and not frozen. */
#define SPINNER_COL 8
static const char spinner_chars[4] = { '|', '/', '-', '\\' };
static uint8_t spinner_phase = 0;
static uint8_t spinner_active = 0;
/* ==================================================================
* tiny helpers
* ================================================================== */
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);
}
}
static void put_str(uint8_t x, uint8_t y, const char *s)
{
gotoxy(x, y);
puts(s);
}
static void fill_row(uint8_t y, uint8_t attr)
{
for (uint8_t c = 0; c < SCREEN_W; c++) {
wrchar(c, y, ' ', attr);
}
}
/* Advance the spinner by one frame. No-op when inactive — safe to call
* unconditionally from any loop that wants to show "still working". */
static void spinner_tick(void)
{
if (!spinner_active) return;
wrchar(SPINNER_COL, 0, spinner_chars[spinner_phase & 3], ATTR_BAR_SPINNER);
spinner_phase++;
}
/* Enable / disable the spinner. Disabling also wipes the glyph from the
* title bar so the slot looks like a plain space. */
static void spinner_show(uint8_t on)
{
spinner_active = on;
if (!on) wrchar(SPINNER_COL, 0, ' ', ATTR_BAR);
}
/* Map the EMM page that holds file offset >= page * PAGE_SIZE into W3.
* No-op if already mapped. */
static void map_page(uint8_t page)
{
if (page != cur_page) {
/* file_phys[] saves a BIOS mem_get_page() call on every page swap. */
sprinter_page_w3(file_phys[page]);
cur_page = page;
}
}
/* Read one byte from the logical file at byte offset `p`. Swaps the W3
* mapping if `p` falls into a different page than the one currently
* mapped. Bounds checking is the caller's job.
*
* Hot path: index_lines() calls this for almost every input byte. On z80
* 32-bit shifts/masks are helper calls, so we decode page/offset from the
* little-endian byte layout of `p`: page = bits 14..16, offset = bits 0..13. */
static char fb(uint32_t p)
{
uint8_t *pb = (uint8_t *)&p;
uint8_t page = (uint8_t)((pb[1] >> 6) | (pb[2] << 2));
uint16_t off = (uint16_t)(((uint16_t)(pb[1] & 0x3F) << 8) | pb[0]);
map_page(page);
return FILE_BUF[off];
}
/* Treat space/tab/EOL/NUL as whitespace for emphasis flanking checks. */
static uint8_t ws_or_eol(char c)
{
return (uint8_t)(c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == 0);
}
static uint8_t ws_or_eol_or_delim(char c)
{
return (uint8_t)(c == ' ' || c == ',' || c == '.' || c == '!' || c == '?' || c == ':' || c == ';' || c == '/'
|| c == '\t' || c == '\n' || c == '\r' || c == 0);
}
/* Inline emphasis marker (asterisk, double-asterisk, underscore) is
* recognised only when ONE side is whitespace/EOL and the other is not
* (XOR). This treats "2 * 3" and "2 ** 3" (whitespace both sides) as
* literal, as well as intraword cases like COLOR_YELLOW (no whitespace
* either side), while keeping *italic*, **bold**, _under_ working. */
static uint8_t is_emph_flanked(char prev_ch, char next_ch)
{
return (uint8_t)(ws_or_eol(prev_ch) != ws_or_eol_or_delim(next_ch));
}
/* Continuation-flag bitmap helpers (see cont_flag[] above). */
static uint8_t is_cont(uint16_t idx)
{
return (uint8_t)((cont_flag[idx >> 3] >> (idx & 7)) & 1u);
}
static void set_cont(uint16_t idx)
{
cont_flag[idx >> 3] |= (uint8_t)(1u << (idx & 7));
}
static uint8_t is_nowrap(uint16_t idx)
{
return (uint8_t)((nowrap_flag[idx >> 3] >> (idx & 7)) & 1u);
}
static void set_nowrap(uint16_t idx)
{
nowrap_flag[idx >> 3] |= (uint8_t)(1u << (idx & 7));
}
static uint8_t is_blank(uint16_t idx)
{
return (uint8_t)((blank_flag[idx >> 3] >> (idx & 7)) & 1u);
}
static void set_blank(uint16_t idx)
{
blank_flag[idx >> 3] |= (uint8_t)(1u << (idx & 7));
}
/* Continuation kind: 0=PLAIN, 1=QUOTE, 2=LIST (ULIST/OLIST), 3=OTHER (HR/CODE/TABLE). */
#define CK_PLAIN 0
#define CK_QUOTE 1
#define CK_LIST 2
#define CK_OTHER 3
static uint8_t get_ckind(uint16_t idx)
{
return (uint8_t)((line_kind[idx >> 2] >> ((idx & 3) << 1)) & 3u);
}
static void set_ckind(uint16_t idx, uint8_t kind)
{
uint8_t shift = (uint8_t)((idx & 3) << 1);
line_kind[idx >> 2] = (uint8_t)((line_kind[idx >> 2] & ~(3u << shift)) | ((kind & 3u) << shift));
}
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];
// buff[j * 4] = i * 16;
// buff[j * 4 + 1] = i * 16;
// buff[j * 4 + 2] = i * 16;
// buff[j * 4 + 3] = 0;
}
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);
}
}
/* ==================================================================
* file loading
* ================================================================== */
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 || (uint32_t)sz > MAX_FILE) {
close(fd);
return -2;
}
file_size = (uint32_t)sz;
lseek(fd, 0, SEEK_SET);
/* Allocate exactly the number of EMM pages we need (1..MAX_PAGES). */
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;
}
/* Read the file 16 KB at a time, mapping each page into W3 in turn.
* `remaining` decreases by exactly PAGE_SIZE on each non-final iter;
* the final iter reads whatever is left. */
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;
}
static void unload_file(void)
{
if (file_blk) {
mem_free_block(file_blk);
file_blk = 0;
}
}
/* ==================================================================
* line indexing
* ================================================================== */
/* Returns 1 iff the seg at index `idx` is the start of a logical line
* that begins with ``` (a fenced-code block delimiter). Continuation
* segs are never fence delimiters. */
static uint8_t is_fence_delim(uint16_t idx)
{
if (idx >= n_lines) return 0;
if (is_cont(idx)) return 0;
uint32_t off = line_offset[idx];
if (off + 2 >= file_size) return 0;
return (uint8_t)(fb(off) == '`' &&
fb(off + 1) == '`' &&
fb(off + 2) == '`');
}
static uint8_t is_code_body(uint16_t idx)
{
if (idx >= n_lines) return 0;
return (uint8_t)((in_code[idx >> 3] >> (idx & 7)) & 1u);
}
#define INIT_STYLE_PLAIN 0x0
#define INIT_STYLE_BOLD 0x1
#define INIT_STYLE_ITALIC 0x2
#define INIT_STYLE_UNDER 0x3
#define INIT_STYLE_CODE 0x4
static const uint8_t styles_map[] = {
ATTR_TEXT, ATTR_TEXT_BOLD, ATTR_TEXT_ITALIC, ATTR_TEXT_UNDERSORE, ATTR_TEXT_CODE
};
static uint8_t get_init_style(uint16_t idx)
{
if (idx >= n_lines) return styles_map[INIT_STYLE_PLAIN];
return styles_map[init_style[idx] & 7u];
}
static uint8_t get_init_style_raw(uint16_t idx)
{
if (idx >= n_lines) return INIT_STYLE_PLAIN;
return init_style[idx] & 7u;
}
static void set_init_style_raw(uint16_t idx, uint8_t style)
{
if (idx < MAX_LINES) init_style[idx] = style & 7u;
}
/* Returns 1 iff the logical line at file offset `p_start` should NOT be
* wrapped — it must render on a single seg even if longer than SCREEN_W.
* Currently covers fence delimiters and horizontal rules; tables will be
* added when Phase 4 tables / Phase 6 table layout lands. */
static uint8_t is_nowrap_line(uint32_t p_start)
{
if (p_start + 2 < file_size &&
fb(p_start) == '`' &&
fb(p_start + 1) == '`' &&
fb(p_start + 2) == '`') {
return 1;
}
char c0 = fb(p_start);
if (c0 == '-' || c0 == '*' || c0 == '_') {
char marker = 0;
uint8_t count = 0;
uint8_t ok = 1;
uint32_t p = p_start;
while (p < file_size) {
char ch = fb(p++);
if (ch == '\n' || ch == '\r') break;
if (ch == ' ' || ch == '\t') continue;
if (ch != '-' && ch != '*' && ch != '_') { ok = 0; break; }
if (marker == 0) marker = ch;
else if (ch != marker) { ok = 0; break; }
count++;
}
if (ok && count >= 3) return 1;
}
return 0;
}
/* Line kind returned by classify_line(). */
#define LK_PLAIN 0
#define LK_H1 1
#define LK_H2 2
#define LK_H3 3
#define LK_H4 4 /* also used for H5/H6 */
#define LK_HR 5
#define LK_ULIST 6 /* "- ", "* ", "+ " bullet */
#define LK_OLIST 7 /* "12. " or "12) " number */
#define LK_QUOTE 8 /* "> " blockquote */
/* Classify a logical line.
* - Headers: 1..4 leading '#' followed by space/tab (H5+/H6 collapse to H4).
* - Horizontal rule: line containing only '-', '*' or '_' (one kind, >=3
* of them), optionally separated by spaces/tabs. Matches `---`, `***`,
* `___`, `- - -`, etc. Checked BEFORE ulist so that `- - -` wins.
* - Unordered list: "- ", "* " or "+ " followed by content.
* - Ordered list: one or more digits followed by '.' or ')' then space.
* - Blockquote: '>' optionally followed by space.
* - Otherwise plain text.
* For headers/lists/quotes, *out_content is updated to the offset of the
* first content byte (past the marker and its trailing space).
*/
static uint8_t classify_line(uint32_t p_start, uint32_t *out_content)
{
if (p_start >= file_size) return LK_PLAIN;
char c0 = fb(p_start);
/* Header? */
if (c0 == '#') {
uint8_t lvl = 0;
uint32_t p = p_start;
while (p < file_size && fb(p) == '#' && lvl < 6) {
lvl++; p++;
}
if (p < file_size) {
char ch = fb(p);
if (ch == ' ' || ch == '\t') {
p++;
*out_content = p;
if (lvl > 4) lvl = 4;
return lvl; /* LK_H1 .. LK_H4 */
}
}
/* Bare '#' with no space → not a header, fall through. */
}
/* Horizontal rule? (must precede ulist so `- - -` wins over `- `) */
if (c0 == '-' || c0 == '*' || c0 == '_') {
char marker = 0;
uint8_t count = 0;
uint8_t ok = 1;
uint32_t p = p_start;
while (p < file_size) {
char ch = fb(p++);
if (ch == '\n' || ch == '\r') break;
if (ch == ' ' || ch == '\t') continue;
if (ch != '-' && ch != '*' && ch != '_') { ok = 0; break; }
if (marker == 0) marker = ch;
else if (ch != marker) { ok = 0; break; }
count++;
}
if (ok && count >= 3) return LK_HR;
}
/* For lists and blockquotes we additionally allow leading spaces
* (light nested-list support). HR / header / fence delim stay
* strict col-0 — done above with `c0`. */
uint32_t lp = p_start;
while (lp < file_size && fb(lp) == ' ') lp++;
char cl = (lp < file_size) ? fb(lp) : 0;
/* Unordered list? */
if ((cl == '-' || cl == '*' || cl == '+') &&
(lp + 1) < file_size &&
fb(lp + 1) == ' ') {
*out_content = lp + 2;
return LK_ULIST;
}
/* Ordered list? */
if (cl >= '0' && cl <= '9') {
uint32_t p = lp;
while (p < file_size) {
char ch = fb(p);
if (ch < '0' || ch > '9') break;
p++;
}
if (p < file_size) {
char ch = fb(p);
if ((ch == '.' || ch == ')') &&
(p + 1) < file_size && fb(p + 1) == ' ') {
*out_content = p + 2;
return LK_OLIST;
}
}
}
/* Blockquote? */
if (cl == '>') {
uint32_t p = lp + 1;
if (p < file_size && fb(p) == ' ') p++;
*out_content = p;
return LK_QUOTE;
}
return LK_PLAIN;
}
/* Visible column count for the marker prefix when rendered, including
* any leading indent for nested lists/quotes (the render side keeps the
* 1 byte = 1 visible col invariant for those, so content_off - p_start
* is the visible width). */
static uint8_t marker_visible_col(uint8_t kind, uint32_t p_start, uint32_t content_off)
{
switch (kind) {
case LK_ULIST:
case LK_QUOTE:
case LK_OLIST: return (uint8_t)(content_off - p_start);
default: return 0; /* PLAIN / headers (skipped) */
}
}
/* Emit a new segment entry. */
static void emit_seg(uint32_t off, uint8_t style, uint8_t ckind, uint8_t cont)
{
if (n_lines >= MAX_LINES) return;
line_offset[n_lines] = off;
set_init_style_raw(n_lines, style);
set_ckind(n_lines, ckind);
if (cont) set_cont(n_lines);
n_lines++;
}
static uint8_t is_line_blank(uint32_t p)
{
while (p < file_size) {
char c = fb(p);
if (c == '\n') return 1;
if (c != ' ' && c != '\t') return 0;
p++;
}
return 1;
}
static uint8_t is_fence_raw(uint32_t p)
{
if (p + 2 >= file_size) return 0;
return (fb(p) == '`' && fb(p + 1) == '`' && fb(p + 2) == '`');
}
static uint8_t is_hr_raw(uint32_t p)
{
char c0 = fb(p);
if (c0 != '-' && c0 != '*' && c0 != '_') return 0;
char marker = 0;
uint8_t count = 0, ok = 1;
uint32_t hr_p = p;
while (hr_p < file_size) {
char ch = fb(hr_p++);
if (ch == '\n' || ch == '\r') break;
if (ch == ' ' || ch == '\t') continue;
if (ch != '-' && ch != '*' && ch != '_') { ok = 0; break; }
if (marker == 0) marker = ch;
else if (ch != marker) { ok = 0; break; }
count++;
}
return ok && count >= 3;
}
/* Shared inline-emphasis / wrap scanner for a single line or paragraph stream.
* Starts at `q`, processes until `q_end` (or paragraph break for plain text),
* emits continuation segs as needed. `col` is the starting visible column
* (0 for plain/headers, marker width for list/quote). `ckind` is the
* continuation-kind for wrap segs. Returns the final `line_style` (for
* paragraph streams that carry emphasis across soft/hard breaks). */
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;
char prev_ch = ' ';
uint8_t seg_col = col;
while (q < q_end && n_lines < MAX_LINES) {
char ch = fb(q);
if (ch == '\n') {
/* hard break (already checked by caller) or stream end */
q++;
emit_seg(q, line_style, ckind, (ckind == CK_LIST || ckind == CK_QUOTE) ? 1 : 0);
last_space = 0xFFFFFFFFu; prev_ch = ' '; seg_col = col;
continue;
}
if (ch == '`') {
if (line_style == INIT_STYLE_CODE) line_style = INIT_STYLE_PLAIN;
else line_style = INIT_STYLE_CODE;
q++; continue;
}
if (ch == '*' && (q + 1) < q_end && fb(q + 1) == '*') {
char next_ch = ((q + 2) < q_end) ? fb(q + 2) : '\n';
if (is_emph_flanked(prev_ch, next_ch)) {
if (line_style == INIT_STYLE_PLAIN && ws_or_eol(prev_ch)) {
line_style = INIT_STYLE_BOLD; q += 2; continue;
} else if (line_style == INIT_STYLE_BOLD && ws_or_eol_or_delim(next_ch)) {
line_style = INIT_STYLE_PLAIN; q += 2; continue;
}
}
seg_col += 2; q += 2; prev_ch = '*'; continue;
}
if (ch == '*' || ch == '_') {
char next_ch = ((q + 1) < q_end) ? fb(q + 1) : '\n';
if (is_emph_flanked(prev_ch, next_ch)) {
uint8_t new_style = (ch == '*') ? INIT_STYLE_ITALIC : INIT_STYLE_UNDER;
if (line_style == INIT_STYLE_PLAIN && ws_or_eol(prev_ch)) {
line_style = new_style; q++; continue;
} else if (line_style == new_style && ws_or_eol_or_delim(next_ch)) {
line_style = INIT_STYLE_PLAIN; q++; continue;
}
}
seg_col++; q++; prev_ch = ch; continue;
}
if (ch == '\t') {
uint8_t tgt = (uint8_t)((seg_col & (uint8_t)~(TAB_STOP - 1)) + TAB_STOP);
if (tgt > SCREEN_W) tgt = SCREEN_W;
seg_col = tgt; q++; prev_ch = ' '; continue;
}
if (ch == ' ') {
last_space = q + 1;
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 > line_offset[n_lines - 1])
? last_space : q;
if (wrap_at >= q_end || wrap_at == line_offset[n_lines - 1]) break;
emit_seg(wrap_at, line_style, ckind, 1);
seg_col = col; last_space = 0xFFFFFFFFu; prev_ch = ' '; q = wrap_at;
}
}
return line_style;
}
static void index_lines(void)
{
uint8_t in_block = 0;
n_lines = 0;
memset(in_code, 0, sizeof(in_code));
memset(cont_flag, 0, sizeof(cont_flag));
memset(nowrap_flag, 0, sizeof(nowrap_flag));
memset(blank_flag, 0, sizeof(blank_flag));
memset(line_kind, 0, sizeof(line_kind));
memset(init_style, 0, sizeof(init_style));
if (file_size == 0) return;
uint32_t p = 0;
while (p < file_size && n_lines < MAX_LINES) {
if ((n_lines & 15) == 0) spinner_tick();
/* Blank line = paragraph separator. Emit one empty screen row
* (only if the previous emitted line was not already blank). */
if (is_line_blank(p)) {
while (p < file_size && fb(p) != '\n') p++;
if (p < file_size) p++;
if (n_lines == 0 || line_offset[n_lines - 1] != p) {
emit_seg(p, INIT_STYLE_PLAIN, CK_PLAIN, 0);
set_blank(n_lines - 1);
}
continue;
}
/* ---- fenced code block (delimiter or body) ---- */
if (is_fence_raw(p)) {
in_block = !in_block;
emit_seg(p, INIT_STYLE_PLAIN, CK_OTHER, 0);
set_nowrap(n_lines - 1);
while (p < file_size && fb(p) != '\n') p++;
if (p < file_size) p++;
continue;
}
if (in_block) {
emit_seg(p, INIT_STYLE_PLAIN, CK_OTHER, 0);
set_nowrap(n_lines - 1);
in_code[(n_lines - 1) >> 3] |= (uint8_t)(1u << ((n_lines - 1) & 7));
while (p < file_size && fb(p) != '\n') p++;
if (p < file_size) p++;
continue;
}
/* ---- horizontal rule ---- */
if (is_hr_raw(p)) {
emit_seg(p, INIT_STYLE_PLAIN, CK_OTHER, 0);
set_nowrap(n_lines - 1);
while (p < file_size && fb(p) != '\n') p++;
if (p < file_size) p++;
continue;
}
/* ---- classify current line ---- */
uint32_t content_off = p;
uint8_t kind = classify_line(p, &content_off);
/* ---- Header: single line, not merged into paragraphs ---- */
if (kind >= LK_H1 && kind <= LK_H4) {
emit_seg(p, INIT_STYLE_PLAIN, CK_OTHER, 0);
uint32_t line_end = content_off;
while (line_end < file_size && fb(line_end) != '\n') line_end++;
(void)inline_scan(content_off, line_end, 0, CK_OTHER, INIT_STYLE_PLAIN);
while (p < file_size && fb(p) != '\n') p++;
if (p < file_size) p++;
continue;
}
/* ---- List item: single line, not merged ---- */
if (kind == LK_ULIST || kind == LK_OLIST) {
uint8_t col = marker_visible_col(kind, p, content_off);
emit_seg(p, INIT_STYLE_PLAIN, CK_LIST, 0);
uint32_t line_end = content_off;
while (line_end < file_size && fb(line_end) != '\n') line_end++;
(void)inline_scan(content_off, line_end, col, CK_LIST, INIT_STYLE_PLAIN);
while (p < file_size && fb(p) != '\n') p++;
if (p < file_size) p++;
continue;
}
/* ---- Quote line: single line, not merged ---- */
if (kind == LK_QUOTE) {
uint8_t col = marker_visible_col(kind, p, content_off);
emit_seg(p, INIT_STYLE_PLAIN, CK_QUOTE, 0);
uint32_t line_end = content_off;
while (line_end < file_size && fb(line_end) != '\n') line_end++;
(void)inline_scan(content_off, line_end, col, CK_QUOTE, INIT_STYLE_PLAIN);
while (p < file_size && fb(p) != '\n') p++;
if (p < file_size) p++;
continue;
}
/* ---- Plain text paragraph: merge across soft breaks ---- */
{
uint8_t line_style = INIT_STYLE_PLAIN;
emit_seg(p, line_style, CK_PLAIN, 0);
uint32_t q = p;
uint32_t last_space = 0xFFFFFFFFu;
char prev_ch = ' ';
uint8_t seg_col = 0;
while (q < file_size && n_lines < MAX_LINES) {
char ch = fb(q);
uint8_t soft_break = 0;
if (ch == '\n') {
/* Paragraph break? */
if (q + 1 >= file_size) break;
if (fb(q + 1) == '\n') break; /* blank line */
uint32_t next = q + 1;
if (is_fence_raw(next)) break;
if (is_hr_raw(next)) break;
uint32_t dummy;
if (classify_line(next, &dummy) != LK_PLAIN) break;
if (is_line_blank(next)) break;
/* Hard break? */
uint8_t is_hard = 0;
if (q >= p + 2) {
char b1 = fb(q - 1);
char b2 = fb(q - 2);
if ((b1 == ' ' || b1 == '\t') && (b2 == ' ' || b2 == '\t'))
is_hard = 1;
}
if (is_hard) {
q++;
emit_seg(q, line_style, CK_PLAIN, 0);
last_space = 0xFFFFFFFFu; prev_ch = ' '; seg_col = 0;
continue;
}
/* Soft break — treat as a single space. */
q++;
soft_break = 1;
ch = ' ';
}
if (ch == '`') {
if (line_style == INIT_STYLE_CODE) line_style = INIT_STYLE_PLAIN;
else line_style = INIT_STYLE_CODE;
if (!soft_break) q++;
continue;
}
if (ch == '*' && (q + 1) < file_size && fb(q + 1) == '*') {
char next_ch = ((q + 2) < file_size) ? fb(q + 2) : '\n';
if (is_emph_flanked(prev_ch, next_ch)) {
if (line_style == INIT_STYLE_PLAIN && ws_or_eol(prev_ch)) {
line_style = INIT_STYLE_BOLD;
if (!soft_break) q += 2; else q++;
continue;
} else if (line_style == INIT_STYLE_BOLD && ws_or_eol_or_delim(next_ch)) {
line_style = INIT_STYLE_PLAIN;
if (!soft_break) q += 2; else q++;
continue;
}
}
seg_col += 2;
if (!soft_break) q += 2; else q++;
prev_ch = '*'; continue;
}
if (ch == '*' || ch == '_') {
char next_ch = ((q + 1) < file_size) ? fb(q + 1) : '\n';
if (is_emph_flanked(prev_ch, next_ch)) {
uint8_t new_style = (ch == '*') ? INIT_STYLE_ITALIC : INIT_STYLE_UNDER;
if (line_style == INIT_STYLE_PLAIN && ws_or_eol(prev_ch)) {
line_style = new_style;
if (!soft_break) q++; else q++;
continue;
} else if (line_style == new_style && ws_or_eol_or_delim(next_ch)) {
line_style = INIT_STYLE_PLAIN;
if (!soft_break) q++; else q++;
continue;
}
}
seg_col++;
if (!soft_break) q++; else q++;
prev_ch = ch; continue;
}
if (ch == '\t') {
uint8_t tgt = (uint8_t)((seg_col & (uint8_t)~(TAB_STOP - 1)) + TAB_STOP);
if (tgt > SCREEN_W) tgt = SCREEN_W;
seg_col = tgt;
if (!soft_break) q++; else q++;
prev_ch = ' '; continue;
}
if (ch == ' ') {
if (soft_break) last_space = q;
else last_space = q + 1;
seg_col++;
if (!soft_break) q++; else q++;
prev_ch = ' ';
} else {
seg_col++;
if (!soft_break) q++; else q++;
prev_ch = ch;
}
if (seg_col >= SCREEN_W) {
uint32_t wrap_at = (last_space != 0xFFFFFFFFu && last_space > line_offset[n_lines - 1])
? last_space : q;
if (wrap_at >= file_size || wrap_at == line_offset[n_lines - 1]) break;
emit_seg(wrap_at, line_style, CK_PLAIN, 1);
seg_col = 0; last_space = 0xFFFFFFFFu; prev_ch = ' '; q = wrap_at;
}
}
if (q >= file_size) {
p = file_size;
} else {
while (q < file_size && fb(q) != '\n') q++;
if (q + 1 < file_size && fb(q + 1) == '\n') q += 2;
else if (q < file_size) q++;
p = q;
}
}
}
}
#define EM_NONE 0
#define EM_BOLD 1
#define EM_ITALIC 2
#define EM_UNDER 3
#define EM_CODE 4
static void render_line(uint16_t line_idx, uint8_t row)
{
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); // ATTR_TEXT;
uint8_t emph = is_code_body(line_idx) ? EM_CODE : get_init_style_raw(line_idx);
uint8_t parse_inline = 1; /* skip inline parsing for fenced code body */
if (is_blank(line_idx)) {
while (col < SCREEN_W) wrchar(col++, row, ' ', ATTR_TEXT);
return;
}
if (line_idx < n_lines) {
uint32_t p = line_offset[line_idx];
uint32_t content_off = p;
uint8_t cont = is_cont(line_idx);
/* Right bound for this seg: start of next seg, or file end. */
uint32_t seg_end = (line_idx + 1 < n_lines)
? line_offset[line_idx + 1]
: file_size;
if (seg_end > file_size) seg_end = file_size;
uint8_t kind;
/* Continuation seg → repeat prefix/indent from the first non-CONT
* segment of this logical line, then render plain text. */
if (cont) {
uint16_t first = line_idx;
while (first > 0 && is_cont((uint16_t)(first - 1))) first--;
uint32_t p_start = line_offset[first];
uint32_t content_off = p_start;
uint8_t kind = classify_line(p_start, &content_off);
uint8_t indent = marker_visible_col(kind, p_start, content_off);
if (kind == LK_QUOTE) {
uint8_t q = 0;
while (q + 2 < content_off && col < SCREEN_W && fb(p_start + q) == ' ') {
wrchar(col++, row, ' ', ATTR_TEXT);
q++;
}
if (col < SCREEN_W) wrchar(col++, row, 0xB3, ATTR_QUOTE_MARKER);
if (col < SCREEN_W) wrchar(col++, row, ' ', ATTR_TEXT);
} else if (kind == LK_ULIST || kind == LK_OLIST) {
uint8_t q = 0;
while (q < indent && col < SCREEN_W) {
wrchar(col++, row, ' ', ATTR_TEXT);
q++;
}
}
/* start rendering from the wrapped segment offset */
p = line_offset[line_idx];
content_off = p;
/* For cont segs, inline parsing continues normally. */
}
/* Fenced code block: delimiter line → blank row; body lines render
* verbatim in ATTR_TEXT_CODE without any inline-emphasis parsing. */
if (is_fence_delim(line_idx)) {
while (col < SCREEN_W) wrchar(col++, row, ' ', ATTR_TEXT);
return;
}
if (is_code_body(line_idx)) {
base_attr = ATTR_TEXT_CODE;
cur_attr = ATTR_TEXT_CODE;
parse_inline = 0;
kind = LK_PLAIN;
} else {
kind = classify_line(p, &content_off);
}
if (kind == LK_HR) {
while (col < SCREEN_W) wrchar(col++, row, 0xC4, ATTR_HR);
return;
}
if (kind >= LK_H1 && kind <= LK_H4) {
switch (kind) {
case LK_H1: base_attr = ATTR_TEXT_TITLE1; break;
case LK_H2: base_attr = ATTR_TEXT_TITLE2; break;
case LK_H3: base_attr = ATTR_TEXT_TITLE3; break;
default: base_attr = ATTR_TEXT_TITLE4; break;
}
cur_attr = base_attr;
p = content_off;
} else if (kind == LK_ULIST) {
/* Light nested-list support: any leading spaces before '-' / '*'
* / '+' render as plain spaces (preserves nesting visually);
* then a bullet + a content space. */
uint32_t q = p;
while (q + 2 < content_off && col < SCREEN_W && fb(q) == ' ') {
wrchar(col++, row, ' ', ATTR_TEXT);
q++;
}
if (col < SCREEN_W) wrchar(col++, row, 0x07, ATTR_LIST_MARKER); /* • */
if (col < SCREEN_W) wrchar(col++, row, ' ', ATTR_TEXT);
p = content_off;
} else if (kind == LK_OLIST) {
/* Indent (if any) → spaces; digits and '.' or ')' in marker
* colour; trailing space in plain. */
uint32_t q = p;
while (q < content_off && col < SCREEN_W && fb(q) == ' ') {
wrchar(col++, row, ' ', ATTR_TEXT);
q++;
}
while (q < content_off - 1 && col < SCREEN_W) {
wrchar(col++, row, fb(q), ATTR_LIST_MARKER);
q++;
}
if (col < SCREEN_W) wrchar(col++, row, ' ', ATTR_TEXT);
p = content_off;
} else if (kind == LK_QUOTE) {
/* Indent (if any) → spaces; '│' (0xB3) prefix in marker
* colour, then a space, then content. */
uint32_t q = p;
while (q + 1 < content_off && col < SCREEN_W && fb(q) == ' ') {
wrchar(col++, row, ' ', ATTR_TEXT);
q++;
}
if (col < SCREEN_W) wrchar(col++, row, 0xB3, ATTR_QUOTE_MARKER);
if (col < SCREEN_W) wrchar(col++, row, ' ', ATTR_TEXT);
p = content_off;
}
/* Effective horizontal offset: only nowrap lines honour viewport_x. */
uint8_t effective_vx = is_nowrap(line_idx) ? viewport_x : 0;
/* `cc` is the VISIBLE column inside the content area (the part
* after any fixed marker). For nowrap lines, chars at cc < viewport_x
* are skipped so the user can pan horizontally. */
uint8_t cc = 0;
char prev_ch = ' '; /* for `_` flanking check */
while (p < seg_end && col < SCREEN_W) {
char ch = fb(p);
/* Soft break (newline inside a paragraph segment) — render as a
* single space. Hard breaks are already separate segments. */
if (ch == '\n' || ch == '\r') ch = ' ';
/* --- inline emphasis markers (consumed, not rendered) ---
* Asterisk / double-asterisk / underscore are markers only
* when XOR-flanked (whitespace on exactly one side). Keeps
* COLOR_YELLOW, intraword slashes, "2 * 3", "2 ** 3" etc.
* as literal text. A marker that would conflict with the
* currently-active style is still consumed (zero-width)
* so the index_lines column count stays in sync with the
* rendered output. Skipped entirely inside fenced code
* blocks (parse_inline=0). */
if (parse_inline) {
/* `**` (bold) */
if (ch == '*' && (p + 1) < seg_end && fb(p + 1) == '*') {
char next_ch = ((p + 2) < seg_end) ? fb(p + 2) : '\n';
if (is_emph_flanked(prev_ch, next_ch)) {
if (emph == EM_NONE && ws_or_eol(prev_ch)) {
emph = EM_BOLD; cur_attr = ATTR_TEXT_BOLD;
p += 2; continue;
} else if (emph == EM_BOLD && ws_or_eol_or_delim(next_ch)) {
emph = EM_NONE; cur_attr = base_attr;
p += 2; continue;
}
}
/* literal `**` — render BOTH stars, do NOT fall
* through to single-`*` handler. */
p += 2;
if (cc >= effective_vx && col < SCREEN_W) wrchar(col++, row, '*', cur_attr);
cc++;
if (cc >= effective_vx && col < SCREEN_W) wrchar(col++, row, '*', cur_attr);
cc++;
prev_ch = '*';
continue;
}
/* `*` (italic) and `_` (underline) — same flanking rule */
if (ch == '*' || ch == '_') {
char next_ch = ((p + 1) < seg_end) ? fb(p + 1) : '\n';
if (is_emph_flanked(prev_ch, next_ch)) {
uint8_t active = (ch == '*') ? EM_ITALIC : EM_UNDER;
uint8_t active_attr = (ch == '*') ? ATTR_TEXT_ITALIC : ATTR_TEXT_UNDERSORE;
if (emph == EM_NONE && ws_or_eol(prev_ch)) {
emph = active; cur_attr = active_attr;
p++; continue;
} else if (emph != EM_NONE && ws_or_eol_or_delim(next_ch)) {
emph = EM_NONE; cur_attr = base_attr;
p++; continue;
}
}
/* intraword / both-flanked — fall through, literal */
}
if (ch == '`') {
if (emph == EM_NONE) {
emph = EM_CODE; cur_attr = ATTR_TEXT_CODE;
p++; continue;
}
if (emph == EM_CODE) {
emph = EM_NONE; cur_attr = base_attr;
p++; continue;
}
}
}
p++;
if (ch == '\t') {
/* Tab expands in CONTENT-col space (cc), each generated
* space is then conditionally rendered through hpan. */
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++;
}
prev_ch = ' ';
} else {
if (cc >= effective_vx && col < SCREEN_W) {
wrchar(col++, row, ch, cur_attr);
}
cc++;
prev_ch = ch;
}
}
/* Right-edge truncation indicator for nowrap lines: when the screen
* ran out (col == SCREEN_W) and this row is nowrap, peek forward
* past any zero-width inline markers — if there's still real visible
* content before the end of the line, overlay the last cell with '>'. */
if (is_nowrap(line_idx) && col >= SCREEN_W) {
uint32_t pp = p;
uint32_t left_bound = line_offset[line_idx];
while (pp < seg_end) {
char c = fb(pp);
if (c == '\n' || c == '\r') break;
if (c == '`') { pp++; continue; }
if (c == '*' && (pp + 1) < seg_end && fb(pp + 1) == '*') {
char pc = (pp > left_bound) ? fb(pp - 1) : ' ';
char nc = (pp + 2 < seg_end) ? fb(pp + 2) : '\n';
if (is_emph_flanked(pc, nc)) { pp += 2; continue; }
/* literal `**` — there IS visible content */
} else if (c == '*' || c == '_') {
char pc = (pp > left_bound) ? fb(pp - 1) : ' ';
char nc = (pp + 1 < seg_end) ? fb(pp + 1) : '\n';
if (is_emph_flanked(pc, nc)) { pp++; continue; }
}
wrchar(SCREEN_W - 1, row, '>', ATTR_TRUNC);
break;
}
}
}
/* Pad rest of the row so previous render content is wiped. For code-
* body rows use the code attribute so the block extends to the right
* edge — visually distinguishes the block from surrounding text. */
{
uint8_t pad_attr = (parse_inline ? ATTR_TEXT : ATTR_TEXT_CODE);
while (col < SCREEN_W) wrchar(col++, row, ' ', pad_attr);
}
}
static void render_viewport(void)
{
for (uint8_t i = 0; i < VIEW_H; i++) {
render_line((uint16_t)(top_line + i), (uint8_t)(VIEW_TOP_ROW + i));
}
}
static uint8_t calc_pct(void)
{
uint8_t pct;
if (n_lines <= VIEW_H)
pct = 100;
else
pct = (uint8_t)((uint32_t)top_line * 100UL / (uint32_t)(n_lines - VIEW_H));
return pct;
}
static void render_full_status(void)
{
uint16_t last = top_line + VIEW_H;
if (last > n_lines) last = n_lines;
uint8_t pct = calc_pct();
fill_row(0, ATTR_BAR);
gotoxy(1, 0);
/* "MDVIEW", then col 7 = space, col 8 = spinner slot, col 9 = space,
* col 10+ = filename. Spinner_tick draws over col 8 in ATTR_BAR. */
printf("MDVIEW %s", filename);
gotoxy(45, 0);
printf("\xB3 L %5u-%u / %u", (top_line + 1), last, n_lines);
gotoxy(70, 0);
printf("\xB3 %3u%%", pct);
}
static void render_updated_status(void)
{
uint16_t last = top_line + VIEW_H;
if (last > n_lines) last = n_lines;
uint8_t pct = calc_pct();
gotoxy(45, 0);
printf("\xB3 L %5u-%u / %u", (top_line + 1), last, n_lines);
gotoxy(70, 0);
printf("\xB3 %3u%%", pct);
}
static void render_menu(void)
{
fill_row(MENU_ROW, ATTR_MENU_T);
put_str_attr(1, MENU_ROW, "F1", ATTR_MENU_K);
put_str_attr(4, MENU_ROW, "Help", ATTR_MENU_T);
/* F2 slot removed — no wrap/unwrap toggle any more. */
put_str_attr(SCREEN_W - 10, MENU_ROW, "F10", ATTR_MENU_K);
put_str_attr(SCREEN_W - 5, MENU_ROW, "Exit", ATTR_MENU_T);
}
/* ==================================================================
* scrolling
* ================================================================== */
static void clamp_top(void)
{
if (n_lines <= VIEW_H) {
top_line = 0;
} else if (top_line > n_lines - VIEW_H) {
top_line = (uint16_t)(n_lines - VIEW_H);
}
}
static void scroll_up(uint16_t n)
{
uint16_t new_top_line = (top_line >= n) ? (uint16_t)(top_line - n) : 0;
if( new_top_line != top_line) {
top_line = new_top_line;
if (n == 1) {
scroll(0, VIEW_TOP_ROW, SCREEN_W, VIEW_H, 2, 0);
render_line((uint16_t)(top_line), (uint8_t)(VIEW_TOP_ROW));
} else {
render_viewport();
}
}
}
static void scroll_down(uint16_t n)
{
uint16_t new_top_line = (top_line + n < n_lines - VIEW_H) ?
(uint16_t)(top_line + n) : n_lines - VIEW_H;
if( new_top_line != top_line) {
top_line = new_top_line;
if (n == 1) {
scroll(0, VIEW_TOP_ROW, SCREEN_W, VIEW_H, 1, 0);
render_line((uint16_t)(top_line + VIEW_H - 1), (uint8_t)(VIEW_TOP_ROW + VIEW_H - 1));
} else {
clamp_top();
render_viewport();
}
}
}
/* Horizontal pan (only when viewport contains nowrap lines). */
static void scroll_h(int8_t delta)
{
uint8_t any_nowrap = 0;
for (uint8_t i = 0; i < VIEW_H; i++) {
if (is_nowrap((uint16_t)(top_line + i))) { any_nowrap = 1; break; }
}
if (!any_nowrap) return;
int16_t nx = (int16_t)viewport_x + delta;
if (nx < 0) nx = 0;
if (nx > 240) nx = 240;
if ((uint8_t)nx != viewport_x) {
viewport_x = (uint8_t)nx;
render_viewport();
}
}
/* Toggle wrap/truncate and rebuild the seg index. Tries to preserve
* the user's scroll position by remembering the byte offset of the
* current top seg and finding the seg that contains that offset after
* the rebuild. Also resets horizontal pan — wrap mode doesn't use it. */
static void toggle_wrap(void)
{
/* Removed — no longer needed. Kept as stub to avoid
* linker warnings if something still calls it. */
(void)0;
}
/* ==================================================================
* help screen
* ================================================================== */
static const char * const help_lines[] = {
"",
" MDVIEW v0.1 - Markdown text viewer for Sprinter",
" =================================================",
"",
" Navigation:",
" Up / Down - scroll one line",
" PgUp / PgDn - scroll one page",
" Home - go to top of document",
" End - go to bottom of document",
" Left / Right - pan horizontally (nowrap lines only)",
"",
" Other:",
" F1 - show this help",
" F2 - (reserved)",
" F10 / Esc - exit",
"",
" Press any key to return...",
(const char *)0
};
static void help_screen(void)
{
clrscr_attr(ATTR_TEXT);
for (uint8_t i = 0; help_lines[i]; i++) {
put_str_attr(0, (uint8_t)(i + 1), help_lines[i], ATTR_TEXT);
}
(void)getkey();
clrscr_attr(ATTR_TEXT);
render_menu();
render_full_status();
render_viewport();
}
/* ==================================================================
* error
* ================================================================== */
static void die(const char *msg)
{
clrscr_attr(ATTR_TEXT);
puts(msg);
puts("Press any key to exit...");
(void)getkey();
}
/* ==================================================================
* main
* ================================================================== */
int main(int argc, char **argv)
{
memset(line_offset, 0, sizeof(line_offset));
memset(cont_flag, 0, sizeof(cont_flag));
memset(in_code, 0, sizeof(in_code));
memset(nowrap_flag, 0, sizeof(nowrap_flag));
memset(blank_flag, 0, sizeof(blank_flag));
memset(line_kind, 0, sizeof(line_kind));
memset(init_style, 0, sizeof(init_style));
const char *path;
if (argc >= 2) path = argv[1];
else path = "SAMPLE.MD";
/* Copy path → static filename[] for the status bar. */
{
uint8_t i = 0;
while (i < (uint8_t)(sizeof(filename) - 1) && path[i]) {
filename[i] = path[i];
i++;
}
filename[i] = 0;
}
/* Bring the UI up FIRST so the user sees the menu + title bar (with
* a spinner) immediately, instead of a black screen while we read
* the file and index lines. */
set_videotextmode(TEXT_MODE_80x32);
clrscr_attr(ATTR_TEXT);
set_pallete();
render_menu();
render_full_status();
spinner_show(1);
int rc = load_file(path);
if (rc < 0) {
spinner_show(0);
clrscr_attr(ATTR_TEXT);
puts("mdview: cannot load file");
puts(path);
switch (rc) {
case -1: puts("(open failed)"); break;
case -2: puts("(size > 128K or seek failed)"); break;
case -3: puts("(mem_alloc_pages failed)"); break;
case -4: puts("(short read)"); break;
}
puts("Press any key to exit...");
(void)getkey();
return 1;
}
index_lines();
spinner_show(0);
if (n_lines == 0) {
die("mdview: empty file");
unload_file();
return 1;
}
render_full_status();
render_viewport();
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;
}
switch (scan) {
case KEY_F10: goto exit_loop;
case KEY_UP: scroll_up(1); break;
case KEY_DOWN: scroll_down(1); break;
case KEY_LEFT: scroll_h(-(int8_t)HPAN_STEP); break;
case KEY_RIGHT: scroll_h(+(int8_t)HPAN_STEP); break;
case KEY_PGUP: scroll_up(VIEW_H); break;
case KEY_PGDN: scroll_down(VIEW_H); break;
case KEY_HOME: top_line = 0;
viewport_x = 0;
render_viewport(); break;
case KEY_END: top_line = (n_lines > VIEW_H)
? (uint16_t)(n_lines - VIEW_H) : 0;
viewport_x = 0;
render_viewport(); break;
default: continue;
}
render_updated_status();
}
exit_loop:
unload_file();
pal_reset(PAL_CGA);
clrscr_attr(ATTR_TEXT);
return 0;
}