/* * terminal.hpp * MontaukOS terminal emulator with ANSI escape sequence support * Copyright (c) 2026 Daniel Hammer */ #pragma once #include "gui/gui.hpp" #include "gui/font.hpp" #include #include #include namespace gui { struct TermCell { char ch; Color fg; Color bg; }; static constexpr int TERM_MAX_SCROLLBACK = 500; static constexpr int TERM_READ_BUF_SIZE = 8192; static constexpr int TERM_MAX_DRAIN_BYTES_PER_POLL = 256 * 1024; static constexpr int TERM_DRAIN_GRACE_YIELDS = 2; struct TerminalState { TermCell* cells; TermCell* alt_cells; // alternate screen buffer TermCell* render_cells; // last visible cells painted into the window buffer int cols, rows; int cursor_x, cursor_y; int saved_cursor_x, saved_cursor_y; // saved cursor for alternate screen int scrollback_lines; // lines of history above visible screen (0..max_scrollback) int max_scrollback; // 0 for viewers (klog), TERM_MAX_SCROLLBACK for terminal int view_offset; // how many rows scrolled back (0 = live view) int child_pid; void* desktop; // DesktopState* for closing window on child exit Color current_fg; Color current_bg; bool cursor_visible; bool alt_screen_active; bool reverse_video; bool dirty; // true when content changed since last render bool render_valid; bool render_cursor_visible; int render_cols, render_rows; int render_term_cols, render_term_rows; int render_base_row; int render_pw, render_ph; int render_cell_w, render_cell_h; int render_cursor_x, render_cursor_y; uint32_t* render_pixels; enum { STATE_NORMAL, STATE_ESC, STATE_CSI } parse_state; bool csi_private; // true if '?' was seen after CSI int csi_params[8]; int csi_param_count; int csi_current_param; }; // Standard ANSI color palette as ARGB pixels static inline Color term_ansi_color(int idx) { switch (idx) { case 0: return Color::from_hex(0x000000); case 1: return Color::from_hex(0xCC0000); case 2: return Color::from_hex(0x4E9A06); case 3: return Color::from_hex(0xC4A000); case 4: return Color::from_hex(0x3465A4); case 5: return Color::from_hex(0x75507B); case 6: return Color::from_hex(0x06989A); case 7: return Color::from_hex(0xD3D7CF); case 8: return Color::from_hex(0x555753); case 9: return Color::from_hex(0xEF2929); case 10: return Color::from_hex(0x8AE234); case 11: return Color::from_hex(0xFCE94F); case 12: return Color::from_hex(0x729FCF); case 13: return Color::from_hex(0xAD7FA8); case 14: return Color::from_hex(0x34E2E2); case 15: return Color::from_hex(0xEEEEEC); default: return colors::TERM_FG; } } // ANSI 256-color palette (0-15 = standard, 16-231 = RGB cube, 232-255 = grayscale) static inline Color term_ansi_256_color(int idx) { if (idx < 0) return colors::TERM_FG; if (idx <= 15) return term_ansi_color(idx); if (idx <= 231) { // 6x6x6 RGB cube: idx 16 = (0,0,0), idx 231 = (5,5,5) int r = ((idx - 16) / 36) * 51; int g = (((idx - 16) % 36) / 6) * 51; int b = ((idx - 16) % 6) * 51; return Color::from_rgb(r, g, b); } // Grayscale: idx 232 = #080808, idx 255 = #eeeeee int gray = 8 + (idx - 232) * 10; if (gray > 238) gray = 238; return Color::from_rgb(gray, gray, gray); } // Helper: pointer to start of screen-relative row r in the cells buffer static inline TermCell* term_screen_row(TerminalState* t, int r) { return &t->cells[(t->scrollback_lines + r) * t->cols]; } static inline bool term_color_equal(Color a, Color b) { return a.r == b.r && a.g == b.g && a.b == b.b && a.a == b.a; } static inline bool term_cell_equal(const TermCell& a, const TermCell& b) { return a.ch == b.ch && term_color_equal(a.fg, b.fg) && term_color_equal(a.bg, b.bg); } static inline void terminal_invalidate_render_cache(TerminalState* t) { if (!t) return; t->render_valid = false; t->render_cursor_visible = false; } static inline Color terminal_erase_bg(TerminalState* t) { return t->current_bg; } static inline void terminal_scroll_up(TerminalState* t) { if (!t->alt_screen_active && t->scrollback_lines < t->max_scrollback) { // Room for scrollback: top visible row becomes scrollback, no data movement t->scrollback_lines++; } else if (!t->alt_screen_active && t->max_scrollback > 0) { // Scrollback full: discard oldest line, shift entire buffer up by one row int total = (t->max_scrollback + t->rows - 1) * t->cols * sizeof(TermCell); montauk::memmove(t->cells, t->cells + t->cols, total); } else { // Alt screen or no scrollback (klog): shift visible area up TermCell* screen = term_screen_row(t, 0); montauk::memmove(screen, screen + t->cols, (t->rows - 1) * t->cols * sizeof(TermCell)); } // Clear the new bottom visible row TermCell* bottom = term_screen_row(t, t->rows - 1); for (int c = 0; c < t->cols; c++) { bottom[c] = {' ', t->current_fg, terminal_erase_bg(t)}; } // Keep user's scrolled-back viewport stable if (t->view_offset > 0) { t->view_offset++; if (t->view_offset > t->scrollback_lines) t->view_offset = t->scrollback_lines; } } // Initialize only the cell grid (no child process). Used by viewers like klog. // max_sb = 0 for viewers, TERM_MAX_SCROLLBACK for the real terminal. static inline void terminal_init_cells(TerminalState* t, int cols, int rows, int max_sb = 0) { t->cols = cols; t->rows = rows; t->cursor_x = 0; t->cursor_y = 0; t->saved_cursor_x = 0; t->saved_cursor_y = 0; t->scrollback_lines = 0; t->max_scrollback = max_sb; t->view_offset = 0; t->current_fg = colors::TERM_FG; t->current_bg = colors::TERM_BG; t->cursor_visible = false; t->alt_screen_active = false; t->reverse_video = false; t->dirty = true; t->render_cells = nullptr; t->render_valid = false; t->render_cursor_visible = false; t->render_cols = 0; t->render_rows = 0; t->render_term_cols = 0; t->render_term_rows = 0; t->render_base_row = 0; t->render_pw = 0; t->render_ph = 0; t->render_cell_w = 0; t->render_cell_h = 0; t->render_cursor_x = 0; t->render_cursor_y = 0; t->render_pixels = nullptr; t->parse_state = TerminalState::STATE_NORMAL; t->csi_private = false; t->csi_param_count = 0; t->csi_current_param = 0; t->child_pid = 0; int total_cells = (rows + max_sb) * cols; int screen_cells = rows * cols; t->cells = (TermCell*)montauk::alloc(total_cells * sizeof(TermCell)); t->alt_cells = (TermCell*)montauk::alloc(screen_cells * sizeof(TermCell)); if (!t->cells || !t->alt_cells) { // Allocation failed — leave terminal in a safe but unusable state if (t->cells) { montauk::free(t->cells); t->cells = nullptr; } if (t->alt_cells) { montauk::free(t->alt_cells); t->alt_cells = nullptr; } t->cols = 0; t->rows = 0; return; } for (int i = 0; i < total_cells; i++) { t->cells[i] = {' ', colors::TERM_FG, colors::TERM_BG}; } for (int i = 0; i < screen_cells; i++) { t->alt_cells[i] = {' ', colors::TERM_FG, colors::TERM_BG}; } } static inline void terminal_init(TerminalState* t, int cols, int rows) { terminal_init_cells(t, cols, rows, TERM_MAX_SCROLLBACK); t->cursor_visible = true; t->child_pid = montauk::spawn_redir("0:/os/shell.elf"); if (t->child_pid > 0) montauk::childio_settermsz(t->child_pid, cols, rows); } static inline void terminal_put_char(TerminalState* t, char ch) { if (t->cursor_x >= t->cols) { t->cursor_x = 0; t->cursor_y++; } if (t->cursor_y >= t->rows) { terminal_scroll_up(t); t->cursor_y = t->rows - 1; } TermCell* row = term_screen_row(t, t->cursor_y); row[t->cursor_x].ch = ch; row[t->cursor_x].fg = t->current_fg; row[t->cursor_x].bg = t->current_bg; t->cursor_x++; } static inline void terminal_enter_alt_screen(TerminalState* t) { if (t->alt_screen_active) return; t->alt_screen_active = true; t->dirty = true; // Save cursor t->saved_cursor_x = t->cursor_x; t->saved_cursor_y = t->cursor_y; // Save visible screen to alt_cells, clear visible screen int total = t->cols * t->rows; TermCell* screen = term_screen_row(t, 0); for (int i = 0; i < total; i++) { t->alt_cells[i] = screen[i]; screen[i] = {' ', colors::TERM_FG, colors::TERM_BG}; } t->view_offset = 0; t->cursor_x = 0; t->cursor_y = 0; } static inline void terminal_exit_alt_screen(TerminalState* t) { if (!t->alt_screen_active) return; t->alt_screen_active = false; t->dirty = true; // Restore visible screen from alt_cells int total = t->cols * t->rows; TermCell* screen = term_screen_row(t, 0); for (int i = 0; i < total; i++) { screen[i] = t->alt_cells[i]; } // Restore cursor t->cursor_x = t->saved_cursor_x; t->cursor_y = t->saved_cursor_y; } static inline void terminal_process_private_mode(TerminalState* t, char cmd) { int p0 = t->csi_param_count > 0 ? t->csi_params[0] : 0; if (cmd == 'h') { // Set private mode if (p0 == 25) { t->cursor_visible = true; } else if (p0 == 1049) { terminal_enter_alt_screen(t); } } else if (cmd == 'l') { // Reset private mode if (p0 == 25) { t->cursor_visible = false; } else if (p0 == 1049) { terminal_exit_alt_screen(t); } } } static inline void terminal_process_csi(TerminalState* t, char cmd) { // Finalize current param if (t->csi_param_count < 8) { t->csi_params[t->csi_param_count] = t->csi_current_param; t->csi_param_count++; } // Handle private mode sequences (ESC[?...) if (t->csi_private) { terminal_process_private_mode(t, cmd); return; } int p0 = t->csi_param_count > 0 ? t->csi_params[0] : 0; int p1 = t->csi_param_count > 1 ? t->csi_params[1] : 0; switch (cmd) { case 'H': case 'f': { // Cursor position: ESC[row;colH (1-based) int row = (p0 > 0 ? p0 : 1) - 1; int col = (p1 > 0 ? p1 : 1) - 1; if (row < 0) row = 0; if (row >= t->rows) row = t->rows - 1; if (col < 0) col = 0; if (col >= t->cols) col = t->cols - 1; t->cursor_y = row; t->cursor_x = col; break; } case 'A': { // Cursor up int n = p0 > 0 ? p0 : 1; t->cursor_y -= n; if (t->cursor_y < 0) t->cursor_y = 0; break; } case 'B': { // Cursor down int n = p0 > 0 ? p0 : 1; t->cursor_y += n; if (t->cursor_y >= t->rows) t->cursor_y = t->rows - 1; break; } case 'C': { // Cursor forward int n = p0 > 0 ? p0 : 1; t->cursor_x += n; if (t->cursor_x >= t->cols) t->cursor_x = t->cols - 1; break; } case 'D': { // Cursor backward int n = p0 > 0 ? p0 : 1; t->cursor_x -= n; if (t->cursor_x < 0) t->cursor_x = 0; break; } case 'J': { // Erase in display Color erase_bg = terminal_erase_bg(t); if (p0 == 0) { // Clear from cursor to end TermCell* row = term_screen_row(t, t->cursor_y); for (int x = t->cursor_x; x < t->cols; x++) row[x] = {' ', t->current_fg, erase_bg}; for (int r = t->cursor_y + 1; r < t->rows; r++) { TermCell* rp = term_screen_row(t, r); for (int c = 0; c < t->cols; c++) rp[c] = {' ', t->current_fg, erase_bg}; } } else if (p0 == 1) { // Clear from start to cursor for (int r = 0; r < t->cursor_y; r++) { TermCell* rp = term_screen_row(t, r); for (int c = 0; c < t->cols; c++) rp[c] = {' ', t->current_fg, erase_bg}; } TermCell* row = term_screen_row(t, t->cursor_y); for (int x = 0; x <= t->cursor_x; x++) row[x] = {' ', t->current_fg, erase_bg}; } else if (p0 == 2) { // Clear entire screen for (int r = 0; r < t->rows; r++) { TermCell* rp = term_screen_row(t, r); for (int c = 0; c < t->cols; c++) rp[c] = {' ', t->current_fg, erase_bg}; } t->cursor_x = 0; t->cursor_y = 0; } break; } case 'K': { // Erase in line int start = 0, end = t->cols; if (p0 == 0) { start = t->cursor_x; end = t->cols; } else if (p0 == 1) { start = 0; end = t->cursor_x + 1; } else if (p0 == 2) { start = 0; end = t->cols; } Color erase_bg = terminal_erase_bg(t); TermCell* row = term_screen_row(t, t->cursor_y); for (int x = start; x < end; x++) row[x] = {' ', t->current_fg, erase_bg}; break; } case 'm': { // SGR - Set Graphics Rendition for (int i = 0; i < t->csi_param_count; i++) { int code = t->csi_params[i]; // Handle extended color sequences: 38;5;N or 48;5;N or 38;2;R;G;B or 48;2;R;G;B if ((code == 38 || code == 48) && i + 2 < t->csi_param_count) { if (t->csi_params[i + 1] == 5) { // 256-color: 38;5;N or 48;5;N int color_idx = t->csi_params[i + 2]; if (code == 38) { t->current_fg = term_ansi_256_color(color_idx); } else { t->current_bg = term_ansi_256_color(color_idx); } i += 2; continue; } else if (t->csi_params[i + 1] == 2 && i + 4 < t->csi_param_count) { // Truecolor: 38;2;R;G;B or 48;2;R;G;B int r = t->csi_params[i + 2]; int g = t->csi_params[i + 3]; int b = t->csi_params[i + 4]; if (code == 38) { t->current_fg = Color::from_rgb(r, g, b); } else { t->current_bg = Color::from_rgb(r, g, b); } i += 4; continue; } } if (code == 0) { t->current_fg = colors::TERM_FG; t->current_bg = colors::TERM_BG; t->reverse_video = false; } else if (code == 1) { // Bold: map to bright version of current color uint8_t r = t->current_fg.r; uint8_t g = t->current_fg.g; uint8_t b = t->current_fg.b; int add = 50; r = (r + add > 255) ? 255 : r + add; g = (g + add > 255) ? 255 : g + add; b = (b + add > 255) ? 255 : b + add; t->current_fg = Color::from_rgb(r, g, b); } else if (code == 2) { // Dim: darken current fg color t->current_fg.r = t->current_fg.r / 2; t->current_fg.g = t->current_fg.g / 2; t->current_fg.b = t->current_fg.b / 2; } else if (code == 7) { // Reverse video if (!t->reverse_video) { t->reverse_video = true; Color tmp = t->current_fg; t->current_fg = t->current_bg; t->current_bg = tmp; } } else if (code == 27) { // Reverse off if (t->reverse_video) { t->reverse_video = false; Color tmp = t->current_fg; t->current_fg = t->current_bg; t->current_bg = tmp; } } else if (code >= 30 && code <= 37) { t->current_fg = term_ansi_color(code - 30); if (t->reverse_video) { // In reverse mode, fg is displayed as bg Color tmp = t->current_fg; t->current_fg = t->current_bg; t->current_bg = tmp; } } else if (code >= 40 && code <= 47) { t->current_bg = term_ansi_color(code - 40); } else if (code >= 90 && code <= 97) { t->current_fg = term_ansi_color(code - 90 + 8); } else if (code >= 100 && code <= 107) { t->current_bg = term_ansi_color(code - 100 + 8); } else if (code == 39) { t->current_fg = colors::TERM_FG; } else if (code == 49) { t->current_bg = colors::TERM_BG; } } if (t->csi_param_count == 0) { // ESC[m with no params = reset t->current_fg = colors::TERM_FG; t->current_bg = colors::TERM_BG; t->reverse_video = false; } break; } default: break; } } static inline void terminal_feed(TerminalState* t, const char* data, int len) { if (len > 0) t->dirty = true; for (int i = 0; i < len; i++) { char ch = data[i]; switch (t->parse_state) { case TerminalState::STATE_NORMAL: if (ch == '\033') { t->parse_state = TerminalState::STATE_ESC; } else if (ch == '\n') { t->cursor_x = 0; // CR+LF: shell sends \n without \r t->cursor_y++; if (t->cursor_y >= t->rows) { terminal_scroll_up(t); t->cursor_y = t->rows - 1; } } else if (ch == '\r') { t->cursor_x = 0; } else if (ch == '\b') { if (t->cursor_x > 0) t->cursor_x--; } else if (ch == '\t') { int next = (t->cursor_x + 8) & ~7; if (next > t->cols) next = t->cols; while (t->cursor_x < next) { terminal_put_char(t, ' '); } } else if (ch >= 32 || ch < 0) { // Printable character (also treat high-bit chars as printable) terminal_put_char(t, ch); } break; case TerminalState::STATE_ESC: if (ch == '[') { t->parse_state = TerminalState::STATE_CSI; t->csi_private = false; t->csi_param_count = 0; t->csi_current_param = 0; for (int j = 0; j < 8; j++) t->csi_params[j] = 0; } else if (ch == 'c') { // Reset terminal t->current_fg = colors::TERM_FG; t->current_bg = colors::TERM_BG; t->cursor_x = 0; t->cursor_y = 0; t->parse_state = TerminalState::STATE_NORMAL; } else { // Unknown ESC sequence, ignore t->parse_state = TerminalState::STATE_NORMAL; } break; case TerminalState::STATE_CSI: if (ch >= '0' && ch <= '9') { t->csi_current_param = t->csi_current_param * 10 + (ch - '0'); } else if (ch == ';') { if (t->csi_param_count < 8) { t->csi_params[t->csi_param_count] = t->csi_current_param; t->csi_param_count++; } t->csi_current_param = 0; } else if (ch == '?') { t->csi_private = true; } else if (ch >= 0x40 && ch <= 0x7E) { // Final byte - execute command terminal_process_csi(t, ch); t->parse_state = TerminalState::STATE_NORMAL; } else { // Unknown, abort CSI t->parse_state = TerminalState::STATE_NORMAL; } break; } } } static inline void terminal_fill_pixel_rect(uint32_t* pixels, int pw, int ph, int x, int y, int w, int h, uint32_t color) { if (!pixels || pw <= 0 || ph <= 0 || w <= 0 || h <= 0) return; int x0 = x < 0 ? 0 : x; int y0 = y < 0 ? 0 : y; int x1 = x + w > pw ? pw : x + w; int y1 = y + h > ph ? ph : y + h; for (int row = y0; row < y1; row++) { uint32_t* dst = &pixels[row * pw + x0]; for (int col = x0; col < x1; col++) *dst++ = color; } } static inline void terminal_draw_bitmap_char(uint32_t* pixels, int pw, int ph, int x, int y, unsigned char ch, uint32_t color) { const uint8_t* glyph = &font_data[ch * FONT_HEIGHT]; for (int fy = 0; fy < FONT_HEIGHT; fy++) { int dy = y + fy; if (dy >= ph) break; if (dy < 0) continue; uint8_t bits = glyph[fy]; for (int fx = 0; fx < FONT_WIDTH; fx++) { if (bits & (0x80 >> fx)) { int dx = x + fx; if (dx >= pw) break; if (dx >= 0) pixels[dy * pw + dx] = color; } } } } static inline void terminal_draw_cell_pixels(const TermCell& cell, uint32_t* pixels, int pw, int ph, int px, int py, int cell_w, int cell_h, bool fill_bg, bool use_ttf, GlyphCache* gc) { if (fill_bg) { terminal_fill_pixel_rect(pixels, pw, ph, px, py, cell_w, cell_h, cell.bg.to_pixel()); } if (cell.ch <= 32 && cell.ch >= 0) return; if (use_ttf) { int baseline = py + gc->ascent; fonts::mono->draw_char_to_buffer(pixels, pw, ph, px, baseline, (unsigned char)cell.ch, cell.fg, gc); } else { terminal_draw_bitmap_char(pixels, pw, ph, px, py, (unsigned char)cell.ch, cell.fg.to_pixel()); } } static inline void terminal_draw_cursor_pixels(const TermCell& cell, uint32_t* pixels, int pw, int ph, int px, int py, int cell_w, int cell_h, bool use_ttf, GlyphCache* gc) { terminal_fill_pixel_rect(pixels, pw, ph, px, py, cell_w, cell_h, colors::WHITE.to_pixel()); if (cell.ch <= 32 && cell.ch >= 0) return; if (use_ttf) { int baseline = py + gc->ascent; fonts::mono->draw_char_to_buffer(pixels, pw, ph, px, baseline, (unsigned char)cell.ch, colors::BLACK, gc); } else { terminal_draw_bitmap_char(pixels, pw, ph, px, py, (unsigned char)cell.ch, colors::BLACK.to_pixel()); } } static inline bool terminal_prepare_render_cache(TerminalState* t, int visible_cols, int visible_rows) { if (visible_cols <= 0 || visible_rows <= 0) return false; if (t->render_cells && t->render_cols == visible_cols && t->render_rows == visible_rows) return true; if (t->render_cells) { montauk::free(t->render_cells); t->render_cells = nullptr; } int cells = visible_cols * visible_rows; t->render_cells = (TermCell*)montauk::alloc(cells * sizeof(TermCell)); if (!t->render_cells) { terminal_invalidate_render_cache(t); t->render_cols = 0; t->render_rows = 0; return false; } t->render_cols = visible_cols; t->render_rows = visible_rows; terminal_invalidate_render_cache(t); return true; } static inline TermCell terminal_visible_cell(TerminalState* t, int base_row, int x, int y) { return t->cells[(base_row + y) * t->cols + x]; } static inline void terminal_render(TerminalState* t, uint32_t* pixels, int pw, int ph) { if (!t->dirty || !t->cells || !pixels || pw <= 0 || ph <= 0) return; int cell_w = mono_cell_width(); int cell_h = mono_cell_height(); if (cell_w <= 0 || cell_h <= 0) return; bool use_ttf = fonts::mono && fonts::mono->valid; GlyphCache* gc = use_ttf ? fonts::mono->get_cache(fonts::TERM_SIZE) : nullptr; int base_row = t->scrollback_lines - t->view_offset; if (base_row < 0) base_row = 0; int visible_rows = ph / cell_h; int visible_cols = pw / cell_w; if (visible_rows > t->rows) visible_rows = t->rows; if (visible_cols > t->cols) visible_cols = t->cols; if (visible_rows <= 0 || visible_cols <= 0) return; bool have_cache = terminal_prepare_render_cache(t, visible_cols, visible_rows); bool full_redraw = !have_cache || !t->render_valid || t->render_pixels != pixels || t->render_pw != pw || t->render_ph != ph || t->render_cell_w != cell_w || t->render_cell_h != cell_h || t->render_base_row != base_row || t->render_term_cols != t->cols || t->render_term_rows != t->rows; uint32_t bg_px = colors::TERM_BG.to_pixel(); if (full_redraw) { int row_bytes = pw * sizeof(uint32_t); for (int i = 0; i < pw; i++) pixels[i] = bg_px; for (int r = 1; r < ph; r++) montauk::memcpy(&pixels[r * pw], pixels, row_bytes); } else if (t->render_cursor_visible) { int x = t->render_cursor_x; int y = t->render_cursor_y; if (x >= 0 && x < visible_cols && y >= 0 && y < visible_rows) { TermCell cell = terminal_visible_cell(t, base_row, x, y); terminal_draw_cell_pixels(cell, pixels, pw, ph, x * cell_w, y * cell_h, cell_w, cell_h, true, use_ttf, gc); if (have_cache) t->render_cells[y * visible_cols + x] = cell; } } for (int r = 0; r < visible_rows; r++) { int py = r * cell_h; for (int c = 0; c < visible_cols; c++) { TermCell cell = terminal_visible_cell(t, base_row, c, r); int cache_idx = r * visible_cols + c; bool changed = full_redraw || !have_cache || !term_cell_equal(t->render_cells[cache_idx], cell); if (!changed) continue; bool fill_bg = !full_redraw || cell.bg.to_pixel() != bg_px; terminal_draw_cell_pixels(cell, pixels, pw, ph, c * cell_w, py, cell_w, cell_h, fill_bg, use_ttf, gc); if (have_cache) t->render_cells[cache_idx] = cell; } } bool cursor_now_visible = t->view_offset == 0 && t->cursor_visible && t->cursor_x >= 0 && t->cursor_y >= 0 && t->cursor_x < visible_cols && t->cursor_y < visible_rows; if (cursor_now_visible) { TermCell cell = terminal_visible_cell(t, base_row, t->cursor_x, t->cursor_y); terminal_draw_cursor_pixels(cell, pixels, pw, ph, t->cursor_x * cell_w, t->cursor_y * cell_h, cell_w, cell_h, use_ttf, gc); } t->render_valid = have_cache; t->render_cursor_visible = cursor_now_visible; t->render_cursor_x = t->cursor_x; t->render_cursor_y = t->cursor_y; t->render_term_cols = t->cols; t->render_term_rows = t->rows; t->render_base_row = base_row; t->render_pw = pw; t->render_ph = ph; t->render_cell_w = cell_w; t->render_cell_h = cell_h; t->render_pixels = pixels; t->dirty = false; } static inline void terminal_resize(TerminalState* t, int new_cols, int new_rows) { if (new_cols == t->cols && new_rows == t->rows) return; if (new_cols < 1 || new_rows < 1) return; t->dirty = true; int new_capacity = new_rows + t->max_scrollback; int new_total = new_capacity * new_cols; TermCell* new_cells = (TermCell*)montauk::alloc(new_total * sizeof(TermCell)); TermCell* new_alt = (TermCell*)montauk::alloc(new_rows * new_cols * sizeof(TermCell)); if (!new_cells || !new_alt) { if (new_cells) montauk::free(new_cells); if (new_alt) montauk::free(new_alt); return; // keep existing buffers } // Clear new buffers for (int i = 0; i < new_total; i++) new_cells[i] = {' ', colors::TERM_FG, colors::TERM_BG}; for (int i = 0; i < new_rows * new_cols; i++) new_alt[i] = {' ', colors::TERM_FG, colors::TERM_BG}; // Copy content: scrollback + visible screen int old_content = t->scrollback_lines + t->rows; int keep = old_content < new_capacity ? old_content : new_capacity; int discard = old_content - keep; int copy_cols = t->cols < new_cols ? t->cols : new_cols; for (int r = 0; r < keep; r++) { for (int c = 0; c < copy_cols; c++) { new_cells[r * new_cols + c] = t->cells[(discard + r) * t->cols + c]; } } // Anchor the new visible region to the cursor's line. Treating the bottom // new_rows of kept content as the screen (the naive keep - new_rows) only // works when the screen is full: with a partly filled screen -- e.g. a // shell prompt a few lines down with blank rows below it -- it would push // the real text up into scrollback and drop the view onto the blank region, // so a zoom appears to scroll down and strands the cursor in empty space. // Instead pin the cursor to the bottom row when there is enough history // above it, otherwise keep the content anchored at the top. int abs_cursor_y = t->scrollback_lines + t->cursor_y - discard; if (abs_cursor_y < 0) abs_cursor_y = 0; int new_scrollback = abs_cursor_y - (new_rows - 1); if (new_scrollback < 0) new_scrollback = 0; if (new_scrollback > t->max_scrollback) new_scrollback = t->max_scrollback; // Adjust cursor int new_cursor_y = abs_cursor_y - new_scrollback; if (new_cursor_y < 0) new_cursor_y = 0; if (new_cursor_y >= new_rows) new_cursor_y = new_rows - 1; int new_cursor_x = t->cursor_x < new_cols ? t->cursor_x : new_cols - 1; if (t->cells) montauk::free(t->cells); if (t->alt_cells) montauk::free(t->alt_cells); if (t->render_cells) { montauk::free(t->render_cells); t->render_cells = nullptr; } t->cells = new_cells; t->alt_cells = new_alt; t->cols = new_cols; t->rows = new_rows; t->scrollback_lines = new_scrollback; t->cursor_x = new_cursor_x; t->cursor_y = new_cursor_y; terminal_invalidate_render_cache(t); // Clamp view offset if (t->view_offset > t->scrollback_lines) t->view_offset = t->scrollback_lines; // Notify child process of new terminal size if (t->child_pid > 0) { montauk::childio_settermsz(t->child_pid, new_cols, new_rows); } } static inline void terminal_handle_key(TerminalState* t, const montauk::abi::KeyEvent& key) { // Snap to live on any keyboard input if (t->view_offset > 0) { t->view_offset = 0; t->dirty = true; } if (t->child_pid > 0) { montauk::childio_writekey(t->child_pid, &key); } } // Returns false if the child process has exited static inline bool terminal_poll(TerminalState* t) { if (t->child_pid <= 0) return false; char buf[TERM_READ_BUF_SIZE]; int drained = 0; int grace_yields = 0; // Drain bursty TUI output as one frame. When a child was blocked on a full // stream, a short yield lets it finish the redraw before we present. for (;;) { int n = montauk::childio_read(t->child_pid, buf, sizeof(buf)); if (n > 0) { terminal_feed(t, buf, n); drained += n; grace_yields = 0; if (drained >= TERM_MAX_DRAIN_BYTES_PER_POLL) break; } else { // n == -1 means child process is gone; n == 0 means no data yet if (n < 0) { t->child_pid = 0; return false; } if (drained > 0 && grace_yields < TERM_DRAIN_GRACE_YIELDS) { grace_yields++; montauk::yield(); continue; } break; } } return true; } } // namespace gui