From 84f200db8b9cc8b9adaa46fe7c1084ef78b43514 Mon Sep 17 00:00:00 2001 From: Daniel Hammer Date: Wed, 17 Jun 2026 18:00:47 +0200 Subject: [PATCH] feat: add Console Session mode --- kernel/src/Api/BuildNo.hpp | 2 +- programs/src/login/login.hpp | 12 ++ programs/src/login/login_input.cpp | 38 +++- programs/src/login/login_render.cpp | 26 +++ programs/src/terminal/main.cpp | 300 ++++++++++++++++++++++++---- 5 files changed, 332 insertions(+), 46 deletions(-) diff --git a/kernel/src/Api/BuildNo.hpp b/kernel/src/Api/BuildNo.hpp index 1c68e2b..888cadb 100644 --- a/kernel/src/Api/BuildNo.hpp +++ b/kernel/src/Api/BuildNo.hpp @@ -12,4 +12,4 @@ #pragma once -#define MONTAUK_BUILD_NUMBER 104 +#define MONTAUK_BUILD_NUMBER 106 diff --git a/programs/src/login/login.hpp b/programs/src/login/login.hpp index b838b81..73eb527 100644 --- a/programs/src/login/login.hpp +++ b/programs/src/login/login.hpp @@ -32,12 +32,21 @@ enum LoginMode { MODE_LOGIN, }; +// Which session to launch after a successful login. Desktop is the default +// graphical environment; Console launches the Terminal full-screen in place of +// the desktop (no window server). +enum SessionMode { + SESSION_DESKTOP, + SESSION_CONSOLE, +}; + struct LoginState { gui::Framebuffer fb; int screen_w, screen_h; LoginMode mode; int active_field; + SessionMode session_mode; char username[32]; int username_len; @@ -115,6 +124,9 @@ struct LoginLayout { gui::Rect submit_button; gui::Rect shutdown_button; gui::Rect reboot_button; + gui::Rect desktop_radio; + gui::Rect console_radio; + int session_label_y; int content_x; int content_w; int heading_y; diff --git a/programs/src/login/login_input.cpp b/programs/src/login/login_input.cpp index f1f6740..f9c5167 100644 --- a/programs/src/login/login_input.cpp +++ b/programs/src/login/login_input.cpp @@ -33,14 +33,33 @@ void finish_desktop_session(LoginState* ls) { ls->show_error = false; } -void launch_desktop_session(LoginState* ls) { +// Build "--console 0:/users/" so the full-screen Terminal starts in +// the user's home directory, mirroring how the desktop is handed the home dir. +void build_console_args(LoginState* ls, char* out, int cap) { + const char* prefix = "--console 0:/users/"; + int i = 0; + while (prefix[i] && i < cap - 1) { out[i] = prefix[i]; i++; } + int j = 0; + while (ls->username[j] && i < cap - 1) out[i++] = ls->username[j++]; + out[i] = '\0'; +} + +void launch_session(LoginState* ls) { // Authentication already succeeded and the session is set, so the password - // is no longer needed. Wipe it before the desktop runs rather than waiting + // is no longer needed. Wipe it before the session runs rather than waiting // for finish_desktop_session, so it does not sit in the heap for the whole // session. montauk::memset(ls->password, 0, sizeof(ls->password)); ls->password_len = 0; - int pid = montauk::spawn("0:/os/desktop.elf", ls->username); + + int pid; + if (ls->session_mode == SESSION_CONSOLE) { + char console_args[96]; + build_console_args(ls, console_args, (int)sizeof(console_args)); + pid = montauk::spawn("0:/apps/terminal/terminal.elf", console_args); + } else { + pid = montauk::spawn("0:/os/desktop.elf", ls->username); + } if (pid >= 0) { montauk::setuser(pid, ls->username); montauk::waitpid(pid); @@ -49,7 +68,8 @@ void launch_desktop_session(LoginState* ls) { // The desktop hands off a graceful power-off by posting a request and // exiting (see desktop_request_power). Pick it up now that waitpid has // returned and run the shutdown stages; perform_graceful_shutdown never - // returns. A normal logout leaves no request, so we fall through. + // returns. A normal logout (or the console session exiting) leaves no + // request, so we fall through and return to the login screen. int req = montauk::power_request(montauk::abi::POWER_REQ_QUERY); if (req == montauk::abi::POWER_REQ_SHUTDOWN || req == montauk::abi::POWER_REQ_REBOOT) { perform_graceful_shutdown(ls, req); @@ -172,7 +192,7 @@ void handle_key(LoginState* ls, const montauk::abi::KeyEvent& key) { if (ls->mode == MODE_FIRST_BOOT) { if (try_first_boot_submit(ls)) transition_to_login_mode(ls); } else if (try_login(ls)) { - launch_desktop_session(ls); + launch_session(ls); } return; } @@ -212,11 +232,17 @@ void handle_mouse(LoginState* ls) { if (clicked >= 0) { ls->active_field = clicked; ls->show_error = false; + } else if (ls->mode == MODE_LOGIN && lo.desktop_radio.contains(mx, my)) { + ls->session_mode = SESSION_DESKTOP; + ls->show_error = false; + } else if (ls->mode == MODE_LOGIN && lo.console_radio.contains(mx, my)) { + ls->session_mode = SESSION_CONSOLE; + ls->show_error = false; } else if (lo.submit_button.contains(mx, my)) { if (ls->mode == MODE_FIRST_BOOT) { if (try_first_boot_submit(ls)) transition_to_login_mode(ls); } else if (try_login(ls)) { - launch_desktop_session(ls); + launch_session(ls); } return; } else if (lo.shutdown_button.contains(mx, my)) { diff --git a/programs/src/login/login_render.cpp b/programs/src/login/login_render.cpp index 632edb1..d666124 100644 --- a/programs/src/login/login_render.cpp +++ b/programs/src/login/login_render.cpp @@ -51,6 +51,20 @@ LoginLayout layout_login_screen(LoginState* ls) { y = lo.password_field.y + FIELD_H + FIELD_GAP; } + // Session selector (login mode only): a "Session" label above two radio + // options that pick the desktop or the full-screen console. + if (ls->mode == MODE_LOGIN) { + lo.session_label_y = y; + int ry = y + sfh + LABEL_GAP; + int radio_h = 22; + int half = lo.content_w / 2; + lo.desktop_radio = {lo.content_x, ry, half, radio_h}; + lo.console_radio = {lo.content_x + half, ry, half, radio_h}; + y = ry + radio_h + FIELD_GAP; + } else { + lo.session_label_y = -1; + } + if (ls->show_error) { lo.error_y = y; y += sfh + 12; @@ -81,11 +95,14 @@ LoginLayout layout_login_screen(LoginState* ls) { lo.shutdown_button = offset_rect(lo.shutdown_button, card_x, card_y); lo.reboot_button = offset_rect(lo.reboot_button, card_x, card_y); lo.submit_button = offset_rect(lo.submit_button, card_x, card_y); + lo.desktop_radio = offset_rect(lo.desktop_radio, card_x, card_y); + lo.console_radio = offset_rect(lo.console_radio, card_x, card_y); lo.content_x += card_x; lo.heading_y += card_y; lo.subtitle_y += card_y; if (lo.error_y >= 0) lo.error_y += card_y; + if (lo.session_label_y >= 0) lo.session_label_y += card_y; return lo; } @@ -155,6 +172,15 @@ void draw_login_screen(LoginState* ls) { ls->password_input, true, ls->active_field == 1); } + // ==== Session selector ==== + if (ls->mode == MODE_LOGIN && lo.session_label_y >= 0) { + c.text(lo.content_x, lo.session_label_y, "Session", th.text_muted); + mtk::draw_radio(c, lo.desktop_radio, "Desktop", + ls->session_mode == SESSION_DESKTOP, th); + mtk::draw_radio(c, lo.console_radio, "Console", + ls->session_mode == SESSION_CONSOLE, th); + } + if (ls->show_error && lo.error_y >= 0) c.text(lo.content_x, lo.error_y, ls->error_msg, th.danger); diff --git a/programs/src/terminal/main.cpp b/programs/src/terminal/main.cpp index 9df3eb6..8a91f68 100644 --- a/programs/src/terminal/main.cpp +++ b/programs/src/terminal/main.cpp @@ -1,7 +1,13 @@ /* * main.cpp * MontaukOS Terminal - standalone Window Server app - * Preserves the old desktop-integrated terminal layout and tab behavior + * 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 */ @@ -10,6 +16,8 @@ #include #include #include +#include +#include #include #include #include @@ -45,6 +53,8 @@ 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; @@ -169,25 +179,63 @@ static bool term_poll_tabs() { return changed || g_force_redraw; } -static bool term_render() { - if (!g_win.pixels || g_tabs.tab_count <= 0) return false; +// 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 = g_win.height - TERM_TAB_BAR_H; + int term_h = height - TERM_TAB_BAR_H; if (term_h < 1) term_h = 1; - int new_cols = g_win.width / mono_cell_width(); + 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]; - // Only resize the terminal buffer when window pixel dimensions change, + // Only resize the terminal buffer when surface pixel dimensions change, // not when font size (zoom) changes - if (g_win.width != g_last_win_w || term_h != g_last_win_h) { - g_last_win_w = g_win.width; + if (width != g_last_win_w || term_h != g_last_win_h) { + 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); @@ -199,17 +247,17 @@ static bool term_render() { g_tabs.prev_active_tab = g_tabs.active_tab; } - if (g_win.pixels != g_tabs.last_pixels) { + if (pixels != g_tabs.last_pixels) { g_tabs.tabs[g_tabs.active_tab]->dirty = true; - g_tabs.last_pixels = g_win.pixels; + g_tabs.last_pixels = pixels; } ts = g_tabs.tabs[g_tabs.active_tab]; if (!g_force_redraw && !ts->dirty) return false; - Canvas c = g_win.canvas(); + Canvas c(pixels, width, height); Color bar_bg = Color::from_hex(0x1C1C1C); - c.fill_rect(0, 0, g_win.width, TERM_TAB_BAR_H, bar_bg); + c.fill_rect(0, 0, width, TERM_TAB_BAR_H, bar_bg); int fh = system_font_height(); int tab_x = TERM_TAB_PAD; @@ -241,18 +289,25 @@ static bool term_render() { 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 = g_win.width - TERM_PLUS_PAD - TERM_PLUS_W; + 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 = g_win.pixels + TERM_TAB_BAR_H * g_win.width; - terminal_render(ts, term_pixels, g_win.width, term_h); + 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(); @@ -260,9 +315,9 @@ static bool term_render() { 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); + Canvas sc(term_pixels, width, term_h); int sb_w = 4; - int sb_x = g_win.width - sb_w - 2; + 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; @@ -278,25 +333,31 @@ static bool term_render() { return true; } -static void term_handle_mouse(const montauk::abi::WinEvent& ev) { +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 (ev.mouse.scroll != 0 && !ts->alt_screen_active) { - ts->view_offset += (ev.mouse.scroll < 0) ? 3 : -3; + 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_pressed(ev)) return; - if (ev.mouse.y >= TERM_TAB_BAR_H) return; + 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 (ev.mouse.x >= tab_x && ev.mouse.x < tab_x + TERM_TAB_W) { - if (ev.mouse.x >= tab_x + TERM_TAB_W - 24) { + 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; @@ -308,24 +369,31 @@ static void term_handle_mouse(const montauk::abi::WinEvent& ev) { tab_x += TERM_TAB_W + TERM_TAB_GAP; } - int px = g_win.width - TERM_PLUS_PAD - TERM_PLUS_W; + int px = term_tabbar_right(width).plus_x; 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(); + 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_key(const montauk::abi::KeyEvent& key) { +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. +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 = g_win.width / mono_cell_width(); - int rows = (g_win.height - TERM_TAB_BAR_H) / mono_cell_height(); + 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); @@ -363,21 +431,19 @@ static void term_handle_key(const montauk::abi::KeyEvent& key) { 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; } -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); +// ==== Windowed mode (Window Server client) ==== +static void run_windowed() { if (!g_win.create("Terminal", INIT_W, INIT_H)) montauk::exit(1); @@ -465,5 +531,161 @@ extern "C" void _start() { 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; + + 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); + 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); }