esc

Externally Scriptable Editor

git clone git://mccd.space/esc

commit 3bdbc2bce5fe2e46613b7b70c4d97e09964e5f72
parent 415fa9026d05389297d0af1f2bd155f94f93d96a
Author: Marc Coquand <marc@coquand.email>
Date:   Sat, 21 Feb 2026 20:08:30 +0100

Save load

Diffstat:
Meditor.c | 145++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
Meditor.h | 6+++++-
Mmain.c | 743+++++++++++++++++++++++++++++++++++++++++--------------------------------------
Munix_utils.c | 2+-
4 files changed, 528 insertions(+), 368 deletions(-)
diff --git a/editor.c b/editor.c
@@ -1,5 +1,6 @@
 #include "editor.h"
 #include <ctype.h>
+#include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
 
@@ -19,6 +20,7 @@ Editor *editor_create(int char_width, float line_height) {
 	ed->ranges = NULL;
 	ed->ranges_count = 0;
 	ed->ranges_capacity = 0;
+	ed->filename = NULL;
 
 	ed->char_width = char_width;
 	ed->line_height = line_height;
@@ -26,12 +28,64 @@ Editor *editor_create(int char_width, float line_height) {
 }
 
 void editor_destroy(Editor *ed) {
+	if (ed->filename)
+		free(ed->filename);
 	free(ed->buffer);
 	free(ed);
 }
 
 void editor_clear_ranges(Editor *ed) { ed->ranges_count = 0; }
 
+bool editor_load_file(Editor *ed, const char *filename) {
+	FILE *f = fopen(filename, "rb");
+	if (!f)
+		return false;
+
+	fseek(f, 0, SEEK_END);
+	size_t size = ftell(f);
+	fseek(f, 0, SEEK_SET);
+
+	if (size + 1 > ed->capacity) {
+		ed->capacity = size + 1024;
+		char *new_buf = realloc(ed->buffer, ed->capacity);
+		if (!new_buf) {
+			fclose(f);
+			return false;
+		}
+		ed->buffer = new_buf;
+	}
+
+	fread(ed->buffer, 1, size, f);
+	ed->buffer[size] = '\0';
+	ed->length = size;
+	fclose(f);
+
+	if (ed->filename)
+		free(ed->filename);
+	ed->filename = strdup(filename);
+	ed->cursor_idx = 0;
+	ed->selection_anchor = 0;
+	editor_clear_ranges(ed);
+	return true;
+}
+
+bool editor_write_file(Editor *ed, const char *filename) {
+	const char *to_write;
+	if (filename)
+		to_write = filename;
+	else if (ed->filename) {
+		filename = ed->filename;
+	} else {
+		return false;
+	}
+	FILE *f = fopen(ed->filename, "wb");
+	if (!f)
+		return false;
+	fwrite(ed->buffer, 1, ed->length, f);
+	fclose(f);
+	return true;
+}
+
 void editor_add_range(Editor *ed, int start, int end, int visual_cols) {
 	if (ed->ranges_count >= ed->ranges_capacity) {
 		ed->ranges_capacity =
@@ -40,24 +94,53 @@ void editor_add_range(Editor *ed, int start, int end, int visual_cols) {
 				     ed->ranges_capacity * sizeof(EditorRange));
 	}
 	EditorRange r = {start, end, visual_cols};
-	ed->ranges[ed->ranges_count++] = r;
+	// Insert in sorted order to allow binary search
+	size_t i = ed->ranges_count;
+	while (i > 0 && ed->ranges[i - 1].start_byte > start) {
+		ed->ranges[i] = ed->ranges[i - 1];
+		i--;
+	}
+	ed->ranges[i] = r;
+	ed->ranges_count++;
 }
 
 EditorRange *editor_get_range_at(Editor *ed, int byte_idx) {
-	// XXX Use binary search
-	for (size_t i = 0; i < ed->ranges_count; i++) {
-		if (byte_idx >= ed->ranges[i].start_byte &&
-		    byte_idx < ed->ranges[i].end_byte) {
-			return &ed->ranges[i];
+	int left = 0;
+	int right = (int)ed->ranges_count - 1;
+
+	while (left <= right) {
+		int mid = left + (right - left) / 2;
+		EditorRange *r = &ed->ranges[mid];
+
+		if (byte_idx >= r->start_byte && byte_idx < r->end_byte) {
+			return r;
+		}
+
+		if (byte_idx < r->start_byte) {
+			right = mid - 1;
+		} else {
+			left = mid + 1;
 		}
 	}
 	return NULL;
 }
 
 EditorRange *editor_get_range_ending_at(Editor *ed, int byte_idx) {
-	for (size_t i = 0; i < ed->ranges_count; i++) {
-		if (byte_idx == ed->ranges[i].end_byte) {
-			return &ed->ranges[i];
+	int left = 0;
+	int right = (int)ed->ranges_count - 1;
+
+	while (left <= right) {
+		int mid = left + (right - left) / 2;
+		EditorRange *r = &ed->ranges[mid];
+
+		if (byte_idx == r->end_byte) {
+			return r;
+		}
+
+		if (byte_idx <= r->start_byte) {
+			right = mid - 1;
+		} else {
+			left = mid + 1;
 		}
 	}
 	return NULL;
@@ -95,6 +178,17 @@ void editor_insert_text(Editor *ed, const char *text, bool replace) {
 		ed->buffer = new_buf;
 	}
 
+	// Shift ranges affected by insertion
+	for (size_t i = 0; i < ed->ranges_count; i++) {
+		if (ed->ranges[i].start_byte >= ed->cursor_idx) {
+			ed->ranges[i].start_byte += input_len;
+			ed->ranges[i].end_byte += input_len;
+		} else if (ed->cursor_idx > ed->ranges[i].start_byte &&
+			   ed->cursor_idx < ed->ranges[i].end_byte) {
+			ed->ranges[i].end_byte += input_len;
+		}
+	}
+
 	memmove(&ed->buffer[ed->cursor_idx + input_len],
 		&ed->buffer[ed->cursor_idx], ed->length - ed->cursor_idx + 1);
 	memcpy(&ed->buffer[ed->cursor_idx], text, input_len);
@@ -114,6 +208,39 @@ void editor_delete_range(Editor *ed, int start, int end) {
 		start = end;
 		end = tmp;
 	}
+	// Shift/Delete ranges affected by deletion
+	int len = end - start;
+	size_t new_count = 0;
+	for (size_t i = 0; i < ed->ranges_count; i++) {
+		EditorRange r = ed->ranges[i];
+
+		// Drop range completely contained within the deleted region
+		if (r.start_byte >= start && r.end_byte <= end) {
+			continue;
+		}
+
+		// Shift ranges entirely after the deleted section
+		if (r.start_byte >= end) {
+			r.start_byte -= len;
+			r.end_byte -= len;
+		} else {
+			// Handle partial overlaps bounds
+			if (r.start_byte > start && r.start_byte < end)
+				r.start_byte = start;
+			if (r.end_byte > start && r.end_byte < end)
+				r.end_byte = start;
+
+			// If deletion happened entirely inside a specific range
+			if (start > r.start_byte && end < r.end_byte) {
+				r.end_byte -= len;
+			}
+		}
+
+		if (r.end_byte > r.start_byte) {
+			ed->ranges[new_count++] = r;
+		}
+	}
+	ed->ranges_count = new_count;
 
 	memmove(&ed->buffer[start], &ed->buffer[end], ed->length - end + 1);
 	ed->length -= (end - start);
diff --git a/editor.h b/editor.h
@@ -18,6 +18,7 @@ typedef struct {
 	int cursor_idx;
 	int selection_anchor; // Where the selection started
 
+	char *filename;
 	EditorRange *ranges;
 	size_t ranges_count;
 	size_t ranges_capacity;
@@ -38,6 +39,9 @@ void editor_delete_back(Editor *ed);
 void editor_delete_forward(Editor *ed);
 void editor_clear_selection(Editor *ed);
 void editor_delete_range(Editor *ed, int start, int end);
+bool editor_load_file(Editor *ed, const char *filename);
+bool editor_write_file(Editor *ed, const char *filename);
+void editor_add_range(Editor *ed, int start, int end, int visual_cols);
 
 // Cursor Movement
 void editor_cursor_left(Editor *ed);
@@ -56,5 +60,5 @@ void editor_goto_pos(Editor *ed, int pos);
 char *editor_get_selection(Editor *ed);
 void editor_select_word(Editor *ed);
 bool editor_has_selection(Editor *ed);
-
+EditorRange *editor_get_range_at(Editor *ed, int byte_idx);
 #endif
diff --git a/main.c b/main.c
@@ -13,296 +13,230 @@
 #include <string.h>
 #include <unistd.h>
 
-int main(int argc, char *argv[]) {
-	// Otherwise wayland just wouldn't load.
-	SDL_SetHintWithPriority(SDL_HINT_VIDEO_DRIVER, "wayland",
-				SDL_HINT_OVERRIDE);
-
-	SDL_Init(SDL_INIT_VIDEO);
-	TTF_Init();
-
-	SDL_Window *window = SDL_CreateWindow(
-	    "esc", 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("IosevkaTermSS13-Extended.ttf", 24.0f);
-	int char_width;
-	TTF_GetGlyphMetrics(font, 'A', NULL, NULL, NULL, NULL, &char_width);
-	float line_height = (float)TTF_GetFontHeight(font)*1.5;
-	float cursor_height = (float)TTF_GetFontHeight(font);
-
-	float scroll_x = 0.0f;
-	float scroll_y = 0.0f;
-	bool running = true;
-	bool is_dragging = false;
-
+void handle_events(Editor *ed, SDL_Renderer *renderer, float *scroll_x,
+		   float *scroll_y, float line_height, bool *running,
+		   bool *is_dragging) {
+	SDL_Event event;
 	char *cmd;
-	SDL_StartTextInput(window);
 
-	Editor *ed = editor_create(char_width, line_height);
-	ed->cursor_idx = 0;
-
-	while (running) {
-		SDL_Event event;
+	if (SDL_WaitEvent(&event)) {
+		do {
+			if (event.type == SDL_EVENT_QUIT)
+				*running = false;
 
-		if (SDL_WaitEvent(&event)) {
-			do {
-				if (event.type == SDL_EVENT_QUIT)
-					running = false;
+			else if (event.type == SDL_EVENT_MOUSE_WHEEL) {
+				*scroll_y -= event.wheel.y * line_height;
+				*scroll_x -= -event.wheel.x * 30.0f;
 
-				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;
-				}
+				if (*scroll_x < 0)
+					*scroll_x = 0;
+				if (*scroll_y < 0)
+					*scroll_y = 0;
+			}
 
-				switch (event.type) {
-				case SDL_EVENT_KEY_DOWN:
-					switch (event.key.key) {
-					case SDLK_BACKSPACE:
-						editor_delete_back(ed);
-						break;
-					case SDLK_DELETE:
-						editor_delete_forward(ed);
-						break;
-					case SDLK_TAB:
-						editor_insert_text(ed, "\t",
-								   true);
-						editor_clear_selection(ed);
-						break;
-					case SDLK_RETURN:
-						editor_newline(ed);
-						editor_clear_selection(ed);
+			switch (event.type) {
+			case SDL_EVENT_KEY_DOWN:
+				switch (event.key.key) {
+				case SDLK_BACKSPACE:
+					editor_delete_back(ed);
+					break;
+				case SDLK_DELETE:
+					editor_delete_forward(ed);
+					break;
+				case SDLK_TAB:
+					editor_insert_text(ed, "\t", true);
+					editor_clear_selection(ed);
+					break;
+				case SDLK_RETURN:
+					editor_newline(ed);
+					editor_clear_selection(ed);
+					break;
+				case SDLK_1:
+					if (!(event.key.mod & SDL_KMOD_ALT))
 						break;
-					case SDLK_1:
-						if (!(event.key.mod &
-						      SDL_KMOD_ALT))
-							break;
-						cmd = unix_select_command();
-						if (cmd) {
-							char *selection =
-							    editor_get_selection(
-								ed);
-							char *output =
-							    unix_run_command(
-								cmd, selection);
-
-							if (output) {
-								editor_insert_text(
-								    ed, output,
-								    true);
-								free(output);
-							}
-							if (selection)
-								free(selection);
-							free(cmd);
+					cmd = unix_select_command();
+					if (cmd) {
+						char *selection =
+						    editor_get_selection(ed);
+						char *output = unix_run_command(
+						    cmd, selection);
+						if (output) {
+							editor_insert_text(
+							    ed, output, true);
+							free(output);
 						}
+						if (selection)
+							free(selection);
+						free(cmd);
+					}
+					break;
+				case SDLK_EXCLAIM:
+					if (!(event.key.mod & SDL_KMOD_ALT))
 						break;
-					case SDLK_EXCLAIM:
-						if (!(event.key.mod &
-						      SDL_KMOD_ALT))
-							break;
-						cmd = unix_select_command();
-						if (cmd) {
-							char *selection =
-							    editor_get_selection(
-								ed);
-							char *output =
-							    unix_run_command(
-								cmd, selection);
-
-							if (output) {
-								editor_insert_text(
-								    ed, output,
-								    false);
-								free(output);
-							}
-							if (selection)
-								free(selection);
-							free(cmd);
+					cmd = unix_select_command();
+					if (cmd) {
+						char *selection =
+						    editor_get_selection(ed);
+						char *output = unix_run_command(
+						    cmd, selection);
+						if (output) {
+							editor_insert_text(
+							    ed, output, false);
+							free(output);
 						}
+						if (selection)
+							free(selection);
+						free(cmd);
+					}
+					break;
+				case SDLK_A:
+					if (!(event.key.mod & SDL_KMOD_ALT))
 						break;
-					case SDLK_A:
-						if (!(event.key.mod &
-						      SDL_KMOD_ALT))
-							break;
-						editor_select_all(ed);
-						break;
-					case SDLK_B:
-						if (!(event.key.mod &
-						      SDL_KMOD_ALT) &&
-						    !event.key.repeat)
-							break;
-					case SDLK_LEFT:
-						editor_cursor_left(ed);
-						if (!(event.key.mod &
-						      SDL_KMOD_SHIFT) ||
-						    event.key.repeat)
-							editor_clear_selection(
-							    ed);
-						break;
-					case SDLK_F:
-						if (!(event.key.mod &
-						      SDL_KMOD_ALT) &&
-						    !event.key.repeat)
-							break;
-					case SDLK_RIGHT:
-						editor_cursor_right(ed);
-						if (!(event.key.mod &
-						      SDL_KMOD_SHIFT))
-							editor_clear_selection(
-							    ed);
+					editor_select_all(ed);
+					break;
+				case SDLK_B:
+					if (!(event.key.mod & SDL_KMOD_ALT) &&
+					    !event.key.repeat)
 						break;
-					case SDLK_P:
-						if (!(event.key.mod &
-						      SDL_KMOD_ALT) &&
-						    !event.key.repeat)
-							break;
-					case SDLK_UP:
-						editor_cursor_up(ed);
-						if (!(event.key.mod &
-						      SDL_KMOD_SHIFT))
-							editor_clear_selection(
-							    ed);
+				case SDLK_LEFT:
+					editor_cursor_left(ed);
+					if (!(event.key.mod & SDL_KMOD_SHIFT) ||
+					    event.key.repeat)
+						editor_clear_selection(ed);
+					break;
+				case SDLK_F:
+					if (!(event.key.mod & SDL_KMOD_ALT) &&
+					    !event.key.repeat)
 						break;
-					case SDLK_N:
-						if (!(event.key.mod &
-						      SDL_KMOD_ALT) &&
-						    !event.key.repeat)
-							break;
-					case SDLK_DOWN:
-						editor_cursor_down(ed);
-						if (!(event.key.mod &
-						      SDL_KMOD_SHIFT))
-							editor_clear_selection(
-							    ed);
+				case SDLK_RIGHT:
+					editor_cursor_right(ed);
+					if (!(event.key.mod & SDL_KMOD_SHIFT))
+						editor_clear_selection(ed);
+					break;
+				case SDLK_P:
+					if (!(event.key.mod & SDL_KMOD_ALT) &&
+					    !event.key.repeat)
 						break;
-					}
-				case SDL_EVENT_MOUSE_BUTTON_DOWN:
-					if (event.button.button ==
-					    SDL_BUTTON_LEFT) {
-						float mx, my;
-						SDL_RenderCoordinatesFromWindow(
-						    renderer, event.button.x,
-						    event.button.y, &mx, &my);
-						editor_set_cursor_from_coords(
-						    ed, mx, my, scroll_x,
-						    scroll_y);
-						ed->selection_anchor =
-						    ed->cursor_idx;
-						is_dragging = true;
-					} else if (event.button.button ==
-						   SDL_BUTTON_MIDDLE) {
-						SDL_Keymod mod =
-						    SDL_GetModState();
-						if (mod & SDL_KMOD_ALT) {
-							if (!editor_has_selection(
-								ed)) {
-								float mx, my;
-								SDL_RenderCoordinatesFromWindow(
-								    renderer,
-								    event.button
-									.x,
-								    event.button
-									.y,
-								    &mx, &my);
-								editor_set_cursor_from_coords(
-								    ed, mx, my,
-								    scroll_x,
-								    scroll_y);
-								ed->selection_anchor =
-								    ed->cursor_idx;
-								editor_select_word(
-								    ed);
-							}
-							char *selection =
-							    editor_get_selection(
-								ed);
-							if (selection) {
-								char cmd[1024];
-								snprintf(
-								    cmd,
-								    sizeof(cmd),
-								    "xdg-open "
-								    "%s",
-								    selection);
-								unix_exec_command(
-								    cmd, NULL);
-
-								free(selection);
-							}
-						}
-					}
+				case SDLK_UP:
+					editor_cursor_up(ed);
+					if (!(event.key.mod & SDL_KMOD_SHIFT))
+						editor_clear_selection(ed);
 					break;
-				case SDL_EVENT_MOUSE_BUTTON_UP:
-					if (event.button.button ==
-					    SDL_BUTTON_LEFT) {
-						is_dragging = false;
-					}
+				case SDLK_N:
+					if (!(event.key.mod & SDL_KMOD_ALT) &&
+					    !event.key.repeat)
+						break;
+				case SDLK_DOWN:
+					editor_cursor_down(ed);
+					if (!(event.key.mod & SDL_KMOD_SHIFT))
+						editor_clear_selection(ed);
 					break;
-
-				case SDL_EVENT_MOUSE_MOTION:
-					if (is_dragging) {
-						float mx, my;
-						SDL_RenderCoordinatesFromWindow(
-						    renderer, event.motion.x,
-						    event.motion.y, &mx, &my);
-						editor_set_cursor_from_coords(
-						    ed, mx, my, scroll_x,
-						    scroll_y);
+				case SDLK_W:
+					if (event.key.mod & SDL_KMOD_ALT) {
+						editor_write_file(ed, NULL);
 					}
 					break;
-
-				case SDL_EVENT_TEXT_INPUT:
-					if ((SDL_GetModState() &
-					     (SDL_KMOD_ALT | SDL_KMOD_CTRL))) {
-						break;
+				}
+			case SDL_EVENT_MOUSE_BUTTON_DOWN:
+				if (event.button.button == SDL_BUTTON_LEFT) {
+					float mx, my;
+					SDL_RenderCoordinatesFromWindow(
+					    renderer, event.button.x,
+					    event.button.y, &mx, &my);
+					editor_set_cursor_from_coords(
+					    ed, mx, my, *scroll_x, *scroll_y);
+					ed->selection_anchor = ed->cursor_idx;
+					*is_dragging = true;
+				} else if (event.button.button ==
+					   SDL_BUTTON_MIDDLE) {
+					SDL_Keymod mod = SDL_GetModState();
+					if (mod & SDL_KMOD_ALT) {
+						if (!editor_has_selection(ed)) {
+							float mx, my;
+							SDL_RenderCoordinatesFromWindow(
+							    renderer,
+							    event.button.x,
+							    event.button.y, &mx,
+							    &my);
+							editor_set_cursor_from_coords(
+							    ed, mx, my,
+							    *scroll_x,
+							    *scroll_y);
+							ed->selection_anchor =
+							    ed->cursor_idx;
+							editor_select_word(ed);
+						}
+						char *selection =
+						    editor_get_selection(ed);
+						if (selection) {
+							char sys_cmd[1024];
+							snprintf(
+							    sys_cmd,
+							    sizeof(sys_cmd),
+							    "xdg-open %s",
+							    selection);
+							unix_exec_command(
+							    sys_cmd, NULL);
+							free(selection);
+						}
 					}
-					editor_insert_text(ed, event.text.text,
-							   true);
+				}
+				break;
+			case SDL_EVENT_MOUSE_BUTTON_UP:
+				if (event.button.button == SDL_BUTTON_LEFT) {
+					*is_dragging = false;
+				}
+				break;
+			case SDL_EVENT_MOUSE_MOTION:
+				if (*is_dragging) {
+					float mx, my;
+					SDL_RenderCoordinatesFromWindow(
+					    renderer, event.motion.x,
+					    event.motion.y, &mx, &my);
+					editor_set_cursor_from_coords(
+					    ed, mx, my, *scroll_x, *scroll_y);
+				}
+				break;
+			case SDL_EVENT_TEXT_INPUT:
+				if ((SDL_GetModState() &
+				     (SDL_KMOD_ALT | SDL_KMOD_CTRL))) {
 					break;
 				}
-			} while (SDL_PollEvent(&event));
-		}
-
-		// --- RENDER PHASE ---
-
-		int render_w, render_h;
-		SDL_GetRenderOutputSize(renderer, &render_w, &render_h);
+				editor_insert_text(ed, event.text.text, true);
+				break;
+			}
+		} while (SDL_PollEvent(&event));
+	}
+}
 
-		SDL_SetRenderDrawColor(renderer, 255, 255, 255, 255);
-		SDL_RenderClear(renderer);
-		if (ed->cursor_idx != ed->selection_anchor) {
-			int sel_min = (ed->cursor_idx < ed->selection_anchor)
-					  ? ed->cursor_idx
-					  : ed->selection_anchor;
-			int sel_max = (ed->cursor_idx > ed->selection_anchor)
-					  ? ed->cursor_idx
-					  : ed->selection_anchor;
+void render_editor(SDL_Renderer *renderer, Editor *ed, TTF_Font *font,
+		   int char_width, float line_height, float cursor_height,
+		   float scroll_x, float scroll_y) {
+	int render_w, render_h;
+	SDL_GetRenderOutputSize(renderer, &render_w, &render_h);
 
-			SDL_SetRenderDrawColor(renderer, 200, 220, 255,
-					       255); // Light Blue
+	SDL_SetRenderDrawColor(renderer, 255, 255, 255, 255);
+	SDL_RenderClear(renderer);
 
-			int cur_row = 0, cur_col = 0;
-			const char *p = ed->buffer;
-			while (p < ed->buffer + ed->length) {
-				int start_of_char_idx = (int)(p - ed->buffer);
+	if (ed->cursor_idx != ed->selection_anchor) {
+		int sel_min = (ed->cursor_idx < ed->selection_anchor)
+				  ? ed->cursor_idx
+				  : ed->selection_anchor;
+		int sel_max = (ed->cursor_idx > ed->selection_anchor)
+				  ? ed->cursor_idx
+				  : ed->selection_anchor;
 
-				Uint32 cp = SDL_StepUTF8(&p, NULL);
+		SDL_SetRenderDrawColor(renderer, 200, 220, 255,
+				       255); // Light Blue
 
-				int cols_for_char = 1;
-				if (cp == '\t') {
-					cols_for_char =
-					    TAB_SIZE - (cur_col % TAB_SIZE);
-				}
+		int cur_row = 0, cur_col = 0;
+		const char *p = ed->buffer;
+		while (p < ed->buffer + ed->length) {
+			int start_of_char_idx = (int)(p - ed->buffer);
 
+			EditorRange *r =
+			    editor_get_range_at(ed, start_of_char_idx);
+			if (r) {
 				if (start_of_char_idx >= sel_min &&
 				    start_of_char_idx < sel_max) {
 					SDL_FRect sel_rect = {
@@ -310,119 +244,214 @@ int main(int argc, char *argv[]) {
 						scroll_x,
 					    20.0f + (cur_row * line_height) -
 						scroll_y,
-					    (float)(cols_for_char *
-						    char_width), 
+					    (float)(r->visual_cols *
+						    char_width),
 					    cursor_height};
 					SDL_RenderFillRect(renderer, &sel_rect);
 				}
-
-				if (cp == '\n') {
-					cur_row++;
-					cur_col = 0;
-				} else {
-					cur_col += cols_for_char;
-				}
-
+				cur_col += r->visual_cols;
+				p = ed->buffer + r->end_byte;
 				if ((int)(p - ed->buffer) >= sel_max)
 					break;
+				continue;
+			}
+
+			Uint32 cp = SDL_StepUTF8(&p, NULL);
+			int cols_for_char = 1;
+			if (cp == '\t') {
+				cols_for_char = TAB_SIZE - (cur_col % TAB_SIZE);
+			}
+
+			if (start_of_char_idx >= sel_min &&
+			    start_of_char_idx < sel_max) {
+				SDL_FRect sel_rect = {
+				    20.0f + (cur_col * char_width) - scroll_x,
+				    20.0f + (cur_row * line_height) - scroll_y,
+				    (float)(cols_for_char * char_width),
+				    cursor_height};
+				SDL_RenderFillRect(renderer, &sel_rect);
+			}
+
+			if (cp == '\n') {
+				cur_row++;
+				cur_col = 0;
+			} else {
+				cur_col += cols_for_char;
 			}
+
+			if ((int)(p - ed->buffer) >= sel_max)
+				break;
 		}
+	}
 
-		if (font && ed->length > 0) {
-			SDL_Color black = {0, 0, 0, 255};
-
-			float current_y = 20.0f - scroll_y;
-
-			const char *line_start = ed->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) {
-
-					// Expand tabs into spaces for correct
-					// rendering alignment
-					char *temp_line =
-					    malloc(len * TAB_SIZE + 1);
-					int t_idx = 0;
-					int vis_col = 0;
-
-					for (size_t i = 0; i < len; i++) {
-						if (line_start[i] == '\t') {
-							int spaces = TAB_SIZE -
-								     (vis_col %
-								      TAB_SIZE);
-							for (int s = 0;
-							     s < spaces; s++) {
-								temp_line
-								    [t_idx++] =
-									' ';
-							}
-							vis_col += spaces;
-						} else {
-							if ((line_start[i] &
-							     0xC0) != 0x80)
-								vis_col++; // Track
-									   // start
-									   // bytes
+	if (font && ed->length > 0) {
+		SDL_Color black = {0, 0, 0, 255};
+		float current_y = 20.0f - scroll_y;
+
+		const char *line_start = ed->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) {
+				char *temp_line = malloc(
+				    len * TAB_SIZE +
+				    256); // Added padding for range visuals
+				int t_idx = 0;
+				int vis_col = 0;
+
+				for (size_t i = 0; i < len; i++) {
+					size_t abs_idx =
+					    (line_start - ed->buffer) + i;
+					EditorRange *r = editor_get_range_at(
+					    ed, (int)abs_idx);
+
+					if (r) {
+						for (int v = 0;
+						     v < r->visual_cols; v++) {
 							temp_line[t_idx++] =
-							    line_start[i];
+							    ' ';
+						}
+						vis_col += r->visual_cols;
+
+						// Safety handling if the fold
+						// bounds across the new line
+						if (r->end_byte >
+						    (line_start - ed->buffer) +
+							len) {
+							const char
+							    *next_line_start =
+								ed->buffer +
+								r->end_byte;
+							line_end =
+							    next_line_start - 1;
+							break;
+						} else {
+							i = (size_t)(r->end_byte -
+								     (line_start -
+								      ed->buffer)) -
+							    1;
 						}
+						continue;
 					}
-					temp_line[t_idx] = '\0';
-					SDL_Surface *surf =
-					    TTF_RenderText_Blended(
-						font, temp_line, t_idx, black);
-					free(temp_line);
-
-					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);
+
+					if (line_start[i] == '\t') {
+						int spaces =
+						    TAB_SIZE -
+						    (vis_col % TAB_SIZE);
+						for (int s = 0; s < spaces;
+						     s++) {
+							temp_line[t_idx++] =
+							    ' ';
+						}
+						vis_col += spaces;
+					} else {
+						if ((line_start[i] & 0xC0) !=
+						    0x80)
+							vis_col++;
+						temp_line[t_idx++] =
+						    line_start[i];
 					}
 				}
-				current_y += line_height;
-				line_start = line_end ? line_end + 1 : NULL;
+				temp_line[t_idx] = '\0';
+				SDL_Surface *surf = TTF_RenderText_Blended(
+				    font, temp_line, t_idx, black);
+				free(temp_line);
+
+				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;
 		}
+	}
 
-		int cur_row = 0, cur_col = 0;
-		const char *ptr = ed->buffer;
-		while (ptr < ed->buffer + ed->cursor_idx) {
-			Uint32 cp = SDL_StepUTF8(&ptr, NULL);
-			if (cp == '\t') {
-				cur_col += TAB_SIZE - (cur_col % TAB_SIZE);
-			} else if (cp == '\n') {
-				cur_row++;
-				cur_col = 0;
-			} else {
-				cur_col++;
-			}
-		}
+	int cur_row = 0, cur_col = 0;
+	const char *ptr = ed->buffer;
+	while (ptr < ed->buffer + ed->cursor_idx) {
+		int current_idx = (int)(ptr - ed->buffer);
+		EditorRange *r = editor_get_range_at(ed, current_idx);
 
-		SDL_FRect cursor_rect = {
-		    20.0f + (cur_col * char_width) - scroll_x,
-		    20.0f + (cur_row * line_height) - scroll_y, 2.0f,
-		    (float)cursor_height};
+		if (r) {
+			cur_col += r->visual_cols;
+			ptr = ed->buffer + r->end_byte;
+			continue;
+		}
 
-		if (cursor_rect.y + line_height > 0 &&
-		    cursor_rect.y < render_h) {
-			SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
-			SDL_RenderFillRect(renderer, &cursor_rect);
+		Uint32 cp = SDL_StepUTF8(&ptr, NULL);
+		if (cp == '\t') {
+			cur_col += TAB_SIZE - (cur_col % TAB_SIZE);
+		} else if (cp == '\n') {
+			cur_row++;
+			cur_col = 0;
+		} else {
+			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)cursor_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);
+}
+
+int main(int argc, char *argv[]) {
+	// Otherwise wayland just wouldn't load.
+	SDL_SetHintWithPriority(SDL_HINT_VIDEO_DRIVER, "wayland",
+				SDL_HINT_OVERRIDE);
+
+	SDL_Init(SDL_INIT_VIDEO);
+	TTF_Init();
+
+	SDL_Window *window = SDL_CreateWindow(
+	    "esc", 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("IosevkaTermSS13-Extended.ttf", 24.0f);
+	int char_width;
+	TTF_GetGlyphMetrics(font, 'A', NULL, NULL, NULL, NULL, &char_width);
+	float line_height = (float)TTF_GetFontHeight(font) * 1.5;
+	float cursor_height = (float)TTF_GetFontHeight(font);
+	Editor *ed = editor_create(char_width, line_height);
+	if (argc > 1) {
+    		setenv("ESC_BUFFILE", argv[1], true);
+		editor_load_file(ed, argv[1]);
+	}
 
-		SDL_RenderPresent(renderer);
+	float scroll_x = 0.0f;
+	float scroll_y = 0.0f;
+	bool running = true;
+	bool is_dragging = false;
+
+	SDL_StartTextInput(window);
+
+
+	while (running) {
+		handle_events(ed, renderer, &scroll_x, &scroll_y, line_height,
+			      &running, &is_dragging);
+		render_editor(renderer, ed, font, char_width, line_height,
+			      cursor_height, scroll_x, scroll_y);
 	}
 
 	editor_destroy(ed);
diff --git a/unix_utils.c b/unix_utils.c
@@ -7,7 +7,7 @@
 #include <unistd.h>
 
 char *unix_select_command(void) {
-	const char *menu_cmd = getenv("EI_MENU");
+	const char *menu_cmd = getenv("ESC_MENU");
 	if (!menu_cmd)
 		menu_cmd = "wmenu";