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 <noreply@anthropic.com>
This commit is contained in:
Yukihiro "Matz" Matsumoto
2025-12-12 23:07:48 +09:00
parent f44e7409ba
commit 5f85c1beae
5 changed files with 318 additions and 2 deletions
+4
View File
@@ -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';
@@ -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);
}
@@ -10,6 +10,7 @@
#include <mruby.h>
#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 */
@@ -0,0 +1,180 @@
/*
** mirb_history.c - Command history for mirb editor
**
** See Copyright Notice in mruby.h
*/
#include "mirb_history.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/*
* 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;
}
@@ -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 <mruby.h>
#include <stddef.h>
/* 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 */