diff --git a/README.md b/README.md index 916502b..2aed983 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Brew OS 1.01 Alpha +# Brew OS 1.03 Pre-Alpha ## Brewkernel is now BrewOS! Brewkernel will from now on be deprecated as it's core became too messy. I have built a less bloated kernel and wrote a DE above it, which is why it is now an OS instead of a kernel (in my opinion). diff --git a/brewos.iso b/brewos.iso index 9919545..cbd9ab5 100644 Binary files a/brewos.iso and b/brewos.iso differ diff --git a/build/about.o b/build/about.o index 615c8ed..db989db 100644 Binary files a/build/about.o and b/build/about.o differ diff --git a/build/brewos.elf b/build/brewos.elf index 0d688ca..73156f2 100755 Binary files a/build/brewos.elf and b/build/brewos.elf differ diff --git a/build/cli_apps/about.o b/build/cli_apps/about.o index 5c18369..7b7e9c6 100644 Binary files a/build/cli_apps/about.o and b/build/cli_apps/about.o differ diff --git a/build/cli_apps/memcmd.o b/build/cli_apps/memcmd.o index d3f1401..254256b 100644 Binary files a/build/cli_apps/memcmd.o and b/build/cli_apps/memcmd.o differ diff --git a/build/cli_apps/meminfo.o b/build/cli_apps/meminfo.o index ce739e1..b639223 100644 Binary files a/build/cli_apps/meminfo.o and b/build/cli_apps/meminfo.o differ diff --git a/build/cmd.o b/build/cmd.o index 591e34a..5290ca9 100644 Binary files a/build/cmd.o and b/build/cmd.o differ diff --git a/build/editor.o b/build/editor.o index f9ebd1d..122dcde 100644 Binary files a/build/editor.o and b/build/editor.o differ diff --git a/build/explorer.o b/build/explorer.o index d8634ec..f1510a7 100644 Binary files a/build/explorer.o and b/build/explorer.o differ diff --git a/build/markdown.o b/build/markdown.o new file mode 100644 index 0000000..21e749e Binary files /dev/null and b/build/markdown.o differ diff --git a/build/wm.o b/build/wm.o index 41886a3..8ecf201 100644 Binary files a/build/wm.o and b/build/wm.o differ diff --git a/iso_root/brewos.elf b/iso_root/brewos.elf index 0d688ca..73156f2 100755 Binary files a/iso_root/brewos.elf and b/iso_root/brewos.elf differ diff --git a/src/kernel/about.c b/src/kernel/about.c index 604d4a1..145dc07 100644 --- a/src/kernel/about.c +++ b/src/kernel/about.c @@ -35,7 +35,8 @@ static void about_paint(Window *win) { // Version info draw_string(offset_x, offset_y + 105, "BrewOS", COLOR_BLACK); - draw_string(offset_x, offset_y + 120, "Version 1.0", COLOR_BLACK); + draw_string(offset_x, offset_y + 120, "BrewOS Version 1.03", COLOR_BLACK); + draw_string(offset_x, offset_y + 135, "Kernel Version 2.0.3", COLOR_BLACK); // Copyright draw_string(offset_x, offset_y + 150, "(C) 2026 boreddevnl.", COLOR_BLACK); diff --git a/src/kernel/cli_apps/about.c b/src/kernel/cli_apps/about.c index 4958df0..e3a05c7 100644 --- a/src/kernel/cli_apps/about.c +++ b/src/kernel/cli_apps/about.c @@ -2,6 +2,6 @@ void cli_cmd_brewver(char *args) { (void)args; - cli_write("BrewOS v1.02 Alpha\n"); - cli_write("BrewOS Kernel V2.0.2 Pre-Alpha\n"); + cli_write("BrewOS v1.03 Alpha\n"); + cli_write("BrewOS Kernel V2.0.3 Pre-Alpha\n"); } diff --git a/src/kernel/cmd.c b/src/kernel/cmd.c index 705341e..5fcbe02 100644 --- a/src/kernel/cmd.c +++ b/src/kernel/cmd.c @@ -624,86 +624,95 @@ static void create_test_files(void) { FAT32_FileHandle *fh = fat32_open("README.md", "w"); if (fh) { const char *content = - "BREW OS 1.01 ALPHA\n" - "==================\n\n" - "BREWKERNEL IS NOW BREWOS!\n\n" - "Brewkernel will from now on be deprecated as its core became too messy.\n" - "I have built a less bloated kernel and wrote a DE above it, which is why\n" - "it is now an OS instead of a kernel.\n\n" + "# Brew OS 1.01 Alpha\n\n" + "## Brewkernel is now BrewOS!\n" + "Brewkernel will from now on be deprecated as it's core became too messy. I have built a less bloated kernel and wrote a DE above it, which is why it is now an OS instead of a kernel (in my opinion).\n\n" "Brew Kernel is a simple x86_64 hobbyist operating system.\n" - "It features a DE (and WM), a FAT32 filesystem, customizable UI and much much more!\n" - "ramdisk-like filesystem.\n\n" - "FEATURES\n" - "--------\n" - "* Brew WM (Window Manager)\n" - "* FAT32 Filesystem\n" - "* 64-bit long mode support\n" - "* Multiboot2 compliant\n" - "* Text editor and file explorer\n" - "* IDT (Interrupt Descriptor Table)\n" - "* Ability to run on actual x86_64 hardware\n" - "* Command-line interface (CLI)\n\n" - "PREREQUISITES\n" - "-------------\n" + "It features a DE (and WM), a FAT32 filesystem, customizable UI and much much more!\n\n" + "## Features\n" + "- Brew WM\n" + "- Fat 32 FS\n" + "- 64-bit long mode support\n" + "- Multiboot2 compliant\n" + "- Text editor\n" + "- IDT\n" + "- Ability to run on actual x86_64 hardware\n" + "- CLI\n\n" + "## Prerequisites\n\n" "To build BrewOS, you'll need the following tools installed:\n\n" - "* x86_64 ELF Toolchain (x86_64-elf-gcc, x86_64-elf-ld)\n" - "* NASM (Netwide Assembler)\n" - "* xorriso (for creating bootable ISO images)\n" - "* QEMU (optional, for testing in emulator)\n\n" - "On macOS, install via Homebrew:\n" - " brew install x86_64-elf-binutils x86_64-elf-gcc nasm xorriso qemu\n\n" - "BUILDING\n" - "--------\n" - "Simply run 'make' from the project root:\n\n" - " make\n\n" + "- **x86_64 ELF Toolchain**: `x86_64-elf-gcc`, `x86_64-elf-ld`\n" + "- **NASM**: Netwide Assembler for compiling assembly code\n" + "- **xorriso**: For creating bootable ISO images\n" + "- **QEMU** (optional): For testing the kernel in an emulator\n\n" + "On macOS, you can install these using Homebrew:\n" + "```sh\n" + "brew install x86_64-elf-binutils x86_64-elf-gcc nasm xorriso qemu\n" + "```\n\n" + "## Building\n\n" + "Simply run `make` from the project root:\n\n" + "```sh\n" + "make\n" + "```\n\n" "This will:\n" - "1. Download Limine v7.0.0 bootloader files (if not present)\n" - "2. Compile all kernel C sources and assembly files\n" - "3. Link the kernel ELF binary\n" - "4. Generate a bootable ISO image (brewos.iso)\n\n" - "Build output:\n" - "* Compiled object files: build/\n" - "* ISO root filesystem: iso_root/\n" - "* Final ISO image: brewos.iso\n\n" - "RUNNING\n" - "-------\n" - "QEMU EMULATION:\n" - "Run in QEMU with:\n" - " make run\n\n" + "1. Compile all kernel C sources and assembly files\n" + "2. Link the kernel ELF binary\n" + "3. Generate a bootable ISO image (`brewos.iso`)\n\n" + "The build output is organized as follows:\n" + "- Compiled object files: `build/`\n" + "- ISO root filesystem: `iso_root/`\n" + "- Final ISO image: `brewos.iso`\n\n" + "## Running\n\n" + "### QEMU Emulation\n\n" + "Run the kernel in QEMU:\n\n" + "```sh\n" + "make run\n" + "```\n\n" "Or manually:\n" - " qemu-system-x86_64 -m 2G -serial stdio -cdrom brewos.iso -boot d\n\n" - "RUNNING ON REAL HARDWARE:\n" - "WARNING: This is at YOUR OWN RISK. This software comes with ZERO warranty\n" - "and may break your system.\n\n" - "1. Create bootable USB using Balena Etcher to flash brewos.iso\n" - "2. Enable legacy (BIOS) boot in your system BIOS/UEFI settings\n" - "3. Disable Secure Boot if needed\n" - "4. Insert USB drive and select it in boot menu during startup\n\n" - "Tested Hardware:\n" - "* HP EliteDesk 705 G4 DM (AMD Ryzen 5 PRO 2400G, Radeon Vega)\n" - "* Lenovo ThinkPad A475 20KL002VMH (AMD Pro A12-8830B, Radeon R7)\n\n" - "PROJECT STRUCTURE\n" - "-----------------\n" - "* src/kernel/ - Main kernel implementation\n" - " - boot.asm - Boot assembly code\n" - " - main.c - Kernel entry point\n" - " - *.c / *.h - Core kernel modules\n" - " - cli_apps/ - Command-line applications\n" - " - wallpaper.ppm - Default desktop wallpaper\n" - "* build/ - Compiled object files (generated during build)\n" - "* iso_root/ - ISO filesystem layout (generated during build)\n" - "* limine/ - Limine bootloader files (downloaded automatically)\n" - "* linker.ld - Linker script for x86_64 ELF\n" - "* limine.cfg - Limine bootloader configuration\n" - "* Makefile - Build configuration and targets\n\n" - "LICENSE\n" - "-------\n" + "```sh\n" + "qemu-system-x86_64 -m 2G -serial stdio -cdrom brewos.iso -boot d\n" + "```\n\n" + "### Running on Real Hardware\n\n" + "*Warning: This is at YOUR OWN RISK. This software comes with ZERO warranty and may break your system.*\n\n" + "1. **Create bootable USB**: Use [Balena Etcher](https://www.balena.io/etcher/) to flash `brewos.iso` to a USB drive\n\n" + "2. **Prepare the system**:\n" + " - Enable legacy (BIOS) boot in your system BIOS/UEFI settings\n" + " - Disable Secure Boot if needed\n\n" + "3. **Boot**: Insert the USB drive and select it in the boot menu during startup\n\n" + "4. **Tested Hardware**:\n" + " - HP EliteDesk 705 G4 DM (AMD Ryzen 5 PRO 2400G, Radeon Vega)\n" + " - Lenovo ThinkPad A475 20KL002VMH (AMD Pro A12-8830B, Radeon R7)\n\n" + "## Project Structure\n\n" + "- `src/kernel/` - Main kernel implementation\n" + " - `boot.asm` - Boot assembly code\n" + " - `main.c` - Kernel entry point\n" + " - `*.c / *.h` - Core kernel modules (graphics, interrupts, filesystem, etc.)\n" + " - `cli_apps/` - Command-line applications\n" + " - `wallpaper.ppm` - Default desktop wallpaper\n" + "- `build/` - Compiled object files (generated during build)\n" + "- `iso_root/` - ISO filesystem layout (generated during build)\n" + "- `limine/` - Limine bootloader files (downloaded automatically)\n" + "- `linker.ld` - Linker script for x86_64 ELF\n" + "- `limine.cfg` - Limine bootloader configuration\n" + "- `Makefile` - Build configuration and targets\n\n" + "## License\n\n" "Copyright (C) 2024-2026 boreddevnl\n\n" - "This program is free software: you can redistribute it and/or modify\n" - "it under the terms of the GNU General Public License as published by\n" - "the Free Software Foundation, either version 3 of the License, or\n" - "(at your option) any later version.\n\n" - "For full license details, see the LICENSE file in the repository.\n"; + "This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.\n\n" + "NOTICE\n" + "------\n\n" + "This product includes software developed by Chris (\"boreddevnl\") as part of the BrewKernel project.\n\n" + "Copyright (C) 2024–2026 Chris / boreddevnl (previously boreddevhq)\n\n" + "All source files in this repository contain copyright and license\n" + "headers that must be preserved in redistributions and derivative works.\n\n" + "If you distribute or modify this project (in whole or in part),\n" + "you MUST:\n\n" + " - Retain all copyright and license headers at the top of each file.\n" + " - Include this NOTICE file along with any redistributions or\n" + " derivative works.\n" + " - Provide clear attribution to the original author in documentation\n" + " or credits where appropriate.\n\n" + "The above attribution requirements are informational and intended to\n" + "ensure proper credit is given. They do not alter or supersede the\n" + "terms of the GNU General Public License (GPL), which governs this work.\n"; fat32_write(fh, (void *)content, cmd_strlen(content)); fat32_close(fh); } diff --git a/src/kernel/editor.c b/src/kernel/editor.c index 66b05be..52fc8f7 100644 --- a/src/kernel/editor.c +++ b/src/kernel/editor.c @@ -54,7 +54,10 @@ static void editor_ensure_cursor_visible(void); static void editor_clear_all(void) { for (int i = 0; i < EDITOR_MAX_LINES; i++) { - lines[i].content[0] = 0; + // Zero out entire buffer to prevent ghost text from previous file + for (int j = 0; j < EDITOR_MAX_LINE_LEN; j++) { + lines[i].content[j] = 0; + } lines[i].length = 0; } line_count = 1; @@ -264,44 +267,104 @@ static void editor_paint(Window *win) { // Fill editor background draw_rect(offset_x, offset_y + 30, content_width, content_height - 55, COLOR_WHITE); - // Draw line numbers and content - int visible_lines = (content_height - 55) / EDITOR_LINE_HEIGHT; - int max_line = scroll_top + visible_lines; - if (max_line > line_count) max_line = line_count; + // Calculate available width for text (accounting for line numbers) + int text_start_x = offset_x + 40; + int available_width = content_width - 40; + int max_chars_per_line = available_width / EDITOR_CHAR_WIDTH; + if (max_chars_per_line < 1) max_chars_per_line = 1; - for (int i = scroll_top; i < max_line; i++) { - int display_y = offset_y + 35 + (i - scroll_top) * EDITOR_LINE_HEIGHT; + // Draw line numbers and content with word wrapping + int display_line = 0; + int visible_lines = (content_height - 55) / EDITOR_LINE_HEIGHT; + int max_display_lines = visible_lines; + + int line_idx = scroll_top; + while (line_idx < line_count && display_line < max_display_lines) { + int display_y = offset_y + 35 + display_line * EDITOR_LINE_HEIGHT; - // Draw line number - char line_num_str[16]; - int temp = i + 1; - int str_len = 0; - if (temp == 0) { - line_num_str[0] = '0'; - str_len = 1; - } else { - while (temp > 0) { - line_num_str[str_len++] = (temp % 10) + '0'; - temp /= 10; + // Only draw line number for first wrapped line of this editor line + if (display_line == 0 || line_idx < line_count) { + // Draw line number + char line_num_str[16]; + int temp = line_idx + 1; + int str_len = 0; + if (temp == 0) { + line_num_str[0] = '0'; + str_len = 1; + } else { + while (temp > 0) { + line_num_str[str_len++] = (temp % 10) + '0'; + temp /= 10; + } + // Reverse + for (int j = 0; j < str_len / 2; j++) { + char t = line_num_str[j]; + line_num_str[j] = line_num_str[str_len - 1 - j]; + line_num_str[str_len - 1 - j] = t; + } } - // Reverse - for (int j = 0; j < str_len / 2; j++) { - char t = line_num_str[j]; - line_num_str[j] = line_num_str[str_len - 1 - j]; - line_num_str[str_len - 1 - j] = t; + line_num_str[str_len] = 0; + draw_string(offset_x + 4, display_y, line_num_str, COLOR_DKGRAY); + } + + // Word-based text wrapping for this line + const char *text = lines[line_idx].content; + int text_len = lines[line_idx].length; + int char_idx = 0; + int local_display_line = 0; + + while (char_idx < text_len && display_line < max_display_lines) { + int current_display_y = offset_y + 35 + display_line * EDITOR_LINE_HEIGHT; + + // Extract segment (up to max_chars_per_line) + char segment[256]; + int segment_len = 0; + int segment_start = char_idx; + + while (char_idx < text_len && segment_len < max_chars_per_line) { + segment[segment_len++] = text[char_idx++]; } + segment[segment_len] = 0; + + // Word-based wrapping: find last space if we didn't reach end + if (char_idx < text_len && segment_len > 0) { + int last_space = -1; + for (int i = segment_len - 1; i >= 0; i--) { + if (segment[i] == ' ') { + last_space = i; + break; + } + } + + if (last_space > 0) { + segment_len = last_space; + segment[segment_len] = 0; + char_idx = segment_start + last_space + 1; + // Skip additional spaces + while (char_idx < text_len && text[char_idx] == ' ') { + char_idx++; + } + } + } + + // Draw the text segment + if (segment_len > 0) { + draw_string(text_start_x, current_display_y, segment, COLOR_BLACK); + } + + // Draw cursor if on this line and wrapped segment + if (line_idx == cursor_line && cursor_col >= segment_start && cursor_col < segment_start + segment_len) { + int cursor_x = text_start_x + ((cursor_col - segment_start) * EDITOR_CHAR_WIDTH); + draw_rect(cursor_x, current_display_y, 2, 10, COLOR_BLACK); + } + + display_line++; + local_display_line++; + + if (char_idx >= text_len) break; } - line_num_str[str_len] = 0; - draw_string(offset_x + 4, display_y, line_num_str, COLOR_DKGRAY); - // Draw line content - draw_string(offset_x + 40, display_y, lines[i].content, COLOR_BLACK); - - // Draw cursor if on this line - if (i == cursor_line) { - int cursor_x = offset_x + 40 + (cursor_col * EDITOR_CHAR_WIDTH); - draw_rect(cursor_x, display_y, 2, 10, COLOR_BLACK); - } + line_idx++; } // Draw status bar at bottom diff --git a/src/kernel/explorer.c b/src/kernel/explorer.c index 0e83140..a22916f 100644 --- a/src/kernel/explorer.c +++ b/src/kernel/explorer.c @@ -3,6 +3,7 @@ #include "fat32.h" #include "wm.h" #include "editor.h" +#include "markdown.h" #include #include @@ -57,6 +58,15 @@ static int dropdown_menu_item_height = 25; #define DROPDOWN_MENU_WIDTH 120 #define DROPDOWN_MENU_ITEMS 3 +// File context menu state +static bool file_context_menu_visible = false; +static int file_context_menu_x = 0; +static int file_context_menu_y = 0; +static int file_context_menu_item = -1; // Which item is being right-clicked +#define FILE_CONTEXT_MENU_WIDTH 140 +#define FILE_CONTEXT_MENU_HEIGHT 50 +#define FILE_CONTEXT_ITEMS 2 // "Open with Text Editor" and "Open with Markdown Viewer" + // === Helper Functions === static size_t explorer_strlen(const char *str); @@ -64,6 +74,8 @@ static void explorer_strcpy(char *dest, const char *src); static int explorer_strcmp(const char *s1, const char *s2); static void explorer_strcat(char *dest, const char *src); static void explorer_load_directory(const char *path); +static void explorer_handle_right_click(Window *win, int x, int y); +static void explorer_handle_file_context_menu_click(Window *win, int x, int y); static size_t explorer_strlen(const char *str) { size_t len = 0; @@ -89,6 +101,28 @@ static void explorer_strcat(char *dest, const char *src) { explorer_strcpy(dest, src); } +// Get file extension (e.g., "md" from "file.md") +static const char* explorer_get_extension(const char *filename) { + const char *dot = filename; + const char *ext = ""; + + // Find the last dot + while (*dot) { + if (*dot == '.') { + ext = dot + 1; + } + dot++; + } + + return ext; +} + +// Check if file is markdown +static bool explorer_is_markdown_file(const char *filename) { + const char *ext = explorer_get_extension(filename); + return explorer_strcmp(ext, "md") == 0; +} + // === Dialog and File Operations === static void dialog_open_create_file(void) { @@ -417,12 +451,40 @@ static void explorer_paint(Window *win) { draw_button(dlg_x + 50, dlg_y + 65, 80, 25, "Delete", false); draw_button(dlg_x + 170, dlg_y + 65, 80, 25, "Cancel", false); } + + // Draw file context menu if visible + if (file_context_menu_visible && file_context_menu_item >= 0) { + // Convert window-relative coordinates to screen coordinates for drawing + int menu_screen_x = win->x + file_context_menu_x; + int menu_screen_y = win->y + file_context_menu_y; + + // Draw menu background + draw_rect(menu_screen_x, menu_screen_y, FILE_CONTEXT_MENU_WIDTH, FILE_CONTEXT_MENU_HEIGHT, COLOR_LTGRAY); + draw_bevel_rect(menu_screen_x, menu_screen_y, FILE_CONTEXT_MENU_WIDTH, FILE_CONTEXT_MENU_HEIGHT, true); + + // Draw menu items + int item_height = FILE_CONTEXT_MENU_HEIGHT / FILE_CONTEXT_ITEMS; + + // Item 1: "Open with Text Editor" + draw_string(menu_screen_x + 5, menu_screen_y + 5, "Open w/ Editor", COLOR_BLACK); + + // Item 2: "Open with Markdown Viewer" (only show if file is .md) + if (explorer_is_markdown_file(items[file_context_menu_item].name)) { + draw_string(menu_screen_x + 5, menu_screen_y + item_height + 5, "Open w/ Markdown", COLOR_BLACK); + } + } } // === Mouse Handler === static void explorer_handle_click(Window *win, int x, int y) { - // Handle dialog clicks first + // Handle file context menu clicks first + if (file_context_menu_visible) { + explorer_handle_file_context_menu_click(win, x, y); + return; + } + + // Handle dialog clicks if (dialog_state == DIALOG_CREATE_FILE || dialog_state == DIALOG_CREATE_FOLDER) { int dlg_x = win->w / 2 - 150; int dlg_y = win->h / 2 - 60; @@ -549,7 +611,7 @@ static void explorer_handle_click(Window *win, int x, int y) { if (items[i].is_directory) { explorer_navigate_to(items[i].name); } else { - // Open file in editor + // Open file - check type char full_path[256]; explorer_strcpy(full_path, current_path); if (full_path[explorer_strlen(full_path) - 1] != '/') { @@ -557,19 +619,32 @@ static void explorer_handle_click(Window *win, int x, int y) { } explorer_strcat(full_path, items[i].name); - // Open in editor and bring to front - win_editor.visible = true; - win_editor.focused = true; - int max_z = 0; - for (int j = 0; j < 5; j++) { // window_count is 5 - // Need to find max z_index - check all windows + // Check if markdown file + if (explorer_is_markdown_file(items[i].name)) { + // Open with markdown viewer + win_markdown.visible = true; + win_markdown.focused = true; + int max_z = 0; if (win_explorer.z_index > max_z) max_z = win_explorer.z_index; if (win_cmd.z_index > max_z) max_z = win_cmd.z_index; if (win_notepad.z_index > max_z) max_z = win_notepad.z_index; if (win_calculator.z_index > max_z) max_z = win_calculator.z_index; + if (win_editor.z_index > max_z) max_z = win_editor.z_index; + win_markdown.z_index = max_z + 1; + markdown_open_file(full_path); + } else { + // Open with text editor + win_editor.visible = true; + win_editor.focused = true; + int max_z = 0; + if (win_explorer.z_index > max_z) max_z = win_explorer.z_index; + if (win_cmd.z_index > max_z) max_z = win_cmd.z_index; + if (win_notepad.z_index > max_z) max_z = win_notepad.z_index; + if (win_calculator.z_index > max_z) max_z = win_calculator.z_index; + if (win_markdown.z_index > max_z) max_z = win_markdown.z_index; + win_editor.z_index = max_z + 1; + editor_open_file(full_path); } - win_editor.z_index = max_z + 1; - editor_open_file(full_path); } last_clicked_item = -1; } else { @@ -678,6 +753,100 @@ static void explorer_handle_key(Window *win, char c) { } } +// === Right-Click Handler === + +static void explorer_handle_right_click(Window *win, int x, int y) { + // File items start at y=64 relative to window + int content_start_y = 64; + int offset_x = 4; + + for (int i = 0; i < item_count; i++) { + int row = i / EXPLORER_COLS; + int col = i % EXPLORER_COLS; + + int item_x = offset_x + 10 + (col * (EXPLORER_ITEM_WIDTH + EXPLORER_PADDING)); + int item_y = content_start_y + (row * (EXPLORER_ITEM_HEIGHT + EXPLORER_PADDING)); + + if (x >= item_x && x < item_x + EXPLORER_ITEM_WIDTH && + y >= item_y && y < item_y + EXPLORER_ITEM_HEIGHT) { + + // Right-click on a file item + if (!items[i].is_directory) { + // Show context menu + file_context_menu_visible = true; + file_context_menu_item = i; + file_context_menu_x = x; + file_context_menu_y = y; + return; + } + } + } + + // Close menu if clicking elsewhere + file_context_menu_visible = false; + file_context_menu_item = -1; +} + +static void explorer_handle_file_context_menu_click(Window *win, int x, int y) { + (void)win; // Suppress unused warning - we use absolute coordinates instead + + if (!file_context_menu_visible || file_context_menu_item < 0) { + return; + } + + // Adjust coordinates to be relative to context menu + int relative_x = x - file_context_menu_x; + int relative_y = y - file_context_menu_y; + + if (relative_x < 0 || relative_x > FILE_CONTEXT_MENU_WIDTH || + relative_y < 0 || relative_y > FILE_CONTEXT_MENU_HEIGHT) { + // Clicked outside menu - close it + file_context_menu_visible = false; + file_context_menu_item = -1; + return; + } + + int item_height = FILE_CONTEXT_MENU_HEIGHT / FILE_CONTEXT_ITEMS; + int clicked_item = relative_y / item_height; + + // Build full path + char full_path[256]; + explorer_strcpy(full_path, current_path); + if (full_path[explorer_strlen(full_path) - 1] != '/') { + explorer_strcat(full_path, "/"); + } + explorer_strcat(full_path, items[file_context_menu_item].name); + + if (clicked_item == 0) { + // "Open with Text Editor" + win_editor.visible = true; + win_editor.focused = true; + int max_z = 0; + if (win_explorer.z_index > max_z) max_z = win_explorer.z_index; + if (win_cmd.z_index > max_z) max_z = win_cmd.z_index; + if (win_notepad.z_index > max_z) max_z = win_notepad.z_index; + if (win_calculator.z_index > max_z) max_z = win_calculator.z_index; + if (win_markdown.z_index > max_z) max_z = win_markdown.z_index; + win_editor.z_index = max_z + 1; + editor_open_file(full_path); + } else if (clicked_item == 1 && explorer_is_markdown_file(items[file_context_menu_item].name)) { + // "Open with Markdown Viewer" + win_markdown.visible = true; + win_markdown.focused = true; + int max_z = 0; + if (win_explorer.z_index > max_z) max_z = win_explorer.z_index; + if (win_cmd.z_index > max_z) max_z = win_cmd.z_index; + if (win_notepad.z_index > max_z) max_z = win_notepad.z_index; + if (win_calculator.z_index > max_z) max_z = win_calculator.z_index; + if (win_editor.z_index > max_z) max_z = win_editor.z_index; + win_markdown.z_index = max_z + 1; + markdown_open_file(full_path); + } + + file_context_menu_visible = false; + file_context_menu_item = -1; +} + // === Initialization === void explorer_init(void) { @@ -692,7 +861,7 @@ void explorer_init(void) { win_explorer.paint = explorer_paint; win_explorer.handle_key = explorer_handle_key; win_explorer.handle_click = explorer_handle_click; - win_explorer.handle_right_click = NULL; + win_explorer.handle_right_click = explorer_handle_right_click; explorer_load_directory("/"); } diff --git a/src/kernel/explorer.h b/src/kernel/explorer.h index fcbff52..874905c 100644 --- a/src/kernel/explorer.h +++ b/src/kernel/explorer.h @@ -8,6 +8,7 @@ extern Window win_editor; extern Window win_cmd; extern Window win_notepad; extern Window win_calculator; +extern Window win_markdown; void explorer_init(void); void explorer_reset(void); diff --git a/src/kernel/markdown.c b/src/kernel/markdown.c new file mode 100644 index 0000000..2e48bb9 --- /dev/null +++ b/src/kernel/markdown.c @@ -0,0 +1,512 @@ +#include "markdown.h" +#include "graphics.h" +#include "fat32.h" +#include "wm.h" +#include +#include + +// === Markdown Viewer State === +Window win_markdown; + +#define MD_MAX_CONTENT 16384 +#define MD_MAX_LINES 256 +#define MD_CHAR_WIDTH 8 +#define MD_LINE_HEIGHT 16 +#define MD_CONTENT_Y 40 +#define MD_PADDING_X 12 +#define MD_CONTENT_WIDTH 400 + +typedef enum { + MD_LINE_NORMAL, + MD_LINE_HEADING1, + MD_LINE_HEADING2, + MD_LINE_HEADING3, + MD_LINE_BOLD, + MD_LINE_ITALIC, + MD_LINE_LIST, + MD_LINE_BLOCKQUOTE, + MD_LINE_CODE +} MDLineType; + +typedef struct { + char content[256]; + int length; + MDLineType type; + int indent_level; +} MDLine; + +static MDLine lines[MD_MAX_LINES]; +static int line_count = 0; +static int scroll_top = 0; +static char open_filename[256] = ""; + +// === Helper Functions === + +static size_t md_strlen(const char *str) { + size_t len = 0; + while (str[len]) len++; + return len; +} + +static void md_strcpy(char *dest, const char *src) { + while (*src) *dest++ = *src++; + *dest = 0; +} + +static int md_strncpy(char *dest, const char *src, int n) { + int i = 0; + while (i < n && src[i]) { + dest[i] = src[i]; + i++; + } + dest[i] = 0; + return i; +} + +static int md_strcmp(const char *s1, const char *s2) { + (void)s1; // Suppress unused warning + (void)s2; // Suppress unused warning + // Reserved for future use + return 0; +} + +// Check if string starts with pattern +static bool md_starts_with(const char *str, const char *pattern) { + (void)str; // Suppress unused warning + (void)pattern; // Suppress unused warning + // Reserved for future use + return false; +} + +// Parse markdown line and extract formatted text +static void md_parse_line(const char *raw_line, char *output, MDLineType *type, int *indent) { + int i = 0; + int out_idx = 0; + *indent = 0; + *type = MD_LINE_NORMAL; + + // Skip leading whitespace and count indentation + while (raw_line[i] == ' ' || raw_line[i] == '\t') { + if (raw_line[i] == '\t') *indent += 2; + else *indent += 1; + i++; + } + + // Detect line type + if (raw_line[i] == '#') { + // Heading + int hash_count = 0; + while (raw_line[i] == '#') { + hash_count++; + i++; + } + // Skip space after hashes + if (raw_line[i] == ' ') i++; + + if (hash_count == 1) *type = MD_LINE_HEADING1; + else if (hash_count == 2) *type = MD_LINE_HEADING2; + else if (hash_count <= 6) *type = MD_LINE_HEADING3; + } else if (raw_line[i] == '-' || raw_line[i] == '*') { + // Could be list or horizontal rule + if ((raw_line[i] == '-' || raw_line[i] == '*') && (raw_line[i+1] == ' ' || raw_line[i+1] == '\t')) { + *type = MD_LINE_LIST; + i += 2; // Skip '- ' or '* ' + while (raw_line[i] == ' ' || raw_line[i] == '\t') i++; // Skip extra spaces + } + } else if (raw_line[i] == '>') { + // Blockquote + *type = MD_LINE_BLOCKQUOTE; + i++; + if (raw_line[i] == ' ') i++; + } else if (raw_line[i] == '`') { + // Code block + *type = MD_LINE_CODE; + i++; + } + + // Parse inline formatting and copy content + while (raw_line[i] && out_idx < 255) { + // Handle bold **text** + if (raw_line[i] == '*' && raw_line[i+1] == '*') { + i += 2; + while (raw_line[i] && !(raw_line[i] == '*' && raw_line[i+1] == '*') && out_idx < 255) { + output[out_idx++] = raw_line[i++]; + } + if (raw_line[i] == '*' && raw_line[i+1] == '*') i += 2; + continue; + } + + // Handle italic *text* or _text_ + if ((raw_line[i] == '*' || raw_line[i] == '_') && out_idx > 0 && raw_line[i-1] != '\\') { + char delim = raw_line[i]; + i++; + while (raw_line[i] && raw_line[i] != delim && out_idx < 255) { + output[out_idx++] = raw_line[i++]; + } + if (raw_line[i] == delim) i++; + continue; + } + + // Handle inline code `code` + if (raw_line[i] == '`') { + i++; + while (raw_line[i] && raw_line[i] != '`' && out_idx < 255) { + output[out_idx++] = raw_line[i++]; + } + if (raw_line[i] == '`') i++; + continue; + } + + // Handle links [text](url) - keep only text + if (raw_line[i] == '[') { + i++; + while (raw_line[i] && raw_line[i] != ']' && out_idx < 255) { + output[out_idx++] = raw_line[i++]; + } + if (raw_line[i] == ']') i++; + // Skip (url) + if (raw_line[i] == '(') { + while (raw_line[i] && raw_line[i] != ')') i++; + if (raw_line[i] == ')') i++; + } + continue; + } + + output[out_idx++] = raw_line[i++]; + } + + output[out_idx] = 0; +} + +// Clear all markdown lines +static void md_clear_all(void) { + for (int i = 0; i < MD_MAX_LINES; i++) { + lines[i].content[0] = 0; + lines[i].length = 0; + lines[i].type = MD_LINE_NORMAL; + lines[i].indent_level = 0; + } + line_count = 0; + scroll_top = 0; + open_filename[0] = 0; +} + +// Load and parse markdown file +void markdown_open_file(const char *filename) { + md_clear_all(); + md_strcpy(open_filename, filename); + + FAT32_FileHandle *fh = fat32_open(filename, "r"); + if (!fh) { + // File not found + return; + } + + // Read file content + char buffer[MD_MAX_CONTENT]; + int bytes_read = fat32_read(fh, buffer, sizeof(buffer) - 1); + fat32_close(fh); + + if (bytes_read <= 0) { + return; + } + + buffer[bytes_read] = 0; + + // Parse into markdown lines + int line = 0; + int col = 0; + char raw_line[256] = ""; + + for (int i = 0; i < bytes_read && line < MD_MAX_LINES; i++) { + char ch = buffer[i]; + + if (ch == '\n') { + raw_line[col] = 0; + + // Parse the raw line + char parsed_content[256]; + MDLineType type; + int indent; + md_parse_line(raw_line, parsed_content, &type, &indent); + + // Store parsed line + md_strcpy(lines[line].content, parsed_content); + lines[line].length = md_strlen(parsed_content); + lines[line].type = type; + lines[line].indent_level = indent; + + line++; + col = 0; + raw_line[0] = 0; + } else if (col < 255) { + raw_line[col++] = ch; + } + } + + // Handle last line if no trailing newline + if (col > 0 && line < MD_MAX_LINES) { + raw_line[col] = 0; + char parsed_content[256]; + MDLineType type; + int indent; + md_parse_line(raw_line, parsed_content, &type, &indent); + + md_strcpy(lines[line].content, parsed_content); + lines[line].length = md_strlen(parsed_content); + lines[line].type = type; + lines[line].indent_level = indent; + line++; + } + + line_count = line; +} + +// === Paint Function === + +// Helper to draw text with emphasis (bold effect by overlaying) +static void md_draw_text_bold(int x, int y, const char *text, uint32_t color) { + draw_string(x, y, text, color); + draw_string(x + 1, y, text, color); +} + +static void md_paint(Window *win) { + int offset_x = win->x + 4; + int offset_y = win->y + 24; + int content_width = win->w - 8; + int content_height = win->h - 28; + + // Draw filename bar below title + draw_rect(offset_x, offset_y, content_width, 20, COLOR_GRAY); + draw_string(offset_x + 4, offset_y + 4, "File: ", COLOR_BLACK); + draw_string(offset_x + 50, offset_y + 4, open_filename, COLOR_BLACK); + + // Draw scroll buttons on top right + int btn_x_up = offset_x + content_width - 50; + int btn_y = offset_y + 2; + draw_button(btn_x_up, btn_y, 20, 16, "^", false); + draw_button(btn_x_up + 24, btn_y, 20, 16, "v", false); + + // Content area - starts below filename bar + int content_start_y = offset_y + 24; + int content_start_x = offset_x + 4; + int usable_content_width = content_width - 8 - 20; // Reserved space for scroll button + int usable_content_height = content_height - 28; + int max_display_lines = usable_content_height / MD_LINE_HEIGHT; + + // Draw content background + draw_rect(offset_x, content_start_y, content_width - 20, usable_content_height, COLOR_WHITE); + + + + int display_line = 0; + int i = scroll_top; + + while (i < line_count && display_line < max_display_lines) { + MDLine *line = &lines[i]; + + // Determine spacing and text properties based on heading level + int line_height = MD_LINE_HEIGHT; + int extra_spacing = 0; + uint32_t text_color = COLOR_BLACK; + bool use_bold = false; + + switch (line->type) { + case MD_LINE_HEADING1: + line_height = MD_LINE_HEIGHT * 2; // Double height + text_color = 0xFF004080; // Dark blue + use_bold = true; + extra_spacing = 4; + break; + case MD_LINE_HEADING2: + line_height = MD_LINE_HEIGHT + 6; // 1.5x height + text_color = 0xFF1060A0; // Medium blue + use_bold = true; + extra_spacing = 2; + break; + case MD_LINE_HEADING3: + line_height = MD_LINE_HEIGHT + 2; // Slightly larger + text_color = 0xFF2080C0; // Light blue + use_bold = false; + break; + case MD_LINE_BLOCKQUOTE: + text_color = 0xFF808080; // Gray + break; + case MD_LINE_CODE: + text_color = 0xFF800000; // Dark red + break; + default: + text_color = COLOR_BLACK; + break; + } + + // Check if this heading will fit on the screen + if (display_line + (line_height / MD_LINE_HEIGHT) > max_display_lines) { + break; // Stop rendering if heading won't fit + } + + // Adjust X position based on indentation + int x_offset = content_start_x + (line->indent_level * 4); + int available_width = usable_content_width - (line->indent_level * 4); + int max_chars_per_line = available_width / MD_CHAR_WIDTH; + + if (max_chars_per_line < 1) max_chars_per_line = 1; + + // Handle line wrapping (word-based) + const char *text = line->content; + int text_len = line->length; + int char_idx = 0; + int local_display_line = 0; + int wrapped_line_count = 0; + + while (char_idx < text_len) { + int line_y = content_start_y + display_line * MD_LINE_HEIGHT + (local_display_line * MD_LINE_HEIGHT); + + // Extract line segment - copy up to max_chars_per_line characters + char line_segment[256]; + int segment_len = 0; + int segment_start = char_idx; + + // Copy characters up to max_chars_per_line OR until end of string + while (char_idx < text_len && segment_len < max_chars_per_line) { + line_segment[segment_len++] = text[char_idx++]; + } + line_segment[segment_len] = 0; + + // Word-based wrapping: if we didn't reach end of string, find last space + if (char_idx < text_len && segment_len > 0) { + // Look for the last space in the segment + int last_space = -1; + for (int i = segment_len - 1; i >= 0; i--) { + if (line_segment[i] == ' ') { + last_space = i; + break; + } + } + + // If we found a space, break there + if (last_space > 0) { + segment_len = last_space; + line_segment[segment_len] = 0; + // Backtrack char_idx to position after the space + char_idx = segment_start + last_space + 1; + // Skip any additional spaces at the start of next line + while (char_idx < text_len && text[char_idx] == ' ') { + char_idx++; + } + } + } + + // Draw special elements for first wrapped line of this markdown line + if (local_display_line == 0) { + switch (line->type) { + case MD_LINE_LIST: + // Draw bullet point + draw_rect(x_offset, line_y + MD_LINE_HEIGHT/2 - 1, 2, 2, COLOR_BLACK); + x_offset += 12; + // Redraw segment without leading space + if (segment_len > 0 && line_segment[0] == ' ') { + for (int j = 0; j < segment_len - 1; j++) { + line_segment[j] = line_segment[j + 1]; + } + segment_len--; + } + break; + case MD_LINE_BLOCKQUOTE: + // Draw left border + draw_rect(x_offset - 4, line_y, 2, line_height, 0xFF404080); + break; + case MD_LINE_CODE: + // Draw background for code + draw_rect(x_offset - 2, line_y, (max_chars_per_line * MD_CHAR_WIDTH) + 4, line_height, 0xFFF0F0F0); + break; + default: + break; + } + } + + // Draw the text segment with appropriate styling + if (segment_len > 0) { + if (use_bold) { + md_draw_text_bold(x_offset, line_y + extra_spacing, line_segment, text_color); + } else { + draw_string(x_offset, line_y, line_segment, text_color); + } + } + + local_display_line++; + wrapped_line_count++; + + if (char_idx >= text_len) break; + } + + // Move display line forward by the actual number of wrapped lines created + // Each wrapped line uses one MD_LINE_HEIGHT worth of space + display_line += wrapped_line_count; + + i++; + } +} + +// === Input Handling === + +static void md_handle_key(Window *win, char c) { + (void)win; // Suppress unused warning + + // Handle scrolling with arrow keys and W/S + // 17 = UP arrow, 18 = DOWN arrow (from ps2 keyboard mapping) + if (c == 'w' || c == 'W' || c == 17) { // Page up or UP arrow + scroll_top -= 3; + if (scroll_top < 0) scroll_top = 0; + } else if (c == 's' || c == 'S' || c == 18) { // Page down or DOWN arrow + scroll_top += 3; + int max_scroll = line_count - 10; + if (scroll_top > max_scroll) scroll_top = max_scroll; + if (scroll_top < 0) scroll_top = 0; + } +} + +static void md_handle_click(Window *win, int x, int y) { + // x and y are relative to window origin + int content_width = win->w - 8; + + // Top right up button: 4 + content_width - 50, 24 + 2, 20x16 + int btn_x_up = 4 + content_width - 50; + int btn_y = 24 + 2; + if (x >= btn_x_up && x < btn_x_up + 20 && y >= btn_y && y < btn_y + 16) { + // Scroll up + scroll_top -= 3; + if (scroll_top < 0) scroll_top = 0; + return; + } + + // Top right down button: 4 + content_width - 50 + 24, 24 + 2, 20x16 + int btn_x_down_top = 4 + content_width - 50 + 24; + if (x >= btn_x_down_top && x < btn_x_down_top + 20 && y >= btn_y && y < btn_y + 16) { + // Scroll down + scroll_top += 3; + int max_scroll = line_count - 10; + if (scroll_top > max_scroll) scroll_top = max_scroll; + if (scroll_top < 0) scroll_top = 0; + return; + } +} + +// === Initialization === + +void markdown_init(void) { + win_markdown.title = "Markdown Viewer"; + win_markdown.x = 150; + win_markdown.y = 180; + win_markdown.w = 600; + win_markdown.h = 400; + win_markdown.visible = false; + win_markdown.focused = false; + win_markdown.z_index = 0; + win_markdown.paint = md_paint; + win_markdown.handle_key = md_handle_key; + win_markdown.handle_click = md_handle_click; + win_markdown.handle_right_click = NULL; + + md_clear_all(); +} diff --git a/src/kernel/markdown.h b/src/kernel/markdown.h new file mode 100644 index 0000000..c3fec87 --- /dev/null +++ b/src/kernel/markdown.h @@ -0,0 +1,11 @@ +#ifndef MARKDOWN_H +#define MARKDOWN_H + +#include "wm.h" + +extern Window win_markdown; + +void markdown_init(void); +void markdown_open_file(const char *filename); + +#endif diff --git a/src/kernel/wm.c b/src/kernel/wm.c index 074cb54..e704837 100644 --- a/src/kernel/wm.c +++ b/src/kernel/wm.c @@ -6,6 +6,7 @@ #include "cli_apps/cli_utils.h" #include "explorer.h" #include "editor.h" +#include "markdown.h" #include #include #include "notepad.h" @@ -25,7 +26,7 @@ static int drag_offset_x = 0; static int drag_offset_y = 0; // Windows array for z-order management -static Window *all_windows[8]; +static Window *all_windows[9]; static int window_count = 0; // Redraw system @@ -121,7 +122,7 @@ void draw_folder_icon(int x, int y, const char *label) { draw_rect(x + 19, y, 1, 6, COLOR_BLACK); // Folder body - draw_rect(x + 5, y + 6, 25, 15, COLOR_LTGRAY); + draw_rect(x + 5, y + 6, 25, 15, COLOR_APPLE_YELLOW); draw_rect(x + 5, y + 6, 25, 1, COLOR_BLACK); draw_rect(x + 5, y + 6, 1, 15, COLOR_BLACK); draw_rect(x + 29, y + 6, 1, 15, COLOR_BLACK); @@ -678,6 +679,7 @@ void wm_handle_key(char c) { else if (win_calculator.focused && win_calculator.visible) target = &win_calculator; else if (win_explorer.focused && win_explorer.visible) target = &win_explorer; else if (win_editor.focused && win_editor.visible) target = &win_editor; + else if (win_markdown.focused && win_markdown.visible) target = &win_markdown; else if (win_control_panel.focused && win_control_panel.visible) target = &win_control_panel; if (!target) return; @@ -704,6 +706,7 @@ void wm_init(void) { calculator_init(); explorer_init(); editor_init(); + markdown_init(); control_panel_init(); about_init(); minesweeper_init(); @@ -714,9 +717,10 @@ void wm_init(void) { win_calculator.z_index = 2; win_explorer.z_index = 3; win_editor.z_index = 4; - win_control_panel.z_index = 5; - win_about.z_index = 6; - win_minesweeper.z_index = 7; + win_markdown.z_index = 5; + win_control_panel.z_index = 6; + win_about.z_index = 7; + win_minesweeper.z_index = 8; // Register windows in array all_windows[0] = &win_notepad; @@ -724,10 +728,11 @@ void wm_init(void) { all_windows[2] = &win_calculator; all_windows[3] = &win_explorer; all_windows[4] = &win_editor; - all_windows[5] = &win_control_panel; - all_windows[6] = &win_about; - all_windows[7] = &win_minesweeper; - window_count = 8; + all_windows[5] = &win_markdown; + all_windows[6] = &win_control_panel; + all_windows[7] = &win_about; + all_windows[8] = &win_minesweeper; + window_count = 9; // Only show Explorer and Notepad on desktop (Explorer on top) win_explorer.visible = false; @@ -742,6 +747,7 @@ void wm_init(void) { win_cmd.visible = false; win_calculator.visible = false; win_editor.visible = false; + win_markdown.visible = false; win_control_panel.visible = false; win_about.visible = false; win_minesweeper.visible = false;