Files
nt-lite/code/user/apps/edit/edit.c
T

577 lines
17 KiB
C

// User application entry points for edit.
#include <stdint.h>
#include <stddef.h>
#include <stdbool.h>
#include <string.h>
#include <ntlite/syscall.h>
#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;
}