summaryrefslogtreecommitdiff
path: root/src/dged
diff options
context:
space:
mode:
authorAlbert Cervin <albert@acervin.com>2023-04-06 23:23:46 +0200
committerAlbert Cervin <albert@acervin.com>2023-05-01 22:19:14 +0200
commita123725a12e948d78badb2cb686d38548f1c633b (patch)
treec92c46134ef5536fbbf3bf08983c4f0dea1aaf58 /src/dged
parentb5ed4cf757afc50afb6ac499eee7b87a2648fa4c (diff)
downloaddged-a123725a12e948d78badb2cb686d38548f1c633b.tar.gz
dged-a123725a12e948d78badb2cb686d38548f1c633b.tar.xz
dged-a123725a12e948d78badb2cb686d38548f1c633b.zip
Implement window handling
Also implement searching. fix undo boundaries when it checked for other save point, it used && instead of == which caused it to overwrite other types. Fix bytes vs chars bug in text_get_region
Diffstat (limited to 'src/dged')
-rw-r--r--src/dged/allocator.c24
-rw-r--r--src/dged/allocator.h46
-rw-r--r--src/dged/binding.c75
-rw-r--r--src/dged/binding.h136
-rw-r--r--src/dged/btree.h113
-rw-r--r--src/dged/buffer.c1110
-rw-r--r--src/dged/buffer.h219
-rw-r--r--src/dged/buffers.c43
-rw-r--r--src/dged/buffers.h17
-rw-r--r--src/dged/command.c79
-rw-r--r--src/dged/command.h179
-rw-r--r--src/dged/display.c386
-rw-r--r--src/dged/display.h212
-rw-r--r--src/dged/hash.h11
-rw-r--r--src/dged/hashmap.h86
-rw-r--r--src/dged/keyboard.c159
-rw-r--r--src/dged/keyboard.h145
-rw-r--r--src/dged/lang.c160
-rw-r--r--src/dged/lang.h46
-rw-r--r--src/dged/minibuffer.c223
-rw-r--r--src/dged/minibuffer.h108
-rw-r--r--src/dged/reactor-epoll.c76
-rw-r--r--src/dged/reactor.h17
-rw-r--r--src/dged/settings.c95
-rw-r--r--src/dged/settings.h137
-rw-r--r--src/dged/text.c496
-rw-r--r--src/dged/text.h54
-rw-r--r--src/dged/undo.c181
-rw-r--r--src/dged/undo.h66
-rw-r--r--src/dged/utf8.c67
-rw-r--r--src/dged/utf8.h17
-rw-r--r--src/dged/vec.h63
-rw-r--r--src/dged/window.c445
-rw-r--r--src/dged/window.h48
34 files changed, 5339 insertions, 0 deletions
diff --git a/src/dged/allocator.c b/src/dged/allocator.c
new file mode 100644
index 0000000..308b97c
--- /dev/null
+++ b/src/dged/allocator.c
@@ -0,0 +1,24 @@
+#include "allocator.h"
+
+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_destroy(struct frame_allocator *alloc) {
+ free(alloc->buf);
+ alloc->buf = NULL;
+}
+
+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; }
diff --git a/src/dged/allocator.h b/src/dged/allocator.h
new file mode 100644
index 0000000..49e3aec
--- /dev/null
+++ b/src/dged/allocator.h
@@ -0,0 +1,46 @@
+#include <stddef.h>
+#include <stdint.h>
+#include <stdlib.h>
+
+/**
+ * Simple bump allocator that can be used for
+ * allocations with a frame lifetime.
+ */
+struct frame_allocator {
+ uint8_t *buf;
+ size_t offset;
+ size_t capacity;
+};
+
+/**
+ * Create a new frame allocator
+ *
+ * @param capacity The capacity in bytes of the frame allocator
+ * @returns The frame allocator
+ */
+struct frame_allocator frame_allocator_create(size_t capacity);
+
+/**
+ * Destroy a frame allocator.
+ *
+ * @param alloc The @ref frame_allocator "frame allocator" to destroy.
+ */
+void frame_allocator_destroy(struct frame_allocator *alloc);
+
+/**
+ * Allocate memory in this @ref frame_allocator "frame allocator"
+ *
+ * @param alloc The allocator to allocate in
+ * @param sz The size in bytes to allocate.
+ * @returns void* representing the start of the allocated region on success,
+ * NULL on failure.
+ */
+void *frame_allocator_alloc(struct frame_allocator *alloc, size_t sz);
+
+/**
+ * Clear this @ref frame_allocator "frame allocator".
+ *
+ * This does not free any memory, but simply resets the offset to 0.
+ * @param alloc The frame allocator to clear
+ */
+void frame_allocator_clear(struct frame_allocator *alloc);
diff --git a/src/dged/binding.c b/src/dged/binding.c
new file mode 100644
index 0000000..5111548
--- /dev/null
+++ b/src/dged/binding.c
@@ -0,0 +1,75 @@
+#include "binding.h"
+#include "command.h"
+
+#include <stdlib.h>
+#include <string.h>
+
+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 lookup_result lookup_key(struct keymap *keymaps, uint32_t nkeymaps,
+ struct key *key, struct commands *commands) {
+ // lookup in reverse order in the keymaps
+ uint32_t kmi = nkeymaps;
+ while (kmi > 0) {
+ --kmi;
+ struct keymap *keymap = &keymaps[kmi];
+
+ for (uint32_t bi = keymap->nbindings; bi > 0; --bi) {
+ struct binding *binding = &keymap->bindings[bi - 1];
+ if (key_equal(key, &binding->key)) {
+ switch (binding->type) {
+ case BindingType_Command: {
+ return (struct lookup_result){
+ .found = true,
+ .type = BindingType_Command,
+ .command = lookup_command_by_hash(commands, binding->command),
+ };
+ }
+ case BindingType_Keymap: {
+ return (struct lookup_result){
+ .found = true,
+ .type = BindingType_Keymap,
+ .keymap = binding->keymap,
+ };
+ }
+ case BindingType_DirectCommand:
+ return (struct lookup_result){
+ .found = true,
+ .type = BindingType_Command,
+ .command = binding->direct_command,
+ };
+ }
+ }
+ }
+ }
+
+ return (struct lookup_result){.found = false};
+}
diff --git a/src/dged/binding.h b/src/dged/binding.h
new file mode 100644
index 0000000..f2a531d
--- /dev/null
+++ b/src/dged/binding.h
@@ -0,0 +1,136 @@
+#include "hash.h"
+#include "keyboard.h"
+
+/**
+ * Directory of keyboard mappings.
+ */
+struct keymap {
+ /** Keymap name */
+ const char *name;
+
+ /** The bindings in this keymap */
+ struct binding *bindings;
+
+ /** The number of bindings in this keymap */
+ uint32_t nbindings;
+
+ /** The number of bindings this keymap can currently hold */
+ uint32_t capacity;
+};
+
+/**
+ * Type of a keyboard binding
+ */
+enum binding_type {
+ /** This binding is to a command */
+ BindingType_Command,
+
+ /** This binding is to another keymap */
+ BindingType_Keymap,
+
+ /** This binding is to an already resolved command,
+ * a.k.a. anonymous binding.
+ */
+ BindingType_DirectCommand
+};
+
+#define BINDING_INNER(mod_, c_, command_) \
+ (struct binding) { \
+ .key = {.mod = mod_, .key = c_}, .type = BindingType_Command, \
+ .command = hash_name(command_) \
+ }
+
+#define ANONYMOUS_BINDING_INNER(mod_, c_, command_) \
+ (struct binding) { \
+ .key = {.mod = mod_, .key = c_}, .type = BindingType_DirectCommand, \
+ .direct_command = command_ \
+ }
+
+#define PREFIX_INNER(mod_, c_, keymap_) \
+ (struct binding) { \
+ .key = {.mod = mod_, .key = c_}, .type = BindingType_Keymap, \
+ .keymap = keymap_ \
+ }
+
+#define BINDING(...) BINDING_INNER(__VA_ARGS__)
+#define PREFIX(...) PREFIX_INNER(__VA_ARGS__)
+#define ANONYMOUS_BINDING(...) ANONYMOUS_BINDING_INNER(__VA_ARGS__)
+
+/**
+ * A keyboard key bound to an action
+ */
+struct binding {
+ /** The keyboard key that triggers the action in this binding */
+ struct key key;
+
+ /** Type of this binding, see @ref binding_type */
+ uint8_t type;
+
+ union {
+ /** A hash of a command name */
+ uint32_t command;
+ /** A command */
+ struct command *direct_command;
+ /** A keymap */
+ struct keymap *keymap;
+ };
+};
+
+/**
+ * Result of a binding lookup
+ */
+struct lookup_result {
+ /** True if a binding was found */
+ bool found;
+
+ /** Type of binding in the result */
+ uint8_t type;
+
+ union {
+ /** A command */
+ struct command *command;
+ /** A keymap */
+ struct keymap *keymap;
+ };
+};
+
+struct commands;
+
+/**
+ * Create a new keymap
+ *
+ * @param name Name of the keymap
+ * @param capacity Initial capacity of the keymap.
+ * @returns The created keymap
+ */
+struct keymap keymap_create(const char *name, uint32_t capacity);
+
+/**
+ * Bind keys in a keymap.
+ *
+ * @param keymap The keymap to bind keys in.
+ * @param bindings Bindings to add.
+ * @param nbindings Number of bindings in @ref bindings.
+ */
+void keymap_bind_keys(struct keymap *keymap, struct binding *bindings,
+ uint32_t nbindings);
+
+/**
+ * Destroy a keymap.
+ *
+ * This clears all keybindings associated with the keymap.
+ * @param keymap Keymap to destroy.
+ */
+void keymap_destroy(struct keymap *keymap);
+
+/**
+ * Lookup the binding for a key in a set of keymaps.
+ *
+ * @param keymaps The keymaps to look in.
+ * @param nkeymaps The number of keymaps in @ref keymaps.
+ * @param key The keystroke to look up bindings for.
+ * @param commands Available commands for lookup.
+ * @returns A @ref lookup_result with the result of the lookup.
+ */
+struct lookup_result lookup_key(struct keymap *keymaps, uint32_t nkeymaps,
+ struct key *key, struct commands *commands);
diff --git a/src/dged/btree.h b/src/dged/btree.h
new file mode 100644
index 0000000..8743b32
--- /dev/null
+++ b/src/dged/btree.h
@@ -0,0 +1,113 @@
+#ifndef _BTREE_H
+#define _BTREE_H
+
+#include "vec.h"
+
+#include <stdlib.h>
+
+#define BINTREE_ENTRY_TYPE(name, entry) \
+ struct name { \
+ struct name *parent; \
+ struct name *left; \
+ struct name *right; \
+ entry value; \
+ }
+
+#define BINTREE(entry) \
+ struct { \
+ struct entry *root; \
+ }
+
+#define BINTREE_INIT(tree) ((tree)->root = NULL)
+#define BINTREE_DESTROY(tree, entry_type) BINTREE_INIT(tree)
+
+#define BINTREE_ROOT(tree) (tree)->root
+
+#define BINTREE_LEFT(node) (node)->left
+#define BINTREE_RIGHT(node) (node)->right
+#define BINTREE_PARENT(node) (node)->parent
+#define BINTREE_VALUE(node) (node)->value
+#define BINTREE_HAS_PARENT(node) ((node)->parent != NULL)
+#define BINTREE_HAS_LEFT(node) ((node)->left != NULL)
+#define BINTREE_HAS_RIGHT(node) ((node)->right != NULL)
+
+#define BINTREE_FREE_NODE(node) free(node)
+#define BINTREE_FREE_NODES(root, entry_type) \
+ { \
+ BINTREE_FIRST(root); \
+ VEC(struct entry_type *) to_delete; \
+ VEC_INIT(&to_delete, 10); \
+ while (root != NULL) { \
+ VEC_PUSH(&to_delete, root); \
+ BINTREE_NEXT(root); \
+ } \
+ VEC_FOR_EACH(&to_delete, struct entry_type **e) { BINTREE_FREE_NODE(*e); } \
+ VEC_DESTROY(&to_delete); \
+ }
+
+#define BINTREE_FIRST(res) \
+ if (res == NULL) { \
+ res = NULL; \
+ } else { \
+ while (BINTREE_HAS_LEFT(res)) { \
+ res = BINTREE_LEFT(res); \
+ } \
+ }
+
+#define BINTREE_NEXT(res) \
+ if (res == NULL) { \
+ res = NULL; \
+ } else { \
+ if (BINTREE_HAS_RIGHT(res)) { \
+ res = BINTREE_RIGHT(res); \
+ BINTREE_FIRST(res) \
+ } else { \
+ while (BINTREE_HAS_PARENT(res) && \
+ res == BINTREE_RIGHT(BINTREE_PARENT(res))) \
+ res = BINTREE_PARENT(res); \
+ res = BINTREE_PARENT(res); \
+ } \
+ }
+
+#define BINTREE_INSERT(parent, entry) \
+ if (parent != NULL) { \
+ if (!BINTREE_HAS_LEFT(parent)) { \
+ BINTREE_LEFT(parent) = calloc(1, sizeof(*(parent))); \
+ BINTREE_PARENT(BINTREE_LEFT(parent)) = parent; \
+ BINTREE_VALUE(BINTREE_LEFT(parent)) = entry; \
+ } else { \
+ BINTREE_RIGHT(parent) = calloc(1, sizeof(*(parent))); \
+ BINTREE_PARENT(BINTREE_RIGHT(parent)) = parent; \
+ BINTREE_VALUE(BINTREE_RIGHT(parent)) = entry; \
+ } \
+ }
+
+#define BINTREE_REMOVE(node) \
+ if (BINTREE_HAS_PARENT(node)) { \
+ if (BINTREE_LEFT(BINTREE_PARENT(node)) == node) { \
+ BINTREE_LEFT(BINTREE_PARENT(node)) = NULL; \
+ } else { \
+ BINTREE_RIGHT(BINTREE_PARENT(node)) = NULL; \
+ } \
+ BINTREE_PARENT(node) = NULL; \
+ }
+
+#define BINTREE_SET_ROOT(tree, value) \
+ (tree)->root = calloc(1, sizeof(*(tree)->root)); \
+ BINTREE_VALUE((tree)->root) = value;
+
+#define BINTREE_FIND(tree, needle, res) \
+ { \
+ res = BINTREE_ROOT(tree); \
+ BINTREE_FIRST(res); \
+ bool found = false; \
+ while (res != NULL) { \
+ if (BINTREE_VALUE(res) == needle) { \
+ found = true; \
+ break; \
+ } \
+ BINTREE_NEXT(res); \
+ } \
+ res = found ? res : NULL; \
+ }
+#endif
diff --git a/src/dged/buffer.c b/src/dged/buffer.c
new file mode 100644
index 0000000..25a8a4a
--- /dev/null
+++ b/src/dged/buffer.c
@@ -0,0 +1,1110 @@
+#include "buffer.h"
+#include "binding.h"
+#include "bits/stdint-uintn.h"
+#include "dged/vec.h"
+#include "display.h"
+#include "errno.h"
+#include "lang.h"
+#include "minibuffer.h"
+#include "reactor.h"
+#include "settings.h"
+#include "utf8.h"
+
+#include <fcntl.h>
+#include <libgen.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+#include <unistd.h>
+#include <wchar.h>
+
+struct modeline {
+ uint8_t *buffer;
+ uint32_t sz;
+};
+
+#define KILL_RING_SZ 64
+static struct kill_ring {
+ struct text_chunk buffer[KILL_RING_SZ];
+ struct buffer_location last_paste;
+ bool paste_up_to_date;
+ uint32_t curr_idx;
+ uint32_t paste_idx;
+} g_kill_ring = {.curr_idx = 0,
+ .buffer = {0},
+ .last_paste = {0},
+ .paste_idx = 0,
+ .paste_up_to_date = false};
+
+#define MAX_CREATE_HOOKS 32
+static struct create_hook {
+ create_hook_cb callback;
+ void *userdata;
+} g_create_hooks[MAX_CREATE_HOOKS];
+static uint32_t g_num_create_hooks = 0;
+
+struct update_hook_result buffer_linenum_hook(struct buffer_view *view,
+ struct command_list *commands,
+ uint32_t width, uint32_t height,
+ uint64_t frame_time,
+ void *userdata);
+
+struct update_hook_result buffer_modeline_hook(struct buffer_view *view,
+ struct command_list *commands,
+ uint32_t width, uint32_t height,
+ uint64_t frame_time,
+ void *userdata);
+
+struct buffer_view buffer_view_create(struct buffer *buffer, bool modeline,
+ bool line_numbers) {
+ struct buffer_view view = {
+ .dot = {0},
+ .mark = {0},
+ .mark_set = false,
+ .scroll = {0},
+ .buffer = buffer,
+ .modeline = NULL,
+ .line_numbers = line_numbers,
+ };
+
+ if (modeline) {
+ view.modeline = calloc(1, sizeof(struct modeline));
+ view.modeline->buffer = malloc(1024);
+ view.modeline->sz = 1024;
+ view.modeline->buffer[0] = '\0';
+ }
+
+ return view;
+}
+
+struct buffer_view buffer_view_clone(struct buffer_view *view) {
+ struct buffer_view c = {
+ .dot = view->dot,
+ .mark = view->mark,
+ .mark_set = view->mark_set,
+ .scroll = view->scroll,
+ .buffer = view->buffer,
+ .modeline = NULL,
+ .line_numbers = view->line_numbers,
+ };
+
+ if (view->modeline) {
+ c.modeline = calloc(1, sizeof(struct modeline));
+ c.modeline->buffer = malloc(view->modeline->sz);
+ memcpy(c.modeline->buffer, view->modeline->buffer, view->modeline->sz);
+ }
+
+ return c;
+}
+
+void buffer_view_destroy(struct buffer_view *view) {
+ if (view->modeline != NULL) {
+ free(view->modeline->buffer);
+ free(view->modeline);
+ }
+}
+
+uint32_t buffer_add_create_hook(create_hook_cb hook, void *userdata) {
+ if (g_num_create_hooks < MAX_CREATE_HOOKS) {
+ g_create_hooks[g_num_create_hooks] = (struct create_hook){
+ .callback = hook,
+ .userdata = userdata,
+ };
+ ++g_num_create_hooks;
+ }
+
+ return g_num_create_hooks - 1;
+}
+
+struct buffer buffer_create(char *name) {
+ struct buffer b = (struct buffer){
+ .filename = NULL,
+ .name = strdup(name),
+ .text = text_create(10),
+ .modified = false,
+ .readonly = false,
+ .lang = lang_from_id("fnd"),
+ };
+
+ undo_init(&b.undo, 100);
+
+ for (uint32_t hooki = 0; hooki < g_num_create_hooks; ++hooki) {
+ g_create_hooks[hooki].callback(&b, g_create_hooks[hooki].userdata);
+ }
+
+ return b;
+}
+
+void buffer_destroy(struct buffer *buffer) {
+ text_destroy(buffer->text);
+ buffer->text = NULL;
+
+ free(buffer->name);
+ buffer->name = NULL;
+
+ free(buffer->filename);
+ buffer->filename = NULL;
+
+ undo_destroy(&buffer->undo);
+}
+
+void buffer_clear(struct buffer_view *view) {
+ text_clear(view->buffer->text);
+ view->dot.col = view->dot.line = 0;
+}
+
+void buffer_static_init() {
+ settings_register_setting(
+ "editor.tab-width",
+ (struct setting_value){.type = Setting_Number, .number_value = 4});
+
+ settings_register_setting(
+ "editor.show-whitespace",
+ (struct setting_value){.type = Setting_Bool, .bool_value = true});
+}
+
+void buffer_static_teardown() {
+ for (uint32_t i = 0; i < KILL_RING_SZ; ++i) {
+ if (g_kill_ring.buffer[i].allocated) {
+ free(g_kill_ring.buffer[i].text);
+ }
+ }
+}
+
+bool buffer_is_empty(struct buffer *buffer) {
+ return text_num_lines(buffer->text) == 0;
+}
+
+bool buffer_is_modified(struct buffer *buffer) { return buffer->modified; }
+
+bool buffer_is_readonly(struct buffer *buffer) { return buffer->readonly; }
+void buffer_set_readonly(struct buffer *buffer, bool readonly) {
+ buffer->readonly = readonly;
+}
+
+void delete_with_undo(struct buffer *buffer, struct buffer_location start,
+ struct buffer_location end) {
+ if (buffer->readonly) {
+ minibuffer_echo_timeout(4, "buffer is read-only");
+ return;
+ }
+
+ struct text_chunk txt =
+ text_get_region(buffer->text, start.line, start.col, end.line, end.col);
+
+ undo_push_delete(
+ &buffer->undo,
+ (struct undo_delete){.data = txt.text,
+ .nbytes = txt.nbytes,
+ .pos = {.row = start.line, .col = start.col}});
+ undo_push_boundary(&buffer->undo,
+ (struct undo_boundary){.save_point = false});
+
+ text_delete(buffer->text, start.line, start.col, end.line, end.col);
+ buffer->modified = true;
+}
+
+void buffer_goto_beginning(struct buffer_view *view) {
+ view->dot.col = 0;
+ view->dot.line = 0;
+}
+
+void buffer_goto_end(struct buffer_view *view) {
+ view->dot.line = text_num_lines(view->buffer->text);
+ view->dot.col = 0;
+}
+
+bool movev(struct buffer_view *view, int rowdelta) {
+ int64_t new_line = (int64_t)view->dot.line + rowdelta;
+
+ if (new_line < 0) {
+ view->dot.line = 0;
+ return false;
+ } else if (new_line > text_num_lines(view->buffer->text)) {
+ view->dot.line = text_num_lines(view->buffer->text);
+ return false;
+ } else {
+ view->dot.line = (uint32_t)new_line;
+
+ // make sure column stays on the line
+ uint32_t linelen = text_line_length(view->buffer->text, view->dot.line);
+ view->dot.col = view->dot.col > linelen ? linelen : view->dot.col;
+ return true;
+ }
+}
+
+// move dot `coldelta` chars
+bool moveh(struct buffer_view *view, int coldelta) {
+ int64_t new_col = (int64_t)view->dot.col + coldelta;
+
+ if (new_col > (int64_t)text_line_length(view->buffer->text, view->dot.line)) {
+ if (movev(view, 1)) {
+ view->dot.col = 0;
+ }
+ } else if (new_col < 0) {
+ if (movev(view, -1)) {
+ view->dot.col = text_line_length(view->buffer->text, view->dot.line);
+ } else {
+ return false;
+ }
+ } else {
+ view->dot.col = new_col;
+ }
+
+ return true;
+}
+
+void buffer_goto(struct buffer_view *view, uint32_t line, uint32_t col) {
+ int64_t linedelta = (int64_t)line - (int64_t)view->dot.line;
+ movev(view, linedelta);
+
+ int64_t coldelta = (int64_t)col - (int64_t)view->dot.col;
+ moveh(view, coldelta);
+}
+
+struct region {
+ struct buffer_location begin;
+ struct buffer_location end;
+};
+
+struct region to_region(struct buffer_location dot,
+ struct buffer_location mark) {
+ struct region reg = {.begin = mark, .end = dot};
+
+ if (dot.line < mark.line || (dot.line == mark.line && dot.col < mark.col)) {
+ reg.begin = dot;
+ reg.end = mark;
+ }
+
+ return reg;
+}
+
+struct region buffer_get_region(struct buffer_view *view) {
+ return to_region(view->dot, view->mark);
+}
+
+bool buffer_region_has_size(struct buffer_view *view) {
+ return view->mark_set &&
+ (labs((int64_t)view->mark.line - (int64_t)view->dot.line) +
+ labs((int64_t)view->mark.col - (int64_t)view->dot.col)) > 0;
+}
+
+struct text_chunk *copy_region(struct buffer *buffer, struct region region) {
+ struct text_chunk *curr = &g_kill_ring.buffer[g_kill_ring.curr_idx];
+ g_kill_ring.curr_idx = g_kill_ring.curr_idx + 1 % KILL_RING_SZ;
+
+ if (curr->allocated) {
+ free(curr->text);
+ }
+
+ struct text_chunk txt =
+ text_get_region(buffer->text, region.begin.line, region.begin.col,
+ region.end.line, region.end.col);
+ *curr = txt;
+ return curr;
+}
+
+void buffer_copy(struct buffer_view *view) {
+ if (buffer_region_has_size(view)) {
+ struct region reg = buffer_get_region(view);
+ struct text_chunk *curr = copy_region(view->buffer, reg);
+ buffer_clear_mark(view);
+ }
+}
+
+void paste(struct buffer_view *view, uint32_t ring_idx) {
+ if (ring_idx > 0) {
+ struct text_chunk *curr = &g_kill_ring.buffer[ring_idx - 1];
+ if (curr->text != NULL) {
+ g_kill_ring.last_paste = view->mark_set ? view->mark : view->dot;
+ buffer_add_text(view, curr->text, curr->nbytes);
+ g_kill_ring.paste_up_to_date = true;
+ }
+ }
+}
+
+void buffer_paste(struct buffer_view *view) {
+ g_kill_ring.paste_idx = g_kill_ring.curr_idx;
+ paste(view, g_kill_ring.curr_idx);
+}
+
+void buffer_paste_older(struct buffer_view *view) {
+ if (g_kill_ring.paste_up_to_date) {
+
+ // remove previous paste
+ struct text_chunk *curr = &g_kill_ring.buffer[g_kill_ring.curr_idx];
+ delete_with_undo(view->buffer, g_kill_ring.last_paste, view->dot);
+
+ // place ourselves right
+ view->dot = g_kill_ring.last_paste;
+
+ // paste older
+ if (g_kill_ring.paste_idx - 1 > 0) {
+ --g_kill_ring.paste_idx;
+ } else {
+ g_kill_ring.paste_idx = g_kill_ring.curr_idx;
+ }
+
+ paste(view, g_kill_ring.paste_idx);
+
+ } else {
+ buffer_paste(view);
+ }
+}
+
+void buffer_cut(struct buffer_view *view) {
+ if (buffer_region_has_size(view)) {
+ struct region reg = buffer_get_region(view);
+ copy_region(view->buffer, reg);
+ delete_with_undo(view->buffer, reg.begin, reg.end);
+ buffer_clear_mark(view);
+ view->dot = reg.begin;
+ }
+}
+
+bool maybe_delete_region(struct buffer_view *view) {
+ if (buffer_region_has_size(view)) {
+ struct region reg = buffer_get_region(view);
+ delete_with_undo(view->buffer, reg.begin, reg.end);
+ buffer_clear_mark(view);
+ view->dot = reg.begin;
+ return true;
+ }
+
+ return false;
+}
+
+void buffer_kill_line(struct buffer_view *view) {
+ uint32_t nchars =
+ text_line_length(view->buffer->text, view->dot.line) - view->dot.col;
+ if (nchars == 0) {
+ nchars = 1;
+ }
+
+ struct region reg = {
+ .begin = view->dot,
+ .end =
+ {
+ .line = view->dot.line,
+ .col = view->dot.col + nchars,
+ },
+ };
+ copy_region(view->buffer, reg);
+ delete_with_undo(view->buffer, view->dot,
+ (struct buffer_location){
+ .line = view->dot.line,
+ .col = view->dot.col + nchars,
+ });
+}
+
+void buffer_forward_delete_char(struct buffer_view *view) {
+ if (maybe_delete_region(view)) {
+ return;
+ }
+
+ delete_with_undo(view->buffer, view->dot,
+ (struct buffer_location){
+ .line = view->dot.line,
+ .col = view->dot.col + 1,
+ });
+}
+
+void buffer_backward_delete_char(struct buffer_view *view) {
+ if (maybe_delete_region(view)) {
+ return;
+ }
+
+ if (moveh(view, -1)) {
+ buffer_forward_delete_char(view);
+ }
+}
+
+void buffer_backward_char(struct buffer_view *view) { moveh(view, -1); }
+void buffer_forward_char(struct buffer_view *view) { moveh(view, 1); }
+
+struct buffer_location find_next(struct buffer_view *view, uint8_t chars[],
+ uint32_t nchars, int direction) {
+ struct text_chunk line = text_get_line(view->buffer->text, view->dot.line);
+ int64_t bytei =
+ text_col_to_byteindex(view->buffer->text, view->dot.line, view->dot.col);
+ while (bytei < line.nbytes && bytei > 0 &&
+ (line.text[bytei] == ' ' || line.text[bytei] == '.')) {
+ bytei += direction;
+ }
+
+ for (; bytei < line.nbytes && bytei > 0; bytei += direction) {
+ uint8_t b = line.text[bytei];
+ if (b == ' ' || b == '.') {
+ break;
+ }
+ }
+
+ uint32_t target_col =
+ text_byteindex_to_col(view->buffer->text, view->dot.line, bytei);
+ return (struct buffer_location){.line = view->dot.line, .col = target_col};
+}
+
+void buffer_forward_word(struct buffer_view *view) {
+ moveh(view, 1);
+ uint8_t chars[] = {' ', '.'};
+ view->dot = find_next(view, chars, 2, 1);
+}
+
+void buffer_backward_word(struct buffer_view *view) {
+ moveh(view, -1);
+ uint8_t chars[] = {' ', '.'};
+ view->dot = find_next(view, chars, 2, -1);
+}
+
+void buffer_backward_line(struct buffer_view *view) { movev(view, -1); }
+void buffer_forward_line(struct buffer_view *view) { movev(view, 1); }
+
+void buffer_end_of_line(struct buffer_view *view) {
+ view->dot.col = text_line_length(view->buffer->text, view->dot.line);
+}
+
+void buffer_beginning_of_line(struct buffer_view *view) { view->dot.col = 0; }
+
+struct buffer buffer_from_file(char *filename) {
+ struct buffer b = buffer_create(basename((char *)filename));
+ b.filename = strdup(filename);
+ if (access(b.filename, F_OK) == 0) {
+ FILE *file = fopen(filename, "r");
+
+ if (file == NULL) {
+ minibuffer_echo("Error opening %s: %s", filename, strerror(errno));
+ return b;
+ }
+
+ while (true) {
+ uint8_t buff[4096];
+ int bytes = fread(buff, 1, 4096, file);
+ if (bytes > 0) {
+ uint32_t ignore;
+ text_append(b.text, buff, bytes, &ignore, &ignore);
+ } else if (bytes == 0) {
+ break; // EOF
+ } else {
+ minibuffer_echo("error reading from %s: %s", filename, strerror(errno));
+ fclose(file);
+ return b;
+ }
+ }
+
+ fclose(file);
+ }
+
+ const char *ext = strrchr(b.filename, '.');
+ if (ext != NULL) {
+ b.lang = lang_from_extension(ext + 1);
+ }
+ undo_push_boundary(&b.undo, (struct undo_boundary){.save_point = true});
+ return b;
+}
+
+void write_line(struct text_chunk *chunk, void *userdata) {
+ FILE *file = (FILE *)userdata;
+ fwrite(chunk->text, 1, chunk->nbytes, file);
+
+ // final newline is not optional!
+ fputc('\n', file);
+}
+
+void buffer_to_file(struct buffer *buffer) {
+ if (!buffer->filename) {
+ minibuffer_echo("buffer \"%s\" is not associated with a file",
+ buffer->name);
+ return;
+ }
+
+ if (!buffer->modified) {
+ minibuffer_echo_timeout(4, "buffer already saved");
+ return;
+ }
+
+ FILE *file = fopen(buffer->filename, "w");
+ if (file == NULL) {
+ minibuffer_echo("failed to open file %s for writing: %s", buffer->filename,
+ strerror(errno));
+ return;
+ }
+
+ uint32_t nlines = text_num_lines(buffer->text);
+ struct text_chunk lastline = text_get_line(buffer->text, nlines - 1);
+ uint32_t nlines_to_write = lastline.nbytes == 0 ? nlines - 1 : nlines;
+
+ text_for_each_line(buffer->text, 0, nlines_to_write, write_line, file);
+ minibuffer_echo_timeout(4, "wrote %d lines to %s", nlines_to_write,
+ buffer->filename);
+ fclose(file);
+
+ buffer->modified = false;
+ undo_push_boundary(&buffer->undo, (struct undo_boundary){.save_point = true});
+}
+
+void buffer_write_to(struct buffer *buffer, const char *filename) {
+ buffer->filename = strdup(filename);
+ buffer_to_file(buffer);
+}
+
+struct search_data {
+ VEC(struct match) matches;
+ const char *pattern;
+};
+
+// TODO: maybe should live in text
+void search_line(struct text_chunk *chunk, void *userdata) {
+ struct search_data *data = (struct search_data *)userdata;
+ size_t pattern_len = strlen(data->pattern);
+ uint32_t pattern_nchars = utf8_nchars((uint8_t *)data->pattern, pattern_len);
+
+ char *line = malloc(chunk->nbytes + 1);
+ memcpy(line, chunk->text, chunk->nbytes);
+ line[chunk->nbytes] = '\0';
+ char *hit = NULL;
+ uint32_t byteidx = 0;
+ while ((hit = strstr(line + byteidx, data->pattern)) != NULL) {
+ byteidx = hit - line;
+ uint32_t begin = utf8_nchars(chunk->text, byteidx);
+ struct match match = (struct match){
+ .begin = {.col = begin, .line = chunk->line},
+ .end = {.col = begin + pattern_nchars, .line = chunk->line},
+ };
+
+ VEC_PUSH(&data->matches, match);
+
+ // proceed to after match
+ byteidx += pattern_len;
+ }
+}
+
+void buffer_find(struct buffer *buffer, const char *pattern,
+ struct match **matches, uint32_t *nmatches) {
+
+ struct search_data data = (struct search_data){.pattern = pattern};
+ VEC_INIT(&data.matches, 16);
+ text_for_each_line(buffer->text, 0, text_num_lines(buffer->text), search_line,
+ &data);
+
+ *matches = VEC_ENTRIES(&data.matches);
+ *nmatches = VEC_SIZE(&data.matches);
+}
+
+void buffer_set_text(struct buffer *buffer, uint8_t *text, uint32_t nbytes) {
+ text_clear(buffer->text);
+ uint32_t lines, cols;
+ text_append(buffer->text, text, nbytes, &lines, &cols);
+}
+
+int buffer_add_text(struct buffer_view *view, uint8_t *text, uint32_t nbytes) {
+ if (view->buffer->readonly) {
+ minibuffer_echo_timeout(4, "buffer is read-only");
+ return 0;
+ }
+
+ // invalidate last paste
+ g_kill_ring.paste_up_to_date = false;
+
+ /* If we currently have a selection active,
+ * replace it with the text to insert. */
+ maybe_delete_region(view);
+
+ struct buffer_location initial = view->dot;
+
+ uint32_t lines_added, cols_added;
+ text_insert_at(view->buffer->text, initial.line, initial.col, text, nbytes,
+ &lines_added, &cols_added);
+
+ // move to after inserted text
+ movev(view, lines_added);
+ if (lines_added > 0) {
+ // does not make sense to use position from another line
+ view->dot.col = 0;
+ }
+ moveh(view, cols_added);
+
+ struct buffer_location final = view->dot;
+ undo_push_add(
+ &view->buffer->undo,
+ (struct undo_add){.begin = {.row = initial.line, .col = initial.col},
+ .end = {.row = final.line, .col = final.col}});
+
+ if (lines_added > 0) {
+ undo_push_boundary(&view->buffer->undo,
+ (struct undo_boundary){.save_point = false});
+ }
+
+ view->buffer->modified = true;
+ return lines_added;
+}
+
+void buffer_newline(struct buffer_view *view) {
+ buffer_add_text(view, (uint8_t *)"\n", 1);
+}
+
+void buffer_indent(struct buffer_view *view) {
+ uint32_t tab_width = view->buffer->lang.tab_width;
+ buffer_add_text(view, (uint8_t *)" ",
+ tab_width > 16 ? 16 : tab_width);
+}
+
+uint32_t buffer_add_update_hook(struct buffer *buffer, update_hook_cb hook,
+ void *userdata) {
+ struct update_hook *h =
+ &buffer->update_hooks.hooks[buffer->update_hooks.nhooks];
+ h->callback = hook;
+ h->userdata = userdata;
+
+ ++buffer->update_hooks.nhooks;
+
+ // TODO: cant really have this if we actually want to remove a hook
+ return buffer->update_hooks.nhooks - 1;
+}
+
+void buffer_set_mark(struct buffer_view *view) {
+ view->mark_set ? buffer_clear_mark(view)
+ : buffer_set_mark_at(view, view->dot.line, view->dot.col);
+}
+
+void buffer_clear_mark(struct buffer_view *view) {
+ view->mark_set = false;
+ minibuffer_echo_timeout(2, "mark cleared");
+}
+
+void buffer_set_mark_at(struct buffer_view *view, uint32_t line, uint32_t col) {
+ view->mark_set = true;
+ view->mark.line = line;
+ view->mark.col = col;
+ minibuffer_echo_timeout(2, "mark set");
+}
+
+void buffer_undo(struct buffer_view *view) {
+ struct undo_stack *undo = &view->buffer->undo;
+ undo_begin(undo);
+
+ // fetch and handle records
+ struct undo_record *records = NULL;
+ uint32_t nrecords = 0;
+
+ if (undo_current_position(undo) == INVALID_TOP) {
+ minibuffer_echo_timeout(4,
+ "no more undo information, starting from top...");
+ }
+
+ undo_next(undo, &records, &nrecords);
+
+ undo_push_boundary(undo, (struct undo_boundary){.save_point = false});
+ for (uint32_t reci = 0; reci < nrecords; ++reci) {
+ struct undo_record *rec = &records[reci];
+ switch (rec->type) {
+ case Undo_Boundary: {
+ struct undo_boundary *b = &rec->boundary;
+ if (b->save_point) {
+ view->buffer->modified = false;
+ }
+ break;
+ }
+ case Undo_Add: {
+ struct undo_add *add = &rec->add;
+
+ delete_with_undo(view->buffer,
+ (struct buffer_location){
+ .line = add->begin.row,
+ .col = add->begin.col,
+ },
+ (struct buffer_location){
+ .line = add->end.row,
+ .col = add->end.col,
+ });
+
+ buffer_goto(view, add->begin.row, add->begin.col);
+ break;
+ }
+ case Undo_Delete: {
+ struct undo_delete *del = &rec->delete;
+ buffer_goto(view, del->pos.row, del->pos.col);
+ buffer_add_text(view, del->data, del->nbytes);
+ break;
+ }
+ }
+ }
+ undo_push_boundary(undo, (struct undo_boundary){.save_point = false});
+
+ free(records);
+ undo_end(undo);
+}
+
+struct cmdbuf {
+ struct command_list *cmds;
+ struct buffer_location scroll;
+ uint32_t line_offset;
+ uint32_t left_margin;
+ uint32_t width;
+
+ struct region region;
+ bool mark_set;
+
+ bool show_ws;
+
+ struct line_render_hook *line_render_hooks;
+ uint32_t nlinerender_hooks;
+};
+
+void render_line(struct text_chunk *line, void *userdata) {
+ struct cmdbuf *cmdbuf = (struct cmdbuf *)userdata;
+ uint32_t visual_line = line->line - cmdbuf->scroll.line + cmdbuf->line_offset;
+
+ for (uint32_t hooki = 0; hooki < cmdbuf->nlinerender_hooks; ++hooki) {
+ struct line_render_hook *hook = &cmdbuf->line_render_hooks[hooki];
+ hook->callback(line, visual_line, cmdbuf->cmds, hook->userdata);
+ }
+
+ uint32_t scroll_bytes =
+ utf8_nbytes(line->text, line->nbytes, cmdbuf->scroll.col);
+ uint32_t text_nbytes_scroll =
+ scroll_bytes > line->nbytes ? 0 : line->nbytes - scroll_bytes;
+ uint8_t *text = line->text + scroll_bytes;
+
+ // calculate how many chars we can fit in 'width'
+ uint32_t linewidth = cmdbuf->left_margin;
+ uint32_t text_nbytes = 0;
+ for (uint32_t bytei = 0;
+ bytei < text_nbytes_scroll && linewidth < cmdbuf->width; ++bytei) {
+ uint8_t *txt = &text[bytei];
+ if (*txt == '\t') {
+ linewidth += 3;
+ } else if (utf8_byte_is_unicode_start(*txt)) {
+ wchar_t wc;
+ if (mbrtowc(&wc, (char *)txt, 6, NULL) >= 0) {
+ linewidth += wcwidth(wc) - 1;
+ }
+ }
+
+ ++linewidth;
+ ++text_nbytes;
+ }
+
+ command_list_set_show_whitespace(cmdbuf->cmds, cmdbuf->show_ws);
+ struct buffer_location *begin = &cmdbuf->region.begin,
+ *end = &cmdbuf->region.end;
+
+ // should we draw region?
+ if (cmdbuf->mark_set && line->line >= begin->line &&
+ line->line <= end->line) {
+ uint32_t byte_offset = 0;
+ uint32_t col_offset = 0;
+
+ // draw any text on the line that should not be part of region
+ if (begin->line == line->line) {
+ if (begin->col > cmdbuf->scroll.col) {
+ uint32_t nbytes =
+ utf8_nbytes(text, text_nbytes, begin->col - cmdbuf->scroll.col);
+ command_list_draw_text(cmdbuf->cmds, cmdbuf->left_margin, visual_line,
+ text, nbytes);
+
+ byte_offset += nbytes;
+ }
+
+ col_offset = begin->col - cmdbuf->scroll.col;
+ }
+
+ // activate region color
+ command_list_set_index_color_bg(cmdbuf->cmds, 5);
+
+ // draw any text on line that should be part of region
+ if (end->line == line->line) {
+ if (end->col > cmdbuf->scroll.col) {
+ uint32_t nbytes =
+ utf8_nbytes(text + byte_offset, text_nbytes - byte_offset,
+ end->col - col_offset - cmdbuf->scroll.col);
+ command_list_draw_text(cmdbuf->cmds, cmdbuf->left_margin + col_offset,
+ visual_line, text + byte_offset, nbytes);
+ byte_offset += nbytes;
+ }
+
+ col_offset = end->col - cmdbuf->scroll.col;
+ command_list_reset_color(cmdbuf->cmds);
+ }
+
+ // draw rest of line
+ if (text_nbytes - byte_offset > 0) {
+ command_list_draw_text(cmdbuf->cmds, cmdbuf->left_margin + col_offset,
+ visual_line, text + byte_offset,
+ text_nbytes - byte_offset);
+ }
+
+ // done rendering region
+ command_list_reset_color(cmdbuf->cmds);
+
+ } else {
+ command_list_draw_text(cmdbuf->cmds, cmdbuf->left_margin, visual_line, text,
+ text_nbytes);
+ }
+
+ command_list_set_show_whitespace(cmdbuf->cmds, false);
+
+ if (linewidth < cmdbuf->width) {
+ command_list_draw_repeated(cmdbuf->cmds, linewidth, visual_line, ' ',
+ cmdbuf->width - linewidth);
+ }
+}
+
+void scroll(struct buffer_view *view, int line_delta, int col_delta) {
+ uint32_t nlines = text_num_lines(view->buffer->text);
+ int64_t new_line = (int64_t)view->scroll.line + line_delta;
+ if (new_line >= 0 && new_line < nlines) {
+ view->scroll.line = (uint32_t)new_line;
+ } else if (new_line < 0) {
+ view->scroll.line = 0;
+ }
+
+ int64_t new_col = (int64_t)view->scroll.col + col_delta;
+ if (new_col >= 0 &&
+ new_col < text_line_length(view->buffer->text, view->dot.line)) {
+ view->scroll.col = (uint32_t)new_col;
+ } else if (new_col < 0) {
+ view->scroll.col = 0;
+ }
+}
+
+void to_relative(struct buffer_view *view, uint32_t line, uint32_t col,
+ int64_t *rel_line, int64_t *rel_col) {
+ *rel_col = (int64_t)col - (int64_t)view->scroll.col;
+ *rel_line = (int64_t)line - (int64_t)view->scroll.line;
+}
+
+uint32_t visual_dot_col(struct buffer_view *view, uint32_t dot_col) {
+ uint32_t visual_dot_col = dot_col;
+ struct text_chunk line = text_get_line(view->buffer->text, view->dot.line);
+ for (uint32_t bytei = 0;
+ bytei <
+ text_col_to_byteindex(view->buffer->text, view->dot.line, view->dot.col);
+ ++bytei) {
+ if (line.text[bytei] == '\t') {
+ visual_dot_col += 3;
+ } else if (utf8_byte_is_unicode_start(line.text[bytei])) {
+ wchar_t wc;
+ if (mbrtowc(&wc, (char *)line.text + bytei, 6, NULL) >= 0) {
+ visual_dot_col += wcwidth(wc) - 1;
+ }
+ }
+ }
+
+ return visual_dot_col;
+}
+
+void render_modeline(struct modeline *modeline, struct buffer_view *view,
+ struct command_list *commands, uint32_t width,
+ uint32_t height, uint64_t frame_time) {
+ char buf[width * 4];
+
+ static uint64_t samples[10] = {0};
+ static uint32_t samplei = 0;
+ static uint64_t avg = 0;
+
+ // calc a moving average with a window of the last 10 frames
+ ++samplei;
+ samplei %= 10;
+ avg += 0.1 * (frame_time - samples[samplei]);
+ samples[samplei] = frame_time;
+
+ time_t now = time(NULL);
+ struct tm *lt = localtime(&now);
+ char left[128], right[128];
+
+ snprintf(left, 128, " %c%c %-16s (%d, %d) (%s)",
+ view->buffer->modified ? '*' : '-',
+ view->buffer->readonly ? '%' : '-', view->buffer->name,
+ view->dot.line + 1, visual_dot_col(view, view->dot.col),
+ view->buffer->lang.name);
+ snprintf(right, 128, "(%.2f ms) %02d:%02d", frame_time / 1e6, lt->tm_hour,
+ lt->tm_min);
+
+ snprintf(buf, width * 4, "%s%*s%s", left,
+ (int)(width - (strlen(left) + strlen(right))), "", right);
+
+ if (strcmp(buf, (char *)modeline->buffer) != 0) {
+ modeline->buffer = realloc(modeline->buffer, width * 4);
+ modeline->sz = width * 4;
+ strcpy((char *)modeline->buffer, buf);
+ }
+
+ command_list_set_index_color_bg(commands, 8);
+ command_list_draw_text(commands, 0, height - 1, modeline->buffer,
+ strlen((char *)modeline->buffer));
+ command_list_reset_color(commands);
+}
+
+struct linenumdata {
+ uint32_t longest_nchars;
+ uint32_t dot_line;
+} linenum_data;
+
+void linenum_render_hook(struct text_chunk *line_data, uint32_t line,
+ struct command_list *commands, void *userdata) {
+ struct linenumdata *data = (struct linenumdata *)userdata;
+ static char buf[16];
+ command_list_set_index_color_bg(commands, 236);
+ command_list_set_index_color_fg(
+ commands, line_data->line == data->dot_line ? 253 : 244);
+ uint32_t chars =
+ snprintf(buf, 16, "%*d", data->longest_nchars + 1, line_data->line + 1);
+ command_list_draw_text_copy(commands, 0, line, (uint8_t *)buf, chars);
+ command_list_reset_color(commands);
+ command_list_draw_text(commands, data->longest_nchars + 1, line,
+ (uint8_t *)" ", 1);
+}
+
+void clear_empty_linenum_lines(uint32_t line, struct command_list *commands,
+ void *userdata) {
+ struct linenumdata *data = (struct linenumdata *)userdata;
+ uint32_t longest_nchars = data->longest_nchars;
+ command_list_draw_repeated(commands, 0, line, ' ', longest_nchars + 2);
+}
+
+uint32_t longest_linenum(struct buffer *buffer) {
+ uint32_t total_lines = text_num_lines(buffer->text);
+ uint32_t longest_nchars = 10;
+ if (total_lines < 10) {
+ longest_nchars = 1;
+ } else if (total_lines < 100) {
+ longest_nchars = 2;
+ } else if (total_lines < 1000) {
+ longest_nchars = 3;
+ } else if (total_lines < 10000) {
+ longest_nchars = 4;
+ } else if (total_lines < 100000) {
+ longest_nchars = 5;
+ } else if (total_lines < 1000000) {
+ longest_nchars = 6;
+ } else if (total_lines < 10000000) {
+ longest_nchars = 7;
+ } else if (total_lines < 100000000) {
+ longest_nchars = 8;
+ } else if (total_lines < 1000000000) {
+ longest_nchars = 9;
+ }
+
+ return longest_nchars;
+}
+
+void buffer_update(struct buffer_view *view, uint32_t width, uint32_t height,
+ struct command_list *commands, uint64_t frame_time,
+ uint32_t *relline, uint32_t *relcol) {
+ if (width == 0 || height == 0) {
+ return;
+ }
+
+ uint32_t total_width = width, total_height = height;
+ struct margin total_margins = {0};
+ struct line_render_hook line_hooks[16 + 1];
+ uint32_t nlinehooks = 0;
+ for (uint32_t hooki = 0; hooki < view->buffer->update_hooks.nhooks; ++hooki) {
+ struct update_hook *h = &view->buffer->update_hooks.hooks[hooki];
+ struct update_hook_result res =
+ h->callback(view, commands, width, height, frame_time, h->userdata);
+
+ if (res.line_render_hook.callback != NULL) {
+ line_hooks[nlinehooks] = res.line_render_hook;
+ ++nlinehooks;
+ }
+
+ total_margins.left += res.margins.left;
+ total_margins.right += res.margins.right;
+ total_margins.bottom += res.margins.bottom;
+ total_margins.top += res.margins.top;
+
+ height -= total_margins.top + total_margins.bottom;
+ width -= total_margins.left + total_margins.right;
+ }
+
+ if (view->line_numbers) {
+ linenum_data.longest_nchars = longest_linenum(view->buffer);
+ linenum_data.dot_line = view->dot.line;
+ line_hooks[nlinehooks].callback = linenum_render_hook;
+ line_hooks[nlinehooks].empty_callback = clear_empty_linenum_lines;
+ line_hooks[nlinehooks].userdata = &linenum_data;
+ ++nlinehooks;
+
+ total_margins.left += linenum_data.longest_nchars + 2;
+ }
+
+ if (view->modeline != NULL) {
+ render_modeline(view->modeline, view, commands, width, height, frame_time);
+ total_margins.bottom += 1;
+ }
+
+ height -= total_margins.top + total_margins.bottom;
+ width -= total_margins.left + total_margins.right;
+
+ int64_t rel_line, rel_col;
+ to_relative(view, view->dot.line, view->dot.col, &rel_line, &rel_col);
+ int line_delta = 0, col_delta = 0;
+ if (rel_line < 0) {
+ line_delta = rel_line - ((int)height / 2);
+ } else if (rel_line >= height) {
+ line_delta = (rel_line - height) + height / 2;
+ }
+
+ if (rel_col < 0) {
+ col_delta = rel_col - ((int)width / 2);
+ } else if (rel_col >= width) {
+ col_delta = (rel_col - width) + width / 2;
+ }
+
+ scroll(view, line_delta, col_delta);
+
+ struct setting *show_ws = settings_get("editor.show-whitespace");
+
+ struct cmdbuf cmdbuf = (struct cmdbuf){
+ .cmds = commands,
+ .scroll = view->scroll,
+ .left_margin = total_margins.left,
+ .width = total_width,
+ .line_offset = total_margins.top,
+ .line_render_hooks = line_hooks,
+ .nlinerender_hooks = nlinehooks,
+ .mark_set = view->mark_set,
+ .region = to_region(view->dot, view->mark),
+ .show_ws = show_ws != NULL ? show_ws->value.bool_value : true,
+ };
+ text_for_each_line(view->buffer->text, view->scroll.line, height, render_line,
+ &cmdbuf);
+
+ // draw empty lines
+ uint32_t nlines = text_num_lines(view->buffer->text);
+ for (uint32_t linei = nlines - view->scroll.line + total_margins.top;
+ linei < height; ++linei) {
+
+ for (uint32_t hooki = 0; hooki < nlinehooks; ++hooki) {
+ struct line_render_hook *hook = &line_hooks[hooki];
+ hook->empty_callback(linei, commands, hook->userdata);
+ }
+
+ command_list_draw_repeated(commands, total_margins.left, linei, ' ',
+ total_width - total_margins.left);
+ }
+
+ // update the visual cursor position
+ to_relative(view, view->dot.line, view->dot.col, &rel_line, &rel_col);
+ uint32_t visual_col = visual_dot_col(view, view->dot.col);
+ to_relative(view, view->dot.line, visual_col, &rel_line, &rel_col);
+
+ *relline = rel_line < 0 ? 0 : (uint32_t)rel_line + total_margins.top;
+ *relcol = rel_col < 0 ? 0 : (uint32_t)rel_col + total_margins.left;
+}
+
+struct text_chunk buffer_get_line(struct buffer *buffer, uint32_t line) {
+ return text_get_line(buffer->text, line);
+}
+
+void buffer_view_scroll_down(struct buffer_view *view, uint32_t height) {
+ buffer_goto(view, view->dot.line + height, view->dot.col);
+ scroll(view, height, 0);
+}
+void buffer_view_scroll_up(struct buffer_view *view, uint32_t height) {
+ buffer_goto(view, view->dot.line - height, view->dot.col);
+ scroll(view, -height, 0);
+}
diff --git a/src/dged/buffer.h b/src/dged/buffer.h
new file mode 100644
index 0000000..539c427
--- /dev/null
+++ b/src/dged/buffer.h
@@ -0,0 +1,219 @@
+#include <stddef.h>
+#include <stdint.h>
+#include <stdio.h>
+
+#include "bits/stdint-uintn.h"
+#include "command.h"
+#include "lang.h"
+#include "text.h"
+#include "undo.h"
+#include "window.h"
+
+struct keymap;
+struct command_list;
+
+/**
+ * Margins where buffer text should not be
+ */
+struct margin {
+ uint32_t left;
+ uint32_t right;
+ uint32_t top;
+ uint32_t bottom;
+};
+
+/** Callback for line rendering hooks */
+typedef void (*line_render_cb)(struct text_chunk *line_data, uint32_t line,
+ struct command_list *commands, void *userdata);
+
+typedef void (*line_render_empty_cb)(uint32_t line,
+ struct command_list *commands,
+ void *userdata);
+
+/**
+ * A line render hook
+ *
+ * A callback paired with userdata
+ */
+struct line_render_hook {
+ line_render_cb callback;
+ line_render_empty_cb empty_callback;
+ void *userdata;
+};
+
+/**
+ * Result of updating a buffer hook
+ */
+struct update_hook_result {
+ /** Desired margins for this hook */
+ struct margin margins;
+
+ /** Hook to be added to rendering of buffer lines */
+ struct line_render_hook line_render_hook;
+};
+
+/** Buffer update hook callback function */
+typedef struct update_hook_result (*update_hook_cb)(
+ struct buffer_view *view, struct command_list *commands, uint32_t width,
+ uint32_t height, uint64_t frame_time, void *userdata);
+
+/**
+ * A buffer update hook.
+ *
+ * Can be used to implement custom behavior on top of a buffer. Used for
+ * minibuffer, line numbers, modeline etc.
+ */
+struct update_hook {
+ /** Callback function */
+ update_hook_cb callback;
+
+ /** Optional userdata to pass to the callback function unmodified */
+ void *userdata;
+};
+
+typedef void (*create_hook_cb)(struct buffer *buffer, void *userdata);
+
+/**
+ * A set of update hooks
+ */
+struct update_hooks {
+ /** The update hooks */
+ struct update_hook hooks[32];
+
+ /** The number of update hooks */
+ uint32_t nhooks;
+};
+
+struct buffer_location {
+ uint32_t line;
+ uint32_t col;
+};
+
+struct match {
+ struct buffer_location begin;
+ struct buffer_location end;
+};
+
+struct buffer_view {
+ /** Location of dot (cursor) */
+ struct buffer_location dot;
+
+ /** Location of mark (where a selection starts) */
+ struct buffer_location mark;
+
+ /** Current buffer scroll position */
+ struct buffer_location scroll;
+
+ /** True if the start of a selection has been set */
+ bool mark_set;
+
+ /** Modeline buffer (may be NULL) */
+ struct modeline *modeline;
+
+ bool line_numbers;
+
+ struct buffer *buffer;
+};
+
+struct buffer_view buffer_view_create(struct buffer *buffer, bool modeline,
+ bool line_numbers);
+struct buffer_view buffer_view_clone(struct buffer_view *view);
+
+void buffer_view_scroll_down(struct buffer_view *view, uint32_t height);
+void buffer_view_scroll_up(struct buffer_view *view, uint32_t height);
+
+void buffer_view_destroy(struct buffer_view *view);
+
+/**
+ * A buffer of text that can be modified, read from and written to disk.
+ *
+ * This is the central data structure of dged and most other behavior is
+ * implemented on top of it.
+ */
+struct buffer {
+
+ /** Buffer name */
+ char *name;
+
+ /** Associated filename, this is where the buffer will be saved to */
+ char *filename;
+
+ /** Text data structure */
+ struct text *text;
+
+ /** Buffer update hooks */
+ struct update_hooks update_hooks;
+
+ /** Buffer undo stack */
+ struct undo_stack undo;
+
+ /** Has this buffer been modified from when it was last saved */
+ bool modified;
+
+ /** Can this buffer be changed */
+ bool readonly;
+
+ /** Buffer programming language */
+ struct language lang;
+};
+
+struct buffer buffer_create(char *name);
+void buffer_destroy(struct buffer *buffer);
+
+void buffer_static_init();
+void buffer_static_teardown();
+
+int buffer_add_text(struct buffer_view *view, uint8_t *text, uint32_t nbytes);
+void buffer_set_text(struct buffer *buffer, uint8_t *text, uint32_t nbytes);
+void buffer_clear(struct buffer_view *view);
+bool buffer_is_empty(struct buffer *buffer);
+bool buffer_is_modified(struct buffer *buffer);
+bool buffer_is_readonly(struct buffer *buffer);
+void buffer_set_readonly(struct buffer *buffer, bool readonly);
+
+void buffer_kill_line(struct buffer_view *view);
+void buffer_forward_delete_char(struct buffer_view *view);
+void buffer_backward_delete_char(struct buffer_view *view);
+void buffer_backward_char(struct buffer_view *view);
+void buffer_backward_word(struct buffer_view *view);
+void buffer_forward_char(struct buffer_view *view);
+void buffer_forward_word(struct buffer_view *view);
+void buffer_backward_line(struct buffer_view *view);
+void buffer_forward_line(struct buffer_view *view);
+void buffer_end_of_line(struct buffer_view *view);
+void buffer_beginning_of_line(struct buffer_view *view);
+void buffer_newline(struct buffer_view *view);
+void buffer_indent(struct buffer_view *view);
+
+void buffer_undo(struct buffer_view *view);
+
+void buffer_goto_beginning(struct buffer_view *view);
+void buffer_goto_end(struct buffer_view *view);
+void buffer_goto(struct buffer_view *view, uint32_t line, uint32_t col);
+
+void buffer_find(struct buffer *buffer, const char *pattern,
+ struct match **matches, uint32_t *nmatches);
+
+void buffer_set_mark(struct buffer_view *view);
+void buffer_clear_mark(struct buffer_view *view);
+void buffer_set_mark_at(struct buffer_view *view, uint32_t line, uint32_t col);
+
+void buffer_copy(struct buffer_view *view);
+void buffer_paste(struct buffer_view *view);
+void buffer_paste_older(struct buffer_view *view);
+void buffer_cut(struct buffer_view *view);
+
+struct text_chunk buffer_get_line(struct buffer *buffer, uint32_t line);
+
+uint32_t buffer_add_update_hook(struct buffer *buffer, update_hook_cb hook,
+ void *userdata);
+
+uint32_t buffer_add_create_hook(create_hook_cb hook, void *userdata);
+
+struct buffer buffer_from_file(char *filename);
+void buffer_to_file(struct buffer *buffer);
+void buffer_write_to(struct buffer *buffer, const char *filename);
+
+void buffer_update(struct buffer_view *view, uint32_t width, uint32_t height,
+ struct command_list *commands, uint64_t frame_time,
+ uint32_t *relline, uint32_t *relcol);
diff --git a/src/dged/buffers.c b/src/dged/buffers.c
new file mode 100644
index 0000000..38b51b7
--- /dev/null
+++ b/src/dged/buffers.c
@@ -0,0 +1,43 @@
+#include "buffers.h"
+#include "buffer.h"
+
+#include <stdlib.h>
+#include <string.h>
+
+void buffers_init(struct buffers *buffers, uint32_t initial_capacity) {
+ buffers->buffers = calloc(initial_capacity, sizeof(struct buffer));
+ buffers->nbuffers = 0;
+ buffers->capacity = initial_capacity;
+}
+
+struct buffer *buffers_add(struct buffers *buffers, struct buffer buffer) {
+ if (buffers->nbuffers == buffers->capacity) {
+ buffers->capacity *= 2;
+ buffers->buffers =
+ realloc(buffers->buffers, sizeof(struct buffer) * buffers->capacity);
+ }
+
+ buffers->buffers[buffers->nbuffers] = buffer;
+ ++buffers->nbuffers;
+
+ return &buffers->buffers[buffers->nbuffers - 1];
+}
+
+struct buffer *buffers_find(struct buffers *buffers, const char *name) {
+ for (uint32_t bufi = 0; bufi < buffers->nbuffers; ++bufi) {
+ if (strcmp(name, buffers->buffers[bufi].name) == 0) {
+ return &buffers->buffers[bufi];
+ }
+ }
+
+ return NULL;
+}
+
+void buffers_destroy(struct buffers *buffers) {
+ for (uint32_t bufi = 0; bufi < buffers->nbuffers; ++bufi) {
+ buffer_destroy(&buffers->buffers[bufi]);
+ }
+
+ buffers->nbuffers = 0;
+ free(buffers->buffers);
+}
diff --git a/src/dged/buffers.h b/src/dged/buffers.h
new file mode 100644
index 0000000..edf772c
--- /dev/null
+++ b/src/dged/buffers.h
@@ -0,0 +1,17 @@
+#include <stdint.h>
+
+struct buffer;
+
+struct buffers {
+ // TODO: more buffers
+ struct buffer *buffers;
+ uint32_t nbuffers;
+ uint32_t capacity;
+};
+
+void buffers_init(struct buffers *buffers, uint32_t initial_capacity);
+
+struct buffer *buffers_add(struct buffers *buffers, struct buffer buffer);
+struct buffer *buffers_find(struct buffers *buffers, const char *name);
+
+void buffers_destroy(struct buffers *buffers);
diff --git a/src/dged/command.c b/src/dged/command.c
new file mode 100644
index 0000000..9144058
--- /dev/null
+++ b/src/dged/command.c
@@ -0,0 +1,79 @@
+#include "command.h"
+#include "buffer.h"
+#include "buffers.h"
+#include "hash.h"
+#include "hashmap.h"
+#include "minibuffer.h"
+
+#include <string.h>
+
+struct commands command_registry_create(uint32_t capacity) {
+
+ struct commands cmds = {0};
+ HASHMAP_INIT(&cmds.commands, capacity, hash_name);
+ return cmds;
+}
+
+void command_registry_destroy(struct commands *commands) {
+ HASHMAP_DESTROY(&commands->commands);
+}
+
+uint32_t register_command(struct commands *commands, struct command command) {
+ uint32_t hash = 0;
+ HASHMAP_INSERT(&commands->commands, struct command_entry, command.name,
+ command, hash);
+ 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) {
+ HASHMAP_GET(&command_list->commands, struct command_entry, name,
+ struct command * command);
+ return command;
+}
+
+struct command *lookup_command_by_hash(struct commands *commands,
+ uint32_t hash) {
+ HASHMAP_GET_BY_HASH(&commands->commands, struct command_entry, hash,
+ struct command * command);
+ return command;
+}
+
+int32_t execute_command(struct command *command, struct commands *commands,
+ struct window *active_window, struct buffers *buffers,
+ int argc, const char *argv[]) {
+
+ return command->fn(
+ (struct command_ctx){
+ .buffers = buffers,
+ .active_window = active_window,
+ .userdata = command->userdata,
+ .commands = commands,
+ .self = command,
+ .saved_argv = {0},
+ .saved_argc = 0,
+ },
+ argc, argv);
+}
+
+void command_ctx_push_arg(struct command_ctx *ctx, const char *argv) {
+ if (ctx->saved_argc < 64) {
+ ctx->saved_argv[ctx->saved_argc] = strdup(argv);
+ ++ctx->saved_argc;
+ }
+}
+
+void command_ctx_free(struct command_ctx *ctx) {
+ for (uint32_t i = 0; i < ctx->saved_argc; ++i) {
+ free((char *)ctx->saved_argv[i]);
+ }
+
+ ctx->saved_argc = 0;
+}
diff --git a/src/dged/command.h b/src/dged/command.h
new file mode 100644
index 0000000..bbc57f2
--- /dev/null
+++ b/src/dged/command.h
@@ -0,0 +1,179 @@
+#ifndef _COMMAND_H
+#define _COMMAND_H
+
+/** @file command.h
+ * Commands and command registries
+ */
+#include "hashmap.h"
+#include <stdint.h>
+
+struct buffer;
+struct buffers;
+struct window;
+
+/**
+ * Execution context for a command
+ */
+struct command_ctx {
+ /**
+ * The current list of buffers.
+ *
+ * Can be used to insert new buffers or
+ * inspect existing.
+ */
+ struct buffers *buffers;
+
+ /**
+ * The currently active window.
+ */
+ struct window *active_window;
+
+ /**
+ * A registry of available commands.
+ *
+ * Can be used to execute other commands as part of a command implementation.
+ */
+ struct commands *commands;
+
+ /**
+ * The command that is currently being executed
+ */
+ struct command *self;
+
+ /**
+ * User data set up by the command currently being executed.
+ */
+ void *userdata;
+
+ const char *saved_argv[64];
+
+ int saved_argc;
+};
+
+/** A command function callback which holds the implementation of a command */
+typedef int32_t (*command_fn)(struct command_ctx ctx, int argc,
+ const char *argv[]);
+
+/**
+ * A command that can be bound to a key or executed directly
+ */
+struct command {
+ /**
+ * Name of the command
+ *
+ * Used to look the command up for execution and keybinds.
+ */
+ const char *name;
+
+ /**
+ * Implementation of command behavior
+ */
+ command_fn fn;
+
+ /**
+ * Userdata passed to each invocation of the command.
+ */
+ void *userdata;
+};
+
+/**
+ * A command registry
+ */
+HASHMAP_ENTRY_TYPE(command_entry, struct command);
+
+struct commands {
+ HASHMAP(struct command_entry) commands;
+};
+
+/**
+ * Create a new command registry.
+ *
+ * @param[in] capacity The initial capacity for the registry
+ */
+struct commands command_registry_create(uint32_t capacity);
+
+/**
+ * Destroy a command registry.
+ *
+ * This will free all memory associated with stored commands.
+ * @param[in] commands A pointer to a commands structure created by @ref
+ * command_registry_create(uint32_t)
+ */
+void command_registry_destroy(struct commands *commands);
+
+/**
+ * Register a new command in the registry @ref commands.
+ *
+ * @param[in] commands The registry to insert into
+ * @param[in] command The command to insert
+ */
+uint32_t register_command(struct commands *commands, struct command command);
+
+/**
+ * Register multiple commands in the registry @ref commands "command_list".
+ *
+ * @param[in] command_list The registry to insert into
+ * @param[in] commands The commands to insert
+ * @param[in] ncommands Number of commands contained in @ref commands
+ */
+void register_commands(struct commands *command_list, struct command *commands,
+ uint32_t ncommands);
+
+/**
+ * Execute a command and return the result.
+ *
+ * @param[in] command The @ref command to execute
+ * @param[in] commands A @ref command "command registry" to use for context in
+ * the executed command. Can for example be used to implement commands that
+ * execute arbitrary other commands.
+ * @param[in] active_window A @ref window representing the currently active
+ * window in the editor. This provides a way to access the current buffer as
+ * well.
+ * @param[in] buffers The current list of buffers for context. Can be used for
+ * example to create a buffer list.
+ * @param[in] argc Number of arguments to the command.
+ * @param[in] argv The arguments to the command.
+ *
+ * @returns Integer representing the exit status where 0 means success and
+ * anything else means there was an error.
+ */
+int32_t execute_command(struct command *command, struct commands *commands,
+ struct window *active_window, struct buffers *buffers,
+ int argc, const char *argv[]);
+
+/**
+ * Hash the name of a command.
+ *
+ * @param[in] name The command name
+ * @returns An integer representing the hash of the name
+ */
+uint32_t hash_command_name(const char *name);
+
+/**
+ * Lookup a command by name.
+ *
+ * @param[in] commands The @ref commands "command registry" to look for the @ref
+ * command in.
+ * @param[in] name The name of the command to look for
+ * @returns A pointer to the command if found, NULL otherwise.
+ */
+struct command *lookup_command(struct commands *commands, const char *name);
+
+/**
+ * Lookup a command by hash.
+ *
+ * The hash value is expected to have been computed with @ref
+ * hash_command_name(const char* name).
+ *
+ * @param[in] commands The @ref commands "command registry" to look for the @ref
+ * command in.
+ * @param[in] hash The hash value for the name of the command to look for
+ * @returns A pointer to the command if found, NULL otherwise.
+ */
+struct command *lookup_command_by_hash(struct commands *commands,
+ uint32_t hash);
+
+void command_ctx_push_arg(struct command_ctx *ctx, const char *argv);
+void command_ctx_free(struct command_ctx *ctx);
+
+#endif
diff --git a/src/dged/display.c b/src/dged/display.c
new file mode 100644
index 0000000..d9eeb11
--- /dev/null
+++ b/src/dged/display.c
@@ -0,0 +1,386 @@
+#define _DEFAULT_SOURCE
+#include "display.h"
+
+#include "buffer.h"
+
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/ioctl.h>
+#include <termios.h>
+#include <unistd.h>
+
+#define ESC 0x1b
+
+struct display {
+ struct termios term;
+ struct termios orig_term;
+ uint32_t width;
+ uint32_t height;
+};
+
+enum render_cmd_type {
+ RenderCommand_DrawText = 0,
+ RenderCommand_PushFormat = 1,
+ RenderCommand_Repeat = 2,
+ RenderCommand_ClearFormat = 3,
+ RenderCommand_SetShowWhitespace = 4,
+};
+
+struct render_command {
+ enum render_cmd_type type;
+ union {
+ struct draw_text_cmd *draw_txt;
+ struct push_fmt_cmd *push_fmt;
+ struct repeat_cmd *repeat;
+ struct show_ws_cmd *show_ws;
+ };
+};
+
+struct draw_text_cmd {
+ uint32_t col;
+ uint32_t row;
+
+ uint8_t *data;
+ uint32_t len;
+};
+
+struct push_fmt_cmd {
+ uint8_t fmt[64];
+ uint32_t len;
+};
+
+struct repeat_cmd {
+ uint32_t col;
+ uint32_t row;
+ uint8_t c;
+ uint32_t nrepeat;
+};
+
+struct show_ws_cmd {
+ bool show;
+};
+
+struct command_list {
+ struct render_command *cmds;
+ uint64_t ncmds;
+ uint64_t capacity;
+
+ uint32_t xoffset;
+ uint32_t yoffset;
+
+ void *(*allocator)(size_t);
+
+ char name[16];
+};
+
+struct winsize getsize() {
+ struct winsize ws;
+ ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws);
+ return ws;
+}
+
+struct display *display_create() {
+
+ struct winsize ws = getsize();
+
+ // save old settings
+ struct termios orig_term;
+ tcgetattr(0, &orig_term);
+
+ // set terminal to raw mode
+ struct termios term = {0};
+ cfmakeraw(&term);
+
+ tcsetattr(0, TCSADRAIN, &term);
+
+ struct display *d = calloc(1, sizeof(struct display));
+ d->orig_term = orig_term;
+ d->term = term;
+ d->height = ws.ws_row;
+ d->width = ws.ws_col;
+ return d;
+}
+
+void display_resize(struct display *display) {
+ struct winsize sz = getsize();
+ display->width = sz.ws_col;
+ display->height = sz.ws_row;
+}
+
+void display_destroy(struct display *display) {
+ // reset old terminal mode
+ tcsetattr(0, TCSADRAIN, &display->orig_term);
+
+ free(display);
+}
+
+uint32_t display_width(struct display *display) { return display->width; }
+uint32_t display_height(struct display *display) { return display->height; }
+
+void putbyte(uint8_t c) {
+ if (c != '\r') {
+ putc(c, stdout);
+ }
+}
+
+void putbyte_ws(uint8_t c, bool show_whitespace) {
+ if (show_whitespace && c == '\t') {
+ fputs("\x1b[90m → \x1b[39m", stdout);
+ } else if (show_whitespace && c == ' ') {
+ fputs("\x1b[90m·\x1b[39m", stdout);
+ } else {
+ putbyte(c);
+ }
+}
+
+void putbytes(uint8_t *line_bytes, uint32_t line_length, bool show_whitespace) {
+ for (uint32_t bytei = 0; bytei < line_length; ++bytei) {
+ uint8_t byte = line_bytes[bytei];
+ putbyte_ws(byte, show_whitespace);
+ }
+}
+
+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, false);
+}
+
+struct command_list *command_list_create(uint32_t capacity,
+ void *(*allocator)(size_t),
+ uint32_t xoffset, uint32_t yoffset,
+ const char *name) {
+ struct command_list *command_list = allocator(sizeof(struct command_list));
+
+ command_list->capacity = capacity;
+ command_list->ncmds = 0;
+ command_list->xoffset = xoffset;
+ command_list->yoffset = yoffset;
+ strncpy(command_list->name, name, 15);
+
+ command_list->cmds = allocator(sizeof(struct render_command) * capacity);
+ command_list->allocator = allocator;
+
+ return command_list;
+}
+
+struct render_command *add_command(struct command_list *list,
+ enum render_cmd_type tp) {
+ if (list->ncmds == list->capacity) {
+ /* TODO: better. Currently a bit tricky to provide dynamic scaling of this
+ * since it is initially allocated with the frame allocator that does not
+ * support realloc.
+ */
+ return NULL;
+ }
+
+ struct render_command *cmd = &list->cmds[list->ncmds];
+ cmd->type = tp;
+ switch (tp) {
+ case RenderCommand_DrawText:
+ cmd->draw_txt = list->allocator(sizeof(struct draw_text_cmd));
+ break;
+ case RenderCommand_Repeat:
+ cmd->repeat = list->allocator(sizeof(struct repeat_cmd));
+ break;
+ case RenderCommand_PushFormat:
+ cmd->push_fmt = list->allocator(sizeof(struct push_fmt_cmd));
+ break;
+ case RenderCommand_SetShowWhitespace:
+ cmd->show_ws = list->allocator(sizeof(struct show_ws_cmd));
+ break;
+ case RenderCommand_ClearFormat:
+ break;
+ }
+ ++list->ncmds;
+ return cmd;
+}
+
+void command_list_draw_text(struct command_list *list, uint32_t col,
+ uint32_t row, uint8_t *data, uint32_t len) {
+ struct draw_text_cmd *cmd =
+ add_command(list, RenderCommand_DrawText)->draw_txt;
+ cmd->data = data;
+ cmd->col = col;
+ cmd->row = row;
+ cmd->len = len;
+}
+
+void command_list_draw_text_copy(struct command_list *list, uint32_t col,
+ uint32_t row, uint8_t *data, uint32_t len) {
+ uint8_t *bytes = (uint8_t *)list->allocator(len);
+ memcpy(bytes, data, len);
+
+ command_list_draw_text(list, col, row, bytes, len);
+}
+
+void command_list_draw_repeated(struct command_list *list, uint32_t col,
+ uint32_t row, uint8_t c, uint32_t nrepeat) {
+ struct repeat_cmd *cmd = add_command(list, RenderCommand_Repeat)->repeat;
+ cmd->col = col;
+ cmd->row = row;
+ cmd->c = c;
+ cmd->nrepeat = nrepeat;
+}
+
+void command_list_set_index_color_fg(struct command_list *list,
+ uint8_t color_idx) {
+ struct push_fmt_cmd *cmd =
+ add_command(list, RenderCommand_PushFormat)->push_fmt;
+
+ if (color_idx < 8) {
+ cmd->len = snprintf((char *)cmd->fmt, 64, "%d", 30 + color_idx);
+ } else if (color_idx < 16) {
+ cmd->len = snprintf((char *)cmd->fmt, 64, "%d", 90 + color_idx - 8);
+ } else {
+ cmd->len = snprintf((char *)cmd->fmt, 64, "38;5;%d", color_idx);
+ }
+}
+
+void command_list_set_color_fg(struct command_list *list, uint8_t red,
+ uint8_t green, uint8_t blue) {
+ struct push_fmt_cmd *cmd =
+ add_command(list, RenderCommand_PushFormat)->push_fmt;
+ cmd->len = snprintf((char *)cmd->fmt, 64, "38;2;%d;%d;%d", red, green, blue);
+}
+
+void command_list_set_index_color_bg(struct command_list *list,
+ uint8_t color_idx) {
+ struct push_fmt_cmd *cmd =
+ add_command(list, RenderCommand_PushFormat)->push_fmt;
+ if (color_idx < 8) {
+ cmd->len = snprintf((char *)cmd->fmt, 64, "%d", 40 + color_idx);
+ } else if (color_idx < 16) {
+ cmd->len = snprintf((char *)cmd->fmt, 64, "%d", 100 + color_idx - 8);
+ } else {
+ cmd->len = snprintf((char *)cmd->fmt, 64, "48;5;%d", color_idx);
+ }
+}
+
+void command_list_set_color_bg(struct command_list *list, uint8_t red,
+ uint8_t green, uint8_t blue) {
+ struct push_fmt_cmd *cmd =
+ add_command(list, RenderCommand_PushFormat)->push_fmt;
+ cmd->len = snprintf((char *)cmd->fmt, 64, "48;2;%d;%d;%d", red, green, blue);
+}
+
+void command_list_reset_color(struct command_list *list) {
+ add_command(list, RenderCommand_ClearFormat);
+}
+
+void command_list_set_show_whitespace(struct command_list *list, bool show) {
+ add_command(list, RenderCommand_SetShowWhitespace)->show_ws->show = show;
+}
+
+void display_render(struct display *display,
+ struct command_list *command_list) {
+
+ struct command_list *cl = command_list;
+ uint8_t fmt_stack[256] = {0};
+ fmt_stack[0] = ESC;
+ fmt_stack[1] = '[';
+ fmt_stack[2] = '0';
+ uint32_t fmt_stack_len = 3;
+ bool show_whitespace_state = false;
+
+ for (uint64_t cmdi = 0; cmdi < cl->ncmds; ++cmdi) {
+ struct render_command *cmd = &cl->cmds[cmdi];
+ switch (cmd->type) {
+ case RenderCommand_DrawText: {
+ struct draw_text_cmd *txt_cmd = cmd->draw_txt;
+ display_move_cursor(display, txt_cmd->row + cl->yoffset,
+ txt_cmd->col + cl->xoffset);
+ putbytes(fmt_stack, fmt_stack_len, false);
+ putbyte('m');
+ putbytes(txt_cmd->data, txt_cmd->len, show_whitespace_state);
+ break;
+ }
+
+ case RenderCommand_Repeat: {
+ struct repeat_cmd *repeat_cmd = cmd->repeat;
+ display_move_cursor(display, repeat_cmd->row + cl->yoffset,
+ repeat_cmd->col + cl->xoffset);
+ putbytes(fmt_stack, fmt_stack_len, false);
+ putbyte('m');
+ if (show_whitespace_state) {
+ for (uint32_t i = 0; i < repeat_cmd->nrepeat; ++i) {
+ putbyte_ws(repeat_cmd->c, show_whitespace_state);
+ }
+ } else {
+ char *buf = malloc(repeat_cmd->nrepeat + 1);
+ memset(buf, repeat_cmd->c, repeat_cmd->nrepeat);
+ buf[repeat_cmd->nrepeat] = '\0';
+ fputs(buf, stdout);
+ free(buf);
+ }
+ break;
+ }
+
+ case RenderCommand_PushFormat: {
+ struct push_fmt_cmd *fmt_cmd = cmd->push_fmt;
+
+ fmt_stack[fmt_stack_len] = ';';
+ ++fmt_stack_len;
+
+ memcpy(fmt_stack + fmt_stack_len, fmt_cmd->fmt, fmt_cmd->len);
+ fmt_stack_len += fmt_cmd->len;
+ break;
+ }
+
+ case RenderCommand_ClearFormat:
+ fmt_stack_len = 3;
+ break;
+
+ case RenderCommand_SetShowWhitespace:
+ show_whitespace_state = cmd->show_ws->show;
+ break;
+ }
+ }
+}
+
+void hide_cursor() {
+ putbyte(ESC);
+ putbyte('[');
+ putbyte('?');
+ putbyte('2');
+ putbyte('5');
+ putbyte('l');
+}
+
+void show_cursor() {
+ putbyte(ESC);
+ putbyte('[');
+ putbyte('?');
+ putbyte('2');
+ putbyte('5');
+ putbyte('h');
+}
+
+void display_begin_render(struct display *display) { hide_cursor(); }
+void display_end_render(struct display *display) {
+ show_cursor();
+ fflush(stdout);
+}
diff --git a/src/dged/display.h b/src/dged/display.h
new file mode 100644
index 0000000..14dd246
--- /dev/null
+++ b/src/dged/display.h
@@ -0,0 +1,212 @@
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+
+struct display;
+
+struct render_command;
+struct command_list;
+
+/**
+ * Create a new display
+ *
+ * The only implementation of this is currently a termios one.
+ * @returns A pointer to the display.
+ */
+struct display *display_create();
+
+/**
+ * Resize the display
+ *
+ * Resize the display to match the underlying size of the device.
+ * @param display The display to resize.
+ */
+void display_resize(struct display *display);
+
+/**
+ * Destroy the display.
+ *
+ * Clear all resources associated with the display and reset the underlying
+ * device to original state.
+ * @param display The display to destroy.
+ */
+void display_destroy(struct display *display);
+
+/**
+ * Get the current width of the display.
+ *
+ * @param display The display to get width for.
+ * @returns The width in number of chars as a positive integer.
+ */
+uint32_t display_width(struct display *display);
+
+/**
+ * Get the current height of the display.
+ *
+ * @param display The display to get height for.
+ * @returns The height in number of chars as a positive integer.
+ */
+uint32_t display_height(struct display *display);
+
+/**
+ * Clear the display
+ *
+ * This will clear all text from the display.
+ * @param display The display to clear.
+ */
+void display_clear(struct display *display);
+
+/**
+ * Move the cursor to the specified location
+ *
+ * Move the cursor to the specified row and column.
+ * @param display The display to move the cursor for.
+ * @param row The row to move the cursor to.
+ * @param col The col to move the cursor to.
+ */
+void display_move_cursor(struct display *display, uint32_t row, uint32_t col);
+
+/**
+ * Start a render pass on the display.
+ *
+ * A begin_render call can be followed by any number of render calls followed by
+ * a single end_render call.
+ * @param display The display to begin rendering on.
+ */
+void display_begin_render(struct display *display);
+
+/**
+ * Render a command list on the display.
+ *
+ * Render a command list on the given display. A command list is a series of
+ * rendering instructions.
+ * @param display The display to render on.
+ * @param command_list The command list to render.
+ */
+void display_render(struct display *display, struct command_list *command_list);
+
+/**
+ * Finish a render pass on the display.
+ *
+ * This tells the display that rendering is done for now and a flush is
+ * triggered to update the display hardware.
+ * @param display The display to end rendering on.
+ */
+void display_end_render(struct display *display);
+
+/**
+ * Create a new command list.
+ *
+ * A command list records a series of commands for drawing text to a display.
+ * @param capacity The capacity of the command list.
+ * @param allocator Allocation callback to use for data in the command list.
+ * @param xoffset Column offset to apply to all operations in the list.
+ * @param yoffset Row offset to apply to all operations in the list.
+ * @param name Name for the command list. Useful for debugging.
+ * @returns A pointer to the created command list.
+ */
+struct command_list *command_list_create(uint32_t capacity,
+ void *(*allocator)(size_t),
+ uint32_t xoffset, uint32_t yoffset,
+ const char *name);
+
+/**
+ * Enable/disable rendering of whitespace characters.
+ *
+ * ' ' will be rendered with a dot and '\t' as an arrow.
+ * @param list Command list to record command in.
+ * @param show True if whitespace chars should be displayed, false otherwise.
+ */
+void command_list_set_show_whitespace(struct command_list *list, bool show);
+
+/**
+ * Set background color
+ *
+ * Set the background color to use for following draw commands to the specified
+ * index. Note that color indices > 15 might not be supported on all displays.
+ * @param list The command list to record command in.
+ * @param color_idx The color index to use as background (0-255)
+ */
+void command_list_set_index_color_bg(struct command_list *list,
+ uint8_t color_idx);
+
+/**
+ * Set background color
+ *
+ * Set the background color to use for following draw commands to the specified
+ * RGB color. Note that this might not be supported on all displays.
+ * @param list The command list to record command in.
+ * @param red Red value.
+ * @param green Green value.
+ * @param blue Blue value.
+ */
+void command_list_set_color_bg(struct command_list *list, uint8_t red,
+ uint8_t green, uint8_t blue);
+
+/**
+ * Set foreground color
+ *
+ * Set the foreground color to use for following draw commands to the specified
+ * index. Note that color indices > 15 might not be supported on all displays.
+ * @param list The command list to record command in.
+ * @param color_idx The color index to use as foreground (0-255)
+ */
+void command_list_set_index_color_fg(struct command_list *list,
+ uint8_t color_idx);
+
+/**
+ * Set foreground color
+ *
+ * Set the foreground color to use for following draw commands to the specified
+ * RGB color. Note that this might not be supported on all displays.
+ * @param list The command list to record command in.
+ * @param red Red value.
+ * @param green Green value.
+ * @param blue Blue value.
+ */
+void command_list_set_color_fg(struct command_list *list, uint8_t red,
+ uint8_t green, uint8_t blue);
+
+/**
+ * Reset the color and styling information.
+ *
+ * The following draw commands will have their formatting reset to the default.
+ * @param list The command list to record command in.
+ */
+void command_list_reset_color(struct command_list *list);
+
+/**
+ * Draw text
+ *
+ * @param list Command list to record draw command in.
+ * @param col Column to start text at.
+ * @param row Row to start text at.
+ * @param data Bytes to write.
+ * @param len Number of bytes to write.
+ */
+void command_list_draw_text(struct command_list *list, uint32_t col,
+ uint32_t row, uint8_t *data, uint32_t len);
+
+/**
+ * Draw text, copying it to internal storage first.
+ *
+ * @param list Command list to record draw command in.
+ * @param col Column to start text at.
+ * @param row Row to start text at.
+ * @param data Bytes to write.
+ * @param len Number of bytes to write.
+ */
+void command_list_draw_text_copy(struct command_list *list, uint32_t col,
+ uint32_t row, uint8_t *data, uint32_t len);
+
+/**
+ * Draw a repeated character.
+ *
+ * @param list Command list to record draw command in.
+ * @param col Column to start text at.
+ * @param row Row to start text at.
+ * @param c Byte to repeat.
+ * @param nrepeat Number of times to repeat byte.
+ */
+void command_list_draw_repeated(struct command_list *list, uint32_t col,
+ uint32_t row, uint8_t c, uint32_t nrepeat);
diff --git a/src/dged/hash.h b/src/dged/hash.h
new file mode 100644
index 0000000..0fd689b
--- /dev/null
+++ b/src/dged/hash.h
@@ -0,0 +1,11 @@
+#include <stdint.h>
+
+static uint32_t hash_name(const char *s) {
+ unsigned long hash = 5381;
+ int c;
+
+ while ((c = *s++))
+ hash = ((hash << 5) + hash) + c; /* hash * 33 + c */
+
+ return hash;
+}
diff --git a/src/dged/hashmap.h b/src/dged/hashmap.h
new file mode 100644
index 0000000..405c193
--- /dev/null
+++ b/src/dged/hashmap.h
@@ -0,0 +1,86 @@
+#ifndef _HASHMAP_H
+#define _HASHMAP_H
+
+#include "vec.h"
+#include <stdint.h>
+
+#define HASHMAP_ENTRY_TYPE(name, entry) \
+ struct name { \
+ uint32_t key; \
+ entry value; \
+ }
+
+#define HASHMAP(entry) \
+ struct { \
+ VEC(entry) entries; \
+ uint32_t (*hash_fn)(const char *); \
+ }
+
+#define HASHMAP_INIT(map, initial_capacity, hasher) \
+ VEC_INIT(&(map)->entries, initial_capacity) \
+ (map)->hash_fn = hasher;
+
+#define HASHMAP_DESTROY(map) VEC_DESTROY(&(map)->entries)
+
+#define HASHMAP_INSERT(map, type, k, v, hash_var) \
+ uint32_t key = (map)->hash_fn(k); \
+ bool duplicate = false; \
+ VEC_FOR_EACH(&(map)->entries, type *pair) { \
+ if (pair->key == key) { \
+ duplicate = true; \
+ break; \
+ } \
+ } \
+ if (!duplicate) { \
+ VEC_PUSH(&(map)->entries, ((type){.key = key, .value = v})); \
+ } \
+ hash_var = key;
+
+#define HASHMAP_APPEND(map, type, k, var) \
+ uint32_t key = (map)->hash_fn(k); \
+ bool duplicate = false; \
+ VEC_FOR_EACH(&(map)->entries, type *pair) { \
+ if (pair->key == key) { \
+ duplicate = true; \
+ break; \
+ } \
+ } \
+ type *v = NULL; \
+ if (!duplicate) { \
+ VEC_APPEND(&(map)->entries, v); \
+ v->key = key; \
+ } \
+ var = v;
+
+#define HASHMAP_GET(map, type, k, var) \
+ HASHMAP_GET_BY_HASH(map, type, (map)->hash_fn(k), var)
+
+#define HASHMAP_GET_BY_HASH(map, type, h, var) \
+ type *res = NULL; \
+ uint32_t needle = h; \
+ VEC_FOR_EACH(&(map)->entries, type *pair) { \
+ if (needle == pair->key) { \
+ res = pair; \
+ break; \
+ } \
+ } \
+ var = res != NULL ? &(res->value) : NULL;
+
+#define HASHMAP_CONTAINS_KEY(map, key) \
+ uint32_t needle = (map)->hash_fn(key); \
+ bool exists = false; \
+ VEC_FOR_EACH((map)->entries, struct pair *pair) { \
+ if (needle == pair->key) { \
+ exists = true; \
+ break; \
+ } \
+ } \
+ exists
+
+#define HASHMAP_FOR_EACH(map, var) VEC_FOR_EACH_INDEXED(&(map)->entries, var, i)
+
+#define HASHMAP_SIZE(map) VEC_SIZE(&(map)->entries)
+#define HASHMAP_CAPACITY(map) VEC_CAPACITY(&(map)->entries)
+#define HASHMAP_EMPTY(map) HASHMAP_SIZE == 0
+
+#endif
diff --git a/src/dged/keyboard.c b/src/dged/keyboard.c
new file mode 100644
index 0000000..4b142ee
--- /dev/null
+++ b/src/dged/keyboard.c
@@ -0,0 +1,159 @@
+#define _DEFAULT_SOURCE
+#include "keyboard.h"
+#include "reactor.h"
+#include "stdio.h"
+#include "utf8.h"
+
+#include <ctype.h>
+#include <errno.h>
+#include <stdlib.h>
+#include <string.h>
+#include <termios.h>
+#include <unistd.h>
+
+struct keyboard keyboard_create(struct reactor *reactor) {
+ struct termios term;
+ tcgetattr(0, &term);
+
+ // set input to non-blocking
+ term.c_cc[VMIN] = 0;
+ term.c_cc[VTIME] = 0;
+ tcsetattr(0, TCSADRAIN, &term);
+ return keyboard_create_fd(reactor, STDIN_FILENO);
+}
+
+struct keyboard keyboard_create_fd(struct reactor *reactor, int fd) {
+ return (struct keyboard){
+ .fd = fd,
+ .reactor_event_id = reactor_register_interest(reactor, fd, ReadInterest),
+ };
+}
+
+void parse_keys(uint8_t *bytes, uint32_t nbytes, struct key *out_keys,
+ uint32_t *out_nkeys) {
+ // TODO: can be optimized if "bytes" contains no special chars
+ uint32_t nkps = 0;
+ for (uint32_t bytei = 0; bytei < nbytes; ++bytei) {
+ uint8_t b = bytes[bytei];
+ bool has_more = bytei + 1 < nbytes;
+ uint8_t next = has_more ? bytes[bytei + 1] : 0;
+
+ struct key *kp = &out_keys[nkps];
+ if (b == 0x1b) { // meta
+ kp->start = bytei;
+ kp->mod = Meta;
+ } else if (has_more && isalnum(next) && kp->mod & Meta &&
+ (b == '[' ||
+ b == '0')) { // special char (function keys, pgdn, etc)
+ kp->mod = Spec;
+ } else if (b == 0x7f) { // ?
+ kp->mod |= Ctrl;
+ kp->key = '?';
+ kp->start = bytei;
+ kp->end = bytei + 1;
+ ++nkps;
+ } else if (iscntrl(b)) { // ctrl char
+ kp->mod |= Ctrl;
+ kp->key = b | 0x40;
+ kp->start = bytei;
+ kp->end = bytei + 1;
+ ++nkps;
+ } else {
+ if (kp->mod & Spec && b == '~') {
+ // skip tilde in special chars
+ kp->end = bytei + 1;
+ ++nkps;
+ } else if (kp->mod & Meta || kp->mod & Spec) {
+ kp->key = b;
+ kp->end = bytei + 1;
+
+ if (kp->mod & Meta || (kp->mod & Spec && next != '~')) {
+ ++nkps;
+ }
+ } else if (utf8_byte_is_unicode_continuation(b)) {
+ // do nothing for these
+ } else if (utf8_byte_is_unicode_start(b)) { // unicode char
+ kp->mod = None;
+ kp->key = 0;
+ kp->start = bytei;
+ kp->end = bytei + utf8_nbytes(bytes + bytei, nbytes - bytei, 1);
+ ++nkps;
+ } else { // normal ASCII char
+ kp->mod = None;
+ kp->key = b;
+ kp->start = bytei;
+ kp->end = bytei + 1;
+ ++nkps;
+ }
+ }
+ }
+
+ *out_nkeys = nkps;
+}
+
+struct keyboard_update keyboard_update(struct keyboard *kbd,
+ struct reactor *reactor,
+ void *(*frame_alloc)(size_t)) {
+
+ struct keyboard_update upd = (struct keyboard_update){
+ .keys = NULL,
+ .nkeys = 0,
+ .nbytes = 0,
+ .raw = NULL,
+ };
+
+ // check if there is anything to do
+ if (!reactor_poll_event(reactor, kbd->reactor_event_id)) {
+ return upd;
+ }
+
+ // read all input in chunks of `bufsize` bytes
+ const uint32_t bufsize = 128;
+ uint8_t *buf = malloc(bufsize), *writepos = buf;
+ int nbytes = 0, nread = 0;
+ while ((nread = read(kbd->fd, writepos, bufsize)) == bufsize) {
+ nbytes += bufsize;
+ buf = realloc(buf, nbytes + bufsize);
+ writepos = buf + nbytes;
+ }
+
+ nbytes += nread;
+
+ if (nbytes > 0) {
+ upd.nbytes = nbytes;
+ upd.raw = frame_alloc(nbytes);
+ memcpy(upd.raw, buf, nbytes);
+ upd.keys = frame_alloc(sizeof(struct key) * nbytes);
+ memset(upd.keys, 0, sizeof(struct key) * nbytes);
+
+ parse_keys(upd.raw, upd.nbytes, upd.keys, &upd.nkeys);
+ }
+
+ free(buf);
+ return upd;
+}
+
+bool key_equal_char(struct key *key, uint8_t mod, uint8_t c) {
+ return key->key == c && key->mod == mod;
+}
+
+bool key_equal(struct key *key1, struct key *key2) {
+ return key_equal_char(key1, key2->mod, key2->key);
+}
+
+uint32_t key_name(struct key *key, char *buf, size_t capacity) {
+ const char *mod = "";
+ switch (key->mod) {
+ case Ctrl:
+ mod = "c-";
+ break;
+ case Meta:
+ mod = "m-";
+ break;
+ case Spec:
+ mod = "special-";
+ break;
+ }
+
+ snprintf(buf, capacity, "%s%c", mod, tolower(key->key));
+}
diff --git a/src/dged/keyboard.h b/src/dged/keyboard.h
new file mode 100644
index 0000000..e602b69
--- /dev/null
+++ b/src/dged/keyboard.h
@@ -0,0 +1,145 @@
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+
+/**
+ * Key press modifiers
+ *
+ * Modifiers a key press can have.
+ */
+enum modifiers {
+ /** No modifier, bare key press */
+ None = 0,
+
+ /** Ctrl key */
+ Ctrl = 1 << 0,
+
+ /** Meta (Alt) key */
+ Meta = 1 << 1,
+
+ /** Special key (F keys, arrow keys, etc) */
+ Spec = 1 << 2,
+};
+
+/** Backspace key */
+#define BACKSPACE Ctrl, '?'
+/** Tab key */
+#define TAB Ctrl, 'I'
+/** Enter key */
+#define ENTER Ctrl, 'M'
+/** Delete key */
+#define DELETE Spec, '3'
+
+/** Up arrow key */
+#define UP Spec, 'A'
+/** Down arrow key */
+#define DOWN Spec, 'B'
+/** Right arrow key */
+#define RIGHT Spec, 'C'
+/** Left arrow key */
+#define LEFT Spec, 'D'
+
+/**
+ * A key press
+ */
+struct key {
+ /** The key pressed, will be 0 for a unicode char */
+ uint8_t key;
+ /** Modifier keys pressed (or-ed together) */
+ uint8_t mod;
+ /** Index where this key press starts in the raw input buffer */
+ uint8_t start;
+ /** Index where this key press ends in the raw input buffer */
+ uint8_t end;
+};
+
+/**
+ * The keyboard used to input characters.
+ */
+struct keyboard {
+ uint32_t reactor_event_id;
+ int fd;
+};
+
+/**
+ * The result of updating the keyboard
+ */
+struct keyboard_update {
+ /** The key presses */
+ struct key *keys;
+ /** Number of key presses in @ref keys */
+ uint32_t nkeys;
+
+ /** The raw input */
+ uint8_t *raw;
+ /** The number of bytes in the raw input */
+ uint32_t nbytes;
+};
+
+struct reactor;
+
+/**
+ * Create a new keyboard
+ *
+ * @param reactor @ref reactor "Reactor" to use for polling keyboard for
+ * readiness.
+ * @returns The created keyboard.
+ */
+struct keyboard keyboard_create(struct reactor *reactor);
+
+/**
+ * Create a new keyboard, reading input from fd
+ *
+ * @param reactor @ref reactor "Reactor" to use for polling keyboard for
+ * readiness.
+ * @param fd The file descriptor to get input from
+ * @returns The created keyboard.
+ */
+struct keyboard keyboard_create_fd(struct reactor *reactor, int fd);
+
+/**
+ * Update the keyboard.
+ *
+ * This will check the reactor for readiness to avoid blocking. If there is
+ * data, it will be read and converted to key presses.
+ *
+ * @param kbd The @ref keyboard to update.
+ * @param reactor The @ref reactor used when creating the @ref keyboard.
+ * @returns An instance of @ref keyboard_update representing the result of the
+ * update operation.
+ */
+struct keyboard_update keyboard_update(struct keyboard *kbd,
+ struct reactor *reactor,
+ void *(*frame_alloc)(size_t));
+
+/**
+ * Does key represent the same key press as mod and c.
+ *
+ * @param key The key to check.
+ * @param mod Modifier of a key to compare against.
+ * @param c Char of a key to compare against.
+ * @returns true if key represents the same key press as mod together with c,
+ * false otherwise
+ */
+bool key_equal_char(struct key *key, uint8_t mod, uint8_t c);
+
+/**
+ * Does key1 represent the same key press as key2?
+ *
+ * @param key1 First key to compare.
+ * @param key2 Second key to compare.
+ * @returns true if key1 and key2 represents the same key press, false
+ * otherwise.
+ */
+bool key_equal(struct key *key1, struct key *key2);
+
+/**
+ * Get a text representation of a key
+ *
+ * @param key @ref key "Key" to get text representation for.
+ * @param buf character buffer for holding the result.
+ * @param capacity The capacity of buf.
+ *
+ * @returns The number of characters written to buf.
+ */
+uint32_t key_name(struct key *key, char *buf, size_t capacity);
diff --git a/src/dged/lang.c b/src/dged/lang.c
new file mode 100644
index 0000000..6919780
--- /dev/null
+++ b/src/dged/lang.c
@@ -0,0 +1,160 @@
+#include "lang.h"
+#include "minibuffer.h"
+#include "settings.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+void define_lang(const char *name, const char *id, const char *extensions,
+ uint32_t tab_width, const char *lang_srv) {
+ char namebuf[128] = {0};
+
+ size_t offs = snprintf(namebuf, 128, "languages.%s.", id);
+
+ char *b = namebuf + offs;
+ snprintf(b, 128 - offs, "%s", "extensions");
+ settings_register_setting(
+ namebuf, (struct setting_value){.type = Setting_String,
+ .string_value = (char *)extensions});
+
+ snprintf(b, 128 - offs, "%s", "tab-width");
+ settings_register_setting(namebuf,
+ (struct setting_value){.type = Setting_Number,
+ .number_value = tab_width});
+
+ snprintf(b, 128 - offs, "%s", "lang-srv");
+ settings_register_setting(
+ namebuf, (struct setting_value){.type = Setting_String,
+ .string_value = (char *)lang_srv});
+
+ snprintf(b, 128 - offs, "%s", "name");
+ settings_register_setting(
+ namebuf, (struct setting_value){.type = Setting_String,
+ .string_value = (char *)name});
+}
+
+static struct language g_fundamental = {
+ .name = "Fundamental",
+ .tab_width = 4,
+ .lang_srv = NULL,
+};
+
+void languages_init(bool register_default) {
+ if (register_default) {
+ define_lang("C", "c", "c h", 2, "clangd");
+ define_lang("C++", "cxx", "cpp cxx cc c++ hh h", 2, "clangd");
+ define_lang("Rust", "rs", "rs", 4, "rust-analyzer");
+ define_lang("Nix", "nix", "nix", 4, "rnix-lsp");
+ }
+}
+
+struct language lang_from_settings(const char *lang_path) {
+ char setting_name_buf[128] = {0};
+ size_t offs = snprintf(setting_name_buf, 128, "%s.", lang_path);
+ char *b = setting_name_buf + offs;
+
+ struct language l;
+
+ snprintf(b, 128 - offs, "%s", "name");
+ struct setting *name = settings_get(setting_name_buf);
+ l.name = name != NULL ? name->value.string_value : "Unknown";
+
+ snprintf(b, 128 - offs, "%s", "tab-width");
+ struct setting *tab_width = settings_get(setting_name_buf);
+
+ // fall back to global value
+ if (tab_width == NULL) {
+ tab_width = settings_get("editor.tab-width");
+ }
+ l.tab_width = tab_width != NULL ? tab_width->value.number_value : 4;
+
+ snprintf(b, 128 - offs, "%s", "lang-srv");
+ struct setting *lang_srv = settings_get(setting_name_buf);
+ l.lang_srv = lang_srv->value.string_value;
+
+ return l;
+}
+
+void next_ext(const char *curr, const char **nxt, const char **end) {
+ if (curr == NULL) {
+ *nxt = *end = NULL;
+ return;
+ }
+
+ *nxt = curr;
+ *end = curr + strlen(curr);
+
+ const char *spc = strchr(curr, ' ');
+ if (spc != NULL) {
+ *end = spc;
+ }
+}
+
+struct language lang_from_extension(const char *ext) {
+
+ uint32_t extlen = strlen(ext);
+ if (extlen == 0) {
+ return g_fundamental;
+ }
+
+ // get "languages.*" settings
+ struct setting **settings = NULL;
+ uint32_t nsettings = 0;
+ settings_get_prefix("languages.", &settings, &nsettings);
+
+ // find the first one with a matching extension list
+ for (uint32_t i = 0; i < nsettings; ++i) {
+ struct setting *setting = settings[i];
+ char *setting_name = strrchr(setting->path, '.');
+ if (setting_name != NULL &&
+ strncmp(setting_name + 1, "extensions", 10) == 0) {
+ const char *val = setting->value.string_value;
+
+ // go over extensions
+ const char *cext = val, *nxt = NULL, *end = NULL;
+ next_ext(cext, &nxt, &end);
+ while (nxt != end) {
+ if (extlen == (end - nxt) && strncmp(ext, nxt, (end - nxt)) == 0) {
+ char lang_path[128] = {0};
+ strncpy(lang_path, setting->path, setting_name - setting->path);
+
+ free(settings);
+ return lang_from_settings(lang_path);
+ }
+
+ cext = end + 1;
+ next_ext(cext, &nxt, &end);
+ }
+ }
+ }
+
+ free(settings);
+
+ // fall back to fundamental
+ return g_fundamental;
+}
+
+struct language lang_from_id(const char *id) {
+ if (id == NULL || (strlen(id) == 3 && strncmp(id, "fnd", 3) == 0) ||
+ strlen(id) == 0) {
+ return g_fundamental;
+ }
+
+ char lang_path[128] = {0};
+ snprintf(lang_path, 128, "languages.%s", id);
+
+ // check that it exists
+ struct setting **settings = NULL;
+ uint32_t nsettings = 0;
+
+ settings_get_prefix(lang_path, &settings, &nsettings);
+ free(settings);
+
+ if (nsettings > 0) {
+ return lang_from_settings(lang_path);
+ } else {
+ minibuffer_echo_timeout(4, "failed to find language \"%s\"", id);
+ return lang_from_settings("languages.fnd");
+ }
+}
diff --git a/src/dged/lang.h b/src/dged/lang.h
new file mode 100644
index 0000000..984e207
--- /dev/null
+++ b/src/dged/lang.h
@@ -0,0 +1,46 @@
+#ifndef _LANG_H
+#define _LANG_H
+
+#include <stdbool.h>
+#include <stdint.h>
+
+/**
+ * Settings for a programming language.
+ */
+struct language {
+ /** Descriptive name of the programming language */
+ const char *name;
+
+ /** Tab width for indentation */
+ uint32_t tab_width;
+
+ /** Path to the language server */
+ const char *lang_srv;
+};
+
+/**
+ * Initialize languages.
+ *
+ * @param[in] register_default Set to true to register some well known
+ * languages.
+ */
+void languages_init(bool register_default);
+
+/**
+ * Get a language config by file name extension.
+ *
+ * @param[in] ext File extension
+ * @returns A language config instance or the default language if not found.
+ */
+struct language lang_from_extension(const char *ext);
+
+/**
+ * Get a language config by id. The language id is a short (all-lowercase)
+ * string identifying the language.
+ *
+ * @param[in] id The language id.
+ * @returns A language config instance or the default language if not found.
+ */
+struct language lang_from_id(const char *id);
+
+#endif
diff --git a/src/dged/minibuffer.c b/src/dged/minibuffer.c
new file mode 100644
index 0000000..0ff32a8
--- /dev/null
+++ b/src/dged/minibuffer.c
@@ -0,0 +1,223 @@
+#include "minibuffer.h"
+#include "binding.h"
+#include "buffer.h"
+#include "command.h"
+#include "display.h"
+
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+static struct minibuffer {
+ struct buffer *buffer;
+ struct timespec expires;
+
+ char prompt[128];
+ struct command_ctx prompt_command_ctx;
+ bool prompt_active;
+ bool clear;
+
+ void (*update_callback)();
+
+} g_minibuffer = {0};
+
+void draw_prompt(struct command_list *commands, void *userdata) {
+ uint32_t len = strlen(g_minibuffer.prompt);
+ command_list_set_index_color_fg(commands, 4);
+ command_list_draw_text(commands, 0, 0, (uint8_t *)g_minibuffer.prompt, len);
+ command_list_reset_color(commands);
+}
+
+int32_t minibuffer_execute() {
+ if (g_minibuffer.prompt_active) {
+ struct command_ctx *c = &g_minibuffer.prompt_command_ctx;
+
+ struct text_chunk line = minibuffer_content();
+ char *l = (char *)malloc(line.nbytes + 1);
+ memcpy(l, line.text, line.nbytes);
+ l[line.nbytes] = '\0';
+
+ // propagate any saved arguments
+ char *argv[64];
+ for (uint32_t i = 0; i < c->saved_argc; ++i) {
+ argv[i] = (char *)c->saved_argv[i];
+ }
+ argv[c->saved_argc] = l;
+ uint32_t argc = c->saved_argc + (line.nbytes > 0 ? 1 : 0);
+
+ // split on ' '
+ for (uint32_t bytei = 0; bytei < line.nbytes; ++bytei) {
+ uint8_t byte = line.text[bytei];
+ if (byte == ' ' && argc < 64) {
+ l[bytei] = '\0';
+ argv[argc] = l + bytei + 1;
+ ++argc;
+ }
+ }
+
+ minibuffer_abort_prompt();
+ int32_t res = execute_command(c->self, c->commands, c->active_window,
+ c->buffers, argc, (const char **)argv);
+
+ free(l);
+
+ return res;
+ } else {
+ return 0;
+ }
+}
+
+struct update_hook_result update(struct buffer_view *view,
+ struct command_list *commands, uint32_t width,
+ uint32_t height, uint64_t frame_time,
+ void *userdata) {
+ struct timespec current;
+ struct minibuffer *mb = (struct minibuffer *)userdata;
+ clock_gettime(CLOCK_MONOTONIC, &current);
+ if ((!mb->prompt_active && current.tv_sec >= mb->expires.tv_sec) ||
+ mb->clear) {
+ buffer_clear(view);
+ mb->clear = false;
+ }
+
+ struct update_hook_result res = {0};
+ if (mb->prompt_active) {
+ res.margins.left = strlen(mb->prompt);
+ draw_prompt(commands, NULL);
+ }
+
+ if (mb->update_callback != NULL) {
+ mb->update_callback();
+ }
+
+ return res;
+}
+
+void minibuffer_init(struct buffer *buffer) {
+ if (g_minibuffer.buffer != NULL) {
+ return;
+ }
+
+ g_minibuffer.buffer = buffer;
+ g_minibuffer.clear = false;
+ buffer_add_update_hook(g_minibuffer.buffer, update, &g_minibuffer);
+}
+
+void echo(uint32_t timeout, const char *fmt, va_list args) {
+ if (g_minibuffer.prompt_active || g_minibuffer.buffer == NULL) {
+ return;
+ }
+
+ char buff[2048];
+ size_t nbytes = vsnprintf(buff, 2048, fmt, args);
+
+ // vsnprintf returns how many characters it would have wanted to write in case
+ // of overflow
+ buffer_set_text(g_minibuffer.buffer, (uint8_t *)buff,
+ nbytes > 2048 ? 2048 : nbytes);
+ g_minibuffer.clear = false;
+
+ clock_gettime(CLOCK_MONOTONIC, &g_minibuffer.expires);
+ g_minibuffer.expires.tv_sec += timeout;
+}
+
+void minibuffer_destroy() {
+ command_ctx_free(&g_minibuffer.prompt_command_ctx);
+}
+
+struct text_chunk minibuffer_content() {
+ return buffer_get_line(g_minibuffer.buffer, 0);
+}
+
+struct buffer *minibuffer_buffer() {
+ return g_minibuffer.buffer;
+}
+
+void minibuffer_echo(const char *fmt, ...) {
+ va_list args;
+ va_start(args, fmt);
+ echo(1000, fmt, args);
+ va_end(args);
+}
+
+void minibuffer_echo_timeout(uint32_t timeout, const char *fmt, ...) {
+ va_list args;
+ va_start(args, fmt);
+ echo(timeout, fmt, args);
+ va_end(args);
+}
+
+void minibuffer_set_prompt_internal(const char *fmt, va_list args) {
+ vsnprintf(g_minibuffer.prompt, sizeof(g_minibuffer.prompt), fmt, args);
+}
+
+int32_t minibuffer_prompt_internal(struct command_ctx command_ctx,
+ void (*update_callback)(), const char *fmt,
+ va_list args) {
+ if (g_minibuffer.buffer == NULL) {
+ return 1;
+ }
+
+ minibuffer_clear();
+ g_minibuffer.prompt_active = true;
+
+ command_ctx_free(&g_minibuffer.prompt_command_ctx);
+ g_minibuffer.prompt_command_ctx = command_ctx;
+
+ minibuffer_set_prompt_internal(fmt, args);
+
+ if (update_callback != NULL) {
+ g_minibuffer.update_callback = update_callback;
+ }
+
+ return 0;
+}
+
+int32_t minibuffer_prompt(struct command_ctx command_ctx, const char *fmt,
+ ...) {
+ va_list args;
+ va_start(args, fmt);
+ int32_t r = minibuffer_prompt_internal(command_ctx, NULL, fmt, args);
+ va_end(args);
+
+ return r;
+}
+
+int32_t minibuffer_prompt_interactive(struct command_ctx command_ctx,
+ void (*update_callback)(),
+ const char *fmt, ...) {
+ va_list args;
+ va_start(args, fmt);
+ int32_t r =
+ minibuffer_prompt_internal(command_ctx, update_callback, fmt, args);
+ va_end(args);
+
+ return r;
+}
+
+void minibuffer_set_prompt(const char *fmt, ...) {
+ va_list args;
+ va_start(args, fmt);
+ minibuffer_set_prompt_internal(fmt, args);
+ va_end(args);
+}
+
+void minibuffer_abort_prompt() {
+ minibuffer_clear();
+ g_minibuffer.update_callback = NULL;
+ g_minibuffer.prompt_active = false;
+}
+
+bool minibuffer_empty() { return !minibuffer_displaying(); }
+
+bool minibuffer_displaying() {
+ return g_minibuffer.buffer != NULL && !buffer_is_empty(g_minibuffer.buffer);
+}
+
+void minibuffer_clear() {
+ g_minibuffer.expires.tv_sec = 0;
+ g_minibuffer.clear = true;
+}
+
+bool minibuffer_focused() { return g_minibuffer.prompt_active; }
diff --git a/src/dged/minibuffer.h b/src/dged/minibuffer.h
new file mode 100644
index 0000000..24f54cf
--- /dev/null
+++ b/src/dged/minibuffer.h
@@ -0,0 +1,108 @@
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <time.h>
+
+struct buffer;
+struct command_ctx;
+struct keymap;
+
+/**
+ * Initialize the minibuffer.
+ *
+ * Note that the minibuffer is a global instance and this function will do
+ * nothing if called more than once.
+ * @param buffer underlying buffer to use for text IO in the minibuffer.
+ */
+void minibuffer_init(struct buffer *buffer);
+
+/**
+ * Destroy the minibuffer
+ *
+ * Note that this does not release the buffer used.
+ */
+void minibuffer_destroy();
+
+struct text_chunk minibuffer_content();
+
+struct buffer *minibuffer_buffer();
+
+/**
+ * Echo a message to the minibuffer.
+ *
+ * @param fmt Format string for the message.
+ * @param ... Format arguments.
+ */
+void minibuffer_echo(const char *fmt, ...);
+
+/**
+ * Echo a message to the minibuffer that disappears after @ref timeout.
+ *
+ * @param timeout The timeout in seconds after which the message should
+ * disappear.
+ * @param fmt Format string for the message.
+ * @param ... Format arguments.
+ */
+void minibuffer_echo_timeout(uint32_t timeout, const char *fmt, ...);
+
+/**
+ * Prompt for user input in the minibuffer.
+ *
+ * This will move focus to the minibuffer and wait for user input, with the
+ * given prompt.
+ * @param command_ctx The command context to use to re-execute the calling
+ * command (or other command) when the user confirms the input.
+ * @param fmt Format string for the prompt.
+ * @param ... Format arguments.
+ * @returns 0 on success.
+ */
+int32_t minibuffer_prompt(struct command_ctx command_ctx, const char *fmt, ...);
+
+int32_t minibuffer_prompt_interactive(struct command_ctx command_ctx,
+ void (*update_callback)(),
+ const char *fmt, ...);
+
+void minibuffer_set_prompt(const char *fmt, ...);
+
+/**
+ * Evaluate the current contents of the minibuffer
+ *
+ * @returns zero on success, non-zero to indicate failure
+ */
+int32_t minibuffer_execute();
+
+/**
+ * Abort the current minibuffer prompt.
+ *
+ * This returns focus to the previously focused window.
+ */
+void minibuffer_abort_prompt();
+
+/**
+ * Minibuffer prompt args
+ */
+struct minibuffer_prompt_args {
+ int argc;
+ const char **argv;
+};
+
+/**
+ * Clear the current text in the minibuffer.
+ */
+void minibuffer_clear();
+
+bool minibuffer_empty();
+
+/**
+ * Is the minibuffer currently displaying something?
+ *
+ * @returns True if the minibuffer is displaying anything, false otherwise.
+ */
+bool minibuffer_displaying();
+
+/**
+ * Is the minibuffer currently focused?
+ *
+ * @returns True if the minibuffer is currently focused, receiving user input.
+ */
+bool minibuffer_focused();
diff --git a/src/dged/reactor-epoll.c b/src/dged/reactor-epoll.c
new file mode 100644
index 0000000..e488fef
--- /dev/null
+++ b/src/dged/reactor-epoll.c
@@ -0,0 +1,76 @@
+#include "reactor.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/epoll.h>
+
+struct reactor {
+ int epoll_fd;
+ void *events;
+};
+
+struct events {
+ struct epoll_event events[10];
+ uint32_t nevents;
+};
+
+struct reactor *reactor_create() {
+ int epollfd = epoll_create1(0);
+ if (epollfd == -1) {
+ perror("epoll_create1");
+ }
+
+ struct reactor *r = (struct reactor *)calloc(1, sizeof(struct reactor));
+ r->epoll_fd = epollfd;
+ r->events = calloc(1, sizeof(struct events));
+
+ return r;
+}
+
+void reactor_destroy(struct reactor *reactor) {
+ free(reactor->events);
+ free(reactor);
+}
+
+uint32_t reactor_register_interest(struct reactor *reactor, int fd,
+ enum interest interest) {
+ struct epoll_event ev;
+ ev.events = 0;
+ ev.events |= (interest & ReadInterest) != 0 ? EPOLLIN : 0;
+ ev.events |= (interest & WriteInterest) != 0 ? EPOLLOUT : 0;
+ ev.data.fd = fd;
+ if (epoll_ctl(reactor->epoll_fd, EPOLL_CTL_ADD, fd, &ev) < 0) {
+ perror("epoll_ctl");
+ return -1;
+ }
+
+ return fd;
+}
+
+void reactor_unregister_interest(struct reactor *reactor, uint32_t ev_id) {
+ epoll_ctl(reactor->epoll_fd, EPOLL_CTL_DEL, ev_id, NULL);
+}
+
+bool reactor_poll_event(struct reactor *reactor, uint32_t ev_id) {
+ struct events *events = (struct events *)reactor->events;
+ for (uint32_t ei = 0; ei < events->nevents; ++ei) {
+ struct epoll_event *ev = &events->events[ei];
+
+ if (ev->data.fd == ev_id) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+void reactor_update(struct reactor *reactor) {
+ struct events *events = (struct events *)reactor->events;
+ int nfds = epoll_wait(reactor->epoll_fd, events->events, 10, -1);
+
+ if (nfds == -1) {
+ // TODO: log failure
+ }
+
+ events->nevents = nfds;
+}
diff --git a/src/dged/reactor.h b/src/dged/reactor.h
new file mode 100644
index 0000000..e54afda
--- /dev/null
+++ b/src/dged/reactor.h
@@ -0,0 +1,17 @@
+#include <stdbool.h>
+#include <stdint.h>
+
+enum interest {
+ ReadInterest = 1,
+ WriteInterest = 2,
+};
+
+struct reactor;
+
+struct reactor *reactor_create();
+void reactor_destroy(struct reactor *reactor);
+void reactor_update(struct reactor *reactor);
+bool reactor_poll_event(struct reactor *reactor, uint32_t ev_id);
+uint32_t reactor_register_interest(struct reactor *reactor, int fd,
+ enum interest interest);
+void reactor_unregister_interest(struct reactor *reactor, uint32_t ev_id);
diff --git a/src/dged/settings.c b/src/dged/settings.c
new file mode 100644
index 0000000..524aa9b
--- /dev/null
+++ b/src/dged/settings.c
@@ -0,0 +1,95 @@
+#include "settings.h"
+#include "command.h"
+#include "hash.h"
+#include "hashmap.h"
+#include "minibuffer.h"
+#include "vec.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+static struct settings g_settings = {0};
+
+void settings_init(uint32_t initial_capacity) {
+ HASHMAP_INIT(&g_settings.settings, initial_capacity, hash_name);
+}
+
+void settings_destroy() {
+ HASHMAP_FOR_EACH(&g_settings.settings, struct setting_entry * entry) {
+ struct setting *setting = &entry->value;
+ if (setting->value.type == Setting_String) {
+ free(setting->value.string_value);
+ }
+ }
+
+ HASHMAP_DESTROY(&g_settings.settings);
+}
+
+void setting_set_value(struct setting *setting, struct setting_value val) {
+ if (setting->value.type == val.type) {
+ if (setting->value.type == Setting_String && val.string_value != NULL) {
+ setting->value.string_value = strdup(val.string_value);
+ } else {
+ setting->value = val;
+ }
+ }
+}
+
+void settings_register_setting(const char *path,
+ struct setting_value default_value) {
+ HASHMAP_APPEND(&g_settings.settings, struct setting_entry, path,
+ struct setting_entry * s);
+
+ if (s != NULL) {
+ struct setting *new_setting = &s->value;
+ new_setting->value.type = default_value.type;
+ setting_set_value(new_setting, default_value);
+ strncpy(new_setting->path, path, 128);
+ new_setting->path[127] = '\0';
+ }
+}
+
+struct setting *settings_get(const char *path) {
+ HASHMAP_GET(&g_settings.settings, struct setting_entry, path,
+ struct setting * s);
+ return s;
+}
+
+void settings_get_prefix(const char *prefix, struct setting **settings_out[],
+ uint32_t *nsettings_out) {
+
+ uint32_t capacity = 16;
+ VEC(struct setting *) res;
+ VEC_INIT(&res, 16);
+ HASHMAP_FOR_EACH(&g_settings.settings, struct setting_entry * entry) {
+ struct setting *setting = &entry->value;
+ if (strncmp(prefix, setting->path, strlen(prefix)) == 0) {
+ VEC_PUSH(&res, setting);
+ }
+ }
+
+ *nsettings_out = VEC_SIZE(&res);
+ *settings_out = VEC_ENTRIES(&res);
+}
+
+void settings_set(const char *path, struct setting_value value) {
+ struct setting *setting = settings_get(path);
+ if (setting != NULL) {
+ setting_set_value(setting, value);
+ }
+}
+
+void setting_to_string(struct setting *setting, char *buf, size_t n) {
+ switch (setting->value.type) {
+ case Setting_Bool:
+ snprintf(buf, n, "%s", setting->value.bool_value ? "true" : "false");
+ break;
+ case Setting_Number:
+ snprintf(buf, n, "%ld", setting->value.number_value);
+ break;
+ case Setting_String:
+ snprintf(buf, n, "%s", setting->value.string_value);
+ break;
+ }
+}
diff --git a/src/dged/settings.h b/src/dged/settings.h
new file mode 100644
index 0000000..5d245d9
--- /dev/null
+++ b/src/dged/settings.h
@@ -0,0 +1,137 @@
+#include "hashmap.h"
+
+#include <stdbool.h>
+#include <stdint.h>
+
+struct commands;
+
+/**
+ * The type of setting value.
+ */
+enum setting_type {
+ /** String setting. */
+ Setting_String = 0,
+
+ /** Number setting (a signed 64 bit integer). */
+ Setting_Number,
+
+ /** Boolean setting. */
+ Setting_Bool,
+};
+
+/**
+ * Value for a setting.
+ */
+struct setting_value {
+ /** Type of setting. */
+ enum setting_type type;
+
+ union {
+ /** String setting. */
+ char *string_value;
+
+ /** Real number setting. */
+ int64_t number_value;
+
+ /** Boolean setting value. */
+ bool bool_value;
+ };
+};
+
+/**
+ * A single setting.
+ *
+ * A setting has a "path", denoted by a string
+ * containing a number (0-) of dots.
+ * Example: editor.tab-width.
+ */
+struct setting {
+
+ /** Path of the setting. */
+ char path[128];
+
+ /** Value of the setting. */
+ struct setting_value value;
+};
+
+HASHMAP_ENTRY_TYPE(setting_entry, struct setting);
+
+/**
+ * A collection of settings.
+ */
+struct settings {
+ HASHMAP(struct setting_entry) settings;
+};
+
+/**
+ * Initialize the global collection of settings.
+ *
+ * @param initial_capacity Initial capacity of the settings collection.
+ * @returns Nothing, the settings collection is a global instance.
+ */
+void settings_init(uint32_t initial_capacity);
+
+/**
+ * Destroy the global collection of settings.
+ */
+void settings_destroy();
+
+/**
+ * Register a new setting.
+ *
+ * @param path The path of the new setting on
+ * the form <category>.<sub-category>.<setting-name>.
+ * @param default_value The default value for the setting.
+ * All settings are required to declare a default value.
+ */
+void settings_register_setting(const char *path,
+ struct setting_value default_value);
+
+/**
+ * Retrieve a single setting by path.
+ *
+ * @param path The exact path of the setting on
+ * the form <category>.<sub-category>.<setting-name>.
+ * @returns A pointer to the setting if found, NULL otherwise.
+ */
+struct setting *settings_get(const char *path);
+
+/**
+ * Retrieve a collection of settings by prefix
+ *
+ * @param prefix Path prefix for the settings to retrieve.
+ * @param settings_out Pointer to an array that will be modified to point to the
+ * result.
+ * @param nsettings_out Pointer to an integer that will be set to the number of
+ * settings pointers in the result.
+ */
+void settings_get_prefix(const char *prefix, struct setting **settings_out[],
+ uint32_t *nsettings_out);
+
+/**
+ * Set a value for a setting.
+ *
+ * @param path The exact path of the setting on
+ * the form <category>.<sub-category>.<setting-name>.
+ * @param value The new value of the setting. The type has to match the declared
+ * type for the setting. If not, the new value is ignored.
+ */
+void settings_set(const char *path, struct setting_value value);
+
+/**
+ * Set a value for a setting.
+ *
+ * @param setting Pointer to a setting to set.
+ * @param value The new value of the setting. The type has to match the declared
+ * type for the setting. If not, the new value is ignored.
+ */
+void setting_set_value(struct setting *setting, struct setting_value val);
+
+/**
+ * Create a string representation for a setting.
+ *
+ * @param setting Pointer to a setting to turn into a string.
+ * @param buf Character buffer to store resulting string in.
+ * @param n Size in bytes of @ref buf.
+ */
+void setting_to_string(struct setting *setting, char *buf, size_t n);
diff --git a/src/dged/text.c b/src/dged/text.c
new file mode 100644
index 0000000..f8ba72d
--- /dev/null
+++ b/src/dged/text.c
@@ -0,0 +1,496 @@
+#include "text.h"
+
+#include <assert.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "display.h"
+#include "signal.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;
+ txt->nlines = 0;
+
+ 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);
+
+ free(text);
+}
+
+void text_clear(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;
+ }
+
+ text->nlines = 0;
+}
+
+// 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->nbytes;
+ }
+ return utf8_nbytes(line->data, line->nbytes, char_idx);
+}
+
+uint32_t text_col_to_byteindex(struct text *text, uint32_t line, uint32_t col) {
+ return charidx_to_byteidx(&text->lines[line], col);
+}
+
+// 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 text_byteindex_to_col(struct text *text, uint32_t line,
+ uint32_t byteindex) {
+ return byteidx_to_charidx(&text->lines[line], byteindex);
+}
+
+void append_empty_lines(struct text *text, uint32_t numlines) {
+
+ if (text->nlines + numlines >= text->capacity) {
+ text->capacity += text->capacity + numlines > text->capacity * 2
+ ? numlines + 1
+ : text->capacity;
+ text->lines = realloc(text->lines, sizeof(struct line) * text->capacity);
+ }
+
+ for (uint32_t i = 0; i < numlines; ++i) {
+ struct line *nline = &text->lines[text->nlines];
+ nline->data = NULL;
+ nline->nbytes = 0;
+ nline->nchars = 0;
+ nline->flags = 0;
+
+ ++text->nlines;
+ }
+
+ if (text->nlines > text->capacity) {
+ printf("text->nlines: %d, text->capacity: %d\n", text->nlines,
+ text->capacity);
+ raise(SIGTRAP);
+ }
+}
+
+void ensure_line(struct text *text, uint32_t line) {
+ if (line >= text->nlines) {
+ append_empty_lines(text, line - text->nlines + 1);
+ }
+}
+
+// It is assumed that `data` does not contain any \n, that is handled by
+// higher-level functions
+void insert_at(struct text *text, uint32_t line, uint32_t col, uint8_t *data,
+ uint32_t len, uint32_t nchars) {
+
+ if (len == 0) {
+ return;
+ }
+
+ ensure_line(text, line);
+
+ struct line *l = &text->lines[line];
+
+ l->nbytes += len;
+ l->nchars += nchars;
+ l->flags = LineChanged;
+ l->data = realloc(l->data, l->nbytes);
+
+ uint32_t bytei = charidx_to_byteidx(l, col);
+
+ // move following bytes out of the way
+ if (bytei + len < l->nbytes) {
+ uint32_t start = bytei + len;
+ memmove(l->data + start, l->data + bytei, l->nbytes - start);
+ }
+
+ // insert new chars
+ memcpy(l->data + bytei, data, 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; }
+
+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;
+
+ next->data = NULL;
+ line->data = NULL;
+
+ // 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 *)realloc(next->data, next->nbytes);
+ memcpy(next->data, data + bytei, next->nbytes);
+
+ line->data = (uint8_t *)realloc(line->data, 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) {
+ ensure_line(text, line);
+
+ uint32_t newline = line + 1;
+ append_empty_lines(text, 1);
+
+ mark_lines_changed(text, line, text->nlines - line);
+
+ // move following lines out of the way, if there are any
+ if (newline + 1 < text->nlines) {
+ shift_lines(text, newline, 1);
+ }
+
+ // split line if needed
+ split_line(col, &text->lines[line], &text->lines[newline]);
+}
+
+void delete_line(struct text *text, uint32_t line) {
+ if (text->nlines == 0) {
+ return;
+ }
+
+ mark_lines_changed(text, line, text->nlines - line);
+
+ free(text->lines[line].data);
+ text->lines[line].data = NULL;
+
+ if (line + 1 < text->nlines) {
+ shift_lines(text, line + 1, -1);
+ }
+
+ --text->nlines;
+ text->lines[text->nlines].data = NULL;
+ text->lines[text->nlines].nbytes = 0;
+ text->lines[text->nlines].nchars = 0;
+}
+
+void text_insert_at_inner(struct text *text, uint32_t line, uint32_t col,
+ uint8_t *bytes, uint32_t nbytes,
+ uint32_t *lines_added, uint32_t *cols_added,
+ bool force_newline) {
+ uint32_t linelen = 0, start_line = line;
+
+ *cols_added = 0;
+ for (uint32_t bytei = 0; bytei < nbytes; ++bytei) {
+ uint8_t byte = bytes[bytei];
+ if (byte == '\n') {
+ uint8_t *line_data = bytes + (bytei - linelen);
+ uint32_t nchars = utf8_nchars(line_data, linelen);
+
+ insert_at(text, line, col, line_data, linelen, nchars);
+
+ col += nchars;
+
+ // only insert a newline if we have to
+ if (force_newline || linelen == 0 || col < text_line_length(text, line) ||
+ line + 1 < text->nlines) {
+ new_line_at(text, line, col);
+ }
+
+ ++line;
+ linelen = 0;
+ col = 0;
+ } else {
+ ++linelen;
+ }
+ }
+
+ // handle remaining
+ if (linelen > 0) {
+ uint8_t *line_data = bytes + (nbytes - linelen);
+ uint32_t nchars = utf8_nchars(line_data, linelen);
+ insert_at(text, line, col, line_data, linelen, nchars);
+ *cols_added = nchars;
+ }
+
+ *lines_added = line - start_line;
+}
+
+void text_append(struct text *text, uint8_t *bytes, uint32_t nbytes,
+ uint32_t *lines_added, uint32_t *cols_added) {
+ uint32_t line = text->nlines > 0 ? text->nlines - 1 : 0;
+ uint32_t col = text_line_length(text, line);
+
+ text_insert_at_inner(text, line, col, bytes, nbytes, lines_added, cols_added,
+ true);
+}
+
+void text_insert_at(struct text *text, uint32_t line, uint32_t col,
+ uint8_t *bytes, uint32_t nbytes, uint32_t *lines_added,
+ uint32_t *cols_added) {
+ text_insert_at_inner(text, line, col, bytes, nbytes, lines_added, cols_added,
+ false);
+}
+
+void text_delete(struct text *text, uint32_t start_line, uint32_t start_col,
+ uint32_t end_line, uint32_t end_col) {
+
+ if (text->nlines == 0) {
+ return;
+ }
+
+ uint32_t maxline = text->nlines > 0 ? text->nlines - 1 : 0;
+
+ // make sure we stay inside
+ if (start_line > maxline) {
+ start_line = maxline;
+ start_col = text->lines[start_line].nchars > 0
+ ? text->lines[start_line].nchars - 1
+ : 0;
+ }
+
+ if (end_line > maxline) {
+ end_line = maxline;
+ end_col = text->lines[end_line].nchars;
+ }
+
+ struct line *firstline = &text->lines[start_line];
+ struct line *lastline = &text->lines[end_line];
+
+ // clamp column
+ if (start_col > firstline->nchars) {
+ start_col = firstline->nchars > 0 ? firstline->nchars - 1 : 0;
+ }
+
+ // handle deletion of newlines
+ if (end_col > lastline->nchars) {
+ if (end_line + 1 < text->nlines) {
+ end_col = 0;
+ ++end_line;
+ lastline = &text->lines[end_line];
+ } else {
+ end_col = lastline->nchars;
+ }
+ }
+
+ uint32_t bytei = utf8_nbytes(lastline->data, lastline->nbytes, end_col);
+ if (lastline == firstline) {
+ // in this case we can "overwrite"
+ uint32_t dstbytei =
+ utf8_nbytes(firstline->data, firstline->nbytes, start_col);
+ memcpy(firstline->data + dstbytei, lastline->data + bytei,
+ lastline->nbytes - bytei);
+ } else {
+ // otherwise we actually have to copy from the last line
+ insert_at(text, start_line, start_col, lastline->data + bytei,
+ lastline->nbytes - bytei, lastline->nchars - end_col);
+ }
+
+ firstline->nchars = start_col + (lastline->nchars - end_col);
+ firstline->nbytes =
+ utf8_nbytes(firstline->data, firstline->nbytes, start_col) +
+ (lastline->nbytes - bytei);
+
+ // delete full lines, backwards to not shift old, crappy data upwards
+ for (uint32_t linei = end_line >= text->nlines ? end_line - 1 : end_line;
+ linei > start_line; --linei) {
+ delete_line(text, linei);
+ }
+
+ // if this is the last line in the buffer, and it turns out empty, remove it
+ if (firstline->nbytes == 0 && start_line == text->nlines - 1) {
+ delete_line(text, start_line);
+ }
+}
+
+void text_for_each_chunk(struct text *text, chunk_cb callback, void *userdata) {
+ // if representation of text is changed, this can be changed as well
+ text_for_each_line(text, 0, text->nlines, callback, userdata);
+}
+
+void text_for_each_line(struct text *text, uint32_t line, uint32_t nlines,
+ chunk_cb callback, void *userdata) {
+ uint32_t nlines_max =
+ (line + nlines) > text->nlines ? text->nlines : (line + nlines);
+ for (uint32_t li = line; li < nlines_max; ++li) {
+ struct line *src_line = &text->lines[li];
+ struct text_chunk line = (struct text_chunk){
+ .text = src_line->data,
+ .nbytes = src_line->nbytes,
+ .nchars = src_line->nchars,
+ .line = li,
+ };
+ callback(&line, userdata);
+ }
+}
+
+struct text_chunk text_get_line(struct text *text, uint32_t line) {
+ struct line *src_line = &text->lines[line];
+ return (struct text_chunk){
+ .text = src_line->data,
+ .nbytes = src_line->nbytes,
+ .nchars = src_line->nchars,
+ .line = line,
+ };
+}
+
+struct copy_cmd {
+ uint32_t line;
+ uint32_t byteoffset;
+ uint32_t nbytes;
+};
+
+struct text_chunk text_get_region(struct text *text, uint32_t start_line,
+ uint32_t start_col, uint32_t end_line,
+ uint32_t end_col) {
+ if (start_line == end_line && start_col == end_col) {
+ return (struct text_chunk){0};
+ }
+
+ struct line *first_line = &text->lines[start_line];
+ struct line *last_line = &text->lines[end_line];
+
+ if (start_col > first_line->nchars) {
+ return (struct text_chunk){0};
+ }
+
+ // handle copying of newlines
+ if (end_col > last_line->nchars) {
+ ++end_line;
+ end_col = 0;
+ last_line = &text->lines[end_line];
+ }
+
+ uint32_t nlines = end_line - start_line + 1;
+ struct copy_cmd *copy_cmds = calloc(nlines, sizeof(struct copy_cmd));
+
+ uint32_t total_chars = 0, total_bytes = 0;
+ for (uint32_t line = start_line; line <= end_line; ++line) {
+ struct line *l = &text->lines[line];
+ total_chars += l->nchars;
+ total_bytes += l->nbytes;
+
+ struct copy_cmd *cmd = &copy_cmds[line - start_line];
+ cmd->line = line;
+ cmd->byteoffset = 0;
+ cmd->nbytes = l->nbytes;
+ }
+
+ // correct first line
+ struct copy_cmd *cmd_first = &copy_cmds[0];
+ uint32_t byteoff =
+ utf8_nbytes(first_line->data, first_line->nbytes, start_col);
+ cmd_first->byteoffset += byteoff;
+ cmd_first->nbytes -= byteoff;
+ total_bytes -= byteoff;
+ total_chars -= start_col;
+
+ // correct last line
+ struct copy_cmd *cmd_last = &copy_cmds[nlines - 1];
+ uint32_t byteindex = utf8_nbytes(last_line->data, last_line->nbytes, end_col);
+ cmd_last->nbytes -= (last_line->nbytes - byteindex);
+ total_bytes -= (last_line->nbytes - byteindex);
+ total_chars -= (last_line->nchars - end_col);
+
+ uint8_t *data = (uint8_t *)malloc(
+ total_bytes + /* nr of newline chars */ (end_line - start_line));
+
+ // copy data
+ for (uint32_t cmdi = 0, curr = 0; cmdi < nlines; ++cmdi) {
+ struct copy_cmd *c = &copy_cmds[cmdi];
+ struct line *l = &text->lines[c->line];
+ memcpy(data + curr, l->data + c->byteoffset, c->nbytes);
+ curr += c->nbytes;
+
+ if (cmdi != (nlines - 1)) {
+ data[curr] = '\n';
+ ++curr;
+ ++total_bytes;
+ ++total_chars;
+ }
+ }
+
+ free(copy_cmds);
+ return (struct text_chunk){
+ .text = data,
+ .line = 0,
+ .nbytes = total_bytes,
+ .nchars = total_chars,
+ .allocated = true,
+ };
+}
+
+bool text_line_contains_unicode(struct text *text, uint32_t line) {
+ return text->lines[line].nbytes != text->lines[line].nchars;
+}
diff --git a/src/dged/text.h b/src/dged/text.h
new file mode 100644
index 0000000..fbee89b
--- /dev/null
+++ b/src/dged/text.h
@@ -0,0 +1,54 @@
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+
+// opaque so it is easier to change representation to gap, rope etc.
+struct text;
+
+struct render_command;
+
+struct text *text_create(uint32_t initial_capacity);
+void text_destroy(struct text *text);
+
+/**
+ * Clear the text without reclaiming memory
+ */
+void text_clear(struct text *text);
+
+void text_insert_at(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_append(struct text *text, uint8_t *bytes, uint32_t nbytes,
+ uint32_t *lines_added, uint32_t *cols_added);
+
+void text_delete(struct text *text, uint32_t start_line, uint32_t start_col,
+ uint32_t end_line, uint32_t end_col);
+
+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);
+uint32_t text_col_to_byteindex(struct text *text, uint32_t line, uint32_t col);
+uint32_t text_byteindex_to_col(struct text *text, uint32_t line,
+ uint32_t byteindex);
+
+struct text_chunk {
+ uint8_t *text;
+ uint32_t nbytes;
+ uint32_t nchars;
+ uint32_t line;
+ bool allocated;
+};
+
+typedef void (*chunk_cb)(struct text_chunk *chunk, void *userdata);
+void text_for_each_line(struct text *text, uint32_t line, uint32_t nlines,
+ chunk_cb callback, void *userdata);
+
+void text_for_each_chunk(struct text *text, chunk_cb callback, void *userdata);
+
+struct text_chunk text_get_line(struct text *text, uint32_t line);
+struct text_chunk text_get_region(struct text *text, uint32_t start_line,
+ uint32_t start_col, uint32_t end_line,
+ uint32_t end_col);
+
+bool text_line_contains_unicode(struct text *text, uint32_t line);
diff --git a/src/dged/undo.c b/src/dged/undo.c
new file mode 100644
index 0000000..8f00f0f
--- /dev/null
+++ b/src/dged/undo.c
@@ -0,0 +1,181 @@
+#include "undo.h"
+#include "string.h"
+#include "vec.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+
+void undo_init(struct undo_stack *undo, uint32_t initial_capacity) {
+ undo->top = INVALID_TOP;
+ undo->undo_in_progress = false;
+ VEC_INIT(&undo->records, initial_capacity);
+}
+
+void undo_clear(struct undo_stack *undo) {
+ undo->top = INVALID_TOP;
+ VEC_CLEAR(&undo->records);
+}
+
+void undo_destroy(struct undo_stack *undo) {
+ VEC_FOR_EACH(&undo->records, struct undo_record * rec) {
+ if (rec->type == Undo_Delete && rec->delete.data != NULL &&
+ rec->delete.nbytes > 0) {
+ free(rec->delete.data);
+ }
+ }
+
+ undo_clear(undo);
+
+ VEC_DESTROY(&undo->records);
+}
+
+uint32_t undo_push_boundary(struct undo_stack *undo,
+ struct undo_boundary boundary) {
+
+ // we can only have one save point
+ if (boundary.save_point) {
+ VEC_FOR_EACH(&undo->records, struct undo_record * rec) {
+ if (rec->type == Undo_Boundary && rec->boundary.save_point) {
+ rec->boundary.save_point = false;
+ }
+ }
+ }
+
+ VEC_APPEND(&undo->records, struct undo_record * rec);
+ rec->type = Undo_Boundary;
+ rec->boundary = boundary;
+
+ if (!undo->undo_in_progress) {
+ undo->top = VEC_SIZE(&undo->records) - 1;
+ }
+
+ return VEC_SIZE(&undo->records) - 1;
+}
+
+bool pos_equal(struct position *a, struct position *b) {
+ return a->row == b->row && a->col == b->col;
+}
+
+uint32_t undo_push_add(struct undo_stack *undo, struct undo_add add) {
+
+ // "compress"
+ if (!VEC_EMPTY(&undo->records) &&
+ VEC_BACK(&undo->records)->type == Undo_Add &&
+ pos_equal(&VEC_BACK(&undo->records)->add.end, &add.begin)) {
+ VEC_BACK(&undo->records)->add.end = add.end;
+ } else {
+ VEC_APPEND(&undo->records, struct undo_record * rec);
+ rec->type = Undo_Add;
+ rec->add = add;
+ }
+
+ if (!undo->undo_in_progress) {
+ undo->top = VEC_SIZE(&undo->records) - 1;
+ }
+
+ return VEC_SIZE(&undo->records) - 1;
+}
+
+uint32_t undo_push_delete(struct undo_stack *undo, struct undo_delete delete) {
+ VEC_APPEND(&undo->records, struct undo_record * rec);
+ rec->type = Undo_Delete;
+ rec->delete = delete;
+
+ if (!undo->undo_in_progress) {
+ undo->top = VEC_SIZE(&undo->records) - 1;
+ }
+
+ return VEC_SIZE(&undo->records) - 1;
+}
+
+void undo_begin(struct undo_stack *undo) { undo->undo_in_progress = true; }
+
+void undo_next(struct undo_stack *undo, struct undo_record **records_out,
+ uint32_t *nrecords_out) {
+ *nrecords_out = 0;
+ *records_out = NULL;
+
+ if (VEC_EMPTY(&undo->records)) {
+ return;
+ }
+
+ if (undo->top == INVALID_TOP) {
+ // reset back to the top (redo)
+ undo->top = VEC_SIZE(&undo->records) - 1;
+ }
+
+ uint32_t nrecords = 1;
+ struct undo_record *current = &VEC_ENTRIES(&undo->records)[undo->top];
+
+ // skip any leading boundaries
+ while (undo->top > 0 && current->type == Undo_Boundary) {
+ ++nrecords;
+ --undo->top;
+ current = &VEC_ENTRIES(&undo->records)[undo->top];
+ }
+
+ // find the next boundary
+ while (undo->top > 0 && current->type != Undo_Boundary) {
+ ++nrecords;
+ --undo->top;
+ current = &VEC_ENTRIES(&undo->records)[undo->top];
+ }
+
+ if (nrecords > 0) {
+ *records_out = calloc(nrecords, sizeof(struct undo_record));
+ *nrecords_out = nrecords;
+
+ struct undo_record *dest = *records_out;
+
+ // copy backwards
+ for (uint32_t reci = undo->top + nrecords, outi = 0; reci > undo->top;
+ --reci, ++outi) {
+ dest[outi] = VEC_ENTRIES(&undo->records)[reci - 1];
+ }
+ }
+
+ if (undo->top > 0) {
+ --undo->top;
+ } else {
+ undo->top = INVALID_TOP;
+ }
+}
+
+void undo_end(struct undo_stack *undo) { undo->undo_in_progress = false; }
+
+uint32_t undo_size(struct undo_stack *undo) { return VEC_SIZE(&undo->records); }
+uint32_t undo_current_position(struct undo_stack *undo) { return undo->top; }
+
+size_t rec_to_str(struct undo_record *rec, char *buffer, size_t n) {
+ switch (rec->type) {
+ case Undo_Add:
+ return snprintf(buffer, n, "add { begin: (%d, %d) end: (%d, %d)}",
+ rec->add.begin.row, rec->add.begin.col, rec->add.end.row,
+ rec->add.end.col);
+ case Undo_Delete:
+ return snprintf(buffer, n, "delete { pos: (%d, %d), ptr: 0x%p, nbytes: %d}",
+ rec->delete.pos.row, rec->delete.pos.col, rec->delete.data,
+ rec->delete.nbytes);
+ default:
+ return snprintf(buffer, n, "boundary { save_point: %s }",
+ rec->boundary.save_point ? "yes" : "no");
+ }
+}
+
+const char *undo_dump(struct undo_stack *undo) {
+ uint32_t left = 8192;
+ const char *buf = malloc(left);
+ char *pos = (char *)buf;
+ pos[0] = '\0';
+
+ char rec_buf[256];
+ VEC_FOR_EACH_INDEXED(&undo->records, struct undo_record * rec, reci) {
+ rec_to_str(rec, rec_buf, 256);
+ uint32_t written = snprintf(pos, left, "%d: [%s]%s\n", reci, rec_buf,
+ reci == undo->top ? " <- top" : "");
+ left = written > left ? 0 : left - written;
+ pos += written;
+ }
+
+ return buf;
+}
diff --git a/src/dged/undo.h b/src/dged/undo.h
new file mode 100644
index 0000000..1ce3a8a
--- /dev/null
+++ b/src/dged/undo.h
@@ -0,0 +1,66 @@
+#include "vec.h"
+#include <stdbool.h>
+#include <stdint.h>
+
+enum undo_record_type {
+ Undo_Boundary = 1,
+ Undo_Add = 2,
+ Undo_Delete = 3,
+};
+
+struct position {
+ uint32_t row;
+ uint32_t col;
+};
+
+struct undo_boundary {
+ bool save_point;
+};
+
+struct undo_add {
+ struct position begin;
+ struct position end;
+};
+
+struct undo_delete {
+ struct position pos;
+ uint8_t *data;
+ uint32_t nbytes;
+};
+
+struct undo_record {
+ enum undo_record_type type;
+
+ union {
+ struct undo_boundary boundary;
+ struct undo_add add;
+ struct undo_delete delete;
+ };
+};
+
+#define INVALID_TOP -1
+
+struct undo_stack {
+ VEC(struct undo_record) records;
+ uint32_t top;
+ bool undo_in_progress;
+};
+
+void undo_init(struct undo_stack *undo, uint32_t initial_capacity);
+void undo_clear(struct undo_stack *undo);
+void undo_destroy(struct undo_stack *undo);
+
+uint32_t undo_push_boundary(struct undo_stack *undo,
+ struct undo_boundary boundary);
+
+uint32_t undo_push_add(struct undo_stack *undo, struct undo_add add);
+uint32_t undo_push_delete(struct undo_stack *undo, struct undo_delete delete);
+
+void undo_begin(struct undo_stack *undo);
+void undo_next(struct undo_stack *undo, struct undo_record **records_out,
+ uint32_t *nrecords_out);
+void undo_end(struct undo_stack *undo);
+
+uint32_t undo_size(struct undo_stack *undo);
+uint32_t undo_current_position(struct undo_stack *undo);
+const char *undo_dump(struct undo_stack *undo);
diff --git a/src/dged/utf8.c b/src/dged/utf8.c
new file mode 100644
index 0000000..abf5ef7
--- /dev/null
+++ b/src/dged/utf8.c
@@ -0,0 +1,67 @@
+#include "utf8.h"
+
+#include <stdio.h>
+
+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); }
+
+uint32_t utf8_nbytes_in_char(uint8_t byte) {
+ // length of char is the number of leading ones
+ // flip it and count number of leading zeros
+ uint8_t invb = ~byte;
+ return __builtin_clz((uint32_t)invb) - 24;
+}
+
+// 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;
+ uint32_t expected = 0;
+ for (uint32_t bi = 0; bi < nbytes; ++bi) {
+ uint8_t byte = bytes[bi];
+ if (utf8_byte_is_unicode(byte)) {
+ if (utf8_byte_is_unicode_start(byte)) {
+ expected = utf8_nbytes_in_char(byte) - 1;
+ } else { // continuation byte
+ --expected;
+ if (expected == 0) {
+ ++nchars;
+ }
+ }
+ } else { // ascii
+ ++nchars;
+ }
+ }
+ return nchars;
+}
+
+// TODO: grapheme clusters, this uses the number of unicode code points
+uint32_t utf8_nbytes(uint8_t *bytes, uint32_t nbytes, uint32_t nchars) {
+
+ uint32_t bi = 0;
+ uint32_t chars = 0;
+ uint32_t expected = 0;
+
+ while (chars < nchars && bi < nbytes) {
+ uint8_t byte = bytes[bi];
+ if (utf8_byte_is_unicode(byte)) {
+ if (utf8_byte_is_unicode_start(byte)) {
+ expected = utf8_nbytes_in_char(byte) - 1;
+ } else { // continuation char
+ --expected;
+ if (expected == 0) {
+ ++chars;
+ }
+ }
+ } else { // ascii
+ ++chars;
+ }
+
+ ++bi;
+ }
+
+ return bi;
+}
diff --git a/src/dged/utf8.h b/src/dged/utf8.h
new file mode 100644
index 0000000..59a959e
--- /dev/null
+++ b/src/dged/utf8.h
@@ -0,0 +1,17 @@
+#include <stdbool.h>
+#include <stdint.h>
+
+/*!
+ * \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 nbytes, 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);
diff --git a/src/dged/vec.h b/src/dged/vec.h
new file mode 100644
index 0000000..073f978
--- /dev/null
+++ b/src/dged/vec.h
@@ -0,0 +1,63 @@
+#ifndef _VEC_H
+#define _VEC_H
+
+#include <stdlib.h>
+
+#define VEC(entry) \
+ struct { \
+ entry *entries; \
+ uint32_t nentries; \
+ uint32_t capacity; \
+ }
+
+#define VEC_INIT(vec, initial_capacity) \
+ (vec)->entries = malloc(sizeof((vec)->entries[0]) * initial_capacity); \
+ (vec)->capacity = initial_capacity; \
+ (vec)->nentries = 0;
+
+#define VEC_DESTROY(vec) \
+ free((vec)->entries); \
+ (vec)->entries = NULL; \
+ (vec)->capacity = 0; \
+ (vec)->nentries = 0;
+
+#define VEC_GROW(vec, new_size) \
+ if (new_size > (vec)->capacity) { \
+ (vec)->capacity = new_size; \
+ (vec)->entries = realloc((vec)->entries, \
+ (sizeof((vec)->entries[0]) * (vec)->capacity)); \
+ }
+
+#define VEC_PUSH(vec, entry) \
+ if ((vec)->nentries + 1 >= (vec)->capacity) { \
+ VEC_GROW(vec, (vec)->capacity * 2); \
+ } \
+ \
+ (vec)->entries[(vec)->nentries] = entry; \
+ ++(vec)->nentries;
+
+#define VEC_APPEND(vec, var) \
+ if ((vec)->nentries + 1 >= (vec)->capacity) { \
+ VEC_GROW(vec, (vec)->capacity * 2); \
+ } \
+ \
+ var = &((vec)->entries[(vec)->nentries]); \
+ ++(vec)->nentries;
+
+#define VEC_FOR_EACH(vec, var) VEC_FOR_EACH_INDEXED(vec, var, i)
+
+#define VEC_FOR_EACH_INDEXED(vec, var, idx) \
+ for (uint32_t keep = 1, idx = 0, size = (vec)->nentries; \
+ keep && idx != size; keep = !keep, idx++) \
+ for (var = (vec)->entries + idx; keep; keep = !keep)
+
+#define VEC_SIZE(vec) (vec)->nentries
+#define VEC_CAPACITY(vec) (vec)->capacity
+#define VEC_ENTRIES(vec) (vec)->entries
+#define VEC_EMPTY(vec) ((vec)->nentries == 0)
+
+#define VEC_CLEAR(vec) (vec)->nentries = 0
+#define VEC_BACK(vec) \
+ ((vec)->nentries > 0 ? &((vec)->entries[(vec)->nentries - 1]) : NULL)
+
+#endif
diff --git a/src/dged/window.c b/src/dged/window.c
new file mode 100644
index 0000000..f24997c
--- /dev/null
+++ b/src/dged/window.c
@@ -0,0 +1,445 @@
+#include "binding.h"
+#include "btree.h"
+#include "buffer.h"
+#include "command.h"
+#include "display.h"
+#include "minibuffer.h"
+
+enum window_type {
+ Window_Buffer,
+ Window_HSplit,
+ Window_VSplit,
+};
+
+struct window {
+ uint32_t x;
+ uint32_t y;
+ uint32_t width;
+ uint32_t height;
+ enum window_type type;
+ struct buffer_view buffer_view;
+ struct buffer *prev_buffer;
+ struct command_list *commands;
+ uint32_t relline;
+ uint32_t relcol;
+};
+
+BINTREE_ENTRY_TYPE(window_node, struct window);
+
+static struct windows {
+ BINTREE(window_node) windows;
+ struct window_node *active;
+ struct keymap keymap;
+} g_windows;
+
+static struct window g_minibuffer_window;
+
+void windows_init(uint32_t height, uint32_t width,
+ struct buffer *initial_buffer, struct buffer *minibuffer) {
+ BINTREE_INIT(&g_windows.windows);
+
+ g_minibuffer_window = (struct window){
+ .buffer_view = buffer_view_create(minibuffer, false, false),
+ .prev_buffer = NULL,
+ .x = 0,
+ .y = height - 1,
+ .height = 1,
+ .width = width,
+ };
+
+ struct window root_window = (struct window){
+ .buffer_view = buffer_view_create(initial_buffer, true, true),
+ .prev_buffer = NULL,
+ .height = height - 1,
+ .width = width,
+ .x = 0,
+ .y = 0,
+ };
+ BINTREE_SET_ROOT(&g_windows.windows, root_window);
+ g_windows.active = BINTREE_ROOT(&g_windows.windows);
+}
+
+static void window_tree_clear_sub(struct window_node *root_node) {
+ struct window_node *n = root_node;
+ BINTREE_FIRST(n);
+ while (n != NULL) {
+ struct window *w = &BINTREE_VALUE(n);
+ if (w->type == Window_Buffer) {
+ buffer_view_destroy(&w->buffer_view);
+ }
+ BINTREE_NEXT(n);
+ }
+ BINTREE_FREE_NODES(root_node, window_node);
+}
+
+static void window_tree_clear() {
+ window_tree_clear_sub(BINTREE_ROOT(&g_windows.windows));
+}
+
+void windows_destroy() { window_tree_clear(); }
+
+struct window *root_window() {
+ return &BINTREE_VALUE(BINTREE_ROOT(&g_windows.windows));
+}
+
+struct window *minibuffer_window() {
+ return &g_minibuffer_window;
+}
+
+static void window_tree_resize(struct window_node *root, uint32_t height,
+ uint32_t width) {
+
+ /* due to the way tree traversal works, we need to disconnect the subtree from
+ * its potential parent. Otherwise the algorithm will traverse above the root
+ * of the subtree. */
+ struct window_node *orig_parent = BINTREE_PARENT(root);
+ BINTREE_PARENT(root) = NULL;
+
+ struct window *root_window = &BINTREE_VALUE(root);
+ uint32_t width_ratio_percent = (width * 100) / (root_window->width);
+ uint32_t height_ratio_percent = (height * 100) / (root_window->height);
+ root_window->width = width;
+ root_window->height = height;
+
+ struct window_node *n = root;
+ BINTREE_FIRST(n);
+ while (n != NULL) {
+ struct window *w = &BINTREE_VALUE(n);
+ if (BINTREE_PARENT(n) != NULL && n != root) {
+ if (BINTREE_LEFT(BINTREE_PARENT(n)) == n) {
+ // if left child, use scale from root
+ w->width = (width_ratio_percent * w->width) / 100;
+ w->height = (height_ratio_percent * w->height) / 100;
+ } else {
+ // if right child, fill rest of space after left and parent resize
+ struct window *left_sibling =
+ &BINTREE_VALUE(BINTREE_LEFT(BINTREE_PARENT(n)));
+ struct window *parent = &BINTREE_VALUE(BINTREE_PARENT(n));
+
+ w->width = parent->width;
+ w->height = parent->height;
+ if (parent->type == Window_HSplit) {
+ w->y = parent->y + left_sibling->height;
+ w->height -= left_sibling->height;
+ } else {
+ w->x = parent->x + left_sibling->width;
+ w->width -= left_sibling->width;
+ }
+ }
+ }
+ BINTREE_NEXT(n);
+ }
+
+ BINTREE_PARENT(root) = orig_parent;
+}
+
+void windows_resize(uint32_t height, uint32_t width) {
+ g_minibuffer_window.width = width;
+ g_minibuffer_window.y = height - 1;
+
+ window_tree_resize(BINTREE_ROOT(&g_windows.windows), height - 1, width);
+}
+
+void windows_update(void *(*frame_alloc)(size_t), uint64_t frame_time) {
+ struct window_node *n = BINTREE_ROOT(&g_windows.windows);
+ BINTREE_FIRST(n);
+ while (n != NULL) {
+ struct window *w = &BINTREE_VALUE(n);
+ if (w->type == Window_Buffer) {
+ w->commands = command_list_create(w->height * w->width, frame_alloc, w->x,
+ w->y, w->buffer_view.buffer->name);
+
+ buffer_update(&w->buffer_view, w->width, w->height, w->commands,
+ frame_time, &w->relline, &w->relcol);
+ }
+
+ BINTREE_NEXT(n);
+ }
+
+ struct window *w = &g_minibuffer_window;
+ w->commands = command_list_create(w->height * w->width, frame_alloc, w->x,
+ w->y, w->buffer_view.buffer->name);
+ buffer_update(&w->buffer_view, w->width, w->height, w->commands, frame_time,
+ &w->relline, &w->relcol);
+}
+
+void windows_render(struct display *display) {
+ struct window_node *n = BINTREE_ROOT(&g_windows.windows);
+ BINTREE_FIRST(n);
+ while (n != NULL) {
+ struct window *w = &BINTREE_VALUE(n);
+ if (w->type == Window_Buffer) {
+ display_render(display, w->commands);
+ }
+ BINTREE_NEXT(n);
+ }
+
+ display_render(display, g_minibuffer_window.commands);
+}
+
+struct window_node *find_window(struct window *window) {
+ struct window_node *n = BINTREE_ROOT(&g_windows.windows);
+ BINTREE_FIRST(n);
+ while (n != NULL) {
+ struct window *w = &BINTREE_VALUE(n);
+ if (w == window) {
+ return n;
+ }
+ BINTREE_NEXT(n);
+ }
+
+ return NULL;
+}
+
+void windows_set_active(struct window *window) {
+ struct window_node *n = find_window(window);
+ if (n != NULL) {
+ g_windows.active = n;
+ }
+}
+
+struct window *windows_get_active() {
+ return &BINTREE_VALUE(g_windows.active);
+}
+
+void window_set_buffer(struct window *window, struct buffer *buffer) {
+ window->prev_buffer = window->buffer_view.buffer;
+ buffer_view_destroy(&window->buffer_view);
+ window->buffer_view = buffer_view_create(buffer, true, true);
+}
+
+struct buffer *window_buffer(struct window *window) {
+ return window->buffer_view.buffer;
+}
+
+struct buffer_view *window_buffer_view(struct window *window) {
+ return &window->buffer_view;
+}
+
+struct buffer *window_prev_buffer(struct window *window) {
+ return window->prev_buffer;
+}
+
+bool window_has_prev_buffer(struct window *window) {
+ return window->prev_buffer != NULL;
+}
+
+struct buffer_location window_cursor_location(struct window *window) {
+ return (struct buffer_location){
+ .col = window->relcol,
+ .line = window->relline,
+ };
+}
+struct buffer_location window_absolute_cursor_location(struct window *window) {
+ return (struct buffer_location){
+ .col = window->x + window->relcol,
+ .line = window->y + window->relline,
+ };
+}
+
+void window_close(struct window *window) {
+ // do not want to delete last window
+ if (window == root_window()) {
+ return;
+ }
+
+ struct window_node *to_delete = find_window(window);
+ if (to_delete == NULL) {
+ return;
+ }
+
+ // promote other child to parent
+ struct window_node *target = BINTREE_PARENT(to_delete);
+ struct window_node *source = BINTREE_RIGHT(target) == to_delete
+ ? BINTREE_LEFT(target)
+ : BINTREE_RIGHT(target);
+
+ buffer_view_destroy(&window->buffer_view);
+ BINTREE_REMOVE(to_delete);
+ BINTREE_FREE_NODE(to_delete);
+
+ BINTREE_VALUE(source).x = BINTREE_VALUE(target).x;
+ BINTREE_VALUE(source).y = BINTREE_VALUE(target).y;
+ uint32_t target_width = BINTREE_VALUE(target).width;
+ uint32_t target_height = BINTREE_VALUE(target).height;
+
+ // copy the node value and set it's children as children of the target node
+ BINTREE_VALUE(target) = BINTREE_VALUE(source);
+ BINTREE_LEFT(target) = BINTREE_LEFT(source);
+ BINTREE_RIGHT(target) = BINTREE_RIGHT(source);
+
+ // adopt the children
+ if (BINTREE_HAS_LEFT(source)) {
+ BINTREE_PARENT(BINTREE_LEFT(source)) = target;
+ }
+
+ if (BINTREE_HAS_RIGHT(source)) {
+ BINTREE_PARENT(BINTREE_RIGHT(source)) = target;
+ }
+
+ BINTREE_FREE_NODE(source);
+
+ window_tree_resize(target, target_height, target_width);
+ BINTREE_FIRST(target);
+ windows_set_active(&BINTREE_VALUE(target));
+}
+
+void window_close_others(struct window *window) {
+ struct window_node *root = BINTREE_ROOT(&g_windows.windows);
+
+ // copy window and make it suitable as a root window
+ struct window new_root = *window;
+ new_root.x = 0;
+ new_root.y = 0;
+ new_root.buffer_view = buffer_view_clone(&window->buffer_view);
+ new_root.width = BINTREE_VALUE(root).width;
+ new_root.height = BINTREE_VALUE(root).height;
+
+ window_tree_clear();
+
+ // create new root window
+ BINTREE_SET_ROOT(&g_windows.windows, new_root);
+ windows_set_active(&BINTREE_VALUE(BINTREE_ROOT(&g_windows.windows)));
+}
+
+void window_hsplit(struct window *window, struct window **new_window_a,
+ struct window **new_window_b) {
+ struct window_node *n = find_window(window);
+ if (n != NULL) {
+ struct window w = BINTREE_VALUE(n);
+
+ if (w.type == Window_Buffer) {
+ struct window parent = {0};
+ parent.type = Window_HSplit;
+ parent.x = w.x;
+ parent.y = w.y;
+ parent.width = w.width;
+ parent.height = w.height;
+ BINTREE_VALUE(n) = parent;
+
+ /* Reuse the current window as the 'left' child, halving the height */
+ w.height /= 2;
+ BINTREE_INSERT(n, w);
+ *new_window_a = &BINTREE_VALUE(BINTREE_LEFT(n));
+ windows_set_active(*new_window_a);
+
+ /* Create a new window for the split, showing the same buffer as the
+ * original window.
+ */
+ struct window new_window = {0};
+ new_window.type = Window_Buffer;
+ new_window.buffer_view =
+ buffer_view_create(w.buffer_view.buffer, true, true);
+ buffer_goto(&new_window.buffer_view, w.buffer_view.dot.line,
+ w.buffer_view.dot.col);
+ new_window.prev_buffer = w.prev_buffer;
+ new_window.x = w.x;
+ new_window.y = w.y + w.height;
+ new_window.width = w.width;
+ new_window.height = parent.height - w.height;
+ BINTREE_INSERT(n, new_window);
+ *new_window_b = &BINTREE_VALUE(BINTREE_RIGHT(n));
+ }
+ }
+}
+
+void window_vsplit(struct window *window, struct window **new_window_a,
+ struct window **new_window_b) {
+ struct window_node *n = find_window(window);
+ if (n != NULL) {
+ struct window w = BINTREE_VALUE(n);
+
+ if (w.type == Window_Buffer) {
+ /* Create a new split container to use as parent */
+ struct window parent = {0};
+ parent.type = Window_VSplit;
+ parent.x = w.x;
+ parent.y = w.y;
+ parent.width = w.width;
+ parent.height = w.height;
+ BINTREE_VALUE(n) = parent;
+
+ /* Reuse the current window as the 'left' child, halving the width */
+ w.width /= 2;
+ BINTREE_INSERT(n, w);
+ *new_window_a = &BINTREE_VALUE(BINTREE_LEFT(n));
+ windows_set_active(*new_window_a);
+
+ /* Create a new window for the split, showing the same buffer as the
+ * original window.
+ */
+ struct window new_window = {0};
+ new_window.type = Window_Buffer;
+ new_window.buffer_view =
+ buffer_view_create(w.buffer_view.buffer, true, true);
+ buffer_goto(&new_window.buffer_view, w.buffer_view.dot.line,
+ w.buffer_view.dot.col);
+ new_window.prev_buffer = w.prev_buffer;
+ new_window.x = w.x + w.width;
+ new_window.y = w.y;
+ new_window.width = parent.width - w.width;
+ new_window.height = w.height;
+ BINTREE_INSERT(n, new_window);
+ *new_window_b = &BINTREE_VALUE(BINTREE_RIGHT(n));
+ }
+ }
+}
+
+void window_split(struct window *window, struct window **new_window_a,
+ struct window **new_window_b) {
+ /* The height * 2 is a horrible hack, we would need to know how big the font
+ actually is */
+ window->height * 2 > window->width
+ ? window_hsplit(window, new_window_a, new_window_b)
+ : window_vsplit(window, new_window_a, new_window_b);
+}
+
+struct window *windows_focus_next() {
+ struct window *active = windows_get_active();
+ struct window_node *n = find_window(active);
+ BINTREE_NEXT(n);
+ while (n != NULL) {
+ struct window *w = &BINTREE_VALUE(n);
+ if (w->type == Window_Buffer) {
+ windows_set_active(w);
+ return w;
+ }
+ BINTREE_NEXT(n);
+ }
+
+ // we have moved around
+ n = BINTREE_ROOT(&g_windows.windows);
+ BINTREE_FIRST(n);
+ if (n != NULL) {
+ windows_set_active(&BINTREE_VALUE(n));
+ return &BINTREE_VALUE(n);
+ }
+
+ // fall back to root
+ windows_set_active(root_window());
+ return root_window();
+}
+
+struct window *windows_focus(uint32_t id) {
+ uint32_t curr_id = 0;
+
+ struct window_node *n = BINTREE_ROOT(&g_windows.windows);
+ BINTREE_FIRST(n);
+ while (n != NULL) {
+ struct window *w = &BINTREE_VALUE(n);
+ if (w->type == Window_Buffer) {
+ if (curr_id == id) {
+ windows_set_active(w);
+ return w;
+ }
+ ++curr_id;
+ }
+ BINTREE_NEXT(n);
+ }
+
+ return NULL;
+}
+
+uint32_t window_width(struct window *window) { return window->width; }
+
+uint32_t window_height(struct window *window) { return window->height; }
diff --git a/src/dged/window.h b/src/dged/window.h
new file mode 100644
index 0000000..b3284e9
--- /dev/null
+++ b/src/dged/window.h
@@ -0,0 +1,48 @@
+#include <stdbool.h>
+#include <stdint.h>
+
+#include "btree.h"
+
+struct command_list;
+struct display;
+struct keymap;
+struct commands;
+struct buffer;
+
+struct window;
+struct windows;
+
+void windows_init(uint32_t height, uint32_t width,
+ struct buffer *initial_buffer, struct buffer *minibuffer);
+
+void windows_destroy();
+void windows_resize(uint32_t height, uint32_t width);
+void windows_update(void *(*frame_alloc)(size_t), uint64_t frame_time);
+void windows_render(struct display *display);
+
+struct window *root_window();
+struct window *minibuffer_window();
+
+void windows_set_active(struct window *window);
+struct window *windows_focus(uint32_t id);
+struct window *windows_get_active();
+struct window *windows_focus_next();
+
+void window_set_buffer(struct window *window, struct buffer *buffer);
+struct buffer *window_buffer(struct window *window);
+struct buffer_view *window_buffer_view(struct window *window);
+struct buffer *window_prev_buffer(struct window *window);
+bool window_has_prev_buffer(struct window *window);
+struct buffer_location window_cursor_location(struct window *window);
+struct buffer_location window_absolute_cursor_location(struct window *window);
+uint32_t window_width(struct window *window);
+uint32_t window_height(struct window *window);
+
+void window_close(struct window *window);
+void window_close_others(struct window *window);
+void window_split(struct window *window, struct window **new_window_a,
+ struct window **new_window_b);
+void window_hsplit(struct window *window, struct window **new_window_a,
+ struct window **new_window_b);
+void window_vsplit(struct window *window, struct window **new_window_a,
+ struct window **new_window_b);