#include "Core.h" #if CC_WIN_BACKEND == CC_WIN_BACKEND_TERMINAL #include "_WindowBase.h" #include "String.h" #include "Funcs.h" #include "Bitmap.h" #include "Options.h" #include "Errors.h" #include "Utils.h" #include #include #include #include #ifdef CC_BUILD_WIN #include #else #include #include #include #include #endif #ifdef CC_BUILD_LINUX #include #include #endif /*########################################################################################################################* *------------------------------------------------------Console output-----------------------------------------------------* *#########################################################################################################################*/ #ifdef CC_BUILD_WIN #define OutputConsole(buf, len) WriteConsoleA(hStdout, buf, len, NULL, NULL) #define BOX_CHAR "\xE2\x96\x84" #else #define OutputConsole(buf, len) write(STDOUT_FILENO, buf, len) #define BOX_CHAR "\xE2\x96\x84" #endif #ifdef CC_BUILD_MACOS // iTerm only displays trucolour properly with : #define SEP_STR ":" #define SEP_CHAR ':' #else #define SEP_STR ";" #define SEP_CHAR ';' #endif static void SetMousePosition(int x, int y); static cc_bool pendingResize, pendingClose; static int supportsTruecolor; #define CHARS_PER_CELL 2 #define CSI "\x1B[" #define ERASE_CMD(cmd) CSI cmd "J" #define DEC_PM_SET(cmd) CSI "?" cmd "h" #define DEC_PM_RESET(cmd) CSI "?" cmd "1" #define OutputConst(str) OutputConsole(str, sizeof(str) - 1) /*########################################################################################################################* *------------------------------------------------------Terminal backend----------------------------------------------------* *#########################################################################################################################*/ #ifdef CC_BUILD_WIN static HANDLE hStdin, hStdout; static DWORD inOldMode, outOldMode; static void UpdateDimensions(void) { CONSOLE_SCREEN_BUFFER_INFO csbi = { 0 }; int cols, rows; GetConsoleScreenBufferInfo(hStdout, &csbi); cols = csbi.srWindow.Right - csbi.srWindow.Left + 1; rows = csbi.srWindow.Bottom - csbi.srWindow.Top + 1; Platform_Log2("RESIZE: %i, %i", &cols, &rows); DisplayInfo.Width = cols; DisplayInfo.Height = rows * CHARS_PER_CELL; Window_Main.Width = DisplayInfo.Width; Window_Main.Height = DisplayInfo.Height; } #ifndef ENABLE_VIRTUAL_TERMINAL_PROCESSING #define ENABLE_VIRTUAL_TERMINAL_PROCESSING 0x0004 #endif static void HookTerminal(void) { hStdin = GetStdHandle(STD_INPUT_HANDLE); hStdout = GetStdHandle(STD_OUTPUT_HANDLE); GetConsoleMode(hStdin, &inOldMode); GetConsoleMode(hStdout, &outOldMode); SetConsoleOutputCP(CP_UTF8); // https://stackoverflow.com/questions/37069599/cant-read-mouse-event-use-readconsoleinput-in-c SetConsoleMode(hStdin, ENABLE_EXTENDED_FLAGS | ENABLE_WINDOW_INPUT | ENABLE_MOUSE_INPUT | ENABLE_PROCESSED_INPUT); SetConsoleMode(hStdout, ENABLE_VIRTUAL_TERMINAL_PROCESSING | ENABLE_PROCESSED_OUTPUT); supportsTruecolor = true; } static void UnhookTerminal(void) { SetConsoleMode(hStdin, inOldMode); SetConsoleMode(hStdout, outOldMode); } static BOOL WINAPI consoleHandler(DWORD signal) { if (signal == CTRL_C_EVENT) pendingClose = true; return true; } static void sigterm_handler(int sig) { pendingClose = true; UnhookTerminal(); } static void HookSignals(void) { SetConsoleCtrlHandler(consoleHandler, TRUE); signal(SIGTERM, sigterm_handler); signal(SIGINT, sigterm_handler); } #else // Inspired from https://github.com/Cubified/tuibox/blob/main/tuibox.h#L606 // Uses '▄' to double the vertical resolution // (this trick was inspired from https://github.com/ichinaski/pxl/blob/master/main.go#L30) static struct termios tio; static struct winsize ws; #ifdef CC_BUILD_LINUX static int orig_KB = K_XLATE; #endif static void UpdateDimensions(void) { ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws); DisplayInfo.Width = ws.ws_col; DisplayInfo.Height = ws.ws_row * CHARS_PER_CELL; Window_Main.Width = DisplayInfo.Width; Window_Main.Height = DisplayInfo.Height; } static void HookTerminal(void) { struct termios raw; tcgetattr(STDIN_FILENO, &tio); raw = tio; raw.c_lflag &= ~(ECHO | ICANON); tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw); // https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Normal-tracking-mode OutputConst(DEC_PM_SET("1049")); // Use Normal Screen Buffer and restore cursor as in DECRC, xterm. OutputConst(CSI "0m"); OutputConst(ERASE_CMD("2")); // Ps = 2 ⇒ Erase All. OutputConst(DEC_PM_SET("1003")); // Ps = 1 0 0 3 ⇒ Use All Motion Mouse Tracking, xterm. See OutputConst(DEC_PM_SET("1015")); // Ps = 1 0 1 5 ⇒ Enable urxvt Mouse Mode. OutputConst(DEC_PM_SET("1006")); // Ps = 1 0 0 6 ⇒ Enable SGR Mouse Mode, xterm. OutputConst(DEC_PM_RESET("25")); // Ps = 2 5 ⇒ Show cursor (DECTCEM), VT220. supportsTruecolor = true; } static void UnhookTerminal(void) { //ioctl(STDIN_FILENO, KDSKBMODE, orig_KB); tcsetattr(STDIN_FILENO, TCSAFLUSH, &tio); OutputConst(DEC_PM_RESET("1049")); OutputConst(CSI "0m"); OutputConst(ERASE_CMD("2")); // Ps = 2 ⇒ Erase All. OutputConst(DEC_PM_SET("25")); OutputConst(DEC_PM_RESET("1003")); OutputConst(DEC_PM_RESET("1015")); OutputConst(DEC_PM_RESET("1006")); } static void sigwinch_handler(int sig) { pendingResize = true; } static void sigterm_handler(int sig) { pendingClose = true; UnhookTerminal(); } static void HookSignals(void) { signal(SIGWINCH, sigwinch_handler); signal(SIGTERM, sigterm_handler); signal(SIGINT, sigterm_handler); } #endif /*########################################################################################################################* *---------------------------------------------------------Input backend---------------------------------------------------* *#########################################################################################################################*/ #ifdef CC_BUILD_WIN static const cc_uint8 key_map[] = { /* 00 */ 0, 0, 0, 0, 0, 0, 0, 0, /* 08 */ CCKEY_BACKSPACE, CCKEY_TAB, 0, 0, CCKEY_F5, CCKEY_ENTER, 0, 0, /* 10 */ 0, 0, 0, CCKEY_PAUSE, CCKEY_CAPSLOCK, 0, 0, 0, /* 18 */ 0, 0, 0, CCKEY_ESCAPE, 0, 0, 0, 0, /* 20 */ CCKEY_SPACE, CCKEY_PAGEUP, CCKEY_PAGEDOWN, CCKEY_END, CCKEY_HOME, CCKEY_LEFT, CCKEY_UP, CCKEY_RIGHT, /* 28 */ CCKEY_DOWN, 0, CCKEY_PRINTSCREEN, 0, CCKEY_PRINTSCREEN, CCKEY_INSERT, CCKEY_DELETE, 0, /* 30 */ '0', '1', '2', '3', '4', '5', '6', '7', /* 38 */ '8', '9', 0, 0, 0, 0, 0, 0, /* 40 */ 0, 'A', 'B', 'C', 'D', 'E', 'F', 'G', /* 48 */ 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', /* 50 */ 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', /* 58 */ 'X', 'Y', 'Z', CCKEY_LWIN, CCKEY_RWIN, CCKEY_MENU, 0, CCKEY_SLEEP, /* 60 */ CCKEY_KP0, CCKEY_KP1, CCKEY_KP2, CCKEY_KP3, CCKEY_KP4, CCKEY_KP5, CCKEY_KP6, CCKEY_KP7, /* 68 */ CCKEY_KP8, CCKEY_KP9, CCKEY_KP_MULTIPLY, CCKEY_KP_PLUS, 0, CCKEY_KP_MINUS, CCKEY_KP_DECIMAL, CCKEY_KP_DIVIDE, /* 70 */ CCKEY_F1, CCKEY_F2, CCKEY_F3, CCKEY_F4, CCKEY_F5, CCKEY_F6, CCKEY_F7, CCKEY_F8, /* 78 */ CCKEY_F9, CCKEY_F10, CCKEY_F11, CCKEY_F12, CCKEY_F13, CCKEY_F14, CCKEY_F15, CCKEY_F16, /* 80 */ CCKEY_F17, CCKEY_F18, CCKEY_F19, CCKEY_F20, CCKEY_F21, CCKEY_F22, CCKEY_F23, CCKEY_F24, /* 88 */ 0, 0, 0, 0, 0, 0, 0, 0, /* 90 */ CCKEY_NUMLOCK, CCKEY_SCROLLLOCK, 0, 0, 0, 0, 0, 0, /* 98 */ 0, 0, 0, 0, 0, 0, 0, 0, /* A0 */ CCKEY_LSHIFT, CCKEY_RSHIFT, CCKEY_LCTRL, CCKEY_RCTRL, CCKEY_LALT, CCKEY_RALT, CCKEY_BROWSER_PREV, CCKEY_BROWSER_NEXT, /* A8 */ CCKEY_BROWSER_REFRESH, CCKEY_BROWSER_STOP, CCKEY_BROWSER_SEARCH, CCKEY_BROWSER_FAVORITES, CCKEY_BROWSER_HOME, CCKEY_VOLUME_MUTE, CCKEY_VOLUME_DOWN, CCKEY_VOLUME_UP, /* B0 */ CCKEY_MEDIA_NEXT, CCKEY_MEDIA_PREV, CCKEY_MEDIA_STOP, CCKEY_MEDIA_PLAY, CCKEY_LAUNCH_MAIL, CCKEY_LAUNCH_MEDIA, CCKEY_LAUNCH_APP1, CCKEY_LAUNCH_CALC, /* B8 */ 0, 0, CCKEY_SEMICOLON, CCKEY_EQUALS, CCKEY_COMMA, CCKEY_MINUS, CCKEY_PERIOD, CCKEY_SLASH, /* C0 */ CCKEY_TILDE, 0, 0, 0, 0, 0, 0, 0, /* C8 */ 0, 0, 0, 0, 0, 0, 0, 0, /* D0 */ 0, 0, 0, 0, 0, 0, 0, 0, /* D8 */ 0, 0, 0, CCKEY_LBRACKET, CCKEY_BACKSLASH, CCKEY_RBRACKET, CCKEY_QUOTE, 0, }; // TODO lshift, rshift static int MapNativeKey(DWORD vk_key) { int key = vk_key < Array_Elems(key_map) ? key_map[vk_key] : 0; if (!key) Platform_Log1("Unknown key: %x", &vk_key); return key; } static void KeyEventProc(KEY_EVENT_RECORD ker) { int key = MapNativeKey(ker.wVirtualKeyCode); int uni = ker.uChar.UnicodeChar; if (ker.bKeyDown) { Input_SetPressed(key); if (uni) Event_RaiseInt(&InputEvents.Press, (cc_unichar)uni); } else { Input_SetReleased(key); } } static void MouseEventProc(MOUSE_EVENT_RECORD mer) { switch (mer.dwEventFlags) { case 0: case DOUBLE_CLICK: Input_Set(CCMOUSE_L, mer.dwButtonState & FROM_LEFT_1ST_BUTTON_PRESSED); Input_Set(CCMOUSE_R, mer.dwButtonState & RIGHTMOST_BUTTON_PRESSED); // TODO other mouse buttons break; case MOUSE_MOVED: SetMousePosition(mer.dwMousePosition.X, mer.dwMousePosition.Y * CHARS_PER_CELL); break; case MOUSE_WHEELED: Mouse_ScrollVWheel((int)mer.dwButtonState > 0 ? 1 : -1); break; default: Platform_LogConst("unknown mouse event"); break; } } static void ProcessConsoleEvents(float delta) { DWORD events = 0; GetNumberOfConsoleInputEvents(hStdin, &events); if (!events) return; INPUT_RECORD buffer[128]; if (!ReadConsoleInput(hStdin, buffer, 128, &events)) return; for (int i = 0; i < events; i++) { switch (buffer[i].EventType) { case KEY_EVENT: KeyEventProc(buffer[i].Event.KeyEvent); break; case MOUSE_EVENT: MouseEventProc(buffer[i].Event.MouseEvent); break; case WINDOW_BUFFER_SIZE_EVENT: pendingResize = true; break; } } } #else static int MapNativeMouse(int button) { if (button == 1) return CCMOUSE_L; if (button == 2) return CCMOUSE_M; if (button == 3) return CCMOUSE_R; if (button == 8) return CCMOUSE_X1; if (button == 9) return CCMOUSE_X2; /* Mouse horizontal and vertical scroll */ if (button >= 4 && button <= 7) return 0; Platform_Log1("Unknown mouse button: %i", &button); return 0; } static int stdin_available(void) { struct pollfd pfd; pfd.fd = STDIN_FILENO; pfd.events = POLLIN; if (poll(&pfd, 1, 0)) { if (pfd.revents & POLLIN) return 1; } return 0; } static void UpdatePointerPosition(char* tok) { int x, y; tok = strtok(NULL, ";"); x = atoi(tok); tok = strtok(NULL, ";"); y = atoi(tok) * CHARS_PER_CELL; SetMousePosition(x, y); } static void ProcessMouse(char* buf, int n) { char cpy[256 + 2]; strncpy(cpy, buf, n); char* tok = strtok(cpy + 3, ";"); int mouse; if (!tok) return; switch (tok[0]) { case '0': mouse = strchr(buf, 'm') == NULL; UpdatePointerPosition(tok); Input_SetNonRepeatable(CCMOUSE_L, mouse); break; case '1': mouse = strchr(buf, 'm') == NULL; UpdatePointerPosition(tok); Input_SetNonRepeatable(CCMOUSE_M, mouse); break; case '2': mouse = strchr(buf, 'm') == NULL; UpdatePointerPosition(tok); Input_SetNonRepeatable(CCMOUSE_R, mouse); break; case '3': mouse = (strcmp(tok, "32") == 0); UpdatePointerPosition(tok); break; } } static int MapKey(int key) { if (key == ' ') return CCKEY_SPACE; if (key >= 'a' && key <= 'z') key -= 32; if (key >= 'A' && key <= 'Z') return key; Platform_Log1("Unknown key: %i", &key); return 0; } static float event_time; static float press_start[256]; static void ProcessKey(int raw) { int key = MapKey(raw); if (key) { Input_SetPressed(key); press_start[raw] = event_time; } if (raw >= 32 && raw < 127) { Event_RaiseInt(&InputEvents.Press, raw); } } static void ProcessConsoleInput(void) { char buf[256]; int n = read(STDIN_FILENO, buf, sizeof(buf)); int A = buf[0]; //Platform_Log2("IN: %i, %i", &n, &A); if (n >= 4 && buf[0] == '\x1b' && buf[1] == '[' && buf[2] == '<') { ProcessMouse(buf, n); } else if (buf[0] >= 32 && buf[0] < 127) { ProcessKey(buf[0]); } } static void ProcessConsoleEvents(float delta) { if (stdin_available()) ProcessConsoleInput(); event_time += delta; // Auto release keys after a while for (int i = 0; i < 256; i++) { if (press_start[i] && (event_time - press_start[i]) > 1.0f) { Input_SetReleased(MapKey(i)); press_start[i] = 0.0f; } } } #endif /*########################################################################################################################* *-------------------------------------------------------Window common-----------------------------------------------------* *#########################################################################################################################*/ void Window_PreInit(void) { } void Window_Init(void) { Input.Sources = INPUT_SOURCE_NORMAL; DisplayInfo.Depth = 4; DisplayInfo.ScaleX = 0.5f; DisplayInfo.ScaleY = 0.5f; //ioctl(STDIN_FILENO , KDGKBMODE, &orig_KB); //ioctl(STDIN_FILENO, KDSKBMODE, K_MEDIUMRAW); HookTerminal(); UpdateDimensions(); HookSignals(); Platform_SingleProcess = true; } void Window_Free(void) { UnhookTerminal(); } static void DoCreateWindow(int width, int height) { Window_Main.Exists = true; Window_Main.Handle = (void*)1; Window_Main.Focused = true; } void Window_Create2D(int width, int height) { DoCreateWindow(width, height); } void Window_Create3D(int width, int height) { DoCreateWindow(width, height); } void Window_SetTitle(const cc_string* title) { // TODO } void Clipboard_GetText(cc_string* value) { // TODO } void Clipboard_SetText(const cc_string* value) { // TODO } int Window_GetWindowState(void) { return WINDOW_STATE_NORMAL; } cc_result Window_EnterFullscreen(void) { return 0; } cc_result Window_ExitFullscreen(void) { return 0; } int Window_IsObscured(void) { return 0; } void Window_Show(void) { } void Window_SetSize(int width, int height) { // TODO } void Window_RequestClose(void) { pendingClose = true; } void Window_ProcessEvents(float delta) { if (pendingResize) { pendingResize = false; UpdateDimensions(); Event_RaiseVoid(&WindowEvents.Resized); } if (pendingClose) { pendingClose = false; Window_Main.Exists = false; Event_RaiseVoid(&WindowEvents.Closing); return; } ProcessConsoleEvents(delta); } void Window_ProcessGamepads(float delta) { } static int mouseX, mouseY; static void SetMousePosition(int x, int y) { mouseX = x; mouseY = y; Pointer_SetPosition(0, x, y); } static void Cursor_GetRawPos(int* x, int* y) { *x = mouseX; *y = mouseY; } void Cursor_SetPosition(int x, int y) { // TODO } static void Cursor_DoSetVisible(cc_bool visible) { // TODO } static void ShowDialogCore(const char* title, const char* msg) { Platform_LogConst(title); Platform_LogConst(msg); } cc_result Window_OpenFileDialog(const struct OpenFileDialogArgs* args) { return ERR_NOT_SUPPORTED; } cc_result Window_SaveFileDialog(const struct SaveFileDialogArgs* args) { return ERR_NOT_SUPPORTED; } void Window_AllocFramebuffer(struct Bitmap* bmp, int width, int height) { bmp->scan0 = (BitmapCol*)Mem_Alloc(width * height, 4, "window pixels"); bmp->width = width; bmp->height = height; } void Window_FreeFramebuffer(struct Bitmap* bmp) { Mem_Free(bmp->scan0); } void OnscreenKeyboard_Open(struct OpenKeyboardArgs* args) { } void OnscreenKeyboard_SetText(const cc_string* text) { } void OnscreenKeyboard_Draw2D(Rect2D* r, struct Bitmap* bmp) { } void OnscreenKeyboard_Draw3D(void) { } void OnscreenKeyboard_Close(void) { } void Window_EnableRawMouse(void) { DefaultEnableRawMouse(); } void Window_UpdateRawMouse(void) { DefaultUpdateRawMouse(); } void Window_DisableRawMouse(void) { DefaultDisableRawMouse(); } /*########################################################################################################################* *-------------------------------------------------------Console output-----------------------------------------------------* *#########################################################################################################################*/ // TODO still wrong static void AppendByteFast(cc_string* str, int value) { if (value >= 100) { String_Append(str, '0' + (value / 100)); value %= 100; String_Append(str, '0' + (value / 10)); value %= 10; } else if (value >= 10) { String_Append(str, '0' + (value / 10)); value %= 10; } String_Append(str, '0' + value); } static int Index256(int value) { if (value <= 0x5F) return value; // Add 20 to round to nearest return (value - 0x5F + 20) / 40; } static int CalcIndex(BitmapCol rgb) { int r = Index256(BitmapCol_R(rgb)); int g = Index256(BitmapCol_G(rgb)); int b = Index256(BitmapCol_B(rgb)); return 16 + 36 * r + 6 * g + b; } void Window_DrawFramebuffer(Rect2D r, struct Bitmap* bmp) { char buf[256]; cc_string str; int len; String_InitArray(str, buf); for (int y = r.y & ~0x01; y < r.y + r.height; y += 2) { //len = sprintf(buf, CSI "%i;%iH", y / 2, r.x); // move cursor to start //OutputConsole(buf, len); str.length = 0; String_AppendConst(&str, CSI); String_AppendInt( &str, y / CHARS_PER_CELL); String_Append( &str, ';'); String_AppendInt( &str, r.x); String_Append( &str, 'H'); OutputConsole(buf, str.length); for (int x = r.x; x < r.x + r.width; x++) { BitmapCol top = Bitmap_GetPixel(bmp, x, y + 0); BitmapCol bot = Bitmap_GetPixel(bmp, x, y + 1); // Use '▄' so each cell can use a background and foreground colour // This essentially doubles the vertical resolution of the displayed image //printf(CSI "48;2;%i;%i;%im", BitmapCol_R(top), BitmapCol_G(top), BitmapCol_B(top)); //printf(CSI "38;2;%i;%i;%im", BitmapCol_R(bot), BitmapCol_G(bot), BitmapCol_B(bot)); //printf("\xE2\x96\x84"); //len = sprintf(buf, CSI "48;2;%i;%i;%im" CSI "38;2;%i;%i;%im" BOX_CHAR, // BitmapCol_R(top), BitmapCol_G(top), BitmapCol_B(top), // BitmapCol_R(bot), BitmapCol_G(bot), BitmapCol_B(bot)); // https://en.wikipedia.org/wiki/ANSI_escape_code#Colors str.length = 0; if (supportsTruecolor) { String_AppendConst(&str, CSI "48" SEP_STR "2" SEP_STR); String_AppendInt( &str, BitmapCol_R(top)); String_Append( &str, SEP_CHAR); String_AppendInt( &str, BitmapCol_G(top)); String_Append( &str, SEP_CHAR); String_AppendInt( &str, BitmapCol_B(top)); String_Append( &str, 'm'); String_AppendConst(&str, CSI "38" SEP_STR "2" SEP_STR); String_AppendInt( &str, BitmapCol_R(bot)); String_Append( &str, SEP_CHAR); String_AppendInt( &str, BitmapCol_G(bot)); String_Append( &str, SEP_CHAR); String_AppendInt( &str, BitmapCol_B(bot)); String_Append( &str, 'm'); } else { String_AppendConst(&str, CSI "48" SEP_STR "5" SEP_STR); String_AppendInt( &str, CalcIndex(top)); String_Append( &str, 'm'); String_AppendConst(&str, CSI "38" SEP_STR "5" SEP_STR); String_AppendInt( &str, CalcIndex(bot)); String_Append( &str, 'm'); } String_AppendConst(&str, BOX_CHAR); OutputConsole(buf, str.length); } } } #endif