summary refs log tree commit diff
path: root/src/Window_Terminal.c
diff options
context:
space:
mode:
authorWlodekM <[email protected]>2024-06-16 10:35:45 +0300
committerWlodekM <[email protected]>2024-06-16 10:35:45 +0300
commitabef6da56913f1c55528103e60a50451a39628b1 (patch)
treeb3c8092471ecbb73e568cd0d336efa0e7871ee8d /src/Window_Terminal.c
initial commit
Diffstat (limited to 'src/Window_Terminal.c')
-rw-r--r--src/Window_Terminal.c645
1 files changed, 645 insertions, 0 deletions
diff --git a/src/Window_Terminal.c b/src/Window_Terminal.c
new file mode 100644
index 0000000..36a27e4
--- /dev/null
+++ b/src/Window_Terminal.c
@@ -0,0 +1,645 @@
+#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 <stdio.h>
+#include <signal.h>
+#include <stdlib.h>
+#include <string.h>
+
+#ifdef CC_BUILD_WIN
+#include <windows.h>
+#else
+#include <unistd.h>
+#include <termios.h>
+#include <poll.h>
+#include <sys/ioctl.h>
+#endif
+
+#ifdef CC_BUILD_LINUX
+#include <sys/kd.h>
+#include <linux/keyboard.h>
+#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