From d1ea94ecbb1dc58b083646991c02f9fe2f79716b Mon Sep 17 00:00:00 2001 From: Alexeev Bronislav Date: Mon, 9 Nov 2026 23:35:57 +0700 Subject: [PATCH] improve ktuilib and add markdown editor --- Makefile | 4 +- README.md | 4 + docs/HIEROGLYPH_RU.md | 83 +++++++++ src/user/diagnostics.c | 19 ++- src/user/hieroglyph.c | 158 +++++++++++++++++ src/user/ktuilib.c | 2 + src/user/ktuilib.h | 18 ++ src/user/pager.c | 10 +- src/user/shell.c | 4 + src/user/tuidoc.c | 377 +++++++++++++++++++++++++++++++++++++++++ tools/smoke_test.py | 21 +++ tools/test_tui.c | 56 ++++++ 12 files changed, 739 insertions(+), 17 deletions(-) create mode 100644 docs/HIEROGLYPH_RU.md create mode 100644 src/user/hieroglyph.c create mode 100644 src/user/tuidoc.c diff --git a/Makefile b/Makefile index d71f7b3..c00028c 100644 --- a/Makefile +++ b/Makefile @@ -38,7 +38,7 @@ $(BIN_DIR)/user/SHELL.ELF: src/user/shell.c src/user/shellabi.h src/user/runtime $(BIN_DIR)/user/INIT.ELF: src/user/init.c src/user/runtime.h src/user/user.ld $(BIN_DIR)/user/runtime.o $(BIN_DIR)/user/start.o $(CC) $(CFLAGS) -c $< -o $(BIN_DIR)/user/INIT.o $(LD) -m elf_i386 -T src/user/user.ld $(BIN_DIR)/user/start.o $(BIN_DIR)/user/runtime.o $(BIN_DIR)/user/INIT.o -o $@ -TUI_PROGRAMS := KUNED VIK KBSM KMEMMAN KC KPROCM KDISKMAN KTUI KTTE PAGER JOURNAL +TUI_PROGRAMS := KUNED VIK KBSM KMEMMAN KC KPROCM KDISKMAN KTUI KTTE PAGER JOURNAL HIERO UTIL_PROGRAMS := TREE FIND GREP TAIL WC SORT KFSCK PROC USER_ELFS += $(addprefix $(BIN_DIR)/user/,$(addsuffix .ELF,$(UTIL_PROGRAMS))) USER_ELFS += $(addprefix $(BIN_DIR)/user/,$(addsuffix .ELF,$(TUI_PROGRAMS))) @@ -61,6 +61,8 @@ $(eval $(call TUI_RULE,KC,kc)) $(eval $(call TUI_RULE,KTTE,ktte)) $(eval $(call TUI_RULE,PAGER,pager)) $(eval $(call TUI_RULE,JOURNAL,journal)) +$(eval $(call TUI_RULE,HIERO,hieroglyph)) +$(BIN_DIR)/user/ktuilib.o: src/user/tuidoc.c define DIAG_RULE $(BIN_DIR)/user/$(1).ELF: src/user/diagnostics.c src/user/sysabi.h $(BIN_DIR)/user/ktuilib.o $(BIN_DIR)/user/runtime.o $(BIN_DIR)/user/start.o $$(CC) $$(CFLAGS) -DAPP=$(2) -c $$< -o $$(BIN_DIR)/user/$(1).o diff --git a/README.md b/README.md index 80b5264..a5e6aad 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,9 @@ # KintsugiOS +**Hieroglyph** — TUI для чтения и написания Markdown-документов: +`hieroglyph /HOME/NOTE.MD`, переключение режимов F3. +[Управление и формат](docs/HIEROGLYPH_RU.md). +

Минималистичная операционная 32 разрядная система с монолитным ядром и собственным загрузчиком на (N)ASM и GCC 15


