From 112d604669e91330ba15702ea15206b89fbfe956 Mon Sep 17 00:00:00 2001 From: Lluciocc Date: Sat, 6 Jun 2026 15:01:25 +0200 Subject: [PATCH 01/10] getting a prototype --- Makefile | 4 +- libapp/app.c | 9 + libapp/app.h | 4 + src/term.c | 635 +++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 650 insertions(+), 2 deletions(-) create mode 100644 src/term.c diff --git a/Makefile b/Makefile index 084a85d..d1d93e2 100644 --- a/Makefile +++ b/Makefile @@ -27,14 +27,14 @@ DESTDIR ?= $(abspath build/dist) CFLAGS = -Wall -Wextra -std=gnu11 -ffreestanding -O2 -fno-stack-protector \ -fno-stack-check -fno-lto -fno-pie -m64 -march=x86-64 -mno-red-zone \ - -isystem $(SDK_PATH)/include -Ilibnovaproto -Ilibtheme -Ilibui -Ilibapp -Ilibwidget -I. + -isystem $(SDK_PATH)/include -Ilibnovaproto -Ilibtheme -Ilibui -Ilibapp -Ilibwidget -Isrc -I. LDFLAGS = -m elf_x86_64 -nostdlib -static -no-pie -Ttext=0x40000000 \ --no-dynamic-linker -z text -z max-page-size=0x1000 -e _start \ -L$(SDK_PATH)/lib LIBS = obj/libnovaproto.a obj/libtheme.a obj/libui.a obj/libapp.a obj/libwidget.a -APPS = nova.elf taskbar.elf wallpaperd.elf about.elf helloworld.elf +APPS = nova.elf taskbar.elf wallpaperd.elf about.elf helloworld.elf term.elf all: bootstrap-sdk $(MAKE) export-sdk diff --git a/libapp/app.c b/libapp/app.c index d6b52bc..2e531e0 100644 --- a/libapp/app.c +++ b/libapp/app.c @@ -48,6 +48,7 @@ struct NovaApp { AppTextCallback cb_text; AppPointerCallback cb_pointer; AppCloseCallback cb_close; + AppIdleCallback cb_idle; void *userdata; @@ -375,6 +376,10 @@ int app_run(NovaApp *app) { } } + if (app->cb_idle) { + app->cb_idle(app); + } + if (app->dirty) { _do_draw(app); } @@ -426,6 +431,10 @@ void app_on_close(NovaApp *app, AppCloseCallback cb) { if (app) app->cb_close = cb; } +void app_on_idle(NovaApp *app, AppIdleCallback cb) { + if (app) app->cb_idle = cb; +} + // WM void app_set_title(NovaApp *app, const char *title) { diff --git a/libapp/app.h b/libapp/app.h index 92fd2c3..2aaabb2 100644 --- a/libapp/app.h +++ b/libapp/app.h @@ -31,6 +31,9 @@ typedef void (*AppPointerCallback)(NovaApp *app, int x, int y, uint32_t buttons) // Return true to allow the close, false to cancel it. typedef bool (*AppCloseCallback)(NovaApp *app); +// Called once per event-loop iteration (before redraw). Use for polling I/O. +typedef void (*AppIdleCallback)(NovaApp *app); + // Create a new application window with the given title and initial dimensions. // Loads the system theme automatically. Returns NULL on failure. NovaApp *app_create(const char *title, uint32_t width, uint32_t height); @@ -55,6 +58,7 @@ void app_on_key(NovaApp *app, AppKeyCallback cb); void app_on_text(NovaApp *app, AppTextCallback cb); void app_on_pointer(NovaApp *app, AppPointerCallback cb); void app_on_close(NovaApp *app, AppCloseCallback cb); +void app_on_idle(NovaApp *app, AppIdleCallback cb); // Change the window title at runtime. void app_set_title(NovaApp *app, const char *title); diff --git a/src/term.c b/src/term.c new file mode 100644 index 0000000..2deeca2 --- /dev/null +++ b/src/term.c @@ -0,0 +1,635 @@ +// Copyright (c) 2026 Lluciocc (https://github.com/lluciocc) +// This software is released under the GNU General Public License v3.0. See LICENSE file for details. + +// BOREDOS_APP_DESC: Graphical terminal emulator using kernel PTY. +// BOREDOS_APP_ICONS: /Library/images/icons/colloid/utilities-terminal.png + +#include +#include +#include +#include +#include + +#include +#include +#include "libapp/app.h" +#include "libui/ui.h" + +#define NOVA_KMOD_CTRL 0x0002u + +#define CELL_W 8 +#define CELL_H 16 +#define PTY_READ_SZ 4096 +#define ESC_BUF_SZ 32 + +#define TERM_DEFAULT_FG 0xFFCCCCCCu +#define TERM_DEFAULT_BG 0xFF1E1E1Eu + +typedef struct { + char utf8[5]; + uint8_t wide_cont; + uint32_t fg; + uint32_t bg; +} TermCell; + +typedef enum { + PARSE_NORMAL = 0, + PARSE_ESC, + PARSE_CSI, +} ParseState; + +typedef struct { + int pty_id; + int shell_pid; + int cols; + int rows; + TermCell *cells; + + int cursor_x; + int cursor_y; + uint32_t cur_fg; + uint32_t cur_bg; + + ParseState parse_state; + char esc_buf[ESC_BUF_SZ]; + int esc_len; + + unsigned char utf8_buf[4]; + int utf8_len; + int utf8_needed; +} TermState; + +static const uint32_t ANSI_COLORS[16] = { + 0xFF1E1E1Eu, 0xFFCC6666u, 0xFF66CC66u, 0xFFCCCC66u, + 0xFF6666CCu, 0xFFCC66CCu, 0xFF66CCCCu, 0xFFCCCCCCu, + 0xFF555555u, 0xFFFF6666u, 0xFF66FF66u, 0xFFFFFF66u, + 0xFF6666FFu, 0xFFFF66FFu, 0xFF66FFFFu, 0xFFFFFFFFu, +}; + +static int term_cols(uint32_t width) { + int cols = (int)width / CELL_W; + return cols > 0 ? cols : 1; +} + +static int term_rows(uint32_t height) { + int rows = (int)height / CELL_H; + return rows > 0 ? rows : 1; +} + +static void term_cell_clear(TermCell *cell, uint32_t fg, uint32_t bg) { + if (!cell) return; + cell->utf8[0] = '\0'; + cell->wide_cont = 0; + cell->fg = fg; + cell->bg = bg; +} + +static int term_utf8_lead_len(unsigned char lead) { + if ((lead & 0x80u) == 0u) return 1; + if ((lead & 0xE0u) == 0xC0u) return 2; + if ((lead & 0xF0u) == 0xE0u) return 3; + if ((lead & 0xF8u) == 0xF0u) return 4; + return 1; +} + +static int term_char_width(uint32_t cp) { + if (cp < 0x1100u) return 1; + if ((cp >= 0x1100u && cp <= 0x115Fu) || + (cp >= 0x2E80u && cp <= 0x9FFFu) || + (cp >= 0xAC00u && cp <= 0xD7A3u) || + (cp >= 0xF900u && cp <= 0xFAFFu) || + (cp >= 0xFE10u && cp <= 0xFE19u) || + (cp >= 0xFE30u && cp <= 0xFE6Fu) || + (cp >= 0xFF00u && cp <= 0xFF60u) || + (cp >= 0xFFE0u && cp <= 0xFFE6u) || + (cp >= 0x20000u && cp <= 0x2FFFDu) || + (cp >= 0x30000u && cp <= 0x3FFFDu)) { + return 2; + } + return 1; +} + +static void term_backspace(TermState *st) { + if (!st || st->cursor_x <= 0) return; + + st->cursor_x--; + if (st->cursor_x > 0 && + st->cells[st->cursor_y * st->cols + st->cursor_x].wide_cont) { + st->cursor_x--; + } +} + +static void term_resize_grid(TermState *st, int cols, int rows) { + if (!st) return; + if (cols < 1) cols = 1; + if (rows < 1) rows = 1; + + TermCell *next = calloc((size_t)cols * (size_t)rows, sizeof(TermCell)); + if (!next) return; + + if (st->cells && st->cols > 0 && st->rows > 0) { + int copy_cols = cols < st->cols ? cols : st->cols; + int copy_rows = rows < st->rows ? rows : st->rows; + for (int y = 0; y < copy_rows; y++) { + memcpy(next + y * cols, + st->cells + y * st->cols, + (size_t)copy_cols * sizeof(TermCell)); + } + } else { + for (int i = 0; i < cols * rows; i++) { + term_cell_clear(&next[i], st->cur_fg, st->cur_bg); + } + } + + free(st->cells); + st->cells = next; + st->cols = cols; + st->rows = rows; + + if (st->cursor_x >= cols) st->cursor_x = cols - 1; + if (st->cursor_y >= rows) st->cursor_y = rows - 1; +} + +static void term_scroll_up(TermState *st) { + if (!st || !st->cells || st->rows <= 1) return; + + memmove(st->cells, + st->cells + st->cols, + (size_t)st->cols * (size_t)(st->rows - 1) * sizeof(TermCell)); + + TermCell *last = st->cells + st->cols * (st->rows - 1); + for (int x = 0; x < st->cols; x++) { + term_cell_clear(&last[x], st->cur_fg, st->cur_bg); + } +} + +static void term_advance_cursor(TermState *st, int cols_used) { + if (!st) return; + + st->cursor_x += cols_used; + if (st->cursor_x >= st->cols) { + st->cursor_x = 0; + st->cursor_y++; + } + if (st->cursor_y >= st->rows) { + term_scroll_up(st); + st->cursor_y = st->rows - 1; + } +} + +static void term_put_utf8(TermState *st, const char *utf8, int byte_len, uint32_t cp) { + if (!st || !st->cells || !utf8 || byte_len <= 0) return; + + int width = term_char_width(cp); + if (width == 2 && st->cursor_x + 2 > st->cols) { + width = 1; + } + if (st->cursor_x + width > st->cols) { + st->cursor_x = 0; + st->cursor_y++; + if (st->cursor_y >= st->rows) { + term_scroll_up(st); + st->cursor_y = st->rows - 1; + } + } + + TermCell *cell = &st->cells[st->cursor_y * st->cols + st->cursor_x]; + int copy_len = byte_len < 4 ? byte_len : 4; + memcpy(cell->utf8, utf8, (size_t)copy_len); + cell->utf8[copy_len] = '\0'; + cell->wide_cont = 0; + cell->fg = st->cur_fg; + cell->bg = st->cur_bg; + + if (width == 2) { + TermCell *cont = &st->cells[st->cursor_y * st->cols + st->cursor_x + 1]; + term_cell_clear(cont, st->cur_fg, st->cur_bg); + cont->wide_cont = 1; + } + + term_advance_cursor(st, width); +} + +static void term_clear_screen(TermState *st) { + if (!st || !st->cells) return; + for (int i = 0; i < st->cols * st->rows; i++) { + term_cell_clear(&st->cells[i], st->cur_fg, st->cur_bg); + } + st->cursor_x = 0; + st->cursor_y = 0; + st->utf8_len = 0; + st->utf8_needed = 0; +} + +static void term_clear_eol(TermState *st) { + if (!st || !st->cells) return; + if (st->cursor_y < 0 || st->cursor_y >= st->rows) return; + for (int x = st->cursor_x; x < st->cols; x++) { + term_cell_clear(&st->cells[st->cursor_y * st->cols + x], + st->cur_fg, st->cur_bg); + } +} + +static void term_set_cursor(TermState *st, int row, int col) { + if (!st) return; + if (row < 1) row = 1; + if (col < 1) col = 1; + st->cursor_y = row - 1; + st->cursor_x = col - 1; + if (st->cursor_y >= st->rows) st->cursor_y = st->rows - 1; + if (st->cursor_x >= st->cols) st->cursor_x = st->cols - 1; +} + +static uint32_t term_ansi_color(int code, bool bg) { + if (code == 0) { + return bg ? TERM_DEFAULT_BG : TERM_DEFAULT_FG; + } + if (code >= 30 && code <= 37) { + return ANSI_COLORS[code - 30]; + } + if (code >= 40 && code <= 47) { + return ANSI_COLORS[code - 40]; + } + if (code >= 90 && code <= 97) { + return ANSI_COLORS[8 + (code - 90)]; + } + if (code >= 100 && code <= 107) { + return ANSI_COLORS[8 + (code - 100)]; + } + return bg ? TERM_DEFAULT_BG : TERM_DEFAULT_FG; +} + +static void term_apply_sgr(TermState *st, const char *params) { + if (!st || !params) return; + + int values[16]; + int count = 0; + const char *p = params; + + while (*p && count < 16) { + int val = 0; + while (*p >= '0' && *p <= '9') { + val = val * 10 + (*p - '0'); + p++; + } + values[count++] = val; + if (*p == ';') p++; + else if (*p == '\0') break; + else break; + } + + if (count == 0) { + st->cur_fg = TERM_DEFAULT_FG; + st->cur_bg = TERM_DEFAULT_BG; + return; + } + + for (int i = 0; i < count; i++) { + int code = values[i]; + if (code == 0) { + st->cur_fg = TERM_DEFAULT_FG; + st->cur_bg = TERM_DEFAULT_BG; + } else if (code == 1) { + /* bold: use bright foreground if currently default palette */ + } else if (code >= 30 && code <= 37) { + st->cur_fg = term_ansi_color(code, false); + } else if (code >= 40 && code <= 47) { + st->cur_bg = term_ansi_color(code, true); + } else if (code >= 90 && code <= 97) { + st->cur_fg = term_ansi_color(code, false); + } else if (code >= 100 && code <= 107) { + st->cur_bg = term_ansi_color(code, true); + } + } +} + +static void term_handle_csi(TermState *st, char final_ch) { + if (!st) return; + + st->esc_buf[st->esc_len] = '\0'; + + if (final_ch == 'm') { + term_apply_sgr(st, st->esc_buf); + return; + } + + if (final_ch == 'K') { + term_clear_eol(st); + return; + } + + if (final_ch == 'J') { + int mode = 0; + if (st->esc_len > 0) mode = st->esc_buf[0] - '0'; + if (mode == 2) term_clear_screen(st); + return; + } + + if (final_ch == 'H' || final_ch == 'f') { + int row = 1; + int col = 1; + char *semi = strchr(st->esc_buf, ';'); + if (semi) { + *semi = '\0'; + row = st->esc_buf[0] ? atoi(st->esc_buf) : 1; + col = semi[1] ? atoi(semi + 1) : 1; + } else if (st->esc_len > 0) { + row = atoi(st->esc_buf); + } + term_set_cursor(st, row, col); + } +} + +static void term_flush_invalid_utf8(TermState *st) { + if (!st || st->utf8_len <= 0) return; + + static const char repl[] = "\xEF\xBF\xBD"; + term_put_utf8(st, repl, 3, 0xFFFDu); + st->utf8_len = 0; + st->utf8_needed = 0; +} + +static void term_feed_text_byte(TermState *st, unsigned char byte) { + if (!st) return; + + if (st->utf8_len > 0 && (byte & 0xC0u) != 0x80u) { + term_flush_invalid_utf8(st); + } + + if (st->utf8_len == 0) { + st->utf8_needed = term_utf8_lead_len(byte); + if (st->utf8_needed == 1) { + char ch = (char)byte; + term_put_utf8(st, &ch, 1, (uint32_t)byte); + return; + } + } + + if (st->utf8_len >= (int)sizeof(st->utf8_buf)) { + term_flush_invalid_utf8(st); + } + + st->utf8_buf[st->utf8_len++] = byte; + if (st->utf8_len < st->utf8_needed) { + return; + } + + st->utf8_buf[st->utf8_len] = '\0'; + int adv = 0; + uint32_t cp = text_decode_utf8((const char *)st->utf8_buf, &adv); + if (adv > 0) { + term_put_utf8(st, (const char *)st->utf8_buf, adv, cp); + } else { + term_flush_invalid_utf8(st); + } + + st->utf8_len = 0; + st->utf8_needed = 0; +} + +static void term_feed_byte(TermState *st, unsigned char byte) { + if (!st) return; + + switch (st->parse_state) { + case PARSE_NORMAL: + if (byte == 0x1b) { + term_flush_invalid_utf8(st); + st->parse_state = PARSE_ESC; + st->esc_len = 0; + } else if (byte == '\r') { + term_flush_invalid_utf8(st); + st->cursor_x = 0; + } else if (byte == '\n') { + term_flush_invalid_utf8(st); + st->cursor_x = 0; + st->cursor_y++; + if (st->cursor_y >= st->rows) { + term_scroll_up(st); + st->cursor_y = st->rows - 1; + } + } else if (byte == '\b') { + term_flush_invalid_utf8(st); + term_backspace(st); + } else if (byte == '\t') { + term_flush_invalid_utf8(st); + int next = ((st->cursor_x / 8) + 1) * 8; + st->cursor_x = next; + } else if (byte >= 0x20 && byte != 0x7f) { + term_feed_text_byte(st, byte); + } + break; + + case PARSE_ESC: + if (byte == '[') { + st->parse_state = PARSE_CSI; + st->esc_len = 0; + } else { + st->parse_state = PARSE_NORMAL; + } + break; + + case PARSE_CSI: + if ((byte >= '0' && byte <= '9') || byte == ';' || byte == '?') { + if (st->esc_len + 1 < ESC_BUF_SZ) { + st->esc_buf[st->esc_len++] = (char)byte; + } + } else { + term_handle_csi(st, (char)byte); + st->parse_state = PARSE_NORMAL; + st->esc_len = 0; + } + break; + } +} + +static void term_feed(TermState *st, const char *data, int len) { + for (int i = 0; i < len; i++) { + term_feed_byte(st, (unsigned char)data[i]); + } +} + +static void term_pty_write(TermState *st, const char *data, int len) { + if (!st || st->pty_id < 0 || !data || len <= 0) return; + sys_tty_write_in(st->pty_id, data, len); +} + +static bool term_spawn_shell(TermState *st) { + if (!st) return false; + + st->pty_id = sys_pty_create(); + if (st->pty_id < 0) { + fprintf(stderr, "term: failed to create PTY\n"); + return false; + } + + st->shell_pid = sys_spawn("/bin/bsh.elf", NULL, + SPAWN_FLAG_TERMINAL | SPAWN_FLAG_TTY_ID, + (uint64_t)st->pty_id); + if (st->shell_pid < 0) { + fprintf(stderr, "term: failed to spawn bsh on PTY %d\n", st->pty_id); + sys_pty_destroy(st->pty_id); + st->pty_id = -1; + return false; + } + + sys_tty_set_fg(st->pty_id, st->shell_pid); + return true; +} + +static void term_shutdown(TermState *st) { + if (!st) return; + if (st->pty_id >= 0) { + sys_tty_kill_all(st->pty_id); + sys_pty_destroy(st->pty_id); + st->pty_id = -1; + st->shell_pid = -1; + } +} + +static void on_draw(NovaApp *app) { + TermState *st = app_get_userdata(app); + if (!st || !st->cells) return; + + uint32_t *pixels = app_pixels(app); + int width = (int)app_width(app); + int height = (int)app_height(app); + if (!pixels) return; + + for (int y = 0; y < st->rows; y++) { + for (int x = 0; x < st->cols; x++) { + TermCell *cell = &st->cells[y * st->cols + x]; + int px = x * CELL_W; + int py = y * CELL_H; + + ui_draw_panel(pixels, width, height, + px, py, CELL_W, CELL_H, + cell->bg, cell->bg, 0); + + if (!cell->wide_cont && cell->utf8[0] != '\0') { + ui_draw_string(pixels, width, height, + px, py + 1, cell->utf8, cell->fg); + } + } + } +} + +static void on_idle(NovaApp *app) { + TermState *st = app_get_userdata(app); + if (!st || st->pty_id < 0) return; + + int cols = term_cols(app_width(app)); + int rows = term_rows(app_height(app)); + if (cols != st->cols || rows != st->rows) { + term_resize_grid(st, cols, rows); + app_request_redraw(app); + } + + char buf[PTY_READ_SZ]; + bool updated = false; + + for (;;) { + int n = sys_tty_read_out(st->pty_id, buf, (int)sizeof(buf)); + if (n <= 0) break; + term_feed(st, buf, n); + updated = true; + } + + if (updated) { + app_request_redraw(app); + } +} + +static void send_bytes(TermState *st, const char *data, int len) { + term_pty_write(st, data, len); +} + +static void on_key(NovaApp *app, uint32_t keycode, uint32_t modifiers, bool pressed) { + if (!pressed) return; + + TermState *st = app_get_userdata(app); + if (!st || st->pty_id < 0) return; + + bool ctrl = (modifiers & NOVA_KMOD_CTRL) != 0; + + if (ctrl && keycode >= KEY_A && keycode <= KEY_Z) { + char ch = (char)keycode; + send_bytes(st, &ch, 1); + return; + } + + switch (keycode) { + case KEY_ENTER: + send_bytes(st, "\r", 1); + break; + case KEY_BACKSPACE: + send_bytes(st, "\x7f", 1); + break; + case KEY_TAB: + send_bytes(st, "\t", 1); + break; + case KEY_ESCAPE: + send_bytes(st, "\x1b", 1); + break; + case KEY_UP: + send_bytes(st, "\x1b[A", 3); + break; + case KEY_DOWN: + send_bytes(st, "\x1b[B", 3); + break; + case KEY_RIGHT: + send_bytes(st, "\x1b[C", 3); + break; + case KEY_LEFT: + send_bytes(st, "\x1b[D", 3); + break; + default: + break; + } +} + +static void on_text(NovaApp *app, const char *text, uint32_t codepoint) { + (void)codepoint; + TermState *st = app_get_userdata(app); + if (!st || st->pty_id < 0 || !text || !text[0]) return; + term_pty_write(st, text, (int)strlen(text)); +} + +static bool on_close(NovaApp *app) { + TermState *st = app_get_userdata(app); + term_shutdown(st); + return true; +} + +int main(void) { + TermState st = { + .pty_id = -1, + .shell_pid = -1, + .cur_fg = TERM_DEFAULT_FG, + .cur_bg = TERM_DEFAULT_BG, + }; + + NovaApp *app = app_create("Terminal", 720, 480); + if (!app) return 1; + + app_set_userdata(app, &st); + term_resize_grid(&st, term_cols(app_width(app)), term_rows(app_height(app))); + + if (!term_spawn_shell(&st)) { + app_destroy(app); + free(st.cells); + return 1; + } + + app_on_draw(app, on_draw); + app_on_idle(app, on_idle); + app_on_key(app, on_key); + app_on_text(app, on_text); + app_on_close(app, on_close); + + int rc = app_run(app); + + term_shutdown(&st); + free(st.cells); + app_destroy(app); + return rc; +} From b7cee3dc4e72b2bf2df9524379a45b0699c10d75 Mon Sep 17 00:00:00 2001 From: Lluciocc Date: Sat, 6 Jun 2026 15:28:51 +0200 Subject: [PATCH 02/10] fix ansi --- src/term.c | 242 +++++++++++++++++++++++++++++++++++------------------ 1 file changed, 160 insertions(+), 82 deletions(-) diff --git a/src/term.c b/src/term.c index 2deeca2..fac25d3 100644 --- a/src/term.c +++ b/src/term.c @@ -19,8 +19,8 @@ #define CELL_W 8 #define CELL_H 16 -#define PTY_READ_SZ 4096 -#define ESC_BUF_SZ 32 +#define PTY_READ_SZ 4096 +#define TERM_ESC_MAX_PARAMS 15 #define TERM_DEFAULT_FG 0xFFCCCCCCu #define TERM_DEFAULT_BG 0xFF1E1E1Eu @@ -36,6 +36,7 @@ typedef enum { PARSE_NORMAL = 0, PARSE_ESC, PARSE_CSI, + PARSE_CSI_PRIVATE, } ParseState; typedef struct { @@ -51,19 +52,39 @@ typedef struct { uint32_t cur_bg; ParseState parse_state; - char esc_buf[ESC_BUF_SZ]; - int esc_len; + int esc_params[TERM_ESC_MAX_PARAMS + 1]; + int esc_param_idx; unsigned char utf8_buf[4]; int utf8_len; int utf8_needed; } TermState; -static const uint32_t ANSI_COLORS[16] = { - 0xFF1E1E1Eu, 0xFFCC6666u, 0xFF66CC66u, 0xFFCCCC66u, - 0xFF6666CCu, 0xFFCC66CCu, 0xFF66CCCCu, 0xFFCCCCCCu, - 0xFF555555u, 0xFFFF6666u, 0xFF66FF66u, 0xFFFFFF66u, - 0xFF6666FFu, 0xFFFF66FFu, 0xFF66FFFFu, 0xFFFFFFFFu, +static const uint32_t ANSI_STD_FG[8] = { + 0xFF000000u, 0xFFFF4444u, 0xFF6A9955u, 0xFFFFCC00u, + 0xFF569CD6u, 0xFFC586C0u, 0xFF4EC9B0u, 0xFFFFFFFFu, +}; + +static const uint32_t ANSI_STD_BG[8] = { + 0xFF000000u, 0xFFFF4444u, 0xFF6A9955u, 0xFFFFCC00u, + 0xFF569CD6u, 0xFFC586C0u, 0xFF4EC9B0u, 0xFFFFFFFFu, +}; + +static const uint32_t ANSI_BRIGHT_FG[8] = { + 0xFF555555u, 0xFFFF5555u, 0xFF55FF55u, 0xFFFFFF55u, + 0xFF5555FFu, 0xFFFF55FFu, 0xFF55FFFFu, 0xFFFFFFFFu, +}; + +static const uint32_t ANSI_BRIGHT_BG[8] = { + 0xFF555555u, 0xFFFF5555u, 0xFF55FF55u, 0xFFFFFF55u, + 0xFF5555FFu, 0xFFFF55FFu, 0xFF55FFFFu, 0xFFFFFFFFu, +}; + +static const uint32_t ANSI_16[16] = { + 0xFF000000u, 0xFF800000u, 0xFF008000u, 0xFF808000u, + 0xFF000080u, 0xFF800080u, 0xFF008080u, 0xFFC0C0C0u, + 0xFF808080u, 0xFFFF0000u, 0xFF00FF00u, 0xFFFFFF00u, + 0xFF0000FFu, 0xFFFF00FFu, 0xFF00FFFFu, 0xFFFFFFFFu, }; static int term_cols(uint32_t width) { @@ -240,65 +261,77 @@ static void term_set_cursor(TermState *st, int row, int col) { if (st->cursor_x >= st->cols) st->cursor_x = st->cols - 1; } -static uint32_t term_ansi_color(int code, bool bg) { - if (code == 0) { - return bg ? TERM_DEFAULT_BG : TERM_DEFAULT_FG; - } - if (code >= 30 && code <= 37) { - return ANSI_COLORS[code - 30]; - } - if (code >= 40 && code <= 47) { - return ANSI_COLORS[code - 40]; - } - if (code >= 90 && code <= 97) { - return ANSI_COLORS[8 + (code - 90)]; - } - if (code >= 100 && code <= 107) { - return ANSI_COLORS[8 + (code - 100)]; - } - return bg ? TERM_DEFAULT_BG : TERM_DEFAULT_FG; +static void term_csi_begin(TermState *st) { + if (!st) return; + st->esc_param_idx = 0; + memset(st->esc_params, 0, sizeof(st->esc_params)); } -static void term_apply_sgr(TermState *st, const char *params) { - if (!st || !params) return; - - int values[16]; - int count = 0; - const char *p = params; +static uint32_t term_color_256(int index) { + if (index < 0) return TERM_DEFAULT_FG; + if (index < 16) return ANSI_16[index]; - while (*p && count < 16) { - int val = 0; - while (*p >= '0' && *p <= '9') { - val = val * 10 + (*p - '0'); - p++; - } - values[count++] = val; - if (*p == ';') p++; - else if (*p == '\0') break; - else break; + if (index < 232) { + static const int levels[6] = { 0, 95, 135, 175, 215, 255 }; + int rem = index - 16; + int r = levels[rem / 36]; + rem %= 36; + int g = levels[rem / 6]; + int b = levels[rem % 6]; + return 0xFF000000u | ((uint32_t)r << 16) | ((uint32_t)g << 8) | (uint32_t)b; } - if (count == 0) { - st->cur_fg = TERM_DEFAULT_FG; - st->cur_bg = TERM_DEFAULT_BG; - return; - } + int gray = 8 + (index - 232) * 10; + if (gray > 255) gray = 255; + return 0xFF000000u | ((uint32_t)gray << 16) | + ((uint32_t)gray << 8) | (uint32_t)gray; +} + +static void term_apply_sgr(TermState *st) { + if (!st) return; - for (int i = 0; i < count; i++) { - int code = values[i]; - if (code == 0) { + for (int j = 0; j <= st->esc_param_idx; j++) { + int p = st->esc_params[j]; + + if (p == 0) { + st->cur_fg = TERM_DEFAULT_FG; + st->cur_bg = TERM_DEFAULT_BG; + } else if (p == 1) { + /* bold/intense - palette already includes bright variants */ + } else if (p >= 30 && p <= 37) { + st->cur_fg = ANSI_STD_FG[p - 30]; + } else if (p == 38) { + if (j + 2 <= st->esc_param_idx && st->esc_params[j + 1] == 5) { + st->cur_fg = term_color_256(st->esc_params[j + 2]); + j += 2; + } else if (j + 4 <= st->esc_param_idx && st->esc_params[j + 1] == 2) { + st->cur_fg = 0xFF000000u | + ((uint32_t)st->esc_params[j + 2] << 16) | + ((uint32_t)st->esc_params[j + 3] << 8) | + (uint32_t)st->esc_params[j + 4]; + j += 4; + } + } else if (p == 39) { st->cur_fg = TERM_DEFAULT_FG; + } else if (p >= 40 && p <= 47) { + st->cur_bg = ANSI_STD_BG[p - 40]; + } else if (p == 48) { + if (j + 2 <= st->esc_param_idx && st->esc_params[j + 1] == 5) { + st->cur_bg = term_color_256(st->esc_params[j + 2]); + j += 2; + } else if (j + 4 <= st->esc_param_idx && st->esc_params[j + 1] == 2) { + st->cur_bg = 0xFF000000u | + ((uint32_t)st->esc_params[j + 2] << 16) | + ((uint32_t)st->esc_params[j + 3] << 8) | + (uint32_t)st->esc_params[j + 4]; + j += 4; + } + } else if (p == 49) { st->cur_bg = TERM_DEFAULT_BG; - } else if (code == 1) { - /* bold: use bright foreground if currently default palette */ - } else if (code >= 30 && code <= 37) { - st->cur_fg = term_ansi_color(code, false); - } else if (code >= 40 && code <= 47) { - st->cur_bg = term_ansi_color(code, true); - } else if (code >= 90 && code <= 97) { - st->cur_fg = term_ansi_color(code, false); - } else if (code >= 100 && code <= 107) { - st->cur_bg = term_ansi_color(code, true); + } else if (p >= 90 && p <= 97) { + st->cur_fg = ANSI_BRIGHT_FG[p - 90]; + } else if (p >= 100 && p <= 107) { + st->cur_bg = ANSI_BRIGHT_BG[p - 100]; } } } @@ -306,10 +339,8 @@ static void term_apply_sgr(TermState *st, const char *params) { static void term_handle_csi(TermState *st, char final_ch) { if (!st) return; - st->esc_buf[st->esc_len] = '\0'; - if (final_ch == 'm') { - term_apply_sgr(st, st->esc_buf); + term_apply_sgr(st); return; } @@ -319,24 +350,51 @@ static void term_handle_csi(TermState *st, char final_ch) { } if (final_ch == 'J') { - int mode = 0; - if (st->esc_len > 0) mode = st->esc_buf[0] - '0'; - if (mode == 2) term_clear_screen(st); + int mode = st->esc_params[0]; + if (mode == 2) { + term_clear_screen(st); + } return; } if (final_ch == 'H' || final_ch == 'f') { - int row = 1; - int col = 1; - char *semi = strchr(st->esc_buf, ';'); - if (semi) { - *semi = '\0'; - row = st->esc_buf[0] ? atoi(st->esc_buf) : 1; - col = semi[1] ? atoi(semi + 1) : 1; - } else if (st->esc_len > 0) { - row = atoi(st->esc_buf); - } + int row = st->esc_params[0]; + int col = (st->esc_param_idx >= 1) ? st->esc_params[1] : 0; + if (row <= 0) row = 1; + if (col <= 0) col = 1; term_set_cursor(st, row, col); + return; + } + + if (final_ch == 'A') { + int n = st->esc_params[0]; + if (n <= 0) n = 1; + st->cursor_y -= n; + if (st->cursor_y < 0) st->cursor_y = 0; + return; + } + + if (final_ch == 'B') { + int n = st->esc_params[0]; + if (n <= 0) n = 1; + st->cursor_y += n; + if (st->cursor_y >= st->rows) st->cursor_y = st->rows - 1; + return; + } + + if (final_ch == 'C') { + int n = st->esc_params[0]; + if (n <= 0) n = 1; + st->cursor_x += n; + if (st->cursor_x >= st->cols) st->cursor_x = st->cols - 1; + return; + } + + if (final_ch == 'D') { + int n = st->esc_params[0]; + if (n <= 0) n = 1; + st->cursor_x -= n; + if (st->cursor_x < 0) st->cursor_x = 0; } } @@ -395,7 +453,6 @@ static void term_feed_byte(TermState *st, unsigned char byte) { if (byte == 0x1b) { term_flush_invalid_utf8(st); st->parse_state = PARSE_ESC; - st->esc_len = 0; } else if (byte == '\r') { term_flush_invalid_utf8(st); st->cursor_x = 0; @@ -422,21 +479,42 @@ static void term_feed_byte(TermState *st, unsigned char byte) { case PARSE_ESC: if (byte == '[') { st->parse_state = PARSE_CSI; - st->esc_len = 0; + term_csi_begin(st); } else { st->parse_state = PARSE_NORMAL; } break; case PARSE_CSI: - if ((byte >= '0' && byte <= '9') || byte == ';' || byte == '?') { - if (st->esc_len + 1 < ESC_BUF_SZ) { - st->esc_buf[st->esc_len++] = (char)byte; + if (byte == '?') { + st->parse_state = PARSE_CSI_PRIVATE; + term_csi_begin(st); + } else if (byte >= '0' && byte <= '9') { + st->esc_params[st->esc_param_idx] = + st->esc_params[st->esc_param_idx] * 10 + (byte - '0'); + } else if (byte == ';') { + if (st->esc_param_idx < TERM_ESC_MAX_PARAMS) { + st->esc_param_idx++; } } else { term_handle_csi(st, (char)byte); st->parse_state = PARSE_NORMAL; - st->esc_len = 0; + } + break; + + case PARSE_CSI_PRIVATE: + if (byte >= '0' && byte <= '9') { + st->esc_params[st->esc_param_idx] = + st->esc_params[st->esc_param_idx] * 10 + (byte - '0'); + } else if (byte == ';') { + if (st->esc_param_idx < TERM_ESC_MAX_PARAMS) { + st->esc_param_idx++; + } + } else if (byte == 'h' || byte == 'l') { + /* DEC private modes, e.g. ESC[?25h cursor show */ + st->parse_state = PARSE_NORMAL; + } else { + st->parse_state = PARSE_NORMAL; } break; } From 53bc524cdb3e82e30289867bd1f3e3f26ce0a352 Mon Sep 17 00:00:00 2001 From: Lluciocc Date: Sat, 6 Jun 2026 15:31:25 +0200 Subject: [PATCH 03/10] change name of the file --- Makefile | 2 +- src/{term.c => terminal.c} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename src/{term.c => terminal.c} (100%) diff --git a/Makefile b/Makefile index d1d93e2..1fd3e05 100644 --- a/Makefile +++ b/Makefile @@ -34,7 +34,7 @@ LDFLAGS = -m elf_x86_64 -nostdlib -static -no-pie -Ttext=0x40000000 \ -L$(SDK_PATH)/lib LIBS = obj/libnovaproto.a obj/libtheme.a obj/libui.a obj/libapp.a obj/libwidget.a -APPS = nova.elf taskbar.elf wallpaperd.elf about.elf helloworld.elf term.elf +APPS = nova.elf taskbar.elf wallpaperd.elf about.elf helloworld.elf terminal.elf all: bootstrap-sdk $(MAKE) export-sdk diff --git a/src/term.c b/src/terminal.c similarity index 100% rename from src/term.c rename to src/terminal.c From 2e3693dfa68e1f3a0ccd5d9d531093ffb25e1c04 Mon Sep 17 00:00:00 2001 From: Lluciocc Date: Sat, 6 Jun 2026 15:51:56 +0200 Subject: [PATCH 04/10] term opti --- src/terminal.c | 90 ++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 77 insertions(+), 13 deletions(-) diff --git a/src/terminal.c b/src/terminal.c index fac25d3..ba432f6 100644 --- a/src/terminal.c +++ b/src/terminal.c @@ -145,9 +145,13 @@ static void term_resize_grid(TermState *st, int cols, int rows) { if (cols < 1) cols = 1; if (rows < 1) rows = 1; - TermCell *next = calloc((size_t)cols * (size_t)rows, sizeof(TermCell)); + TermCell *next = malloc((size_t)cols * (size_t)rows * sizeof(TermCell)); if (!next) return; + for (int i = 0; i < cols * rows; i++) { + term_cell_clear(&next[i], TERM_DEFAULT_FG, TERM_DEFAULT_BG); + } + if (st->cells && st->cols > 0 && st->rows > 0) { int copy_cols = cols < st->cols ? cols : st->cols; int copy_rows = rows < st->rows ? rows : st->rows; @@ -156,10 +160,6 @@ static void term_resize_grid(TermState *st, int cols, int rows) { st->cells + y * st->cols, (size_t)copy_cols * sizeof(TermCell)); } - } else { - for (int i = 0; i < cols * rows; i++) { - term_cell_clear(&next[i], st->cur_fg, st->cur_bg); - } } free(st->cells); @@ -564,6 +564,74 @@ static void term_shutdown(TermState *st) { } } +static void term_fill_rect(uint32_t *pixels, int width, int height, + int x, int y, int rw, int rh, uint32_t color) { + if (!pixels || width <= 0 || height <= 0 || rw <= 0 || rh <= 0) return; + + int x1 = x; + int y1 = y; + int x2 = x + rw; + int y2 = y + rh; + + if (x1 < 0) x1 = 0; + if (y1 < 0) y1 = 0; + if (x2 > width) x2 = width; + if (y2 > height) y2 = height; + if (x2 <= x1 || y2 <= y1) return; + + int fill_w = x2 - x1; + for (int py = y1; py < y2; py++) { + uint32_t *p = pixels + py * width + x1; + uint32_t *end = p + fill_w; + while (p < end) { + *p++ = color; + } + } +} + +static void term_draw_backgrounds(TermState *st, + uint32_t *pixels, int width, int height) { + term_fill_rect(pixels, width, height, 0, 0, width, height, TERM_DEFAULT_BG); + + for (int y = 0; y < st->rows; y++) { + int run_start = -1; + uint32_t run_bg = TERM_DEFAULT_BG; + + for (int x = 0; x < st->cols; x++) { + uint32_t bg = st->cells[y * st->cols + x].bg; + if (bg == TERM_DEFAULT_BG) { + if (run_start >= 0) { + term_fill_rect(pixels, width, height, + run_start * CELL_W, y * CELL_H, + (x - run_start) * CELL_W, CELL_H, + run_bg); + run_start = -1; + } + continue; + } + + if (run_start < 0) { + run_start = x; + run_bg = bg; + } else if (bg != run_bg) { + term_fill_rect(pixels, width, height, + run_start * CELL_W, y * CELL_H, + (x - run_start) * CELL_W, CELL_H, + run_bg); + run_start = x; + run_bg = bg; + } + } + + if (run_start >= 0) { + term_fill_rect(pixels, width, height, + run_start * CELL_W, y * CELL_H, + (st->cols - run_start) * CELL_W, CELL_H, + run_bg); + } + } +} + static void on_draw(NovaApp *app) { TermState *st = app_get_userdata(app); if (!st || !st->cells) return; @@ -573,19 +641,15 @@ static void on_draw(NovaApp *app) { int height = (int)app_height(app); if (!pixels) return; + term_draw_backgrounds(st, pixels, width, height); + for (int y = 0; y < st->rows; y++) { for (int x = 0; x < st->cols; x++) { TermCell *cell = &st->cells[y * st->cols + x]; - int px = x * CELL_W; - int py = y * CELL_H; - - ui_draw_panel(pixels, width, height, - px, py, CELL_W, CELL_H, - cell->bg, cell->bg, 0); - if (!cell->wide_cont && cell->utf8[0] != '\0') { ui_draw_string(pixels, width, height, - px, py + 1, cell->utf8, cell->fg); + x * CELL_W, y * CELL_H + 1, + cell->utf8, cell->fg); } } } From 20fa02b59e1c18ba64847fe0f9fc3cc6b6b77353 Mon Sep 17 00:00:00 2001 From: Lluciocc Date: Sat, 6 Jun 2026 15:57:17 +0200 Subject: [PATCH 05/10] fix initla prompt idlidng --- src/terminal.c | 50 +++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 39 insertions(+), 11 deletions(-) diff --git a/src/terminal.c b/src/terminal.c index ba432f6..d9dff9a 100644 --- a/src/terminal.c +++ b/src/terminal.c @@ -21,6 +21,8 @@ #define CELL_H 16 #define PTY_READ_SZ 4096 #define TERM_ESC_MAX_PARAMS 15 +#define TERM_STARTUP_DRAIN_TRIES 64 +#define TERM_STARTUP_EMPTY_SETTLE 2 #define TERM_DEFAULT_FG 0xFFCCCCCCu #define TERM_DEFAULT_BG 0xFF1E1E1Eu @@ -526,6 +528,41 @@ static void term_feed(TermState *st, const char *data, int len) { } } +static bool term_drain_pty(TermState *st) { + if (!st || st->pty_id < 0) return false; + + char buf[PTY_READ_SZ]; + bool updated = false; + + for (;;) { + int n = sys_tty_read_out(st->pty_id, buf, (int)sizeof(buf)); + if (n <= 0) break; + term_feed(st, buf, n); + updated = true; + } + + return updated; +} + +static void term_prime_initial_output(TermState *st) { + bool saw_output = false; + int empty_after_output = 0; + + for (int i = 0; i < TERM_STARTUP_DRAIN_TRIES; i++) { + if (term_drain_pty(st)) { + saw_output = true; + empty_after_output = 0; + } else if (saw_output) { + empty_after_output++; + if (empty_after_output >= TERM_STARTUP_EMPTY_SETTLE) { + break; + } + } + + sys_yield(); + } +} + static void term_pty_write(TermState *st, const char *data, int len) { if (!st || st->pty_id < 0 || !data || len <= 0) return; sys_tty_write_in(st->pty_id, data, len); @@ -666,17 +703,7 @@ static void on_idle(NovaApp *app) { app_request_redraw(app); } - char buf[PTY_READ_SZ]; - bool updated = false; - - for (;;) { - int n = sys_tty_read_out(st->pty_id, buf, (int)sizeof(buf)); - if (n <= 0) break; - term_feed(st, buf, n); - updated = true; - } - - if (updated) { + if (term_drain_pty(st)) { app_request_redraw(app); } } @@ -761,6 +788,7 @@ int main(void) { free(st.cells); return 1; } + term_prime_initial_output(&st); app_on_draw(app, on_draw); app_on_idle(app, on_idle); From b6147ec5220aead3d42b4e21d7475f1bbd8b1daa Mon Sep 17 00:00:00 2001 From: Lluciocc Date: Sun, 7 Jun 2026 10:39:08 +0200 Subject: [PATCH 06/10] change size --- src/terminal.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/terminal.c b/src/terminal.c index d9dff9a..c5c2393 100644 --- a/src/terminal.c +++ b/src/terminal.c @@ -777,7 +777,7 @@ int main(void) { .cur_bg = TERM_DEFAULT_BG, }; - NovaApp *app = app_create("Terminal", 720, 480); + NovaApp *app = app_create("Terminal", 820, 480); if (!app) return 1; app_set_userdata(app, &st); From 50c5fc2c57df8c69512604e2ef3a4c82f5b85619 Mon Sep 17 00:00:00 2001 From: Lluciocc Date: Sun, 7 Jun 2026 10:48:50 +0200 Subject: [PATCH 07/10] comment out the code --- src/terminal.c | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/src/terminal.c b/src/terminal.c index c5c2393..a99a882 100644 --- a/src/terminal.c +++ b/src/terminal.c @@ -62,6 +62,7 @@ typedef struct { int utf8_needed; } TermState; +// ANSI color palette for standard and bright colors, used for SGR color codes static const uint32_t ANSI_STD_FG[8] = { 0xFF000000u, 0xFFFF4444u, 0xFF6A9955u, 0xFFFFCC00u, 0xFF569CD6u, 0xFFC586C0u, 0xFF4EC9B0u, 0xFFFFFFFFu, @@ -115,6 +116,7 @@ static int term_utf8_lead_len(unsigned char lead) { return 1; } +// utf8 static int term_char_width(uint32_t cp) { if (cp < 0x1100u) return 1; if ((cp >= 0x1100u && cp <= 0x115Fu) || @@ -132,6 +134,7 @@ static int term_char_width(uint32_t cp) { return 1; } +// move cursor left + handle wide char continuation cells static void term_backspace(TermState *st) { if (!st || st->cursor_x <= 0) return; @@ -142,6 +145,7 @@ static void term_backspace(TermState *st) { } } +// Resize the terminal grid, preserving existing content as much as possible static void term_resize_grid(TermState *st, int cols, int rows) { if (!st) return; if (cols < 1) cols = 1; @@ -173,6 +177,7 @@ static void term_resize_grid(TermState *st, int cols, int rows) { if (st->cursor_y >= rows) st->cursor_y = rows - 1; } +// Scroll the terminal content up by one line, clearing the new bottom line static void term_scroll_up(TermState *st) { if (!st || !st->cells || st->rows <= 1) return; @@ -200,6 +205,7 @@ static void term_advance_cursor(TermState *st, int cols_used) { } } +// Put a UTF-8 character at the current cursor position, handling wide chars and line wrapping static void term_put_utf8(TermState *st, const char *utf8, int byte_len, uint32_t cp) { if (!st || !st->cells || !utf8 || byte_len <= 0) return; @@ -269,6 +275,7 @@ static void term_csi_begin(TermState *st) { memset(st->esc_params, 0, sizeof(st->esc_params)); } +// Convert a 256-color palette index to an ARGB color value (found on the internet) static uint32_t term_color_256(int index) { if (index < 0) return TERM_DEFAULT_FG; if (index < 16) return ANSI_16[index]; @@ -289,6 +296,7 @@ static uint32_t term_color_256(int index) { ((uint32_t)gray << 8) | (uint32_t)gray; } +// Apply SGR (Select Graphic Rendition) parameters to update current foreground/background colors static void term_apply_sgr(TermState *st) { if (!st) return; @@ -299,7 +307,7 @@ static void term_apply_sgr(TermState *st) { st->cur_fg = TERM_DEFAULT_FG; st->cur_bg = TERM_DEFAULT_BG; } else if (p == 1) { - /* bold/intense - palette already includes bright variants */ + // bold/bright -> for simplicity, we just switch to the bright palette for fg colors } else if (p >= 30 && p <= 37) { st->cur_fg = ANSI_STD_FG[p - 30]; } else if (p == 38) { @@ -400,6 +408,7 @@ static void term_handle_csi(TermState *st, char final_ch) { } } +// Invalid feedback static void term_flush_invalid_utf8(TermState *st) { if (!st || st->utf8_len <= 0) return; @@ -770,6 +779,7 @@ static bool on_close(NovaApp *app) { } int main(void) { + // Initialize terminal state with defaults and invalid PTY/shell IDs TermState st = { .pty_id = -1, .shell_pid = -1, @@ -777,19 +787,22 @@ int main(void) { .cur_bg = TERM_DEFAULT_BG, }; + // Create the application window and initialize terminal state NovaApp *app = app_create("Terminal", 820, 480); if (!app) return 1; - app_set_userdata(app, &st); + app_set_userdata(app, &st); // Associate our terminal state with the app for callback access term_resize_grid(&st, term_cols(app_width(app)), term_rows(app_height(app))); + // Spawn the shell process connected to our PTY and prime the initial output if (!term_spawn_shell(&st)) { app_destroy(app); free(st.cells); return 1; } - term_prime_initial_output(&st); + term_prime_initial_output(&st); // Draw the first frame of bsh output + // Set up app callbacks for drawing, idle processing, key input, text input, and close events app_on_draw(app, on_draw); app_on_idle(app, on_idle); app_on_key(app, on_key); @@ -798,6 +811,7 @@ int main(void) { int rc = app_run(app); + // Clean up terminal state and app resources on exit term_shutdown(&st); free(st.cells); app_destroy(app); From abf5a620e4ded47b50db415866104aed70f50121 Mon Sep 17 00:00:00 2001 From: Lluciocc Date: Sun, 7 Jun 2026 23:44:17 +0200 Subject: [PATCH 08/10] fix ansi and utf8 for term --- src/terminal.c | 352 ++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 332 insertions(+), 20 deletions(-) diff --git a/src/terminal.c b/src/terminal.c index a99a882..4ff6874 100644 --- a/src/terminal.c +++ b/src/terminal.c @@ -23,6 +23,7 @@ #define TERM_ESC_MAX_PARAMS 15 #define TERM_STARTUP_DRAIN_TRIES 64 #define TERM_STARTUP_EMPTY_SETTLE 2 +#define TERM_FONT_SIZE 14 #define TERM_DEFAULT_FG 0xFFCCCCCCu #define TERM_DEFAULT_BG 0xFF1E1E1Eu @@ -50,14 +51,18 @@ typedef struct { int cursor_x; int cursor_y; + int saved_cursor_x; + int saved_cursor_y; uint32_t cur_fg; uint32_t cur_bg; + bool reverse; + bool cursor_visible; ParseState parse_state; int esc_params[TERM_ESC_MAX_PARAMS + 1]; int esc_param_idx; - unsigned char utf8_buf[4]; + unsigned char utf8_buf[5]; int utf8_len; int utf8_needed; } TermState; @@ -108,12 +113,22 @@ static void term_cell_clear(TermCell *cell, uint32_t fg, uint32_t bg) { cell->bg = bg; } +static uint32_t term_effective_fg(const TermState *st) { + if (!st) return TERM_DEFAULT_FG; + return st->reverse ? st->cur_bg : st->cur_fg; +} + +static uint32_t term_effective_bg(const TermState *st) { + if (!st) return TERM_DEFAULT_BG; + return st->reverse ? st->cur_fg : st->cur_bg; +} + static int term_utf8_lead_len(unsigned char lead) { if ((lead & 0x80u) == 0u) return 1; if ((lead & 0xE0u) == 0xC0u) return 2; if ((lead & 0xF0u) == 0xE0u) return 3; if ((lead & 0xF8u) == 0xF0u) return 4; - return 1; + return 0; } // utf8 @@ -186,8 +201,10 @@ static void term_scroll_up(TermState *st) { (size_t)st->cols * (size_t)(st->rows - 1) * sizeof(TermCell)); TermCell *last = st->cells + st->cols * (st->rows - 1); + uint32_t fg = term_effective_fg(st); + uint32_t bg = term_effective_bg(st); for (int x = 0; x < st->cols; x++) { - term_cell_clear(&last[x], st->cur_fg, st->cur_bg); + term_cell_clear(&last[x], fg, bg); } } @@ -205,12 +222,26 @@ static void term_advance_cursor(TermState *st, int cols_used) { } } +static void term_prepare_cell_for_write(TermState *st, int x, int y, + uint32_t fg, uint32_t bg) { + if (!st || !st->cells) return; + if (x < 0 || x >= st->cols || y < 0 || y >= st->rows) return; + + int idx = y * st->cols + x; + if (st->cells[idx].wide_cont && x > 0) { + term_cell_clear(&st->cells[idx - 1], fg, bg); + } + if (x + 1 < st->cols && st->cells[idx + 1].wide_cont) { + term_cell_clear(&st->cells[idx + 1], fg, bg); + } +} + // Put a UTF-8 character at the current cursor position, handling wide chars and line wrapping static void term_put_utf8(TermState *st, const char *utf8, int byte_len, uint32_t cp) { if (!st || !st->cells || !utf8 || byte_len <= 0) return; int width = term_char_width(cp); - if (width == 2 && st->cursor_x + 2 > st->cols) { + if (width > st->cols) { width = 1; } if (st->cursor_x + width > st->cols) { @@ -222,17 +253,24 @@ static void term_put_utf8(TermState *st, const char *utf8, int byte_len, uint32_ } } + uint32_t fg = term_effective_fg(st); + uint32_t bg = term_effective_bg(st); + term_prepare_cell_for_write(st, st->cursor_x, st->cursor_y, fg, bg); + if (width == 2) { + term_prepare_cell_for_write(st, st->cursor_x + 1, st->cursor_y, fg, bg); + } + TermCell *cell = &st->cells[st->cursor_y * st->cols + st->cursor_x]; int copy_len = byte_len < 4 ? byte_len : 4; memcpy(cell->utf8, utf8, (size_t)copy_len); cell->utf8[copy_len] = '\0'; cell->wide_cont = 0; - cell->fg = st->cur_fg; - cell->bg = st->cur_bg; + cell->fg = fg; + cell->bg = bg; if (width == 2) { TermCell *cont = &st->cells[st->cursor_y * st->cols + st->cursor_x + 1]; - term_cell_clear(cont, st->cur_fg, st->cur_bg); + term_cell_clear(cont, fg, bg); cont->wide_cont = 1; } @@ -241,24 +279,60 @@ static void term_put_utf8(TermState *st, const char *utf8, int byte_len, uint32_ static void term_clear_screen(TermState *st) { if (!st || !st->cells) return; + uint32_t fg = term_effective_fg(st); + uint32_t bg = term_effective_bg(st); for (int i = 0; i < st->cols * st->rows; i++) { - term_cell_clear(&st->cells[i], st->cur_fg, st->cur_bg); + term_cell_clear(&st->cells[i], fg, bg); } - st->cursor_x = 0; - st->cursor_y = 0; st->utf8_len = 0; st->utf8_needed = 0; } +static void term_clear_row_range(TermState *st, int y, int x0, int x1) { + if (!st || !st->cells) return; + if (y < 0 || y >= st->rows) return; + if (x0 < 0) x0 = 0; + if (x1 >= st->cols) x1 = st->cols - 1; + if (x0 > x1) return; + + uint32_t fg = term_effective_fg(st); + uint32_t bg = term_effective_bg(st); + for (int x = x0; x <= x1; x++) { + term_cell_clear(&st->cells[y * st->cols + x], fg, bg); + } +} + static void term_clear_eol(TermState *st) { if (!st || !st->cells) return; - if (st->cursor_y < 0 || st->cursor_y >= st->rows) return; - for (int x = st->cursor_x; x < st->cols; x++) { - term_cell_clear(&st->cells[st->cursor_y * st->cols + x], - st->cur_fg, st->cur_bg); + term_clear_row_range(st, st->cursor_y, st->cursor_x, st->cols - 1); +} + +static void term_clear_bol(TermState *st) { + if (!st || !st->cells) return; + term_clear_row_range(st, st->cursor_y, 0, st->cursor_x); +} + +static void term_clear_line(TermState *st) { + if (!st || !st->cells) return; + term_clear_row_range(st, st->cursor_y, 0, st->cols - 1); +} + +static void term_clear_to_eos(TermState *st) { + if (!st || !st->cells) return; + term_clear_eol(st); + for (int y = st->cursor_y + 1; y < st->rows; y++) { + term_clear_row_range(st, y, 0, st->cols - 1); } } +static void term_clear_to_bos(TermState *st) { + if (!st || !st->cells) return; + for (int y = 0; y < st->cursor_y; y++) { + term_clear_row_range(st, y, 0, st->cols - 1); + } + term_clear_bol(st); +} + static void term_set_cursor(TermState *st, int row, int col) { if (!st) return; if (row < 1) row = 1; @@ -306,8 +380,13 @@ static void term_apply_sgr(TermState *st) { if (p == 0) { st->cur_fg = TERM_DEFAULT_FG; st->cur_bg = TERM_DEFAULT_BG; + st->reverse = false; } else if (p == 1) { // bold/bright -> for simplicity, we just switch to the bright palette for fg colors + } else if (p == 7) { + st->reverse = true; + } else if (p == 27) { + st->reverse = false; } else if (p >= 30 && p <= 37) { st->cur_fg = ANSI_STD_FG[p - 30]; } else if (p == 38) { @@ -346,6 +425,35 @@ static void term_apply_sgr(TermState *st) { } } +static void term_send_input_response(TermState *st, const char *data, int len) { + if (!st || st->pty_id < 0 || !data || len <= 0) return; + sys_tty_write_in(st->pty_id, data, len); +} + +static void term_report_cursor(TermState *st) { + if (!st) return; + char buf[32]; + int len = snprintf(buf, sizeof(buf), "\x1b[%d;%dR", + st->cursor_y + 1, st->cursor_x + 1); + term_send_input_response(st, buf, len); +} + +static void term_save_cursor(TermState *st) { + if (!st) return; + st->saved_cursor_x = st->cursor_x; + st->saved_cursor_y = st->cursor_y; +} + +static void term_restore_cursor(TermState *st) { + if (!st) return; + st->cursor_x = st->saved_cursor_x; + st->cursor_y = st->saved_cursor_y; + if (st->cursor_x < 0) st->cursor_x = 0; + if (st->cursor_y < 0) st->cursor_y = 0; + if (st->cursor_x >= st->cols) st->cursor_x = st->cols - 1; + if (st->cursor_y >= st->rows) st->cursor_y = st->rows - 1; +} + static void term_handle_csi(TermState *st, char final_ch) { if (!st) return; @@ -355,14 +463,25 @@ static void term_handle_csi(TermState *st, char final_ch) { } if (final_ch == 'K') { - term_clear_eol(st); + int mode = st->esc_params[0]; + if (mode == 1) { + term_clear_bol(st); + } else if (mode == 2) { + term_clear_line(st); + } else { + term_clear_eol(st); + } return; } if (final_ch == 'J') { int mode = st->esc_params[0]; - if (mode == 2) { + if (mode == 1) { + term_clear_to_bos(st); + } else if (mode == 2 || mode == 3) { term_clear_screen(st); + } else { + term_clear_to_eos(st); } return; } @@ -376,6 +495,22 @@ static void term_handle_csi(TermState *st, char final_ch) { return; } + if (final_ch == 'G') { + int col = st->esc_params[0]; + if (col <= 0) col = 1; + st->cursor_x = col - 1; + if (st->cursor_x >= st->cols) st->cursor_x = st->cols - 1; + return; + } + + if (final_ch == 'd') { + int row = st->esc_params[0]; + if (row <= 0) row = 1; + st->cursor_y = row - 1; + if (st->cursor_y >= st->rows) st->cursor_y = st->rows - 1; + return; + } + if (final_ch == 'A') { int n = st->esc_params[0]; if (n <= 0) n = 1; @@ -405,6 +540,23 @@ static void term_handle_csi(TermState *st, char final_ch) { if (n <= 0) n = 1; st->cursor_x -= n; if (st->cursor_x < 0) st->cursor_x = 0; + return; + } + + if (final_ch == 'n') { + if (st->esc_params[0] == 6) { + term_report_cursor(st); + } + return; + } + + if (final_ch == 's') { + term_save_cursor(st); + return; + } + + if (final_ch == 'u') { + term_restore_cursor(st); } } @@ -432,6 +584,11 @@ static void term_feed_text_byte(TermState *st, unsigned char byte) { term_put_utf8(st, &ch, 1, (uint32_t)byte); return; } + if (st->utf8_needed <= 0) { + static const char repl[] = "\xEF\xBF\xBD"; + term_put_utf8(st, repl, 3, 0xFFFDu); + return; + } } if (st->utf8_len >= (int)sizeof(st->utf8_buf)) { @@ -482,6 +639,14 @@ static void term_feed_byte(TermState *st, unsigned char byte) { term_flush_invalid_utf8(st); int next = ((st->cursor_x / 8) + 1) * 8; st->cursor_x = next; + if (st->cursor_x >= st->cols) { + st->cursor_x = 0; + st->cursor_y++; + if (st->cursor_y >= st->rows) { + term_scroll_up(st); + st->cursor_y = st->rows - 1; + } + } } else if (byte >= 0x20 && byte != 0x7f) { term_feed_text_byte(st, byte); } @@ -491,6 +656,20 @@ static void term_feed_byte(TermState *st, unsigned char byte) { if (byte == '[') { st->parse_state = PARSE_CSI; term_csi_begin(st); + } else if (byte == '7' || byte == 's') { + term_save_cursor(st); + st->parse_state = PARSE_NORMAL; + } else if (byte == '8' || byte == 'u') { + term_restore_cursor(st); + st->parse_state = PARSE_NORMAL; + } else if (byte == 'c') { + st->cur_fg = TERM_DEFAULT_FG; + st->cur_bg = TERM_DEFAULT_BG; + st->reverse = false; + st->cursor_visible = true; + term_set_cursor(st, 1, 1); + term_clear_screen(st); + st->parse_state = PARSE_NORMAL; } else { st->parse_state = PARSE_NORMAL; } @@ -522,7 +701,11 @@ static void term_feed_byte(TermState *st, unsigned char byte) { st->esc_param_idx++; } } else if (byte == 'h' || byte == 'l') { - /* DEC private modes, e.g. ESC[?25h cursor show */ + for (int i = 0; i <= st->esc_param_idx; i++) { + if (st->esc_params[i] == 25) { + st->cursor_visible = (byte == 'h'); + } + } st->parse_state = PARSE_NORMAL; } else { st->parse_state = PARSE_NORMAL; @@ -635,6 +818,120 @@ static void term_fill_rect(uint32_t *pixels, int width, int height, } } +static uint32_t term_cell_codepoint(const TermCell *cell) { + if (!cell || cell->utf8[0] == '\0') return 0; + int adv = 0; + return text_decode_utf8(cell->utf8, &adv); +} + +static bool term_box_segments(uint32_t cp, + bool *top, bool *right, + bool *bottom, bool *left) { + if (!top || !right || !bottom || !left) return false; + + *top = false; + *right = false; + *bottom = false; + *left = false; + + switch (cp) { + case 0x2500: /* horizontal */ + *left = true; *right = true; return true; + case 0x2502: /* vertical */ + *top = true; *bottom = true; return true; + case 0x250C: /* down and right */ + *right = true; *bottom = true; return true; + case 0x2510: /* down and left */ + *bottom = true; *left = true; return true; + case 0x2514: /* up and right */ + *top = true; *right = true; return true; + case 0x2518: /* up and left */ + *top = true; *left = true; return true; + case 0x251C: /* vertical and right */ + *top = true; *right = true; *bottom = true; return true; + case 0x2524: /* vertical and left */ + *top = true; *bottom = true; *left = true; return true; + case 0x252C: /* down and horizontal */ + *right = true; *bottom = true; *left = true; return true; + case 0x2534: /* up and horizontal */ + *top = true; *right = true; *left = true; return true; + case 0x253C: /* cross */ + *top = true; *right = true; *bottom = true; *left = true; return true; + default: + return false; + } +} + +static bool term_draw_box_glyph(uint32_t *pixels, int width, int height, + int x, int y, uint32_t cp, uint32_t color) { + if (cp == 0x2588) { + term_fill_rect(pixels, width, height, x, y, CELL_W, CELL_H, color); + return true; + } + + bool top, right, bottom, left; + if (!term_box_segments(cp, &top, &right, &bottom, &left)) { + return false; + } + + const int thickness = 2; + int cx = x + CELL_W / 2 - thickness / 2; + int cy = y + CELL_H / 2 - thickness / 2; + + if (left) { + term_fill_rect(pixels, width, height, + x, cy, cx - x + thickness, thickness, color); + } + if (right) { + term_fill_rect(pixels, width, height, + cx, cy, x + CELL_W - cx, thickness, color); + } + if (top) { + term_fill_rect(pixels, width, height, + cx, y, thickness, cy - y + thickness, color); + } + if (bottom) { + term_fill_rect(pixels, width, height, + cx, cy, thickness, y + CELL_H - cy, color); + } + + return true; +} + +static void term_invert_rect(uint32_t *pixels, int width, int height, + int x, int y, int rw, int rh) { + if (!pixels || width <= 0 || height <= 0 || rw <= 0 || rh <= 0) return; + + int x1 = x; + int y1 = y; + int x2 = x + rw; + int y2 = y + rh; + + if (x1 < 0) x1 = 0; + if (y1 < 0) y1 = 0; + if (x2 > width) x2 = width; + if (y2 > height) y2 = height; + if (x2 <= x1 || y2 <= y1) return; + + for (int py = y1; py < y2; py++) { + uint32_t *row = pixels + py * width; + for (int px = x1; px < x2; px++) { + row[px] = (row[px] ^ 0x00FFFFFFu) | 0xFF000000u; + } + } +} + +static void term_draw_cursor(TermState *st, + uint32_t *pixels, int width, int height) { + if (!st || !st->cursor_visible) return; + if (st->cursor_x < 0 || st->cursor_x >= st->cols) return; + if (st->cursor_y < 0 || st->cursor_y >= st->rows) return; + + term_invert_rect(pixels, width, height, + st->cursor_x * CELL_W, st->cursor_y * CELL_H, + CELL_W, CELL_H); +} + static void term_draw_backgrounds(TermState *st, uint32_t *pixels, int width, int height) { term_fill_rect(pixels, width, height, 0, 0, width, height, TERM_DEFAULT_BG); @@ -693,12 +990,19 @@ static void on_draw(NovaApp *app) { for (int x = 0; x < st->cols; x++) { TermCell *cell = &st->cells[y * st->cols + x]; if (!cell->wide_cont && cell->utf8[0] != '\0') { - ui_draw_string(pixels, width, height, - x * CELL_W, y * CELL_H + 1, - cell->utf8, cell->fg); + uint32_t cp = term_cell_codepoint(cell); + if (!term_draw_box_glyph(pixels, width, height, + x * CELL_W, y * CELL_H, + cp, cell->fg)) { + ui_draw_string(pixels, width, height, + x * CELL_W, y * CELL_H + 1, + cell->utf8, cell->fg); + } } } } + + term_draw_cursor(st, pixels, width, height); } static void on_idle(NovaApp *app) { @@ -778,6 +1082,12 @@ static bool on_close(NovaApp *app) { return true; } +static void term_init_monospace_font(void) { + if (ui_font_init("/Library/Fonts/FiraCode-Regular.ttf", TERM_FONT_SIZE) == 0) return; + if (ui_font_init("/Library/Fonts/Hack-Regular.ttf", TERM_FONT_SIZE) == 0) return; + ui_font_init(NULL, TERM_FONT_SIZE); +} + int main(void) { // Initialize terminal state with defaults and invalid PTY/shell IDs TermState st = { @@ -785,11 +1095,13 @@ int main(void) { .shell_pid = -1, .cur_fg = TERM_DEFAULT_FG, .cur_bg = TERM_DEFAULT_BG, + .cursor_visible = true, }; // Create the application window and initialize terminal state NovaApp *app = app_create("Terminal", 820, 480); if (!app) return 1; + term_init_monospace_font(); app_set_userdata(app, &st); // Associate our terminal state with the app for callback access term_resize_grid(&st, term_cols(app_width(app)), term_rows(app_height(app))); From 2c6404a23fa175f4992ad1729368361b2cf8b5d9 Mon Sep 17 00:00:00 2001 From: Lluciocc Date: Tue, 9 Jun 2026 00:11:18 +0200 Subject: [PATCH 09/10] fix term --- assets/terminal.conf | 9 ++ src/terminal.c | 230 +++++++++++++++++++++++++++++++++++++------ 2 files changed, 209 insertions(+), 30 deletions(-) create mode 100644 assets/terminal.conf diff --git a/assets/terminal.conf b/assets/terminal.conf new file mode 100644 index 0000000..80ba67f --- /dev/null +++ b/assets/terminal.conf @@ -0,0 +1,9 @@ +# Terminal configuration for nova terminal +# Supported keys: font, font_size, fg, bg, cursor +# Colors can be specified as 0xAARRGGBB or decimal. + +font=/Library/Fonts/FiraCode-Regular.ttf +font_size=14 +fg=0xFFCCCCCC +bg=0xFF1E1E1E +cursor=0xFFFFFFFF diff --git a/src/terminal.c b/src/terminal.c index 4ff6874..cdaecb3 100644 --- a/src/terminal.c +++ b/src/terminal.c @@ -49,6 +49,11 @@ typedef struct { int rows; TermCell *cells; + /* Backpointer to app and dirty box for efficient redraws */ + NovaApp *app; + int dirty_x0, dirty_y0, dirty_x1, dirty_y1; + bool has_dirty; + int cursor_x; int cursor_y; int saved_cursor_x; @@ -67,6 +72,60 @@ typedef struct { int utf8_needed; } TermState; +typedef struct { + char font_path[256]; + int font_size; + uint32_t fg; + uint32_t bg; + uint32_t cursor; +} TermConfig; + +static void term_load_config(TermConfig *cfg) { + /* defaults */ + if (!cfg) return; + cfg->font_path[0] = '\0'; + cfg->font_size = TERM_FONT_SIZE; + cfg->fg = TERM_DEFAULT_FG; + cfg->bg = TERM_DEFAULT_BG; + cfg->cursor = 0xFFFFFFFFu; + + const char *paths[] = {"/etc/nova/terminal.conf", NULL}; + for (int i = 0; paths[i]; i++) { + FILE *f = fopen(paths[i], "r"); + if (!f) continue; + char line[512]; + while (fgets(line, sizeof(line), f)) { + char *p = line; + while (*p == ' ' || *p == '\t') p++; + if (*p == '#' || *p == '\n' || *p == '\0') continue; + char key[128], val[384]; + if (sscanf(p, "%127[^=]=%383[^ +]", key, val) == 2) { + /* trim whitespace on val */ + char *v = val; + while (*v == ' ' || *v == '\t') v++; + char *e = v + strlen(v) - 1; + while (e > v && (*e == '\n' || *e == '\r' || *e == ' ' || *e == '\t')) *e-- = '\0'; + if (strcmp(key, "font") == 0) { + strncpy(cfg->font_path, v, sizeof(cfg->font_path)-1); + cfg->font_path[sizeof(cfg->font_path)-1] = '\0'; + } else if (strcmp(key, "font_size") == 0) { + cfg->font_size = atoi(v); + if (cfg->font_size <= 0) cfg->font_size = TERM_FONT_SIZE; + } else if (strcmp(key, "foreground") == 0 || strcmp(key, "fg") == 0) { + cfg->fg = (uint32_t)strtoul(v, NULL, 0); + } else if (strcmp(key, "background") == 0 || strcmp(key, "bg") == 0) { + cfg->bg = (uint32_t)strtoul(v, NULL, 0); + } else if (strcmp(key, "cursor") == 0) { + cfg->cursor = (uint32_t)strtoul(v, NULL, 0); + } + } + } + fclose(f); + break; /* stop at first found config */ + } +} + // ANSI color palette for standard and bright colors, used for SGR color codes static const uint32_t ANSI_STD_FG[8] = { 0xFF000000u, 0xFFFF4444u, 0xFF6A9955u, 0xFFFFCC00u, @@ -275,6 +334,25 @@ static void term_put_utf8(TermState *st, const char *utf8, int byte_len, uint32_ } term_advance_cursor(st, width); + /* mark the just-written cell region dirty */ + if (st) { + int last_x = st->cursor_x - width; + if (last_x < 0) last_x = 0; + int px0 = last_x * CELL_W; + int py0 = st->cursor_y * CELL_H; + int px1 = (st->cursor_x) * CELL_W + CELL_W - 1; + int py1 = py0 + CELL_H - 1; + if (!st->has_dirty) { + st->dirty_x0 = px0; st->dirty_y0 = py0; + st->dirty_x1 = px1; st->dirty_y1 = py1; + st->has_dirty = true; + } else { + if (px0 < st->dirty_x0) st->dirty_x0 = px0; + if (py0 < st->dirty_y0) st->dirty_y0 = py0; + if (px1 > st->dirty_x1) st->dirty_x1 = px1; + if (py1 > st->dirty_y1) st->dirty_y1 = py1; + } + } } static void term_clear_screen(TermState *st) { @@ -286,6 +364,14 @@ static void term_clear_screen(TermState *st) { } st->utf8_len = 0; st->utf8_needed = 0; + /* mark full area dirty */ + if (st) { + st->has_dirty = true; + st->dirty_x0 = 0; + st->dirty_y0 = 0; + st->dirty_x1 = st->cols * CELL_W - 1; + st->dirty_y1 = st->rows * CELL_H - 1; + } } static void term_clear_row_range(TermState *st, int y, int x0, int x1) { @@ -300,23 +386,22 @@ static void term_clear_row_range(TermState *st, int y, int x0, int x1) { for (int x = x0; x <= x1; x++) { term_cell_clear(&st->cells[y * st->cols + x], fg, bg); } -} - -static void term_clear_eol(TermState *st) { - if (!st || !st->cells) return; - term_clear_row_range(st, st->cursor_y, st->cursor_x, st->cols - 1); -} - -static void term_clear_bol(TermState *st) { - if (!st || !st->cells) return; - term_clear_row_range(st, st->cursor_y, 0, st->cursor_x); -} - -static void term_clear_line(TermState *st) { - if (!st || !st->cells) return; - term_clear_row_range(st, st->cursor_y, 0, st->cols - 1); -} - + if (st) { + int px0 = x0 * CELL_W; + int py0 = y * CELL_H; + int px1 = (x1 + 1) * CELL_W - 1; + int py1 = (y + 1) * CELL_H - 1; + if (!st->has_dirty) { + st->dirty_x0 = px0; st->dirty_y0 = py0; + st->dirty_x1 = px1; st->dirty_y1 = py1; + st->has_dirty = true; + } else { + if (px0 < st->dirty_x0) st->dirty_x0 = px0; + if (py0 < st->dirty_y0) st->dirty_y0 = py0; + if (px1 > st->dirty_x1) st->dirty_x1 = px1; + if (py1 > st->dirty_y1) st->dirty_y1 = py1; + } + } static void term_clear_to_eos(TermState *st) { if (!st || !st->cells) return; term_clear_eol(st); @@ -786,7 +871,10 @@ static bool term_spawn_shell(TermState *st) { static void term_shutdown(TermState *st) { if (!st) return; if (st->pty_id >= 0) { + /* ask kernel to notify/kill attached processes first */ sys_tty_kill_all(st->pty_id); + /* yield a few times to let kernel/gc settle attached fds */ + for (int i = 0; i < 4; i++) sys_yield(); sys_pty_destroy(st->pty_id); st->pty_id = -1; st->shell_pid = -1; @@ -987,19 +1075,77 @@ static void on_draw(NovaApp *app) { term_draw_backgrounds(st, pixels, width, height); for (int y = 0; y < st->rows; y++) { + int run_x = -1; + uint32_t run_fg = 0; + char run_buf[4096]; + int run_len = 0; + for (int x = 0; x < st->cols; x++) { TermCell *cell = &st->cells[y * st->cols + x]; - if (!cell->wide_cont && cell->utf8[0] != '\0') { - uint32_t cp = term_cell_codepoint(cell); - if (!term_draw_box_glyph(pixels, width, height, - x * CELL_W, y * CELL_H, - cp, cell->fg)) { + if (cell->utf8[0] == '\0' || cell->wide_cont) { + /* flush any pending run */ + if (run_len > 0) { + run_buf[run_len] = '\0'; ui_draw_string(pixels, width, height, - x * CELL_W, y * CELL_H + 1, - cell->utf8, cell->fg); + run_x * CELL_W, y * CELL_H + 1, + run_buf, run_fg); + run_len = 0; run_x = -1; + } + /* draw box glyphs or skip */ + if (!cell->wide_cont && cell->utf8[0] != '\0') { + uint32_t cp = term_cell_codepoint(cell); + if (!term_draw_box_glyph(pixels, width, height, + x * CELL_W, y * CELL_H, + cp, cell->fg)) { + ui_draw_string(pixels, width, height, + x * CELL_W, y * CELL_H + 1, + cell->utf8, cell->fg); + } } + continue; + } + + /* start a new run if necessary */ + if (run_x < 0) { + run_x = x; + run_fg = cell->fg; + run_len = 0; + } + + /* if fg changes, flush and start new run */ + if (cell->fg != run_fg) { + run_buf[run_len] = '\0'; + ui_draw_string(pixels, width, height, + run_x * CELL_W, y * CELL_H + 1, + run_buf, run_fg); + run_x = x; + run_fg = cell->fg; + run_len = 0; + } + + /* append cell utf8 to run buffer (safe append, small strings) + * ensure we don't overflow run_buf + */ + int cp_len = (int)strlen(cell->utf8); + if (run_len + cp_len < (int)sizeof(run_buf) - 1) { + memcpy(run_buf + run_len, cell->utf8, (size_t)cp_len); + run_len += cp_len; + } else { + /* flush if buffer full */ + run_buf[run_len] = '\0'; + ui_draw_string(pixels, width, height, + run_x * CELL_W, y * CELL_H + 1, + run_buf, run_fg); + run_x = -1; run_len = 0; x--; /* reprocess this cell next iter */ } } + + if (run_len > 0 && run_x >= 0) { + run_buf[run_len] = '\0'; + ui_draw_string(pixels, width, height, + run_x * CELL_W, y * CELL_H + 1, + run_buf, run_fg); + } } term_draw_cursor(st, pixels, width, height); @@ -1017,7 +1163,20 @@ static void on_idle(NovaApp *app) { } if (term_drain_pty(st)) { - app_request_redraw(app); + if (st->has_dirty) { + int dx = st->dirty_x0; + int dy = st->dirty_y0; + int dw = st->dirty_x1 - st->dirty_x0 + 1; + int dh = st->dirty_y1 - st->dirty_y0 + 1; + if (dw > 0 && dh > 0) { + app_request_redraw_rect(app, dx, dy, dw, dh); + } else { + app_request_redraw(app); + } + st->has_dirty = false; + } else { + app_request_redraw(app); + } } } @@ -1082,10 +1241,13 @@ static bool on_close(NovaApp *app) { return true; } -static void term_init_monospace_font(void) { - if (ui_font_init("/Library/Fonts/FiraCode-Regular.ttf", TERM_FONT_SIZE) == 0) return; - if (ui_font_init("/Library/Fonts/Hack-Regular.ttf", TERM_FONT_SIZE) == 0) return; - ui_font_init(NULL, TERM_FONT_SIZE); +static void term_init_monospace_font(const char *font_path, int font_size) { + if (font_path && font_path[0]) { + if (ui_font_init(font_path, font_size) == 0) return; + } + if (ui_font_init("/Library/Fonts/FiraCode-Regular.ttf", font_size) == 0) return; + if (ui_font_init("/Library/Fonts/Hack-Regular.ttf", font_size) == 0) return; + ui_font_init(NULL, font_size); } int main(void) { @@ -1101,9 +1263,17 @@ int main(void) { // Create the application window and initialize terminal state NovaApp *app = app_create("Terminal", 820, 480); if (!app) return 1; - term_init_monospace_font(); + TermConfig cfg; + term_load_config(&cfg); + /* initialize font from config (falls back internally) */ + term_init_monospace_font(cfg.font_path, cfg.font_size); app_set_userdata(app, &st); // Associate our terminal state with the app for callback access + st.app = app; + st.has_dirty = false; + /* apply configured colors */ + st.cur_fg = cfg.fg; + st.cur_bg = cfg.bg; term_resize_grid(&st, term_cols(app_width(app)), term_rows(app_height(app))); // Spawn the shell process connected to our PTY and prime the initial output From 89142fc5e1793101d6e314f49c4776f79cb87180 Mon Sep 17 00:00:00 2001 From: Lluciocc Date: Tue, 9 Jun 2026 00:13:57 +0200 Subject: [PATCH 10/10] yes --- src/terminal.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/terminal.c b/src/terminal.c index cdaecb3..cb62ca0 100644 --- a/src/terminal.c +++ b/src/terminal.c @@ -89,7 +89,7 @@ static void term_load_config(TermConfig *cfg) { cfg->bg = TERM_DEFAULT_BG; cfg->cursor = 0xFFFFFFFFu; - const char *paths[] = {"/etc/nova/terminal.conf", NULL}; + const char *paths[] = {"./terminal.conf", "/etc/nova/terminal.conf", NULL}; for (int i = 0; paths[i]; i++) { FILE *f = fopen(paths[i], "r"); if (!f) continue; @@ -99,8 +99,8 @@ static void term_load_config(TermConfig *cfg) { while (*p == ' ' || *p == '\t') p++; if (*p == '#' || *p == '\n' || *p == '\0') continue; char key[128], val[384]; - if (sscanf(p, "%127[^=]=%383[^ ]", key, val) == 2) { + if (sscanf(p, "%127[^=]=%383[^\\n]", key, val) == 2) { /* trim whitespace on val */ char *v = val; while (*v == ' ' || *v == '\t') v++;