feat: make terminal and klog Window Server apps
This commit is contained in:
+14
-4
@@ -59,7 +59,7 @@ BINDIR := bin
|
|||||||
PROGRAMS := $(notdir $(wildcard src/*))
|
PROGRAMS := $(notdir $(wildcard src/*))
|
||||||
|
|
||||||
# Programs with custom Makefiles (built separately).
|
# Programs with custom Makefiles (built separately).
|
||||||
CUSTOM_BUILDS := doom fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer volume music bluetooth desktop login shell rpgdemo paint tcc lua screenshot texteditor
|
CUSTOM_BUILDS := doom fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer volume music bluetooth terminal klog desktop login shell rpgdemo paint tcc lua screenshot texteditor
|
||||||
SYSTEM_PROGRAMS := $(filter-out $(CUSTOM_BUILDS),$(PROGRAMS))
|
SYSTEM_PROGRAMS := $(filter-out $(CUSTOM_BUILDS),$(PROGRAMS))
|
||||||
|
|
||||||
# Build targets: system programs go to bin/os/, apps go to bin/apps/<name>/.
|
# Build targets: system programs go to bin/os/, apps go to bin/apps/<name>/.
|
||||||
@@ -81,9 +81,9 @@ CA_CERTS := $(BINDIR)/etc/ca-certificates.crt
|
|||||||
# Common shared assets (wallpapers, etc.)
|
# Common shared assets (wallpapers, etc.)
|
||||||
COMMONKEEP := $(BINDIR)/common/.keep
|
COMMONKEEP := $(BINDIR)/common/.keep
|
||||||
|
|
||||||
.PHONY: all clean doom fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer volume music bluetooth login desktop shell rpgdemo paint tcc lua screenshot texteditor icons fonts bearssl libc tls libjpeg libjpegwrite install-apps
|
.PHONY: all clean doom fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer volume music bluetooth terminal klog login desktop shell rpgdemo paint tcc lua screenshot texteditor icons fonts bearssl libc tls libjpeg libjpegwrite install-apps
|
||||||
|
|
||||||
all: bearssl libc libjpeg libjpegwrite tls $(TARGETS) fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer volume music bluetooth doom rpgdemo paint tcc lua screenshot texteditor login desktop shell icons fonts install-apps $(MANDST) $(WWWDST) $(CA_CERTS) $(COMMONKEEP)
|
all: bearssl libc libjpeg libjpegwrite tls $(TARGETS) fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer volume music bluetooth terminal klog doom rpgdemo paint tcc lua screenshot texteditor login desktop shell icons fonts install-apps $(MANDST) $(WWWDST) $(CA_CERTS) $(COMMONKEEP)
|
||||||
|
|
||||||
# Build BearSSL static library (cross-compiled for freestanding x86_64).
|
# Build BearSSL static library (cross-compiled for freestanding x86_64).
|
||||||
BEARSSL_INCLUDES := -isystem $(shell cd .. && pwd)/kernel/freestnd-c-hdrs/x86_64/include -isystem $(abspath include/libc)
|
BEARSSL_INCLUDES := -isystem $(shell cd .. && pwd)/kernel/freestnd-c-hdrs/x86_64/include -isystem $(abspath include/libc)
|
||||||
@@ -172,6 +172,14 @@ music: libc
|
|||||||
bluetooth: libc
|
bluetooth: libc
|
||||||
$(MAKE) -C src/bluetooth
|
$(MAKE) -C src/bluetooth
|
||||||
|
|
||||||
|
# Build terminal standalone GUI tool (depends on libc).
|
||||||
|
terminal: libc
|
||||||
|
$(MAKE) -C src/terminal
|
||||||
|
|
||||||
|
# Build kernel log standalone GUI tool (depends on libc).
|
||||||
|
klog: libc
|
||||||
|
$(MAKE) -C src/klog
|
||||||
|
|
||||||
# Build login screen via its own Makefile (depends on bearssl and libc).
|
# Build login screen via its own Makefile (depends on bearssl and libc).
|
||||||
login: bearssl libc
|
login: bearssl libc
|
||||||
$(MAKE) -C src/login
|
$(MAKE) -C src/login
|
||||||
@@ -226,7 +234,7 @@ lua: libc
|
|||||||
$(MAKE) -C src/lua
|
$(MAKE) -C src/lua
|
||||||
|
|
||||||
# Install app bundles (manifests, icons, data files) into bin/apps/<name>/.
|
# Install app bundles (manifests, icons, data files) into bin/apps/<name>/.
|
||||||
install-apps: doom rpgdemo paint spreadsheet wordprocessor weather wikipedia imageviewer fontpreview pdfviewer disks devexplorer installer volume music bluetooth screenshot texteditor
|
install-apps: doom rpgdemo paint spreadsheet wordprocessor weather wikipedia imageviewer fontpreview pdfviewer disks devexplorer installer volume music bluetooth terminal klog screenshot texteditor
|
||||||
../scripts/install_apps.sh
|
../scripts/install_apps.sh
|
||||||
|
|
||||||
# Copy man pages into bin/man/ so mkramdisk.sh picks them up.
|
# Copy man pages into bin/man/ so mkramdisk.sh picks them up.
|
||||||
@@ -280,6 +288,8 @@ clean:
|
|||||||
$(MAKE) -C src/volume clean
|
$(MAKE) -C src/volume clean
|
||||||
$(MAKE) -C src/music clean
|
$(MAKE) -C src/music clean
|
||||||
$(MAKE) -C src/bluetooth clean
|
$(MAKE) -C src/bluetooth clean
|
||||||
|
$(MAKE) -C src/terminal clean
|
||||||
|
$(MAKE) -C src/klog clean
|
||||||
$(MAKE) -C src/paint clean
|
$(MAKE) -C src/paint clean
|
||||||
$(MAKE) -C src/rpgdemo clean
|
$(MAKE) -C src/rpgdemo clean
|
||||||
$(MAKE) -C src/screenshot clean
|
$(MAKE) -C src/screenshot clean
|
||||||
|
|||||||
@@ -1,201 +1,13 @@
|
|||||||
/*
|
/*
|
||||||
* app_klog.cpp
|
* 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
|
* Copyright (c) 2026 Daniel Hammer
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "apps_common.hpp"
|
#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) {
|
void open_klog(DesktopState* ds) {
|
||||||
int idx = desktop_create_window(ds, "Kernel Log", 160, 60, 720, 480);
|
(void)ds;
|
||||||
if (idx < 0) return;
|
montauk::spawn("0:/apps/klog/klog.elf");
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,358 +1,16 @@
|
|||||||
/*
|
/*
|
||||||
* app_terminal.cpp
|
* app_terminal.cpp
|
||||||
* MontaukOS Desktop
|
* MontaukOS Desktop - Terminal launcher
|
||||||
|
* Spawns standalone terminal.elf Window Server app
|
||||||
* Copyright (c) 2026 Daniel Hammer
|
* Copyright (c) 2026 Daniel Hammer
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "apps_common.hpp"
|
#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) {
|
void open_terminal(DesktopState* ds) {
|
||||||
int idx = desktop_create_window(ds, "Terminal", 200, 80, 648, 480);
|
if (ds && ds->home_dir[0] != '\0') {
|
||||||
if (idx < 0) return;
|
montauk::spawn("0:/apps/terminal/terminal.elf", ds->home_dir);
|
||||||
|
} else {
|
||||||
Window* win = &ds->windows[idx];
|
montauk::spawn("0:/apps/terminal/terminal.elf");
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -116,11 +116,9 @@ struct EmbeddedAppDef {
|
|||||||
};
|
};
|
||||||
|
|
||||||
static const EmbeddedAppDef embedded_apps[] = {
|
static const EmbeddedAppDef embedded_apps[] = {
|
||||||
{ "Terminal", 0, 0 },
|
|
||||||
{ "Files", 1, 0 },
|
{ "Files", 1, 0 },
|
||||||
{ "Calculator", 3, 0 },
|
{ "Calculator", 3, 0 },
|
||||||
{ "System Info", 2, 2 },
|
{ "System Info", 2, 2 },
|
||||||
{ "Kernel Log", 5, 2 },
|
|
||||||
{ "Processes", 6, 2 },
|
{ "Processes", 6, 2 },
|
||||||
{ "Mandelbrot", 7, 3 },
|
{ "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
|
// Resolve embedded app_id to an icon pointer in DesktopState
|
||||||
static SvgIcon* icon_for_embedded(DesktopState* ds, int app_id) {
|
static SvgIcon* icon_for_embedded(DesktopState* ds, int app_id) {
|
||||||
switch (app_id) {
|
switch (app_id) {
|
||||||
case 0: return &ds->icon_terminal;
|
|
||||||
case 1: return &ds->icon_filemanager;
|
case 1: return &ds->icon_filemanager;
|
||||||
case 2: return &ds->icon_sysinfo;
|
case 2: return &ds->icon_sysinfo;
|
||||||
case 3: return &ds->icon_calculator;
|
case 3: return &ds->icon_calculator;
|
||||||
case 5: return &ds->icon_terminal; // Kernel Log uses terminal icon
|
|
||||||
case 6: return &ds->icon_procmgr;
|
case 6: return &ds->icon_procmgr;
|
||||||
case 7: return &ds->icon_mandelbrot;
|
case 7: return &ds->icon_mandelbrot;
|
||||||
default: return nullptr;
|
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>
|
||||||
@@ -30,6 +30,8 @@ APPS=(
|
|||||||
"music|apps/scalable/audio-player.svg"
|
"music|apps/scalable/audio-player.svg"
|
||||||
"video|apps/scalable/multimedia-video-player.svg"
|
"video|apps/scalable/multimedia-video-player.svg"
|
||||||
"bluetooth|apps/scalable/bluetooth.svg"
|
"bluetooth|apps/scalable/bluetooth.svg"
|
||||||
|
"terminal|apps/scalable/utilities-terminal.svg"
|
||||||
|
"klog|apps/scalable/utilities-terminal.svg"
|
||||||
"rpgdemo|apps/scalable/utilities-terminal.svg"
|
"rpgdemo|apps/scalable/utilities-terminal.svg"
|
||||||
"paint|apps/scalable/kolourpaint.svg"
|
"paint|apps/scalable/kolourpaint.svg"
|
||||||
"screenshot|apps/scalable/gnome-screenshot.svg"
|
"screenshot|apps/scalable/gnome-screenshot.svg"
|
||||||
|
|||||||
Reference in New Issue
Block a user