/** * @file notepad.c * A full-featured, reactive text editor built on YASL Approach A. * * Features: * - Open & Save files (CLI argument `yasl-notepad [file]`, Ctrl+O, Ctrl+S) * - Tab key inserts 4 spaces; Shift+Tab unindents * - Clipboard Copy (Ctrl+C), Cut (Ctrl+X), Paste (Ctrl+V) via YASL clipboard API * - Select All (Ctrl+A), text selection with mouse drag and Shift+Arrows * - Undo support (Ctrl+Z) * - Find / Search in document (Ctrl+F) * - Drag-and-drop files directly into the window to open them * - Dynamic HiDPI scale awareness & 0% CPU consumption when idle * - Asynchronous background word & character analysis via yasl_post_event() */ #define _DEFAULT_SOURCE #include #include #include #include #include #include #include #include #include #define MAX_LINES 4096 #define MAX_LINE_LEN 1024 #define MAX_UNDO 32 /* Classic 8x8 font bitmap covering ASCII 32 (' ') to 126 ('~') */ static const uint8_t font8x8[95][8] = { {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, /* */ {0x18,0x3C,0x3C,0x18,0x18,0x00,0x18,0x00}, /* ! */ {0x66,0x66,0x24,0x00,0x00,0x00,0x00,0x00}, /* " */ {0x6C,0x6C,0xFE,0x6C,0xFE,0x6C,0x6C,0x00}, /* # */ {0x18,0x3E,0x60,0x3C,0x06,0x7C,0x18,0x00}, /* $ */ {0x00,0x63,0x66,0x0C,0x18,0x33,0x63,0x00}, /* % */ {0x38,0x6C,0x38,0x76,0xDC,0xCC,0x76,0x00}, /* & */ {0x18,0x18,0x30,0x00,0x00,0x00,0x00,0x00}, /* ' */ {0x0C,0x18,0x30,0x30,0x30,0x18,0x0C,0x00}, /* ( */ {0x30,0x18,0x0C,0x0C,0x0C,0x18,0x30,0x00}, /* ) */ {0x00,0x66,0x3C,0xFF,0x3C,0x66,0x00,0x00}, /* * */ {0x00,0x18,0x18,0x7E,0x18,0x18,0x00,0x00}, /* + */ {0x00,0x00,0x00,0x00,0x00,0x18,0x18,0x30}, /* , */ {0x00,0x00,0x00,0x7E,0x00,0x00,0x00,0x00}, /* - */ {0x00,0x00,0x00,0x00,0x00,0x18,0x18,0x00}, /* . */ {0x06,0x0C,0x18,0x30,0x60,0xC0,0x80,0x00}, /* / */ {0x3C,0x66,0x6E,0x76,0x66,0x66,0x3C,0x00}, /* 0 */ {0x18,0x38,0x18,0x18,0x18,0x18,0x7E,0x00}, /* 1 */ {0x3C,0x66,0x06,0x0C,0x18,0x30,0x7E,0x00}, /* 2 */ {0x3C,0x66,0x06,0x1C,0x06,0x66,0x3C,0x00}, /* 3 */ {0x0C,0x1C,0x34,0x64,0x7E,0x04,0x04,0x00}, /* 4 */ {0x7E,0x60,0x7C,0x06,0x06,0x66,0x3C,0x00}, /* 5 */ {0x1C,0x30,0x60,0x7C,0x66,0x66,0x3C,0x00}, /* 6 */ {0x7E,0x06,0x0C,0x18,0x30,0x30,0x30,0x00}, /* 7 */ {0x3C,0x66,0x66,0x3C,0x66,0x66,0x3C,0x00}, /* 8 */ {0x3C,0x66,0x66,0x3E,0x06,0x0C,0x38,0x00}, /* 9 */ {0x00,0x18,0x18,0x00,0x00,0x18,0x18,0x00}, /* : */ {0x00,0x18,0x18,0x00,0x00,0x18,0x18,0x30}, /* ; */ {0x0C,0x18,0x30,0x60,0x30,0x18,0x0C,0x00}, /* < */ {0x00,0x00,0x7E,0x00,0x7E,0x00,0x00,0x00}, /* = */ {0x30,0x18,0x0C,0x06,0x0C,0x18,0x30,0x00}, /* > */ {0x3C,0x66,0x06,0x0C,0x18,0x00,0x18,0x00}, /* ? */ {0x3C,0x66,0x6E,0x6A,0x6E,0x60,0x3C,0x00}, /* @ */ {0x18,0x3C,0x66,0x7E,0x66,0x66,0x66,0x00}, /* A */ {0x7C,0x66,0x66,0x7C,0x66,0x66,0x7C,0x00}, /* B */ {0x3C,0x66,0x60,0x60,0x60,0x66,0x3C,0x00}, /* C */ {0x78,0x6C,0x66,0x66,0x66,0x6C,0x78,0x00}, /* D */ {0x7E,0x60,0x60,0x7C,0x60,0x60,0x7E,0x00}, /* E */ {0x7E,0x60,0x60,0x7C,0x60,0x60,0x60,0x00}, /* F */ {0x3C,0x66,0x60,0x6E,0x66,0x66,0x3C,0x00}, /* G */ {0x66,0x66,0x66,0x7E,0x66,0x66,0x66,0x00}, /* H */ {0x3C,0x18,0x18,0x18,0x18,0x18,0x3C,0x00}, /* I */ {0x0E,0x06,0x06,0x06,0x66,0x66,0x3C,0x00}, /* J */ {0x66,0x6C,0x78,0x70,0x78,0x6C,0x66,0x00}, /* K */ {0x60,0x60,0x60,0x60,0x60,0x60,0x7E,0x00}, /* L */ {0x63,0x77,0x7F,0x6B,0x63,0x63,0x63,0x00}, /* M */ {0x66,0x76,0x7E,0x7E,0x6E,0x66,0x66,0x00}, /* N */ {0x3C,0x66,0x66,0x66,0x66,0x66,0x3C,0x00}, /* O */ {0x7C,0x66,0x66,0x7C,0x60,0x60,0x60,0x00}, /* P */ {0x3C,0x66,0x66,0x66,0x6E,0x3C,0x0E,0x00}, /* Q */ {0x7C,0x66,0x66,0x7C,0x6C,0x66,0x66,0x00}, /* R */ {0x3C,0x66,0x30,0x18,0x0C,0x66,0x3C,0x00}, /* S */ {0x7E,0x18,0x18,0x18,0x18,0x18,0x18,0x00}, /* T */ {0x66,0x66,0x66,0x66,0x66,0x66,0x3C,0x00}, /* U */ {0x66,0x66,0x66,0x66,0x66,0x3C,0x18,0x00}, /* V */ {0x63,0x63,0x63,0x6B,0x7F,0x77,0x63,0x00}, /* W */ {0x66,0x66,0x3C,0x18,0x3C,0x66,0x66,0x00}, /* X */ {0x66,0x66,0x66,0x3C,0x18,0x18,0x18,0x00}, /* Y */ {0x7E,0x06,0x0C,0x18,0x30,0x60,0x7E,0x00}, /* Z */ {0x3C,0x30,0x30,0x30,0x30,0x30,0x3C,0x00}, /* [ */ {0x80,0xC0,0x60,0x30,0x18,0x0C,0x06,0x00}, /* \ */ {0x3C,0x0C,0x0C,0x0C,0x0C,0x0C,0x3C,0x00}, /* ] */ {0x18,0x3C,0x66,0x00,0x00,0x00,0x00,0x00}, /* ^ */ {0x00,0x00,0x00,0x00,0x00,0x00,0xFF,0x00}, /* _ */ {0x30,0x18,0x0C,0x00,0x00,0x00,0x00,0x00}, /* ` */ {0x00,0x00,0x3C,0x06,0x3E,0x66,0x3B,0x00}, /* a */ {0x60,0x60,0x7C,0x66,0x66,0x66,0x7C,0x00}, /* b */ {0x00,0x00,0x3C,0x66,0x60,0x66,0x3C,0x00}, /* c */ {0x06,0x06,0x3E,0x66,0x66,0x66,0x3E,0x00}, /* d */ {0x00,0x00,0x3C,0x66,0x7E,0x60,0x3C,0x00}, /* e */ {0x1C,0x30,0x30,0x78,0x30,0x30,0x30,0x00}, /* f */ {0x00,0x00,0x3B,0x66,0x66,0x3E,0x06,0x3C}, /* g */ {0x60,0x60,0x7C,0x66,0x66,0x66,0x66,0x00}, /* h */ {0x18,0x00,0x38,0x18,0x18,0x18,0x3C,0x00}, /* i */ {0x06,0x00,0x0E,0x06,0x06,0x66,0x66,0x3C}, /* j */ {0x60,0x60,0x66,0x6C,0x78,0x6C,0x66,0x00}, /* k */ {0x38,0x18,0x18,0x18,0x18,0x18,0x3C,0x00}, /* l */ {0x00,0x00,0x66,0x7F,0x7F,0x6B,0x63,0x00}, /* m */ {0x00,0x00,0x7C,0x66,0x66,0x66,0x66,0x00}, /* n */ {0x00,0x00,0x3C,0x66,0x66,0x66,0x3C,0x00}, /* o */ {0x00,0x00,0x7C,0x66,0x66,0x7C,0x60,0x60}, /* p */ {0x00,0x00,0x3E,0x66,0x66,0x3E,0x06,0x06}, /* q */ {0x00,0x00,0x7C,0x66,0x60,0x60,0x60,0x00}, /* r */ {0x00,0x00,0x3E,0x60,0x3C,0x06,0x7C,0x00}, /* s */ {0x18,0x18,0x7E,0x18,0x18,0x18,0x0E,0x00}, /* t */ {0x00,0x00,0x66,0x66,0x66,0x66,0x3B,0x00}, /* u */ {0x00,0x00,0x66,0x66,0x66,0x3C,0x18,0x00}, /* v */ {0x00,0x00,0x63,0x6B,0x7F,0x3E,0x36,0x00}, /* w */ {0x00,0x00,0x66,0x3C,0x18,0x3C,0x66,0x00}, /* x */ {0x00,0x00,0x66,0x66,0x66,0x3E,0x06,0x3C}, /* y */ {0x00,0x00,0x7E,0x0C,0x18,0x30,0x7E,0x00}, /* z */ {0x0E,0x18,0x18,0x70,0x18,0x18,0x0E,0x00}, /* { */ {0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x00}, /* | */ {0x70,0x18,0x18,0x0E,0x18,0x18,0x70,0x00}, /* } */ {0x76,0xDC,0x00,0x00,0x00,0x00,0x00,0x00} /* ~ */ }; #define MSG_STATS_READY 100 typedef enum prompt_mode { PROMPT_NONE = 0, PROMPT_OPEN, PROMPT_SAVE, PROMPT_FIND, } prompt_mode; struct doc_stats { int words; int chars; }; struct undo_snapshot { char lines[MAX_LINES][MAX_LINE_LEN]; int line_count; int cursor_row; int cursor_col; }; struct editor { struct yasl_display *display; struct yasl_surface *surface; /* File tracking */ char file_path[512]; bool has_file; bool is_modified; /* Text buffer */ char lines[MAX_LINES][MAX_LINE_LEN]; int line_count; /* Cursor & Selection */ int cursor_row; int cursor_col; int sel_anchor_row; int sel_anchor_col; bool has_selection; bool mouse_dragging; /* Viewport */ int scroll_y; bool caret_visible; /* Interactive mini-buffer prompt */ prompt_mode prompt; char prompt_buf[256]; int prompt_len; /* Notification message in status bar */ char status_msg[128]; uint64_t status_msg_time; /* Simple Undo stack */ struct undo_snapshot undo_stack[MAX_UNDO]; int undo_count; /* Background stats from async worker */ struct doc_stats stats; bool worker_running; bool worker_dirty; pthread_t worker_tid; pthread_mutex_t worker_mutex; pthread_cond_t worker_cond; }; static void editor_set_status(struct editor *ed, const char *msg) { snprintf(ed->status_msg, sizeof(ed->status_msg), "%s", msg); ed->status_msg_time = yasl_loop_now_ns(); } static void editor_update_title(struct editor *ed) { if (!ed->surface) return; const char *name = ed->has_file ? ed->file_path : "Untitled"; /* Extract filename component */ const char *slash = strrchr(name, '/'); if (slash) name = slash + 1; char title[540]; snprintf(title, sizeof(title), "%s%s - YASL Notepad", ed->is_modified ? "*" : "", name); yasl_surface_set_title(ed->surface, title); } static void editor_save_undo(struct editor *ed) { if (ed->undo_count >= MAX_UNDO) { memmove(&ed->undo_stack[0], &ed->undo_stack[1], sizeof(struct undo_snapshot) * (MAX_UNDO - 1)); ed->undo_count = MAX_UNDO - 1; } struct undo_snapshot *s = &ed->undo_stack[ed->undo_count++]; s->line_count = ed->line_count; s->cursor_row = ed->cursor_row; s->cursor_col = ed->cursor_col; for (int i = 0; i < ed->line_count; i++) { memcpy(s->lines[i], ed->lines[i], MAX_LINE_LEN); } } static void editor_undo(struct editor *ed) { if (ed->undo_count <= 0) { editor_set_status(ed, "Already at oldest change"); return; } struct undo_snapshot *s = &ed->undo_stack[--ed->undo_count]; ed->line_count = s->line_count; ed->cursor_row = s->cursor_row; ed->cursor_col = s->cursor_col; for (int i = 0; i < ed->line_count; i++) { memcpy(ed->lines[i], s->lines[i], MAX_LINE_LEN); } ed->has_selection = false; ed->is_modified = true; editor_update_title(ed); editor_set_status(ed, "Undo"); } static void editor_notify_worker(struct editor *ed) { pthread_mutex_lock(&ed->worker_mutex); ed->worker_dirty = true; pthread_cond_signal(&ed->worker_cond); pthread_mutex_unlock(&ed->worker_mutex); } static void *worker_thread(void *arg) { struct editor *ed = arg; while (ed->worker_running) { pthread_mutex_lock(&ed->worker_mutex); while (!ed->worker_dirty && ed->worker_running) { pthread_cond_wait(&ed->worker_cond, &ed->worker_mutex); } if (!ed->worker_running) { pthread_mutex_unlock(&ed->worker_mutex); break; } ed->worker_dirty = false; pthread_mutex_unlock(&ed->worker_mutex); usleep(30000); int word_count = 0; int char_count = 0; for (int i = 0; i < ed->line_count; i++) { const char *p = ed->lines[i]; bool in_word = false; while (*p) { char_count++; if (isspace((unsigned char)*p)) { in_word = false; } else if (!in_word) { in_word = true; word_count++; } p++; } } struct doc_stats *s = malloc(sizeof(*s)); if (s) { s->words = word_count; s->chars = char_count; yasl_post_event(ed->display, ed->surface, MSG_STATS_READY, s, sizeof(*s)); } } return NULL; } static uint64_t on_blink_timer(void *ud, yasl_timer_id id, uint64_t interval_ns) { (void)id; struct editor *ed = ud; ed->caret_visible = !ed->caret_visible; yasl_surface_request_frame(ed->surface); return interval_ns; } /* File I/O */ static bool editor_open_file(struct editor *ed, const char *path) { FILE *f = fopen(path, "r"); if (!f) { char err[128]; snprintf(err, sizeof(err), "Could not open: %s", path); editor_set_status(ed, err); return false; } ed->line_count = 0; char buf[MAX_LINE_LEN]; while (fgets(buf, sizeof(buf), f) && ed->line_count < MAX_LINES) { size_t len = strlen(buf); while (len > 0 && (buf[len - 1] == '\n' || buf[len - 1] == '\r')) { buf[--len] = '\0'; } snprintf(ed->lines[ed->line_count++], MAX_LINE_LEN, "%s", buf); } fclose(f); if (ed->line_count == 0) { ed->lines[0][0] = '\0'; ed->line_count = 1; } snprintf(ed->file_path, sizeof(ed->file_path), "%s", path); ed->has_file = true; ed->is_modified = false; ed->cursor_row = 0; ed->cursor_col = 0; ed->scroll_y = 0; ed->has_selection = false; ed->undo_count = 0; editor_update_title(ed); char msg[128]; snprintf(msg, sizeof(msg), "Opened %s (%d lines)", path, ed->line_count); editor_set_status(ed, msg); editor_notify_worker(ed); return true; } static bool editor_save_file(struct editor *ed, const char *path) { FILE *f = fopen(path, "w"); if (!f) { char err[128]; snprintf(err, sizeof(err), "Could not save: %s", path); editor_set_status(ed, err); return false; } for (int i = 0; i < ed->line_count; i++) { fprintf(f, "%s\n", ed->lines[i]); } fclose(f); snprintf(ed->file_path, sizeof(ed->file_path), "%s", path); ed->has_file = true; ed->is_modified = false; editor_update_title(ed); char msg[128]; snprintf(msg, sizeof(msg), "Saved to %s", path); editor_set_status(ed, msg); return true; } /* Decode file:// URL to filesystem path */ static void decode_uri_path(const char *uri, char *out, size_t max_out) { if (strncmp(uri, "file://", 7) == 0) { uri += 7; /* Handle file://localhost/... */ if (strncmp(uri, "localhost/", 10) == 0) { uri += 9; } } size_t out_len = 0; while (*uri && *uri != '\r' && *uri != '\n' && out_len < max_out - 1) { if (*uri == '%' && isxdigit((unsigned char)uri[1]) && isxdigit((unsigned char)uri[2])) { char hex[3] = {uri[1], uri[2], '\0'}; out[out_len++] = (char)strtol(hex, NULL, 16); uri += 3; } else { out[out_len++] = *uri++; } } out[out_len] = '\0'; } /* Selection helper */ static void get_selection_bounds(const struct editor *ed, int *r0, int *c0, int *r1, int *c1) { if (!ed->has_selection) { *r0 = *r1 = ed->cursor_row; *c0 = *c1 = ed->cursor_col; return; } if (ed->sel_anchor_row < ed->cursor_row || (ed->sel_anchor_row == ed->cursor_row && ed->sel_anchor_col <= ed->cursor_col)) { *r0 = ed->sel_anchor_row; *c0 = ed->sel_anchor_col; *r1 = ed->cursor_row; *c1 = ed->cursor_col; } else { *r0 = ed->cursor_row; *c0 = ed->cursor_col; *r1 = ed->sel_anchor_row; *c1 = ed->sel_anchor_col; } } static bool is_char_selected(const struct editor *ed, int r, int c) { if (!ed->has_selection) return false; int r0, c0, r1, c1; get_selection_bounds(ed, &r0, &c0, &r1, &c1); if (r < r0 || r > r1) return false; if (r == r0 && r == r1) return c >= c0 && c < c1; if (r == r0) return c >= c0; if (r == r1) return c < c1; return true; } static void editor_delete_selection(struct editor *ed) { if (!ed->has_selection) return; editor_save_undo(ed); int r0, c0, r1, c1; get_selection_bounds(ed, &r0, &c0, &r1, &c1); if (r0 == r1) { char *line = ed->lines[r0]; int len = (int)strlen(line); memmove(line + c0, line + c1, (size_t)(len - c1 + 1)); } else { char *first = ed->lines[r0]; char *last = ed->lines[r1]; char temp[MAX_LINE_LEN]; snprintf(temp, sizeof(temp), "%s", last + c1); snprintf(first + c0, MAX_LINE_LEN - c0, "%s", temp); int lines_deleted = r1 - r0; memmove(ed->lines[r0 + 1], ed->lines[r1 + 1], (size_t)(ed->line_count - r1 - 1) * MAX_LINE_LEN); ed->line_count -= lines_deleted; } ed->cursor_row = r0; ed->cursor_col = c0; ed->has_selection = false; ed->is_modified = true; editor_update_title(ed); editor_notify_worker(ed); } /* Clipboard operations */ static void editor_copy(struct editor *ed) { char *buf = malloc(MAX_LINES * MAX_LINE_LEN); if (!buf) return; buf[0] = '\0'; if (ed->has_selection) { int r0, c0, r1, c1; get_selection_bounds(ed, &r0, &c0, &r1, &c1); if (r0 == r1) { int len = c1 - c0; if (len > 0) { strncpy(buf, ed->lines[r0] + c0, (size_t)len); buf[len] = '\0'; } } else { strcat(buf, ed->lines[r0] + c0); strcat(buf, "\n"); for (int r = r0 + 1; r < r1; r++) { strcat(buf, ed->lines[r]); strcat(buf, "\n"); } strncat(buf, ed->lines[r1], (size_t)c1); } } else { /* Copy entire current line */ snprintf(buf, MAX_LINES * MAX_LINE_LEN, "%s\n", ed->lines[ed->cursor_row]); } yasl_display_copy(ed->display, YASL_CLIPBOARD, buf); free(buf); editor_set_status(ed, "Copied to clipboard"); } static void editor_cut(struct editor *ed) { editor_copy(ed); if (ed->has_selection) { editor_delete_selection(ed); } else { editor_save_undo(ed); if (ed->line_count > 1) { memmove(ed->lines[ed->cursor_row], ed->lines[ed->cursor_row + 1], (size_t)(ed->line_count - ed->cursor_row - 1) * MAX_LINE_LEN); ed->line_count--; if (ed->cursor_row >= ed->line_count) ed->cursor_row = ed->line_count - 1; ed->cursor_col = 0; ed->is_modified = true; editor_update_title(ed); } else { ed->lines[0][0] = '\0'; ed->cursor_col = 0; } } } /* Search / Find */ static void editor_find_next(struct editor *ed, const char *query) { if (!query || !*query) return; int qlen = (int)strlen(query); for (int i = 0; i < ed->line_count; i++) { int r = (ed->cursor_row + i) % ed->line_count; const char *line = ed->lines[r]; int start_col = (r == ed->cursor_row) ? ed->cursor_col + 1 : 0; if (start_col < (int)strlen(line)) { const char *match = strstr(line + start_col, query); if (match) { int c = (int)(match - line); ed->cursor_row = r; ed->cursor_col = c + qlen; ed->sel_anchor_row = r; ed->sel_anchor_col = c; ed->has_selection = true; editor_set_status(ed, "Found match"); return; } } } editor_set_status(ed, "Not found"); } /* Drawing primitives */ static void fill_rect(uint32_t *pixels, int w, int h, int rx, int ry, int rw, int rh, uint32_t col) { int x0 = rx < 0 ? 0 : rx; int y0 = ry < 0 ? 0 : ry; int x1 = rx + rw > w ? w : rx + rw; int y1 = ry + rh > h ? h : ry + rh; for (int y = y0; y < y1; y++) { uint32_t *row = pixels + (size_t)y * w; for (int x = x0; x < x1; x++) { row[x] = col; } } } static void draw_char(uint32_t *pixels, int w, int h, int px, int py, char c, uint32_t col, int scale) { if (c < 32 || c > 126) c = ' '; const uint8_t *glyph = font8x8[c - 32]; for (int y = 0; y < 8; y++) { uint8_t row = glyph[y]; for (int x = 0; x < 8; x++) { if (row & (0x80 >> x)) { fill_rect(pixels, w, h, px + x * scale, py + y * scale, scale, scale, col); } } } } static void draw_string(uint32_t *pixels, int w, int h, int px, int py, const char *str, uint32_t col, int scale) { while (*str) { draw_char(pixels, w, h, px, py, *str, col, scale); px += 8 * scale; str++; } } /* Editing operations */ static void insert_char_at_cursor(struct editor *ed, char c) { char *line = ed->lines[ed->cursor_row]; int len = (int)strlen(line); if (len >= MAX_LINE_LEN - 2) return; memmove(line + ed->cursor_col + 1, line + ed->cursor_col, (size_t)(len - ed->cursor_col + 1)); line[ed->cursor_col] = c; ed->cursor_col++; ed->is_modified = true; ed->caret_visible = true; editor_update_title(ed); editor_notify_worker(ed); } static void insert_text(struct editor *ed, const char *text, size_t len) { if (ed->has_selection) { editor_delete_selection(ed); } editor_save_undo(ed); for (size_t i = 0; i < len; i++) { if (text[i] == '\n') { if (ed->line_count >= MAX_LINES - 1) break; char *curr = ed->lines[ed->cursor_row]; char remainder[MAX_LINE_LEN]; snprintf(remainder, sizeof(remainder), "%s", curr + ed->cursor_col); curr[ed->cursor_col] = '\0'; memmove(ed->lines[ed->cursor_row + 2], ed->lines[ed->cursor_row + 1], (size_t)(ed->line_count - ed->cursor_row - 1) * MAX_LINE_LEN); ed->line_count++; snprintf(ed->lines[ed->cursor_row + 1], MAX_LINE_LEN, "%s", remainder); ed->cursor_row++; ed->cursor_col = 0; } else if (text[i] == '\t') { /* Tab inserts 4 spaces */ for (int k = 0; k < 4; k++) insert_char_at_cursor(ed, ' '); } else if (text[i] >= 32 && text[i] <= 126) { insert_char_at_cursor(ed, text[i]); } } ed->is_modified = true; ed->caret_visible = true; editor_update_title(ed); editor_notify_worker(ed); } static void handle_tab(struct editor *ed, bool unindent) { editor_save_undo(ed); if (!unindent) { /* Indent: insert 4 spaces */ if (!ed->has_selection) { insert_text(ed, " ", 4); } else { int r0, c0, r1, c1; get_selection_bounds(ed, &r0, &c0, &r1, &c1); for (int r = r0; r <= r1; r++) { char *line = ed->lines[r]; if (strlen(line) + 4 < MAX_LINE_LEN) { memmove(line + 4, line, strlen(line) + 1); memcpy(line, " ", 4); } } ed->cursor_col += 4; ed->sel_anchor_col += 4; ed->is_modified = true; editor_update_title(ed); } } else { /* Unindent: remove up to 4 leading spaces */ int r0 = ed->cursor_row, r1 = ed->cursor_row; if (ed->has_selection) { int c0, c1; get_selection_bounds(ed, &r0, &c0, &r1, &c1); } for (int r = r0; r <= r1; r++) { char *line = ed->lines[r]; int spaces = 0; while (spaces < 4 && line[spaces] == ' ') spaces++; if (spaces > 0) { memmove(line, line + spaces, strlen(line) - spaces + 1); if (r == ed->cursor_row && ed->cursor_col >= spaces) ed->cursor_col -= spaces; ed->is_modified = true; editor_update_title(ed); } } } } static void handle_backspace(struct editor *ed) { if (ed->has_selection) { editor_delete_selection(ed); return; } editor_save_undo(ed); char *line = ed->lines[ed->cursor_row]; if (ed->cursor_col > 0) { int len = (int)strlen(line); memmove(line + ed->cursor_col - 1, line + ed->cursor_col, (size_t)(len - ed->cursor_col + 1)); ed->cursor_col--; } else if (ed->cursor_row > 0) { char *prev = ed->lines[ed->cursor_row - 1]; int prev_len = (int)strlen(prev); int curr_len = (int)strlen(line); if (prev_len + curr_len < MAX_LINE_LEN - 1) { strncat(prev, line, (size_t)(MAX_LINE_LEN - prev_len - 1)); memmove(ed->lines[ed->cursor_row], ed->lines[ed->cursor_row + 1], (size_t)(ed->line_count - ed->cursor_row - 1) * MAX_LINE_LEN); ed->line_count--; ed->cursor_row--; ed->cursor_col = prev_len; } } ed->is_modified = true; ed->caret_visible = true; editor_update_title(ed); editor_notify_worker(ed); } static void handle_delete(struct editor *ed) { if (ed->has_selection) { editor_delete_selection(ed); return; } editor_save_undo(ed); char *line = ed->lines[ed->cursor_row]; int len = (int)strlen(line); if (ed->cursor_col < len) { memmove(line + ed->cursor_col, line + ed->cursor_col + 1, (size_t)(len - ed->cursor_col)); } else if (ed->cursor_row < ed->line_count - 1) { char *next = ed->lines[ed->cursor_row + 1]; int next_len = (int)strlen(next); if (len + next_len < MAX_LINE_LEN - 1) { strncat(line, next, (size_t)(MAX_LINE_LEN - len - 1)); memmove(ed->lines[ed->cursor_row + 1], ed->lines[ed->cursor_row + 2], (size_t)(ed->line_count - ed->cursor_row - 2) * MAX_LINE_LEN); ed->line_count--; } } ed->is_modified = true; ed->caret_visible = true; editor_update_title(ed); editor_notify_worker(ed); } static void handle_enter(struct editor *ed) { if (ed->has_selection) { editor_delete_selection(ed); } editor_save_undo(ed); if (ed->line_count >= MAX_LINES - 1) return; char *curr = ed->lines[ed->cursor_row]; char remainder[MAX_LINE_LEN]; snprintf(remainder, sizeof(remainder), "%s", curr + ed->cursor_col); curr[ed->cursor_col] = '\0'; memmove(ed->lines[ed->cursor_row + 2], ed->lines[ed->cursor_row + 1], (size_t)(ed->line_count - ed->cursor_row - 1) * MAX_LINE_LEN); ed->line_count++; snprintf(ed->lines[ed->cursor_row + 1], MAX_LINE_LEN, "%s", remainder); ed->cursor_row++; ed->cursor_col = 0; ed->is_modified = true; ed->caret_visible = true; editor_update_title(ed); editor_notify_worker(ed); } /* Prompt actions */ static void prompt_execute(struct editor *ed) { switch (ed->prompt) { case PROMPT_OPEN: editor_open_file(ed, ed->prompt_buf); break; case PROMPT_SAVE: editor_save_file(ed, ed->prompt_buf); break; case PROMPT_FIND: editor_find_next(ed, ed->prompt_buf); break; default: break; } ed->prompt = PROMPT_NONE; } /* YASL App callbacks */ static int on_init(void *ud, struct yasl_display *d) { struct editor *ed = ud; ed->display = d; struct yasl_surface_spec spec = { .title = "Untitled - YASL Notepad", .size = {900, 600}, }; ed->surface = yasl_surface_create_toplevel(d, &spec); if (!ed->surface) return YASL_APP_QUIT; yasl_surface_set_pacing(ed->surface, YASL_PACING_ON_DEMAND); yasl_app_add_timer(d, YASL_MS_TO_NS(500), on_blink_timer, ed); ed->worker_running = true; pthread_mutex_init(&ed->worker_mutex, NULL); pthread_cond_init(&ed->worker_cond, NULL); pthread_create(&ed->worker_tid, NULL, worker_thread, ed); if (ed->has_file) { editor_open_file(ed, ed->file_path); } else { editor_update_title(ed); } editor_notify_worker(ed); yasl_surface_request_frame(ed->surface); return YASL_APP_CONTINUE; } static int on_event(void *ud, const struct yasl_event *ev) { struct editor *ed = ud; switch (ev->type) { case YASL_EVENT_TEXT: if (ed->prompt != PROMPT_NONE) { for (size_t i = 0; i < ev->text.len && ed->prompt_len < (int)sizeof(ed->prompt_buf) - 1; i++) { if (ev->text.text[i] >= 32 && ev->text.text[i] <= 126) { ed->prompt_buf[ed->prompt_len++] = ev->text.text[i]; ed->prompt_buf[ed->prompt_len] = '\0'; } } } else { insert_text(ed, ev->text.text, ev->text.len); } yasl_surface_request_frame(ed->surface); break; case YASL_EVENT_PASTE: if (ev->paste.text && ev->paste.len > 0) { insert_text(ed, ev->paste.text, ev->paste.len); yasl_surface_request_frame(ed->surface); } break; case YASL_EVENT_DROP_FILES: if (ev->drop_files.files && ev->drop_files.len > 0) { char path[512]; decode_uri_path(ev->drop_files.files, path, sizeof(path)); if (path[0]) { editor_open_file(ed, path); yasl_surface_request_frame(ed->surface); } } break; case YASL_EVENT_KEY: if (ev->key.action == YASL_KEY_PRESS || ev->key.action == YASL_KEY_REPEAT) { bool ctrl = (ev->key.mods & (YASL_MOD_CTRL | YASL_MOD_SUPER)) != 0; bool shift = (ev->key.mods & YASL_MOD_SHIFT) != 0; /* Interactive Prompt Mode */ if (ed->prompt != PROMPT_NONE) { if (ev->key.key == YASL_KEY_ESCAPE) { ed->prompt = PROMPT_NONE; } else if (ev->key.key == YASL_KEY_ENTER) { prompt_execute(ed); } else if (ev->key.key == YASL_KEY_BACKSPACE && ed->prompt_len > 0) { ed->prompt_buf[--ed->prompt_len] = '\0'; } yasl_surface_request_frame(ed->surface); break; } /* Shortcuts */ if (ctrl) { switch (ev->key.key) { case YASL_KEY_S: if (shift || !ed->has_file) { ed->prompt = PROMPT_SAVE; ed->prompt_len = 0; ed->prompt_buf[0] = '\0'; } else { editor_save_file(ed, ed->file_path); } yasl_surface_request_frame(ed->surface); return YASL_APP_CONTINUE; case YASL_KEY_O: ed->prompt = PROMPT_OPEN; ed->prompt_len = 0; ed->prompt_buf[0] = '\0'; yasl_surface_request_frame(ed->surface); return YASL_APP_CONTINUE; case YASL_KEY_F: ed->prompt = PROMPT_FIND; ed->prompt_len = 0; ed->prompt_buf[0] = '\0'; yasl_surface_request_frame(ed->surface); return YASL_APP_CONTINUE; case YASL_KEY_A: ed->sel_anchor_row = 0; ed->sel_anchor_col = 0; ed->cursor_row = ed->line_count - 1; ed->cursor_col = (int)strlen(ed->lines[ed->line_count - 1]); ed->has_selection = true; yasl_surface_request_frame(ed->surface); return YASL_APP_CONTINUE; case YASL_KEY_C: editor_copy(ed); yasl_surface_request_frame(ed->surface); return YASL_APP_CONTINUE; case YASL_KEY_X: editor_cut(ed); yasl_surface_request_frame(ed->surface); return YASL_APP_CONTINUE; case YASL_KEY_V: yasl_display_paste(ed->display, YASL_CLIPBOARD, ed->surface); return YASL_APP_CONTINUE; case YASL_KEY_Z: editor_undo(ed); yasl_surface_request_frame(ed->surface); return YASL_APP_CONTINUE; default: break; } } /* Tab key */ if (ev->key.key == YASL_KEY_TAB) { handle_tab(ed, shift); yasl_surface_request_frame(ed->surface); break; } /* Navigation with optional Shift selection */ bool was_selecting = ed->has_selection; if (shift && !was_selecting) { ed->sel_anchor_row = ed->cursor_row; ed->sel_anchor_col = ed->cursor_col; ed->has_selection = true; } switch (ev->key.key) { case YASL_KEY_BACKSPACE: handle_backspace(ed); break; case YASL_KEY_DELETE: handle_delete(ed); break; case YASL_KEY_ENTER: handle_enter(ed); break; case YASL_KEY_LEFT: if (ed->cursor_col > 0) { ed->cursor_col--; } else if (ed->cursor_row > 0) { ed->cursor_row--; ed->cursor_col = (int)strlen(ed->lines[ed->cursor_row]); } if (!shift) ed->has_selection = false; break; case YASL_KEY_RIGHT: { int len = (int)strlen(ed->lines[ed->cursor_row]); if (ed->cursor_col < len) { ed->cursor_col++; } else if (ed->cursor_row < ed->line_count - 1) { ed->cursor_row++; ed->cursor_col = 0; } if (!shift) ed->has_selection = false; break; } case YASL_KEY_UP: if (ed->cursor_row > 0) { ed->cursor_row--; int len = (int)strlen(ed->lines[ed->cursor_row]); if (ed->cursor_col > len) ed->cursor_col = len; } if (!shift) ed->has_selection = false; break; case YASL_KEY_DOWN: if (ed->cursor_row < ed->line_count - 1) { ed->cursor_row++; int len = (int)strlen(ed->lines[ed->cursor_row]); if (ed->cursor_col > len) ed->cursor_col = len; } if (!shift) ed->has_selection = false; break; case YASL_KEY_HOME: ed->cursor_col = 0; if (!shift) ed->has_selection = false; break; case YASL_KEY_END: ed->cursor_col = (int)strlen(ed->lines[ed->cursor_row]); if (!shift) ed->has_selection = false; break; case YASL_KEY_ESCAPE: ed->has_selection = false; break; default: break; } ed->caret_visible = true; yasl_surface_request_frame(ed->surface); } break; case YASL_EVENT_MOUSE_BUTTON: { struct yasl_geometry geom = yasl_surface_geometry(ed->surface); int scale = geom.scale_120 >= 240 ? 2 : 1; int char_w = 8 * scale; int line_h = 16 * scale; int gutter_w = 48 * scale; int header_h = 24 * scale; int row = (ev->mouse_button.pos.y - header_h) / line_h + ed->scroll_y; int col = (ev->mouse_button.pos.x - gutter_w) / char_w; if (row < 0) row = 0; if (row >= ed->line_count) row = ed->line_count - 1; int len = (int)strlen(ed->lines[row]); if (col < 0) col = 0; if (col > len) col = len; if (ev->mouse_button.pressed && ev->mouse_button.button == 0) { ed->cursor_row = row; ed->cursor_col = col; ed->sel_anchor_row = row; ed->sel_anchor_col = col; ed->has_selection = false; ed->mouse_dragging = true; ed->caret_visible = true; yasl_surface_request_frame(ed->surface); } else if (!ev->mouse_button.pressed && ev->mouse_button.button == 0) { ed->mouse_dragging = false; } break; } case YASL_EVENT_MOUSE_MOTION: if (ed->mouse_dragging) { struct yasl_geometry geom = yasl_surface_geometry(ed->surface); int scale = geom.scale_120 >= 240 ? 2 : 1; int char_w = 8 * scale; int line_h = 16 * scale; int gutter_w = 48 * scale; int header_h = 24 * scale; int row = (ev->mouse_motion.pos.y - header_h) / line_h + ed->scroll_y; int col = (ev->mouse_motion.pos.x - gutter_w) / char_w; if (row < 0) row = 0; if (row >= ed->line_count) row = ed->line_count - 1; int len = (int)strlen(ed->lines[row]); if (col < 0) col = 0; if (col > len) col = len; ed->cursor_row = row; ed->cursor_col = col; ed->has_selection = (ed->cursor_row != ed->sel_anchor_row || ed->cursor_col != ed->sel_anchor_col); yasl_surface_request_frame(ed->surface); } break; case YASL_EVENT_MOUSE_SCROLL: ed->scroll_y -= (int)(ev->mouse_scroll.scroll.y * 3.0); if (ed->scroll_y < 0) ed->scroll_y = 0; if (ed->scroll_y >= ed->line_count) ed->scroll_y = ed->line_count - 1; yasl_surface_request_frame(ed->surface); break; case YASL_EVENT_USER: if (ev->user.type == MSG_STATS_READY && ev->user.payload) { struct doc_stats *s = (struct doc_stats *)ev->user.payload; ed->stats = *s; free(s); yasl_surface_request_frame(ed->surface); } break; case YASL_EVENT_WINDOW_CLOSE_REQUEST: return YASL_APP_QUIT; default: break; } return YASL_APP_CONTINUE; } static void on_frame(void *ud, struct yasl_surface *s, struct yasl_frame_context *ctx) { (void)s; struct editor *ed = ud; int w = ctx->size.x; int h = ctx->size.y; uint32_t *pix = ctx->cpu.pixels; if (!pix || w <= 0 || h <= 0) return; int scale = ctx->scale_120 >= 240 ? 2 : 1; int char_w = 8 * scale; int line_h = 16 * scale; int gutter_w = 48 * scale; int header_h = 24 * scale; int status_h = 24 * scale; /* Theme colors */ uint32_t bg_main = 0xff1e1e2e; uint32_t bg_header = 0xff181825; uint32_t bg_gutter = 0xff181825; uint32_t bg_active_line = 0xff252538; uint32_t bg_selection = 0xff3b4261; uint32_t bg_status = 0xff11111b; uint32_t bg_prompt = 0xff2a2b3d; uint32_t text_fg = 0xffcdd6f4; uint32_t text_gutter = 0xff6c7086; uint32_t text_header = 0xff89b4fa; uint32_t text_status = 0xffa6adc8; uint32_t caret_col = 0xfff38ba8; uint32_t status_accent = 0xffa6e3a1; /* 1. Clear background */ fill_rect(pix, w, h, 0, 0, w, h, bg_main); /* 2. Top Header Menu bar */ fill_rect(pix, w, h, 0, 0, w, header_h, bg_header); char header_str[128]; snprintf(header_str, sizeof(header_str), "^S Save ^O Open ^F Find ^C Copy ^V Paste ^Z Undo Tab 4-Spaces"); draw_string(pix, w, h, 12 * scale, 6 * scale, header_str, text_header, scale); /* 3. Gutter background */ fill_rect(pix, w, h, 0, header_h, gutter_w, h - header_h - status_h, bg_gutter); /* 4. Ensure cursor is in visible bounds */ int visible_lines = (h - header_h - status_h) / line_h; if (ed->cursor_row < ed->scroll_y) { ed->scroll_y = ed->cursor_row; } else if (ed->cursor_row >= ed->scroll_y + visible_lines) { ed->scroll_y = ed->cursor_row - visible_lines + 1; } /* 5. Active line highlight */ int active_line_y = header_h + (ed->cursor_row - ed->scroll_y) * line_h; if (active_line_y >= header_h && active_line_y < h - status_h) { fill_rect(pix, w, h, gutter_w, active_line_y, w - gutter_w, line_h, bg_active_line); } /* 6. Render lines, gutter, and selection */ for (int i = 0; i < visible_lines; i++) { int line_idx = ed->scroll_y + i; if (line_idx >= ed->line_count) break; int y = header_h + i * line_h + 4 * scale; /* Line number */ char num_buf[16]; snprintf(num_buf, sizeof(num_buf), "%3d", line_idx + 1); draw_string(pix, w, h, 8 * scale, y, num_buf, text_gutter, scale); /* Text and selection highlighting */ const char *line_text = ed->lines[line_idx]; int text_x = gutter_w + 8 * scale; int col = 0; while (line_text[col]) { int char_x = text_x + col * char_w; if (is_char_selected(ed, line_idx, col)) { fill_rect(pix, w, h, char_x, header_h + i * line_h, char_w, line_h, bg_selection); } draw_char(pix, w, h, char_x, y, line_text[col], text_fg, scale); col++; } } /* 7. Caret */ if (ed->caret_visible && active_line_y >= header_h && active_line_y < h - status_h) { int caret_x = gutter_w + 8 * scale + ed->cursor_col * char_w; int caret_y = active_line_y + 3 * scale; fill_rect(pix, w, h, caret_x, caret_y, 2 * scale, 10 * scale, caret_col); } /* 8. Bottom Status bar or Mini-buffer Prompt */ int status_y = h - status_h; if (ed->prompt != PROMPT_NONE) { fill_rect(pix, w, h, 0, status_y, w, status_h, bg_prompt); const char *prompt_label = ed->prompt == PROMPT_OPEN ? "Open File: " : ed->prompt == PROMPT_SAVE ? "Save As: " : "Find: "; char prompt_disp[300]; snprintf(prompt_disp, sizeof(prompt_disp), "%s%s_", prompt_label, ed->prompt_buf); draw_string(pix, w, h, 12 * scale, status_y + 6 * scale, prompt_disp, 0xfffab387, scale); } else { fill_rect(pix, w, h, 0, status_y, w, status_h, bg_status); char status_left[160]; if (ed->status_msg[0] && (yasl_loop_now_ns() - ed->status_msg_time < 3000000000ull)) { snprintf(status_left, sizeof(status_left), "%s", ed->status_msg); } else { snprintf(status_left, sizeof(status_left), "Ln %d, Col %d Lines: %d Words: %d Chars: %d", ed->cursor_row + 1, ed->cursor_col + 1, ed->line_count, ed->stats.words, ed->stats.chars); } draw_string(pix, w, h, 12 * scale, status_y + 6 * scale, status_left, text_status, scale); char status_right[64]; snprintf(status_right, sizeof(status_right), "%s [0%% CPU Idle]", ed->is_modified ? "[Modified]" : "[Saved]"); int right_w = (int)strlen(status_right) * char_w; draw_string(pix, w, h, w - right_w - 12 * scale, status_y + 6 * scale, status_right, ed->is_modified ? 0xfffab387 : status_accent, scale); } yasl_frame_present_cpu(ctx, NULL, 0); } static void on_quit(void *ud, struct yasl_display *d) { (void)d; struct editor *ed = ud; ed->worker_running = false; pthread_mutex_lock(&ed->worker_mutex); pthread_cond_signal(&ed->worker_cond); pthread_mutex_unlock(&ed->worker_mutex); pthread_join(ed->worker_tid, NULL); pthread_mutex_destroy(&ed->worker_mutex); pthread_cond_destroy(&ed->worker_cond); } int main(int argc, char **argv) { static struct editor ed; /* Check command-line file argument */ if (argc > 1) { snprintf(ed.file_path, sizeof(ed.file_path), "%s", argv[1]); ed.has_file = true; } else { const char *initial_content[] = { "// Welcome to YASL Notepad!", "// A full-featured desktop text editor built on YASL Approach A.", "", "#include ", "", "int main(int argc, char **argv) {", " printf(\"Hello, modern YASL!\\n\");", " return 0;", "}", "", "Shortcuts & Features:", " - Open File: Ctrl+O (or drag-and-drop file here)", " - Save / SaveAs: Ctrl+S", " - Find / Search: Ctrl+F", " - Copy / Cut: Ctrl+C / Ctrl+X", " - Paste: Ctrl+V (using native clipboard API)", " - Undo: Ctrl+Z", " - Select All: Ctrl+A (or mouse click & drag)", " - Tab / Shift+Tab: Indent / unindent 4 spaces", "", "Try editing or opening a file!" }; ed.line_count = sizeof(initial_content) / sizeof(initial_content[0]); for (int i = 0; i < ed.line_count; i++) { snprintf(ed.lines[i], MAX_LINE_LEN, "%s", initial_content[i]); } } struct yasl_app_desc desc = { .app_id = "org.yasl.notepad", .backend = YASL_BACKEND_AUTO, .userdata = &ed, .init = on_init, .event = on_event, .frame = on_frame, .quit = on_quit, }; return yasl_run(&desc); }