feat(gui): Make the desktop terminal run real commands

The GUI terminal recognised four hardcoded strings and answered everything else
with "Executed: '<cmd>'". It now dispatches to the same LazyBox applet table and
shell builtins the kernel TTY uses, so ls, free, uname, lspci, ktest, neofetch,
sort, cd and the rest all work, with cwd tracked across commands.

LazyBox reports through printk, so before running a command the terminal
installs a printk sink and streams the output into its own scrollback:

  - ANSI SGR codes become per-line colours instead of being stripped.
  - CP437 box-drawing and block characters fall back to ASCII, so the colour
    swatch in neofetch renders as '#' rather than a row of '?'.
  - 256 lines of scrollback, command history on Up/Down, PgUp/PgDn/Home/End.
  - Applets that read the keyboard directly (nano, snake, sh, top) are refused
    with a pointer to the TTY, since they would block the compositor.

The window's paint and event handlers moved out of gui_apps.c into
gui/gui_terminal.c, and gui_gfx.c now draws from the shared lib/font8x8.c table
rather than its own copy.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01L5yKJT4Wo2cV3rfPrwY5MP
This commit is contained in:
subhobhai943andClaude Opus 5 committed 2026-08-25 01:36:02 +05:30
1 parent 56a378b142
commit eacba4f4d3
8 files changed
+543 -196

No files matched your search

