aboutsummaryrefslogtreecommitdiff
path: root/src/buffers.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/buffers.c')
-rw-r--r--src/buffers.c701
1 files changed, 701 insertions, 0 deletions
diff --git a/src/buffers.c b/src/buffers.c
new file mode 100644
index 0000000..e8c1d23
--- /dev/null
+++ b/src/buffers.c
@@ -0,0 +1,701 @@
+#include "buffers.h"
+
+#include "common.h"
+#include "util.h"
+
+#include <assert.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#define BUFFER_INITIAL_CAP 64
+
+static int buffer_is_word_char(char c) {
+ return (c >= 'a' && c <= 'z') ||
+ (c >= 'A' && c <= 'Z') ||
+ (c >= '0' && c <= '9') ||
+ c == '_';
+}
+
+static int buffer_require_editable(buffer_t *buffer) {
+ if (!buffer || buffer->read_only) return ECEX_ERR;
+ return ECEX_OK;
+}
+
+static void buffer_undo_entry_free(ecex_undo_entry_t *entry) {
+ if (!entry) return;
+ free(entry->data);
+ entry->data = NULL;
+ entry->len = 0;
+ entry->point = 0;
+ entry->mark = 0;
+ entry->mark_active = 0;
+}
+
+static void buffer_undo_stack_clear(ecex_undo_entry_t *stack, size_t count) {
+ if (!stack) return;
+ for (size_t i = 0; i < count; i++) buffer_undo_entry_free(&stack[i]);
+}
+
+static int buffer_push_snapshot_to(ecex_undo_entry_t **stack,
+ size_t *count,
+ size_t *cap,
+ buffer_t *buffer) {
+ if (!stack || !count || !cap || !buffer) return ECEX_ERR;
+ if (ECEX_GROW_ARRAY(*stack, *count, *cap, 64) != ECEX_OK) return ECEX_ERR;
+ ecex_undo_entry_t *entry = &(*stack)[(*count)++];
+ memset(entry, 0, sizeof(*entry));
+ entry->data = malloc(buffer->len + 1);
+ if (!entry->data) { (*count)--; return ECEX_ERR; }
+ memcpy(entry->data, buffer->data, buffer->len + 1);
+ entry->len = buffer->len;
+ entry->point = buffer->point;
+ entry->mark = buffer->mark;
+ entry->mark_active = buffer->mark_active;
+ return ECEX_OK;
+}
+
+static void buffer_clear_redo(buffer_t *buffer) {
+ if (!buffer) return;
+ buffer_undo_stack_clear(buffer->redo_stack, buffer->redo_count);
+ buffer->redo_count = 0;
+}
+
+static int buffer_record_undo(buffer_t *buffer) {
+ if (!buffer || buffer->undo_disabled) return ECEX_OK;
+ if (buffer_push_snapshot_to(&buffer->undo_stack,
+ &buffer->undo_count,
+ &buffer->undo_cap,
+ buffer) != ECEX_OK) {
+ return ECEX_ERR;
+ }
+ buffer_clear_redo(buffer);
+ return ECEX_OK;
+}
+
+static int buffer_restore_snapshot(buffer_t *buffer, ecex_undo_entry_t *entry) {
+ if (!buffer || !entry || !entry->data) return ECEX_ERR;
+ char *copy = malloc(entry->len + 1);
+ if (!copy) return ECEX_ERR;
+ memcpy(copy, entry->data, entry->len + 1);
+ free(buffer->data);
+ buffer->data = copy;
+ buffer->len = entry->len;
+ buffer->cap = entry->len + 1;
+ buffer->point = ECEX_MIN(entry->point, buffer->len);
+ buffer->mark = ECEX_MIN(entry->mark, buffer->len);
+ buffer->mark_active = entry->mark_active;
+ buffer->modified = 1;
+ return ECEX_OK;
+}
+
+buffer_t *buffer_new(const char *name, const char *path, int read_only) {
+ assert(name && "buffer name cannot be NULL");
+
+ buffer_t *buffer = calloc(1, sizeof(*buffer));
+ if (!buffer) return NULL;
+
+ buffer->name = ecex_strdup(name);
+ buffer->path = ecex_strdup(path);
+ buffer->data = malloc(1);
+
+ if (!buffer->name || (path && !buffer->path) || !buffer->data) {
+ buffer_free(buffer);
+ return NULL;
+ }
+
+ buffer->data[0] = '\0';
+ buffer->len = 0;
+ buffer->cap = 1;
+ buffer->read_only = read_only;
+
+ return buffer;
+}
+
+void buffer_free(buffer_t *buffer) {
+ if (!buffer) return;
+
+ buffer_clear_interactive_actions(buffer);
+ buffer_undo_stack_clear(buffer->undo_stack, buffer->undo_count);
+ buffer_undo_stack_clear(buffer->redo_stack, buffer->redo_count);
+ free(buffer->undo_stack);
+ free(buffer->redo_stack);
+ free(buffer->interactive_actions);
+ free(buffer->name);
+ free(buffer->path);
+ free(buffer->data);
+ free(buffer);
+}
+
+int buffer_reserve(buffer_t *buffer, size_t needed) {
+ ECEX_RETURN_ERR_IF_NULL(buffer);
+ if (needed <= buffer->cap) return ECEX_OK;
+
+ size_t new_cap = buffer->cap ? buffer->cap : BUFFER_INITIAL_CAP;
+ while (new_cap < needed) {
+ new_cap *= 2;
+ }
+
+ char *new_data = realloc(buffer->data, new_cap);
+ if (!new_data) return ECEX_ERR;
+
+ buffer->data = new_data;
+ buffer->cap = new_cap;
+ return ECEX_OK;
+}
+
+int buffer_clear(buffer_t *buffer) {
+ if (buffer_require_editable(buffer) != ECEX_OK) return ECEX_ERR;
+ if (buffer_record_undo(buffer) != ECEX_OK) return ECEX_ERR;
+
+ if (buffer_reserve(buffer, 1) != ECEX_OK) return ECEX_ERR;
+
+ buffer->data[0] = '\0';
+ buffer->len = 0;
+ buffer->point = 0;
+ buffer->mark = 0;
+ buffer->mark_active = 0;
+ buffer->scroll_line = 0;
+ buffer->scroll_col = 0;
+ buffer_clear_interactive_actions(buffer);
+ buffer->modified = 1;
+ return ECEX_OK;
+}
+
+int buffer_set_text(buffer_t *buffer, const char *text) {
+ if (buffer_require_editable(buffer) != ECEX_OK || !text) return ECEX_ERR;
+ if (buffer_record_undo(buffer) != ECEX_OK) return ECEX_ERR;
+
+ size_t len = strlen(text);
+ if (buffer_reserve(buffer, len + 1) != ECEX_OK) return ECEX_ERR;
+
+ memcpy(buffer->data, text, len + 1);
+ buffer->len = len;
+ buffer->point = len;
+ buffer->mark = 0;
+ buffer->mark_active = 0;
+ buffer->scroll_line = 0;
+ buffer->scroll_col = 0;
+ buffer_clear_interactive_actions(buffer);
+ buffer->modified = 1;
+ return ECEX_OK;
+}
+
+int buffer_insert_at(buffer_t *buffer, size_t pos, const char *text) {
+ if (buffer_require_editable(buffer) != ECEX_OK || !text) return ECEX_ERR;
+ if (pos > buffer->len) return ECEX_ERR;
+
+ size_t text_len = strlen(text);
+ if (text_len == 0) return ECEX_OK;
+ if (buffer_record_undo(buffer) != ECEX_OK) return ECEX_ERR;
+ size_t needed = buffer->len + text_len + 1;
+
+ if (buffer_reserve(buffer, needed) != ECEX_OK) return ECEX_ERR;
+
+ memmove(buffer->data + pos + text_len,
+ buffer->data + pos,
+ buffer->len - pos + 1);
+ memcpy(buffer->data + pos, text, text_len);
+
+ buffer->len += text_len;
+ buffer->point = pos + text_len;
+ buffer->modified = 1;
+ return ECEX_OK;
+}
+
+int buffer_insert(buffer_t *buffer, const char *text) {
+ ECEX_RETURN_ERR_IF_NULL(buffer);
+ return buffer_insert_at(buffer, buffer->point, text);
+}
+
+int buffer_append(buffer_t *buffer, const char *text) {
+ ECEX_RETURN_ERR_IF_NULL(buffer);
+ return buffer_insert_at(buffer, buffer->len, text);
+}
+
+int buffer_prepend(buffer_t *buffer, const char *text) {
+ return buffer_insert_at(buffer, 0, text);
+}
+
+int buffer_insert_char_at(buffer_t *buffer, size_t pos, char c) {
+ if (buffer_require_editable(buffer) != ECEX_OK) return ECEX_ERR;
+ if (pos > buffer->len) return ECEX_ERR;
+ if (buffer_record_undo(buffer) != ECEX_OK) return ECEX_ERR;
+
+ if (buffer_reserve(buffer, buffer->len + 2) != ECEX_OK) return ECEX_ERR;
+
+ memmove(buffer->data + pos + 1,
+ buffer->data + pos,
+ buffer->len - pos + 1);
+
+ buffer->data[pos] = c;
+ buffer->len++;
+ buffer->point = pos + 1;
+ buffer->modified = 1;
+ return ECEX_OK;
+}
+
+int buffer_insert_char(buffer_t *buffer, char c) {
+ ECEX_RETURN_ERR_IF_NULL(buffer);
+ return buffer_insert_char_at(buffer, buffer->point, c);
+}
+
+int buffer_delete_range(buffer_t *buffer, size_t start, size_t end) {
+ if (buffer_require_editable(buffer) != ECEX_OK) return ECEX_ERR;
+ if (start > end || end > buffer->len) return ECEX_ERR;
+ if (start == end) return ECEX_OK;
+ if (buffer_record_undo(buffer) != ECEX_OK) return ECEX_ERR;
+
+ size_t deleted = end - start;
+
+ memmove(buffer->data + start,
+ buffer->data + end,
+ buffer->len - end + 1);
+
+ buffer->len -= deleted;
+
+ if (buffer->point > end) {
+ buffer->point -= deleted;
+ } else if (buffer->point > start) {
+ buffer->point = start;
+ }
+
+ if (buffer->mark > end) {
+ buffer->mark -= deleted;
+ } else if (buffer->mark > start) {
+ buffer->mark = start;
+ }
+
+ if (buffer->mark_active && buffer->mark == buffer->point) {
+ buffer->mark_active = 0;
+ }
+
+ buffer->modified = 1;
+ return ECEX_OK;
+}
+
+int buffer_delete_selection(buffer_t *buffer) {
+ if (!buffer_has_selection(buffer)) return ECEX_OK;
+
+ size_t start = 0;
+ size_t end = 0;
+ buffer_selection_range(buffer, &start, &end);
+
+ int result = buffer_delete_range(buffer, start, end);
+ if (result == ECEX_OK) buffer_clear_mark(buffer);
+ return result;
+}
+
+int buffer_replace_selection(buffer_t *buffer, const char *text) {
+ if (!buffer || !text) return ECEX_ERR;
+ if (!buffer_has_selection(buffer)) return buffer_insert(buffer, text);
+
+ size_t start = 0;
+ size_t end = 0;
+ buffer_selection_range(buffer, &start, &end);
+
+ if (buffer_delete_range(buffer, start, end) != ECEX_OK) return ECEX_ERR;
+ buffer->point = start;
+ buffer_clear_mark(buffer);
+ return buffer_insert(buffer, text);
+}
+
+int buffer_backspace(buffer_t *buffer) {
+ if (!buffer) return ECEX_ERR;
+ if (buffer_has_selection(buffer)) return buffer_delete_selection(buffer);
+ if (buffer->point == 0) return ECEX_OK;
+ return buffer_delete_range(buffer, buffer->point - 1, buffer->point);
+}
+
+int buffer_delete_forward(buffer_t *buffer) {
+ if (!buffer) return ECEX_ERR;
+ if (buffer_has_selection(buffer)) return buffer_delete_selection(buffer);
+ if (buffer->point >= buffer->len) return ECEX_OK;
+ return buffer_delete_range(buffer, buffer->point, buffer->point + 1);
+}
+
+int buffer_kill_line(buffer_t *buffer) {
+ if (!buffer) return ECEX_ERR;
+
+ size_t end = buffer_line_end_at(buffer, buffer->point);
+ if (end == buffer->point && end < buffer->len && buffer->data[end] == '\n') {
+ end++;
+ }
+
+ return buffer_delete_range(buffer, buffer->point, end);
+}
+
+void buffer_set_point(buffer_t *buffer, size_t point) {
+ if (!buffer) return;
+ buffer->point = ECEX_MIN(point, buffer->len);
+}
+
+void buffer_move_left(buffer_t *buffer) {
+ if (!buffer || buffer->point == 0) return;
+ buffer->point--;
+}
+
+void buffer_move_right(buffer_t *buffer) {
+ if (!buffer || buffer->point >= buffer->len) return;
+ buffer->point++;
+}
+
+void buffer_move_up(buffer_t *buffer) {
+ if (!buffer) return;
+
+ size_t current_start = buffer_line_start_at(buffer, buffer->point);
+ if (current_start == 0) {
+ buffer->point = 0;
+ return;
+ }
+
+ size_t wanted_col = buffer->point - current_start;
+ size_t prev_end = current_start - 1;
+ size_t prev_start = buffer_line_start_at(buffer, prev_end);
+ size_t prev_len = prev_end - prev_start;
+
+ buffer->point = prev_start + ECEX_MIN(wanted_col, prev_len);
+}
+
+void buffer_move_down(buffer_t *buffer) {
+ if (!buffer) return;
+
+ size_t current_start = buffer_line_start_at(buffer, buffer->point);
+ size_t current_end = buffer_line_end_at(buffer, buffer->point);
+ if (current_end >= buffer->len) {
+ buffer->point = buffer->len;
+ return;
+ }
+
+ size_t wanted_col = buffer->point - current_start;
+ size_t next_start = current_end + 1;
+ size_t next_end = buffer_line_end_at(buffer, next_start);
+ size_t next_len = next_end - next_start;
+
+ buffer->point = next_start + ECEX_MIN(wanted_col, next_len);
+}
+
+void buffer_move_word_left(buffer_t *buffer) {
+ if (!buffer) return;
+
+ size_t pos = buffer->point;
+ while (pos > 0 && !buffer_is_word_char(buffer->data[pos - 1])) pos--;
+ while (pos > 0 && buffer_is_word_char(buffer->data[pos - 1])) pos--;
+ buffer->point = pos;
+}
+
+void buffer_move_word_right(buffer_t *buffer) {
+ if (!buffer) return;
+
+ size_t pos = buffer->point;
+ while (pos < buffer->len && !buffer_is_word_char(buffer->data[pos])) pos++;
+ while (pos < buffer->len && buffer_is_word_char(buffer->data[pos])) pos++;
+ buffer->point = pos;
+}
+
+void buffer_move_beginning_of_line(buffer_t *buffer) {
+ if (!buffer) return;
+ buffer->point = buffer_line_start_at(buffer, buffer->point);
+}
+
+void buffer_move_end_of_line(buffer_t *buffer) {
+ if (!buffer) return;
+ buffer->point = buffer_line_end_at(buffer, buffer->point);
+}
+
+void buffer_move_beginning_of_buffer(buffer_t *buffer) {
+ if (!buffer) return;
+ buffer->point = 0;
+}
+
+void buffer_move_end_of_buffer(buffer_t *buffer) {
+ if (!buffer) return;
+ buffer->point = buffer->len;
+}
+
+void buffer_set_mark(buffer_t *buffer, size_t mark) {
+ if (!buffer) return;
+ buffer->mark = ECEX_MIN(mark, buffer->len);
+ buffer->mark_active = 1;
+}
+
+void buffer_clear_mark(buffer_t *buffer) {
+ if (!buffer) return;
+ buffer->mark_active = 0;
+}
+
+int buffer_has_selection(buffer_t *buffer) {
+ return buffer && buffer->mark_active && buffer->mark != buffer->point;
+}
+
+void buffer_selection_range(buffer_t *buffer, size_t *out_start, size_t *out_end) {
+ size_t start = 0;
+ size_t end = 0;
+
+ if (buffer && buffer_has_selection(buffer)) {
+ start = ECEX_MIN(buffer->point, buffer->mark);
+ end = ECEX_MAX(buffer->point, buffer->mark);
+ }
+
+ if (out_start) *out_start = start;
+ if (out_end) *out_end = end;
+}
+
+size_t buffer_line_start_at(buffer_t *buffer, size_t pos) {
+ if (!buffer) return 0;
+ pos = ECEX_MIN(pos, buffer->len);
+
+ while (pos > 0 && buffer->data[pos - 1] != '\n') {
+ pos--;
+ }
+
+ return pos;
+}
+
+size_t buffer_line_end_at(buffer_t *buffer, size_t pos) {
+ if (!buffer) return 0;
+ pos = ECEX_MIN(pos, buffer->len);
+
+ while (pos < buffer->len && buffer->data[pos] != '\n') {
+ pos++;
+ }
+
+ return pos;
+}
+
+size_t buffer_current_line_start(buffer_t *buffer) {
+ if (!buffer) return 0;
+ return buffer_line_start_at(buffer, buffer->point);
+}
+
+size_t buffer_current_line_end(buffer_t *buffer) {
+ if (!buffer) return 0;
+ return buffer_line_end_at(buffer, buffer->point);
+}
+
+size_t buffer_current_column(buffer_t *buffer) {
+ if (!buffer) return 0;
+ return buffer->point - buffer_line_start_at(buffer, buffer->point);
+}
+
+size_t buffer_current_line_number(buffer_t *buffer) {
+ if (!buffer) return 0;
+
+ size_t line = 1;
+ for (size_t i = 0; i < buffer->point && i < buffer->len; i++) {
+ if (buffer->data[i] == '\n') line++;
+ }
+
+ return line;
+}
+
+size_t buffer_line_count(buffer_t *buffer) {
+ if (!buffer || buffer->len == 0) return 1;
+
+ size_t lines = 1;
+ for (size_t i = 0; i < buffer->len; i++) {
+ if (buffer->data[i] == '\n') lines++;
+ }
+
+ return lines;
+}
+
+char *buffer_substring(buffer_t *buffer, size_t start, size_t end) {
+ if (!buffer || start > end || end > buffer->len) return NULL;
+
+ size_t len = end - start;
+ char *copy = malloc(len + 1);
+ if (!copy) return NULL;
+
+ memcpy(copy, buffer->data + start, len);
+ copy[len] = '\0';
+ return copy;
+}
+
+char *buffer_current_line_copy(buffer_t *buffer) {
+ if (!buffer) return NULL;
+ return buffer_substring(buffer,
+ buffer_current_line_start(buffer),
+ buffer_current_line_end(buffer));
+}
+
+int buffer_load_file(buffer_t *buffer, const char *path) {
+ if (buffer_require_editable(buffer) != ECEX_OK || !path) return ECEX_ERR;
+
+ size_t size = 0;
+ char *new_data = ecex_read_entire_file(path, &size);
+ if (!new_data) return ECEX_ERR;
+
+ char *new_path = ecex_strdup(path);
+ if (!new_path) {
+ free(new_data);
+ return ECEX_ERR;
+ }
+
+ free(buffer->data);
+ free(buffer->path);
+
+ buffer->data = new_data;
+ buffer->path = new_path;
+ buffer->len = size;
+ buffer->cap = size + 1;
+ buffer->point = 0;
+ buffer->mark = 0;
+ buffer->mark_active = 0;
+ buffer->scroll_line = 0;
+ buffer->scroll_col = 0;
+ buffer->modified = 0;
+ buffer_clear_undo(buffer);
+ return ECEX_OK;
+}
+
+int buffer_save(buffer_t *buffer) {
+ if (!buffer || buffer->read_only || !buffer->path) return ECEX_ERR;
+
+ FILE *file = fopen(buffer->path, "wb");
+ if (!file) return ECEX_ERR;
+
+ size_t written = fwrite(buffer->data, 1, buffer->len, file);
+ int close_result = fclose(file);
+
+ if (close_result != 0 || written != buffer->len) return ECEX_ERR;
+
+ buffer->modified = 0;
+ return ECEX_OK;
+}
+
+int buffer_save_as(buffer_t *buffer, const char *path) {
+ if (buffer_require_editable(buffer) != ECEX_OK || !path) return ECEX_ERR;
+
+ char *new_path = ecex_strdup(path);
+ if (!new_path) return ECEX_ERR;
+
+ free(buffer->path);
+ buffer->path = new_path;
+ return buffer_save(buffer);
+}
+
+
+
+int buffer_undo(buffer_t *buffer) {
+ if (buffer_require_editable(buffer) != ECEX_OK) return ECEX_ERR;
+ if (buffer->undo_count == 0) return ECEX_OK;
+ if (buffer_push_snapshot_to(&buffer->redo_stack, &buffer->redo_count, &buffer->redo_cap, buffer) != ECEX_OK) return ECEX_ERR;
+ ecex_undo_entry_t entry = buffer->undo_stack[--buffer->undo_count];
+ int result = buffer_restore_snapshot(buffer, &entry);
+ buffer_undo_entry_free(&entry);
+ return result;
+}
+
+int buffer_redo(buffer_t *buffer) {
+ if (buffer_require_editable(buffer) != ECEX_OK) return ECEX_ERR;
+ if (buffer->redo_count == 0) return ECEX_OK;
+ if (buffer_push_snapshot_to(&buffer->undo_stack, &buffer->undo_count, &buffer->undo_cap, buffer) != ECEX_OK) return ECEX_ERR;
+ ecex_undo_entry_t entry = buffer->redo_stack[--buffer->redo_count];
+ int result = buffer_restore_snapshot(buffer, &entry);
+ buffer_undo_entry_free(&entry);
+ return result;
+}
+
+void buffer_clear_undo(buffer_t *buffer) {
+ if (!buffer) return;
+ buffer_undo_stack_clear(buffer->undo_stack, buffer->undo_count);
+ buffer_undo_stack_clear(buffer->redo_stack, buffer->redo_count);
+ buffer->undo_count = 0;
+ buffer->redo_count = 0;
+}
+
+int buffer_search_forward(buffer_t *buffer, const char *query, size_t start, size_t *out_pos) {
+ if (!buffer || !query || !query[0]) return ECEX_ERR;
+ if (start > buffer->len) start = buffer->len;
+ char *hit = strstr(buffer->data + start, query);
+ if (!hit && start > 0) hit = strstr(buffer->data, query);
+ if (!hit) return ECEX_ERR;
+ if (out_pos) *out_pos = (size_t)(hit - buffer->data);
+ return ECEX_OK;
+}
+
+int buffer_search_backward(buffer_t *buffer, const char *query, size_t start, size_t *out_pos) {
+ if (!buffer || !query || !query[0]) return ECEX_ERR;
+ size_t qlen = strlen(query);
+ if (qlen > buffer->len) return ECEX_ERR;
+ if (start > buffer->len) start = buffer->len;
+ size_t best = (size_t)-1;
+ for (size_t i = 0; i + qlen <= start; i++) {
+ if (memcmp(buffer->data + i, query, qlen) == 0) best = i;
+ }
+ if (best == (size_t)-1) {
+ for (size_t i = start; i + qlen <= buffer->len; i++) {
+ if (memcmp(buffer->data + i, query, qlen) == 0) best = i;
+ }
+ }
+ if (best == (size_t)-1) return ECEX_ERR;
+ if (out_pos) *out_pos = best;
+ return ECEX_OK;
+}
+
+void buffer_set_interactive(buffer_t *buffer, int interactive) {
+ if (!buffer) return;
+ buffer->interactive = interactive ? 1 : 0;
+}
+
+int buffer_is_interactive(buffer_t *buffer) {
+ return buffer && buffer->interactive;
+}
+
+int buffer_clear_interactive_actions(buffer_t *buffer) {
+ if (!buffer) return ECEX_ERR;
+
+ for (size_t i = 0; i < buffer->interactive_action_count; i++) {
+ free(buffer->interactive_actions[i].payload);
+ buffer->interactive_actions[i].payload = NULL;
+ buffer->interactive_actions[i].fn = NULL;
+ buffer->interactive_actions[i].userdata = NULL;
+ buffer->interactive_actions[i].line = 0;
+ }
+
+ buffer->interactive_action_count = 0;
+ return ECEX_OK;
+}
+
+int buffer_add_interactive_action(buffer_t *buffer,
+ size_t line,
+ ecex_interactive_line_fn fn,
+ const char *payload,
+ void *userdata) {
+ if (!buffer || !fn) return ECEX_ERR;
+
+ if (ECEX_GROW_ARRAY(buffer->interactive_actions,
+ buffer->interactive_action_count,
+ buffer->interactive_action_cap,
+ 16) != ECEX_OK) {
+ return ECEX_ERR;
+ }
+
+ char *payload_copy = ecex_strdup(payload);
+ if (payload && !payload_copy) return ECEX_ERR;
+
+ ecex_interactive_line_action_t *action =
+ &buffer->interactive_actions[buffer->interactive_action_count++];
+
+ action->line = line;
+ action->fn = fn;
+ action->payload = payload_copy;
+ action->userdata = userdata;
+
+ buffer->interactive = 1;
+ return ECEX_OK;
+}
+
+ecex_interactive_line_action_t *buffer_interactive_action_at_line(buffer_t *buffer,
+ size_t line) {
+ if (!buffer || !buffer->interactive) return NULL;
+
+ for (size_t i = 0; i < buffer->interactive_action_count; i++) {
+ if (buffer->interactive_actions[i].line == line) {
+ return &buffer->interactive_actions[i];
+ }
+ }
+
+ return NULL;
+}