esc
Externally Scriptable Editor
git clone git://mccd.space/esc
| Log | Files | Refs | README |
commit 45a19884912c41f7cd4d58de675c9eddefb49dbe Author: Marc Coquand <marc@coquand.email> Date: Thu, 19 Feb 2026 11:36:16 +0100 initial commit Diffstat:
| A | .clang-format | | | 2 | ++ |
| A | Go-Mono.ttf | | | 0 | |
| A | main.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;
+}
+