From 5f85c1beae16feaae7f5b4d9a4f9cc32f74ef242 Mon Sep 17 00:00:00 2001 From: "Yukihiro \"Matz\" Matsumoto" Date: Fri, 12 Dec 2025 23:07:48 +0900 Subject: [PATCH] mruby-bin-mirb: add command history with Up/Down navigation Add in-memory command history for mirb sessions: - Up arrow on first line: navigate to older history entries - Down arrow on last line: navigate to newer history entries - Current input is preserved when browsing and restored when navigating past the newest entry - History uses a circular buffer (100 entries max) - Duplicate consecutive entries are not added Co-authored-by: Claude --- mrbgems/mruby-bin-mirb/tools/mirb/mirb.c | 4 + .../mruby-bin-mirb/tools/mirb/mirb_editor.c | 52 ++++- .../mruby-bin-mirb/tools/mirb/mirb_editor.h | 8 + .../mruby-bin-mirb/tools/mirb/mirb_history.c | 180 ++++++++++++++++++ .../mruby-bin-mirb/tools/mirb/mirb_history.h | 76 ++++++++ 5 files changed, 318 insertions(+), 2 deletions(-) create mode 100644 mrbgems/mruby-bin-mirb/tools/mirb/mirb_history.c create mode 100644 mrbgems/mruby-bin-mirb/tools/mirb/mirb_history.h diff --git a/mrbgems/mruby-bin-mirb/tools/mirb/mirb.c b/mrbgems/mruby-bin-mirb/tools/mirb/mirb.c index ffa102a29..6186d9f8f 100644 --- a/mrbgems/mruby-bin-mirb/tools/mirb/mirb.c +++ b/mrbgems/mruby-bin-mirb/tools/mirb/mirb.c @@ -750,6 +750,10 @@ main(int argc, char **argv) *(mrb->c->ci->stack + 1) = result; #endif } + /* Add to history after evaluation (success or error) */ + if (use_editor) { + mirb_editor_history_add(&editor, ruby_code); + } } ruby_code[0] = '\0'; last_code_line[0] = '\0'; diff --git a/mrbgems/mruby-bin-mirb/tools/mirb/mirb_editor.c b/mrbgems/mruby-bin-mirb/tools/mirb/mirb_editor.c index 47e565e67..628947c5e 100644 --- a/mrbgems/mruby-bin-mirb/tools/mirb/mirb_editor.c +++ b/mrbgems/mruby-bin-mirb/tools/mirb/mirb_editor.c @@ -192,6 +192,12 @@ mirb_editor_init(mirb_editor *ed) return FALSE; } + if (!mirb_history_init(&ed->hist, MIRB_HISTORY_SIZE)) { + mirb_buffer_free(&ed->buf); + mirb_term_cleanup(&ed->term); + return FALSE; + } + ed->prompt = "> "; ed->prompt_cont = "* "; ed->prompt_len = 2; @@ -210,6 +216,7 @@ mirb_editor_cleanup(mirb_editor *ed) { if (!ed->initialized) return; + mirb_history_free(&ed->hist); mirb_buffer_free(&ed->buf); mirb_term_cleanup(&ed->term); ed->initialized = FALSE; @@ -336,6 +343,8 @@ handle_key(mirb_editor *ed, int key, mirb_edit_result *result) { switch (key) { case MIRB_KEY_ENTER: + /* Stop history browsing */ + mirb_history_browse_stop(&ed->hist); /* Check if input is complete */ if (ed->check_complete) { char *code = mirb_buffer_to_string(&ed->buf); @@ -392,12 +401,40 @@ handle_key(mirb_editor *ed, int key, mirb_edit_result *result) case MIRB_KEY_UP: case MIRB_KEY_CTRL_P: - mirb_buffer_cursor_up(&ed->buf); + /* If on first line, navigate history; otherwise move cursor up */ + if (ed->buf.cursor_line == 0) { + /* Start history browsing if not already */ + if (!ed->hist.browsing) { + char *current = mirb_buffer_to_string(&ed->buf); + mirb_history_browse_start(&ed->hist, current); + free(current); + } + const char *prev = mirb_history_prev(&ed->hist); + if (prev) { + mirb_buffer_set_string(&ed->buf, prev); + mirb_buffer_cursor_finish(&ed->buf); + } + } + else { + mirb_buffer_cursor_up(&ed->buf); + } return TRUE; case MIRB_KEY_DOWN: case MIRB_KEY_CTRL_N: - mirb_buffer_cursor_down(&ed->buf); + /* If on last line, navigate history; otherwise move cursor down */ + if (ed->buf.cursor_line == ed->buf.line_count - 1) { + if (ed->hist.browsing) { + const char *next = mirb_history_next(&ed->hist); + if (next) { + mirb_buffer_set_string(&ed->buf, next); + mirb_buffer_cursor_finish(&ed->buf); + } + } + } + else { + mirb_buffer_cursor_down(&ed->buf); + } return TRUE; case MIRB_KEY_HOME: @@ -447,6 +484,8 @@ handle_key(mirb_editor *ed, int key, mirb_edit_result *result) default: /* Insert printable characters */ if (key >= 32 && key < 127) { + /* Stop history browsing when user types */ + mirb_history_browse_stop(&ed->hist); mirb_buffer_insert_char(&ed->buf, (char)key); /* Check for auto-dedent after typing 'end' or '}' */ if (should_dedent(&ed->buf, (char)key)) { @@ -605,3 +644,12 @@ mirb_editor_read_simple(mirb_editor *ed, char **out_str) *out_str = total; return MIRB_EDIT_OK; } + +/* + * Add entry to history + */ +void +mirb_editor_history_add(mirb_editor *ed, const char *entry) +{ + mirb_history_add(&ed->hist, entry); +} diff --git a/mrbgems/mruby-bin-mirb/tools/mirb/mirb_editor.h b/mrbgems/mruby-bin-mirb/tools/mirb/mirb_editor.h index b0c3ee00d..2ccc8f229 100644 --- a/mrbgems/mruby-bin-mirb/tools/mirb/mirb_editor.h +++ b/mrbgems/mruby-bin-mirb/tools/mirb/mirb_editor.h @@ -10,6 +10,7 @@ #include #include "mirb_term.h" #include "mirb_buffer.h" +#include "mirb_history.h" /* * Editor result codes @@ -34,6 +35,7 @@ typedef mrb_bool mirb_check_complete_fn(const char *code, void *user_data); typedef struct mirb_editor { mirb_term term; /* terminal state */ mirb_buffer buf; /* editing buffer */ + mirb_history hist; /* command history */ const char *prompt; /* primary prompt (e.g., "> ") */ const char *prompt_cont; /* continuation prompt (e.g., "* ") */ @@ -99,4 +101,10 @@ mirb_edit_result mirb_editor_read(mirb_editor *ed, char **out_str); */ mirb_edit_result mirb_editor_read_simple(mirb_editor *ed, char **out_str); +/* + * Add entry to history + * Called after successful command execution + */ +void mirb_editor_history_add(mirb_editor *ed, const char *entry); + #endif /* MIRB_EDITOR_H */ diff --git a/mrbgems/mruby-bin-mirb/tools/mirb/mirb_history.c b/mrbgems/mruby-bin-mirb/tools/mirb/mirb_history.c new file mode 100644 index 000000000..65045f0d5 --- /dev/null +++ b/mrbgems/mruby-bin-mirb/tools/mirb/mirb_history.c @@ -0,0 +1,180 @@ +/* +** mirb_history.c - Command history for mirb editor +** +** See Copyright Notice in mruby.h +*/ + +#include "mirb_history.h" +#include +#include +#include + +/* + * Initialize history + */ +mrb_bool +mirb_history_init(mirb_history *hist, size_t capacity) +{ + memset(hist, 0, sizeof(*hist)); + + if (capacity == 0) capacity = MIRB_HISTORY_SIZE; + + hist->entries = (char**)calloc(capacity, sizeof(char*)); + if (hist->entries == NULL) return FALSE; + + hist->capacity = capacity; + hist->count = 0; + hist->start = 0; + hist->pos = 0; + hist->saved_input = NULL; + hist->browsing = FALSE; + + return TRUE; +} + +/* + * Free history resources + */ +void +mirb_history_free(mirb_history *hist) +{ + if (hist->entries) { + for (size_t i = 0; i < hist->capacity; i++) { + free(hist->entries[i]); + } + free(hist->entries); + hist->entries = NULL; + } + free(hist->saved_input); + hist->saved_input = NULL; + hist->count = 0; + hist->capacity = 0; +} + +/* + * Get actual index in circular buffer + */ +static size_t +actual_index(mirb_history *hist, size_t logical_idx) +{ + return (hist->start + logical_idx) % hist->capacity; +} + +/* + * Add entry to history + */ +void +mirb_history_add(mirb_history *hist, const char *entry) +{ + if (entry == NULL || entry[0] == '\0') return; + + /* Don't add if same as last entry */ + if (hist->count > 0) { + size_t last_idx = actual_index(hist, hist->count - 1); + if (strcmp(hist->entries[last_idx], entry) == 0) { + return; + } + } + + char *copy = strdup(entry); + if (copy == NULL) return; + + if (hist->count < hist->capacity) { + /* Still have room */ + size_t idx = actual_index(hist, hist->count); + hist->entries[idx] = copy; + hist->count++; + } + else { + /* Buffer is full, overwrite oldest */ + size_t idx = hist->start; + free(hist->entries[idx]); + hist->entries[idx] = copy; + hist->start = (hist->start + 1) % hist->capacity; + } + + /* Reset browsing state */ + hist->browsing = FALSE; + hist->pos = hist->count; +} + +/* + * Start browsing history + */ +void +mirb_history_browse_start(mirb_history *hist, const char *current_input) +{ + if (hist->browsing) return; + + free(hist->saved_input); + hist->saved_input = current_input ? strdup(current_input) : NULL; + hist->browsing = TRUE; + hist->pos = hist->count; /* Start past the end (at current input) */ +} + +/* + * Stop browsing history + */ +void +mirb_history_browse_stop(mirb_history *hist) +{ + free(hist->saved_input); + hist->saved_input = NULL; + hist->browsing = FALSE; + hist->pos = hist->count; +} + +/* + * Get previous entry (older) + */ +const char * +mirb_history_prev(mirb_history *hist) +{ + if (hist->count == 0) return NULL; + + if (!hist->browsing) { + /* Should call browse_start first, but handle gracefully */ + hist->browsing = TRUE; + hist->pos = hist->count; + } + + if (hist->pos == 0) { + /* Already at oldest entry */ + return NULL; + } + + hist->pos--; + return hist->entries[actual_index(hist, hist->pos)]; +} + +/* + * Get next entry (newer) + */ +const char * +mirb_history_next(mirb_history *hist) +{ + if (!hist->browsing) return NULL; + + if (hist->pos >= hist->count) { + /* Already at current input */ + return NULL; + } + + hist->pos++; + + if (hist->pos >= hist->count) { + /* Moved past newest entry, return saved input */ + return hist->saved_input ? hist->saved_input : ""; + } + + return hist->entries[actual_index(hist, hist->pos)]; +} + +/* + * Get current entry count + */ +size_t +mirb_history_count(mirb_history *hist) +{ + return hist->count; +} diff --git a/mrbgems/mruby-bin-mirb/tools/mirb/mirb_history.h b/mrbgems/mruby-bin-mirb/tools/mirb/mirb_history.h new file mode 100644 index 000000000..6ac074c19 --- /dev/null +++ b/mrbgems/mruby-bin-mirb/tools/mirb/mirb_history.h @@ -0,0 +1,76 @@ +/* +** mirb_history.h - Command history for mirb editor +** +** See Copyright Notice in mruby.h +*/ + +#ifndef MIRB_HISTORY_H +#define MIRB_HISTORY_H + +#include +#include + +/* Default history size */ +#define MIRB_HISTORY_SIZE 100 + +/* + * History entry + */ +typedef struct mirb_history { + char **entries; /* array of history entries */ + size_t capacity; /* max number of entries */ + size_t count; /* current number of entries */ + size_t start; /* index of oldest entry (circular buffer) */ + size_t pos; /* current browsing position */ + char *saved_input; /* saved current input when browsing */ + mrb_bool browsing; /* TRUE if currently browsing history */ +} mirb_history; + +/* + * Initialize history + * Returns TRUE on success + */ +mrb_bool mirb_history_init(mirb_history *hist, size_t capacity); + +/* + * Free history resources + */ +void mirb_history_free(mirb_history *hist); + +/* + * Add entry to history + * Empty strings are not added + * Duplicate of last entry is not added + */ +void mirb_history_add(mirb_history *hist, const char *entry); + +/* + * Start browsing history + * Saves the current input for later restoration + */ +void mirb_history_browse_start(mirb_history *hist, const char *current_input); + +/* + * Stop browsing history + */ +void mirb_history_browse_stop(mirb_history *hist); + +/* + * Get previous entry (older) + * Returns NULL if at oldest entry or history is empty + */ +const char *mirb_history_prev(mirb_history *hist); + +/* + * Get next entry (newer) + * Returns saved input if moving past newest entry + * Returns NULL if not browsing + */ +const char *mirb_history_next(mirb_history *hist); + +/* + * Get current entry count + */ +size_t mirb_history_count(mirb_history *hist); + +#endif /* MIRB_HISTORY_H */