/* * main.cpp * MontaukOS Text Editor - standalone Window Server app * Single-buffer text editor with line numbers, cursor, scrolling, file I/O, * syntax highlighting for C and Lua files * Copyright (c) 2026 Daniel Hammer */ #include #include #include #include #include #include #include extern "C" { #include #include } using namespace gui; #include "syntax_highlight.hpp" // ============================================================================ // Constants // ============================================================================ static constexpr int INIT_W = 600; static constexpr int INIT_H = 450; static constexpr int TOOLBAR_H = 36; static constexpr int PATHBAR_H = 32; static constexpr int STATUS_H = 24; static constexpr int LINE_NUM_W = 48; static constexpr int SCROLLBAR_W = 12; static constexpr int INIT_CAP = 4096; static constexpr int MAX_CAP = 262144; // 256KB static constexpr int MAX_LINES = 16384; static constexpr int TAB_WIDTH = 4; static constexpr int FONT_SIZE = 16; static constexpr int SYN_LINE_BUF_CAP = 1024; // Colors static constexpr Color BG_COLOR = Color::from_rgb(0xFF, 0xFF, 0xFF); static constexpr Color TOOLBAR_BG = Color::from_rgb(0xF5, 0xF5, 0xF5); static constexpr Color BTN_BG = Color::from_rgb(0xE8, 0xE8, 0xE8); static constexpr Color BORDER_COLOR = Color::from_rgb(0xD0, 0xD0, 0xD0); static constexpr Color LINENUM_COLOR = Color::from_rgb(0x99, 0x99, 0x99); static constexpr Color LINENUM_BG = Color::from_rgb(0xF0, 0xF0, 0xF0); static constexpr Color CURSOR_LINE_BG = Color::from_rgb(0xFF, 0xFD, 0xE8); static constexpr Color TEXT_COLOR = Color::from_rgb(0x1E, 0x1E, 0x1E); static constexpr Color SEL_BG = Color::from_rgb(0xB0, 0xD0, 0xF0); static constexpr Color ACCENT = Color::from_rgb(0x36, 0x7B, 0xF0); static constexpr Color STATUS_BG = Color::from_rgb(0x2B, 0x3E, 0x50); static constexpr Color STATUS_TEXT = Color::from_rgb(0xE0, 0xE0, 0xE0); static constexpr Color PATHBAR_BG = Color::from_rgb(0xF0, 0xF0, 0xF0); static constexpr Color WHITE = Color::from_rgb(0xFF, 0xFF, 0xFF); // ============================================================================ // Global state // ============================================================================ int g_win_w = INIT_W; int g_win_h = INIT_H; TrueTypeFont* g_font = nullptr; // UI font (Roboto) TrueTypeFont* g_font_mono = nullptr; // Editor font (JetBrains Mono) SvgIcon g_icon_folder = {}; SvgIcon g_icon_save = {}; // Buffer char* g_buffer = nullptr; int g_buf_len = 0; int g_buf_cap = 0; // Line index int* g_line_offsets = nullptr; int g_line_count = 0; // Cursor int g_cursor_pos = 0; // byte position int g_cursor_line = 0; int g_cursor_col = 0; int g_scroll_y = 0; // first visible line int g_scroll_x = 0; // horizontal scroll in pixels bool g_modified = false; bool g_cursor_moved = true; // File char g_filepath[256] = {}; char g_filename[64] = {}; // Selection int g_sel_anchor = 0; int g_sel_end = 0; bool g_has_selection = false; bool g_mouse_selecting = false; // Pathbar bool g_pathbar_open = false; bool g_pathbar_save = false; char g_pathbar_text[256] = {}; int g_pathbar_len = 0; int g_pathbar_cursor = 0; // Syntax highlighting SynLanguage g_syntax_language = SYN_LANG_NONE; // ============================================================================ // Pixel drawing helpers // ============================================================================ static void px_fill(uint32_t* px, int bw, int bh, int x, int y, int w, int h, Color c) { uint32_t v = c.to_pixel(); int x0 = x < 0 ? 0 : x, y0 = y < 0 ? 0 : y; int x1 = x + w > bw ? bw : x + w; int y1 = y + h > bh ? bh : y + h; for (int row = y0; row < y1; row++) for (int col = x0; col < x1; col++) px[row * bw + col] = v; } static void px_hline(uint32_t* px, int bw, int bh, int x, int y, int w, Color c) { if (y < 0 || y >= bh) return; uint32_t v = c.to_pixel(); int x0 = x < 0 ? 0 : x; int x1 = x + w > bw ? bw : x + w; for (int col = x0; col < x1; col++) px[y * bw + col] = v; } static void px_vline(uint32_t* px, int bw, int bh, int x, int y, int h, Color c) { if (x < 0 || x >= bw) return; uint32_t v = c.to_pixel(); int y0 = y < 0 ? 0 : y; int y1 = y + h > bh ? bh : y + h; for (int row = y0; row < y1; row++) px[row * bw + x] = v; } static void px_fill_rounded(uint32_t* px, int bw, int bh, int x, int y, int w, int h, int r, Color c) { uint32_t v = c.to_pixel(); for (int row = 0; row < h; row++) { int py = y + row; if (py < 0 || py >= bh) continue; int inset = 0; if (row < r) { int dy = r - 1 - row; if (r == 3) { if (dy >= 2) inset = 2; else if (dy >= 1) inset = 1; } } else if (row >= h - r) { int dy = row - (h - r); if (r == 3) { if (dy >= 2) inset = 2; else if (dy >= 1) inset = 1; } } int x0 = x + inset; int x1 = x + w - inset; if (x0 < 0) x0 = 0; if (x1 > bw) x1 = bw; for (int col = x0; col < x1; col++) px[py * bw + col] = v; } } static void px_icon(uint32_t* px, int bw, int bh, int x, int y, const SvgIcon& ic) { if (!ic.pixels) return; for (int row = 0; row < ic.height; row++) { int dy = y + row; if (dy < 0 || dy >= bh) continue; for (int col = 0; col < ic.width; col++) { int dx = x + col; if (dx < 0 || dx >= bw) continue; uint32_t src = ic.pixels[row * ic.width + col]; uint8_t sa = (src >> 24) & 0xFF; if (sa == 0) continue; if (sa == 255) { px[dy * bw + dx] = src; } else { uint32_t dst = px[dy * bw + dx]; uint8_t sr = (src >> 16) & 0xFF, sg = (src >> 8) & 0xFF, sb = src & 0xFF; uint8_t dr = (dst >> 16) & 0xFF, dg = (dst >> 8) & 0xFF, db = dst & 0xFF; uint32_t a = sa, inv = 255 - sa; uint32_t rr = (a * sr + inv * dr + 128) / 255; uint32_t gg = (a * sg + inv * dg + 128) / 255; uint32_t bb = (a * sb + inv * db + 128) / 255; px[dy * bw + dx] = 0xFF000000 | (rr << 16) | (gg << 8) | bb; } } } } // ============================================================================ // Font metrics // ============================================================================ static constexpr int MONO_SIZE = 18; static int font_cell_w() { if (g_font_mono && g_font_mono->valid) { GlyphCache* gc = g_font_mono->get_cache(MONO_SIZE); CachedGlyph* g = g_font_mono->get_glyph(gc, 'M'); if (g) return g->advance; } return 8; } static int font_cell_h() { if (g_font_mono && g_font_mono->valid) return g_font_mono->get_line_height(MONO_SIZE); return 16; } // ============================================================================ // Line index management // ============================================================================ static void recompute_lines() { if (!g_line_offsets) g_line_offsets = (int*)montauk::malloc(MAX_LINES * sizeof(int)); g_line_count = 0; g_line_offsets[g_line_count++] = 0; for (int i = 0; i < g_buf_len; i++) { if (g_buffer[i] == '\n' && g_line_count < MAX_LINES) g_line_offsets[g_line_count++] = i + 1; } } static void update_cursor_pos() { g_cursor_line = 0; for (int i = g_line_count - 1; i >= 0; i--) { if (g_cursor_pos >= g_line_offsets[i]) { g_cursor_line = i; break; } } g_cursor_col = g_cursor_pos - g_line_offsets[g_cursor_line]; g_cursor_moved = true; } static int line_length(int line) { if (line < 0 || line >= g_line_count) return 0; int start = g_line_offsets[line]; int end = (line + 1 < g_line_count) ? g_line_offsets[line + 1] - 1 : g_buf_len; return end - start; } // ============================================================================ // Buffer operations // ============================================================================ static void ensure_capacity(int needed) { if (g_buf_len + needed <= g_buf_cap) return; int new_cap = g_buf_cap * 2; if (new_cap > MAX_CAP) new_cap = MAX_CAP; if (new_cap < g_buf_len + needed) new_cap = g_buf_len + needed; g_buffer = (char*)montauk::realloc(g_buffer, new_cap); g_buf_cap = new_cap; } static void delete_range(int start, int count) { for (int i = start; i < g_buf_len - count; i++) g_buffer[i] = g_buffer[i + count]; g_buf_len -= count; } static void insert_char(char c) { if (g_buf_len >= MAX_CAP - 1) return; ensure_capacity(1); for (int i = g_buf_len; i > g_cursor_pos; i--) g_buffer[i] = g_buffer[i - 1]; g_buffer[g_cursor_pos] = c; g_buf_len++; g_cursor_pos++; g_modified = true; recompute_lines(); update_cursor_pos(); } static void insert_string(const char* s, int len) { if (g_buf_len + len >= MAX_CAP) return; ensure_capacity(len); for (int i = g_buf_len - 1; i >= g_cursor_pos; i--) g_buffer[i + len] = g_buffer[i]; for (int i = 0; i < len; i++) g_buffer[g_cursor_pos + i] = s[i]; g_buf_len += len; g_cursor_pos += len; g_modified = true; recompute_lines(); update_cursor_pos(); } static void backspace() { if (g_cursor_pos <= 0) return; g_cursor_pos--; delete_range(g_cursor_pos, 1); g_modified = true; recompute_lines(); update_cursor_pos(); } static void delete_char() { if (g_cursor_pos >= g_buf_len) return; delete_range(g_cursor_pos, 1); g_modified = true; recompute_lines(); update_cursor_pos(); } // ============================================================================ // Cursor movement // ============================================================================ static void move_up() { if (g_cursor_line <= 0) return; int col = g_cursor_col; int prev = g_cursor_line - 1; int len = line_length(prev); if (col > len) col = len; g_cursor_pos = g_line_offsets[prev] + col; update_cursor_pos(); } static void move_down() { if (g_cursor_line >= g_line_count - 1) return; int col = g_cursor_col; int next = g_cursor_line + 1; int len = line_length(next); if (col > len) col = len; g_cursor_pos = g_line_offsets[next] + col; update_cursor_pos(); } static void move_left() { if (g_cursor_pos > 0) { g_cursor_pos--; update_cursor_pos(); } } static void move_right() { if (g_cursor_pos < g_buf_len) { g_cursor_pos++; update_cursor_pos(); } } static void move_home() { g_cursor_pos = g_line_offsets[g_cursor_line]; update_cursor_pos(); } static void move_end() { g_cursor_pos = g_line_offsets[g_cursor_line] + line_length(g_cursor_line); update_cursor_pos(); } // ============================================================================ // Selection // ============================================================================ static void clear_selection() { g_has_selection = false; g_sel_anchor = 0; g_sel_end = 0; } static void sel_range(int* out_s, int* out_e) { if (g_sel_anchor < g_sel_end) { *out_s = g_sel_anchor; *out_e = g_sel_end; } else { *out_s = g_sel_end; *out_e = g_sel_anchor; } } static void start_selection() { if (!g_has_selection) { g_sel_anchor = g_cursor_pos; g_sel_end = g_cursor_pos; g_has_selection = true; } } static void update_selection() { g_sel_end = g_cursor_pos; if (g_sel_anchor == g_sel_end) g_has_selection = false; } static void delete_selection() { if (!g_has_selection) return; int ss, se; sel_range(&ss, &se); int count = se - ss; if (count <= 0) { clear_selection(); return; } delete_range(ss, count); g_cursor_pos = ss; g_modified = true; clear_selection(); recompute_lines(); update_cursor_pos(); } // ============================================================================ // Scrolling // ============================================================================ static void ensure_cursor_visible(int visible_lines, int text_area_w) { if (g_cursor_line < g_scroll_y) g_scroll_y = g_cursor_line; if (g_cursor_line >= g_scroll_y + visible_lines) g_scroll_y = g_cursor_line - visible_lines + 1; int max_scroll_y = g_line_count - visible_lines; if (max_scroll_y < 0) max_scroll_y = 0; if (g_scroll_y > max_scroll_y) g_scroll_y = max_scroll_y; if (g_scroll_y < 0) g_scroll_y = 0; int cell_w = font_cell_w(); int cursor_px = g_cursor_col * cell_w; int view_w = text_area_w - LINE_NUM_W - SCROLLBAR_W; if (cursor_px - g_scroll_x > view_w - cell_w * 2) g_scroll_x = cursor_px - view_w + cell_w * 4; if (cursor_px < g_scroll_x) { g_scroll_x = cursor_px - cell_w * 2; if (g_scroll_x < 0) g_scroll_x = 0; } } // ============================================================================ // File I/O // ============================================================================ static void load_file(const char* path) { int fd = montauk::open(path); if (fd < 0) return; uint64_t size = montauk::getsize(fd); if (size > (uint64_t)MAX_CAP) size = MAX_CAP; if ((int)size >= g_buf_cap) { int new_cap = (int)size + 1024; if (new_cap > MAX_CAP) new_cap = MAX_CAP; g_buffer = (char*)montauk::realloc(g_buffer, new_cap); g_buf_cap = new_cap; } montauk::read(fd, (uint8_t*)g_buffer, 0, size); montauk::close(fd); g_buf_len = (int)size; g_cursor_pos = 0; g_scroll_y = 0; g_scroll_x = 0; g_modified = false; montauk::strncpy(g_filepath, path, 255); int last_slash = -1; for (int i = 0; path[i]; i++) if (path[i] == '/') last_slash = i; if (last_slash >= 0) montauk::strncpy(g_filename, path + last_slash + 1, 63); else montauk::strncpy(g_filename, path, 63); g_syntax_language = syn_detect_language(g_filepath); recompute_lines(); update_cursor_pos(); } static void save_file() { if (g_filepath[0] == '\0') return; int fd = montauk::fcreate(g_filepath); if (fd < 0) return; montauk::fwrite(fd, (const uint8_t*)g_buffer, 0, g_buf_len); montauk::close(fd); g_modified = false; } // ============================================================================ // Hit testing // ============================================================================ static int hit_test(int mx, int my, int editor_y) { int cell_w = font_cell_w(); int cell_h = font_cell_h(); int clicked_line = g_scroll_y + (my - editor_y) / cell_h; if (clicked_line >= g_line_count) clicked_line = g_line_count - 1; if (clicked_line < 0) clicked_line = 0; int clicked_col = (mx - LINE_NUM_W - 4 + g_scroll_x + cell_w / 2) / cell_w; if (clicked_col < 0) clicked_col = 0; int ll = line_length(clicked_line); if (clicked_col > ll) clicked_col = ll; return g_line_offsets[clicked_line] + clicked_col; } // ============================================================================ // Rendering // ============================================================================ static void render(uint32_t* pixels) { int W = g_win_w, H = g_win_h; int cell_w = font_cell_w(); int cell_h = font_cell_h(); // Clear px_fill(pixels, W, H, 0, 0, W, H, BG_COLOR); // ---- Toolbar ---- px_fill(pixels, W, H, 0, 0, W, TOOLBAR_H, TOOLBAR_BG); // Open button px_fill_rounded(pixels, W, H, 4, 6, 24, 24, 3, BTN_BG); px_icon(pixels, W, H, 8, 10, g_icon_folder); // Save button px_fill_rounded(pixels, W, H, 32, 6, 24, 24, 3, BTN_BG); px_icon(pixels, W, H, 36, 10, g_icon_save); // Separator px_vline(pixels, W, H, 60, 4, 28, BORDER_COLOR); // Filename + modified char label[128]; if (g_filename[0]) snprintf(label, 128, "%s%s", g_filename, g_modified ? " [modified]" : ""); else snprintf(label, 128, "Untitled%s", g_modified ? " [modified]" : ""); if (g_font) g_font->draw_to_buffer(pixels, W, H, 68, (TOOLBAR_H - FONT_SIZE) / 2, label, TEXT_COLOR, FONT_SIZE); // Toolbar bottom line px_hline(pixels, W, H, 0, TOOLBAR_H - 1, W, BORDER_COLOR); // ---- Pathbar (conditional) ---- int editor_y = TOOLBAR_H; if (g_pathbar_open) { int pb_y = TOOLBAR_H; px_fill(pixels, W, H, 0, pb_y, W, PATHBAR_H, PATHBAR_BG); int btn_w = 56; int inp_x = 8; int inp_y = pb_y + 4; int inp_w = W - inp_x - btn_w - 12; int inp_h = 24; // Input box px_fill(pixels, W, H, inp_x, inp_y, inp_w, inp_h, WHITE); // Input border (top, bottom, left, right) px_hline(pixels, W, H, inp_x, inp_y, inp_w, ACCENT); px_hline(pixels, W, H, inp_x, inp_y + inp_h - 1, inp_w, ACCENT); px_vline(pixels, W, H, inp_x, inp_y, inp_h, ACCENT); px_vline(pixels, W, H, inp_x + inp_w - 1, inp_y, inp_h, ACCENT); // Path text if (g_font && g_pathbar_text[0]) g_font->draw_to_buffer(pixels, W, H, inp_x + 4, inp_y + (inp_h - FONT_SIZE) / 2, g_pathbar_text, TEXT_COLOR, FONT_SIZE); // Cursor in pathbar char prefix[256]; int plen = g_pathbar_cursor > 255 ? 255 : g_pathbar_cursor; for (int i = 0; i < plen; i++) prefix[i] = g_pathbar_text[i]; prefix[plen] = '\0'; int cx = inp_x + 4 + (g_font ? g_font->measure_text(prefix, FONT_SIZE) : 0); px_fill(pixels, W, H, cx, inp_y + 3, 2, inp_h - 6, ACCENT); // Button int ob_x = inp_x + inp_w + 6; px_fill_rounded(pixels, W, H, ob_x, inp_y, btn_w, inp_h, 3, ACCENT); const char* btn_label = g_pathbar_save ? "Save" : "Open"; if (g_font) { int tw = g_font->measure_text(btn_label, FONT_SIZE); g_font->draw_to_buffer(pixels, W, H, ob_x + (btn_w - tw) / 2, inp_y + (inp_h - FONT_SIZE) / 2, btn_label, WHITE, FONT_SIZE); } // Pathbar bottom line px_hline(pixels, W, H, 0, pb_y + PATHBAR_H - 1, W, BORDER_COLOR); editor_y = TOOLBAR_H + PATHBAR_H; } // ---- Editor area ---- int text_area_h = H - editor_y - STATUS_H; int visible_lines = text_area_h / cell_h; if (visible_lines < 1) visible_lines = 1; int text_area_w = W - SCROLLBAR_W; if (g_cursor_moved) { ensure_cursor_visible(visible_lines, W); g_cursor_moved = false; } // Line number gutter px_fill(pixels, W, H, 0, editor_y, LINE_NUM_W, text_area_h, LINENUM_BG); px_vline(pixels, W, H, LINE_NUM_W, editor_y, text_area_h, BORDER_COLOR); // Selection range int sel_s = 0, sel_e = 0; if (g_has_selection) sel_range(&sel_s, &sel_e); int text_start_x = LINE_NUM_W + 4; // Syntax highlighting: compute multi-line syntax state up to the first visible line SynState syn_state = syn_make_state(); SynToken syn_line_buf[SYN_LINE_BUF_CAP]; if (g_syntax_language != SYN_LANG_NONE) { for (int i = 0; i < g_scroll_y && i < g_line_count; i++) { int ls = g_line_offsets[i]; int ll = line_length(i); syn_highlight_line(g_buffer + ls, ll, nullptr, 0, g_syntax_language, syn_state); } } GlyphCache* gc = (g_font_mono && g_font_mono->valid) ? g_font_mono->get_cache(MONO_SIZE) : nullptr; for (int vis = 0; vis < visible_lines + 1; vis++) { int line = g_scroll_y + vis; if (line >= g_line_count) break; int py = editor_y + vis * cell_h; if (py >= editor_y + text_area_h) break; // Cursor line highlight if (line == g_cursor_line && !g_has_selection) { int hl_h = cell_h; if (py + hl_h > editor_y + text_area_h) hl_h = editor_y + text_area_h - py; if (hl_h > 0) px_fill(pixels, W, H, LINE_NUM_W + 1, py, text_area_w - LINE_NUM_W - 1, hl_h, CURSOR_LINE_BG); } // Line number char num_str[8]; snprintf(num_str, 8, "%4d", line + 1); if (g_font_mono) g_font_mono->draw_to_buffer(pixels, W, H, 4, py, num_str, LINENUM_COLOR, MONO_SIZE); // Line text int line_start = g_line_offsets[line]; int line_len = line_length(line); // Syntax highlight this line int syn_len = line_len > SYN_LINE_BUF_CAP ? SYN_LINE_BUF_CAP : line_len; if (g_syntax_language != SYN_LANG_NONE) syn_highlight_line(g_buffer + line_start, line_len, syn_line_buf, SYN_LINE_BUF_CAP, g_syntax_language, syn_state); for (int ci = 0; ci < line_len; ci++) { int px_x = text_start_x + ci * cell_w - g_scroll_x; if (px_x + cell_w <= LINE_NUM_W + 1) continue; if (px_x >= text_area_w) break; int byte_pos = line_start + ci; char ch = g_buffer[byte_pos]; // Selection highlight if (g_has_selection && byte_pos >= sel_s && byte_pos < sel_e) { int hl_h = cell_h; if (py + hl_h > editor_y + text_area_h) hl_h = editor_y + text_area_h - py; if (hl_h > 0) px_fill(pixels, W, H, px_x, py, cell_w, hl_h, SEL_BG); } Color char_color = TEXT_COLOR; if (g_syntax_language != SYN_LANG_NONE && ci < syn_len) char_color = syn_color(syn_line_buf[ci]); if (ch >= 32 || ch < 0) { if (gc) { g_font_mono->draw_char_to_buffer(pixels, W, H, px_x, py + gc->ascent, ch, char_color, gc); } } } // Selection highlight for newline at end of line if (g_has_selection && line + 1 < g_line_count) { int nl_pos = line_start + line_len; if (nl_pos >= sel_s && nl_pos < sel_e) { int px_x = text_start_x + line_len * cell_w - g_scroll_x; if (px_x > LINE_NUM_W && px_x < text_area_w) { int hl_h = cell_h; if (py + hl_h > editor_y + text_area_h) hl_h = editor_y + text_area_h - py; if (hl_h > 0) px_fill(pixels, W, H, px_x, py, cell_w / 2, hl_h, SEL_BG); } } } // Cursor if (line == g_cursor_line) { int cx = text_start_x + g_cursor_col * cell_w - g_scroll_x; if (cx > LINE_NUM_W && cx + 2 <= text_area_w) { int cur_h = cell_h; if (py + cur_h > editor_y + text_area_h) cur_h = editor_y + text_area_h - py; if (cur_h > 0) px_fill(pixels, W, H, cx, py, 2, cur_h, ACCENT); } } } // ---- Scrollbar ---- int content_h = g_line_count * cell_h; if (content_h > text_area_h) { int sb_x = W - SCROLLBAR_W; px_fill(pixels, W, H, sb_x, editor_y, SCROLLBAR_W, text_area_h, Color::from_rgb(0xF0, 0xF0, 0xF0)); int max_scr = g_line_count - visible_lines; if (max_scr < 1) max_scr = 1; int thumb_h = (text_area_h * text_area_h) / content_h; if (thumb_h < 20) thumb_h = 20; int range = text_area_h - thumb_h; int thumb_y = editor_y + (g_scroll_y * range) / max_scr; px_fill(pixels, W, H, sb_x + 1, thumb_y, SCROLLBAR_W - 2, thumb_h, Color::from_rgb(0xAA, 0xAA, 0xAA)); } // ---- Status bar ---- int status_y = H - STATUS_H; px_fill(pixels, W, H, 0, status_y, W, STATUS_H, STATUS_BG); int sty = status_y + (STATUS_H - FONT_SIZE) / 2; // Left: filename char status_left[128]; if (g_filename[0]) snprintf(status_left, 128, " %s%s", g_filename, g_modified ? " [modified]" : ""); else snprintf(status_left, 128, " Untitled%s", g_modified ? " [modified]" : ""); if (g_font) g_font->draw_to_buffer(pixels, W, H, 4, sty, status_left, STATUS_TEXT, FONT_SIZE); // Right: cursor position char status_right[48]; if (g_has_selection) { int ss, se; sel_range(&ss, &se); snprintf(status_right, 48, "%d selected Ln %d, Col %d ", se - ss, g_cursor_line + 1, g_cursor_col + 1); } else { snprintf(status_right, 48, "Ln %d, Col %d ", g_cursor_line + 1, g_cursor_col + 1); } if (g_font) { int rw = g_font->measure_text(status_right, FONT_SIZE); g_font->draw_to_buffer(pixels, W, H, W - rw - 4, sty, status_right, STATUS_TEXT, FONT_SIZE); } } // ============================================================================ // Pathbar actions // ============================================================================ static void pathbar_confirm(int win_id) { if (g_pathbar_text[0] == '\0') return; if (g_pathbar_save) { montauk::strncpy(g_filepath, g_pathbar_text, 255); const char* name = g_pathbar_text; for (int i = 0; g_pathbar_text[i]; i++) if (g_pathbar_text[i] == '/') name = g_pathbar_text + i + 1; montauk::strncpy(g_filename, name, 63); g_syntax_language = syn_detect_language(g_filepath); save_file(); } else { load_file(g_pathbar_text); } g_pathbar_open = false; } static void open_pathbar_open() { g_pathbar_open = true; g_pathbar_save = false; montauk::strncpy(g_pathbar_text, g_filepath, 255); g_pathbar_len = montauk::slen(g_pathbar_text); g_pathbar_cursor = g_pathbar_len; } static void open_pathbar_save() { g_pathbar_open = true; g_pathbar_save = true; g_pathbar_text[0] = '\0'; g_pathbar_len = 0; g_pathbar_cursor = 0; } // ============================================================================ // Keyboard handler // ============================================================================ static bool handle_key(Montauk::KeyEvent& key, int win_id) { if (!key.pressed) return false; // ---- Pathbar mode ---- if (g_pathbar_open) { if (key.ascii == '\n' || key.ascii == '\r') { pathbar_confirm(win_id); return true; } if (key.scancode == 0x01) { // Escape g_pathbar_open = false; return true; } if (key.ascii == '\b' || key.scancode == 0x0E) { if (g_pathbar_cursor > 0) { for (int i = g_pathbar_cursor - 1; i < g_pathbar_len - 1; i++) g_pathbar_text[i] = g_pathbar_text[i + 1]; g_pathbar_len--; g_pathbar_cursor--; g_pathbar_text[g_pathbar_len] = '\0'; } return true; } if (key.scancode == 0x4B) { if (g_pathbar_cursor > 0) g_pathbar_cursor--; return true; } if (key.scancode == 0x4D) { if (g_pathbar_cursor < g_pathbar_len) g_pathbar_cursor++; return true; } if (key.ascii >= 32 && key.ascii < 127 && g_pathbar_len < 254) { for (int i = g_pathbar_len; i > g_pathbar_cursor; i--) g_pathbar_text[i] = g_pathbar_text[i - 1]; g_pathbar_text[g_pathbar_cursor] = key.ascii; g_pathbar_cursor++; g_pathbar_len++; g_pathbar_text[g_pathbar_len] = '\0'; return true; } return false; } // ---- Editor mode ---- // Ctrl+S: save if (key.ctrl && (key.ascii == 's' || key.ascii == 'S')) { if (g_filepath[0] == '\0') open_pathbar_save(); else save_file(); return true; } // Ctrl+O: open if (key.ctrl && (key.ascii == 'o' || key.ascii == 'O')) { if (g_pathbar_open) g_pathbar_open = false; else open_pathbar_open(); return true; } // Ctrl+A: select all if (key.ctrl && (key.ascii == 'a' || key.ascii == 'A')) { g_sel_anchor = 0; g_sel_end = g_buf_len; g_has_selection = (g_buf_len > 0); g_cursor_pos = g_buf_len; update_cursor_pos(); return true; } // Arrow keys if (key.scancode == 0x48) { // Up if (key.shift) start_selection(); else if (g_has_selection) clear_selection(); move_up(); if (key.shift) update_selection(); return true; } if (key.scancode == 0x50) { // Down if (key.shift) start_selection(); else if (g_has_selection) clear_selection(); move_down(); if (key.shift) update_selection(); return true; } if (key.scancode == 0x4B) { // Left if (key.shift) { start_selection(); move_left(); update_selection(); } else if (g_has_selection) { int s, e; sel_range(&s, &e); g_cursor_pos = s; update_cursor_pos(); clear_selection(); } else move_left(); return true; } if (key.scancode == 0x4D) { // Right if (key.shift) { start_selection(); move_right(); update_selection(); } else if (g_has_selection) { int s, e; sel_range(&s, &e); g_cursor_pos = e; update_cursor_pos(); clear_selection(); } else move_right(); return true; } // Home if (key.scancode == 0x47) { if (key.shift) start_selection(); else if (g_has_selection) clear_selection(); move_home(); if (key.shift) update_selection(); return true; } // End if (key.scancode == 0x4F) { if (key.shift) start_selection(); else if (g_has_selection) clear_selection(); move_end(); if (key.shift) update_selection(); return true; } // Page Up if (key.scancode == 0x49) { if (key.shift) start_selection(); else if (g_has_selection) clear_selection(); int cell_h = font_cell_h(); int vis = cell_h > 0 ? (g_win_h - TOOLBAR_H - STATUS_H) / cell_h : 20; for (int i = 0; i < vis; i++) move_up(); if (key.shift) update_selection(); return true; } // Page Down if (key.scancode == 0x51) { if (key.shift) start_selection(); else if (g_has_selection) clear_selection(); int cell_h = font_cell_h(); int vis = cell_h > 0 ? (g_win_h - TOOLBAR_H - STATUS_H) / cell_h : 20; for (int i = 0; i < vis; i++) move_down(); if (key.shift) update_selection(); return true; } // Delete if (key.scancode == 0x53) { if (g_has_selection) delete_selection(); else delete_char(); return true; } // Backspace if (key.ascii == '\b' || key.scancode == 0x0E) { if (g_has_selection) delete_selection(); else backspace(); return true; } // Enter if (key.ascii == '\n' || key.ascii == '\r') { if (g_has_selection) delete_selection(); insert_char('\n'); return true; } // Tab if (key.ascii == '\t') { if (g_has_selection) delete_selection(); for (int i = 0; i < TAB_WIDTH; i++) insert_char(' '); return true; } // Printable if (key.ascii >= 32 && key.ascii < 127) { if (g_has_selection) delete_selection(); insert_char(key.ascii); return true; } return false; } // ============================================================================ // Mouse handler // ============================================================================ static bool handle_mouse(Montauk::WinEvent& ev, int win_id) { int mx = ev.mouse.x; int my = ev.mouse.y; uint8_t btns = ev.mouse.buttons; uint8_t prev = ev.mouse.prev_buttons; bool clicked = (btns & 1) && !(prev & 1); bool held = (btns & 1) != 0; bool released = !(btns & 1) && (prev & 1); int scroll = ev.mouse.scroll; int cell_h = font_cell_h(); int editor_y = TOOLBAR_H + (g_pathbar_open ? PATHBAR_H : 0); int text_area_h = g_win_h - editor_y - STATUS_H; // ---- Toolbar clicks ---- if (clicked && my < TOOLBAR_H) { // Open button if (mx >= 4 && mx < 28 && my >= 6 && my < 30) { if (g_pathbar_open) g_pathbar_open = false; else open_pathbar_open(); return true; } // Save button if (mx >= 32 && mx < 56 && my >= 6 && my < 30) { if (g_filepath[0] == '\0') open_pathbar_save(); else save_file(); return true; } return false; } // ---- Pathbar clicks ---- if (g_pathbar_open && my >= TOOLBAR_H && my < TOOLBAR_H + PATHBAR_H) { if (clicked) { int btn_w = 56; int inp_w = g_win_w - 8 - btn_w - 12; int ob_x = 8 + inp_w + 6; if (mx >= ob_x && mx < ob_x + btn_w) pathbar_confirm(win_id); } return true; } // ---- Editor click ---- if (clicked && my >= editor_y && my < editor_y + text_area_h && mx > LINE_NUM_W && mx < g_win_w - SCROLLBAR_W) { int pos = hit_test(mx, my, editor_y); g_cursor_pos = pos; update_cursor_pos(); g_sel_anchor = pos; g_sel_end = pos; g_has_selection = false; g_mouse_selecting = true; return true; } // ---- Editor drag ---- if (g_mouse_selecting && held && my >= editor_y - 20) { int pos = hit_test(mx, my, editor_y); g_sel_end = pos; g_cursor_pos = pos; update_cursor_pos(); g_has_selection = (g_sel_anchor != g_sel_end); return true; } // ---- Mouse release ---- if (g_mouse_selecting && released) { g_mouse_selecting = false; return true; } // ---- Scroll ---- if (scroll != 0 && my >= editor_y && my < editor_y + text_area_h) { int old_scroll = g_scroll_y; g_scroll_y -= scroll * 3; if (g_scroll_y < 0) g_scroll_y = 0; int visible_lines = text_area_h / cell_h; int max_scroll = g_line_count - visible_lines; if (max_scroll < 0) max_scroll = 0; if (g_scroll_y > max_scroll) g_scroll_y = max_scroll; // Move cursor with scroll to keep it visible int delta = g_scroll_y - old_scroll; if (delta != 0) { int new_line = g_cursor_line + delta; if (new_line < g_scroll_y) new_line = g_scroll_y; if (new_line >= g_scroll_y + visible_lines) new_line = g_scroll_y + visible_lines - 1; if (new_line < 0) new_line = 0; if (new_line >= g_line_count) new_line = g_line_count - 1; int col = g_cursor_col; int ll = line_length(new_line); if (col > ll) col = ll; g_cursor_pos = g_line_offsets[new_line] + col; g_cursor_line = new_line; g_cursor_col = col; } return true; } return false; } // ============================================================================ // Entry point // ============================================================================ extern "C" void _start() { // Initialize buffer g_buffer = (char*)montauk::malloc(INIT_CAP); g_buf_cap = INIT_CAP; g_buf_len = 0; recompute_lines(); update_cursor_pos(); // Load fonts auto load_font = [](const char* path) -> TrueTypeFont* { TrueTypeFont* f = (TrueTypeFont*)montauk::malloc(sizeof(TrueTypeFont)); if (!f) return nullptr; montauk::memset(f, 0, sizeof(TrueTypeFont)); if (!f->init(path)) { montauk::mfree(f); return nullptr; } return f; }; g_font = load_font("0:/fonts/Roboto-Medium.ttf"); g_font_mono = load_font("0:/fonts/JetBrainsMono-Regular.ttf"); // Load toolbar icons Color icon_color = Color::from_rgb(0x44, 0x44, 0x44); g_icon_folder = svg_load("0:/icons/folder.svg", 16, 16, icon_color); g_icon_save = svg_load("0:/icons/document-save-symbolic.svg", 16, 16, icon_color); // Check for file argument char args[512] = {}; int arglen = montauk::getargs(args, sizeof(args)); if (arglen > 0 && args[0]) load_file(args); // Build window title char title[64] = "Text Editor"; if (g_filename[0]) { snprintf(title, 64, "%s - Editor", g_filename); } // Create window Montauk::WinCreateResult wres; if (montauk::win_create(title, INIT_W, INIT_H, &wres) < 0 || wres.id < 0) montauk::exit(1); int win_id = wres.id; uint32_t* pixels = (uint32_t*)(uintptr_t)wres.pixelVa; render(pixels); montauk::win_present(win_id); while (true) { Montauk::WinEvent ev; int r = montauk::win_poll(win_id, &ev); if (r < 0) break; if (r == 0) { montauk::sleep_ms(16); continue; } // Close if (ev.type == 3) break; // Resize if (ev.type == 2) { g_win_w = ev.resize.w; g_win_h = ev.resize.h; pixels = (uint32_t*)(uintptr_t)montauk::win_resize(win_id, g_win_w, g_win_h); render(pixels); montauk::win_present(win_id); continue; } bool redraw = false; // Keyboard if (ev.type == 0) redraw = handle_key(ev.key, win_id); // Mouse if (ev.type == 1) redraw = handle_mouse(ev, win_id); if (redraw) { render(pixels); montauk::win_present(win_id); } } // Cleanup if (g_buffer) montauk::mfree(g_buffer); if (g_line_offsets) montauk::mfree(g_line_offsets); if (g_font) montauk::mfree(g_font); if (g_font_mono) montauk::mfree(g_font_mono); montauk::win_destroy(win_id); montauk::exit(0); }