Add mdview markdown viewer, reorganize tests/examples and libc layout

- Split tests/ (libc feature tests) and examples/ (real apps); shared
  app.mk in repo root, was examples/example.mk
- libc/io/* split into libc/{conio,env,errno,file,mouse,string,sys,
  time,video}/ — clearer module boundaries
- New examples/mdview/: markdown viewer (Phases 1-5 + light nested
  lists). Headers (H1-H4), HR, ulist/olist/quote with nesting via
  leading spaces, fenced code blocks, inline emphasis (bold/italic/
  underscore/code), wrap/unwrap mode with soft wrap (F2), horizontal
  pan (← →) with '>' truncation indicator
- libc additions: scroll() in conio (ESTEX SCROLL), strlwr/strupr,
  gets() test
- Makefile updates across tests/ for the new shared app.mk path

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-06-04 22:23:36 +03:00
parent b851e22fa6
commit 737c974400
104 changed files with 2485 additions and 223 deletions
+55
View File
@@ -58,6 +58,30 @@ char getche(void) __naked
__endasm;
}
/* getkey — like getch() but exposes BOTH the ASCII value and the
* positional scan code, so callers can distinguish extended keys
* (arrows, F1..F12, PgUp/PgDn, Home/End, Ins/Del all of which carry
* ASCII == 0 from ESTEX) from plain ASCII keys.
*
* return = (scan << 8) | ascii
*
* For plain keys: ascii holds the character, scan holds the positional
* code (bit 7 set when Ctrl/Alt/Shift is held).
* For extended keys: ascii == 0, scan identifies the key (see KEY_* in
* <conio.h>).
*/
uint16_t getkey(void) __naked
{
__asm
push ix
ld c, #0x30 ; ESTEX WAITKEY: A=ASCII, D=scan, E=ASCII
rst #0x10
pop ix
ld e, a ; E = ASCII (defensive: A is the canonical copy)
ret ; __sdcccall(1) returns uint16_t in DE
__endasm;
}
/* ---- putch / cputs: Turbo-C conio convention ---------------------- *
* Both APPLY the current text attribute (g_text_attr). When attr is
* KEEP_EXIST_ATTR (>0xFF), they short-circuit to the FAST stdio path
@@ -462,6 +486,37 @@ uint16_t wherexy(void) __naked
__endasm;
}
/* wrchar(uint8_t x, uint8_t y, char ch, uint8_t attr)
*
* SDCC __sdcccall(1): x in A, y in L (2 uint8 A, L); ch and attr
* packed and pushed on the stack as a single 16-bit value (caller does
* `ld hl, #(attr<<8)|ch; push hl`). Layout after CALL:
* [SP+0..1] = return address
* [SP+2] = ch (low half of pushed pair)
* [SP+3] = attr (high half)
* Void return callee-pops the 2 stack-arg bytes via `pop bc` + jp (iy).
*/
void scroll(uint8_t x, uint8_t y, uint8_t w, uint8_t h, uint8_t direction, uint8_t clear) __naked
{
(void)x; (void)y; (void)w; (void)h; (void)direction; (void)clear;
__asm
pop iy ; return address
ld d, l ; D = row (y)
ld e, a ; E = col (x)
pop hl ; H = heigth(h), L = width(w)
pop bc ; C = direction, B = clear
ld a, b ; A = clear(B)
ld b, c ; B = direction(C)
push ix
ld c, #0x55 ; ESTEX SCROLL
rst #0x10
pop ix
jp (iy)
__endasm;
}
/* wrchar(uint8_t x, uint8_t y, char ch, uint8_t attr)
*
View File
+4 -3
View File
@@ -143,7 +143,8 @@ int fputc(int c, FILE *fp)
{
if (!fp) { errno = EBADF; return EOF; }
if (fp->flags & _F_CONOUT) {
return putchar(c);
putchar(c);
return (int)c;
}
if (!(fp->flags & _F_WRITE)) { errno = EBADF; return EOF; }
uint8_t ch = (uint8_t)c;
@@ -175,7 +176,7 @@ int fputs(const char *s, FILE *fp)
if (!fp || !s) { errno = EBADF; return EOF; }
if (fp->flags & _F_CONOUT) {
while (*s) {
if (putchar((unsigned char)*s++) == EOF) return EOF;
putchar((unsigned char)*s++);;
}
return 0;
}
@@ -239,7 +240,7 @@ size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *fp)
const char *p = (const char *)ptr;
size_t total = size * nmemb;
for (size_t i = 0; i < total; i++) {
if (putchar((unsigned char)p[i]) == EOF) return i / size;
putchar((unsigned char)p[i]);
}
return nmemb;
}
+54 -4
View File
@@ -38,6 +38,42 @@
char kbhit (void);
char getch (void);
char getche(void);
/* Extended-key reader. Returns (scan << 8) | ascii. Plain ASCII keys
* have ascii in the low byte; extended keys (arrows / F1..F12 /
* PgUp/PgDn / Home / End / Ins / Del) carry ascii == 0 and the
* KEY_* code in the high byte. */
uint16_t getkey(void);
/* Scan codes returned in the high byte of getkey() when the low byte
* (ASCII) is 0. Empirically verified in MAME — the ProgrammerManual.txt
* "positional code" column is misleading; BIOS returns IBM-style codes
* for the F-keys and a "5N + numpad-position" pattern for the cursor /
* editing keys.
*
* Verified 2026-06-04 by reading raw getkey() output. */
#define KEY_F1 0x3B
#define KEY_F2 0x3C
#define KEY_F3 0x3D
#define KEY_F4 0x3E
#define KEY_F5 0x3F
#define KEY_F6 0x40
#define KEY_F7 0x41
#define KEY_F8 0x42
#define KEY_F9 0x43
#define KEY_F10 0x44
#define KEY_F11 0x45 /* not verified */
#define KEY_F12 0x46 /* not verified */
#define KEY_END 0x51
#define KEY_DOWN 0x52
#define KEY_PGDN 0x53
#define KEY_LEFT 0x54
#define KEY_RIGHT 0x56
#define KEY_HOME 0x57
#define KEY_UP 0x58
#define KEY_PGUP 0x59
#define KEY_INS 0x50 /* numpad 0; not verified */
#define KEY_DEL 0x55 /* numpad 5/.; not verified */
char putch (char c);
char cputs (const char *s);
int cprintf(const char *fmt, ...);
@@ -63,6 +99,8 @@ uint8_t wherex (void);
uint8_t wherey (void);
uint16_t wherexy(void); // high byte = Y, low byte = X coords.
void scroll(uint8_t, uint8_t, uint8_t, uint8_t, uint8_t, uint8_t);
/* Direct character/attribute screen access (ESTEX $57 / $58).
* wrchar — write char + attribute at (x, y); does NOT advance the cursor
* and does NOT interpret control characters. Useful for
@@ -143,10 +181,22 @@ uint8_t get_putch_raw_mode(void);
* Colour order is standard CGA / Borland-conio.h. Constants 0..7 are
* usable for both fg and bg; 8..15 are foreground-only. */
enum {
COLOR_BLACK = 0, COLOR_BLUE, COLOR_GREEN, COLOR_CYAN,
COLOR_RED, COLOR_MAGENTA, COLOR_BROWN, COLOR_LIGHTGRAY,
COLOR_DARKGRAY, COLOR_LIGHTBLUE, COLOR_LIGHTGREEN, COLOR_LIGHTCYAN,
COLOR_LIGHTRED, COLOR_LIGHTMAGENTA, COLOR_YELLOW, COLOR_WHITE
COLOR_BLACK = 0,
COLOR_BLUE,
COLOR_GREEN,
COLOR_CYAN,
COLOR_RED,
COLOR_MAGENTA,
COLOR_BROWN,
COLOR_LIGHTGRAY,
COLOR_DARKGRAY,
COLOR_LIGHTBLUE,
COLOR_LIGHTGREEN,
COLOR_LIGHTCYAN,
COLOR_LIGHTRED,
COLOR_LIGHTMAGENTA,
COLOR_YELLOW,
COLOR_WHITE
};
#define COLOR_BLINK 0x80u
#define COLOR(fg, bg) ((uint8_t)((((bg) & 0x07) << 4) | ((fg) & 0x0F)))
+15 -7
View File
@@ -16,14 +16,22 @@
#ifndef FCNTL_H
#define FCNTL_H
#define O_RDONLY 0
#define O_WRONLY 1
#define O_RDWR 2
#ifndef _STD_SEEK_
#define _STD_SEEK_
/* constants to be used as 3rd argument for "fseek" function */
#define SEEK_SET 0
#define SEEK_CUR 1
#define SEEK_END 2
#endif
#define O_CREAT 0x040
#define O_EXCL 0x080
#define O_TRUNC 0x200
#define O_APPEND 0x400
/* Definition "open flags" */
#define O_WRONLY 0x01 /* 0 file write only */
#define O_RDONLY 0x02 /* 1 file read only */
#define O_RDWR 0x03 /* 1,0 file read/write */
#define O_TRUNC 0x04 /* 2 open with truncation */
#define O_CREAT 0x08 /* 3 create and open file */
#define O_EXCL 0x10 /* 4 exclusive open */
#define O_APPEND 0x20 /* 5 to end of file */
int open (const char *path, int flags);
int creat(const char *path, int mode); /* mode arg ignored on Sprinter */
+1 -1
View File
@@ -30,7 +30,7 @@ int vprintf(const char *, va_list);
int vsprintf(char *, const char *, va_list);
/* puts / putchar / getchar — overridden by our libc to use ESTEX. */
int puts (const char *);
char puts (const char *);
int putchar(int);
int getchar(void);
-29
View File
@@ -1,29 +0,0 @@
/*
* solid_compat.c — Solid-C compatibility helpers that need real code
* (rather than just header macros).
*/
#include <sprinter_compat.h>
#include <ctype.h>
char *strlwr(char *s)
{
char *p = s;
while (*p) {
if (*p >= 'A' && *p <= 'Z') *p += 'a' - 'A';
p++;
}
return s;
}
char *strupr(char *s)
{
char *p = s;
while (*p) {
if (*p >= 'a' && *p <= 'z') *p -= 'a' - 'A';
p++;
}
return s;
}
/* div() comes from SDCC's z80.lib. */
-6
View File
@@ -17,13 +17,7 @@ int getchar(void) __naked
ld c, #0x30 ; ESTEX WAITKEY
rst #0x10
pop ix
ld a, e ; E = ASCII (already the low byte of our return DE)
or a, a
jr Z, no_ascii
ld d, #0
ret
no_ascii:
ld de, #-1
ret
__endasm;
}
+6 -14
View File
@@ -19,23 +19,15 @@ int putchar(int c) __naked
{
(void)c;
__asm
ld a, l ; SDCC __sdcccall(1) int HL
push ix
ld a, l
cp #0x0A
jr nz, _pc_emit
ld a, #0x0D ; CR before LF
push af
jr nz, cputc
call cputc
ld a, #0x0D
cputc:
ld c, #0x5B
rst #0x10
pop af
ld a, #0x0A
_pc_emit:
push af
ld c, #0x5B
rst #0x10
pop af
pop ix
ld e, a
ld e, l
ld d, #0
ret
__endasm;
+21 -34
View File
@@ -14,17 +14,11 @@
* - Avoid trailing PUTCHAR after PCHARS — empirically that sometimes
* drops the next char. Embed the line ending inside the PCHARS
* buffer instead.
* - Strings longer than the buffer fall back to per-char putchar so
* we never silently truncate.
*/
#include <stdio.h>
#include <stdint.h>
#define PUTS_BUF_SIZE 256 /* body bytes before CR expansion */
static char puts_buf[PUTS_BUF_SIZE + 3]; /* +3 for trailing CR LF NUL */
static void pchars(const char *s) __naked
{
(void)s;
@@ -37,33 +31,26 @@ static void pchars(const char *s) __naked
__endasm;
}
int puts(const char *s)
char puts(const char *s) __naked
{
uint16_t n = 0;
uint16_t i = 0;
while (s[i] && n < PUTS_BUF_SIZE - 1) {
char c = s[i++];
if (c == '\n') {
puts_buf[n++] = '\r';
puts_buf[n++] = '\n';
} else {
puts_buf[n++] = c;
}
}
if (s[i]) {
/* Overflow — char-by-char fallback so we never truncate. */
for (uint16_t k = 0; s[k]; k++)
putchar((unsigned char)s[k]);
putchar('\n');
return 0;
}
puts_buf[n++] = '\r';
puts_buf[n++] = '\n';
puts_buf[n] = 0;
pchars(puts_buf);
return 0;
(void)s;
__asm
puts_:
ld a, (hl)
or a
jr z, fin_
push hl
ld l, a
ld h, #0
call _putchar
pop hl
inc hl
jp puts_
;
fin_:
ld l, #0x0A
ld h, #0
call _putchar
ret
__endasm;
}
-27
View File
@@ -1,27 +0,0 @@
/*
* solid_helpers.c — small Solid-C compatibility helpers.
*
* dec8 / dec16 / dec32 / hex8 / hex16 / hex32 are in dec_hex.c (compact
* asm port from solid-c's STDLIB.ASM, ~150 bytes total — vs ~3-5 KB if
* routed through printf). This file now only holds gets().
*/
#include <stdio.h>
/* ---- gets — dangerous but Solid-C provides it ---------------------- */
char *gets(char *buf)
{
int i = 0;
int c;
for (;;) {
c = getchar();
if (c == EOF) {
if (i == 0) return 0;
break;
}
if (c == '\n' || c == '\r') break;
buf[i++] = (char)c;
}
buf[i] = 0;
return buf;
}
+23
View File
@@ -0,0 +1,23 @@
/*
* solid_compat.c — Solid-C compatibility helpers that need real code
* (rather than just header macros).
*
* CP866 Cyrillic support: strlwr/strupr handle uppercase/lowercase
* conversion for both Latin and Cyrillic characters in CP866 code page
* (bytes 0x800xFF).
*/
#include <sprinter_compat.h>
#include <ctype.h>
char *strlwr(char *s)
{
char *p = s;
while (*p) {
if ((*p >= 'A' && *p <= 'Z') || (*p >= 0x80 && *p <= 0x8F)) *p += 'a' - 'A';
else if ((*p >= 0x90 && *p <= 0x9F)) *p += 0x50;
else if ((*p == 0xF0)) *p = 0xF1;
p++;
}
return s;
}
+23
View File
@@ -0,0 +1,23 @@
/*
* solid_compat.c — Solid-C compatibility helpers that need real code
* (rather than just header macros).
*
* CP866 Cyrillic support: strlwr/strupr handle uppercase/lowercase
* conversion for both Latin and Cyrillic characters in CP866 code page
* (bytes 0x800xFF).
*/
#include <sprinter_compat.h>
#include <ctype.h>
char *strupr(char *s)
{
char *p = s;
while (*p) {
if ((*p >= 'a' && *p <= 'z') || (*p >= 0xA0 && *p <= 0xAF)) *p -= 'a' - 'A';
else if ((*p >= 0xE0 && *p <= 0xEF)) *p -= 0x50;
else if ((*p == 0xF1)) *p = 0xF0;
p++;
}
return s;
}
View File