From fd72f1a0ac01d6f78089f1bd820ebc010699b34c Mon Sep 17 00:00:00 2001 From: Alexeev Bronislav Date: Mon, 9 Nov 2026 17:57:34 +0700 Subject: [PATCH] add TUI widgets, improve TUI programs and TUI lib --- docs/TUI_WIDGETS_RU.md | 85 +++++++++++ src/kernel/drivers/keyboard.c | 7 +- src/kernel/drivers/keyboard.h | 13 +- src/user/diagnostics.c | 34 ++++- src/user/kc.c | 133 ++++++++++++++--- src/user/ktte.c | 52 +++++-- src/user/ktuilib.c | 182 ++++++++++++++++++----- src/user/ktuilib.h | 57 ++++++- src/user/kuned.c | 107 +++++++++---- src/user/syntax.h | 129 ++++++++++++++++ src/user/vikmode.h | 272 ++++++++++++++++++++++++++++++++++ tools/smoke_test.py | 4 + tools/test_tui.c | 65 ++++++++ 13 files changed, 1037 insertions(+), 103 deletions(-) create mode 100644 docs/TUI_WIDGETS_RU.md create mode 100644 src/user/syntax.h create mode 100644 src/user/vikmode.h diff --git a/docs/TUI_WIDGETS_RU.md b/docs/TUI_WIDGETS_RU.md new file mode 100644 index 0000000..dfdb90c --- /dev/null +++ b/docs/TUI_WIDGETS_RU.md @@ -0,0 +1,85 @@ +# Общие виджеты и командерное управление + +## Библиотека + +`ktuilib` предоставляет окна с clipping, вложенные модальные окна, меню, +списки с прокруткой, полосы прогресса и следующие переиспользуемые виджеты: + +- `tui_input`: ограниченный NUL-terminated буфер, курсор, Home/End, + Left/Right, Backspace/Delete, вставка в середину и горизонтальная прокрутка; +- `tui_button`: обычное, сфокусированное и disabled-состояние; +- `tui_checkbox`, `tui_radio`: отображение состояния; +- `tui_toggle_key`, `tui_focus_key`: Space для переключения и Tab для обхода; +- `tui_list_draw`: общий список с маркером выбора и scrollbar. + +Поле ввода не владеет буфером; capacity включает завершающий NUL. Перед +отрисовкой буфер должен быть корректной строкой. Radio рисует один элемент, +а взаимоисключение элементов группы контролирует приложение. +В KTUI клавиша H открывает интерактивный пример виджетов. +Общие prompt/confirm/message используют новые поля и кнопки. + +## Роли тем + +Старый формат `XX=YY` и все прежние атрибуты сохраняются. Новые роли: + +| Ключ | Назначение | +|---|---| +| 73 | Поле ввода | +| 74 | Кнопка | +| 75 | Фокус | +| 76 | Недоступный элемент | +| 77 / 78 / 79 | Ошибка / предупреждение / успех | +| 7A / 7B / 7C | Маркировка файлов / каталог / рамка | +| 7D / 7E / 7F / 80 | Комментарий / строка / число / ключевое слово | +| 81 / 82 | Visual-выделение / статус (доступная роль для приложений) | + +Отсутствующие роли наследуют соответствующие старые цвета, а не отображаются +как VGA-атрибут собственного номера. KTTE позволяет редактировать эти роли +в прокручиваемом простом режиме и сохраняет все 256 отображений. +Не каждый старый жёстко заданный цвет приложений заменён семантической ролью. + +## KC + +F1 справка, F2 сведения, F3 просмотр, F4 редактор, F5 копирование, +F6 перемещение, F7 mkdir, F8 удаление, F9 настройки, F10 выход. +Прежние буквенные команды работают. Tab меняет панель, T создаёт вкладку, +`[`/`]` переключают, X закрывает вкладку, `/` открывает каталог. + +Insert или Space маркируют запись и переходят к следующей; `+` маркирует все, +`-` снимает маркировку. Родитель `..` не маркируется. C/M/D и F5/F6/F8 +применяются к отмеченным записям, а при отсутствии отметок — к текущей. +Перед групповой операцией требуется подтверждение. При частичном успехе +показывается отдельное сообщение; отката нет. Маркировка привязана к именам, +при смене каталога сбрасывается. + +Это не полная копия MC: нет рекурсивного копирования/удаления, перезаписи +существующих целей, VFS архивов и удалённых ФС. Действуют ограничения FAT12 +ASCII 8.3, до 256 записей в панели и четыре вкладки. + +## Vik и kuned + +Vik не использует Ctrl+Q и Ctrl+S. Выход: `:q`, `:q!`, сохранение: `:w`, +`:w PATH`, `:wq`. Esc возвращает из insert в normal, из visual — снимает выбор. + +Дополнительно к прежним командам: + +- `h/j/k/l`, `w/b/e`, `0/^/$`, `G`, `числоG`, числовые повторы движений; +- `d/c/y` + движение, `dd/cc/yy`, числовые повторы операторов (`2dw`, `d2w`); +- `D/C`, `x`, `p/P`, `v/V` с `d/c/y`; +- `"a`–`"z` выбирают регистр следующего yank/delete/paste; +- `u` и Ctrl+R переключают undo/redo (восемь снимков). + +Именованные регистры независимы; совместимость со всеми специальными +регистрами Vim не заявляется. Insert пока сохраняет снимок на каждый символ, +а не на всю сессию вставки. Нет regex, macros, dot-repeat, text objects, +split windows, plugins, полной Ex-грамматики и полной семантики курсора Vim. +Редактор ограничен 64 КиБ. Read-only запрещает изменение через операторы. + +Оба редактора выбирают подсветку по расширению: C/H/CPP/JS/TS, ASM/S/INC, +SH/PY, CFG/INI/THM; остальные файлы — обычный текст. Лексер различает строки, +числа, базовые ключевые слова и комментарии соответствующего семейства, +включая многострочные C-комментарии. Это не полноценный парсер языка. + +KPROCM/KDISKMAN/KTUI поддерживают F1 и F10; KDISKMAN показывает состояние +тома через роли успеха/ошибки. Изменены клавиатурные события: пересобирайте +ядро и системные ELF вместе. diff --git a/src/kernel/drivers/keyboard.c b/src/kernel/drivers/keyboard.c index 89a6aa6..d4a4e4e 100644 --- a/src/kernel/drivers/keyboard.c +++ b/src/kernel/drivers/keyboard.c @@ -59,6 +59,9 @@ static void keyboard_callback(registers_t registers) { } if (ext) { switch (code) { + case 0x52: + enqueue(KEY_INSERT); + break; case 0x49: enqueue(KEY_PAGE_UP); break; @@ -89,7 +92,9 @@ static void keyboard_callback(registers_t registers) { } return; } - if (code == 0x1c) { + if (code >= 0x3b && code <= 0x44) { + enqueue(KEY_F1 + code - 0x3b); + } else if (code == 0x1c) { enqueue('\n'); } else if (code == 1) { enqueue(27); diff --git a/src/kernel/drivers/keyboard.h b/src/kernel/drivers/keyboard.h index 2306d3e..3ee04ec 100644 --- a/src/kernel/drivers/keyboard.h +++ b/src/kernel/drivers/keyboard.h @@ -12,7 +12,18 @@ enum { KEY_SCROLL_UP, KEY_SCROLL_DOWN, KEY_PAGE_UP, - KEY_PAGE_DOWN + KEY_PAGE_DOWN, + KEY_INSERT, + KEY_F1, + KEY_F2, + KEY_F3, + KEY_F4, + KEY_F5, + KEY_F6, + KEY_F7, + KEY_F8, + KEY_F9, + KEY_F10 }; void init_keyboard(void); diff --git a/src/user/diagnostics.c b/src/user/diagnostics.c index 6fb0a23..9ef7b06 100644 --- a/src/user/diagnostics.c +++ b/src/user/diagnostics.c @@ -41,7 +41,7 @@ int main(int argc, char** argv) { for (int i = top; i < count && i < (int)top + 14; ++i) { int y = i - top + 4; unsigned char color = i == (int)selected ? 0x70 : 0x1f; - tui_fill(1, y, 77, 1, color); + tui_choice_row(1, y, 77, "", i == (int)selected, 0x1f); tui_number(2, y, list[i].pid, color); tui_number(8, y, list[i].parent, color); tui_text(14, y, list[i].state < 5 ? states[list[i].state] : "?", color); @@ -66,6 +66,11 @@ int main(int argc, char** argv) { tui_number(35, 6 + i * 2, values[i], 0x1e); } tui_gauge(3, 15, 70, info.clusters - info.free_clusters, info.clusters); + tui_text( + 3, + 18, + info.mounted ? "Volume online" : "Volume unavailable", + info.mounted ? TUI_SUCCESS : TUI_ERROR); tui_bar(23, "C check filesystem | No partitioning, formatting or repair operations"); #else tui_bar(0, "KTUI - Kintsugi application launcher"); @@ -81,6 +86,12 @@ int main(int argc, char** argv) { paused ? "Paused | Space resume | H help | Q / Esc quit" : "Space pause | H help | Q / Esc quit"); tui_present(79, 24); int key = tui_key(paused ? 1 : 2); + if (key == TUI_F1) { + key = 'h'; + } + if (key == TUI_F10) { + key = 'q'; + } if (key == 'q' || key == 27) { return 0; } @@ -151,7 +162,26 @@ int main(int argc, char** argv) { } } if (key == 'h') { - tui_message("KTUI demo", "Clipped text, boxes, modal input and persistent settings."); + int checked = 1; + u32 focus = 0; + if (tui_modal_begin("Shared widgets - Tab focus / Space toggle / Esc close", 70, 13)) { + for (;;) { + tui_checkbox(8, 9, "Example option", checked, focus == 0); + tui_radio(8, 11, "Selected radio option", 1, focus == 1); + tui_button(8, 14, 18, "Close", focus == 2, 1); + tui_button(30, 14, 18, "Unavailable", 0, 0); + tui_present(79, 24); + int event = tui_key(1); + if (event == 27 || (event == '\n' && focus == 2)) { + break; + } + tui_focus_key(event, &focus, 3); + if (focus == 0) { + tui_toggle_key(event, &checked); + } + } + tui_modal_end(); + } } #endif } diff --git a/src/user/kc.c b/src/user/kc.c index a0dd227..4179932 100644 --- a/src/user/kc.c +++ b/src/user/kc.c @@ -6,17 +6,90 @@ typedef struct { char directory[256]; char previous[256]; u32 selected, top; + char marked[ENTRIES][13]; + u32 marks; } tab; typedef struct { tab tabs[TABS]; u32 active, count, entries_count; tui_entry entries[ENTRIES]; + char loaded_directory[256]; } panel; static panel panels[2]; static unsigned char settings[2] = { 1, 1 }; /* sizes, directories first */ +static int same(const char* a, const char* b) { + while (*a && *a == *b) { + ++a; + ++b; + } + return *a == *b; +} + +static int marked(tab* t, const char* name) { + for (u32 i = 0; i < t->marks; ++i) { + if (same(t->marked[i], name)) { + return i; + } + } + return -1; +} + +static int is_parent(const tui_entry* e) { + return same(e->name, ".."); +} + +static void toggle(tab* t, const tui_entry* e) { + if (is_parent(e)) { + return; + } + int at = marked(t, e->name); + if (at >= 0) { + --t->marks; + tui_copy(t->marked[at], t->marked[t->marks], 13); + } else if (t->marks < ENTRIES) { + tui_copy(t->marked[t->marks++], e->name, 13); + } +} + +static const char* batch(panel* p, panel* other, int key) { + tab* t = &p->tabs[p->active]; + if (!p->entries_count) { + return "Empty directory"; + } + if (!tui_confirm( + key == 'd' ? "Delete marked entries (or current entry)?" + : key == 'm' ? "Move marked entries to other panel?" + : "Copy marked entries to other panel?")) { + return "Cancelled"; + } + u32 failed = 0, done = 0; + for (u32 i = 0; i < p->entries_count; ++i) { + tui_entry* e = &p->entries[i]; + if (is_parent(e) || (t->marks ? marked(t, e->name) < 0 : i != t->selected)) { + continue; + } + char source[256], dest[256]; + tui_path(source, t->directory, e->name); + tui_path(dest, other->tabs[other->active].directory, e->name); + int result = key == 'd' ? syscall(11, (u32)source, e->stat.directory ? 2 : 1, 0, 0) + : syscall(12, (u32)source, (u32)dest, key == 'm', 0); + if (result) { + ++failed; + } else { + ++done; + } + } + t->marks = 0; + if (failed) { + return done ? "Partially completed; failures: existing target/nonempty directory/I/O" + : "Failed: existing target, nonempty directory, permissions or I/O"; + } + return done ? "Group operation completed" : "No actionable entries"; +} + static int compare(const tui_entry* a, const tui_entry* b) { if (settings[1] && a->stat.directory != b->stat.directory) { return b->stat.directory - a->stat.directory; @@ -33,6 +106,11 @@ static int compare(const tui_entry* a, const tui_entry* b) { } static int refresh(panel* p) { + tab* current = &p->tabs[p->active]; + if (!same(p->loaded_directory, current->directory)) { + current->marks = 0; + tui_copy(p->loaded_directory, current->directory, 256); + } u32 cursor = 0; int result = 0; p->entries_count = 0; @@ -81,12 +159,19 @@ static void draw(panel* p, int x, int active) { tui_textn(x + 1, 3, t->directory, 38, 0x1e); for (u32 i = t->top; i < p->entries_count && i < t->top + 18; ++i) { int y = 4 + i - t->top; - unsigned char color = active && i == t->selected ? 0x70 : 0x17; - tui_fill(x + 1, y, 38, 1, color); - tui_text(x + 2, y, p->entries[i].stat.directory ? "/" : " ", color); - tui_textn(x + 3, y, p->entries[i].name, 13, color); + int mark = marked(t, p->entries[i].name) >= 0; + unsigned char color = mark ? TUI_MARKED : p->entries[i].stat.directory ? TUI_DIRECTORY : 0x17; + tui_choice_row(x + 1, y, 37, "", active && i == t->selected, color); + tui_text( + x + 3, + y, + mark ? "*" + : p->entries[i].stat.directory ? "/" + : " ", + active && i == t->selected ? 0x70 : color); + tui_textn(x + 4, y, p->entries[i].name, 13, active && i == t->selected ? 0x70 : color); if (settings[0]) { - tui_number(x + 26, y, p->entries[i].stat.size, color); + tui_number(x + 26, y, p->entries[i].stat.size, active && i == t->selected ? 0x70 : color); } } tui_scrollbar(x + 38, 4, 18, t->top, 18, p->entries_count); @@ -103,7 +188,7 @@ int main(int argc, char** argv) { syscall(4, (u32)panels[i].tabs[0].directory, 256, 0, 0); } int active = 0; - const char* status = "Tab panel | T new tab | [ ] switch tab | X close tab | O settings"; + const char* status = "Tab panel | Ins/Space mark | +/- all/none | T new tab | [ ] tabs"; for (;;) { for (int i = 0; i < 2; ++i) { if (refresh(&panels[i]) < 0) { @@ -115,9 +200,13 @@ int main(int argc, char** argv) { draw(&panels[0], 0, active == 0); draw(&panels[1], 40, active == 1); tui_bar(23, status); - tui_bar(24, "Enter edit | V view | / path | R rename | C copy M move D delete N mkdir Q quit"); + tui_bar(24, "1 Help 2 Info 3 View 4 Edit 5 Copy 6 Move 7 Mkdir 8 Delete 9 Menu 10 Quit"); tui_present(79, 24); int key = tui_key(1); + static const int functions[] = { 'h', 'i', 'v', '\n', 'c', 'm', 'n', 'd', 'o', 'q' }; + if (key >= TUI_F1 && key <= TUI_F10) { + key = functions[key - TUI_F1]; + } panel* p = &panels[active]; tab* t = &p->tabs[p->active]; if (key == 'q' || key == 27) { @@ -127,8 +216,22 @@ int main(int argc, char** argv) { active ^= 1; continue; } - if (key == 'h') { - tui_message("Commander help", "Copy/move targets other panel. Enter opens files in KUNED."); + if (key == TUI_INSERT || key == ' ') { + if (p->entries_count) { + toggle(t, &p->entries[t->selected]); + tui_list_key(TUI_DOWN, &t->selected, &t->top, p->entries_count, 18); + } + } else if (key == '+' || key == '-') { + t->marks = 0; + if (key == '+') { + for (u32 i = 0; i < p->entries_count; ++i) { + toggle(t, &p->entries[i]); + } + } + } else if (key == 'c' || key == 'm' || key == 'd') { + status = batch(p, &panels[active ^ 1], key); + } else if (key == 'h') { + tui_message("Commander help", "F1-F10 actions; Ins marks; +/- all/none; C/M/D act on marks."); } else if (key == '/') { char destination[256]; tui_stat st; @@ -223,18 +326,6 @@ int main(int argc, char** argv) { } else if (key == 'n' && tui_prompt("New directory:", input, sizeof(input))) { tui_path(dest, t->directory, input); status = syscall(11, (u32)dest, 0, 0, 0) ? "mkdir failed" : "Created"; - } else if (key == 'd' && e && tui_confirm("Delete selected entry?")) { - status = syscall(11, (u32)source, e->stat.directory ? 2 : 1, 0, 0) - ? "Delete failed (directory must be empty)" - : "Deleted"; - } else if ((key == 'c' || key == 'm') && e) { - panel* other = &panels[active ^ 1]; - tui_path(dest, other->tabs[other->active].directory, e->name); - if (tui_confirm(key == 'c' ? "Copy to other panel?" : "Move to other panel?")) { - status = syscall(12, (u32)source, (u32)dest, key == 'm', 0) - ? "Failed: no overwrite or recursive directory copy" - : "Done"; - } } } } diff --git a/src/user/ktte.c b/src/user/ktte.c index ffb7e87..cdad820 100644 --- a/src/user/ktte.c +++ b/src/user/ktte.c @@ -7,13 +7,36 @@ static const char* names[] = { "Black", "Blue", "Green", " "Dark gray", "Bright blue", "Bright green", "Bright cyan", "Bright red", "Bright magenta", "Yellow", "White" }; -static const char* roles[] = { - "Desktop background", "Panel background", "Panel text", "Panel accents", "Editor text / background", - "Selection", "Popup windows", "Shadows", "Gauges" -}; -static const unsigned char attributes[] = { - TUI_DESKTOP, 0x17, 0x1f, 0x1e, 0x07, 0x70, TUI_POPUP, 0x08, 0x2f -}; +static const char* roles[] = { "Desktop background", + "Panel background", + "Panel text", + "Panel accents", + "Editor text / background", + "Selection", + "Popup windows", + "Shadows", + "Gauges", + "Input fields", + "Buttons", + "Keyboard focus", + "Disabled controls", + "Errors", + "Warnings", + "Success", + "Marked files", + "Directories", + "Window borders", + "Syntax comments", + "Syntax strings", + "Syntax numbers", + "Syntax keywords", + "Visual selection", + "Status text" }; +static const unsigned char attributes[] = { TUI_DESKTOP, 0x17, 0x1f, 0x1e, 0x07, + 0x70, TUI_POPUP, 0x08, 0x2f, TUI_INPUT, + TUI_BUTTON, TUI_FOCUS, TUI_DISABLED, TUI_ERROR, TUI_WARNING, + TUI_SUCCESS, TUI_MARKED, TUI_DIRECTORY, TUI_BORDER, TUI_COMMENT, + TUI_STRING, TUI_NUMBER, TUI_KEYWORD, TUI_VISUAL, TUI_STATUS }; static int choose_color(int value, int background) { if (!tui_modal_begin(background ? "Choose background color" : "Choose text color", 64, 21)) { @@ -116,12 +139,12 @@ int main(int argc, char** argv) { tui_bar(0, "KTTE - Kintsugi TUI Theme Editor"); if (simple) { tui_text(2, 2, "Simple mode: select an interface element", 0x1e); - for (u32 i = 0; i < sizeof(attributes); ++i) { - tui_choice_row(1, 4 + i, 46, roles[i], i == selected, 0x1f); - tui_sample(32, 4 + i, " Aa 123 ", colors[attributes[i]]); + tui_list_key(0, &selected, &top, sizeof(attributes), 16); + for (u32 i = top; i < sizeof(attributes) && i < top + 16; ++i) { + tui_choice_row(1, 4 + i - top, 46, roles[i], i == selected, 0x1f); + tui_sample(36, 4 + i - top, " Aa 123 ", colors[attributes[i]]); } - tui_text(2, 15, "Enter: choose color by name and sample", 0x1e); - tui_text(2, 16, "Tab: text / background | m: advanced mode", 0x1f); + tui_scrollbar(47, 4, 16, top, 16, sizeof(attributes)); } else { tui_text(2, 2, "Source Target Sample Role / attribute", 0x1e); tui_list_key(0, &selected, &top, 256, 16); @@ -189,6 +212,11 @@ int main(int argc, char** argv) { } colors[TUI_POPUP] = 0x70; colors[TUI_DESKTOP] = 0x17; + static const unsigned char defaults[] = { 7, 0x70, 0x70, 8, 0x4f, 0x1e, 0x2f, 0x1e, + 0x1f, 0x1f, 8, 0x0a, 0x0e, 0x0b, 0x70, 0x70 }; + for (u32 i = 0; i < sizeof(defaults); ++i) { + colors[TUI_INPUT + i] = defaults[i]; + } path[0] = 0; dirty = 1; status = "New identity palette"; diff --git a/src/user/ktuilib.c b/src/user/ktuilib.c index d8ad8f8..75d10b0 100644 --- a/src/user/ktuilib.c +++ b/src/user/ktuilib.c @@ -45,6 +45,7 @@ static int hex(char c) { int tui_theme_parse(const char* text, u32 length) { unsigned char next[256]; int popup = 0, desktop = 0; + unsigned char specified[256] = { 0 }; for (int i = 0; i < 256; ++i) { next[i] = i; } @@ -72,6 +73,7 @@ int tui_theme_parse(const char* text, u32 length) { return 0; } next[a * 16 + b] = c * 16 + d; + specified[a * 16 + b] = 1; if (a * 16 + b == TUI_POPUP) { popup = 1; } @@ -85,6 +87,13 @@ int tui_theme_parse(const char* text, u32 length) { if (!desktop) { next[TUI_DESKTOP] = next[0x17]; } + static const unsigned char fallback[] = { 0x07, 0x70, 0x70, 0x08, 0x4f, 0x1e, 0x2f, 0x1e, + 0x1f, 0x1f, 0x08, 0x0a, 0x0e, 0x0b, 0x70, 0x70 }; + for (u32 i = 0; i < sizeof(fallback); ++i) { + if (!specified[TUI_INPUT + i]) { + next[TUI_INPUT + i] = next[fallback[i]]; + } + } for (int i = 0; i < 256; ++i) { palette[i] = next[i]; } @@ -215,7 +224,7 @@ int tui_command(const char* prefix, char* buffer, u32 capacity) { void tui_window_draw(const tui_window* w) { tui_fill(w->x + 1, w->y + 1, w->width, w->height, 0x08); - tui_box(w->x, w->y, w->width, w->height, w->title, w->focused ? 0x1f : 0x17); + tui_box(w->x, w->y, w->width, w->height, w->title, w->focused ? TUI_BORDER : 0x17); } void tui_window_text(const tui_window* w, int x, int y, const char* text, unsigned char color) { @@ -348,10 +357,7 @@ int tui_menu(const char* title, const char* const* items, int count, int initial int y = (25 - visible - 4) / 2; for (;;) { tui_list_key(0, &selected, &top, count, visible); - for (int row = 0; row < visible; ++row) { - int i = top + row; - tui_choice_row(10, y + 1 + row, 60, items[i], i == (int)selected, TUI_POPUP); - } + tui_list_draw(10, y + 1, 61, visible, items, count, selected, top, 1); tui_text(11, y + visible + 2, "Arrows/PgDn select | Enter accept | Esc cancel", TUI_POPUP); tui_scrollbar(70, y + 1, visible, top, visible, count); tui_present(79, 24); @@ -419,8 +425,8 @@ void tui_message(const char* title, const char* message) { if (!tui_modal_begin(title, 70, 7)) { return; } - tui_textn(7, 11, message, 66, 0x1f); - tui_text(7, 13, "Press any key", 0x1e); + tui_textn(7, 11, message, 66, TUI_POPUP); + tui_button(7, 13, 22, "OK - press any key", 1, 1); tui_present(79, 24); tui_key(1); tui_modal_end(); @@ -552,7 +558,7 @@ int tui_prompt(const char* label, char* buffer, u32 capacity) { if (!capacity) { return 0; } - u32 n = 0, cursor = 0; + tui_input input = { buffer, capacity, 0 }; buffer[0] = 0; if (!tui_modal_begin("Input - Enter accept / Esc cancel", 74, 7)) { return 0; @@ -560,9 +566,8 @@ int tui_prompt(const char* label, char* buffer, u32 capacity) { for (;;) { tui_fill(4, 10, 72, 4, TUI_POPUP); tui_textn(5, 10, label, 70, TUI_POPUP); - u32 start = cursor > 68 ? cursor - 68 : 0; - tui_textn(5, 12, buffer + start, 69, TUI_POPUP); - tui_present(5 + cursor - start, 12); + tui_input_draw(&input, 5, 12, 69, 1); + tui_present(5 + (input.cursor < 69 ? input.cursor : 68), 12); int key = tui_key(1); if (key == 27 || key == 3) { tui_modal_end(); @@ -570,31 +575,9 @@ int tui_prompt(const char* label, char* buffer, u32 capacity) { } if (key == '\n') { tui_modal_end(); - return n != 0; - } - if (key == TUI_LEFT && cursor) { - --cursor; - } else if (key == TUI_RIGHT && cursor < n) { - ++cursor; - } else if (key == TUI_HOME) { - cursor = 0; - } else if (key == TUI_END) { - cursor = n; - } else if ((key == 8 && cursor) || (key == TUI_DELETE && cursor < n)) { - if (key == 8) { - --cursor; - } - for (u32 i = cursor; i < n; ++i) { - buffer[i] = buffer[i + 1]; - } - --n; - } else if (key >= 32 && key < 127 && n + 1 < capacity) { - for (u32 i = n + 1; i > cursor; --i) { - buffer[i] = buffer[i - 1]; - } - buffer[cursor++] = key; - ++n; + return buffer[0] != 0; } + tui_input_key(&input, key); } } @@ -605,6 +588,8 @@ int tui_confirm(const char* label) { tui_fill(4, 10, 72, 4, TUI_POPUP); tui_textn(5, 10, label, 70, TUI_POPUP); tui_text(5, 12, "Y yes / any other key cancels", TUI_POPUP); + tui_button(51, 12, 10, "Yes [Y]", 0, 1); + tui_button(62, 12, 12, "Cancel", 1, 1); tui_present(79, 24); int key = tui_key(1); tui_modal_end(); @@ -623,3 +608,130 @@ void tui_path(char out[256], const char* directory, const char* name) { int tui_stat_path(const char* path, tui_stat* stat) { return syscall(11, (u32)path, 4, (u32)stat, 0); } + +int tui_input_key(tui_input* input, int key) { + if (!input || !input->text || !input->capacity) { + return 0; + } + u32 n = 0; + while (n + 1 < input->capacity && input->text[n]) { + ++n; + } + input->text[n] = 0; + if (input->cursor > n) { + input->cursor = n; + } + u32* cursor = &input->cursor; + if (key == TUI_LEFT && *cursor) { + --*cursor; + } else if (key == TUI_RIGHT && *cursor < n) { + ++*cursor; + } else if (key == TUI_HOME) { + *cursor = 0; + } else if (key == TUI_END) { + *cursor = n; + } else if ((key == 8 && *cursor) || (key == TUI_DELETE && *cursor < n)) { + if (key == 8) { + --*cursor; + } + for (u32 i = *cursor; i < n; ++i) { + input->text[i] = input->text[i + 1]; + } + return 1; + } else if (key >= 32 && key < 127 && n + 1 < input->capacity) { + for (u32 i = n + 1; i > *cursor; --i) { + input->text[i] = input->text[i - 1]; + } + input->text[(*cursor)++] = key; + return 1; + } + return 0; +} + +void tui_input_draw(tui_input* input, int x, int y, int width, int focused) { + if (!input || !input->text || width < 1 || width > 80) { + return; + } + u32 n = tui_len(input->text); + if (input->cursor > n) { + input->cursor = n; + } + u32 start = input->cursor >= (u32)width ? input->cursor - width + 1 : 0; + tui_fill(x, y, width, 1, TUI_INPUT); + tui_textn(x, y, input->text + start, width, TUI_INPUT); + if (focused) { + char ch[2] = { input->cursor < n ? input->text[input->cursor] : ' ', 0 }; + tui_text(x + input->cursor - start, y, ch, TUI_FOCUS); + } +} + +void tui_button(int x, int y, int width, const char* label, int focused, int enabled) { + if (width < 2 || width > 80) { + return; + } + unsigned char color = !enabled ? TUI_DISABLED : focused ? TUI_FOCUS : TUI_BUTTON; + tui_fill(x, y, width, 1, color); + tui_text(x, y, "[", color); + tui_textn(x + 1, y, label, width - 2, color); + tui_text(x + width - 1, y, "]", color); +} + +void tui_checkbox(int x, int y, const char* label, int checked, int focused) { + unsigned char color = focused ? TUI_FOCUS : TUI_POPUP; + tui_text(x, y, checked ? "[x] " : "[ ] ", color); + tui_textn(x + 4, y, label, 76, color); +} + +void tui_radio(int x, int y, const char* label, int checked, int focused) { + unsigned char color = focused ? TUI_FOCUS : TUI_POPUP; + tui_text(x, y, checked ? "(*) " : "( ) ", color); + tui_textn(x + 4, y, label, 76, color); +} + +int tui_toggle_key(int key, int* checked) { + if (key != ' ' || !checked) { + return 0; + } + *checked = !*checked; + return 1; +} + +int tui_focus_key(int key, u32* focus, u32 count) { + if (!focus || !count) { + return 0; + } + if (*focus >= count) { + *focus = 0; + } + if (key != '\t') { + return 0; + } + *focus = (*focus + 1) % count; + return 1; +} + +void tui_list_draw( + int x, + int y, + int width, + int height, + const char* const* items, + u32 count, + u32 selected, + u32 top, + int focused) { + if (width < 3 || width > 80 || height < 1 || height > 25) { + return; + } + for (int row = 0; row < height; ++row) { + u32 i = top + row; + tui_choice_row( + x, + y + row, + width - 1, + i < count ? items[i] : "", + focused && i < count && i == selected, + TUI_POPUP); + } + tui_scrollbar(x + width - 1, y, height, top, height, count); +} diff --git a/src/user/ktuilib.h b/src/user/ktuilib.h index a7cea2c..1dc8f26 100644 --- a/src/user/ktuilib.h +++ b/src/user/ktuilib.h @@ -15,7 +15,18 @@ enum { TUI_SCROLL_UP, TUI_SCROLL_DOWN, TUI_PAGE_UP, - TUI_PAGE_DOWN + TUI_PAGE_DOWN, + TUI_INSERT, + TUI_F1, + TUI_F2, + TUI_F3, + TUI_F4, + TUI_F5, + TUI_F6, + TUI_F7, + TUI_F8, + TUI_F9, + TUI_F10 }; typedef kos_stat tui_stat; @@ -24,6 +35,50 @@ typedef kos_dirent tui_entry; /* Dedicated popup role; older themes inherit their selection color. */ #define TUI_POPUP 0x71 #define TUI_DESKTOP 0x72 + +/* Semantic slots; omitted mappings inherit the corresponding legacy color. */ +enum { + TUI_INPUT = 0x73, + TUI_BUTTON, + TUI_FOCUS, + TUI_DISABLED, + TUI_ERROR, + TUI_WARNING, + TUI_SUCCESS, + TUI_MARKED, + TUI_DIRECTORY, + TUI_BORDER, + TUI_COMMENT, + TUI_STRING, + TUI_NUMBER, + TUI_KEYWORD, + TUI_VISUAL, + TUI_STATUS +}; + +typedef struct { + char* text; + u32 capacity, cursor; +} tui_input; + +int tui_input_key(tui_input* input, int key); +void tui_input_draw(tui_input* input, int x, int y, int width, int focused); +void tui_button(int x, int y, int width, const char* label, int focused, int enabled); +void tui_checkbox(int x, int y, const char* label, int checked, int focused); +void tui_radio(int x, int y, const char* label, int checked, int focused); +/* Space toggles a checkbox; arrows select a radio member. */ +int tui_toggle_key(int key, int* checked); +int tui_focus_key(int key, u32* focus, u32 count); +void tui_list_draw( + int x, + int y, + int width, + int height, + const char* const* items, + u32 count, + u32 selected, + u32 top, + int focused); /* Normalize a short name or path to /ETC/THEMES/NAME.THM; ASCII 8.3 only. */ int tui_theme_path(char out[256], const char* name); diff --git a/src/user/kuned.c b/src/user/kuned.c index 38ea460..b85754c 100644 --- a/src/user/kuned.c +++ b/src/user/kuned.c @@ -7,8 +7,13 @@ #define HISTORY 8 static char* undo[HISTORY]; static u32 undo_length[HISTORY], undo_cursor[HISTORY], undos; +static char* redo[HISTORY]; +static u32 redo_length[HISTORY], redo_cursor[HISTORY], redos; static void remember(const char* text, u32 length, u32 cursor) { + while (redos) { + free(redo[--redos]); + } char* snapshot = malloc(length + 1); if (!snapshot) { return; @@ -30,6 +35,39 @@ static void remember(const char* text, u32 length, u32 cursor) { undo_cursor[undos++] = cursor; } +#include "syntax.h" +#include "vikmode.h" +static vik_state vim; + +static void history_swap(char* text, u32* length, u32* cursor, int forward) { + char** source = forward ? redo : undo; + char** target = forward ? undo : redo; + u32 *sizes = forward ? redo_length : undo_length, *positions = forward ? redo_cursor : undo_cursor; + u32 *out_sizes = forward ? undo_length : redo_length, + *out_positions = forward ? undo_cursor : redo_cursor; + u32 *count = forward ? &redos : &undos, *out_count = forward ? &undos : &redos; + if (!*count || *out_count >= HISTORY) { + return; + } + char* copy = malloc(*length + 1); + if (!copy) { + return; + } + for (u32 i = 0; i < *length; ++i) { + copy[i] = text[i]; + } + target[*out_count] = copy; + out_sizes[*out_count] = *length; + out_positions[(*out_count)++] = *cursor; + --*count; + *length = sizes[*count]; + *cursor = positions[*count]; + for (u32 i = 0; i < *length; ++i) { + text[i] = source[*count][i]; + } + free(source[*count]); +} + static int find(const char* text, u32 length, const char* query, u32 start) { u32 n = tui_len(query); if (!n || n > length) { @@ -74,7 +112,8 @@ int main(int argc, char** argv) { tui_load_settings("/ETC/KUNED.CFG", settings, sizeof(settings)); settings[0] = !!settings[0]; settings[1] = !!settings[1]; - const char* status = "Ctrl+S save | Ctrl+Q quit | arrows/Home/End edit"; + const char* status = + VIK ? "VIK | :w save :q quit :q! discard" : "Ctrl+S save | Ctrl+Q quit | arrows/Home/End edit"; if (*path) { tui_stat stat; int result = tui_stat_path(path, &stat); @@ -132,7 +171,7 @@ int main(int argc, char** argv) { tui_text(78, 0, "*", 0x70); } u32 r = 0, c = 0; - int comment = 0, string = 0, escaped = 0; + syntax_state syntax = { .language = syntax_language(path) }; u32 line = 1; if (settings[0] && !top) { tui_number(0, 1, line, 0x08); @@ -140,8 +179,19 @@ int main(int argc, char** argv) { int row_number_drawn = !top; for (u32 i = 0; i < length && r < top + 22; ++i) { unsigned char ch = text[i]; + unsigned char ink = syntax_color(&syntax, text, length, i, color); + if (VIK && vim.visual) { + u32 a = vim.anchor < cursor ? vim.anchor : cursor; + u32 b = vim.anchor > cursor ? vim.anchor : cursor; + if (vim.linewise) { + a = vik_start(text, a); + b = vik_end(text, length, b); + } + if (i >= a && i <= b) { + ink = TUI_VISUAL; + } + } if (ch == '\n') { - comment = string = escaped = 0; ++r; ++line; c = 0; @@ -157,31 +207,17 @@ int main(int argc, char** argv) { row_number_drawn = 1; } char s[2] = { ch >= 32 && ch < 127 ? ch : '.', 0 }; - unsigned char ink = color; - if (comment) { - ink = (color & 0xf0) | 8; - } else if (string || ch == '"') { - ink = (color & 0xf0) | 10; - } else if (ch >= '0' && ch <= '9') { - ink = (color & 0xf0) | 14; - } else if (ch == '(' || ch == ')' || ch == '{' || ch == '}') { - ink = (color & 0xf0) | 11; - } tui_text(c + gutter, r - top + 1, s, ink); } - if (!string && ch == ';') { - comment = 1; - } - if (!comment && ch == '"' && !escaped) { - string = !string; - } - escaped = string && ch == '\\' && !escaped; if (++c == width) { ++r; c = 0; } } - tui_bar(23, readonly ? "Read-only viewer | Ctrl+F find | Ctrl+Q quit" : status); + tui_bar( + 23, + readonly ? (VIK ? "Read-only | / find :q quit" : "Read-only viewer | Ctrl+F find | Ctrl+Q quit") + : status); tui_text(38, 23, "Ln", 0x70); tui_number(41, 23, logical_line, 0x70); tui_text(48, 23, "Col", 0x70); @@ -194,11 +230,19 @@ int main(int argc, char** argv) { tui_text(1, 0, *path ? path : "[No Name]", 0x70); tui_bar( 24, - insert ? "-- INSERT -- | Esc normal | ^S save" - : "-- NORMAL -- | hjkl move i/a/o insert x delete u undo / find n next : command"); + insert ? "-- INSERT -- | Esc normal | :w save in normal mode" + : vim.visual ? "-- VISUAL -- | motions select | y yank d delete c change | Esc cancel" + : "-- NORMAL -- | hjkl i/a/o | d/c/y+motion | v/V p/P u ^R | / :w :q"); } tui_present(col + gutter, row - top + 1); int key = tui_key(1); + if (VIK && !insert && key == 18) { + if (!readonly) { + history_swap(text, &length, &cursor, 1); + dirty = 1; + } + continue; + } if (VIK && key > 0 && key < 32 && key != 27 && key != '\n' && key != '\t' && key != 8) { continue; } @@ -210,6 +254,9 @@ int main(int argc, char** argv) { continue; } if (VIK && !insert) { + if (vik_key(&vim, text, &length, &cursor, key, readonly, &insert, &dirty)) { + continue; + } if (key != 'g' && key != 'd') { pending = 0; } @@ -401,13 +448,7 @@ int main(int argc, char** argv) { } } else if (key == 26 && !readonly) { if (undos) { - --undos; - length = undo_length[undos]; - cursor = undo_cursor[undos]; - for (u32 i = 0; i < length; ++i) { - text[i] = undo[undos][i]; - } - free(undo[undos]); + history_swap(text, &length, &cursor, 0); dirty = 1; status = "Undo"; } else { @@ -589,5 +630,11 @@ int main(int argc, char** argv) { while (undos) { free(undo[--undos]); } + while (redos) { + free(redo[--redos]); + } + for (int i = 0; i < 27; ++i) { + free(vim.registers[i]); + } return 0; } diff --git a/src/user/syntax.h b/src/user/syntax.h new file mode 100644 index 0000000..0f3eb6f --- /dev/null +++ b/src/user/syntax.h @@ -0,0 +1,129 @@ +/* Small extension-driven lexer. State is advanced for off-screen text too. */ +enum { + SYN_TEXT, + SYN_C, + SYN_ASM, + SYN_SCRIPT, + SYN_CONFIG +}; + +typedef struct { + int language, block, quote, escaped, comment; + u32 word_end; +} syntax_state; + +static int syntax_equal(const char* a, const char* b) { + while (*a && *b) { + char c = *a++, d = *b++; + if (c >= 'A' && c <= 'Z') { + c += 'a' - 'A'; + } + if (d >= 'A' && d <= 'Z') { + d += 'a' - 'A'; + } + if (c != d) { + return 0; + } + } + return !*a && !*b; +} + +static int syntax_language(const char* path) { + const char* ext = ""; + for (const char* p = path; *p; ++p) { + if (*p == '/') { + ext = ""; + } else if (*p == '.') { + ext = p + 1; + } + } + if (syntax_equal(ext, "c") || syntax_equal(ext, "h") || syntax_equal(ext, "cpp") + || syntax_equal(ext, "js") || syntax_equal(ext, "ts")) { + return SYN_C; + } + if (syntax_equal(ext, "asm") || syntax_equal(ext, "s") || syntax_equal(ext, "inc")) { + return SYN_ASM; + } + if (syntax_equal(ext, "sh") || syntax_equal(ext, "py")) { + return SYN_SCRIPT; + } + if (syntax_equal(ext, "cfg") || syntax_equal(ext, "ini") || syntax_equal(ext, "thm")) { + return SYN_CONFIG; + } + return SYN_TEXT; +} + +static int syntax_word(char c) { + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_'; +} + +static unsigned char syntax_color(syntax_state* s, const char* text, u32 n, u32 i, unsigned char normal) { + char c = text[i], next = i + 1 < n ? text[i + 1] : 0; + if (!s->language) { + return normal; + } + if (c == '\n') { + s->comment = s->quote = s->escaped = 0; + return normal; + } + if (s->comment) { + return TUI_COMMENT; + } + if (s->block) { + if (c == '/' && i && text[i - 1] == '*') { + s->block = 0; + } + return TUI_COMMENT; + } + if (s->quote) { + if (c == s->quote && !s->escaped) { + s->quote = 0; + } + s->escaped = c == '\\' && !s->escaped; + return TUI_STRING; + } + if ((s->language == SYN_C && c == '/' && next == '/') || (s->language == SYN_ASM && c == ';') + || ((s->language == SYN_SCRIPT || s->language == SYN_CONFIG) && c == '#') + || (s->language == SYN_CONFIG && c == ';')) { + s->comment = 1; + return TUI_COMMENT; + } + if (s->language == SYN_C && c == '/' && next == '*') { + s->block = 1; + return TUI_COMMENT; + } + if (c == '"' || c == '\'') { + s->quote = c; + return TUI_STRING; + } + if (i < s->word_end) { + return TUI_KEYWORD; + } + if (syntax_word(c) && (!i || !syntax_word(text[i - 1]))) { + static const char* words[] = { "if", "else", "for", "while", "return", "static", + "const", "int", "char", "void", "struct", "typedef", + "enum", "unsigned", "include", "define", "break", "continue", + "switch", "case", "def", "class", "import", "from", + "then", "fi", "do", "done", "export", "mov", + "push", "pop", "call", "ret", "jmp", "cmp", + "db", "dw", "dd" }; + u32 end = i; + while (end < n && syntax_word(text[end])) { + ++end; + } + for (u32 w = 0; w < sizeof(words) / sizeof(words[0]); ++w) { + u32 j = 0; + while (i + j < end && words[w][j] == text[i + j]) { + ++j; + } + if (i + j == end && !words[w][j]) { + s->word_end = end; + return TUI_KEYWORD; + } + } + } + if (c >= '0' && c <= '9') { + return TUI_NUMBER; + } + return normal; +} diff --git a/src/user/vikmode.h b/src/user/vikmode.h new file mode 100644 index 0000000..ac1619e --- /dev/null +++ b/src/user/vikmode.h @@ -0,0 +1,272 @@ +/* Bounded Vim-style motions/operators, shared with host tests. No terminal I/O. */ +typedef struct { + u32 count, operator_count, anchor; + int op, visual, linewise, reg, choose_reg; + char* registers[27]; + u32 sizes[27]; + unsigned char lines[27]; +} vik_state; + +static u32 vik_start(const char* t, u32 p) { + while (p && t[p - 1] != '\n') { + --p; + } + return p; +} + +static u32 vik_end(const char* t, u32 n, u32 p) { + while (p < n && t[p] != '\n') { + ++p; + } + return p; +} + +static int vik_class(char c) { + if (c <= ' ') { + return 0; + } + return ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_') ? 1 : 2; +} + +static u32 vik_motion(const char* t, u32 n, u32 p, int key, u32 count) { + while (count--) { + u32 start = vik_start(t, p), end = vik_end(t, n, p), col = p - start; + if (key == 'h' || key == TUI_LEFT) { + if (p > start) { + --p; + } + } else if (key == 'l' || key == TUI_RIGHT) { + if (p < end) { + ++p; + } + } else if (key == '0' || key == TUI_HOME) { + p = start; + } else if (key == '$' || key == TUI_END) { + p = end > start ? end - 1 : end; + } else if (key == '^') { + p = start; + while (p < end && (t[p] == ' ' || t[p] == '\t')) { + ++p; + } + } else if (key == 'j' || key == TUI_DOWN) { + if (end < n) { + p = end + 1; + end = vik_end(t, n, p); + p += col < end - p ? col : end - p; + } + } else if (key == 'k' || key == TUI_UP) { + if (start) { + end = start - 1; + p = vik_start(t, end); + p += col < end - p ? col : end - p; + } + } else if (key == 'w') { + int kind = p < n ? vik_class(t[p]) : 0; + while (p < n && vik_class(t[p]) == kind) { + ++p; + } + while (p < n && !vik_class(t[p])) { + ++p; + } + } else if (key == 'b') { + while (p && !vik_class(t[p - 1])) { + --p; + } + int kind = p ? vik_class(t[p - 1]) : 0; + while (p && vik_class(t[p - 1]) == kind) { + --p; + } + } else if (key == 'e') { + if (p < n) { + ++p; + } + while (p < n && !vik_class(t[p])) { + ++p; + } + int kind = p < n ? vik_class(t[p]) : 0; + while (p + 1 < n && vik_class(t[p + 1]) == kind) { + ++p; + } + } + } + return p; +} + +static void vik_yank(vik_state* v, const char* t, u32 begin, u32 end, int lines) { + char* copy = malloc(end - begin + 1); + if (!copy) { + return; + } + for (u32 i = begin; i < end; ++i) { + copy[i - begin] = t[i]; + } + copy[end - begin] = 0; + free(v->registers[v->reg]); + v->registers[v->reg] = copy; + v->sizes[v->reg] = end - begin; + v->lines[v->reg] = lines; +} + +/* Returns 1 when consumed; ordinary insert/Ex/search remain in the editor. */ +static int vik_key(vik_state* v, char* t, u32* n, u32* p, int key, int readonly, int* insert, int* dirty) { + if (key == 27) { + v->count = v->operator_count = 0; + v->op = v->visual = v->choose_reg = 0; + return 1; + } + if (v->choose_reg) { + v->reg = key >= 'a' && key <= 'z' ? key - 'a' + 1 : 0; + v->choose_reg = 0; + return 1; + } + if (key == '"') { + v->choose_reg = 1; + return 1; + } + if (key >= '0' && key <= '9' && (key != '0' || v->count)) { + if (v->count < 1000) { + v->count = v->count * 10 + key - '0'; + } + return 1; + } + u32 count = v->count ? v->count : 1; + int explicit_count = v->count != 0; + v->count = 0; + if (key == 'v' || key == 'V') { + v->visual = !v->visual; + v->linewise = key == 'V'; + v->anchor = *p; + return 1; + } + if ((key == 'd' || key == 'c' || key == 'y') && !v->op && !v->visual) { + v->op = key; + v->operator_count = count; + return 1; + } + u32 begin = *p, end = *p; + int lines = 0, op = v->op; + if (v->visual && (key == 'd' || key == 'c' || key == 'y' || key == 'x')) { + begin = *p < v->anchor ? *p : v->anchor; + end = *p > v->anchor ? *p : v->anchor; + if (end < *n) { + ++end; + } + lines = v->linewise; + op = key == 'x' ? 'd' : key; + if (lines) { + begin = vik_start(t, begin); + end = vik_end(t, *n, end ? end - 1 : 0); + if (end < *n) { + ++end; + } + } + } else if (op && key == op) { + lines = 1; + begin = vik_start(t, *p); + end = begin; + count *= v->operator_count; + while (count--) { + end = vik_end(t, *n, end); + if (end < *n) { + ++end; + } + } + } else if (key == 'p' || key == 'P') { + u32 size = v->sizes[v->reg]; + if (!readonly && size && count <= (CAPACITY - *n) / size) { + remember(t, *n, *p); + u32 at = *p; + if (v->lines[v->reg]) { + at = key == 'P' ? vik_start(t, at) : vik_end(t, *n, at); + if (key == 'p' && at < *n) { + ++at; + } + } else if (key == 'p' && at < *n && t[at] != '\n') { + ++at; + } + u32 separator = v->lines[v->reg] && key == 'p' && at == *n && at && t[at - 1] != '\n'; + u32 total = size * count + separator; + if (total > CAPACITY - *n) { + return 1; + } + for (u32 i = *n; i > at; --i) { + t[i + total - 1] = t[i - 1]; + } + if (separator) { + t[at] = '\n'; + } + for (u32 i = separator; i < total; ++i) { + t[at + i] = v->registers[v->reg][(i - separator) % size]; + } + *n += total; + *p = at; + *dirty = 1; + } + v->reg = 0; + return 1; + } else if (key == 'x' || key == 'D' || key == 'C') { + op = key == 'C' ? 'c' : 'd'; + end = key == 'x' ? *p + (count < *n - *p ? count : *n - *p) : vik_end(t, *n, *p); + } else if (key == 'G') { + end = explicit_count ? 0 : vik_start(t, *n); + if (explicit_count) { + while (--count && end < *n) { + end = vik_end(t, *n, end); + if (end < *n) { + ++end; + } + } + } + if (!op) { + *p = end; + return 1; + } + } else if ( + key == 'h' || key == 'j' || key == 'k' || key == 'l' || key == 'w' || key == 'b' || key == 'e' + || key == '0' || key == '$' || key == '^' || (key >= TUI_UP && key <= TUI_END)) { + end = vik_motion(t, *n, *p, key, count * (op ? v->operator_count : 1)); + if (!op) { + *p = end; + return 1; + } + if ((key == '$' || key == 'e') && end < *n) { + ++end; + } + if (end < begin) { + u32 swap = begin; + begin = end; + end = swap; + } + if (key == 'j' || key == 'k' || key == TUI_UP || key == TUI_DOWN) { + lines = 1; + begin = vik_start(t, begin); + end = vik_end(t, *n, end); + if (end < *n) { + ++end; + } + } + } else { + v->op = 0; + return 0; + } + if (op) { + if (!readonly || op == 'y') { + vik_yank(v, t, begin, end, lines); + if (op != 'y' && end > begin) { + remember(t, *n, *p); + for (u32 i = end; i < *n; ++i) { + t[begin + i - end] = t[i]; + } + *n -= end - begin; + *p = begin; + *dirty = 1; + } + if (op == 'c') { + *insert = 1; + } + } + v->op = v->visual = v->reg = 0; + return 1; + } + return 0; +} diff --git a/tools/smoke_test.py b/tools/smoke_test.py index 02ce810..1ad12a8 100644 --- a/tools/smoke_test.py +++ b/tools/smoke_test.py @@ -436,6 +436,10 @@ def command(text): command("vik-content") monitor("sendkey esc 10") time.sleep(0.1) + for key in ("g", "g", "y", "y", "p", "u", "ctrl-r", "u", "ctrl-q"): + monitor(f"sendkey {key} 10") + time.sleep(0.15) + wait_for("-- NORMAL --") command(":wq") command("cat /vik.txt") wait_for("vik-content") diff --git a/tools/test_tui.c b/tools/test_tui.c index 2c4852f..2353360 100644 --- a/tools/test_tui.c +++ b/tools/test_tui.c @@ -1,6 +1,7 @@ /* Host-side tests: no QEMU, no kernel or hardware dependencies. */ #include #include +#include #include #include "../src/user/tuiabi.h" @@ -25,8 +26,72 @@ int syscall(u32 number, u32 a, u32 b, u32 c, u32 d) { /* runtime.h declares freestanding malloc with u32 size. Avoid libc conflicts. */ #define USER_RUNTIME_H #include "../src/user/ktuilib.c" +#define CAPACITY 1024u + +static void remember(const char* t, u32 n, u32 p) { + (void)t; + (void)n; + (void)p; +} + +#include "../src/user/syntax.h" +#include "../src/user/vikmode.h" int main(void) { + vik_state vim = { 0 }; + char text[CAPACITY] = "one two\nthree\nfour\n"; + u32 length = strlen(text), cursor = 0; + int insert = 0, dirty = 0; + assert(vik_key(&vim, text, &length, &cursor, 'd', 0, &insert, &dirty)); + assert(vik_key(&vim, text, &length, &cursor, 'w', 0, &insert, &dirty)); + assert(length == 15 && !memcmp(text, "two\nthree\nfour\n", length)); + vik_key(&vim, text, &length, &cursor, 'p', 0, &insert, &dirty); + assert(length == 19); + cursor = 0; + vik_key(&vim, text, &length, &cursor, '2', 0, &insert, &dirty); + vik_key(&vim, text, &length, &cursor, 'y', 0, &insert, &dirty); + vik_key(&vim, text, &length, &cursor, 'y', 0, &insert, &dirty); + assert(vim.lines[0] && vim.sizes[0] == 14); + u32 before = length; + vik_key(&vim, text, &length, &cursor, 'd', 1, &insert, &dirty); + vik_key(&vim, text, &length, &cursor, 'd', 1, &insert, &dirty); + assert(length == before); + vik_key(&vim, text, &length, &cursor, 'v', 0, &insert, &dirty); + vik_key(&vim, text, &length, &cursor, 'l', 0, &insert, &dirty); + vik_key(&vim, text, &length, &cursor, 'c', 0, &insert, &dirty); + assert(insert && !vim.visual && length == before - 2); + for (int i = 0; i < 27; ++i) { + free(vim.registers[i]); + } + assert(syntax_language("/HOME/TEST.C") == SYN_C); + assert(syntax_language("x.asm") == SYN_ASM); + assert(syntax_language("x.txt") == SYN_TEXT); + syntax_state syntax = { .language = SYN_C }; + const char* code = "int x; // comment\n\"hi\""; + assert(syntax_color(&syntax, code, strlen(code), 0, 7) == TUI_KEYWORD); + for (u32 i = 1; i < 7; ++i) { + syntax_color(&syntax, code, strlen(code), i, 7); + } + assert(syntax_color(&syntax, code, strlen(code), 7, 7) == TUI_COMMENT); + char value[5] = "ab"; + tui_input input = { value, sizeof(value), 1 }; + assert(tui_input_key(&input, 'X') && !strcmp(value, "aXb")); + assert(tui_input_key(&input, TUI_DELETE) && !strcmp(value, "aX")); + tui_input_key(&input, TUI_HOME); + assert(tui_input_key(&input, '1')); + assert(tui_input_key(&input, '2')); + assert(!tui_input_key(&input, '3') && !strcmp(value, "12aX")); + int checked = 0; + assert(tui_toggle_key(' ', &checked) && checked); + u32 focus = 2; + assert(tui_focus_key('\t', &focus, 3) && focus == 0); + assert(!tui_focus_key('\t', &focus, 0)); + assert(tui_theme_parse("07=17\n73=30\n", 12)); + assert(palette[TUI_INPUT] == 0x30 && palette[TUI_STRING] == 0x0a); + tui_input_draw(&input, 2, 2, 2, 1); + tui_checkbox(1, 3, "Enabled", checked, 1); + tui_radio(1, 4, "Choice", 1, 0); + tui_button(1, 5, 12, "Apply", 1, 1); char path[256]; assert(tui_theme_path(path, "custom")); assert(!strcmp(path, "/ETC/THEMES/CUSTOM.THM"));