Update 1.03

New feature(s):
- Markdown reader (Automatically opens when opening .MD files, right click a .MD file to edit.
Bug fixes:
-N/A
This commit is contained in:
Chris 2026-02-05 20:02:20 +01:00
parent a852075701
commit d9fc8fbeda
22 changed files with 904 additions and 132 deletions

View file

@ -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).

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
build/markdown.o Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -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);

View file

@ -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");
}

View file

@ -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) 20242026 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);
}

View file

@ -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

View file

@ -3,6 +3,7 @@
#include "fat32.h"
#include "wm.h"
#include "editor.h"
#include "markdown.h"
#include <stdbool.h>
#include <stddef.h>
@ -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("/");
}

View file

@ -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);

512
src/kernel/markdown.c Normal file
View file

@ -0,0 +1,512 @@
#include "markdown.h"
#include "graphics.h"
#include "fat32.h"
#include "wm.h"
#include <stdbool.h>
#include <stddef.h>
// === 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();
}

11
src/kernel/markdown.h Normal file
View file

@ -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

View file

@ -6,6 +6,7 @@
#include "cli_apps/cli_utils.h"
#include "explorer.h"
#include "editor.h"
#include "markdown.h"
#include <stdbool.h>
#include <stddef.h>
#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;