712 lines
23 KiB
C++
712 lines
23 KiB
C++
/*
|
|
* main.cpp
|
|
* MontaukOS Terminal - standalone Window Server app
|
|
* Preserves the old desktop-integrated terminal layout and tab behavior.
|
|
*
|
|
* Two run modes:
|
|
* - Windowed (default): runs as a Window Server client inside the desktop.
|
|
* - Console (--console): takes over the whole framebuffer with no window
|
|
* server, so it can stand in for desktop.elf as a console-only session
|
|
* launched directly from login.elf.
|
|
* Copyright (c) 2026 Daniel Hammer
|
|
*/
|
|
|
|
#include <montauk/syscall.h>
|
|
#include <montauk/string.h>
|
|
#include <montauk/heap.h>
|
|
#include <montauk/keyboard.h>
|
|
#include <gui/gui.hpp>
|
|
#include <gui/canvas.hpp>
|
|
#include <gui/draw.hpp>
|
|
#include <gui/framebuffer.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 int g_last_win_w = 0;
|
|
static int g_last_win_h = 0;
|
|
static bool g_show_clock = false; // console (full-screen) mode draws a live clock
|
|
static char g_clock_text[16] = {};
|
|
static montauk::abi::ProcInfo g_kill_procs[256];
|
|
static int g_kill_pids[256];
|
|
static int g_kill_pid_count = 0;
|
|
|
|
static bool left_pressed(const montauk::abi::WinEvent& ev) {
|
|
return (ev.mouse.buttons & 1) && !(ev.mouse.prev_buttons & 1);
|
|
}
|
|
|
|
static void term_request_redraw() {
|
|
g_force_redraw = true;
|
|
}
|
|
|
|
static bool term_proc_alive(uint8_t state) {
|
|
return state == 1 || state == 2 || state == 3;
|
|
}
|
|
|
|
static void term_collect_child_pids(int parent_pid, int proc_count) {
|
|
for (int i = 0; i < proc_count; i++) {
|
|
if (!term_proc_alive(g_kill_procs[i].state)) continue;
|
|
if (g_kill_procs[i].parentPid != parent_pid) continue;
|
|
|
|
int child_pid = g_kill_procs[i].pid;
|
|
term_collect_child_pids(child_pid, proc_count);
|
|
if (g_kill_pid_count < (int)(sizeof(g_kill_pids) / sizeof(g_kill_pids[0])))
|
|
g_kill_pids[g_kill_pid_count++] = child_pid;
|
|
}
|
|
}
|
|
|
|
static void term_kill_process_tree(int root_pid) {
|
|
if (root_pid <= 0) return;
|
|
|
|
g_kill_pid_count = 0;
|
|
int proc_count = montauk::proclist(g_kill_procs,
|
|
(int)(sizeof(g_kill_procs) / sizeof(g_kill_procs[0])));
|
|
if (proc_count > 0)
|
|
term_collect_child_pids(root_pid, proc_count);
|
|
|
|
for (int i = 0; i < g_kill_pid_count; i++)
|
|
montauk::kill(g_kill_pids[i]);
|
|
|
|
montauk::kill(root_pid);
|
|
}
|
|
|
|
static void term_free_tab(TerminalState* ts) {
|
|
if (!ts) return;
|
|
if (ts->child_pid > 0) term_kill_process_tree(ts->child_pid);
|
|
if (ts->cells) montauk::free(ts->cells);
|
|
if (ts->alt_cells) montauk::free(ts->alt_cells);
|
|
if (ts->render_cells) montauk::free(ts->render_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;
|
|
|
|
terminal_invalidate_render_cache(g_tabs.tabs[g_tabs.active_tab]);
|
|
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;
|
|
}
|
|
|
|
// Right-hand tab-bar furniture. In console mode a live clock sits at the far
|
|
// right and the new-tab (+) button shifts left to make room; in windowed mode
|
|
// there is no clock and the + keeps its original far-right position.
|
|
struct TabBarRight {
|
|
int clock_x;
|
|
int plus_x;
|
|
bool has_clock;
|
|
};
|
|
|
|
static TabBarRight term_tabbar_right(int width) {
|
|
TabBarRight r;
|
|
r.has_clock = g_show_clock && g_clock_text[0];
|
|
int right_limit = width - TERM_PLUS_PAD;
|
|
r.clock_x = right_limit;
|
|
if (r.has_clock) {
|
|
int cw = text_width(fonts::system_font, g_clock_text, fonts::UI_SIZE);
|
|
r.clock_x = width - TERM_PLUS_PAD - cw;
|
|
right_limit = r.clock_x - 12;
|
|
}
|
|
r.plus_x = right_limit - TERM_PLUS_W;
|
|
return r;
|
|
}
|
|
|
|
// Refresh the cached clock string from the wall clock; returns true when the
|
|
// displayed text changed (so the caller can request a repaint).
|
|
static bool term_refresh_clock() {
|
|
montauk::abi::DateTime dt;
|
|
montauk::gettime(&dt);
|
|
char buf[16];
|
|
snprintf(buf, sizeof(buf), "%02d:%02d:%02d", dt.Hour, dt.Minute, dt.Second);
|
|
if (montauk::streq(buf, g_clock_text)) return false;
|
|
montauk::strcpy(g_clock_text, buf);
|
|
return true;
|
|
}
|
|
|
|
// Render the tab bar + active terminal into a tightly packed pixel buffer of
|
|
// the given dimensions. Shared by the windowed (window surface) and console
|
|
// (framebuffer-backed) run loops.
|
|
static bool term_render_into(uint32_t* pixels, int width, int height) {
|
|
if (!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 = height - TERM_TAB_BAR_H;
|
|
if (term_h < 1) term_h = 1;
|
|
|
|
int new_cols = 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];
|
|
|
|
// Resize the cell grid when the visible cell layout changes: either the
|
|
// surface pixel dimensions changed, or a zoom (font size) change altered
|
|
// how many whole cells now fit. Skipping the zoom case leaves the grid at
|
|
// its old row count, so at a larger font fewer rows fit on screen and the
|
|
// bottom rows (cursor and prompt included) scroll off below the viewport;
|
|
// resizing reflows the overflow into scrollback and keeps the cursor in
|
|
// view. terminal_resize is a no-op per tab whose dimensions already match.
|
|
if (width != g_last_win_w || term_h != g_last_win_h ||
|
|
new_cols != ts->cols || new_rows != ts->rows) {
|
|
g_last_win_w = width;
|
|
g_last_win_h = term_h;
|
|
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) {
|
|
terminal_invalidate_render_cache(g_tabs.tabs[g_tabs.active_tab]);
|
|
g_tabs.tabs[g_tabs.active_tab]->dirty = true;
|
|
g_tabs.prev_active_tab = g_tabs.active_tab;
|
|
}
|
|
|
|
if (pixels != g_tabs.last_pixels) {
|
|
g_tabs.tabs[g_tabs.active_tab]->dirty = true;
|
|
g_tabs.last_pixels = pixels;
|
|
}
|
|
|
|
ts = g_tabs.tabs[g_tabs.active_tab];
|
|
if (!g_force_redraw && !ts->dirty) return false;
|
|
|
|
Canvas c(pixels, width, height);
|
|
Color bar_bg = Color::from_hex(0x1C1C1C);
|
|
c.fill_rect(0, 0, 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;
|
|
}
|
|
|
|
TabBarRight right = term_tabbar_right(width);
|
|
|
|
if (right.has_clock) {
|
|
int text_y = (TERM_TAB_BAR_H - fh) / 2;
|
|
c.text(right.clock_x, text_y, g_clock_text, Color::from_hex(0x9A9A9A));
|
|
}
|
|
|
|
if (g_tabs.tab_count < TERM_MAX_TABS) {
|
|
int plus_h = 22;
|
|
int py = 7;
|
|
int px = right.plus_x;
|
|
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 = pixels + TERM_TAB_BAR_H * width;
|
|
terminal_render(ts, term_pixels, 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, width, term_h);
|
|
int sb_w = 4;
|
|
int sb_x = 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 bool term_render() {
|
|
return term_render_into(g_win.pixels, g_win.width, g_win.height);
|
|
}
|
|
|
|
// Mouse handling shared by both run modes. left_click is the press edge.
|
|
static void term_handle_mouse_core(int mx, int my, int scroll, bool left_click,
|
|
int width, int height) {
|
|
if (g_tabs.tab_count <= 0) return;
|
|
|
|
TerminalState* ts = g_tabs.tabs[g_tabs.active_tab];
|
|
if (scroll != 0 && !ts->alt_screen_active) {
|
|
ts->view_offset += (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_click) return;
|
|
if (my >= TERM_TAB_BAR_H) return;
|
|
|
|
int tab_x = TERM_TAB_PAD;
|
|
for (int i = 0; i < g_tabs.tab_count; i++) {
|
|
if (mx >= tab_x && mx < tab_x + TERM_TAB_W) {
|
|
if (mx >= 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 = term_tabbar_right(width).plus_x;
|
|
if (g_tabs.tab_count < TERM_MAX_TABS &&
|
|
mx >= px && mx < px + TERM_PLUS_W) {
|
|
int cols = width / mono_cell_width();
|
|
int rows = (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_mouse(const montauk::abi::WinEvent& ev) {
|
|
term_handle_mouse_core(ev.mouse.x, ev.mouse.y, ev.mouse.scroll,
|
|
left_pressed(ev), g_win.width, g_win.height);
|
|
}
|
|
|
|
// Keyboard handling shared by both run modes.
|
|
// User whose keyboard layout selection console mode follows.
|
|
static char g_layout_user[32] = "default";
|
|
|
|
static void init_layout_user() {
|
|
if (montauk::getuser(g_layout_user, sizeof(g_layout_user)) <= 0)
|
|
montauk::strcpy(g_layout_user, "default");
|
|
}
|
|
|
|
static void term_handle_key_core(const montauk::abi::KeyEvent& key,
|
|
int width, int height) {
|
|
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 = width / mono_cell_width();
|
|
int rows = (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;
|
|
}
|
|
|
|
// Ctrl+Plus/Equal: zoom in
|
|
if (key.ctrl && key.pressed && (key.ascii == '+' || key.ascii == '=')) {
|
|
if (fonts::TERM_SIZE < 64) {
|
|
fonts::TERM_SIZE += 2;
|
|
for (int i = 0; i < g_tabs.tab_count; i++)
|
|
g_tabs.tabs[i]->dirty = true;
|
|
term_request_redraw();
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Ctrl+Minus: zoom out
|
|
if (key.ctrl && key.pressed && key.ascii == '-') {
|
|
if (fonts::TERM_SIZE > 8) {
|
|
fonts::TERM_SIZE -= 2;
|
|
for (int i = 0; i < g_tabs.tab_count; i++)
|
|
g_tabs.tabs[i]->dirty = true;
|
|
term_request_redraw();
|
|
}
|
|
return;
|
|
}
|
|
|
|
terminal_handle_key(g_tabs.tabs[g_tabs.active_tab], key);
|
|
}
|
|
|
|
static void term_handle_key(const montauk::abi::KeyEvent& key) {
|
|
term_handle_key_core(key, g_win.width, g_win.height);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
// ==== Windowed mode (Window Server client) ====
|
|
|
|
static void run_windowed() {
|
|
if (!g_win.create("Terminal", INIT_W, INIT_H))
|
|
montauk::exit(1);
|
|
|
|
// The kernel seeds newly created window surfaces with white so apps that
|
|
// haven't drawn their first frame don't show as transparent/black. For
|
|
// Terminal that produces a visible white flash while we run terminal_init
|
|
// and the first TrueType render. Paint the dark background and present
|
|
// immediately so the compositor sees terminal colors from frame one.
|
|
{
|
|
uint32_t bar_px = Color::from_hex(0x1C1C1C).to_pixel();
|
|
uint32_t bg_px = colors::TERM_BG.to_pixel();
|
|
int bar_pixels = TERM_TAB_BAR_H * g_win.width;
|
|
if (bar_pixels > g_win.width * g_win.height)
|
|
bar_pixels = g_win.width * g_win.height;
|
|
for (int i = 0; i < bar_pixels; i++)
|
|
g_win.pixels[i] = bar_px;
|
|
for (int i = bar_pixels; i < g_win.width * g_win.height; i++)
|
|
g_win.pixels[i] = bg_px;
|
|
g_win.present();
|
|
}
|
|
|
|
int term_h = g_win.height - TERM_TAB_BAR_H;
|
|
g_last_win_w = g_win.width;
|
|
g_last_win_h = term_h;
|
|
|
|
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 = false;
|
|
montauk::abi::WinEvent ev;
|
|
bool quit = false;
|
|
int r = 0;
|
|
|
|
while ((r = g_win.poll(&ev)) > 0) {
|
|
redraw = true;
|
|
|
|
if (ev.type == 3) {
|
|
quit = true;
|
|
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) {
|
|
quit = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (r < 0 || quit) break;
|
|
|
|
redraw |= term_poll_tabs();
|
|
if (g_should_exit) break;
|
|
|
|
if (redraw && term_render())
|
|
g_win.present();
|
|
|
|
if (r == 0)
|
|
montauk::sleep_ms(16);
|
|
}
|
|
|
|
term_cleanup();
|
|
g_win.destroy();
|
|
}
|
|
|
|
// ==== Console mode (full-screen, no window server) ====
|
|
|
|
static void console_present(Framebuffer& fb, uint32_t* pixels, int sw, int mx, int my) {
|
|
// The terminal renders into a tightly packed scene buffer; copy it across
|
|
// (handling pitch), composite the mouse cursor, then flip. The full copy
|
|
// each present keeps the cursor trail-free without per-tile damage tracking.
|
|
fb.copy_from(pixels, sw * (int)sizeof(uint32_t));
|
|
draw_cursor(fb, mx, my);
|
|
fb.flip();
|
|
}
|
|
|
|
static void run_console() {
|
|
Framebuffer fb;
|
|
int sw = fb.width();
|
|
int sh = fb.height();
|
|
if (sw <= 0 || sh <= 0)
|
|
montauk::exit(1);
|
|
|
|
montauk::set_mouse_bounds(sw - 1, sh - 1);
|
|
g_show_clock = true;
|
|
term_refresh_clock();
|
|
|
|
uint32_t* pixels = (uint32_t*)montauk::alloc((uint64_t)sw * sh * sizeof(uint32_t));
|
|
if (!pixels)
|
|
montauk::exit(1);
|
|
|
|
// Paint the dark background immediately so the first frame is not a flash
|
|
// of uninitialized memory while the shell spawns and the first render runs.
|
|
{
|
|
uint32_t bar_px = Color::from_hex(0x1C1C1C).to_pixel();
|
|
uint32_t bg_px = colors::TERM_BG.to_pixel();
|
|
int bar_pixels = TERM_TAB_BAR_H * sw;
|
|
int total = sw * sh;
|
|
if (bar_pixels > total) bar_pixels = total;
|
|
for (int i = 0; i < bar_pixels; i++) pixels[i] = bar_px;
|
|
for (int i = bar_pixels; i < total; i++) pixels[i] = bg_px;
|
|
}
|
|
|
|
int term_h = sh - TERM_TAB_BAR_H;
|
|
g_last_win_w = sw;
|
|
g_last_win_h = term_h;
|
|
|
|
int cols = sw / mono_cell_width();
|
|
int rows = term_h / mono_cell_height();
|
|
if (cols < 1) cols = 1;
|
|
if (rows < 1) rows = 1;
|
|
|
|
init_layout_user();
|
|
|
|
g_tabs.tabs[0] = term_create_tab(cols, rows);
|
|
if (!g_tabs.tabs[0])
|
|
montauk::exit(1);
|
|
g_tabs.tab_count = 1;
|
|
g_tabs.active_tab = 0;
|
|
g_tabs.prev_active_tab = -1;
|
|
g_tabs.last_pixels = pixels;
|
|
|
|
montauk::abi::MouseState mouse = {};
|
|
montauk::mouse_state(&mouse);
|
|
int prev_mx = mouse.x;
|
|
int prev_my = mouse.y;
|
|
uint8_t prev_buttons = mouse.buttons;
|
|
|
|
term_render_into(pixels, sw, sh);
|
|
console_present(fb, pixels, sw, mouse.x, mouse.y);
|
|
|
|
uint64_t input_serial = montauk::input_wait(0, 0);
|
|
while (!g_should_exit) {
|
|
bool redraw = false;
|
|
|
|
while (montauk::is_key_available()) {
|
|
montauk::abi::KeyEvent key;
|
|
montauk::getkey(&key);
|
|
// Console mode reads the kernel buffer directly, so it does not
|
|
// get the desktop's layout translation the windowed path does.
|
|
montauk::keyboard::translate_direct(&key, g_layout_user);
|
|
term_handle_key_core(key, sw, sh);
|
|
redraw = true;
|
|
if (g_should_exit) break;
|
|
}
|
|
if (g_should_exit) break;
|
|
|
|
montauk::mouse_state(&mouse);
|
|
bool moved = mouse.x != prev_mx || mouse.y != prev_my;
|
|
bool left_click = (mouse.buttons & 1) && !(prev_buttons & 1);
|
|
int scroll = mouse.scrollDelta;
|
|
if (left_click || scroll != 0)
|
|
term_handle_mouse_core(mouse.x, mouse.y, scroll, left_click, sw, sh);
|
|
// A bare mouse move still needs a present so the cursor follows the
|
|
// pointer, even when the terminal scene itself is unchanged.
|
|
if (moved || left_click || scroll != 0)
|
|
redraw = true;
|
|
prev_mx = mouse.x;
|
|
prev_my = mouse.y;
|
|
prev_buttons = mouse.buttons;
|
|
|
|
// Tick the clock: when the displayed time changes, force a tab-bar
|
|
// repaint even if the terminal content itself is idle.
|
|
if (term_refresh_clock())
|
|
term_request_redraw();
|
|
|
|
redraw |= term_poll_tabs();
|
|
if (g_should_exit) break;
|
|
|
|
if (redraw) {
|
|
term_render_into(pixels, sw, sh);
|
|
console_present(fb, pixels, sw, mouse.x, mouse.y);
|
|
}
|
|
|
|
// Wake on input, but cap the wait so child (shell) output is drained
|
|
// and rendered promptly even with no keyboard/mouse activity.
|
|
input_serial = montauk::input_wait(input_serial, 16);
|
|
}
|
|
|
|
term_cleanup();
|
|
}
|
|
|
|
// ==== Entry point ====
|
|
|
|
// Recognizes a leading "--console" token (optionally followed by a working
|
|
// directory). Returns the number of bytes to skip past the flag, or 0 if the
|
|
// arguments do not start with the flag.
|
|
static int console_flag_skip(const char* args) {
|
|
const char* flag = "--console";
|
|
int i = 0;
|
|
while (flag[i]) {
|
|
if (args[i] != flag[i]) return 0;
|
|
i++;
|
|
}
|
|
if (args[i] != '\0' && args[i] != ' ') return 0;
|
|
return i;
|
|
}
|
|
|
|
extern "C" void _start() {
|
|
if (!fonts::init())
|
|
montauk::exit(1);
|
|
|
|
char args[256] = {};
|
|
int arglen = montauk::getargs(args, sizeof(args));
|
|
|
|
bool console_mode = false;
|
|
const char* workdir = args;
|
|
if (arglen > 0) {
|
|
int skip = console_flag_skip(args);
|
|
if (skip > 0) {
|
|
console_mode = true;
|
|
workdir = args + skip;
|
|
while (*workdir == ' ') workdir++;
|
|
}
|
|
}
|
|
|
|
if (workdir && workdir[0])
|
|
montauk::chdir(workdir);
|
|
|
|
if (console_mode)
|
|
run_console();
|
|
else
|
|
run_windowed();
|
|
|
|
montauk::exit(0);
|
|
}
|