diff --git a/docs/HIEROGLYPH_RU.md b/docs/HIEROGLYPH_RU.md new file mode 100644 index 0000000..6250231 --- /dev/null +++ b/docs/HIEROGLYPH_RU.md @@ -0,0 +1,83 @@ +# Hieroglyph — текстовые документы + +Запуск: `hieroglyph /HOME/NOTE.MD` или `hiero /HOME/NOTE.MD`. +Без пути создаётся новый документ. Отсутствующий файл открывается как новый; +существующий — в режиме чтения. В KTUI: пункт 9. + +Исполняемый файл `/BIN/HIERO.ELF`: полное имя не помещается в FAT 8.3, +поэтому `hieroglyph` — явный псевдоним shell. + +## Управление + +| Клавиша | Действие | +|---|---| +| F1 | Справка по разметке | +| F2 | Сохранить | +| F3 | Чтение / написание | +| F4 | Сохранить как | +| F10 / Esc | Выход; при изменениях — подтверждение отказа от них | +| Ctrl+T | Общая тема | +| Ctrl+Z / Ctrl+R | Undo / redo в режиме написания | + +В чтении: стрелки, PgUp/PgDn, Space, Home/End. Справа scrollbar, +внизу номер экранной страницы. Это не печатная пагинация. +В написании: стрелки, Home/End, PgUp/PgDn, Backspace/Delete, Enter и Tab +(четыре пробела). Длинные строки визуально переносятся без изменения файла. +Вертикальные движения редактора идут по исходным строкам, не экранным переносам. + +F3 показывает текущий буфер без необходимости сохранять его. После сохранения +путь обновляется; при ошибке буфер и отметка изменений остаются. Save as +спрашивает подтверждение замены существующего файла. + +## Разметка + +````text +# Заголовок +## Подзаголовок + +Абзац с **выделением**, *акцентом* и `кодом`. + +- Первый пункт +- Второй пункт + +1. Нумерованный пункт + +> Цитата + +--- + +```c +int main(void) { return 0; } +``` +```` + +В текущей ОС текст отображается печатным ASCII: примеры на русском иллюстрируют +синтаксис, но для корректного отображения содержимого используйте ASCII. +Заголовки и inline-выделение обозначаются цветами общих ролей темы; настоящего +курсива/изменения размера шрифта в VGA text mode нет. Цитаты имеют боковой +маркер, списки — маркеры, разделители и code fences — горизонтальные линии. +Внутри code fences inline-разметка не интерпретируется. Нумерованные списки +сохраняют явно записанную нумерацию. Пустые строки разделяют абзацы; +исходные переводы строк сохраняются при отображении. + +Это небольшой Markdown-поднабор, не CommonMark: нет вложенных списков, +таблиц, ссылок, изображений, HTML и встроенного выполнения команд. + +## Общие виджеты ktuilib + +`tui_editor` — владеющий историей, но не текстовым буфером многострочный виджет. +Caller предоставляет `text[capacity+1]`, длину и завершающий NUL. Состояние +нужно обнулить перед использованием. До восьми undo/redo-снимков; на каждое +изменение создаётся один снимок. Ошибка выделения памяти не меняет текст. +`tui_editor_destroy` освобождает историю; буфер освобождает приложение. + +`tui_document_key` — ограниченная прокрутка документа, используется также pager. +`tui_markdown` — общий проход layout/render: при height=0 только считает строки, +при ненулевой высоте рисует окно документа. Все этапы используют один перенос. +Реализация находится в `src/user/tuidoc.c`, включается в ktuilib и отдельно +не компилируется, чтобы сохранить простой механизм линковки TUI-программ. + +Документ ограничен 64 КиБ. Чтение принимает CRLF и нормализует его в LF; +сохранение использует LF. Бинарные NUL отвергаются. История не сохраняется +между запусками; undo пока посимвольный. Отметка изменений консервативная: +возврат через undo к сохранённому тексту не снимает её автоматически. diff --git a/src/user/diagnostics.c b/src/user/diagnostics.c index 3389c5f..119a292 100644 --- a/src/user/diagnostics.c +++ b/src/user/diagnostics.c @@ -81,7 +81,7 @@ int main(int argc, char** argv) { tui_window_draw(&applications); tui_text(11, 7, "1 Editor 2 Commander 3 Resource monitor", 0x1f); tui_text(11, 10, "4 Processes 5 Disks 6 Vik 7 Themes 8 Memory", 0x1f); - tui_text(11, 14, "Shared panels, modal dialogs, settings and gauges", 0x1e); + tui_text(11, 14, "9 Hieroglyph documents | H widget demo", 0x1e); tui_bar(23, "Choose application by number | Enter opens application menu"); #endif tui_bar( @@ -153,20 +153,23 @@ int main(int argc, char** argv) { : "FAT12 allocation and directory structure are consistent"); } #elif APP == 3 - const char* programs[] = { "KUNED", "KC", "KBSM", "KPROCM", "KDISKMAN", "VIK", "KTTE", "KMEMMAN" }; - const char* paths[] = { "/BIN/KUNED.ELF", "/BIN/KC.ELF", "/BIN/KBSM.ELF", "/BIN/KPROCM.ELF", - "/BIN/KDISKMAN.ELF", "/BIN/VIK.ELF", "/BIN/KTTE.ELF", "/BIN/KMEMMAN.ELF" }; + const char* programs[] = { "KUNED", "KC", "KBSM", "KPROCM", "KDISKMAN", + "VIK", "KTTE", "KMEMMAN", "HIERO" }; + const char* paths[] = { "/BIN/KUNED.ELF", "/BIN/KC.ELF", "/BIN/KBSM.ELF", + "/BIN/KPROCM.ELF", "/BIN/KDISKMAN.ELF", "/BIN/VIK.ELF", + "/BIN/KTTE.ELF", "/BIN/KMEMMAN.ELF", "/BIN/HIERO.ELF" }; if (key == '\n') { - const char* items[] = { "Editor", "Commander", "Resource monitor", "Processes", - "Disks", "Vik", "Theme editor", "Memory manager" }; - int choice = tui_menu("Launch application", items, 8, selected); + const char* items[] = { "Editor", "Commander", "Resource monitor", + "Processes", "Disks", "Vik", + "Theme editor", "Memory manager", "Hieroglyph documents" }; + int choice = tui_menu("Launch application", items, 9, selected); if (choice >= 0) { selected = choice; key = '1' + choice; } } (void)top; - if (key >= '1' && key <= '8') { + if (key >= '1' && key <= '9') { char* args[] = { (char*)programs[key - '1'], 0 }; int pid = spawn(paths[key - '1'], 1, args); if (pid < 0 || wait(pid)) { diff --git a/src/user/hieroglyph.c b/src/user/hieroglyph.c new file mode 100644 index 0000000..ca4545d --- /dev/null +++ b/src/user/hieroglyph.c @@ -0,0 +1,158 @@ +#include "ktuilib.h" +#define LIMIT 65536u +#define ROWS 20 + +static int save(tui_editor* editor, char path[256], int save_as) { + char destination[256]; + tui_copy(destination, path, 256); + if (save_as || !*path) { + if (!tui_prompt("Save document as (8.3 path, e.g. /HOME/NOTE.MD):", destination, 256)) { + return 0; + } + tui_stat stat; + if (!tui_stat_path(destination, &stat) && !tui_confirm("Replace existing document?")) { + return 0; + } + } + int result = syscall(10, (u32)destination, (u32)editor->text, editor->length, 0); + if (result) { + tui_message("Save failed", kos_error(result)); + return 0; + } + tui_copy(path, destination, 256); + return 1; +} + +int main(int argc, char** argv) { + if (argc > 2) { + write_all(2, "Usage: hieroglyph [FILE.MD]\n", 27); + return 2; + } + tui_editor editor = { 0 }; + editor.capacity = LIMIT; + editor.text = malloc(LIMIT + 1); + if (!editor.text) { + return 1; + } + char path[256] = ""; + int writing = argc < 2, dirty = 0; + if (argc == 2) { + tui_copy(path, argv[1], 256); + tui_stat stat; + int result = tui_stat_path(path, &stat); + if (result == -1) { + writing = 1; + } else if (result || stat.directory || stat.size > LIMIT) { + tui_clear(0x07); + tui_message( + "Cannot open document", + result ? kos_error(result) : "Not a regular document or exceeds 64 KiB"); + free(editor.text); + return 1; + } else { + int fd = open(path, KOS_O_READ); + if (fd < 0) { + free(editor.text); + return 1; + } + while (editor.length < stat.size) { + u32 chunk = stat.size - editor.length; + if (chunk > 4096) { + chunk = 4096; + } + int n = read(fd, editor.text + editor.length, chunk); + if (n <= 0) { + close(fd); + free(editor.text); + return 1; + } + editor.length += n; + } + close(fd); + /* Normalize CRLF; reject binary NUL rather than silently hiding data. */ + u32 out = 0; + for (u32 i = 0; i < editor.length; ++i) { + if (!editor.text[i]) { + free(editor.text); + return 1; + } + if (editor.text[i] == '\r' && i + 1 < editor.length && editor.text[i + 1] == '\n') { + continue; + } + editor.text[out++] = editor.text[i]; + } + editor.length = out; + } + } + editor.text[editor.length] = 0; + u32 top = 0; + const char* status = "Markdown document | F3 switches source / formatted view"; + for (;;) { + tui_clear(TUI_DESKTOP); + tui_bar(0, "Hieroglyph - documents"); + tui_textn(29, 0, *path ? path : "[New document]", 47, 0x70); + if (dirty) { + tui_text(78, 0, "*", 0x70); + } + tui_window paper = { 1, 1, 78, + 22, 1, writing ? "WRITE - Markdown source" : "READ - formatted document" }; + tui_window_draw(&paper); + u32 total = 1; + if (writing) { + tui_editor_draw(&editor, 3, 2, 73, ROWS); + } else { + total = tui_markdown(editor.text, editor.length, 3, 2, 73, 0, 0); + tui_document_key(0, &top, total, ROWS); + tui_markdown(editor.text, editor.length, 3, 2, 73, ROWS, top); + tui_scrollbar(76, 2, ROWS, top, ROWS, total); + } + tui_bar(23, status); + if (!writing) { + tui_text(57, 23, "Page", 0x70); + tui_number(62, 23, (top + ROWS - 1) / ROWS + 1, 0x70); + tui_text(67, 23, "/", 0x70); + tui_number(69, 23, (total + ROWS - 1) / ROWS, 0x70); + } + tui_bar(24, "F1 Help F2 Save F3 Read/Write F4 Save as F10 Quit | ^Z Undo ^R Redo ^T Theme"); + tui_present(79, 24); + int key = tui_key(1); + if (key == TUI_F10 || key == 27) { + if (!dirty || tui_confirm("Discard unsaved document changes?")) { + break; + } + } else if (key == TUI_F1) { + const char* help[] = { "# Heading (levels 1-6)", + "**strong** *emphasis* `inline code`", + "- List item; numbered lists: 1. Item", + "> Quoted paragraph", + "--- Horizontal separator", + "``` starts / ends a literal code block", + "READ: arrows, PgUp/PgDn, Space, Home/End", + "WRITE: arrows, Home/End, Tab, ^Z undo, ^R redo", + "F3 previews the current buffer without saving", + "ASCII text, 64 KiB; LF output; no HTML or images" }; + tui_menu("Hieroglyph - Markdown help", help, 10, 0); + } else if (key == TUI_F3) { + writing = !writing; + } else if (key == TUI_F2 || key == TUI_F4) { + if (save(&editor, path, key == TUI_F4)) { + dirty = 0; + status = "Document saved"; + } + } else if (writing) { + int result = tui_editor_key(&editor, key); + if (result > 0) { + dirty = 1; + status = "Modified | F3 preview"; + } + if (result < 0) { + status = "Cannot edit: document full or insufficient memory"; + } + } else { + tui_document_key(key, &top, total, ROWS); + } + } + tui_editor_destroy(&editor); + free(editor.text); + return 0; +} diff --git a/src/user/ktuilib.c b/src/user/ktuilib.c index 75d10b0..e0d43d3 100644 --- a/src/user/ktuilib.c +++ b/src/user/ktuilib.c @@ -1,4 +1,6 @@ #include "ktuilib.h" + +#include "tuidoc.c" static ktui_cell screen[25][80]; #define MODALS 4 static ktui_cell backgrounds[MODALS][25][80]; diff --git a/src/user/ktuilib.h b/src/user/ktuilib.h index 1dc8f26..80d2f8a 100644 --- a/src/user/ktuilib.h +++ b/src/user/ktuilib.h @@ -32,6 +32,24 @@ enum { typedef kos_stat tui_stat; typedef kos_dirent tui_entry; +/* Bounded multiline editor. Capacity excludes the required trailing NUL byte. + * Caller owns text[capacity+1]; widget owns up to eight undo/redo snapshots. */ +typedef struct { + char* text; + u32 length, capacity, cursor, top; + char* history[8]; + u32 lengths[8], cursors[8], history_count, history_position; +} tui_editor; + +void tui_editor_destroy(tui_editor* editor); +/* 1 changed, 0 navigation/unhandled, -1 full/allocation failure. */ +int tui_editor_key(tui_editor* editor, int key); +void tui_editor_draw(tui_editor* editor, int x, int y, int width, int height); +int tui_document_key(int key, u32* top, u32 total, u32 visible); +/* Same layout pass measures and draws: no separate wrapping implementation. + * Returns total rendered rows; height=0 measures without drawing. */ +u32 tui_markdown(const char* text, u32 length, int x, int y, int width, int height, u32 top); + /* Dedicated popup role; older themes inherit their selection color. */ #define TUI_POPUP 0x71 #define TUI_DESKTOP 0x72 diff --git a/src/user/pager.c b/src/user/pager.c index ae47fb1..e92182c 100644 --- a/src/user/pager.c +++ b/src/user/pager.c @@ -32,7 +32,7 @@ int main(int argc, char** argv) { column = 0; } } - u32 top = 0, selected = 0; + u32 top = 0; for (;;) { tui_clear(0x07); tui_bar(0, argc > 1 ? argv[1] : "PAGER - standard input"); @@ -56,13 +56,7 @@ int main(int argc, char** argv) { if (key == 'q' || key == 27) { break; } - u32 maximum = lines > 23 ? lines - 23 : 0; - if (key == ' ') { - key = TUI_PAGE_DOWN; - } - selected = top; - tui_list_key(key, &selected, &top, maximum + 1, 23); - top = selected; + tui_document_key(key, &top, lines, 23); } free(data); return 0; diff --git a/src/user/shell.c b/src/user/shell.c index 61b2802..0cf2993 100644 --- a/src/user/shell.c +++ b/src/user/shell.c @@ -156,6 +156,10 @@ static int parse(char* text, char** argv, int capacity) { } static void path(char* out, const char* name) { + if (equal(name, "hieroglyph")) { + copy(out, "/BIN/HIERO.ELF"); + return; + } if (equal(name, "sh")) { copy(out, "/BIN/SHELL.ELF"); return; diff --git a/src/user/tuidoc.c b/src/user/tuidoc.c new file mode 100644 index 0000000..2145c85 --- /dev/null +++ b/src/user/tuidoc.c @@ -0,0 +1,377 @@ +/* Included by ktuilib.c: document widgets share its drawing/clipping API. */ +static u32 doc_start(const char* t, u32 p) { + while (p && t[p - 1] != '\n') { + --p; + } + return p; +} + +static u32 doc_end(const char* t, u32 n, u32 p) { + while (p < n && t[p] != '\n') { + ++p; + } + return p; +} + +void tui_editor_destroy(tui_editor* e) { + for (u32 i = 0; i < e->history_count; ++i) { + free(e->history[i]); + } + e->history_count = e->history_position = 0; +} + +static int doc_snapshot(tui_editor* e) { + char* copy = malloc(e->length + 1); + if (!copy) { + return 0; + } + for (u32 i = 0; i <= e->length; ++i) { + copy[i] = e->text[i]; + } + while (e->history_count > e->history_position) { + free(e->history[--e->history_count]); + } + if (e->history_count == 8) { + free(e->history[0]); + for (u32 i = 1; i < 8; ++i) { + e->history[i - 1] = e->history[i]; + e->lengths[i - 1] = e->lengths[i]; + e->cursors[i - 1] = e->cursors[i]; + } + --e->history_count; + } + u32 at = e->history_count++; + e->history[at] = copy; + e->lengths[at] = e->length; + e->cursors[at] = e->cursor; + e->history_position = e->history_count; + return 1; +} + +int tui_editor_key(tui_editor* e, int key) { + if (!e || !e->text || e->length > e->capacity) { + return -1; + } + if (e->cursor > e->length) { + e->cursor = e->length; + } + if (key == 26 || key == 18) { + if ((key == 26 && !e->history_position) || (key == 18 && e->history_position == e->history_count)) { + return 0; + } + u32 at = key == 26 ? --e->history_position : e->history_position++; + char* copy = malloc(e->length + 1); + if (!copy) { + if (key == 26) { + ++e->history_position; + } else { + --e->history_position; + } + return -1; + } + for (u32 i = 0; i <= e->length; ++i) { + copy[i] = e->text[i]; + } + u32 n = e->length, p = e->cursor; + e->length = e->lengths[at]; + e->cursor = e->cursors[at]; + for (u32 i = 0; i <= e->length; ++i) { + e->text[i] = e->history[at][i]; + } + free(e->history[at]); + e->history[at] = copy; + e->lengths[at] = n; + e->cursors[at] = p; + return 1; + } + if (key == TUI_LEFT) { + if (e->cursor) { + --e->cursor; + } + return 0; + } + if (key == TUI_RIGHT) { + if (e->cursor < e->length) { + ++e->cursor; + } + return 0; + } + if (key == TUI_HOME) { + e->cursor = doc_start(e->text, e->cursor); + return 0; + } + if (key == TUI_END) { + e->cursor = doc_end(e->text, e->length, e->cursor); + return 0; + } + if (key == TUI_UP || key == TUI_DOWN || key == TUI_PAGE_UP || key == TUI_PAGE_DOWN) { + u32 col = e->cursor - doc_start(e->text, e->cursor); + u32 steps = key == TUI_PAGE_UP || key == TUI_PAGE_DOWN ? 20 : 1; + while (steps--) { + u32 start = doc_start(e->text, e->cursor), end = doc_end(e->text, e->length, e->cursor); + if (key == TUI_UP || key == TUI_PAGE_UP) { + if (!start) { + break; + } + end = start - 1; + start = doc_start(e->text, end); + } else { + if (end == e->length) { + break; + } + start = end + 1; + end = doc_end(e->text, e->length, start); + } + e->cursor = start + (col < end - start ? col : end - start); + } + return 0; + } + int remove = (key == 8 && e->cursor) || (key == TUI_DELETE && e->cursor < e->length); + u32 add = key == '\t' ? 4 : key == '\n' || (key >= 32 && key < 127) ? 1 : 0; + if (!remove && !add) { + return 0; + } + if (add > e->capacity - e->length || !doc_snapshot(e)) { + return -1; + } + if (remove) { + if (key == 8) { + --e->cursor; + } + for (u32 i = e->cursor; i < e->length; ++i) { + e->text[i] = e->text[i + 1]; + } + --e->length; + } else { + for (u32 i = e->length + 1; i > e->cursor; --i) { + e->text[i + add - 1] = e->text[i - 1]; + } + for (u32 i = 0; i < add; ++i) { + e->text[e->cursor++] = key == '\t' ? ' ' : key; + } + e->length += add; + } + return 1; +} + +int tui_document_key(int key, u32* top, u32 total, u32 visible) { + if (!top || !visible) { + return 0; + } + u32 maximum = total > visible ? total - visible : 0, old = *top; + if (*top > maximum) { + *top = maximum; + } + if (key == TUI_HOME) { + *top = 0; + } else if (key == TUI_END) { + *top = maximum; + } else if (key == TUI_UP || key == TUI_SCROLL_UP || key == TUI_PAGE_UP) { + u32 n = key == TUI_PAGE_UP ? visible : 1; + *top = *top > n ? *top - n : 0; + } else if (key == TUI_DOWN || key == TUI_SCROLL_DOWN || key == TUI_PAGE_DOWN || key == ' ') { + u32 n = key == TUI_PAGE_DOWN || key == ' ' ? visible : 1; + *top += n < maximum - *top ? n : maximum - *top; + } + return old != *top; +} + +void tui_editor_draw(tui_editor* e, int x, int y, int width, int height) { + if (width < 2 || width > 80 || height < 1 || height > 25) { + return; + } + u32 row = 0, col = 0, cr = 0, cc = 0; + for (u32 i = 0; i <= e->length; ++i) { + if (i == e->cursor) { + cr = row; + cc = col; + } + if (i == e->length) { + break; + } + if (e->text[i] == '\n' || ++col == (u32)width) { + ++row; + col = 0; + } + } + if (cr < e->top) { + e->top = cr; + } + if (cr >= e->top + height) { + e->top = cr - height + 1; + } + tui_fill(x, y, width, height, 0x07); + u32 r = 0, c = 0; + for (u32 i = 0; i < e->length; ++i) { + char ch = e->text[i]; + if (ch != '\n' && r >= e->top && r < e->top + height) { + char out[2] = { ch >= 32 && ch < 127 ? ch : ' ', 0 }; + tui_text(x + c, y + r - e->top, out, 0x07); + } + if (ch == '\n' || ++c == (u32)width) { + ++r; + c = 0; + } + } + char caret[2] = { e->cursor < e->length && e->text[e->cursor] >= 32 ? e->text[e->cursor] : ' ', 0 }; + tui_text(x + cc, y + cr - e->top, caret, TUI_FOCUS); + tui_scrollbar(x + width, y, height, e->top, height, row + 1); +} + +typedef struct { + int x, y, width, height; + u32 top, row, col; +} doc_layout; + +static void doc_put(doc_layout* d, char ch, unsigned char color) { + if (d->col == (u32)d->width) { + ++d->row; + d->col = 0; + } + if (d->row >= d->top && d->row - d->top < (u32)d->height) { + char out[2] = { ch >= 32 && ch < 127 ? ch : ' ', 0 }; + tui_text(d->x + d->col, d->y + d->row - d->top, out, color); + } + ++d->col; +} + +u32 tui_markdown(const char* text, u32 length, int x, int y, int width, int height, u32 top) { + if (!text || width < 4 || width > 80 || height < 0 || height > 25) { + return 0; + } + doc_layout d = { x, y, width, height, top, 0, 0 }; + if (height) { + tui_fill(x, y, width, height, 0x07); + } + int fence = 0; + for (u32 start = 0; start < length;) { + u32 end = start; + while (end < length && text[end] != '\n') { + ++end; + } + u32 p = start; + unsigned char base = 0x07; + if (end - p >= 3 && text[p] == '`' && text[p + 1] == '`' && text[p + 2] == '`') { + fence = !fence; + base = TUI_BORDER; + for (int i = 0; i < width; ++i) { + doc_put(&d, '-', base); + } + } else { + if (fence) { + base = TUI_STRING; + } else { + u32 hashes = 0; + while (p + hashes < end && text[p + hashes] == '#' && hashes < 6) { + ++hashes; + } + if (hashes && p + hashes < end && text[p + hashes] == ' ') { + p += hashes + 1; + base = TUI_KEYWORD; + } else if (p < end && text[p] == '>') { + ++p; + if (p < end && text[p] == ' ') { + ++p; + } + base = TUI_COMMENT; + doc_put(&d, '|', base); + doc_put(&d, ' ', base); + } else if ( + end - p >= 2 && (text[p] == '-' || text[p] == '*' || text[p] == '+') + && text[p + 1] == ' ') { + p += 2; + doc_put(&d, ' ', TUI_MARKED); + doc_put(&d, '*', TUI_MARKED); + doc_put(&d, ' ', base); + } else { + u32 q = p; + while (q < end && text[q] >= '0' && text[q] <= '9') { + ++q; + } + if (q > p && q + 1 < end && text[q] == '.' && text[q + 1] == ' ') { + doc_put(&d, ' ', base); + while (p <= q) { + doc_put(&d, text[p++], TUI_MARKED); + } + ++p; + doc_put(&d, ' ', base); + } + } + if (end - p >= 3 && text[p] == '-' && text[p + 1] == '-' && text[p + 2] == '-') { + u32 q = p; + while (q < end && (text[q] == '-' || text[q] == ' ' || text[q] == '\r')) { + ++q; + } + if (q == end) { + for (int i = 0; i < width; ++i) { + doc_put(&d, '-', TUI_BORDER); + } + p = end; + } + } + } + int bold = 0, italic = 0, code = 0; + while (p < end) { + char ch = text[p]; + if (ch == '\r') { + ++p; + continue; + } + if (!fence && ch == '\\' && p + 1 < end) { + ++p; + ch = text[p++]; + doc_put(&d, ch, base); + continue; + } + if (!fence && ch == '`') { + u32 close = p + 1; + while (close < end && text[close] != '`') { + ++close; + } + if (code || close < end) { + code = !code; + ++p; + continue; + } + } + if (!fence && !code && ch == '*') { + int strong = p + 1 < end && text[p + 1] == '*'; + u32 close = p + (strong ? 2 : 1); + while ( + close < end + && !( + text[close] == '*' && (!strong || (close + 1 < end && text[close + 1] == '*')))) { + ++close; + } + if ((strong ? bold : italic) || close < end) { + if (strong) { + bold = !bold; + p += 2; + } else { + italic = !italic; + ++p; + } + continue; + } + } + /* Word wrapping before a source word; long words still hard-wrap. */ + if (!fence && ch != ' ' && (p == start || text[p - 1] == ' ')) { + u32 word = 0; + while (p + word < end && text[p + word] > ' ') { + ++word; + } + if (word <= (u32)width && d.col && word > (u32)width - d.col) { + ++d.row; + d.col = 0; + } + } + doc_put(&d, ch, code ? TUI_STRING : bold ? TUI_KEYWORD : italic ? TUI_WARNING : base); + ++p; + } + } + ++d.row; + d.col = 0; + start = end < length ? end + 1 : end; + } + return d.row ? d.row : 1; +} diff --git a/tools/smoke_test.py b/tools/smoke_test.py index b5c35eb..3067e41 100644 --- a/tools/smoke_test.py +++ b/tools/smoke_test.py @@ -227,6 +227,7 @@ def command(text): ":": "shift-semicolon", "=": "equal", "$": "shift-4", + "#": "shift-3", }.get(ch, ch) # Explicit transitions avoid deferred HMP release timers. keys = key.split("-") @@ -452,6 +453,25 @@ def command(text): wait_for("Kintsugi application launcher") monitor("sendkey q 10") time.sleep(0.2) + command("hieroglyph /note.md") + wait_for("WRITE - Markdown source") + command("# Document") + command("- Item") + monitor("sendkey f3 10") + wait_for("READ - formatted document") + wait_for("Document") + monitor("sendkey f2 10") + wait_for("Document saved") + monitor("sendkey f10 10") + time.sleep(.2) + command("hieroglyph /note.md") + wait_for("READ - formatted document") + monitor("sendkey end 10") + time.sleep(.1) + monitor("sendkey home 10") + time.sleep(.1) + monitor("sendkey f10 10") + time.sleep(.2) command("kuned /edit.txt") wait_for("Kintsugi Universal Editor") command("editor-content") @@ -665,6 +685,7 @@ def command(text): and "ACPI S5 power-off requested" in serial ) actual = subprocess.check_output(["mtype", "-i", str(disk), "::/CHECK.TXT"]) + assert subprocess.check_output(["mtype", "-i", str(disk), "::/NOTE.MD"]) == b"# Document\n- Item\n" assert ( subprocess.check_output(["mtype", "-i", str(disk), "::/EDIT.TXT"]) == b"editor-content\n" diff --git a/tools/test_tui.c b/tools/test_tui.c index 2353360..8976dfe 100644 --- a/tools/test_tui.c +++ b/tools/test_tui.c @@ -38,6 +38,62 @@ static void remember(const char* t, u32 n, u32 p) { #include "../src/user/vikmode.h" int main(void) { + char document[65] = "abc\nxyz"; + tui_editor editor = { .text = document, .length = 7, .capacity = 64, .cursor = 1 }; + assert(tui_editor_key(&editor, 'Q') == 1 && !strcmp(document, "aQbc\nxyz")); + assert(tui_editor_key(&editor, 26) == 1 && !strcmp(document, "abc\nxyz")); + assert(tui_editor_key(&editor, 18) == 1 && !strcmp(document, "aQbc\nxyz")); + tui_editor_key(&editor, 26); + tui_editor_key(&editor, 'W'); + assert(!tui_editor_key(&editor, 18) && !strcmp(document, "aWbc\nxyz")); + tui_editor_key(&editor, TUI_DOWN); + assert(editor.cursor == 7); + tui_editor_key(&editor, TUI_END); + assert(editor.cursor == editor.length); + tui_editor_draw(&editor, 1, 1, 8, 3); + tui_editor_destroy(&editor); + char small[17] = ""; + tui_editor bounded = { .text = small, .capacity = 16 }; + for (int i = 0; i < 12; ++i) { + assert(tui_editor_key(&bounded, 'a' + i) == 1); + } + for (int i = 0; i < 8; ++i) { + assert(tui_editor_key(&bounded, 26) == 1); + } + assert(bounded.length == 4 && !tui_editor_key(&bounded, 26)); + for (int i = 0; i < 8; ++i) { + assert(tui_editor_key(&bounded, 18) == 1); + } + assert(bounded.length == 12 && !strcmp(small, "abcdefghijkl")); + tui_editor_destroy(&bounded); + editor.capacity = editor.length; + assert(tui_editor_key(&editor, 'x') == -1); + u32 doc_top = 0; + assert(tui_document_key(TUI_END, &doc_top, 51, 20) && doc_top == 31); + assert(tui_document_key(TUI_PAGE_UP, &doc_top, 51, 20) && doc_top == 11); + tui_document_key(TUI_END, &doc_top, 1, 20); + assert(doc_top == 0); + const char* md = "# Title\n\n- Item\n> Quote\n```\n**literal**\n```\n"; + u32 measured = tui_markdown(md, strlen(md), 1, 1, 30, 0, 0); + assert(measured == 7); + assert(tui_markdown(md, strlen(md), 1, 1, 30, 10, 0) == measured); + assert(screen[1][1].character == 'T' && screen[3][2].character == '*'); + assert(screen[6][1].character == '*' && screen[6][2].character == '*'); + const char* wrapped = "abc defgh ijk"; + assert(tui_markdown(wrapped, strlen(wrapped), 1, 1, 8, 0, 0) == 3); + tui_markdown(md, strlen(md), 1, 1, 30, 2, 5); + assert(screen[1][1].character == '*'); + char pages[201]; + for (int i = 0; i < 100; ++i) { + pages[i * 2] = 'x'; + pages[i * 2 + 1] = '\n'; + } + pages[200] = 0; + assert(tui_markdown(pages, 200, 1, 1, 30, 0, 0) == 100); + assert(tui_markdown(pages, 200, 1, 1, 30, 20, 80) == 100); + assert(screen[20][1].character == 'x'); + tui_markdown("*unclosed", 9, 1, 1, 30, 1, 0); + assert(screen[1][1].character == '*'); vik_state vim = { 0 }; char text[CAPACITY] = "one two\nthree\nfour\n"; u32 length = strlen(text), cursor = 0;