feat: make terminal and klog Window Server apps
This commit is contained in:
@@ -1,201 +1,13 @@
|
||||
/*
|
||||
* app_klog.cpp
|
||||
* MontaukOS Desktop - Kernel Log viewer (tails the kernel ring buffer)
|
||||
* MontaukOS Desktop - Kernel Log launcher
|
||||
* Spawns standalone klog.elf Window Server app
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include "apps_common.hpp"
|
||||
|
||||
// ============================================================================
|
||||
// Kernel Log state
|
||||
// ============================================================================
|
||||
|
||||
static constexpr int KLOG_READ_SIZE = 65536; // matches kernel ring buffer size
|
||||
static constexpr int KLOG_POLL_MS = 250;
|
||||
|
||||
struct KlogState {
|
||||
TerminalState term;
|
||||
char* klog_buf;
|
||||
int last_len;
|
||||
char last_tail_byte;
|
||||
uint64_t last_poll_ms;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Re-feed log into the terminal (fills scrollback + visible screen)
|
||||
// ============================================================================
|
||||
|
||||
static void klog_refeed(KlogState* klog, int n) {
|
||||
// Find enough lines to fill scrollback + visible screen
|
||||
int lines_needed = klog->term.rows + klog->term.max_scrollback;
|
||||
int start = 0;
|
||||
int line_count = 0;
|
||||
for (int i = n - 1; i >= 0; i--) {
|
||||
if (klog->klog_buf[i] == '\n') {
|
||||
line_count++;
|
||||
if (line_count >= lines_needed) { start = i + 1; break; }
|
||||
}
|
||||
}
|
||||
|
||||
// Reset terminal state
|
||||
klog->term.scrollback_lines = 0;
|
||||
klog->term.view_offset = 0;
|
||||
klog->term.cursor_x = 0;
|
||||
klog->term.cursor_y = 0;
|
||||
klog->term.current_fg = colors::TERM_FG;
|
||||
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);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Callbacks
|
||||
// ============================================================================
|
||||
|
||||
static void klog_on_draw(Window* win, Framebuffer& fb) {
|
||||
KlogState* klog = (KlogState*)win->app_data;
|
||||
if (!klog) return;
|
||||
|
||||
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);
|
||||
|
||||
// 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) {
|
||||
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) {
|
||||
// Read-only viewer — no keyboard input
|
||||
}
|
||||
|
||||
static void klog_on_poll(Window* win) {
|
||||
KlogState* klog = (KlogState*)win->app_data;
|
||||
if (!klog) return;
|
||||
|
||||
uint64_t now = montauk::get_milliseconds();
|
||||
if (now - klog->last_poll_ms < KLOG_POLL_MS) return;
|
||||
klog->last_poll_ms = now;
|
||||
|
||||
int n = (int)montauk::read_klog(klog->klog_buf, KLOG_READ_SIZE);
|
||||
if (n <= 0 && klog->last_len <= 0) return;
|
||||
|
||||
if (n > klog->last_len) {
|
||||
// Buffer grew — feed only the new portion
|
||||
terminal_feed(&klog->term, klog->klog_buf + klog->last_len, n - klog->last_len);
|
||||
} else if (n == klog->last_len && n > 0) {
|
||||
// Same size — check if the tail changed (ring buffer wrapped)
|
||||
if (klog->klog_buf[n - 1] != klog->last_tail_byte) {
|
||||
klog_refeed(klog, n);
|
||||
} else {
|
||||
return; // nothing changed
|
||||
}
|
||||
} else if (n < klog->last_len) {
|
||||
// Unexpected shrink — full re-render
|
||||
klog_refeed(klog, n);
|
||||
}
|
||||
|
||||
klog->last_len = n;
|
||||
if (n > 0) klog->last_tail_byte = klog->klog_buf[n - 1];
|
||||
win->dirty = true;
|
||||
}
|
||||
|
||||
static void klog_on_close(Window* win) {
|
||||
KlogState* klog = (KlogState*)win->app_data;
|
||||
if (klog) {
|
||||
if (klog->term.cells) montauk::free(klog->term.cells);
|
||||
if (klog->term.alt_cells) montauk::free(klog->term.alt_cells);
|
||||
if (klog->klog_buf) montauk::mfree(klog->klog_buf);
|
||||
montauk::mfree(klog);
|
||||
win->app_data = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Kernel Log launcher
|
||||
// ============================================================================
|
||||
|
||||
void open_klog(DesktopState* ds) {
|
||||
int idx = desktop_create_window(ds, "Kernel Log", 160, 60, 720, 480);
|
||||
if (idx < 0) return;
|
||||
|
||||
Window* win = &ds->windows[idx];
|
||||
Rect cr = win->content_rect();
|
||||
int cols = cr.w / mono_cell_width();
|
||||
int rows = cr.h / mono_cell_height();
|
||||
|
||||
KlogState* klog = (KlogState*)montauk::malloc(sizeof(KlogState));
|
||||
montauk::memset(klog, 0, sizeof(KlogState));
|
||||
|
||||
// Initialize with scrollback enabled
|
||||
terminal_init_cells(&klog->term, cols, rows, TERM_MAX_SCROLLBACK);
|
||||
|
||||
// Allocate klog read buffer
|
||||
klog->klog_buf = (char*)montauk::malloc(KLOG_READ_SIZE);
|
||||
klog->last_len = 0;
|
||||
klog->last_tail_byte = 0;
|
||||
klog->last_poll_ms = 0;
|
||||
|
||||
// Do an initial read to show existing log content
|
||||
int n = (int)montauk::read_klog(klog->klog_buf, KLOG_READ_SIZE);
|
||||
if (n > 0) {
|
||||
klog_refeed(klog, n);
|
||||
klog->last_len = n;
|
||||
klog->last_tail_byte = klog->klog_buf[n - 1];
|
||||
}
|
||||
|
||||
win->app_data = klog;
|
||||
win->on_draw = klog_on_draw;
|
||||
win->on_mouse = klog_on_mouse;
|
||||
win->on_key = klog_on_key;
|
||||
win->on_close = klog_on_close;
|
||||
win->on_poll = klog_on_poll;
|
||||
(void)ds;
|
||||
montauk::spawn("0:/apps/klog/klog.elf");
|
||||
}
|
||||
|
||||
@@ -1,358 +1,16 @@
|
||||
/*
|
||||
* app_terminal.cpp
|
||||
* MontaukOS Desktop
|
||||
* MontaukOS Desktop - Terminal launcher
|
||||
* Spawns standalone terminal.elf Window Server app
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include "apps_common.hpp"
|
||||
|
||||
// ============================================================================
|
||||
// Tab data model
|
||||
// ============================================================================
|
||||
|
||||
static constexpr int TERM_TAB_BAR_H = 34;
|
||||
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;
|
||||
|
||||
static constexpr int TERM_TAB_PAD = 8;
|
||||
|
||||
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->child_pid > 0) montauk::kill(ts->child_pid);
|
||||
if (ts->cells) montauk::free(ts->cells);
|
||||
if (ts->alt_cells) montauk::free(ts->alt_cells);
|
||||
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;
|
||||
if (tts->active_tab < 0 || tts->active_tab >= tts->tab_count)
|
||||
tts->active_tab = 0;
|
||||
|
||||
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_TAB_PAD;
|
||||
|
||||
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_TAB_PAD;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
static void terminal_on_poll(Window* win) {
|
||||
TermTabState* tts = (TermTabState*)win->app_data;
|
||||
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);
|
||||
win->dirty = true;
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
if (tts->tab_count > 0 && tts->tabs[tts->active_tab]->dirty) {
|
||||
win->dirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Terminal launcher
|
||||
// ============================================================================
|
||||
|
||||
void open_terminal(DesktopState* ds) {
|
||||
int idx = desktop_create_window(ds, "Terminal", 200, 80, 648, 480);
|
||||
if (idx < 0) return;
|
||||
|
||||
Window* win = &ds->windows[idx];
|
||||
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;
|
||||
|
||||
TermTabState* tts = (TermTabState*)montauk::malloc(sizeof(TermTabState));
|
||||
montauk::memset(tts, 0, sizeof(TermTabState));
|
||||
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 = tts;
|
||||
win->on_draw = terminal_on_draw;
|
||||
win->on_mouse = terminal_on_mouse;
|
||||
win->on_key = terminal_on_key;
|
||||
win->on_close = terminal_on_close;
|
||||
win->on_poll = terminal_on_poll;
|
||||
if (ds && ds->home_dir[0] != '\0') {
|
||||
montauk::spawn("0:/apps/terminal/terminal.elf", ds->home_dir);
|
||||
} else {
|
||||
montauk::spawn("0:/apps/terminal/terminal.elf");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,11 +116,9 @@ struct EmbeddedAppDef {
|
||||
};
|
||||
|
||||
static const EmbeddedAppDef embedded_apps[] = {
|
||||
{ "Terminal", 0, 0 },
|
||||
{ "Files", 1, 0 },
|
||||
{ "Calculator", 3, 0 },
|
||||
{ "System Info", 2, 2 },
|
||||
{ "Kernel Log", 5, 2 },
|
||||
{ "Processes", 6, 2 },
|
||||
{ "Mandelbrot", 7, 3 },
|
||||
};
|
||||
@@ -130,11 +128,9 @@ static constexpr int NUM_EMBEDDED = sizeof(embedded_apps) / sizeof(embedded_apps
|
||||
// Resolve embedded app_id to an icon pointer in DesktopState
|
||||
static SvgIcon* icon_for_embedded(DesktopState* ds, int app_id) {
|
||||
switch (app_id) {
|
||||
case 0: return &ds->icon_terminal;
|
||||
case 1: return &ds->icon_filemanager;
|
||||
case 2: return &ds->icon_sysinfo;
|
||||
case 3: return &ds->icon_calculator;
|
||||
case 5: return &ds->icon_terminal; // Kernel Log uses terminal icon
|
||||
case 6: return &ds->icon_procmgr;
|
||||
case 7: return &ds->icon_mandelbrot;
|
||||
default: return nullptr;
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
# Makefile for klog (standalone Window Server app) on MontaukOS
|
||||
# Copyright (c) 2026 Daniel Hammer
|
||||
|
||||
MAKEFLAGS += -rR
|
||||
.SUFFIXES:
|
||||
|
||||
TOOLCHAIN_PREFIX := $(shell cd ../../.. && pwd)/toolchain/local/bin/x86_64-elf-
|
||||
ifneq ($(wildcard $(TOOLCHAIN_PREFIX)gcc),)
|
||||
CXX := $(TOOLCHAIN_PREFIX)g++
|
||||
else
|
||||
CXX := g++
|
||||
endif
|
||||
|
||||
PROG_INC := ../../include
|
||||
LINK_LD := ../../link.ld
|
||||
BINDIR := ../../bin
|
||||
OBJDIR := obj
|
||||
LIBDIR := ../../lib
|
||||
|
||||
CXXFLAGS := \
|
||||
-std=gnu++20 \
|
||||
-g -O2 -pipe \
|
||||
-Wall \
|
||||
-Wextra \
|
||||
-Wno-unused-parameter \
|
||||
-Wno-unused-function \
|
||||
-nostdinc \
|
||||
-ffreestanding \
|
||||
-fno-stack-protector \
|
||||
-fno-stack-check \
|
||||
-fno-PIC \
|
||||
-fno-rtti \
|
||||
-fno-exceptions \
|
||||
-ffunction-sections \
|
||||
-fdata-sections \
|
||||
-m64 \
|
||||
-march=x86-64 \
|
||||
-msse \
|
||||
-msse2 \
|
||||
-mno-red-zone \
|
||||
-mcmodel=small \
|
||||
-I $(PROG_INC) \
|
||||
-isystem $(PROG_INC)/libc \
|
||||
-isystem ../../../kernel/freestnd-c-hdrs/x86_64/include \
|
||||
-isystem ../../../kernel/freestnd-cxx-hdrs/x86_64/include
|
||||
|
||||
LDFLAGS := \
|
||||
-nostdlib \
|
||||
-static \
|
||||
-Wl,--build-id=none \
|
||||
-Wl,--gc-sections \
|
||||
-Wl,-m,elf_x86_64 \
|
||||
-z max-page-size=0x1000 \
|
||||
-T $(LINK_LD)
|
||||
|
||||
SRCS := main.cpp stb_truetype_impl.cpp font_data.cpp
|
||||
OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o))
|
||||
|
||||
TARGET := $(BINDIR)/apps/klog/klog.elf
|
||||
|
||||
.PHONY: all clean
|
||||
|
||||
all: $(TARGET)
|
||||
|
||||
$(TARGET): $(OBJS) $(LINK_LD) Makefile
|
||||
mkdir -p $(BINDIR)/apps/klog
|
||||
$(CXX) $(CXXFLAGS) $(LDFLAGS) $(OBJS) $(LIBDIR)/libc/liblibc.a -o $@
|
||||
|
||||
$(OBJDIR)/%.o: %.cpp Makefile
|
||||
mkdir -p $(OBJDIR)
|
||||
$(CXX) $(CXXFLAGS) -c $< -o $@
|
||||
|
||||
clean:
|
||||
rm -rf $(OBJDIR) $(TARGET)
|
||||
@@ -0,0 +1 @@
|
||||
#include "../desktop/font_data.cpp"
|
||||
@@ -0,0 +1,221 @@
|
||||
/*
|
||||
* main.cpp
|
||||
* MontaukOS Kernel Log - standalone Window Server app
|
||||
* Preserves the old desktop-integrated log viewer layout
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include <montauk/syscall.h>
|
||||
#include <montauk/string.h>
|
||||
#include <montauk/heap.h>
|
||||
#include <gui/gui.hpp>
|
||||
#include <gui/canvas.hpp>
|
||||
#include <gui/standalone.hpp>
|
||||
#include <gui/terminal.hpp>
|
||||
#include <gui/truetype.hpp>
|
||||
|
||||
using namespace gui;
|
||||
|
||||
static constexpr int INIT_W = 718;
|
||||
static constexpr int INIT_H = 449;
|
||||
|
||||
static constexpr int KLOG_READ_SIZE = 65536;
|
||||
static constexpr int KLOG_POLL_MS = 250;
|
||||
|
||||
struct KlogState {
|
||||
TerminalState term;
|
||||
char* klog_buf;
|
||||
int last_len;
|
||||
char last_tail_byte;
|
||||
uint64_t last_poll_ms;
|
||||
uint32_t* last_pixels;
|
||||
};
|
||||
|
||||
static WsWindow g_win;
|
||||
static KlogState g_klog = {};
|
||||
|
||||
static void klog_refeed(int n) {
|
||||
int lines_needed = g_klog.term.rows + g_klog.term.max_scrollback;
|
||||
int start = 0;
|
||||
int line_count = 0;
|
||||
for (int i = n - 1; i >= 0; i--) {
|
||||
if (g_klog.klog_buf[i] == '\n') {
|
||||
line_count++;
|
||||
if (line_count >= lines_needed) {
|
||||
start = i + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
g_klog.term.scrollback_lines = 0;
|
||||
g_klog.term.view_offset = 0;
|
||||
g_klog.term.cursor_x = 0;
|
||||
g_klog.term.cursor_y = 0;
|
||||
g_klog.term.current_fg = colors::TERM_FG;
|
||||
g_klog.term.current_bg = colors::TERM_BG;
|
||||
|
||||
TermCell* screen = term_screen_row(&g_klog.term, 0);
|
||||
int total = g_klog.term.cols * g_klog.term.rows;
|
||||
for (int i = 0; i < total; i++)
|
||||
screen[i] = {' ', colors::TERM_FG, colors::TERM_BG};
|
||||
|
||||
terminal_feed(&g_klog.term, g_klog.klog_buf + start, n - start);
|
||||
}
|
||||
|
||||
static bool klog_poll() {
|
||||
uint64_t now = montauk::get_milliseconds();
|
||||
if (now - g_klog.last_poll_ms < KLOG_POLL_MS) return false;
|
||||
g_klog.last_poll_ms = now;
|
||||
|
||||
int n = (int)montauk::read_klog(g_klog.klog_buf, KLOG_READ_SIZE);
|
||||
if (n <= 0 && g_klog.last_len <= 0) return false;
|
||||
|
||||
if (n > g_klog.last_len) {
|
||||
terminal_feed(&g_klog.term, g_klog.klog_buf + g_klog.last_len, n - g_klog.last_len);
|
||||
} else if (n == g_klog.last_len && n > 0) {
|
||||
if (g_klog.klog_buf[n - 1] != g_klog.last_tail_byte) {
|
||||
klog_refeed(n);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} else if (n < g_klog.last_len) {
|
||||
klog_refeed(n);
|
||||
}
|
||||
|
||||
g_klog.last_len = n;
|
||||
if (n > 0) g_klog.last_tail_byte = g_klog.klog_buf[n - 1];
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool klog_render() {
|
||||
if (!g_win.pixels || !g_klog.term.cells) return false;
|
||||
|
||||
int new_cols = g_win.width / mono_cell_width();
|
||||
int new_rows = g_win.height / mono_cell_height();
|
||||
if (new_cols < 1) new_cols = 1;
|
||||
if (new_rows < 1) new_rows = 1;
|
||||
|
||||
if (new_cols != g_klog.term.cols || new_rows != g_klog.term.rows) {
|
||||
terminal_resize(&g_klog.term, new_cols, new_rows);
|
||||
if (g_klog.last_len > 0)
|
||||
klog_refeed(g_klog.last_len);
|
||||
}
|
||||
|
||||
if (g_win.pixels != g_klog.last_pixels) {
|
||||
g_klog.last_pixels = g_win.pixels;
|
||||
g_klog.term.dirty = true;
|
||||
}
|
||||
|
||||
if (!g_klog.term.dirty) return false;
|
||||
|
||||
bool was_dirty = g_klog.term.dirty;
|
||||
terminal_render(&g_klog.term, g_win.pixels, g_win.width, g_win.height);
|
||||
|
||||
if (was_dirty && g_klog.term.scrollback_lines > 0) {
|
||||
int cell_h = mono_cell_height();
|
||||
int total_rows = g_klog.term.scrollback_lines + g_klog.term.rows;
|
||||
int total_h = total_rows * cell_h;
|
||||
int view_h = g_win.height;
|
||||
if (total_h > view_h) {
|
||||
Canvas c = g_win.canvas();
|
||||
int sb_w = 4;
|
||||
int sb_x = g_win.width - sb_w - 2;
|
||||
int thumb_h = (view_h * view_h) / total_h;
|
||||
if (thumb_h < 20) thumb_h = 20;
|
||||
int scroll_from_top = g_klog.term.scrollback_lines - g_klog.term.view_offset;
|
||||
int max_scroll = g_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));
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static void klog_handle_mouse(const Montauk::WinEvent& ev) {
|
||||
if (ev.mouse.scroll == 0) return;
|
||||
|
||||
g_klog.term.view_offset += (ev.mouse.scroll < 0) ? 3 : -3;
|
||||
if (g_klog.term.view_offset < 0) g_klog.term.view_offset = 0;
|
||||
if (g_klog.term.view_offset > g_klog.term.scrollback_lines)
|
||||
g_klog.term.view_offset = g_klog.term.scrollback_lines;
|
||||
g_klog.term.dirty = true;
|
||||
}
|
||||
|
||||
static void klog_cleanup() {
|
||||
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.klog_buf) montauk::mfree(g_klog.klog_buf);
|
||||
montauk::memset(&g_klog, 0, sizeof(g_klog));
|
||||
}
|
||||
|
||||
extern "C" void _start() {
|
||||
if (!fonts::init())
|
||||
montauk::exit(1);
|
||||
|
||||
if (!g_win.create("Kernel Log", INIT_W, INIT_H))
|
||||
montauk::exit(1);
|
||||
|
||||
int cols = g_win.width / mono_cell_width();
|
||||
int rows = g_win.height / mono_cell_height();
|
||||
if (cols < 1) cols = 1;
|
||||
if (rows < 1) rows = 1;
|
||||
|
||||
terminal_init_cells(&g_klog.term, cols, rows, TERM_MAX_SCROLLBACK);
|
||||
if (!g_klog.term.cells || !g_klog.term.alt_cells) {
|
||||
g_win.destroy();
|
||||
montauk::exit(1);
|
||||
}
|
||||
|
||||
g_klog.klog_buf = (char*)montauk::malloc(KLOG_READ_SIZE);
|
||||
if (!g_klog.klog_buf) {
|
||||
klog_cleanup();
|
||||
g_win.destroy();
|
||||
montauk::exit(1);
|
||||
}
|
||||
|
||||
g_klog.last_pixels = g_win.pixels;
|
||||
|
||||
int n = (int)montauk::read_klog(g_klog.klog_buf, KLOG_READ_SIZE);
|
||||
if (n > 0) {
|
||||
klog_refeed(n);
|
||||
g_klog.last_len = n;
|
||||
g_klog.last_tail_byte = g_klog.klog_buf[n - 1];
|
||||
}
|
||||
|
||||
if (klog_render())
|
||||
g_win.present();
|
||||
|
||||
while (true) {
|
||||
bool redraw = klog_poll();
|
||||
|
||||
Montauk::WinEvent ev;
|
||||
int r = g_win.poll(&ev);
|
||||
if (r < 0) break;
|
||||
|
||||
if (r == 0) {
|
||||
if (redraw && klog_render())
|
||||
g_win.present();
|
||||
montauk::sleep_ms(16);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ev.type == 3) break;
|
||||
|
||||
if (ev.type == 1) {
|
||||
klog_handle_mouse(ev);
|
||||
} else if (ev.type == 2 || ev.type == 4) {
|
||||
g_klog.term.dirty = true;
|
||||
}
|
||||
|
||||
if (klog_render())
|
||||
g_win.present();
|
||||
}
|
||||
|
||||
klog_cleanup();
|
||||
g_win.destroy();
|
||||
montauk::exit(0);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
[app]
|
||||
name = "Kernel Log"
|
||||
binary = "klog.elf"
|
||||
icon = "utilities-terminal.svg"
|
||||
|
||||
[menu]
|
||||
category = "System"
|
||||
visible = true
|
||||
|
||||
[launch]
|
||||
pass_home_dir = false
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* stb_truetype_impl.cpp
|
||||
* Single compilation unit for stb_truetype in MontaukOS freestanding environment
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
#include <montauk/heap.h>
|
||||
#include <montauk/string.h>
|
||||
#include <gui/stb_math.h>
|
||||
|
||||
#define STBTT_ifloor(x) ((int) stb_floor(x))
|
||||
#define STBTT_iceil(x) ((int) stb_ceil(x))
|
||||
#define STBTT_sqrt(x) stb_sqrt(x)
|
||||
#define STBTT_pow(x,y) stb_pow(x,y)
|
||||
#define STBTT_fmod(x,y) stb_fmod(x,y)
|
||||
#define STBTT_cos(x) stb_cos(x)
|
||||
#define STBTT_acos(x) stb_acos(x)
|
||||
#define STBTT_fabs(x) stb_fabs(x)
|
||||
|
||||
#define STBTT_malloc(x,u) ((void)(u), montauk::malloc(x))
|
||||
#define STBTT_free(x,u) ((void)(u), montauk::mfree(x))
|
||||
|
||||
#define STBTT_memcpy(d,s,n) montauk::memcpy(d,s,n)
|
||||
#define STBTT_memset(d,v,n) montauk::memset(d,v,n)
|
||||
|
||||
#define STBTT_strlen(x) montauk::slen(x)
|
||||
|
||||
#define STBTT_assert(x) ((void)(x))
|
||||
|
||||
#define STB_TRUETYPE_IMPLEMENTATION
|
||||
#include <gui/stb_truetype.h>
|
||||
@@ -0,0 +1,74 @@
|
||||
# Makefile for terminal (standalone Window Server app) on MontaukOS
|
||||
# Copyright (c) 2026 Daniel Hammer
|
||||
|
||||
MAKEFLAGS += -rR
|
||||
.SUFFIXES:
|
||||
|
||||
TOOLCHAIN_PREFIX := $(shell cd ../../.. && pwd)/toolchain/local/bin/x86_64-elf-
|
||||
ifneq ($(wildcard $(TOOLCHAIN_PREFIX)gcc),)
|
||||
CXX := $(TOOLCHAIN_PREFIX)g++
|
||||
else
|
||||
CXX := g++
|
||||
endif
|
||||
|
||||
PROG_INC := ../../include
|
||||
LINK_LD := ../../link.ld
|
||||
BINDIR := ../../bin
|
||||
OBJDIR := obj
|
||||
LIBDIR := ../../lib
|
||||
|
||||
CXXFLAGS := \
|
||||
-std=gnu++20 \
|
||||
-g -O2 -pipe \
|
||||
-Wall \
|
||||
-Wextra \
|
||||
-Wno-unused-parameter \
|
||||
-Wno-unused-function \
|
||||
-nostdinc \
|
||||
-ffreestanding \
|
||||
-fno-stack-protector \
|
||||
-fno-stack-check \
|
||||
-fno-PIC \
|
||||
-fno-rtti \
|
||||
-fno-exceptions \
|
||||
-ffunction-sections \
|
||||
-fdata-sections \
|
||||
-m64 \
|
||||
-march=x86-64 \
|
||||
-msse \
|
||||
-msse2 \
|
||||
-mno-red-zone \
|
||||
-mcmodel=small \
|
||||
-I $(PROG_INC) \
|
||||
-isystem $(PROG_INC)/libc \
|
||||
-isystem ../../../kernel/freestnd-c-hdrs/x86_64/include \
|
||||
-isystem ../../../kernel/freestnd-cxx-hdrs/x86_64/include
|
||||
|
||||
LDFLAGS := \
|
||||
-nostdlib \
|
||||
-static \
|
||||
-Wl,--build-id=none \
|
||||
-Wl,--gc-sections \
|
||||
-Wl,-m,elf_x86_64 \
|
||||
-z max-page-size=0x1000 \
|
||||
-T $(LINK_LD)
|
||||
|
||||
SRCS := main.cpp stb_truetype_impl.cpp font_data.cpp
|
||||
OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o))
|
||||
|
||||
TARGET := $(BINDIR)/apps/terminal/terminal.elf
|
||||
|
||||
.PHONY: all clean
|
||||
|
||||
all: $(TARGET)
|
||||
|
||||
$(TARGET): $(OBJS) $(LINK_LD) Makefile
|
||||
mkdir -p $(BINDIR)/apps/terminal
|
||||
$(CXX) $(CXXFLAGS) $(LDFLAGS) $(OBJS) $(LIBDIR)/libc/liblibc.a -o $@
|
||||
|
||||
$(OBJDIR)/%.o: %.cpp Makefile
|
||||
mkdir -p $(OBJDIR)
|
||||
$(CXX) $(CXXFLAGS) -c $< -o $@
|
||||
|
||||
clean:
|
||||
rm -rf $(OBJDIR) $(TARGET)
|
||||
@@ -0,0 +1 @@
|
||||
#include "../desktop/font_data.cpp"
|
||||
@@ -0,0 +1,371 @@
|
||||
/*
|
||||
* main.cpp
|
||||
* MontaukOS Terminal - standalone Window Server app
|
||||
* Preserves the old desktop-integrated terminal layout and tab behavior
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include <montauk/syscall.h>
|
||||
#include <montauk/string.h>
|
||||
#include <montauk/heap.h>
|
||||
#include <gui/gui.hpp>
|
||||
#include <gui/canvas.hpp>
|
||||
#include <gui/standalone.hpp>
|
||||
#include <gui/terminal.hpp>
|
||||
#include <gui/truetype.hpp>
|
||||
|
||||
extern "C" {
|
||||
#include <stdio.h>
|
||||
}
|
||||
|
||||
using namespace gui;
|
||||
|
||||
static constexpr int INIT_W = 646;
|
||||
static constexpr int INIT_H = 449;
|
||||
|
||||
static constexpr int TERM_TAB_BAR_H = 34;
|
||||
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;
|
||||
static constexpr int TERM_TAB_PAD = 8;
|
||||
|
||||
struct TermTabs {
|
||||
TerminalState* tabs[TERM_MAX_TABS];
|
||||
int tab_count;
|
||||
int active_tab;
|
||||
int prev_active_tab;
|
||||
uint32_t* last_pixels;
|
||||
};
|
||||
|
||||
static WsWindow g_win;
|
||||
static TermTabs g_tabs = {};
|
||||
static bool g_should_exit = false;
|
||||
static bool g_force_redraw = true;
|
||||
|
||||
static bool left_pressed(const Montauk::WinEvent& ev) {
|
||||
return (ev.mouse.buttons & 1) && !(ev.mouse.prev_buttons & 1);
|
||||
}
|
||||
|
||||
static void term_request_redraw() {
|
||||
g_force_redraw = true;
|
||||
}
|
||||
|
||||
static void term_free_tab(TerminalState* ts) {
|
||||
if (!ts) return;
|
||||
if (ts->child_pid > 0) montauk::kill(ts->child_pid);
|
||||
if (ts->cells) montauk::free(ts->cells);
|
||||
if (ts->alt_cells) montauk::free(ts->alt_cells);
|
||||
montauk::mfree(ts);
|
||||
}
|
||||
|
||||
static TerminalState* term_create_tab(int cols, int rows) {
|
||||
TerminalState* ts = (TerminalState*)montauk::malloc(sizeof(TerminalState));
|
||||
if (!ts) return nullptr;
|
||||
montauk::memset(ts, 0, sizeof(TerminalState));
|
||||
terminal_init(ts, cols, rows);
|
||||
if (!ts->cells || !ts->alt_cells) {
|
||||
montauk::mfree(ts);
|
||||
return nullptr;
|
||||
}
|
||||
return ts;
|
||||
}
|
||||
|
||||
static void term_close_tab(int idx) {
|
||||
if (idx < 0 || idx >= g_tabs.tab_count) return;
|
||||
|
||||
term_free_tab(g_tabs.tabs[idx]);
|
||||
|
||||
for (int i = idx; i < g_tabs.tab_count - 1; i++)
|
||||
g_tabs.tabs[i] = g_tabs.tabs[i + 1];
|
||||
g_tabs.tab_count--;
|
||||
g_tabs.tabs[g_tabs.tab_count] = nullptr;
|
||||
|
||||
if (g_tabs.tab_count == 0) {
|
||||
g_should_exit = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (idx == g_tabs.active_tab) {
|
||||
if (g_tabs.active_tab > 0)
|
||||
g_tabs.active_tab--;
|
||||
} else if (g_tabs.active_tab > idx) {
|
||||
g_tabs.active_tab--;
|
||||
}
|
||||
|
||||
if (g_tabs.active_tab >= g_tabs.tab_count)
|
||||
g_tabs.active_tab = g_tabs.tab_count - 1;
|
||||
|
||||
g_tabs.tabs[g_tabs.active_tab]->dirty = true;
|
||||
term_request_redraw();
|
||||
}
|
||||
|
||||
static bool term_add_tab(int cols, int rows) {
|
||||
if (g_tabs.tab_count >= TERM_MAX_TABS) return false;
|
||||
|
||||
TerminalState* ts = term_create_tab(cols, rows);
|
||||
if (!ts) return false;
|
||||
|
||||
g_tabs.tabs[g_tabs.tab_count] = ts;
|
||||
g_tabs.active_tab = g_tabs.tab_count;
|
||||
g_tabs.tab_count++;
|
||||
term_request_redraw();
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool term_poll_tabs() {
|
||||
bool changed = false;
|
||||
|
||||
for (int i = g_tabs.tab_count - 1; i >= 0; i--) {
|
||||
if (!terminal_poll(g_tabs.tabs[i])) {
|
||||
term_close_tab(i);
|
||||
changed = true;
|
||||
if (g_should_exit) return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (g_tabs.tab_count > 0 && g_tabs.tabs[g_tabs.active_tab]->dirty)
|
||||
changed = true;
|
||||
|
||||
return changed || g_force_redraw;
|
||||
}
|
||||
|
||||
static bool term_render() {
|
||||
if (!g_win.pixels || g_tabs.tab_count <= 0) return false;
|
||||
if (g_tabs.active_tab < 0 || g_tabs.active_tab >= g_tabs.tab_count)
|
||||
g_tabs.active_tab = 0;
|
||||
|
||||
int term_h = g_win.height - TERM_TAB_BAR_H;
|
||||
if (term_h < 1) term_h = 1;
|
||||
|
||||
int new_cols = g_win.width / 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;
|
||||
|
||||
TerminalState* ts = g_tabs.tabs[g_tabs.active_tab];
|
||||
|
||||
if (new_cols != ts->cols || new_rows != ts->rows) {
|
||||
for (int i = 0; i < g_tabs.tab_count; i++)
|
||||
terminal_resize(g_tabs.tabs[i], new_cols, new_rows);
|
||||
}
|
||||
|
||||
if (g_tabs.active_tab != g_tabs.prev_active_tab) {
|
||||
g_tabs.tabs[g_tabs.active_tab]->dirty = true;
|
||||
g_tabs.prev_active_tab = g_tabs.active_tab;
|
||||
}
|
||||
|
||||
if (g_win.pixels != g_tabs.last_pixels) {
|
||||
g_tabs.tabs[g_tabs.active_tab]->dirty = true;
|
||||
g_tabs.last_pixels = g_win.pixels;
|
||||
}
|
||||
|
||||
ts = g_tabs.tabs[g_tabs.active_tab];
|
||||
if (!g_force_redraw && !ts->dirty) return false;
|
||||
|
||||
Canvas c = g_win.canvas();
|
||||
Color bar_bg = Color::from_hex(0x1C1C1C);
|
||||
c.fill_rect(0, 0, g_win.width, TERM_TAB_BAR_H, bar_bg);
|
||||
|
||||
int fh = system_font_height();
|
||||
int tab_x = TERM_TAB_PAD;
|
||||
|
||||
for (int i = 0; i < g_tabs.tab_count; i++) {
|
||||
bool active = (i == g_tabs.active_tab);
|
||||
char label[16];
|
||||
snprintf(label, sizeof(label), "Tab %d", i + 1);
|
||||
|
||||
if (active) {
|
||||
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 {
|
||||
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;
|
||||
}
|
||||
|
||||
if (g_tabs.tab_count < TERM_MAX_TABS) {
|
||||
int plus_h = 22;
|
||||
int py = 7;
|
||||
int px = g_win.width - 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(fonts::system_font, "+", fonts::UI_SIZE);
|
||||
c.text(px + (TERM_PLUS_W - pw_text) / 2, py + (plus_h - fh) / 2, "+",
|
||||
Color::from_hex(0x6E6E6E));
|
||||
}
|
||||
|
||||
uint32_t* term_pixels = g_win.pixels + TERM_TAB_BAR_H * g_win.width;
|
||||
terminal_render(ts, term_pixels, g_win.width, term_h);
|
||||
|
||||
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, g_win.width, term_h);
|
||||
int sb_w = 4;
|
||||
int sb_x = g_win.width - 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));
|
||||
}
|
||||
}
|
||||
|
||||
g_force_redraw = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
static void term_handle_mouse(const Montauk::WinEvent& ev) {
|
||||
if (g_tabs.tab_count <= 0) return;
|
||||
|
||||
TerminalState* ts = g_tabs.tabs[g_tabs.active_tab];
|
||||
if (ev.mouse.scroll != 0 && !ts->alt_screen_active) {
|
||||
ts->view_offset += (ev.mouse.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;
|
||||
}
|
||||
|
||||
if (!left_pressed(ev)) return;
|
||||
if (ev.mouse.y >= TERM_TAB_BAR_H) return;
|
||||
|
||||
int tab_x = TERM_TAB_PAD;
|
||||
for (int i = 0; i < g_tabs.tab_count; i++) {
|
||||
if (ev.mouse.x >= tab_x && ev.mouse.x < tab_x + TERM_TAB_W) {
|
||||
if (ev.mouse.x >= tab_x + TERM_TAB_W - 24) {
|
||||
term_close_tab(i);
|
||||
} else if (i != g_tabs.active_tab) {
|
||||
g_tabs.active_tab = i;
|
||||
g_tabs.tabs[i]->dirty = true;
|
||||
term_request_redraw();
|
||||
}
|
||||
return;
|
||||
}
|
||||
tab_x += TERM_TAB_W + TERM_TAB_GAP;
|
||||
}
|
||||
|
||||
int px = g_win.width - TERM_PLUS_PAD - TERM_PLUS_W;
|
||||
if (g_tabs.tab_count < TERM_MAX_TABS &&
|
||||
ev.mouse.x >= px && ev.mouse.x < px + TERM_PLUS_W) {
|
||||
int cols = g_win.width / mono_cell_width();
|
||||
int rows = (g_win.height - TERM_TAB_BAR_H) / mono_cell_height();
|
||||
if (cols < 1) cols = 1;
|
||||
if (rows < 1) rows = 1;
|
||||
term_add_tab(cols, rows);
|
||||
}
|
||||
}
|
||||
|
||||
static void term_handle_key(const Montauk::KeyEvent& key) {
|
||||
if (g_tabs.tab_count <= 0) return;
|
||||
|
||||
if (key.ctrl && key.pressed && key.scancode == 0x14) {
|
||||
if (g_tabs.tab_count < TERM_MAX_TABS) {
|
||||
int cols = g_win.width / mono_cell_width();
|
||||
int rows = (g_win.height - TERM_TAB_BAR_H) / mono_cell_height();
|
||||
if (cols < 1) cols = 1;
|
||||
if (rows < 1) rows = 1;
|
||||
term_add_tab(cols, rows);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (key.ctrl && key.pressed && key.scancode == 0x11) {
|
||||
term_close_tab(g_tabs.active_tab);
|
||||
return;
|
||||
}
|
||||
|
||||
terminal_handle_key(g_tabs.tabs[g_tabs.active_tab], key);
|
||||
}
|
||||
|
||||
static void term_cleanup() {
|
||||
for (int i = 0; i < g_tabs.tab_count; i++)
|
||||
term_free_tab(g_tabs.tabs[i]);
|
||||
g_tabs.tab_count = 0;
|
||||
}
|
||||
|
||||
extern "C" void _start() {
|
||||
if (!fonts::init())
|
||||
montauk::exit(1);
|
||||
|
||||
char args[256] = {};
|
||||
int arglen = montauk::getargs(args, sizeof(args));
|
||||
if (arglen > 0 && args[0])
|
||||
montauk::chdir(args);
|
||||
|
||||
if (!g_win.create("Terminal", INIT_W, INIT_H))
|
||||
montauk::exit(1);
|
||||
|
||||
int cols = g_win.width / mono_cell_width();
|
||||
int rows = (g_win.height - TERM_TAB_BAR_H) / mono_cell_height();
|
||||
if (cols < 1) cols = 1;
|
||||
if (rows < 1) rows = 1;
|
||||
|
||||
g_tabs.tabs[0] = term_create_tab(cols, rows);
|
||||
if (!g_tabs.tabs[0]) {
|
||||
g_win.destroy();
|
||||
montauk::exit(1);
|
||||
}
|
||||
g_tabs.tab_count = 1;
|
||||
g_tabs.active_tab = 0;
|
||||
g_tabs.prev_active_tab = -1;
|
||||
g_tabs.last_pixels = g_win.pixels;
|
||||
|
||||
if (term_render())
|
||||
g_win.present();
|
||||
|
||||
while (!g_should_exit) {
|
||||
bool redraw = term_poll_tabs();
|
||||
if (g_should_exit) break;
|
||||
|
||||
Montauk::WinEvent ev;
|
||||
int r = g_win.poll(&ev);
|
||||
if (r < 0) break;
|
||||
|
||||
if (r == 0) {
|
||||
if (redraw && term_render())
|
||||
g_win.present();
|
||||
montauk::sleep_ms(16);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ev.type == 3) break;
|
||||
|
||||
if (ev.type == 0) {
|
||||
term_handle_key(ev.key);
|
||||
} else if (ev.type == 1) {
|
||||
term_handle_mouse(ev);
|
||||
} else if (ev.type == 2 || ev.type == 4) {
|
||||
term_request_redraw();
|
||||
if (g_tabs.tab_count > 0)
|
||||
g_tabs.tabs[g_tabs.active_tab]->dirty = true;
|
||||
}
|
||||
|
||||
if (!g_should_exit && term_render())
|
||||
g_win.present();
|
||||
}
|
||||
|
||||
term_cleanup();
|
||||
g_win.destroy();
|
||||
montauk::exit(0);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
[app]
|
||||
name = "Terminal"
|
||||
binary = "terminal.elf"
|
||||
icon = "utilities-terminal.svg"
|
||||
|
||||
[menu]
|
||||
category = "Applications"
|
||||
visible = true
|
||||
|
||||
[launch]
|
||||
pass_home_dir = true
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* stb_truetype_impl.cpp
|
||||
* Single compilation unit for stb_truetype in MontaukOS freestanding environment
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
#include <montauk/heap.h>
|
||||
#include <montauk/string.h>
|
||||
#include <gui/stb_math.h>
|
||||
|
||||
#define STBTT_ifloor(x) ((int) stb_floor(x))
|
||||
#define STBTT_iceil(x) ((int) stb_ceil(x))
|
||||
#define STBTT_sqrt(x) stb_sqrt(x)
|
||||
#define STBTT_pow(x,y) stb_pow(x,y)
|
||||
#define STBTT_fmod(x,y) stb_fmod(x,y)
|
||||
#define STBTT_cos(x) stb_cos(x)
|
||||
#define STBTT_acos(x) stb_acos(x)
|
||||
#define STBTT_fabs(x) stb_fabs(x)
|
||||
|
||||
#define STBTT_malloc(x,u) ((void)(u), montauk::malloc(x))
|
||||
#define STBTT_free(x,u) ((void)(u), montauk::mfree(x))
|
||||
|
||||
#define STBTT_memcpy(d,s,n) montauk::memcpy(d,s,n)
|
||||
#define STBTT_memset(d,v,n) montauk::memset(d,v,n)
|
||||
|
||||
#define STBTT_strlen(x) montauk::slen(x)
|
||||
|
||||
#define STBTT_assert(x) ((void)(x))
|
||||
|
||||
#define STB_TRUETYPE_IMPLEMENTATION
|
||||
#include <gui/stb_truetype.h>
|
||||
Reference in New Issue
Block a user