#include #include #include #include #include #include #include #include "Console.h" #define ERROR_SIZE 256 #define HISTORY_LENGTH 4 #define OUTPUT_BUFFER_SIZE (1024 * 1024) static char error[ERROR_SIZE] = {'\0'}; static struct termios original; static ConsoleLine buffer[2]; static bool bufferIndex = 0; static int move = 0; static ConsoleLine history[HISTORY_LENGTH]; static int historyOffset = 0; static int historyIndex = 0; static int historyLength = 0; static char outputBuffer[OUTPUT_BUFFER_SIZE]; static void setError(const char* msg) { snprintf(error, ERROR_SIZE, "%s: %s", msg, strerror(errno)); } static bool getAttributes(struct termios* t) { if(tcgetattr(STDIN_FILENO, t)) { setError("cannot get terminal attributes"); return true; } return false; } static void restoreAttributes() { if(tcsetattr(STDIN_FILENO, TCSAFLUSH, &original)) { printf("cannot restore terminal attributes: %s", strerror(errno)); } } bool initConsole() { buffer[0].length = 0; buffer[1].length = 0; if(setvbuf(stdout, outputBuffer, _IOFBF, OUTPUT_BUFFER_SIZE)) { setError("cannot buffer output"); return true; } // https://viewsourcecode.org/snaptoken/kilo/02.enteringRawMode.html if(getAttributes(&original)) { return true; } struct termios raw; if(getAttributes(&raw)) { return true; } raw.c_iflag &= ~(ICRNL | IXON); raw.c_lflag &= ~(ECHO | ICANON | IEXTEN | ISIG); raw.c_cc[VMIN] = 0; raw.c_cc[VTIME] = 0; if(tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw)) { setError("cannot set terminal attributes"); return true; } atexit(restoreAttributes); return false; } const char* getConsoleError() { return error; } static bool readChar(uint8_t* c) { int bytes = read(STDIN_FILENO, c, 1); if(bytes == 0) { setError("expected part of unicode character"); return true; } else if(bytes < 0) { setError("cannot read character from terminal"); return true; } return false; } static uint32_t readUnicode2(uint8_t base) { uint8_t c = 0; if(readChar(&c)) { return 0; } return ((base & 0x1F) << 6) | (c & 0x3F); } static uint32_t readUnicode3(uint8_t base) { uint8_t c1 = 0; uint8_t c2 = 0; if(readChar(&c1) || readChar(&c2)) { return 0; } return ((base & 0x0F) << 12) | ((c1 & 0x3F) << 6) | (c2 & 0x3F); } static uint32_t readUnicode4(uint8_t base) { uint8_t c1 = 0; uint8_t c2 = 0; uint8_t c3 = 0; if(readChar(&c1) || readChar(&c2) || readChar(&c3)) { return 0; } return ((base & 0x07) << 18) | ((c1 & 0x3F) << 12) | ((c2 & 0x3F) << 6) | (c3 & 0x3F); } static uint32_t readUnicode() { uint8_t c = 0; int bytes = read(STDIN_FILENO, &c, 1); if(bytes == 0) { return 0; } else if(bytes < 0) { setError("cannot read character from terminal"); return 0; } if((c & 0x80) == 0x00) { return c; } else if((c & 0xE0) == 0xC0) { return readUnicode2(c); } else if((c & 0xF0) == 0xE0) { return readUnicode3(c); } else if((c & 0xF8) == 0xF0) { return readUnicode4(c); } setError("invalid UTF8 character"); return 0; } static void addChar(uint32_t c) { ConsoleLine* line = buffer + bufferIndex; if(line->length >= CONSOLE_BUFFER_SIZE) { return; } for(int i = 0; i < move; i++) { line->data[line->length - i] = line->data[line->length - i - 1]; } line->length++; line->data[line->length - move - 1] = c; } static void print(const char* s) { fputs(s, stdout); } static void printBuffer() { ConsoleLine* line = buffer + bufferIndex; for(int i = 0; i < line->length; i++) { uint32_t c = line->data[i]; if(c <= 0x0000007F) { putchar(c); } else if(c <= 0x000007FF) { putchar(((c >> 6) & 0x1F) | 0xC0); putchar((c & 0x3F) | 0x80); } else if(c <= 0x0000FFFF) { putchar(((c >> 12) & 0x0F) | 0xE0); putchar(((c >> 6) & 0x3F) | 0x80); putchar((c & 0x3F) | 0x80); } else if(c <= 0x0010FFFF) { putchar(((c >> 18) & 0x07) | 0xF0); putchar(((c >> 12) & 0x3F) | 0x80); putchar(((c >> 6) & 0x3F) | 0x80); putchar((c & 0x3F) | 0x80); } else { putchar('?'); } } } static void refreshLine(const char* prefix) { print(prefix); printBuffer(); if(move > 0) { printf("\33[%dD", move); } fflush(stdout); print("\33[2K\r"); } static bool clear() { move = 0; buffer[bufferIndex].length = 0; historyOffset = 0; return false; } static void addToHistory() { if(historyLength < HISTORY_LENGTH) { historyLength++; } history[historyIndex] = buffer[bufferIndex]; historyIndex = (historyIndex + 1) % HISTORY_LENGTH; } static const ConsoleLine* swapBuffers() { addToHistory(); bufferIndex = !bufferIndex; clear(); return buffer + (!bufferIndex); } static bool removeChar() { ConsoleLine* line = buffer + bufferIndex; int pos = line->length - move - 1; if(pos >= 0) { line->length--; for(int i = pos; i < line->length; i++) { line->data[i] = line->data[i + 1]; } } while(line->length - move < 0) { move--; } return false; } static bool handleControlKey(char c) { switch(c) { case 3: return clear(); case 13: return true; case 127: return removeChar(); } return false; } static void copyHistory() { buffer[bufferIndex] = history[(historyIndex - historyOffset + HISTORY_LENGTH) % HISTORY_LENGTH]; move = 0; } static void handleUpArrow() { if(historyOffset >= historyLength) { return; } historyOffset++; copyHistory(); } static void handleDownArrow() { if(historyOffset <= 1) { return; } historyOffset--; copyHistory(); } static void handleLeftArrow() { if(move < buffer[bufferIndex].length) { move++; } } static void handleRightArrow() { if(move > 0) { move--; } } static void readEscapeSequence() { if(readUnicode() != '[') { return; } switch(readUnicode()) { case 'A': handleUpArrow(); break; case 'B': handleDownArrow(); break; case 'C': handleRightArrow(); break; case 'D': handleLeftArrow(); break; } } const ConsoleLine* readConsoleLine(const char* prefix) { while(true) { uint32_t c = readUnicode(); if(c == 0) { break; } else if(c == 27) { readEscapeSequence(); } else if(iscntrl(c)) { if(handleControlKey(c)) { return swapBuffers(); } } else { addChar(c); } } refreshLine(prefix); return NULL; }