From: Tulio A M Mendes Date: Fri, 13 Feb 2026 07:22:15 +0000 (-0300) Subject: feat: kconsole overhaul -- bugs fixed + readline + scrollback + extended keyboard X-Git-Url: https://projects.tadryanom.me/docs/static/git-favicon.png?a=commitdiff_plain;h=a2e7d0a209635fa837119f52f267996ea0998468;p=AdrOS.git feat: kconsole overhaul -- bugs fixed + readline + scrollback + extended keyboard Bug fixes: 1. dmesg pollution from mem/ls/cat: added klog_set_suppress() flag in console.c. kconsole wraps command execution with suppression so interactive output doesn't contaminate the kernel log buffer. 2. UTF-8 em dash rendering as garbage on VGA: replaced all UTF-8 em dashes in output strings (main.c, init.c) with ASCII '--'. VGA text mode only supports CP437, not UTF-8. 3. PANIC message appearing in dmesg: changed from kprintf() to console_write() so the emergency banner goes to screen/serial but NOT to the kernel log ring buffer. Features: 1. VGA scrollback buffer (200 lines): lines that scroll off the top of the screen are saved in a circular buffer. Shift+PgUp scrolls back half-page, Shift+PgDn scrolls forward. Any new output auto-returns to live view (like Linux). 2. Command history (16 entries): Up/Down arrow keys navigate through previous commands. Duplicate suppression. Current line is saved/restored when navigating. 3. Full line editing: Left/Right arrows move cursor, Home/End jump to start/end, Delete key, insert-at-cursor mode. Emacs keybindings: Ctrl-A (home), Ctrl-E (end), Ctrl-U (kill line), Ctrl-K (kill to end). 4. Extended keyboard driver: HAL x86 keyboard now tracks shift state, handles 0xE0 extended scancodes, emits VT100 escape sequences for arrow keys/Home/End/PgUp/PgDn/Delete. Shift scancode map added for uppercase letters and symbols. Shift+PgUp/PgDn calls vga_scroll_back/fwd directly. 5. Serial input works with readline: VT100 sequences from terminal emulators are parsed by the same escape state machine, so -serial stdio now supports full line editing. Build: clean, cppcheck: clean, smoke: 19/19 pass --- diff --git a/include/console.h b/include/console.h index de201f2..7829470 100644 --- a/include/console.h +++ b/include/console.h @@ -18,6 +18,7 @@ void kprintf(const char* fmt, ...); int kgetc(void); +void klog_set_suppress(int suppress); size_t klog_read(char* out, size_t out_size); #endif diff --git a/include/vga_console.h b/include/vga_console.h index 544c215..c4d3728 100644 --- a/include/vga_console.h +++ b/include/vga_console.h @@ -8,5 +8,7 @@ void vga_put_char(char c); void vga_print(const char* str); void vga_set_color(uint8_t fg, uint8_t bg); void vga_clear(void); +void vga_scroll_back(void); +void vga_scroll_fwd(void); #endif diff --git a/src/drivers/vga_console.c b/src/drivers/vga_console.c index 69ae716..fe1f080 100644 --- a/src/drivers/vga_console.c +++ b/src/drivers/vga_console.c @@ -5,8 +5,8 @@ #include "spinlock.h" static volatile uint16_t* VGA_BUFFER = 0; -static const int VGA_WIDTH = 80; -static const int VGA_HEIGHT = 25; +#define VGA_WIDTH 80 +#define VGA_HEIGHT 25 static int term_col = 0; static int term_row = 0; @@ -14,7 +14,28 @@ static uint8_t term_color = 0x0F; // White on Black static spinlock_t vga_lock = {0}; +/* --- Scrollback buffer --- */ +#define SB_LINES 200 +static uint16_t sb_buf[SB_LINES * VGA_WIDTH]; +static int sb_head = 0; /* next write line (circular) */ +static int sb_count = 0; /* stored lines (max SB_LINES) */ + +static int view_offset = 0; /* 0 = live view, >0 = scrolled back N lines */ +static uint16_t live_buf[VGA_HEIGHT * VGA_WIDTH]; /* saved live VGA when scrolled */ + +static void vga_update_hw_cursor(void) { + hal_video_set_cursor(term_row, term_col); +} + static void vga_scroll(void) { + /* Save row 0 (about to be lost) into scrollback ring */ + for (int x = 0; x < VGA_WIDTH; x++) { + sb_buf[sb_head * VGA_WIDTH + x] = VGA_BUFFER[x]; + } + sb_head = (sb_head + 1) % SB_LINES; + if (sb_count < SB_LINES) sb_count++; + + /* Shift visible content up */ for (int y = 1; y < VGA_HEIGHT; y++) { for (int x = 0; x < VGA_WIDTH; x++) { VGA_BUFFER[(y - 1) * VGA_WIDTH + x] = VGA_BUFFER[y * VGA_WIDTH + x]; @@ -26,13 +47,57 @@ static void vga_scroll(void) { term_row = VGA_HEIGHT - 1; } -static void vga_update_hw_cursor(void) { - hal_video_set_cursor(term_row, term_col); +/* Restore live view if currently scrolled back */ +static void vga_unscroll(void) { + if (view_offset > 0) { + for (int i = 0; i < VGA_HEIGHT * VGA_WIDTH; i++) { + VGA_BUFFER[i] = live_buf[i]; + } + view_offset = 0; + } +} + +/* Render scrollback + live content at current view_offset */ +static void render_scrollback_view(void) { + for (int y = 0; y < VGA_HEIGHT; y++) { + /* + * line_from_end: how far this row is from the bottom of live content. + * 0..VGA_HEIGHT-1 = live rows, VGA_HEIGHT+ = scrollback + */ + int line_from_end = view_offset + (VGA_HEIGHT - 1 - y); + + if (line_from_end < VGA_HEIGHT) { + /* Live content */ + int live_row = VGA_HEIGHT - 1 - line_from_end; + for (int x = 0; x < VGA_WIDTH; x++) { + VGA_BUFFER[y * VGA_WIDTH + x] = live_buf[live_row * VGA_WIDTH + x]; + } + } else { + /* Scrollback: sb_idx 0 = most recent scrolled-off line */ + int sb_idx = line_from_end - VGA_HEIGHT; + if (sb_idx < sb_count) { + int buf_line = (sb_head - 1 - sb_idx + SB_LINES) % SB_LINES; + for (int x = 0; x < VGA_WIDTH; x++) { + VGA_BUFFER[y * VGA_WIDTH + x] = sb_buf[buf_line * VGA_WIDTH + x]; + } + } else { + /* Beyond scrollback — blank */ + for (int x = 0; x < VGA_WIDTH; x++) { + VGA_BUFFER[y * VGA_WIDTH + x] = (uint16_t)' ' | (uint16_t)term_color << 8; + } + } + } + } + /* Hide cursor when scrolled back */ + hal_video_set_cursor(VGA_HEIGHT, 0); } static void vga_put_char_unlocked(char c) { if (!VGA_BUFFER) return; + /* Any new output auto-returns to live view */ + vga_unscroll(); + switch (c) { case '\n': term_col = 0; @@ -131,14 +196,60 @@ void vga_clear(void) { return; } - for (int y = 0; y < VGA_HEIGHT; y++) { - for (int x = 0; x < VGA_WIDTH; x++) { - VGA_BUFFER[y * VGA_WIDTH + x] = (uint16_t)' ' | (uint16_t)term_color << 8; - } + for (int i = 0; i < VGA_HEIGHT * VGA_WIDTH; i++) { + VGA_BUFFER[i] = (uint16_t)' ' | (uint16_t)term_color << 8; } term_col = 0; term_row = 0; + view_offset = 0; + sb_count = 0; + sb_head = 0; vga_update_hw_cursor(); spin_unlock_irqrestore(&vga_lock, flags); } + +void vga_scroll_back(void) { + uintptr_t flags = spin_lock_irqsave(&vga_lock); + + if (!VGA_BUFFER || sb_count == 0) { + spin_unlock_irqrestore(&vga_lock, flags); + return; + } + + if (view_offset == 0) { + /* First scroll back — save current live screen */ + for (int i = 0; i < VGA_HEIGHT * VGA_WIDTH; i++) { + live_buf[i] = VGA_BUFFER[i]; + } + } + + view_offset += VGA_HEIGHT / 2; + if (view_offset > sb_count) view_offset = sb_count; + + render_scrollback_view(); + spin_unlock_irqrestore(&vga_lock, flags); +} + +void vga_scroll_fwd(void) { + uintptr_t flags = spin_lock_irqsave(&vga_lock); + + if (!VGA_BUFFER || view_offset == 0) { + spin_unlock_irqrestore(&vga_lock, flags); + return; + } + + if (view_offset <= VGA_HEIGHT / 2) { + /* Return to live view */ + for (int i = 0; i < VGA_HEIGHT * VGA_WIDTH; i++) { + VGA_BUFFER[i] = live_buf[i]; + } + view_offset = 0; + vga_update_hw_cursor(); + } else { + view_offset -= VGA_HEIGHT / 2; + render_scrollback_view(); + } + + spin_unlock_irqrestore(&vga_lock, flags); +} diff --git a/src/hal/x86/keyboard.c b/src/hal/x86/keyboard.c index 546e1eb..11fbe4c 100644 --- a/src/hal/x86/keyboard.c +++ b/src/hal/x86/keyboard.c @@ -3,10 +3,17 @@ #if defined(__i386__) #include "arch/x86/idt.h" #include "io.h" +#include "vga_console.h" static hal_keyboard_char_cb_t g_cb = 0; static hal_keyboard_scan_cb_t g_scan_cb = 0; +/* Modifier state */ +static volatile int shift_held = 0; + +/* Extended scancode state (0xE0 prefix) */ +static volatile int e0_prefix = 0; + static char scancode_map[128] = { 0, 27, '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '-', '=', '\b', '\t', @@ -45,25 +52,140 @@ static char scancode_map[128] = { 0, }; +static char scancode_map_shift[128] = { + 0, 27, '!', '@', '#', '$', '%', '^', '&', '*', '(', ')', '_', '+', '\b', + '\t', + 'Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P', '{', '}', '\n', + 0, + 'A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', ':', '"', '~', 0, + '|', 'Z', 'X', 'C', 'V', 'B', 'N', 'M', '<', '>', '?', 0, + '*', + 0, + ' ', + 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + -1, + 0, + 0, + 0, + '+', + 0, + 0, + 0, + 0, + 0, + 0, 0, 0, + 0, + 0, + 0, +}; + +/* Emit a VT100 escape sequence through the char callback */ +static void emit_escape_seq(const char* seq) { + if (!g_cb) return; + for (int i = 0; seq[i]; i++) { + g_cb(seq[i]); + } +} + +/* PS/2 scan set 1 scancodes */ +#define SC_LSHIFT_PRESS 0x2A +#define SC_RSHIFT_PRESS 0x36 +#define SC_LSHIFT_REL 0xAA +#define SC_RSHIFT_REL 0xB6 + +/* Extended (0xE0-prefixed) scancodes */ +#define SC_E0_UP 0x48 +#define SC_E0_DOWN 0x50 +#define SC_E0_LEFT 0x4B +#define SC_E0_RIGHT 0x4D +#define SC_E0_HOME 0x47 +#define SC_E0_END 0x4F +#define SC_E0_PGUP 0x49 +#define SC_E0_PGDN 0x51 +#define SC_E0_DEL 0x53 + +static void handle_extended_press(uint8_t sc) { + switch (sc) { + case SC_E0_UP: emit_escape_seq("\033[A"); break; + case SC_E0_DOWN: emit_escape_seq("\033[B"); break; + case SC_E0_RIGHT: emit_escape_seq("\033[C"); break; + case SC_E0_LEFT: emit_escape_seq("\033[D"); break; + case SC_E0_HOME: emit_escape_seq("\033[H"); break; + case SC_E0_END: emit_escape_seq("\033[F"); break; + case SC_E0_PGUP: + if (shift_held) { + vga_scroll_back(); + } else { + emit_escape_seq("\033[5~"); + } + break; + case SC_E0_PGDN: + if (shift_held) { + vga_scroll_fwd(); + } else { + emit_escape_seq("\033[6~"); + } + break; + case SC_E0_DEL: emit_escape_seq("\033[3~"); break; + default: break; + } +} + static void kbd_irq(struct registers* regs) { (void)regs; uint8_t status = inb(0x64); - if (status & 0x01) { - uint8_t scancode = inb(0x60); + if (!(status & 0x01)) return; - /* Raw scancode callback (key press and release) */ - if (g_scan_cb) { - g_scan_cb(scancode); - } + uint8_t scancode = inb(0x60); + + /* Raw scancode callback (key press and release) */ + if (g_scan_cb) { + g_scan_cb(scancode); + } + + /* 0xE0 prefix: next byte is an extended scancode */ + if (scancode == 0xE0) { + e0_prefix = 1; + return; + } + if (e0_prefix) { + e0_prefix = 0; if (!(scancode & 0x80)) { - if (scancode < 128) { - char c = scancode_map[scancode]; - if (c != 0 && g_cb) { - g_cb(c); - } - } + handle_extended_press(scancode); + } + return; + } + + /* Track shift state */ + if (scancode == SC_LSHIFT_PRESS || scancode == SC_RSHIFT_PRESS) { + shift_held = 1; + return; + } + if (scancode == SC_LSHIFT_REL || scancode == SC_RSHIFT_REL) { + shift_held = 0; + return; + } + + /* Ignore key releases for normal keys */ + if (scancode & 0x80) return; + + if (scancode < 128) { + char c = shift_held ? scancode_map_shift[scancode] : scancode_map[scancode]; + if (c != 0 && g_cb) { + g_cb(c); } } } diff --git a/src/kernel/console.c b/src/kernel/console.c index 283e081..10219b4 100644 --- a/src/kernel/console.c +++ b/src/kernel/console.c @@ -21,6 +21,7 @@ static char klog_buf[KLOG_BUF_SIZE]; static size_t klog_head = 0; // next write position static size_t klog_count = 0; // total bytes stored (capped at KLOG_BUF_SIZE) static spinlock_t klog_lock = {0}; +static volatile int klog_suppress_flag = 0; static void klog_append(const char* s, size_t len) { for (size_t i = 0; i < len; i++) { @@ -222,7 +223,7 @@ void kprintf(const char* fmt, ...) { int len = kvsnprintf(buf, sizeof(buf), fmt, ap); va_end(ap); - if (len > 0) { + if (len > 0 && !klog_suppress_flag) { size_t slen = (size_t)len; if (slen >= sizeof(buf)) slen = sizeof(buf) - 1; @@ -234,6 +235,10 @@ void kprintf(const char* fmt, ...) { console_write(buf); } +void klog_set_suppress(int suppress) { + klog_suppress_flag = suppress ? 1 : 0; +} + int kgetc(void) { for (;;) { char c = 0; diff --git a/src/kernel/init.c b/src/kernel/init.c index 0c7bcc3..f2c5562 100644 --- a/src/kernel/init.c +++ b/src/kernel/init.c @@ -123,7 +123,7 @@ int init_start(const struct boot_info* bi) { } if (!fs_root) { - kprintf("[INIT] No root filesystem — cannot start userspace.\n"); + kprintf("[INIT] No root filesystem -- cannot start userspace.\n"); return -1; } diff --git a/src/kernel/kconsole.c b/src/kernel/kconsole.c index 7cbc440..03a301f 100644 --- a/src/kernel/kconsole.c +++ b/src/kernel/kconsole.c @@ -13,10 +13,230 @@ #define KCMD_MAX 128 +/* ---- Output helpers ---- */ + static void kc_puts(const char* s) { console_write(s); } +static void kc_putc(char c) { + console_put_char(c); +} + +/* ---- Command history ---- */ + +#define HIST_MAX 16 +static char hist_buf[HIST_MAX][KCMD_MAX]; +static int hist_head = 0; /* next write slot */ +static int hist_count = 0; /* entries stored */ + +static void hist_add(const char* line) { + if (line[0] == '\0') return; + /* Don't add duplicates of the last command */ + if (hist_count > 0) { + int prev = (hist_head - 1 + HIST_MAX) % HIST_MAX; + if (strcmp(hist_buf[prev], line) == 0) return; + } + strncpy(hist_buf[hist_head], line, KCMD_MAX - 1); + hist_buf[hist_head][KCMD_MAX - 1] = '\0'; + hist_head = (hist_head + 1) % HIST_MAX; + if (hist_count < HIST_MAX) hist_count++; +} + +static const char* hist_get(int idx) { + /* idx 0 = most recent, 1 = second most recent, etc. */ + if (idx < 0 || idx >= hist_count) return 0; + int slot = (hist_head - 1 - idx + HIST_MAX) % HIST_MAX; + return hist_buf[slot]; +} + +/* ---- Line editing helpers ---- */ + +static void kc_cursor_left(int n) { + for (int i = 0; i < n; i++) kc_putc('\b'); +} + +static void kc_erase_line(char* buf, int* len, int* cur) { + kc_cursor_left(*cur); + for (int i = 0; i < *len; i++) kc_putc(' '); + kc_cursor_left(*len); + *len = 0; + *cur = 0; + buf[0] = '\0'; +} + +static void kc_replace_line(char* buf, int* len, int* cur, const char* text) { + kc_erase_line(buf, len, cur); + int nlen = (int)strlen(text); + if (nlen >= KCMD_MAX) nlen = KCMD_MAX - 1; + memcpy(buf, text, (size_t)nlen); + buf[nlen] = '\0'; + *len = nlen; + *cur = nlen; + for (int i = 0; i < nlen; i++) kc_putc(buf[i]); +} + +/* ---- Readline with VT100 escape parsing ---- */ + +static int kc_readline(char* buf, int maxlen) { + int len = 0; + int cur = 0; + int hist_nav = -1; /* -1 = editing current, 0+ = history index */ + char saved[KCMD_MAX]; /* saved current line when navigating history */ + saved[0] = '\0'; + + enum { ST_NORMAL, ST_ESC, ST_CSI } state = ST_NORMAL; + char csi_buf[8]; + int csi_len = 0; + + buf[0] = '\0'; + + for (;;) { + int ch = kgetc(); + if (ch < 0) continue; + + if (state == ST_ESC) { + if (ch == '[') { + state = ST_CSI; + csi_len = 0; + } else { + state = ST_NORMAL; + } + continue; + } + + if (state == ST_CSI) { + if (ch >= '0' && ch <= '9' && csi_len < 7) { + csi_buf[csi_len++] = (char)ch; + continue; + } + csi_buf[csi_len] = '\0'; + state = ST_NORMAL; + + switch (ch) { + case 'A': /* Up arrow — previous history */ + if (hist_nav + 1 < hist_count) { + if (hist_nav == -1) { + memcpy(saved, buf, (size_t)(len + 1)); + } + hist_nav++; + kc_replace_line(buf, &len, &cur, hist_get(hist_nav)); + } + break; + case 'B': /* Down arrow — next history / restore */ + if (hist_nav > 0) { + hist_nav--; + kc_replace_line(buf, &len, &cur, hist_get(hist_nav)); + } else if (hist_nav == 0) { + hist_nav = -1; + kc_replace_line(buf, &len, &cur, saved); + } + break; + case 'C': /* Right arrow */ + if (cur < len) { + kc_putc(buf[cur]); + cur++; + } + break; + case 'D': /* Left arrow */ + if (cur > 0) { + cur--; + kc_putc('\b'); + } + break; + case 'H': /* Home */ + kc_cursor_left(cur); + cur = 0; + break; + case 'F': /* End */ + for (int i = cur; i < len; i++) kc_putc(buf[i]); + cur = len; + break; + case '~': { /* Delete (CSI 3 ~) */ + int param = 0; + for (int i = 0; i < csi_len; i++) + param = param * 10 + (csi_buf[i] - '0'); + if (param == 3 && cur < len) { + memmove(&buf[cur], &buf[cur + 1], (size_t)(len - cur - 1)); + len--; + buf[len] = '\0'; + for (int i = cur; i < len; i++) kc_putc(buf[i]); + kc_putc(' '); + kc_cursor_left(len - cur + 1); + } + break; + } + default: + break; + } + continue; + } + + /* ST_NORMAL */ + if (ch == 0x1B) { + state = ST_ESC; + continue; + } + + if (ch == '\n' || ch == '\r') { + kc_putc('\n'); + buf[len] = '\0'; + return len; + } + + if (ch == '\b' || ch == 127) { + if (cur > 0) { + cur--; + memmove(&buf[cur], &buf[cur + 1], (size_t)(len - cur - 1)); + len--; + buf[len] = '\0'; + kc_putc('\b'); + for (int i = cur; i < len; i++) kc_putc(buf[i]); + kc_putc(' '); + kc_cursor_left(len - cur + 1); + } + continue; + } + + /* Ctrl-A = Home, Ctrl-E = End, Ctrl-U = kill line, Ctrl-K = kill to end */ + if (ch == 0x01) { /* Ctrl-A */ + kc_cursor_left(cur); + cur = 0; + continue; + } + if (ch == 0x05) { /* Ctrl-E */ + for (int i = cur; i < len; i++) kc_putc(buf[i]); + cur = len; + continue; + } + if (ch == 0x15) { /* Ctrl-U: kill whole line */ + kc_erase_line(buf, &len, &cur); + continue; + } + if (ch == 0x0B) { /* Ctrl-K: kill to end of line */ + for (int i = cur; i < len; i++) kc_putc(' '); + kc_cursor_left(len - cur); + len = cur; + buf[len] = '\0'; + continue; + } + + if (ch < ' ' || ch > '~') continue; + + /* Insert printable character at cursor */ + if (len >= maxlen - 1) continue; + memmove(&buf[cur + 1], &buf[cur], (size_t)(len - cur)); + buf[cur] = (char)ch; + len++; + buf[len] = '\0'; + for (int i = cur; i < len; i++) kc_putc(buf[i]); + cur++; + kc_cursor_left(len - cur); + } +} + +/* ---- Commands ---- */ + static void kconsole_help(void) { kc_puts("kconsole commands:\n"); kc_puts(" help - Show this list\n"); @@ -123,6 +343,8 @@ static void kconsole_exec(const char* cmd) { } } +/* ---- Main entry ---- */ + void kconsole_enter(void) { keyboard_set_callback(0); @@ -130,37 +352,17 @@ void kconsole_enter(void) { kc_puts("Type 'help' for available commands.\n"); char line[KCMD_MAX]; - int pos = 0; for (;;) { kc_puts("kconsole> "); + kc_readline(line, KCMD_MAX); - pos = 0; - while (pos < KCMD_MAX - 1) { - int ch = kgetc(); - if (ch < 0) continue; - - if (ch == '\n' || ch == '\r') { - console_write("\n"); - break; - } - - if (ch == '\b' || ch == 127) { - if (pos > 0) { - pos--; - console_write("\b \b"); - } - continue; - } - - if (ch < ' ' || ch > '~') continue; - - line[pos++] = (char)ch; - char echo[2] = { (char)ch, 0 }; - console_write(echo); + if (line[0] != '\0') { + hist_add(line); } - line[pos] = '\0'; + klog_set_suppress(1); kconsole_exec(line); + klog_set_suppress(0); } } diff --git a/src/kernel/main.c b/src/kernel/main.c index 94ddf11..8a9cafc 100644 --- a/src/kernel/main.c +++ b/src/kernel/main.c @@ -82,8 +82,8 @@ void kernel_main(const struct boot_info* bi) { int init_ret = init_start(bi); if (init_ret < 0) { - kprintf("\n[PANIC] Userspace init failed — dropping to emergency console.\n"); - kprintf(" (type 'help' for commands, 'reboot' to restart)\n\n"); + console_write("\n[PANIC] Userspace init failed -- dropping to emergency console.\n"); + console_write(" (type 'help' for commands, 'reboot' to restart)\n\n"); kconsole_enter(); }