From c5d91fc44821b839e1ce5c868aae6bfc169cc3ee Mon Sep 17 00:00:00 2001 From: Lior Halphon Date: Thu, 9 Sep 2021 00:13:09 +0300 Subject: [PATCH] New console readline-like interface for the SDL port --- Makefile | 4 +- SDL/console.c | 998 ++++++++++++++++++++++++++++++++++++++++++++++ SDL/console.h | 49 +++ SDL/main.c | 92 ++++- Windows/pthread.h | 86 ++++ Windows/unistd.h | 1 + 6 files changed, 1225 insertions(+), 5 deletions(-) create mode 100644 SDL/console.c create mode 100644 SDL/console.h create mode 100644 Windows/pthread.h diff --git a/Makefile b/Makefile index f6e4c4c..7bfe580 100644 --- a/Makefile +++ b/Makefile @@ -128,10 +128,10 @@ endif ifeq (,$(PKG_CONFIG)) SDL_CFLAGS := $(shell sdl2-config --cflags) -SDL_LDFLAGS := $(shell sdl2-config --libs) +SDL_LDFLAGS := $(shell sdl2-config --libs) -lpthread else SDL_CFLAGS := $(shell $(PKG_CONFIG) --cflags sdl2) -SDL_LDFLAGS := $(shell $(PKG_CONFIG) --libs sdl2) +SDL_LDFLAGS := $(shell $(PKG_CONFIG) --libs sdl2) -lpthread endif ifeq (,$(PKG_CONFIG)) GL_LDFLAGS := -lGL diff --git a/SDL/console.c b/SDL/console.c new file mode 100644 index 0000000..bacdc3a --- /dev/null +++ b/SDL/console.c @@ -0,0 +1,998 @@ +#include "console.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define ESC(x) "\x1B" x +#define CSI(x) ESC("[" x) +#define SGR(x) CSI(x "m") + +static bool initialized = false; +typedef struct listent_s listent_t; + +struct listent_s { + listent_t *prev; + listent_t *next; + char content[]; +}; + +typedef struct { + listent_t *first; + listent_t *last; +} fifo_t; + +static fifo_t lines; +static fifo_t history; + +static void remove_entry(fifo_t *fifo, listent_t *entry) +{ + if (fifo->last == entry) { + fifo->last = entry->prev; + } + if (fifo->first == entry) { + fifo->first = entry->next; + } + if (entry->next) { + entry->next->prev = entry->prev; + } + if (entry->prev) { + entry->prev->next = entry->next; + } + free(entry); +} + +static void add_entry(fifo_t *fifo, const char *content) +{ + size_t length = strlen(content); + listent_t *entry = malloc(sizeof(*entry) + length + 1); + entry->next = NULL; + entry->prev = fifo->last; + memcpy(entry->content, content, length); + entry->content[length] = 0; + if (fifo->last) { + fifo->last->next = entry; + } + fifo->last = entry; + if (!fifo->first) { + fifo->first = entry; + } +} + +static listent_t *reverse_find(listent_t *entry, const char *string, bool exact) +{ + while (entry) { + if (exact && strcmp(entry->content, string) == 0) { + return entry; + } + if (!exact && strstr(entry->content, string)) { + return entry; + } + entry = entry->prev; + } + return NULL; +} + +static bool is_term(void) +{ +#ifdef _WIN32 + if (AllocConsole()) { + FreeConsole(); + return false; + } +#endif + return isatty(STDIN_FILENO) && isatty(STDOUT_FILENO) +#ifndef _WIN32 + && getenv("TERM") +#endif + ; +} + +static unsigned width, height; + +static char raw_getc(void) +{ +#ifdef _WIN32 + char c; + unsigned long ret; + ReadConsole(GetStdHandle(STD_INPUT_HANDLE), &c, 1, &ret, NULL); +#else + ssize_t ret; + char c; + + do { + ret = read(STDIN_FILENO, &c, 1); + } while (ret == -1 && errno == EINTR); +#endif + return ret == 1? c : EOF; +} + +#ifdef _WIN32 +#pragma clang diagnostic ignored "-Wmacro-redefined" +#include + +static void update_size(void) +{ + CONSOLE_SCREEN_BUFFER_INFO csbi; + GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &csbi); + width = csbi.srWindow.Right - csbi.srWindow.Left + 1; + height = csbi.srWindow.Bottom - csbi.srWindow.Top + 1; +} + +static unsigned long input_mode, output_mode; + +static void cleanup(void) +{ + printf(CSI("!p")); // reset + SetConsoleMode(GetStdHandle(STD_INPUT_HANDLE), input_mode); + SetConsoleMode(GetStdHandle(STD_OUTPUT_HANDLE), output_mode); + fflush(stdout); +} + +static bool initialize(void) +{ + if (!is_term()) return false; + update_size(); + if (width == 0 || height == 0) { + return false; + } + + static bool once = false; + if (!once) { + atexit(cleanup); + GetConsoleMode(GetStdHandle(STD_INPUT_HANDLE), &input_mode); + GetConsoleMode(GetStdHandle(STD_OUTPUT_HANDLE), &output_mode); + once = true; + } + SetConsoleMode(GetStdHandle(STD_INPUT_HANDLE), ENABLE_VIRTUAL_TERMINAL_INPUT); + SetConsoleMode(GetStdHandle(STD_OUTPUT_HANDLE), ENABLE_WRAP_AT_EOL_OUTPUT | ENABLE_PROCESSED_OUTPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING); + + printf(CSI("%dB") "\n" CSI("A") ESC("7") CSI("B"), height); + + fflush(stdout); + initialized = true; + return true; +} +#else +#include + +static void update_size(void) +{ + struct winsize winsize; + ioctl(STDOUT_FILENO, TIOCGWINSZ, &winsize); + width = winsize.ws_col; + height = winsize.ws_row; +} + +static void terminal_resized(int ignored) +{ + update_size(); +} + +#include +static struct termios terminal; + + +static void cleanup(void) +{ + printf(CSI("!p")); // reset + tcsetattr(STDIN_FILENO, TCSAFLUSH, &terminal); + fflush(stdout); +} + +static bool initialize(void) +{ + if (!is_term()) return false; + update_size(); + if (width == 0 || height == 0) { + return false; + } + + static bool once = false; + if (!once) { + atexit(cleanup); + signal(SIGWINCH, terminal_resized); + tcgetattr(STDIN_FILENO, &terminal); +#ifdef _WIN32 + _setmode(STDIN_FILENO, _O_TEXT); +#endif + once = true; + } + struct termios raw_terminal; + raw_terminal = terminal; + raw_terminal.c_lflag = 0; + tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw_terminal); + + printf(CSI("%dB") "\n" CSI("A") ESC("7") CSI("B"), height); + + fflush(stdout); + initialized = true; + return true; +} +#endif + +static struct { + char *content; + size_t allocation_size; + size_t length; + size_t position; + size_t scroll; + bool reverse_search; + listent_t *search_line; +} line; + +#define CTL(x) ((x) - 'A' + 1) + +static const char *prompt = ""; +static size_t prompt_length = 0; +static bool repeat_empty = false; + +static bool redraw_prompt(bool force) +{ + if (line.reverse_search) { + if (!force) return false; + if (line.length == 0) { + printf("\r" CSI("K") "%s" SGR("2") "Reverse Search..." SGR("0") CSI("%zuG"), prompt, prompt_length + 1); + return true; + } + if (!line.search_line) { + printf("\r" CSI("K") "%s" SGR("1") "%s" SGR("0"), prompt, line.content); + return true; + } + const char *loc = strstr(line.search_line->content, line.content); + printf("\r" CSI("K") "%s" "%.*s" SGR("1") "%s" SGR("0") "%s" CSI("%uG"), + prompt, + (int)(loc - line.search_line->content), + line.search_line->content, + line.content, + loc + line.length, + (unsigned)(loc - line.search_line->content + line.length + prompt_length + 1)); + return true; + } + + size_t max = width - 1 - prompt_length; + + if (line.scroll && line.length <= max) { + line.scroll = 0; + force = true; + } + + if (line.scroll > line.length - max) { + line.scroll = line.length - max; + force = true; + } + + if (line.position < line.scroll + 1 && line.position) { + line.scroll = line.position - 1; + force = true; + } + + if (line.position == 0 && line.scroll) { + line.scroll = 0; + force = true; + } + + if (line.position > line.scroll + max) { + line.scroll = line.position - max; + force = true; + } + + if (!force && line.length <= max) { + return false; + } + + if (line.length <= max) { + printf("\r" CSI("K") "%s%s" CSI("%uG"), prompt, line.content, (unsigned)(line.position + prompt_length + 1)); + return true; + } + + size_t left = max; + const char *string = line.content + line.scroll; + printf("\r" CSI("K") "%s", prompt); + if (line.scroll) { + printf(SGR("2") "%c" SGR("0"), *string); + string++; + left--; + } + if (line.scroll + max == line.length) { + printf("%s", string); + } + else { + printf("%.*s", (int)(left - 1), string); + string += left; + left = 1; + printf(SGR("2") "%c" SGR("0"), *string); + } + printf(CSI("%uG"), (unsigned)(line.position - line.scroll + prompt_length + 1)); + + return true; +} + +static void set_position(size_t position) +{ + if (position > line.length) { + printf("\a"); + return; + } + line.position = position; + if (!redraw_prompt(false)) { + printf(CSI("%uG"), (unsigned)(position + prompt_length + 1)); + } +} + +static void set_line(const char *content) +{ + line.length = strlen(content); + if (line.length + 1 > line.allocation_size) { + line.content = realloc(line.content, line.length + 1); + line.allocation_size = line.length + 1; + } + else if (line.allocation_size > 256 && line.length < 128) { + line.content = realloc(line.content, line.length + 1); + line.allocation_size = line.length + 1; + } + line.position = line.length; + strcpy(line.content, content); + redraw_prompt(true); +} + +static void insert(const char *string) +{ + size_t insertion_length = strlen(string); + size_t new_length = insertion_length + line.length; + bool need_realloc = false; + while (line.allocation_size < new_length + 1) { + line.allocation_size *= 2; + need_realloc = true; + } + if (need_realloc) { + line.content = realloc(line.content, line.allocation_size); + } + memmove(line.content + line.position + insertion_length, + line.content + line.position, + line.length - line.position); + memcpy(line.content + line.position, string, insertion_length); + line.position += insertion_length; + line.content[new_length] = 0; + line.length = new_length; + if (!redraw_prompt(line.position != line.length)) { + printf("%s", string); + } +} + +static void delete(size_t size, bool forward) +{ + if (line.length < size) { + printf("\a"); + return; + } + if (forward) { + if (line.position > line.length - size) { + printf("\a"); + return; + } + else { + line.position += size; + } + } + else if (line.position < size) { + printf("\a"); + return; + } + memmove(line.content + line.position - size, + line.content + line.position, + line.length - line.position); + line.length -= size; + line.content[line.length] = 0; + line.position -= size; + + if (!redraw_prompt(line.position != line.length)) { + printf(CSI("%uG") CSI("K"), + (unsigned)(line.position + prompt_length + 1)); + } +} + +static void move_word(bool forward) +{ + signed offset = forward? 1 : -1; + size_t end = forward? line.length : 0; + signed check_offset = forward? 0 : -1; + if (line.position == end) { + printf("\a"); + return; + } + line.position += offset; + while (line.position != end && isalnum(line.content[line.position + check_offset])) { + line.position += offset; + } + if (!redraw_prompt(false)) { + printf(CSI("%uG"), (unsigned)(line.position + prompt_length + 1)); + } +} + +static void delete_word(bool forward) +{ + size_t original_pos = line.position; + signed offset = forward? 1 : -1; + size_t end = forward? line.length : 0; + signed check_offset = forward? 0 : -1; + if (line.position == end) { + printf("\a"); + return; + } + line.position += offset; + while (line.position != end && isalnum(line.content[line.position + check_offset])) { + line.position += offset; + } + if (forward) { + delete(line.position - original_pos, false); + } + else { + delete(original_pos - line.position, true); + } +} + +#define MOD_ALT(x) (0x100 | x) +#define MOD_SHIFT(x) (0x200 | x) +#define MOD_CTRL(x) (0x400 | x) +#define MOD_SPECIAL(x) (0x800 | x) + +static unsigned get_extended_key(void) +{ + unsigned modifiers = 0; + char c = 0; +restart: + c = raw_getc(); + if (c == 0x1B) { + modifiers = MOD_SHIFT(MOD_ALT(0)); + goto restart; + } + else if (c != '[' && c != 'O') { + return MOD_ALT(c); + } + unsigned ret = 0; + while (true) { + c = raw_getc(); + if (c >= '0' && c <= '9') { + ret = ret * 10 + c - '0'; + } + else if (c == ';') { + if (ret == 1) { + modifiers |= MOD_ALT(0); + } + else if (ret == 2) { + modifiers |= MOD_SHIFT(0); + } + else if (ret == 5) { + modifiers |= MOD_CTRL(0); + } + ret = 0; + } + else if (c == '~') { + return MOD_SPECIAL(ret) | modifiers; + } + else { + if (ret == 1) { + modifiers |= MOD_ALT(0); + } + else if (ret == 2) { + modifiers |= MOD_SHIFT(0); + } + else if (ret == 5) { + modifiers |= MOD_CTRL(0); + } + return c | modifiers; + } + } +} + +#define SWAP(x, y) do {typeof(*(x)) _tmp = *(x); *(x) = *(y);*(y) = _tmp;} while (0) +static pthread_mutex_t terminal_lock = PTHREAD_MUTEX_INITIALIZER; +static pthread_mutex_t lines_lock = PTHREAD_MUTEX_INITIALIZER; +static pthread_cond_t lines_cond = PTHREAD_COND_INITIALIZER; + +static char reverse_search_mainloop(void) +{ + while (true) { + char c = raw_getc(); + pthread_mutex_lock(&terminal_lock); + + switch (c) { + case CTL('C'): + line.search_line = NULL; + set_line(""); + pthread_mutex_unlock(&terminal_lock); + return CTL('A'); + case CTL('R'): + line.search_line = reverse_find(line.search_line? line.search_line->prev : history.last, line.content, false); + if (!line.search_line) { + printf("\a"); + } + redraw_prompt(true); + break; + case CTL('W'): + delete_word(false); + redraw_prompt(true); + break; +#ifndef _WIN32 + case CTL('Z'): + set_line(""); + raise(SIGSTOP); + initialize(); // Reinitialize + redraw_prompt(true); + break; +#endif + case CTL('H'): + case 0x7F: // Backspace + delete(1, false); + redraw_prompt(true); + break; + default: + if (c >= ' ') { + char string[2] = {c, 0}; + insert(string); + line.search_line = reverse_find(line.search_line?: history.last, line.content, false); + if (!line.search_line) { + printf("\a"); + } + redraw_prompt(true); + } + else { + pthread_mutex_unlock(&terminal_lock); + return c; + } + break; + } + pthread_mutex_unlock(&terminal_lock); + fflush(stdout); + } + +} + + +static +#ifdef _WIN32 +int __stdcall +#else +void * +#endif +mainloop(char *(*completer)(const char *substring, uintptr_t *context)) +{ + listent_t *history_line = NULL; + uintptr_t complete_context = 0; + size_t completion_length = 0; + while (true) { + char c; + if (line.reverse_search) { + c = reverse_search_mainloop(); + line.reverse_search = false; + if (line.search_line) { + size_t pos = strstr(line.search_line->content, line.content) - line.search_line->content + line.length; + set_line(line.search_line->content); + line.search_line = NULL; + set_position(pos); + } + else { + redraw_prompt(true); + } + } + else { + c = raw_getc(); + } + pthread_mutex_lock(&terminal_lock); + + switch (c) { + case CTL('A'): + set_position(0); + complete_context = completion_length = 0; + break; + case CTL('B'): + set_position(line.position - 1); + complete_context = completion_length = 0; + break; + case CTL('C'): + if (line.length) { + set_line(""); + history_line = NULL; + complete_context = completion_length = 0; + } + else { +#ifdef _WIN32 + raise(SIGINT); +#else + kill(getpid(), SIGINT); +#endif + } + break; + case CTL('D'): + if (line.length) { + delete(1, true); + complete_context = completion_length = 0; + } + else { + pthread_mutex_lock(&lines_lock); + add_entry(&lines, CON_EOF); + pthread_cond_signal(&lines_cond); + pthread_mutex_unlock(&lines_lock); + } + break; + case CTL('E'): + set_position(line.length); + complete_context = completion_length = 0; + break; + case CTL('F'): + set_position(line.position + 1); + complete_context = completion_length = 0; + break; + case CTL('K'): + printf(CSI("K")); + if (!redraw_prompt(false)) { + line.length = line.position; + line.content[line.length] = 0; + } + complete_context = completion_length = 0; + break; + case CTL('R'): + complete_context = completion_length = 0; + line.reverse_search = true; + set_line(""); + + break; + case CTL('T'): + if (line.length < 2) { + printf("\a"); + break; + } + if (line.position && line.position == line.length) { + line.position--; + } + if (line.position == 0) { + printf("\a"); + break; + } + SWAP(line.content + line.position, + line.content + line.position - 1); + line.position++; + redraw_prompt(true); + complete_context = completion_length = 0; + break; + case CTL('W'): + delete_word(false); + complete_context = completion_length = 0; + break; +#ifndef _WIN32 + case CTL('Z'): + set_line(""); + complete_context = completion_length = 0; + raise(SIGSTOP); + initialize(); // Reinitialize + break; +#endif + case '\r': + case '\n': + pthread_mutex_lock(&lines_lock); + if (line.length == 0 && repeat_empty && history.last) { + add_entry(&lines, history.last->content); + } + else { + add_entry(&lines, line.content); + } + pthread_cond_signal(&lines_cond); + pthread_mutex_unlock(&lines_lock); + if (line.length) { + listent_t *dup = reverse_find(history.last, line.content, true); + if (dup) { + remove_entry(&history, dup); + } + add_entry(&history, line.content); + set_line(""); + history_line = NULL; + } + complete_context = completion_length = 0; + break; + case CTL('H'): + case 0x7F: // Backspace + delete(1, false); + complete_context = completion_length = 0; + break; + case 0x1B: + switch (get_extended_key()) { + case MOD_SPECIAL(1): // Home + case MOD_SPECIAL(7): + case 'H': + set_position(0); + complete_context = completion_length = 0; + break; + case MOD_SPECIAL(8): // End + case 'F': + set_position(line.length); + complete_context = completion_length = 0; + break; + case MOD_SPECIAL(3): // Delete + delete(1, true); + complete_context = completion_length = 0; + break; + case 'A': // Up + if (!history_line) { + history_line = history.last; + } + else { + history_line = history_line->prev; + } + if (history_line) { + set_line(history_line->content); + complete_context = completion_length = 0; + } + else { + history_line = history.first; + printf("\a"); + } + + break; + case 'B': // Down + if (!history_line) { + printf("\a"); + break; + } + history_line = history_line->next; + if (history_line) { + set_line(history_line->content); + complete_context = completion_length = 0; + } + else { + set_line(""); + complete_context = completion_length = 0; + } + break; + case 'C': // Right + set_position(line.position + 1); + complete_context = completion_length = 0; + break; + case 'D': // Left + set_position(line.position - 1); + complete_context = completion_length = 0; + break; + case MOD_ALT('b'): + case MOD_ALT('D'): + move_word(false); + complete_context = completion_length = 0; + break; + case MOD_ALT('f'): + case MOD_ALT('C'): + move_word(true); + complete_context = completion_length = 0; + break; + case MOD_ALT(0x7f): // ALT+Backspace + delete_word(false); + complete_context = completion_length = 0; + break; + case MOD_ALT('('): // ALT+Delete + delete_word(true); + complete_context = completion_length = 0; + break; + default: + printf("Unsupported extended key %x\n", c); + printf("\a"); + break; + } + break; + case '\t': { + char temp = line.content[line.position - completion_length]; + line.content[line.position - completion_length] = 0; + char *completion = completer? completer(line.content, &complete_context) : NULL; + line.content[line.position - completion_length] = temp; + if (completion) { + if (completion_length) { + delete(completion_length, false); + } + insert(completion); + completion_length = strlen(completion); + free(completion); + } + else { + printf("\a"); + } + break; + } + default: + if (c >= ' ') { + char string[2] = {c, 0}; + insert(string); + complete_context = completion_length = 0; + } + else { + printf("Unsupported key %x\n", c); + printf("\a"); + } + break; + } + fflush(stdout); + pthread_mutex_unlock(&terminal_lock); + } + return 0; +} + +char *CON_readline(const char *new_prompt) +{ + pthread_mutex_lock(&terminal_lock); + const char *old_prompt = prompt; + prompt = new_prompt; + prompt_length = strlen(prompt); + redraw_prompt(true); + fflush(stdout); + pthread_mutex_unlock(&terminal_lock); + + pthread_mutex_lock(&lines_lock); + while (!lines.first) { + pthread_cond_wait(&lines_cond, &lines_lock); + } + char *ret = strdup(lines.first->content); + remove_entry(&lines, lines.first); + pthread_mutex_unlock(&lines_lock); + + pthread_mutex_lock(&terminal_lock); + prompt = old_prompt; + prompt_length = strlen(prompt); + redraw_prompt(true); + fflush(stdout); + pthread_mutex_unlock(&terminal_lock); + return ret; +} + +char *CON_readline_async(void) +{ + char *ret = NULL; + pthread_mutex_lock(&lines_lock); + if (lines.first) { + ret = strdup(lines.first->content); + remove_entry(&lines, lines.first); + } + pthread_mutex_unlock(&lines_lock); + return ret; +} + +bool CON_start(char *(*completer)(const char *substring, uintptr_t *context)) +{ + if (!initialize()) { + return false; + } + set_line(""); + pthread_t thread; + return pthread_create(&thread, NULL, (void *)mainloop, completer) == 0; +} + +void CON_attributed_print(const char *string, CON_attributes_t *attributes) +{ + if (!initialized) { + printf("%s", string); + return; + } + static bool pending_newline = false; + pthread_mutex_lock(&terminal_lock); + printf(ESC("8")); + bool needs_reset = false; + if (attributes) { + if (attributes->color) { + if (attributes->color >= 0x10) { + printf(SGR("%d"), attributes->color - 0x11 + 90); + } + else { + printf(SGR("%d"), attributes->color - 1 + 30); + } + needs_reset = true; + } + if (attributes->background) { + if (attributes->background >= 0x10) { + printf(SGR("%d"), attributes->background - 0x11 + 100); + } + else { + printf(SGR("%d"), attributes->background - 1 + 40); + } + needs_reset = true; + } + if (attributes->bold) { + printf(SGR("1")); + needs_reset = true; + } + if (attributes->italic) { + printf(SGR("3")); + needs_reset = true; + } + if (attributes->underline) { + printf(SGR("4")); + needs_reset = true; + } + } + const char *it = string; + bool need_redraw_prompt = false; + while (*it) { + if (pending_newline) { + need_redraw_prompt = true; + printf("\n" CSI("K") "\n" CSI("A")); + pending_newline = false; + continue; + } + if (*it == '\n') { + printf("%.*s", (int)(it - string), string); + string = it + 1; + pending_newline = true; + } + it++; + } + if (*string) { + printf("%s", string); + } + if (needs_reset) { + printf(SGR("0")); + } + printf(ESC("7") CSI("B")); + if (need_redraw_prompt) { + redraw_prompt(true); + } + else { + set_position(line.position); + } + fflush(stdout); + pthread_mutex_unlock(&terminal_lock); +} + +void CON_print(const char *string) +{ + CON_attributed_print(string, NULL); +} + +void CON_vprintf(const char *fmt, va_list args) +{ + char *string = NULL; + vasprintf(&string, fmt, args); + CON_attributed_print(string, NULL); + free(string); +} + +void CON_attributed_vprintf(const char *fmt, CON_attributes_t *attributes, va_list args) +{ + char *string = NULL; + vasprintf(&string, fmt, args); + CON_attributed_print(string, attributes); + free(string); +} + +void CON_printf(const char *fmt, ...) +{ + va_list args; + va_start(args, fmt); + CON_vprintf(fmt, args); + va_end(args); +} + + +void CON_attributed_printf(const char *fmt, CON_attributes_t *attributes,...) +{ + va_list args; + va_start(args, attributes); + CON_attributed_vprintf(fmt, attributes, args); + va_end(args); +} + +void CON_set_async_prompt(const char *string) +{ + pthread_mutex_lock(&terminal_lock); + prompt = string; + prompt_length = strlen(string); + redraw_prompt(true); + fflush(stdout); + pthread_mutex_unlock(&terminal_lock); +} + +void CON_set_repeat_empty(bool repeat) +{ + repeat_empty = repeat; +} diff --git a/SDL/console.h b/SDL/console.h new file mode 100644 index 0000000..d158988 --- /dev/null +++ b/SDL/console.h @@ -0,0 +1,49 @@ +#include +#include +#include + +#define CON_EOF "\x04" +bool CON_start(char *(*completer)(const char *substring, uintptr_t *context)); +char *CON_readline(const char *prompt); +char *CON_readline_async(void); + +typedef struct { + enum { + CON_COLOR_NONE = 0, + CON_COLOR_BLACK, + CON_COLOR_RED, + CON_COLOR_GREEN, + CON_COLOR_YELLOW, + CON_COLOR_BLUE, + CON_COLOR_MAGENTA, + CON_COLOR_CYAN, + CON_COLOR_LIGHT_GREY, + + CON_COLOR_DARK_GREY = CON_COLOR_BLACK + 0x10, + CON_COLOR_BRIGHT_RED = CON_COLOR_RED + 0x10, + CON_COLOR_BRIGHT_GREEN = CON_COLOR_GREEN + 0x10, + CON_COLOR_BRIGHT_YELLOW = CON_COLOR_YELLOW + 0x10, + CON_COLOR_BRIGHT_BLUE = CON_COLOR_BLUE + 0x10, + CON_COLOR_BRIGHT_MAGENTA = CON_COLOR_MAGENTA + 0x10, + CON_COLOR_BRIGHT_CYAN = CON_COLOR_CYAN + 0x10, + CON_COLOR_WHITE = CON_COLOR_LIGHT_GREY + 0x10, + } color:8, background:8; + bool bold; + bool italic; + bool underline; +} CON_attributes_t; + +#ifndef __printflike +/* Missing from Linux headers. */ +#define __printflike(fmtarg, firstvararg) \ +__attribute__((__format__ (__printf__, fmtarg, firstvararg))) +#endif + +void CON_print(const char *string); +void CON_attributed_print(const char *string, CON_attributes_t *attributes); +void CON_vprintf(const char *fmt, va_list args); +void CON_attributed_vprintf(const char *fmt, CON_attributes_t *attributes, va_list args); +void CON_printf(const char *fmt, ...) __printflike(1, 2); +void CON_attributed_printf(const char *fmt, CON_attributes_t *attributes,...) __printflike(1, 3); +void CON_set_async_prompt(const char *string); +void CON_set_repeat_empty(bool repeat); diff --git a/SDL/main.c b/SDL/main.c index 7861ac6..c5af370 100644 --- a/SDL/main.c +++ b/SDL/main.c @@ -10,6 +10,7 @@ #include "gui.h" #include "shader.h" #include "audio/audio.h" +#include "console.h" #ifndef _WIN32 #include @@ -29,6 +30,7 @@ static char *filename = NULL; static typeof(free) *free_function = NULL; static char *battery_save_path_ptr = NULL; static SDL_GLContext gl_context = NULL; +static bool console_supported = false; bool uses_gl(void) { @@ -44,6 +46,79 @@ void set_filename(const char *new_filename, typeof(free) *new_free_function) free_function = new_free_function; } +static char *completer(const char *substring, uintptr_t *context) +{ + char *temp = strdup(substring); + char *ret = GB_debugger_complete_substring(&gb, temp, context); + free(temp); + return ret; +} + +static void log_callback(GB_gameboy_t *gb, const char *string, GB_log_attributes attributes) +{ + CON_attributes_t con_attributes = {0,}; + con_attributes.bold = attributes & GB_LOG_BOLD; + con_attributes.underline = attributes & GB_LOG_UNDERLINE; + if (attributes & GB_LOG_DASHED_UNDERLINE) { + while (*string) { + con_attributes.underline ^= true; + CON_attributed_printf("%c", &con_attributes, *string); + string++; + } + } + else { + CON_attributed_print(string, &con_attributes); + } +} + +static void handle_eof(void) +{ + CON_set_async_prompt(""); + char *line = CON_readline("Quit? [y]/n > "); + if (line[0] == 'n' || line[0] == 'N') { + free(line); + CON_set_async_prompt("> "); + } + else { + exit(0); + } +} + +static char *input_callback(GB_gameboy_t *gb) +{ +retry: { + char *ret = CON_readline("Stopped> "); + if (strcmp(ret, CON_EOF) == 0) { + handle_eof(); + free(ret); + goto retry; + } + else { + CON_attributes_t attributes = {.bold = true}; + CON_attributed_printf("> %s\n", &attributes, ret); + } + return ret; +} +} + +static char *asyc_input_callback(GB_gameboy_t *gb) +{ +retry: { + char *ret = CON_readline_async(); + if (ret && strcmp(ret, CON_EOF) == 0) { + handle_eof(); + free(ret); + goto retry; + } + else if (ret) { + CON_attributes_t attributes = {.bold = true}; + CON_attributed_printf("> %s\n", &attributes, ret); + } + return ret; +} +} + + static char *captured_log = NULL; static void log_capture_callback(GB_gameboy_t *gb, const char *string, GB_log_attributes attributes) @@ -67,7 +142,7 @@ static void start_capturing_logs(void) static const char *end_capturing_logs(bool show_popup, bool should_exit, uint32_t popup_flags, const char *title) { - GB_set_log_callback(&gb, NULL); + GB_set_log_callback(&gb, console_supported? log_callback : NULL); if (captured_log[0] == 0) { free(captured_log); captured_log = NULL; @@ -256,6 +331,7 @@ static void handle_events(GB_gameboy_t *gb) } case SDL_SCANCODE_C: if (event.type == SDL_KEYDOWN && (event.key.keysym.mod & KMOD_CTRL)) { + CON_print("^C\a\n"); GB_debugger_break(gb); } break; @@ -408,12 +484,15 @@ static void rumble(GB_gameboy_t *gb, double amp) static void debugger_interrupt(int ignore) { - if (!GB_is_inited(&gb)) return; + if (!GB_is_inited(&gb)) exit(0); /* ^C twice to exit */ if (GB_debugger_is_stopped(&gb)) { GB_save_battery(&gb, battery_save_path_ptr); exit(0); } + if (console_supported) { + CON_print("^C\n"); + } GB_debugger_break(&gb); } @@ -442,7 +521,6 @@ static void gb_audio_callback(GB_gameboy_t *gb, GB_sample_t *sample) GB_audio_queue_sample(sample); } - static bool handle_pending_command(void) { @@ -577,6 +655,14 @@ restart: GB_set_rtc_mode(&gb, configuration.rtc_mode); GB_set_update_input_hint_callback(&gb, handle_events); GB_apu_set_sample_callback(&gb, gb_audio_callback); + + if ((console_supported = CON_start(completer))) { + CON_set_async_prompt("> "); + CON_set_repeat_empty(true); + GB_set_log_callback(&gb, log_callback); + GB_set_input_callback(&gb, input_callback); + GB_set_async_input_callback(&gb, asyc_input_callback); + } } if (stop_on_start) { stop_on_start = false; diff --git a/Windows/pthread.h b/Windows/pthread.h new file mode 100644 index 0000000..7bf328b --- /dev/null +++ b/Windows/pthread.h @@ -0,0 +1,86 @@ +/* Very minimal pthread implementation for Windows */ +#include +#include + +typedef HANDLE pthread_t; +typedef struct pthread_attr_s pthread_attr_t; + +static inline int pthread_create(pthread_t *pthread, + const pthread_attr_t *attrs, + LPTHREAD_START_ROUTINE function, + void *context) +{ + assert(!attrs); + *pthread = CreateThread(NULL, 0, function, + context, 0, NULL); + return *pthread? 0 : GetLastError(); +} + + +typedef struct { + unsigned status; + CRITICAL_SECTION cs; +} pthread_mutex_t; +#define PTHREAD_MUTEX_INITIALIZER {0,} + +static inline CRITICAL_SECTION *pthread_mutex_to_cs(pthread_mutex_t *mutex) +{ +retry: + if (mutex->status == 2) return &mutex->cs; + + unsigned expected = 0; + unsigned desired = 1; + if (__atomic_compare_exchange(&mutex->status, &expected, &desired, false, __ATOMIC_RELAXED, __ATOMIC_RELAXED)) { + InitializeCriticalSection(&mutex->cs); + mutex->status = 2; + return &mutex->cs; + } + goto retry; +} + +static inline int pthread_mutex_lock(pthread_mutex_t *mutex) +{ + EnterCriticalSection(pthread_mutex_to_cs(mutex)); + return 0; +} + +static inline int pthread_mutex_unlock(pthread_mutex_t *mutex) +{ + LeaveCriticalSection(pthread_mutex_to_cs(mutex)); + return 0; +} + +typedef struct { + unsigned status; + CONDITION_VARIABLE cond; +} pthread_cond_t; +#define PTHREAD_COND_INITIALIZER {0,} + +static inline CONDITION_VARIABLE *pthread_cond_to_win(pthread_cond_t *cond) +{ +retry: + if (cond->status == 2) return &cond->cond; + + unsigned expected = 0; + unsigned desired = 1; + if (__atomic_compare_exchange(&cond->status, &expected, &desired, false, __ATOMIC_RELAXED, __ATOMIC_RELAXED)) { + InitializeConditionVariable(&cond->cond); + cond->status = 2; + return &cond->cond; + } + goto retry; +} + + +static inline int pthread_cond_signal(pthread_cond_t *cond) +{ + WakeConditionVariable(pthread_cond_to_win(cond)); + return 0; +} + +static inline int pthread_cond_wait(pthread_cond_t *cond, + pthread_mutex_t *mutex) +{ + // Mutex is locked therefore already initialized + return SleepConditionVariableCS(pthread_cond_to_win(cond), &mutex->cs, INFINITE); +} diff --git a/Windows/unistd.h b/Windows/unistd.h index b7aabf2..62e2337 100644 --- a/Windows/unistd.h +++ b/Windows/unistd.h @@ -5,3 +5,4 @@ #define read(...) _read(__VA_ARGS__) #define write(...) _write(__VA_ARGS__) +#define isatty(...) _isatty(__VA_ARGS__)