/* vim: tabstop=4 shiftwidth=4 noexpandtab * This file is part of ToaruOS and is released under the terms * of the NCSA / University of Illinois License - see LICENSE.md * Copyright (C) 2018 K. Lange * * Experimental rline replacement with syntax highlighting, based * on bim's highlighting and line editing. * */ #define _XOPEN_SOURCE #define _DEFAULT_SOURCE #include #include #include #include #include #include #include #include #include #include #include #include #include "rline.h" char * rline_history[RLINE_HISTORY_ENTRIES]; int rline_history_count = 0; int rline_history_offset = 0; int rline_scroll = 0; char * rline_exit_string = "exit\n"; void rline_history_insert(char * str) { if (str[strlen(str)-1] == '\n') { str[strlen(str)-1] = '\0'; } if (rline_history_count) { if (!strcmp(str, rline_history_prev(1))) { free(str); return; } } if (rline_history_count == RLINE_HISTORY_ENTRIES) { free(rline_history[rline_history_offset]); rline_history[rline_history_offset] = str; rline_history_offset = (rline_history_offset + 1) % RLINE_HISTORY_ENTRIES; } else { rline_history[rline_history_count] = str; rline_history_count++; } } void rline_history_append_line(char * str) { if (rline_history_count) { char ** s = &rline_history[(rline_history_count - 1 + rline_history_offset) % RLINE_HISTORY_ENTRIES]; char * c = malloc(strlen(*s) + strlen(str) + 2); sprintf(c, "%s\n%s", *s, str); if (c[strlen(c)-1] == '\n') { c[strlen(c)-1] = '\0'; } free(*s); *s = c; } else { /* wat */ } } char * rline_history_get(int item) { return rline_history[(item + rline_history_offset) % RLINE_HISTORY_ENTRIES]; } char * rline_history_prev(int item) { return rline_history_get(rline_history_count - item); } #define UTF8_ACCEPT 0 #define UTF8_REJECT 1 /** * Conceptually similar to its predecessor, this implementation is much * less cool, as it uses three separate state tables and more shifts. */ static inline uint32_t decode(uint32_t* state, uint32_t* codep, uint32_t byte) { static int state_table[32] = { 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, /* 0xxxxxxx */ 1,1,1,1,1,1,1,1, /* 10xxxxxx */ 2,2,2,2, /* 110xxxxx */ 3,3, /* 1110xxxx */ 4, /* 11110xxx */ 1 /* 11111xxx */ }; static int mask_bytes[32] = { 0x7F,0x7F,0x7F,0x7F,0x7F,0x7F,0x7F,0x7F, 0x7F,0x7F,0x7F,0x7F,0x7F,0x7F,0x7F,0x7F, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x1F,0x1F,0x1F,0x1F, 0x0F,0x0F, 0x07, 0x00 }; static int next[5] = { 0, 1, 0, 2, 3 }; if (*state == UTF8_ACCEPT) { *codep = byte & mask_bytes[byte >> 3]; *state = state_table[byte >> 3]; } else if (*state > 0) { *codep = (byte & 0x3F) | (*codep << 6); *state = next[*state]; } return *state; } #define ENTER_KEY '\n' #define BACKSPACE_KEY 0x08 #define DELETE_KEY 0x7F #define MINIMUM_SIZE 10 /** * Same structures as in bim. * A single character has: * - A codepoint (Unicode) of up to 21 bits. * - Flags for syntax highlighting. * - A display width for rendering. */ typedef struct { uint32_t display_width:4; uint32_t flags:7; uint32_t codepoint:21; } __attribute__((packed)) char_t; /** * We generally only have the one line, * but this matches bim for compatibility reasons. */ typedef struct { int available; int actual; int istate; char_t text[]; } line_t; /** * We operate on a single line of text. * Maybe we can expand this in the future * for continuations of edits such as when * a quote is unclosed? */ static line_t * the_line = NULL; /** * Line editor state */ static int loading = 0; static int column = 0; static int offset = 0; static int width = 0; static int full_width = 0; static int show_right_side = 0; static int show_left_side = 0; static int prompt_width_calc = 0; static int buf_size_max = 0; /** * Prompt strings. * Defaults to just a "> " prompt with no right side. * Support for right side prompts is important * for the ToaruOS shell. */ static int prompt_width = 2; static char * prompt = "> "; static int prompt_right_width = 0; static char * prompt_right = ""; int rline_exp_set_prompts(char * left, char * right, int left_width, int right_width) { prompt = left; prompt_right = right; prompt_width = left_width; prompt_right_width = right_width; return 0; } /** * Extra shell commands to highlight as keywords. * These are basically just copied from the * shell's tab completion database on startup. */ static char ** shell_commands = {0}; static int shell_commands_len = 0; int rline_exp_set_shell_commands(char ** cmds, int len) { shell_commands = cmds; shell_commands_len = len; return 0; } /** * Tab completion callback. * Compatible with the original rline version. */ static rline_callback_t tab_complete_func = NULL; int rline_exp_set_tab_complete_func(rline_callback_t func) { tab_complete_func = func; return 0; } static int _unget = -1; static void _ungetc(int c) { _unget = c; } static int getch(int immediate, int timeout) { if (_unget != -1) { int out = _unget; _unget = -1; return out; } struct pollfd fds[1]; fds[0].fd = STDIN_FILENO; fds[0].events = POLLIN; int ret = poll(fds,1,(timeout == 1) ? 50 : -1); if (ret > 0 && fds[0].revents & POLLIN) { unsigned char buf[1]; int unused = read(STDIN_FILENO, buf, 1); (void)unused; return buf[0]; } else { return -1; } } /** * Convert from Unicode string to utf-8. */ static int to_eight(uint32_t codepoint, char * out) { memset(out, 0x00, 7); if (codepoint < 0x0080) { out[0] = (char)codepoint; } else if (codepoint < 0x0800) { out[0] = 0xC0 | (codepoint >> 6); out[1] = 0x80 | (codepoint & 0x3F); } else if (codepoint < 0x10000) { out[0] = 0xE0 | (codepoint >> 12); out[1] = 0x80 | ((codepoint >> 6) & 0x3F); out[2] = 0x80 | (codepoint & 0x3F); } else if (codepoint < 0x200000) { out[0] = 0xF0 | (codepoint >> 18); out[1] = 0x80 | ((codepoint >> 12) & 0x3F); out[2] = 0x80 | ((codepoint >> 6) & 0x3F); out[3] = 0x80 | ((codepoint) & 0x3F); } else if (codepoint < 0x4000000) { out[0] = 0xF8 | (codepoint >> 24); out[1] = 0x80 | (codepoint >> 18); out[2] = 0x80 | ((codepoint >> 12) & 0x3F); out[3] = 0x80 | ((codepoint >> 6) & 0x3F); out[4] = 0x80 | ((codepoint) & 0x3F); } else { out[0] = 0xF8 | (codepoint >> 30); out[1] = 0x80 | ((codepoint >> 24) & 0x3F); out[2] = 0x80 | ((codepoint >> 18) & 0x3F); out[3] = 0x80 | ((codepoint >> 12) & 0x3F); out[4] = 0x80 | ((codepoint >> 6) & 0x3F); out[5] = 0x80 | ((codepoint) & 0x3F); } return strlen(out); } /** * Obtain codepoint display width. * * This is copied from bim. Supports a few useful * things like rendering escapes as codepoints. */ static int codepoint_width(wchar_t codepoint) { if (codepoint == '\t') { return 1; /* Recalculate later */ } if (codepoint < 32) { /* We render these as ^@ */ return 2; } if (codepoint == 0x7F) { /* Renders as ^? */ return 2; } if (codepoint > 0x7f && codepoint < 0xa0) { /* Upper control bytes */ return 4; } if (codepoint == 0xa0) { /* Non-breaking space _ */ return 1; } /* Skip wcwidth for anything under 256 */ if (codepoint > 256) { /* Higher codepoints may be wider (eg. Japanese) */ int out = wcwidth(codepoint); if (out >= 1) return out; /* Invalid character, render as [U+ABCD] or [U+ABCDEF] */ return (codepoint < 0x10000) ? 8 : 10; } return 1; } void recalculate_tabs(line_t * line) { int j = 0; for (int i = 0; i < line->actual; ++i) { if (line->text[i].codepoint == '\t') { line->text[i].display_width = 4 - (j % 4); } j += line->text[i].display_width; } } /** * Color themes have also been copied from bim. * * Slimmed down to only the ones we use for syntax * highlighting; the UI colors have been removed. */ static const char * COLOR_FG = "@9"; static const char * COLOR_BG = "@9"; static const char * COLOR_ALT_FG = "@5"; static const char * COLOR_ALT_BG = "@9"; static const char * COLOR_KEYWORD = "@4"; static const char * COLOR_STRING = "@2"; static const char * COLOR_COMMENT = "@5"; static const char * COLOR_TYPE = "@3"; static const char * COLOR_PRAGMA = "@1"; static const char * COLOR_NUMERAL = "@1"; static const char * COLOR_RED = "@1"; static const char * COLOR_GREEN = "@2"; static const char * COLOR_ESCAPE = "@2"; static const char * COLOR_SEARCH_FG = "@0"; static const char * COLOR_SEARCH_BG = "@3"; /** * Themes are selected from the $RLINE_THEME * environment variable. */ static void rline_exp_load_colorscheme_default(void) { COLOR_FG = "@9"; COLOR_BG = "@9"; COLOR_ALT_FG = "@10"; COLOR_ALT_BG = "@9"; COLOR_KEYWORD = "@14"; COLOR_STRING = "@2"; COLOR_COMMENT = "@10"; COLOR_TYPE = "@3"; COLOR_PRAGMA = "@1"; COLOR_NUMERAL = "@1"; COLOR_RED = "@1"; COLOR_GREEN = "@2"; COLOR_ESCAPE = "@12"; COLOR_SEARCH_FG = "@0"; COLOR_SEARCH_BG = "@13"; } static void rline_exp_load_colorscheme_sunsmoke(void) { COLOR_FG = "2;230;230;230"; COLOR_BG = "@9"; COLOR_ALT_FG = "2;122;122;122"; COLOR_ALT_BG = "2;46;43;46"; COLOR_KEYWORD = "2;51;162;230"; COLOR_STRING = "2;72;176;72"; COLOR_COMMENT = "2;158;153;129;3"; COLOR_TYPE = "2;230;206;110"; COLOR_PRAGMA = "2;194;70;54"; COLOR_NUMERAL = "2;230;43;127"; COLOR_RED = "2;222;53;53"; COLOR_GREEN = "2;55;167;0"; COLOR_ESCAPE = "2;113;203;173"; COLOR_SEARCH_FG = "5;234"; COLOR_SEARCH_BG = "5;226"; } /** * Syntax highlighting flags. */ #define FLAG_NONE 0 #define FLAG_KEYWORD 1 #define FLAG_STRING 2 #define FLAG_COMMENT 3 #define FLAG_TYPE 4 #define FLAG_PRAGMA 5 #define FLAG_NUMERAL 6 #define FLAG_ERROR 7 #define FLAG_DIFFPLUS 8 #define FLAG_DIFFMINUS 9 #define FLAG_NOTICE 10 #define FLAG_BOLD 11 #define FLAG_LINK 12 #define FLAG_ESCAPE 13 #define FLAG_SELECT (1 << 5) #define FLAG_SEARCH (1 << 6) struct syntax_state { line_t * line; int line_no; int state; int i; }; #define paint(length, flag) do { for (int i = 0; i < (length) && state->i < state->line->actual; i++, state->i++) { state->line->text[state->i].flags = (flag); } } while (0) #define charat() (state->i < state->line->actual ? state->line->text[(state->i)].codepoint : -1) #define nextchar() (state->i + 1 < state->line->actual ? state->line->text[(state->i+1)].codepoint : -1) #define lastchar() (state->i - 1 >= 0 ? state->line->text[(state->i-1)].codepoint : -1) #define skip() (state->i++) #define charrel(x) (state->i + (x) < state->line->actual ? state->line->text[(state->i+(x))].codepoint : -1) /** * Match and paint a single keyword. Returns 1 if the keyword was matched and 0 otherwise, * so it can be used for prefix checking for things that need further special handling. */ int match_and_paint(struct syntax_state * state, const char * keyword, int flag, int (*keyword_qualifier)(int c)) { if (keyword_qualifier(lastchar())) return 0; if (!keyword_qualifier(charat())) return 0; int i = state->i; int slen = 0; while (i < state->line->actual || *keyword == '\0') { if (*keyword == '\0' && (i >= state->line->actual || !keyword_qualifier(state->line->text[i].codepoint))) { for (int j = 0; j < slen; ++j) { paint(1, flag); } return 1; } if (*keyword != state->line->text[i].codepoint) return 0; i++; keyword++; slen++; } return 0; } /** * Find keywords from a list and paint them, assuming they aren't in the middle of other words. * Returns 1 if a keyword from the last was found, otherwise 0. */ int find_keywords(struct syntax_state * state, char ** keywords, int flag, int (*keyword_qualifier)(int c)) { if (keyword_qualifier(lastchar())) return 0; if (!keyword_qualifier(charat())) return 0; for (char ** keyword = keywords; *keyword; ++keyword) { int d = 0; while (state->i + d < state->line->actual && state->line->text[state->i+d].codepoint == (*keyword)[d]) d++; if ((*keyword)[d] == '\0' && (state->i + d >= state->line->actual || !keyword_qualifier(state->line->text[state->i+d].codepoint))) { paint((int)strlen(*keyword), flag); return 1; } } return 0; } /** * This is a basic character matcher for "keyword" characters. */ int simple_keyword_qualifier(int c) { return isalnum(c) || (c == '_'); } int common_comment_buzzwords(struct syntax_state * state) { if (match_and_paint(state, "TODO", FLAG_NOTICE, simple_keyword_qualifier)) { return 1; } else if (match_and_paint(state, "XXX", FLAG_NOTICE, simple_keyword_qualifier)) { return 1; } else if (match_and_paint(state, "FIXME", FLAG_ERROR, simple_keyword_qualifier)) { return 1; } return 0; } /** * Paint a comment until end of line, assumes this comment can not continue. * (Some languages have comments that can continue with a \ - don't use this!) * Assumes you've already painted your comment start characters. */ int paint_comment(struct syntax_state * state) { while (charat() != -1) { if (common_comment_buzzwords(state)) continue; else { paint(1, FLAG_COMMENT); } } return -1; } int c_keyword_qualifier(int c) { return isalnum(c) || (c == '_'); } void paint_simple_string(struct syntax_state * state) { /* Assumes you came in from a check of charat() == '"' */ paint(1, FLAG_STRING); while (charat() != -1) { if (charat() == '\\' && nextchar() == '"') { paint(2, FLAG_ESCAPE); } else if (charat() == '"') { paint(1, FLAG_STRING); return; } else if (charat() == '\\') { paint(2, FLAG_ESCAPE); } else { paint(1, FLAG_STRING); } } } void paint_single_string(struct syntax_state * state) { /* Assumes you came in from a check of charat() == '\'' */ paint(1, FLAG_NUMERAL); while (charat() != -1) { if (charat() == '\\' && nextchar() == '\'') { paint(2, FLAG_ESCAPE); } else if (charat() == '\'') { paint(1, FLAG_NUMERAL); return; } else if (charat() == '\\') { paint(2, FLAG_ESCAPE); } else { paint(1, FLAG_NUMERAL); } } } char * syn_krk_keywords[] = { "and","class","def","else","export","for","if","in","import", "let","not","or","print","return","while","try","except","raise", "continue","break", NULL }; char * syn_krk_types[] = { /* built-in functions */ "self", "super", /* implicit in a class method */ "len", "str", "int", "float", "dir", /* global functions from __builtins__ */ "list","dict","range", /* builtin classes */ "object","exception","isinstance", NULL }; char * syn_krk_special[] = { "True","False","None", NULL }; int paint_krk_numeral(struct syntax_state * state) { if (charat() == '0' && (nextchar() == 'x' || nextchar() == 'X')) { paint(2, FLAG_NUMERAL); while (isxdigit(charat())) paint(1, FLAG_NUMERAL); } else if (charat() == '0' && (nextchar() == 'o' || nextchar() == 'O')) { paint(2, FLAG_NUMERAL); while (charat() >= '0' && charat() <= '7') paint(1, FLAG_NUMERAL); } else if (charat() == '0' && (nextchar() == 'b' || nextchar() == 'B')) { paint(2, FLAG_NUMERAL); while (charat() == '0' || charat() == '1') paint(1, FLAG_NUMERAL); } else { while (isdigit(charat())) paint(1, FLAG_NUMERAL); if (charat() == '.') { paint(1, FLAG_NUMERAL); while (isdigit(charat())) paint(1, FLAG_NUMERAL); } } return 0; } int syn_krk_calculate(struct syntax_state * state) { switch (state->state) { case -1: case 0: if (charat() == '#') { paint_comment(state); } else if (charat() == '"') { paint_simple_string(state); return 0; } else if (charat() == '\'') { paint_single_string(state); return 0; } else if (find_keywords(state, syn_krk_keywords, FLAG_KEYWORD, c_keyword_qualifier)) { return 0; } else if (lastchar() != '.' && find_keywords(state, syn_krk_types, FLAG_TYPE, c_keyword_qualifier)) { return 0; } else if (find_keywords(state, syn_krk_special, FLAG_NUMERAL, c_keyword_qualifier)) { return 0; } else if (!c_keyword_qualifier(lastchar()) && isdigit(charat())) { paint_krk_numeral(state); return 0; } else if (charat() != -1) { skip(); return 0; } break; } return -1; } /** * Convert syntax hilighting flag to color code */ static const char * flag_to_color(int _flag) { int flag = _flag & 0xF; switch (flag) { case FLAG_KEYWORD: return COLOR_KEYWORD; case FLAG_STRING: return COLOR_STRING; case FLAG_COMMENT: return COLOR_COMMENT; case FLAG_TYPE: return COLOR_TYPE; case FLAG_NUMERAL: return COLOR_NUMERAL; case FLAG_PRAGMA: return COLOR_PRAGMA; case FLAG_DIFFPLUS: return COLOR_GREEN; case FLAG_DIFFMINUS: return COLOR_RED; case FLAG_SELECT: return COLOR_FG; // case FLAG_BOLD: // return COLOR_BOLD; // case FLAG_LINK: // return COLOR_LINK; case FLAG_ESCAPE: return COLOR_ESCAPE; default: return COLOR_FG; } } struct syntax_definition { char * name; int (*calculate)(struct syntax_state *); } syntaxes[] = { {"krk",syn_krk_calculate}, {NULL, NULL}, }; static struct syntax_definition * syntax; int rline_exp_set_syntax(char * name) { for (struct syntax_definition * s = syntaxes; s->name; ++s) { if (!strcmp(name,s->name)) { syntax = s; return 0; } } return 1; } /** * Syntax highlighting * Slimmed down from the bim implementation a bit, * but generally compatible with the same definitions. * * Type highlighting has been removed as the sh highlighter * didn't use it. This should be made pluggable again, and * the bim syntax highlighters should probably be broken * out into dynamically-loaded libraries? */ static void recalculate_syntax(line_t * line) { /* Clear syntax for this line first */ int line_no = 0; //int is_original = 1; while (1) { for (int i = 0; i < line->actual; ++i) { line->text[i].flags = 0; } if (!syntax) { return; } /* Start from the line's stored in initial state */ struct syntax_state state; state.line = line; state.line_no = line_no; state.state = line->istate; state.i = 0; while (1) { state.state = syntax->calculate(&state); if (state.state != 0) { /* TODO: Figure out a way to make this work for multiline input */ #if 0 if (line_no == -1) return; if (!is_original) { redraw_line(line_no); } if (line_no + 1 < env->line_count && env->lines[line_no+1]->istate != state.state) { line_no++; line = env->lines[line_no]; line->istate = state.state; if (env->loading) return; is_original = 0; goto _next; } #endif return; } } //_next: // (void)0; } } /** * Set colors */ static void set_colors(const char * fg, const char * bg) { printf("\033[22;23;"); if (*bg == '@') { int _bg = atoi(bg+1); if (_bg < 10) { printf("4%d;", _bg); } else { printf("10%d;", _bg-10); } } else { printf("48;%s;", bg); } if (*fg == '@') { int _fg = atoi(fg+1); if (_fg < 10) { printf("3%dm", _fg); } else { printf("9%dm", _fg-10); } } else { printf("38;%sm", fg); } fflush(stdout); } /** * Set just the foreground color * * (See set_colors above) */ static void set_fg_color(const char * fg) { printf("\033[22;23;"); if (*fg == '@') { int _fg = atoi(fg+1); if (_fg < 10) { printf("3%dm", _fg); } else { printf("9%dm", _fg-10); } } else { printf("38;%sm", fg); } fflush(stdout); } /** * Mostly copied from bim, but with some minor * alterations and removal of selection support. */ static void render_line(void) { printf("\033[?25l"); if (show_left_side) { printf("\033[0m\r%s", prompt); } else { printf("\033[0m\r$"); } if (offset && prompt_width_calc) { set_colors(COLOR_ALT_FG, COLOR_ALT_BG); printf("\b<"); } int i = 0; /* Offset in char_t line data entries */ int j = 0; /* Offset in terminal cells */ const char * last_color = NULL; int was_searching = 0; /* Set default text colors */ set_colors(COLOR_FG, COLOR_BG); /* * When we are rendering in the middle of a wide character, * we render -'s to fill the remaining amount of the * charater's width */ int remainder = 0; int is_spaces = 1; line_t * line = the_line; /* For each character in the line ... */ while (i < line->actual) { /* If there is remaining text... */ if (remainder) { /* If we should be drawing by now... */ if (j >= offset) { /* Fill remainder with -'s */ set_colors(COLOR_ALT_FG, COLOR_ALT_BG); printf("-"); set_colors(COLOR_FG, COLOR_BG); } /* One less remaining width cell to fill */ remainder--; /* Terminal offset moves forward */ j++; /* * If this was the last remaining character, move to * the next codepoint in the line */ if (remainder == 0) { i++; } continue; } /* Get the next character to draw */ char_t c = line->text[i]; if (c.codepoint != ' ') is_spaces = 0; /* If we should be drawing by now... */ if (j >= offset) { /* If this character is going to fall off the edge of the screen... */ if (j - offset + c.display_width >= width - prompt_width_calc) { /* We draw this with special colors so it isn't ambiguous */ set_colors(COLOR_ALT_FG, COLOR_ALT_BG); /* If it's wide, draw ---> as needed */ while (j - offset < width - prompt_width_calc - 1) { printf("-"); j++; } /* End the line with a > to show it overflows */ printf(">"); set_colors(COLOR_FG, COLOR_BG); j++; break; } /* Syntax hilighting */ const char * color = flag_to_color(c.flags); if ((c.flags & FLAG_SEARCH) || (c.flags == FLAG_NOTICE)) { set_colors(COLOR_SEARCH_FG, COLOR_SEARCH_BG); was_searching = 1; } else if (was_searching) { set_colors(color, COLOR_BG); last_color = color; } else if (!last_color || strcmp(color, last_color)) { set_fg_color(color); last_color = color; } /* Render special characters */ if (c.codepoint == '\t') { set_colors(COLOR_ALT_FG, COLOR_ALT_BG); printf("»"); for (int i = 1; i < c.display_width; ++i) { printf("·"); } set_colors(last_color ? last_color : COLOR_FG, COLOR_BG); } else if (c.codepoint < 32) { /* Codepoints under 32 to get converted to ^@ escapes */ set_colors(COLOR_ALT_FG, COLOR_ALT_BG); printf("^%c", '@' + c.codepoint); set_colors(last_color ? last_color : COLOR_FG, COLOR_BG); } else if (c.codepoint == 0x7f) { set_colors(COLOR_ALT_FG, COLOR_ALT_BG); printf("^?"); set_colors(last_color ? last_color : COLOR_FG, COLOR_BG); } else if (c.codepoint > 0x7f && c.codepoint < 0xa0) { set_colors(COLOR_ALT_FG, COLOR_ALT_BG); printf("<%2x>", c.codepoint); set_colors(last_color ? last_color : COLOR_FG, COLOR_BG); } else if (c.codepoint == 0xa0) { set_colors(COLOR_ALT_FG, COLOR_ALT_BG); printf("_"); set_colors(last_color ? last_color : COLOR_FG, COLOR_BG); } else if (c.display_width == 8) { set_colors(COLOR_ALT_FG, COLOR_ALT_BG); printf("[U+%04x]", c.codepoint); set_colors(last_color ? last_color : COLOR_FG, COLOR_BG); } else if (c.display_width == 10) { set_colors(COLOR_ALT_FG, COLOR_ALT_BG); printf("[U+%06x]", c.codepoint); set_colors(last_color ? last_color : COLOR_FG, COLOR_BG); #if 0 } else if (c.codepoint == ' ' && i == line->actual - 1) { /* Special case: space at end of line */ set_colors(COLOR_ALT_FG, COLOR_ALT_BG); printf("·"); set_colors(COLOR_FG, COLOR_BG); #endif } else if (i > 0 && is_spaces && c.codepoint == ' ' && !(i % 4)) { set_colors(COLOR_ALT_FG, COLOR_BG); /* Normal background so this is more subtle */ printf("▏"); set_colors(last_color ? last_color : COLOR_FG, COLOR_BG); } else { /* Normal characters get output */ char tmp[7]; /* Max six bytes, use 7 to ensure last is always nil */ to_eight(c.codepoint, tmp); printf("%s", tmp); } /* Advance the terminal cell offset by the render width of this character */ j += c.display_width; /* Advance to the next character */ i++; } else if (c.display_width > 1) { /* * If this is a wide character but we aren't ready to render yet, * we may need to draw some filler text for the remainder of its * width to ensure we don't jump around when horizontally scrolling * past wide characters. */ remainder = c.display_width - 1; j++; } else { /* Regular character, not ready to draw, advance without doing anything */ j++; i++; } } set_colors(COLOR_FG, COLOR_BG); /* Fill to end right hand side */ for (; j < width + offset - prompt_width_calc; ++j) { printf(" "); } /* Print right hand side */ if (show_right_side) { printf("\033[0m%s", prompt_right); } } /** * Create a line_t */ static line_t * line_create(void) { line_t * line = malloc(sizeof(line_t) + sizeof(char_t) * 32); line->available = 32; line->actual = 0; line->istate = 0; return line; } /** * Insert a character into a line */ static line_t * line_insert(line_t * line, char_t c, int offset) { /* If there is not enough space... */ if (line->actual == line->available) { /* Expand the line buffer */ line->available *= 2; line = realloc(line, sizeof(line_t) + sizeof(char_t) * line->available); } /* If this was not the last character, then shift remaining characters forward. */ if (offset < line->actual) { memmove(&line->text[offset+1], &line->text[offset], sizeof(char_t) * (line->actual - offset)); } /* Insert the new character */ line->text[offset] = c; /* There is one new character in the line */ line->actual += 1; if (!loading) { recalculate_tabs(line); recalculate_syntax(line); } return line; } /** * Update terminal size * * We don't listen for sigwinch for various reasons... */ static void get_size(void) { struct winsize w; ioctl(STDOUT_FILENO, TIOCGWINSZ, &w); full_width = w.ws_col; if (full_width - prompt_right_width - prompt_width > MINIMUM_SIZE) { show_right_side = 1; show_left_side = 1; prompt_width_calc = prompt_width; width = full_width - prompt_right_width; } else { show_right_side = 0; if (full_width - prompt_width > MINIMUM_SIZE) { show_left_side = 1; prompt_width_calc = prompt_width; } else { show_left_side = 0; prompt_width_calc = 1; } width = full_width; } } /** * Place the cursor within the line */ static void place_cursor_actual(void) { int x = prompt_width_calc + 1 - offset; for (int i = 0; i < column; ++i) { char_t * c = &the_line->text[i]; x += c->display_width; } if (x > width - 1) { /* Adjust the offset appropriately to scroll horizontally */ int diff = x - (width - 1); offset += diff; x -= diff; render_line(); } /* Same for scrolling horizontally to the left */ if (x < prompt_width_calc + 1) { int diff = (prompt_width_calc + 1) - x; offset -= diff; x += diff; render_line(); } printf("\033[?25h\033[%dG", x); fflush(stdout); } /** * Delete a character */ static void line_delete(line_t * line, int offset) { /* Can't delete character before start of line. */ if (offset == 0) return; /* If this isn't the last character, we need to move all subsequent characters backwards */ if (offset < line->actual) { memmove(&line->text[offset-1], &line->text[offset], sizeof(char_t) * (line->actual - offset)); } /* The line is one character shorter */ line->actual -= 1; if (!loading) { recalculate_tabs(line); recalculate_syntax(line); } } /** * Backspace from the cursor position */ static void delete_at_cursor(void) { if (column > 0) { if (the_line->text[column-1].codepoint == ' ') { /* Delete this space */ line_delete(the_line, column); column--; if (offset > 0) offset--; while (column > 0 && the_line->text[column-1].codepoint == ' ' && (column % 4 != 0)) { line_delete(the_line, column); column--; if (offset > 0) offset--; } } else { line_delete(the_line, column); column--; if (offset > 0) offset--; } } } /** * Delete whole word */ static void delete_word(void) { if (!the_line->actual) return; if (!column) return; do { if (column > 0) { line_delete(the_line, column); column--; if (offset > 0) offset--; } } while (column && the_line->text[column-1].codepoint != ' '); } /** * Insert at cursor position */ static void insert_char(uint32_t c) { char_t _c; _c.codepoint = c; _c.flags = 0; _c.display_width = codepoint_width(c); the_line = line_insert(the_line, _c, column); column++; } /** * Move cursor left */ static void cursor_left(void) { if (column > 0) column--; place_cursor_actual(); } /** * Move cursor right */ static void cursor_right(void) { if (column < the_line->actual) column++; place_cursor_actual(); } /** * Move cursor one whole word left */ static void word_left(void) { if (column == 0) return; column--; while (column && the_line->text[column].codepoint == ' ') { column--; } while (column > 0) { if (the_line->text[column-1].codepoint == ' ') break; column--; } place_cursor_actual(); } /** * Move cursor one whole word right */ static void word_right(void) { while (column < the_line->actual && the_line->text[column].codepoint == ' ') { column++; } while (column < the_line->actual) { column++; if (the_line->text[column].codepoint == ' ') break; } place_cursor_actual(); } /** * Move cursor to start of line */ static void cursor_home(void) { column = 0; place_cursor_actual(); } /* * Move cursor to end of line */ static void cursor_end(void) { column = the_line->actual; place_cursor_actual(); } /** * Temporary buffer for holding utf-8 data */ static char temp_buffer[1024]; /** * Cycle to previous history entry */ static void history_previous(void) { if (rline_scroll == 0) { /* Convert to temporaary buffer */ unsigned int off = 0; memset(temp_buffer, 0, sizeof(temp_buffer)); for (int j = 0; j < the_line->actual; j++) { char_t c = the_line->text[j]; off += to_eight(c.codepoint, &temp_buffer[off]); } } if (rline_scroll < rline_history_count) { rline_scroll++; /* Copy in from history */ the_line->actual = 0; column = 0; loading = 1; unsigned char * buf = (unsigned char *)rline_history_prev(rline_scroll); uint32_t istate = 0, c = 0; for (unsigned int i = 0; i < strlen((char *)buf); ++i) { if (!decode(&istate, &c, buf[i])) { insert_char(c); } } loading = 0; } /* Set cursor at end */ column = the_line->actual; offset = 0; recalculate_tabs(the_line); recalculate_syntax(the_line); render_line(); place_cursor_actual(); } /** * Cycle to next history entry */ static void history_next(void) { if (rline_scroll > 1) { rline_scroll--; /* Copy in from history */ the_line->actual = 0; column = 0; loading = 1; unsigned char * buf = (unsigned char *)rline_history_prev(rline_scroll); uint32_t istate = 0, c = 0; for (unsigned int i = 0; i < strlen((char *)buf); ++i) { if (!decode(&istate, &c, buf[i])) { insert_char(c); } } loading = 0; } else if (rline_scroll == 1) { /* Copy in from temp */ rline_scroll = 0; the_line->actual = 0; column = 0; loading = 1; char * buf = temp_buffer; uint32_t istate = 0, c = 0; for (unsigned int i = 0; i < strlen(buf); ++i) { if (!decode(&istate, &c, buf[i])) { insert_char(c); } } loading = 0; } /* Set cursor at end */ column = the_line->actual; offset = 0; recalculate_tabs(the_line); recalculate_syntax(the_line); render_line(); place_cursor_actual(); } /** * Handle escape sequences (arrow keys, etc.) */ static int handle_escape(int * this_buf, int * timeout, int c) { if (*timeout >= 1 && this_buf[*timeout-1] == '\033' && c == '\033') { this_buf[*timeout] = c; (*timeout)++; return 1; } if (*timeout >= 1 && this_buf[*timeout-1] == '\033' && c != '[') { *timeout = 0; _ungetc(c); return 1; } if (*timeout >= 1 && this_buf[*timeout-1] == '\033' && c == '[') { *timeout = 1; this_buf[*timeout] = c; (*timeout)++; return 0; } if (*timeout >= 2 && this_buf[0] == '\033' && this_buf[1] == '[' && (isdigit(c) || c == ';')) { this_buf[*timeout] = c; (*timeout)++; return 0; } if (*timeout >= 2 && this_buf[0] == '\033' && this_buf[1] == '[') { switch (c) { case 'A': // up history_previous(); break; case 'B': // down history_next(); break; case 'C': // right if (this_buf[*timeout-1] == '5') { word_right(); } else { cursor_right(); } break; case 'D': // left if (this_buf[*timeout-1] == '5') { word_left(); } else { cursor_left(); } break; case 'H': // home cursor_home(); break; case 'F': // end cursor_end(); break; case '~': switch (this_buf[*timeout-1]) { case '1': cursor_home(); break; case '3': /* Delete forward */ if (column < the_line->actual) { line_delete(the_line, column+1); if (offset > 0) offset--; } break; case '4': cursor_end(); break; } break; default: break; } *timeout = 0; return 0; } *timeout = 0; return 0; } static unsigned int _INTR, _EOF; static struct termios old; static void get_initial_termios(void) { tcgetattr(STDOUT_FILENO, &old); _INTR = old.c_cc[VINTR]; _EOF = old.c_cc[VEOF]; } static void set_unbuffered(void) { struct termios new = old; new.c_lflag &= (~ICANON & ~ECHO & ~ISIG); tcsetattr(STDOUT_FILENO, TCSAFLUSH, &new); } static void set_buffered(void) { tcsetattr(STDOUT_FILENO, TCSAFLUSH, &old); } static int tabbed; static void dummy_redraw(rline_context_t * context) { /* Do nothing */ } /** * Juggle our buffer with an rline context so we can * call original rline functions such as a tab-completion callback * or reverse search. */ static void call_rline_func(rline_callback_t func, rline_context_t * context) { /* Unicode parser state */ uint32_t istate = 0; uint32_t c; /* Don't let rline draw things */ context->quiet = 1; /* Allocate a temporary buffer */ context->buffer = malloc(buf_size_max); memset(context->buffer,0,buf_size_max); /* Convert current data to utf-8 */ unsigned int off = 0; for (int j = 0; j < the_line->actual; j++) { if (j == column) { /* Track cursor position */ context->offset = off; } char_t c = the_line->text[j]; off += to_eight(c.codepoint, &context->buffer[off]); } /* If the cursor was at the end, the loop above didn't catch it */ if (column == the_line->actual) context->offset = off; /* * Did we just press tab before this? This is actually managed * by the tab-completion function. */ context->tabbed = tabbed; /* Empty callbacks */ rline_callbacks_t tmp = {0}; /* * Because some clients expect this to be set... * (we don't need it, we'll redraw ourselves later) */ tmp.redraw_prompt = dummy_redraw; /* Setup context */ context->callbacks = &tmp; context->collected = off; context->buffer[off] = '\0'; context->requested = 1024; /* Reset colors (for tab completion candidates, etc. */ printf("\033[0m"); /* Call the function */ func(context); /* Now convert back */ loading = 1; int final_column = 0; the_line->actual = 0; column = 0; istate = 0; for (int i = 0; i < context->collected; ++i) { if (i == context->offset) { final_column = column; } if (!decode(&istate, &c, ((unsigned char *)context->buffer)[i])) { insert_char(c); } } free(context->buffer); /* Position cursor */ if (context->offset == context->collected) { column = the_line->actual; } else { column = final_column; } tabbed = context->tabbed; loading = 0; /* Recalculate + redraw */ recalculate_tabs(the_line); recalculate_syntax(the_line); render_line(); place_cursor_actual(); } char * rline_preload = NULL; /** * Perform actual interactive line editing. * * This is mostly a reimplementation of bim's * INSERT mode, but with some cleanups and fixes * to work on a single line and to add some new * key bindings we don't have in bim. */ static int read_line(void) { int cin; uint32_t c = 0; int timeout = 0; int this_buf[20]; uint32_t istate = 0; int immediate = 1; set_colors(COLOR_ALT_FG, COLOR_ALT_BG); fprintf(stdout, "◄\033[0m"); /* TODO: This could be retrieved from an envvar */ for (int i = 0; i < full_width - 1; ++i) { fprintf(stdout, " "); } if (rline_preload) { char * c = rline_preload; while (*c) { insert_char(*c); c++; } free(rline_preload); rline_preload = NULL; } render_line(); place_cursor_actual(); while ((cin = getch(immediate,timeout))) { if (cin == -1) continue; get_size(); if (!decode(&istate, &c, cin)) { if (timeout == 0) { if (c != '\t') tabbed = 0; if (_INTR && c == _INTR) { set_colors(COLOR_ALT_FG, COLOR_ALT_BG); printf("^%c", (int)('@' + c)); printf("\033[0m"); loading = 1; the_line->actual = 0; column = 0; insert_char('\n'); immediate = 0; raise(SIGINT); return 1; } if (_EOF && c == _EOF) { if (column == 0 && the_line->actual == 0) { for (char *_c = rline_exit_string; *_c; ++_c) { insert_char(*_c); } render_line(); place_cursor_actual(); if (!*rline_exit_string) { set_colors(COLOR_ALT_FG, COLOR_ALT_BG); printf("^D\033[0m"); } return 1; } else { /* Otherwise act like delete */ if (column < the_line->actual) { line_delete(the_line, column+1); if (offset > 0) offset--; immediate = 0; } continue; } } switch (c) { case '\033': if (timeout == 0) { this_buf[timeout] = c; timeout++; } break; case DELETE_KEY: case BACKSPACE_KEY: delete_at_cursor(); immediate = 0; break; case ENTER_KEY: /* Finished */ loading = 1; column = the_line->actual; render_line(); insert_char('\n'); immediate = 0; return 1; case 22: /* ^V */ /* Don't bother with unicode, just take the next byte */ place_cursor_actual(); printf("^\b"); insert_char(getc(stdin)); immediate = 0; break; case 23: /* ^W */ delete_word(); immediate = 0; break; case 12: /* ^L - Repaint the whole screen */ printf("\033[2J\033[H"); render_line(); place_cursor_actual(); break; case 11: /* ^K - Clear to end */ the_line->actual = column; immediate = 0; break; case 21: /* ^U - Kill to beginning */ while (column) { delete_at_cursor(); } immediate = 0; break; case '\t': if (column == 0 || the_line->text[column-1].codepoint == ' ') { /* Insert tab character */ insert_char(' '); insert_char(' '); insert_char(' '); insert_char(' '); immediate = 0; } else if (tab_complete_func) { /* Tab complete */ rline_context_t context = {0}; call_rline_func(tab_complete_func, &context); immediate = 0; } break; default: insert_char(c); immediate = 0; break; } } else { if (handle_escape(this_buf,&timeout,c)) { render_line(); place_cursor_actual(); continue; } immediate = 0; } render_line(); place_cursor_actual(); } else if (istate == UTF8_REJECT) { istate = 0; } } return 0; } /** * Read a line of text with interactive editing. */ int rline(char * buffer, int buf_size) { setlocale(LC_ALL, ""); get_initial_termios(); set_unbuffered(); get_size(); column = 0; offset = 0; buf_size_max = buf_size; char * theme = getenv("RLINE_THEME"); if (theme && !strcmp(theme,"sunsmoke")) { /* TODO bring back theme tables */ rline_exp_load_colorscheme_sunsmoke(); } else { rline_exp_load_colorscheme_default(); } the_line = line_create(); loading = 0; read_line(); printf("\r\033[?25h\033[0m\n"); unsigned int off = 0; for (int j = 0; j < the_line->actual; j++) { char_t c = the_line->text[j]; off += to_eight(c.codepoint, &buffer[off]); } free(the_line); set_buffered(); return strlen(buffer); }