From 2f4cb88d5c60f725323739300bb49dfa8923e7d5 Mon Sep 17 00:00:00 2001 From: Albert Cervin Date: Wed, 2 Nov 2022 22:20:04 +0100 Subject: =?UTF-8?q?=F0=9F=8E=89=20And=20so=20it=20begins?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/binding.c | 57 +++++++++++ src/binding.h | 43 +++++++++ src/buffer.c | 192 ++++++++++++++++++++++++++++++++++++ src/buffer.h | 68 +++++++++++++ src/command.c | 66 +++++++++++++ src/command.h | 34 +++++++ src/display.c | 100 +++++++++++++++++++ src/display.h | 32 ++++++ src/keyboard.c | 56 +++++++++++ src/keyboard.h | 28 ++++++ src/main.c | 166 ++++++++++++++++++++++++++++++++ src/text.c | 299 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/text.h | 39 ++++++++ src/utf8.c | 42 ++++++++ src/utf8.h | 17 ++++ 15 files changed, 1239 insertions(+) create mode 100644 src/binding.c create mode 100644 src/binding.h create mode 100644 src/buffer.c create mode 100644 src/buffer.h create mode 100644 src/command.c create mode 100644 src/command.h create mode 100644 src/display.c create mode 100644 src/display.h create mode 100644 src/keyboard.c create mode 100644 src/keyboard.h create mode 100644 src/main.c create mode 100644 src/text.c create mode 100644 src/text.h create mode 100644 src/utf8.c create mode 100644 src/utf8.h (limited to 'src') diff --git a/src/binding.c b/src/binding.c new file mode 100644 index 0000000..953c0d8 --- /dev/null +++ b/src/binding.c @@ -0,0 +1,57 @@ +#include "binding.h" +#include "command.h" + +#include +#include + +struct keymap keymap_create(const char *name, uint32_t capacity) { + return (struct keymap){ + .name = name, + .bindings = calloc(capacity, sizeof(struct binding)), + .nbindings = 0, + .capacity = capacity, + }; +} + +void keymap_bind_keys(struct keymap *keymap, struct binding *bindings, + uint32_t nbindings) { + if (keymap->nbindings + nbindings >= keymap->capacity) { + keymap->capacity = + nbindings > keymap->capacity * 2 ? nbindings * 2 : keymap->capacity * 2; + keymap->bindings = + realloc(keymap->bindings, sizeof(struct binding) * keymap->capacity); + } + memcpy(keymap->bindings + keymap->nbindings, bindings, + sizeof(struct binding) * nbindings); + + keymap->nbindings += nbindings; +} + +void keymap_destroy(struct keymap *keymap) { + free(keymap->bindings); + keymap->bindings = 0; + keymap->capacity = 0; + keymap->nbindings = 0; +} + +struct command *lookup_key(struct keymap *keymaps, uint32_t nkeymaps, + struct key *key, struct commands *commands) { + // lookup in order in the keymaps + for (uint32_t kmi = 0; kmi < nkeymaps; ++kmi) { + struct keymap *keymap = &keymaps[kmi]; + + for (uint32_t bi = 0; bi < keymap->nbindings; ++bi) { + struct binding *binding = &keymap->bindings[bi]; + if (key->c == binding->key.c && key->mod == binding->key.mod) { + if (binding->type == BindingType_Command) { + return lookup_command_by_hash(commands, binding->command); + } else if (binding->type == BindingType_Keymap) { + // TODO + return NULL; + } + } + } + } + + return NULL; +} diff --git a/src/binding.h b/src/binding.h new file mode 100644 index 0000000..260a463 --- /dev/null +++ b/src/binding.h @@ -0,0 +1,43 @@ +#include "keyboard.h" + +struct keymap { + const char *name; + struct binding *bindings; + uint32_t nbindings; + uint32_t capacity; +}; + +enum binding_type { BindingType_Command, BindingType_Keymap }; + +#define BINDING(mod_, c_, command_) \ + (struct binding) { \ + .key = {.mod = mod_, .c = c_}, .type = BindingType_Command, \ + .command = hash_command_name(command_) \ + } + +#define PREFIX(mod_, c_, keymap_) \ + (struct binding) { \ + .key = {.mod = mod_, .c = c_}, .type = BindingType_Keymap, \ + .keymap = keymap_ \ + } + +struct binding { + struct key key; + + uint8_t type; + + union { + uint32_t command; + struct keymap *keymap; + }; +}; + +struct commands; + +struct keymap keymap_create(const char *name, uint32_t capacity); +void keymap_bind_keys(struct keymap *keymap, struct binding *bindings, + uint32_t nbindings); +void keymap_destroy(struct keymap *keymap); + +struct command *lookup_key(struct keymap *keymaps, uint32_t nkeymaps, + struct key *key, struct commands *commands); diff --git a/src/buffer.c b/src/buffer.c new file mode 100644 index 0000000..e08bca5 --- /dev/null +++ b/src/buffer.c @@ -0,0 +1,192 @@ +#include "buffer.h" +#include "binding.h" +#include "display.h" + +#include +#include +#include +#include +#include + +struct buffer buffer_create(const char *name) { + struct buffer b = + (struct buffer){.filename = NULL, + .name = name, + .text = text_create(10), + .dot_col = 0, + .dot_line = 0, + .modeline_buf = (uint8_t *)malloc(1024), + .keymaps = calloc(10, sizeof(struct keymap)), + .nkeymaps = 1, + .lines_rendered = -1, + .nkeymaps_max = 10}; + + b.keymaps[0] = keymap_create("buffer-default", 128); + struct binding bindings[] = { + BINDING(Ctrl, 'B', "backward-char"), + BINDING(Ctrl, 'F', "forward-char"), + + BINDING(Ctrl, 'P', "backward-line"), + BINDING(Ctrl, 'N', "forward-line"), + + BINDING(Ctrl, 'A', "beginning-of-line"), + BINDING(Ctrl, 'E', "end-of-line"), + + BINDING(Ctrl, 'M', "newline"), + + BINDING(Ctrl, '?', "backward-delete-char"), + }; + keymap_bind_keys(&b.keymaps[0], bindings, + sizeof(bindings) / sizeof(bindings[0])); + + return b; +} + +void buffer_destroy(struct buffer *buffer) { + free(buffer->modeline_buf); + text_destroy(buffer->text); + free(buffer->text); +} + +uint32_t buffer_keymaps(struct buffer *buffer, struct keymap **keymaps_out) { + *keymaps_out = buffer->keymaps; + return buffer->nkeymaps; +} + +void buffer_add_keymap(struct buffer *buffer, struct keymap *keymap) { + if (buffer->nkeymaps == buffer->nkeymaps_max) { + // TODO: better + return; + } + buffer->keymaps[buffer->nkeymaps] = *keymap; + ++buffer->nkeymaps; +} + +bool movev(struct buffer *buffer, int rowdelta) { + int64_t new_line = (int64_t)buffer->dot_line + rowdelta; + + if (new_line < 0) { + buffer->dot_line = 0; + return false; + } else if (new_line > text_num_lines(buffer->text) - 1) { + buffer->dot_line = text_num_lines(buffer->text) - 1; + return false; + } else { + buffer->dot_line = (uint32_t)new_line; + + // make sure column stays on the line + uint32_t linelen = text_line_length(buffer->text, buffer->dot_line); + buffer->dot_col = buffer->dot_col > linelen ? linelen : buffer->dot_col; + return true; + } +} + +// move dot `coldelta` chars +void moveh(struct buffer *buffer, int coldelta) { + int64_t new_col = (int64_t)buffer->dot_col + coldelta; + + if (new_col > (int64_t)text_line_length(buffer->text, buffer->dot_line)) { + if (movev(buffer, 1)) { + buffer->dot_col = 0; + } + } else if (new_col < 0) { + if (movev(buffer, -1)) { + buffer->dot_col = text_line_length(buffer->text, buffer->dot_line); + } + } else { + buffer->dot_col = new_col; + } +} + +void buffer_backward_delete_char(struct buffer *buffer) { + // TODO: merge lines + if (text_line_length(buffer->text, buffer->dot_line) == 0) { + text_delete_line(buffer->text, buffer->dot_line); + } else if (buffer->dot_col > 0) { + text_delete(buffer->text, buffer->dot_line, buffer->dot_col - 1, 1); + } + moveh(buffer, -1); +} + +void buffer_backward_char(struct buffer *buffer) { moveh(buffer, -1); } +void buffer_forward_char(struct buffer *buffer) { moveh(buffer, 1); } + +void buffer_backward_line(struct buffer *buffer) { movev(buffer, -1); } +void buffer_forward_line(struct buffer *buffer) { movev(buffer, 1); } + +void buffer_end_of_line(struct buffer *buffer) { + buffer->dot_col = text_line_length(buffer->text, buffer->dot_line); +} + +void buffer_beginning_of_line(struct buffer *buffer) { buffer->dot_col = 0; } + +struct buffer buffer_from_file(const char *filename) { + // TODO: create a reader for the file that calls add_text + return (struct buffer){.filename = filename, .name = filename}; +} + +int buffer_to_file(struct buffer *buffer) { return 0; } + +int buffer_add_text(struct buffer *buffer, uint8_t *text, uint32_t nbytes) { + uint32_t lines_added, cols_added; + text_append(buffer->text, buffer->dot_line, buffer->dot_col, text, nbytes, + &lines_added, &cols_added); + movev(buffer, lines_added); + moveh(buffer, cols_added); + + return lines_added; +} + +void buffer_newline(struct buffer *buffer) { + buffer_add_text(buffer, (uint8_t *)"\n", 1); +} + +bool modeline_update(struct buffer *buffer, uint32_t width) { + char buf[width * 4]; + + time_t now = time(NULL); + struct tm *lt = localtime(&now); + char left[128], right[128]; + snprintf(left, 128, "--- %-16s (%d, %d)", buffer->name, buffer->dot_line + 1, + buffer->dot_col); + snprintf(right, 128, "%02d:%02d", lt->tm_hour, lt->tm_min); + + snprintf(buf, width * 4, "\x1b[100m%s%*s%s\x1b[0m", left, + (int)(width - (strlen(left) + strlen(right))), "", right); + if (strcmp(buf, (char *)buffer->modeline_buf) != 0) { + buffer->modeline_buf = realloc(buffer->modeline_buf, width * 4); + strcpy((char *)buffer->modeline_buf, buf); + return true; + } else { + return false; + } +} + +struct buffer_update buffer_begin_frame(struct buffer *buffer, uint32_t width, + uint32_t height, alloc_fn frame_alloc) { + // reserve space for modeline + uint32_t bufheight = height - 1; + uint32_t nlines = + buffer->lines_rendered > bufheight ? bufheight : buffer->lines_rendered; + + struct render_cmd *cmds = + (struct render_cmd *)frame_alloc(sizeof(struct render_cmd) * (height)); + + uint32_t ncmds = text_render(buffer->text, 0, nlines, cmds, nlines); + + buffer->lines_rendered = text_num_lines(buffer->text); + + if (modeline_update(buffer, width)) { + cmds[ncmds] = (struct render_cmd){ + .col = 0, + .row = height - 1, + .data = buffer->modeline_buf, + .len = strlen((char *)buffer->modeline_buf), + }; + ++ncmds; + } + + return (struct buffer_update){.cmds = cmds, .ncmds = ncmds}; +} + +void buffer_end_frame(struct buffer *buffer, struct buffer_update *upd) {} diff --git a/src/buffer.h b/src/buffer.h new file mode 100644 index 0000000..1b73505 --- /dev/null +++ b/src/buffer.h @@ -0,0 +1,68 @@ +#include +#include + +#include "command.h" +#include "text.h" + +struct keymap; + +struct buffer { + const char *name; + const char *filename; + + struct text *text; + + uint32_t dot_line; + uint32_t dot_col; + + uint8_t *modeline_buf; + + // local keymaps + struct keymap *keymaps; + uint32_t nkeymaps; + uint32_t nkeymaps_max; + + uint32_t lines_rendered; +}; + +struct buffer_update { + struct render_cmd *cmds; + uint64_t ncmds; +}; + +typedef void *(alloc_fn)(size_t); + +struct buffer buffer_create(const char *name); +void buffer_destroy(struct buffer *buffer); + +uint32_t buffer_keymaps(struct buffer *buffer, struct keymap **keymaps_out); +void buffer_add_keymap(struct buffer *buffer, struct keymap *keymap); + +int buffer_add_text(struct buffer *buffer, uint8_t *text, uint32_t nbytes); + +void buffer_backward_delete_char(struct buffer *buffer); +void buffer_backward_char(struct buffer *buffer); +void buffer_forward_char(struct buffer *buffer); +void buffer_backward_line(struct buffer *buffer); +void buffer_forward_line(struct buffer *buffer); +void buffer_end_of_line(struct buffer *buffer); +void buffer_beginning_of_line(struct buffer *buffer); +void buffer_newline(struct buffer *buffer); + +struct buffer buffer_from_file(const char *filename); +int buffer_to_file(struct buffer *buffer); + +struct buffer_update buffer_begin_frame(struct buffer *buffer, uint32_t width, + uint32_t height, alloc_fn frame_alloc); +void buffer_end_frame(struct buffer *buffer, struct buffer_update *upd); + +static struct command BUFFER_COMMANDS[] = { + {.name = "backward-delete-char", .fn = buffer_backward_delete_char}, + {.name = "backward-char", .fn = buffer_backward_char}, + {.name = "forward-char", .fn = buffer_forward_char}, + {.name = "backward-line", .fn = buffer_backward_line}, + {.name = "forward-line", .fn = buffer_forward_line}, + {.name = "end-of-line", .fn = buffer_end_of_line}, + {.name = "beginning-of-line", .fn = buffer_beginning_of_line}, + {.name = "newline", .fn = buffer_newline}, +}; diff --git a/src/command.c b/src/command.c new file mode 100644 index 0000000..4b233b2 --- /dev/null +++ b/src/command.c @@ -0,0 +1,66 @@ +#include "command.h" + +#include + +struct commands command_list_create(uint32_t capacity) { + return (struct commands){ + .commands = calloc(capacity, sizeof(struct hashed_command)), + .ncommands = 0, + .capacity = capacity, + }; +} + +void command_list_destroy(struct commands *commands) { + free(commands->commands); + commands->ncommands = 0; + commands->capacity = 0; +} + +uint32_t hash_command_name(const char *name) { + unsigned long hash = 5381; + int c; + + while ((c = *name++)) + hash = ((hash << 5) + hash) + c; /* hash * 33 + c */ + + return hash; +} + +uint32_t register_command(struct commands *commands, struct command *command) { + if (commands->ncommands == commands->capacity) { + commands->capacity *= 2; + commands->commands = realloc( + commands->commands, sizeof(struct hashed_command) * commands->capacity); + } + + uint32_t hash = hash_command_name(command->name); + commands->commands[commands->ncommands] = + (struct hashed_command){.command = command, .hash = hash}; + + ++commands->ncommands; + return hash; +} + +void register_commands(struct commands *command_list, struct command *commands, + uint32_t ncommands) { + for (uint32_t ci = 0; ci < ncommands; ++ci) { + register_command(command_list, &commands[ci]); + } +} + +struct command *lookup_command(struct commands *command_list, + const char *name) { + uint32_t needle = hash_command_name(name); + return lookup_command_by_hash(command_list, needle); +} + +struct command *lookup_command_by_hash(struct commands *commands, + uint32_t hash) { + for (uint32_t ci = 0; ci < commands->ncommands; ++ci) { + if (commands->commands[ci].hash == hash) { + return commands->commands[ci].command; + } + } + + return NULL; +} diff --git a/src/command.h b/src/command.h new file mode 100644 index 0000000..9515282 --- /dev/null +++ b/src/command.h @@ -0,0 +1,34 @@ +#include + +struct buffer; + +typedef void (*command_fn)(struct buffer *buffer); + +struct command { + const char *name; + command_fn fn; +}; + +struct hashed_command { + uint32_t hash; + struct command *command; +}; + +struct commands { + struct hashed_command *commands; + uint32_t ncommands; + uint32_t capacity; +}; + +struct commands command_list_create(uint32_t capacity); +void command_list_destroy(struct commands *commands); + +uint32_t register_command(struct commands *commands, struct command *command); +void register_commands(struct commands *command_list, struct command *commands, + uint32_t ncommands); + +uint32_t hash_command_name(const char *name); + +struct command *lookup_command(struct commands *commands, const char *name); +struct command *lookup_command_by_hash(struct commands *commands, + uint32_t hash); diff --git a/src/display.c b/src/display.c new file mode 100644 index 0000000..b34cbf1 --- /dev/null +++ b/src/display.c @@ -0,0 +1,100 @@ +#define _DEFAULT_SOURCE +#include "display.h" + +#include "buffer.h" + +#include +#include +#include + +#define ESC 0x1b + +struct display display_create() { + + struct winsize ws; + if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) < 0) { + // TODO: if it fails to fetch, do something? + return (struct display){ + .height = 0, + .width = 0, + }; + } + + // save old settings + struct termios orig_term; + tcgetattr(0, &orig_term); + + // set terminal to raw mode + struct termios term; + cfmakeraw(&term); + // non-blocking input + // TODO: move to kbd? + term.c_cc[VMIN] = 0; + term.c_cc[VTIME] = 0; + + tcsetattr(0, TCSADRAIN, &term); + + return (struct display){ + .orig_term = orig_term, + .term = term, + .height = ws.ws_row, + .width = ws.ws_col, + }; +} + +void display_destroy(struct display *display) { + // reset old terminal mode + tcsetattr(0, TCSADRAIN, &display->orig_term); +} + +void putbytes(uint8_t *line_bytes, uint32_t line_length) { + fwrite(line_bytes, 1, line_length, stdout); +} + +void putbyte(uint8_t c) { putc(c, stdout); } + +void put_ansiparm(int n) { + int q = n / 10; + if (q != 0) { + int r = q / 10; + if (r != 0) { + putbyte((r % 10) + '0'); + } + putbyte((q % 10) + '0'); + } + putbyte((n % 10) + '0'); +} + +void display_move_cursor(struct display *display, uint32_t row, uint32_t col) { + putbyte(ESC); + putbyte('['); + put_ansiparm(row + 1); + putbyte(';'); + put_ansiparm(col + 1); + putbyte('H'); +} + +void display_clear(struct display *display) { + display_move_cursor(display, 0, 0); + uint8_t bytes[] = {ESC, '[', 'J'}; + putbytes(bytes, 3); +} + +void delete_to_eol() { + uint8_t bytes[] = {ESC, '[', 'K'}; + putbytes(bytes, 3); +} + +void display_update(struct display *display, struct render_cmd *cmds, + uint32_t ncmds, uint32_t currow, uint32_t curcol) { + for (uint64_t cmdi = 0; cmdi < ncmds; ++cmdi) { + struct render_cmd *cmd = &cmds[cmdi]; + display_move_cursor(display, cmd->row, cmd->col); + putbytes(cmd->data, cmd->len); + delete_to_eol(); + } + + display_move_cursor(display, currow, curcol); + + fflush(stdout); +} diff --git a/src/display.h b/src/display.h new file mode 100644 index 0000000..18200d7 --- /dev/null +++ b/src/display.h @@ -0,0 +1,32 @@ +#include + +#include + +struct display { + struct termios term; + struct termios orig_term; + uint32_t width; + uint32_t height; +}; + +struct render_cmd { + uint32_t col; + uint32_t row; + + uint8_t *data; + uint32_t len; +}; + +struct render_cmd_buf { + char source[16]; + struct render_cmd *cmds; + uint64_t ncmds; +}; + +struct display display_create(); +void display_destroy(struct display *display); + +void display_clear(struct display *display); +void display_move_cursor(struct display *display, uint32_t row, uint32_t col); +void display_update(struct display *display, struct render_cmd *cmds, + uint32_t ncmds, uint32_t currow, uint32_t curcol); diff --git a/src/keyboard.c b/src/keyboard.c new file mode 100644 index 0000000..2cf9e8c --- /dev/null +++ b/src/keyboard.c @@ -0,0 +1,56 @@ +#include "keyboard.h" + +#include +#include + +struct keyboard keyboard_create() { + // TODO: should input term stuff be set here? + return (struct keyboard){}; +} + +void parse_keys(uint8_t *bytes, uint32_t nbytes, struct key *out_keys, + uint32_t *out_nkeys) { + uint32_t nkps = 0; + for (uint32_t bytei = 0; bytei < nbytes; ++bytei) { + uint8_t b = bytes[bytei]; + + struct key *kp = &out_keys[nkps]; + + if (b == 0x1b) { // meta + kp->mod |= Meta; + } else if (b >= 0x00 && b <= 0x1f) { // ctrl char + kp->mod |= Ctrl; + kp->c = b | 0x40; + + } else if (b == 0x7f) { // ^? + kp->mod |= Ctrl; + kp->c = '?'; + } else { // normal char (or part of char) + kp->c = b; + } + + ++nkps; + } + + *out_nkeys = nkps; +} + +struct keyboard_update keyboard_begin_frame(struct keyboard *kbd) { + uint8_t bytes[32] = {0}; + int nbytes = read(STDIN_FILENO, bytes, 32); + + struct keyboard_update upd = + (struct keyboard_update){.keys = {0}, .nkeys = 0}; + + if (nbytes > 0) { + parse_keys(bytes, nbytes, upd.keys, &upd.nkeys); + } + + return upd; +} + +void keyboard_end_frame(struct keyboard *kbd) {} + +bool key_equal(struct key *key, uint8_t mod, uint8_t c) { + return key->c == c && key->mod == mod; +} diff --git a/src/keyboard.h b/src/keyboard.h new file mode 100644 index 0000000..439e60d --- /dev/null +++ b/src/keyboard.h @@ -0,0 +1,28 @@ +#include +#include + +enum modifiers { + Ctrl = 1 << 0, + Meta = 1 << 1, +}; + +// note that unicode chars are split over multiple keypresses +// TODO: make unicode chars nicer to deal with +struct key { + uint8_t c; + uint8_t mod; +}; + +struct keyboard {}; + +struct keyboard_update { + struct key keys[32]; + uint32_t nkeys; +}; + +struct keyboard keyboard_create(); + +struct keyboard_update keyboard_begin_frame(struct keyboard *kbd); +void keyboard_end_frame(struct keyboard *kbd); + +bool key_equal(struct key *key, uint8_t mod, uint8_t c); diff --git a/src/main.c b/src/main.c new file mode 100644 index 0000000..140b281 --- /dev/null +++ b/src/main.c @@ -0,0 +1,166 @@ +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "binding.h" +#include "buffer.h" +#include "display.h" + +struct reactor { + int epoll_fd; +}; + +struct reactor reactor_create(); +int reactor_register_interest(struct reactor *reactor, int fd); + +struct frame_allocator { + uint8_t *buf; + size_t offset; + size_t capacity; +}; + +struct frame_allocator frame_allocator_create(size_t capacity) { + return (struct frame_allocator){ + .capacity = capacity, .offset = 0, .buf = (uint8_t *)malloc(capacity)}; +} + +void *frame_allocator_alloc(struct frame_allocator *alloc, size_t sz) { + if (alloc->offset + sz > alloc->capacity) { + return NULL; + } + + void *mem = alloc->buf + alloc->offset; + alloc->offset += sz; + + return mem; +} + +void frame_allocator_clear(struct frame_allocator *alloc) { alloc->offset = 0; } + +struct frame_allocator frame_allocator; + +void *frame_alloc(size_t sz) { + return frame_allocator_alloc(&frame_allocator, sz); +} + +bool running = true; + +void terminate() { running = false; } + +void unimplemented_command(struct buffer *buffer) {} +void exit_editor(struct buffer *buffer) { terminate(); } + +static struct command GLOBAL_COMMANDS[] = { + {.name = "find-file", .fn = unimplemented_command}, + {.name = "exit", .fn = exit_editor}}; + +int main(int argc, char *argv[]) { + const char *filename = NULL; + if (argc >= 1) { + filename = argv[1]; + } + + setlocale(LC_ALL, ""); + signal(SIGTERM, terminate); + + frame_allocator = frame_allocator_create(1024 * 1024); + + // create reactor + struct reactor reactor = reactor_create(); + + // initialize display + struct display display = display_create(); + display_clear(&display); + + // init keyboard + struct keyboard kbd = keyboard_create(); + + // commands + struct commands commands = command_list_create(32); + register_commands(&commands, GLOBAL_COMMANDS, + sizeof(GLOBAL_COMMANDS) / sizeof(GLOBAL_COMMANDS[0])); + register_commands(&commands, BUFFER_COMMANDS, + sizeof(BUFFER_COMMANDS) / sizeof(BUFFER_COMMANDS[0])); + + // keymaps + struct keymap global_keymap = keymap_create("global", 32); + struct binding global_binds[] = { + BINDING(Ctrl, 'X', "exit"), + }; + keymap_bind_keys(&global_keymap, global_binds, + sizeof(global_binds) / sizeof(global_binds[0])); + + // TODO: load initial buffer + struct buffer curbuf = buffer_create("welcome"); + const char *welcome_txt = "Welcome to the editor for datagubbar 👴\n"; + buffer_add_text(&curbuf, (uint8_t *)welcome_txt, strlen(welcome_txt)); + + while (running) { + // update keyboard + struct keymap *local_keymaps = NULL; + uint32_t nbuffer_keymaps = buffer_keymaps(&curbuf, &local_keymaps); + struct keyboard_update kbd_upd = keyboard_begin_frame(&kbd); + for (uint32_t ki = 0; ki < kbd_upd.nkeys; ++ki) { + struct key *k = &kbd_upd.keys[ki]; + + // check first the global keymap, then the buffer ones + struct command *cmd = lookup_key(&global_keymap, 1, k, &commands); + if (cmd == NULL) { + cmd = lookup_key(local_keymaps, nbuffer_keymaps, k, &commands); + } + + if (cmd != NULL) { + cmd->fn(&curbuf); + } else { + buffer_add_text(&curbuf, &k->c, 1); + } + } + + // update current buffer + struct buffer_update buf_upd = buffer_begin_frame( + &curbuf, display.width, display.height - 1, frame_alloc); + + // update screen + if (buf_upd.ncmds > 0) { + display_update(&display, buf_upd.cmds, buf_upd.ncmds, curbuf.dot_line, + curbuf.dot_col); + } + + buffer_end_frame(&curbuf, &buf_upd); + frame_allocator_clear(&frame_allocator); + } + + display_clear(&display); + display_destroy(&display); + keymap_destroy(&global_keymap); + command_list_destroy(&commands); + + return 0; +} + +struct reactor reactor_create() { + int epollfd = epoll_create1(0); + if (epollfd == -1) { + perror("epoll_create1"); + } + + return (struct reactor){.epoll_fd = epollfd}; +} + +int reactor_register_interest(struct reactor *reactor, int fd) { + struct epoll_event ev; + ev.events = EPOLLIN; + ev.data.fd = fd; + if (epoll_ctl(reactor->epoll_fd, EPOLL_CTL_ADD, fd, &ev) < 0) { + perror("epoll_ctl"); + return -1; + } + + return fd; +} diff --git a/src/text.c b/src/text.c new file mode 100644 index 0000000..4162e94 --- /dev/null +++ b/src/text.c @@ -0,0 +1,299 @@ +#include "text.h" + +#include +#include +#include + +#include "display.h" +#include "utf8.h" + +enum flags { + LineChanged = 1 << 0, +}; + +struct line { + uint8_t *data; + uint8_t flags; + uint32_t nbytes; + uint32_t nchars; +}; + +struct text { + // raw bytes without any null terminators + struct line *lines; + uint32_t nlines; + uint32_t capacity; +}; + +struct text *text_create(uint32_t initial_capacity) { + struct text *txt = calloc(1, sizeof(struct text)); + txt->lines = calloc(initial_capacity, sizeof(struct line)); + txt->capacity = initial_capacity; + + // we always have one line, since add line adds a second one + txt->nlines = 1; + + return txt; +} + +void text_destroy(struct text *text) { + for (uint32_t li = 0; li < text->nlines; ++li) { + free(text->lines[li].data); + text->lines[li].data = NULL; + text->lines[li].flags = 0; + text->lines[li].nbytes = 0; + text->lines[li].nchars = 0; + } + + free(text->lines); +} + +void append_to_line(struct line *line, uint32_t col, uint8_t *text, + uint32_t len, uint32_t nchars) { + + if (len == 0) { + return; + } + + line->nbytes += len; + line->nchars += nchars; + line->flags = LineChanged; + line->data = realloc(line->data, line->nbytes + len); + + // move chars out of the way + memmove(line->data + col + 1, line->data + col, line->nbytes - (col + 1)); + + // insert new chars + memcpy(line->data + col, text, len); +} + +uint32_t text_line_length(struct text *text, uint32_t lineidx) { + return text->lines[lineidx].nchars; +} + +uint32_t text_line_size(struct text *text, uint32_t lineidx) { + return text->lines[lineidx].nbytes; +} + +uint32_t text_num_lines(struct text *text) { return text->nlines; } + +// given `char_idx` as a character index, return the byte index +uint32_t charidx_to_byteidx(struct line *line, uint32_t char_idx) { + if (char_idx > line->nchars) { + return line->nchars; + } + return utf8_nbytes(line->data, char_idx); +} + +// TODO: grapheme clusters +// given `byte_idx` as a byte index, return the character index +uint32_t byteidx_to_charidx(struct line *line, uint32_t byte_idx) { + if (byte_idx > line->nbytes) { + return line->nchars; + } + + return utf8_nchars(line->data, byte_idx); +} + +uint32_t char_byte_size(struct line *line, uint32_t byte_idx) { + return utf8_nbytes(line->data + byte_idx, 1); +} + +void split_line(uint32_t col, struct line *line, struct line *next) { + uint8_t *data = line->data; + uint32_t nbytes = line->nbytes; + uint32_t nchars = line->nchars; + + uint32_t chari = col; + uint32_t bytei = charidx_to_byteidx(line, chari); + + line->nbytes = bytei; + line->nchars = chari; + next->nbytes = nbytes - bytei; + next->nchars = nchars - chari; + line->flags = next->flags = line->flags; + + // first, handle some cases where the new line or the pre-existing one is + // empty + if (next->nbytes == 0) { + line->data = data; + } else if (line->nbytes == 0) { + next->data = data; + } else { + // actually split the line + next->data = (uint8_t *)malloc(next->nbytes); + memcpy(next->data, data + bytei, next->nbytes); + + line->data = (uint8_t *)malloc(line->nbytes); + memcpy(line->data, data, line->nbytes); + + free(data); + } +} + +void mark_lines_changed(struct text *text, uint32_t line, uint32_t nlines) { + for (uint32_t linei = line; linei < (line + nlines); ++linei) { + text->lines[linei].flags |= LineChanged; + } +} + +void shift_lines(struct text *text, uint32_t start, int32_t direction) { + struct line *dest = text->lines + ((int64_t)start + direction); + struct line *src = text->lines + start; + uint32_t nlines = text->nlines - (dest > src ? (start + direction) : start); + memmove(dest, src, nlines * sizeof(struct line)); +} + +void new_line_at(struct text *text, uint32_t line, uint32_t col) { + if (text->nlines == text->capacity) { + text->capacity *= 2; + text->lines = realloc(text->lines, sizeof(struct line) * text->capacity); + } + + struct line *nline = &text->lines[text->nlines]; + nline->data = NULL; + nline->nbytes = 0; + nline->nchars = 0; + nline->flags = 0; + + ++text->nlines; + + mark_lines_changed(text, line, text->nlines - line); + + // move following lines out of the way + shift_lines(text, line + 1, 1); + + // split line if needed + struct line *pl = &text->lines[line]; + struct line *cl = &text->lines[line + 1]; + split_line(col, pl, cl); +} + +void text_delete_line(struct text *text, uint32_t line) { + // always keep a single line + if (text->nlines == 1) { + return; + } + + mark_lines_changed(text, line, text->nlines - line); + free(text->lines[line].data); + text->lines[line].data = NULL; + + shift_lines(text, line + 1, -1); + + if (text->nlines > 0) { + --text->nlines; + text->lines[text->nlines].data = NULL; + text->lines[text->nlines].nbytes = 0; + text->lines[text->nlines].nchars = 0; + } +} + +void text_append(struct text *text, uint32_t line, uint32_t col, uint8_t *bytes, + uint32_t nbytes, uint32_t *lines_added, uint32_t *cols_added) { + uint32_t linelen = 0; + uint32_t nchars_counted = 0; + uint32_t nlines_added = 0; + uint32_t ncols_added = 0; + for (uint32_t bytei = 0; bytei < nbytes; ++bytei) { + uint8_t byte = bytes[bytei]; + if (byte == '\n') { + append_to_line(&text->lines[line], col, bytes + (bytei - linelen), + linelen, nchars_counted); + + col += nchars_counted; + new_line_at(text, line, col); + ++line; + ++nlines_added; + + col = text_line_length(text, line); + linelen = 0; + nchars_counted = 0; + } else { + if (utf8_byte_is_ascii(byte) || utf8_byte_is_unicode_start(byte)) { + ++nchars_counted; + } + ++linelen; + } + } + + // handle remaining + if (linelen > 0) { + append_to_line(&text->lines[line], col, bytes + (nbytes - linelen), linelen, + nchars_counted); + ncols_added = nchars_counted; + } + + *lines_added = nlines_added; + *cols_added = ncols_added; +} + +void text_delete(struct text *text, uint32_t line, uint32_t col, + uint32_t nchars) { + struct line *lp = &text->lines[line]; + uint32_t max_chars = nchars > lp->nchars ? lp->nchars : nchars; + if (lp->nchars > 0) { + + // get byte index and size for char to remove + uint32_t bytei = charidx_to_byteidx(lp, col); + uint32_t nbytes = utf8_nbytes(lp->data + bytei, max_chars); + + memcpy(lp->data + bytei, lp->data + bytei + nbytes, + lp->nbytes - (bytei + nbytes)); + + lp->nbytes -= nbytes; + lp->nchars -= max_chars; + lp->flags |= LineChanged; + } +} + +uint32_t text_render(struct text *text, uint32_t line, uint32_t nlines, + struct render_cmd *cmds, uint32_t max_ncmds) { + uint32_t nlines_max = nlines > text->capacity ? text->capacity : nlines; + + uint32_t ncmds = 0; + for (uint32_t lineidx = line; lineidx < nlines_max; ++lineidx) { + struct line *lp = &text->lines[lineidx]; + if (lp->flags & LineChanged) { + + cmds[ncmds] = (struct render_cmd){ + .row = lineidx, + .col = 0, // TODO: do not redraw full line + .data = lp->data, + .len = lp->nbytes, + }; + + lp->flags &= ~(LineChanged); + + ++ncmds; + } + } + + return ncmds; +} + +void text_for_each_line(struct text *text, uint32_t line, uint32_t nlines, + line_cb callback) { + for (uint32_t li = line; li < (line + nlines); ++li) { + struct line *src_line = &text->lines[li]; + struct txt_line line = (struct txt_line){ + .text = src_line->data, + .nbytes = src_line->nbytes, + .nchars = src_line->nchars, + }; + callback(&line); + } +} + +struct txt_line text_get_line(struct text *text, uint32_t line) { + struct line *src_line = &text->lines[line]; + return (struct txt_line){ + .text = src_line->data, + .nbytes = src_line->nbytes, + .nchars = src_line->nchars, + }; +} + +bool text_line_contains_unicode(struct text *text, uint32_t line) { + return text->lines[line].nbytes != text->lines[line].nchars; +} diff --git a/src/text.h b/src/text.h new file mode 100644 index 0000000..21de899 --- /dev/null +++ b/src/text.h @@ -0,0 +1,39 @@ +#include +#include +#include + +// opaque so it is easier to change representation to gap, rope etc. +struct text; + +struct render_cmd; + +struct text *text_create(uint32_t initial_capacity); +void text_destroy(struct text *text); + +void text_append(struct text *text, uint32_t line, uint32_t col, uint8_t *bytes, + uint32_t nbytes, uint32_t *lines_added, uint32_t *cols_added); + +void text_delete(struct text *text, uint32_t line, uint32_t col, + uint32_t nchars); +void text_delete_line(struct text *text, uint32_t line); + +uint32_t text_render(struct text *text, uint32_t line, uint32_t nlines, + struct render_cmd *cmds, uint32_t max_ncmds); + +uint32_t text_num_lines(struct text *text); +uint32_t text_line_length(struct text *text, uint32_t lineidx); +uint32_t text_line_size(struct text *text, uint32_t lineidx); + +struct txt_line { + uint8_t *text; + uint32_t nbytes; + uint32_t nchars; +}; + +typedef void (*line_cb)(struct txt_line *line); +void text_for_each_line(struct text *text, uint32_t line, uint32_t nlines, + line_cb callback); + +struct txt_line text_get_line(struct text *text, uint32_t line); + +bool text_line_contains_unicode(struct text *text, uint32_t line); diff --git a/src/utf8.c b/src/utf8.c new file mode 100644 index 0000000..3afef40 --- /dev/null +++ b/src/utf8.c @@ -0,0 +1,42 @@ +#include "utf8.h" + +#include + +bool utf8_byte_is_unicode_start(uint8_t byte) { return (byte & 0xc0) == 0xc0; } +bool utf8_byte_is_unicode_continuation(uint8_t byte) { + return utf8_byte_is_unicode(byte) && !utf8_byte_is_unicode_start(byte); +} +bool utf8_byte_is_unicode(uint8_t byte) { return (byte & 0x80) != 0x0; } +bool utf8_byte_is_ascii(uint8_t byte) { return !utf8_byte_is_unicode(byte); } + +// TODO: grapheme clusters, this returns the number of unicode code points +uint32_t utf8_nchars(uint8_t *bytes, uint32_t nbytes) { + uint32_t nchars = 0; + for (uint32_t bi = 0; bi < nbytes; ++bi) { + if (utf8_byte_is_ascii(bytes[bi]) || utf8_byte_is_unicode_start(bytes[bi])) + ++nchars; + } + return nchars; +} + +// TODO: grapheme clusters, this uses the number of unicode code points +uint32_t utf8_nbytes(uint8_t *bytes, uint32_t nchars) { + uint32_t bi = 0; + uint32_t chars = 0; + while (chars < nchars) { + uint8_t byte = bytes[bi]; + if (utf8_byte_is_unicode_start(byte)) { + ++chars; + + // length of char is the number of leading ones + // flip it and count number of leading zeros + uint8_t invb = ~byte; + bi += __builtin_clz((uint32_t)invb) - 24; + } else { + ++chars; + ++bi; + } + } + + return bi; +} diff --git a/src/utf8.h b/src/utf8.h new file mode 100644 index 0000000..901b1af --- /dev/null +++ b/src/utf8.h @@ -0,0 +1,17 @@ +#include +#include + +/*! + * \brief Return the number of chars the utf-8 sequence pointed at by `bytes` of + * length `nbytes`, represents + */ +uint32_t utf8_nchars(uint8_t *bytes, uint32_t nbytes); + +/* Return the number of bytes used to make up the next `nchars` characters */ +uint32_t utf8_nbytes(uint8_t *bytes, uint32_t nchars); + +/* true if `byte` is a unicode byte sequence start byte */ +bool utf8_byte_is_unicode_start(uint8_t byte); +bool utf8_byte_is_unicode_continuation(uint8_t byte); +bool utf8_byte_is_ascii(uint8_t byte); +bool utf8_byte_is_unicode(uint8_t byte); -- cgit v1.2.3