aboutsummaryrefslogtreecommitdiffstats
path: root/prototypes/connections/edit-tui.c
diff options
context:
space:
mode:
Diffstat (limited to 'prototypes/connections/edit-tui.c')
-rw-r--r--prototypes/connections/edit-tui.c945
1 files changed, 945 insertions, 0 deletions
diff --git a/prototypes/connections/edit-tui.c b/prototypes/connections/edit-tui.c
new file mode 100644
index 0000000..fbd73c1
--- /dev/null
+++ b/prototypes/connections/edit-tui.c
@@ -0,0 +1,945 @@
+#define _XOPEN_SOURCE 700
+/* O_NOFOLLOW is hidden by Darwin's strict X/Open namespace. */
+#ifdef __APPLE__
+#define _DARWIN_C_SOURCE
+#endif
+/* Optional terminal document editor. The program and store effect are the
+ * same ones used by the CLI and HTTP hosts; terminal state stays here. */
+#include "runtime.h"
+#include "store.h"
+#include "store-port.h"
+#ifdef PURE_EMBEDDED_PROGRAM
+#include "embedded-plan.h"
+#endif
+#include <dirent.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <inttypes.h>
+#include <locale.h>
+#include <poll.h>
+#include <signal.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/ioctl.h>
+#include <termios.h>
+#include <unistd.h>
+#include <wchar.h>
+
+typedef struct {
+ char name[65];
+} DocumentKey;
+
+typedef struct {
+ PureRuntime runtime;
+ PureWork work[2];
+ const PureProgram *program;
+ const char *store;
+ char key[65], prompt[65], message[160];
+ unsigned char *text;
+ size_t capacity, gap_start, gap_end, scroll_row, prefix, prompt_size;
+ /* Logical byte position and its visual coordinate in anchor_columns. */
+ size_t anchor_position, anchor_row, anchor_column, anchor_columns;
+ DocumentKey *documents;
+ size_t document_count, document_selected, document_scroll;
+ int64_t revision;
+ bool exists, dirty, prompt_open, list_open, confirm_quit, confirm_reload, anchor_valid;
+ struct termios original_term;
+} Editor;
+
+static volatile sig_atomic_t stopping, resized, wake_write = -1;
+
+static void wake_signal(int signal_number) {
+ int saved_errno = errno;
+ if (signal_number == SIGWINCH) resized = 1;
+ else stopping = 1;
+ if (wake_write >= 0) {
+ const char byte = 1;
+ (void)write((int)wake_write, &byte, 1);
+ }
+ errno = saved_errno;
+}
+
+static bool valid_key(const char *key) {
+ size_t length = strlen(key);
+ if (!length || length > 64) return false;
+ for (size_t i = 0; i < length; ++i) {
+ unsigned char c = (unsigned char)key[i];
+ if (!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
+ (c >= '0' && c <= '9') || c == '_' || c == '-')) return false;
+ }
+ return true;
+}
+
+static int compare_document_key(const void *left, const void *right) {
+ return strcmp(((const DocumentKey *)left)->name, ((const DocumentKey *)right)->name);
+}
+
+static void close_list(Editor *editor) {
+ free(editor->documents);
+ editor->documents = NULL;
+ editor->document_count = editor->document_selected = editor->document_scroll = 0;
+ editor->prompt_size = 0;
+ editor->prompt[0] = 0;
+ editor->list_open = false;
+}
+
+/* Names are already sorted; find the first key with this prefix in-place. */
+static bool find_list_prefix(Editor *editor) {
+ if (!editor->prompt_size) return true;
+ size_t first = 0, end = editor->document_count;
+ while (first < end) {
+ size_t middle = first + (end - first) / 2;
+ if (strcmp(editor->documents[middle].name, editor->prompt) < 0) first = middle + 1;
+ else end = middle;
+ }
+ if (first == editor->document_count ||
+ strncmp(editor->documents[first].name, editor->prompt, editor->prompt_size)) return false;
+ editor->document_selected = first;
+ return true;
+}
+
+/* List names from the selected byte store, not from the execution core.
+ * Opening a name still goes through the store's checked READ path. Bound the
+ * optional browser to 8,192 names (520 KiB of key storage). */
+static bool open_list(Editor *editor) {
+ int descriptor = open(editor->store, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
+ DIR *directory = descriptor >= 0 ? fdopendir(descriptor) : NULL;
+ if (!directory) {
+ if (descriptor >= 0) close(descriptor);
+ snprintf(editor->message, sizeof(editor->message), "Could not read document directory");
+ return false;
+ }
+ DocumentKey *items = NULL;
+ size_t count = 0, capacity = 0;
+ bool complete = true;
+ for (;;) {
+ errno = 0;
+ struct dirent *entry = readdir(directory);
+ if (!entry) { if (errno) complete = false; break; }
+ if (strncmp(entry->d_name, "obj-", 4) || !valid_key(entry->d_name + 4)) continue;
+ if (count == 8192) { complete = false; break; }
+ if (count == capacity) {
+ size_t next = capacity ? capacity * 2 : 32;
+ DocumentKey *grown = realloc(items, next * sizeof(*grown));
+ if (!grown) { complete = false; break; }
+ items = grown;
+ capacity = next;
+ }
+ memcpy(items[count++].name, entry->d_name + 4, strlen(entry->d_name + 4) + 1);
+ }
+ if (closedir(directory)) complete = false;
+ if (!complete) {
+ free(items);
+ snprintf(editor->message, sizeof(editor->message), "Document list exceeds 8,192 keys or cannot be read");
+ return false;
+ }
+ if (count > 1) qsort(items, count, sizeof(*items), compare_document_key);
+ close_list(editor);
+ editor->documents = items;
+ editor->document_count = count;
+ for (size_t i = 0; i < count; ++i)
+ if (!strcmp(items[i].name, editor->key)) { editor->document_selected = i; break; }
+ editor->list_open = true;
+ return true;
+}
+
+static const char *result_name(PureResult result) {
+ switch (result) {
+ case PURE_NOT_FOUND: return "not found";
+ case PURE_CONFLICT: return "version conflict";
+ case PURE_BUSY: return "busy";
+ case PURE_BUDGET: return "request budget exceeded";
+ case PURE_LIMIT: return "document limit or invalid range";
+ case PURE_UNCERTAIN: return "durability unknown; inspect the stored revision before retrying";
+ case PURE_DENIED: return "operation denied";
+ case PURE_ALLOCATION: return "allocation failed";
+ default: return "operation failed";
+ }
+}
+
+static bool edit_layout(const PureProgram *program, size_t *prefix) {
+ if (!program->splice || !program->write.to_end || program->write_step ||
+ program->input_steps || !program->splice_offset.from_u32le ||
+ !program->splice_remove.from_u32le || program->read.offset ||
+ !program->read.to_end || program->read_required) return false;
+ size_t start = program->splice_offset.value, remove = program->splice_remove.value;
+ *prefix = program->write.offset;
+ if (*prefix > PURE_OBJECT_LIMIT || program->input_required > *prefix ||
+ start > *prefix || remove > *prefix || *prefix - start < 4 ||
+ *prefix - remove < 4) return false;
+ return start < remove ? remove - start >= 4 : start - remove >= 4;
+}
+
+static void put32(unsigned char *to, uint32_t value) {
+ for (unsigned i = 0; i < 4; ++i) to[i] = (unsigned char)(value >> (8 * i));
+}
+
+static PureOutcome finish(Editor *editor, PureTicket ticket) {
+ PureOutcome result;
+ while (!pure_runtime_result(ticket, &result))
+ if (!pure_runtime_step(&editor->runtime)) return (PureOutcome){.status = PURE_INVALID};
+ return result;
+}
+
+static size_t length(const Editor *editor) {
+ return editor->capacity - (editor->gap_end - editor->gap_start);
+}
+
+static unsigned char byte_at(const Editor *editor, size_t index) {
+ return editor->text[index < editor->gap_start ? index :
+ index + editor->gap_end - editor->gap_start];
+}
+
+/* Skip printable ASCII directly within one side of the gap. Its terminal
+ * width is always one cell; UTF-8, controls and tabs use the glyph path. */
+static size_t plain_run(const Editor *editor, size_t position, size_t maximum) {
+ size_t available = position < editor->gap_start ? editor->gap_start - position
+ : length(editor) - position;
+ if (available > maximum) available = maximum;
+ const unsigned char *bytes = editor->text + (position < editor->gap_start
+ ? position : position + editor->gap_end - editor->gap_start);
+ size_t count = 0;
+ while (count < available && bytes[count] >= 32 && bytes[count] < 127) ++count;
+ return count;
+}
+
+static void advance_plain(size_t count, size_t columns, size_t *row, size_t *column) {
+ size_t cells = *column + count;
+ *row += cells / columns;
+ *column = cells % columns;
+}
+
+static void changed_at(Editor *editor, size_t position) {
+ if (editor->anchor_valid && position < editor->anchor_position)
+ editor->anchor_valid = false;
+}
+
+static void move_gap(Editor *editor, size_t position) {
+ if (position < editor->gap_start) {
+ size_t count = editor->gap_start - position;
+ memmove(editor->text + editor->gap_end - count, editor->text + position, count);
+ editor->gap_start -= count;
+ editor->gap_end -= count;
+ } else if (position > editor->gap_start) {
+ size_t count = position - editor->gap_start;
+ memmove(editor->text + editor->gap_start, editor->text + editor->gap_end, count);
+ editor->gap_start += count;
+ editor->gap_end += count;
+ }
+}
+
+static bool reserve_gap(Editor *editor, size_t needed) {
+ if (editor->gap_end - editor->gap_start >= needed) return true;
+ size_t used = length(editor);
+ if (needed > PURE_OBJECT_LIMIT - used) return false;
+ size_t target = editor->capacity * 2;
+ if (target < used + needed + 256) target = used + needed + 256;
+ if (target > PURE_OBJECT_LIMIT + 256) target = PURE_OBJECT_LIMIT + 256;
+ unsigned char *fresh = malloc(target);
+ if (!fresh) return false;
+ size_t right = editor->capacity - editor->gap_end;
+ memcpy(fresh, editor->text, editor->gap_start);
+ memcpy(fresh + target - right, editor->text + editor->gap_end, right);
+ free(editor->text);
+ editor->text = fresh;
+ editor->gap_end = target - right;
+ editor->capacity = target;
+ return true;
+}
+
+static bool insert_byte(Editor *editor, unsigned char byte) {
+ if (length(editor) >= PURE_OBJECT_LIMIT || !reserve_gap(editor, 1)) return false;
+ changed_at(editor, editor->gap_start);
+ editor->text[editor->gap_start++] = byte;
+ editor->dirty = true;
+ return true;
+}
+
+static size_t previous_codepoint(const Editor *editor, size_t position) {
+ if (!position) return 0;
+ --position;
+ while (position && (byte_at(editor, position) & 0xc0) == 0x80) --position;
+ return position;
+}
+
+static size_t next_codepoint(const Editor *editor, size_t position) {
+ size_t count = length(editor);
+ if (position == count) return count;
+ ++position;
+ while (position < count && (byte_at(editor, position) & 0xc0) == 0x80) ++position;
+ return position;
+}
+
+static bool valid_text_span(PureSpan span) {
+ for (size_t position = 0; position < span.size;) {
+ mbstate_t state = {0};
+ wchar_t character;
+ size_t used = mbrtowc(&character, (const char *)span.data + position,
+ span.size - position, &state);
+ if (used == (size_t)-1 || used == (size_t)-2 || !used ||
+ (character < 32 && character != L'\n' && character != L'\t') ||
+ character == 127 ||
+ (character != L'\n' && character != L'\t' && wcwidth(character) < 0)) return false;
+ position += used;
+ }
+ return true;
+}
+
+static bool read_document(Editor *editor, const char *key, bool require_existing) {
+ PureTicket ticket;
+ PureResult admitted = pure_runtime_accept(&editor->runtime, PURE_INPUT_READ,
+ key, 0, 0, &ticket);
+ if (admitted != PURE_OK) {
+ snprintf(editor->message, sizeof(editor->message), "Read rejected: %s", result_name(admitted));
+ return false;
+ }
+ PureOutcome result = finish(editor, ticket);
+ if (require_existing && result.status == PURE_NOT_FOUND) {
+ snprintf(editor->message, sizeof(editor->message), "Document disappeared; refresh the list");
+ pure_runtime_release(ticket);
+ return false;
+ }
+ if (result.status != PURE_OK && result.status != PURE_NOT_FOUND) {
+ snprintf(editor->message, sizeof(editor->message), "Read failed: %s", result_name(result.status));
+ pure_runtime_release(ticket);
+ return false;
+ }
+ size_t size = result.status == PURE_OK ? result.body.size : 0;
+ if (size > PURE_OBJECT_LIMIT || (size && !result.body.data) ||
+ !valid_text_span(result.body)) {
+ snprintf(editor->message, sizeof(editor->message), "Document is too large or is not plain UTF-8 text");
+ pure_runtime_release(ticket);
+ return false;
+ }
+ size_t capacity = size + 256;
+ unsigned char *fresh = malloc(capacity);
+ if (!fresh) {
+ snprintf(editor->message, sizeof(editor->message), "Could not allocate document buffer");
+ pure_runtime_release(ticket);
+ return false;
+ }
+ if (size) memcpy(fresh, result.body.data, size);
+ pure_runtime_release(ticket);
+ free(editor->text);
+ editor->text = fresh;
+ editor->capacity = capacity;
+ editor->gap_start = size;
+ editor->gap_end = capacity;
+ editor->scroll_row = 0;
+ editor->anchor_valid = false;
+ editor->revision = result.status == PURE_OK ? result.revision : 0;
+ editor->exists = result.status == PURE_OK;
+ editor->dirty = false;
+ editor->confirm_reload = false;
+ memcpy(editor->key, key, strlen(key) + 1);
+ snprintf(editor->message, sizeof(editor->message), "%s %zu bytes · revision %" PRId64,
+ editor->exists ? "Opened" : "New document", size, editor->revision);
+ return true;
+}
+
+static void save_document(Editor *editor) {
+ if (!editor->dirty && editor->exists) {
+ snprintf(editor->message, sizeof(editor->message), "No changes to save");
+ return;
+ }
+ PureTicket baseline;
+ PureResult admitted = pure_runtime_accept(&editor->runtime, PURE_INPUT_READ,
+ editor->key, 0, 0, &baseline);
+ if (admitted != PURE_OK) {
+ snprintf(editor->message, sizeof(editor->message), "Read rejected: %s", result_name(admitted));
+ return;
+ }
+ PureOutcome old = finish(editor, baseline);
+ if (old.status != PURE_OK && old.status != PURE_NOT_FOUND) {
+ snprintf(editor->message, sizeof(editor->message), "Could not inspect stored version: %s", result_name(old.status));
+ pure_runtime_release(baseline);
+ return;
+ }
+ int64_t stored_revision = old.status == PURE_OK ? old.revision : 0;
+ if (stored_revision != editor->revision) {
+ snprintf(editor->message, sizeof(editor->message), "Version conflict: stored %" PRId64 "; draft preserved",
+ stored_revision);
+ pure_runtime_release(baseline);
+ return;
+ }
+ size_t old_size = old.status == PURE_OK ? old.body.size : 0;
+ size_t draft_size = length(editor);
+ size_t prefix = 0;
+ while (prefix < old_size && prefix < draft_size &&
+ old.body.data[prefix] == byte_at(editor, prefix)) ++prefix;
+ while (prefix && prefix < old_size && (old.body.data[prefix] & 0xc0) == 0x80) --prefix;
+ size_t suffix = 0;
+ while (suffix < old_size - prefix && suffix < draft_size - prefix &&
+ old.body.data[old_size - suffix - 1] == byte_at(editor, draft_size - suffix - 1)) ++suffix;
+ while (suffix && (((old.body.data[old_size - suffix] & 0xc0) == 0x80) ||
+ ((byte_at(editor, draft_size - suffix) & 0xc0) == 0x80))) --suffix;
+ size_t remove = old_size - prefix - suffix;
+ size_t insert = draft_size - prefix - suffix;
+ if (!remove && !insert && editor->exists) {
+ editor->dirty = false;
+ snprintf(editor->message, sizeof(editor->message), "No changes to save");
+ pure_runtime_release(baseline);
+ return;
+ }
+ if (prefix > UINT32_MAX || remove > UINT32_MAX || insert > PURE_OBJECT_LIMIT - editor->prefix) {
+ snprintf(editor->message, sizeof(editor->message), "Edit exceeds selected program limit");
+ pure_runtime_release(baseline);
+ return;
+ }
+ PureTicket write_ticket;
+ admitted = pure_runtime_accept(&editor->runtime, PURE_INPUT_WRITE, editor->key,
+ editor->revision, editor->prefix + insert, &write_ticket);
+ if (admitted != PURE_OK) {
+ snprintf(editor->message, sizeof(editor->message), "Write rejected: %s", result_name(admitted));
+ pure_runtime_release(baseline);
+ return;
+ }
+ unsigned char *input = pure_runtime_ingress(write_ticket, NULL);
+ memset(input, 0, editor->prefix);
+ put32(input + editor->program->splice_offset.value, (uint32_t)prefix);
+ put32(input + editor->program->splice_remove.value, (uint32_t)remove);
+ for (size_t i = 0; i < insert; ++i) input[editor->prefix + i] = byte_at(editor, prefix + i);
+ pure_runtime_release(baseline);
+ if (!pure_runtime_ready(write_ticket)) {
+ pure_runtime_release(write_ticket);
+ snprintf(editor->message, sizeof(editor->message), "Could not publish edit");
+ return;
+ }
+ PureOutcome result = finish(editor, write_ticket);
+ pure_runtime_release(write_ticket);
+ if (result.status != PURE_OK) {
+ snprintf(editor->message, sizeof(editor->message), "Save failed: %s; draft preserved", result_name(result.status));
+ return;
+ }
+ editor->revision = result.revision;
+ editor->exists = true;
+ editor->dirty = false;
+ editor->confirm_quit = editor->confirm_reload = false;
+ snprintf(editor->message, sizeof(editor->message), "Saved %zu-byte edit at byte %zu · revision %" PRId64,
+ insert, prefix, result.revision);
+}
+
+static size_t glyph(const Editor *editor, size_t position, wchar_t *character,
+ int *width, char bytes[4], bool *valid) {
+ unsigned char first = byte_at(editor, position);
+ if (first < 128) {
+ bytes[0] = (char)first;
+ *character = (wchar_t)first;
+ *width = 1;
+ if ((first < 32 && first != '\n' && first != '\t') || first == 127 || !first) {
+ *valid = false;
+ bytes[0] = '?';
+ *character = L'?';
+ }
+ return 1;
+ }
+ size_t available = length(editor) - position;
+ if (available > 4) available = 4;
+ for (size_t i = 0; i < available; ++i) bytes[i] = (char)byte_at(editor, position + i);
+ mbstate_t state = {0};
+ size_t used = mbrtowc(character, bytes, available, &state);
+ if (used == (size_t)-1 || used == (size_t)-2 || !used ||
+ (*character < 32 && *character != L'\n' && *character != L'\t') ||
+ *character == 127) {
+ *valid = false;
+ *character = L'?';
+ *width = 1;
+ bytes[0] = '?';
+ return 1;
+ }
+ *width = wcwidth(*character);
+ if (*width < 0) {
+ *valid = false;
+ *character = L'?';
+ *width = 1;
+ bytes[0] = '?';
+ return 1;
+ }
+ return used;
+}
+
+static bool text_valid(const Editor *editor) {
+ for (size_t position = 0; position < length(editor);) {
+ wchar_t character;
+ int width;
+ char bytes[4];
+ bool valid = true;
+ size_t used = glyph(editor, position, &character, &width, bytes, &valid);
+ if (!valid) return false;
+ position += used;
+ }
+ return true;
+}
+
+static void advance(wchar_t character, int width, size_t columns,
+ size_t *row, size_t *column) {
+ if (character == L'\n') { ++*row; *column = 0; return; }
+ if (character == L'\t') width = 4 - (int)(*column % 4);
+ if (*column && *column + (size_t)width > columns) { ++*row; *column = 0; }
+ *column += (size_t)width;
+ if (*column >= columns) { ++*row; *column = 0; }
+}
+
+static void position_of(const Editor *editor, size_t target, size_t columns,
+ size_t *row, size_t *column) {
+ size_t position = 0;
+ *row = *column = 0;
+ if (editor->anchor_valid && editor->anchor_columns == columns &&
+ target >= editor->anchor_position) {
+ position = editor->anchor_position;
+ *row = editor->anchor_row;
+ *column = editor->anchor_column;
+ }
+ for (; position < target;) {
+ size_t run = plain_run(editor, position, target - position);
+ if (run) {
+ advance_plain(run, columns, row, column);
+ position += run;
+ continue;
+ }
+ wchar_t character;
+ int width;
+ char bytes[4];
+ bool valid = true;
+ size_t used = glyph(editor, position, &character, &width, bytes, &valid);
+ advance(character, width, columns, row, column);
+ position += used;
+ }
+}
+
+static size_t closest_position(const Editor *editor, size_t target_row,
+ size_t target_column, size_t columns) {
+ size_t row = 0, column = 0, position = 0, best = 0;
+ bool found = false;
+ if (editor->anchor_valid && editor->anchor_columns == columns &&
+ target_row > editor->anchor_row) {
+ row = editor->anchor_row;
+ column = editor->anchor_column;
+ position = editor->anchor_position;
+ }
+ for (; position < length(editor);) {
+ if (row == target_row && column <= target_column) { best = position; found = true; }
+ if (row > target_row || (row == target_row && column > target_column))
+ return found ? best : position;
+ wchar_t character;
+ int width;
+ char bytes[4];
+ bool valid = true;
+ size_t used = glyph(editor, position, &character, &width, bytes, &valid);
+ advance(character, width, columns, &row, &column);
+ position += used;
+ if (row == target_row && column <= target_column) { best = position; found = true; }
+ }
+ return found ? best : length(editor);
+}
+
+static void dimensions(size_t *rows, size_t *columns) {
+ struct winsize window = {0};
+ if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &window) || !window.ws_row || !window.ws_col) {
+ *rows = 24; *columns = 80;
+ } else { *rows = window.ws_row; *columns = window.ws_col; }
+ if (*rows < 4) *rows = 4;
+ if (*columns < 8) *columns = 8;
+}
+
+static void draw_list(Editor *editor, size_t rows, size_t columns) {
+ size_t visible = rows - 3;
+ bool matching = editor->document_count && (!editor->prompt_size ||
+ !strncmp(editor->documents[editor->document_selected].name,
+ editor->prompt, editor->prompt_size));
+ if (editor->document_selected < editor->document_scroll)
+ editor->document_scroll = editor->document_selected;
+ if (editor->document_selected >= editor->document_scroll + visible)
+ editor->document_scroll = editor->document_selected - visible + 1;
+ fputs("\033[?25l\033[H\033[2J", stdout);
+ char header[80];
+ snprintf(header, sizeof(header), " Pure | documents | %zu keys", editor->document_count);
+ printf("%.*s", (int)columns, header);
+ for (size_t row = 0; row < visible && editor->document_scroll + row < editor->document_count; ++row) {
+ size_t index = editor->document_scroll + row;
+ printf("\033[%zu;1H%c %.*s", row + 2,
+ matching && index == editor->document_selected ? '>' : ' ',
+ (int)(columns - 2), editor->documents[index].name);
+ }
+ printf("\033[%zu;1H%.*s", rows - 1, (int)columns,
+ " Enter open Type to find Backspace clear Esc back");
+ char footer[230];
+ if (editor->prompt_size)
+ snprintf(footer, sizeof(footer), " Find: %.64s_ %.140s", editor->prompt,
+ matching ? editor->message : "No matching key");
+ else if (editor->document_count)
+ snprintf(footer, sizeof(footer), " %zu/%zu %s",
+ editor->document_selected + 1, editor->document_count, editor->message);
+ else snprintf(footer, sizeof(footer), " No stored documents; Ctrl+O creates a new key");
+ printf("\033[%zu;1H%.*s", rows, (int)columns, footer);
+ fflush(stdout);
+}
+
+static void draw(Editor *editor) {
+ size_t rows, columns;
+ dimensions(&rows, &columns);
+ if (editor->list_open) { draw_list(editor, rows, columns); return; }
+ if (editor->anchor_columns != columns) editor->anchor_valid = false;
+ size_t content_rows = rows - 3;
+ size_t cursor_row, cursor_column;
+ position_of(editor, editor->gap_start, columns, &cursor_row, &cursor_column);
+ if (cursor_row < editor->scroll_row) editor->scroll_row = cursor_row;
+ if (cursor_row >= editor->scroll_row + content_rows)
+ editor->scroll_row = cursor_row - content_rows + 1;
+ fputs("\033[?25l\033[H\033[2J", stdout);
+ char header[160];
+ snprintf(header, sizeof(header), " Pure | %s | revision %" PRId64 "%s | %zu bytes",
+ editor->key, editor->revision, editor->dirty ? " *" : "", length(editor));
+ printf("%.*s", (int)columns, header);
+ size_t row = 0, column = 0, position = 0, drawn_row = SIZE_MAX;
+ size_t anchor_row = editor->scroll_row > content_rows
+ ? editor->scroll_row - content_rows : 0;
+ bool keep_anchor = editor->anchor_valid && editor->anchor_row <= editor->scroll_row &&
+ editor->scroll_row - editor->anchor_row <= 2 * content_rows;
+ if (editor->anchor_valid && editor->anchor_row <= editor->scroll_row) {
+ row = editor->anchor_row;
+ column = editor->anchor_column;
+ position = editor->anchor_position;
+ }
+ if (!keep_anchor) editor->anchor_valid = false;
+ for (; position < length(editor);) {
+ if (!editor->anchor_valid && row >= anchor_row) {
+ editor->anchor_position = position;
+ editor->anchor_row = row;
+ editor->anchor_column = column;
+ editor->anchor_columns = columns;
+ editor->anchor_valid = true;
+ }
+ if (row >= editor->scroll_row + content_rows) break;
+ if (row < editor->scroll_row) {
+ size_t remaining = length(editor) - position;
+ size_t rows_to_view = editor->scroll_row - row;
+ size_t cells_to_view = rows_to_view > (remaining + column) / columns
+ ? remaining : rows_to_view * columns - column;
+ size_t run = plain_run(editor, position, cells_to_view);
+ if (run) {
+ advance_plain(run, columns, &row, &column);
+ position += run;
+ continue;
+ }
+ }
+ wchar_t character;
+ int width;
+ char bytes[4];
+ bool valid = true;
+ size_t used = glyph(editor, position, &character, &width, bytes, &valid);
+ size_t next_row = row, next_column = column;
+ size_t visible_width = character == L'\t' ? 4 - column % 4 : (size_t)width;
+ if (character != L'\n' && column && column + visible_width > columns) {
+ ++row;
+ column = 0;
+ }
+ if (row >= editor->scroll_row && row < editor->scroll_row + content_rows &&
+ character != L'\n') {
+ if (drawn_row != row) {
+ printf("\033[%zu;1H", row - editor->scroll_row + 2);
+ drawn_row = row;
+ }
+ if (character == L'\t') {
+ int spaces = 4 - (int)(column % 4);
+ while (spaces--) putchar(' ');
+ } else fwrite(bytes, 1, used, stdout);
+ }
+ row = next_row;
+ column = next_column;
+ advance(character, width, columns, &row, &column);
+ position += used;
+ }
+ printf("\033[%zu;1H%.*s", rows - 1, (int)columns,
+ " Ctrl+S save Ctrl+O open Ctrl+R reload Ctrl+Q quit arrows move");
+ char footer[230];
+ if (editor->prompt_open)
+ snprintf(footer, sizeof(footer), " Open key: %s_ (Enter to open, Esc to cancel)", editor->prompt);
+ else snprintf(footer, sizeof(footer), " %s", editor->message);
+ printf("\033[%zu;1H%.*s", rows, (int)columns, footer);
+ if (editor->prompt_open) {
+ size_t prompt_column = editor->prompt_size + strlen(" Open key: ") + 1;
+ if (prompt_column > columns) prompt_column = columns;
+ printf("\033[%zu;%zuH", rows, prompt_column);
+ } else printf("\033[%zu;%zuH", cursor_row - editor->scroll_row + 2,
+ cursor_column + 1);
+ fputs("\033[?25h", stdout);
+ fflush(stdout);
+}
+
+static int read_byte(int wake_read, int timeout_ms) {
+ struct pollfd fds[2] = {{.fd = STDIN_FILENO, .events = POLLIN},
+ {.fd = wake_read, .events = POLLIN}};
+ int ready = poll(fds, 2, timeout_ms);
+ if (ready < 0) return errno == EINTR ? -2 : -1;
+ if (!ready) return -3;
+ if (fds[1].revents) {
+ char buffer[32];
+ while (read(wake_read, buffer, sizeof(buffer)) > 0) { }
+ return -2;
+ }
+ unsigned char byte;
+ return read(STDIN_FILENO, &byte, 1) == 1 ? byte : -1;
+}
+
+static void move_vertical(Editor *editor, int delta) {
+ size_t rows, columns, row, column;
+ dimensions(&rows, &columns);
+ (void)rows;
+ position_of(editor, editor->gap_start, columns, &row, &column);
+ if (delta < 0 && !row) return;
+ move_gap(editor, closest_position(editor, delta < 0 ? row - 1 : row + 1,
+ column, columns));
+}
+
+static void handle_escape(Editor *editor, int wake_read) {
+ int a = read_byte(wake_read, 30);
+ if (a != '[') {
+ if (editor->list_open) close_list(editor);
+ editor->prompt_open = false;
+ return;
+ }
+ int b = read_byte(wake_read, 30);
+ if (editor->list_open) {
+ if (b == 'A' || b == 'B' || b == 'H' || b == 'F') {
+ editor->prompt_size = 0;
+ editor->prompt[0] = 0;
+ }
+ if (b == 'A' && editor->document_selected) --editor->document_selected;
+ else if (b == 'B' && editor->document_selected + 1 < editor->document_count)
+ ++editor->document_selected;
+ else if (b == 'H') editor->document_selected = 0;
+ else if (b == 'F' && editor->document_count)
+ editor->document_selected = editor->document_count - 1;
+ return;
+ }
+ if (b == 'A') move_vertical(editor, -1);
+ else if (b == 'B') move_vertical(editor, 1);
+ else if (b == 'C') move_gap(editor, next_codepoint(editor, editor->gap_start));
+ else if (b == 'D') move_gap(editor, previous_codepoint(editor, editor->gap_start));
+ else if (b == 'H') {
+ size_t position = editor->gap_start;
+ while (position && byte_at(editor, position - 1) != '\n') --position;
+ move_gap(editor, position);
+ } else if (b == 'F') {
+ size_t position = editor->gap_start;
+ while (position < length(editor) && byte_at(editor, position) != '\n') ++position;
+ move_gap(editor, position);
+ } else if (b == '3' && read_byte(wake_read, 30) == '~') {
+ size_t next = next_codepoint(editor, editor->gap_start);
+ if (next > editor->gap_start) {
+ changed_at(editor, editor->gap_start);
+ editor->gap_end += next - editor->gap_start;
+ editor->dirty = true;
+ }
+ }
+}
+
+static void handle_key(Editor *editor, int key, int wake_read) {
+ if (key < 0) return;
+ if (editor->prompt_open) {
+ if (key == 27) {
+ if (read_byte(wake_read, 30) == '[') {
+ int next = read_byte(wake_read, 30);
+ if (next >= '0' && next <= '9') (void)read_byte(wake_read, 30);
+ } else editor->prompt_open = false;
+ return;
+ }
+ if (key == 127 || key == 8) {
+ if (editor->prompt_size) editor->prompt[--editor->prompt_size] = 0;
+ return;
+ }
+ if (key == 13 || key == 10) {
+ if (valid_key(editor->prompt)) (void)read_document(editor, editor->prompt, false);
+ else snprintf(editor->message, sizeof(editor->message), "Key must be 1–64 ASCII letters, digits, _ or -");
+ editor->prompt_open = false;
+ return;
+ }
+ if (editor->prompt_size < 64 &&
+ ((key >= 'a' && key <= 'z') || (key >= 'A' && key <= 'Z') ||
+ (key >= '0' && key <= '9') || key == '_' || key == '-')) {
+ editor->prompt[editor->prompt_size++] = (char)key;
+ editor->prompt[editor->prompt_size] = 0;
+ }
+ return;
+ }
+ if (editor->list_open) {
+ if (key == 27) { handle_escape(editor, wake_read); return; }
+ if ((key == 127 || key == 8) && editor->prompt_size) {
+ editor->prompt[--editor->prompt_size] = 0;
+ (void)find_list_prefix(editor);
+ return;
+ }
+ if (editor->prompt_size < 64 &&
+ ((key >= 'a' && key <= 'z') || (key >= 'A' && key <= 'Z') ||
+ (key >= '0' && key <= '9') || key == '_' || key == '-')) {
+ editor->prompt[editor->prompt_size++] = (char)key;
+ editor->prompt[editor->prompt_size] = 0;
+ (void)find_list_prefix(editor);
+ return;
+ }
+ if ((key == 13 || key == 10) && editor->document_count &&
+ (!editor->prompt_size || !strncmp(editor->documents[editor->document_selected].name,
+ editor->prompt, editor->prompt_size))) {
+ if (read_document(editor, editor->documents[editor->document_selected].name, true))
+ close_list(editor);
+ return;
+ }
+ if (key == 12) { (void)open_list(editor); return; }
+ if (key == 15) close_list(editor);
+ else if (key != 17 && key != 3) return;
+ }
+ if (key == 17 || key == 3) {
+ if (editor->dirty && !editor->confirm_quit) {
+ editor->confirm_quit = true;
+ snprintf(editor->message, sizeof(editor->message), "Unsaved draft; press Ctrl+Q again to discard and quit");
+ } else stopping = 1;
+ return;
+ }
+ if (key != 18) editor->confirm_reload = false;
+ if (key != 17) editor->confirm_quit = false;
+ if (key == 19) { if (text_valid(editor)) save_document(editor);
+ else snprintf(editor->message, sizeof(editor->message), "Document contains invalid UTF-8 or control bytes");
+ return; }
+ if (key == 18) {
+ if (editor->dirty && !editor->confirm_reload) {
+ editor->confirm_reload = true;
+ snprintf(editor->message, sizeof(editor->message), "Unsaved draft; press Ctrl+R again to discard and reload");
+ } else (void)read_document(editor, editor->key, false);
+ return;
+ }
+ if (key == 15) {
+ if (editor->dirty) snprintf(editor->message, sizeof(editor->message), "Save or reload the draft before opening another key");
+ else { editor->prompt_open = true; editor->prompt_size = 0; editor->prompt[0] = 0; }
+ return;
+ }
+ if (key == 12) {
+ if (editor->dirty)
+ snprintf(editor->message, sizeof(editor->message), "Save or reload the draft before browsing documents");
+ else (void)open_list(editor);
+ return;
+ }
+ if (key == 27) { handle_escape(editor, wake_read); return; }
+ if (key == 127 || key == 8) {
+ size_t before = previous_codepoint(editor, editor->gap_start);
+ if (before < editor->gap_start) {
+ changed_at(editor, before);
+ editor->gap_start = before;
+ editor->dirty = true;
+ }
+ return;
+ }
+ if (key == 13) key = '\n';
+ if (key == '\n' || key == '\t' || key >= 32) {
+ if (!insert_byte(editor, (unsigned char)key))
+ snprintf(editor->message, sizeof(editor->message), "Document reached the byte limit");
+ }
+}
+
+static bool nonblocking(int fd) {
+ int flags = fcntl(fd, F_GETFL, 0);
+ return flags >= 0 && fcntl(fd, F_SETFL, flags | O_NONBLOCK) == 0;
+}
+
+static bool raw_terminal(const struct termios *original) {
+ struct termios raw = *original;
+ raw.c_iflag &= (tcflag_t)~(BRKINT | ICRNL | INPCK | ISTRIP | IXON);
+ raw.c_oflag &= (tcflag_t)~OPOST;
+ raw.c_cflag |= CS8;
+ raw.c_lflag &= (tcflag_t)~(ECHO | ICANON | IEXTEN | ISIG);
+ raw.c_cc[VMIN] = 1;
+ raw.c_cc[VTIME] = 0;
+ return tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw) == 0;
+}
+
+int main(int argc, char **argv) {
+#ifdef PURE_EMBEDDED_PROGRAM
+ if (argc != 4 || strcmp(argv[1], "--store") || !valid_key(argv[3])) {
+ fputs("usage: pure-edit-tui-fixed --store DIRECTORY KEY\n", stderr);
+ return 2;
+ }
+ const char *store_path = argv[2], *key = argv[3];
+ const PureProgram *program = &pure_embedded_program;
+#else
+ if (argc != 6 || strcmp(argv[1], "--program") || strcmp(argv[3], "--store") ||
+ !valid_key(argv[5])) {
+ fputs("usage: pure-edit-tui --program FILE --store DIRECTORY KEY\n", stderr);
+ return 2;
+ }
+ const char *store_path = argv[4], *key = argv[5];
+ PureProgram loaded;
+ char error[160];
+ if (!pure_program_load(argv[2], &loaded, error, sizeof(error))) {
+ fprintf(stderr, "Program: %s\n", error);
+ return 2;
+ }
+ const PureProgram *program = &loaded;
+#endif
+ size_t prefix;
+ if (!edit_layout(program, &prefix)) {
+ fputs("Program needs a complete-document read and two u32le splice fields with trailing insertion bytes\n", stderr);
+ return 2;
+ }
+ if (!isatty(STDIN_FILENO) || !isatty(STDOUT_FILENO)) {
+ fputs("terminal editor requires a TTY\n", stderr);
+ return 2;
+ }
+ setlocale(LC_CTYPE, "");
+ Editor editor = {.store = store_path, .program = program, .prefix = prefix};
+ if (pure_store_init(editor.store) != PURE_OK ||
+ !pure_runtime_init(&editor.runtime, editor.work, 2, program,
+ pure_store_host(editor.store), 4u * 1024u * 1024u) ||
+ !read_document(&editor, key, false)) {
+ fprintf(stderr, "Could not open document: %s\n", editor.message);
+ free(editor.text);
+ return 2;
+ }
+ int wake[2];
+ if (pipe(wake) || !nonblocking(wake[0]) || !nonblocking(wake[1]) ||
+ tcgetattr(STDIN_FILENO, &editor.original_term)) {
+ perror("terminal setup");
+ free(editor.text);
+ return 2;
+ }
+ wake_write = wake[1];
+ struct sigaction action = {.sa_handler = wake_signal};
+ sigemptyset(&action.sa_mask);
+ if (sigaction(SIGINT, &action, NULL) || sigaction(SIGTERM, &action, NULL) ||
+ sigaction(SIGWINCH, &action, NULL)) {
+ perror("sigaction");
+ wake_write = -1;
+ close(wake[0]);
+ close(wake[1]);
+ free(editor.text);
+ return 2;
+ }
+ if (!raw_terminal(&editor.original_term)) {
+ perror("raw terminal");
+ tcsetattr(STDIN_FILENO, TCSAFLUSH, &editor.original_term);
+ wake_write = -1;
+ close(wake[0]);
+ close(wake[1]);
+ free(editor.text);
+ return 2;
+ }
+ fputs("\033[?1049h", stdout);
+ while (!stopping) {
+ draw(&editor);
+ int key = read_byte(wake[0], -1);
+ if (resized) resized = 0;
+ if (key == -1) { stopping = 1; break; }
+ handle_key(&editor, key, wake[0]);
+ }
+ fputs("\033[?25h\033[?1049l", stdout);
+ fflush(stdout);
+ wake_write = -1;
+ close(wake[0]);
+ close(wake[1]);
+ tcsetattr(STDIN_FILENO, TCSAFLUSH, &editor.original_term);
+ pure_runtime_stop(&editor.runtime);
+ close_list(&editor);
+ free(editor.text);
+ return 0;
+}