mirror of
https://github.com/BoredDevNL/BoredOS.git
synced 2026-05-15 10:48:38 +00:00
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:
parent
a852075701
commit
d9fc8fbeda
22 changed files with 904 additions and 132 deletions
|
|
@ -1,4 +1,4 @@
|
||||||
# Brew OS 1.01 Alpha
|
# Brew OS 1.03 Pre-Alpha
|
||||||
|
|
||||||
## Brewkernel is now BrewOS!
|
## 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).
|
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).
|
||||||
|
|
|
||||||
BIN
brewos.iso
BIN
brewos.iso
Binary file not shown.
BIN
build/about.o
BIN
build/about.o
Binary file not shown.
BIN
build/brewos.elf
BIN
build/brewos.elf
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
build/cmd.o
BIN
build/cmd.o
Binary file not shown.
BIN
build/editor.o
BIN
build/editor.o
Binary file not shown.
BIN
build/explorer.o
BIN
build/explorer.o
Binary file not shown.
BIN
build/markdown.o
Normal file
BIN
build/markdown.o
Normal file
Binary file not shown.
BIN
build/wm.o
BIN
build/wm.o
Binary file not shown.
Binary file not shown.
|
|
@ -35,7 +35,8 @@ static void about_paint(Window *win) {
|
||||||
|
|
||||||
// Version info
|
// Version info
|
||||||
draw_string(offset_x, offset_y + 105, "BrewOS", COLOR_BLACK);
|
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
|
// Copyright
|
||||||
draw_string(offset_x, offset_y + 150, "(C) 2026 boreddevnl.", COLOR_BLACK);
|
draw_string(offset_x, offset_y + 150, "(C) 2026 boreddevnl.", COLOR_BLACK);
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,6 @@
|
||||||
|
|
||||||
void cli_cmd_brewver(char *args) {
|
void cli_cmd_brewver(char *args) {
|
||||||
(void)args;
|
(void)args;
|
||||||
cli_write("BrewOS v1.02 Alpha\n");
|
cli_write("BrewOS v1.03 Alpha\n");
|
||||||
cli_write("BrewOS Kernel V2.0.2 Pre-Alpha\n");
|
cli_write("BrewOS Kernel V2.0.3 Pre-Alpha\n");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
159
src/kernel/cmd.c
159
src/kernel/cmd.c
|
|
@ -624,86 +624,95 @@ static void create_test_files(void) {
|
||||||
FAT32_FileHandle *fh = fat32_open("README.md", "w");
|
FAT32_FileHandle *fh = fat32_open("README.md", "w");
|
||||||
if (fh) {
|
if (fh) {
|
||||||
const char *content =
|
const char *content =
|
||||||
"BREW OS 1.01 ALPHA\n"
|
"# Brew OS 1.01 Alpha\n\n"
|
||||||
"==================\n\n"
|
"## Brewkernel is now BrewOS!\n"
|
||||||
"BREWKERNEL IS NOW BREWOS!\n\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"
|
||||||
"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 Kernel is a simple x86_64 hobbyist operating system.\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"
|
"It features a DE (and WM), a FAT32 filesystem, customizable UI and much much more!\n\n"
|
||||||
"ramdisk-like filesystem.\n\n"
|
"## Features\n"
|
||||||
"FEATURES\n"
|
"- Brew WM\n"
|
||||||
"--------\n"
|
"- Fat 32 FS\n"
|
||||||
"* Brew WM (Window Manager)\n"
|
"- 64-bit long mode support\n"
|
||||||
"* FAT32 Filesystem\n"
|
"- Multiboot2 compliant\n"
|
||||||
"* 64-bit long mode support\n"
|
"- Text editor\n"
|
||||||
"* Multiboot2 compliant\n"
|
"- IDT\n"
|
||||||
"* Text editor and file explorer\n"
|
"- Ability to run on actual x86_64 hardware\n"
|
||||||
"* IDT (Interrupt Descriptor Table)\n"
|
"- CLI\n\n"
|
||||||
"* Ability to run on actual x86_64 hardware\n"
|
"## Prerequisites\n\n"
|
||||||
"* Command-line interface (CLI)\n\n"
|
|
||||||
"PREREQUISITES\n"
|
|
||||||
"-------------\n"
|
|
||||||
"To build BrewOS, you'll need the following tools installed:\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"
|
"- **x86_64 ELF Toolchain**: `x86_64-elf-gcc`, `x86_64-elf-ld`\n"
|
||||||
"* NASM (Netwide Assembler)\n"
|
"- **NASM**: Netwide Assembler for compiling assembly code\n"
|
||||||
"* xorriso (for creating bootable ISO images)\n"
|
"- **xorriso**: For creating bootable ISO images\n"
|
||||||
"* QEMU (optional, for testing in emulator)\n\n"
|
"- **QEMU** (optional): For testing the kernel in an emulator\n\n"
|
||||||
"On macOS, install via Homebrew:\n"
|
"On macOS, you can install these using Homebrew:\n"
|
||||||
" brew install x86_64-elf-binutils x86_64-elf-gcc nasm xorriso qemu\n\n"
|
"```sh\n"
|
||||||
"BUILDING\n"
|
"brew install x86_64-elf-binutils x86_64-elf-gcc nasm xorriso qemu\n"
|
||||||
"--------\n"
|
"```\n\n"
|
||||||
"Simply run 'make' from the project root:\n\n"
|
"## Building\n\n"
|
||||||
" make\n\n"
|
"Simply run `make` from the project root:\n\n"
|
||||||
|
"```sh\n"
|
||||||
|
"make\n"
|
||||||
|
"```\n\n"
|
||||||
"This will:\n"
|
"This will:\n"
|
||||||
"1. Download Limine v7.0.0 bootloader files (if not present)\n"
|
"1. Compile all kernel C sources and assembly files\n"
|
||||||
"2. Compile all kernel C sources and assembly files\n"
|
"2. Link the kernel ELF binary\n"
|
||||||
"3. Link the kernel ELF binary\n"
|
"3. Generate a bootable ISO image (`brewos.iso`)\n\n"
|
||||||
"4. Generate a bootable ISO image (brewos.iso)\n\n"
|
"The build output is organized as follows:\n"
|
||||||
"Build output:\n"
|
"- Compiled object files: `build/`\n"
|
||||||
"* Compiled object files: build/\n"
|
"- ISO root filesystem: `iso_root/`\n"
|
||||||
"* ISO root filesystem: iso_root/\n"
|
"- Final ISO image: `brewos.iso`\n\n"
|
||||||
"* Final ISO image: brewos.iso\n\n"
|
"## Running\n\n"
|
||||||
"RUNNING\n"
|
"### QEMU Emulation\n\n"
|
||||||
"-------\n"
|
"Run the kernel in QEMU:\n\n"
|
||||||
"QEMU EMULATION:\n"
|
"```sh\n"
|
||||||
"Run in QEMU with:\n"
|
"make run\n"
|
||||||
" make run\n\n"
|
"```\n\n"
|
||||||
"Or manually:\n"
|
"Or manually:\n"
|
||||||
" qemu-system-x86_64 -m 2G -serial stdio -cdrom brewos.iso -boot d\n\n"
|
"```sh\n"
|
||||||
"RUNNING ON REAL HARDWARE:\n"
|
"qemu-system-x86_64 -m 2G -serial stdio -cdrom brewos.iso -boot d\n"
|
||||||
"WARNING: This is at YOUR OWN RISK. This software comes with ZERO warranty\n"
|
"```\n\n"
|
||||||
"and may break your system.\n\n"
|
"### Running on Real Hardware\n\n"
|
||||||
"1. Create bootable USB using Balena Etcher to flash brewos.iso\n"
|
"*Warning: This is at YOUR OWN RISK. This software comes with ZERO warranty and may break your system.*\n\n"
|
||||||
"2. Enable legacy (BIOS) boot in your system BIOS/UEFI settings\n"
|
"1. **Create bootable USB**: Use [Balena Etcher](https://www.balena.io/etcher/) to flash `brewos.iso` to a USB drive\n\n"
|
||||||
"3. Disable Secure Boot if needed\n"
|
"2. **Prepare the system**:\n"
|
||||||
"4. Insert USB drive and select it in boot menu during startup\n\n"
|
" - Enable legacy (BIOS) boot in your system BIOS/UEFI settings\n"
|
||||||
"Tested Hardware:\n"
|
" - Disable Secure Boot if needed\n\n"
|
||||||
"* HP EliteDesk 705 G4 DM (AMD Ryzen 5 PRO 2400G, Radeon Vega)\n"
|
"3. **Boot**: Insert the USB drive and select it in the boot menu during startup\n\n"
|
||||||
"* Lenovo ThinkPad A475 20KL002VMH (AMD Pro A12-8830B, Radeon R7)\n\n"
|
"4. **Tested Hardware**:\n"
|
||||||
"PROJECT STRUCTURE\n"
|
" - HP EliteDesk 705 G4 DM (AMD Ryzen 5 PRO 2400G, Radeon Vega)\n"
|
||||||
"-----------------\n"
|
" - Lenovo ThinkPad A475 20KL002VMH (AMD Pro A12-8830B, Radeon R7)\n\n"
|
||||||
"* src/kernel/ - Main kernel implementation\n"
|
"## Project Structure\n\n"
|
||||||
" - boot.asm - Boot assembly code\n"
|
"- `src/kernel/` - Main kernel implementation\n"
|
||||||
" - main.c - Kernel entry point\n"
|
" - `boot.asm` - Boot assembly code\n"
|
||||||
" - *.c / *.h - Core kernel modules\n"
|
" - `main.c` - Kernel entry point\n"
|
||||||
" - cli_apps/ - Command-line applications\n"
|
" - `*.c / *.h` - Core kernel modules (graphics, interrupts, filesystem, etc.)\n"
|
||||||
" - wallpaper.ppm - Default desktop wallpaper\n"
|
" - `cli_apps/` - Command-line applications\n"
|
||||||
"* build/ - Compiled object files (generated during build)\n"
|
" - `wallpaper.ppm` - Default desktop wallpaper\n"
|
||||||
"* iso_root/ - ISO filesystem layout (generated during build)\n"
|
"- `build/` - Compiled object files (generated during build)\n"
|
||||||
"* limine/ - Limine bootloader files (downloaded automatically)\n"
|
"- `iso_root/` - ISO filesystem layout (generated during build)\n"
|
||||||
"* linker.ld - Linker script for x86_64 ELF\n"
|
"- `limine/` - Limine bootloader files (downloaded automatically)\n"
|
||||||
"* limine.cfg - Limine bootloader configuration\n"
|
"- `linker.ld` - Linker script for x86_64 ELF\n"
|
||||||
"* Makefile - Build configuration and targets\n\n"
|
"- `limine.cfg` - Limine bootloader configuration\n"
|
||||||
"LICENSE\n"
|
"- `Makefile` - Build configuration and targets\n\n"
|
||||||
"-------\n"
|
"## License\n\n"
|
||||||
"Copyright (C) 2024-2026 boreddevnl\n\n"
|
"Copyright (C) 2024-2026 boreddevnl\n\n"
|
||||||
"This program is free software: you can redistribute it and/or modify\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"
|
||||||
"it under the terms of the GNU General Public License as published by\n"
|
"NOTICE\n"
|
||||||
"the Free Software Foundation, either version 3 of the License, or\n"
|
"------\n\n"
|
||||||
"(at your option) any later version.\n\n"
|
"This product includes software developed by Chris (\"boreddevnl\") as part of the BrewKernel project.\n\n"
|
||||||
"For full license details, see the LICENSE file in the repository.\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_write(fh, (void *)content, cmd_strlen(content));
|
||||||
fat32_close(fh);
|
fat32_close(fh);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,10 @@ static void editor_ensure_cursor_visible(void);
|
||||||
|
|
||||||
static void editor_clear_all(void) {
|
static void editor_clear_all(void) {
|
||||||
for (int i = 0; i < EDITOR_MAX_LINES; i++) {
|
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;
|
lines[i].length = 0;
|
||||||
}
|
}
|
||||||
line_count = 1;
|
line_count = 1;
|
||||||
|
|
@ -264,44 +267,104 @@ static void editor_paint(Window *win) {
|
||||||
// Fill editor background
|
// Fill editor background
|
||||||
draw_rect(offset_x, offset_y + 30, content_width, content_height - 55, COLOR_WHITE);
|
draw_rect(offset_x, offset_y + 30, content_width, content_height - 55, COLOR_WHITE);
|
||||||
|
|
||||||
// Draw line numbers and content
|
// 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;
|
||||||
|
|
||||||
|
// Draw line numbers and content with word wrapping
|
||||||
|
int display_line = 0;
|
||||||
int visible_lines = (content_height - 55) / EDITOR_LINE_HEIGHT;
|
int visible_lines = (content_height - 55) / EDITOR_LINE_HEIGHT;
|
||||||
int max_line = scroll_top + visible_lines;
|
int max_display_lines = visible_lines;
|
||||||
if (max_line > line_count) max_line = line_count;
|
|
||||||
|
|
||||||
for (int i = scroll_top; i < max_line; i++) {
|
int line_idx = scroll_top;
|
||||||
int display_y = offset_y + 35 + (i - scroll_top) * EDITOR_LINE_HEIGHT;
|
while (line_idx < line_count && display_line < max_display_lines) {
|
||||||
|
int display_y = offset_y + 35 + display_line * EDITOR_LINE_HEIGHT;
|
||||||
|
|
||||||
// Draw line number
|
// Only draw line number for first wrapped line of this editor line
|
||||||
char line_num_str[16];
|
if (display_line == 0 || line_idx < line_count) {
|
||||||
int temp = i + 1;
|
// Draw line number
|
||||||
int str_len = 0;
|
char line_num_str[16];
|
||||||
if (temp == 0) {
|
int temp = line_idx + 1;
|
||||||
line_num_str[0] = '0';
|
int str_len = 0;
|
||||||
str_len = 1;
|
if (temp == 0) {
|
||||||
} else {
|
line_num_str[0] = '0';
|
||||||
while (temp > 0) {
|
str_len = 1;
|
||||||
line_num_str[str_len++] = (temp % 10) + '0';
|
} else {
|
||||||
temp /= 10;
|
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
|
line_num_str[str_len] = 0;
|
||||||
for (int j = 0; j < str_len / 2; j++) {
|
draw_string(offset_x + 4, display_y, line_num_str, COLOR_DKGRAY);
|
||||||
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;
|
// 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;
|
||||||
line_num_str[str_len] = 0;
|
|
||||||
draw_string(offset_x + 4, display_y, line_num_str, COLOR_DKGRAY);
|
|
||||||
|
|
||||||
// Draw line content
|
// Word-based wrapping: find last space if we didn't reach end
|
||||||
draw_string(offset_x + 40, display_y, lines[i].content, COLOR_BLACK);
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Draw cursor if on this line
|
if (last_space > 0) {
|
||||||
if (i == cursor_line) {
|
segment_len = last_space;
|
||||||
int cursor_x = offset_x + 40 + (cursor_col * EDITOR_CHAR_WIDTH);
|
segment[segment_len] = 0;
|
||||||
draw_rect(cursor_x, display_y, 2, 10, COLOR_BLACK);
|
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_idx++;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw status bar at bottom
|
// Draw status bar at bottom
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
#include "fat32.h"
|
#include "fat32.h"
|
||||||
#include "wm.h"
|
#include "wm.h"
|
||||||
#include "editor.h"
|
#include "editor.h"
|
||||||
|
#include "markdown.h"
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
#include <stddef.h>
|
#include <stddef.h>
|
||||||
|
|
||||||
|
|
@ -57,6 +58,15 @@ static int dropdown_menu_item_height = 25;
|
||||||
#define DROPDOWN_MENU_WIDTH 120
|
#define DROPDOWN_MENU_WIDTH 120
|
||||||
#define DROPDOWN_MENU_ITEMS 3
|
#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 ===
|
// === Helper Functions ===
|
||||||
|
|
||||||
static size_t explorer_strlen(const char *str);
|
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 int explorer_strcmp(const char *s1, const char *s2);
|
||||||
static void explorer_strcat(char *dest, const char *src);
|
static void explorer_strcat(char *dest, const char *src);
|
||||||
static void explorer_load_directory(const char *path);
|
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) {
|
static size_t explorer_strlen(const char *str) {
|
||||||
size_t len = 0;
|
size_t len = 0;
|
||||||
|
|
@ -89,6 +101,28 @@ static void explorer_strcat(char *dest, const char *src) {
|
||||||
explorer_strcpy(dest, 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 ===
|
// === Dialog and File Operations ===
|
||||||
|
|
||||||
static void dialog_open_create_file(void) {
|
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 + 50, dlg_y + 65, 80, 25, "Delete", false);
|
||||||
draw_button(dlg_x + 170, dlg_y + 65, 80, 25, "Cancel", 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 ===
|
// === Mouse Handler ===
|
||||||
|
|
||||||
static void explorer_handle_click(Window *win, int x, int y) {
|
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) {
|
if (dialog_state == DIALOG_CREATE_FILE || dialog_state == DIALOG_CREATE_FOLDER) {
|
||||||
int dlg_x = win->w / 2 - 150;
|
int dlg_x = win->w / 2 - 150;
|
||||||
int dlg_y = win->h / 2 - 60;
|
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) {
|
if (items[i].is_directory) {
|
||||||
explorer_navigate_to(items[i].name);
|
explorer_navigate_to(items[i].name);
|
||||||
} else {
|
} else {
|
||||||
// Open file in editor
|
// Open file - check type
|
||||||
char full_path[256];
|
char full_path[256];
|
||||||
explorer_strcpy(full_path, current_path);
|
explorer_strcpy(full_path, current_path);
|
||||||
if (full_path[explorer_strlen(full_path) - 1] != '/') {
|
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);
|
explorer_strcat(full_path, items[i].name);
|
||||||
|
|
||||||
// Open in editor and bring to front
|
// Check if markdown file
|
||||||
win_editor.visible = true;
|
if (explorer_is_markdown_file(items[i].name)) {
|
||||||
win_editor.focused = true;
|
// Open with markdown viewer
|
||||||
int max_z = 0;
|
win_markdown.visible = true;
|
||||||
for (int j = 0; j < 5; j++) { // window_count is 5
|
win_markdown.focused = true;
|
||||||
// Need to find max z_index - check all windows
|
int max_z = 0;
|
||||||
if (win_explorer.z_index > max_z) max_z = win_explorer.z_index;
|
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_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_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_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;
|
last_clicked_item = -1;
|
||||||
} else {
|
} 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 ===
|
// === Initialization ===
|
||||||
|
|
||||||
void explorer_init(void) {
|
void explorer_init(void) {
|
||||||
|
|
@ -692,7 +861,7 @@ void explorer_init(void) {
|
||||||
win_explorer.paint = explorer_paint;
|
win_explorer.paint = explorer_paint;
|
||||||
win_explorer.handle_key = explorer_handle_key;
|
win_explorer.handle_key = explorer_handle_key;
|
||||||
win_explorer.handle_click = explorer_handle_click;
|
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("/");
|
explorer_load_directory("/");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ extern Window win_editor;
|
||||||
extern Window win_cmd;
|
extern Window win_cmd;
|
||||||
extern Window win_notepad;
|
extern Window win_notepad;
|
||||||
extern Window win_calculator;
|
extern Window win_calculator;
|
||||||
|
extern Window win_markdown;
|
||||||
|
|
||||||
void explorer_init(void);
|
void explorer_init(void);
|
||||||
void explorer_reset(void);
|
void explorer_reset(void);
|
||||||
|
|
|
||||||
512
src/kernel/markdown.c
Normal file
512
src/kernel/markdown.c
Normal 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
11
src/kernel/markdown.h
Normal 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
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
#include "cli_apps/cli_utils.h"
|
#include "cli_apps/cli_utils.h"
|
||||||
#include "explorer.h"
|
#include "explorer.h"
|
||||||
#include "editor.h"
|
#include "editor.h"
|
||||||
|
#include "markdown.h"
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
#include <stddef.h>
|
#include <stddef.h>
|
||||||
#include "notepad.h"
|
#include "notepad.h"
|
||||||
|
|
@ -25,7 +26,7 @@ static int drag_offset_x = 0;
|
||||||
static int drag_offset_y = 0;
|
static int drag_offset_y = 0;
|
||||||
|
|
||||||
// Windows array for z-order management
|
// Windows array for z-order management
|
||||||
static Window *all_windows[8];
|
static Window *all_windows[9];
|
||||||
static int window_count = 0;
|
static int window_count = 0;
|
||||||
|
|
||||||
// Redraw system
|
// 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);
|
draw_rect(x + 19, y, 1, 6, COLOR_BLACK);
|
||||||
|
|
||||||
// Folder body
|
// 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, 25, 1, COLOR_BLACK);
|
||||||
draw_rect(x + 5, y + 6, 1, 15, COLOR_BLACK);
|
draw_rect(x + 5, y + 6, 1, 15, COLOR_BLACK);
|
||||||
draw_rect(x + 29, 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_calculator.focused && win_calculator.visible) target = &win_calculator;
|
||||||
else if (win_explorer.focused && win_explorer.visible) target = &win_explorer;
|
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_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;
|
else if (win_control_panel.focused && win_control_panel.visible) target = &win_control_panel;
|
||||||
|
|
||||||
if (!target) return;
|
if (!target) return;
|
||||||
|
|
@ -704,6 +706,7 @@ void wm_init(void) {
|
||||||
calculator_init();
|
calculator_init();
|
||||||
explorer_init();
|
explorer_init();
|
||||||
editor_init();
|
editor_init();
|
||||||
|
markdown_init();
|
||||||
control_panel_init();
|
control_panel_init();
|
||||||
about_init();
|
about_init();
|
||||||
minesweeper_init();
|
minesweeper_init();
|
||||||
|
|
@ -714,9 +717,10 @@ void wm_init(void) {
|
||||||
win_calculator.z_index = 2;
|
win_calculator.z_index = 2;
|
||||||
win_explorer.z_index = 3;
|
win_explorer.z_index = 3;
|
||||||
win_editor.z_index = 4;
|
win_editor.z_index = 4;
|
||||||
win_control_panel.z_index = 5;
|
win_markdown.z_index = 5;
|
||||||
win_about.z_index = 6;
|
win_control_panel.z_index = 6;
|
||||||
win_minesweeper.z_index = 7;
|
win_about.z_index = 7;
|
||||||
|
win_minesweeper.z_index = 8;
|
||||||
|
|
||||||
// Register windows in array
|
// Register windows in array
|
||||||
all_windows[0] = &win_notepad;
|
all_windows[0] = &win_notepad;
|
||||||
|
|
@ -724,10 +728,11 @@ void wm_init(void) {
|
||||||
all_windows[2] = &win_calculator;
|
all_windows[2] = &win_calculator;
|
||||||
all_windows[3] = &win_explorer;
|
all_windows[3] = &win_explorer;
|
||||||
all_windows[4] = &win_editor;
|
all_windows[4] = &win_editor;
|
||||||
all_windows[5] = &win_control_panel;
|
all_windows[5] = &win_markdown;
|
||||||
all_windows[6] = &win_about;
|
all_windows[6] = &win_control_panel;
|
||||||
all_windows[7] = &win_minesweeper;
|
all_windows[7] = &win_about;
|
||||||
window_count = 8;
|
all_windows[8] = &win_minesweeper;
|
||||||
|
window_count = 9;
|
||||||
|
|
||||||
// Only show Explorer and Notepad on desktop (Explorer on top)
|
// Only show Explorer and Notepad on desktop (Explorer on top)
|
||||||
win_explorer.visible = false;
|
win_explorer.visible = false;
|
||||||
|
|
@ -742,6 +747,7 @@ void wm_init(void) {
|
||||||
win_cmd.visible = false;
|
win_cmd.visible = false;
|
||||||
win_calculator.visible = false;
|
win_calculator.visible = false;
|
||||||
win_editor.visible = false;
|
win_editor.visible = false;
|
||||||
|
win_markdown.visible = false;
|
||||||
win_control_panel.visible = false;
|
win_control_panel.visible = false;
|
||||||
win_about.visible = false;
|
win_about.visible = false;
|
||||||
win_minesweeper.visible = false;
|
win_minesweeper.visible = false;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue