mdview2: статус-бар в отдельный модуль, упростить alloc_set_storage
- mdview2_status.c: вынести статус-бар/меню/спиннер из ядра - mdview2.c: убрать retry-цикл в alloc_set_storage (fail-fast вместо ложной устойчивости — при нехватке EMM под индекс контент тоже не влезет) - mdview2.h: дополнить экспортами статус-модуля - mdview2_md.c / mdview2_raw.c: зачистка после расщепления - mdview/mdview.c: переименовать scroll_* → md_scroll_* (симметрия) - docs/fast_ram.md, docs/turboc.txt: добавить справочные доки - examples/mdview2/README.MD, READMEBG.MD: обновить описание Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -8,7 +8,7 @@
|
||||
|
||||
PROJ_ROOT := $(abspath $(CURDIR)/../..)
|
||||
EXAMPLE := mdview2
|
||||
EXTRA_SRCS := mdview2_raw.c mdview2_help.c mdview2_table.c mdview2_enc.c mdview2_md.c mdview2_index.c
|
||||
EXTRA_SRCS := mdview2_raw.c mdview2_help.c mdview2_table.c mdview2_enc.c mdview2_md.c mdview2_status.c mdview2_index.c
|
||||
MEMORY := small
|
||||
|
||||
include $(PROJ_ROOT)/app.mk
|
||||
|
||||
@@ -112,10 +112,51 @@ int main(void) {
|
||||
- **Кодировки:** CP866 / CP1251 / KOI8-R / UTF-8 (автоопределение, `F8`)
|
||||
- **Максимальный размер файла:** 128 КБ
|
||||
- **Максимальное число строк в индексе:** 16 384
|
||||
- **Максимальная длина строки в рендер-кэше:** 255 ячеек (см. ограничение ниже)
|
||||
- **Режим памяти:** small
|
||||
- Код программы, cтек, данные, куча — окнa W1-W2 (32 КБ, адреса 0x4000–0xBFFF).
|
||||
- Буфер файла — страницы EMM, отображаемые в W3 (0xC000–0xFFFF)
|
||||
|
||||
## Известные ограничения
|
||||
|
||||
### Длина строки в MD-режиме — 255 ячеек
|
||||
|
||||
Рендер-кэш хранит каждую логическую строку как **не более 255 пар (символ, атрибут)**
|
||||
— константа `MAX_CACHE_LINE_LEN`. Лимит задан типами: `g_ncells` и
|
||||
`cache_rec_t.len` — `uint8_t`. Касается всех строк, но заметнее всего на
|
||||
**горизонтально скроллируемых** строках (блоки кода и строки таблиц, флаг
|
||||
`IF_HSCROLL`), которые в MD-режиме можно листать вправо.
|
||||
|
||||
**Что происходит с более длинной строкой:** при индексации `gc_put()` молча
|
||||
отбрасывает каждую ячейку после 255-й (`if (g_ncells < MAX_CACHE_LINE_LEN)`).
|
||||
В кэш попадают только первые 255 ячеек, остаток **теряется** — до него нельзя
|
||||
доскроллить и **нет маркера обрезки** на 255-й позиции (маркер `>` означает лишь
|
||||
«есть ещё в пределах кэша»). Переполнения буфера нет — `gc_put` проверяет границу.
|
||||
|
||||
Важно: «255 ячеек» — это **отрендеренная ширина**, не байты исходника. Табы в
|
||||
коде разворачиваются в пробелы (до `TAB_STOP`), а ячейки таблицы добиваются
|
||||
пробелами до ширины колонки + рамки `│` — поэтому кап достигается раньше, чем
|
||||
255 «полезных» символов.
|
||||
|
||||
> **RAW-режим (`F2`) этого лимита не имеет** — он рисует прямо из файла
|
||||
> побайтово, длинные строки видны целиком (через wrap `F3` или гориз. скролл).
|
||||
|
||||
**Идея снятия лимита** (оценка, не реализовано) — расширить длину до `uint16_t`:
|
||||
- `cache_rec_t.len` `uint8_t→uint16_t` — структура остаётся **ровно 8 байт**
|
||||
(len съедает один pad-байт), адресация `idx<<3` не меняется. Бесплатно.
|
||||
- `g_ncells` / `g_ncells_at_space` → `uint16_t` — главная цена по **коду/скорости**:
|
||||
16-битная арифметика на Z80 в горячем `gc_put` (вызов на каждую ячейку) и в
|
||||
scan-циклах. Ориентир: **+0.2…0.4 КБ кода** + замедление индексации.
|
||||
- Буфер `g_cells[MAX_CACHE_LINE_LEN*2]` в near-RAM (W2) — главная цена по
|
||||
**памяти**: `2 × кап` байт. Сейчас 510 Б; кап 512 → +0.5 КБ, 1024 → +1.5 КБ.
|
||||
EMM-кэш контента (1 МБ) длинные строки тянет легко — узкое место именно near.
|
||||
- `viewport_x` (и копия в `docset_t`) + потолок `max_vx` (сейчас 248) → `uint16_t`,
|
||||
иначе хранить >255 можно, а доскроллить нельзя. Плюс `widths[]`/`ccx` в таблицах/коде.
|
||||
|
||||
Реалистичный компромисс — кап 512–1023: хватит почти всем листингам/таблицам,
|
||||
цена ~+0.3 КБ кода и +0.5…1.5 КБ near-RAM. **Дешёвая полумера без `uint16_t`** —
|
||||
ставить честный маркер обрезки на 255-й позиции, чтобы потеря была видна.
|
||||
|
||||
## TODO
|
||||
|
||||
1. **Увеличение размера документов.** Снять лимит 128 КБ: Достаточно
|
||||
|
||||
@@ -0,0 +1,799 @@
|
||||
# MDView — Просмотрщик Markdown для Sprinter
|
||||
|
||||
**MDView** — программа для просмотра документов в формате *Markdown* на компьютере
|
||||
Sprinter (процессор Z80). Документ хранится в отдельном W3 окне и не занимает
|
||||
основную RAM программы.
|
||||
|
||||
## Возможности
|
||||
|
||||
- Документы до **128 КБ** (8 страниц EMM по 16 КБ каждая)
|
||||
- До **16 384** экранных строк в индексе
|
||||
- Автоматический перенос слов по ширине экрана (80 столбцов)
|
||||
- Горизонтальный сдвиг для широких строк (блоки кода, таблицы)
|
||||
- Статус-бар: имя файла, диапазон строк, процент прокрутки
|
||||
- Спиннер в строке состояния во время загрузки и индексации
|
||||
- Поддержка «мягкого» склеивания строк в абзацах и цитатах
|
||||
|
||||
## Запуск
|
||||
|
||||
```
|
||||
mdview [имя_файла.md]
|
||||
```
|
||||
|
||||
Если имя файла не задано, загружается `README.MD`.
|
||||
|
||||
## Управление
|
||||
|
||||
```
|
||||
Клавиша Действие
|
||||
───────────── ────────────────────────────────────────
|
||||
Up Down Прокрутка на одну строку вверх / вниз
|
||||
PgUp PgDn Прокрутка на страницу (30 строк)
|
||||
Home Начало документа
|
||||
End Конец документа
|
||||
Left Right Горизонтальный сдвиг (только nowrap-строки)
|
||||
F1 Окно справки
|
||||
F8 Кодировка: CP866 → CP1251 → KOI8-R → UTF-8 → ...
|
||||
F10 / Esc Выход из программы
|
||||
```
|
||||
|
||||
Кодировка определяется автоматически при открытии (BOM + эвристика по
|
||||
первым 4 КБ); `F8` переключает её вручную, если детекция ошиблась.
|
||||
8-битные кодировки (CP866/CP1251/KOI8-R) переключаются мгновенно (ремап на
|
||||
отрисовке). Второй набор индекс/кэша (например UTF-8) строится лениво — при
|
||||
первом переключении в него (короткая пауза со спиннером), дальше мгновенно.
|
||||
|
||||
## Синтаксис Markdown
|
||||
|
||||
### Заголовки
|
||||
|
||||
Поддерживаются уровни H1–H4. Уровни H5 и H6 отображаются как H4.
|
||||
|
||||
# Заголовок первого уровня
|
||||
## Заголовок второго уровня
|
||||
### Заголовок третьего уровня
|
||||
#### Заголовок четвёртого уровня
|
||||
|
||||
### Текстовое форматирование
|
||||
|
||||
**Жирный текст** выделяется двойными звёздочками: `**текст**`
|
||||
|
||||
*Курсив* выделяется одиночными звёздочками `*текст*` или знаком подчёркивания `_текст_`
|
||||
|
||||
`Встроенный код` обозначается обратными кавычками
|
||||
|
||||
~~Зачёркнутый текст~~ — двойные тильды: `~~текст~~`
|
||||
|
||||
### Ненумерованный список
|
||||
|
||||
Маркеры `-`, `*` или `+`:
|
||||
|
||||
- Первый пункт списка
|
||||
- Второй пункт списка
|
||||
- Третий пункт с достаточно длинным текстом, который при необходимости
|
||||
будет перенесён на следующую строку с сохранением отступа
|
||||
|
||||
### Нумерованный список
|
||||
|
||||
1. Первый элемент
|
||||
2. Второй элемент
|
||||
3. Третий элемент
|
||||
|
||||
### Цитата
|
||||
|
||||
> Блок цитаты начинается с символа `>`. Несколько последовательных
|
||||
> строк одной цитаты склеиваются в единый абзац с автоматическим
|
||||
> переносом слов.
|
||||
|
||||
### Блок кода (verbatim)
|
||||
|
||||
Блок кода заключается в тройные обратные кавычки. Внутри блока
|
||||
текст отображается «как есть» без разбора Markdown:
|
||||
|
||||
```
|
||||
#include <stdio.h>
|
||||
#include <sprinter.h>
|
||||
|
||||
int main(void) {
|
||||
puts("Hello, Sprinter!");
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
### Горизонтальная линия
|
||||
|
||||
Три или более символов `---`, `***` или `___` на отдельной строке:
|
||||
|
||||
---
|
||||
|
||||
## Технические характеристики
|
||||
|
||||
- **Платформа:** Sprinter, процессор Z80 @ 21 МГц
|
||||
- **Кодировки:** CP866 / CP1251 / KOI8-R / UTF-8 (автоопределение, `F8`)
|
||||
- **Максимальный размер файла:** 128 КБ
|
||||
- **Максимальное число строк в индексе:** 16 384
|
||||
- **Режим памяти:** small
|
||||
- Код программы, cтек, данные, куча — окнa W1-W2 (32 КБ, адреса 0x4000–0xBFFF).
|
||||
- Буфер файла — страницы EMM, отображаемые в W3 (0xC000–0xFFFF)
|
||||
|
||||
## TODO
|
||||
|
||||
1. **Увеличение размера документов.** Снять лимит 128 КБ: Достаточно
|
||||
разрешить работать с большим кол-вом страниц памяти, пока оттестированно
|
||||
на работе с 8-мю страницами по 16Кб.
|
||||
|
||||
Сделано: форматированные таблицы с рамкой; поддержка кодировок
|
||||
CP866 / CP1251 / KOI8-R / UTF-8 с автоопределением и переключением по `F8`
|
||||
(второй набор строится лениво, по первому переключению).
|
||||
|
||||
4. **Ускорение рендеринга.** Кэш строк экрана. Оптимизация цикла вывода
|
||||
символов через BIOS WRCHAR (пакетный вывод, DMA).
|
||||
|
||||
---
|
||||
|
||||
*MDView v0.2 · (c) 2026 Петров А.Г.*
|
||||
|
||||
|
||||
---
|
||||
|
||||
# Sprinter C Compiler — v1.0
|
||||
|
||||
C toolchain for **Sprinter** — the Z80-based home computer by Peters Plus, running
|
||||
ESTEX DSS. Host: macOS / Linux. Target: `.EXE` files in SprintEXE format.
|
||||
|
||||
Built on top of **SDCC 4.5** (vendored in `third_party/sdcc/`). This repository adds
|
||||
everything Sprinter-specific: crt0, linker integration, libc wrappers over ESTEX,
|
||||
banked-call trampolines, graphics & accelerator API, mouse driver wrappers, and the
|
||||
`mkexe` utility for producing SprintEXE images.
|
||||
|
||||
## What you get
|
||||
|
||||
* **`bin/sprinter-cc`** — one-line driver: `sprinter-cc -o foo.exe foo.c`
|
||||
* **Memory modes**: `tiny`, `small`, `big`, `huge`, `manual` — see below.
|
||||
* **stdio + conio**: printf, puts, putchar, getchar, fopen/fread/..., cprintf, cputs, putch, textcolor/textbackground/textattr, gotoxy, kbhit/getch.
|
||||
* **Graphics**: 320×256×256 and 640×256×16 modes, accelerator-backed primitives (hline / vline / rect / fill_rect / line via Bresenham, plus clear), bitmap-font text in both modes via BIOS character generator.
|
||||
* **File I/O**: POSIX (`open`/`read`/`write`/`close`/`lseek`/`unlink`/`creat`), FILE\* streams (`fopen`/`fgets`/`fwrite`/...), directory listing (`ffirst`/`fnext`), `chdir`/`getcwd`/`mkdir`/`rmdir`, `stat`/`fstat`.
|
||||
* **Memory**: 32 KB heap (W2-resident), banking-aware page allocator (`mem_alloc_pages`/`bank_read`/`bank_write`), explicit memory modes for sub-16 KB programs.
|
||||
* **Mouse**: full Sprinter driver wrapper (14 functions including custom cursor bitmaps).
|
||||
* **Environment**: `getenv`/`putenv`/`sysenv` over ESTEX `$46`.
|
||||
* **Time**: `getdatetime`/`setdatetime` + POSIX `time`/`localtime`/`mktime`/`asctime`/`ctime`.
|
||||
* **Misc**: `errno`/`strerror`/`perror`, `atexit`, `setjmp`/`longjmp`, `sleep`, full argv parsing in crt0.
|
||||
|
||||
## Quick start
|
||||
|
||||
```sh
|
||||
git clone <this repo> sprinter-c
|
||||
cd sprinter-c
|
||||
make sdcc # one-time: fetch SDCC 4.5 binary (~25 MB)
|
||||
make all # build mkexe + libsprinter.lib + 27 examples
|
||||
make floppy # pack everything into mame/v306/IMG/mc.img
|
||||
cd mame/v306 && ./run_mame.sh # boot Sprinter in MAME
|
||||
```
|
||||
|
||||
Compile a single program:
|
||||
|
||||
```sh
|
||||
cat > hello.c <<EOF
|
||||
#include <stdio.h>
|
||||
int main(void) { puts("Hello, Sprinter!"); return 0; }
|
||||
EOF
|
||||
|
||||
bin/sprinter-cc -o hello.exe hello.c
|
||||
```
|
||||
|
||||
That's it — `hello.exe` is now a valid SprintEXE you can `RUN HELLO` from the ESTEX shell.
|
||||
|
||||
## Memory modes
|
||||
|
||||
Sprinter's address space is four 16 KB windows (W0 / W1 / W2 / W3). DSS allocates
|
||||
pages by program size — small programs get only one page. Pick a memory mode based
|
||||
on what your program needs:
|
||||
|
||||
| Mode | Code lives in | Banking | Use when | Note |
|
||||
|---|---|---|---|---|
|
||||
| `tiny` (default) | W2 (0x8100+) | no | code+data < 14 KB | |
|
||||
| `small` | W1-W2 (0x4100+) | no | code+data < 30 KB | |
|
||||
| `big` | W2 + W1 banking | yes (W1) | tiny + extra code modules | |
|
||||
| `huge` | W1-W2 + W3 banking | yes (W3) | small + extra code modules | |
|
||||
| `manual` | user-specified | optional | special layouts | Not implemented |
|
||||
|
||||
```sh
|
||||
sprinter-cc --memory small -o big.exe bigprog.c
|
||||
sprinter-cc --memory huge -o app.exe main.c --bank 1=engine.c --bank 2=ai.c
|
||||
```
|
||||
|
||||
Banked functions are declared with `__banked`:
|
||||
|
||||
```c
|
||||
void engine_tick(int dt) __banked; // lives in BANK1, automatically swapped
|
||||
```
|
||||
|
||||
## Examples (27 total)
|
||||
|
||||
| Example | What it demonstrates |
|
||||
|---|---|
|
||||
| `hello` | Hello world with stdio + conio Turbo-C-style colors |
|
||||
| `argv` | argv parsing in crt0 |
|
||||
| `cat` | File I/O — read & print TEST.TXT |
|
||||
| `seek` | 32-bit lseek over a 100 KB file |
|
||||
| `ls` | Directory listing via ffirst/fnext |
|
||||
| `filetest` | FILE\* streams (fopen/fread/...) |
|
||||
| `stattest` | `stat`/`fstat` on files and directories |
|
||||
| `errno` | errno / strerror / perror |
|
||||
| `mem_test` | Page allocator + bank\_read/bank\_write |
|
||||
| `malloc` | Heap stress test (200+ allocations) |
|
||||
| `banked` | Banked code in W3 (huge mode) |
|
||||
| `bankedbg` | Banked code in W1 (big mode) |
|
||||
| `banklocl` | Bank-local static data and BSS |
|
||||
| `mouse` | Mouse driver in text mode |
|
||||
| `gfx_mous` | Mouse with custom bitmap cursor in graphics mode |
|
||||
| `gfx_demo` | 320×256×256 lines / rects / accelerator |
|
||||
| `gfx_d16` | 640×256×16 same primitives |
|
||||
| `gfx_text` | Bitmap-font text on graphics screen |
|
||||
| `timedir` | Date/time + directory listing |
|
||||
| `ptime` | POSIX time API |
|
||||
| `openenv` | open() flags + environment vars |
|
||||
| `conio` | conio API smoke test |
|
||||
| `attrprob` | Probe Sprinter text attribute byte layout |
|
||||
| `strtest` | string.h test (from SDCC's z80.lib) |
|
||||
| `stdlib` | stdlib.h test (qsort / rand / strtol / etc.) |
|
||||
| `assrtest` | assert() |
|
||||
| `rt_test` | Runtime helpers (sleep, setjmp, atexit) |
|
||||
|
||||
## Headers
|
||||
|
||||
Standard:
|
||||
* `<stdio.h>` — puts / printf / FILE\* + Sprinter-specific dec/hex helpers
|
||||
* `<stdlib.h>` — atoi / atof / malloc / qsort / ... (from SDCC z80.lib)
|
||||
* `<string.h>` / `<ctype.h>` / `<math.h>` — from SDCC z80.lib
|
||||
* `<unistd.h>` — read / write / close / lseek / unlink
|
||||
* `<fcntl.h>` — open / creat + O\_RDONLY / O\_CREAT / ...
|
||||
* `<errno.h>` — errno + error names + strerror
|
||||
* `<sys/stat.h>` — stat / fstat
|
||||
* `<setjmp.h>` / `<assert.h>` — from SDCC
|
||||
|
||||
Sprinter-specific:
|
||||
* `<conio.h>` — putch / cputs / cprintf, textcolor / textbackground / textattr, kbhit / getch, clrscr, gotoxy, wherex/y
|
||||
* `<gfx.h>` — gfx_init/done, palette, putpixel, hline/vline/rect/fill_rect/line, text — for both 320 and 640 modes (gfx_\*16 variants)
|
||||
* `<mouse.h>` — full 14-function driver wrapper + mouse_cursor_t with bitmap support
|
||||
* `<dir.h>` — chdir / getcwd / mkdir / rmdir / ffirst / fnext / ffblk
|
||||
* `<time.h>` — getdatetime / setdatetime + POSIX time / localtime / etc.
|
||||
* `<sprinter.h>` — raw ports, ESTEX/BIOS function numbers, env API
|
||||
* `<sprinter_exit.h>` — exit / \_exit / atexit
|
||||
* `<sprinter_mem.h>` — mem\_alloc\_pages / mem\_free\_block / bank\_read / bank\_write
|
||||
* `<sprinter_compat.h>` — Solid-C compatibility layer (aliases + BOOL/WORD/uint types)
|
||||
|
||||
## Toolchain commands
|
||||
|
||||
```sh
|
||||
make all # build mkexe + lib + every example
|
||||
make floppy # repack mame/v306/IMG/mc.img with all .exe files
|
||||
make check # 17 mkexe unit-tests
|
||||
make clean # remove all build artefacts
|
||||
make sdcc # one-time: fetch SDCC 4.5 binary
|
||||
```
|
||||
|
||||
### sprinter-cc options
|
||||
|
||||
```
|
||||
sprinter-cc -o foo.exe foo.c [more.c ...] [options]
|
||||
|
||||
--memory MODE tiny | small | big | huge | manual (default: tiny)
|
||||
--memory-manual SPEC explicit placement (CODE=W1|W2,DATA=W1|W2|SAME,BANKED=W1|W3)
|
||||
--stack-size N bytes reserved for the stack (default ~1278)
|
||||
--crt0=TYPE default | minimal | banked | small
|
||||
--bank N=FILE.c compile FILE.c into bank N (repeatable, max 15)
|
||||
--debug enable runtime diagnostics (defines DEBUG_RT)
|
||||
-I PATH extra include path
|
||||
-L 0xADDR / -E / -S override load / entry / stack addresses
|
||||
-Wl FLAG pass FLAG to sdldz80
|
||||
--mkexe FLAG pass FLAG to mkexe (e.g. --mkexe -p --mkexe 0 for bank padding)
|
||||
-v verbose
|
||||
```
|
||||
|
||||
## Status
|
||||
|
||||
What works in v1.0:
|
||||
* Compile / link / pack to SprintEXE — verified on all 27 examples
|
||||
* Four memory modes (tiny / small / big / huge)
|
||||
* Graphics (both modes) with accelerator
|
||||
* Mouse (text + graphics cursor)
|
||||
* File I/O, directories, environment, time
|
||||
* All headers listed above
|
||||
|
||||
Deferred to v2.0 (see `docs/TODO.md`):
|
||||
* **Turbo-C-style BGI graphics API** — `initgraph` / `setcolor` / `circle` /
|
||||
`getimage` / `putimage` / etc. on top of our `gfx_*` primitives
|
||||
* Remaining Solid-C compatibility gaps (Phase 2/3) — see `docs/solid_c_compatibility.md`
|
||||
* Manual memory mode
|
||||
* Rewrite FILE\* stream API (current implementation is very primitive and doesn't use buffers)
|
||||
|
||||
Deferred to v3.0:
|
||||
* **IM2 interrupt handlers** — research complete (`docs/im2_isr_design.md`),
|
||||
implementation scheduled for v3
|
||||
* **Audio API** (AY-3-8910 + COVOX) — requires IM2
|
||||
* **ISA-8 slot drivers** — requires IM2 (???)
|
||||
|
||||
## Documentation
|
||||
|
||||
* `docs/TODO.md` — roadmap and open work items
|
||||
* `docs/solid_c_compatibility.md` — gap analysis vs Solid-C 2004
|
||||
* `docs/im2_isr_design.md` — interrupt handler design (v2)
|
||||
* `docs/converted/` — source documentation (ESTEX, BIOS, architecture)
|
||||
converted to plain text for `grep`
|
||||
* `docs/reference/`, `docs/samples/`, `docs/memory management/` — original
|
||||
Russian docs and code samples from Peters Plus
|
||||
|
||||
## Repository layout
|
||||
|
||||
```
|
||||
bin/sprinter-cc one-line compiler driver (bash)
|
||||
toolchain/mkexe/ host-side tool: .ihx -> .exe SprintEXE
|
||||
toolchain/check_banks.py post-link bank size enforcer
|
||||
runtime/ crt0 variants (default, minimal, small, banked)
|
||||
bank trampolines, heap, heap_top
|
||||
libc/include/ headers
|
||||
libc/io|stdio|mem|gfx/ C and asm sources for libsprinter.lib
|
||||
lib/ Makefile that archives libsprinter.lib via sdar
|
||||
examples/ 27 example programs
|
||||
mame/v306/ MAME binary + Sprinter ROM/HDD images + floppy script
|
||||
third_party/sdcc/ vendored SDCC 4.5 (fetched via `make sdcc`)
|
||||
third_party/solid-c/ reference: original Sprinter native C (for compat target)
|
||||
docs/ documentation
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
This repository contains:
|
||||
* Original code in `bin/`, `toolchain/`, `runtime/`, `libc/`, `lib/`, `examples/` —
|
||||
MIT-licensed.
|
||||
* `third_party/sdcc/` — SDCC 4.5 under GPLv2 with linking exception
|
||||
(see `third_party/sdcc/COPYING.txt`)
|
||||
* `third_party/solid-c/` — original Sprinter Solid C, used only as a reference
|
||||
|
||||
## Credits
|
||||
|
||||
* **Sprinter / Peters Plus** — Иван Мак, Дмитрий Паринов and the original team
|
||||
* **SDCC** — for the underlying Z80 compiler
|
||||
* **MAME** — for the Sprinter Sp2000 emulation
|
||||
|
||||
---
|
||||
|
||||
For questions / patches: see CONTRIBUTING.md (TBD) or open an issue.
|
||||
|
||||
---
|
||||
|
||||
# TODO / Roadmap
|
||||
|
||||
Открытые задачи в порядке убывания приоритета. По мере появления реальных программ — приоритеты будут смещаться.
|
||||
|
||||
## Этап 5 — malloc / free + banking-aware page allocator ✅ ГОТОВО
|
||||
|
||||
- [x] SDCC's `malloc`/`free` + наш `runtime/heap.s` (полностью заменяет library heap.rel, 14000-байтный heap в окне 2)
|
||||
- [x] `libc/mem/mem_alloc.c` — page allocator: `mem_alloc_pages`/`mem_free_block`/`mem_get_page`/`mem_info` через ESTEX `$3C/$3D/$3E` + BIOS `$C4`
|
||||
- [x] `libc/mem/bank_io.c` — HOME-резидентные `bank_read`/`bank_write`/`bank_load_byte`/`bank_store_byte` со свопом W3 внутри
|
||||
- [x] `examples/malloc_test/` — проверка SDCC's malloc (~210 64-байтных allocations через всю heap)
|
||||
- [x] `examples/mem_test/` — проверка page allocator: 3 страницы, разные паттерны через bank_write, верификация через bank_read
|
||||
|
||||
## Этап 6 — argv parsing + sprinter-cc wrapper ✅ ГОТОВО
|
||||
|
||||
- [x] crt0 парсит ESTEX command-line из IX-prefix (inline asm в `runtime/crt0.s`)
|
||||
- [x] Strip leading CP/M-style space (DSS quirk)
|
||||
- [x] Передача `argc`/`argv` в main() через HL/DE (SDCC __sdcccall(1) ABI)
|
||||
- [x] argv[0] = basename .EXE через ESTEX APPINFO ($47 subfn 2)
|
||||
- [x] `runtime/crt0_minimal.s` — opt-out для очень маленьких программ
|
||||
- [x] `runtime/crt0_banked.s` — теперь тоже парсит argv (parse_argv + get_progname скопированы из crt0.s; будет factored в argv.s когда возьмёмся за libsprinter.lib)
|
||||
- [x] Bash-обёртка `bin/sprinter-cc`: `sprinter-cc -o foo.exe foo.c` одной строкой
|
||||
- [x] Поддержка опций: `--memory`, `--memory-manual`, `--stack-size`, `--crt0=`, `--bank N=FILE.c`, `--debug`, `-I`, `-L`/`-E`/`-S`, `-Wl`, `--mkexe`
|
||||
|
||||
## Этап 8 — графика (320×256×256 + 640×256×16 + accel + bitmap font) ✅ ГОТОВО
|
||||
|
||||
- [x] **8a** Graphics core: `gfx_init`/`gfx_done`/`gfx_clear`/`gfx_putpixel`/`gfx_pal_load`/`gfx_pal_set` (libc/gfx/gfx_core.c). Палитра через BIOS PIC_SET_PAL ($A4). Verified 320×256×256.
|
||||
- [x] **8b** Линии/прямоугольники/fill через accelerator (libc/gfx/gfx_lines.c): `gfx_hline`/`gfx_vline` через accel Fill (LD C,C / LD E,E + SMC block-size), `gfx_rect`/`gfx_fill_rect` с heuristic выбором ориентации (h/v bursts count), `gfx_line` с Bresenham для диагоналей. `gfx_clear` тоже переписан на column-major accel (~4× быстрее).
|
||||
- [x] **8c** 640×256×16 mode (libc/gfx/gfx_16.c): `gfx_*16` API, HIGH nibble = LEFT pixel (документация misleading), per-row RMW для vline (один байт = 2 горизонтальных пикселя).
|
||||
- [x] **8d** Bitmap font + gfx_text (libc/gfx/gfx_text.c): шрифт через BIOS WIN_GET_ZG ($B8), interleaved layout `font[row*256+char]`, `gfx_text`/`gfx_putchar` для 320 mode, `gfx_text16`/`gfx_putchar16` для 640 mode с pair-table lookup.
|
||||
|
||||
См. memory/sprinter_graphics.md, sprinter_accelerator.md, sprinter_graphics_16.md, sprinter_font_format.md.
|
||||
|
||||
Открытые мелочи (не блокируют):
|
||||
- [ ] Шрифт-quad для 640: per-cell палитра (mode 0x82 разрешает 1 из 4 палитр per 16×8 cell) — через прямой доступ к area-описания экрана 0x0300..0x039F
|
||||
|
||||
## Auto-banking (см. `memory/banking_roadmap.md` для деталей)
|
||||
|
||||
Phase 1 — file-level bin-packing — реализовывать когда проект перерастёт ~30 KB кода.
|
||||
|
||||
- [ ] `toolchain/auto_bank.py`:
|
||||
- Парсит размеры из `.rel`-файлов (или из .map после dry-run link'а)
|
||||
- First-fit-decreasing bin-packing
|
||||
- Уважает `#pragma codeseg BANKn` как manual override
|
||||
- Перелинковывает с новыми `-Wl-b_BANKn=...` параметрами
|
||||
- Печатает план распределения
|
||||
|
||||
Phase 2-5: incremental rebalance, declarative `banks.toml`, function-level, call-graph-aware. Только если/когда понадобится.
|
||||
|
||||
## Bank-local static data (mutable data в том же банке что и код) — ✅ ГОТОВО
|
||||
|
||||
- [x] Пример `examples/bank_local_data/` — функция в BANK1 со своим writable BSS array + const table + malloc-тест
|
||||
- [x] `mkexe -p 0` для нулевого padding банков (BSS-storage обнуляется при загрузке)
|
||||
- [x] Канонический рецепт: `--codeseg BANK1 --constseg BANK1 --dataseg BANK1` для bank1.c + `-Wl-b_BANK1=0x1C000` для линковки. **`--dataseg BANK1` РАБОТАЕТ** — раньше казалось обратное из-за trampoline bug который маскировал результат.
|
||||
- [x] **Критичный фикс trampoline'a в runtime/bank.s** — старый `pop af; out (n), a` клобберил A → все banked-функции возвращающие uint8_t тихо возвращали мусор. Новый `pop bc; out (c), b` сохраняет A.
|
||||
- [x] **malloc из banked-функции работает прозрачно** — heap живёт в W2 (HOME), W2 никогда не свапается trampoline'ом, pointer валиден из любого контекста. См. memory/bank_local_data_pattern.md.
|
||||
- [x] Документация в memory: `memory/bank_local_data_pattern.md` (полный рецепт + malloc + nuances), `memory/sdcc_banking.md` (trampoline fix)
|
||||
- [ ] Опционально — расширить `check_banks.py` чтобы показывать разбивку size = code + const + bss per bank (cosmetic)
|
||||
|
||||
Зачем: для модулей с большим private state (level loader, audio engine, scene data). Экономит W2 heap для динамики, а статика остаётся в бэке.
|
||||
|
||||
## Подсказки из solid-c (нативный Sprinter C — `third_party/solid-c/`)
|
||||
|
||||
После анализа solid-c'овской libc (см. `memory/solid_c_findings.md`) выявлены готовые паттерны для следующих недостающих функций. Приоритет от **высокого** к низкому:
|
||||
|
||||
### High-priority gaps (легко портировать, большая польза)
|
||||
- [x] **`errno` + `strerror`/`perror`** — табличка 32 ошибок (libc/io/errno.c)
|
||||
- [x] **Расширенный `open()`** для O_CREAT/O_TRUNC/O_APPEND/O_EXCL state machine
|
||||
- [x] **`atexit`** — 8-callback LIFO + `exit()` + `_exit()` (libc/io/atexit.c)
|
||||
- [x] **`setjmp`/`longjmp`** — 6-байт jmp_buf={sp,ix,pc} (libc/io/setjmp.c)
|
||||
- [x] **`sleep(seconds)`** — 50Hz halt-loop (libc/io/sleep.c)
|
||||
- [x] **ESTEX ENV API** ($46, getenv/putenv) — libc/io/env.c. Учли doc-bug: реально A=0 это NOT FOUND
|
||||
|
||||
### Medium-priority (нужно для shell-like утилит)
|
||||
- [ ] **Mouse driver** — `rst $30h`, 17 функций. **Сначала тест что работает в MAME**.
|
||||
- [x] **`ffirst`/`fnext` + ffblk_t struct** для directory listing — реализовано, demo: ls.exe
|
||||
- [x] **`getdatetime`/`setdatetime`** через ESTEX $21/$22 — libc/io/time.c, demo: time_dir_test
|
||||
- [x] **`chdir`/`getcwd`/`mkdir`/`rmdir`** — wrappers для ESTEX $1B-$1E — libc/io/fsdir.c
|
||||
- [x] **conio: `kbhit`/`getch`/`getche`/`cputs`/`clrscr`/`gotoxy`** — реализовано
|
||||
- [x] **conio extras**: `wherex`/`wherey` ($53), `wrchar`/`rdchar` ($58/$57), `textmode_get/set` ($50/$51), `clrscr_attr` ($56) + COLOR macros
|
||||
|
||||
### Low-priority — ✅ FILE* stack ГОТОВО
|
||||
|
||||
- [x] **Минимальный unbuffered FILE\*** — `libc/stdio/file.c` + `libc/include/stdio.h`. fopen/fclose/fputs/fgets/fread/fwrite/fseek/ftell/rewind/feof/ferror/clearerr/fflush + stdin/stdout/stderr как pseudo-streams. См. `memory/file_star_design.md` и `examples/filetest`.
|
||||
- [ ] fprintf / fscanf — нужна printf-через-callback machinery. Пока пользователь может `sprintf(buf, ...) + fputs(buf, fp)`.
|
||||
- [ ] Опциональный buffered mode (setvbuf, line/block buffering) — если когда-то понадобится.
|
||||
|
||||
### POSIX time API — ✅ ГОТОВО
|
||||
- [x] `libc/io/posix_time.c` — time/localtime/gmtime/mktime/asctime/ctime поверх getdatetime. SDCC's time.rel избегаем (нельзя override _RtcRead). См. `examples/ptime`.
|
||||
|
||||
### sys/stat — ✅ ГОТОВО
|
||||
- [x] `libc/io/stat.c` — POSIX stat/fstat. Гибрид open+fstat для файлов, ffirst+iter для папок (включая "."/".."). См. `examples/stattest` и `memory/estex_ffirst_dotdot.md`.
|
||||
|
||||
### assert — ✅ ГОТОВО (используем SDCC's __assert через fallback include path)
|
||||
|
||||
## libc/stdlib — ✅ не нужно делать (см. memory/sdcc_stdlib_works.md)
|
||||
|
||||
Проверено через `examples/stdlib_test/`: SDCC z80.lib содержит работающие реализации:
|
||||
- `atoi/atol/atof, strtol/strtoul, rand/srand, qsort/bsearch, abs/labs, div/ldiv`
|
||||
- Полный `<string.h>` (memchr/cmp/set/cpy, strcat/cmp/cpy/len/chr/spn/etc.)
|
||||
- `<ctype.h>` (toupper/tolower)
|
||||
- `<math.h>` (sinf/cosf/sqrtf/etc.)
|
||||
|
||||
Линкер автоматически тянет из z80.lib когда нужно. **НЕ переписывать**.
|
||||
|
||||
Наши Sprinter-specific обязательные модули остаются: atexit, env, errno, setjmp, putchar/puts/getchar, conio, fsdir, time, mouse, open/read/lseek/close.
|
||||
|
||||
## Build-system: libsprinter.lib + sprinter-cc — ✅ ГОТОВО
|
||||
|
||||
- [x] `lib/Makefile` — собирает каждый libc/*.c в `.rel`, архивирует через sdar в `lib/sprinter.lib`
|
||||
- [x] Включает runtime/bank.s и runtime/heap.s (auto-pulled при __banked/malloc)
|
||||
- [x] `bin/sprinter-cc` — bash-wrapper: `sprinter-cc -o foo.exe foo.c` одной строкой
|
||||
- [x] Поддержка опций `--crt0=default|minimal|banked`, `--bank N=FILE.c`, `-I`, `-L`/`-E`/`-S`, `-Wl`, `--mkexe`
|
||||
- [x] `examples/hello_sccc/` — демо: `hello.c` собирается за один shell-вызов, размер совпадает с ручным Makefile (925 байт)
|
||||
- [x] Split `putchar.c` → `putchar.c` + `puts.c` для per-function granularity (puts override SDCC's z80.lib version)
|
||||
- [x] Включён в `make all` (зависимость `lib` перед `examples`)
|
||||
|
||||
Возможные улучшения (опционально):
|
||||
- [ ] Мигрировать остальные examples на sprinter-cc вместо ручных Makefile (косметика)
|
||||
- [ ] Дальнейшая декомпозиция libc/*.c per-function (но текущая granularity уже даёт нужный размер — линкер пакетует .rel целиком, и для большинства файлов это одна функция)
|
||||
|
||||
## Этап 9 — memory modes для sprinter-cc
|
||||
|
||||
DSS выделяет страницы памяти по размеру приложения: < 16 KB → одна страница, в остальные окна подключается «страница #FF» (read=0xFF, write игнорится). Из-за этого CODE-в-W1 + DATA-в-W2 для маленькой программы молча ломается. См. [memory/sprinter_memory_modes.md](../../.claude/projects/-Volumes-SAM8-Projects-DIY-Z80-Sprinter-C-Compiler/memory/sprinter_memory_modes.md).
|
||||
|
||||
- [x] **`tiny`**: всё (CODE+DATA+стек) в W2. Default. Verified hello/argv/conio/malloc/file/etc.
|
||||
- [x] **`--memory MODE` флаг в sprinter-cc**: parser + per-mode дефолты CODE_LOC/DATA_LOC, override через явные `--code-loc`/`--data-loc`. tiny работает; small/big/huge компилируются с warning'ом (runtime не готов). Реализовано 2026-05-30.
|
||||
- [x] **`--memory-manual SPEC`**: парсит `CODE=W1|W2,DATA=W1|W2|SAME,BANKED=W1|W3`. Реализовано 2026-05-30.
|
||||
- [x] **`small` runtime**: `runtime/crt0_small.s` использует ESTEX `$3D GETMEM` + `$3A SETWIN2` чтобы выделить и замапить W2-страницу ДО gsinit. **НЕ** BIOS `$C4` — стек на этом этапе в W1 (boot_stack в HOME), а BIOS требует стек в W2. После маппинга SP переключается на 0xBFFE, дальше стандартный flow. Реализовано 2026-05-30, verified hello.exe.
|
||||
- [x] **`small` auto-detect для >16 KB программ**: `crt0_small.s` читает порт `0xC2` (текущая страница в W2 — не `0xA2`! это W1). Если 0xFF — выделяет page; иначе DSS уже сделала это (программа сама вылезла в W2). Один crt0 покрывает 0..30 KB. mkexe также разрешает HOME span W1+W2 (0x4000..0xBFFF). Verified hello: small (5 KB файл, SETWIN2 path) + 32 KB файл (auto-skip). Реализовано 2026-05-30.
|
||||
- [x] **`big` runtime** (tiny + banked code в W1): параметризовали `crt0_banked.s` + `bank.s` через `.ifdef BANK_W1` — другой banking port (0xA2 vs 0xE2), другой load-addr (0x4000 vs 0xC000). sprinter-cc prepend'ит `BANK_W1 = 1` при `--memory big`, передаёт `mkexe -B 0x4000`. Пример `examples/banked_big/`. Реализовано 2026-05-30.
|
||||
- [x] **`huge` runtime** (small + banked code в W3): merge W2-detect логики из `crt0_small.s` в `crt0_banked.s`. Существующий пример `examples/banked/` теперь использует MEMORY=huge. Реализовано 2026-05-30.
|
||||
- [x] **`--debug` флаг**: prepend `DEBUG_RT = 1` в crt0 + `-DDEBUG_RT` в sdcc. Открывает symbol `_w2_self_allocated` (uint8_t) — runtime diagnostic кто аллоцировал W2. Реализовано 2026-05-30.
|
||||
|
||||
### Дизайн-решения по libc и crt0
|
||||
|
||||
**Одна `sprinter.lib`** работает для всех memory mode — `.rel`-члены relocatable, SDLD делает dead-code elimination per-member (без графики не подтягивает `gfx_core.rel` и т.д.). Verified hello vs malloc_test через map-файлы.
|
||||
|
||||
**`gfx.lib` отдельно — НЕ нужен**: dead-code elimination уже работает.
|
||||
|
||||
**`libc_banked` (libc в bank вместо HOME)** — идея на потом, когда HOME (16 KB) забит user-кодом + libc в `huge` mode. Реализуется через `--codeseg BANK0` при компиляции libc; trade-off: trampoline ~30 циклов на каждый libc-вызов. Триггер: реальная программа упрётся в HOME budget.
|
||||
|
||||
**HW-зависимые модули — `sprinter_home.lib` отдельно.** Часть libc физически не может быть забанкована в W3, потому что она РАБОТАЕТ с W3:
|
||||
- `gfx_*` — пишет в видеопамять `0xC000+` после swap W3 на video page
|
||||
- `bank_io` (mem_alloc_pages/bank_read/bank_write) — swap'ит W3 через `OUT (0xE2)`
|
||||
- Будущие ISR — прерывание может прийти когда W3 на чём угодно
|
||||
|
||||
В huge mode эти модули ДОЛЖНЫ остаться в HOME (W1). Когда будем делать `libc_banked`, **одновременно** выделяем `sprinter_home.lib` (HOME-only) из `sprinter.lib` (bankable). Финальная схема:
|
||||
```
|
||||
sprinter_home.lib HOME-only: gfx, bank_io, ISR shims
|
||||
sprinter.lib bankable: printf, malloc, string, conio, stdio, env, ...
|
||||
sprinter_banked.lib тот же sprinter.lib но --codeseg BANK0 (для huge)
|
||||
```
|
||||
Триггер: реализация `--memory huge` runtime.
|
||||
|
||||
**crt0 — по одному на mode:**
|
||||
- `crt0.s` — текущий, для **tiny/big**: SP=0xBFFE, парсит argv (W2-ресурс уже выделен DSS).
|
||||
- `crt0_minimal.s` — текущий, для tiny без argv.
|
||||
- `crt0_small.s` — **новый, step 3**: для **small/huge**, аллоцирует W2 через `mem_alloc_pages` ДО gsinit, маппит в порт `0xA2`, потом стандартный flow.
|
||||
- `crt0_banked.s` — текущий, для **big**: trampoline-таблица для W3 банков, CODE в W2.
|
||||
- `crt0_banked_small.s` — **новый**: huge = small (W2-alloc) + banked (W3 trampolines).
|
||||
|
||||
sprinter-cc подбирает crt0 по `--memory` mode (сейчас `--crt0=` это override).
|
||||
- [x] **Настраиваемый размер стека**: флаг `sprinter-cc --stack-size BYTES`. Wrapper генерирует `heap_top.s` с `___sdcc_heap_end = stack_top + 1 - stack_size`, отдельный .rel линкуется per-program. Default ≈1278 байт (heap_top=0xBB00) из `runtime/heap_top.s`. Реализовано 2026-05-30.
|
||||
|
||||
Интерфейс: `sprinter-cc --memory [tiny|small|big|huge|manual] [--memory-manual SPEC] [--stack-size N] foo.c`. `--memory-manual` имеет смысл только с `--memory manual`.
|
||||
|
||||
## Known issues / quirks
|
||||
|
||||
- **ESTEX $46 ENV API**: ✅ работает. Док-ция в `DiskSyscalls.txt v1.6` ошибочно описывает return-status — A=0 это NOT FOUND, не FOUND. Зафиксировано в `memory/sprinter_platform.md`.
|
||||
|
||||
## ОБЯЗАТЕЛЬНЫЕ ЗАДАЧИ ДЛЯ V2 (после релиза v1)
|
||||
|
||||
### Turbo-C-style graphics API (BGI-like) — **MUST для v2**
|
||||
|
||||
Расширить наш `gfx_*` API до уровня **Turbo-C `<graphics.h>`** (BGI) для MS-DOS.
|
||||
Программисты привыкшие к Turbo-C должны переносить графический код 1-в-1.
|
||||
|
||||
**Что должно быть** (на основе Borland BGI):
|
||||
|
||||
Setup/teardown:
|
||||
- `initgraph()` / `closegraph()` — у нас сейчас `gfx_init`/`gfx_done`, добавить alias
|
||||
- `getmaxx()` / `getmaxy()` — макрос на GFX_WIDTH-1 / GFX_HEIGHT-1
|
||||
- `cleardevice()` — alias to gfx_clear
|
||||
- `getgraphmode()` / `setgraphmode()` — у нас get_videomode/set_videomode
|
||||
|
||||
Color/palette:
|
||||
- `setcolor(c)`, `getcolor()` — current draw color
|
||||
- `setbkcolor(c)`, `getbkcolor()` — background color
|
||||
- `setpalette(idx, c)` — палитра entry
|
||||
- `getpalette(&info)` — read all palette
|
||||
|
||||
Primitives (мы уже имеем эквиваленты — добавить BGI-имена как aliases):
|
||||
- `putpixel(x, y, c)` — есть как gfx_putpixel
|
||||
- `getpixel(x, y)` — нужно реализовать (RMW обратное — IN)
|
||||
- `moveto(x, y)`, `lineto(x, y)`, `linerel(dx, dy)` — current point + line drawing
|
||||
- `line(x1, y1, x2, y2)` — есть как gfx_line
|
||||
- `rectangle(x1, y1, x2, y2)` — есть как gfx_rect (но другой API: x1,y1,x2,y2 vs x,y,w,h!)
|
||||
- `bar(x1, y1, x2, y2)` — есть как gfx_fill_rect
|
||||
- `bar3d(x1, y1, x2, y2, depth, topflag)` — новое: rect + 3d edges
|
||||
- `circle(x, y, r)`, `arc(...)`, `ellipse(...)`, `pieslice(...)` — новые primitives
|
||||
- `fillpoly()`, `drawpoly()` — полигоны
|
||||
- `floodfill(x, y, border_color)` — заливка
|
||||
|
||||
Text on graphics screen:
|
||||
- `outtext(s)` / `outtextxy(x, y, s)` — есть как gfx_text (alias)
|
||||
- `settextstyle(font, dir, size)` — multiple bitmap fonts
|
||||
- `gettextsettings(&info)`
|
||||
- `textwidth(s)` / `textheight(s)` — measure
|
||||
|
||||
Image manipulation:
|
||||
- `imagesize(x1, y1, x2, y2)` — bytes needed for getimage
|
||||
- `getimage(x1, y1, x2, y2, buf)` — save rect to buffer
|
||||
- `putimage(x, y, buf, op)` — paste back with COPY_PUT/XOR_PUT/AND_PUT/OR_PUT/NOT_PUT
|
||||
|
||||
Clipping/viewport:
|
||||
- `setviewport(x1, y1, x2, y2, clip)` — drawing clip rect
|
||||
- `getviewsettings(&info)`
|
||||
- `clearviewport()`
|
||||
- `setactivepage(p)` / `setvisualpage(p)` — двойная буферизация (Sprinter имеет 2 screen)
|
||||
|
||||
Line style:
|
||||
- `setlinestyle(style, pattern, thickness)` — SOLID_LINE / DOTTED_LINE / etc.
|
||||
- `getlinesettings(&info)`
|
||||
|
||||
**Acceptance:** перенос типичной Turbo-C BGI программы (рисующей с использованием
|
||||
moveto/lineto/circle/bar/setcolor) должен работать без существенных правок.
|
||||
|
||||
**Notes:**
|
||||
- BGI fonts (TRIPLEX/SANS_SERIF/GOTHIC) — у нас один BIOS font, остальные нужно
|
||||
добавить (как bitmap data в lib)
|
||||
- imagesize/getimage/putimage — самые востребованные для game/animation
|
||||
- Active/visual page (двойная буферизация) — Sprinter поддерживает 2 graphics pages,
|
||||
нужен API switching
|
||||
|
||||
См. также `examples/` Turbo C 2.x BGIDEMO как reference что нужно.
|
||||
|
||||
### IM2 Interrupt Handlers — **MUST для v2**
|
||||
|
||||
User-задаваемые ISR через Z80 IM 2 mode. Нужны для:
|
||||
- Timer ticks (50 Hz frame counter, плавная анимация)
|
||||
- Music playback (AY, COVOX)
|
||||
- Real-time games (input + game logic + render в interrupt-driven)
|
||||
- Async keyboard / mouse handling
|
||||
|
||||
**Status:** ОТЛОЖЕНО до v2. Полный research + design в `docs/im2_isr_design.md`.
|
||||
|
||||
**Решение по архитектуре:** реализовать как отдельный memory mode `--memory im2`
|
||||
(вместо того чтобы лезть во все существующие crt0). Detail'и в design-doc.
|
||||
|
||||
**Резюме research'а** (полный текст в `docs/im2_isr_design.md`):
|
||||
- Vector 0xFF — frame + keyboard + CBL. Disambiguation по портам 0x19 / 0xFE
|
||||
- Mouse hardware-IRQ не приходит (на текущей плате)
|
||||
- Vector table / ISR / stack ОБЯЗАНЫ быть в W2 (0x8000..0xBFFF)
|
||||
- DSS имеет свой IM 2 handler — нужно chain'иться (иначе клавиатура / SYSTIME ломаются)
|
||||
|
||||
### Прочие крупные пункты для v2
|
||||
|
||||
- [ ] **FILE API rewrite — buffered streams** — текущая реализация в
|
||||
`libc/stdio/file.c` это provisional unbuffered shim (каждый fputc/fgetc
|
||||
= один read/write syscall). Нужна полноценная buffered семантика
|
||||
как в Solid-C:
|
||||
|
||||
```c
|
||||
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;
|
||||
```
|
||||
|
||||
stdin/stdout/stderr — fd-маркеры `0 / -1 / -2`. Отрицательные для
|
||||
stdout/stderr выбраны намеренно: ESTEX OPEN может вернуть positive
|
||||
small fd (1, 2, …) для обычного файла → если бы stdout=1, реальный
|
||||
fd=1 сталкивался бы с идентификатором. fd=0 для stdin безопасно
|
||||
(ESTEX 0 не возвращает). Сами fd не передаются в syscall'ы —
|
||||
диспетчеризация по флагам `_F_CONIN/_F_CONOUT`.
|
||||
|
||||
Принтер-потоки (stdaux/stdprn) НЕ реализуем — Sprinter принтерной
|
||||
API не имеет.
|
||||
|
||||
Альтернатива — взять реализацию из third_party/solid-c (sources в
|
||||
`SRC/CLIB/`); там есть готовый buffered FILE + fopen/fread/fwrite/
|
||||
fseek/setvbuf и т.д. Адаптировать к нашим open/read/write/lseek.
|
||||
|
||||
При rewrite заодно решить deferred issues stdio-review:
|
||||
- `fwrite` short-write должен ставить `_F_ERROR`
|
||||
- `fgets(buf, 1, fp)` — стандарт говорит "empty string", мы вернули NULL
|
||||
- `mode_to_flags` — break-out на '+' (cosmetic)
|
||||
|
||||
- [ ] **Audio API** — AY-3-8910 + COVOX через прерывания (требует IM2)
|
||||
- [ ] **ISA-8 slot support** — ZX-Bus карты (sound, network, etc.) — требует IM2 + чтения portов
|
||||
|
||||
## Прочие задачи (v1 backlog, не блокирующие)
|
||||
|
||||
- [x] **#9: text I/O split (Turbo-C style)** — stdio (puts/printf/putchar) теперь fast no-attr через PCHARS/PUTCHAR. conio (cputs/cprintf/putch) применяет attr через textcolor/textbackground/textattr. KEEP_EXIST_ATTR → conio fallback на fast path. Verified в hello.exe. См. `memory/text_output_api_split.md`. Реализовано 2026-05-31.
|
||||
- [x] **Mouse API полный** (резидентный driver, RST 30h) — все 14 функций обёрнуты (init/show/hide/refresh/read/goto/bounds/text_cursor/load_cursor/get_cursor/get/set_sensitivity/video_mode_changed). См. `memory/mouse_api.md`. Verified в MAME 2026-05-31. Sensitivity = divider (меньше = быстрее).
|
||||
- [ ] Interrupt handlers — IM 2 vector table в HOME для user ISR'ов
|
||||
- [ ] Поддержка `restore SP on EXIT` (паттерн из z88dk +pps) — проверить нужно ли
|
||||
- [ ] CI: автоматически запускать MAME с `-aviwrite` для screenshot-сравнения, чтобы тесты примеров проходили без человека
|
||||
|
||||
## Идеи на потом
|
||||
|
||||
- Поддержка `<setjmp.h>` (есть в SDCC stdlib — нужно протестировать что наш crt0 совместим)
|
||||
- `<time.h>` через ESTEX SYSTIME (`$21`) и CMOS BIOS-функции
|
||||
- ZX Spectrum-совместимый режим как отдельный target (для портирования спектрумовских программ)
|
||||
- Поддержка ZX-Bus карт (sound, network, etc.) — нужны драйверы
|
||||
- Profile-guided optimization tools (hot/cold detection) для крупных программ
|
||||
|
||||
## Linker duplicate-symbol warnings (благоприятные, отфильтрованы)
|
||||
|
||||
Когда мы сознательно overrides'им SDCC z80.lib функции собственной версией в `sprinter.lib`, `sdldz80` пишет `?ASlink-Warning-Definition of public symbol '...' found more than once`. Линкер берёт первое найденное определение (наше), поэтому поведение корректное — warning только noise.
|
||||
|
||||
Текущие overrides:
|
||||
- `_puts` — наша версия через PCHARS+\r\n vs SDCC posix puts
|
||||
- `___sdcc_heap` — наш heap в W2 vs SDCC's стандартный
|
||||
- `_asctime`, `_localtime` (и возможно другие из time) — наш `posix_time.c` через ESTEX SYSTIME vs SDCC's `time.rel` который зависит от `_RtcRead`
|
||||
|
||||
**Текущее решение:** `bin/sprinter-cc` отфильтровывает warning-блок (warning + 2 follow-up `Library:` строки) из вывода `sdcc`. Через `-v` (verbose) всё показывается. Реализовано через awk-pipe.
|
||||
|
||||
**Возможные улучшения:**
|
||||
- Перейти на explicit `--nostdlib` + ручной список нужных модулей из z80.lib (string, math, stdlib без override'нутых) — убрать ИСТОЧНИК warning'ов, не маскировать
|
||||
- Или: переименовать наши `_puts` → `_puts_sprinter` + alias через linker flag (не уверен что SDCC поддерживает)
|
||||
- Или: оставить как сейчас (рабочее и benign) — приоритет низкий
|
||||
|
||||
## TODO: проверить на реальном железе
|
||||
|
||||
- [ ] **Port_Y banking trick** (`docs/part2/SprinterGraphics programming.txt`):
|
||||
доку утверждает что после `OUT (0x89), Y` адреса 0xC000+0x400*N в окне W3
|
||||
маппятся на строки Y..Y+15 (одно программирование → 16 строк).
|
||||
Empirical 2026-06-01 в MAME 0.283 этот trick **не работает** — пиксели
|
||||
по адресам выше 0xC000+row_width уходят в невидимую область. Канонический
|
||||
`docs/samples/plasma2.asm` тоже не использует banking, переустанавливает
|
||||
Port_Y per row.
|
||||
План:
|
||||
1. Получить доступ к реальному Sprinter
|
||||
2. Запустить тест dual-write (`_gfx_putpixel_raw` + второй write в `0xD000+x`)
|
||||
3. Если на железе видны двойные линии → бага MAME, открыть issue с
|
||||
минимальным репро
|
||||
4. Если на железе тоже одна линия → документ неверный, удалить упоминание
|
||||
из доки и просто оставить текущую реализацию (Port_Y per pixel)
|
||||
5. Если banking работает на железе → внедрить кэширование Port_Y в
|
||||
`_gfx_putpixel_raw` (sentinel out-of-range, см. memory/gfx_port_y_banking.md)
|
||||
|
||||
Связанный выигрыш для Bresenham (60-pixel диагональ) — около 8× меньше
|
||||
OUT (0x89) операций, для `gfx_fill_rect 320x256` — 16× меньше. Не блокирует
|
||||
release v1.
|
||||
|
||||
## GFX: расширения по `docs/part2/accelerator_doc.txt`
|
||||
|
||||
После прочтения детального accelerator doc выявлены незакрытые направления.
|
||||
Сейчас в коде используется только горизонтальный/вертикальный Fill mode.
|
||||
|
||||
### Quick wins для текущих primitives
|
||||
|
||||
- [ ] **Заменить SMC на `LD A, (var)` для block-size**. Документ явно
|
||||
разрешает `LD A, (HL)`, `LD A, (BC)`, `LD A, (DE)` (но не `LD A, r`).
|
||||
Это уберёт SMC complexity в `gfx_lines.c:hfill_chunk/vfill_chunk` и
|
||||
`gfx_16.c:g16_hfill_chunk`. Запрещено только register-to-register.
|
||||
- [ ] **Кэширование block-size**. Документ показывает что accel запоминает
|
||||
block size между bursts (см. `Horizontal_Line_Fill`: устанавливают
|
||||
size + `LD B,B` отключение, потом включают Fill mode и используют
|
||||
сохранённый size). Для `gfx_fill_rect` с 100 одинаковыми
|
||||
строками — установить size 1 раз, а не 100.
|
||||
|
||||
### Bank-prefix modes (port 0xE2 bits)
|
||||
|
||||
Документ показывает три варианта банка видеостраницы помимо стандартного 0x50:
|
||||
|
||||
| Bank byte | Effect |
|
||||
|---|---|
|
||||
| 0x50 | Normal write — пишется в shadow + видимый |
|
||||
| 0x54 | "no copy in main shadow RAM" |
|
||||
| 0x58 | **"FF is transparent"** — байт 0xFF при write оставляет background |
|
||||
| 0x5C | both |
|
||||
|
||||
Bank 0x58 объясняет почему mouse cursor рисуется с 0xFF-прозрачностью.
|
||||
Это путь к **sprite-blending через accel block copy**:
|
||||
|
||||
- [ ] **`gfx_set_bank_transparent(on)`** или флаг в `gfx_set_bank` для
|
||||
выбора 0x50/0x58 при отрисовке sprite'ов
|
||||
- [ ] Использовать в новом `gfx_blit()` чтобы по факту получать
|
||||
transparent sprites через accel-копию
|
||||
|
||||
### Block copy mode (sprite blit'ы)
|
||||
|
||||
`LD L,L` (horizontal) и `LD A,A` (vertical) — режим копирования блока через
|
||||
256-байтную accel memory. Это базис для blit'ов.
|
||||
|
||||
- [ ] **`gfx_blit(src_data, x, y, w, h)`** — копирование sprite'а
|
||||
(произвольный размер, через accel)
|
||||
- [ ] **`gfx_blit_transparent(src, x, y, w, h)`** — с использованием bank 0x58
|
||||
|
||||
См. `Draw_Restangle_Data` в accelerator_doc.txt как референс.
|
||||
|
||||
### AND / OR / XOR operations через accel
|
||||
|
||||
Документ показывает что accel поддерживает логические операции с блоками
|
||||
данных. Применения:
|
||||
- XOR — инверсия области (выделение selection в UI)
|
||||
- OR / AND — masking, alpha-style blending
|
||||
- См. пример в accelerator_doc.txt: "256 bytes block coding via XOR"
|
||||
|
||||
- [ ] **`gfx_xor_rect`** / **`gfx_or_rect`** / **`gfx_and_rect`** —
|
||||
примитивы логических операций над прямоугольником
|
||||
- [ ] **`gfx_invert_rect(x, y, w, h)`** — alias на xor с 0xFF
|
||||
|
||||
### Bitmap fonts разных размеров
|
||||
|
||||
Сейчас `gfx_text` / `gfx_putchar` хардкоженно работают с 8×8 шрифтом
|
||||
(BIOS WIN_GET_ZG возвращает 256×8 байт). Для будущих UI / титульников
|
||||
нужны:
|
||||
- [ ] **`gfx_set_font_size(w, h)`** — переключить ширину/высоту glyph'а
|
||||
- [ ] **`gfx_set_font_data(ptr, w, h, advance)`** — заменить указатель
|
||||
на пользовательский шрифт + размеры
|
||||
- [ ] Поддержка **proportional** (advance != w) шрифтов — добавить
|
||||
array advance[256] на ширину каждого glyph'а
|
||||
- [ ] **Big-font режимы**: 8×16, 16×16, 16×8 (для титульников)
|
||||
- [ ] Возможно отдельный API `gfx_text_ex(x, y, str, font_id)` где
|
||||
font_id выбирает один из загруженных шрифтов
|
||||
- [ ] **Anti-alias 2-bit шрифты** (бит фон / бит граница / 2-бит alpha?)
|
||||
— far future, для smooth UI
|
||||
|
||||
## Финальный этап оптимизаций (не сейчас)
|
||||
|
||||
- **`gfx_line` через accel для пологих диагоналей** — Bresenham для линии с |dy| << |dx| (или наоборот) выдаёт длинные runs одинакового Y (или X): пиксель, пиксель, пиксель, шаг Y, пиксель, пиксель... Каждый такой run — это готовый аргумент для `gfx_hline` (или `vline`).
|
||||
План исследования: посчитать длину runs как функцию от наклона; решить минимальный run length, при котором выгоднее accel hline чем N×putpixel (overhead accel ~20µs, putpixel ~5µs — accel выгоднее при run ≥ 4-5 px); для крутых диагоналей (dx ≈ dy) оставить Bresenham, для пологих — run-length-based fill.
|
||||
Сейчас `gfx_line` orthogonal cases уже через accel — оптимизировать только косые.
|
||||
|
||||
- **`gfx_fill_rect` с одним W3-swap на всю операцию** — сейчас каждый внутренний `gfx_hline`/`gfx_vline` делает свой DI/save-W3/restore-W3/EI. Можно сделать internal `_fill_rect_inner` который держит W3 замапленным и DI весь цикл; ~20µs × количество строк/столбцов экономии. Применимо ко всем композитным примитивам.
|
||||
+155
-190
@@ -35,6 +35,7 @@
|
||||
#include <unistd.h>
|
||||
#include <sprinter.h>
|
||||
#include <sprinter_mem.h>
|
||||
#include <sprinter_exit.h>
|
||||
#include <palette.h>
|
||||
#include "mdview2.h" /* геометрия, атрибуты, общий интерфейс модулей */
|
||||
|
||||
@@ -94,7 +95,7 @@ static const uint8_t md_pallete[16 * 4] = {
|
||||
/* 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 2048u
|
||||
#define CACHE_DIR_RECS_PER_PAGE (16384 / CACHE_DIR_REC_SIZE) /* 2048 */
|
||||
#define MAX_CACHE_DIR_PAGES 8u
|
||||
/* MAX_CACHE_LINE_LEN — в mdview2.h (буфер ячеек g_cells живёт в mdview2_index.c). */
|
||||
#define MAX_CACHE_CONTENT_PAGES 64u /* запас: до 1 МБ кэша контента */
|
||||
@@ -113,14 +114,14 @@ static uint16_t cache_cur_off = 0; /* текущее смещ
|
||||
void cache_dir_get(uint16_t idx, cache_rec_t *r)
|
||||
{
|
||||
uint8_t page = (uint8_t)(idx >> 11);
|
||||
uint16_t off = (uint16_t)((idx & 2047u) << 3);
|
||||
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 & 2047u) << 3);
|
||||
uint16_t off = (uint16_t)((idx & 0x07FF) << 3); /* 2047 */
|
||||
bank_write(cache_dir_phys[page], off, r, CACHE_DIR_REC_SIZE);
|
||||
}
|
||||
|
||||
@@ -140,9 +141,11 @@ uint8_t cache_reserve(uint16_t nbytes, uint8_t *out_page, uint16_t *out_off)
|
||||
* не вмещает запись целиком. */
|
||||
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;
|
||||
if (cache_content_n_pages >= MAX_CACHE_CONTENT_PAGES)
|
||||
return 0;
|
||||
uint8_t blk = mem_alloc_pages(1);
|
||||
if (blk == 0) return 0;
|
||||
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++;
|
||||
@@ -171,40 +174,40 @@ void cache_commit(uint8_t page, uint16_t off, const void *buf, uint16_t len)
|
||||
/* emph_to_attr определена ниже (рядом с inline-логикой), нужна в inline_scan. */
|
||||
/* emph_to_attr — в mdview2_index.c. */
|
||||
|
||||
uint16_t n_lines = 0; /* extern: вью читает */
|
||||
uint16_t max_lines = 0; /* ёмкость индекса: index_pages * 2048 */
|
||||
uint16_t top_line = 0; /* extern: вью/скролл */
|
||||
uint32_t file_size = 0;
|
||||
uint16_t n_lines = 0; /* extern: всего строк в индексе текущего набора */
|
||||
uint16_t max_lines = 0; /* extern: ёмкость индекса: index_pages * 2048 */
|
||||
uint16_t top_line = 0; /* extern: верхняя видимая строка */
|
||||
uint32_t file_size = 0; /* extern: размер активного буфера документа */
|
||||
static uint8_t file_blk = 0;
|
||||
static uint8_t file_pages = 0;
|
||||
static uint8_t file_phys[MAX_PAGES] = {0};
|
||||
uint8_t index_blk = 0;
|
||||
uint8_t index_pages = 0;
|
||||
uint8_t index_phys[MAX_INDEX_PAGES] = {0};
|
||||
uint8_t index_truncated = 0; /* выставляется при исчерпании ёмкости индекса */
|
||||
uint8_t index_blk = 0; /* extern: дескриптор EMM-блока индекса */
|
||||
uint8_t index_pages = 0; /* */
|
||||
uint8_t index_phys[MAX_INDEX_PAGES] = {0}; /* extern: физ. страницы индекса */
|
||||
uint8_t index_truncated = 0; /* выставляется при исчерпании ёмкости индекса */
|
||||
|
||||
/* cur_seg_off/cur_rec (near-состояние индексатора) — в mdview2_index.c. */
|
||||
uint8_t cur_page = 0xFF; /* 0xFF = в W3 ещё не показана ни одна страница (extern: кодек инвалидирует) */
|
||||
uint8_t viewport_x = 0; /* горизонтальный сдвиг для nowrap-строк (extern: вью) */
|
||||
char filename[64] = {0}; /* extern: статус-бар */
|
||||
uint8_t cur_page = 0xFF; /* extern: 0xFF = в W3 ещё не показана ни одна страница (extern: кодек инвалидирует) */
|
||||
uint8_t viewport_x = 0; /* extern: горизонтальный сдвиг для nowrap-строк */
|
||||
char filename[64] = {0}; /* extern: статус-бар */
|
||||
|
||||
/* Прогрессивная загрузка: рисуем первый экран и пускаем скролл, пока
|
||||
* index_lines() ещё досканирует файл (кооперативно, без прерываний). */
|
||||
uint8_t g_view = VIEW_MD; /* extern: активный режим просмотра (существует даже когда доступен только MD режим) */
|
||||
#if WITH_RAW
|
||||
uint8_t g_view = VIEW_MD; /* режим просмотра (общий с mdview2_raw.c) */
|
||||
uint8_t g_ready = 0; /* 1 когда первичный документ построен — F2 (RAW) рабочий (extern: вью) */
|
||||
static uint8_t g_raw_last = VIEW_RAW_WRAP; /* последний под-режим RAW (возврат по F2) */
|
||||
uint8_t g_ready = 0; /* extern: 1 когда первичный документ построен — F2 (RAW) рабочий */
|
||||
static uint8_t g_raw_last = VIEW_RAW_WRAP; /* последний под-режим RAW (возврат по F2) */
|
||||
#endif
|
||||
uint8_t g_loading = 0; /* 1 во время index_lines() (extern: вью) */
|
||||
static uint8_t g_first_drawn = 0; /* первый экран уже нарисован */
|
||||
uint8_t g_f8_enabled = 0; /* можно ли сейчас переключать кодировку (extern: вью-меню) */
|
||||
static uint8_t g_utf_failed = 0; /* UTF-8 набор не удалось построить (нет EMM) — пропускаем UTF-8 в цикле F8 */
|
||||
uint8_t g_abort = 0; /* F10/Esc во время загрузки → прервать индексацию */
|
||||
static uint16_t g_last_prog = 0; /* n_lines на последнем обновлении статуса */
|
||||
/* progress_tick — объявлена в mdview2.h (определена ниже, зовётся из index_lines). */
|
||||
uint8_t g_loading = 0; /* extern: 1 во время index_lines() */
|
||||
static uint8_t g_first_drawn = 0; /* первый экран уже нарисован */
|
||||
uint8_t g_f8_enabled = 0; /* extern: можно ли сейчас переключать кодировку */
|
||||
static uint8_t g_utf_failed = 0; /* UTF-8 набор не удалось построить (нет EMM) — пропускаем UTF-8 в цикле F8 */
|
||||
uint8_t g_abort = 0; /* extern: F10/Esc во время загрузки → прервать индексацию */
|
||||
static uint16_t g_last_prog = 0; /* n_lines на последнем обновлении статуса */
|
||||
|
||||
/* Кодировки (ENC_*) и g_encoding — в mdview2.h / mdview2_enc.c. */
|
||||
const uint8_t *g_remap = 0; /* активная таблица ремапа (CP1251/KOI8), 0 = без ремапа */
|
||||
uint8_t g_scratch_phys = 0; /* EMM-страница под ремап-буфер строки для win_rest */
|
||||
const uint8_t *g_remap = 0; /* extern: активная таблица ремапа (CP1251/KOI8), 0 = без ремапа (CP866)*/
|
||||
uint8_t g_scratch_phys = 0; /* extern: EMM-страница под ремап-буфер строки для win_rest */
|
||||
|
||||
/* UTF-8 набор (utf_blk/utf_pages/utf_phys/utf_size/utf_avail, конвертер и его
|
||||
* состояние) — в mdview2_enc.c; разделяемые символы и API — в mdview2.h.
|
||||
@@ -214,11 +217,11 @@ uint8_t g_scratch_phys = 0; /* EMM-страница под ремап
|
||||
/* Снимок исходного (8-битного) файлового буфера — набор DOC_8BIT читает его
|
||||
* как есть, набор DOC_UTF8 читает utf_phys. */
|
||||
static uint8_t orig_file_blk = 0;
|
||||
uint8_t orig_file_pages = 0; /* extern: читает кодек (utf_alloc) */
|
||||
uint32_t orig_file_size = 0; /* extern: читает кодек (utf_convert_more) */
|
||||
uint8_t orig_file_phys[MAX_PAGES] = {0}; /* extern: читает кодек (cv_read) */
|
||||
uint8_t orig_file_pages = 0; /* extern: читает кодек (utf_alloc) */
|
||||
uint32_t orig_file_size = 0; /* extern: читает кодек (utf_convert_more) */
|
||||
uint8_t orig_file_phys[MAX_PAGES] = {0}; /* extern: читает кодек (cv_read) */
|
||||
|
||||
/* ---------- Два сосуществующих набора индекс/кэш (Фазы 4-5) ----------
|
||||
/* ----------------- Два сосуществующих набора индекс/кэш -----------------
|
||||
* DOC_8BIT — исходные байты (CP866/CP1251/KOI8R, различаются ремапом на
|
||||
* отрисовке); DOC_UTF8 — UTF-8, декодированный в CP866. Оба строятся eager
|
||||
* при старте (кооперативно), поэтому F8 переключает активный набор мгновенно.
|
||||
@@ -242,6 +245,7 @@ typedef struct {
|
||||
uint16_t top_line;
|
||||
uint8_t viewport_x;
|
||||
} docset_t;
|
||||
|
||||
#define DOC_8BIT 0
|
||||
#define DOC_UTF8 1
|
||||
static docset_t g_doc[2] = {{0}, {0}}; /* ОБЯЗАТЕЛЬНО инициализировать: built=0 для обоих
|
||||
@@ -252,13 +256,7 @@ static uint8_t g_disp_doc = DOC_8BIT; /* какой набор показа
|
||||
|
||||
static void doc_switch(uint8_t target); /* forward (нужна в progress_tick) */
|
||||
|
||||
/* Спиннер в статус-баре (col=8,row=0).
|
||||
* Включается во время загрузки/индексации, чтобы показать,
|
||||
* что программа занята и не зависла. */
|
||||
#define SPINNER_COL 8
|
||||
static const char spinner_chars[4] = { '|', '/', '-', '\\' };
|
||||
static uint8_t spinner_phase = 0;
|
||||
static uint8_t spinner_active = 0;
|
||||
/* Статус-бар, меню и спиннер — в mdview2_status.c. */
|
||||
|
||||
/* ==================================================================
|
||||
* Примитивы экрана, доступ к EMM/окну W3 (fb/win_rest), палитра
|
||||
@@ -270,50 +268,32 @@ static uint8_t spinner_active = 0;
|
||||
* можно передать в BIOS LP_PRINT_LN напрямую, без промежуточной копии. */
|
||||
void put_str_attr(uint8_t x, uint8_t y, const char *s, uint8_t 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;
|
||||
uint8_t len = (x < SCREEN_W) ? (uint8_t)(SCREEN_W - x) : 0;
|
||||
if (len == 0) return;
|
||||
bios_set_place(y, x);
|
||||
bios_writeattr(s, len, attr);
|
||||
bios_writeattr_until(s, len, attr, 0);
|
||||
}
|
||||
|
||||
|
||||
/* Полностью очищает указанную строку экрана символами пробела с attr. */
|
||||
void fill_row(uint8_t y, uint8_t attr)
|
||||
{
|
||||
// bios_set_place(y, 0);
|
||||
// bios_fillcharattr(' ', attr, SCREEN_W);
|
||||
bios_clearwin(y, 0, 1, SCREEN_W, attr);
|
||||
}
|
||||
|
||||
/* Продвигает спиннер на один кадр.
|
||||
* Если спиннер выключен — функция ничего не делает. */
|
||||
void spinner_tick(void)
|
||||
{
|
||||
if (!spinner_active) return;
|
||||
wrchar(SPINNER_COL, 0, spinner_chars[spinner_phase & 3], ATTR_BAR_SPINNER);
|
||||
spinner_phase++;
|
||||
}
|
||||
|
||||
/* Включает/выключает спиннер.
|
||||
* При выключении очищает его позицию пробелом в статус-баре. */
|
||||
static void spinner_show(uint8_t on)
|
||||
{
|
||||
spinner_active = on;
|
||||
if (!on) wrchar(SPINNER_COL, 0, ' ', ATTR_BAR);
|
||||
}
|
||||
|
||||
/* Отображает указанную 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;
|
||||
}
|
||||
}
|
||||
// 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.
|
||||
* Проверка границ выполняется вызывающей стороной.
|
||||
@@ -458,13 +438,15 @@ static void set_pallete(void) {
|
||||
static int load_file(const char *path)
|
||||
{
|
||||
int fd = open(path, O_RDONLY);
|
||||
if (fd < 0) return -1;
|
||||
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);
|
||||
|
||||
@@ -506,40 +488,25 @@ static int load_file(const char *path)
|
||||
* Возврат: 0 — успех, -5/-6 — нехватка EMM. */
|
||||
static int alloc_set_storage(void)
|
||||
{
|
||||
/* Блок индекса (8 байт на сегмент, 2048 записей/страница). */
|
||||
{
|
||||
uint8_t want = (uint8_t)(file_pages + 1u);
|
||||
if (want > MAX_INDEX_PAGES) want = MAX_INDEX_PAGES;
|
||||
index_blk = 0;
|
||||
while (want >= 1u) {
|
||||
index_blk = mem_alloc_pages(want);
|
||||
if (index_blk) break;
|
||||
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;
|
||||
}
|
||||
uint8_t want;
|
||||
|
||||
/* Директория рендер-кэша — та же ёмкость, что у индекса. */
|
||||
{
|
||||
uint8_t want = (uint8_t)(file_pages + 1u);
|
||||
if (want > MAX_CACHE_DIR_PAGES) want = MAX_CACHE_DIR_PAGES;
|
||||
cache_dir_blk = 0;
|
||||
while (want >= 1u) {
|
||||
cache_dir_blk = mem_alloc_pages(want);
|
||||
if (cache_dir_blk) break;
|
||||
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);
|
||||
}
|
||||
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;
|
||||
@@ -617,7 +584,8 @@ static void doc_load_live(uint8_t i)
|
||||
|
||||
static void doc_switch(uint8_t target)
|
||||
{
|
||||
if (target == g_active_doc) return;
|
||||
if (target == g_active_doc)
|
||||
return;
|
||||
doc_save_live(g_active_doc);
|
||||
doc_load_live(target);
|
||||
g_active_doc = target;
|
||||
@@ -628,18 +596,6 @@ static void doc_switch(uint8_t target)
|
||||
/* Отрисовка области/статус-бара/меню и прокрутка — в модуле mdview2_md.c. */
|
||||
|
||||
|
||||
/* ==================================================================
|
||||
* Вывод ошибки
|
||||
* ================================================================== */
|
||||
|
||||
/* Печатает сообщение об ошибке и ждёт нажатия клавиши. */
|
||||
static void die(const char *msg)
|
||||
{
|
||||
clrscr_attr(ATTR_TEXT);
|
||||
puts(msg);
|
||||
puts("Press any key to exit...");
|
||||
(void)getkey();
|
||||
}
|
||||
|
||||
/* ==================================================================
|
||||
* Прогрессивная загрузка и диспетчер клавиш (MD)
|
||||
@@ -668,10 +624,10 @@ static void load_key(uint16_t k)
|
||||
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_status_numbers(); return;
|
||||
case KEY_END: top_line = maxtop; viewport_x = 0; draw_viewport_from_cache(); render_status_numbers(); return;
|
||||
case KEY_LEFT: scroll_h(-(int8_t)HPAN_STEP); render_status_numbers(); return;
|
||||
case KEY_RIGHT: scroll_h(+(int8_t)HPAN_STEP); render_status_numbers(); return;
|
||||
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-битный
|
||||
* (ремап на отрисовке; индексация не зависит от кодировки) */
|
||||
@@ -689,7 +645,7 @@ static void load_key(uint16_t k)
|
||||
if (nt != top_line) {
|
||||
top_line = nt;
|
||||
draw_viewport_from_cache();
|
||||
render_status_numbers();
|
||||
render_md_status_numbers();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -704,7 +660,7 @@ void progress_tick(void)
|
||||
if (g_view != VIEW_MD) {
|
||||
if (!g_first_drawn) {
|
||||
raw_draw();
|
||||
raw_status_numbers();
|
||||
// render_raw_status();
|
||||
g_first_drawn = 1;
|
||||
cur_page = 0xFF; /* raw_draw сбил W3-маппинг, на который опирается fb() */
|
||||
}
|
||||
@@ -716,14 +672,14 @@ void progress_tick(void)
|
||||
uint8_t touched = 0;
|
||||
if (!g_first_drawn && drawable_lines() >= VIEW_H) {
|
||||
draw_viewport_from_cache();
|
||||
render_status_numbers();
|
||||
render_md_status_numbers();
|
||||
g_first_drawn = 1;
|
||||
touched = 1;
|
||||
}
|
||||
if (n_lines >= (uint16_t)(g_last_prog + 20)) {
|
||||
g_last_prog = n_lines;
|
||||
spinner_tick();
|
||||
if (g_first_drawn) render_status_numbers();
|
||||
if (g_first_drawn) render_md_status_numbers();
|
||||
touched = 1;
|
||||
}
|
||||
if (g_first_drawn && kbhit()) {
|
||||
@@ -833,6 +789,15 @@ static void switch_encoding(uint8_t next)
|
||||
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;
|
||||
@@ -844,16 +809,20 @@ int main(int argc, char **argv)
|
||||
{
|
||||
int chk = open(path, O_RDONLY);
|
||||
if (chk < 0) {
|
||||
cputs("MDView v0.2 -- Markdown Viewer for Sprinter\r\n");
|
||||
cputs("(c) 2026 Petrov A.G.\r\n\r\n");
|
||||
cputs("Usage: mdview2 <file.md>\r\n");
|
||||
cputs(" Displays a Markdown document (CP866, up to 128 KB).\r\n");
|
||||
return 1;
|
||||
// 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;
|
||||
@@ -869,8 +838,10 @@ int main(int argc, char **argv)
|
||||
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);
|
||||
@@ -890,7 +861,7 @@ int main(int argc, char **argv)
|
||||
case -6: cputs("render-cache dir alloc failed"); break;
|
||||
}
|
||||
cputs("\n\r\n\r");
|
||||
return 1;
|
||||
exit(-1);
|
||||
}
|
||||
/* Scratch-страница под ремап-буфер строки (win_rest_remap). */
|
||||
g_scratch_phys = mem_get_page(mem_alloc_pages(1), 0);
|
||||
@@ -928,13 +899,15 @@ int main(int argc, char **argv)
|
||||
clrscr_attr(ATTR_RESET);
|
||||
textattr(ATTR_RESET);
|
||||
cputs("MDView: index/cache alloc failed\n\r\n\r");
|
||||
return 1;
|
||||
exit(-1);
|
||||
}
|
||||
if (g_abort) goto exit_loop; /* F10/Esc во время загрузки — корректный выход */
|
||||
if (g_abort)
|
||||
exit(0); /* F10/Esc во время загрузки — корректный выход */
|
||||
if (n_lines == 0) {
|
||||
die("mdview: empty file");
|
||||
unload_file();
|
||||
return 1;
|
||||
clrscr_attr(ATTR_RESET);
|
||||
textattr(ATTR_RESET);
|
||||
cputs("mdview: empty file");
|
||||
exit(-1);
|
||||
}
|
||||
|
||||
/* Первичный готов — гасим спиннер, включаем F8 (второй набор дособерётся
|
||||
@@ -956,11 +929,17 @@ int main(int argc, char **argv)
|
||||
uint8_t scan = (uint8_t)((k >> 8) & 0x7F);
|
||||
|
||||
if (ascii) {
|
||||
if (ascii == 0x1B) break; /* клавиша Esc */
|
||||
if (ascii == 0x1B)
|
||||
break; /* клавиша Esc */
|
||||
continue;
|
||||
}
|
||||
|
||||
#if WITH_RAW
|
||||
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;
|
||||
@@ -968,8 +947,10 @@ int main(int argc, char **argv)
|
||||
g_view = g_raw_last; /* → RAW (последний под-режим) */
|
||||
raw_seed_from(top_line); /* позиция из MD */
|
||||
raw_screen_init();
|
||||
render_full_status(); render_menu();
|
||||
raw_status_numbers(); raw_draw();
|
||||
status_encoding();
|
||||
render_menu();
|
||||
render_raw_status();
|
||||
raw_draw();
|
||||
} else {
|
||||
g_raw_last = g_view; /* запомнить под-режим RAW */
|
||||
g_view = VIEW_MD;
|
||||
@@ -979,64 +960,48 @@ int main(int argc, char **argv)
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
/* RAW-режим: F3 Wrap/Unwrap, своя навигация; F8 — кодировка; F1/F10. */
|
||||
if (g_view != VIEW_MD) {
|
||||
if (scan == KEY_F10) goto exit_loop;
|
||||
if (scan == KEY_F1) {
|
||||
show_help();
|
||||
raw_screen_init();
|
||||
render_full_status(); render_menu();
|
||||
raw_status_numbers(); raw_draw();
|
||||
continue;
|
||||
}
|
||||
if (scan == KEY_F3) { /* Wrap ↔ Unwrap */
|
||||
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();
|
||||
raw_status_numbers(); raw_draw();
|
||||
continue;
|
||||
}
|
||||
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;
|
||||
uint8_t target = (next == ENC_UTF8) ? DOC_UTF8 : DOC_8BIT;
|
||||
/* другой буфер (в/из UTF-8) → в начало ДО сборки, чтобы прогрессивный
|
||||
* raw_draw в progress_tick показывал верх нового набора. */
|
||||
if (target != g_active_doc) raw_home();
|
||||
switch_encoding(next); /* меняет набор/ремап */
|
||||
if (g_abort) goto exit_loop;
|
||||
render_full_status(); raw_status_numbers(); raw_draw();
|
||||
continue;
|
||||
}
|
||||
raw_key(scan);
|
||||
continue;
|
||||
}
|
||||
#endif /* WITH_RAW */
|
||||
|
||||
/* MD-режим: F1/F8/F10 здесь, навигация — в md_key() (peer к raw_key). */
|
||||
if (scan == KEY_F10) goto exit_loop;
|
||||
if (scan == KEY_F1) { show_help(); continue; } /* show_help сам перерисует */
|
||||
if (scan == KEY_F8) { /* цикл CP866→CP1251→KOI8R→UTF8→CP866; набор дособерётся лениво */
|
||||
/* цикл 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 недоступен */
|
||||
(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) goto exit_loop; /* Esc/F10 во время ленивой досборки */
|
||||
if (g_abort)
|
||||
exit(0); /* Esc/F10 во время ленивой досборки */
|
||||
render_full_status(); /* перерисовка — в MD-режиме */
|
||||
draw_viewport_from_cache();
|
||||
continue;
|
||||
}
|
||||
md_key(scan); /* навигация MD (сама рисует+статус); незнакомые игнорит */
|
||||
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;
|
||||
}
|
||||
}
|
||||
exit_loop:
|
||||
unload_file();
|
||||
pal_reset(PAL_CGA);
|
||||
clrscr_attr(ATTR_TEXT);
|
||||
exit(0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
+28
-12
@@ -22,6 +22,12 @@
|
||||
#define TAB_STOP 4
|
||||
#define HPAN_STEP 8u /* шаг горизонтального сдвига (←/→) */
|
||||
|
||||
/* ---- Геометрия статус-бара (строка 0): фиксированные поля и разделители ---- */
|
||||
#define SPINNER_COL 8 /* слот спиннера загрузки */
|
||||
#define DIV1_X 45 /* разделитель │ перед числами диапазона строк */
|
||||
#define DIV2_X 71 /* разделитель │ перед процентом */
|
||||
/* Числа/метка пишутся с DIVn_X+2; область между разделителями — [DIV1_X+1 .. DIV2_X-1]. */
|
||||
|
||||
/* ---- Параметры файла и памяти ------------------------------------ */
|
||||
#define PAGE_BITS 14u
|
||||
#define PAGE_SIZE (1u << PAGE_BITS) /* размер EMM-страницы: 16 КБ */
|
||||
@@ -130,7 +136,7 @@ uint32_t seg_off(uint16_t idx); /* offset исходника для ло
|
||||
void raw_seed_from(uint16_t md_top_line); /* вход в RAW: позиция по top_line */
|
||||
void raw_home(void); /* сброс в начало (смена буфера при F8 в/из UTF-8) */
|
||||
void raw_draw(void); /* перерисовать область документа в RAW */
|
||||
void raw_status_numbers(void); /* числовая зона статус-бара для RAW */
|
||||
uint8_t raw_pct(void); /* % прокрутки по байтам (для render_raw_status_numbers) */
|
||||
uint8_t raw_key(uint8_t scan); /* навигация RAW; 1 = обработано */
|
||||
void raw_screen_init(void); /* очистка экрана с нужным аттрибутом */
|
||||
void raw_renorm(void); /* выровнять позицию при смене под-режима (F3) */
|
||||
@@ -163,18 +169,29 @@ extern uint8_t g_ready; /* 1 когда первичный до
|
||||
extern uint8_t g_f8_enabled; /* можно ли сейчас переключать кодировку */
|
||||
|
||||
/* ================================================================== *
|
||||
* Отрисовка области/статус-бара/меню и прокрутка (mdview2_md.c).
|
||||
* Статус-бар (строка 0), меню (строка 31), спиннер — mdview2_status.c.
|
||||
* Атомарные части: prerender (фикс. хром, один раз) / encoding (поле кодировки)
|
||||
* / numbers (диапазон строк + %) / menu / spinner — обновляются по отдельности.
|
||||
* ================================================================== */
|
||||
void prerender_status(void); /* фикс. часть: фон, разделители, MDVIEW, имя файла (1 раз) */
|
||||
void status_encoding(void); /* только поле кодировки (col 37) */
|
||||
void render_md_status_numbers(void); /* числа MD: диапазон строк + % */
|
||||
void render_raw_status(void); /* RAW mode: метка режима */
|
||||
void render_full_status(void); /* encoding + numbers (MD) */
|
||||
void render_menu(void); /* строка меню (по смене режима/готовности) */
|
||||
uint16_t drawable_lines(void); /* число строк, готовых к показу */
|
||||
void spinner_tick(void); /* кадр спиннера загрузки */
|
||||
void spinner_show(uint8_t on); /* вкл/выкл спиннер */
|
||||
void render_percent_progress(uint8_t pct); /* показать процентный прогресс просмотра */
|
||||
|
||||
/* ================================================================== *
|
||||
* Отрисовка области MD-документа и прокрутка (mdview2_md.c).
|
||||
* ================================================================== */
|
||||
void draw_viewport_from_cache(void); /* перерисовать область документа из кэша */
|
||||
uint16_t drawable_lines(void); /* число строк, готовых к показу */
|
||||
void render_status_numbers(void); /* числовая часть статус-бара */
|
||||
void render_full_status(void); /* полная перерисовка статус-бара */
|
||||
void render_updated_status(void); /* обновить только числа статус-бара */
|
||||
void render_menu(void); /* перерисовать строку меню */
|
||||
void clamp_top(void); /* привести top_line к диапазону */
|
||||
void scroll_up(uint16_t n);
|
||||
void scroll_down(uint16_t n);
|
||||
void scroll_h(int8_t delta); /* горизонтальный сдвиг nowrap-блоков */
|
||||
void md_scroll_up(uint16_t n);
|
||||
void md_scroll_down(uint16_t n);
|
||||
void md_scroll_horizon(int8_t delta); /* горизонтальный сдвиг nowrap-блоков */
|
||||
uint8_t md_key(uint8_t scan); /* навигация MD (после загрузки); 1 = обработано */
|
||||
|
||||
/* ================================================================== *
|
||||
@@ -186,7 +203,7 @@ void show_help(void); /* модальный диалог F1 */
|
||||
* Парсер/индексатор (mdview2_index.c) и разделяемое с ним состояние.
|
||||
* Хранилище индекса (index_blk/pages/phys, n_lines/max_lines) — в ядре
|
||||
* (аллокация/doc-slot), индексатор пишет в него; cache_*-писатели и
|
||||
* progress_tick/spinner_tick ядро экспортирует индексатору.
|
||||
* progress_tick ядро экспортирует индексатору (spinner_tick — из status).
|
||||
* ================================================================== */
|
||||
#define INDEX_RECS_PER_PAGE 2048u /* 16384 / 8; запись всегда в одной странице */
|
||||
#define MAX_CACHE_LINE_LEN 255u /* кап длины контента строки в ячейках (буфер g_cells) */
|
||||
@@ -200,7 +217,6 @@ extern uint8_t g_abort; /* F10/Esc во время загруз
|
||||
uint8_t cache_reserve(uint16_t nbytes, uint8_t *out_page, uint16_t *out_off);
|
||||
void cache_commit(uint8_t page, uint16_t off, const void *buf, uint16_t len);
|
||||
void cache_dir_put(uint16_t idx, const cache_rec_t *r);
|
||||
void spinner_tick(void); /* анимация индикатора загрузки */
|
||||
void progress_tick(void); /* кооперативный шаг loading-loop (рисует/листает) */
|
||||
void index_lines(void); /* построить индекс по загруженному файлу */
|
||||
|
||||
|
||||
+48
-114
@@ -1,6 +1,7 @@
|
||||
/*
|
||||
* mdview2_md.c — представление MD-документа (peer к mdview2_raw.c): отрисовка
|
||||
* области из рендер-кэша, статус-бар, нижнее меню и прокрутка (верт./гориз.).
|
||||
* области из рендер-кэша + прокрутка (верт./гориз.) + навигация md_key().
|
||||
* Статус-бар, меню и спиннер — в mdview2_status.c (общие для MD/RAW).
|
||||
*
|
||||
* Работает поверх готового рендер-кэша (cache_dir_get → cache_rec_t) и
|
||||
* состояния навигации (top_line/viewport_x/n_lines, см. mdview2.h). Никакого
|
||||
@@ -11,8 +12,7 @@
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdio.h> /* dec8/dec16 */
|
||||
#include <conio.h> /* wrchar/gotoxy/cputs/textattr/scroll/COLOR */
|
||||
#include <conio.h> /* wrchar/scroll/COLOR */
|
||||
#include <bios/text.h> /* bios_set_place/bios_fillcharattr */
|
||||
#include "mdview2.h"
|
||||
|
||||
@@ -61,101 +61,6 @@ void draw_viewport_from_cache(void)
|
||||
}
|
||||
}
|
||||
|
||||
/* Число строк, готовых к показу. Во время загрузки последняя эмитированная
|
||||
* строка ещё в g_cells (не зафлашена), поэтому доступны [0..n_lines-2]. */
|
||||
uint16_t drawable_lines(void)
|
||||
{
|
||||
if (g_loading) return (n_lines >= 1) ? (uint16_t)(n_lines - 1) : 0;
|
||||
return n_lines;
|
||||
}
|
||||
|
||||
/* Возвращает процент прокрутки (0..100) относительно доступного диапазона. */
|
||||
static uint8_t calc_pct(void)
|
||||
{
|
||||
uint16_t total = drawable_lines();
|
||||
uint8_t pct;
|
||||
if (total <= VIEW_H)
|
||||
pct = 100;
|
||||
else
|
||||
pct = (uint8_t)((uint32_t)top_line * 100UL / (uint32_t)(total - VIEW_H));
|
||||
return pct;
|
||||
}
|
||||
|
||||
/* Рисует динамическую часть статус-бара без printf.
|
||||
* Полностью перезаписывает колонки 45..79, чтобы не оставлять «хвосты». */
|
||||
void render_status_numbers(void)
|
||||
{
|
||||
uint16_t total = drawable_lines();
|
||||
uint16_t last = top_line + VIEW_H;
|
||||
if (last > total) last = total;
|
||||
uint8_t pct = calc_pct();
|
||||
|
||||
textattr(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);
|
||||
cputs("L ");
|
||||
dec16(top_line + 1);
|
||||
cputs("-");
|
||||
dec16(last);
|
||||
cputs(" / ");
|
||||
dec16(total);
|
||||
if (g_loading) cputs("..."); /* ещё грузится */
|
||||
|
||||
wrchar(70, 0, 0xB3, ATTR_BAR); /* вертикальный разделитель */
|
||||
gotoxy( 73, 0);
|
||||
dec8(pct);
|
||||
cputs("%");
|
||||
}
|
||||
|
||||
/* Полностью перерисовывает статус-бар (заголовок + числа). */
|
||||
void render_full_status(void)
|
||||
{
|
||||
|
||||
fill_row(0, ATTR_BAR);
|
||||
/* Размещение в шапке: "MDVIEW", затем слот спиннера и имя файла. */
|
||||
put_str_attr(1, 0, "MDVIEW", ATTR_BAR);
|
||||
put_str_attr(10, 0, filename, ATTR_BAR);
|
||||
put_str_attr(37, 0, enc_name(g_encoding), ATTR_BAR); /* текущая кодировка */
|
||||
render_status_numbers();
|
||||
}
|
||||
|
||||
/* Обновляет только изменяемую числовую часть статус-бара. */
|
||||
void render_updated_status(void)
|
||||
{
|
||||
render_status_numbers();
|
||||
}
|
||||
|
||||
/* Отрисовывает нижнюю строку меню. */
|
||||
void render_menu(void)
|
||||
{
|
||||
/* Блок i = колонка i*8 (8 шириной): 2 поз. НОМЕР клавиши без 'F' (стиль
|
||||
* ATTR_MENU_K, ведущий «чёрный» пробел кроме F10) + 6 поз. ТЕКСТ-функция
|
||||
* (ATTR_MENU_T) сразу за номером. Номера рисуем для ВСЕХ 10 клавиш; текст —
|
||||
* только у задействованных и доступных сейчас. */
|
||||
fill_row(MENU_ROW, ATTR_MENU_T);
|
||||
|
||||
char num[3]; num[2] = 0;
|
||||
for (uint8_t i = 0; i < 9; i++) { /* F1..F9: ' 1'..' 9' */
|
||||
num[0] = ' '; num[1] = (char)('1' + i);
|
||||
put_str_attr((uint8_t)(i * 8), MENU_ROW, num, ATTR_MENU_K);
|
||||
}
|
||||
put_str_attr(9 * 8, MENU_ROW, "10", ATTR_MENU_K); /* F10 */
|
||||
|
||||
put_str_attr(0 * 8 + 2, MENU_ROW, "Help", ATTR_MENU_T);
|
||||
#if WITH_RAW
|
||||
if (g_ready) /* RAW доступен после постройки документа */
|
||||
put_str_attr(1 * 8 + 2, MENU_ROW, (g_view == VIEW_MD) ? "RAW" : "MD", ATTR_MENU_T);
|
||||
if (g_view != VIEW_MD) /* Wrap/Unwrap — только в RAW */
|
||||
put_str_attr(2 * 8 + 2, MENU_ROW,
|
||||
(g_view == VIEW_RAW_WRAP) ? "UnWrap" : "Wrap", ATTR_MENU_T);
|
||||
#endif
|
||||
if (g_f8_enabled) /* смена кодировки сейчас возможна */
|
||||
put_str_attr(7 * 8 + 2, MENU_ROW, "CodePg", ATTR_MENU_T);
|
||||
put_str_attr(9 * 8 + 2, MENU_ROW, "Exit", ATTR_MENU_T);
|
||||
}
|
||||
|
||||
/* ==================================================================
|
||||
* Прокрутка
|
||||
@@ -172,7 +77,7 @@ void clamp_top(void)
|
||||
}
|
||||
|
||||
/* Прокрутка вверх на n строк с частичной перерисовкой при n == 1. */
|
||||
void scroll_up(uint16_t n)
|
||||
void md_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) {
|
||||
@@ -187,7 +92,7 @@ void scroll_up(uint16_t n)
|
||||
}
|
||||
|
||||
/* Прокрутка вниз на n строк с частичной перерисовкой при n == 1. */
|
||||
void scroll_down(uint16_t n)
|
||||
void md_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;
|
||||
@@ -208,7 +113,7 @@ void scroll_down(uint16_t n)
|
||||
* Фаза 5: ширина берётся прямо из директории кэша (rec.len — уже
|
||||
* посчитанная реальная длина в ячейках после рендера), без единого
|
||||
* обращения к fb()/seg_off()/исходному файлу. */
|
||||
void scroll_h(int8_t delta)
|
||||
void md_scroll_horizon(int8_t delta)
|
||||
{
|
||||
uint16_t maxw = 0;
|
||||
for (uint8_t i = 0; i < VIEW_H; i++) {
|
||||
@@ -247,6 +152,14 @@ void scroll_h(int8_t delta)
|
||||
draw_viewport_from_cache();
|
||||
}
|
||||
|
||||
/* Процент прокрутки (0..100) относительно доступного диапазона. */
|
||||
static uint8_t calc_md_pct(void)
|
||||
{
|
||||
uint16_t total = drawable_lines();
|
||||
if (total <= VIEW_H) return 100;
|
||||
return (uint8_t)((uint32_t)top_line * 100UL / (uint32_t)(total - VIEW_H));
|
||||
}
|
||||
|
||||
/* ---- навигация MD после загрузки (peer к raw_key) ---------------- */
|
||||
|
||||
/* Обрабатывает навигационную клавишу в MD-режиме (вызывается из главного
|
||||
@@ -255,19 +168,40 @@ void scroll_h(int8_t delta)
|
||||
uint8_t md_key(uint8_t scan)
|
||||
{
|
||||
switch (scan) {
|
||||
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;
|
||||
draw_viewport_from_cache(); break;
|
||||
case KEY_END: top_line = (n_lines > VIEW_H) ? (uint16_t)(n_lines - VIEW_H) : 0;
|
||||
viewport_x = 0;
|
||||
draw_viewport_from_cache(); break;
|
||||
default: return 0; /* не наша клавиша */
|
||||
case KEY_UP:
|
||||
md_scroll_up(1);
|
||||
break;
|
||||
case KEY_DOWN:
|
||||
md_scroll_down(1);
|
||||
break;
|
||||
case KEY_LEFT:
|
||||
md_scroll_horizon(-(int8_t)HPAN_STEP);
|
||||
break;
|
||||
case KEY_RIGHT:
|
||||
md_scroll_horizon(+(int8_t)HPAN_STEP);
|
||||
break;
|
||||
case KEY_PGUP:
|
||||
md_scroll_up(VIEW_H);
|
||||
break;
|
||||
case KEY_PGDN:
|
||||
md_scroll_down(VIEW_H);
|
||||
break;
|
||||
case KEY_HOME:
|
||||
top_line = 0;
|
||||
viewport_x = 0;
|
||||
draw_viewport_from_cache();
|
||||
break;
|
||||
case KEY_END:
|
||||
top_line = (n_lines > VIEW_H) ? (uint16_t)(n_lines - VIEW_H) : 0;
|
||||
viewport_x = 0;
|
||||
draw_viewport_from_cache();
|
||||
break;
|
||||
|
||||
default:
|
||||
return 0; /* не наша клавиша */
|
||||
}
|
||||
render_updated_status();
|
||||
|
||||
render_percent_progress(calc_md_pct());
|
||||
render_md_status_numbers();
|
||||
return 1;
|
||||
}
|
||||
|
||||
+75
-117
@@ -45,21 +45,6 @@ static uint32_t raw_next_wrap(uint32_t off)
|
||||
return e; /* EOF */
|
||||
}
|
||||
|
||||
/* WRAP: начало предыдущего ряда. */
|
||||
// static uint32_t raw_prev_wrap(uint32_t off)
|
||||
// {
|
||||
// if (off == 0) return 0;
|
||||
// uint32_t ls = raw_line_start(off);
|
||||
// if (off > ls) /* тот же абзац: на 80 назад */
|
||||
// return off - SCREEN_W;
|
||||
// /* off == начало строки: последний ряд предыдущей физической строки.
|
||||
// * Последнюю 80-границу ищем циклом (без 32-битного деления). */
|
||||
// uint32_t pls = raw_line_start(off - 1); /* off-1 — это \n пред. строки */
|
||||
// uint32_t last = pls;
|
||||
// while (last + SCREEN_W < (off - 1)) last += SCREEN_W;
|
||||
// return last;
|
||||
// }
|
||||
|
||||
static uint32_t raw_prev_wrap(uint32_t off)
|
||||
{
|
||||
if (off == 0) return 0;
|
||||
@@ -74,15 +59,6 @@ static uint32_t raw_next_line(uint32_t off)
|
||||
while (off < file_size && fb(off) != '\n') off++;
|
||||
return (off < file_size) ? off + 1 : off;
|
||||
}
|
||||
// static uint32_t raw_prev_line(uint32_t off)
|
||||
// {
|
||||
// if (off == 0) return 0;
|
||||
// uint32_t p = off - 1; /* \n, завершивший пред. строку */
|
||||
// if (p == 0) return 0;
|
||||
// p--;
|
||||
// while (p != 0 && fb(p) != '\n') p--;
|
||||
// return (fb(p) == '\n') ? p + 1 : 0;
|
||||
// }
|
||||
|
||||
static uint32_t raw_prev_line(uint32_t off)
|
||||
{
|
||||
@@ -128,27 +104,6 @@ static uint32_t raw_draw_wrap_row(uint32_t off, uint8_t row)
|
||||
return raw_next_wrap(off);
|
||||
}
|
||||
|
||||
// static uint32_t raw_draw_hscroll_row(uint32_t off, uint8_t row)
|
||||
// {
|
||||
// uint32_t e = off;
|
||||
// while (e < file_size && fb(e) != '\n') e++; /* e = конец строки */
|
||||
// uint16_t len = e - off;
|
||||
|
||||
// uint8_t i = 0;
|
||||
// uint32_t p = off + raw_hpan;
|
||||
// while (i < SCREEN_W && p < e) {
|
||||
// char c = fb(p);
|
||||
// raw_cell(i, (uint8_t)(c == '\t' ? ' ' : c));
|
||||
// i++; p++;
|
||||
// }
|
||||
// for (; i < SCREEN_W; i++) raw_buf[i] = ' ';
|
||||
|
||||
// if (raw_hpan > 0) raw_buf[0] = '<';
|
||||
// if (len > (uint16_t)raw_hpan + SCREEN_W) raw_buf[SCREEN_W - 1] = '>';
|
||||
// raw_flush(row);
|
||||
// return (e < file_size) ? e + 1 : e;
|
||||
// }
|
||||
|
||||
static uint32_t raw_draw_hscroll_row(uint32_t off, uint8_t row)
|
||||
{
|
||||
uint32_t e = off;
|
||||
@@ -203,54 +158,18 @@ static uint32_t raw_prev(uint32_t off)
|
||||
return (g_view == VIEW_RAW_WRAP) ? raw_prev_wrap(off) : raw_prev_line(off);
|
||||
}
|
||||
|
||||
static void raw_scroll_down1(void)
|
||||
{
|
||||
/* Скроллим вниз ТОЛЬКО если контент уходит за нижний край. Проходим VIEW_H
|
||||
* рядов от raw_top: если контент кончился раньше (экран не полон) — внизу
|
||||
* была бы пустая строка, не скроллим; если ровно по нижний ряд (off==EOF) —
|
||||
* ниже пусто, тоже не скроллим. Иначе off = ряд сразу под экраном. */
|
||||
uint32_t off = raw_top;
|
||||
for (uint8_t r = 0; r < VIEW_H; r++) {
|
||||
if (off >= file_size) return; /* экран не заполнен контентом */
|
||||
off = raw_next(off);
|
||||
}
|
||||
if (off >= file_size) return; /* контент ровно по экран, ниже пусто */
|
||||
/* ---- статус-бар: % по байтам (отрисовка — render_raw_status_numbers
|
||||
* в mdview2_status.c, читает позицию через этот raw_pct) ---- */
|
||||
|
||||
scroll(0, VIEW_TOP_ROW, SCREEN_W, VIEW_H, 1, 0); /* содержимое вверх */
|
||||
raw_draw_one(off, (uint8_t)(VIEW_TOP_ROW + VIEW_H - 1)); /* новый нижний ряд */
|
||||
raw_top = raw_next(raw_top);
|
||||
}
|
||||
static void raw_scroll_up1(void)
|
||||
/* Процент по байтам без 32-битного деления: масштабируем offset'ы (>>8),
|
||||
* дальше 32-битная арифметика. raw_top приватен модулю, поэтому % считаем
|
||||
* здесь и отдаём готовым числом в статус-модуль. */
|
||||
uint8_t calc_raw_pct(void)
|
||||
{
|
||||
if (raw_top == 0) return;
|
||||
uint32_t nt = raw_prev(raw_top);
|
||||
scroll(0, VIEW_TOP_ROW, SCREEN_W, VIEW_H, 2, 0); /* содержимое вниз */
|
||||
(void)raw_draw_one(nt, VIEW_TOP_ROW);
|
||||
raw_top = nt;
|
||||
}
|
||||
|
||||
/* ---- статус-бар (без подсчёта строк: процент по байтам + режим) ---- */
|
||||
|
||||
/* Процент по байтам без 32-битного деления: масштабируем offset'ы в единицы
|
||||
* по 1 КБ (>>10), дальше 16-битная арифметика (a*100 ≤ 12800). */
|
||||
static uint8_t raw_pct(void)
|
||||
{
|
||||
uint16_t a = (uint16_t)(raw_top >> 10);
|
||||
uint16_t b = (uint16_t)(file_size >> 10);
|
||||
uint16_t a = (uint16_t)(raw_top >> 8);
|
||||
uint16_t b = (uint16_t)(file_size >> 8);
|
||||
if (b == 0) return 0; /* файл < 1 КБ */
|
||||
return (uint8_t)(a * 100u / b);
|
||||
}
|
||||
|
||||
void raw_status_numbers(void)
|
||||
{
|
||||
uint8_t pct = raw_pct();
|
||||
textattr(ATTR_BAR);
|
||||
gotoxy(47, 0);
|
||||
bios_writeattr_until(g_view == VIEW_RAW_WRAP ? "RAW wrap" : "RAW pan",
|
||||
20, ATTR_BAR, 0);
|
||||
gotoxy(73, 0);
|
||||
dec8(pct);
|
||||
cputs("% ");
|
||||
return (uint8_t)(a * 100UL / b);
|
||||
}
|
||||
|
||||
/* ---- посев позиции / пересев при смене активного буфера ----------- */
|
||||
@@ -271,6 +190,14 @@ void raw_home(void)
|
||||
raw_hpan = 0;
|
||||
}
|
||||
|
||||
void raw_end(void)
|
||||
{
|
||||
raw_top = file_size; /* отмотать VIEW_H рядов назад */
|
||||
for (uint8_t k = 0; k < VIEW_H; k++)
|
||||
raw_top = raw_prev(raw_top);
|
||||
raw_hpan = 0;
|
||||
}
|
||||
|
||||
void raw_screen_init(void) {
|
||||
bios_clearwin(1, 0, 30, 80, ATTR_TEXT);
|
||||
}
|
||||
@@ -283,58 +210,89 @@ void raw_renorm(void)
|
||||
raw_hpan = 0;
|
||||
}
|
||||
|
||||
void raw_scroll_down(uint8_t delta) {
|
||||
uint32_t off = raw_top;
|
||||
for (uint8_t r = 0; r < VIEW_H; r++) {
|
||||
if (off >= file_size)
|
||||
return; /* экран не заполнен контентом */
|
||||
off = raw_next(off);
|
||||
}
|
||||
if (off >= file_size)
|
||||
return; /* контент ровно по экран, ниже пусто */
|
||||
|
||||
if(delta ==1) {
|
||||
scroll(0, VIEW_TOP_ROW, SCREEN_W, VIEW_H, 1, 0); /* содержимое вверх */
|
||||
off = raw_draw_one(off, (uint8_t)(VIEW_TOP_ROW + VIEW_H - 1)); /* новый нижний ряд */
|
||||
raw_top = raw_next(raw_top);
|
||||
} else {
|
||||
for (uint8_t k = 0; k < delta; k++) {
|
||||
uint32_t n2 = raw_next(raw_top);
|
||||
if (n2 >= file_size)
|
||||
break;
|
||||
raw_top = n2;
|
||||
}
|
||||
raw_draw(); /* PgUp/PgDn/Home/End — полный экран */
|
||||
}
|
||||
}
|
||||
|
||||
void raw_scroll_up(uint8_t delta) {
|
||||
if (raw_top == 0)
|
||||
return;
|
||||
|
||||
if (delta == 1) {
|
||||
raw_top = raw_prev(raw_top);
|
||||
scroll(0, VIEW_TOP_ROW, SCREEN_W, VIEW_H, 2, 0); /* содержимое вниз */
|
||||
(void)raw_draw_one(raw_top, VIEW_TOP_ROW);
|
||||
} else {
|
||||
for (uint8_t k = 0; k < delta; k++)
|
||||
raw_top = raw_prev(raw_top);
|
||||
raw_draw(); /* PgUp/PgDn/Home/End — полный экран */
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- навигация (вызывается из главного цикла при g_view != VIEW_MD) - */
|
||||
|
||||
uint8_t raw_key(uint8_t scan)
|
||||
{
|
||||
uint32_t nt;
|
||||
|
||||
switch (scan) {
|
||||
case KEY_DOWN: raw_scroll_down1(); raw_status_numbers(); return 1;
|
||||
case KEY_UP: raw_scroll_up1(); raw_status_numbers(); return 1;
|
||||
|
||||
case KEY_DOWN:
|
||||
raw_scroll_down(1);
|
||||
break;
|
||||
case KEY_UP:
|
||||
raw_scroll_up(1);
|
||||
break;
|
||||
case KEY_PGDN:
|
||||
nt = raw_top;
|
||||
for (uint8_t k = 0; k < VIEW_H; k++) {
|
||||
uint32_t n2 = raw_next(nt);
|
||||
if (n2 >= file_size) break;
|
||||
nt = n2;
|
||||
}
|
||||
raw_scroll_down(VIEW_H);
|
||||
break;
|
||||
case KEY_PGUP:
|
||||
nt = raw_top;
|
||||
for (uint8_t k = 0; k < VIEW_H; k++) nt = raw_prev(nt);
|
||||
raw_scroll_up(VIEW_H);
|
||||
break;
|
||||
case KEY_HOME:
|
||||
nt = 0; raw_hpan = 0;
|
||||
raw_home();
|
||||
raw_draw();
|
||||
break;
|
||||
case KEY_END:
|
||||
nt = file_size; /* отмотать VIEW_H рядов назад */
|
||||
for (uint8_t k = 0; k < VIEW_H; k++) nt = raw_prev(nt);
|
||||
raw_hpan = 0;
|
||||
raw_end();
|
||||
raw_draw();
|
||||
break;
|
||||
|
||||
case KEY_LEFT:
|
||||
if (g_view == VIEW_RAW_HSCROLL && raw_hpan) {
|
||||
raw_hpan = (raw_hpan >= HPAN_STEP) ? (uint16_t)(raw_hpan - HPAN_STEP) : 0;
|
||||
raw_draw();
|
||||
raw_status_numbers();
|
||||
}
|
||||
return 1;
|
||||
break;
|
||||
case KEY_RIGHT:
|
||||
if (g_view == VIEW_RAW_HSCROLL) {
|
||||
if (raw_hpan < 248u) raw_hpan = (uint16_t)(raw_hpan + HPAN_STEP);
|
||||
if (g_view == VIEW_RAW_HSCROLL && raw_hpan < 248u) {
|
||||
raw_hpan = (uint16_t)(raw_hpan + HPAN_STEP);
|
||||
raw_draw();
|
||||
raw_status_numbers();
|
||||
}
|
||||
return 1;
|
||||
break;
|
||||
default:
|
||||
return 0; /* не наша клавиша */
|
||||
}
|
||||
|
||||
raw_top = nt;
|
||||
raw_draw(); /* PgUp/PgDn/Home/End — полный экран */
|
||||
raw_status_numbers();
|
||||
// render_raw_status_numbers();
|
||||
render_percent_progress(calc_raw_pct());
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
/*
|
||||
* mdview2_status.c — статус-бар (строка 0), нижнее меню (строка 31) и спиннер.
|
||||
*
|
||||
* Хром бара АТОМИЗИРОВАН — обновляется по частям, без перерисовки всего бара:
|
||||
* prerender_status() — фон + разделители │ + "MDVIEW" + имя файла (1 раз);
|
||||
* status_encoding() — только поле кодировки (col 37), по F8;
|
||||
* render_md_status_numbers() — числа MD: диапазон строк + % (при скролле);
|
||||
* render_raw_status_numbers()— числа RAW: метка режима + % по байтам;
|
||||
* render_full_status() — encoding + numbers (MD), при смене набора/режима;
|
||||
* render_menu() — строка меню, по смене режима/готовности;
|
||||
* spinner_tick/show — индикатор занятости во время загрузки.
|
||||
* Оба *_status_numbers() самоочищаются по геометрии разделителей (DIV1_X/DIV2_X
|
||||
* в mdview2.h) — фикс. хром не перерисовывается. RAW-позицию даёт raw_pct()
|
||||
* из mdview2_raw.c (raw_top там приватен).
|
||||
*
|
||||
* Раскладка строки 0:
|
||||
* 0 1..6 8 10.. 37..43 45 46..70 71 72..79
|
||||
* . MDVIEW spinner filename encod. │ L a-b / total │ pct%
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdio.h> /* dec8/dec16, cputs */
|
||||
#include <conio.h> /* textattr/gotoxy/wrchar/COLOR */
|
||||
#include <bios/text.h> /* bios_fillcharattr/bios_write_until */
|
||||
#include "mdview2.h"
|
||||
|
||||
/* ---- Спиннер (слот SPINNER_COL в баре) --------------------------- */
|
||||
static const char spinner_chars[4] = { '|', '/', '-', '\\' };
|
||||
static uint8_t spinner_phase = 0;
|
||||
static uint8_t spinner_active = 0;
|
||||
|
||||
/* Продвигает спиннер на один кадр (если включён). */
|
||||
void spinner_tick(void)
|
||||
{
|
||||
if (!spinner_active) return;
|
||||
wrchar(SPINNER_COL, 0, spinner_chars[spinner_phase & 3], ATTR_BAR_SPINNER);
|
||||
spinner_phase++;
|
||||
}
|
||||
|
||||
/* Вкл/выкл спиннер; при выключении гасит его позицию. */
|
||||
void spinner_show(uint8_t on)
|
||||
{
|
||||
spinner_active = on;
|
||||
if (!on) wrchar(SPINNER_COL, 0, ' ', ATTR_BAR);
|
||||
}
|
||||
|
||||
/* ---- Вычисления для чисел статус-бара ----------------------------- */
|
||||
|
||||
/* Число строк, готовых к показу. Во время загрузки последняя эмитированная
|
||||
* строка ещё в g_cells (не зафлашена), поэтому доступны [0..n_lines-2]. */
|
||||
uint16_t drawable_lines(void)
|
||||
{
|
||||
if (g_loading)
|
||||
return (n_lines >= 1) ? (uint16_t)(n_lines - 1) : 0;
|
||||
return n_lines;
|
||||
}
|
||||
|
||||
|
||||
/* ---- Атомарные части статус-бара ---------------------------------- */
|
||||
|
||||
/* Фиксированный хром бара — рисуется ОДИН раз при старте: фон, два
|
||||
* разделителя │, метка "MDVIEW" и имя файла (они не меняются за сессию). */
|
||||
void prerender_status(void)
|
||||
{
|
||||
fill_row(0, ATTR_BAR);
|
||||
wrchar(DIV1_X, 0, 0xB3, ATTR_BAR); /* │ */
|
||||
wrchar(DIV2_X, 0, 0xB3, ATTR_BAR); /* │ */
|
||||
put_str_attr(1, 0, "MDVIEW", ATTR_BAR);
|
||||
put_str_attr(10, 0, filename, ATTR_BAR);
|
||||
}
|
||||
|
||||
/* Поле текущей кодировки (col 37, 7 ячеек). Меняется только по F8. */
|
||||
void status_encoding(void)
|
||||
{
|
||||
textattr(ATTR_BAR);
|
||||
gotoxy(37, 0);
|
||||
bios_write_until(enc_name(g_encoding), 7, 0);
|
||||
}
|
||||
|
||||
/* Числовая часть (MD): "L a-b / total" между разделителями + "pct%" справа.
|
||||
* Перезаписывает ТОЛЬКО области между/после разделителей, сами │ и фикс.
|
||||
* хром не трогает. */
|
||||
void render_md_status_numbers(void)
|
||||
{
|
||||
uint16_t total = drawable_lines();
|
||||
uint16_t last = top_line + VIEW_H;
|
||||
if (last > total)
|
||||
last = total;
|
||||
|
||||
textattr(ATTR_BAR);
|
||||
gotoxy(DIV1_X + 1, 0);
|
||||
bios_fillcharattr(' ', ATTR_BAR, DIV2_X - DIV1_X - 1); /* очистить [DIV1_X+1 .. DIV2_X-1] */
|
||||
gotoxy(DIV1_X + 2, 0);
|
||||
cputs("L ");
|
||||
dec16(top_line + 1);
|
||||
cputs("-");
|
||||
dec16(last);
|
||||
cputs(" / ");
|
||||
dec16(total);
|
||||
if (g_loading) cputs("..."); /* ещё грузится */
|
||||
|
||||
}
|
||||
|
||||
/* Полное обновление переменной части (MD): кодировка + числа. */
|
||||
void render_full_status(void)
|
||||
{
|
||||
status_encoding();
|
||||
render_md_status_numbers();
|
||||
}
|
||||
|
||||
void render_percent_progress(uint8_t pct) {
|
||||
gotoxy(DIV2_X + 1, 0);
|
||||
bios_fillcharattr(' ', ATTR_BAR, SCREEN_W - DIV2_X - 1); /* очистить [DIV2_X+1 .. конец] */
|
||||
gotoxy(DIV2_X + 2, 0);
|
||||
dec8(pct);
|
||||
cputs("%");
|
||||
}
|
||||
|
||||
#if WITH_RAW
|
||||
/* RAW-вариант числовой части бара (peer к render_md_status_numbers): метка
|
||||
* режима между разделителями + процент справа. Сам очищает свои области (та же
|
||||
* геометрия DIVn_X), атомарен — не требует общего fill всего бара. Позиция
|
||||
* берётся готовым числом из raw_pct() (raw_top приватен mdview2_raw.c). */
|
||||
void render_raw_status(void)
|
||||
{
|
||||
// uint8_t pct = raw_pct();
|
||||
textattr(ATTR_BAR);
|
||||
gotoxy(DIV1_X + 1, 0);
|
||||
bios_writeattr_until(g_view == VIEW_RAW_WRAP ? " RAW wrap" : " RAW pan",
|
||||
DIV2_X - DIV1_X - 1, ATTR_BAR, 0); /* метка + пад до DIV2_X-1 */
|
||||
}
|
||||
#endif /* WITH_RAW */
|
||||
|
||||
/* ---- Нижнее меню (строка 31) -------------------------------------- */
|
||||
|
||||
void render_menu(void)
|
||||
{
|
||||
/* Блок i = колонка i*8 (8 шириной): 2 поз. НОМЕР клавиши без 'F' (стиль
|
||||
* ATTR_MENU_K, ведущий «чёрный» пробел кроме F10) + 6 поз. ТЕКСТ-функция
|
||||
* (ATTR_MENU_T) сразу за номером. Номера рисуем для ВСЕХ 10 клавиш; текст —
|
||||
* только у задействованных и доступных сейчас. */
|
||||
fill_row(MENU_ROW, ATTR_MENU_T);
|
||||
|
||||
char num[3]; num[2] = 0;
|
||||
for (uint8_t i = 0; i < 9; i++) { /* F1..F9: ' 1'..' 9' */
|
||||
num[0] = ' '; num[1] = (char)('1' + i);
|
||||
put_str_attr((uint8_t)(i * 8), MENU_ROW, num, ATTR_MENU_K);
|
||||
}
|
||||
put_str_attr(9 * 8, MENU_ROW, "10", ATTR_MENU_K); /* F10 */
|
||||
|
||||
put_str_attr(0 * 8 + 2, MENU_ROW, "Help", ATTR_MENU_T);
|
||||
#if WITH_RAW
|
||||
if (g_ready) /* RAW доступен после постройки документа */
|
||||
put_str_attr(1 * 8 + 2, MENU_ROW, (g_view == VIEW_MD) ? "RAW" : "MD", ATTR_MENU_T);
|
||||
if (g_view != VIEW_MD) /* Wrap/Unwrap — только в RAW */
|
||||
put_str_attr(2 * 8 + 2, MENU_ROW,
|
||||
(g_view == VIEW_RAW_WRAP) ? "UnWrap" : "Wrap", ATTR_MENU_T);
|
||||
#endif
|
||||
if (g_f8_enabled) /* смена кодировки сейчас возможна */
|
||||
put_str_attr(7 * 8 + 2, MENU_ROW, "CodePg", ATTR_MENU_T);
|
||||
put_str_attr(9 * 8 + 2, MENU_ROW, "Exit", ATTR_MENU_T);
|
||||
}
|
||||
Reference in New Issue
Block a user