diff options
Diffstat (limited to 'prototypes/connections/edit-tui.c')
| -rw-r--r-- | prototypes/connections/edit-tui.c | 945 |
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; +} |
