From 6162378175fa4d5298bf1bb9b0148cdbdc742397 Mon Sep 17 00:00:00 2001 From: Ox03bb Date: Tue, 22 Sep 2026 15:38:22 +0100 Subject: [PATCH 01/13] update readme file --- README.md | 70 +++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 66 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 16cdd1b..fe52171 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,11 @@ +[![Assembly](https://img.shields.io/badge/x86%20Assembly-6e4c13?style=flat&logo=assembly&logoColor=white)](https://www.nasm.us/) +[![C](https://img.shields.io/badge/-C/C+-142282?style=flat&logo=C&logoColor=white)](https://en.cppreference.com/w/c) +[![QEMU](https://img.shields.io/badge/-QEMU-FF6600?style=flat&logo=qemu&logoColor=white)](https://www.qemu.org/) +[![GDB](https://img.shields.io/badge/-GDB-4EAA25?style=flat&logo=gnu&logoColor=white)](https://www.gnu.org/software/gdb/) # kernel-from-scratch An educational kernel built completely from scratch to explore low-level systems programming, memory management, interrupts, scheduling, and hardware interaction. - - -# Required Tools: +## Required Tools: - [QEMU](https://www.qemu.org/) - An open-source emulator that allows you to run the kernel in a virtual environment. - [GCC](https://gcc.gnu.org/) - The GNU Compiler Collection, used to compile the kernel code. - [i686-elf-gcc](https://gcc.gnu.org/) - A cross-compiler for the i686 architecture, used to compile the kernel code for the target architecture. @@ -13,6 +15,38 @@ An educational kernel built completely from scratch to explore low-level systems - [GDB](https://www.gnu.org/software/gdb/) - The GNU Debugger, used for debugging the kernel during development. - [PWNdbg](https://github.com/pwndbg/pwndbg) - A TUI debugger for GDB, providing an enhanced debugging experience. +## Project Structure +``` +. +├── conf.gdb +├── Dockerfile +├── linker.ld +├── Makefile +├── README.md +├── bin/ +├── build/ +├── docs/ +├── includes/ +│ ├── kernel.h +│ ├── panic.h +│ ├── timer.h +│ ├── driver/ +│ ├── interrupt/ +│ ├── lib/ +│ └── utils/ +├── scripts/ +│ └── requirements.sh +├── src/ +│ ├── bootloader/ +│ ├── driver/ +│ ├── interrupt/ +│ ├── kernel/ +│ ├── libs/ +│ └── utils/ +└── test/ +``` + + To install all the required tools, you can use the following commands: * **if u'r using Linux:** @@ -24,4 +58,32 @@ To install all the required tools, you can use the following commands: 1. Uninstall windows 2. Install linux - 3. Run the above command \ No newline at end of file + 3. Run the above command + +## Getting Started +- clone the repository and navigate to the project directory: + +```bash + git clone https://github.com/Ox03bb/kernel-from-scratch.git + cd kernel-from-scratch +``` + +- install the required tools using the provided script: + +```bash + sh ./scripts/install.sh +``` + +- build the kernel using the Makefile: + +```bash + make setup # to setup the build environment(build docker image...etc) && build the kernel + # or + make build # if u'r already setup the build environment +``` + +- run the kernel in QEMU: + +```bash + make run +``` \ No newline at end of file From 8bd708fca8031c14272f03b069db096459aa20d1 Mon Sep 17 00:00:00 2001 From: Ox03bb Date: Tue, 22 Sep 2026 17:19:49 +0100 Subject: [PATCH 02/13] fix: update code based on pr review --- includes/driver/keyboard/event_queue.h | 6 +- includes/driver/keyboard/keyboard.h | 1 + includes/driver/vga.h | 24 ++++++- includes/lib/string.h | 2 +- src/driver/keyboard/event_queue.c | 42 ++++++++++-- src/driver/keyboard/handler.c | 1 - src/driver/keyboard/keyboard.c | 49 ++++++++++---- src/driver/keyboard/layout/us.c | 5 +- src/driver/ps2.c | 20 ++++-- src/driver/vga.c | 89 +++++++++++++++++++++++++- src/kernel/kernel.c | 26 ++++++-- src/libs/stdio.c | 68 ++++++++++++++++---- src/libs/string.c | 23 ++++--- 13 files changed, 292 insertions(+), 64 deletions(-) diff --git a/includes/driver/keyboard/event_queue.h b/includes/driver/keyboard/event_queue.h index bc5ae83..4c62afa 100644 --- a/includes/driver/keyboard/event_queue.h +++ b/includes/driver/keyboard/event_queue.h @@ -16,6 +16,10 @@ typedef struct { extern keyboard_event_queue_t keyboard_queue; +void keyboard_queue_push(key_event_t event); +bool keyboard_queue_pop(key_event_t *event); +void keyboard_queue_clear(void); + typedef enum { INPUT_CHAR, INPUT_ENTER, @@ -33,7 +37,7 @@ typedef struct { } input_event_t; void input_queue_push(input_event_t event); -void input_queue_pop(input_event_t *event); +bool input_queue_pop(input_event_t *event); void input_queue_print(); void input_queue_print_last(); void input_queue_clear(); diff --git a/includes/driver/keyboard/keyboard.h b/includes/driver/keyboard/keyboard.h index 51842ef..48e6be3 100644 --- a/includes/driver/keyboard/keyboard.h +++ b/includes/driver/keyboard/keyboard.h @@ -16,6 +16,7 @@ typedef enum { void init_keyboard(void); void reset_keyboard(void); +void set_scanning(bool state); void set_scaning(bool state); void select_scancode_set(uint8_t set); void keyboard_set_layout(const keyboard_layout_t *layout); diff --git a/includes/driver/vga.h b/includes/driver/vga.h index 7b90c93..1ddff0b 100644 --- a/includes/driver/vga.h +++ b/includes/driver/vga.h @@ -28,7 +28,7 @@ #define BROWN 0x06 // YELLOW #define LIGHT_GRAY 0x07 // WHITE -#define DARK_GRAY BLACk | LIGHT_FLAG +#define DARK_GRAY BLACK | LIGHT_FLAG #define LIGHT_BLUE BLUE | LIGHT_FLAG #define LIGHT_GREEN GREEN | LIGHT_FLAG #define LIGHT_CYAN CYAN | LIGHT_FLAG @@ -83,4 +83,26 @@ uint8_t vga_get_cursor_x(void); uint8_t vga_get_cursor_y(void); +void vga_put_at(uint8_t x, uint8_t y, char c, uint8_t color); +void vga_clear_region( + uint8_t x, + uint8_t y, + uint8_t width, + uint8_t height, + uint8_t color +); + +void vga_print_at( + uint8_t x, + uint8_t y, + const char *str, + uint8_t color +); +void vga_print_at_center( + uint8_t y, + const char *str, + uint8_t color +); + + #endif \ No newline at end of file diff --git a/includes/lib/string.h b/includes/lib/string.h index 004e7dd..a75f1fa 100644 --- a/includes/lib/string.h +++ b/includes/lib/string.h @@ -3,7 +3,7 @@ #include "types.h" -int strlen(char *str); +int strlen(const char *str); void strcpy(char *dest, char *src); void strcat(char *dest, const char *src); diff --git a/src/driver/keyboard/event_queue.c b/src/driver/keyboard/event_queue.c index 5d87d42..0c1853e 100644 --- a/src/driver/keyboard/event_queue.c +++ b/src/driver/keyboard/event_queue.c @@ -3,10 +3,37 @@ // Input event queue +keyboard_event_queue_t keyboard_queue = {0}; input_event_t input_queue[KEYBOARD_EVENT_QUEUE_SIZE]; uint32_t input_queue_head = 0; uint32_t input_queue_tail = 0; +void keyboard_queue_push(key_event_t event) { + uint32_t next = (keyboard_queue.head + 1) % KEYBOARD_EVENT_QUEUE_SIZE; + + if (next == keyboard_queue.tail) { + return; + } + + keyboard_queue.events[keyboard_queue.head] = event; + keyboard_queue.head = next; +} + +bool keyboard_queue_pop(key_event_t *event) { + if (keyboard_queue.head == keyboard_queue.tail) { + return false; + } + + *event = keyboard_queue.events[keyboard_queue.tail]; + keyboard_queue.tail = (keyboard_queue.tail + 1) % KEYBOARD_EVENT_QUEUE_SIZE; + return true; +} + +void keyboard_queue_clear(void) { + keyboard_queue.head = 0; + keyboard_queue.tail = 0; +} + void input_queue_push(input_event_t event) { uint32_t next = (input_queue_head + 1) % KEYBOARD_EVENT_QUEUE_SIZE; @@ -18,20 +45,25 @@ void input_queue_push(input_event_t event) { input_queue_head = next; } -void input_queue_pop(input_event_t *event) { +bool input_queue_pop(input_event_t *event) { if (input_queue_head == input_queue_tail) { - return; /* queue empty */ + if (event != NULL) { + *event = (input_event_t){.type = (input_type_t)-1, .character = '\0'}; + } + return false; /* queue empty */ } *event = input_queue[input_queue_tail]; - input_queue_tail = (input_queue_tail + 1) % KEYBOARD_EVENT_QUEUE_SIZE; + return true; } void input_queue_print() { printf("\033[32mInput Queue: \033[0m head=%d, tail=%d\n", input_queue_head, input_queue_tail); - for (uint32_t i = 0; i < input_queue_head - input_queue_tail; i++) { - input_event_t event = input_queue[i]; + uint32_t count = (input_queue_head - input_queue_tail + KEYBOARD_EVENT_QUEUE_SIZE) % KEYBOARD_EVENT_QUEUE_SIZE; + for (uint32_t i = 0; i < count; i++) { + uint32_t index = (input_queue_tail + i) % KEYBOARD_EVENT_QUEUE_SIZE; + input_event_t event = input_queue[index]; printf("Event[%d]: type=%d, character=%c\n", i, event.type, event.character); } } diff --git a/src/driver/keyboard/handler.c b/src/driver/keyboard/handler.c index baaf715..37d0079 100644 --- a/src/driver/keyboard/handler.c +++ b/src/driver/keyboard/handler.c @@ -18,7 +18,6 @@ void keyboard_irq_handler(void) { } uint8_t data = ps2_read(); - keyboard_process_scancode(data); } diff --git a/src/driver/keyboard/keyboard.c b/src/driver/keyboard/keyboard.c index f325aa6..2d20fc3 100644 --- a/src/driver/keyboard/keyboard.c +++ b/src/driver/keyboard/keyboard.c @@ -10,9 +10,9 @@ static const keyboard_layout_t *active_layout; void init_keyboard(void) { reset_keyboard(); - set_scaning(false); + set_scanning(false); select_scancode_set(0x02); - set_scaning(true); + set_scanning(true); ps2_flush_output(); @@ -44,9 +44,12 @@ void reset_keyboard(void) { } } -void set_scaning(bool state) { +void set_scanning(bool state) { ps2_flush_output(); - ps2_wait_input_clear(); + if (!ps2_wait_input_clear()) { + printf("\033[31mERROR:\033[0m Keyboard input timeout while toggling scanning.\n"); + return; + } if (state) { ps2_write(PS2_DC_ENABLE_SCANNING); @@ -54,23 +57,36 @@ void set_scaning(bool state) { ps2_write(PS2_DC_DISABLE_SCANNING); } - ps2_wait_output_full(); + if (!ps2_wait_output_full()) { + printf("\033[31mERROR:\033[0m Keyboard scan ack timeout.\n"); + return; + } if (ps2_read() != K_ACK) { printf("\033[31mERROR:\033[0m Keyboard did not acknowledge scanning command.\n"); - return; } } +void set_scaning(bool state) { set_scanning(state); } + void select_scancode_set(uint8_t set) { - ps2_flush_output(); - ps2_wait_input_clear(); + if (set > 3) { + printf("\033[31mERROR:\033[0m Invalid scancode set %u.\n", set); + return; + } - if (set <= 3) { - ps2_write(PS2_DC_SELECT_SET); + ps2_flush_output(); + if (!ps2_wait_input_clear()) { + printf("\033[31mERROR:\033[0m Keyboard input timeout before scancode set select.\n"); + return; } - ps2_wait_output_full(); + ps2_write(PS2_DC_SELECT_SET); + + if (!ps2_wait_output_full()) { + printf("\033[31mERROR:\033[0m Keyboard ack timeout while selecting scancode set.\n"); + return; + } if (ps2_read() != K_ACK) { printf("\033[31mERROR:\033[0m keyboard.h:53 select_scancode_set .\n"); @@ -78,14 +94,19 @@ void select_scancode_set(uint8_t set) { } ps2_flush_output(); - ps2_wait_input_clear(); + if (!ps2_wait_input_clear()) { + printf("\033[31mERROR:\033[0m Keyboard input timeout before scancode value.\n"); + return; + } ps2_write(set); - ps2_wait_output_full(); + if (!ps2_wait_output_full()) { + printf("\033[31mERROR:\033[0m Keyboard ack timeout after scancode value.\n"); + return; + } if (ps2_read() != K_ACK) { printf("\033[31mERROR:\033[0m keyboard.h:65 select_scancode_set .\n"); - return; } } diff --git a/src/driver/keyboard/layout/us.c b/src/driver/keyboard/layout/us.c index b334008..3222b41 100644 --- a/src/driver/keyboard/layout/us.c +++ b/src/driver/keyboard/layout/us.c @@ -103,7 +103,7 @@ const keyboard_layout_t keyboard_layout_us = { char keyboard_layout_translate(const keyboard_layout_t *layout, keycode_t key, const keyboard_state_t *state) { - if (key >= KEY_COUNT) { + if (layout == NULL || key >= KEY_COUNT || state == NULL) { return '\0'; } @@ -114,8 +114,9 @@ char keyboard_layout_translate(const keyboard_layout_t *layout, keycode_t key, } bool shifted = state->left_shift || state->right_shift; + bool caps_active = state->caps_lock && ((key >= KEY_A && key <= KEY_Z) || (key >= KEY_0 && key <= KEY_9)); - if (shifted) { + if (shifted ^ caps_active) { return entry.shift; } diff --git a/src/driver/ps2.c b/src/driver/ps2.c index 561de21..2f94517 100644 --- a/src/driver/ps2.c +++ b/src/driver/ps2.c @@ -22,16 +22,24 @@ bool ps2_parity_error(void) { return ps2_status().parity_error; } bool ps2_zero(void) { return ps2_status().zero; } +#define PS2_WAIT_MAX 100000 + bool ps2_wait_input_clear(void) { - while (ps2_input_buffer_full()) - ; - return true; + for (uint32_t i = 0; i < PS2_WAIT_MAX; i++) { + if (!ps2_input_buffer_full()) { + return true; + } + } + return false; } bool ps2_wait_output_full(void) { - while (!ps2_output_buffer_full()) - ; - return true; + for (uint32_t i = 0; i < PS2_WAIT_MAX; i++) { + if (ps2_output_buffer_full()) { + return true; + } + } + return false; } void ps2_flush_output(void) { diff --git a/src/driver/vga.c b/src/driver/vga.c index 0fe1104..b8ce31e 100644 --- a/src/driver/vga.c +++ b/src/driver/vga.c @@ -1,4 +1,5 @@ #include "vga.h" +#include "string.h" #include "utils.h" static uint8_t cursor_x = 0; @@ -100,9 +101,21 @@ void vga_print_char(char c) { break; default: + if (cursor_position >= VGA_RES) { + vga_scroll_up(); + cursor_position = (VGA_HEIGHT - 1) * VGA_WIDTH; + } + vga_buf[cursor_position] = ((uint16_t)current_color << 8) | (uint8_t)c; - vga_set_cursor_position(cursor_position + 1); + if (cursor_position + 1 >= VGA_RES) { + vga_scroll_up(); + cursor_position = (VGA_HEIGHT - 1) * VGA_WIDTH; + } else { + cursor_position++; + } + + vga_set_cursor_position(cursor_position); break; } } @@ -172,6 +185,9 @@ void vga_set_cursor(uint8_t x, uint8_t y) { } void vga_set_cursor_position(uint16_t p) { + if (p >= VGA_RES) { + p = VGA_RES - 1; + } outb(VGA_INDEX_PORT, VGA_CURSOR_HIGH); outb(VGA_DATA_PORT, (p >> 8)); @@ -243,3 +259,74 @@ void vga_scroll_up(void) { vga_set_cursor(0, VGA_HEIGHT - 1); } + + +// update specific position + +void vga_put_at(uint8_t x, uint8_t y, char c, uint8_t color){ + uint16_t index = y * VGA_WIDTH + x; + + vga_buf[index] = + ((uint16_t)color << 8) | (uint8_t)c; +} + +void vga_clear_region( + uint8_t x, + uint8_t y, + uint8_t width, + uint8_t height, + uint8_t color +) +{ + for (uint8_t row = 0; row < height; row++) { + for (uint8_t col = 0; col < width; col++) { + vga_put_at( + x + col, + y + row, + ' ', + color + ); + } + } +} + +void vga_print_at( + uint8_t x, + uint8_t y, + const char *str, + uint8_t color +) +{ + for (uint32_t i = 0; str[i] != '\0'; i++) { + vga_put_at( + x + i, + y, + str[i], + color + ); + } +} + +void vga_print_at_center( + uint8_t y, + const char *str, + uint8_t color +) +{ + size_t len = strlen(str); + if (len > VGA_WIDTH) { + len = VGA_WIDTH; + } + + uint8_t x = (VGA_WIDTH - len) / 2; + + for (size_t i = 0; i < len; i++) { + vga_put_at( + x + i, + y, + str[i], + color + ); + } +} + \ No newline at end of file diff --git a/src/kernel/kernel.c b/src/kernel/kernel.c index dfda71f..1cb38d2 100644 --- a/src/kernel/kernel.c +++ b/src/kernel/kernel.c @@ -62,19 +62,31 @@ void kernel_main() { while (1) { input_event_t event; - input_queue_pop(&event); + if (!input_queue_pop(&event)) { + asm volatile("hlt"); + continue; + } if (event.type == INPUT_CHAR) { print("Character: "); print_char(event.character); print("\n"); - event = (input_event_t){0}; - event.type = 11; - } - } - for (;;) { - asm volatile("hlt"); + } else if (event.type == INPUT_ENTER) { + print("Enter key pressed\n"); + } else if (event.type == INPUT_BACKSPACE) { + print("Backspace key pressed\n"); + } else if (event.type == INPUT_TAB) { + print("Tab key pressed\n"); + } else if (event.type == INPUT_UP) { + print("Up arrow key pressed\n"); + } else if (event.type == INPUT_DOWN) { + print("Down arrow key pressed\n"); + } else if (event.type == INPUT_LEFT) { + print("Left arrow key pressed\n"); + } else if (event.type == INPUT_RIGHT) { + print("Right arrow key pressed\n"); + } } return; diff --git a/src/libs/stdio.c b/src/libs/stdio.c index fa49dad..4ba60c6 100644 --- a/src/libs/stdio.c +++ b/src/libs/stdio.c @@ -133,68 +133,110 @@ char *formatter(const char *format, ...) { char *vformatter(const char *format, va_list args) { buffer[0] = '\0'; + size_t used = 0; while (*format) { - if (*format != '%') { char temp[2] = {*format, '\0'}; - strcat(buffer, temp); - + size_t len = strlen(temp); + if (used + len + 1 < sizeof(buffer)) { + strcat(buffer, temp); + used += len; + } format++; continue; } format++; + if (*format == '\0') { + if (used + 1 < sizeof(buffer)) { + buffer[used++] = '%'; + buffer[used] = '\0'; + } + break; + } switch (*format) { case 's': { const char *str = va_arg(args, const char *); - strcat(buffer, str); + size_t len = strlen(str); + if (used + len + 1 < sizeof(buffer)) { + strcat(buffer, str); + used += len; + } break; } case 'd': { int number = va_arg(args, int); - strcat(buffer, int2str(number)); + char *str = int2str(number); + size_t len = strlen(str); + if (used + len + 1 < sizeof(buffer)) { + strcat(buffer, str); + used += len; + } break; } case 'x': { uint32_t number = va_arg(args, uint32_t); - strcat(buffer, uint2str(number, 16, false)); + char *str = uint2str(number, 16, false); + size_t len = strlen(str); + if (used + len + 1 < sizeof(buffer)) { + strcat(buffer, str); + used += len; + } break; } case 'h': { uint32_t number = va_arg(args, uint32_t); - strcat(buffer, uint2str(number, 16, true)); + char *str = uint2str(number, 16, true); + size_t len = strlen(str); + if (used + len + 1 < sizeof(buffer)) { + strcat(buffer, str); + used += len; + } break; } case 'b': { uint32_t number = va_arg(args, uint32_t); - strcat(buffer, uint2str(number, 2, false)); + char *str = uint2str(number, 2, false); + size_t len = strlen(str); + if (used + len + 1 < sizeof(buffer)) { + strcat(buffer, str); + used += len; + } break; } case 'c': { char temp[2]; - temp[0] = (char)va_arg(args, int); temp[1] = '\0'; - - strcat(buffer, temp); + size_t len = strlen(temp); + if (used + len + 1 < sizeof(buffer)) { + strcat(buffer, temp); + used += len; + } break; } case '%': { - strcat(buffer, "%"); + if (used + 2 < sizeof(buffer)) { + strcat(buffer, "%"); + used++; + } break; } default: { - strcat(buffer, "%"); + if (used + 2 < sizeof(buffer)) { + strcat(buffer, "%"); + used++; + } break; } } diff --git a/src/libs/string.c b/src/libs/string.c index 4911539..ed8d14a 100644 --- a/src/libs/string.c +++ b/src/libs/string.c @@ -2,7 +2,7 @@ #include "types.h" #include -int strlen(char *str) { +int strlen(const char *str) { int len = 0; while (*str++) { len++; @@ -14,24 +14,23 @@ char *int2str(int num) { static char str[33]; char *p = str + 32; - *p = '\0'; if (num == 0) { *(--p) = '0'; - } else { - bool negative = num < 0; + return p; + } - if (negative) - num = -num; + bool negative = num < 0; + uint32_t value = (uint32_t)(negative ? -(num + 1) + 1 : num); - while (num > 0) { - *(--p) = (num % 10) + '0'; - num /= 10; - } + while (value > 0) { + *(--p) = (char)('0' + (value % 10)); + value /= 10; + } - if (negative) - *(--p) = '-'; + if (negative) { + *(--p) = '-'; } return p; From a6b710e5be2dd931088d2ca5a72ce300699e98e7 Mon Sep 17 00:00:00 2001 From: Ox03bb Date: Tue, 22 Sep 2026 19:38:25 +0100 Subject: [PATCH 03/13] refactor: simplify the keyboard abstraction layer --- includes/driver/keyboard/event_queue.h | 48 +++++++-------- src/driver/keyboard/event_queue.c | 84 +++++++++++--------------- src/driver/keyboard/handler.c | 66 +++++++------------- src/kernel/kernel.c | 20 +++--- 4 files changed, 90 insertions(+), 128 deletions(-) diff --git a/includes/driver/keyboard/event_queue.h b/includes/driver/keyboard/event_queue.h index 4c62afa..91226c5 100644 --- a/includes/driver/keyboard/event_queue.h +++ b/includes/driver/keyboard/event_queue.h @@ -6,42 +6,38 @@ #include "ps2.h" #include "types.h" -#define KEYBOARD_EVENT_QUEUE_SIZE 256 - -typedef struct { - key_event_t events[KEYBOARD_EVENT_QUEUE_SIZE]; - uint32_t head; - uint32_t tail; -} keyboard_event_queue_t; - -extern keyboard_event_queue_t keyboard_queue; - -void keyboard_queue_push(key_event_t event); -bool keyboard_queue_pop(key_event_t *event); -void keyboard_queue_clear(void); +#define INPUT_EVENT_QUEUE_SIZE 256 typedef enum { - INPUT_CHAR, - INPUT_ENTER, - INPUT_BACKSPACE, - INPUT_TAB, - INPUT_UP, - INPUT_DOWN, - INPUT_LEFT, - INPUT_RIGHT, -} input_type_t; + INPUT_EVENT_KEY, + INPUT_EVENT_CHAR, +} input_event_type_t; typedef struct { - input_type_t type; + input_event_type_t type; + keycode_t key; + key_action_t action; + bool shift; + bool ctrl; + bool alt; char character; } input_event_t; +typedef struct { + input_event_t events[INPUT_EVENT_QUEUE_SIZE]; + uint32_t head; + uint32_t tail; +} input_event_queue_t; + +extern input_event_queue_t input_event_queue; + +void input_queue_init(void); void input_queue_push(input_event_t event); bool input_queue_pop(input_event_t *event); +bool input_queue_is_empty(void); +bool input_queue_is_full(void); void input_queue_print(); void input_queue_print_last(); -void input_queue_clear(); - -extern input_event_t input_queue[KEYBOARD_EVENT_QUEUE_SIZE]; +void input_queue_clear(void); #endif \ No newline at end of file diff --git a/src/driver/keyboard/event_queue.c b/src/driver/keyboard/event_queue.c index 0c1853e..ab2bbcd 100644 --- a/src/driver/keyboard/event_queue.c +++ b/src/driver/keyboard/event_queue.c @@ -1,86 +1,70 @@ #include "event_queue.h" #include "stdio.h" -// Input event queue +input_event_queue_t input_event_queue = {0}; -keyboard_event_queue_t keyboard_queue = {0}; -input_event_t input_queue[KEYBOARD_EVENT_QUEUE_SIZE]; -uint32_t input_queue_head = 0; -uint32_t input_queue_tail = 0; - -void keyboard_queue_push(key_event_t event) { - uint32_t next = (keyboard_queue.head + 1) % KEYBOARD_EVENT_QUEUE_SIZE; - - if (next == keyboard_queue.tail) { - return; - } - - keyboard_queue.events[keyboard_queue.head] = event; - keyboard_queue.head = next; -} - -bool keyboard_queue_pop(key_event_t *event) { - if (keyboard_queue.head == keyboard_queue.tail) { - return false; - } - - *event = keyboard_queue.events[keyboard_queue.tail]; - keyboard_queue.tail = (keyboard_queue.tail + 1) % KEYBOARD_EVENT_QUEUE_SIZE; - return true; -} - -void keyboard_queue_clear(void) { - keyboard_queue.head = 0; - keyboard_queue.tail = 0; +void input_queue_init(void) { + input_event_queue.head = 0; + input_event_queue.tail = 0; } void input_queue_push(input_event_t event) { - uint32_t next = (input_queue_head + 1) % KEYBOARD_EVENT_QUEUE_SIZE; + uint32_t next = (input_event_queue.head + 1) % INPUT_EVENT_QUEUE_SIZE; - if (next == input_queue_tail) { + if (next == input_event_queue.tail) { return; /* queue full */ } - input_queue[input_queue_head] = event; - input_queue_head = next; + input_event_queue.events[input_event_queue.head] = event; + input_event_queue.head = next; } bool input_queue_pop(input_event_t *event) { - if (input_queue_head == input_queue_tail) { + if (input_event_queue.head == input_event_queue.tail) { if (event != NULL) { - *event = (input_event_t){.type = (input_type_t)-1, .character = '\0'}; + *event = (input_event_t){0}; } return false; /* queue empty */ } - *event = input_queue[input_queue_tail]; - input_queue_tail = (input_queue_tail + 1) % KEYBOARD_EVENT_QUEUE_SIZE; + if (event != NULL) { + *event = input_event_queue.events[input_event_queue.tail]; + } + + input_event_queue.tail = (input_event_queue.tail + 1) % INPUT_EVENT_QUEUE_SIZE; return true; } +bool input_queue_is_empty(void) { + return input_event_queue.head == input_event_queue.tail; +} + +bool input_queue_is_full(void) { + return ((input_event_queue.head + 1) % INPUT_EVENT_QUEUE_SIZE) == input_event_queue.tail; +} + void input_queue_print() { - printf("\033[32mInput Queue: \033[0m head=%d, tail=%d\n", input_queue_head, input_queue_tail); - uint32_t count = (input_queue_head - input_queue_tail + KEYBOARD_EVENT_QUEUE_SIZE) % KEYBOARD_EVENT_QUEUE_SIZE; + printf("\033[32mInput Queue: \033[0m head=%d, tail=%d\n", input_event_queue.head, input_event_queue.tail); + uint32_t count = (input_event_queue.head - input_event_queue.tail + INPUT_EVENT_QUEUE_SIZE) % INPUT_EVENT_QUEUE_SIZE; for (uint32_t i = 0; i < count; i++) { - uint32_t index = (input_queue_tail + i) % KEYBOARD_EVENT_QUEUE_SIZE; - input_event_t event = input_queue[index]; - printf("Event[%d]: type=%d, character=%c\n", i, event.type, event.character); + uint32_t index = (input_event_queue.tail + i) % INPUT_EVENT_QUEUE_SIZE; + input_event_t event = input_event_queue.events[index]; + printf("Event[%d]: key=%d action=%d char=%c\n", i, event.key, event.action, event.character); } } void input_queue_print_last() { - if (input_queue_head == input_queue_tail) { + if (input_queue_is_empty()) { printf("\033[31mInput Queue: \033[0m Empty\n"); return; } - uint32_t last = (input_queue_head - 1 + KEYBOARD_EVENT_QUEUE_SIZE) % KEYBOARD_EVENT_QUEUE_SIZE; - input_event_t event = input_queue[last]; - printf("\033[32mInput Queue:\033[0m Last event[%d] : type=%d, character=%c\n", last, event.type, + uint32_t last = (input_event_queue.head - 1 + INPUT_EVENT_QUEUE_SIZE) % INPUT_EVENT_QUEUE_SIZE; + input_event_t event = input_event_queue.events[last]; + printf("\033[32mInput Queue:\033[0m Last event key=%d action=%d char=%c\n", event.key, event.action, event.character); } -void input_queue_clear() { - input_queue_head = 0; - input_queue_tail = 0; +void input_queue_clear(void) { + input_queue_init(); } diff --git a/src/driver/keyboard/handler.c b/src/driver/keyboard/handler.c index 37d0079..5f2bf92 100644 --- a/src/driver/keyboard/handler.c +++ b/src/driver/keyboard/handler.c @@ -104,70 +104,50 @@ void keyboard_process_scancode(uint8_t data) { void keyboard_handle_key(keycode_t key, key_action_t action) { keyboard_update_state(key, action); - /* - * Key release events are used only to update the keyboard state. - */ - if (action != KEY_PRESSED) { - return; - } - input_event_t input = {0}; - - /* ---------------------------------------------------------------------- */ - /* Character input */ - /* ---------------------------------------------------------------------- */ + input.type = INPUT_EVENT_KEY; + input.key = key; + input.action = action; + input.shift = keyboard_state.left_shift || keyboard_state.right_shift; + input.ctrl = keyboard_state.left_ctrl || keyboard_state.right_ctrl; + input.alt = keyboard_state.left_alt || keyboard_state.right_alt; char character = keyboard_translate(key, &keyboard_state); - if (character != '\0') { - - input.type = INPUT_CHAR; + input.type = INPUT_EVENT_CHAR; input.character = character; - input_queue_push(input); - return; } - /* ---------------------------------------------------------------------- */ - /* Special keys */ - /* ---------------------------------------------------------------------- */ - switch (key) { - case KEY_ENTER: - input.type = INPUT_ENTER; - break; - case KEY_BACKSPACE: - input.type = INPUT_BACKSPACE; - break; - case KEY_TAB: - input.type = INPUT_TAB; - break; - case KEY_UP: - input.type = INPUT_UP; - break; - case KEY_DOWN: - input.type = INPUT_DOWN; - break; - case KEY_LEFT: - input.type = INPUT_LEFT; - break; - case KEY_RIGHT: - input.type = INPUT_RIGHT; - break; + case KEY_HOME: + case KEY_END: + case KEY_DELETE: + case KEY_ESC: + case KEY_LEFT_SHIFT: + case KEY_RIGHT_SHIFT: + case KEY_LEFT_CTRL: + case KEY_RIGHT_CTRL: + case KEY_LEFT_ALT: + case KEY_RIGHT_ALT: + case KEY_CAPS_LOCK: + case KEY_NUM_LOCK: + case KEY_SCROLL_LOCK: + input.character = '\0'; + input_queue_push(input); + return; default: return; } - - input_queue_push(input); } /* -------------------------------------------------------------------------- */ diff --git a/src/kernel/kernel.c b/src/kernel/kernel.c index 1cb38d2..e180959 100644 --- a/src/kernel/kernel.c +++ b/src/kernel/kernel.c @@ -67,25 +67,27 @@ void kernel_main() { continue; } - if (event.type == INPUT_CHAR) { + if (event.type == INPUT_EVENT_CHAR) { print("Character: "); print_char(event.character); print("\n"); - - } else if (event.type == INPUT_ENTER) { + } else if (event.key == KEY_ENTER && event.action == KEY_PRESSED) { print("Enter key pressed\n"); - } else if (event.type == INPUT_BACKSPACE) { + } else if (event.key == KEY_BACKSPACE && event.action == KEY_PRESSED) { print("Backspace key pressed\n"); - } else if (event.type == INPUT_TAB) { + } else if (event.key == KEY_TAB && event.action == KEY_PRESSED) { print("Tab key pressed\n"); - } else if (event.type == INPUT_UP) { + } else if (event.key == KEY_UP && event.action == KEY_PRESSED) { print("Up arrow key pressed\n"); - } else if (event.type == INPUT_DOWN) { + } else if (event.key == KEY_DOWN && event.action == KEY_PRESSED) { print("Down arrow key pressed\n"); - } else if (event.type == INPUT_LEFT) { + } else if (event.key == KEY_LEFT && event.action == KEY_PRESSED) { print("Left arrow key pressed\n"); - } else if (event.type == INPUT_RIGHT) { + } else if (event.key == KEY_RIGHT && event.action == KEY_PRESSED) { print("Right arrow key pressed\n"); + } else if (event.key == KEY_LEFT_CTRL) { + print("Ctrl key "); + print(event.action == KEY_PRESSED ? "pressed\n" : "released\n"); } } From 061e557e03bfa10ac7d69732fd39901c58b8a602 Mon Sep 17 00:00:00 2001 From: Ox03bb Date: Tue, 22 Sep 2026 19:39:29 +0100 Subject: [PATCH 04/13] chore: foramte code --- includes/driver/vga.h | 24 +++----------- src/driver/keyboard/event_queue.c | 21 ++++++------- src/driver/keyboard/layout/us.c | 3 +- src/driver/vga.c | 52 +++++-------------------------- 4 files changed, 24 insertions(+), 76 deletions(-) diff --git a/includes/driver/vga.h b/includes/driver/vga.h index 1ddff0b..00ebdbf 100644 --- a/includes/driver/vga.h +++ b/includes/driver/vga.h @@ -84,25 +84,9 @@ uint8_t vga_get_cursor_x(void); uint8_t vga_get_cursor_y(void); void vga_put_at(uint8_t x, uint8_t y, char c, uint8_t color); -void vga_clear_region( - uint8_t x, - uint8_t y, - uint8_t width, - uint8_t height, - uint8_t color -); - -void vga_print_at( - uint8_t x, - uint8_t y, - const char *str, - uint8_t color -); -void vga_print_at_center( - uint8_t y, - const char *str, - uint8_t color -); - +void vga_clear_region(uint8_t x, uint8_t y, uint8_t width, uint8_t height, uint8_t color); + +void vga_print_at(uint8_t x, uint8_t y, const char *str, uint8_t color); +void vga_print_at_center(uint8_t y, const char *str, uint8_t color); #endif \ No newline at end of file diff --git a/src/driver/keyboard/event_queue.c b/src/driver/keyboard/event_queue.c index ab2bbcd..2c186e4 100644 --- a/src/driver/keyboard/event_queue.c +++ b/src/driver/keyboard/event_queue.c @@ -35,21 +35,22 @@ bool input_queue_pop(input_event_t *event) { return true; } -bool input_queue_is_empty(void) { - return input_event_queue.head == input_event_queue.tail; -} +bool input_queue_is_empty(void) { return input_event_queue.head == input_event_queue.tail; } bool input_queue_is_full(void) { return ((input_event_queue.head + 1) % INPUT_EVENT_QUEUE_SIZE) == input_event_queue.tail; } void input_queue_print() { - printf("\033[32mInput Queue: \033[0m head=%d, tail=%d\n", input_event_queue.head, input_event_queue.tail); - uint32_t count = (input_event_queue.head - input_event_queue.tail + INPUT_EVENT_QUEUE_SIZE) % INPUT_EVENT_QUEUE_SIZE; + printf("\033[32mInput Queue: \033[0m head=%d, tail=%d\n", input_event_queue.head, + input_event_queue.tail); + uint32_t count = (input_event_queue.head - input_event_queue.tail + INPUT_EVENT_QUEUE_SIZE) % + INPUT_EVENT_QUEUE_SIZE; for (uint32_t i = 0; i < count; i++) { uint32_t index = (input_event_queue.tail + i) % INPUT_EVENT_QUEUE_SIZE; input_event_t event = input_event_queue.events[index]; - printf("Event[%d]: key=%d action=%d char=%c\n", i, event.key, event.action, event.character); + printf("Event[%d]: key=%d action=%d char=%c\n", i, event.key, event.action, + event.character); } } @@ -61,10 +62,8 @@ void input_queue_print_last() { uint32_t last = (input_event_queue.head - 1 + INPUT_EVENT_QUEUE_SIZE) % INPUT_EVENT_QUEUE_SIZE; input_event_t event = input_event_queue.events[last]; - printf("\033[32mInput Queue:\033[0m Last event key=%d action=%d char=%c\n", event.key, event.action, - event.character); + printf("\033[32mInput Queue:\033[0m Last event key=%d action=%d char=%c\n", event.key, + event.action, event.character); } -void input_queue_clear(void) { - input_queue_init(); -} +void input_queue_clear(void) { input_queue_init(); } diff --git a/src/driver/keyboard/layout/us.c b/src/driver/keyboard/layout/us.c index 3222b41..813dfce 100644 --- a/src/driver/keyboard/layout/us.c +++ b/src/driver/keyboard/layout/us.c @@ -114,7 +114,8 @@ char keyboard_layout_translate(const keyboard_layout_t *layout, keycode_t key, } bool shifted = state->left_shift || state->right_shift; - bool caps_active = state->caps_lock && ((key >= KEY_A && key <= KEY_Z) || (key >= KEY_0 && key <= KEY_9)); + bool caps_active = + state->caps_lock && ((key >= KEY_A && key <= KEY_Z) || (key >= KEY_0 && key <= KEY_9)); if (shifted ^ caps_active) { return entry.shift; diff --git a/src/driver/vga.c b/src/driver/vga.c index b8ce31e..414c827 100644 --- a/src/driver/vga.c +++ b/src/driver/vga.c @@ -260,59 +260,29 @@ void vga_scroll_up(void) { vga_set_cursor(0, VGA_HEIGHT - 1); } - // update specific position -void vga_put_at(uint8_t x, uint8_t y, char c, uint8_t color){ +void vga_put_at(uint8_t x, uint8_t y, char c, uint8_t color) { uint16_t index = y * VGA_WIDTH + x; - vga_buf[index] = - ((uint16_t)color << 8) | (uint8_t)c; + vga_buf[index] = ((uint16_t)color << 8) | (uint8_t)c; } -void vga_clear_region( - uint8_t x, - uint8_t y, - uint8_t width, - uint8_t height, - uint8_t color -) -{ +void vga_clear_region(uint8_t x, uint8_t y, uint8_t width, uint8_t height, uint8_t color) { for (uint8_t row = 0; row < height; row++) { for (uint8_t col = 0; col < width; col++) { - vga_put_at( - x + col, - y + row, - ' ', - color - ); + vga_put_at(x + col, y + row, ' ', color); } } } -void vga_print_at( - uint8_t x, - uint8_t y, - const char *str, - uint8_t color -) -{ +void vga_print_at(uint8_t x, uint8_t y, const char *str, uint8_t color) { for (uint32_t i = 0; str[i] != '\0'; i++) { - vga_put_at( - x + i, - y, - str[i], - color - ); + vga_put_at(x + i, y, str[i], color); } } -void vga_print_at_center( - uint8_t y, - const char *str, - uint8_t color -) -{ +void vga_print_at_center(uint8_t y, const char *str, uint8_t color) { size_t len = strlen(str); if (len > VGA_WIDTH) { len = VGA_WIDTH; @@ -321,12 +291,6 @@ void vga_print_at_center( uint8_t x = (VGA_WIDTH - len) / 2; for (size_t i = 0; i < len; i++) { - vga_put_at( - x + i, - y, - str[i], - color - ); + vga_put_at(x + i, y, str[i], color); } } - \ No newline at end of file From 409af7383be6c40f5c22006959c19a4974df309a Mon Sep 17 00:00:00 2001 From: Ox03bb Date: Tue, 22 Sep 2026 22:29:09 +0100 Subject: [PATCH 05/13] feat: implement TTY interface and event handling for keyboard input --- includes/driver/keyboard/keyboard_keys.h | 5 - includes/driver/tty/tty.h | 50 ++++++ src/driver/tty/tty.c | 196 +++++++++++++++++++++++ src/kernel/kernel.c | 2 + 4 files changed, 248 insertions(+), 5 deletions(-) create mode 100644 includes/driver/tty/tty.h create mode 100644 src/driver/tty/tty.c diff --git a/includes/driver/keyboard/keyboard_keys.h b/includes/driver/keyboard/keyboard_keys.h index 40e3dfc..e4fedda 100644 --- a/includes/driver/keyboard/keyboard_keys.h +++ b/includes/driver/keyboard/keyboard_keys.h @@ -184,11 +184,6 @@ typedef enum { typedef enum { KEY_PRESSED, KEY_RELEASED } key_action_t; -typedef struct { - keycode_t key; - key_action_t action; -} key_event_t; - typedef struct { keycode_t normal[256]; keycode_t extended[256]; diff --git a/includes/driver/tty/tty.h b/includes/driver/tty/tty.h new file mode 100644 index 0000000..100fb6f --- /dev/null +++ b/includes/driver/tty/tty.h @@ -0,0 +1,50 @@ +#ifndef TTY_H +#define TTY_H + +#include "keyboard_keys.h" +#include "keyboard_layout.h" +#include "types.h" +#include "driver/keyboard/event_queue.h" + +#define TTY_BUFFER_SIZE 4096 + +// modifiers +typedef enum { + MOD_NONE = 0, + MOD_SHIFT = 1 << 0, + MOD_CTRL = 1 << 1, + MOD_ALT = 1 << 2, + MOD_ALTGR = 1 << 3, + MOD_CAPS = 1 << 4, + MOD_NUM = 1 << 5, + MOD_SCROLL = 1 << 6, + MOD_SUPER = 1 << 7 +} keyboard_modifiers_t; + +struct tty_t; + +typedef struct { + void (*init)(struct tty_t *tty); + void (*putc)(struct tty_t *tty, char c); + void (*write)(struct tty_t *tty, const char *str); + void (*read)(struct tty_t *tty, char *buf, size_t len); + void (*clear)(struct tty_t *tty); +} tty_interface_t; + +typedef struct tty_t { + bool echo; + bool line_ready; + + int input_length; + int buffer_index; + char buffer[TTY_BUFFER_SIZE]; + + keyboard_modifiers_t modifiers; + + tty_interface_t method; +} tty_t; + +void tty_handle_event(tty_t *tty, const input_event_t *event); +void tty_update_modifiers(tty_t *tty, const input_event_t *event); + +#endif \ No newline at end of file diff --git a/src/driver/tty/tty.c b/src/driver/tty/tty.c new file mode 100644 index 0000000..1208e00 --- /dev/null +++ b/src/driver/tty/tty.c @@ -0,0 +1,196 @@ +#include "tty.h" +#include "keyboard/event_queue.h" +#include "keyboard_keys.h" +#include "driver/vga.h" + +void init(tty_t *tty){ + tty->buffer_index = 0; +} + +void putc(tty_t *tty, char c){ + tty->buffer[tty->buffer_index] = c; + tty->buffer_index++; +} + +void read(tty_t *tty, char *buf, size_t len){ + int i = 0; + while (i < len && tty->buffer[i] != '\0'){ + buf[i] = tty->buffer[i]; + i++; + } + buf[i] = '\0'; +} + +void write(tty_t *tty, const char *buf){ + int i = 0; + while (buf[i] != '\0'){ + putc(tty, buf[i]); + i++; + } +} + +void clear(tty_t *tty){ + int i = 0; + while (i < TTY_BUFFER_SIZE){ + tty->buffer[i] = '\0'; + i++; + } + tty->input_length = 0; +} + + +void tty_update_modifiers(tty_t *tty, const input_event_t *event) +{ + bool pressed = event->action == KEY_PRESSED; + + switch (event->key) { + + case KEY_LEFT_SHIFT: + case KEY_RIGHT_SHIFT: + if (pressed) + tty->modifiers |= MOD_SHIFT; + else + tty->modifiers &= ~MOD_SHIFT; + break; + + case KEY_LEFT_CTRL: + case KEY_RIGHT_CTRL: + if (pressed) + tty->modifiers |= MOD_CTRL; + else + tty->modifiers &= ~MOD_CTRL; + break; + + case KEY_LEFT_ALT: + if (pressed) + tty->modifiers |= MOD_ALT; + else + tty->modifiers &= ~MOD_ALT; + break; + + case KEY_RIGHT_ALT: + if (pressed) + tty->modifiers |= MOD_ALTGR; + else + tty->modifiers &= ~MOD_ALTGR; + break; + + case KEY_CAPS_LOCK: + if (pressed) + tty->modifiers ^= MOD_CAPS; + break; + + case KEY_NUM_LOCK: + if (pressed) + tty->modifiers ^= MOD_NUM; + break; + + case KEY_SCROLL_LOCK: + if (pressed) + tty->modifiers ^= MOD_SCROLL; + break; + + default: + break; + } +} + +void tty_handle_event(tty_t *tty, const input_event_t *event) +{ + if (tty == NULL || event == NULL) + return; + + /* + * Update Shift/Ctrl/Alt/Caps/Num/Scroll state. + */ + tty_update_modifiers(tty, event); + + /* + * Key releases do not produce normal terminal input. + */ + if (event->action != KEY_PRESSED) + return; + + /* + * Modifier and lock keys have already been handled above. + */ + switch (event->key) { + + case KEY_LEFT_SHIFT: + case KEY_RIGHT_SHIFT: + case KEY_LEFT_CTRL: + case KEY_RIGHT_CTRL: + case KEY_LEFT_ALT: + case KEY_RIGHT_ALT: + case KEY_CAPS_LOCK: + case KEY_NUM_LOCK: + case KEY_SCROLL_LOCK: + return; + + default: + break; + } + + /* + * Terminal control combinations. + */ + if (tty->modifiers & MOD_CTRL) { + switch (event->key) { + + case KEY_C: + /* Ctrl+C */ + return; + + default: + break; + } + } + + /* + * Handle special terminal keys. + */ + switch (event->key) { + + case KEY_BACKSPACE: + if (tty->buffer_index > 0) { + tty->buffer_index--; + tty->input_length--; + + tty->buffer[tty->buffer_index] = '\0'; + } + return; + + case KEY_ENTER: + if (tty->buffer_index < TTY_BUFFER_SIZE - 1) { + tty->buffer[tty->buffer_index++] = '\n'; + tty->buffer[tty->buffer_index] = '\0'; + tty->input_length++; + tty->line_ready = true; + } + return; + + default: + break; + } + + /* + * Convert the key into a character. + * + * This should come from your keyboard-layout layer. + */ + char c = event->character; + + if (c == '\0') + return; + + /* + * Prevent buffer overflow. + */ + + if (tty->buffer_index >= TTY_BUFFER_SIZE - 1) + return; + + tty->buffer[tty->buffer_index++] = c; + tty->buffer[tty->buffer_index] = '\0'; + tty->input_length++; +} \ No newline at end of file diff --git a/src/kernel/kernel.c b/src/kernel/kernel.c index e180959..c3c252c 100644 --- a/src/kernel/kernel.c +++ b/src/kernel/kernel.c @@ -71,6 +71,8 @@ void kernel_main() { print("Character: "); print_char(event.character); print("\n"); + print("action: "); + print(event.action == KEY_PRESSED ? "pressed\n" : "released\n"); } else if (event.key == KEY_ENTER && event.action == KEY_PRESSED) { print("Enter key pressed\n"); } else if (event.key == KEY_BACKSPACE && event.action == KEY_PRESSED) { From 8ef71c0b0b5e28a59ac595e7415e2a5bd3cb75df Mon Sep 17 00:00:00 2001 From: Ox03bb Date: Wed, 23 Sep 2026 16:40:15 +0100 Subject: [PATCH 06/13] tty diagrames --- docs/assets/tty/tty.drawio.svg | 273 +++++++++++++++++++++++++++++++++ docs/assets/tty/tty.png | Bin 0 -> 57463 bytes 2 files changed, 273 insertions(+) create mode 100644 docs/assets/tty/tty.drawio.svg create mode 100644 docs/assets/tty/tty.png diff --git a/docs/assets/tty/tty.drawio.svg b/docs/assets/tty/tty.drawio.svg new file mode 100644 index 0000000..14dbc6c --- /dev/null +++ b/docs/assets/tty/tty.drawio.svg @@ -0,0 +1,273 @@ + + + + + + + + + + + + + + + +
+
+
+ user +
+
+
+
+ + user + +
+
+
+ + + + + + + + +
+
+
+ output +
+
+
+
+ + output + +
+
+
+ + + + + + + + + + + + +
+
+
+ read +
+
+
+
+ + read + +
+
+
+ + + + + + + +
+
+
+
+ + TTY + +
+
+
+
+
+ + TTY + +
+
+
+ + + + + + + + +
+
+
+ input +
+
+
+
+ + input + +
+
+
+ + + + + + + +
+
+
+ Terminal +
+
+
+
+ + Terminal + +
+
+
+ + + + + + + + + + + +
+
+
+ Console +
+
+
+
+ + Console + +
+
+
+ + + + + + + +
+
+
+ secreen +
+
+
+
+ + secreen + +
+
+
+ + + + + + + +
+
+
+ network +
+
+
+
+ + network + +
+
+
+ + + + + + + + +
+
+
+ write +
+
+
+
+ + write + +
+
+
+ + + + + + + +
+
+
+
+
+
+
+
+
+
+ Shell/App +
+
+
+
+
+ + Shell/App... + +
+
+
+
+ + + + + Text is not SVG - cannot display + + + +
\ No newline at end of file diff --git a/docs/assets/tty/tty.png b/docs/assets/tty/tty.png new file mode 100644 index 0000000000000000000000000000000000000000..3a2021c926161c77a213731cdb3228f31f80b933 GIT binary patch literal 57463 zcmYhiby!r<+6M|l4>feRbobCmtE4F1Al=eot?!eqC)A}oO{nmDsnydehKUyhiegv%!Zz~6t-kh zzvA<7bx3oB*L>_WbTl%weAR5@Jd{Iavpgbo^*iU~eO_Pa|L>B;M9G!KWaze$LYMr% z>thmZz-ARIV!&5TaVuO|8D$Z3EZgC7x1^{|92xX@Y6BGIc@w&$p5)uUn~(x?P$Qi+IEDu`{Vfj zzZaDJ1T`!iC-3k7J&uYv`u%lLXDBN~!CKR*@*BKD&NR}4*jdw8WlcZw6zsy$Qg)Kq zNd@L*mn?S8>#PNT<)r=sZ|r;xmj2&g{f&n*X{S5L`BxLFy5<N#g4UHCW^$euAl1gRXfp=<0IJ7dx_+q={|z7`ozqC4u*Rto%x!jAxoW3>mcU zqBgmmKw0Bwq`eUZPLQRC_ve+ne4>%*Mr>q8yCi`y7IEl+`5;g(w89VsPlBjZ6Yh z75yInU2P5Wwx`ksUq27N*&@5S*~#0wJ8n-Km3cH;_S>QHJ8In7?7*N)E&I`Cpf0}m zY%b_(sn}trQsC~OX2*NtW0oQl+Z47?u<_d4NNo}wos$kr1aQ%#k;iy~_Z=L_I0C<& zw18Fyohdacl8Lr6Y-sgeS}&}Av|fIEyx);=|93Gn2siKAj3hHjzFH2@XuDp=&JeQu zcoTI_!#eD9zvQ#>X(unlgw}tTq49Fw;T<9rd7|0JMc<*K)+$kKyMK=0xj%a!&SNPkepS!0 zx|q^U%tZ7Pv^HBl3{#I^;vX_ruO=+Red?{+7MsOVrxmNQ3Ld37uoutEaIE$5jDR4cBbYSb&I%%An-bPSz@=K{}%aoya` zez6X&c7$)u+2nmXZo5;LzCV#~ygO!4=zvrVpFuPgmg`(v=!X0rT{jN`Y-x56Q9ouXhisgjSBQ;j!Ig$QDa^V`if%YMa& zv*kyq3V9H(T3rN*uF*KxM3T%y!~Nxg%h_&GQf9*PXG3RpY_8-Qd`Ca}LWjOYns?2I zbt!@)Poo>OglM)a?7ao?z}DMhvzIdE&JstCZw46E+LI{$$U zIZ$CsGQ58e>sLg$SoGB4d0}8L6$EV7zN}(%J&sp|ix-OPN=B-0_2ICiEw~5Ho-+^TP`Mv@Z!7k+o*sBn*Sr zR_}fnA36I6>8GteQ4i+O~j%KnYEHQ2pzE+QK>2(B_?9RD%D@k2hgW3|L_ z%Osm3ZP4FwrI`+j2bRnzaI3tfkiGEP{~hlSJKOOK3q zpZQ}W*-$R`XEZcrf8V{wV^hran$}nHBdo#%e;A4Ix2Mcne6%}w-o1g?pnv;_f_v5+ z1r1dh+pBXt;c5N*`UcoK@rloEwS>Jsdh5G(pd~Q(*yIJV1deJYdv1r0P?J)G|8%sD z)QfH)!o0>oWN0Dkk#AhVCL#a4x?*F6y$7im%WFpL*)-Y4HlZ^5n+H;F~BVC0K?C@x9qD(DVb9=syI8VE68aYiM# zn&$KK2niF`*an|;V1!k|*i~-OD*H>n=5h>CEG(vK%xH{S(sz)b$n%n-DAW3Vxhc6g zT&SN_CNy@LxBDvGL>$<6wGtBI!-H*)esqmXIa&Z|hA4x~O?wD0zw|E)vxi;EWR}C1 zp4YKE=Dz}tNrDs4ws87ggSIk(M|TXtS^ML^(eOUlBi+d(O4T)8!)Y&7!Xx>WVdOE0 znk>g-M648o)$GB^BDE5D{tGyX3>uQB5#?1%iOhFLw~`LD>lkXitlbH+4KKA&Rvi|g ztWCf!LwiJao3@(bpLu3xA{cz{+fm-@9iyxq>Oz=D(^$6* zJOF#3M2m|E9hcH7kSzbg1x$54QDG=bA>m!lzvejxPcG3iqyFaRC})I;Dybf?RgRDv zQ;Dx&x8}J!tk3&(H)w@prgJ`Pne=Po71Mh=cA3jr%Xjrl-YM;m+kqcG3xr(!2W@;K zLDv-O*62_qbGui4^@A%au#bV#;N|_41rm|Eq{w=G-)FnPk087v>#2q#20T`Ugs4mu{p3yD zE5F6*g{sv!bnm8}2ZMN33Xg4+-~c@6D$>=qcc!^ow_p*=KG`2N`c>iNl&WnI8g(MS z=dR`kIHM(cnm<&BE4b?v#oWO?lvujU?bvZJ@ZdiHOp_aD(R;yZ;`eW>5rusGwpaCw z?s2CMvdi)d{K_` zyZNu!7iSd%Mt@}m%Zc&Ylc#KSx( zI(%fK94nQg#%sYYUU)^Fa)$YLoGet!CZIA5%h08@cGaN9V`qfrn77yx$t8Yu><{z( z#f&M)L`LECDKUoewSHMf{4J%X|ihTh0^^JH4eOLLxT48c8eP4VX5Z1 zCO&A*Drbuw%<|OG03^W*6!xNDcdu^9^~@tl;<0euxfccil=7^8CAfL`cGi;K0_S?0 zk^p5VH=uHL6fqJ6v})p^ik-|sl{lxWq(KA{hD<>-%q#Q!+ACA(*$=HlxeBZZOD}sA zVpZt%YWUXe{tr>RkoV{)G~%9o+X`&_^P~Y6f0%c@V;;}{yLBKadhD)*6;4rVxhoNR zjtWGhA`6b&5_n{@b_5^pE7jZn{4yZK#MpOK*X1kF$dd+2c3S$+c7-@thE1-%>-Wvq z`Pp`OZz-KL+G?M`@rq?WF-|`5VJ6p6?TkrqiY^%RsvO&=RXK_FNL$pd#2W9Dy(0a= zw-oR<;W@o4h{+~63)XeiyY%JP^{co1 zp(cF*isIg785wSwm#RA!$9`}Is{HDaF%&cGb=X`-L+=*E980FRzm}OEvwXf@= z4fy@dXC>jO3QX{y05B4J1!rG8!vpoY>QL%(VD}z!&px*1XwJ>iDJ9AI^fAu_+ciTC~- z{?^v`wAK}kjs5s{QiHchDIpW&YSJS{(G`W8VnFw;r!&EV*YW%V`20S6YM z;{1FQ$+x@Q+N_1y^Vav3-QXvD_C-qXeZis7S$J{FXI=FP`Kp7hG}F(Dr25akIxMY=6 zdq>yQhK2tX9q2{y_fa#s=GY>Qq&y~uzxPExwkpZ6t_}~Y_4qQZK%%Th?^uE&q1h3E z&D)|$9SPRn=Dp}cYMU@8oBx1N`YXX7mvFXW*UiRq^*=4(YmUgyTKnUJ1S?&d>P)3E zb4o_bx1!<^2D_D@E8}tbY@k)DE{W}APj=U~1p?%!8bIb`74lm9r_7D*U-Nt*^c$ER6S7OsJ8GSHIG$)<1-aM zYS>s8M(8MSxafo-XWKmWcri8AY^av&my2VdU8a%^42kcTZ|g8rMl|XdW5<4iw*U`% z$8j+>JKVGa`ukZ^*2yux+vn2t@#JF`4a1wqy*bZkTB)dVAvEK$V9>i1jpbib_@^DA zWp9(LxhQW=DRSsu2=lvk6DX@B(Mq{D`2r-W+nCzjnY4ekpaxO#omd`WmNKfhN1w8m zr7|E-Odjf#;LhkoGGVxu8+~mST1)zF^B|^fvf-=TGX@S&L3kny+yHSuNC96yV@M>I z{XNzm@@Qs@f`NxKF{OH4_q7}Gqy2#+odA>$c{m&7)z<2O8O%cM1*b+nPV%w8+&#q(|Q*dRS4^eNIXd zqU!*`6G8=VAaFAMs#Ui3)6SfinttJmZi**010N$nT9GA%-)(0k=UnP&FcyK7dAjSZ zNrIi-dbOPOB^uQXsivPYR_U|EFxgcy3i6o^MCe$<0R#;; zHhPJ7=K|2^dVM-pR1`MGiiSchIs(a~wOI}Zd5yn(0hb5tf$2BqhGj?+1)p>bUQ{+8 ziKkSsz+^G@%WD!NK9R13JO-_q33Ddqn}{Js+Dl7c(1zT*nYX1+86=V%c63@_q4+SQ z{KAZlHgv3tlsrM8EaD+mc7-CNScQh|$76}Er|O@e%7s>cB-9fwSm}`WosA%8I%E(d zpY_|xL9=-`M=<*JlmJW;`4TIlr}ip{L>1xQ@$8+g5PoaRr zrtVH;jG_Q>ecK=KjRzgVNk3iP+OPsL+}^MB|Wv{`1InUV|K}GOHxWC zY{`2;?XzjHpP=pLs0qPPexW^?C%)qbSH1{M;+)q^m~6f&mwxBVIQUUT50ckQhsm8IKz zVWx~~Y1AAH5Z9*Q(8DSK^@!|kd^gW;!fig#zcTy!=W;&1qD!D&P+82eGXhH_i|A^1 zT%Huz8QInL?@sI>ItB{ei1mkYWx7v`6=e&r<97-8v9nPY2%0bf=^}CXw&5kYt77bt zX#UHk8|$)oShCOfKhC{MLXbJ{AvxVxm>dy{N9Gkn2l?s{PV-<_*8J{x5s_q;FFwtj z7`8x>!cki%n3Md6tv9Q(7^2W`)||uxZ1f$m`u!>WDl|n1AFzE<8~hGqe|i28`N#dUBbtwzsRgW52&1z5`3r5M;~XS>Nq+-B zXhct(3m_6hqcn0-=9do6J5Sh(zu9xp;FTao1Ad))70*nw=bi-}f7L6ibWq59Vw58n zYL5jKs;h)4T$BQzx#mS!>MQ#43Yy9<^lW+^=*))x>>)kkRamrV?+f`u z14CwveTGXPd#PN=CzXx9SrDds)I?DdwDL^@ppPT zlOw8vCz5q!>>(IL(jYd#$b||I9TI$Cg-fRad_1K3_L#CBPl$$1+K{a5imG&aG+i^AZ2|zoX{k z?y0v0TsY>%3bS)-;*n?R(D{IASlaL%loBVODNI|)piA+Gs1qvD9!TNQV~9UG=l5-< zo=U-&r$;G1G|zIXt9n8A6%$2pRI;Yw__MK9YzbLhDZFT82lJcH@$wn)6JYg^i?Apu znx?DoIZAc}tjf+ZdBL}(Sviaewqbe^TZh#>gQv_;ul4H{}if`AxOV86}(t59Bu zbE#M+$823NJLK^;+Jay%Ic+p3)b9Q1ZM!hALM9o|_BPY7R5?Ts9NMD*Jc zg~UG(bn`Yw&Prk)Rfj_E2M;CZYYiXVQc4=5m!06=WD;l*g&#zv=sp3DMQ#?VQjqfSz25XI&a<_F{rX}S5^;j zc&o6CSb^g>;S+qHs?#j8UDm#_8ydT|9=J0&im?=mIK9UL5YqxVq7#(>MR=j!h1WGk zB|wD^y~$^^080~}&F*6{A*Nd01+K^}f@L8-D00(06vfMoNYI^l0xA*uGrE%;leYy* z&hbV9=qx-HhtcAuQk#!#V0V00k?v2SpTrg#e%Fy>-(seBlbmyPqh@0mUL{H#@vReO z;rGSRmp?`dPwRC-FxSL4jsKy0QW@o-9n~Na2J|N(>*n=@^qp_8aTh}-)zs9y#Rcsw zq#_IDOHn(;>$Watly^F|1GANI8VoM_lP95l-?qCMNmvR`6!dL(@G)R*_~N_tRd#1Q z@f3ne<$9q@KLdXJ77CICIPf+<-uv}X+7$NF&h_|p4(db;TB)l=kDexdYn*IqFhlaO zekwnMiMY=Z*At&KL(lw?%WTw$q16l1)C-hNb~C!1(ls6C#z*5@efa#(rv>Tb>o?T? zeI1l0L&S0P5ckYIdynik>q%*KR^YG>6@DDi*Ad}7_%}`_TM9W0uCE=Hz8hi-U_7qH zZf(u`^%K5r3YZT?$xmox){uo()H&O1U1d97a3r~Q+91yASLo~pP@0Gi7WD(|LaSCM z$V1R0Nge)>gbB|WX+u}Pjs?|IKjJ+pR-U8P;(38*FV@0)?j5DF8Ti|tOoi&TC@L^E z`65w(40Uu%Hx??gQNojY?~ez_2S(Qfc|LqdYj!n9E{mU;nU}3n)T|zEc1Dq7&g|pc z0dGU>-`Mf~k9Oy#CZL{_G(<1>dkCVZjY*i)q%wSxqaP~5^?98PaH=oX82w{;(YIzu zB0mmq00RU5g3g8?VXgrE6vjOwH`GanE{U1zx1ABDuKf7$uSO%*N`PW*Q+V?)+lCWR zNI`w*h|4@<(byn>me(|lzQ77$Qcfh+>(7qGtXfY@s3CoFTx#gkJluDXF{sB%N{eVX zboigtc%;=eK;9QNid1E>Jb?q!Etooi%DLe=RBYlR+-$+sa_FqG=?{lnBl#*0o&x(K zZvC**iwz2r!qgi%&7K!Y2Y@s|1hr?YUw5bek-4kv8cJ699T^3^U)MCKXAF0>54Mu< z>&*@5!|0=Pn={mngppyT6`I-zl*`fY;1qeP($|;~h!^73JtFfL_1dAx>I4z5tvUJ! z_2DW3wjGEkrIV|(2NWSUn8Fx{`eEmM+|t}CO~(E=TjNpFJ}SOXV7JV}wdLHqG~)v% z=00G@zaE+$INSgf>l8q1FWz=j7_2P0SHv#LMMsvI@$?321tqm zlQzwkt*HwAJ2d$I+N{n^+zg+Gf*MvJADtQAx%%uhQKGEjy$e1gYm6M`3kleqb(Y#H z^<1E^Wej_H+}Qh0e>CU`Ow1xXQ-T4&%OYIh}&ar`)6*0|Zd zVbK+0F8$0mZ)QrhS)YF(Uy90(ox{nhclgmd&9=x`DW`*9zy9P}wLjc)8gkxf?Hc=V zc`E@=s8j{(Y3Ak_@lK@1I<~w1pbXmuEJCuK6HIMf9m(c{Np%UeldF-_I$M*hEUeai zFK0K;_4lk`XB#4M63Z%!K9iWH`m3M={@!|Ko&RY8znmHK1%GC-ETdYtR3Q@k-7!aD}9G+B}?;gc7AbPJ#8FHAr(OK z#;^B>=6sX<96b^`Y;d%R%@W5984DEhBA_C60+U9kP;ATHXkw7qBRP*Aujfx#ACyhO@oiPch%Q~UkZ)j@8}5d3 zDat=g1=^p;h_f)7$XwLRM$NKH9lqSZi~O@fqoAAH*1eLfXN$Z8(3p2;zYshvCvq^( zHUG=bIV^P_c&X2)s_Nn>AwmwGWy6vVjzsccTt?(T4B^ z{G|8cyZu&Y@& zb$uIa9s_^=3Tkm*Bg*}WM$87Zdza^2p-bFc+(sV(`g{sv9w=1HoZN;pZ_|;I632dm zp#SW`mi>vuATNz`bNeK4qD9m94<&yZKL+$&-dz`2vFL?{i)q7zcc2KuM0tAK*{zMe zWXWz4OKch47t29A@jpJ+-Pb&s1Q)=kUhoQ;yr39PZRPnnCI?!`JLbK$_0__e8Q{Q= zio*tqhw5@bjNu|x;N4*q9P$96`p zQx`yvqkudju#0H{BR5-PsYB{Rl$^?kR)ZDLBG4ag)9)I+9E~l6?6}|J`heU}w~*o( z9RnwZWDYVf0quHAM?bVO>Qn3UpM1It5Mmh_%q&)PURQh-L&Scr8c|WsD{2xxn+4K0 z!GP41yDvaNXCpw1krChboP{Wanx3+@-Lfpk=3F=-XJod&gPrFu?%`}zk*iM3-BOZ; z?&uuN2wy}n+yXNk;5c3>ncnRi;?ZVmGoBrqTrkM*K`Q|?s<=Ps*v-v+^O0GX1w8MO ziS=U6kspzE`RPqo^Ws8~V$iygZvI3MQb*!>3{GK~X^BS8#l6a~i5p1l{1>&*Yp%kQ za)(#`I`L4*UJ9nlQZKWR^q&Us@jdKBw8*4pRY*(3)o~Y`0CQ<;W{;J-kywl>%hR%@ ze!)9x<=(c@k-0qI68rrN?tPJ&T)MC&n5pgl;qF+Bo6)fTj6nhal0_k+*>%C)!h4G$ z_J#l7U4|Y)%%58L{Z{TK79)_=V6(xv8@xB7^X;o>%JgC+Od}D?H+oLNUocLgkR+5B zqEbVvx#D?1U*fxK0q5tr|HyhSFhqa-S;!jaK?B=-q z2=Y3or^Q%zONOsJm!bKArR71PRzza-6u4lwy6G*!{KuQwH^9(xTGa9eLFocmqu8{~ zfTWxd=C7IDsjw?STfSGJR;veMfa7D;v=}X6Le0}WmUJv6Lo7%^ODXYq%0-Pxm^7IK@2;o$FJ$^KdPdR(Nl z+o{QbKi1WG;V8~^kcvIwPQ$0IQlQAtBmp==GDSf+nwVUMLWfTg8`(g9cqQJUc_b0B zD(1hECpBU>&D<-J(~8tel4@ND%fo4?Wn4L2dtf`ppDlC2?R9ic`!LbQNS**DeL0;` zii>2&Yb90VzmPHQaLzaZMk+15`mQ%QJv8G^9v*^k(FA=|7u8q-U@?fogSrK0qVnD9 z9+t4KxHG82d-j+P=Kt2|0+Ltsfi_fF+Ar%Q2hz#VYHb%3V9Z?J@GDAYytvo)zr?Uw zJ${;M=(Mz#kDcw0NBpNBzU`AOr9m7vtJ3+uG#8Ad@3x!lvBnUT&+e^~4Y{1N0j;{A zv${j8x-itQvwQ$SC)5A87c$v!dS>%G=XtNKq`egc3?)HVafr1PlDGYSf~AbI>!FIq6-1Gm zIV@^HEuQe|Bu5ilGJnb8ov!AcdfqGhTmQ&4aR0`C;~?}bh>q&dsD;9BhHjzhW=n`!hee_4_g}dGZPXkjX%bB-V}jIbh_~>M&c( z&$wKy6aOI&{ol;?ks3(rUb6Hj!T&{bD;S3=;wI_AGy9umrF2Pqq>My2jzfh~N!!SQ znx-hIV$E}tt4OP4d=z=tO(RI1Tz4CZX4NV`L5k$;ORBB5tw90EzxbGyS#GG2@@xZK ze8KaKqk2Z!;bnZ4rMj4lSGc+uUp1-o*MF}lOlq(h6+o!5D(&c=khKxcZIr}YL ze|LcXNcVM)hK29N2H<-W2G=)^c1D6!-98MS59bJC-&$}{1gL-N*i5HXB{V8=vAe>MLvJC{x6OF7-F(3kAS^YJzXd8Z2 zzaGm;T2VEq9JzzE+@VC}^emnrTFp1%izN1)!%$*zdvEc*sh!trgI1NLDr z|853aVHVK{Jmw=X@?DEm6C=B-S6i0%=ZE;uSab7Q*A)l$%fNA~_v`m8fTUF$^hY@+ z$21x#eSNDc3lnuyw{u1fIQ`khT#p=-tE!u@#2?_s*mX&&gKykK!unoc}dc@{pn~0@0zzW9WoUcf=IWhVtAH6 z8LCrOqDm9n)^M--8TK*DX>xQ{uLJ(f&-(YL$3amnV;Z}~Rlq^B>yS0t;g7~UppGOX z@z&&nty}@tto=~_oMyUQYeT#FReeQ$pb7>0tin8dm0zu0g3KE>j#N4N7E41b8+$*mK->iqhFdFetuAgu*6$@H-Xaoo_Mxvpo=`T3FS*jBdEZls!>j-v)5}5~a8L!(#pnOR z-tVzCL}3V!C8vy}Nx*+;#+yE?vo-|7APsw)zq{%vFRvzH$aj_FSRFbIwfaDa0+&Bg zU2f+sb;)Q!-k7fCf8JrB3-S7P)+|z)cv_dwe8H(+c)b^Z7)#l29KhH{iz|H6=-@Z! zJ?vewyT%v}euk4=k+Pwm-pxCpwoh3X;s$r5P8i6S|D1S9c-fMKc~jNq_75LyMx%=^ZbZB3G72$u_)57DA3%uh-+kB8lzr_j-_4 zEIg;H1p_rK%L#?;C4kRy|9zN(5t%azGAQSAlPrRhUv9S(AxME69Pl*aDclW7l#Wi&@pLkSe_j`^s=8P zO9Q-_p9QX{QB4ejDfPJ`^La5{CY#_i9H)Em4P@*AQUxb<_eYzwnv}Y$k02d<5Bmdo z1OC;Ww|MBTN%zUpKrWdAjc9dXO`GN5u6vQ|ENm*J;?G0x(-(;Z95KA<6ANp74%o?0 zCt_`htV=g>{?#^1RY}f_;!i2LZoL%dV(h_0^u&1Iz63p%_sPEGgNKf65TY$Ixl~qR z={+;c2<1%$kK#NhR~k(dgap;PJ?kvUY2;pfe^)|31;lAHCVINxt0 z`eg0-LIXB;yhNaq;8_%36~#uJ9Y=rOY6P;}rRYCE*B=0a!i)|JqQtzwr#Xyyt*pxd zlf4T>yhj@iVOirX?Lv~qJ@~RHNbjgm%zsoLjZen|xW8o)*?htdcL!gf!V`3y75!`y z*BBe|$cs!X1t{H8XF$3T4#bNF06@Q$Zkfn`V{ZfmfX>zuk6cgL<%?B z{htzBN;svD{{1#!af2J% ze$}693)-mdKw5Un$>rmIocl%gi7Q$*-&`G+N#wvhLWe*W>}_A@zsDXacjbkJr$lNxcT|i;K=b#i zyM?wxk;RX1Y9&^D6M$CnHN_{0IljmU#BlA>1d!>?Ctttdm8tLuxfz+I)H|N)e#YvX zZCLSJwdkc<2i*?I=p2#_SMfe-!hfi!Kerwp#fRVW?`2(*L)Ic{_K69^sNuO%0bq;T zr=%`*Ri;jv@$OWa?${h43UNUZ*x*3MadHIs5OD76J<`9tHyfVz94}bJKUkRWDgvJY zMeWzcOcyfCPC+htOqeE>q^~PqBw)94tITkAjMxJ%r~9H?U!#1wv7ikHeOJ!?r%4b{ zyDY+hjg?MbGiL%oxDqQO_K;`^132B){1Af_mx;Oj z@O&-10wWO21oV9sh~h9D7mSc2THToC5uQ^)X~V3?#CTOp4Q3_>3?3j2w_Ho`Za%UA%$ zU>Ig6g(1G$hFoe_{`TZgpj}=d5aRRfnd=S8COVTWe-9UQ0h^m@F68l_8c=T(Vt_{| zq?iCw!y8@$I4mE7p2Eba*ii>utAD%fhOJ-40ao%ZRwTbg<0fHCc;=V(XAqPaRvsY9 zZnlX*9r{hCzn23B?4)>IW3Nx1yJHfHK55Q0A;}{ShAsNsq^x^-^umq z##R_MPck6r4wLrrc!%sCiPC$cT%d4(Q3`6WV+(0HG)rHD)?PbZDWg5Fj9O5ip#2z) zENyY%PVQIlykrIBJfBfG%mRWkT1`JwBD9l+mAH$ZJG2PN=6MP*d-4Vs<^KG6Dy>BT zg8K%TpV$Hj3Td-M{na`-U4GVoqGUL@omt^is=cO!(a7C;fU4F;BbtrU{X@jm-H07_ z5_j3(r^occW28g-+l$**VSFb5Mci(Y&@1$!4Sund7jV=lVBU#}b>k?@3Zv`eo+obt zQvykO2nBlGwKi8gStN8L7 z1Y5p_sWNuJ7;|q?iGG39^?ojf1Ll=%-Ln6_m71(_9Q(7!fCgt1yW&qjpJ7TkfFr8} zg>vOPfJo>|K7MvW~-iLl8>~65y&wp zF_Q<$reoy$XbA)OS`bVbyDJ-x^pm{D`@Zgnk2pApL~$U99^Rp}{wQTzafyW&;{PNl z>vPcl$tL(aBUHY6Z5O+T2#k>e3LxgqNJrh$i%AwBbS>T)<`3jR1_9a8Vc?_x-b!Ac z%HS?wGNIwWXtaQl`rF+9(2=}n!$d`Q`)soO7Lw|2R`cN!5`vQ*D6qp&8=s=Rp=W03 z{;Q4Fy`SL>$T_~qlMY&ZzuIvG=Ju5sURiLs78E`k(cHC6I7+!Q&?IexD1I%bUoak3Y19ERrK}Cq|G`&Qb2j3e& z6;_)!U5`U1EKks2i?m`OHF39_I&76~0|*6D+zQnb4kIrpLZ7_9gUHRS+A`uz-_FynDx7 z^bF!WO(2<154BejFU$1%Lh%5wX|dbg)fVy&)_Wkar(Mr30|ab9&6hwB^?e}DE1t$L zt6w*O>4M}j>QmR~V31=f1=I1YX!;wRRoWW^8c7NMm=|iXvJm*G8<~~|lOoyQByYfh zF;fb)PC%U^yr(<>Vi&#t_9L)}7>{iRx$B7oQ<*?n!03$QouI8nD5!dCneuL| zfjnL5Yj7dx5j()nQ7(_7pkZ@HIm!>v#gdG(%dMH^vOrd~dC| z)tq-7)z+7_;6Id470;!Kqha4?Zh^Sy)@nFD-mMsr4`WtuTf1QsgeuTOT`c;%fujM= zrDab7L!Tc5vl~Kfr}F9rB9qd&ZyHn?k|iAGqdlKW{MG(%XH7lz4%A>W$cKRA*7{$f z^IHIs1q76?Dfz*ntgI|U!UsxhoYj|NKkH0NQJP;dOlabQV92!J(@aTFk7G^fkX5@; zW`#q55f*DcThqSC05kM2X&vDh`(Ey=ObLo0YP4>hlw*Yyxt7jHHlsKo1heS$S9Ta- zAH`%XG$VX0@6X5AuD-?i1UieWuwn|91JBhqphJXZ+e#=Vd?d~*Z9N#oCD}*4g~;Os zW$foPeG|_l*q~Qp+Xi!Iu|@U-nA8}<7}Gc>fbZgr(??}pciQ?iBh~wllPTN&dnt}Y zo~O}jnrScr>7Nr{k!4@YjTWpeI3Ou&XcA-3lnMbx>B1oJ_N?%Ol3aiL1?t34aMmcz z*`~%Hq$bcu8vc;yXsZ{hS_i*@zKeU;3#nnYJGMR zVYSE*381IrS~~Fq;j^ZTL&3g;w%CI=1!$wdzXA_U1*bRx0-4o*tlG& zrHi@&U!L16V+og1+Hbr_V*(jRiRYVT0f)6|!@Z0Tz@Q6`;S9}C(|#rAQ9~n!C^DAELFQ;noPrfwkYXa;O zRN=aLd(I4~n?>@~`*|}YZP)Ua>}A(m?++(AlpK2Jm$Qiia2Z0OZxr~ZLpY|rQ|W9KRjl?>)du+c>npDa?!zd<-eTUHlu zUuhwziPzWJ)k2Dq%#jOVAzpvzKaQ`@qHcRcC8D?gZ50wJ08|z}+%Ag}<7V%YAyb1s z1g_{JJ&+*&UfO*{l~k6(q9))MQYwx9lp6+@tg35t2`AV-FZvT{0Kq&^h4SrSlyu;y zXkP_CO1*Z#A)vfU4u_p?^iio<#q9&pg;&l%1~psUtzQyohVI9$)GbgyA;2dInJpdP z%o0}bxVZ%eV!FzupqS(_T`mn1p+Ojb>BUQq9SWW_yq>DijrC-XDcg+=6` zZ^Jy4+qXd2ZjGZ#{}zEzg>4%RHW$)A_OlU*V}R`fS#|C|m4N{aIP?#4zb)~9PcHmm zgO}L9SuzWTP91jsls5TsU@t^SA#9(DC~^0R-;Qcmh=S>w`1Pz_m;IRCeV^|eY4Hk+ z?ytcHO0s)R8_}v&3=s`GV1_K7lvL17&f&wO|Q*W%zK&bb1fP zV8J_KDMds(DwhS`M#oR0A0K(!t_#hHHf+{`@=Ow=@>d2aI4wP+TDAak3;gB(42h%} zxxZhP2e}03eNr9QdWCF+Mfy%vkv|RQFy6YQqgI(^%bUcG2sT!=B!c#DpO4CrotS~Y zyCS>%4X;-Zb^-FAigT^2V-5h$Ae-lE{t!dAnRAW{i}_-w;F^wk-Y&l^|Ce0$*{H!@ z_5`MA@``jJVpjsER*w?|b5`Dc7l1~@$1TqugP$6cF7Px;a_`!~EkR+>efkS<@0mW| ze&yl`B}d+p_mTXX>;FAM-NMslMmXUC#fV%%w5+1nfE)+oJB)@i_tp0vdsFRzE*X@T zr3GQ!g+Qf*a76PmzyOCW9spQEQNII>#b(ZlZ7|@UvyBw5YSR9Zi+?AeZQy3{3;C|q zvM;eyk^#);AwN!H0i$`=zVv}fnR1DsA#9a}T;&yrO6)J~nEb6u+$z^<^2?MD(dzT- z!eF?G=wB}Hl{P#a^b_D zlLPL9+&1O*2%jpa--WbBDs9YlKLF0BqOCniJkWF5mNeLhw4&IyG2%xagIzyKI!-n5 z?Y7`EDyZi9iFv?*(kLsbBA|Bg#uI=|z#5W&Q?Ui5vI=%xo zMH&!NnQ}!N^F)t6kkxlIuvZL;Y|sn&cY#){ONxuf7sh5z*1DZJCLk8X^F<7b?P815 zXUUmRgxR2MeG#HXh226~HI_&#Wq{}jJZHe2XnuyBJGh-M~(XbgIn>!byxfUw1C=m2Mrhu7_E9MGH=UNH6Z^qZhRU5 z2lQBVEm1r)2nGb}NN-lLD!^-SJ-auMj34hHz$P1BXYM9b`7Rsto0dK@bN|)fUku=V zHdN$ko^{Y|FAlL!!-04>*z2WiD%{YT z?2dP_?RJ_(xvnnFUv3a?XoA}bApG%Ia_^6afOk=xmPaEGm9OtWr@}OGsTrpKE5In|4BxboRJK072Qc9;ONgCx9EptDjl}Tc4BT+*A8k4Ee zQp>G@>{g|vvoMawo>Wo<`9NUJKL21lp0L*UzPP3QT{9ao)%0}i75m~BcuN?TucP9@ zI1sBon`)p3%aWn^N%A1Nlv9fYUDqPXNSA~yd%^)p$Q^EbWa;_{<-$rz+$G*xt_Z=q z6kQ?bGzcRCef`Kk6XN-c8u`J59zxTKr~Z3E&*nT^ZWsksZdhX2JI3fE{%-W>a}RfKJF;c{Mi|LRQrC70d6&sHJ(ug~$V3K; zX^7q`tDS!0Na*%bXuT2_0ndb--Wlb41W!9K1CLfkz_nBS*qNp`mM6-Nra`&q#wt8)(s!q1PP5biAt!GjC|@OPaL33(Vf zTHT53iyo7y*MNOyIV%jIkT*Ga3oas7UG0l3;XhLarYX_*5hFO4hrk$mxHvTr%3C^J zW*PL@a*|1<3DU7dWxJ8>^PU5|52Nc%0lQ-bRd3_}lnB@zd|pc)0<<@8j%;2jO~HPM z$D)w^92nfcj(A%uG>Pst&Ykuub(ac;@yR znTNT3=Vu@Al)G~ zFr*@_fS~*@?&tVD?}1~_u=lk;vDUfxnBH~m_avji55T@RddT?Tj{}{YHM?>Vtlrql z=qyjz)6A#(pf!vNKb5s1jn@$M){u)L*kMs1(HlKx_)MI>OGzkExRs4A*m%JvjNx44@M0k_J_q-&a(}P?t(P;uBC=unG9em7&O) z$jUhBt9|=?o!{lBUcr6gK|;T;R)fB)`xn%PT>jVQ=(YqXIX*4swL8|*J8eg5smxvf za-OCoAUZ>2?-Y+}q@^Q}` zrGsZ*e#sD28@*vqrclVly?iVqEluE4yHR7jqxJ+0@do&G_MUAi^pgwXsX z<0S9u=LzfnBV+)uX!uPlxc9r~5|t3-)e8{0)^v8ha&wUaJaQQyD;PZzHC zdl6f(^QP=cy(U9yJM@ho0avgv+>o8y<+?RL?lSOdh$eSmfnXyUhGQg_L2GQB1wlsRwZQ9F7yNa_~Y*5F5r(_ zD63*p*thV#Hgq>vV<>7l|uY@8{4M zqZasCIec)iEYw6X$)sGfF~uO8nS{aiL#qOw+|hrB$jDk>OCG66rP)PpeJY@KaE&^j z(kaAF*yMlhaN=~~id*=Zy=sE@OdDEMFMBG?yyAp6oKGOc>41f}_QTvsHwvfzb&zLY z%Ow$mVe3|+Dmu01(~|dCI)#nRhKym9lzmkTVirIIiItbA;m zhMS#yntX*{RvTQ}M&^KBGI2pbhJ>{<6AygNuf+c>v6F1!=BUB^`$;8>&EjGVpNt%b z$Yhq4yI}lcQ8bMXmfFda9`UgL+?zATLVJo1K(|OntM;tiM?>_HWSDqD+u{N0u#^Sm zEkd`p(iP{)=vMn&L_A#dtLNJKeEQleKf_vVvf0-TQ}+Fm;CON7uV-L$Eu7;R++Tp1 z{z^e8cwPwUlV7MU)cur%%Je53g%K~B1%oJByfn-={jUT zEqzsgS)x+<^;NWF^PSFe6JL{DxMKhFCK}JEEL5fiPGYQ2npEB5z%h=4)x!9eF5Oj0 z-w7?L=A=eJ_S1DSfd8J!D_(jz|MFw9_yf{cjYWl@WXE^>910ByqmJrxwB8a8%2y&+ zWp|w6G~d5Q-Gb30PP~+4^?~yp#pHNUDqN7ye^(K>(gQf%VDVnKw8B=iC?RX=Li|UZ zR>N+B%W5UL(cxS#0&eQhab2*EppJbsv2=c5)y=r%t z!Q7*Vvy+X#LggK^bS>9FIw2x-BJI^b;E5BV54Ti|p)~8MSa!f|i5N*-5NXPL(AxzE8y4W(jCd4ubwlaD zzImJfg&LVyz5r9lp%O|R=b2ks|A`o1sd9^ zLPsPmyt2<_U}rgb7a0I^`r5->SWr+dKy*XPo(T{L)KVrR@id&wc0BOE?C?0XBor?F z8RUF0L4KRNemcLf54823eZ_gLFPgg|7V(P{dwrK)@kwj55t|SqFxLZJSuJ>qU#6Z( zc9h9CZ{`~i#$f!0%Z4WLUh2%c`A2(?lnxVh-@JXLQt-8mUo_tF8yKz$@z<+)ZwB@? zA(&?V{k<+^%Lzhk{ZX6@ySc1fSR?`V{dbU%yx#Y=*_>`SW#S{G2*@z?qm3RNWYI~v zUN)ki+}cn1(8v6Magqr>LRgWtwDD#iuLwgo@rCY-z#~(*6&VDgnmL zuL-CFi~E)sPzR9hN&+fg91TazZ8NCNMJdCmF~d z)grsk_HS@?_q5{V+t;V>ziEp1$ue7MzP8ANI>ngQrSD`s>Jo0zHbtP85uGtVmT1(; zrihs6#vARO9Nr9krMHnCjk-!dYgaRHKN{rGeugDZux@%@jPf}hYe1tF+-4Zrs>qX` zNf#w$YGPsbmCl|p5n#+ylgjB@tDe1ij7Ke)Y*;67<=TAV$CM`Av}y1sP2JW z$rtQKSfCCUMfjWd;i(2WWN)q0DXF0i&Ur+o?9KpOl|0i!w1C$$o=&zL8LR_}%Tw{L zM0yb>#7A$)jg_f|^`j&D55;-BZRlwBU4^Yz@iR$=Jvi_d(n10mjlE+8Z3%qt<5`mA zmCUhWti}(i^R`O4EV{?oHkh6=zFn_X;E8EasGV@|3Ow>^=cF(uPF0C0VsL}B*U*`( z%VM#1`TA2jMb4?WOZI>^X-}eIt^%phIBH&AbF3=tmN>pIzMK*tO*0eYa&X6Ex^cVo zi?V3!y3vH5@X%U77Dr9 z^W)!qFw^v!>8xy%ZAO5IKd4Fw*`Z1#FW40`rHf-iyh5ZH+U+eK|FFfrE#U#fCgwS5 zrrc@gX%J)s(explOdO&h(VVKaI5x;bvVZ~$6T*bvTZ}M#wHPC_RLp;Dmp^}h@0XvO zW^OlrnQQHT($b?9TkNOP%ZZ=cB^)qw^WNkp4m**)|FaI`Ay=vHc^Ld1%yJgPWWC}( z-NrJi@o!b73GHsQFl=ki>m4Q$t|_L^TfIhqhx%>Vw4odlj6&tLhHLynp%w-dwdhN) zo5^=Q>iDt;KCArG$jI1nJOt~yZzo#lA2u#Axn{f%8^7HtjNR66Yd{fsZJN|y6kZSG z%~8d8d#M+lCUVf{Ah{`oS3$nDevs*q62BC&$X!M@HNk(fkitnu^lXuuVEL&qU| zp&j#&;qv78$3535}UxX zBPa2h*%L9|@n<@Sv`BkTz@=gg_dFWpNJvsU>Pm-{HrBF~)&4+Fi+A}@PNhXs5@88p zdUdXeud{Yy9zu;76WjtvR-_8C|79Fw$6(2LdD0ev**eiW^)PvT-H+fmDV$@DNsHM> z3@K$ZJI)2RFihu@b?G?1O@{xQ1^93ciCbZ^@RwO{f8THmEsP<-VxsNq?aRY<`n9E; zge8S`t{TV1$#{uklHI=Zg49)W^`MwWo?>xgW&75wiG4*-bv38^%Fo;0z^-o9D5&|g zJMhjGE@34%#D0{tWNNd=!WzwpdfUifb6CxBdBF|nNOg2LH6_Cf1NNJ0dKJ5F99zy5 z6^4J71%-u>-_$UzI>{ZKeBX@{WyT%;PPNt^!Z>w=_g6B?(zs`g$Z&Q~bp@Jjey_{= zxdcIs{xOwzg3n4H6B)neuT*;_3mb+yR!E_&dEpGEF$OH9`teD;F3$t1cC#f)K? zTpXE#k7(6+lHZdgdNaACrB<#Mva}P52YtXvmKy^lj)1O8;6r8YCZur6SX9u_RGa>k z{Ac7!xa6y3Wy#ZZ917~t)iLJi`i8>rEUFkg(2jme}@F)O;f*iF=7##PR|sq+r~ z9H{SgE5iyZMOCqHiu!p;CSGfUbNv`hm=;}4bhJ5SDpFNL%cX4tUh)$8uONM-i2ird z&YDqVv)i(yxfHRCJ)fojvV70Qf=gPAT8LRP|7>6^6JNDP8&V(Fmzqq)_q)txm!$ z?XC5lK~agg%RFlxsQ(>MjP{h~E>gAk$E3sL#E6Fy5@9M>EUNPNsQooz3#Jj0{zqU> zS909v^G5zA^9m24M9R(>T?^@d?u*rEk;Tx{vzeIcPbMU+OsF`b;!{$uX`&o*Y~Rel z|CBkq#ks%S?|EC}BjObqC$;W$`q+f4{m=j59^(eV`0}6>yVy0#UUJl*4HpD)jmQg{ zo&`VS;CT=}GWf6$qupR}B(Ph-k3eSlJHgP|l~0S4YR#Z*4>(&#Ay`Eay!qB}(PSyF zpui0K{zUv7s>Vne5?MDJkFNhPUA=eY`Z{<`;)$RRSqUlkRlNG_&w7iTx$vika7nV* zR-8Kf$qyrXF{hKVuLim+)R<#ziGECqiL8`ATy2fvnef z*@@Irt=_@-<7QC)a z_R+Ub{2Uz`&>CL|vMRnpICj{)%XRO)3hx-GkTh9~E0C|HMH%3phMc(G>CEg zCJv8`RNlp${*O1#g-4P7l*7!%@02KNe>ipdNf4G=w4`t|%*;K>B1$w#C2LCIXZrlP z3*i{y2Z5JN@T$?L8peM}1zle)yFGZV@r(#dTEX^QT4c16ilri6HNabI?rKJqw~k1Wo{`rSU@Eos$pb3AogyOT)wn@vTA`UUDcIoAZDp$tvY zq9inWslskYrh3rxe)4x3aqep3*ma9CeOGt-yXBtS)RCb7CaSH%`l+cP-(Rh4335{W z%N5HQPja(e(*oRY-f?~kahJ*ui}>Jf|A$NHgrJf`Mir{|;>K?_4j9`>cs9@vD` z%M_jG#gDpe^)@+i-2a2nXLz>9Mt$^L;nit`U{EbrCAe|)q5KYx`QiY}^oUjXmR1wr zYcmELkFcN{RK&+G2pnSlIoUc+tv?*-1wNur@s1~ZhJ3OXY?H=FMlYr+mgDpf-oElw z%kncuZA7j{v;h-?JYmkw>meOEnIsEj$;s_=|E3w4b^TG=55dFt^wgQ*ozZd{oHf2C zdcArfwAnLv07_Dt!N8=?-pW&c1^9?BzYMqF6vm3P9}@^f!XNUDWjrd=?YaIoJWjYu zgVj7{5m$^IL&?|9{+TYc>zvS%!)oQSNOc#RRDFvjXOjK&VU0@&&MK8MdAU*_9`-@O zPal@CfZ)A3w#Pq|?4_fb96Z!)dY4o$2GX-n`R#8mPh!W(C4PYg_LBFh$QXMRV((Ue z#stCBn1!;AOMkV)#2Wz?Pn3Vw^+_(GbMttyVy=oquQ8r0il%RxVYge1QZ;1TJVO5? zG{NO}T9G;9!e5c$3a_#gQYk$UO82f8)Iiz~(mF1Gb7Wg!z;3a9x=!+k6FU5Mp^lxj z4&LpEq2tNF<(SR39Rg*-s8CFL6qtZ)+Nk+Gsd$)SHwzt(wk)gP(ClU^O6F(E2)Bm& z6s7U&VAyjqUO3Pxbv9Dq8z`bU>ly+qR95aQ6&L+5pd5};<70bxO6i?c)T_l<7(Ivi z(XK<+QQeu)G(-)(1WVDvGZp+FduVcHMFr-E;*Zuj)p= zW}MA&J4U#;!YYGwg6<^$X_tegg+WcRFrH7LNn;O!kU^{sD@;@xlS-<&KBk={7$%>~QQz|EC`v zZqGQN9(RUsPzkQ1LAx{lZDA-Q0gmmoTv+$}^%gR1F55E_s>~&Y>{aubOPR^)dm9hE zX`08DJ&6TPxhPhw8$8uhm@AY`^H4;1v8|lcN%1&FeDN9+Dixq>O1^$?95!ax&{mNr zuDyiqvkKOBc%*)MY*SI>*dOg9YxX@L%$ytadeH2yzuso=dZC|2^4XwsBk9kNY*8tE z)~}NJYjJusBNbw*OQu@zp?1F$Wefs0NpEgPaVM-J{#>R!9QGY?3N6CSDW;Di;dkB=;4( z*UqzKBIy+1M|gCWsJj`erDhLbRpqFnoV&F`UgLNjWqRG+rNjE^zNaKXAIAtk;Uj}m zqtsDN`DpK{8=V+|t?~+5^xGCPq@>#3VnSD~;T_(>texhfh$9$f|Hk8jA?TW)wUijC zlUw@bcl^>O3*!Q(D)Xq34o4QR)>}MHqLwG)Flr>)Mx@Zt{G|(4jBHM?R>*B+%`bi* zVtU1pIt7!c*_jr|LxX4UywOdehH2Q%WBk8RJ2b8`b-KT7Ib++=D?nJ$r=Y3d=Q+Z_ zJ)As`he-=NR2X|p-#fZk?*w`JeUCQ2(}8>gDjCOrTc)is$Z|H-^g&zF^9Cp?aC}OO zz6`wGX5Cxi{)vNCMYHx*)31r@qj?2%E?UJO#ytY_H%S{&s#0SU7EGP1l6j;$!$4cj zs|AlAYlSdVI}tiY#AN!Z#I_WqpTK`eH9iUa(WwyO(HA*;N!$IE>Wjm;JAB~!Wo5g7 zDgy6|;Mf9Z-IUm0xNbhpYF;dE3%ga9qoBf^sZ{##8-xtam&{-+%+y}3OE!Xo1)WrS zwT}*{@9zST6@Cwv9|Yyyq7*d1&F>iCp)i~vy>YS`R_P~#0J41MM# zz={q3FnPI40Q*ZMaQ1+isY|onvR(xP{ZrP2l06;#yP3t)Q-5H;^o*5DW*&+ zK(eqILDo6-d_JtV;_Bd2FqWgZ@xVBiXwygUxFgHnkhggM z0|FOfKPdA%Mm~|_;q8-%ik$P{RICl`&v-q zamBR^cu_bi@Yndou7B94^zoW~Ut-uR-dP-uA|?~0;42(|!bOhXA@?NELI$686vb`4 z$WQKAby#|~b1swiol<$))^E8ZYapiS*PVH@N+Trh$3V!I|Y&lNv^`a+glY`6YDmcaFX=7&fU+GkJ%zy3u)SE-|q$Hm91$Po2 zgQpoBohCaRN9I2o>z;KqFU*^BdcTYuA8GTdSF4Wg-^(v{l1k}zL962)9BfQ!-x*mK z*6J6Ojqi#m*H34d8F6z2BIQ9|ya%6UtLzG&jEY1-D;&Eba}O0nJ{9qQH!6_QJ02of zz*uFjGPL0g_)XAv{sR`O^y18|s#}(8yp(~(b!!9IvdM+)kO|k&eKS6$cvTe7rX}$< ztJ=wE(CePD#$F(R<0TTI4|#m}-uRNDQ*t3Slz*lVwQ8fiQuTMFI7wW{%#%MFTom{nWA+s4bTKot{9e@mdq5us<21BG zf(!yEcPYNzN)W~@yF|ULLx`VjBIG}tH+1zlC@ce&gSW)#qO3(YF;G@1?lfvFH(bB> zwv=U5XZh3qXsjh=px+MI;H7w|-;-a|5tXro8ma8iQInFXImc8&? zhB2lqhDUb4BaJpP$-fFZ*G17#=+pML$SK@VmD`QF@VGJ^$Iu<)qN0GNyN~GV8Bl~G zOiv6PazxXa=8*m6!>fOGJP|RrpTi(Z@fO!=p|b=L^u>0r+45`GP9xck+pQ89Sr6Um1*|1s;a*e|2GR@ zljF%ae~G&PA~}XtDNGm4K1Yvr(wPO2pByB>d}umFH{dzXEAl`uq{!N|RNwJDBGOh3 z{9{Zx_C4LC9Q@CXHWgX)b>gG9ho1*QGrIc)k&wH>Xm(WQdk~or?=)Te(R}2t$vE{1 zBn%fByw`Z0QOOaNJAWA!-;#h@&OC208&(r*!y6AEahTs5UEpTQQnlZ#`myZ|+2C&U zIMUtcgTW75#Y|!;eoa#IiF;6w9LJ>-R@BZ(k*>&~iC9(T}z#C6m}98}pwkxd&D$-KKeVJ;~ty5W77huEge+ z!s_*(=r}op%?7GS8*~XF2_0jOs%rX6aF7D$FVpw!#FuHYMe3vhLRWgxyqet zlEywU7!(SH5f!<2@cFd}s)zU#HFS>vj#j_$d}5FqJ6$>bB!jYF>}11lQVoxwRKPW+ zWS{T6XNeon?dG{UvbRUR1yu;|r@_QFN|^y5{SCa0J_FZEl8U^&QH5Ww;w=FrRUf9Y z&`%kzxFpoaWDOPrCY1WKY8wb~@#^}R383lw+?)|zsmnV+PQSohJYDak!vNd|TD^k~ z+1?n=62Lm}M@($ltASzHX7m8;re#(%r?;`ZF7L3OLho)ZmV4!v5*X2Q56c$uqO4~m z^;-g0+x!l&TL&;SiNF<=6dhC)sG4h*4G^UbtJV3+qd4Jh%PoFruTYIXj*J(J@)Nnm z&(UE`W$ChTB_ct(9=wcHfE1p$uE5iT{TA@*^!_%u--mH480>P{S-1bdQSo4-q0H_} z)TUd<3{K}CaqEs81NCCPL52{BXX6>|vEx3}UBjGP?8K}?%Em1a%OS7x#1pu2Lq{Z> z-8

%n~;T%&t>;WChxDWE8)m@wCH34DDqH@<{=zqp4m}bdF$ru|8Wibn!nODcHJ! z0a&;^XDPTo^tSXAkCJa%4Yuwkk#(;UshlOpMgG31GJTEUW`anJ+;mDd3j&g z{V%+zS#Hk=oj{xhV?Umrm>=PWbhG{eLQwW4G4cnKD|URH>FU#1tP%^;Y5_OGnsK~Z zlMP)SWfsrq_DVW_JtC5&_>isW$#|MO>uCRL?gV>QLW$bW`a!MT$$2Pv%7|>@0Boi$ z=gkc9&E=|elpTk=gAvOyE@A8?Px+{U-+voGi!D}pw4`^+(E=^>SsbFm=REqpOr}Y~ zLHNM8LZMFMP6VK!x0T_eY$V^LweWq2))@aF2gr5l&_AMpO!yUW3h4OKkfH}O$b8;+ zNJM$Y))cf7G}FP`jw)&~v0rE@iQ^-;vQ_b~ zj85q~L^Vuc!Gi9mQ9nL2bOtCspA)@;ul(F{_Jq%$C5{DALId2Z1+paMLNu~O*Ck6Jvb*_KE-O#R^gf*b1<4xtrLyC!T|5fTmLY~t9j@M0 zekr}ghTMvE9Ac)6;LYAQHzF1<@THCep*sXSV@~sXSCd8dIxh2Cg&a;{0ktW-T=Dod z`tZY1=7%6<#`|U;nhTmEvVJE;_3$tjVsL~2|xkMTYPXu5P5rjUS3!0C5Fk0Y3m50(h9CH>@r-s3K`znTJ?#2s=m? z;YTq_QMyYtXeZ5{jT0fQdI;pdvG>>!gWMVWNpc%)55hruM!nvBPWJFEd@IS&tkfhw z?Q#>3o)7X=i;0h=Sl`=YrO>q7-e(^LoK=ZI=779#zg83iFC+LMc#=Wx=D4?t9t|$i zomyfe>T%$d){YZf@NYAB1u%Co_VNaaC#z3&ET2gvybk*=kPq;W&TadXPYZ2eQPbsm z0~*eNzLI9i?rreiWPP@W2o0@sCT8v@rby^7%lA)a?^A)?*js38;E*k3e1TwTuryxs()Z{B^ zPybSd1*|>KyB_gkDj=aSvGs`Ut2#mzx^Y0giX=B%lB%l(+EQSvhdF(FU37e((GK{4<1mfb zp+iHwC)un?j0cIr;iA8$z^dq8NKPPc*o$nK^{Ge^`8+AJ!C7;qA%4TQp#zI(5~T~$ zZ{VYLSK_)d{X+$xaHN9$S>Gi^eqJ~`P{-W}+K;}0#I?Xp#EeBf$E&V%5(#Zm%pGW=0kXMTcs0mjL4b3lU4)>d=~pl6!_{CQk#*_N?3@>I8r?%i7T?rhEJ zn1Q6n_x9#{IC7*J`kR;+7MR0F)wF#+Ln}0nBIcoKwS-P;c)R$^z=7@OQ#4kgawo*! zQn}sW&$WNQ#{*$_lb0Hkz)8+))LuRN(~5|;5tT;PFACoEs`5iDrv%y zOHihnTJ2iogw|i%M1{V*=98TuO=CU`X(hK=ieK4RL@7C){+?Z1?gt@@%MgNY?>v9EwJ@0OTJX0)7X8?fnPwAf9RABYO_ zmrEOX^2z*cRGX%~YM~)Eke)LG4#sLfgZF*D5Yr8yZ-iMOP6gy(?;qcq?G;sBv0>bj zEL)|XJ#N_7PO5KgJ%5n;VARV*d!pW1y5b+DJ-Zr?y4a2t@1zEP>XV=Et6yPPyV^lB z34Y#8k80sZ5#6~MpjG`;k!{-Zj)hXV5Ppf`m7=vI2D%Qvvz4=KV)Y35l< z6vyt`IYL&ghJXldrFWkGynCDV}?h8@(b8ZfUg9hMc_6>x32!zBjN79Wc-CfYb^lJTf7(rFR!>tAm~`n zWX+fL6x#e5j#Yxs?M}0Du3TFm0ZwC*U1MOziMYxCqMnZ1xHCg}{LyVE_Q8L8m-l-C z>UTWuHlCUTqi4;6)4Op>d*DD?E5I}0UJ~tE)U~(uk-TB~+?RNmaGNoA4Ef_k0w00I zl;iL3KJbRzs~}V2w%Tx#^xd!R`}3UbU@z*DL-!jeTNN`P$^LU1u55ob&UfrQ?dFnp z2Z#zWO>{NTdQRr;&_#vs?cjF$x1^v$Pc73tp#yG$^soTIOo-m-kJ(5=rzD)T2UO%d z`CkEvshRh8?m&0++4u(Q&8v_3cwZhW+KII7l?oKv{+NjgNA1!0(VKo-^ZkME_QOZ; zLMdjF(EoHr{{BcRRu729xu=mxI_HH(sKw#7W-IARdfqGNiE!pS-Oja8Swd#aq+|9G zsr;iCBbokv`gHCham20rakJBARsTL2pgdGVVe9Y`mb~{>&wl4jOj6-jY#dA1zUK0n zjOaxGs=u*%!T0WQZiueExkC#+(8sxZRrSL50C<92)5gKsvLn);v!Y><2B~BAa}x6~ z2eA1A0HVe2*T5$055ufuU}_ZK~ctKl%C+{e*!RR@bXh-{t&Pl^s!x_oQFQ(5p4%(-l4Ao zzAra_sOdNz{s@e@`XQ;8EEd4U1BstL{(StT)FUz2#PP9n&O@A|!u5#kA1@DqF@#Qt z;rOiy@rHW%CtQ5`s)-C_;Bvngr`mZAj1~-;j4#{3VRaZ%uvF zc5a5MVsj*#*QYsPL6i`N6seaesqwtbiQGpDBR zTa0`h>@Q#6GTh!3AOQ5p&4jzyB4^W>613?a`awSD8^nnHWyX&#$XT&k+}73Z$*@h7 zlU}$)`9a@c63hMRN($Da!Y0(G1!LKIIkqYWaBKh7CfsQ&885Ny#9{ctP z7@)J2VRE?YNAe`3F!seUYXxA~s{oEj-c^3O7QAtrYdQBfLKGnJj-SGz_@qN zd=FBI=T4joKAuoKOEvrhtpJQ*G-{PEuq{toWN6VGC%F5D2p&TOck`4Im_JZQ@QcX4vJG=@1xh>a@|uU|$LgJ{M$>ppU)^2~*^_QN zZUNXS_i3ghU^)05A5uZLor6XP@8yv{;FJMPBI<7-2x>c|zHVW7BSLofx5<1rsuDoz zAYrT#BJ;0se|-Y5mudq5lLF9qcdXBX2s9ohpQO#k{2i_ z20Gj|;NkO&V7JG;h>CT^zvv?VV*bGv3$OdhX4vx(o1~{Kk11_7pHceJkt`wk$VQ3H{YnxAW++3E2lQh?RhfNV4oU-qkoT!M7+0HwxYg`dt~bV5bNeiJqwdSQOdfkhmG#ev1`QW#=| zR&|mo7b;ir}1LH}_d`c0J&5?RsbB14v%U4+aM3v@_W&fP9h0W`P zKIB-f_q>4&=V*nNo1YXKjLeZZ8dgvJB*QJ_nqHkFPkpsBfr;ge|T2qF^|NR zjktspv~tf|pAf-`!JGpEQvRSdoNSTjSvDX#%b7%~ym-l!X~~M?IY%*Jmk}i&YH&p! z_xR(*R(6aOf$4Ul^)t?O0xm?as&AHE6SyQkDkEUByO zAilUnJ*OIR&yT#9J|XVZ)U8UdJ`v%B0{*o?SNMlQJ>xJKi{~*DQ9kpJTC9?!O)&j` z6uch!G+-dXx(E^q90d}0M$P`9`)>dR#%DdTmEp~MW^fNxGbe7)PHIKJpt-r~JtZ=) zcKrtycUb`a^9_0UNY`j{!R}d3S-SjxH4b;hMbFpBr9|`jOL#nYf<5HUEA;{zrLyx# zjNIRYw&TYm*5_#9z7TSP|E20pycYXe%D`I{+w)LU_W2QHcp-vLEOAHZ6$p6HnlXXY zqHqYChnIMLrWVP6FbF@+$G z!p@}c`21UbWzgBDDpL}jaHa@x-lZVaPy17233_~ak;n%_@|TR6hw@tF5SL=UF8U91 z$09@E<90_%`V=D3*{IF-7>Mpi?tG?z>doO&F`m=ewXo6&y^& z3{9Hzunbr!9zXffGy}Dd^N<(7)LeDIE5wPURCol6Yd}xwc>C*H{b!;LZ`@{3b`t9M z@KS9!gJEgzo!z+c@{YX$#t6XswUb4rax?jI?hI?{6mXH2IF2K)#$6(q?{ZDaSwwn; zAtE%sEgQH5`DXDy9`TS_lfSwJIUk}N8GdTdz%P6*&VYm9E(lwkdsWe+$v1gEZTnj# ziL8X*U%kNpDM32r8h2JAUG!3Hk+g9N*4J zS*grG)4OEWLQ>Y}CCdax)NJP1MoPb4LeqV$7QlC}*rr|G6?%3bUp6^GrUhJU6F&}s0 z7cRx?g8G2j5iU2C$`WD8ADA1s-=OFDW~g<1O$djgadI znz{B}+_F(O!YWD7P$$e13}1c~FkQ0NTQrot@zd65$Rms>gr+2r5W!OGqENWVLLiE~ zuPk`)uo-e^EJVnd^D8(1%KKu+0iJ7P4y=@jo2c5S4Y34Ouak9LvW510bTIERevSrk zbC`V}gK10FUHOyw$#E^|tHWhf?V0EM*2?q4)#YG?B3^b%DSb0&Z~Ci@P4G{4YEXVp zf;)T&NmMCmFc}wWum7V&f8#=`d}$x^*zQY7<9TY-6agQSot&`!;N$)K4!`Byr@1%Q zYy!r3{+F)Waolre;ldfJjYpaYYeOlA4rAh_G`CnP8JHMJCv zN}A~Le=7P#$Ny9F#xt?>HC|wv>I31bE=kX({V)Tm>y8;fJ|YM1ir+>i$!7>Pqrf_t z3MT2t>aazcF~4W=nZ*A5{srS{&i#QdGai){I=9Zi{240H6sihMK@y6 zRZ4-u$gKLwg7r7x1mGUmOr^wN?(_d#_)0>r2C|sMPOHj5S1iIaP@_9=MD+#@ITS*@Od z0&Icid->1VM(U>)EZl?GBj8GW6k#69KRL3_dLOr-X)k$-G^Z60gQo9xo7@s^lk8OH zXR%K;p_q4E_`6iF(DvAd|Lvjg(um0^K|3nAso?M?^0&Ban>_?`7d!WGBdm7&8Q3;4 zK*`M|)P#NK>5EHc2#nZgaVLC4%{_31JsLdO0BpwPKGDr&4N0T4%DpBCtu%fdFh1nz z9B+^=!-2rsMW+t9=kiZI9z)AYmbbD|w9`?sWW|-)$@>r zLh1&JFms+NzMWBJc|kr|X)S|^Ty&Fo*jDT{RGPs?LtP@bNK)k);d>hN{JAs&=Wyk0 z^+O}^XC9&Ek3VgyPudBUXUXr%FpiH!@5ev7cbswf)Ll6`(SGA~Ud{2}5x12Zu{fjH|J4I|-nWqYjTON?>Of5dkI*Jv{mB_>U26txLAt=`6( z345O-hmEZsD`)?d(&fYD@9$6=X?OK1BbVYb3p2vA!CheCfMOGCS2wu3`6o?s7K5Xa z*OAlv-EU$Ip3!MvyA9`Tj^UNDpW;~(2lUXKdztZQ=P`HbJBEyzC!LlKt9~!WM}OLm z15>Dgf=D6k0rd^9zD8Bp8PYbjL^xn_z1at9S?sZ`EM!V9=^=kA9 zZ@5$cE~c{ifn?4=L>y8EE4|2cCtJD;bo?ICTUd{8bVkyI3N^CCsHP+I8~}SllE~EJ z9Y`Pl?EyeN{@f}T!Yg8I3bwANTs?A(wZQ4v`zoOZsdvcN;|%2g@!2AuqFE zAB6<4t7ZSr{pN(~Arh7H87z8zF?aq0e29iUd+b9@{pf-Z9;&x)082&%3lXm1N{<|9 zsE#}Yf&wW`4FT`1cTAhrVGRQjy8oHvgKrP9hqYS#@bA5gV+V}F^gy}jR5-|)rh+}b z2KWewtAPU+hGprp3@1d^sK6=hszjrIhSzxx>&XSby7RJ~AzA~x)8WxU*(_45+tAss zJN+XVHSF{m1nK90j{1o6**HQ2p6dlVHXn1wFQZU zqi0ZQiRZvRV#p!?F=M^f51c%KL;D2a`M7NdF4y7z(*+PC`MR~-e)bKhYiN~6fByJu zJn5&H!a4k}SWIR{Q3$}&tL-|QpY8>XH7IeCH zcS=X%cPET2Ea=m)Sh22W(WWnxwg}~iyS`ziC+)mrg$_c#K)i&8_ldL0l`8yzuyI?; z;dM71Q5!k$jKAJyLW%R1Kx-IQ$v;ubL4|=UJe67hQPvHdx;&?kNfM?U0o8Sz922cG ziJu1j29Vd|cupX|$j{fD=Z=-1JTqbQO|Q##F7UI4z~7_NoON^ZB*7YkIsv|;k}!JI z6E&zSjO+mVG~iTEU!A@1Xd2 z0JD`gBDH~WrSQ;_WKX1+UvHg%rCt#J$e4OKnOggt7O z*503TUiMOKhLU+ZVcb%o=zHX?ubtDLn&~?3mBg*tb|W%1#7!R^ID?0PF8sHe_=E)u zRz#SY`s_5UwUn^WKmCO$VV7Q`q5$sd^Z(EUh>(u=Q}U@J)CTPrM+~S`=iKLkeII@LTZU6nA+79|zJ!aMv+}NRqwnM})13lf+22 zFEYqt9p41~hyw*ds`EqkQ-5Rjl^dHRc_N6JzZ^YlgWQ9rjaX6Uql>CPJ z)($rO+JQpKAa?234 zooHS#8gnmM;AMOvQiFB&(^}^}<@onOh!~#iKIFtxDrZBy8%P#OkzEks@izAOFiqLkHV=3>XQ{=R&J0p@js$BPNfWMfxEA@|FO?PXKm znN2^8lVD<%a?8BtW}ASl#v&^0CQp2dj{#z%g9vGzpZbM59y9435cniKeu2&+T=5Ti zWb(7GZkkcPQUi>|_DWL>Y{ntuCG8>TgVeK!(2KrR`o?(~^)mo^HLN9cjA< zc_2j|;=$&Lv$YPE-OZt_e{rJr_MmyO_a0wD>(BVBLOU89Hgh6_anrJeJSI0r@WL#O zHbfv)i*HZFBf;9pV9k!u3FDF6PE_8y{3i=N6WqVO0{0y$u@+Mc`$P9DMid%|i^@RB zZ=68F_@(3Mae#8 z$3yQ$&rL+z409j;NA|O<3+tVUBQ_BOZ)Vjp1N*=I>53G@xyXSxn;>qcVFS@XdzFi~ z{a~6v%^08bnk8QPAp&l1#dC{?ROvoKvUD~HgvdT|O&7&c4EJxLL|oi?uMfVxonZ_- zT3bzm${DBCZ@)`XKYJa1=sp_yHuvk3(}#i<3F=&>6J8JlM4Yc})2NR4T{Yr(OnFL- z`SeM*9Ldnd>Fo0$3Oqfv)R&UFq|pQv1bVxi7E;U~p4$?-RN*AmcgM*1`!v(!vA6E{CHlO905`oYEs8MiVvLDfI5vs?qzWb`@$1f) z(arGx1?I^uV6pIy2gPu8Uw5^m0sGM#p9J=z;`4Ds|CYvj2GPr8}j& zySovjLqa+vB!>nm2?0S`Pzfm!P`VM25Tuk238h02zW3aosoHzD<_7kKe zZ)4K8f{SZZsANz>eOCxt!NAKG*nA8F`*U7#2iSI#0oDoz6K@PNG6Lh&`S4>@OT&ba zNA|LJFTN`LWlem)9z*z-7gro?P{pKetz2k#Q~C%@u7hdAZ$EIKBYoDMKSws4$oDC< z84vj_tP8(xCYt_BzuVHRk9G5Vy_B3fV55I1^UPW)7Lp%g2F7A(YDZk?EAPLyz0q_j zIIT<2Wmus*{4^Unqr{f^912hRMahImv;qi990-nrM+Rr8j#6DVuozWQt&K6NZbj{G z60D;Tf!_(d-;yQZSohxK2D{46qQ<@j5o|(rMrw^O2@NHpw@?ZzD%h@F)N~4}(<{7d zb)C#(G0C)nE&M(BMR)GoZYt4O&WWSuO!;IQNJUc?I79WjS{jK6GfI0>jx63 z1?YW9+fJ?D^#D`X*C1$PtGG73T{c`^|L39wd$L|Q74tj6VH<J43^6^!((Fs% zJ3>Zo!ctTDd!Oi(j>$8($!Cer65P2o{;W25j@Slg<(p;Q(*v1uouAdViV{v~Tiy4q zY$;0}W3^Afz>6$^W3?6l1idOM+ecdiYF~mGJS^c-AE<@vb73dfRt_X}0$>(-7HdVP z=~TgAa&O}YQqmH{l$dD7z98$PZ zk&@JNcl{8`C(3duqj{(v8WVKzm1FV3LnRHm9@C~DHzn{HYHz}0Mafeh`x`#ar@@PM zGil1d%B@RxctPpOzVVMY^wXI1Q#@VP(Oyx%V^_^vJ@(#D)@JLAq*0Lt6ha>$9e62Q z>M6eGfHZOrdsEbXPr?;J7*@i#F5qX!gG<0Y?Y{skKoiG;(RtgF=3v`TMrNaRI)uIf zJRQP;DP*Hn>EGFv9EjZ&FhW!P1UYso-T|%l-=7lQQhQxsu_}%UXEYqO26ho;%#p8Q z$6Ctt|M|XgEz?q;y|DDv`1oc+Sc-bIssK|jV9Hgyv>-locGLryybmpucZ%7Xz&)es6N8XYXxAx1QxckmMVP(c*hLD56 zVHo9J(p?ec3eu)%hV#Mg`*=B_#0COsR#&F*07VA<_G^#5l11g5xbSv(zwGwj%sr@s z<-67TMLW&m$AF)zr+?15xyrH2l&^e}mnMtjKg276vI_tKVsY#pMuso`*YC1IY!~e@ zarT7rzxo*D^Jsa*}Zejqy7B) z%nQHvd0FmIbXaSUItI7|(vvS7)q+2;(HW(CbzBV$m4dD;Jq%dGNj(T46z5;Nv=1Fz zAD=%@;c}K$2mTh%>j82vGpr7(=8wW`k@Evvlj63EkJ}hwOYpff?kR6i1{!e>KIz~V zZG_*@O|TPiM<*rssn$MR8tM_054;2^?t=D={%VIqgTHS--w~9XvNHh$a)dYU zo!m+T55qxk1h-|-g)71)$euA4da8AMrdYim%OPgAaU|kA#ZyM1o1Q2flzo{-Ex?<8 zVt&8Q2s@(OtN`bOjc2er$zN{|XV%y_JZ|CyI@!NF%-s<%_eTjFuc}AYdXN2IMPY|-=#=*1@*2;t`W;(b8Z5o* zA@2Yj@3C4Kxrme&Ac;Kj0K^45&$)u!?YgQ$OJW_IP3aa2T{rwe74I!N76Dqv!PFbd zR>4nlvgcX#40ZhQY%X9{nbP z(D~9;eQg@`(L)7zpVzwr4LcJQM)Gh`u>avw?BYz1Bl9o&Nc4t4YjeNF07`acxF0pA zRvwe<-S!JGYd#vkl1}Ylplht@toE_ZE_0c+fDh*>3rG1{UPFdbmL{!*+nG{<4⩔d~<;g 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 literal 0 HcmV?d00001 From 35dc2421872bc037bed7e3ed6375e2c5b962e2a7 Mon Sep 17 00:00:00 2001 From: Ox03bb Date: Wed, 23 Sep 2026 16:40:56 +0100 Subject: [PATCH 07/13] feat: add console --- includes/driver/console.h | 23 ++++ includes/driver/tty/tty.h | 50 ------- src/driver/console/vga/vga_console.c | 41 ++++++ src/driver/tty/tty.c | 196 --------------------------- 4 files changed, 64 insertions(+), 246 deletions(-) create mode 100644 includes/driver/console.h delete mode 100644 includes/driver/tty/tty.h create mode 100644 src/driver/console/vga/vga_console.c delete mode 100644 src/driver/tty/tty.c diff --git a/includes/driver/console.h b/includes/driver/console.h new file mode 100644 index 0000000..badc450 --- /dev/null +++ b/includes/driver/console.h @@ -0,0 +1,23 @@ +#ifndef CONSOLE_H +#define CONSOLE_H + +#include "driver/keyboard/event_queue.h" +#include "keyboard_keys.h" +#include "keyboard_layout.h" +#include "types.h" + +struct console_t; + +typedef struct { + void (*write)(struct console_t *console, char *str); + void (*clear)(struct console_t *console); +} console_interface_t; + +typedef struct console_t { + console_interface_t method; +} console_t; + +void console_init(console_t *console); + + +#endif \ No newline at end of file diff --git a/includes/driver/tty/tty.h b/includes/driver/tty/tty.h deleted file mode 100644 index 100fb6f..0000000 --- a/includes/driver/tty/tty.h +++ /dev/null @@ -1,50 +0,0 @@ -#ifndef TTY_H -#define TTY_H - -#include "keyboard_keys.h" -#include "keyboard_layout.h" -#include "types.h" -#include "driver/keyboard/event_queue.h" - -#define TTY_BUFFER_SIZE 4096 - -// modifiers -typedef enum { - MOD_NONE = 0, - MOD_SHIFT = 1 << 0, - MOD_CTRL = 1 << 1, - MOD_ALT = 1 << 2, - MOD_ALTGR = 1 << 3, - MOD_CAPS = 1 << 4, - MOD_NUM = 1 << 5, - MOD_SCROLL = 1 << 6, - MOD_SUPER = 1 << 7 -} keyboard_modifiers_t; - -struct tty_t; - -typedef struct { - void (*init)(struct tty_t *tty); - void (*putc)(struct tty_t *tty, char c); - void (*write)(struct tty_t *tty, const char *str); - void (*read)(struct tty_t *tty, char *buf, size_t len); - void (*clear)(struct tty_t *tty); -} tty_interface_t; - -typedef struct tty_t { - bool echo; - bool line_ready; - - int input_length; - int buffer_index; - char buffer[TTY_BUFFER_SIZE]; - - keyboard_modifiers_t modifiers; - - tty_interface_t method; -} tty_t; - -void tty_handle_event(tty_t *tty, const input_event_t *event); -void tty_update_modifiers(tty_t *tty, const input_event_t *event); - -#endif \ No newline at end of file diff --git a/src/driver/console/vga/vga_console.c b/src/driver/console/vga/vga_console.c new file mode 100644 index 0000000..e3e1362 --- /dev/null +++ b/src/driver/console/vga/vga_console.c @@ -0,0 +1,41 @@ +#include "driver/vga.h" + +#include "types.h" + +#include "console.h" + +#include "driver/tty.h" + +#include "stdio.h" + +void console_write(console_t *console, char *str); +void console_clear(console_t *console); + + +static const console_interface_t console_default_interface = { + .write = console_write, + .clear = console_clear, +}; + + +void console_init(console_t *console) { + if (console == NULL) + return; + + console->method = console_default_interface; +} + +void console_write(console_t *console, char *str) { + + if (console == NULL || str == NULL) + return; + + print(str); +} + +void console_clear(console_t *console) { + if (console == NULL) + return; + + vga_clear(); +} \ No newline at end of file diff --git a/src/driver/tty/tty.c b/src/driver/tty/tty.c deleted file mode 100644 index 1208e00..0000000 --- a/src/driver/tty/tty.c +++ /dev/null @@ -1,196 +0,0 @@ -#include "tty.h" -#include "keyboard/event_queue.h" -#include "keyboard_keys.h" -#include "driver/vga.h" - -void init(tty_t *tty){ - tty->buffer_index = 0; -} - -void putc(tty_t *tty, char c){ - tty->buffer[tty->buffer_index] = c; - tty->buffer_index++; -} - -void read(tty_t *tty, char *buf, size_t len){ - int i = 0; - while (i < len && tty->buffer[i] != '\0'){ - buf[i] = tty->buffer[i]; - i++; - } - buf[i] = '\0'; -} - -void write(tty_t *tty, const char *buf){ - int i = 0; - while (buf[i] != '\0'){ - putc(tty, buf[i]); - i++; - } -} - -void clear(tty_t *tty){ - int i = 0; - while (i < TTY_BUFFER_SIZE){ - tty->buffer[i] = '\0'; - i++; - } - tty->input_length = 0; -} - - -void tty_update_modifiers(tty_t *tty, const input_event_t *event) -{ - bool pressed = event->action == KEY_PRESSED; - - switch (event->key) { - - case KEY_LEFT_SHIFT: - case KEY_RIGHT_SHIFT: - if (pressed) - tty->modifiers |= MOD_SHIFT; - else - tty->modifiers &= ~MOD_SHIFT; - break; - - case KEY_LEFT_CTRL: - case KEY_RIGHT_CTRL: - if (pressed) - tty->modifiers |= MOD_CTRL; - else - tty->modifiers &= ~MOD_CTRL; - break; - - case KEY_LEFT_ALT: - if (pressed) - tty->modifiers |= MOD_ALT; - else - tty->modifiers &= ~MOD_ALT; - break; - - case KEY_RIGHT_ALT: - if (pressed) - tty->modifiers |= MOD_ALTGR; - else - tty->modifiers &= ~MOD_ALTGR; - break; - - case KEY_CAPS_LOCK: - if (pressed) - tty->modifiers ^= MOD_CAPS; - break; - - case KEY_NUM_LOCK: - if (pressed) - tty->modifiers ^= MOD_NUM; - break; - - case KEY_SCROLL_LOCK: - if (pressed) - tty->modifiers ^= MOD_SCROLL; - break; - - default: - break; - } -} - -void tty_handle_event(tty_t *tty, const input_event_t *event) -{ - if (tty == NULL || event == NULL) - return; - - /* - * Update Shift/Ctrl/Alt/Caps/Num/Scroll state. - */ - tty_update_modifiers(tty, event); - - /* - * Key releases do not produce normal terminal input. - */ - if (event->action != KEY_PRESSED) - return; - - /* - * Modifier and lock keys have already been handled above. - */ - switch (event->key) { - - case KEY_LEFT_SHIFT: - case KEY_RIGHT_SHIFT: - case KEY_LEFT_CTRL: - case KEY_RIGHT_CTRL: - case KEY_LEFT_ALT: - case KEY_RIGHT_ALT: - case KEY_CAPS_LOCK: - case KEY_NUM_LOCK: - case KEY_SCROLL_LOCK: - return; - - default: - break; - } - - /* - * Terminal control combinations. - */ - if (tty->modifiers & MOD_CTRL) { - switch (event->key) { - - case KEY_C: - /* Ctrl+C */ - return; - - default: - break; - } - } - - /* - * Handle special terminal keys. - */ - switch (event->key) { - - case KEY_BACKSPACE: - if (tty->buffer_index > 0) { - tty->buffer_index--; - tty->input_length--; - - tty->buffer[tty->buffer_index] = '\0'; - } - return; - - case KEY_ENTER: - if (tty->buffer_index < TTY_BUFFER_SIZE - 1) { - tty->buffer[tty->buffer_index++] = '\n'; - tty->buffer[tty->buffer_index] = '\0'; - tty->input_length++; - tty->line_ready = true; - } - return; - - default: - break; - } - - /* - * Convert the key into a character. - * - * This should come from your keyboard-layout layer. - */ - char c = event->character; - - if (c == '\0') - return; - - /* - * Prevent buffer overflow. - */ - - if (tty->buffer_index >= TTY_BUFFER_SIZE - 1) - return; - - tty->buffer[tty->buffer_index++] = c; - tty->buffer[tty->buffer_index] = '\0'; - tty->input_length++; -} \ No newline at end of file From a4dabdb84bf14226094ee2cac61786b6431eff83 Mon Sep 17 00:00:00 2001 From: Ox03bb Date: Wed, 23 Sep 2026 16:42:16 +0100 Subject: [PATCH 08/13] refactor(tty): cahgen the old stracture and add a console attr to tty --- includes/driver/tty.h | 56 ++++++++++ src/driver/tty.c | 236 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 292 insertions(+) create mode 100644 includes/driver/tty.h create mode 100644 src/driver/tty.c diff --git a/includes/driver/tty.h b/includes/driver/tty.h new file mode 100644 index 0000000..2118d96 --- /dev/null +++ b/includes/driver/tty.h @@ -0,0 +1,56 @@ +#ifndef TTY_H +#define TTY_H + +#include "driver/keyboard/event_queue.h" +#include "keyboard_keys.h" +#include "keyboard_layout.h" +#include "types.h" + +#include "console.h" + +#define TTY_BUFFER_SIZE 4096 + +// modifiers +typedef enum { + MOD_NONE = 0, + MOD_SHIFT = 1 << 0, + MOD_CTRL = 1 << 1, + MOD_ALT = 1 << 2, + MOD_ALTGR = 1 << 3, + MOD_CAPS = 1 << 4, + MOD_NUM = 1 << 5, + MOD_SCROLL = 1 << 6, + MOD_SUPER = 1 << 7 +} keyboard_modifiers_t; + +struct tty_t; + +typedef struct { + void (*putc)(struct tty_t *tty, char c); + void (*write)(struct tty_t *tty, const char *str); + void (*read)(struct tty_t *tty, char *buf, size_t len); + void (*clear)(struct tty_t *tty); +} tty_interface_t; + + +typedef struct tty_t { + bool echo; + bool line_ready; + + int input_length; + int buffer_index; + char buffer[TTY_BUFFER_SIZE]; + + keyboard_modifiers_t modifiers; + + tty_interface_t method; + + console_t *console; +} tty_t; + +void tty_init(tty_t *tty); + +void tty_handle_event(tty_t *tty, const input_event_t *event); +void tty_update_modifiers(tty_t *tty, const input_event_t *event); + +#endif \ No newline at end of file diff --git a/src/driver/tty.c b/src/driver/tty.c new file mode 100644 index 0000000..662a5b9 --- /dev/null +++ b/src/driver/tty.c @@ -0,0 +1,236 @@ +#include "tty.h" +#include "driver/vga.h" +#include "keyboard/event_queue.h" +#include "keyboard_keys.h" + +#include "console.h" + +void tty_putc(tty_t *tty, char c); +void tty_write(tty_t *tty, const char *str); +void tty_read(tty_t *tty, char *buf, size_t len); +void tty_clear(tty_t *tty); + +static const tty_interface_t tty_default_interface = { + .putc = tty_putc, + .write = tty_write, + .read = tty_read, + .clear = tty_clear, +}; + +void tty_init(tty_t *tty) { + + if (tty == NULL) + return; + + tty->echo = true; + tty->line_ready = false; + + tty->input_length = 0; + tty->buffer_index = 0; + + tty->modifiers = MOD_NONE; + + for (size_t i = 0; i < TTY_BUFFER_SIZE; i++) + tty->buffer[i] = '\0'; + + tty->method = tty_default_interface; + + console_init(tty->console); +} + +void tty_putc(tty_t *tty, char c) { + if (tty == NULL) + return; + + if (tty->buffer_index >= TTY_BUFFER_SIZE - 1) + return; + + tty->buffer[tty->buffer_index++] = c; + tty->buffer[tty->buffer_index] = '\0'; + + tty->input_length++; +} + +void tty_write(tty_t *tty, const char *str) { + if (tty == NULL || str == NULL) + return; + + for (size_t i = 0; str[i] != '\0'; i++) + tty_putc(tty, str[i]); +} + +void tty_read(tty_t *tty, char *buf, size_t len) { + if (tty == NULL || buf == NULL || len == 0) + return; + + size_t i = 0; + + /* + * Reserve one byte for '\0'. + */ + while (i < len - 1 && tty->buffer[i] != '\0') { + buf[i] = tty->buffer[i]; + i++; + } + + buf[i] = '\0'; +} + +void tty_clear(tty_t *tty) { + if (tty == NULL) + return; + + for (size_t i = 0; i < TTY_BUFFER_SIZE; i++) + tty->buffer[i] = '\0'; + + tty->buffer_index = 0; + tty->input_length = 0; + tty->line_ready = false; +} + +void tty_update_modifiers(tty_t *tty, const input_event_t *event) { + if (tty == NULL || event == NULL) + return; + + bool pressed = event->action == KEY_PRESSED; + + switch (event->key) { + + case KEY_LEFT_SHIFT: + case KEY_RIGHT_SHIFT: + if (pressed) + tty->modifiers |= MOD_SHIFT; + else + tty->modifiers &= ~MOD_SHIFT; + break; + + case KEY_LEFT_CTRL: + case KEY_RIGHT_CTRL: + if (pressed) + tty->modifiers |= MOD_CTRL; + else + tty->modifiers &= ~MOD_CTRL; + break; + + case KEY_LEFT_ALT: + if (pressed) + tty->modifiers |= MOD_ALT; + else + tty->modifiers &= ~MOD_ALT; + break; + + case KEY_RIGHT_ALT: + if (pressed) + tty->modifiers |= MOD_ALTGR; + else + tty->modifiers &= ~MOD_ALTGR; + break; + + case KEY_CAPS_LOCK: + if (pressed) + tty->modifiers ^= MOD_CAPS; + break; + + case KEY_NUM_LOCK: + if (pressed) + tty->modifiers ^= MOD_NUM; + break; + + case KEY_SCROLL_LOCK: + if (pressed) + tty->modifiers ^= MOD_SCROLL; + break; + + default: + break; + } +} + +void tty_handle_event(tty_t *tty, const input_event_t *event) { + if (tty == NULL || event == NULL) + return; + + tty_update_modifiers(tty, event); + + if (event->action != KEY_PRESSED) + return; + + switch (event->key) { + + case KEY_LEFT_SHIFT: + case KEY_RIGHT_SHIFT: + + case KEY_LEFT_CTRL: + case KEY_RIGHT_CTRL: + + case KEY_LEFT_ALT: + case KEY_RIGHT_ALT: + + case KEY_CAPS_LOCK: + case KEY_NUM_LOCK: + case KEY_SCROLL_LOCK: + + return; + + default: + break; + } + + if (tty->modifiers & MOD_CTRL) { + + switch (event->key) { + + case KEY_C: + /* + * Ctrl+C + * + * Process/signal handling can be added later. + */ + return; + + default: + break; + } + } + + switch (event->key) { + + case KEY_BACKSPACE: + + if (tty->buffer_index > 0) { + + tty->buffer_index--; + + if (tty->input_length > 0) + tty->input_length--; + + tty->buffer[tty->buffer_index] = '\0'; + } + + return; + + case KEY_ENTER: + + if (tty->buffer_index < TTY_BUFFER_SIZE - 1) { + + tty->buffer[tty->buffer_index++] = '\n'; + tty->buffer[tty->buffer_index] = '\0'; + + tty->input_length++; + + tty->line_ready = true; + } + + return; + + default: + break; + } + + char c = event->character; + + if (c == '\0') + return; + + tty_putc(tty, c); +} \ No newline at end of file From a529812e3fe5fbe06d688187db260d8252e92d97 Mon Sep 17 00:00:00 2001 From: Ox03bb Date: Wed, 23 Sep 2026 20:21:09 +0100 Subject: [PATCH 09/13] tty work correctly --- includes/driver/console.h | 5 ++-- includes/driver/tty.h | 3 ++ src/driver/console/vga/vga_console.c | 2 +- src/driver/tty.c | 42 ++++++++++++++++++++++---- src/kernel/kernel.c | 44 ++++++++-------------------- 5 files changed, 56 insertions(+), 40 deletions(-) diff --git a/includes/driver/console.h b/includes/driver/console.h index badc450..3190686 100644 --- a/includes/driver/console.h +++ b/includes/driver/console.h @@ -9,8 +9,8 @@ struct console_t; typedef struct { - void (*write)(struct console_t *console, char *str); - void (*clear)(struct console_t *console); + void (*write)(struct console_t *self, char *str); + void (*clear)(struct console_t *self); } console_interface_t; typedef struct console_t { @@ -19,5 +19,4 @@ typedef struct console_t { void console_init(console_t *console); - #endif \ No newline at end of file diff --git a/includes/driver/tty.h b/includes/driver/tty.h index 2118d96..c67f08d 100644 --- a/includes/driver/tty.h +++ b/includes/driver/tty.h @@ -53,4 +53,7 @@ void tty_init(tty_t *tty); void tty_handle_event(tty_t *tty, const input_event_t *event); void tty_update_modifiers(tty_t *tty, const input_event_t *event); +void tty_process_events(tty_t *tty); + + #endif \ No newline at end of file diff --git a/src/driver/console/vga/vga_console.c b/src/driver/console/vga/vga_console.c index e3e1362..2f8d2b5 100644 --- a/src/driver/console/vga/vga_console.c +++ b/src/driver/console/vga/vga_console.c @@ -2,7 +2,7 @@ #include "types.h" -#include "console.h" +#include "console.h"s #include "driver/tty.h" diff --git a/src/driver/tty.c b/src/driver/tty.c index 662a5b9..4bc9fac 100644 --- a/src/driver/tty.c +++ b/src/driver/tty.c @@ -17,6 +17,8 @@ static const tty_interface_t tty_default_interface = { .clear = tty_clear, }; +console_t vga_console; + void tty_init(tty_t *tty) { if (tty == NULL) @@ -35,6 +37,8 @@ void tty_init(tty_t *tty) { tty->method = tty_default_interface; + tty->console = &vga_console; + console_init(tty->console); } @@ -89,8 +93,8 @@ void tty_clear(tty_t *tty) { } void tty_update_modifiers(tty_t *tty, const input_event_t *event) { - if (tty == NULL || event == NULL) - return; + if (tty == NULL || event == NULL){ + return;} bool pressed = event->action == KEY_PRESSED; @@ -146,7 +150,8 @@ void tty_update_modifiers(tty_t *tty, const input_event_t *event) { } } -void tty_handle_event(tty_t *tty, const input_event_t *event) { +void tty_handle_event(tty_t *tty, const input_event_t *event) +{ if (tty == NULL || event == NULL) return; @@ -205,6 +210,9 @@ void tty_handle_event(tty_t *tty, const input_event_t *event) { tty->input_length--; tty->buffer[tty->buffer_index] = '\0'; + + if (tty->echo) + tty->method.write(tty, "\b"); } return; @@ -217,8 +225,10 @@ void tty_handle_event(tty_t *tty, const input_event_t *event) { tty->buffer[tty->buffer_index] = '\0'; tty->input_length++; - tty->line_ready = true; + + if (tty->echo) + tty->console->method.write(tty->console, "\n"); } return; @@ -232,5 +242,27 @@ void tty_handle_event(tty_t *tty, const input_event_t *event) { if (c == '\0') return; - tty_putc(tty, c); + if (tty->buffer_index < TTY_BUFFER_SIZE - 1) { + + tty->buffer[tty->buffer_index++] = c; + tty->buffer[tty->buffer_index] = '\0'; + + tty->input_length++; + + if (tty->echo) { + char output[2] = {c, '\0'}; + tty->console->method.write(tty->console, output); + } + } +} + +// entry point + +void tty_process_events(tty_t *tty) +{ + input_event_t event; + + while (input_queue_pop(&event)) { + tty_handle_event(tty, &event); + } } \ No newline at end of file diff --git a/src/kernel/kernel.c b/src/kernel/kernel.c index c3c252c..fdfdcd9 100644 --- a/src/kernel/kernel.c +++ b/src/kernel/kernel.c @@ -19,6 +19,8 @@ #include "string.h" +#include "driver/tty.h" + #include "keyboard/event_queue.h" #include "keyboard/handler.h" @@ -57,41 +59,21 @@ void kernel_main() { KERNEL_INIT("PS/2 controller", ps2_init); KERNEL_INIT("Keyboard Driver", init_keyboard); - pic_clear_mask(1); + + tty_t tty0; + tty_init(&tty0); + + + int i = 0; while (1) { - input_event_t event; - if (!input_queue_pop(&event)) { - asm volatile("hlt"); - continue; - } - - if (event.type == INPUT_EVENT_CHAR) { - print("Character: "); - print_char(event.character); - print("\n"); - print("action: "); - print(event.action == KEY_PRESSED ? "pressed\n" : "released\n"); - } else if (event.key == KEY_ENTER && event.action == KEY_PRESSED) { - print("Enter key pressed\n"); - } else if (event.key == KEY_BACKSPACE && event.action == KEY_PRESSED) { - print("Backspace key pressed\n"); - } else if (event.key == KEY_TAB && event.action == KEY_PRESSED) { - print("Tab key pressed\n"); - } else if (event.key == KEY_UP && event.action == KEY_PRESSED) { - print("Up arrow key pressed\n"); - } else if (event.key == KEY_DOWN && event.action == KEY_PRESSED) { - print("Down arrow key pressed\n"); - } else if (event.key == KEY_LEFT && event.action == KEY_PRESSED) { - print("Left arrow key pressed\n"); - } else if (event.key == KEY_RIGHT && event.action == KEY_PRESSED) { - print("Right arrow key pressed\n"); - } else if (event.key == KEY_LEFT_CTRL) { - print("Ctrl key "); - print(event.action == KEY_PRESSED ? "pressed\n" : "released\n"); - } + + + tty_process_events(&tty0); + } + print("[\033[32mready\033[0m] Kernel initialized successfully!\n"); return; } \ No newline at end of file From 2c897a6418ce84679c6ff7888d234784f0948316 Mon Sep 17 00:00:00 2001 From: Ox03bb Date: Wed, 23 Sep 2026 20:33:08 +0100 Subject: [PATCH 10/13] feat: add virtual curosr and arrow controle --- includes/driver/tty.h | 1 + includes/driver/vga.h | 5 ++++ src/driver/tty.c | 59 +++++++++++++++++++++++++++++++++++++++---- src/driver/vga.c | 32 +++++++++++++++++++++-- src/kernel/kernel.c | 1 - 5 files changed, 90 insertions(+), 8 deletions(-) diff --git a/includes/driver/tty.h b/includes/driver/tty.h index c67f08d..8c30951 100644 --- a/includes/driver/tty.h +++ b/includes/driver/tty.h @@ -39,6 +39,7 @@ typedef struct tty_t { int input_length; int buffer_index; + int cursor_index; char buffer[TTY_BUFFER_SIZE]; keyboard_modifiers_t modifiers; diff --git a/includes/driver/vga.h b/includes/driver/vga.h index 00ebdbf..5a62b0a 100644 --- a/includes/driver/vga.h +++ b/includes/driver/vga.h @@ -77,6 +77,11 @@ void vga_set_cursor(uint8_t x, uint8_t y); void vga_set_cursor_position(uint16_t p); +void vga_cursor_up(void); +void vga_cursor_down(void); +void vga_cursor_left(void); +void vga_cursor_right(void); + uint16_t vga_get_cursor_position(void); uint8_t vga_get_cursor_x(void); diff --git a/src/driver/tty.c b/src/driver/tty.c index 4bc9fac..755d329 100644 --- a/src/driver/tty.c +++ b/src/driver/tty.c @@ -29,6 +29,7 @@ void tty_init(tty_t *tty) { tty->input_length = 0; tty->buffer_index = 0; + tty->cursor_index = 0; tty->modifiers = MOD_NONE; @@ -88,6 +89,7 @@ void tty_clear(tty_t *tty) { tty->buffer[i] = '\0'; tty->buffer_index = 0; + tty->cursor_index = 0; tty->input_length = 0; tty->line_ready = false; } @@ -200,19 +202,48 @@ void tty_handle_event(tty_t *tty, const input_event_t *event) switch (event->key) { + case KEY_LEFT: + if (tty->cursor_index > 0) { + tty->cursor_index--; + vga_cursor_left(); + } + return; + + case KEY_RIGHT: + if (tty->cursor_index < tty->buffer_index) { + tty->cursor_index++; + vga_cursor_right(); + } + return; + case KEY_BACKSPACE: - if (tty->buffer_index > 0) { + if (tty->cursor_index > 0) { + uint16_t delete_position = vga_get_cursor_position(); + for (int i = tty->cursor_index - 1; i < tty->buffer_index; i++) + tty->buffer[i] = tty->buffer[i + 1]; + + tty->cursor_index--; tty->buffer_index--; if (tty->input_length > 0) tty->input_length--; - tty->buffer[tty->buffer_index] = '\0'; + if (tty->echo) { + vga_cursor_left(); + delete_position = vga_get_cursor_position(); - if (tty->echo) - tty->method.write(tty, "\b"); + for (int i = tty->cursor_index; i < tty->buffer_index; i++) { + char output[2] = {tty->buffer[i], '\0'}; + tty->console->method.write(tty->console, output); + } + + vga_print_char(' '); + vga_set_cursor_position(delete_position); + } + + tty->buffer[tty->buffer_index] = '\0'; } return; @@ -221,10 +252,16 @@ void tty_handle_event(tty_t *tty, const input_event_t *event) if (tty->buffer_index < TTY_BUFFER_SIZE - 1) { + if (tty->echo) { + vga_set_cursor_position(vga_get_cursor_position() + + (tty->buffer_index - tty->cursor_index)); + } + tty->buffer[tty->buffer_index++] = '\n'; tty->buffer[tty->buffer_index] = '\0'; tty->input_length++; + tty->cursor_index = tty->buffer_index; tty->line_ready = true; if (tty->echo) @@ -243,8 +280,13 @@ void tty_handle_event(tty_t *tty, const input_event_t *event) return; if (tty->buffer_index < TTY_BUFFER_SIZE - 1) { + uint16_t cursor_position = vga_get_cursor_position(); + + for (int i = tty->buffer_index; i > tty->cursor_index; i--) + tty->buffer[i] = tty->buffer[i - 1]; - tty->buffer[tty->buffer_index++] = c; + tty->buffer[tty->cursor_index++] = c; + tty->buffer_index++; tty->buffer[tty->buffer_index] = '\0'; tty->input_length++; @@ -252,6 +294,13 @@ void tty_handle_event(tty_t *tty, const input_event_t *event) if (tty->echo) { char output[2] = {c, '\0'}; tty->console->method.write(tty->console, output); + + for (int i = tty->cursor_index; i < tty->buffer_index; i++) { + output[0] = tty->buffer[i]; + tty->console->method.write(tty->console, output); + } + + vga_set_cursor_position(cursor_position + 1); } } } diff --git a/src/driver/vga.c b/src/driver/vga.c index 414c827..ff1ca61 100644 --- a/src/driver/vga.c +++ b/src/driver/vga.c @@ -157,10 +157,10 @@ void vga_set_color(uint8_t foreground, uint8_t background) { // Cursor Enabling void vga_cursor_enable(void) { outb(VGA_INDEX_PORT, VGA_CURSOR_START); - outb(VGA_DATA_PORT, 0x06); + outb(VGA_DATA_PORT, 0x00); outb(VGA_INDEX_PORT, VGA_CURSOR_END); - outb(VGA_DATA_PORT, 0x07); + outb(VGA_DATA_PORT, 0x0F); } void vga_cursor_disable(void) { @@ -200,6 +200,34 @@ void vga_set_cursor_position(uint16_t p) { cursor_y = p / VGA_WIDTH; } +void vga_cursor_up(void) { + if (cursor_position < VGA_WIDTH) + return; + + vga_set_cursor_position(cursor_position - VGA_WIDTH); +} + +void vga_cursor_down(void) { + if (cursor_position + VGA_WIDTH >= VGA_RES) + return; + + vga_set_cursor_position(cursor_position + VGA_WIDTH); +} + +void vga_cursor_left(void) { + if (cursor_position == 0) + return; + + vga_set_cursor_position(cursor_position - 1); +} + +void vga_cursor_right(void) { + if (cursor_position + 1 >= VGA_RES) + return; + + vga_set_cursor_position(cursor_position + 1); +} + uint16_t vga_get_cursor_position(void) { outb(VGA_INDEX_PORT, VGA_CURSOR_HIGH); diff --git a/src/kernel/kernel.c b/src/kernel/kernel.c index fdfdcd9..21eb623 100644 --- a/src/kernel/kernel.c +++ b/src/kernel/kernel.c @@ -71,7 +71,6 @@ void kernel_main() { tty_process_events(&tty0); - } print("[\033[32mready\033[0m] Kernel initialized successfully!\n"); From fe3cafd311d4530ae66a01409ad1356f5557135d Mon Sep 17 00:00:00 2001 From: Ox03bb Date: Wed, 23 Sep 2026 20:37:23 +0100 Subject: [PATCH 11/13] fix(tty): correct console method usage in tty_write and tty_clear and formate the code --- includes/driver/tty.h | 4 +--- src/driver/console/vga/vga_console.c | 4 +--- src/driver/tty.c | 27 +++++++++++++++++---------- src/kernel/kernel.c | 3 --- 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/includes/driver/tty.h b/includes/driver/tty.h index 8c30951..f402f72 100644 --- a/includes/driver/tty.h +++ b/includes/driver/tty.h @@ -32,7 +32,6 @@ typedef struct { void (*clear)(struct tty_t *tty); } tty_interface_t; - typedef struct tty_t { bool echo; bool line_ready; @@ -46,7 +45,7 @@ typedef struct tty_t { tty_interface_t method; - console_t *console; + console_t *console; } tty_t; void tty_init(tty_t *tty); @@ -56,5 +55,4 @@ void tty_update_modifiers(tty_t *tty, const input_event_t *event); void tty_process_events(tty_t *tty); - #endif \ No newline at end of file diff --git a/src/driver/console/vga/vga_console.c b/src/driver/console/vga/vga_console.c index 2f8d2b5..265366c 100644 --- a/src/driver/console/vga/vga_console.c +++ b/src/driver/console/vga/vga_console.c @@ -2,7 +2,7 @@ #include "types.h" -#include "console.h"s +#include "console.h" #include "driver/tty.h" @@ -11,13 +11,11 @@ void console_write(console_t *console, char *str); void console_clear(console_t *console); - static const console_interface_t console_default_interface = { .write = console_write, .clear = console_clear, }; - void console_init(console_t *console) { if (console == NULL) return; diff --git a/src/driver/tty.c b/src/driver/tty.c index 755d329..55b0891 100644 --- a/src/driver/tty.c +++ b/src/driver/tty.c @@ -60,8 +60,13 @@ void tty_write(tty_t *tty, const char *str) { if (tty == NULL || str == NULL) return; - for (size_t i = 0; str[i] != '\0'; i++) - tty_putc(tty, str[i]); + if (tty->console == NULL || tty->console->method.write == NULL) + return; + + for (size_t i = 0; str[i] != '\0'; i++) { + char ch[2] = {str[i], '\0'}; + tty->console->method.write(tty->console, ch); + } } void tty_read(tty_t *tty, char *buf, size_t len) { @@ -92,11 +97,15 @@ void tty_clear(tty_t *tty) { tty->cursor_index = 0; tty->input_length = 0; tty->line_ready = false; + + if (tty->console != NULL && tty->console->method.clear != NULL) + tty->console->method.clear(tty->console); } void tty_update_modifiers(tty_t *tty, const input_event_t *event) { - if (tty == NULL || event == NULL){ - return;} + if (tty == NULL || event == NULL) { + return; + } bool pressed = event->action == KEY_PRESSED; @@ -152,8 +161,7 @@ void tty_update_modifiers(tty_t *tty, const input_event_t *event) { } } -void tty_handle_event(tty_t *tty, const input_event_t *event) -{ +void tty_handle_event(tty_t *tty, const input_event_t *event) { if (tty == NULL || event == NULL) return; @@ -254,7 +262,7 @@ void tty_handle_event(tty_t *tty, const input_event_t *event) if (tty->echo) { vga_set_cursor_position(vga_get_cursor_position() + - (tty->buffer_index - tty->cursor_index)); + (tty->buffer_index - tty->cursor_index)); } tty->buffer[tty->buffer_index++] = '\n'; @@ -305,10 +313,9 @@ void tty_handle_event(tty_t *tty, const input_event_t *event) } } -// entry point +// entry point -void tty_process_events(tty_t *tty) -{ +void tty_process_events(tty_t *tty) { input_event_t event; while (input_queue_pop(&event)) { diff --git a/src/kernel/kernel.c b/src/kernel/kernel.c index 21eb623..f74c2d7 100644 --- a/src/kernel/kernel.c +++ b/src/kernel/kernel.c @@ -61,15 +61,12 @@ void kernel_main() { KERNEL_INIT("Keyboard Driver", init_keyboard); pic_clear_mask(1); - tty_t tty0; tty_init(&tty0); - int i = 0; while (1) { - tty_process_events(&tty0); } From 6510fb7ec53e627f15b3e3f49ffc27361711354c Mon Sep 17 00:00:00 2001 From: Ox03bb Date: Wed, 23 Sep 2026 20:55:56 +0100 Subject: [PATCH 12/13] feat: add scanf & scan char --- includes/lib/stdio.h | 2 + src/libs/stdio.c | 192 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 194 insertions(+) diff --git a/includes/lib/stdio.h b/includes/lib/stdio.h index d3097bf..2962f4c 100644 --- a/includes/lib/stdio.h +++ b/includes/lib/stdio.h @@ -12,6 +12,8 @@ void print_at_end(const char *str, uint8_t color); void printf(const char *format, ...); void snprintf(char *buffer, size_t buffer_size, const char *format, ...); +char scan_char(void); +int scanf(const char *format, ...); char *vformatter(const char *format, va_list args); char *formatter(const char *format, ...); diff --git a/src/libs/stdio.c b/src/libs/stdio.c index 4ba60c6..c120319 100644 --- a/src/libs/stdio.c +++ b/src/libs/stdio.c @@ -5,11 +5,203 @@ #include "vga_lib.h" #include "string.h" +#include "keyboard/event_queue.h" #include "stdio.h" #define FORMATTER_BUFFER_SIZE 1024 static char buffer[FORMATTER_BUFFER_SIZE]; +static bool scan_has_pending; +static char scan_pending; + +static char scan_read_char(void) { + input_event_t event; + + if (scan_has_pending) { + scan_has_pending = false; + return scan_pending; + } + + while (input_queue_pop(&event)) { + if (event.action != KEY_PRESSED) + continue; + + if (event.type == INPUT_EVENT_CHAR) + return event.character; + + if (event.key == KEY_ENTER) + return '\n'; + } + + return '\0'; +} + +static void scan_unread_char(char character) { + scan_pending = character; + scan_has_pending = true; +} + +static bool scan_is_space(char character) { + return character == ' ' || character == '\t' || character == '\n' || character == '\r'; +} + +char scan_char(void) { + char character; + + do { + character = scan_read_char(); + } while (character == '\0'); + + return character; +} + +int scanf(const char *format, ...) { + va_list args; + int assigned = 0; + + va_start(args, format); + + while (*format) { + if (scan_is_space(*format)) { + while (scan_is_space(*format)) + format++; + + char character; + do { + character = scan_char(); + } while (scan_is_space(character)); + scan_unread_char(character); + continue; + } + + if (*format != '%') { + char character = scan_char(); + if (character != *format) { + scan_unread_char(character); + break; + } + format++; + continue; + } + + format++; + if (*format == '\0') + break; + + switch (*format) { + case 'c': { + char *value = va_arg(args, char *); + *value = scan_char(); + assigned++; + break; + } + + case 's': { + char *value = va_arg(args, char *); + char character; + size_t length = 0; + + do { + character = scan_char(); + } while (scan_is_space(character)); + + while (!scan_is_space(character)) { + value[length++] = character; + character = scan_char(); + } + value[length] = '\0'; + assigned++; + break; + } + + case 'd': { + int *value = va_arg(args, int *); + char character; + int sign = 1; + int number = 0; + + do { + character = scan_char(); + } while (scan_is_space(character)); + + if (character == '-' || character == '+') { + sign = character == '-' ? -1 : 1; + character = scan_char(); + } + + if (character < '0' || character > '9') { + scan_unread_char(character); + break; + } + + do { + number = number * 10 + (character - '0'); + character = scan_char(); + } while (character >= '0' && character <= '9'); + + scan_unread_char(character); + *value = number * sign; + assigned++; + break; + } + + case 'x': { + uint32_t *value = va_arg(args, uint32_t *); + char character; + uint32_t number = 0; + bool has_digit = false; + + do { + character = scan_char(); + } while (scan_is_space(character)); + + if (character == '0') { + char prefix = scan_char(); + if (prefix == 'x' || prefix == 'X') + character = scan_char(); + else + scan_unread_char(prefix); + } + + while ((character >= '0' && character <= '9') || + (character >= 'a' && character <= 'f') || + (character >= 'A' && character <= 'F')) { + uint32_t digit; + if (character >= '0' && character <= '9') + digit = (uint32_t)(character - '0'); + else if (character >= 'a' && character <= 'f') + digit = (uint32_t)(character - 'a' + 10); + else + digit = (uint32_t)(character - 'A' + 10); + + number = number * 16 + digit; + has_digit = true; + character = scan_char(); + } + + if (!has_digit) { + scan_unread_char(character); + break; + } + + scan_unread_char(character); + *value = number; + assigned++; + break; + } + + default: + scan_unread_char('%'); + va_end(args); + return assigned; + } + + format++; + } + + va_end(args); + return assigned; +} void print(const char *str) { while (*str) { From 4daddb7573c9c33dccce1c733b3e770d802efdb4 Mon Sep 17 00:00:00 2001 From: Ox03bb Date: Thu, 24 Sep 2026 21:15:51 +0100 Subject: [PATCH 13/13] refactor: comment out unused code in bootloader and entry files --- src/bootloader/boot.asm | 26 +++++++++++++------------- src/kernel/entry.asm | 32 ++++++++++++++++---------------- 2 files changed, 29 insertions(+), 29 deletions(-) diff --git a/src/bootloader/boot.asm b/src/bootloader/boot.asm index 5d2c8b2..cf2fbad 100644 --- a/src/bootloader/boot.asm +++ b/src/bootloader/boot.asm @@ -34,31 +34,31 @@ int 0x13 jc disk_read_error -jmp start +; jmp start -msg db 13, 10,"Welcom to my bootLoader", 13, 10 - db "press any key to continue...", 0 +; msg db 13, 10,"Welcom to my bootLoader", 13, 10 +; db "press any key to continue...", 0 -s_msg db 13,10,10,"starting...",0 +; s_msg db 13,10,10,"starting...",0 -%include "src/bootloader/utils.asm" +; %include "src/bootloader/utils.asm" -start: +; start: -call clear_screen +; call clear_screen -MOV si, msg -call print +; MOV si, msg +; call print -call get_key +; call get_key -MOV si, s_msg -call print -jmp PM_start +; MOV si, s_msg +; call print +; jmp PM_start PM_start: diff --git a/src/kernel/entry.asm b/src/kernel/entry.asm index d8787f9..356b7b2 100644 --- a/src/kernel/entry.asm +++ b/src/kernel/entry.asm @@ -10,7 +10,7 @@ extern kernel_main _start: cli - call print_kernel_loaded + ; call print_kernel_loaded call kernel_main cli @@ -21,23 +21,23 @@ _start: ; ---------------------------------------- ; Print "Kernel is loaded" ; ---------------------------------------- -print_kernel_loaded: - pushad +; print_kernel_loaded: +; pushad - mov edi, 0xB8000 ; VGA text buffer - mov esi, kernel_msg +; mov edi, 0xB8000 ; VGA text buffer +; mov esi, kernel_msg -.print: - lodsb ; AL = *ESI++ - test al, al - jz .done +; .print: +; lodsb ; AL = *ESI++ +; test al, al +; jz .done - mov ah, 0x0F ; White text on black background - stosw ; Write AX (character + attribute) - jmp .print +; mov ah, 0x0F ; White text on black background +; stosw ; Write AX (character + attribute) +; jmp .print -.done: - popad - ret +; .done: +; popad +; ret -kernel_msg db "Hello from kernel!", 0 \ No newline at end of file +; kernel_msg db "Hello from kernel!", 0 \ No newline at end of file