commit e7154750753d813668b4616f6ecedc709077f075
parent 24f445586a405a0772b5d45a48b55d7c4fc82c00
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Thu, 28 May 2026 22:14:11 -0700
Add dbg line editing and completions
Diffstat:
6 files changed, 730 insertions(+), 5 deletions(-)
diff --git a/doc/DBG_TODO.md b/doc/DBG_TODO.md
@@ -126,8 +126,8 @@ experience is solid.
## Shared REPL Work
-- [ ] Add line editing and history.
-- [ ] Add completion for commands, symbols, locals, files, and breakpoint ids.
+- [x] Add line editing and history.
+- [x] Add completion for commands, symbols, locals, files, and breakpoint ids.
- [ ] Repeat the last stepping command on a blank line.
- [ ] Print a compact source context after source-level stops.
- [ ] Make stop messages distinguish user breakpoints, internal step
diff --git a/driver/dbg.c b/driver/dbg.c
@@ -393,6 +393,7 @@ typedef struct DbgState {
int has_stop;
CfreeStopInfo last_stop;
uint64_t jit_counter;
+ DriverLineHistory line_history;
} DbgState;
/* SIGINT trampoline. The handler in env.c calls our cb with this state;
@@ -2811,6 +2812,266 @@ static int dbg_dispatch(DbgState* s, char* line) {
}
/* ============================================================
+ * Completion
+ * ============================================================ */
+
+static int dbg_slice_has_prefix(CfreeSlice s, const char* prefix,
+ size_t prefix_len) {
+ return s.len >= prefix_len && memcmp(s.s, prefix, prefix_len) == 0;
+}
+
+static int dbg_cstr_has_prefix(const char* s, const char* prefix,
+ size_t prefix_len) {
+ return driver_strlen(s) >= prefix_len && memcmp(s, prefix, prefix_len) == 0;
+}
+
+static int dbg_completion_seen(DriverLineCompletionList* out,
+ const char* text, size_t len) {
+ uint32_t i;
+ for (i = 0; i < out->count; ++i) {
+ if (driver_strlen(out->items[i].text) == len &&
+ memcmp(out->items[i].text, text, len) == 0)
+ return 1;
+ }
+ return 0;
+}
+
+static void dbg_completion_add_cstr(DriverLineCompletionList* out,
+ const char* prefix, size_t prefix_len,
+ const char* text) {
+ size_t len = driver_strlen(text);
+ if (!dbg_cstr_has_prefix(text, prefix, prefix_len)) return;
+ if (dbg_completion_seen(out, text, len)) return;
+ (void)driver_line_completion_add(out, text, len);
+}
+
+static void dbg_completion_add_slice(DriverLineCompletionList* out,
+ const char* prefix, size_t prefix_len,
+ CfreeSlice text) {
+ if (!text.s || !dbg_slice_has_prefix(text, prefix, prefix_len)) return;
+ if (dbg_completion_seen(out, text.s, text.len)) return;
+ (void)driver_line_completion_add(out, text.s, text.len);
+}
+
+static void dbg_completion_add_u64(DriverLineCompletionList* out,
+ const char* prefix, size_t prefix_len,
+ uint64_t v) {
+ char buf[32];
+ size_t n = 0;
+ char rev[32];
+ size_t r = 0;
+ if (v == 0) {
+ rev[r++] = '0';
+ } else {
+ while (v && r < sizeof(rev)) {
+ rev[r++] = (char)('0' + (v % 10u));
+ v /= 10u;
+ }
+ }
+ while (r > 0 && n + 1u < sizeof(buf)) buf[n++] = rev[--r];
+ buf[n] = '\0';
+ dbg_completion_add_cstr(out, prefix, prefix_len, buf);
+}
+
+static void dbg_word_bounds(const char* line, size_t cursor, size_t* start,
+ size_t* end) {
+ size_t len = driver_strlen(line);
+ size_t s = cursor < len ? cursor : len;
+ size_t e = s;
+ while (s > 0 && !dbg_isspace((unsigned char)line[s - 1])) --s;
+ while (e < len && !dbg_isspace((unsigned char)line[e])) ++e;
+ *start = s;
+ *end = e;
+}
+
+static size_t dbg_read_word_at(const char* line, size_t start, char* out,
+ size_t cap) {
+ size_t i = start;
+ size_t n = 0;
+ while (line[i] && dbg_isspace((unsigned char)line[i])) ++i;
+ while (line[i] && !dbg_isspace((unsigned char)line[i])) {
+ if (n + 1u < cap) out[n++] = line[i];
+ ++i;
+ }
+ if (cap) out[n] = '\0';
+ return n;
+}
+
+static void dbg_complete_commands(DriverLineCompletionList* out,
+ const char* prefix, size_t prefix_len) {
+ static const char* const cmds[] = {
+ "help", "quit", "exit", "run", "cont",
+ "continue", "step", "sl", "next", "finish",
+ "jit", "edit", "expr", "bt", "backtrace",
+ "where", "break", "info", "ignore", "delete",
+ "enable", "disable", "print", "set", "jump",
+ "list", "disasm", "x", "x/i", ":language",
+ ":lang", "language", "abort", "h", "q",
+ "r", "c", "s", "sl", "n",
+ "b", "d", "p", "l", "e",
+ };
+ size_t i;
+ for (i = 0; i < sizeof(cmds) / sizeof(cmds[0]); ++i)
+ dbg_completion_add_cstr(out, prefix, prefix_len, cmds[i]);
+}
+
+static void dbg_complete_info(DriverLineCompletionList* out,
+ const char* prefix, size_t prefix_len) {
+ static const char* const words[] = {
+ "b", "break", "breakpoints", "reg", "registers",
+ "locals", "args", "functions", "func", "variables",
+ "var",
+ };
+ size_t i;
+ for (i = 0; i < sizeof(words) / sizeof(words[0]); ++i)
+ dbg_completion_add_cstr(out, prefix, prefix_len, words[i]);
+}
+
+static void dbg_complete_languages(DriverLineCompletionList* out,
+ const char* prefix, size_t prefix_len) {
+ static const char* const langs[] = {"c", "toy", "asm", "wasm", "wat"};
+ size_t i;
+ for (i = 0; i < sizeof(langs) / sizeof(langs[0]); ++i)
+ dbg_completion_add_cstr(out, prefix, prefix_len, langs[i]);
+}
+
+static void dbg_complete_symbols(DbgState* s, DriverLineCompletionList* out,
+ const char* prefix, size_t prefix_len,
+ int funcs, int objects) {
+ CfreeJitSymIter* it = NULL;
+ CfreeJitSym sym;
+ if (!s->jit || cfree_jit_sym_iter_new(s->jit, &it) != CFREE_OK) return;
+ for (;;) {
+ CfreeIterResult r = cfree_jit_sym_iter_next(it, &sym);
+ if (r != CFREE_ITER_ITEM) break;
+ if ((sym.kind == CFREE_SK_FUNC && funcs) ||
+ (sym.kind == CFREE_SK_OBJ && objects))
+ dbg_completion_add_slice(out, prefix, prefix_len, sym.name);
+ }
+ cfree_jit_sym_iter_free(it);
+}
+
+static void dbg_complete_locals(DbgState* s, DriverLineCompletionList* out,
+ const char* prefix, size_t prefix_len) {
+ CfreeDwarfVarIter* it = NULL;
+ CfreeDwarfVar v;
+ if (!s->has_stop || !s->dwarf) return;
+ if (cfree_dwarf_vars_at_new(s->dwarf,
+ dbg_pc_rt_to_img(s, s->last_stop.regs.pc),
+ CFREE_DVRM_LOCAL | CFREE_DVRM_ARG, &it) !=
+ CFREE_OK)
+ return;
+ for (;;) {
+ CfreeIterResult r = cfree_dwarf_vars_at_next(it, &v);
+ if (r != CFREE_ITER_ITEM) break;
+ dbg_completion_add_slice(out, prefix, prefix_len, v.name);
+ }
+ cfree_dwarf_vars_at_free(it);
+}
+
+static void dbg_complete_files(DbgState* s, DriverLineCompletionList* out,
+ const char* prefix, size_t prefix_len) {
+ CfreeDwarfCuIter* cui = NULL;
+ CfreeDwarfCu cu;
+ if (!s->dwarf) return;
+ if (cfree_dwarf_cu_iter_new(s->dwarf, &cui) != CFREE_OK) return;
+ for (;;) {
+ CfreeDwarfLineIter* li = NULL;
+ CfreeDwarfLineRow row;
+ CfreeIterResult cr = cfree_dwarf_cu_iter_next(cui, &cu);
+ if (cr != CFREE_ITER_ITEM) break;
+ if (cfree_dwarf_line_iter_new(s->dwarf, cu.offset, &li) != CFREE_OK)
+ continue;
+ for (;;) {
+ CfreeSlice file = CFREE_SLICE_NULL;
+ CfreeIterResult lr = cfree_dwarf_line_iter_next(li, &row);
+ if (lr != CFREE_ITER_ITEM) break;
+ if (cfree_dwarf_line_file(s->dwarf, cu.offset, row.file_index, &file) ==
+ CFREE_OK)
+ dbg_completion_add_slice(out, prefix, prefix_len, file);
+ }
+ cfree_dwarf_line_iter_free(li);
+ }
+ cfree_dwarf_cu_iter_free(cui);
+}
+
+static void dbg_complete_breakpoint_ids(DbgState* s,
+ DriverLineCompletionList* out,
+ const char* prefix,
+ size_t prefix_len) {
+ uint32_t i;
+ for (i = 0; i < s->nbps; ++i)
+ dbg_completion_add_u64(out, prefix, prefix_len, (uint64_t)s->bps[i].id);
+}
+
+static void dbg_complete(void* user, const char* line, size_t cursor,
+ DriverLineCompletionList* out) {
+ DbgState* s = (DbgState*)user;
+ char cmd[WORD_CAP];
+ char arg1[WORD_CAP];
+ size_t start, end;
+ size_t prefix_len;
+ const char* prefix;
+
+ dbg_word_bounds(line, cursor, &start, &end);
+ out->replace_start = start;
+ out->replace_end = end;
+ prefix = line + start;
+ prefix_len = cursor > start ? cursor - start : 0;
+
+ dbg_read_word_at(line, 0, cmd, sizeof(cmd));
+ if (start == 0 || cmd[0] == '\0') {
+ dbg_complete_commands(out, prefix, prefix_len);
+ return;
+ }
+
+ if (driver_streq(cmd, "info")) {
+ dbg_read_word_at(line, start == 0 ? 0 : driver_strlen(cmd), arg1,
+ sizeof(arg1));
+ if (arg1[0] == '\0' || start <= driver_strlen(cmd) + 1u) {
+ dbg_complete_info(out, prefix, prefix_len);
+ return;
+ }
+ }
+
+ if (driver_streq(cmd, ":language") || driver_streq(cmd, ":lang") ||
+ driver_streq(cmd, "language")) {
+ dbg_complete_languages(out, prefix, prefix_len);
+ return;
+ }
+ if (driver_streq(cmd, "jit") || driver_streq(cmd, "edit") ||
+ driver_streq(cmd, "e")) {
+ dbg_complete_languages(out, prefix, prefix_len);
+ dbg_complete_symbols(s, out, prefix, prefix_len, 1, 0);
+ return;
+ }
+ if (driver_streq(cmd, "b") || driver_streq(cmd, "break")) {
+ dbg_complete_symbols(s, out, prefix, prefix_len, 1, 0);
+ dbg_complete_files(s, out, prefix, prefix_len);
+ return;
+ }
+ if (driver_streq(cmd, "list") || driver_streq(cmd, "l")) {
+ dbg_complete_files(s, out, prefix, prefix_len);
+ return;
+ }
+ if (driver_streq(cmd, "p") || driver_streq(cmd, "print") ||
+ driver_streq(cmd, "set")) {
+ dbg_complete_locals(s, out, prefix, prefix_len);
+ dbg_complete_symbols(s, out, prefix, prefix_len, 0, 1);
+ return;
+ }
+ if (driver_streq(cmd, "d") || driver_streq(cmd, "delete") ||
+ driver_streq(cmd, "enable") || driver_streq(cmd, "disable") ||
+ driver_streq(cmd, "ignore")) {
+ dbg_complete_breakpoint_ids(s, out, prefix, prefix_len);
+ return;
+ }
+
+ dbg_complete_locals(s, out, prefix, prefix_len);
+ dbg_complete_symbols(s, out, prefix, prefix_len, 1, 1);
+}
+
+/* ============================================================
* REPL
* ============================================================ */
@@ -2819,9 +3080,8 @@ static void dbg_repl(DbgState* s) {
driver_printf("cfree dbg — 'h' for help, 'q' to quit\n");
for (;;) {
int n;
- driver_printf("(cfree) ");
- driver_flush_stdout();
- n = driver_read_line(line, sizeof(line));
+ n = driver_read_line_edit(s->env, "(cfree) ", line, sizeof(line),
+ &s->line_history, dbg_complete, s);
if (n == 0) { /* EOF — Ctrl-D */
driver_printf("\n");
return;
@@ -2930,6 +3190,7 @@ int driver_dbg(int argc, char** argv) {
dbg_repl(&st);
+ driver_line_history_fini(&env, &st.line_history);
dbg_bps_release_all(&st);
dbg_compile_sessions_release(&st);
if (st.dwarf) cfree_dwarf_free(st.dwarf);
diff --git a/driver/env.h b/driver/env.h
@@ -173,6 +173,38 @@ void driver_release_bytes(const CfreeFileIO*, DriverLoad*);
* up to the next newline is consumed silently. */
int driver_read_line(char* buf, size_t cap);
+typedef struct DriverLineHistory {
+ char** items;
+ size_t* sizes;
+ uint32_t count;
+ uint32_t cap;
+} DriverLineHistory;
+
+typedef struct DriverLineCompletion {
+ char* text;
+ size_t size;
+} DriverLineCompletion;
+
+typedef struct DriverLineCompletionList {
+ DriverEnv* env;
+ DriverLineCompletion* items;
+ uint32_t count;
+ uint32_t cap;
+ size_t replace_start;
+ size_t replace_end;
+} DriverLineCompletionList;
+
+typedef void (*DriverLineCompleteFn)(void* user, const char* line,
+ size_t cursor,
+ DriverLineCompletionList* out);
+
+int driver_line_completion_add(DriverLineCompletionList*, const char* text,
+ size_t len);
+int driver_read_line_edit(DriverEnv*, const char* prompt, char* buf,
+ size_t cap, DriverLineHistory*,
+ DriverLineCompleteFn, void* complete_user);
+void driver_line_history_fini(DriverEnv*, DriverLineHistory*);
+
/* Flush the host stdout. The dbg REPL prompt has no trailing newline, so
* without an explicit flush the prompt stays buffered until the next
* line of output. */
diff --git a/driver/env/common.c b/driver/env/common.c
@@ -206,3 +206,48 @@ void driver_printf(const char* fmt, ...) {
void driver_flush_stdout(void) { fflush(stdout); }
const char* driver_getenv(const char* name) { return getenv(name); }
+
+int driver_line_completion_add(DriverLineCompletionList* l, const char* text,
+ size_t len) {
+ DriverLineCompletion* ni;
+ char* copy;
+ uint32_t nc;
+ size_t old_size;
+ size_t new_size;
+ if (!l || !l->env || !text) return 1;
+ if (l->count == l->cap) {
+ nc = l->cap ? l->cap * 2u : 16u;
+ old_size = (size_t)l->cap * sizeof(*l->items);
+ new_size = (size_t)nc * sizeof(*l->items);
+ ni = (DriverLineCompletion*)l->env->heap->realloc(
+ l->env->heap, l->items, old_size, new_size,
+ _Alignof(DriverLineCompletion));
+ if (!ni) return 1;
+ l->items = ni;
+ l->cap = nc;
+ }
+ copy = (char*)driver_alloc(l->env, len + 1u);
+ if (!copy) return 1;
+ driver_memcpy(copy, text, len);
+ copy[len] = '\0';
+ l->items[l->count].text = copy;
+ l->items[l->count].size = len + 1u;
+ l->count++;
+ return 0;
+}
+
+void driver_line_history_fini(DriverEnv* env, DriverLineHistory* h) {
+ uint32_t i;
+ if (!env || !h) return;
+ for (i = 0; i < h->count; ++i) {
+ if (h->items && h->items[i]) driver_free(env, h->items[i], h->sizes[i]);
+ }
+ if (h->items)
+ driver_free(env, h->items, (size_t)h->cap * sizeof(*h->items));
+ if (h->sizes)
+ driver_free(env, h->sizes, (size_t)h->cap * sizeof(*h->sizes));
+ {
+ DriverLineHistory z = {0};
+ *h = z;
+ }
+}
diff --git a/driver/env/posix.c b/driver/env/posix.c
@@ -15,6 +15,7 @@
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/wait.h>
+#include <termios.h>
#include <time.h>
#include <unistd.h>
@@ -660,6 +661,341 @@ int driver_read_line(char* buf, size_t cap) {
}
}
+static void line_completion_list_fini(DriverLineCompletionList* l) {
+ uint32_t i;
+ if (!l || !l->env) return;
+ for (i = 0; i < l->count; ++i) {
+ if (l->items[i].text)
+ driver_free(l->env, l->items[i].text, l->items[i].size);
+ }
+ if (l->items)
+ driver_free(l->env, l->items, (size_t)l->cap * sizeof(*l->items));
+}
+
+static int line_history_add(DriverEnv* env, DriverLineHistory* h,
+ const char* line, size_t len) {
+ char** ni;
+ size_t* ns;
+ char* copy;
+ uint32_t nc;
+ size_t old_items_size;
+ size_t new_items_size;
+ size_t old_sizes_size;
+ size_t new_sizes_size;
+ if (!env || !h || !line || len == 0) return 0;
+ if (h->count > 0 && h->items[h->count - 1] &&
+ strlen(h->items[h->count - 1]) == len &&
+ memcmp(h->items[h->count - 1], line, len) == 0)
+ return 0;
+ if (h->count == h->cap) {
+ nc = h->cap ? h->cap * 2u : 32u;
+ old_items_size = (size_t)h->cap * sizeof(*h->items);
+ new_items_size = (size_t)nc * sizeof(*h->items);
+ old_sizes_size = (size_t)h->cap * sizeof(*h->sizes);
+ new_sizes_size = (size_t)nc * sizeof(*h->sizes);
+ ni = (char**)env->heap->realloc(env->heap, h->items, old_items_size,
+ new_items_size, _Alignof(char*));
+ if (!ni) return 1;
+ h->items = ni;
+ ns = (size_t*)env->heap->realloc(env->heap, h->sizes, old_sizes_size,
+ new_sizes_size, _Alignof(size_t));
+ if (!ns) return 1;
+ h->sizes = ns;
+ h->cap = nc;
+ }
+ copy = (char*)driver_alloc(env, len + 1u);
+ if (!copy) return 1;
+ memcpy(copy, line, len);
+ copy[len] = '\0';
+ h->items[h->count] = copy;
+ h->sizes[h->count] = len + 1u;
+ h->count++;
+ return 0;
+}
+
+static void line_redraw(const char* prompt, const char* buf, size_t len,
+ size_t cursor) {
+ size_t back = len - cursor;
+ fputc('\r', stdout);
+ fputs(prompt, stdout);
+ if (len) fwrite(buf, 1, len, stdout);
+ fputs("\033[K", stdout);
+ if (back) fprintf(stdout, "\033[%zuD", back);
+ fflush(stdout);
+}
+
+static void line_set(char* buf, size_t cap, size_t* len, size_t* cursor,
+ const char* src) {
+ size_t n = strlen(src);
+ if (n >= cap) n = cap - 1u;
+ memmove(buf, src, n);
+ buf[n] = '\0';
+ *len = n;
+ *cursor = n;
+}
+
+static int line_replace(char* buf, size_t cap, size_t* len, size_t* cursor,
+ size_t start, size_t end, const char* rep,
+ size_t rep_len) {
+ size_t tail;
+ if (start > end || end > *len) return 1;
+ tail = *len - end;
+ if (start + rep_len + tail + 1u > cap) return 1;
+ memmove(buf + start + rep_len, buf + end, tail + 1u);
+ if (rep_len) memcpy(buf + start, rep, rep_len);
+ *len = start + rep_len + tail;
+ *cursor = start + rep_len;
+ return 0;
+}
+
+static size_t line_common_prefix(const DriverLineCompletionList* l) {
+ size_t n;
+ uint32_t i;
+ if (!l || l->count == 0) return 0;
+ n = strlen(l->items[0].text);
+ for (i = 1; i < l->count; ++i) {
+ size_t j = 0;
+ const char* s = l->items[i].text;
+ while (j < n && s[j] && s[j] == l->items[0].text[j]) ++j;
+ n = j;
+ }
+ return n;
+}
+
+static void line_default_complete_range(const char* buf, size_t cursor,
+ size_t* start, size_t* end) {
+ size_t s = cursor;
+ while (s > 0 && buf[s - 1] != ' ' && buf[s - 1] != '\t') --s;
+ *start = s;
+ *end = cursor;
+}
+
+static void line_complete(DriverEnv* env, char* buf, size_t cap, size_t* len,
+ size_t* cursor, const char* prompt,
+ DriverLineCompleteFn complete, void* complete_user) {
+ DriverLineCompletionList list;
+ size_t common;
+ size_t cur_len;
+ uint32_t i;
+ if (!complete) return;
+ {
+ DriverLineCompletionList z = {0};
+ list = z;
+ }
+ list.env = env;
+ line_default_complete_range(buf, *cursor, &list.replace_start,
+ &list.replace_end);
+ complete(complete_user, buf, *cursor, &list);
+ if (list.replace_end > *len) list.replace_end = *len;
+ if (list.replace_start > list.replace_end)
+ list.replace_start = list.replace_end;
+ if (list.count == 1) {
+ size_t rn = strlen(list.items[0].text);
+ if (line_replace(buf, cap, len, cursor, list.replace_start,
+ list.replace_end, list.items[0].text, rn) != 0)
+ fputc('\a', stdout);
+ line_redraw(prompt, buf, *len, *cursor);
+ line_completion_list_fini(&list);
+ return;
+ }
+ if (list.count > 1) {
+ common = line_common_prefix(&list);
+ cur_len = *cursor > list.replace_start ? *cursor - list.replace_start : 0;
+ if (common > cur_len) {
+ if (line_replace(buf, cap, len, cursor, list.replace_start,
+ list.replace_end, list.items[0].text, common) != 0)
+ fputc('\a', stdout);
+ line_redraw(prompt, buf, *len, *cursor);
+ } else {
+ fputc('\n', stdout);
+ for (i = 0; i < list.count; ++i)
+ fprintf(stdout, " %s\n", list.items[i].text);
+ line_redraw(prompt, buf, *len, *cursor);
+ }
+ } else {
+ fputc('\a', stdout);
+ fflush(stdout);
+ }
+ line_completion_list_fini(&list);
+}
+
+int driver_read_line_edit(DriverEnv* env, const char* prompt, char* buf,
+ size_t cap, DriverLineHistory* hist,
+ DriverLineCompleteFn complete,
+ void* complete_user) {
+ struct termios orig;
+ struct termios raw;
+ char* saved = NULL;
+ size_t saved_size = 0;
+ size_t len = 0;
+ size_t cursor = 0;
+ uint32_t hist_pos = 0;
+ int have_saved = 0;
+ int raw_enabled = 0;
+ int out_rc = -1;
+
+ if (!buf || cap < 2) return -1;
+ if (!prompt) prompt = "";
+ if (!isatty(STDIN_FILENO) || !isatty(STDOUT_FILENO)) {
+ int n;
+ fputs(prompt, stdout);
+ fflush(stdout);
+ n = driver_read_line(buf, cap);
+ if (n > 0 && hist) (void)line_history_add(env, hist, buf, (size_t)n);
+ return n;
+ }
+
+ if (tcgetattr(STDIN_FILENO, &orig) != 0) goto out;
+ raw = orig;
+ raw.c_lflag &= (tcflag_t) ~(ICANON | ECHO | IEXTEN);
+ raw.c_iflag &= (tcflag_t) ~(IXON);
+ raw.c_cc[VMIN] = 1;
+ raw.c_cc[VTIME] = 0;
+ if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw) != 0) goto out;
+ raw_enabled = 1;
+
+ saved = (char*)driver_alloc(env, cap);
+ if (!saved) goto out;
+ saved_size = cap;
+ hist_pos = hist ? hist->count : 0;
+ buf[0] = '\0';
+
+ fputs(prompt, stdout);
+ fflush(stdout);
+ for (;;) {
+ unsigned char c;
+ ssize_t r = read(STDIN_FILENO, &c, 1);
+ if (r < 0) {
+ if (errno == EINTR) {
+ out_rc = -2;
+ goto out;
+ }
+ out_rc = -1;
+ goto out;
+ }
+ if (r == 0) {
+ out_rc = len ? (int)len : 0;
+ goto out;
+ }
+ if (c == '\r' || c == '\n') {
+ buf[len] = '\0';
+ fputc('\n', stdout);
+ if (hist) (void)line_history_add(env, hist, buf, len);
+ out_rc = (int)len;
+ goto out;
+ }
+ if (c == 4) {
+ if (len == 0) {
+ buf[0] = '\0';
+ out_rc = 0;
+ goto out;
+ }
+ continue;
+ }
+ if (c == '\t') {
+ buf[len] = '\0';
+ line_complete(env, buf, cap, &len, &cursor, prompt, complete,
+ complete_user);
+ continue;
+ }
+ if (c == 1) {
+ cursor = 0;
+ line_redraw(prompt, buf, len, cursor);
+ continue;
+ }
+ if (c == 5) {
+ cursor = len;
+ line_redraw(prompt, buf, len, cursor);
+ continue;
+ }
+ if (c == 11) {
+ buf[cursor] = '\0';
+ len = cursor;
+ line_redraw(prompt, buf, len, cursor);
+ continue;
+ }
+ if (c == 127 || c == 8) {
+ if (cursor == 0) {
+ fputc('\a', stdout);
+ fflush(stdout);
+ continue;
+ }
+ memmove(buf + cursor - 1u, buf + cursor, len - cursor + 1u);
+ --cursor;
+ --len;
+ line_redraw(prompt, buf, len, cursor);
+ continue;
+ }
+ if (c == 27) {
+ unsigned char seq[3];
+ ssize_t r1 = read(STDIN_FILENO, seq, 1);
+ ssize_t r2 = read(STDIN_FILENO, seq + 1, 1);
+ if (r1 != 1 || r2 != 1 || seq[0] != '[') continue;
+ if (seq[1] == 'A') {
+ if (hist && hist->count > 0 && hist_pos > 0) {
+ if (!have_saved) {
+ memcpy(saved, buf, len + 1u);
+ have_saved = 1;
+ }
+ --hist_pos;
+ line_set(buf, cap, &len, &cursor, hist->items[hist_pos]);
+ line_redraw(prompt, buf, len, cursor);
+ }
+ } else if (seq[1] == 'B') {
+ if (hist && have_saved && hist_pos < hist->count) {
+ ++hist_pos;
+ if (hist_pos == hist->count)
+ line_set(buf, cap, &len, &cursor, saved);
+ else
+ line_set(buf, cap, &len, &cursor, hist->items[hist_pos]);
+ line_redraw(prompt, buf, len, cursor);
+ }
+ } else if (seq[1] == 'C') {
+ if (cursor < len) {
+ ++cursor;
+ line_redraw(prompt, buf, len, cursor);
+ }
+ } else if (seq[1] == 'D') {
+ if (cursor > 0) {
+ --cursor;
+ line_redraw(prompt, buf, len, cursor);
+ }
+ } else if (seq[1] == 'H') {
+ cursor = 0;
+ line_redraw(prompt, buf, len, cursor);
+ } else if (seq[1] == 'F') {
+ cursor = len;
+ line_redraw(prompt, buf, len, cursor);
+ } else if (seq[1] >= '1' && seq[1] <= '9') {
+ ssize_t r3 = read(STDIN_FILENO, seq + 2, 1);
+ if (r3 == 1 && seq[1] == '3' && seq[2] == '~' && cursor < len) {
+ memmove(buf + cursor, buf + cursor + 1u, len - cursor);
+ --len;
+ line_redraw(prompt, buf, len, cursor);
+ }
+ }
+ continue;
+ }
+ if (c >= 32 && c != 127) {
+ if (len + 1u >= cap) {
+ fputc('\a', stdout);
+ fflush(stdout);
+ continue;
+ }
+ memmove(buf + cursor + 1u, buf + cursor, len - cursor + 1u);
+ buf[cursor] = (char)c;
+ ++cursor;
+ ++len;
+ line_redraw(prompt, buf, len, cursor);
+ }
+ }
+
+out:
+ if (raw_enabled) tcsetattr(STDIN_FILENO, TCSAFLUSH, &orig);
+ if (saved) driver_free(env, saved, saved_size);
+ return out_rc;
+}
+
/* ---------------- dlsym resolver ---------------- */
void* driver_dlsym_resolver(void* user, CfreeSlice name_s) {
diff --git a/driver/env/windows.c b/driver/env/windows.c
@@ -842,6 +842,57 @@ int driver_read_line(char* buf, size_t cap) {
}
}
+static int win_line_history_add(DriverEnv* env, DriverLineHistory* h,
+ const char* line, size_t len) {
+ char** ni;
+ size_t* ns;
+ char* copy;
+ uint32_t nc;
+ if (!env || !h || !line || len == 0) return 0;
+ if (h->count == h->cap) {
+ size_t old_items = (size_t)h->cap * sizeof(*h->items);
+ size_t new_items;
+ size_t old_sizes = (size_t)h->cap * sizeof(*h->sizes);
+ size_t new_sizes;
+ nc = h->cap ? h->cap * 2u : 32u;
+ new_items = (size_t)nc * sizeof(*h->items);
+ new_sizes = (size_t)nc * sizeof(*h->sizes);
+ ni = (char**)env->heap->realloc(env->heap, h->items, old_items, new_items,
+ _Alignof(char*));
+ if (!ni) return 1;
+ h->items = ni;
+ ns = (size_t*)env->heap->realloc(env->heap, h->sizes, old_sizes, new_sizes,
+ _Alignof(size_t));
+ if (!ns) return 1;
+ h->sizes = ns;
+ h->cap = nc;
+ }
+ copy = (char*)driver_alloc(env, len + 1u);
+ if (!copy) return 1;
+ memcpy(copy, line, len);
+ copy[len] = '\0';
+ h->items[h->count] = copy;
+ h->sizes[h->count] = len + 1u;
+ h->count++;
+ return 0;
+}
+
+int driver_read_line_edit(DriverEnv* env, const char* prompt, char* buf,
+ size_t cap, DriverLineHistory* hist,
+ DriverLineCompleteFn complete,
+ void* complete_user) {
+ int n;
+ (void)complete;
+ (void)complete_user;
+ if (prompt && *prompt) {
+ fputs(prompt, stdout);
+ fflush(stdout);
+ }
+ n = driver_read_line(buf, cap);
+ if (n > 0 && hist) (void)win_line_history_add(env, hist, buf, (size_t)n);
+ return n;
+}
+
/* ============================================================
* dlsym via GetProcAddress over loaded modules
* ============================================================ */