// User application entry points for edit. #include #include #include #include #include #define GLYPH_W 8 #define GLYPH_H 16 #define MAX_LINES 512 #define MAX_LINE 256 #define PATH_MAX2 260 #define MSG_MAX 160 #define MAX_SCREEN_COLS 256 #define MAX_SCREEN_ROWS 128 #define COL_BG 0x000000u #define COL_FG 0xC8C8C8u #define COL_DIM 0x808080u #define COL_STATUS 0x303030u #define COL_STATUSF 0xFFFFFFu #define COL_CURSOR 0xE0E0E0u #define COL_CURSORF 0x000000u #define EDIT_KEY_ESC INPUT_KEY_ESCAPE #define EDIT_KEY_BACKSPACE INPUT_KEY_BACKSPACE #define EDIT_KEY_TAB INPUT_KEY_TAB #define EDIT_KEY_Q INPUT_KEY_Q #define EDIT_KEY_S INPUT_KEY_S #define EDIT_KEY_ENTER INPUT_KEY_ENTER #define EDIT_KEY_HOME INPUT_KEY_HOME #define EDIT_KEY_UP INPUT_KEY_UP #define EDIT_KEY_PGUP INPUT_KEY_PAGE_UP #define EDIT_KEY_LEFT INPUT_KEY_LEFT #define EDIT_KEY_RIGHT INPUT_KEY_RIGHT #define EDIT_KEY_END INPUT_KEY_END #define EDIT_KEY_DOWN INPUT_KEY_DOWN #define EDIT_KEY_PGDN INPUT_KEY_PAGE_DOWN #define EDIT_KEY_DEL INPUT_KEY_DELETE typedef struct LINE { int len; char text[MAX_LINE]; } LINE; static LINE gLines[MAX_LINES]; static int gLineCount = 1; static int gCx = 0; static int gCy = 0; static int gTop = 0; static int gLeft = 0; static int gCols = 80; static int gRows = 25; static bool gDirty = false; static bool gQuitArmed = false; static char gFilename[PATH_MAX2] = "untitled.txt"; static char gMessage[MSG_MAX] = "ready"; typedef struct SCREEN_CELL { char c; uint32_t fg; uint32_t bg; } SCREEN_CELL; static SCREEN_CELL gScreenCache[MAX_SCREEN_COLS * MAX_SCREEN_ROWS]; static bool gScreenCacheValid = false; // Implements the invalidate screen cache operation. static void invalidate_screen_cache(void) { gScreenCacheValid = false; } // Updates screen size. static void update_screen_size(void) { int pxW = 0, pxH = 0; int oldCols = gCols; int oldRows = gRows; if (SysGetScreenSize(&pxW, &pxH) == 0 && pxW > 0 && pxH > 0) { gCols = pxW / GLYPH_W; gRows = pxH / GLYPH_H; if (gCols < 20) gCols = 20; if (gRows < 5) gRows = 5; if (gCols > MAX_SCREEN_COLS) gCols = MAX_SCREEN_COLS; if (gRows > MAX_SCREEN_ROWS) gRows = MAX_SCREEN_ROWS; } if (gCols != oldCols || gRows != oldRows) invalidate_screen_cache(); } // Updates cell. static void put_cell(int col, int row, char c, uint32_t fg, uint32_t bg) { if (col < 0 || row < 0 || col >= gCols || row >= gRows) return; if (!c) c = ' '; if (col < MAX_SCREEN_COLS && row < MAX_SCREEN_ROWS) { SCREEN_CELL* cell = &gScreenCache[row * MAX_SCREEN_COLS + col]; if (gScreenCacheValid && cell->c == c && cell->fg == fg && cell->bg == bg) return; cell->c = c; cell->fg = fg; cell->bg = bg; } SysPutScreenChar(c, col * GLYPH_W, row * GLYPH_H, fg, bg); } // Clears row. static void clear_row(int row, uint32_t bg) { for (int x = 0; x < gCols; x++) put_cell(x, row, ' ', COL_FG, bg); } // Draws text. static void draw_text(int col, int row, const char* s, uint32_t fg, uint32_t bg) { while (*s && col < gCols) put_cell(col++, row, *s++, fg, bg); } // Implements the msg set operation. static void msg_set(const char* s) { size_t n = strlen(s); if (n >= sizeof(gMessage)) n = sizeof(gMessage) - 1; memcpy(gMessage, s, n); gMessage[n] = 0; } // Appends char. static void append_char(char* b, size_t cap, size_t* p, char c) { if (*p + 1 < cap) b[(*p)++] = c; b[*p < cap ? *p : cap - 1] = 0; } // Appends str. static void append_str(char* b, size_t cap, size_t* p, const char* s) { while (*s) append_char(b, cap, p, *s++); } // Appends u32. static void append_u32(char* b, size_t cap, size_t* p, uint32_t v) { char tmp[12]; int n = 0; if (v == 0) { append_char(b, cap, p, '0'); return; } while (v && n < (int)sizeof(tmp)) { tmp[n++] = (char)('0' + (v % 10u)); v /= 10u; } while (n > 0) append_char(b, cap, p, tmp[--n]); } // Implements the key to char operation. static char key_to_char(INPUT_KEY key, uint32_t mods) { if (key >= INPUT_KEY_A && key <= INPUT_KEY_Z) { bool shifted = (mods & INPUT_MOD_SHIFT) != 0; bool caps = (mods & INPUT_MOD_CAPS_LOCK) != 0; char c = (char)('a' + (key - INPUT_KEY_A)); return (shifted ^ caps) ? (char)(c - 'a' + 'A') : c; } typedef struct { INPUT_KEY key; char normal; char shifted; } PAIR; static const PAIR map[] = { { INPUT_KEY_1, '1', '!' }, { INPUT_KEY_2, '2', '@' }, { INPUT_KEY_3, '3', '#' }, { INPUT_KEY_4, '4', '$' }, { INPUT_KEY_5, '5', '%' }, { INPUT_KEY_6, '6', '^' }, { INPUT_KEY_7, '7', '&' }, { INPUT_KEY_8, '8', '*' }, { INPUT_KEY_9, '9', '(' }, { INPUT_KEY_0, '0', ')' }, { INPUT_KEY_MINUS, '-', '_' }, { INPUT_KEY_EQUAL, '=', '+' }, { INPUT_KEY_LEFT_BRACKET, '[', '{' }, { INPUT_KEY_RIGHT_BRACKET, ']', '}' }, { INPUT_KEY_BACKSLASH, '\\', '|' }, { INPUT_KEY_SEMICOLON, ';', ':' }, { INPUT_KEY_APOSTROPHE, '\'', '"' }, { INPUT_KEY_GRAVE, '`', '~' }, { INPUT_KEY_COMMA, ',', '<' }, { INPUT_KEY_PERIOD, '.', '>' }, { INPUT_KEY_SLASH, '/', '?' }, { INPUT_KEY_SPACE, ' ', ' ' }, }; bool shifted = (mods & INPUT_MOD_SHIFT) != 0; for (size_t i = 0; i < sizeof(map) / sizeof(map[0]); i++) { if (map[i].key == key) return shifted ? map[i].shifted : map[i].normal; } return 0; } // Implements the clamp cursor operation. static void clamp_cursor(void) { if (gLineCount < 1) gLineCount = 1; if (gCy < 0) gCy = 0; if (gCy >= gLineCount) gCy = gLineCount - 1; if (gCx < 0) gCx = 0; if (gCx > gLines[gCy].len) gCx = gLines[gCy].len; } // Implements the ensure visible operation. static void ensure_visible(void) { int textRows = gRows - 2; if (textRows < 1) textRows = 1; if (gCy < gTop) gTop = gCy; if (gCy >= gTop + textRows) gTop = gCy - textRows + 1; if (gTop < 0) gTop = 0; if (gCx < gLeft) gLeft = gCx; if (gCx >= gLeft + gCols) gLeft = gCx - gCols + 1; if (gLeft < 0) gLeft = 0; } // Implements the render operation. static void render(void) { update_screen_size(); clamp_cursor(); ensure_visible(); char status[256]; size_t p = 0; append_str(status, sizeof(status), &p, " edit "); append_str(status, sizeof(status), &p, gFilename); append_str(status, sizeof(status), &p, gDirty ? " * " : " "); append_str(status, sizeof(status), &p, "Ctrl+S save Ctrl+Q quit Ln "); append_u32(status, sizeof(status), &p, (uint32_t)(gCy + 1)); append_str(status, sizeof(status), &p, ", Col "); append_u32(status, sizeof(status), &p, (uint32_t)(gCx + 1)); size_t statusLen = p; for (int sx = 0; sx < gCols; sx++) { char c = ((size_t)sx < statusLen) ? status[sx] : ' '; put_cell(sx, 0, c, COL_STATUSF, COL_STATUS); } int textRows = gRows - 2; for (int sy = 0; sy < textRows; sy++) { int row = sy + 1; int li = gTop + sy; for (int sx = 0; sx < gCols; sx++) { int ci = gLeft + sx; char c = ' '; bool atCursor = false; if (li < gLineCount) { LINE* ln = &gLines[li]; c = (ci < ln->len) ? ln->text[ci] : ' '; atCursor = (li == gCy && ci == gCx); } put_cell(sx, row, c, atCursor ? COL_CURSORF : COL_FG, atCursor ? COL_CURSOR : COL_BG); } } size_t messageLen = strlen(gMessage); for (int sx = 0; sx < gCols; sx++) { char c = ((size_t)sx < messageLen) ? gMessage[sx] : ' '; put_cell(sx, gRows - 1, c, COL_STATUSF, COL_STATUS); } gScreenCacheValid = true; } // Inserts char at cursor. static void insert_char_at_cursor(char c) { LINE* ln = &gLines[gCy]; if (ln->len >= MAX_LINE - 1) { msg_set("line too long"); return; } memmove(&ln->text[gCx + 1], &ln->text[gCx], (size_t)(ln->len - gCx + 1)); ln->text[gCx] = c; ln->len++; gCx++; gDirty = true; } // Implements the newline at cursor operation. static void newline_at_cursor(void) { if (gLineCount >= MAX_LINES) { msg_set("too many lines"); return; } for (int i = gLineCount; i > gCy + 1; i--) gLines[i] = gLines[i - 1]; LINE* cur = &gLines[gCy]; LINE* next = &gLines[gCy + 1]; int tail = cur->len - gCx; if (tail > 0) memcpy(next->text, &cur->text[gCx], (size_t)tail); next->len = tail; next->text[next->len] = 0; cur->len = gCx; cur->text[cur->len] = 0; gLineCount++; gCy++; gCx = 0; gDirty = true; } // Implements the backspace at cursor operation. static void backspace_at_cursor(void) { if (gCx > 0) { LINE* ln = &gLines[gCy]; memmove(&ln->text[gCx - 1], &ln->text[gCx], (size_t)(ln->len - gCx + 1)); ln->len--; gCx--; gDirty = true; return; } if (gCy <= 0) return; LINE* prev = &gLines[gCy - 1]; LINE* cur = &gLines[gCy]; if (prev->len + cur->len >= MAX_LINE) { msg_set("join would make line too long"); return; } int oldLen = prev->len; memcpy(&prev->text[prev->len], cur->text, (size_t)cur->len + 1); prev->len += cur->len; for (int i = gCy; i < gLineCount - 1; i++) gLines[i] = gLines[i + 1]; gLineCount--; gCy--; gCx = oldLen; gDirty = true; } // Releases at cursor. static void delete_at_cursor(void) { LINE* ln = &gLines[gCy]; if (gCx < ln->len) { memmove(&ln->text[gCx], &ln->text[gCx + 1], (size_t)(ln->len - gCx)); ln->len--; gDirty = true; return; } if (gCy + 1 >= gLineCount) return; LINE* next = &gLines[gCy + 1]; if (ln->len + next->len >= MAX_LINE) { msg_set("join would make line too long"); return; } memcpy(&ln->text[ln->len], next->text, (size_t)next->len + 1); ln->len += next->len; for (int i = gCy + 1; i < gLineCount - 1; i++) gLines[i] = gLines[i + 1]; gLineCount--; gDirty = true; } // Moves left. static void move_left(void) { if (gCx > 0) gCx--; else if (gCy > 0) { gCy--; gCx = gLines[gCy].len; } } // Moves right. static void move_right(void) { if (gCx < gLines[gCy].len) gCx++; else if (gCy + 1 < gLineCount) { gCy++; gCx = 0; } } // Moves up. static void move_up(void) { if (gCy > 0) gCy--; if (gCx > gLines[gCy].len) gCx = gLines[gCy].len; } // Moves down. static void move_down(void) { if (gCy + 1 < gLineCount) gCy++; if (gCx > gLines[gCy].len) gCx = gLines[gCy].len; } // Updates file. static void save_file(void) { uint32_t h = 0; SYS_PATH_HANDLE_REQUEST pathReq = { SysSvFromCstr(gFilename), &h }; uint32_t st = SysCreateFile(&pathReq); if (st != 0) st = SysOpenFile(&pathReq); if (st != 0) { msg_set("save failed: cannot open file"); return; } SysTruncateFile(h, 0); SysSeekFile(h, 0); for (int i = 0; i < gLineCount; i++) { size_t written = 0; if (gLines[i].len) { SYS_FILE_IO_REQUEST writeReq = SysMakeFileIoRequest(h, gLines[i].text, (size_t)gLines[i].len, &written); st = SysWriteFile(&writeReq); if (st != 0) { SysCloseHandle(h); msg_set("save failed: write error"); return; } } if (i + 1 < gLineCount) { const char nl = '\n'; SYS_FILE_IO_REQUEST newlineReq = SysMakeFileIoRequest(h, &nl, 1, &written); st = SysWriteFile(&newlineReq); if (st != 0) { SysCloseHandle(h); msg_set("save failed: write error"); return; } } } SysCloseHandle(h); gDirty = false; msg_set("saved"); } // Implements the reset buffer operation. static void reset_buffer(void) { gLineCount = 1; gLines[0].len = 0; gLines[0].text[0] = 0; gCx = gCy = gTop = gLeft = 0; gDirty = false; } // Loads file. static void load_file(void) { reset_buffer(); uint32_t h = 0; SYS_PATH_HANDLE_REQUEST pathReq = { SysSvFromCstr(gFilename), &h }; if (SysOpenFile(&pathReq) != 0) { msg_set("new file"); return; } char buf[512]; size_t got = 0; bool truncated = false; for (;;) { SYS_FILE_IO_REQUEST readReq = SysMakeFileIoRequest(h, buf, sizeof(buf), &got); if (SysReadFile(&readReq) != 0 || got == 0) break; for (size_t i = 0; i < got; i++) { char c = buf[i]; if (c == '\r') continue; if (c == '\n') { if (gLineCount < MAX_LINES) { gLineCount++; gLines[gLineCount - 1].len = 0; gLines[gLineCount - 1].text[0] = 0; } else truncated = true; continue; } if (c < 0x20 && c != '\t') continue; LINE* ln = &gLines[gLineCount - 1]; if (ln->len + 1 < MAX_LINE) { ln->text[ln->len++] = (c == '\t') ? ' ' : c; ln->text[ln->len] = 0; } else truncated = true; } } SysCloseHandle(h); msg_set(truncated ? "loaded, truncated long file" : "loaded"); } // Implements the skip spaces operation. static const char* skip_spaces(const char* s) { while (s && (*s == ' ' || *s == '\t')) s++; return s; } // Implements the skip token operation. static const char* skip_token(const char* s) { s = skip_spaces(s); if (!s || !*s) return s; if (*s == '"') { s++; while (*s && *s != '"') s++; if (*s == '"') s++; return s; } while (*s && *s != ' ' && *s != '\t') s++; return s; } // Returns token. static bool read_token(const char* s, char* out, size_t cap) { s = skip_spaces(s); if (!s || !*s || cap == 0) return false; size_t n = 0; if (*s == '"') { s++; while (*s && *s != '"') { if (n + 1 < cap) out[n++] = *s; s++; } } else { while (*s && *s != ' ' && *s != '\t') { if (n + 1 < cap) out[n++] = *s; s++; } } out[n] = 0; return n != 0; } // Parses filename from cmdline. static void parse_filename_from_cmdline(void) { SYS_PEB* peb = SysGetPeb(); const char* cmd = (peb && peb->CommandLine.Data) ? peb->CommandLine.Data : NULL; const char* arg = skip_token(cmd); if (read_token(arg, gFilename, sizeof(gFilename))) return; strcpy(gFilename, "untitled.txt"); } // Implements the editor loop operation. static void editor_loop(void) { while (true) { render(); INPUT_KEY key = SysReadKey(); uint32_t mods = SysGetKeyModifiers(); bool ctrl = (mods & INPUT_MOD_CTRL) != 0; if (ctrl && key == EDIT_KEY_Q) { if (gDirty && !gQuitArmed) { gQuitArmed = true; msg_set("unsaved changes: press Ctrl+Q again to quit"); continue; } break; } gQuitArmed = false; if (ctrl && key == EDIT_KEY_S) { save_file(); continue; } if (key == EDIT_KEY_ESC) { msg_set("Ctrl+Q quits, Ctrl+S saves"); continue; } switch (key) { case EDIT_KEY_LEFT: move_left(); break; case EDIT_KEY_RIGHT: move_right(); break; case EDIT_KEY_UP: move_up(); break; case EDIT_KEY_DOWN: move_down(); break; case EDIT_KEY_HOME: gCx = 0; break; case EDIT_KEY_END: gCx = gLines[gCy].len; break; case EDIT_KEY_PGUP: { int n = gRows - 2; if (n < 1) n = 1; gCy -= n; clamp_cursor(); break; } case EDIT_KEY_PGDN: { int n = gRows - 2; if (n < 1) n = 1; gCy += n; clamp_cursor(); break; } case EDIT_KEY_BACKSPACE: backspace_at_cursor(); break; case EDIT_KEY_DEL: delete_at_cursor(); break; case EDIT_KEY_ENTER: newline_at_cursor(); break; case EDIT_KEY_TAB: for (int i = 0; i < 4; i++) insert_char_at_cursor(' '); break; default: { char c = key_to_char(key, mods); if (!ctrl && c >= 0x20 && c <= 0x7E) insert_char_at_cursor(c); break; } } } } // Prints the requested operation. static void print(const char* s) { SYS_WRITE_CONSOLE_REQUEST req = { SysSvFromCstr(s) }; SysWriteConsole(&req); } // Runs the requested operation. int main(void) { parse_filename_from_cmdline(); load_file(); SysClearScreen(COL_BG); invalidate_screen_cache(); editor_loop(); SysClearScreen(0x012456); invalidate_screen_cache(); print("edit: goodbye\n"); SysExitProcess(0); return 0; }