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
-289
View File
@@ -1,289 +0,0 @@
/*
* file.c — *** PROVISIONAL *** minimal unbuffered FILE * implementation
* on top of POSIX-style fd I/O
* (open/read/write/lseek/close).
*
* ============================================================
* TODO (v2): replace with a proper BUFFERED implementation.
* See docs/TODO.md / Solid-C reference layout:
*
* typedef struct {
* uint flags; // +0..1 file status flags
* int level; // +2..3 empty/fill level of buffer
* char *curp; // +4..5 current active pointer
* int fd; // +6..7 underlying low-level fd
* char *buffer; // +8..9 data transfer buffer
* char hold; // +10 ungetc byte if no buffer
* short token; // +11..12 reserved
* char dummy; // +13 reserved
* } FILE;
*
* The current implementation maps each fputc/fgetc to one read/write
* syscall — fine for correctness checks, awful for throughput. Issues
* 3/4/5 from the stdio-review (fwrite short-write flag, fgets n=1,
* mode_to_flags break) are deferred until that rewrite.
* ============================================================
*
* stdin/stdout/stderr are STATIC sentinel FILEs flagged with
* _F_CONIN/_F_CONOUT; fputc/fgetc detect them and call putchar() /
* getchar() which already handle CR/LF translation and ESTEX calls.
*
* Their fd fields are 0 / -1 / -2. Negative values were chosen because
* ESTEX OPEN can return small positive fds (1, 2, …) for ordinary
* files — if we marked stdout/stderr with fd=1/2 a real file could
* collide with their identifier. fd=0 for stdin is kept (POSIX-style)
* because ESTEX does not return 0. Even so, none of these fd fields
* is ever passed to a syscall — the _F_CONIN/_F_CONOUT flags drive
* the dispatch.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
/* ---- console pseudo-streams ----------------------------------------*/
static FILE _stdin = { 0, _F_READ | _F_CONIN };
static FILE _stdout = { -1, _F_WRITE | _F_CONOUT };
static FILE _stderr = { -2, _F_WRITE | _F_CONOUT };
FILE *const stdin = &_stdin;
FILE *const stdout = &_stdout;
FILE *const stderr = &_stderr;
/* ---- fopen / fclose -------------------------------------------------*/
/* Translate a fopen() mode string to the open() flags subset our
* libc/io/open.c understands. Supported: r, w, a, with optional "+"
* and trailing "b" (binary — we ignore as all I/O is binary).
*/
static int mode_to_flags(const char *mode, uint8_t *file_flags)
{
if (!mode || !*mode) {
errno = EINVAL;
return -1;
}
int oflags = 0;
uint8_t ff = 0;
char base = *mode;
int plus = 0;
for (const char *p = mode + 1; *p; p++) {
if (*p == '+') plus = 1;
/* 'b' and 't' are ignored — all I/O is binary on Sprinter. */
}
switch (base) {
case 'r':
oflags = plus ? O_RDWR : O_RDONLY;
ff = _F_READ | (plus ? _F_WRITE : 0);
break;
case 'w':
oflags = (plus ? O_RDWR : O_WRONLY) | O_CREAT | O_TRUNC;
ff = _F_WRITE | (plus ? _F_READ : 0);
break;
case 'a':
oflags = (plus ? O_RDWR : O_WRONLY) | O_CREAT | O_APPEND;
ff = _F_WRITE | _F_APPEND | (plus ? _F_READ : 0);
break;
default:
errno = EINVAL;
return -1;
}
*file_flags = ff;
return oflags;
}
FILE *fopen(const char *path, const char *mode)
{
uint8_t ff;
int oflags = mode_to_flags(mode, &ff);
if (oflags < 0) return NULL;
int fd = open(path, oflags);
if (fd < 0) return NULL;
FILE *fp = (FILE *)malloc(sizeof(FILE));
if (!fp) {
int saved = errno;
close(fd);
errno = saved ? saved : ENOMEM;
return NULL;
}
fp->fd = fd;
fp->flags = ff;
return fp;
}
int fclose(FILE *fp)
{
if (!fp) {
errno = EBADF;
return EOF;
}
/* Don't close stdin/stdout/stderr. */
if (fp == &_stdin || fp == &_stdout || fp == &_stderr) {
return 0;
}
int r = close(fp->fd);
free(fp);
return r < 0 ? EOF : 0;
}
int fflush(FILE *fp)
{
/* Unbuffered — nothing to flush. */
(void)fp;
return 0;
}
/* ---- char-at-a-time -------------------------------------------------*/
int fputc(int c, FILE *fp)
{
if (!fp) { errno = EBADF; return EOF; }
if (fp->flags & _F_CONOUT) {
return putchar(c);
}
if (!(fp->flags & _F_WRITE)) { errno = EBADF; return EOF; }
uint8_t ch = (uint8_t)c;
if (write(fp->fd, &ch, 1) != 1) {
fp->flags |= _F_ERROR;
return EOF;
}
return (int)ch;
}
int fgetc(FILE *fp)
{
if (!fp) { errno = EBADF; return EOF; }
if (fp->flags & _F_CONIN) {
return getchar();
}
if (!(fp->flags & _F_READ)) { errno = EBADF; return EOF; }
uint8_t ch;
int r = read(fp->fd, &ch, 1);
if (r == 0) { fp->flags |= _F_EOF; return EOF; }
if (r < 0) { fp->flags |= _F_ERROR; return EOF; }
return (int)ch;
}
/* ---- string-at-a-time ----------------------------------------------*/
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;
}
return 0;
}
if (!(fp->flags & _F_WRITE)) { errno = EBADF; return EOF; }
size_t n = strlen(s);
int w = write(fp->fd, s, (uint16_t)n);
if (w < 0 || (size_t)w != n) {
fp->flags |= _F_ERROR;
return EOF;
}
return 0;
}
char *fgets(char *buf, int n, FILE *fp)
{
if (!buf || n < 2 || !fp) return NULL;
int i = 0;
while (i < n - 1) {
int c = fgetc(fp);
if (c == EOF) {
if (i == 0) return NULL;
break;
}
buf[i++] = (char)c;
if (c == '\n') break;
}
buf[i] = '\0';
return buf;
}
/* ---- block-at-a-time -----------------------------------------------*/
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *fp)
{
if (!ptr || !fp) return 0;
if (size == 0 || nmemb == 0) return 0;
if (fp->flags & _F_CONIN) {
/* line-buffered console read — not very useful but functional. */
char *p = (char *)ptr;
size_t total = size * nmemb;
for (size_t i = 0; i < total; i++) {
int c = getchar();
if (c == EOF) return i / size;
p[i] = (char)c;
}
return nmemb;
}
if (!(fp->flags & _F_READ)) { errno = EBADF; return 0; }
size_t total = size * nmemb;
int r = read(fp->fd, ptr, (uint16_t)total);
if (r < 0) { fp->flags |= _F_ERROR; return 0; }
if ((size_t)r < total) fp->flags |= _F_EOF;
return (size_t)r / size;
}
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *fp)
{
if (!ptr || !fp) return 0;
if (size == 0 || nmemb == 0) return 0;
if (fp->flags & _F_CONOUT) {
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;
}
return nmemb;
}
if (!(fp->flags & _F_WRITE)) { errno = EBADF; return 0; }
size_t total = size * nmemb;
int w = write(fp->fd, ptr, (uint16_t)total);
if (w < 0) { fp->flags |= _F_ERROR; return 0; }
return (size_t)w / size;
}
/* ---- positioning ---------------------------------------------------*/
int fseek(FILE *fp, long off, int whence)
{
if (!fp || (fp->flags & (_F_CONIN | _F_CONOUT))) {
errno = EBADF;
return -1;
}
long r = lseek(fp->fd, off, whence);
if (r < 0) return -1;
fp->flags &= (uint8_t)~_F_EOF;
return 0;
}
long ftell(FILE *fp)
{
if (!fp || (fp->flags & (_F_CONIN | _F_CONOUT))) {
errno = EBADF;
return -1L;
}
return lseek(fp->fd, 0L, SEEK_CUR);
}
void rewind(FILE *fp)
{
if (!fp) return;
fseek(fp, 0L, SEEK_SET);
fp->flags &= (uint8_t)~(_F_EOF | _F_ERROR);
}
/* ---- status --------------------------------------------------------*/
int feof (FILE *fp) { return fp && (fp->flags & _F_EOF) ? 1 : 0; }
int ferror(FILE *fp) { return fp && (fp->flags & _F_ERROR) ? 1 : 0; }
void clearerr(FILE *fp) {
if (fp) fp->flags &= (uint8_t)~(_F_EOF | _F_ERROR);
}
-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;
}