feat: improve Terminal emulator performance
This commit is contained in:
@@ -13,6 +13,8 @@
|
|||||||
|
|
||||||
namespace Montauk {
|
namespace Montauk {
|
||||||
|
|
||||||
|
static constexpr uint32_t RedirOutputStreamCapacity = 64 * 1024;
|
||||||
|
|
||||||
static int Sys_SpawnRedir(const char* path, const char* args) {
|
static int Sys_SpawnRedir(const char* path, const char* args) {
|
||||||
char resolved[256];
|
char resolved[256];
|
||||||
if (!ResolveProcessPath(path, resolved, sizeof(resolved))) return -1;
|
if (!ResolveProcessPath(path, resolved, sizeof(resolved))) return -1;
|
||||||
@@ -28,7 +30,7 @@ namespace Montauk {
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ipc::Stream* outStream = Ipc::CreateStream();
|
Ipc::Stream* outStream = Ipc::CreateStream(RedirOutputStreamCapacity);
|
||||||
Ipc::Stream* inStream = Ipc::CreateStream();
|
Ipc::Stream* inStream = Ipc::CreateStream();
|
||||||
Ipc::Mailbox* keyMailbox = Ipc::CreateMailbox();
|
Ipc::Mailbox* keyMailbox = Ipc::CreateMailbox();
|
||||||
if (outStream == nullptr || inStream == nullptr || keyMailbox == nullptr) {
|
if (outStream == nullptr || inStream == nullptr || keyMailbox == nullptr) {
|
||||||
|
|||||||
+246
-104
@@ -20,11 +20,14 @@ struct TermCell {
|
|||||||
};
|
};
|
||||||
|
|
||||||
static constexpr int TERM_MAX_SCROLLBACK = 500;
|
static constexpr int TERM_MAX_SCROLLBACK = 500;
|
||||||
static constexpr int TERM_MAX_READS_PER_POLL = 8;
|
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 {
|
struct TerminalState {
|
||||||
TermCell* cells;
|
TermCell* cells;
|
||||||
TermCell* alt_cells; // alternate screen buffer
|
TermCell* alt_cells; // alternate screen buffer
|
||||||
|
TermCell* render_cells; // last visible cells painted into the window buffer
|
||||||
int cols, rows;
|
int cols, rows;
|
||||||
int cursor_x, cursor_y;
|
int cursor_x, cursor_y;
|
||||||
int saved_cursor_x, saved_cursor_y; // saved cursor for alternate screen
|
int saved_cursor_x, saved_cursor_y; // saved cursor for alternate screen
|
||||||
@@ -39,6 +42,15 @@ struct TerminalState {
|
|||||||
bool alt_screen_active;
|
bool alt_screen_active;
|
||||||
bool reverse_video;
|
bool reverse_video;
|
||||||
bool dirty; // true when content changed since last render
|
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;
|
enum { STATE_NORMAL, STATE_ESC, STATE_CSI } parse_state;
|
||||||
bool csi_private; // true if '?' was seen after CSI
|
bool csi_private; // true if '?' was seen after CSI
|
||||||
@@ -92,6 +104,24 @@ static inline TermCell* term_screen_row(TerminalState* t, int r) {
|
|||||||
return &t->cells[(t->scrollback_lines + r) * t->cols];
|
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) {
|
static inline void terminal_scroll_up(TerminalState* t) {
|
||||||
if (!t->alt_screen_active && t->scrollback_lines < t->max_scrollback) {
|
if (!t->alt_screen_active && t->scrollback_lines < t->max_scrollback) {
|
||||||
// Room for scrollback: top visible row becomes scrollback, no data movement
|
// Room for scrollback: top visible row becomes scrollback, no data movement
|
||||||
@@ -109,7 +139,7 @@ static inline void terminal_scroll_up(TerminalState* t) {
|
|||||||
// Clear the new bottom visible row
|
// Clear the new bottom visible row
|
||||||
TermCell* bottom = term_screen_row(t, t->rows - 1);
|
TermCell* bottom = term_screen_row(t, t->rows - 1);
|
||||||
for (int c = 0; c < t->cols; c++) {
|
for (int c = 0; c < t->cols; c++) {
|
||||||
bottom[c] = {' ', t->current_fg, colors::TERM_BG};
|
bottom[c] = {' ', t->current_fg, terminal_erase_bg(t)};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keep user's scrolled-back viewport stable
|
// Keep user's scrolled-back viewport stable
|
||||||
@@ -138,6 +168,21 @@ static inline void terminal_init_cells(TerminalState* t, int cols, int rows, int
|
|||||||
t->alt_screen_active = false;
|
t->alt_screen_active = false;
|
||||||
t->reverse_video = false;
|
t->reverse_video = false;
|
||||||
t->dirty = true;
|
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->parse_state = TerminalState::STATE_NORMAL;
|
||||||
t->csi_private = false;
|
t->csi_private = false;
|
||||||
t->csi_param_count = 0;
|
t->csi_param_count = 0;
|
||||||
@@ -301,32 +346,33 @@ static inline void terminal_process_csi(TerminalState* t, char cmd) {
|
|||||||
}
|
}
|
||||||
case 'J': {
|
case 'J': {
|
||||||
// Erase in display
|
// Erase in display
|
||||||
|
Color erase_bg = terminal_erase_bg(t);
|
||||||
if (p0 == 0) {
|
if (p0 == 0) {
|
||||||
// Clear from cursor to end
|
// Clear from cursor to end
|
||||||
TermCell* row = term_screen_row(t, t->cursor_y);
|
TermCell* row = term_screen_row(t, t->cursor_y);
|
||||||
for (int x = t->cursor_x; x < t->cols; x++)
|
for (int x = t->cursor_x; x < t->cols; x++)
|
||||||
row[x] = {' ', t->current_fg, colors::TERM_BG};
|
row[x] = {' ', t->current_fg, erase_bg};
|
||||||
for (int r = t->cursor_y + 1; r < t->rows; r++) {
|
for (int r = t->cursor_y + 1; r < t->rows; r++) {
|
||||||
TermCell* rp = term_screen_row(t, r);
|
TermCell* rp = term_screen_row(t, r);
|
||||||
for (int c = 0; c < t->cols; c++)
|
for (int c = 0; c < t->cols; c++)
|
||||||
rp[c] = {' ', t->current_fg, colors::TERM_BG};
|
rp[c] = {' ', t->current_fg, erase_bg};
|
||||||
}
|
}
|
||||||
} else if (p0 == 1) {
|
} else if (p0 == 1) {
|
||||||
// Clear from start to cursor
|
// Clear from start to cursor
|
||||||
for (int r = 0; r < t->cursor_y; r++) {
|
for (int r = 0; r < t->cursor_y; r++) {
|
||||||
TermCell* rp = term_screen_row(t, r);
|
TermCell* rp = term_screen_row(t, r);
|
||||||
for (int c = 0; c < t->cols; c++)
|
for (int c = 0; c < t->cols; c++)
|
||||||
rp[c] = {' ', t->current_fg, colors::TERM_BG};
|
rp[c] = {' ', t->current_fg, erase_bg};
|
||||||
}
|
}
|
||||||
TermCell* row = term_screen_row(t, t->cursor_y);
|
TermCell* row = term_screen_row(t, t->cursor_y);
|
||||||
for (int x = 0; x <= t->cursor_x; x++)
|
for (int x = 0; x <= t->cursor_x; x++)
|
||||||
row[x] = {' ', t->current_fg, colors::TERM_BG};
|
row[x] = {' ', t->current_fg, erase_bg};
|
||||||
} else if (p0 == 2) {
|
} else if (p0 == 2) {
|
||||||
// Clear entire screen
|
// Clear entire screen
|
||||||
for (int r = 0; r < t->rows; r++) {
|
for (int r = 0; r < t->rows; r++) {
|
||||||
TermCell* rp = term_screen_row(t, r);
|
TermCell* rp = term_screen_row(t, r);
|
||||||
for (int c = 0; c < t->cols; c++)
|
for (int c = 0; c < t->cols; c++)
|
||||||
rp[c] = {' ', t->current_fg, colors::TERM_BG};
|
rp[c] = {' ', t->current_fg, erase_bg};
|
||||||
}
|
}
|
||||||
t->cursor_x = 0;
|
t->cursor_x = 0;
|
||||||
t->cursor_y = 0;
|
t->cursor_y = 0;
|
||||||
@@ -339,9 +385,10 @@ static inline void terminal_process_csi(TerminalState* t, char cmd) {
|
|||||||
if (p0 == 0) { start = t->cursor_x; 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 == 1) { start = 0; end = t->cursor_x + 1; }
|
||||||
else if (p0 == 2) { start = 0; end = t->cols; }
|
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);
|
TermCell* row = term_screen_row(t, t->cursor_y);
|
||||||
for (int x = start; x < end; x++)
|
for (int x = start; x < end; x++)
|
||||||
row[x] = {' ', t->current_fg, colors::TERM_BG};
|
row[x] = {' ', t->current_fg, erase_bg};
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'm': {
|
case 'm': {
|
||||||
@@ -520,122 +567,199 @@ static inline void terminal_feed(TerminalState* t, const char* data, int len) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
static inline void terminal_render(TerminalState* t, uint32_t* pixels, int pw, int ph) {
|
||||||
if (!t->dirty || !t->cells) return;
|
if (!t->dirty || !t->cells || !pixels || pw <= 0 || ph <= 0) return;
|
||||||
t->dirty = false;
|
|
||||||
|
|
||||||
int cell_w = mono_cell_width();
|
int cell_w = mono_cell_width();
|
||||||
int cell_h = mono_cell_height();
|
int cell_h = mono_cell_height();
|
||||||
|
if (cell_w <= 0 || cell_h <= 0) return;
|
||||||
|
|
||||||
bool use_ttf = fonts::mono && fonts::mono->valid;
|
bool use_ttf = fonts::mono && fonts::mono->valid;
|
||||||
GlyphCache* gc = use_ttf ? fonts::mono->get_cache(fonts::TERM_SIZE) : nullptr;
|
GlyphCache* gc = use_ttf ? fonts::mono->get_cache(fonts::TERM_SIZE) : nullptr;
|
||||||
|
|
||||||
// Fill background using row-copy: fill first row, then memcpy to the rest
|
|
||||||
uint32_t bg_px = colors::TERM_BG.to_pixel();
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine which rows of the buffer to display
|
|
||||||
int base_row = t->scrollback_lines - t->view_offset;
|
int base_row = t->scrollback_lines - t->view_offset;
|
||||||
if (base_row < 0) base_row = 0;
|
if (base_row < 0) base_row = 0;
|
||||||
|
|
||||||
// Render each visible cell
|
|
||||||
int visible_rows = ph / cell_h;
|
int visible_rows = ph / cell_h;
|
||||||
int visible_cols = pw / cell_w;
|
int visible_cols = pw / cell_w;
|
||||||
if (visible_rows > t->rows) visible_rows = t->rows;
|
if (visible_rows > t->rows) visible_rows = t->rows;
|
||||||
if (visible_cols > t->cols) visible_cols = t->cols;
|
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++) {
|
for (int r = 0; r < visible_rows; r++) {
|
||||||
int py = r * cell_h;
|
int py = r * cell_h;
|
||||||
int src_row = base_row + r;
|
|
||||||
for (int c = 0; c < visible_cols; c++) {
|
for (int c = 0; c < visible_cols; c++) {
|
||||||
int idx = src_row * t->cols + c;
|
TermCell cell = terminal_visible_cell(t, base_row, c, r);
|
||||||
TermCell& cell = t->cells[idx];
|
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;
|
||||||
|
|
||||||
int px = c * cell_w;
|
bool fill_bg = !full_redraw || cell.bg.to_pixel() != bg_px;
|
||||||
|
terminal_draw_cell_pixels(cell, pixels, pw, ph,
|
||||||
// Only draw cell background if it differs from terminal bg
|
c * cell_w, py,
|
||||||
uint32_t cell_bg = cell.bg.to_pixel();
|
cell_w, cell_h, fill_bg, use_ttf, gc);
|
||||||
if (cell_bg != bg_px) {
|
if (have_cache)
|
||||||
for (int fy = 0; fy < cell_h && py + fy < ph; fy++) {
|
t->render_cells[cache_idx] = cell;
|
||||||
uint32_t* row = &pixels[(py + fy) * pw + px];
|
|
||||||
for (int fx = 0; fx < cell_w && px + fx < pw; fx++) {
|
|
||||||
row[fx] = cell_bg;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw character glyph
|
|
||||||
if (cell.ch > 32 || cell.ch < 0) {
|
|
||||||
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 {
|
|
||||||
uint32_t cell_fg = cell.fg.to_pixel();
|
|
||||||
const uint8_t* glyph = &font_data[(unsigned char)cell.ch * FONT_HEIGHT];
|
|
||||||
for (int fy = 0; fy < FONT_HEIGHT; fy++) {
|
|
||||||
int dy = py + fy;
|
|
||||||
if (dy >= ph) break;
|
|
||||||
uint8_t bits = glyph[fy];
|
|
||||||
for (int fx = 0; fx < FONT_WIDTH; fx++) {
|
|
||||||
if (bits & (0x80 >> fx)) {
|
|
||||||
int dx = px + fx;
|
|
||||||
if (dx >= pw) break;
|
|
||||||
pixels[dy * pw + dx] = cell_fg;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw cursor (only when viewing live position)
|
bool cursor_now_visible = t->view_offset == 0 && t->cursor_visible &&
|
||||||
if (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) {
|
t->cursor_x < visible_cols && t->cursor_y < visible_rows;
|
||||||
int cx = t->cursor_x * cell_w;
|
if (cursor_now_visible) {
|
||||||
int cy = t->cursor_y * cell_h;
|
TermCell cell = terminal_visible_cell(t, base_row, t->cursor_x, t->cursor_y);
|
||||||
uint32_t cursor_px = colors::WHITE.to_pixel();
|
terminal_draw_cursor_pixels(cell, pixels, pw, ph,
|
||||||
for (int fy = 0; fy < cell_h; fy++) {
|
t->cursor_x * cell_w, t->cursor_y * cell_h,
|
||||||
int dy = cy + fy;
|
cell_w, cell_h, use_ttf, gc);
|
||||||
if (dy >= ph) break;
|
|
||||||
for (int fx = 0; fx < cell_w; fx++) {
|
|
||||||
int dx = cx + fx;
|
|
||||||
if (dx >= pw) break;
|
|
||||||
pixels[dy * pw + dx] = cursor_px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Draw character on top of cursor in black
|
|
||||||
if (t->cursor_y < t->rows && t->cursor_x < t->cols) {
|
|
||||||
TermCell* row = term_screen_row(t, t->cursor_y);
|
|
||||||
char ch = row[t->cursor_x].ch;
|
|
||||||
if (ch > 32 || ch < 0) {
|
|
||||||
if (use_ttf) {
|
|
||||||
int baseline = cy + gc->ascent;
|
|
||||||
fonts::mono->draw_char_to_buffer(pixels, pw, ph,
|
|
||||||
cx, baseline, (unsigned char)ch, colors::BLACK, gc);
|
|
||||||
} else {
|
|
||||||
const uint8_t* glyph = &font_data[(unsigned char)ch * FONT_HEIGHT];
|
|
||||||
uint32_t black_px = colors::BLACK.to_pixel();
|
|
||||||
for (int fy = 0; fy < FONT_HEIGHT; fy++) {
|
|
||||||
int dy = cy + fy;
|
|
||||||
if (dy >= ph) break;
|
|
||||||
uint8_t bits = glyph[fy];
|
|
||||||
for (int fx = 0; fx < FONT_WIDTH; fx++) {
|
|
||||||
if (bits & (0x80 >> fx)) {
|
|
||||||
int dx = cx + fx;
|
|
||||||
if (dx >= pw) break;
|
|
||||||
pixels[dy * pw + dx] = black_px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
static inline void terminal_resize(TerminalState* t, int new_cols, int new_rows) {
|
||||||
@@ -683,6 +807,10 @@ static inline void terminal_resize(TerminalState* t, int new_cols, int new_rows)
|
|||||||
|
|
||||||
if (t->cells) montauk::free(t->cells);
|
if (t->cells) montauk::free(t->cells);
|
||||||
if (t->alt_cells) montauk::free(t->alt_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->cells = new_cells;
|
||||||
t->alt_cells = new_alt;
|
t->alt_cells = new_alt;
|
||||||
@@ -691,6 +819,7 @@ static inline void terminal_resize(TerminalState* t, int new_cols, int new_rows)
|
|||||||
t->scrollback_lines = new_scrollback;
|
t->scrollback_lines = new_scrollback;
|
||||||
t->cursor_x = new_cursor_x;
|
t->cursor_x = new_cursor_x;
|
||||||
t->cursor_y = new_cursor_y;
|
t->cursor_y = new_cursor_y;
|
||||||
|
terminal_invalidate_render_cache(t);
|
||||||
|
|
||||||
// Clamp view offset
|
// Clamp view offset
|
||||||
if (t->view_offset > t->scrollback_lines)
|
if (t->view_offset > t->scrollback_lines)
|
||||||
@@ -716,15 +845,28 @@ static inline void terminal_handle_key(TerminalState* t, const Montauk::KeyEvent
|
|||||||
// Returns false if the child process has exited
|
// Returns false if the child process has exited
|
||||||
static inline bool terminal_poll(TerminalState* t) {
|
static inline bool terminal_poll(TerminalState* t) {
|
||||||
if (t->child_pid <= 0) return false;
|
if (t->child_pid <= 0) return false;
|
||||||
char buf[4096];
|
char buf[TERM_READ_BUF_SIZE];
|
||||||
// Drain a bounded amount per frame so window close/input events stay responsive.
|
int drained = 0;
|
||||||
for (int reads = 0; reads < TERM_MAX_READS_PER_POLL; reads++) {
|
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));
|
int n = montauk::childio_read(t->child_pid, buf, sizeof(buf));
|
||||||
if (n > 0) {
|
if (n > 0) {
|
||||||
terminal_feed(t, buf, n);
|
terminal_feed(t, buf, n);
|
||||||
|
drained += n;
|
||||||
|
grace_yields = 0;
|
||||||
|
if (drained >= TERM_MAX_DRAIN_BYTES_PER_POLL)
|
||||||
|
break;
|
||||||
} else {
|
} else {
|
||||||
// n == -1 means child process is gone; n == 0 means no data yet
|
// n == -1 means child process is gone; n == 0 means no data yet
|
||||||
if (n < 0) { t->child_pid = 0; return false; }
|
if (n < 0) { t->child_pid = 0; return false; }
|
||||||
|
if (drained > 0 && grace_yields < TERM_DRAIN_GRACE_YIELDS) {
|
||||||
|
grace_yields++;
|
||||||
|
montauk::yield();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -148,6 +148,7 @@ static void klog_handle_mouse(const Montauk::WinEvent& ev) {
|
|||||||
static void klog_cleanup() {
|
static void klog_cleanup() {
|
||||||
if (g_klog.term.cells) montauk::free(g_klog.term.cells);
|
if (g_klog.term.cells) montauk::free(g_klog.term.cells);
|
||||||
if (g_klog.term.alt_cells) montauk::free(g_klog.term.alt_cells);
|
if (g_klog.term.alt_cells) montauk::free(g_klog.term.alt_cells);
|
||||||
|
if (g_klog.term.render_cells) montauk::free(g_klog.term.render_cells);
|
||||||
if (g_klog.klog_buf) montauk::mfree(g_klog.klog_buf);
|
if (g_klog.klog_buf) montauk::mfree(g_klog.klog_buf);
|
||||||
montauk::memset(&g_klog, 0, sizeof(g_klog));
|
montauk::memset(&g_klog, 0, sizeof(g_klog));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ static void term_free_tab(TerminalState* ts) {
|
|||||||
if (ts->child_pid > 0) term_kill_process_tree(ts->child_pid);
|
if (ts->child_pid > 0) term_kill_process_tree(ts->child_pid);
|
||||||
if (ts->cells) montauk::free(ts->cells);
|
if (ts->cells) montauk::free(ts->cells);
|
||||||
if (ts->alt_cells) montauk::free(ts->alt_cells);
|
if (ts->alt_cells) montauk::free(ts->alt_cells);
|
||||||
|
if (ts->render_cells) montauk::free(ts->render_cells);
|
||||||
montauk::mfree(ts);
|
montauk::mfree(ts);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,6 +134,7 @@ static void term_close_tab(int idx) {
|
|||||||
if (g_tabs.active_tab >= g_tabs.tab_count)
|
if (g_tabs.active_tab >= g_tabs.tab_count)
|
||||||
g_tabs.active_tab = g_tabs.tab_count - 1;
|
g_tabs.active_tab = g_tabs.tab_count - 1;
|
||||||
|
|
||||||
|
terminal_invalidate_render_cache(g_tabs.tabs[g_tabs.active_tab]);
|
||||||
g_tabs.tabs[g_tabs.active_tab]->dirty = true;
|
g_tabs.tabs[g_tabs.active_tab]->dirty = true;
|
||||||
term_request_redraw();
|
term_request_redraw();
|
||||||
}
|
}
|
||||||
@@ -192,6 +194,7 @@ static bool term_render() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (g_tabs.active_tab != g_tabs.prev_active_tab) {
|
if (g_tabs.active_tab != g_tabs.prev_active_tab) {
|
||||||
|
terminal_invalidate_render_cache(g_tabs.tabs[g_tabs.active_tab]);
|
||||||
g_tabs.tabs[g_tabs.active_tab]->dirty = true;
|
g_tabs.tabs[g_tabs.active_tab]->dirty = true;
|
||||||
g_tabs.prev_active_tab = g_tabs.active_tab;
|
g_tabs.prev_active_tab = g_tabs.active_tab;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,10 +20,14 @@ struct TermCell {
|
|||||||
};
|
};
|
||||||
|
|
||||||
static constexpr int TERM_MAX_SCROLLBACK = 500;
|
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 {
|
struct TerminalState {
|
||||||
TermCell* cells;
|
TermCell* cells;
|
||||||
TermCell* alt_cells; // alternate screen buffer
|
TermCell* alt_cells; // alternate screen buffer
|
||||||
|
TermCell* render_cells; // last visible cells painted into the window buffer
|
||||||
int cols, rows;
|
int cols, rows;
|
||||||
int cursor_x, cursor_y;
|
int cursor_x, cursor_y;
|
||||||
int saved_cursor_x, saved_cursor_y; // saved cursor for alternate screen
|
int saved_cursor_x, saved_cursor_y; // saved cursor for alternate screen
|
||||||
@@ -38,6 +42,15 @@ struct TerminalState {
|
|||||||
bool alt_screen_active;
|
bool alt_screen_active;
|
||||||
bool reverse_video;
|
bool reverse_video;
|
||||||
bool dirty; // true when content changed since last render
|
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;
|
enum { STATE_NORMAL, STATE_ESC, STATE_CSI } parse_state;
|
||||||
bool csi_private; // true if '?' was seen after CSI
|
bool csi_private; // true if '?' was seen after CSI
|
||||||
@@ -69,11 +82,46 @@ static inline Color term_ansi_color(int idx) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
// Helper: pointer to start of screen-relative row r in the cells buffer
|
||||||
static inline TermCell* term_screen_row(TerminalState* t, int r) {
|
static inline TermCell* term_screen_row(TerminalState* t, int r) {
|
||||||
return &t->cells[(t->scrollback_lines + r) * t->cols];
|
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) {
|
static inline void terminal_scroll_up(TerminalState* t) {
|
||||||
if (!t->alt_screen_active && t->scrollback_lines < t->max_scrollback) {
|
if (!t->alt_screen_active && t->scrollback_lines < t->max_scrollback) {
|
||||||
// Room for scrollback: top visible row becomes scrollback, no data movement
|
// Room for scrollback: top visible row becomes scrollback, no data movement
|
||||||
@@ -91,7 +139,7 @@ static inline void terminal_scroll_up(TerminalState* t) {
|
|||||||
// Clear the new bottom visible row
|
// Clear the new bottom visible row
|
||||||
TermCell* bottom = term_screen_row(t, t->rows - 1);
|
TermCell* bottom = term_screen_row(t, t->rows - 1);
|
||||||
for (int c = 0; c < t->cols; c++) {
|
for (int c = 0; c < t->cols; c++) {
|
||||||
bottom[c] = {' ', t->current_fg, colors::TERM_BG};
|
bottom[c] = {' ', t->current_fg, terminal_erase_bg(t)};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keep user's scrolled-back viewport stable
|
// Keep user's scrolled-back viewport stable
|
||||||
@@ -120,6 +168,21 @@ static inline void terminal_init_cells(TerminalState* t, int cols, int rows, int
|
|||||||
t->alt_screen_active = false;
|
t->alt_screen_active = false;
|
||||||
t->reverse_video = false;
|
t->reverse_video = false;
|
||||||
t->dirty = true;
|
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->parse_state = TerminalState::STATE_NORMAL;
|
||||||
t->csi_private = false;
|
t->csi_private = false;
|
||||||
t->csi_param_count = 0;
|
t->csi_param_count = 0;
|
||||||
@@ -283,32 +346,33 @@ static inline void terminal_process_csi(TerminalState* t, char cmd) {
|
|||||||
}
|
}
|
||||||
case 'J': {
|
case 'J': {
|
||||||
// Erase in display
|
// Erase in display
|
||||||
|
Color erase_bg = terminal_erase_bg(t);
|
||||||
if (p0 == 0) {
|
if (p0 == 0) {
|
||||||
// Clear from cursor to end
|
// Clear from cursor to end
|
||||||
TermCell* row = term_screen_row(t, t->cursor_y);
|
TermCell* row = term_screen_row(t, t->cursor_y);
|
||||||
for (int x = t->cursor_x; x < t->cols; x++)
|
for (int x = t->cursor_x; x < t->cols; x++)
|
||||||
row[x] = {' ', t->current_fg, colors::TERM_BG};
|
row[x] = {' ', t->current_fg, erase_bg};
|
||||||
for (int r = t->cursor_y + 1; r < t->rows; r++) {
|
for (int r = t->cursor_y + 1; r < t->rows; r++) {
|
||||||
TermCell* rp = term_screen_row(t, r);
|
TermCell* rp = term_screen_row(t, r);
|
||||||
for (int c = 0; c < t->cols; c++)
|
for (int c = 0; c < t->cols; c++)
|
||||||
rp[c] = {' ', t->current_fg, colors::TERM_BG};
|
rp[c] = {' ', t->current_fg, erase_bg};
|
||||||
}
|
}
|
||||||
} else if (p0 == 1) {
|
} else if (p0 == 1) {
|
||||||
// Clear from start to cursor
|
// Clear from start to cursor
|
||||||
for (int r = 0; r < t->cursor_y; r++) {
|
for (int r = 0; r < t->cursor_y; r++) {
|
||||||
TermCell* rp = term_screen_row(t, r);
|
TermCell* rp = term_screen_row(t, r);
|
||||||
for (int c = 0; c < t->cols; c++)
|
for (int c = 0; c < t->cols; c++)
|
||||||
rp[c] = {' ', t->current_fg, colors::TERM_BG};
|
rp[c] = {' ', t->current_fg, erase_bg};
|
||||||
}
|
}
|
||||||
TermCell* row = term_screen_row(t, t->cursor_y);
|
TermCell* row = term_screen_row(t, t->cursor_y);
|
||||||
for (int x = 0; x <= t->cursor_x; x++)
|
for (int x = 0; x <= t->cursor_x; x++)
|
||||||
row[x] = {' ', t->current_fg, colors::TERM_BG};
|
row[x] = {' ', t->current_fg, erase_bg};
|
||||||
} else if (p0 == 2) {
|
} else if (p0 == 2) {
|
||||||
// Clear entire screen
|
// Clear entire screen
|
||||||
for (int r = 0; r < t->rows; r++) {
|
for (int r = 0; r < t->rows; r++) {
|
||||||
TermCell* rp = term_screen_row(t, r);
|
TermCell* rp = term_screen_row(t, r);
|
||||||
for (int c = 0; c < t->cols; c++)
|
for (int c = 0; c < t->cols; c++)
|
||||||
rp[c] = {' ', t->current_fg, colors::TERM_BG};
|
rp[c] = {' ', t->current_fg, erase_bg};
|
||||||
}
|
}
|
||||||
t->cursor_x = 0;
|
t->cursor_x = 0;
|
||||||
t->cursor_y = 0;
|
t->cursor_y = 0;
|
||||||
@@ -321,15 +385,44 @@ static inline void terminal_process_csi(TerminalState* t, char cmd) {
|
|||||||
if (p0 == 0) { start = t->cursor_x; 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 == 1) { start = 0; end = t->cursor_x + 1; }
|
||||||
else if (p0 == 2) { start = 0; end = t->cols; }
|
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);
|
TermCell* row = term_screen_row(t, t->cursor_y);
|
||||||
for (int x = start; x < end; x++)
|
for (int x = start; x < end; x++)
|
||||||
row[x] = {' ', t->current_fg, colors::TERM_BG};
|
row[x] = {' ', t->current_fg, erase_bg};
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'm': {
|
case 'm': {
|
||||||
// SGR - Set Graphics Rendition
|
// SGR - Set Graphics Rendition
|
||||||
for (int i = 0; i < t->csi_param_count; i++) {
|
for (int i = 0; i < t->csi_param_count; i++) {
|
||||||
int code = t->csi_params[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) {
|
if (code == 0) {
|
||||||
t->current_fg = colors::TERM_FG;
|
t->current_fg = colors::TERM_FG;
|
||||||
t->current_bg = colors::TERM_BG;
|
t->current_bg = colors::TERM_BG;
|
||||||
@@ -474,122 +567,199 @@ static inline void terminal_feed(TerminalState* t, const char* data, int len) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
static inline void terminal_render(TerminalState* t, uint32_t* pixels, int pw, int ph) {
|
||||||
if (!t->dirty || !t->cells) return;
|
if (!t->dirty || !t->cells || !pixels || pw <= 0 || ph <= 0) return;
|
||||||
t->dirty = false;
|
|
||||||
|
|
||||||
int cell_w = mono_cell_width();
|
int cell_w = mono_cell_width();
|
||||||
int cell_h = mono_cell_height();
|
int cell_h = mono_cell_height();
|
||||||
|
if (cell_w <= 0 || cell_h <= 0) return;
|
||||||
|
|
||||||
bool use_ttf = fonts::mono && fonts::mono->valid;
|
bool use_ttf = fonts::mono && fonts::mono->valid;
|
||||||
GlyphCache* gc = use_ttf ? fonts::mono->get_cache(fonts::TERM_SIZE) : nullptr;
|
GlyphCache* gc = use_ttf ? fonts::mono->get_cache(fonts::TERM_SIZE) : nullptr;
|
||||||
|
|
||||||
// Fill background using row-copy: fill first row, then memcpy to the rest
|
|
||||||
uint32_t bg_px = colors::TERM_BG.to_pixel();
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine which rows of the buffer to display
|
|
||||||
int base_row = t->scrollback_lines - t->view_offset;
|
int base_row = t->scrollback_lines - t->view_offset;
|
||||||
if (base_row < 0) base_row = 0;
|
if (base_row < 0) base_row = 0;
|
||||||
|
|
||||||
// Render each visible cell
|
|
||||||
int visible_rows = ph / cell_h;
|
int visible_rows = ph / cell_h;
|
||||||
int visible_cols = pw / cell_w;
|
int visible_cols = pw / cell_w;
|
||||||
if (visible_rows > t->rows) visible_rows = t->rows;
|
if (visible_rows > t->rows) visible_rows = t->rows;
|
||||||
if (visible_cols > t->cols) visible_cols = t->cols;
|
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++) {
|
for (int r = 0; r < visible_rows; r++) {
|
||||||
int py = r * cell_h;
|
int py = r * cell_h;
|
||||||
int src_row = base_row + r;
|
|
||||||
for (int c = 0; c < visible_cols; c++) {
|
for (int c = 0; c < visible_cols; c++) {
|
||||||
int idx = src_row * t->cols + c;
|
TermCell cell = terminal_visible_cell(t, base_row, c, r);
|
||||||
TermCell& cell = t->cells[idx];
|
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;
|
||||||
|
|
||||||
int px = c * cell_w;
|
bool fill_bg = !full_redraw || cell.bg.to_pixel() != bg_px;
|
||||||
|
terminal_draw_cell_pixels(cell, pixels, pw, ph,
|
||||||
// Only draw cell background if it differs from terminal bg
|
c * cell_w, py,
|
||||||
uint32_t cell_bg = cell.bg.to_pixel();
|
cell_w, cell_h, fill_bg, use_ttf, gc);
|
||||||
if (cell_bg != bg_px) {
|
if (have_cache)
|
||||||
for (int fy = 0; fy < cell_h && py + fy < ph; fy++) {
|
t->render_cells[cache_idx] = cell;
|
||||||
uint32_t* row = &pixels[(py + fy) * pw + px];
|
|
||||||
for (int fx = 0; fx < cell_w && px + fx < pw; fx++) {
|
|
||||||
row[fx] = cell_bg;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw character glyph
|
|
||||||
if (cell.ch > 32 || cell.ch < 0) {
|
|
||||||
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 {
|
|
||||||
uint32_t cell_fg = cell.fg.to_pixel();
|
|
||||||
const uint8_t* glyph = &font_data[(unsigned char)cell.ch * FONT_HEIGHT];
|
|
||||||
for (int fy = 0; fy < FONT_HEIGHT; fy++) {
|
|
||||||
int dy = py + fy;
|
|
||||||
if (dy >= ph) break;
|
|
||||||
uint8_t bits = glyph[fy];
|
|
||||||
for (int fx = 0; fx < FONT_WIDTH; fx++) {
|
|
||||||
if (bits & (0x80 >> fx)) {
|
|
||||||
int dx = px + fx;
|
|
||||||
if (dx >= pw) break;
|
|
||||||
pixels[dy * pw + dx] = cell_fg;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw cursor (only when viewing live position)
|
bool cursor_now_visible = t->view_offset == 0 && t->cursor_visible &&
|
||||||
if (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) {
|
t->cursor_x < visible_cols && t->cursor_y < visible_rows;
|
||||||
int cx = t->cursor_x * cell_w;
|
if (cursor_now_visible) {
|
||||||
int cy = t->cursor_y * cell_h;
|
TermCell cell = terminal_visible_cell(t, base_row, t->cursor_x, t->cursor_y);
|
||||||
uint32_t cursor_px = colors::WHITE.to_pixel();
|
terminal_draw_cursor_pixels(cell, pixels, pw, ph,
|
||||||
for (int fy = 0; fy < cell_h; fy++) {
|
t->cursor_x * cell_w, t->cursor_y * cell_h,
|
||||||
int dy = cy + fy;
|
cell_w, cell_h, use_ttf, gc);
|
||||||
if (dy >= ph) break;
|
|
||||||
for (int fx = 0; fx < cell_w; fx++) {
|
|
||||||
int dx = cx + fx;
|
|
||||||
if (dx >= pw) break;
|
|
||||||
pixels[dy * pw + dx] = cursor_px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Draw character on top of cursor in black
|
|
||||||
if (t->cursor_y < t->rows && t->cursor_x < t->cols) {
|
|
||||||
TermCell* row = term_screen_row(t, t->cursor_y);
|
|
||||||
char ch = row[t->cursor_x].ch;
|
|
||||||
if (ch > 32 || ch < 0) {
|
|
||||||
if (use_ttf) {
|
|
||||||
int baseline = cy + gc->ascent;
|
|
||||||
fonts::mono->draw_char_to_buffer(pixels, pw, ph,
|
|
||||||
cx, baseline, (unsigned char)ch, colors::BLACK, gc);
|
|
||||||
} else {
|
|
||||||
const uint8_t* glyph = &font_data[(unsigned char)ch * FONT_HEIGHT];
|
|
||||||
uint32_t black_px = colors::BLACK.to_pixel();
|
|
||||||
for (int fy = 0; fy < FONT_HEIGHT; fy++) {
|
|
||||||
int dy = cy + fy;
|
|
||||||
if (dy >= ph) break;
|
|
||||||
uint8_t bits = glyph[fy];
|
|
||||||
for (int fx = 0; fx < FONT_WIDTH; fx++) {
|
|
||||||
if (bits & (0x80 >> fx)) {
|
|
||||||
int dx = cx + fx;
|
|
||||||
if (dx >= pw) break;
|
|
||||||
pixels[dy * pw + dx] = black_px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
static inline void terminal_resize(TerminalState* t, int new_cols, int new_rows) {
|
||||||
@@ -637,6 +807,10 @@ static inline void terminal_resize(TerminalState* t, int new_cols, int new_rows)
|
|||||||
|
|
||||||
if (t->cells) montauk::free(t->cells);
|
if (t->cells) montauk::free(t->cells);
|
||||||
if (t->alt_cells) montauk::free(t->alt_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->cells = new_cells;
|
||||||
t->alt_cells = new_alt;
|
t->alt_cells = new_alt;
|
||||||
@@ -645,6 +819,7 @@ static inline void terminal_resize(TerminalState* t, int new_cols, int new_rows)
|
|||||||
t->scrollback_lines = new_scrollback;
|
t->scrollback_lines = new_scrollback;
|
||||||
t->cursor_x = new_cursor_x;
|
t->cursor_x = new_cursor_x;
|
||||||
t->cursor_y = new_cursor_y;
|
t->cursor_y = new_cursor_y;
|
||||||
|
terminal_invalidate_render_cache(t);
|
||||||
|
|
||||||
// Clamp view offset
|
// Clamp view offset
|
||||||
if (t->view_offset > t->scrollback_lines)
|
if (t->view_offset > t->scrollback_lines)
|
||||||
@@ -670,15 +845,28 @@ static inline void terminal_handle_key(TerminalState* t, const Montauk::KeyEvent
|
|||||||
// Returns false if the child process has exited
|
// Returns false if the child process has exited
|
||||||
static inline bool terminal_poll(TerminalState* t) {
|
static inline bool terminal_poll(TerminalState* t) {
|
||||||
if (t->child_pid <= 0) return false;
|
if (t->child_pid <= 0) return false;
|
||||||
char buf[4096];
|
char buf[TERM_READ_BUF_SIZE];
|
||||||
// Drain all available data so large output renders in one frame
|
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 (;;) {
|
for (;;) {
|
||||||
int n = montauk::childio_read(t->child_pid, buf, sizeof(buf));
|
int n = montauk::childio_read(t->child_pid, buf, sizeof(buf));
|
||||||
if (n > 0) {
|
if (n > 0) {
|
||||||
terminal_feed(t, buf, n);
|
terminal_feed(t, buf, n);
|
||||||
|
drained += n;
|
||||||
|
grace_yields = 0;
|
||||||
|
if (drained >= TERM_MAX_DRAIN_BYTES_PER_POLL)
|
||||||
|
break;
|
||||||
} else {
|
} else {
|
||||||
// n == -1 means child process is gone; n == 0 means no data yet
|
// n == -1 means child process is gone; n == 0 means no data yet
|
||||||
if (n < 0) { t->child_pid = 0; return false; }
|
if (n < 0) { t->child_pid = 0; return false; }
|
||||||
|
if (drained > 0 && grace_yields < TERM_DRAIN_GRACE_YIELDS) {
|
||||||
|
grace_yields++;
|
||||||
|
montauk::yield();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user