diff --git a/boredos.iso b/boredos.iso index d9822aa..245bb82 100644 Binary files a/boredos.iso and b/boredos.iso differ diff --git a/build/cmd.o b/build/cmd.o index c94019f..579e164 100644 Binary files a/build/cmd.o and b/build/cmd.o differ diff --git a/src/kernel/cmd.c b/src/kernel/cmd.c index b3329d4..efccf4a 100644 --- a/src/kernel/cmd.c +++ b/src/kernel/cmd.c @@ -37,6 +37,7 @@ typedef struct { char c; uint32_t color; + uint8_t attrs; // bit 0: bold, bit 1: reverse, bit 2: blink } CharCell; typedef enum { @@ -113,6 +114,11 @@ static int ansi_state = 0; static int ansi_params[ANSI_MAX_PARAMS]; static int ansi_param_count = 0; static bool ansi_private_mode = false; +static int saved_cursor_row = 0; +static int saved_cursor_col = 0; +static uint8_t current_attrs = 0; +static bool cursor_visible = true; +static bool terminal_raw_mode = false; // Pager State static CmdMode current_mode = MODE_SHELL; @@ -172,11 +178,22 @@ static void ansi_handle_sgr() { if (ansi_param_count == 0) { current_color = 0xFFFFFFFF; current_bg_color = 0; + current_attrs = 0; return; } for (int i = 0; i < ansi_param_count; i++) { int p = ansi_params[i]; - if (p == 0) { current_color = 0xFFFFFFFF; current_bg_color = 0; } + if (p == 0) { + current_color = 0xFFFFFFFF; + current_bg_color = 0; + current_attrs = 0; + } + else if (p == 1) current_attrs |= 1; // Bold + else if (p == 5) current_attrs |= 4; // Blink + else if (p == 7) current_attrs |= 2; // Reverse + else if (p == 22) current_attrs &= ~1; // Normal intensity + else if (p == 25) current_attrs &= ~4; // Stop blink + else if (p == 27) current_attrs &= ~2; // Normal (not reverse) else if (p >= 30 && p <= 37) current_color = ansi_get_256_color(p - 30); else if (p >= 40 && p <= 47) current_bg_color = ansi_get_256_color(p - 40); else if (p >= 90 && p <= 97) current_color = ansi_get_256_color(p - 90 + 8); @@ -615,6 +632,10 @@ void cmd_putchar(char c) { if (c == '\x1b') { ansi_state = 1; return; + } else if (c == '\x07') { + // BEL - Beep + k_beep(750, 100); + return; } } else if (ansi_state == 1) { if (c == '[') { @@ -623,6 +644,32 @@ void cmd_putchar(char c) { for(int i=0; i 0) { + cursor_row--; + } else { + // Scroll screen down + for (int r = terminal_rows - 1; r > 0; r--) { + for (int col = 0; col < terminal_cols; col++) { + screen_buffer[r * terminal_cols + col] = screen_buffer[(r - 1) * terminal_cols + col]; + } + } + for (int col = 0; col < terminal_cols; col++) { + screen_buffer[0 * terminal_cols + col].c = ' '; + screen_buffer[0 * terminal_cols + col].color = shell_config.default_text_color; + screen_buffer[0 * terminal_cols + col].attrs = 0; + } + } + ansi_state = 0; + return; + } else if (c == '=' || c == '>') { + // ALT/NORM Keypad mode - absorb + ansi_state = 0; + return; } else { ansi_state = 0; // Unsupported ESC sequence } @@ -642,7 +689,7 @@ void cmd_putchar(char c) { // End of sequence if (ansi_param_count < ANSI_MAX_PARAMS) ansi_param_count++; - if (c == 'm') { + if (c == 'm') { ansi_handle_sgr(); } else if (c == 'H' || c == 'f') { int r = ansi_params[0] > 0 ? ansi_params[0] - 1 : 0; @@ -663,20 +710,67 @@ void cmd_putchar(char c) { } else if (c == 'D') { int n = ansi_params[0] > 0 ? ansi_params[0] : 1; cursor_col -= n; if (cursor_col < 0) cursor_col = 0; + } else if (c == 's') { + saved_cursor_row = cursor_row; + saved_cursor_col = cursor_col; + } else if (c == 'u') { + cursor_row = saved_cursor_row; + cursor_col = saved_cursor_col; } else if (c == 'J') { if (ansi_params[0] == 2) { cmd_screen_clear(); + } else if (ansi_params[0] == 0) { + // Erase from cursor to end of screen + for (int i = cursor_row * terminal_cols + cursor_col; i < terminal_rows * terminal_cols; i++) { + screen_buffer[i].c = ' '; + screen_buffer[i].color = current_color; + screen_buffer[i].attrs = 0; + } + } else if (ansi_params[0] == 1) { + // Erase from beginning of screen to cursor + for (int i = 0; i <= cursor_row * terminal_cols + cursor_col; i++) { + screen_buffer[i].c = ' '; + screen_buffer[i].color = current_color; + screen_buffer[i].attrs = 0; + } } } else if (c == 'K') { - for (int i = cursor_col; i < terminal_cols; i++) { - screen_buffer[cursor_row * terminal_cols + i].c = ' '; - screen_buffer[cursor_row * terminal_cols + i].color = current_color; + if (ansi_params[0] == 0) { + for (int i = cursor_col; i < terminal_cols; i++) { + screen_buffer[cursor_row * terminal_cols + i].c = ' '; + screen_buffer[cursor_row * terminal_cols + i].color = current_color; + screen_buffer[cursor_row * terminal_cols + i].attrs = current_attrs; + } + } else if (ansi_params[0] == 1) { + for (int i = 0; i <= cursor_col; i++) { + screen_buffer[cursor_row * terminal_cols + i].c = ' '; + screen_buffer[cursor_row * terminal_cols + i].color = current_color; + screen_buffer[cursor_row * terminal_cols + i].attrs = current_attrs; + } + } else if (ansi_params[0] == 2) { + for (int i = 0; i < terminal_cols; i++) { + screen_buffer[cursor_row * terminal_cols + i].c = ' '; + screen_buffer[cursor_row * terminal_cols + i].color = current_color; + screen_buffer[cursor_row * terminal_cols + i].attrs = current_attrs; + } } + } else if (c == 'h' && ansi_private_mode) { + if (ansi_params[0] == 25) cursor_visible = true; + } else if (c == 'l' && ansi_private_mode) { + if (ansi_params[0] == 25) cursor_visible = false; } ansi_state = 0; return; } + } else if (ansi_state == 3) { + // SS3 sequences: ESC O + if (c == 'A') { cursor_row--; if (cursor_row < 0) cursor_row = 0; } + else if (c == 'B') { cursor_row++; if (cursor_row >= terminal_rows) cursor_row = terminal_rows - 1; } + else if (c == 'C') { cursor_col++; if (cursor_col >= terminal_cols) cursor_col = terminal_cols - 1; } + else if (c == 'D') { cursor_col--; if (cursor_col < 0) cursor_col = 0; } + ansi_state = 0; + return; } if (c == '\n') { @@ -707,7 +801,7 @@ void cmd_putchar(char c) { screen_buffer[cursor_row * terminal_cols + cursor_col].c = c; screen_buffer[cursor_row * terminal_cols + cursor_col].color = current_color; - // Optionally set background color if we support it in CharCell + screen_buffer[cursor_row * terminal_cols + cursor_col].attrs = current_attrs; cursor_col++; } @@ -1787,7 +1881,7 @@ static void cmd_paint(Window *win) { draw_string(start_x, start_y + (terminal_rows * LINE_HEIGHT), "-- Press Q to quit --", shell_config.default_text_color); } else { // Draw Cursor - if (win->focused) { + if (win->focused && cursor_visible) { draw_rect(start_x + (cursor_col * CHAR_WIDTH), start_y + (cursor_row * LINE_HEIGHT), CHAR_WIDTH, LINE_HEIGHT, shell_config.cursor_color); } @@ -1795,14 +1889,27 @@ static void cmd_paint(Window *win) { // Draw Shell Buffer for (int r = 0; r < terminal_rows; r++) { for (int c = 0; c < terminal_cols; c++) { - char ch = screen_buffer[r * terminal_cols + c].c; + CharCell cell = screen_buffer[r * terminal_cols + c]; + char ch = cell.c; if (ch != 0 && ch != ' ') { - uint32_t color = screen_buffer[r * terminal_cols + c].color; - // If cursor is on this character, and cursor color is bright, use background color for char - if (r == cursor_row && c == cursor_col && win->focused) { - color = shell_config.bg_color; + uint32_t fg = cell.color; + uint32_t bg = shell_config.bg_color; + + if (cell.attrs & 2) { // Reverse + uint32_t tmp = fg; + fg = bg; + bg = tmp; + // Draw background for reversed text + draw_rect(start_x + (c * CHAR_WIDTH), start_y + (r * LINE_HEIGHT), + CHAR_WIDTH, LINE_HEIGHT, bg); } - draw_char_bitmap(start_x + (c * CHAR_WIDTH), start_y + (r * LINE_HEIGHT), ch, color); + + // If cursor is on this character, and cursor color is bright, use background color for char + if (r == cursor_row && c == cursor_col && win->focused && cursor_visible) { + fg = bg; // Character takes background color when cursor is over it + } + + draw_char_bitmap(start_x + (c * CHAR_WIDTH), start_y + (r * LINE_HEIGHT), ch, fg); } } } @@ -1893,8 +2000,27 @@ void cmd_handle_click(Window *win, int x, int y) { } } +void cmd_set_raw_mode(bool enabled) { + terminal_raw_mode = enabled; +} + static void cmd_key(Window *target, char c) { (void)target; + + if (terminal_raw_mode) { + // Find the process associated with this terminal and push a key event + extern process_t* process_get_by_ui_window(void* win); + extern void process_push_gui_event(process_t *proc, gui_event_t *ev); + process_t *proc = process_get_by_ui_window(&win_cmd); + if (proc && proc->is_terminal_proc) { + gui_event_t ev; + ev.type = GUI_EVENT_KEY; + ev.arg1 = c; + process_push_gui_event(proc, &ev); + return; + } + } + if (current_mode == MODE_PAGER) { if (c == 'q' || c == 'Q') { current_mode = MODE_SHELL; diff --git a/src/kernel/kutils.c b/src/kernel/kutils.c index 0a3638c..473769e 100644 --- a/src/kernel/kutils.c +++ b/src/kernel/kutils.c @@ -110,3 +110,15 @@ void k_reboot(void) { void k_shutdown(void) { outw(0x604, 0x2000); } + +void k_beep(int freq, int ms) { + if (freq <= 0) return; + int div = 1193180 / freq; + outb(0x43, 0xB6); + outb(0x42, div & 0xFF); + outb(0x42, (div >> 8) & 0xFF); + outb(0x61, inb(0x61) | 0x03); + k_sleep(ms); + outb(0x61, inb(0x61) & 0xFC); +} + diff --git a/src/kernel/kutils.h b/src/kernel/kutils.h index a28d577..ec0f342 100644 --- a/src/kernel/kutils.h +++ b/src/kernel/kutils.h @@ -22,5 +22,6 @@ void k_delay(int iterations); void k_sleep(int ms); void k_reboot(void); void k_shutdown(void); +void k_beep(int freq, int ms); #endif diff --git a/src/kernel/syscall.c b/src/kernel/syscall.c index 0cf22f4..f89c05c 100644 --- a/src/kernel/syscall.c +++ b/src/kernel/syscall.c @@ -907,6 +907,10 @@ static uint64_t syscall_handler_inner(uint64_t syscall_num, uint64_t arg1, uint6 path[i] = 0; graphics_set_font(path); return 0; + } else if (cmd == 41) { // SYSTEM_CMD_SET_RAW_MODE + extern void cmd_set_raw_mode(bool enabled); + cmd_set_raw_mode((bool)arg2); + return 0; } return -1; } diff --git a/src/kernel/userland/libc/syscall.h b/src/kernel/userland/libc/syscall.h index 6f564b0..cd7cb75 100644 --- a/src/kernel/userland/libc/syscall.h +++ b/src/kernel/userland/libc/syscall.h @@ -68,6 +68,7 @@ #define SYSTEM_CMD_DNS_LOOKUP 37 #define SYSTEM_CMD_SET_DNS 38 #define SYSTEM_CMD_NET_UNLOCK 39 +#define SYSTEM_CMD_SET_RAW_MODE 41 // Internal assembly entry into Ring 0 extern uint64_t syscall0(uint64_t sys_num); diff --git a/src/kernel/userland/telnet.c b/src/kernel/userland/telnet.c new file mode 100644 index 0000000..5c096f2 --- /dev/null +++ b/src/kernel/userland/telnet.c @@ -0,0 +1,406 @@ +// Copyright (c) 2023-2026 Chris (boreddevnl) +// This software is released under the GNU General Public License v3.0. See LICENSE file for details. +// This header needs to maintain in any file it is present in, as per the GPL license terms. +#include +#include +#include "libc/libui.h" + + +static int term_cols = 116; +static int term_rows = 41; + +#define IAC 255 +#define DONT 254 +#define DO 253 +#define WONT 252 +#define WILL 251 +#define SB 250 // sub-negotiation begin +#define SE 240 // sub-negotiation end +#define GA 249 +#define EL 248 +#define EC 247 +#define AYT 246 +#define AO 245 +#define IP 244 +#define BRK 243 +#define DM 242 +#define NOP 241 + +// Telnet options +#define OPT_ECHO 1 +#define OPT_SUPPRESS_GA 3 +#define OPT_STATUS 5 +#define OPT_TIMING_MARK 6 +#define OPT_NAWS 31 // Negotiate About Window Size +#define OPT_NEW_ENVIRON 39 +#define OPT_TERMINAL_TYPE 24 + +// ─── IAC send helpers ──────────────────────────────────────────────────────── + +static void telnet_send(const uint8_t *data, int len) { + sys_tcp_send(data, len); +} + +static void telnet_send_3(uint8_t a, uint8_t b, uint8_t c) { + uint8_t buf[3] = { a, b, c }; + telnet_send(buf, 3); +} + +// Send NAWS subnegotiation with current terminal dimensions +static void telnet_send_naws(void) { + uint8_t buf[9]; + buf[0] = IAC; + buf[1] = SB; + buf[2] = OPT_NAWS; + buf[3] = (uint8_t)((term_cols >> 8) & 0xFF); + buf[4] = (uint8_t)(term_cols & 0xFF); + buf[5] = (uint8_t)((term_rows >> 8) & 0xFF); + buf[6] = (uint8_t)(term_rows & 0xFF); + buf[7] = IAC; + buf[8] = SE; + telnet_send(buf, 9); +} + +static void telnet_handle_option(uint8_t cmd, uint8_t opt) { + switch (cmd) { + case DO: + if (opt == OPT_NAWS) { + telnet_send_3(IAC, WILL, OPT_NAWS); + telnet_send_naws(); + } else if (opt == OPT_TERMINAL_TYPE) { + telnet_send_3(IAC, WILL, OPT_TERMINAL_TYPE); + } else { + telnet_send_3(IAC, WONT, opt); + } + break; + + case DONT: + telnet_send_3(IAC, WONT, opt); + break; + + case WILL: + if (opt == OPT_SUPPRESS_GA) { + // Good — accept suppressed GA (most BBS systems do this) + telnet_send_3(IAC, DO, OPT_SUPPRESS_GA); + } else if (opt == OPT_ECHO) { + // Server will echo chars (remote echo) — accept it + telnet_send_3(IAC, DO, OPT_ECHO); + } else { + // Refuse other server offers + telnet_send_3(IAC, DONT, opt); + } + break; + + case WONT: + telnet_send_3(IAC, DONT, opt); + break; + + default: + break; + } +} + +static void telnet_handle_sb_terminal_type(const uint8_t *sb_data, int sb_len) { + if (sb_len < 1 || sb_data[0] != 1) return; // 1 = SEND + uint8_t reply[12]; + int i = 0; + reply[i++] = IAC; + reply[i++] = SB; + reply[i++] = OPT_TERMINAL_TYPE; + reply[i++] = 0; // IS + reply[i++] = 'A'; reply[i++] = 'N'; reply[i++] = 'S'; reply[i++] = 'I'; + reply[i++] = IAC; + reply[i++] = SE; + telnet_send(reply, i); +} + + + +typedef enum { + TS_DATA = 0, + TS_IAC, + TS_CMD, + TS_OPT, + TS_SB, + TS_SB_IAC +} TelnetParseState; + +static TelnetParseState ts_state = TS_DATA; +static uint8_t ts_cmd = 0; +static uint8_t ts_sb_opt = 0; +static uint8_t ts_sb_buf[256]; +static int ts_sb_pos = 0; + +// Output buffer — accumulate non-IAC bytes to write in bulk +static char out_buf[4096]; +static int out_pos = 0; + +static void flush_out(void) { + if (out_pos > 0) { + sys_write(1, out_buf, out_pos); + out_pos = 0; + } +} + +static void out_char(char c) { + if (out_pos >= (int)(sizeof(out_buf) - 1)) { + flush_out(); + } + out_buf[out_pos++] = c; +} + +// Process a chunk of raw TCP data from server. Returns false if connection lost. +static int telnet_process(const uint8_t *data, int len) { + for (int i = 0; i < len; i++) { + uint8_t b = data[i]; + + switch (ts_state) { + case TS_DATA: + if (b == IAC) { + ts_state = TS_IAC; + } else { + // Pass directly to display + out_char((char)b); + } + break; + + case TS_IAC: + switch (b) { + case IAC: + // Escaped IAC — literal 0xFF + out_char((char)0xFF); + ts_state = TS_DATA; + break; + case DO: case DONT: case WILL: case WONT: + ts_cmd = b; + ts_state = TS_OPT; + break; + case SB: + ts_sb_pos = 0; + ts_state = TS_CMD; + break; + case GA: case NOP: case DM: case BRK: case IP: + case AO: case AYT: case EC: case EL: + ts_state = TS_DATA; + break; + default: + ts_state = TS_DATA; + break; + } + break; + + case TS_CMD: + ts_sb_opt = b; + ts_sb_pos = 0; + ts_state = TS_SB; + break; + + case TS_OPT: + flush_out(); + telnet_handle_option(ts_cmd, b); + ts_state = TS_DATA; + break; + + case TS_SB: + if (b == IAC) { + ts_state = TS_SB_IAC; + } else { + if (ts_sb_pos < (int)sizeof(ts_sb_buf) - 1) { + ts_sb_buf[ts_sb_pos++] = b; + } + } + break; + + case TS_SB_IAC: + if (b == SE) { + flush_out(); + if (ts_sb_opt == OPT_TERMINAL_TYPE) { + telnet_handle_sb_terminal_type(ts_sb_buf, ts_sb_pos); + } + ts_state = TS_DATA; + } else if (b == IAC) { + if (ts_sb_pos < (int)sizeof(ts_sb_buf) - 1) { + ts_sb_buf[ts_sb_pos++] = IAC; + } + ts_state = TS_SB; + } else { + ts_state = TS_DATA; + } + break; + } + } + flush_out(); + return 1; +} + + +static int map_key(char c, uint8_t *key_out) { + if (c == 29) { + // Ctrl+] + return -1; + } + if (c == 17) { + // UP arrow + key_out[0] = 0x1b; key_out[1] = '['; key_out[2] = 'A'; + return 3; + } + if (c == 18) { + // DOWN arrow + key_out[0] = 0x1b; key_out[1] = '['; key_out[2] = 'B'; + return 3; + } + if (c == 20) { + // RIGHT arrow + key_out[0] = 0x1b; key_out[1] = '['; key_out[2] = 'C'; + return 3; + } + if (c == 19) { + // LEFT arrow + key_out[0] = 0x1b; key_out[1] = '['; key_out[2] = 'D'; + return 3; + } + if (c == '\n') { + // Enter + key_out[0] = '\r'; key_out[1] = '\n'; + return 2; + } + if (c == '\b') { + // Backspace + key_out[0] = '\x7f'; + return 1; + } + // Normal printable character + key_out[0] = (uint8_t)c; + return 1; +} + +static int my_atoi(const char *s) { + int v = 0; + while (*s >= '0' && *s <= '9') { v = v*10 + (*s - '0'); s++; } + return v; +} + +static int parse_ip(const char *s, net_ipv4_address_t *ip) { + int part = 0, val = 0; + while (*s) { + if (*s >= '0' && *s <= '9') { + val = val*10 + (*s - '0'); + if (val > 255) return -1; + } else if (*s == '.') { + if (part > 3) return -1; + ip->bytes[part++] = (uint8_t)val; + val = 0; + } else { + return -1; + } + s++; + } + if (part != 3) return -1; + ip->bytes[3] = (uint8_t)val; + return 0; +} + +int main(int argc, char **argv) { + if (argc < 2) { + printf("Usage: telnet [port]\n"); + printf(" Connect to a Telnet BBS or server.\n"); + printf(" Default port: 23\n"); + printf(" Press Ctrl+] to disconnect.\n"); + return 1; + } + + const char *host = argv[1]; + int port = (argc >= 3) ? my_atoi(argv[2]) : 23; + if (port <= 0 || port > 65535) port = 23; + + if (!sys_network_is_initialized()) { + printf("Initializing network...\n"); + sys_network_init(); + } + if (!sys_network_has_ip()) { + printf("Acquiring DHCP...\n"); + if (sys_network_dhcp_acquire() != 0) { + printf("DHCP failed.\n"); + return 1; + } + } + + net_ipv4_address_t ip; + if (parse_ip(host, &ip) != 0) { + printf("Resolving %s...\n", host); + if (sys_dns_lookup(host, &ip) != 0) { + printf("Failed to resolve: %s\n", host); + return 1; + } + } + + printf("Connecting to %s:%d...\n", host, port); + if (sys_tcp_connect(&ip, (uint16_t)port) != 0) { + printf("Connection failed.\n"); + return 1; + } + printf("Connected. Press Ctrl+] to disconnect.\n\n"); + + sys_system(41, 1, 0, 0, 0); // SYSTEM_CMD_SET_RAW_MODE = 1 + + uint8_t recv_buf[4096]; + int total = 0; + int idle_count = 0; + int connected = 1; + + while (connected) { + gui_event_t ev; + while (ui_get_event(0, &ev)) { // win=0 for console proc + if (ev.type == GUI_EVENT_KEY) { + uint8_t key_data[16]; + int key_len = map_key((char)ev.arg1, key_data); + if (key_len < 0) { + connected = 0; + break; + } + telnet_send(key_data, key_len); + } + } + if (!connected) break; + + int len = sys_tcp_recv(recv_buf, sizeof(recv_buf) - 1); + if (len < 0) { + printf("\r\n[Connection error]\r\n"); + connected = 0; + break; + } + if (len == 0) { + idle_count++; + // Don't timeout too fast if we are just waiting for user input + if (idle_count > 10000000) { + printf("\r\n[Connection timed out]\r\n"); + connected = 0; + break; + } + // Brief spin + for (volatile int d = 0; d < 100; d++); + continue; + } + + idle_count = 0; + total += len; + + if (total > 10000000) { + printf("\r\n[Data limit reached]\r\n"); + connected = 0; + break; + } + + if (!telnet_process(recv_buf, len)) { + connected = 0; + } + } + + // Disable raw mode before exiting + sys_system(41, 0, 0, 0, 0); + + sys_tcp_close(); + printf("\r\n[Telnet session ended]\r\n"); + return 0; +} +