/* * Copyright (c) 2020 Calvin Rose * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to * deal in the Software without restriction, including without limitation the * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or * sell copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS * IN THE SOFTWARE. */ #if !defined(_POSIX_C_SOURCE) #define _POSIX_C_SOURCE 200112L #endif #ifndef JANET_AMALG #include "line.h" #endif static JANET_THREAD_LOCAL JanetTable *gbl_complete_env; /* Common */ Janet janet_line_getter(int32_t argc, Janet *argv) { janet_arity(argc, 0, 3); const char *str = (argc >= 1) ? (const char *) janet_getstring(argv, 0) : ""; JanetBuffer *buf = (argc >= 2) ? janet_getbuffer(argv, 1) : janet_buffer(10); gbl_complete_env = (argc >= 3) ? janet_gettable(argv, 2) : janet_current_fiber()->env; janet_line_get(str, buf); gbl_complete_env = NULL; return janet_wrap_buffer(buf); } static void simpleline(JanetBuffer *buffer) { FILE *in = janet_dynfile("in", stdin); buffer->count = 0; int c; for (;;) { c = fgetc(in); if (feof(in) || c < 0) { break; } janet_buffer_push_u8(buffer, (uint8_t) c); if (c == '\n') break; } } /* Windows */ #ifdef JANET_WINDOWS void janet_line_init() { ; } void janet_line_deinit() { ; } void janet_line_get(const char *p, JanetBuffer *buffer) { FILE *out = janet_dynfile("out", stdout); fputs(p, out); fflush(out); simpleline(buffer); } /* Posix */ #else /* https://github.com/antirez/linenoise/blob/master/linenoise.c */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include /* static state */ #define JANET_LINE_MAX 1024 #define JANET_HISTORY_MAX 100 static JANET_THREAD_LOCAL int gbl_israwmode = 0; static JANET_THREAD_LOCAL const char *gbl_prompt = "> "; static JANET_THREAD_LOCAL int gbl_plen = 2; static JANET_THREAD_LOCAL char gbl_buf[JANET_LINE_MAX]; static JANET_THREAD_LOCAL int gbl_len = 0; static JANET_THREAD_LOCAL int gbl_pos = 0; static JANET_THREAD_LOCAL int gbl_cols = 80; static JANET_THREAD_LOCAL char *gbl_history[JANET_HISTORY_MAX]; static JANET_THREAD_LOCAL int gbl_history_count = 0; static JANET_THREAD_LOCAL int gbl_historyi = 0; static JANET_THREAD_LOCAL int gbl_sigint_flag = 0; static JANET_THREAD_LOCAL struct termios gbl_termios_start; /* Unsupported terminal list from linenoise */ static const char *badterms[] = { "cons25", "dumb", "emacs", NULL }; static char *sdup(const char *s) { size_t len = strlen(s) + 1; char *mem = malloc(len); if (!mem) { return NULL; } return memcpy(mem, s, len); } /* Ansi terminal raw mode */ static int rawmode(void) { struct termios t; if (!isatty(STDIN_FILENO)) goto fatal; if (tcgetattr(STDIN_FILENO, &gbl_termios_start) == -1) goto fatal; t = gbl_termios_start; t.c_iflag &= ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON); t.c_cflag |= (CS8); t.c_lflag &= ~(ECHO | ICANON | IEXTEN | ISIG); t.c_cc[VMIN] = 1; t.c_cc[VTIME] = 0; if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &t) < 0) goto fatal; gbl_israwmode = 1; return 0; fatal: errno = ENOTTY; return -1; } /* Disable raw mode */ static void norawmode(void) { if (gbl_israwmode && tcsetattr(STDIN_FILENO, TCSAFLUSH, &gbl_termios_start) != -1) gbl_israwmode = 0; } static int curpos(void) { char buf[32]; int cols, rows; unsigned int i = 0; if (write(STDOUT_FILENO, "\x1b[6n", 4) != 4) return -1; while (i < sizeof(buf) - 1) { if (read(STDIN_FILENO, buf + i, 1) != 1) break; if (buf[i] == 'R') break; i++; } buf[i] = '\0'; if (buf[0] != 27 || buf[1] != '[') return -1; if (sscanf(buf + 2, "%d;%d", &rows, &cols) != 2) return -1; return cols; } static int getcols(void) { struct winsize ws; if (ioctl(1, TIOCGWINSZ, &ws) == -1 || ws.ws_col == 0) { int start, cols; start = curpos(); if (start == -1) goto failed; if (write(STDOUT_FILENO, "\x1b[999C", 6) != 6) goto failed; cols = curpos(); if (cols == -1) goto failed; if (cols > start) { char seq[32]; snprintf(seq, 32, "\x1b[%dD", cols - start); if (write(STDOUT_FILENO, seq, strlen(seq)) == -1) { exit(1); } } return cols; } else { return ws.ws_col; } failed: return 80; } static void clear(void) { if (write(STDOUT_FILENO, "\x1b[H\x1b[2J", 7) <= 0) { exit(1); } } static void refresh(void) { char seq[64]; JanetBuffer b; /* Keep cursor position on screen */ char *_buf = gbl_buf; int _len = gbl_len; int _pos = gbl_pos; while ((gbl_plen + _pos) >= gbl_cols) { _buf++; _len--; _pos--; } while ((gbl_plen + _len) > gbl_cols) { _len--; } janet_buffer_init(&b, 0); /* Cursor to left edge, gbl_prompt and buffer */ janet_buffer_push_u8(&b, '\r'); janet_buffer_push_cstring(&b, gbl_prompt); janet_buffer_push_bytes(&b, (uint8_t *) _buf, _len); /* Erase to right */ janet_buffer_push_cstring(&b, "\x1b[0K"); /* Move cursor to original position. */ snprintf(seq, 64, "\r\x1b[%dC", (int)(_pos + gbl_plen)); janet_buffer_push_cstring(&b, seq); if (write(STDOUT_FILENO, b.data, b.count) == -1) { exit(1); } janet_buffer_deinit(&b); } static int insert(char c, int draw) { if (gbl_len < JANET_LINE_MAX - 1) { if (gbl_len == gbl_pos) { gbl_buf[gbl_pos++] = c; gbl_buf[++gbl_len] = '\0'; if (draw) { if (gbl_plen + gbl_len < gbl_cols) { /* Avoid a full update of the line in the * trivial case. */ if (write(STDOUT_FILENO, &c, 1) == -1) return -1; } else { refresh(); } } } else { memmove(gbl_buf + gbl_pos + 1, gbl_buf + gbl_pos, gbl_len - gbl_pos); gbl_buf[gbl_pos++] = c; gbl_buf[++gbl_len] = '\0'; if (draw) refresh(); } } return 0; } static void historymove(int delta) { if (gbl_history_count > 1) { free(gbl_history[gbl_historyi]); gbl_history[gbl_historyi] = sdup(gbl_buf); gbl_historyi += delta; if (gbl_historyi < 0) { gbl_historyi = 0; return; } else if (gbl_historyi >= gbl_history_count) { gbl_historyi = gbl_history_count - 1; return; } strncpy(gbl_buf, gbl_history[gbl_historyi], JANET_LINE_MAX - 1); gbl_pos = gbl_len = strlen(gbl_buf); gbl_buf[gbl_len] = '\0'; refresh(); } } static void addhistory(void) { int i, len; char *newline = sdup(gbl_buf); if (!newline) return; len = gbl_history_count; if (len < JANET_HISTORY_MAX) { gbl_history[gbl_history_count++] = newline; len++; } else { free(gbl_history[JANET_HISTORY_MAX - 1]); } for (i = len - 1; i > 0; i--) { gbl_history[i] = gbl_history[i - 1]; } gbl_history[0] = newline; } static void replacehistory(void) { char *newline = sdup(gbl_buf); if (!newline) return; free(gbl_history[0]); gbl_history[0] = newline; } static void kleft(void) { if (gbl_pos > 0) { gbl_pos--; refresh(); } } static void kright(void) { if (gbl_pos != gbl_len) { gbl_pos++; refresh(); } } static void kbackspace(void) { if (gbl_pos > 0) { memmove(gbl_buf + gbl_pos - 1, gbl_buf + gbl_pos, gbl_len - gbl_pos); gbl_pos--; gbl_buf[--gbl_len] = '\0'; refresh(); } } static void kdelete(void) { if (gbl_pos != gbl_len) { memmove(gbl_buf + gbl_pos, gbl_buf + gbl_pos + 1, gbl_len - gbl_pos); gbl_buf[--gbl_len] = '\0'; refresh(); } } /* See tools/symchargen.c */ static int is_symbol_char_gen(uint8_t c) { if (c & 0x80) return 1; if (c >= 'a' && c <= 'z') return 1; if (c >= 'A' && c <= 'Z') return 1; if (c >= '0' && c <= '9') return 1; return (c == '!' || c == '$' || c == '%' || c == '&' || c == '*' || c == '+' || c == '-' || c == '.' || c == '/' || c == ':' || c == '<' || c == '?' || c == '=' || c == '>' || c == '@' || c == '^' || c == '_'); } /* TODO - check against special forms and print in alphabetical order */ static void kshowcomp(void) { JanetTable *env = gbl_complete_env; /* Calculate current partial symbol. Maybe we could actually hook up the Janet * parser here...*/ int i; int32_t sym_len = 0; for (i = gbl_pos - 1; i >= 0; i--) { uint8_t c = (uint8_t) gbl_buf[i]; if (!is_symbol_char_gen(c)) break; sym_len++; } char *sym_start = gbl_buf + i + 1; if (sym_len == 0) return; int32_t common_prefix_len = 0; int32_t max_test_len = 0; int32_t match_count = 0; const uint8_t *first_match = NULL; /* First pass, find match count and common prefix */ while (NULL != env) { JanetKV *kvend = env->data + env->capacity; for (JanetKV *kv = env->data; kv < kvend; kv++) { if (janet_checktype(kv->key, JANET_NIL)) continue; const uint8_t *test_str; int32_t test_len; if (!janet_bytes_view(kv->key, &test_str, &test_len)) continue; if (test_len <= sym_len) continue; if (strncmp((char *)test_str, sym_start, sym_len)) continue; /* Found a prefix match */ match_count++; if (NULL == first_match) { common_prefix_len = max_test_len = test_len; first_match = test_str; } else { if (max_test_len < test_len) max_test_len = test_len; for (int32_t i = 0; i < common_prefix_len; i++) { if (first_match[i] != test_str[i]) { common_prefix_len = i; break; } } } } env = env->proto; } /* Complete common_prefix_len - sym_len characters */ for (int i = sym_len; i < common_prefix_len; i++) { insert(first_match[i], 0); } int num_cols = getcols(); norawmode(); /* Second pass, print */ int col_width = max_test_len + 4; int cols = num_cols / col_width; if (cols == 0) cols = 1; int current_col = 0; env = gbl_complete_env; while (NULL != env) { JanetKV *kvend = env->data + env->capacity; for (JanetKV *kv = env->data; kv < kvend; kv++) { if (janet_checktype(kv->key, JANET_NIL)) continue; const uint8_t *test_str; int32_t test_len; if (!janet_bytes_view(kv->key, &test_str, &test_len)) continue; if (test_len <= sym_len) continue; if (strncmp((char *)test_str, sym_start, sym_len)) continue; /* Found a prefix match */ if (current_col == 0) printf("\n"); printf("%-*s", col_width, test_str); current_col = (current_col + 1) % cols; } env = env->proto; } printf("\n"); rawmode(); } static int line() { gbl_cols = getcols(); gbl_plen = 0; gbl_len = 0; gbl_pos = 0; while (gbl_prompt[gbl_plen]) gbl_plen++; gbl_buf[0] = '\0'; addhistory(); if (write(STDOUT_FILENO, gbl_prompt, gbl_plen) == -1) return -1; for (;;) { char c; int nread; char seq[3]; nread = read(STDIN_FILENO, &c, 1); if (nread <= 0) return -1; switch (c) { default: if (insert(c, 1)) return -1; break; case 9: /* tab */ kshowcomp(); refresh(); break; case 13: /* enter */ return 0; case 3: /* ctrl-c */ errno = EAGAIN; gbl_sigint_flag = 1; return -1; case 127: /* backspace */ case 8: /* ctrl-h */ kbackspace(); break; case 4: /* ctrl-d, eof */ return -1; case 1: /* ctrl-a */ gbl_pos = 0; refresh(); break; case 2: /* ctrl-b */ kleft(); break; case 5: /* ctrl-e */ gbl_pos = gbl_len; refresh(); break; case 6: /* ctrl-f */ kright(); break; case 21: gbl_buf[0] = '\0'; gbl_pos = gbl_len = 0; refresh(); break; case 26: /* ctrl-z */ norawmode(); kill(getpid(), SIGSTOP); rawmode(); refresh(); break; case 12: clear(); refresh(); break; case 27: /* escape sequence */ /* Read the next two bytes representing the escape sequence. * Use two calls to handle slow terminals returning the two * chars at different times. */ if (read(STDIN_FILENO, seq, 1) == -1) break; if (read(STDIN_FILENO, seq + 1, 1) == -1) break; if (seq[0] == '[') { if (seq[1] >= '0' && seq[1] <= '9') { /* Extended escape, read additional byte. */ if (read(STDIN_FILENO, seq + 2, 1) == -1) break; if (seq[2] == '~') { switch (seq[1]) { case '3': /* delete */ kdelete(); break; default: break; } } } else { switch (seq[1]) { default: break; case 'A': historymove(1); break; case 'B': historymove(-1); break; case 'C': /* Right */ kright(); break; case 'D': /* Left */ kleft(); break; case 'H': gbl_pos = 0; refresh(); break; case 'F': gbl_pos = gbl_len; refresh(); break; } } } else if (seq[0] == 'O') { switch (seq[1]) { default: break; case 'H': gbl_pos = 0; refresh(); break; case 'F': gbl_pos = gbl_len; refresh(); break; } } break; } } return 0; } void janet_line_init() { ; } void janet_line_deinit() { int i; norawmode(); for (i = 0; i < gbl_history_count; i++) free(gbl_history[i]); gbl_historyi = 0; } static int checktermsupport() { const char *t = getenv("TERM"); int i; if (!t) return 1; for (i = 0; badterms[i]; i++) if (!strcmp(t, badterms[i])) return 0; return 1; } void janet_line_get(const char *p, JanetBuffer *buffer) { gbl_prompt = p; buffer->count = 0; gbl_historyi = 0; FILE *out = janet_dynfile("out", stdout); if (!isatty(STDIN_FILENO) || !checktermsupport()) { simpleline(buffer); return; } if (rawmode()) { simpleline(buffer); return; } if (line()) { norawmode(); if (gbl_sigint_flag) { raise(SIGINT); } else { fputc('\n', out); } return; } fflush(stdin); norawmode(); fputc('\n', out); janet_buffer_ensure(buffer, gbl_len + 1, 2); memcpy(buffer->data, gbl_buf, gbl_len); buffer->data[gbl_len] = '\n'; buffer->count = gbl_len + 1; replacehistory(); } #endif