diff --git a/stand/common/interp.c b/stand/common/interp.c --- a/stand/common/interp.c +++ b/stand/common/interp.c @@ -37,6 +37,56 @@ #include #include "bootstrap.h" +#ifdef LOADER_EDITING_SUPPORT +#include "prompt_bindings.h" +#include "prompt_editing.h" + +struct prompt_buffer prompt_prompt = { 0 }; + +/* + * Hardcoded key bindings to avoid adding a new file, can be overridden + * with the "keybind" simp command, or the "keybind.register" Lua function. + * + * Since some consoles might not encode modifiers and special keys correctly + * some actions are assigned to multiple keys, just in case a console + * can only do modifiers OR special keys. + */ + +struct { + char *stroke; + char *action; +} default_keybinds[] = { + {"BS", "delete-backward-char"}, + {"", "delete-forward-char"}, + {"", "backward-char"}, + {"", "forward-char"}, + + {"M-", "backward-word"}, + {"M-", "forward-word"}, + + {"M-b", "backward-word"}, + {"M-f", "forward-word"}, + + {"", "previous-history-element"}, + {"", "next-history-element"}, + + {"TAB", "smart-complete"}, + + {"", "move-beginning-of-line"}, + {"", "move-end-of-line"}, + + {"C-a", "move-beginning-of-line"}, + {"C-e", "move-end-of-line"}, + + {"C-y", "yank"}, + {"C-k", "kill-line"}, + {"M-d", "kill-word"}, + {"M-", "backward-kill-word"}, + + {NULL, NULL} +}; +#endif // LOADER_EDITING_SUPPORT + #define MAXARGS 20 /* maximum number of arguments allowed */ const char * volatile interp_identifier; @@ -47,7 +97,9 @@ void interact(void) { +#ifndef LOADER_EDITING_SUPPORT static char input[256]; /* big enough? */ +#endif // LOADER_EDITING_SUPPORT TSENTER(); @@ -68,6 +120,15 @@ */ autoboot_maybe(); +#ifdef LOADER_EDITING_SUPPORT + /* + * Setup sane defaults for key bindings (anything unrecognized is ignored) + */ + for (int i = 0; default_keybinds[i].stroke != NULL; i++) { + prompt_add_stroke_action_binding(default_keybinds[i].stroke, default_keybinds[i].action); + } +#endif // LOADER_EDITING_SUPPORT + /* * Not autobooting, go manual */ @@ -77,12 +138,38 @@ if (getenv("interpret") == NULL) setenv("interpret", "OK", 1); +#ifdef LOADER_EDITING_SUPPORT + prompt_init(); + + for (;;) { + prompt_reset(); + interp_emit_prompt(); + + for (;;) { + char n = prompt_on_input(getchar()); + + if (n == '\r') { + break; + } + else if (n != 0) { + prompt_rawinput(n); + } + } + + char *line = prompt_getline(); + + printf("\r\n"); + + interp_run(line); + } +#else for (;;) { input[0] = '\0'; interp_emit_prompt(); ngets(input, sizeof(input)); interp_run(input); } +#endif // LOADER_EDITING_SUPPORT } /* diff --git a/stand/common/interp_lua.c b/stand/common/interp_lua.c --- a/stand/common/interp_lua.c +++ b/stand/common/interp_lua.c @@ -44,6 +44,9 @@ #include #include +#include "prompt_bindings.h" +#include "prompt_editing.h" + struct interp_lua_softc { lua_State *luap; }; @@ -75,6 +78,317 @@ return realloc(ptr, nsize); } +#ifdef LOADER_EDITING_SUPPORT + +/* + * Appended onto each keybind created from Lua, callback_ref + * is just the registry index of a callback + */ +struct lua_keybind { + int callback_ref; +}; + +static void +lua_keybind_handler(void *data) +{ + struct lua_keybind *bind = data; + lua_State *L = lua_softc.luap; + + lua_rawgeti(L, LUA_REGISTRYINDEX, bind->callback_ref); + + lua_pcall(L, 0, 0, 0); +} + +static void +delete_bind(lua_State *L, struct prompt_keybind *bind) +{ + /* Delete a binding, unref-ing along the way if Lua owns it */ + if (bind->action == lua_keybind_handler) { + void *data = sizeof(struct prompt_keybind) + (void*)bind; + struct lua_keybind *luabind = data; + + luaL_unref(L, LUA_REGISTRYINDEX, luabind->callback_ref); + } + + prompt_remove_binding(bind); +} + +static int +lua_keybind_register(lua_State *L) +{ + int argc = lua_gettop(L); + + if (argc != 2) { + lua_pushnil(L); + return 1; + } + + if (!lua_isstring(L, -2)) { + lua_pushnil(L); + return 1; + } + + const char *stroke = lua_tostring(L, -2); + struct prompt_input input = prompt_parse_stroke(stroke); + + if (input.key == 0) { + lua_pushnil(L); + return 1; + } + + struct prompt_keybind *existing = prompt_find_binding(input.mods, input.key); + + if (existing != NULL) { + /* Delete any prexisting binding to prevent a leak */ + delete_bind(L, existing); + } + + struct prompt_keybind *bind = NULL; + + if (lua_islightuserdata(L, -1)) { + /* + * keybind.register(..., lightuserdata) + * means we've been called with a predefined action + * from the keybind.actions table. + * Instead of wrapping a Lua function, just call to the + * predefined action directly. + */ + struct prompt_predefined_action *predef = lua_touserdata(L, -1); + + bind = prompt_add_binding(input.mods, input.key, predef->action); + } else { + struct prompt_keybind *bind = prompt_add_binding_raw(4, input.mods, input.key, lua_keybind_handler); + struct lua_keybind *luabind = sizeof(struct prompt_keybind) + (void*)bind; + + luabind->callback_ref = luaL_ref(L, LUA_REGISTRYINDEX); + } + + /* + * Return the binding as lightuserdata, so it can be passed to + * keybind.delete to be removed + */ + lua_pushlightuserdata(L, bind); + return 1; +} + +static int +lua_keybind_delete(lua_State *L) +{ + int argc = lua_gettop(L); + + if (argc != 1) { + lua_pushnil(L); + return 1; + } + + if (!lua_islightuserdata(L, -1)) { + lua_pushnil(L); + return 1; + } + + struct prompt_keybind *bind = (struct prompt_keybind*)lua_touserdata(L, -1); + + delete_bind(L, bind); + + lua_pushboolean(L, 1); + return 1; +} + +static int +lua_keybind_find(lua_State *L) +{ + /* Find a binding by name so it can be deleted */ + + int argc = lua_gettop(L); + + if (argc != 1) { + lua_pushnil(L); + return 1; + } + + if (!lua_isstring(L, -1)) { + lua_pushnil(L); + return 1; + } + + const char *stroke = lua_tostring(L, -1); + struct prompt_input input = prompt_parse_stroke(stroke); + + if (input.key == 0) { + lua_pushnil(L); + return 1; + } + + struct prompt_keybind *bind = prompt_find_binding(input.mods, input.key); + + if (bind == NULL) { + lua_pushnil(L); + return 1; + } + + lua_pushlightuserdata(L, bind); + return 1; +} + +static int +lua_keybind_list(lua_State *L) +{ + /* Get a list of all bound keys */ + int argc = lua_gettop(L); + + if (argc != 0) { + lua_pushnil(L); + return 1; + } + + lua_newtable(L); + + struct prompt_keybind *bind = prompt_first_binding(); + int i = 1; + + while (bind != NULL) { + /* + * Longest keystroke is M-C-S- (14 chars), add some for safety + */ + char buf[20] = { 0 }; + prompt_stroke_to_string(buf, sizeof(buf), bind->target); + + lua_pushstring(L, buf); + lua_rawseti(L, -2, i++); + + bind = prompt_next_binding(bind); + } + + return 1; +} + +static const struct luaL_Reg keybindlib[] = { + {"register", lua_keybind_register}, + {"delete", lua_keybind_delete}, + {"find", lua_keybind_find}, + {"list", lua_keybind_list}, + {NULL, NULL} +}; + +int +luaopen_keybind(lua_State *L) +{ + luaL_newlib(L, keybindlib); + + lua_newtable(L); + + /* Build keybind.actions out of the list of predefined actions */ + struct prompt_predefined_action **ppa; + SET_FOREACH(ppa, Xpredef_action_set) { + struct prompt_predefined_action *a = *ppa; + + lua_pushlightuserdata(L, a); + lua_setfield(L, -2, a->name); + } + + lua_setfield(L, -2, "actions"); + + return 1; +} + +static int +lua_history_add(lua_State *L) +{ + /* Artificially add a line the the prompt history */ + int argc = lua_gettop(L); + + if (argc != 1) { + lua_pushnil(L); + return 1; + } + + if (!lua_isstring(L, -1)) { + lua_pushnil(L); + return 1; + } + + const char *entry = lua_tostring(L, -1); + + prompt_history_add(entry, strlen(entry)); + + lua_pushboolean(L, 1); + return 1; +} + +static int +lua_history_remove(lua_State *L) +{ + /* Remove a line from the history, identified by index */ + int argc = lua_gettop(L); + + if (argc != 1) { + lua_pushnil(L); + return 1; + } + + int index = (int)lua_tonumber(L, -1); + + struct prompt_history_entry *entry = prompt_history_first(); + int i = 0; + + while (entry != NULL) { + if (i++ == index) { + prompt_history_remove(entry); + + lua_pushboolean(L, 1); + return 1; + } + + entry = prompt_history_next(entry); + } + + lua_pushnil(L); + return 1; +} + +static int +lua_history_list(lua_State *L) +{ + /* Get a list of all lines in the history */ + int argc = lua_gettop(L); + + if (argc != 0) { + lua_pushnil(L); + return 1; + } + + lua_newtable(L); + + struct prompt_history_entry *entry = prompt_history_first(); + int i = 1; + + while (entry != NULL) { + lua_pushstring(L, entry->line); + lua_rawseti(L, -2, i++); + + entry = prompt_history_next(entry); + } + + return 1; +} + +static const struct luaL_Reg historylib[] = { + {"add", lua_history_add}, + {"remove", lua_history_remove}, + {"list", lua_history_list}, + {NULL, NULL} +}; + +int +luaopen_history(lua_State *L) +{ + luaL_newlib(L, historylib); + + return 1; +} + +#endif // LOADER_EDITING_SUPPORT + /* * The libraries commented out below either lack the proper * support from libsa, or they are unlikely to be useful @@ -96,6 +410,10 @@ {"lfs", luaopen_lfs}, {"loader", luaopen_loader}, {"pager", luaopen_pager}, +#ifdef LOADER_EDITING_SUPPORT + {"keybind", luaopen_keybind}, + {"history", luaopen_history}, +#endif // LOADER_EDITING_SUPPORT {NULL, NULL} }; @@ -204,3 +522,98 @@ return (luaL_dofile(softc->luap, filename)); } + +#if LOADER_EDITING_SUPPORT + +/* + * Keyword/global name completion. + * The actual enumeration logic here is a bit odd, since we need to + * walk through every (string) key in _G, and then artificially append + * language keywords to that list. + */ +static void * +token_first() +{ + lua_State *L = lua_softc.luap; + + lua_pushglobaltable(L); + lua_pushnil(L); + return (void*)(intptr_t)!lua_next(L, -2); +} + +/* + * From the Lua 5.2 Reference Manual, short keywords commented out + * since shortcuts like "d" for "do" aren't likely to be + * useful and will just clutter the list. + */ +static const char* keywords[] = { + "and", "break", /*"do",*/ "else", "elseif", "end", + "false", "for", "function", /*"if",*/ /*"in",*/ "local", + "nil", "not", /*"or",*/ "repeat", "return", "then", + "true", "until", "while" +}; + +static void * +token_next(void *rawlast) +{ + intptr_t idx = (intptr_t)rawlast; + + if (idx + 1 > (sizeof(keywords) / sizeof(keywords[0]))) { + return (void*)-1; + } + else if (idx > 0) { + return (void*)++idx; + } else { + lua_State *L = lua_softc.luap; + + return (void*)(intptr_t)!lua_next(L, -2); + } +} +static void +token_tostring(void *rawlast, char *out, int len) +{ + intptr_t idx = (intptr_t)rawlast; + + if (idx > 0) { + snprintf(out, len, "%s", keywords[idx - 1]); + } + else { + lua_State *L = lua_softc.luap; + + int isfunction = lua_isfunction(L, -1); + lua_pop(L, 1); + + if (lua_isstring(L, -1)) { + lua_pushvalue(L, -1); + const char *value = lua_tolstring(L, -1, NULL); + lua_pop(L, 1); + + snprintf(out, len, isfunction ? "%s(" : "%s", value); + } + } +} + +static void +lua_completer(char *command, char *argv) +{ + lua_State *L = lua_softc.luap; + + /* + * Since the "last" key is always left on the stack, we would need + * to pop it "after" token_first, which isn't possible with how + * the compleition works. Instead, we just set up a pseudo stack + * frame and pop any leftover values. + */ + int top = lua_gettop(L); + prompt_generic_complete(argv, token_first, token_next, (void*)-1, token_tostring); + lua_settop(L, top); +} + +/* + * Fallthrough completion, matches any undefined "command". + * Aka, nearly any Lua code, which allows us to jump in and try + * to complete Lua specifics without matching any "proper" commands. + */ +COMPLETION_SET(_, 0, lua_completer); + +#endif // LOADER_EDITING_SUPPORT diff --git a/stand/common/prompt_bindings.h b/stand/common/prompt_bindings.h new file mode 100644 --- /dev/null +++ b/stand/common/prompt_bindings.h @@ -0,0 +1,75 @@ +/*- + * Copyright (c) 2022 Connor Bailey + * + * SPDX-License-Identifier: BSD-2-clause + */ + +#include + +struct prompt_input { + int key; + char mods; +}; + +typedef void(*prompt_action)(void*); + +struct prompt_keybind { + struct prompt_input target; + prompt_action action; + + STAILQ_ENTRY(prompt_keybind) next; +}; + +struct prompt_predefined_action { + char *name; + prompt_action action; + + STAILQ_ENTRY(prompt_predefined_action) next; +}; + +#define PROMPT_MOD_SHIFT 1 +#define PROMPT_MOD_ALT 2 +#define PROMPT_MOD_CTRL 4 + +#define PROMPT_ANSI_TO_KEY 0x100 +#define PROMPT_KEY_UP (PROMPT_ANSI_TO_KEY + 'A') +#define PROMPT_KEY_DOWN (PROMPT_ANSI_TO_KEY + 'B') +#define PROMPT_KEY_RIGHT (PROMPT_ANSI_TO_KEY + 'C') +#define PROMPT_KEY_LEFT (PROMPT_ANSI_TO_KEY + 'D') +#define PROMPT_KEY_END (PROMPT_ANSI_TO_KEY + 'F') +#define PROMPT_KEY_HOME (PROMPT_ANSI_TO_KEY + 'H') + +#define PROMPT_KEY_INSERT (PROMPT_ANSI_TO_KEY + 'E') +#define PROMPT_KEY_DELETE (PROMPT_ANSI_TO_KEY + 'I') + +#define PROMPT_KEY_PGUP (PROMPT_ANSI_TO_KEY + 'J') +#define PROMPT_KEY_PGDN (PROMPT_ANSI_TO_KEY + 'K') + +struct prompt_input prompt_parse_input(char); +char prompt_input_to_char(struct prompt_input input); + +/* + * Takes a stream of input escapes, parses them and executes any bindings or + * converts the input into a character and returns it. + */ +char prompt_on_input(char); + +struct prompt_keybind *prompt_find_binding(char, int); +struct prompt_keybind *prompt_add_binding_raw(int, char, int, prompt_action); +struct prompt_keybind *prompt_add_binding(char, int, prompt_action); +void prompt_remove_binding(struct prompt_keybind *); + +void prompt_stroke_to_string(char *, size_t, struct prompt_input); +void prompt_print_stroke(struct prompt_input); +struct prompt_input prompt_parse_stroke(const char *); + +struct prompt_keybind *prompt_add_stroke_action_binding(char *, char *); + +struct prompt_keybind *prompt_first_binding(); +struct prompt_keybind *prompt_next_binding(struct prompt_keybind *); + +#define PREDEF_ACTION_SET(name, func) \ + static struct prompt_predefined_action _predef_ ## func = { name, func }; \ + DATA_SET(Xpredef_action_set, _predef_ ## func) + +SET_DECLARE(Xpredef_action_set, struct prompt_predefined_action); diff --git a/stand/common/prompt_bindings.c b/stand/common/prompt_bindings.c new file mode 100644 --- /dev/null +++ b/stand/common/prompt_bindings.c @@ -0,0 +1,585 @@ +/*- + * Copyright (c) 2022 Connor Bailey + * + * SPDX-License-Identifier: BSD-2-clause + */ + +#include +#include "bootstrap.h" + +#include "prompt_bindings.h" + +#define BS '\x8' +#define TAB '\x9' +#define CR '\xd' +#define ESC '\x1b' +#define DEL '\x7f' + +/* + * In the input escape parser, we need to handle: + * "char" + * "ESC char" + * "ESC [ code ~" + * "ESC [ char ; mods" + * "ESC [ mods ; code ~" + * each of which is assigned a state, with the proper transitions between each. + * + * Special keys are just (PROMPT_ANSI_TO_KEY + ANSI_CODE) with unused ANSI codes + * picked for VT codes. + */ + +enum { + ESC_NORMAL, + ESC_ESC, + ESC_BRACKET, + ESC_BRACKET_EITHER, + ESC_CODE_MODS, + ESC_CODE_MODS_END +} prompt_esc_state; + +static char prompt_mods_or_code; +static char prompt_char_or_mods; + +static void +unshift(struct prompt_input *result, char in) +{ + if (isupper(in)) { + result->mods |= PROMPT_MOD_SHIFT; + in = tolower(in); + } + + result->key = in; +} + +/* Map [1-9]~ VT codes back into keycodes */ +static int prompt_vt[] = { + PROMPT_KEY_HOME, PROMPT_KEY_INSERT, PROMPT_KEY_DELETE, PROMPT_KEY_END, + PROMPT_KEY_PGUP, PROMPT_KEY_PGDN, PROMPT_KEY_HOME, PROMPT_KEY_END +}; + +struct prompt_input +prompt_parse_input(char next) +{ + struct prompt_input result = { 0 }; + + switch (prompt_esc_state) { + case ESC_NORMAL: + if (next == ESC) { + prompt_esc_state = ESC_ESC; + } + else if (iscntrl(next) && next != BS && next != TAB && next != CR) { + /* Control characters turn into ctrl+key */ + result.key = next - 1 + 'a'; + result.mods |= PROMPT_MOD_CTRL; + } else { + unshift(&result, next); + } + break; + case ESC_ESC: + if (next == '[') { + prompt_esc_state = ESC_BRACKET; + } else { + /* "ESC char" turns back into alt+char */ + result.mods |= PROMPT_MOD_ALT; + unshift(&result, next); + prompt_esc_state = ESC_NORMAL; + } + break; + case ESC_BRACKET: + if ('A' <= next && next <= 'Z') { + /* + * Plain ANSI escapes without modifiers terminate after the first + * non-numeric character + */ + result.key = PROMPT_ANSI_TO_KEY + next; + prompt_esc_state = ESC_NORMAL; + } + else if ('1' <= next && next <= '9') { + prompt_esc_state = ESC_BRACKET_EITHER; + prompt_mods_or_code = next; + } else { + /* "ESC [ char" into alt+char, mainly for "ESC [ [" to work */ + result.mods |= PROMPT_MOD_ALT; + unshift(&result, next); + prompt_esc_state = ESC_NORMAL; + } + break; + case ESC_BRACKET_EITHER: + if (next == ';') { + prompt_esc_state = ESC_CODE_MODS; + } + else if (next == '~') { + /* VT escape terminated by ~, mods_or_code is a code */ + prompt_mods_or_code -= '1'; + + if (prompt_mods_or_code < 8) { + result.key = prompt_vt[(int)prompt_mods_or_code]; + } + + prompt_esc_state = ESC_NORMAL; + } else { + /* ESC [ mods ; char */ + result.mods = prompt_mods_or_code - '0' - 1; + unshift(&result, next); + prompt_esc_state = ESC_NORMAL; + } + break; + case ESC_CODE_MODS: + prompt_char_or_mods = next; + prompt_esc_state = ESC_CODE_MODS_END; + break; + case ESC_CODE_MODS_END: + result.mods = prompt_char_or_mods - '0' - 1; + + if (next == '~') { + /* VT escape terminated by ~, mods_or_code is a code */ + prompt_mods_or_code -= '1'; + + if (prompt_mods_or_code < 8) { + result.key = prompt_vt[(int)prompt_mods_or_code]; + } + } + else if (prompt_mods_or_code == '1' && ('A' <= next && next <= 'Z')) { + /* ANSI escape in the "ESC [ mods ; char" format */ + result.key = PROMPT_ANSI_TO_KEY + next; + } + + prompt_esc_state = ESC_NORMAL; + + break; + } + + return result; +} + +char +prompt_input_to_char(struct prompt_input input) +{ + if (input.key > 0) { + if (input.mods & PROMPT_MOD_SHIFT) { + if (islower(input.key)) { + return toupper(input.key); + } + } + else if (input.mods) { + return 0; + } + + return input.key; + } + + return 0; +} + +STAILQ_HEAD(prompt_binds, prompt_keybind) prompt_binds_head = + STAILQ_HEAD_INITIALIZER(prompt_binds_head); + +struct prompt_keybind * +prompt_find_binding(char mods, int key) +{ + struct prompt_keybind *p; + + STAILQ_FOREACH(p, &prompt_binds_head, next) { + if (p->target.mods == mods && p->target.key == key) { + return p; + } + } + + return NULL; +} + +struct prompt_keybind * +prompt_add_binding_raw(int extraspace, char mods, int key, prompt_action action) +{ + /* + * Allocate extra space on the end of the result for the caller to use how + * they like, it will be passed to their callback. + */ + struct prompt_keybind* result = prompt_find_binding(mods, key); + int new = result == NULL; + + if (new) { + result = malloc(sizeof(struct prompt_keybind) + extraspace); + } + + result->target.mods = mods; + result->target.key = key; + result->action = action; + + if (new) { + STAILQ_INSERT_TAIL(&prompt_binds_head, result, next); + } + + return result; +} + +struct prompt_keybind * +prompt_add_binding(char mods, int key, prompt_action action) +{ + return prompt_add_binding_raw(0, mods, key, action); +} + +void +prompt_remove_binding(struct prompt_keybind *bind) +{ + STAILQ_REMOVE(&prompt_binds_head, bind, prompt_keybind, next); + + free(bind); +} + +char +prompt_on_input(char in) +{ + struct prompt_input input = prompt_parse_input(in); + + struct prompt_keybind *binding = prompt_find_binding(input.mods, input.key); + + if (binding != NULL) { + /* Pass any caller data stored past the end of the binding */ + binding->action(sizeof(struct prompt_keybind) + (void*)binding); + return 0; + } + + return prompt_input_to_char(input); +} + +struct keyname_map { + char *name; + int key; +}; + +/* + * Control characters + */ +static struct keyname_map emacs_shortname_to_key[] = { + {"BS", BS}, + {"TAB", TAB}, + {"RET", CR}, + {"ESC", ESC}, + {"SPC", ' '}, + {"DEL", DEL}, + {0, 0} +}; + +/* + * Special keys + */ +static struct keyname_map emacs_longname_to_key[] = { + {"", PROMPT_KEY_LEFT}, + {"", PROMPT_KEY_UP}, + {"", PROMPT_KEY_RIGHT}, + {"", PROMPT_KEY_DOWN}, + {"", PROMPT_KEY_END}, + {"", PROMPT_KEY_HOME}, + {"", PROMPT_KEY_INSERT}, + {"", PROMPT_KEY_DELETE}, + {"", PROMPT_KEY_PGUP}, + {"", PROMPT_KEY_PGDN}, + {0, 0} +}; + +static int +lookup_key_from_name(struct keyname_map *map, const char *name) +{ + int i = 0; + + while (map[i].key != 0) { + if (strcmp(map[i].name, name) == 0) { + return map[i].key; + } + + i++; + } + + return 0; +} +static char * +lookup_name_from_key(struct keyname_map *map, const int key) +{ + int i = 0; + + while (map[i].key != 0) { + if (map[i].key == key) { + return map[i].name; + } + + i++; + } + + return 0; +} + +void +prompt_stroke_to_string(char *buf, size_t len, struct prompt_input stroke) +{ + int off = 0; + + if (stroke.mods) { + if (stroke.mods & PROMPT_MOD_ALT) { + off += snprintf(&buf[off], len, "M-"); + } + + if (stroke.mods & PROMPT_MOD_CTRL) { + off += snprintf(&buf[off], len, "C-"); + } + + if (stroke.mods & PROMPT_MOD_SHIFT) { + off += snprintf(&buf[off], len, "S-"); + } + } + + if (iscntrl(stroke.key)) { + char *name = lookup_name_from_key(emacs_shortname_to_key, stroke.key); + + if (name) { + off += snprintf(&buf[off], len, "%s", name); + } else { + off += snprintf(&buf[off], len, "\\x%x", stroke.key); + } + } + else if (isgraph(stroke.key) && stroke.key != ' ') { + /* Printable characters (no isprint, easy enough to fake) */ + off += snprintf(&buf[off], len, "%c", stroke.key); + } else { + /* Special characters */ + char *name = lookup_name_from_key(emacs_longname_to_key, stroke.key); + + if (name) { + off += snprintf(&buf[off], len, "%s", name); + } else { + off += snprintf(&buf[off], len, "\\x%x", stroke.key); + } + } +} + +void +prompt_print_stroke(struct prompt_input stroke) +{ + char buf[20] = { 0 }; + + prompt_stroke_to_string(buf, sizeof(buf), stroke); + + printf("%s", buf); +} + +struct prompt_input +prompt_parse_stroke(const char *stroke) +{ + struct prompt_input result = { 0 }; + + int len = strlen(stroke); + const char *p = stroke; + + /* Any number of "X-" modifiers, back to back */ + while (len >= 3 && p[1] == '-') { + switch (*p) { + case 'C': + result.mods |= PROMPT_MOD_CTRL; + break; + case 'M': + result.mods |= PROMPT_MOD_ALT; + break; + case 'S': + result.mods |= PROMPT_MOD_SHIFT; + break; + } + + p += 2; + len -= 2; + } + + if (len != 1) { + /* More left to parse than a single character like "M-x" */ + if (p[0] == '<') { + /* "M-" */ + result.key = lookup_key_from_name(emacs_longname_to_key, p); + } else { + /* "M-CTRL" */ + result.key = lookup_key_from_name(emacs_shortname_to_key, p); + } + } else { + /* A single character, might include shift as a modifier if it is uppercase */ + char ascii = *p; + + if (isupper(ascii)) { + result.mods |= PROMPT_MOD_SHIFT; + ascii = tolower(ascii); + } + + result.key = ascii; + } + + return result; +} + +struct prompt_keybind * +prompt_first_binding() +{ + return STAILQ_FIRST(&prompt_binds_head); +} +struct prompt_keybind * +prompt_next_binding(struct prompt_keybind* bind) +{ + return STAILQ_NEXT(bind, next); +} + +struct prompt_keybind * +prompt_add_stroke_binding(char *stroke, prompt_action action) +{ + struct prompt_input input = prompt_parse_stroke(stroke); + + if (input.mods == 0 && input.key == 0) { + return NULL; + } + + return prompt_add_binding(input.mods, input.key, action); +} + +/* + * Predefined actions just map names to functions, mainly for simp since it isn't + * possible to define a callback. + * Still useful for Lua though, since otherwise Lua would need to manually + * implement every editing option instead of just using the predefined ones. + * + * Lua translates Xpredef_action_set into the keybind.actions table at runtime + * and then each entry in keybind.actions can be passed to keybind.register to + * accomplish the same as the simp "keybind" command. + */ +static struct prompt_predefined_action * +find_predef_by_name(char *name) +{ + struct prompt_predefined_action **ppa; + + SET_FOREACH(ppa, Xpredef_action_set) { + struct prompt_predefined_action *a = *ppa; + + if (strcmp(a->name, name) == 0) { + return a; + } + } + + return NULL; +} + +static struct prompt_predefined_action * +find_predef_by_action(prompt_action action) +{ + struct prompt_predefined_action **ppa; + + SET_FOREACH(ppa, Xpredef_action_set) { + struct prompt_predefined_action *a = *ppa; + + if (a->action == action) { + return a; + } + } + + return NULL; +} + +struct prompt_keybind * +prompt_add_stroke_action_binding(char *stroke, char *action_name) +{ + struct prompt_predefined_action *predef = find_predef_by_name(action_name); + + if (predef == NULL) { + return NULL; + } + + return prompt_add_stroke_binding(stroke, predef->action); +} + +COMMAND_SET(keybind, "keybind", "bind a key to an action", command_keybind); + +static int +command_keybind(int argc, char *argv[]) +{ + if (argc != 3) { + command_errmsg = "wrong number of arguments"; + return CMD_ERROR; + } + + struct prompt_keybind *bind = prompt_add_stroke_action_binding(argv[1], argv[2]); + + if (bind != NULL) { + return CMD_OK; + } else { + snprintf(command_errbuf, sizeof(command_errbuf), "could not bind '%s' to '%s'", argv[1], argv[2]); + + return CMD_ERROR; + } +} + +COMMAND_SET(keybinds, "keybinds", "list bound keys", command_keybinds); + +static int +command_keybinds(int argc, char *argv[]) +{ + struct prompt_keybind *p; + + STAILQ_FOREACH(p, &prompt_binds_head, next) { + prompt_print_stroke(p->target); + + struct prompt_predefined_action *predef = find_predef_by_action(p->action); + + if (predef != NULL) { + printf(" %s\n", predef->name); + } else { + /* + * Wouldn't be very kind of us to dig into Lua's data stored after the + * binding, so we have to default to a generic name for Lua actions. + */ + printf(" \n", p->action); + } + } + + return CMD_OK; +} + +COMMAND_SET(keyunbind, "keyunbind", "unbind a previously bound key", command_keyunbind); + +static int +command_keyunbind(int argc, char *argv[]) { + if (argc != 2) { + command_errmsg = "wrong number of arguments"; + return CMD_ERROR; + } + + struct prompt_input stroke = prompt_parse_stroke(argv[1]); + + if (stroke.mods == 0 && stroke.key == 0) { + command_errmsg = "could not parse key stroke"; + return CMD_ERROR; + } + + struct prompt_keybind *bind = prompt_find_binding(stroke.mods, stroke.key); + + if (bind == NULL) { + command_errmsg = "could not find any binding for key stroke"; + return CMD_ERROR; + } + + prompt_remove_binding(bind); + + return CMD_OK; +} + +COMMAND_SET(showkey, "showkey", "shows how a single keystroke is parsed", command_showkey); + +static int +command_showkey(int argc, char *argv[]) { + if (argc != 1) { + command_errmsg = "wrong number of arguments"; + return CMD_ERROR; + } + + struct prompt_input in = { 0 }; + + while (in.mods == 0 && in.key == 0) + in = prompt_parse_input(getchar()); + + prompt_print_stroke(in); + printf("\n"); + + return CMD_OK; +} diff --git a/stand/common/prompt_editing.h b/stand/common/prompt_editing.h new file mode 100644 --- /dev/null +++ b/stand/common/prompt_editing.h @@ -0,0 +1,102 @@ +/*- + * Copyright (c) 2022 Connor Bailey + * + * SPDX-License-Identifier: BSD-2-clause + */ + +#define PROMPT_LINE_LENGTH 256 + +/* + * The prompt is just a gap buffer, with a single kill buffer, and a linked list + * of history entries. + */ + +struct prompt_history_entry { + char line[PROMPT_LINE_LENGTH]; + TAILQ_ENTRY(prompt_history_entry) entry; +}; + +struct prompt_buffer { + char line[PROMPT_LINE_LENGTH + 1]; + int cursor; + int gap; + + char kill[PROMPT_LINE_LENGTH + 1]; + int killcursor; + + TAILQ_HEAD(prompt_history_head, prompt_history_entry) history_head; + struct prompt_history_entry *history_cursor; +}; + +extern struct prompt_buffer prompt_prompt; + +void prompt_init(); +void prompt_reset(); +void prompt_rawinput(char); +char *prompt_getline(); + +/* + * Editing actions + */ + +void prompt_forward_char(void *); +void prompt_backward_char(void *); + +void prompt_move_end_of_line(void *); +void prompt_move_beginning_of_line(void *); + +void prompt_forward_word(void *); +void prompt_backward_word(void *); + +void prompt_delete_forward_char(void *); +void prompt_delete_backward_char(void *); + +void prompt_yank(void *); + +void prompt_forward_kill_word(void *); +void prompt_backward_kill_word(void *); +void prompt_kill_line(void *); + +void prompt_next_history_element(void *); +void prompt_previous_history_element(void *); + +/* + * History manipulation + */ + +void prompt_history_add(const char *, int); +void prompt_history_remove(struct prompt_history_entry *); +struct prompt_history_entry *prompt_history_first(); +struct prompt_history_entry *prompt_history_next(struct prompt_history_entry *); + +/* + * Completion + */ + +void prompt_complete_command(void*); +void prompt_complete_smart(void*); + +typedef void*(generic_completer_first)(); +typedef void*(generic_completer_next_item)(void *); +typedef void(generic_completer_item_to_string)(void *, char *, int); + +void prompt_generic_complete(char *, generic_completer_first, generic_completer_next_item, void *, generic_completer_item_to_string); + +typedef void(*prompt_completer)(char *, char *); + +void keyunbind_completer(char *, char *); +void environ_completer(char *, char *); +void predefined_action_completer(char *, char *); +void path_completer(char *, char *); + +typedef struct { + const char *command; + int argn; + prompt_completer completer; +} prompt_completion_entry; + +#define COMPLETION_SET(command, argn, func) \ + static prompt_completion_entry _completer_ ## command ## argn = { #command, argn, func }; \ + DATA_SET(Xcompleter_set, _completer_ ## command ## argn) + +SET_DECLARE(Xcompleter_set, prompt_completion_entry); diff --git a/stand/common/prompt_editing.c b/stand/common/prompt_editing.c new file mode 100644 --- /dev/null +++ b/stand/common/prompt_editing.c @@ -0,0 +1,874 @@ +/*- + * Copyright (c) 2022 Connor Bailey + * + * SPDX-License-Identifier: BSD-2-clause + */ + +#include +#include +#include "bootstrap.h" + +#include "prompt_bindings.h" +#include "prompt_editing.h" + +/* + * Helper macros, just to make each action a bit more readable + */ + +#define LINE (prompt_prompt.line) +#define CURSOR (prompt_prompt.cursor) +#define GAP (prompt_prompt.gap) +#define KILL (prompt_prompt.kill) +#define KILLCURSOR (prompt_prompt.killcursor) +#define HISTORY (&prompt_prompt.history_head) +#define HISTORYCURSOR (prompt_prompt.history_cursor) + +static void +prompt_show_aftergap() +{ + /* + * Clear to end of line, reprint "LINE[GAP:END]", move cursor back so it is + * right before the contents of the gap. + */ + int gaplen = PROMPT_LINE_LENGTH - GAP; + char *aftergap = &LINE[GAP]; + + printf("\x1b[0K"); + + if (gaplen) { + printf("%s", aftergap); + printf("\x1b[%dD", gaplen); + } +} + +static void +prompt_reprint() +{ + interp_emit_prompt(); + + LINE[CURSOR] = 0; + printf("%s", LINE); + + prompt_show_aftergap(); +} + +void +prompt_reset() +{ + /* Called to simulate "end of line" in the gap buffer */ + CURSOR = 0; + GAP = PROMPT_LINE_LENGTH; + LINE[GAP] = '\0'; +} + +void +prompt_init() +{ + /* Called once to get the buffer ready to go */ + prompt_reset(); + KILLCURSOR = 0; + KILL[KILLCURSOR] = '\0'; + + TAILQ_INIT(HISTORY); +} + +void +prompt_rawinput(char in) +{ + /* Add a character to the buffer without processing it as input */ + LINE[CURSOR++] = in; + + printf("%c", in); + prompt_show_aftergap(); +} + +char * +prompt_getline() +{ + /* + * To get a whole line, we just need to move GAP to the end of the line + * which will put the entire line left of the gap, with CURSOR being the + * length of the line + * + * We also use this as a chance to add the line to the history, and reset + * the history pointer. This makes the next "history-previous-element" get + * the item which we just added to the history. + */ + prompt_move_end_of_line(NULL); + LINE[CURSOR] = '\0'; + HISTORYCURSOR = NULL; + + if (CURSOR != 0) { + prompt_history_add(LINE, CURSOR); + } + + return LINE; +} + +void +prompt_forward_char(void *data) +{ + if (GAP != PROMPT_LINE_LENGTH) { + LINE[CURSOR++] = LINE[GAP++]; + + printf("\x1b[1C"); + } +} + +PREDEF_ACTION_SET("forward-char", prompt_forward_char); + +void +prompt_backward_char(void *data) +{ + if (CURSOR != 0) { + LINE[--GAP] = LINE[--CURSOR]; + + printf("\x1b[1D"); + } +} + +PREDEF_ACTION_SET("backward-char", prompt_backward_char); + +void +prompt_delete_backward_char(void *data) +{ + if (CURSOR != 0) { + LINE[--CURSOR] = '\0'; + + putchar('\b'); + prompt_show_aftergap(); + } +} + +PREDEF_ACTION_SET("delete-backward-char", prompt_delete_backward_char); + +void +prompt_delete_forward_char(void *data) +{ + if (GAP != PROMPT_LINE_LENGTH) { + LINE[GAP++] = '\0'; + + prompt_show_aftergap(); + } +} + +PREDEF_ACTION_SET("delete-forward-char", prompt_delete_forward_char); + +void +prompt_move_end_of_line(void *data) +{ + int gapsize = PROMPT_LINE_LENGTH - GAP; + + if (gapsize != 0) { + printf("\x1b[%iC", gapsize); + + for (int i = 0; i < gapsize; i++) { + LINE[CURSOR++] = LINE[GAP++]; + } + } +} + +PREDEF_ACTION_SET("move-end-of-line", prompt_move_end_of_line); + +void +prompt_move_beginning_of_line(void *data) +{ + int cursorsize = CURSOR; + + if (cursorsize != 0) { + printf("\x1b[%iD", cursorsize); + + for (int i = 0; i < cursorsize; i++) { + LINE[--GAP] = LINE[--CURSOR]; + } + } +} + +PREDEF_ACTION_SET("move-beginning-of-line", prompt_move_beginning_of_line); + +static int +count_forward_word() +{ + /* Ignore whitespace, then consume a whole word forward */ + int gapsize = PROMPT_LINE_LENGTH - GAP; + int run = 0; + + for (; run < gapsize && isspace(LINE[GAP + run]); run++) + continue; + for (; run < gapsize && isgraph(LINE[GAP + run]); run++) + continue; + + return run; +} +static int +count_backward_word() +{ + /* Ignore whitespace, then consume a whole word backward */ + int cursorsize = CURSOR; + int run = 0; + + for (; run < cursorsize && isspace(LINE[CURSOR - run - 1]); run++) + continue; + for (; run < cursorsize && isgraph(LINE[CURSOR - run - 1]); run++) + continue; + + return run; +} + +void +prompt_forward_word(void *data) +{ + int run = count_forward_word(); + + if (run != 0) { + for (int i = 0; i < run; i++) { + LINE[CURSOR++] = LINE[GAP++]; + } + + printf("\x1b[%iC", run); + } +} + +PREDEF_ACTION_SET("forward-word", prompt_forward_word); + +void +prompt_backward_word(void *data) +{ + int run = count_backward_word(); + + if (run != 0) { + for (int i = 0; i < run; i++) { + LINE[--GAP] = LINE[--CURSOR]; + } + + printf("\x1b[%iD", run); + } +} + +PREDEF_ACTION_SET("backward-word", prompt_backward_word); + +void +prompt_yank(void *data) +{ + /* + * Copy KILL[0:KILLCURSOR] back into LINE, starting at CURSOR + * (pushing the gap back) + */ + if (KILLCURSOR) { + for (int i = 0; i < KILLCURSOR; i++) { + LINE[CURSOR++] = KILL[i]; + printf("%c", KILL[i]); + } + + prompt_show_aftergap(); + } +} + +PREDEF_ACTION_SET("yank", prompt_yank); + +void +prompt_forward_kill_word(void *data) +{ + int run = count_forward_word(); + + if (run != 0) { + /* Find a word, copy it into KILL, then remove it from the gap */ + memcpy(KILL, &LINE[GAP], run); + KILLCURSOR = run; + + GAP += run; + prompt_show_aftergap(); + } +} + +PREDEF_ACTION_SET("kill-word", prompt_forward_kill_word); + +void +prompt_backward_kill_word(void *data) +{ + int run = count_backward_word(); + + if (run != 0) { + /* Find a word, copy it into KILL, then remove it from the end of CURSOR */ + memcpy(KILL, &LINE[CURSOR - run], run); + KILLCURSOR = run; + + printf("\x1b[%iD", run); + + CURSOR -= run; + prompt_show_aftergap(); + } +} + +PREDEF_ACTION_SET("backward-kill-word", prompt_backward_kill_word); + +void +prompt_kill_line(void *data) +{ + prompt_move_beginning_of_line(NULL); + + int gapsize = PROMPT_LINE_LENGTH - GAP; + + if (gapsize) { + /* + * Kill an entire line, CURSOR is already 0'd by moving to the start of the + * line, so we just need to reset GAP to reset the buffer. + */ + memcpy(KILL, &LINE[GAP], gapsize); + KILLCURSOR = gapsize; + + GAP = PROMPT_LINE_LENGTH; + + prompt_show_aftergap(); + } +} + +PREDEF_ACTION_SET("kill-line", prompt_kill_line); + +void +prompt_recall_history(struct prompt_history_entry *entry) +{ + /* + * Clear the command line, then recall a whole line from history (if there is one) + */ + prompt_move_beginning_of_line(NULL); + prompt_reset(); + prompt_show_aftergap(); + + if (entry != NULL) { + CURSOR = strlen(entry->line); + memcpy(LINE, entry->line, CURSOR); + printf("%s", entry->line); + } +} + +void +prompt_next_history_element(void *data) +{ + /* + * "next-history-element" functions as "delete-line" when at the start of + * history + */ + if (HISTORYCURSOR != NULL) { + HISTORYCURSOR = TAILQ_NEXT(HISTORYCURSOR, entry); + } + + prompt_recall_history(HISTORYCURSOR); +} + +PREDEF_ACTION_SET("next-history-element", prompt_next_history_element); + +void +prompt_previous_history_element(void *data) +{ + /* + * "previous-history-element" at the start of history starts at the most + * recently added entry + */ + if (HISTORYCURSOR == NULL) { + HISTORYCURSOR = TAILQ_LAST(HISTORY, prompt_history_head); + } else { + HISTORYCURSOR = TAILQ_PREV(HISTORYCURSOR, prompt_history_head, entry); + } + + prompt_recall_history(HISTORYCURSOR); +} + +PREDEF_ACTION_SET("previous-history-element", prompt_previous_history_element); + +/* + * History command/Lua interface + */ + +void +prompt_history_add(const char *line, int len) +{ + struct prompt_history_entry *entry = malloc(sizeof(struct prompt_history_entry)); + memcpy(entry->line, line, len); + + TAILQ_INSERT_TAIL(HISTORY, entry, entry); +} +void +prompt_history_remove(struct prompt_history_entry *entry) +{ + TAILQ_REMOVE(HISTORY, entry, entry); + free(entry); +} + +/* + * Used by Lua to populate the "history" global + */ + +struct prompt_history_entry * +prompt_history_first() +{ + return TAILQ_FIRST(HISTORY); +} +struct prompt_history_entry * +prompt_history_next(struct prompt_history_entry *entry) +{ + return TAILQ_NEXT(entry, entry); +} + +COMMAND_SET(history, "history", "display history entries", command_history); + +static int +command_history(int argc, char *argv[]) +{ + /* Pop the "history" entry from the history */ + prompt_history_remove(TAILQ_LAST(HISTORY, prompt_history_head)); + + pager_open(); + + struct prompt_history_entry *e; + TAILQ_FOREACH(e, HISTORY, entry) { + pager_output(e->line); + pager_output("\n"); + } + + pager_close(); + + return 0; +} + +/* + * Completion logic + */ + +#define PROMPT_COLUMNS 80 + +void +prompt_generic_complete(char *argv, generic_completer_first first, generic_completer_next_item next, + void *last, generic_completer_item_to_string tostring) +{ + /* + * Technically, we should be able to call tostring on the same + * "handle" at any time and get the same result, but unfortunately + * that doesn't work for dirents, so instead of remembering the exact + * item that we've matched, in all cases we just remember what the + * item stringified to (and recall that instead). + */ + char buf[40] = { 0 }; + + char maxbuf[40] = { 0 }; + int maxlen = 0; + + char matchbuf[40] = { 0 }; + int matches = 0; + + int arglen = strlen(argv); + + void *p = first(); + + while (p != last) { + tostring(p, buf, sizeof(buf)); + int len = strlen(buf); + + if (len >= arglen && strncmp(argv, buf, arglen) == 0) { + matches++; + + if (matches == 1) { + memcpy(matchbuf, buf, sizeof(buf)); + } + + if (len > maxlen) { + memcpy(maxbuf, buf, sizeof(buf)); + maxlen = len; + } + } + + p = next(p); + } + + if (matches == 0) { + return; + } + else if (matches == 1) { + /* Single match, just complete it. */ + char *remainder = matchbuf + arglen; + int rlen = strlen(remainder); + + printf("%s", remainder); + memcpy(&LINE[CURSOR], remainder, rlen); + CURSOR += rlen; + } else { + /* + * Many matches, print all aligned into columns, and then re-print the + * prompt and command line below all options. + * If all matches share a common prefix though, complete through to the + * end of the prefix so you can "jump" through options by just typing + * the few characters of difference. + */ + char *prefixbuf = maxbuf; + int prefixlen = strlen(prefixbuf); + + int column = 0; + int maxcolums = PROMPT_COLUMNS / maxlen; + + printf("\n"); + pager_open(); + + p = first(); + + while (p != last) { + tostring(p, buf, sizeof(buf)); + int len = strlen(buf); + + if (len >= arglen && strncmp(argv, buf, arglen) == 0) { + pager_output(buf); + + if (++column == maxcolums) { + column = 0; + + if (pager_output("\n")) { + break; + } + } else { + for (int i = len; i <= maxlen; i++) { + pager_output(" "); + } + } + + for (int i = 0; i < prefixlen; i++) { + if (buf[i] != prefixbuf[i]) { + prefixbuf[i] = '\0'; + prefixlen = i; + break; + } + } + } + + p = next(p); + } + + pager_close(); + printf("\n"); + prompt_reprint(); + + if (prefixlen != 0 && prefixlen > arglen) { + char *remainder = prefixbuf + arglen; + int rlen = prefixlen - arglen; + + printf("%s", remainder); + memcpy(&LINE[CURSOR], remainder, rlen); + CURSOR += rlen; + } + } +} + +/* + * Command completer + */ +static void * +command_first() +{ + return SET_BEGIN(Xcommand_set); +} +static void * +command_next(void *rawlast) +{ + struct bootblk_command **pcmd = rawlast; + + return (void*)++pcmd; +} +static void +command_tostring(void *rawlast, char *out, int len) +{ + struct bootblk_command **pcmd = rawlast; + + snprintf(out, len, "%s", (*pcmd)->c_name); +} + +void prompt_complete_command(void *data) { + LINE[CURSOR] = 0; + + prompt_generic_complete(LINE, command_first, command_next, SET_LIMIT(Xcommand_set), command_tostring); +} + +PREDEF_ACTION_SET("command-complete", prompt_complete_command); + +/* + * "smart"/context sensitive completer + */ +void +prompt_complete_smart(void *data) +{ + /* + * "smart" completion, completes a command if there isn't one typed out + * already, or tries to complete command arguments. + */ + if (GAP != PROMPT_LINE_LENGTH) { + return; + } + + int cmdlen = 0; + + for (; cmdlen < CURSOR && isalnum(LINE[cmdlen]); cmdlen++) + continue; + + if (!(isspace(LINE[cmdlen]) || ispunct(LINE[cmdlen])) || cmdlen == CURSOR) { + prompt_complete_command(NULL); + return; + } + + char old = LINE[cmdlen]; + LINE[cmdlen] = '\0'; + + char *command = LINE; + + char args[PROMPT_LINE_LENGTH] = { 0 }; + memcpy(args, &LINE[cmdlen + 1], CURSOR - cmdlen - 1); + + /* + * Find the number and text of the last/most recent argument so the completer + * has enough context to actually complete the argument. + * Doesn't need to be bulletproof since we only need the last arg, and the count. + */ + int argc = 1; + char *last = args; + char *next = strpbrk(args, "\t\f\v "); + + while (next != NULL) { + *next = '\0'; + last = next + 1; + + next = strpbrk(last, "\t\f\v "); + argc++; + } + + /* + * There's two special cases for completion, either a "_" command which + * matches any undefined command, or a "0" arg index, which matches any + * argument number. + * Matching an undefined command is an escape hatch for completing languages + * which are too complicated to properly parse here, and the "0" arg index + * is for commands involving flags and (again) more complicated parsing. + */ + int defined = false; + + struct bootblk_command **pcmd; + SET_FOREACH(pcmd, Xcommand_set) { + if (strcmp((*pcmd)->c_name, command) == 0) { + defined = true; + break; + } + } + + prompt_completion_entry *entry = NULL; + prompt_completion_entry *fallthrough = NULL; + + prompt_completion_entry **pce; + SET_FOREACH(pce, Xcompleter_set) { + prompt_completion_entry *e = *pce; + + if (strcmp(e->command, command) == 0 && (e->argn == 0 || e->argn == argc)) { + entry = e; + } + else if (strcmp(e->command, "_") == 0 && !defined) { + fallthrough = e; + } + } + + LINE[cmdlen] = old; + + if (entry == NULL) { + if (fallthrough != NULL) { + fallthrough->completer(command, last); + } + } else { + entry->completer(command, last); + } +} + +PREDEF_ACTION_SET("smart-complete", prompt_complete_smart); + +/* + * Misc completers, some command-specific, some generic + */ + +static void * +keybind_first() +{ + return (void*)prompt_first_binding(); +} +static void * +keybind_next(void *rawlast) +{ + return (void*)prompt_next_binding((struct prompt_keybind *)rawlast); +} +static void +keybind_tostring(void *raw, char *out, int len) +{ + struct prompt_keybind *p = raw; + + prompt_stroke_to_string(out, len, p->target); +} + +void +keyunbind_completer(char *command, char *argv) +{ + prompt_generic_complete(argv, keybind_first, keybind_next, NULL, keybind_tostring); +} + +COMPLETION_SET(keybind, 2, predefined_action_completer); + +static void * +environ_first() +{ + return environ; +} +static void * +environ_next(void* rawlast) +{ + struct env_var *ev = rawlast; + + return ev->ev_next; +} +static void +environ_tostring(void *raw, char *out, int len) +{ + struct env_var *ev = raw; + + snprintf(out, len, "%s", ev->ev_name); +} + +void +environ_completer(char *command, char *argv) +{ + prompt_generic_complete(argv, environ_first, environ_next, NULL, environ_tostring); +} + +COMPLETION_SET(show, 1, environ_completer); +COMPLETION_SET(set, 1, environ_completer); +COMPLETION_SET(unset, 1, environ_completer); + +static void * +predef_first() +{ + return SET_BEGIN(Xpredef_action_set); +} +static void * +predef_next(void* rawlast) +{ + struct prompt_predefined_action **ppa = rawlast; + + return (void*)++ppa; +} +static void +predef_tostring(void *rawlast, char *out, int len) +{ + struct prompt_predefined_action **ppa = rawlast; + + snprintf(out, len, "%s", (*ppa)->name); +} + +void +predefined_action_completer(char *command, char *argv) +{ + prompt_generic_complete(argv, predef_first, predef_next, SET_LIMIT(Xpredef_action_set), predef_tostring); +} + +/* + * Path completion doesn't fit into the first/next/tostring model super well, + * since we're working with an fd which contains its own state. Plus, since + * first() gets called twice, we need to have it reset the internal state of that + * fd so we can read all dirents again. + * In practice, this just means we need to remember the fd, and the name of the + * fd so it can be reopened. + */ + +static const char *path_dirname; +static int path_fd; + +static void * +path_next(void *raw) +{ + return readdirfd(path_fd); +} + +static void * +path_first() +{ + if (path_fd > 0) { + close(path_fd); + } + + path_fd = open(path_dirname, O_RDONLY); + + return path_next(NULL); +} + +static void +path_tostring(void *rawlast, char *out, int len) +{ + struct dirent *entry = rawlast; + + /* + * We need to ensure that path_dirname contains a path with a trailing "/" + * otherwise we'll spit out a bunch of garbage that can't be completed. + * In practice, the only path *with* a trailing "/" is going to be "/" + * itself since our dirname/basename split will always destroy the last "/". + */ + int dirnamelen = snprintf(out, len, "%s", path_dirname); + out += dirnamelen; + len -= dirnamelen; + + if (*(out - 1) != '/') { + *out++ = '/'; + len--; + } + + /* + * We also want to show directories (not "." and ".." though) with a trailing + * "/" so they can be completed as a whole, and then any of their entries + * can also be completed with minimal typing. + */ + + char *fmt = "%s"; + + if ((entry->d_type & DT_DIR) && strcmp(entry->d_name, ".") && strcmp(entry->d_name, "..")) { + fmt = "%s/"; + } + + snprintf(out, len, fmt, entry->d_name); +} + +void +path_completer(char *command, char *argv) +{ + /* + * We want to split argv into a dirname/basename pair, so we can check each + * entry inside of dirname if it matches the prefix basename. + * So, we just need to find the last "/" in argv, and replace it with a null + * terminator. Then ("/" + 1) is our basename, and argv is our dirname. + * However, since path_tostring gives us an absolute path, we actually need + * to throw away the basename and match against the (already absolute) + * path in argv, which is why we operate on the copy "path" instead. + */ + char path[128] = { 0 }; + snprintf(path, 128, "%s", argv); + + if (strlen(path) == 0) { + path[0] = '/'; + } + + char *dirname = path; + char *basename = path + strlen(path); + + while (basename > dirname && *(basename - 1) != '/') { + basename--; + } + + *(basename - 1) = '\0'; + + if (strlen(dirname) == 0) { + dirname = "/"; + } + + path_dirname = dirname; + path_fd = 0; + + prompt_generic_complete(argv, path_first, path_next, NULL, path_tostring); +} + +COMPLETION_SET(ls, 0, path_completer); diff --git a/stand/efi/libefi/efi_console.c b/stand/efi/libefi/efi_console.c --- a/stand/efi/libefi/efi_console.c +++ b/stand/efi/libefi/efi_console.c @@ -682,6 +682,15 @@ end_term(); } +static void +CR(int x, int y) +{ + if (args[0] > 0) + args[0]--; + curs_move(&curx, &cury, curx + x, cury + y); + end_term(); +} + /* Home cursor (left top corner), also called from mode command. */ void HO(void) @@ -792,6 +801,18 @@ else args[++argc] = 0; break; + case 'C': + if (argc < 0) + CR(1, 0); + else + CR(args[0], 0); + break; + case 'D': + if (argc < 0) + CR(-1, 0); + else + CR(-args[0], 0); + break; case 'H': /* ho = \E[H */ if (argc < 0) HO(); @@ -806,6 +827,12 @@ else bail_out(c); break; + case 'K': + if (argc < 0) + CL(0); + else + CL(args[0]); + break; case 'm': if (argc < 0) { fg_c = DEFAULT_FGCOLOR; @@ -1260,70 +1287,74 @@ return (false); } +#define ANSI_MODS_FLAG_SHIFT 0x1 +#define ANSI_MODS_FLAG_ALT 0x2 +#define ANSI_MODS_FLAG_CTRL 0x4 + /* - * Converts an EFI key shift state into a PC style modifer mask + * Converts an EFI key shift state into a PC style modifier mask */ - static char -keybuf_kss2mod(uint32_t kss) { +keybuf_kss2mod(uint32_t kss) +{ char modifiers = 0; - + if (kss & EFI_RIGHT_SHIFT_PRESSED || kss & EFI_LEFT_SHIFT_PRESSED) { - modifiers |= 1; + modifiers |= ANSI_MODS_FLAG_SHIFT; } - + if (kss & EFI_RIGHT_ALT_PRESSED || kss & EFI_LEFT_ALT_PRESSED) { - modifiers |= 2; + modifiers |= ANSI_MODS_FLAG_ALT; } - + if (kss & EFI_RIGHT_CONTROL_PRESSED || kss & EFI_LEFT_CONTROL_PRESSED) { - modifiers |= 4; + modifiers |= ANSI_MODS_FLAG_CTRL; } - + return '1' + modifiers; } /* * Writes a VT220 style input escape to keybuf, with modifiers */ - static void -keybuf_insvt(const char keycode, uint32_t kss) { +keybuf_insvt(const char keycode, uint32_t kss) +{ int i = 0; - + keybuf[i++] = 0x1b; /* esc */ keybuf[i++] = '['; keybuf[i++] = keycode; - + if (kss & EFI_SHIFT_STATE_VALID) { keybuf[i++] = ';'; keybuf[i++] = keybuf_kss2mod(kss); } - + keybuf[i++] = '~'; } /* - * Writes a xtern style input escape to keybuf, with modifiers + * Writes a xterm style input escape to keybuf, with modifiers */ - static void -keybuf_insxterm(const char key, uint32_t kss) { +keybuf_insxterm(const char key, uint32_t kss) +{ int i = 0; - + keybuf[i++] = 0x1b; /* esc */ keybuf[i++] = '['; - + if (kss & EFI_SHIFT_STATE_VALID) { keybuf[i++] = '1'; keybuf[i++] = ';'; - + keybuf[i++] = keybuf_kss2mod(kss); } - + keybuf[i++] = key; } @@ -1350,20 +1381,19 @@ else if (kss & EFI_RIGHT_ALT_PRESSED || kss & EFI_LEFT_ALT_PRESSED) { /* - * alt+[a-z] to ESC [ char + * alt+[a-z] to esc */ - keybuf[0] = 0x1b; /* esc */ - + if (key->UnicodeChar >= 'a' && key->UnicodeChar <= 'z') { keybuf[1] = key->UnicodeChar; - + return; } } } - + switch (key->ScanCode) { case SCAN_UP: /* UP */ keybuf_insxterm('A', kss); @@ -1384,7 +1414,7 @@ keybuf_insxterm('F', kss); break; case SCAN_INSERT: - /* + /* * Fall back on vt sequences, since xterm sequences don't * cover ins/del/pgup/pgdn */ @@ -1393,14 +1423,14 @@ case SCAN_DELETE: keybuf_insvt('3', kss); break; - case SCAN_PAGE_UP: /* PGUP */ + case SCAN_PAGE_UP: keybuf_insvt('5', kss); break; case SCAN_PAGE_DOWN: keybuf_insvt('6', kss); break; case SCAN_ESC: - keybuf[0] = 0x1b; /* esc */ + keybuf[0] = 0x1b; /* esc */ break; default: keybuf[0] = key->UnicodeChar; diff --git a/stand/i386/libi386/vidconsole.c b/stand/i386/libi386/vidconsole.c --- a/stand/i386/libi386/vidconsole.c +++ b/stand/i386/libi386/vidconsole.c @@ -1070,6 +1070,182 @@ vidc_biosputchar(c); } +#define BIOS_KBD_FLAG_LSHIFT 0x1 +#define BIOS_KBD_FLAG_RSHIFT 0x2 +#define BIOS_KBD_FLAG_CTRL 0x4 +#define BIOS_KBD_FLAG_ALT 0x8 + +#define ANSI_MODS_FLAG_SHIFT 0x1 +#define ANSI_MODS_FLAG_ALT 0x2 +#define ANSI_MODS_FLAG_CTRL 0x4 + +/* + * Converts BIOS keyboard flags into a PC style modifer mask + */ +static char +keybuf_kss2mod(uint32_t kss) +{ + char modifiers = 0; + + if (kss & (BIOS_KBD_FLAG_LSHIFT | BIOS_KBD_FLAG_RSHIFT)) { + /* left shift or right shift */ + modifiers |= ANSI_MODS_FLAG_SHIFT; + } + + if (kss & BIOS_KBD_FLAG_CTRL) { + /* ctrl */ + modifiers |= ANSI_MODS_FLAG_CTRL; + } + + if (kss & BIOS_KBD_FLAG_ALT) { + /* alt */ + modifiers |= ANSI_MODS_FLAG_ALT; + } + + return '1' + modifiers; +} + +/* + * Writes a VT220 style input escape to keybuf, with modifiers + */ +static int +keybuf_insvt(const char keycode, uint32_t kss) +{ + int i = 0; + + keybuf[i++] = '['; + keybuf[i++] = keycode; + + if ((kss & 0xf) != 0) { + /* ignore all BIOS keyboard flags besides shift/ctrl/alt */ + keybuf[i++] = ';'; + keybuf[i++] = keybuf_kss2mod(kss); + } + + keybuf[i++] = '~'; + + return 0x1b; /* esc */ +} + +/* + * Writes a xtern style input escape to keybuf, with modifiers + */ +static int +keybuf_insxterm(const char key, uint32_t kss) +{ + int i = 0; + + keybuf[i++] = '['; + + if ((kss & 0xf) != 0) { + /* ignore all BIOS keyboard flags besides shift/ctrl/alt */ + keybuf[i++] = '1'; + keybuf[i++] = ';'; + + keybuf[i++] = keybuf_kss2mod(kss); + } + + keybuf[i++] = key; + + return 0x1b; /* esc */ +} + +/* Helpers to normalize BIOS scancodes + * + * Unfortunately the BIOS encodes modifiers into the scancode + * and alt even zeros out the ASCII code we get back as well, + * so to properly support alt+key we need a big lookup to convert + * the modified scancodes back into the "normal" ones, and then + * deal with just normal scancodes and modifiers. + * + * Additionally, special keys have seemingly random scancodes with + * no apparent relationship between normal/ctrl/alt versions, so + * we also need to remap those as well. + * + * The only pattern I've found is that the shifted scancodes are + * the same as the normal scancodes, only the embedded ASCII code + * changes. + */ +struct bios_scancode_remap { + union { + struct { + union { + unsigned char normal; + unsigned char shift; + }; + + unsigned char ctrl; + unsigned char alt; + }; + + unsigned char codes[3]; + }; + + char ascii; +}; + +static struct bios_scancode_remap bios_sc_map[] = { +/* normal ctrl alt ascii */ + {0x1e, 0x1e, 0x1e, 'a'}, + {0x30, 0x30, 0x30, 'b'}, + {0x2e, 0x2e, 0x2e, 'c'}, + {0x20, 0x20, 0x20, 'd'}, + {0x12, 0x12, 0x12, 'e'}, + {0x21, 0x21, 0x21, 'f'}, + {0x22, 0x22, 0x22, 'g'}, + {0x23, 0x23, 0x23, 'h'}, + {0x17, 0x17, 0x17, 'i'}, + {0x24, 0x24, 0x24, 'j'}, + {0x25, 0x25, 0x25, 'k'}, + {0x26, 0x26, 0x26, 'l'}, + {0x32, 0x32, 0x32, 'm'}, + {0x31, 0x31, 0x31, 'n'}, + {0x18, 0x18, 0x18, 'o'}, + {0x19, 0x19, 0x19, 'p'}, + {0x10, 0x10, 0x10, 'q'}, + {0x13, 0x13, 0x13, 'r'}, + {0x1f, 0x1f, 0x1f, 's'}, + {0x14, 0x14, 0x14, 't'}, + {0x16, 0x16, 0x16, 'u'}, + {0x2f, 0x2f, 0x2f, 'v'}, + {0x11, 0x11, 0x11, 'w'}, + {0x2d, 0x2d, 0x2d, 'x'}, + {0x15, 0x15, 0x15, 'y'}, + {0x2c, 0x2c, 0x2c, 'z'}, + {0x02, 0xff, 0x78, '1'}, + {0x03, 0x03, 0x79, '2'}, + {0x04, 0xff, 0x7a, '3'}, + {0x05, 0xff, 0x7b, '4'}, + {0x06, 0xff, 0x7c, '5'}, + {0x07, 0x07, 0x7d, '6'}, + {0x08, 0xff, 0x7e, '7'}, + {0x09, 0xff, 0x7f, '8'}, + {0x0a, 0xff, 0x80, '9'}, + {0x0b, 0xff, 0x81, '0'}, + {0x0c, 0x0c, 0x82, '-'}, + {0x0d, 0xff, 0x83, '='}, + {0x1a, 0x1a, 0x1a, '['}, + {0x1b, 0x1b, 0x1b, ']'}, + {0x27, 0xff, 0x27, ';'}, + {0x28, 0xff, 0xff, '\''}, + {0x29, 0xff, 0xff, '`'}, + {0x0e, 0x0e, 0x0e, 0x08}, /* backspace */ + {0x53, 0x93, 0xa3, 0x00}, /* del */ + {0x50, 0x91, 0xa0, 0x00}, /* down */ + {0x4f, 0x75, 0x9f, 0x00}, /* end */ + {0x1c, 0x1c, 0xa6, 0x0a}, /* enter */ + {0x01, 0x01, 0x01, 0x00}, /* esc */ + {0x47, 0x77, 0x97, 0x00}, /* home */ + {0x52, 0x92, 0xa2, 0x00}, /* ins */ + {0x4b, 0x73, 0x9b, 0x00}, /* left */ + {0x51, 0x76, 0xa1, 0x00}, /* pgdn */ + {0x49, 0x84, 0x99, 0x00}, /* pgup */ + {0x4d, 0x74, 0x9d, 0x00}, /* right */ + {0x39, 0x39, 0x39, ' '}, /* spacebar */ + {0x0f, 0x94, 0xa5, '\t'}, /* tab */ + {0x48, 0x8d, 0x98, 0x00}, /* up */ +}; + static int vidc_getchar(void) { @@ -1088,30 +1264,69 @@ v86.addr = 0x16; v86.eax = 0x0; v86int(); - if ((v86.eax & 0xff) != 0) { - return (v86.eax & 0xff); + + int scancode = (v86.eax & 0xff00) >> 8; + int character = v86.eax & 0xff; + + v86.ctl = 0; + v86.addr = 0x16; + v86.eax = 0x200; + v86int(); + + int modifiers = v86.eax & 0xff; + int len = sizeof(bios_sc_map) / sizeof(bios_sc_map[0]); + + for (int i = 0; i < len; i++) { + struct bios_scancode_remap *map = &bios_sc_map[i]; + + for (int c = 0; c < 3; c++) { + if (map->codes[c] == scancode) { + scancode = map->normal; + + if (character == 0) { + /* + * preserve character from BIOS so we don't + * have to manually handle shift/control + */ + character = map->ascii; + } + + break; + } + } } - /* extended keys */ - switch (v86.eax & 0xff00) { - case 0x4800: /* up */ - keybuf[0] = '['; - keybuf[1] = 'A'; - return (0x1b); /* esc */ - case 0x4b00: /* left */ - keybuf[0] = '['; - keybuf[1] = 'D'; - return (0x1b); /* esc */ - case 0x4d00: /* right */ - keybuf[0] = '['; - keybuf[1] = 'C'; - return (0x1b); /* esc */ - case 0x5000: /* down */ - keybuf[0] = '['; - keybuf[1] = 'B'; - return (0x1b); /* esc */ + switch (scancode) { + case 0x48: /* up */ + return keybuf_insxterm('A', modifiers); + case 0x4b: /* left */ + return keybuf_insxterm('D', modifiers); + case 0x4d: /* right */ + return keybuf_insxterm('C', modifiers); + case 0x50: /* down */ + return keybuf_insxterm('B', modifiers); + case 0x47: /* home */ + return keybuf_insxterm('H', modifiers); + case 0x4f: /* end */ + return keybuf_insxterm('F', modifiers); + case 0x52: /* insert */ + return keybuf_insvt('2', modifiers); + case 0x53: /* delete */ + return keybuf_insvt('3', modifiers); + case 0x49: /* pgup */ + return keybuf_insvt('5', modifiers); + case 0x51: /* pgdn */ + return keybuf_insvt('6', modifiers); default: - return (-1); + if (modifiers & BIOS_KBD_FLAG_ALT) { + if (('a' <= character) && (character <= 'z')) { + keybuf[0] = character; + + return (0x1b); /* esc */ + } + } + + return character; } } else { return (-1); diff --git a/stand/loader.mk b/stand/loader.mk --- a/stand/loader.mk +++ b/stand/loader.mk @@ -85,6 +85,11 @@ SRCS+= install.c .endif +.if ${LOADER_EDITING_SUPPORT:Uyes} == "yes" +CFLAGS+= -DLOADER_EDITING_SUPPORT +SRCS+= prompt_bindings.c prompt_editing.c +.endif + # Filesystem support .if ${LOADER_CD9660_SUPPORT:Uno} == "yes" CFLAGS+= -DLOADER_CD9660_SUPPORT