-92
View File
@@ -89,98 +89,6 @@ void gui_app_sysmon_launch(int x, int y, int w, int h) {
}
}
// =================================================================
// 2. GUI Terminal Emulator App (High-Res 440x280)
// =================================================================
typedef struct {
char lines[12][50];
int line_count;
char input[48];
int input_len;
} term_data_t;
static void terminal_paint(gui_window_t* win) {
term_data_t* td = (term_data_t*)win->user_data;
if (!td) return;
int cx = win->x + 10;
int cy = win->y + GUI_TITLEBAR_HEIGHT + 8;
gui_gfx_fill_rect(win->x + 1, win->y + GUI_TITLEBAR_HEIGHT + 1, win->width - 2, win->height - GUI_TITLEBAR_HEIGHT - 2, 0xFF0B0F19);
// Render lines with sharp 8x16 font
for (int i = 0; i < td->line_count && i < 11; i++) {
uint32_t fg = (td->lines[i][0] == '$') ? GUI_THEME_SUCCESS : GUI_THEME_TEXT_MAIN;
gui_gfx_draw_string_16(cx, cy + i * 18, td->lines[i], fg);
}
// Render active prompt line
int prompt_y = cy + td->line_count * 18;
if (prompt_y < win->y + win->height - 20) {
gui_gfx_draw_string_16(cx, prompt_y, "sub-os:/> ", GUI_THEME_PRIMARY);
gui_gfx_draw_string_16(cx + 80, prompt_y, td->input, GUI_THEME_TEXT_MAIN);
// Blinking Cursor block
gui_gfx_fill_rect(cx + 80 + td->input_len * 8, prompt_y + 2, 8, 14, GUI_THEME_SUCCESS);
}
}
static void terminal_event(gui_window_t* win, const gui_event_t* ev) {
term_data_t* td = (term_data_t*)win->user_data;
if (!td) return;
if (ev->type == GUI_EVENT_KEY_DOWN) {
char c = (char)(ev->key & 0xFF);
if (c == '\n' || c == '\r') {
if (td->line_count < 10) {
snprintf(td->lines[td->line_count], 50, "$ %s", td->input);
td->line_count++;
if (strcmp(td->input, "clear") == 0) {
td->line_count = 0;
} else if (strcmp(td->input, "uname") == 0) {
snprintf(td->lines[td->line_count++], 50, "SUB-OS 0.2.0-lts (x86_64)");
} else if (strcmp(td->input, "help") == 0) {
snprintf(td->lines[td->line_count++], 50, "Commands: uname, clear, date, tree, help");
} else if (strcmp(td->input, "tree") == 0) {
snprintf(td->lines[td->line_count++], 50, "/ [bin etc dev proc sys home mnt]");
} else if (td->input_len > 0) {
snprintf(td->lines[td->line_count++], 50, "Executed: '%s'", td->input);
}
} else {
for (int i = 0; i < 9; i++) strcpy(td->lines[i], td->lines[i + 1]);
snprintf(td->lines[9], 50, "$ %s", td->input);
}
td->input[0] = '\0';
td->input_len = 0;
} else if (c == '\b' || (uint8_t)c == 0x7F) {
if (td->input_len > 0) {
td->input_len--;
td->input[td->input_len] = '\0';
}
} else if (c >= 32 && c <= 126 && td->input_len < 36) {
td->input[td->input_len++] = c;
td->input[td->input_len] = '\0';
}
} else if (ev->type == GUI_EVENT_CLOSE) {
if (win->user_data) {
kfree(win->user_data);
win->user_data = NULL;
}
}
}
void gui_app_terminal_launch(int x, int y, int w, int h) {
gui_window_t* win = gui_wm_create_window("Terminal Console", x, y, (w > 0) ? w : 440, (h > 0) ? h : 270);
if (win) {
term_data_t* td = (term_data_t*)kzalloc(sizeof(term_data_t));
strcpy(td->lines[0], "SUB-OS High-Resolution Terminal Emulator");
strcpy(td->lines[1], "Type 'uname', 'tree', or 'help'");
td->line_count = 2;
win->user_data = td;
win->paint = terminal_paint;
win->handle_event = terminal_event;
}
}
// =================================================================
// 3. File Manager Explorer App (High-Res 380x240)
// =================================================================
+3 -2
View File
@@ -10,6 +10,7 @@
#include <gui/gui_cursor.h>
#include <gui/gui_wm.h>
#include <gui/gui_apps.h>
#include <gui/gui_terminal.h>
#include <gui/gui_apps_ext.h>
#include <gui/gui_widgets.h>
#include <gui/gui_icons.h>
@@ -43,7 +44,7 @@ typedef struct {
// Launchers used by both the icon grid and the start menu.
static const desktop_app_t g_apps[] = {
{ "Terminal", GUI_ICON_TERMINAL, gui_app_terminal_launch, 30, 40, 440, 280 },
{ "Terminal", GUI_ICON_TERMINAL, gui_app_terminal_launch, 30, 40, 560, 340 },
{ "Files", GUI_ICON_FOLDER, gui_app_fileman_launch, 80, 70, 380, 240 },
{ "Monitor", GUI_ICON_MONITOR, gui_app_sysmon_launch, 420, 40, 340, 240 },
{ "Tasks", GUI_ICON_TASKS, gui_app_taskmgr_launch, 180, 60, 380, 300 },
@@ -549,7 +550,7 @@ int gui_desktop_run(void) {
// Open a default session so the desktop is not empty on first boot.
gui_app_sysmon_launch(430, 30, 340, 240);
gui_app_terminal_launch(30, 40, 380, 260);
gui_app_terminal_launch(30, 40, 560, 340);
// Discard anything the console left in the keyboard queue; a stale key
// arriving on frame 0 would otherwise act as a desktop shortcut.
+3 -101
View File
@@ -1,5 +1,6 @@
// High-Performance 2D Graphics Engine & Software Compositor for SUB-OS
#include <gui/gui_gfx.h>
#include <lib/font8x8.h>
#include <mm/kmalloc.h>
#include <lib/string.h>
@@ -8,105 +9,6 @@ static uint32_t* g_backbuffer = NULL;
static int g_width = 800;
static int g_height = 600;
// Standard 8x8 font bitmap table for printable ASCII characters (32..126)
static const uint8_t font8x8[96][8] = {
{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, // space
{0x18,0x3c,0x3c,0x18,0x18,0x00,0x18,0x00}, // !
{0x66,0x66,0x24,0x00,0x00,0x00,0x00,0x00}, // "
{0x6c,0x6c,0xfe,0x6c,0xfe,0x6c,0x6c,0x00}, // #
{0x18,0x3e,0x60,0x3c,0x06,0x7c,0x18,0x00}, // $
{0x00,0x63,0x66,0x0c,0x18,0x33,0x63,0x00}, // %
{0x38,0x6c,0x38,0x76,0xdc,0xcc,0x76,0x00}, // &
{0x18,0x18,0x30,0x00,0x00,0x00,0x00,0x00}, // '
{0x0c,0x18,0x30,0x30,0x30,0x18,0x0c,0x00}, // (
{0x30,0x18,0x0c,0x0c,0x0c,0x18,0x30,0x00}, // )
{0x00,0x66,0x3c,0xff,0x3c,0x66,0x00,0x00}, // *
{0x00,0x18,0x18,0x7e,0x18,0x18,0x00,0x00}, // +
{0x00,0x00,0x00,0x00,0x00,0x18,0x18,0x30}, // ,
{0x00,0x00,0x00,0x7e,0x00,0x00,0x00,0x00}, // -
{0x00,0x00,0x00,0x00,0x00,0x18,0x18,0x00}, // .
{0x06,0x0c,0x18,0x30,0x60,0xc0,0x80,0x00}, // /
{0x7c,0xc6,0xce,0xd6,0xe6,0xc6,0x7c,0x00}, // 0
{0x18,0x38,0x18,0x18,0x18,0x18,0x7e,0x00}, // 1
{0x7c,0xc6,0x06,0x1c,0x30,0x66,0xfe,0x00}, // 2
{0x7c,0xc6,0x06,0x3c,0x06,0xc6,0x7c,0x00}, // 3
{0x1c,0x3c,0x6c,0xcc,0xfe,0x0c,0x1e,0x00}, // 4
{0xfe,0xc0,0xfc,0x06,0x06,0xc6,0x7c,0x00}, // 5
{0x7c,0xc6,0xc0,0xfc,0xc6,0xc6,0x7c,0x00}, // 6
{0xfe,0x06,0x0c,0x18,0x30,0x30,0x30,0x00}, // 7
{0x7c,0xc6,0xc6,0x7c,0xc6,0xc6,0x7c,0x00}, // 8
{0x7c,0xc6,0xc6,0x7e,0x06,0xc6,0x7c,0x00}, // 9
{0x00,0x18,0x18,0x00,0x18,0x18,0x00,0x00}, // :
{0x00,0x18,0x18,0x00,0x18,0x18,0x30,0x00}, // ;
{0x06,0x0c,0x18,0x30,0x18,0x0c,0x06,0x00}, // <
{0x00,0x00,0x7e,0x00,0x7e,0x00,0x00,0x00}, // =
{0x60,0x30,0x18,0x0c,0x18,0x30,0x60,0x00}, // >
{0x7c,0xc6,0x0c,0x18,0x18,0x00,0x18,0x00}, // ?
{0x7c,0xc6,0xde,0xde,0xdc,0xc0,0x7c,0x00}, // @
{0x38,0x6c,0xc6,0xfe,0xc6,0xc6,0xc6,0x00}, // A
{0xfc,0x66,0x66,0x7c,0x66,0x66,0xfc,0x00}, // B
{0x3c,0x66,0xc0,0xc0,0xc0,0x66,0x3c,0x00}, // C
{0xf8,0x6c,0x66,0x66,0x66,0x6c,0xf8,0x00}, // D
{0xfe,0x62,0x68,0x78,0x68,0x62,0xfe,0x00}, // E
{0xfe,0x62,0x68,0x78,0x68,0x60,0xf0,0x00}, // F
{0x3c,0x66,0xc0,0xc0,0xce,0x66,0x3e,0x00}, // G
{0xc6,0xc6,0xc6,0xfe,0xc6,0xc6,0xc6,0x00}, // H
{0x3c,0x18,0x18,0x18,0x18,0x18,0x3c,0x00}, // I
{0x1e,0x0c,0x0c,0x0c,0xcc,0xcc,0x78,0x00}, // J
{0xe6,0x66,0x6c,0x78,0x6c,0x66,0xe6,0x00}, // K
{0xf0,0x60,0x60,0x60,0x62,0x66,0xfe,0x00}, // L
{0xc6,0xee,0xfe,0xd6,0xc6,0xc6,0xc6,0x00}, // M
{0xc6,0xe6,0xf6,0xde,0xce,0xc6,0xc6,0x00}, // N
{0x7c,0xc6,0xc6,0xc6,0xc6,0xc6,0x7c,0x00}, // O
{0xfc,0x66,0x66,0x7c,0x60,0x60,0xf0,0x00}, // P
{0x7c,0xc6,0xc6,0xc6,0xd6,0xde,0x7c,0x06}, // Q
{0xfc,0x66,0x66,0x7c,0x6c,0x66,0xe6,0x00}, // R
{0x7c,0xc6,0x60,0x38,0x0c,0xc6,0x7c,0x00}, // S
{0x7e,0x18,0x18,0x18,0x18,0x18,0x18,0x00}, // T
{0xc6,0xc6,0xc6,0xc6,0xc6,0xc6,0x7c,0x00}, // U
{0xc6,0xc6,0xc6,0xc6,0xc6,0x6c,0x38,0x00}, // V
{0xc6,0xc6,0xc6,0xd6,0xfe,0xee,0xc6,0x00}, // W
{0xc6,0xc6,0x6c,0x38,0x6c,0xc6,0xc6,0x00}, // X
{0x66,0x66,0x66,0x3c,0x18,0x18,0x18,0x00}, // Y
{0xfe,0x06,0x0c,0x18,0x30,0x60,0xfe,0x00}, // Z
{0x3c,0x30,0x30,0x30,0x30,0x30,0x3c,0x00}, // [
{0xc0,0x60,0x30,0x18,0x0c,0x06,0x02,0x00}, // backslash
{0x3c,0x0c,0x0c,0x0c,0x0c,0x0c,0x3c,0x00}, // ]
{0x10,0x38,0x6c,0xc6,0x00,0x00,0x00,0x00}, // ^
{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff}, // _
{0x30,0x18,0x0c,0x00,0x00,0x00,0x00,0x00}, // `
{0x00,0x00,0x78,0x0c,0x7c,0xcc,0x76,0x00}, // a
{0xe0,0x60,0x7c,0x66,0x66,0x66,0xdc,0x00}, // b
{0x00,0x00,0x78,0xcc,0xc0,0xcc,0x78,0x00}, // c
{0x1c,0x0c,0x7c,0xcc,0xcc,0xcc,0x76,0x00}, // d
{0x00,0x00,0x78,0xcc,0xfc,0xc0,0x78,0x00}, // e
{0x38,0x6c,0x60,0xf0,0x60,0x60,0xf0,0x00}, // f
{0x00,0x00,0x76,0xcc,0xcc,0x7c,0x0c,0xf8}, // g
{0xe0,0x60,0x6c,0x76,0x66,0x66,0xe6,0x00}, // h
{0x18,0x00,0x38,0x18,0x18,0x18,0x3c,0x00}, // i
{0x0c,0x00,0x1c,0x0c,0x0c,0xcc,0xcc,0x78}, // j
{0xe0,0x60,0x66,0x6c,0x78,0x6c,0xe6,0x00}, // k
{0x38,0x18,0x18,0x18,0x18,0x18,0x3c,0x00}, // l
{0x00,0x00,0xec,0xfe,0xd6,0xc6,0xc6,0x00}, // m
{0x00,0x00,0xdc,0x66,0x66,0x66,0x66,0x00}, // n
{0x00,0x00,0x7c,0xc6,0xc6,0xc6,0x7c,0x00}, // o
{0x00,0x00,0xdc,0x66,0x66,0x7c,0x60,0xf0}, // p
{0x00,0x00,0x76,0xcc,0xcc,0x7c,0x0c,0x1e}, // q
{0x00,0x00,0xdc,0x76,0x60,0x60,0xf0,0x00}, // r
{0x00,0x00,0x7c,0xc0,0x78,0x0c,0xf8,0x00}, // s
{0x10,0x30,0x7c,0x30,0x30,0x34,0x18,0x00}, // t
{0x00,0x00,0xcc,0xcc,0xcc,0xcc,0x76,0x00}, // u
{0x00,0x00,0xc6,0xc6,0xc6,0x6c,0x38,0x00}, // v
{0x00,0x00,0xc6,0xc6,0xd6,0xfe,0x6c,0x00}, // w
{0x00,0x00,0xc6,0x6c,0x38,0x6c,0xc6,0x00}, // x
{0x00,0x00,0xcc,0xcc,0xcc,0x7c,0x0c,0xf8}, // y
{0x00,0x00,0xfc,0x18,0x30,0x60,0xfc,0x00}, // z
{0x0e,0x18,0x18,0x70,0x18,0x18,0x0e,0x00}, // {
{0x18,0x18,0x18,0x00,0x18,0x18,0x18,0x00}, // |
{0x70,0x18,0x18,0x0e,0x18,0x18,0x70,0x00}, // }
{0x76,0xdc,0x00,0x00,0x00,0x00,0x00,0x00}, // ~
{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00} // DEL
};
void gui_gfx_init(uint32_t* fb, int width, int height) {
g_frontbuffer = fb;
@@ -235,7 +137,7 @@ void gui_gfx_draw_gradient_h(int x, int y, int w, int h, uint32_t left_col, uint
void gui_gfx_draw_char(int x, int y, char c, uint32_t fg, uint32_t bg, bool transparent_bg) {
if (c < 32 || c > 126) c = '?';
const uint8_t* glyph = font8x8[c - 32];
const uint8_t* glyph = font8x8_glyph(c);
for (int row = 0; row < 8; row++) {
uint8_t bits = glyph[row];
@@ -252,7 +154,7 @@ void gui_gfx_draw_char(int x, int y, char c, uint32_t fg, uint32_t bg, bool tran
// Sharp High-DPI 8x16 font renderer with 2x vertical pixel fidelity
void gui_gfx_draw_char_16(int x, int y, char c, uint32_t fg, uint32_t bg, bool transparent_bg) {
if (c < 32 || c > 126) c = '?';
const uint8_t* glyph = font8x8[c - 32];
const uint8_t* glyph = font8x8_glyph(c);
for (int row = 0; row < 8; row++) {
uint8_t bits = glyph[row];
+516
View File
@@ -0,0 +1,516 @@
// SUB-OS Desktop Terminal Emulator
//
// Commands typed here run through the same LazyBox applet table the kernel TTY
// uses. LazyBox reports everything via printk, so before dispatching an applet
// the terminal installs a printk sink; the applet's output is streamed into
// the window's scrollback instead of the text console, ANSI colour codes are
// translated to per-line colours, and the sink is removed afterwards.
#include <gui/gui_terminal.h>
#include <gui/gui_widgets.h>
#include <gui/gui_gfx.h>
#include <gui/gui_theme.h>
#include <userland/lazybox.h>
#include <userland/shell.h>
#include <kernel/printk.h>
#include <fs/vfs.h>
#include <mm/kmalloc.h>
#include <drivers/keyboard.h>
#include <lib/string.h>
#include <lib/printf.h>
#define TERM_SCROLLBACK 256
#define TERM_LINE_CHARS 118
#define TERM_INPUT_MAX 120
#define TERM_HISTORY 16
#define TERM_ROW_H 12
#define TERM_ARGV_MAX 16
typedef struct {
char text[TERM_LINE_CHARS + 1];
uint32_t color;
} term_line_t;
typedef struct {
term_line_t lines[TERM_SCROLLBACK];
int line_count; // Total lines ever written, capped at TERM_SCROLLBACK
int scroll; // First visible line
bool follow_tail; // Auto-scroll while new output arrives
char input[TERM_INPUT_MAX + 1];
int input_len;
char history[TERM_HISTORY][TERM_INPUT_MAX + 1];
int history_count;
int history_pos; // -1 when editing a fresh line
// Partial line being assembled from sink writes
char pending[TERM_LINE_CHARS + 1];
int pending_len;
uint32_t pending_color;
bool busy;
} term_data_t;
// ---------------------------------------------------------------------------
// Scrollback
// ---------------------------------------------------------------------------
static void term_scroll_to_tail(term_data_t* td, int visible_rows) {
td->scroll = td->line_count - visible_rows;
if (td->scroll < 0) td->scroll = 0;
}
static void term_push_line(term_data_t* td, const char* text, uint32_t color) {
if (td->line_count == TERM_SCROLLBACK) {
// Full: drop the oldest line and shift the window up by one.
for (int i = 0; i < TERM_SCROLLBACK - 1; i++) {
td->lines[i] = td->lines[i + 1];
}
td->line_count--;
if (td->scroll > 0) td->scroll--;
}
term_line_t* line = &td->lines[td->line_count++];
strncpy(line->text, text ? text : "", TERM_LINE_CHARS);
line->text[TERM_LINE_CHARS] = '\0';
line->color = color;
}
static void term_flush_pending(term_data_t* td) {
td->pending[td->pending_len] = '\0';
term_push_line(td, td->pending, td->pending_color);
td->pending_len = 0;
td->pending[0] = '\0';
}
static void term_printf(term_data_t* td, uint32_t color, const char* fmt, ...) {
char buf[TERM_LINE_CHARS + 1];
va_list args;
va_start(args, fmt);
vsnprintf(buf, sizeof(buf), fmt, args);
va_end(args);
term_push_line(td, buf, color);
}
// ---------------------------------------------------------------------------
// ANSI translation
// ---------------------------------------------------------------------------
// Map an SGR parameter to the palette. Only the colour attributes LazyBox
// actually emits are handled; everything else keeps the current colour.
static uint32_t sgr_to_color(int code, uint32_t current) {
switch (code) {
case 0: return GUI_THEME_TEXT_MAIN; // reset
case 30: case 90: return GUI_THEME_TEXT_DIM;
case 31: case 91: return GUI_THEME_DANGER;
case 32: case 92: return GUI_THEME_SUCCESS;
case 33: case 93: return GUI_THEME_WARNING;
case 34: case 94: return GUI_THEME_ACCENT;
case 35: case 95: return 0xFFEC4899; // magenta
case 36: case 96: return GUI_THEME_PRIMARY;
case 37: case 97: return GUI_THEME_TEXT_MAIN;
default: return current;
}
}
// CP437 fallback.
//
// The VGA text console renders code page 437 natively, so LazyBox draws boxes
// and colour swatches with high-ASCII bytes such as 0xDB (full block). The GUI
// font only covers printable ASCII, so translate the shapes those applets
// actually use rather than dropping them to '?'. Returns 0 to skip the byte.
static char cp437_to_ascii(unsigned char c) {
switch (c) {
case 0xDB: case 0xDC: case 0xDD: case 0xDE: case 0xDF: return '#'; // blocks
case 0xFE: case 0xFD: return '#'; // small squares
case 0xB2: return '#'; // dark shade
case 0xB1: return ':'; // medium shade
case 0xB0: return '.'; // light shade
case 0xC4: case 0xCD: return '-'; // horizontal rules
case 0xB3: case 0xBA: return '|'; // vertical rules
case 0xDA: case 0xBF: case 0xC0: case 0xD9: // single corners
case 0xC9: case 0xBB: case 0xC8: case 0xBC: // double corners
case 0xC3: case 0xB4: case 0xC2: case 0xC1: case 0xC5: // tees and cross
return '+';
case 0x07: case 0xF9: case 0xFA: return '.'; // bullets
case 0x1A: case 0x10: return '>'; // arrows
case 0x1B: case 0x11: return '<';
case 0x18: case 0x1E: return '^';
case 0x19: case 0x1F: return 'v';
case 0xF8: return 'o'; // degree
default: return 0; // drop
}
}
// Consume captured bytes, splitting on newlines and folding ANSI escapes into
// the colour of the line being assembled.
static void term_absorb(term_data_t* td, const char* text, size_t len) {
for (size_t i = 0; i < len; i++) {
char c = text[i];
if (c == '\033') {
// CSI sequence: ESC [ params letter
size_t j = i + 1;
if (j < len && text[j] == '[') {
j++;
int value = 0;
bool have_value = false;
while (j < len) {
char p = text[j];
if (p >= '0' && p <= '9') {
value = value * 10 + (p - '0');
have_value = true;
j++;
} else if (p == ';') {
if (have_value) td->pending_color = sgr_to_color(value, td->pending_color);
value = 0;
have_value = false;
j++;
} else {
if (p == 'm') {
td->pending_color = sgr_to_color(have_value ? value : 0,
td->pending_color);
}
j++;
break;
}
}
i = j - 1;
}
continue;
}
if (c == '\n') {
term_flush_pending(td);
continue;
}
if (c == '\r') {
td->pending_len = 0;
continue;
}
if (c == '\t') {
// Expand to the next 8-column stop.
int stop = (td->pending_len + 8) & ~7;
while (td->pending_len < stop && td->pending_len < TERM_LINE_CHARS) {
td->pending[td->pending_len++] = ' ';
}
continue;
}
unsigned char uc = (unsigned char)c;
if (uc < 32 || uc > 126) {
char sub = cp437_to_ascii(uc);
if (!sub) continue; // Control byte or a glyph with no ASCII stand-in
c = sub;
}
if (td->pending_len >= TERM_LINE_CHARS) term_flush_pending(td);
td->pending[td->pending_len++] = c;
}
}
static void term_sink(const char* text, size_t len, void* ctx) {
term_absorb((term_data_t*)ctx, text, len);
}
// ---------------------------------------------------------------------------
// Command execution
// ---------------------------------------------------------------------------
// Applets that read the keyboard directly would block the compositor, since
// the desktop drives input from its own frame loop.
static bool applet_is_interactive(const char* name) {
return strcmp(name, "nano") == 0 || strcmp(name, "snake") == 0 ||
strcmp(name, "sh") == 0 || strcmp(name, "top") == 0;
}
static void term_run_command(term_data_t* td, const char* cmd) {
char work[TERM_INPUT_MAX + 1];
strncpy(work, cmd, TERM_INPUT_MAX);
work[TERM_INPUT_MAX] = '\0';
// Same quote-aware tokenizer the TTY shell uses.
char* argv[TERM_ARGV_MAX];
int argc = 0;
char* token = work;
while (*token) {
while (*token == ' ') *token++ = '\0';
if (*token == '\0') break;
if (*token == '"' || *token == '\'') {
char quote = *token++;
if (argc < TERM_ARGV_MAX) argv[argc++] = token;
while (*token && *token != quote) token++;
if (*token == quote) *token++ = '\0';
} else {
if (argc < TERM_ARGV_MAX) argv[argc++] = token;
while (*token && *token != ' ') token++;
}
}
if (argc == 0) return;
// Terminal built-ins first.
if (strcmp(argv[0], "clear") == 0 || strcmp(argv[0], "cls") == 0) {
td->line_count = 0;
td->scroll = 0;
return;
}
if (strcmp(argv[0], "exit") == 0 || strcmp(argv[0], "quit") == 0) {
term_printf(td, GUI_THEME_TEXT_DIM, "Close the window to end this session.");
return;
}
bool is_applet = lazybox_has_applet(argv[0]);
bool is_builtin = shell_is_builtin(argv[0]);
if (!is_applet && !is_builtin) {
term_printf(td, GUI_THEME_DANGER,
"%s: command not found. Try 'lazybox' for the applet list.", argv[0]);
return;
}
if (is_applet && applet_is_interactive(argv[0])) {
term_printf(td, GUI_THEME_WARNING,
"%s reads the keyboard directly and would freeze the desktop.",
argv[0]);
term_printf(td, GUI_THEME_TEXT_DIM,
"Run it from the kernel TTY instead (Esc leaves the desktop).");
return;
}
// Capture printk output into this window's scrollback for the duration of
// the command. Applets take precedence over builtins, matching the TTY.
td->busy = true;
td->pending_len = 0;
td->pending_color = GUI_THEME_TEXT_MAIN;
printk_set_sink(term_sink, td);
if (is_applet) {
lazybox_run_applet(argv[0], argc, argv);
} else {
shell_execute_builtin(cmd, argc, argv, false);
}
printk_clear_sink();
if (td->pending_len > 0) term_flush_pending(td);
td->busy = false;
}
static void term_submit(term_data_t* td) {
const char* cwd = vfs_getcwd();
term_printf(td, GUI_THEME_SUCCESS, "%s $ %s", cwd ? cwd : "/", td->input);
if (td->input_len > 0) {
// Record in history, skipping an immediate repeat.
bool repeat = (td->history_count > 0 &&
strcmp(td->history[td->history_count - 1], td->input) == 0);
if (!repeat) {
if (td->history_count == TERM_HISTORY) {
for (int i = 0; i < TERM_HISTORY - 1; i++) {
strcpy(td->history[i], td->history[i + 1]);
}
td->history_count--;
}
strcpy(td->history[td->history_count++], td->input);
}
term_run_command(td, td->input);
}
td->input[0] = '\0';
td->input_len = 0;
td->history_pos = -1;
td->follow_tail = true;
}
// ---------------------------------------------------------------------------
// Input handling
// ---------------------------------------------------------------------------
static void term_recall_history(term_data_t* td, int direction) {
if (td->history_count == 0) return;
if (td->history_pos == -1) {
td->history_pos = (direction < 0) ? td->history_count - 1 : -1;
} else {
td->history_pos += (direction < 0) ? -1 : 1;
}
if (td->history_pos < 0) td->history_pos = 0;
if (td->history_pos >= td->history_count) {
// Past the newest entry: back to an empty prompt.
td->history_pos = -1;
td->input[0] = '\0';
td->input_len = 0;
return;
}
strncpy(td->input, td->history[td->history_pos], TERM_INPUT_MAX);
td->input[TERM_INPUT_MAX] = '\0';
td->input_len = (int)strlen(td->input);
}
static void terminal_event(gui_window_t* win, const gui_event_t* ev) {
term_data_t* td = (term_data_t*)win->user_data;
if (!td) return;
if (ev->type == GUI_EVENT_CLOSE) {
// A sink pointing at freed memory would corrupt the next printk.
if (td->busy) printk_clear_sink();
kfree(td);
win->user_data = NULL;
return;
}
if (ev->type == GUI_EVENT_KEY_DOWN) {
uint16_t key = ev->key;
int visible = (win->height - GUI_TITLEBAR_HEIGHT - 40) / TERM_ROW_H;
if (visible < 1) visible = 1;
if (key & KEY_SPECIAL_FLAG) {
switch (key) {
case KEY_UP: term_recall_history(td, -1); break;
case KEY_DOWN: term_recall_history(td, +1); break;
case KEY_PAGE_UP:
td->follow_tail = false;
td->scroll -= visible;
if (td->scroll < 0) td->scroll = 0;
break;
case KEY_PAGE_DOWN:
td->scroll += visible;
term_scroll_to_tail(td, visible);
td->follow_tail = true;
break;
case KEY_HOME:
td->follow_tail = false;
td->scroll = 0;
break;
case KEY_END:
td->follow_tail = true;
term_scroll_to_tail(td, visible);
break;
default: break;
}
return;
}
char c = (char)(key & 0xFF);
if (c == '\n' || c == '\r') {
term_submit(td);
} else if (c == '\b' || (uint8_t)c == 0x7F) {
if (td->input_len > 0) td->input[--td->input_len] = '\0';
} else if (c >= 32 && c <= 126 && td->input_len < TERM_INPUT_MAX) {
td->input[td->input_len++] = c;
td->input[td->input_len] = '\0';
}
return;
}
gui_widget_feed_event(win, ev);
}
// ---------------------------------------------------------------------------
// Rendering
// ---------------------------------------------------------------------------
static void terminal_paint(gui_window_t* win) {
term_data_t* td = (term_data_t*)win->user_data;
if (!td) return;
int cx = win->x + 1;
int cy = win->y + GUI_TITLEBAR_HEIGHT + 1;
int cw = win->width - 2;
int ch = win->height - GUI_TITLEBAR_HEIGHT - 2;
gui_gfx_fill_rect(cx, cy, cw, ch, 0xFF080B14);
int pad = 6;
int prompt_h = 18;
int status_h = 12;
int view_h = ch - prompt_h - status_h - pad;
int visible = view_h / TERM_ROW_H;
if (visible < 1) visible = 1;
int max_cols = (cw - pad * 2) / 8;
if (max_cols < 1) max_cols = 1;
if (max_cols > TERM_LINE_CHARS) max_cols = TERM_LINE_CHARS;
if (td->follow_tail) term_scroll_to_tail(td, visible);
if (td->scroll > td->line_count - visible) term_scroll_to_tail(td, visible);
if (td->scroll < 0) td->scroll = 0;
// Scrollback
for (int row = 0; row < visible; row++) {
int idx = td->scroll + row;
if (idx >= td->line_count) break;
char clipped[TERM_LINE_CHARS + 1];
strncpy(clipped, td->lines[idx].text, (size_t)max_cols);
clipped[max_cols] = '\0';
gui_gfx_draw_string(cx + pad, cy + pad + row * TERM_ROW_H, clipped,
td->lines[idx].color);
}
// Prompt
int prompt_y = cy + ch - prompt_h - status_h;
gui_gfx_draw_line(cx + 1, prompt_y - 3, cx + cw - 2, prompt_y - 3, GUI_THEME_BORDER);
const char* cwd = vfs_getcwd();
char prompt[40];
snprintf(prompt, sizeof(prompt), "%s $ ", cwd ? cwd : "/");
int prompt_w = (int)strlen(prompt) * 8;
gui_gfx_draw_string(cx + pad, prompt_y + 2, prompt, GUI_THEME_PRIMARY);
// Show the tail of a long command line so the cursor stays visible.
int room = max_cols - (int)strlen(prompt) - 1;
if (room < 1) room = 1;
const char* view = (td->input_len > room) ? td->input + (td->input_len - room)
: td->input;
gui_gfx_draw_string(cx + pad + prompt_w, prompt_y + 2, view, GUI_THEME_TEXT_MAIN);
if (win->active) {
int cursor_x = cx + pad + prompt_w + (int)strlen(view) * 8;
gui_gfx_fill_rect(cursor_x, prompt_y + 1, 8, 10, GUI_THEME_SUCCESS);
}
// Status line
char status[72];
if (td->line_count > visible && !td->follow_tail) {
snprintf(status, sizeof(status), "%d lines showing %d-%d [End] to follow",
td->line_count, td->scroll + 1,
(td->scroll + visible < td->line_count) ? td->scroll + visible
: td->line_count);
} else {
snprintf(status, sizeof(status), "%d lines Up/Down history PgUp/PgDn scroll",
td->line_count);
}
gui_gfx_draw_string(cx + pad, cy + ch - status_h, status, GUI_THEME_TEXT_DIM);
}
void gui_app_terminal_launch(int x, int y, int w, int h) {
gui_window_t* win = gui_wm_create_window("Terminal", x, y,
(w > 0) ? w : 520, (h > 0) ? h : 320);
if (!win) return;
term_data_t* td = (term_data_t*)kzalloc(sizeof(term_data_t));
if (!td) return;
td->history_pos = -1;
td->follow_tail = true;
td->pending_color = GUI_THEME_TEXT_MAIN;
term_printf(td, GUI_THEME_PRIMARY, "SUB-OS Terminal - LazyBox %s", LAZYBOX_VERSION);
term_printf(td, GUI_THEME_TEXT_MUTED,
"LazyBox applets and shell builtins both run here. Try 'ls',");
term_printf(td, GUI_THEME_TEXT_MUTED,
"'uname -a', 'free', 'neofetch', 'lspci', 'ktest', 'help'.");
term_printf(td, GUI_THEME_TEXT_DIM, "");
win->user_data = td;
win->paint = terminal_paint;
win->handle_event = terminal_event;
}