feat: terminal performance improvements, add scrolling and tabs

This commit is contained in:
2026-03-04 16:46:11 +01:00
parent 15915c175a
commit ba7b7dca87
3 changed files with 516 additions and 115 deletions
+151 -78
View File
@@ -27,14 +27,17 @@ struct TerminalState {
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
int scroll_top; int scrollback_lines; // lines of history above visible screen (0..max_scrollback)
int total_rows; 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; int child_pid;
void* desktop; // DesktopState* for closing window on child exit
Color current_fg; Color current_fg;
Color current_bg; Color current_bg;
bool cursor_visible; bool cursor_visible;
bool alt_screen_active; bool alt_screen_active;
bool reverse_video; bool reverse_video;
bool dirty; // true when content changed since last render
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
@@ -66,52 +69,77 @@ static inline Color term_ansi_color(int idx) {
} }
} }
// 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 void terminal_scroll_up(TerminalState* t) { static inline void terminal_scroll_up(TerminalState* t) {
// Move all rows up by one if (!t->alt_screen_active && t->scrollback_lines < t->max_scrollback) {
for (int r = 0; r < t->rows - 1; r++) { // Room for scrollback: top visible row becomes scrollback, no data movement
for (int c = 0; c < t->cols; c++) { t->scrollback_lines++;
t->cells[r * t->cols + c] = t->cells[(r + 1) * t->cols + c]; } 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 last row // Clear the new bottom visible row
int last = 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++) {
t->cells[last * t->cols + c] = {' ', t->current_fg, colors::TERM_BG}; bottom[c] = {' ', t->current_fg, colors::TERM_BG};
}
// 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. // Initialize only the cell grid (no child process). Used by viewers like klog.
static inline void terminal_init_cells(TerminalState* t, int cols, int rows) { // 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->cols = cols;
t->rows = rows; t->rows = rows;
t->cursor_x = 0; t->cursor_x = 0;
t->cursor_y = 0; t->cursor_y = 0;
t->saved_cursor_x = 0; t->saved_cursor_x = 0;
t->saved_cursor_y = 0; t->saved_cursor_y = 0;
t->scroll_top = 0; t->scrollback_lines = 0;
t->total_rows = rows; t->max_scrollback = max_sb;
t->view_offset = 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;
t->cursor_visible = false; t->cursor_visible = false;
t->alt_screen_active = false; t->alt_screen_active = false;
t->reverse_video = false; t->reverse_video = false;
t->dirty = true;
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;
t->csi_current_param = 0; t->csi_current_param = 0;
t->child_pid = 0; t->child_pid = 0;
int total_cells = cols * rows; int total_cells = (rows + max_sb) * cols;
int screen_cells = rows * cols;
t->cells = (TermCell*)montauk::alloc(total_cells * sizeof(TermCell)); t->cells = (TermCell*)montauk::alloc(total_cells * sizeof(TermCell));
t->alt_cells = (TermCell*)montauk::alloc(total_cells * sizeof(TermCell)); t->alt_cells = (TermCell*)montauk::alloc(screen_cells * sizeof(TermCell));
for (int i = 0; i < total_cells; i++) { for (int i = 0; i < total_cells; i++) {
t->cells[i] = {' ', colors::TERM_FG, colors::TERM_BG}; 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}; t->alt_cells[i] = {' ', colors::TERM_FG, colors::TERM_BG};
} }
} }
static inline void terminal_init(TerminalState* t, int cols, int rows) { static inline void terminal_init(TerminalState* t, int cols, int rows) {
terminal_init_cells(t, cols, rows); terminal_init_cells(t, cols, rows, TERM_MAX_SCROLLBACK);
t->cursor_visible = true; t->cursor_visible = true;
t->child_pid = montauk::spawn_redir("0:/os/shell.elf"); t->child_pid = montauk::spawn_redir("0:/os/shell.elf");
@@ -127,25 +155,28 @@ static inline void terminal_put_char(TerminalState* t, char ch) {
terminal_scroll_up(t); terminal_scroll_up(t);
t->cursor_y = t->rows - 1; t->cursor_y = t->rows - 1;
} }
int idx = t->cursor_y * t->cols + t->cursor_x; TermCell* row = term_screen_row(t, t->cursor_y);
t->cells[idx].ch = ch; row[t->cursor_x].ch = ch;
t->cells[idx].fg = t->current_fg; row[t->cursor_x].fg = t->current_fg;
t->cells[idx].bg = t->current_bg; row[t->cursor_x].bg = t->current_bg;
t->cursor_x++; t->cursor_x++;
} }
static inline void terminal_enter_alt_screen(TerminalState* t) { static inline void terminal_enter_alt_screen(TerminalState* t) {
if (t->alt_screen_active) return; if (t->alt_screen_active) return;
t->alt_screen_active = true; t->alt_screen_active = true;
t->dirty = true;
// Save cursor // Save cursor
t->saved_cursor_x = t->cursor_x; t->saved_cursor_x = t->cursor_x;
t->saved_cursor_y = t->cursor_y; t->saved_cursor_y = t->cursor_y;
// Swap buffers: save main screen to alt_cells, clear main // Save visible screen to alt_cells, clear visible screen
int total = t->cols * t->rows; int total = t->cols * t->rows;
TermCell* screen = term_screen_row(t, 0);
for (int i = 0; i < total; i++) { for (int i = 0; i < total; i++) {
t->alt_cells[i] = t->cells[i]; t->alt_cells[i] = screen[i];
t->cells[i] = {' ', colors::TERM_FG, colors::TERM_BG}; screen[i] = {' ', colors::TERM_FG, colors::TERM_BG};
} }
t->view_offset = 0;
t->cursor_x = 0; t->cursor_x = 0;
t->cursor_y = 0; t->cursor_y = 0;
} }
@@ -153,10 +184,12 @@ static inline void terminal_enter_alt_screen(TerminalState* t) {
static inline void terminal_exit_alt_screen(TerminalState* t) { static inline void terminal_exit_alt_screen(TerminalState* t) {
if (!t->alt_screen_active) return; if (!t->alt_screen_active) return;
t->alt_screen_active = false; t->alt_screen_active = false;
// Restore main screen from alt_cells t->dirty = true;
// Restore visible screen from alt_cells
int total = t->cols * t->rows; int total = t->cols * t->rows;
TermCell* screen = term_screen_row(t, 0);
for (int i = 0; i < total; i++) { for (int i = 0; i < total; i++) {
t->cells[i] = t->alt_cells[i]; screen[i] = t->alt_cells[i];
} }
// Restore cursor // Restore cursor
t->cursor_x = t->saved_cursor_x; t->cursor_x = t->saved_cursor_x;
@@ -244,22 +277,31 @@ static inline void terminal_process_csi(TerminalState* t, char cmd) {
// Erase in display // Erase in display
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);
for (int x = t->cursor_x; x < t->cols; x++) for (int x = t->cursor_x; x < t->cols; x++)
t->cells[t->cursor_y * t->cols + x] = {' ', t->current_fg, colors::TERM_BG}; row[x] = {' ', t->current_fg, colors::TERM_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);
for (int c = 0; c < t->cols; c++) for (int c = 0; c < t->cols; c++)
t->cells[r * t->cols + c] = {' ', t->current_fg, colors::TERM_BG}; rp[c] = {' ', t->current_fg, colors::TERM_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);
for (int c = 0; c < t->cols; c++) for (int c = 0; c < t->cols; c++)
t->cells[r * t->cols + c] = {' ', t->current_fg, colors::TERM_BG}; rp[c] = {' ', t->current_fg, colors::TERM_BG};
}
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++)
t->cells[t->cursor_y * t->cols + x] = {' ', t->current_fg, colors::TERM_BG}; row[x] = {' ', t->current_fg, colors::TERM_BG};
} else if (p0 == 2) { } else if (p0 == 2) {
// Clear entire screen // Clear entire screen
for (int i = 0; i < t->rows * t->cols; i++) for (int r = 0; r < t->rows; r++) {
t->cells[i] = {' ', t->current_fg, colors::TERM_BG}; TermCell* rp = term_screen_row(t, r);
for (int c = 0; c < t->cols; c++)
rp[c] = {' ', t->current_fg, colors::TERM_BG};
}
t->cursor_x = 0; t->cursor_x = 0;
t->cursor_y = 0; t->cursor_y = 0;
} }
@@ -271,8 +313,9 @@ 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; }
TermCell* row = term_screen_row(t, t->cursor_y);
for (int x = start; x < end; x++) for (int x = start; x < end; x++)
t->cells[t->cursor_y * t->cols + x] = {' ', t->current_fg, colors::TERM_BG}; row[x] = {' ', t->current_fg, colors::TERM_BG};
break; break;
} }
case 'm': { case 'm': {
@@ -348,6 +391,7 @@ static inline void terminal_process_csi(TerminalState* t, char cmd) {
} }
static inline void terminal_feed(TerminalState* t, const char* data, int len) { 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++) { for (int i = 0; i < len; i++) {
char ch = data[i]; char ch = data[i];
@@ -423,18 +467,26 @@ static inline void terminal_feed(TerminalState* t, const char* data, int len) {
} }
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) 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();
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 // Fill background using row-copy: fill first row, then memcpy to the rest
uint32_t bg_px = colors::TERM_BG.to_pixel(); uint32_t bg_px = colors::TERM_BG.to_pixel();
int total = pw * ph; int row_bytes = pw * sizeof(uint32_t);
for (int i = 0; i < total; i++) { for (int i = 0; i < pw; i++) pixels[i] = bg_px;
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;
if (base_row < 0) base_row = 0;
// Render each visible cell // 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;
@@ -442,23 +494,22 @@ static inline void terminal_render(TerminalState* t, uint32_t* pixels, int pw, i
if (visible_cols > t->cols) visible_cols = t->cols; if (visible_cols > t->cols) visible_cols = t->cols;
for (int r = 0; r < visible_rows; r++) { for (int r = 0; r < visible_rows; r++) {
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 = r * t->cols + c; int idx = src_row * t->cols + c;
TermCell& cell = t->cells[idx]; TermCell& cell = t->cells[idx];
int px = c * cell_w; int px = c * cell_w;
int py = r * cell_h;
// Only draw cell background if it differs from terminal bg
uint32_t cell_bg = cell.bg.to_pixel(); uint32_t cell_bg = cell.bg.to_pixel();
if (cell_bg != bg_px) {
// Draw cell background for (int fy = 0; fy < cell_h && py + fy < ph; fy++) {
for (int fy = 0; fy < cell_h; fy++) { uint32_t* row = &pixels[(py + fy) * pw + px];
int dy = py + fy; for (int fx = 0; fx < cell_w && px + fx < pw; fx++) {
if (dy >= ph) break; row[fx] = cell_bg;
for (int fx = 0; fx < cell_w; fx++) { }
int dx = px + fx;
if (dx >= pw) break;
pixels[dy * pw + dx] = cell_bg;
} }
} }
@@ -488,8 +539,9 @@ static inline void terminal_render(TerminalState* t, uint32_t* pixels, int pw, i
} }
} }
// Draw cursor // Draw cursor (only when viewing live position)
if (t->cursor_visible && t->cursor_x < visible_cols && t->cursor_y < visible_rows) { if (t->view_offset == 0 && t->cursor_visible &&
t->cursor_x < visible_cols && t->cursor_y < visible_rows) {
int cx = t->cursor_x * cell_w; int cx = t->cursor_x * cell_w;
int cy = t->cursor_y * cell_h; int cy = t->cursor_y * cell_h;
uint32_t cursor_px = colors::WHITE.to_pixel(); uint32_t cursor_px = colors::WHITE.to_pixel();
@@ -504,8 +556,8 @@ static inline void terminal_render(TerminalState* t, uint32_t* pixels, int pw, i
} }
// Draw character on top of cursor in black // Draw character on top of cursor in black
if (t->cursor_y < t->rows && t->cursor_x < t->cols) { if (t->cursor_y < t->rows && t->cursor_x < t->cols) {
int idx = t->cursor_y * t->cols + t->cursor_x; TermCell* row = term_screen_row(t, t->cursor_y);
char ch = t->cells[idx].ch; char ch = row[t->cursor_x].ch;
if (ch > 32 || ch < 0) { if (ch > 32 || ch < 0) {
if (use_ttf) { if (use_ttf) {
int baseline = cy + gc->ascent; int baseline = cy + gc->ascent;
@@ -535,48 +587,55 @@ static inline void terminal_render(TerminalState* t, uint32_t* pixels, int pw, i
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) {
if (new_cols == t->cols && new_rows == t->rows) return; if (new_cols == t->cols && new_rows == t->rows) return;
if (new_cols < 1 || new_rows < 1) return; if (new_cols < 1 || new_rows < 1) return;
t->dirty = true;
int new_total = new_cols * new_rows; 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_cells = (TermCell*)montauk::alloc(new_total * sizeof(TermCell));
TermCell* new_alt = (TermCell*)montauk::alloc(new_total * sizeof(TermCell)); TermCell* new_alt = (TermCell*)montauk::alloc(new_rows * new_cols * sizeof(TermCell));
// Clear new buffers // Clear new buffers
for (int i = 0; i < new_total; i++) { for (int i = 0; i < new_total; i++)
new_cells[i] = {' ', colors::TERM_FG, colors::TERM_BG}; 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}; new_alt[i] = {' ', colors::TERM_FG, colors::TERM_BG};
}
// Copy existing content (as much as fits) // Copy content: scrollback + visible screen
int copy_rows = t->rows < new_rows ? t->rows : new_rows; 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; int copy_cols = t->cols < new_cols ? t->cols : new_cols;
// If cursor is beyond the new grid, scroll content up to keep cursor visible for (int r = 0; r < keep; r++) {
int row_offset = 0; for (int c = 0; c < copy_cols; c++) {
if (t->cursor_y >= new_rows) { new_cells[r * new_cols + c] = t->cells[(discard + r) * t->cols + c];
row_offset = t->cursor_y - new_rows + 1; }
} }
for (int r = 0; r < copy_rows && (r + row_offset) < t->rows; r++) { int new_scrollback = keep - new_rows;
for (int c = 0; c < copy_cols; c++) { if (new_scrollback < 0) new_scrollback = 0;
new_cells[r * new_cols + c] = t->cells[(r + row_offset) * t->cols + c];
} // Adjust cursor
} int abs_cursor_y = t->scrollback_lines + t->cursor_y - discard;
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->cells) montauk::free(t->cells);
if (t->alt_cells) montauk::free(t->alt_cells); if (t->alt_cells) montauk::free(t->alt_cells);
t->cells = new_cells; t->cells = new_cells;
t->alt_cells = new_alt; t->alt_cells = new_alt;
int old_cols = t->cols;
t->cols = new_cols; t->cols = new_cols;
t->rows = new_rows; t->rows = new_rows;
t->scrollback_lines = new_scrollback;
t->cursor_x = new_cursor_x;
t->cursor_y = new_cursor_y;
// Adjust cursor position // Clamp view offset
t->cursor_y -= row_offset; if (t->view_offset > t->scrollback_lines)
if (t->cursor_x >= new_cols) t->cursor_x = new_cols - 1; t->view_offset = t->scrollback_lines;
if (t->cursor_y >= new_rows) t->cursor_y = new_rows - 1;
if (t->cursor_y < 0) t->cursor_y = 0;
// Notify child process of new terminal size // Notify child process of new terminal size
if (t->child_pid > 0) { if (t->child_pid > 0) {
@@ -585,18 +644,32 @@ static inline void terminal_resize(TerminalState* t, int new_cols, int new_rows)
} }
static inline void terminal_handle_key(TerminalState* t, const Montauk::KeyEvent& key) { static inline void terminal_handle_key(TerminalState* t, const Montauk::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) { if (t->child_pid > 0) {
montauk::childio_writekey(t->child_pid, &key); montauk::childio_writekey(t->child_pid, &key);
} }
} }
static inline void terminal_poll(TerminalState* t) { // Returns false if the child process has exited
if (t->child_pid <= 0) return; static inline bool terminal_poll(TerminalState* t) {
char buf[512]; if (t->child_pid <= 0) return false;
char buf[4096];
// Drain all available data so large output renders in one frame
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);
} else {
// n == -1 means child process is gone; n == 0 means no data yet
if (n < 0) { t->child_pid = 0; return false; }
break;
} }
}
return true;
} }
} // namespace gui } // namespace gui
+62 -13
View File
@@ -22,29 +22,35 @@ struct KlogState {
}; };
// ============================================================================ // ============================================================================
// Re-feed the last screenful of log into the terminal // Re-feed log into the terminal (fills scrollback + visible screen)
// ============================================================================ // ============================================================================
static void klog_refeed(KlogState* klog, int n) { static void klog_refeed(KlogState* klog, int n) {
// Find start of last `rows` lines from the end of the buffer // Find enough lines to fill scrollback + visible screen
int lines_needed = klog->term.rows; int lines_needed = klog->term.rows + klog->term.max_scrollback;
int start = 0; int start = 0;
for (int i = n - 1; i >= 0 && lines_needed > 0; i--) { int line_count = 0;
for (int i = n - 1; i >= 0; i--) {
if (klog->klog_buf[i] == '\n') { if (klog->klog_buf[i] == '\n') {
lines_needed--; line_count++;
if (lines_needed == 0) { start = i + 1; break; } if (line_count >= lines_needed) { start = i + 1; break; }
} }
} }
// Clear terminal // Reset terminal state
int total = klog->term.cols * klog->term.rows; klog->term.scrollback_lines = 0;
for (int i = 0; i < total; i++) klog->term.view_offset = 0;
klog->term.cells[i] = {' ', colors::TERM_FG, colors::TERM_BG};
klog->term.cursor_x = 0; klog->term.cursor_x = 0;
klog->term.cursor_y = 0; klog->term.cursor_y = 0;
klog->term.current_fg = colors::TERM_FG; klog->term.current_fg = colors::TERM_FG;
klog->term.current_bg = colors::TERM_BG; klog->term.current_bg = colors::TERM_BG;
// Clear visible screen
TermCell* screen = term_screen_row(&klog->term, 0);
int total = klog->term.cols * klog->term.rows;
for (int i = 0; i < total; i++)
screen[i] = {' ', colors::TERM_FG, colors::TERM_BG};
terminal_feed(&klog->term, klog->klog_buf + start, n - start); terminal_feed(&klog->term, klog->klog_buf + start, n - start);
} }
@@ -57,11 +63,54 @@ static void klog_on_draw(Window* win, Framebuffer& fb) {
if (!klog) return; if (!klog) return;
Rect cr = win->content_rect(); Rect cr = win->content_rect();
// Handle resize
int new_cols = cr.w / mono_cell_width();
int new_rows = cr.h / mono_cell_height();
if (new_cols != klog->term.cols || new_rows != klog->term.rows) {
terminal_resize(&klog->term, new_cols, new_rows);
// Refeed to fill the new grid with log content
if (klog->last_len > 0) {
klog_refeed(klog, klog->last_len);
}
}
bool was_dirty = klog->term.dirty;
terminal_render(&klog->term, win->content, cr.w, cr.h); terminal_render(&klog->term, win->content, cr.w, cr.h);
// Draw scrollbar overlay when scrollback exists
if (was_dirty && klog->term.scrollback_lines > 0) {
int cell_h = mono_cell_height();
int total_rows = klog->term.scrollback_lines + klog->term.rows;
int total_h = total_rows * cell_h;
int view_h = cr.h;
if (total_h > view_h) {
Canvas c(win->content, cr.w, cr.h);
int sb_w = 4;
int sb_x = cr.w - sb_w - 2;
int thumb_h = (view_h * view_h) / total_h;
if (thumb_h < 20) thumb_h = 20;
int scroll_from_top = klog->term.scrollback_lines - klog->term.view_offset;
int max_scroll = klog->term.scrollback_lines;
int thumb_y = (max_scroll > 0)
? (scroll_from_top * (view_h - thumb_h)) / max_scroll : 0;
c.fill_rect(sb_x, 0, sb_w, view_h, Color::from_rgb(0x33, 0x33, 0x33));
c.fill_rect(sb_x, thumb_y, sb_w, thumb_h, Color::from_rgb(0x88, 0x88, 0x88));
}
}
} }
static void klog_on_mouse(Window* win, MouseEvent& ev) { static void klog_on_mouse(Window* win, MouseEvent& ev) {
// Read-only viewer — no mouse interaction needed KlogState* klog = (KlogState*)win->app_data;
if (!klog) return;
if (ev.scroll != 0) {
klog->term.view_offset += (ev.scroll < 0) ? 3 : -3;
if (klog->term.view_offset < 0) klog->term.view_offset = 0;
if (klog->term.view_offset > klog->term.scrollback_lines)
klog->term.view_offset = klog->term.scrollback_lines;
klog->term.dirty = true;
}
} }
static void klog_on_key(Window* win, const Montauk::KeyEvent& key) { static void klog_on_key(Window* win, const Montauk::KeyEvent& key) {
@@ -125,8 +174,8 @@ void open_klog(DesktopState* ds) {
KlogState* klog = (KlogState*)montauk::malloc(sizeof(KlogState)); KlogState* klog = (KlogState*)montauk::malloc(sizeof(KlogState));
montauk::memset(klog, 0, sizeof(KlogState)); montauk::memset(klog, 0, sizeof(KlogState));
// Initialize the terminal cell grid (reuse terminal infrastructure) // Initialize with scrollback enabled
terminal_init_cells(&klog->term, cols, rows); terminal_init_cells(&klog->term, cols, rows, TERM_MAX_SCROLLBACK);
// Allocate klog read buffer // Allocate klog read buffer
klog->klog_buf = (char*)montauk::malloc(KLOG_READ_SIZE); klog->klog_buf = (char*)montauk::malloc(KLOG_READ_SIZE);
+316 -37
View File
@@ -1,55 +1,326 @@
/* /*
* app_terminal.cpp * app_terminal.cpp
* MontaukOS Desktop - Terminal application * MontaukOS Desktop - Terminal application with tab support
* Copyright (c) 2026 Daniel Hammer * Copyright (c) 2026 Daniel Hammer
*/ */
#include "apps_common.hpp" #include "apps_common.hpp"
// ============================================================================ // ============================================================================
// Terminal callbacks // Tab data model
// ============================================================================ // ============================================================================
static void terminal_on_draw(Window* win, Framebuffer& fb) { static constexpr int TERM_TAB_BAR_H = 34;
TerminalState* ts = (TerminalState*)win->app_data; static constexpr int TERM_MAX_TABS = 8;
static constexpr int TERM_TAB_W = 130;
static constexpr int TERM_TAB_GAP = 4;
static constexpr int TERM_PLUS_W = 28;
static constexpr int TERM_PLUS_PAD = 8;
// Compute the x-offset that centers all tabs within bar_w
static int term_tabs_origin(int bar_w, int tab_count) {
int total = tab_count * TERM_TAB_W + (tab_count - 1) * TERM_TAB_GAP;
int x = (bar_w - total) / 2;
if (x < 4) x = 4;
return x;
}
struct TermTabState {
TerminalState* tabs[TERM_MAX_TABS];
int tab_count;
int active_tab;
int prev_active_tab; // track tab switches to force redraw
uint32_t* last_content; // detect content buffer reallocation
DesktopState* desktop;
};
// ============================================================================
// Tab helpers
// ============================================================================
static void term_free_tab(TerminalState* ts) {
if (!ts) return; if (!ts) return;
Rect cr = win->content_rect();
// Check if window was resized and update terminal grid
int new_cols = cr.w / mono_cell_width();
int new_rows = cr.h / mono_cell_height();
if (new_cols != ts->cols || new_rows != ts->rows) {
terminal_resize(ts, new_cols, new_rows);
}
terminal_render(ts, win->content, cr.w, cr.h);
}
static void terminal_on_mouse(Window* win, MouseEvent& ev) {
// Terminal doesn't need mouse handling for now
}
static void terminal_on_key(Window* win, const Montauk::KeyEvent& key) {
TerminalState* ts = (TerminalState*)win->app_data;
if (!ts) return;
terminal_handle_key(ts, key);
}
static void terminal_on_close(Window* win) {
TerminalState* ts = (TerminalState*)win->app_data;
if (ts) {
if (ts->child_pid > 0) montauk::kill(ts->child_pid); if (ts->child_pid > 0) montauk::kill(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);
montauk::mfree(ts); montauk::mfree(ts);
}
static TerminalState* term_create_tab(DesktopState* ds, int cols, int rows) {
TerminalState* ts = (TerminalState*)montauk::malloc(sizeof(TerminalState));
montauk::memset(ts, 0, sizeof(TerminalState));
terminal_init(ts, cols, rows);
ts->desktop = ds;
return ts;
}
static void term_close_tab(TermTabState* tts, Window* win, int idx) {
if (idx < 0 || idx >= tts->tab_count) return;
term_free_tab(tts->tabs[idx]);
// Shift remaining tabs left
for (int i = idx; i < tts->tab_count - 1; i++)
tts->tabs[i] = tts->tabs[i + 1];
tts->tab_count--;
tts->tabs[tts->tab_count] = nullptr;
if (tts->tab_count == 0) {
// Last tab closed — close the window
DesktopState* ds = tts->desktop;
if (ds) {
for (int i = 0; i < ds->window_count; i++) {
if (&ds->windows[i] == win) {
desktop_close_window(ds, i);
return;
}
}
}
return;
}
// Adjust active tab index
if (idx == tts->active_tab) {
// Closed the active tab: prefer left neighbor, fall back to right
if (tts->active_tab > 0)
tts->active_tab--;
// else stays at 0 (right neighbor shifted here)
} else if (tts->active_tab > idx) {
tts->active_tab--;
}
if (tts->active_tab >= tts->tab_count)
tts->active_tab = tts->tab_count - 1;
// Mark new active tab dirty so it redraws
tts->tabs[tts->active_tab]->dirty = true;
}
static void term_add_tab(TermTabState* tts, int cols, int rows) {
if (tts->tab_count >= TERM_MAX_TABS) return;
TerminalState* ts = term_create_tab(tts->desktop, cols, rows);
tts->tabs[tts->tab_count] = ts;
tts->active_tab = tts->tab_count;
tts->tab_count++;
}
// ============================================================================
// Terminal callbacks
// ============================================================================
static void terminal_on_draw(Window* win, Framebuffer& fb) {
TermTabState* tts = (TermTabState*)win->app_data;
if (!tts || tts->tab_count == 0) return;
Rect cr = win->content_rect();
int term_h = cr.h - TERM_TAB_BAR_H;
if (term_h < 1) term_h = 1;
// Force full redraw when active tab changed
if (tts->active_tab != tts->prev_active_tab) {
tts->tabs[tts->active_tab]->dirty = true;
tts->prev_active_tab = tts->active_tab;
}
// Force full redraw when content buffer was reallocated (resize/maximize)
if (win->content != tts->last_content) {
tts->tabs[tts->active_tab]->dirty = true;
tts->last_content = win->content;
}
TerminalState* ts = tts->tabs[tts->active_tab];
// Check if window was resized and update terminal grid
int new_cols = cr.w / mono_cell_width();
int new_rows = term_h / mono_cell_height();
if (new_cols < 1) new_cols = 1;
if (new_rows < 1) new_rows = 1;
if (new_cols != ts->cols || new_rows != ts->rows) {
for (int i = 0; i < tts->tab_count; i++)
terminal_resize(tts->tabs[i], new_cols, new_rows);
}
// Only redraw when terminal content changed
if (!ts->dirty) return;
// ---- Draw tab bar ----
// Bar is darker than TERM_BG; active tab matches TERM_BG to merge with content
Canvas c(win->content, cr.w, cr.h);
Color bar_bg = Color::from_hex(0x1C1C1C);
c.fill_rect(0, 0, cr.w, TERM_TAB_BAR_H, bar_bg);
int fh = system_font_height();
int tab_x = term_tabs_origin(cr.w, tts->tab_count);
for (int i = 0; i < tts->tab_count; i++) {
bool active = (i == tts->active_tab);
char label[16];
snprintf(label, sizeof(label), "Tab %d", i + 1);
if (active) {
// Active tab: TERM_BG pill with squared-off bottom → merges into content
int ty = 5;
int th = TERM_TAB_BAR_H - ty;
c.fill_rounded_rect(tab_x, ty, TERM_TAB_W, th, 6, colors::TERM_BG);
c.fill_rect(tab_x, TERM_TAB_BAR_H - 6, TERM_TAB_W, 6, colors::TERM_BG);
int text_y = ty + (th - fh) / 2;
c.text(tab_x + 12, text_y, label, Color::from_hex(0xE0E0E0));
c.text(tab_x + TERM_TAB_W - 20, text_y, "x", Color::from_hex(0x707070));
} else {
// Inactive tab: subtle rounded pill, sits above the bottom edge
int ty = 7;
int th = 22;
c.fill_rounded_rect(tab_x, ty, TERM_TAB_W, th, 5, Color::from_hex(0x262626));
int text_y = ty + (th - fh) / 2;
c.text(tab_x + 12, text_y, label, Color::from_hex(0x6E6E6E));
c.text(tab_x + TERM_TAB_W - 20, text_y, "x", Color::from_hex(0x444444));
}
tab_x += TERM_TAB_W + TERM_TAB_GAP;
}
// "+" button pinned to right edge
if (tts->tab_count < TERM_MAX_TABS) {
int plus_h = 22;
int py = 7;
int px = cr.w - TERM_PLUS_PAD - TERM_PLUS_W;
c.fill_rounded_rect(px, py, TERM_PLUS_W, plus_h, 5, Color::from_hex(0x262626));
int pw_text = text_width("+");
c.text(px + (TERM_PLUS_W - pw_text) / 2, py + (plus_h - fh) / 2, "+",
Color::from_hex(0x6E6E6E));
}
// ---- Render active terminal into offset buffer ----
uint32_t* term_pixels = win->content + TERM_TAB_BAR_H * cr.w;
terminal_render(ts, term_pixels, cr.w, term_h);
// Draw scrollbar overlay when scrollback exists
if (ts->scrollback_lines > 0 && !ts->alt_screen_active) {
int cell_h = mono_cell_height();
int total_rows = ts->scrollback_lines + ts->rows;
int total_h = total_rows * cell_h;
int view_h = term_h;
if (total_h > view_h) {
Canvas sc(term_pixels, cr.w, term_h);
int sb_w = 4;
int sb_x = cr.w - sb_w - 2;
int thumb_h = (view_h * view_h) / total_h;
if (thumb_h < 20) thumb_h = 20;
int scroll_from_top = ts->scrollback_lines - ts->view_offset;
int max_scroll = ts->scrollback_lines;
int thumb_y = (max_scroll > 0)
? (scroll_from_top * (view_h - thumb_h)) / max_scroll : 0;
sc.fill_rect(sb_x, 0, sb_w, view_h, Color::from_rgb(0x33, 0x33, 0x33));
sc.fill_rect(sb_x, thumb_y, sb_w, thumb_h, Color::from_rgb(0x88, 0x88, 0x88));
}
}
}
static void terminal_on_mouse(Window* win, MouseEvent& ev) {
TermTabState* tts = (TermTabState*)win->app_data;
if (!tts || tts->tab_count == 0) return;
// Scroll wheel: forward to active tab regardless of mouse position
TerminalState* ts = tts->tabs[tts->active_tab];
if (ev.scroll != 0 && !ts->alt_screen_active) {
ts->view_offset += (ev.scroll < 0) ? 3 : -3;
if (ts->view_offset < 0) ts->view_offset = 0;
if (ts->view_offset > ts->scrollback_lines)
ts->view_offset = ts->scrollback_lines;
ts->dirty = true;
}
// Tab bar click handling
if (!ev.left_pressed()) return;
Rect cr = win->content_rect();
int lx = ev.x - cr.x;
int ly = ev.y - cr.y;
if (ly >= TERM_TAB_BAR_H) return; // click in terminal area, ignore
int tab_x = term_tabs_origin(cr.w, tts->tab_count);
for (int i = 0; i < tts->tab_count; i++) {
if (lx >= tab_x && lx < tab_x + TERM_TAB_W) {
// Check if click is on the close "x" (last 24px of tab)
if (lx >= tab_x + TERM_TAB_W - 24) {
term_close_tab(tts, win, i);
} else if (i != tts->active_tab) {
tts->active_tab = i;
tts->tabs[i]->dirty = true;
}
return;
}
tab_x += TERM_TAB_W + TERM_TAB_GAP;
}
// "+" button pinned to right edge
int px = cr.w - TERM_PLUS_PAD - TERM_PLUS_W;
if (tts->tab_count < TERM_MAX_TABS && lx >= px && lx < px + TERM_PLUS_W) {
int term_h = cr.h - TERM_TAB_BAR_H;
int cols = cr.w / mono_cell_width();
int rows = term_h / mono_cell_height();
if (cols < 1) cols = 1;
if (rows < 1) rows = 1;
term_add_tab(tts, cols, rows);
}
}
static void terminal_on_key(Window* win, const Montauk::KeyEvent& key) {
TermTabState* tts = (TermTabState*)win->app_data;
if (!tts || tts->tab_count == 0) return;
// Ctrl+T: new tab (scancode 0x14 = 'T')
if (key.ctrl && key.pressed && key.scancode == 0x14) {
if (tts->tab_count < TERM_MAX_TABS) {
Rect cr = win->content_rect();
int term_h = cr.h - TERM_TAB_BAR_H;
int cols = cr.w / mono_cell_width();
int rows = term_h / mono_cell_height();
if (cols < 1) cols = 1;
if (rows < 1) rows = 1;
term_add_tab(tts, cols, rows);
}
return;
}
// Ctrl+W: close current tab (scancode 0x11 = 'W')
if (key.ctrl && key.pressed && key.scancode == 0x11) {
term_close_tab(tts, win, tts->active_tab);
return;
}
// Forward all other keys to active tab
TerminalState* ts = tts->tabs[tts->active_tab];
terminal_handle_key(ts, key);
}
static void terminal_on_close(Window* win) {
TermTabState* tts = (TermTabState*)win->app_data;
if (tts) {
for (int i = 0; i < tts->tab_count; i++)
term_free_tab(tts->tabs[i]);
montauk::mfree(tts);
win->app_data = nullptr; win->app_data = nullptr;
} }
} }
static void terminal_on_poll(Window* win) { static void terminal_on_poll(Window* win) {
TerminalState* ts = (TerminalState*)win->app_data; TermTabState* tts = (TermTabState*)win->app_data;
if (ts) terminal_poll(ts); if (!tts) return;
// Poll ALL tabs — background shells still produce output
for (int i = tts->tab_count - 1; i >= 0; i--) {
if (!terminal_poll(tts->tabs[i])) {
// This tab's shell exited
term_close_tab(tts, win, i);
// term_close_tab may have closed the window (last tab).
// desktop_close_window calls on_close which nulls app_data
// before the window slot is recycled, so win is still valid here.
if (!win->app_data) return;
}
}
} }
// ============================================================================ // ============================================================================
@@ -62,14 +333,22 @@ void open_terminal(DesktopState* ds) {
Window* win = &ds->windows[idx]; Window* win = &ds->windows[idx];
Rect cr = win->content_rect(); Rect cr = win->content_rect();
int term_h = cr.h - TERM_TAB_BAR_H;
int cols = cr.w / mono_cell_width(); int cols = cr.w / mono_cell_width();
int rows = cr.h / mono_cell_height(); int rows = term_h / mono_cell_height();
if (cols < 1) cols = 1;
if (rows < 1) rows = 1;
TerminalState* ts = (TerminalState*)montauk::malloc(sizeof(TerminalState)); TermTabState* tts = (TermTabState*)montauk::malloc(sizeof(TermTabState));
montauk::memset(ts, 0, sizeof(TerminalState)); montauk::memset(tts, 0, sizeof(TermTabState));
terminal_init(ts, cols, rows); tts->desktop = ds;
tts->tabs[0] = term_create_tab(ds, cols, rows);
tts->tab_count = 1;
tts->active_tab = 0;
tts->prev_active_tab = 0;
tts->last_content = win->content;
win->app_data = ts; win->app_data = tts;
win->on_draw = terminal_on_draw; win->on_draw = terminal_on_draw;
win->on_mouse = terminal_on_mouse; win->on_mouse = terminal_on_mouse;
win->on_key = terminal_on_key; win->on_key = terminal_on_key;