esc

Externally Scriptable Editor

git clone git://mccd.space/esc

commit 45a19884912c41f7cd4d58de675c9eddefb49dbe
Author: Marc Coquand <marc@coquand.email>
Date:   Thu, 19 Feb 2026 11:36:16 +0100

initial commit

Diffstat:
A.clang-format | 2++
AGo-Mono.ttf | 0
Amain.c | 392+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 394 insertions(+), 0 deletions(-)
diff --git a/.clang-format b/.clang-format
@@ -0,0 +1,2 @@
+UseTab: Always
+IndentWidth: 8
diff --git a/Go-Mono.ttf b/Go-Mono.ttf
Binary files differ.
diff --git a/main.c b/main.c
@@ -0,0 +1,392 @@
+#include <SDL3/SDL.h>
+#include <SDL3/SDL_main.h>
+#include <SDL3_ttf/SDL_ttf.h>
+#include <stdbool.h>
+#include <stdlib.h>
+#include <string.h>
+
+#define MAX_TEXT_LEN 999999
+
+int main(int argc, char *argv[]) {
+	// 1. SET HINT BEFORE INIT
+	SDL_SetHintWithPriority(SDL_HINT_VIDEO_DRIVER, "wayland",
+				SDL_HINT_OVERRIDE);
+
+	SDL_Init(SDL_INIT_VIDEO);
+	TTF_Init();
+
+	SDL_Window *window = SDL_CreateWindow(
+	    "Sharp Text Editor", 800, 600,
+	    SDL_WINDOW_RESIZABLE | SDL_WINDOW_HIGH_PIXEL_DENSITY);
+	SDL_Renderer *renderer = SDL_CreateRenderer(window, NULL);
+	SDL_SetRenderLogicalPresentation(renderer, 800, 600,
+					 SDL_LOGICAL_PRESENTATION_DISABLED);
+
+	TTF_Font *font = TTF_OpenFont("Go-Mono.ttf", 24.0f);
+	int char_width;
+	TTF_GetGlyphMetrics(font, 'A', NULL, NULL, NULL, NULL, &char_width);
+	float line_height = (float)TTF_GetFontHeight(font);
+
+	size_t text_capacity = 1024; // Start with 1KB
+	char *text_buffer = malloc(text_capacity);
+	text_buffer[0] = '\0'; // Initialize as empty string
+	size_t text_length = 0;
+
+	float scroll_x = 0.0f;
+	float scroll_y = 0.0f;
+	bool running = true;
+	int cursor_idx = 0;
+
+	SDL_StartTextInput(window);
+
+	while (running) {
+		SDL_Event event;
+
+		// 2. USE WAITEVENT TO DROP CPU USAGE TO 0%
+		if (SDL_WaitEvent(&event)) {
+			do {
+				if (event.type == SDL_EVENT_QUIT)
+					running = false;
+
+				else if (event.type ==
+					 SDL_EVENT_MOUSE_BUTTON_DOWN) {
+					float mx, my;
+					SDL_RenderCoordinatesFromWindow(
+					    renderer, event.button.x,
+					    event.button.y, &mx, &my);
+
+					int target_col =
+					    (int)((mx - 20.0f + scroll_x +
+						   (char_width / 2.0f)) /
+						  char_width);
+					int target_row =
+					    (int)((my - 20.0f + scroll_y) /
+						  line_height);
+
+					if (target_col < 0)
+						target_col = 0;
+					if (target_row < 0)
+						target_row = 0;
+
+					int current_row = 0, current_col = 0;
+					const char *ptr = text_buffer;
+					const char *last_ptr = text_buffer;
+
+					// Iterate through the buffer using
+					// UTF-8 steps
+					while (*ptr != '\0') {
+						if (current_row == target_row &&
+						    current_col == target_col) {
+							break;
+						}
+
+						last_ptr =
+						    ptr; // Keep track of the
+							 // start of the current
+							 // character
+						Uint32 codepoint =
+						    SDL_StepUTF8(&ptr, NULL);
+
+						if (codepoint == '\n') {
+							if (current_row ==
+							    target_row) {
+								// We reached
+								// the end of
+								// the target
+								// line
+								ptr = last_ptr;
+								break;
+							}
+							current_row++;
+							current_col = 0;
+						} else {
+							current_col++;
+						}
+
+						// If we've passed the target
+						// row, stop at the end of the
+						// previous line
+						if (current_row > target_row) {
+							ptr = last_ptr;
+							break;
+						}
+					}
+					cursor_idx = (int)(ptr - text_buffer);
+				}
+
+				else if (event.type == SDL_EVENT_MOUSE_WHEEL) {
+					scroll_y -= event.wheel.y * line_height;
+					scroll_x -= -event.wheel.x * 30.0f;
+
+					if (scroll_x < 0)
+						scroll_x = 0;
+					if (scroll_y < 0)
+						scroll_y = 0;
+				}
+
+				else if (event.type == SDL_EVENT_KEY_DOWN) {
+					SDL_Keycode key = event.key.key;
+
+					if (key == SDLK_BACKSPACE &&
+					    cursor_idx > 0) {
+						int prev_idx = cursor_idx;
+						do {
+							prev_idx--;
+						} while (
+						    prev_idx > 0 &&
+						    (text_buffer[prev_idx] &
+						     0xC0) == 0x80);
+
+						int char_size =
+						    cursor_idx - prev_idx;
+						memmove(
+						    &text_buffer[prev_idx],
+						    &text_buffer[cursor_idx],
+						    text_length - cursor_idx +
+							1);
+						text_length -= char_size;
+						cursor_idx = prev_idx;
+					} else if (key == SDLK_RETURN &&
+						   text_length <
+						       MAX_TEXT_LEN - 1) {
+						memmove(
+						    &text_buffer[cursor_idx +
+								 1],
+						    &text_buffer[cursor_idx],
+						    text_length - cursor_idx +
+							1);
+						text_buffer[cursor_idx] = '\n';
+						text_length++;
+						cursor_idx++;
+					} else if (key == SDLK_LEFT &&
+						   cursor_idx > 0) {
+						cursor_idx--;
+
+						while (
+						    cursor_idx > 0 &&
+						    (text_buffer[cursor_idx] &
+						     0xC0) == 0x80) {
+							cursor_idx--;
+						}
+					} else if (key == SDLK_DELETE &&
+						   cursor_idx < text_length) {
+						const char *ptr =
+						    text_buffer + cursor_idx;
+						const char *next = ptr;
+						SDL_StepUTF8(&next, NULL);
+						int char_size =
+						    (int)(next - ptr);
+						memmove(
+						    &text_buffer[cursor_idx],
+						    &text_buffer[cursor_idx +
+								 char_size],
+						    text_length -
+							(cursor_idx +
+							 char_size) +
+							1);
+						text_length -= char_size;
+					}
+
+					else if (key == SDLK_UP ||
+						 key == SDLK_DOWN) {
+						// 1. Find start of current line
+						// and current visual column
+						int line_start_idx = cursor_idx;
+						while (
+						    line_start_idx > 0 &&
+						    text_buffer[line_start_idx -
+								1] != '\n') {
+							line_start_idx--;
+						}
+
+						int visual_col = 0;
+						const char *p = text_buffer +
+								line_start_idx;
+						while (p < text_buffer +
+							       cursor_idx) {
+							SDL_StepUTF8(&p, NULL);
+							visual_col++;
+						}
+
+						int target_idx = -1;
+						if (key == SDLK_UP) {
+							if (line_start_idx >
+							    0) {
+								// Find start of
+								// PREVIOUS line
+								int prev_line_end =
+								    line_start_idx -
+								    1;
+								int prev_line_start =
+								    prev_line_end;
+								while (
+								    prev_line_start >
+									0 &&
+								    text_buffer
+									    [prev_line_start -
+									     1] !=
+									'\n') {
+									prev_line_start--;
+								}
+								target_idx =
+								    prev_line_start;
+							}
+						} else { // SDLK_DOWN
+							const char *next_line =
+							    strchr(
+								text_buffer +
+								    cursor_idx,
+								'\n');
+							if (next_line) {
+								target_idx =
+								    (int)(next_line -
+									  text_buffer) +
+								    1;
+							}
+						}
+
+						if (target_idx != -1) {
+							const char *ptr =
+							    text_buffer +
+							    target_idx;
+							for (int i = 0;
+							     i < visual_col;
+							     i++) {
+								if (*ptr ==
+									'\n' ||
+								    *ptr ==
+									'\0')
+									break;
+								SDL_StepUTF8(
+								    &ptr, NULL);
+							}
+							cursor_idx =
+							    (int)(ptr -
+								  text_buffer);
+						}
+					} else if (key == SDLK_RIGHT &&
+						   cursor_idx < text_length) {
+						const char *next =
+						    text_buffer + cursor_idx;
+						SDL_StepUTF8(
+						    &next,
+						    NULL); // This jumps to the
+							   // start of the next
+							   // valid character
+						cursor_idx =
+						    (int)(next - text_buffer);
+					}
+				}
+
+				else if (event.type == SDL_EVENT_TEXT_INPUT) {
+					size_t input_len =
+					    strlen(event.text.text);
+
+					if (text_length + input_len + 1 >
+					    text_capacity) {
+						text_capacity *= 2;
+						char *new_buffer = realloc(
+						    text_buffer, text_capacity);
+						if (new_buffer) {
+							text_buffer =
+							    new_buffer;
+						} else {
+							break;
+						}
+					}
+
+					memmove(&text_buffer[cursor_idx +
+							     input_len],
+						&text_buffer[cursor_idx],
+						text_length - cursor_idx + 1);
+					memcpy(&text_buffer[cursor_idx],
+					       event.text.text, input_len);
+					text_length += input_len;
+					cursor_idx += input_len;
+				}
+			} while (SDL_PollEvent(&event));
+		}
+
+		// --- RENDER PHASE ---
+		SDL_SetRenderDrawColor(renderer, 255, 255, 255, 255);
+		SDL_RenderClear(renderer);
+
+		int render_w, render_h;
+		SDL_GetRenderOutputSize(renderer, &render_w, &render_h);
+
+		SDL_SetRenderDrawColor(renderer, 255, 255, 255, 255);
+		SDL_RenderClear(renderer);
+
+		if (font && text_length > 0) {
+			SDL_Color black = {0, 0, 0, 255};
+
+			float current_y = 20.0f - scroll_y;
+
+			const char *line_start = text_buffer;
+			while (line_start != NULL && *line_start != '\0') {
+				const char *line_end = strchr(line_start, '\n');
+				size_t len =
+				    line_end ? (size_t)(line_end - line_start)
+					     : strlen(line_start);
+
+				if (len > 0 && current_y + line_height > 0 &&
+				    current_y < render_h) {
+					SDL_Surface *surf =
+					    TTF_RenderText_Blended(
+						font, line_start, len, black);
+					if (surf) {
+						SDL_Texture *tex =
+						    SDL_CreateTextureFromSurface(
+							renderer, surf);
+						SDL_FRect dst = {
+						    20.0f - scroll_x, current_y,
+						    (float)surf->w,
+						    (float)surf->h};
+						SDL_RenderTexture(renderer, tex,
+								  NULL, &dst);
+						SDL_DestroyTexture(tex);
+						SDL_DestroySurface(surf);
+					}
+				}
+				current_y += line_height;
+				line_start = line_end
+						 ? line_end + 1
+						 : NULL; // Move to next line
+			}
+		}
+
+		int cur_row = 0, cur_col = 0;
+		const char *ptr = text_buffer;
+		while (ptr < text_buffer + cursor_idx) {
+			if (*ptr == '\n') {
+				cur_row++;
+				cur_col = 0;
+				ptr++;
+			} else {
+				SDL_StepUTF8(&ptr, NULL);
+				cur_col++;
+			}
+		}
+
+		SDL_FRect cursor_rect = {
+		    20.0f + (cur_col * char_width) - scroll_x,
+		    20.0f + (cur_row * line_height) - scroll_y, 2.0f,
+		    (float)line_height};
+
+		if (cursor_rect.y + line_height > 0 &&
+		    cursor_rect.y < render_h) {
+			SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
+			SDL_RenderFillRect(renderer, &cursor_rect);
+		}
+
+		SDL_RenderPresent(renderer);
+	}
+
+	TTF_CloseFont(font);
+	SDL_DestroyRenderer(renderer);
+	SDL_DestroyWindow(window);
+	TTF_Quit();
+	SDL_Quit();
+	return 0;
+}
+