esc

Externally Scriptable Editor

git clone git://mccd.space/esc

main.c (11699B)

      1 #include "editor.h"
      2 #include "fuse_ipc.h"
      3 #include "renderer.h"
      4 #include "treesitter.h"
      5 #include "unix_utils.h"
      6 
      7 #define SEL_STACK_MAX 32
      8 typedef struct {
      9 #ifdef HAVE_TREESITTER
     10 	TsState *ts;
     11 #endif
     12 	int last_click_byte;
     13 	struct { int anchor; int cursor; } stack[SEL_STACK_MAX];
     14 	int stack_len;
     15 } SelectionState;
     16 #include <SDL3/SDL.h>
     17 #include <SDL3/SDL_clipboard.h>
     18 #include <SDL3/SDL_events.h>
     19 #include <SDL3/SDL_keycode.h>
     20 #include <SDL3/SDL_log.h>
     21 #include <SDL3/SDL_main.h>
     22 #include <SDL3/SDL_mouse.h>
     23 #include <SDL3_ttf/SDL_ttf.h>
     24 #include <signal.h>
     25 #include <stdbool.h>
     26 #include <stdio.h>
     27 #include <stdlib.h>
     28 #include <string.h>
     29 #include <unistd.h>
     30 
     31 void handle_events(Editor *ed, SDL_Renderer *renderer, float *scroll_x,
     32 		   float *scroll_y, float line_height, bool *running,
     33 		   bool *is_dragging, SelectionState *sel) {
     34 	SDL_Event event;
     35 	char *cmd;
     36 
     37 	if (SDL_WaitEvent(&event)) {
     38 		do {
     39 			if (event.type == SDL_EVENT_QUIT)
     40 				*running = false;
     41 
     42 			else if (event.type == SDL_EVENT_MOUSE_WHEEL) {
     43 				*scroll_y -= event.wheel.y * line_height;
     44 				*scroll_x -= -event.wheel.x * 30.0f;
     45 
     46 				if (*scroll_x < 0)
     47 					*scroll_x = 0;
     48 				if (*scroll_y < 0)
     49 					*scroll_y = 0;
     50 			}
     51 
     52 			switch (event.type) {
     53 			case SDL_EVENT_KEY_DOWN:
     54 				switch (event.key.key) {
     55 				case SDLK_BACKSPACE:
     56 					editor_delete_back(ed);
     57 					break;
     58 				case SDLK_DELETE:
     59 					editor_delete_forward(ed);
     60 					break;
     61 				case SDLK_TAB:
     62 					editor_insert_text(ed, "\t", true);
     63 					editor_clear_selection(ed);
     64 					break;
     65 				case SDLK_RETURN:
     66 					editor_newline(ed);
     67 					editor_clear_selection(ed);
     68 					break;
     69 				case SDLK_1:
     70 					if (!(event.key.mod & SDL_KMOD_ALT))
     71 						break;
     72 					cmd = unix_select_command();
     73 					if (cmd) {
     74 						char *selection =
     75 						    editor_get_selection(ed);
     76 						char *output = unix_run_command(
     77 						    cmd, selection);
     78 						if (output) {
     79 							editor_insert_text(
     80 							    ed, output, true);
     81 							free(output);
     82 						}
     83 						if (selection)
     84 							free(selection);
     85 						free(cmd);
     86 					}
     87 					break;
     88 				case SDLK_EXCLAIM:
     89 					if (!(event.key.mod & SDL_KMOD_ALT))
     90 						break;
     91 					cmd = unix_select_command();
     92 					if (cmd) {
     93 						char *selection =
     94 						    editor_get_selection(ed);
     95 						char *output = unix_run_command(
     96 						    cmd, selection);
     97 						if (output) {
     98 							editor_insert_text(
     99 							    ed, output, false);
    100 							free(output);
    101 						}
    102 						if (selection)
    103 							free(selection);
    104 						free(cmd);
    105 					}
    106 					break;
    107 				case SDLK_A:
    108 					if (!(event.key.mod & SDL_KMOD_ALT))
    109 						break;
    110 					editor_select_all(ed);
    111 					break;
    112 				case SDLK_C:
    113 					if (!(event.key.mod & SDL_KMOD_ALT))
    114 						break;
    115 					{
    116 						char *sel =
    117 						    editor_get_selection(ed);
    118 						if (sel) {
    119 							SDL_SetClipboardText(
    120 							    sel);
    121 							free(sel);
    122 						}
    123 					}
    124 					break;
    125 				case SDLK_X:
    126 					if (!(event.key.mod & SDL_KMOD_ALT))
    127 						break;
    128 					{
    129 						char *sel =
    130 						    editor_get_selection(ed);
    131 						if (sel) {
    132 							SDL_SetClipboardText(
    133 							    sel);
    134 							free(sel);
    135 							editor_insert_text(
    136 							    ed, "", true);
    137 						}
    138 					}
    139 					break;
    140 				case SDLK_V:
    141 					if (!(event.key.mod & SDL_KMOD_ALT))
    142 						break;
    143 					if (SDL_HasClipboardText()) {
    144 						char *text =
    145 						    SDL_GetClipboardText();
    146 						if (text && *text) {
    147 							editor_insert_text(
    148 							    ed, text, true);
    149 						}
    150 						SDL_free(text);
    151 					}
    152 					break;
    153 				case SDLK_B:
    154 					if (!(event.key.mod & SDL_KMOD_ALT) &&
    155 					    !event.key.repeat)
    156 						break;
    157 				case SDLK_LEFT:
    158 					editor_cursor_left(ed);
    159 					if (!(event.key.mod & SDL_KMOD_SHIFT) ||
    160 					    event.key.repeat)
    161 						editor_clear_selection(ed);
    162 					break;
    163 				case SDLK_F:
    164 					if (!(event.key.mod & SDL_KMOD_ALT) &&
    165 					    !event.key.repeat)
    166 						break;
    167 				case SDLK_RIGHT:
    168 					editor_cursor_right(ed);
    169 					if (!(event.key.mod & SDL_KMOD_SHIFT))
    170 						editor_clear_selection(ed);
    171 					break;
    172 				case SDLK_P:
    173 					if (!(event.key.mod & SDL_KMOD_ALT) &&
    174 					    !event.key.repeat)
    175 						break;
    176 				case SDLK_UP:
    177 					editor_cursor_up(ed);
    178 					if (!(event.key.mod & SDL_KMOD_SHIFT))
    179 						editor_clear_selection(ed);
    180 					break;
    181 				case SDLK_N:
    182 					if (!(event.key.mod & SDL_KMOD_ALT) &&
    183 					    !event.key.repeat)
    184 						break;
    185 				case SDLK_DOWN:
    186 					editor_cursor_down(ed);
    187 					if (!(event.key.mod & SDL_KMOD_SHIFT))
    188 						editor_clear_selection(ed);
    189 					break;
    190 				case SDLK_W:
    191 					if (event.key.mod & SDL_KMOD_ALT) {
    192 						editor_write_file(ed, NULL);
    193 					}
    194 					break;
    195 				case SDLK_Z:
    196 					if (!(event.key.mod & SDL_KMOD_ALT))
    197 						break;
    198 					if (event.key.mod & SDL_KMOD_SHIFT)
    199 						editor_redo(ed);
    200 					else
    201 						editor_undo(ed);
    202 					break;
    203 				}
    204 			case SDL_EVENT_MOUSE_BUTTON_DOWN:
    205 				if (event.button.button == SDL_BUTTON_LEFT) {
    206 					float mx, my;
    207 					SDL_RenderCoordinatesFromWindow(
    208 					    renderer, event.button.x,
    209 					    event.button.y, &mx, &my);
    210 
    211 					int prev_cursor = ed->cursor_idx;
    212 					int prev_anchor = ed->selection_anchor;
    213 					editor_set_cursor_from_coords(
    214 					    ed, mx, my, *scroll_x, *scroll_y);
    215 					int new_pos = ed->cursor_idx;
    216 
    217 					int sel_min = prev_cursor < prev_anchor
    218 					                 ? prev_cursor : prev_anchor;
    219 					int sel_max = prev_cursor > prev_anchor
    220 					                 ? prev_cursor : prev_anchor;
    221 					bool same_pos   = (new_pos == sel->last_click_byte);
    222 					bool within_sel = (prev_cursor != prev_anchor) &&
    223 					                  (new_pos >= sel_min &&
    224 					                   new_pos < sel_max);
    225 
    226 					if (same_pos || within_sel) {
    227 						/* Expansion click */
    228 						ed->cursor_idx       = prev_cursor;
    229 						ed->selection_anchor = prev_anchor;
    230 
    231 						if (sel->stack_len < SEL_STACK_MAX) {
    232 							sel->stack[sel->stack_len].anchor =
    233 							    prev_anchor;
    234 							sel->stack[sel->stack_len].cursor =
    235 							    prev_cursor;
    236 							sel->stack_len++;
    237 						}
    238 
    239 						bool expanded = false;
    240 #ifdef HAVE_TREESITTER
    241 						int new_start, new_end;
    242 						if (sel->ts) {
    243 							editor_lock(ed);
    244 							ts_state_parse(sel->ts,
    245 							    ed->text.data,
    246 							    ed->text.len);
    247 							editor_unlock(ed);
    248 							expanded = ts_state_expand(
    249 							    sel->ts, sel_min, sel_max,
    250 							    &new_start, &new_end);
    251 							if (expanded)
    252 								editor_set_selection(ed,
    253 								    new_start, new_end);
    254 						}
    255 #endif
    256 						if (!expanded) {
    257 							int lvl = sel->stack_len - 1;
    258 							editor_expand_selection(ed, lvl);
    259 						}
    260 						/* No drag after expansion click */
    261 					} else {
    262 						/* Fresh click — move cursor, reset */
    263 						ed->selection_anchor = new_pos;
    264 						sel->last_click_byte = new_pos;
    265 						sel->stack_len = 0;
    266 						*is_dragging = true;
    267 					}
    268 				} else if (event.button.button == SDL_BUTTON_RIGHT) {
    269 					if (sel->stack_len > 0) {
    270 						sel->stack_len--;
    271 						editor_set_selection(ed,
    272 						    sel->stack[sel->stack_len].anchor,
    273 						    sel->stack[sel->stack_len].cursor);
    274 					} else {
    275 						editor_clear_selection(ed);
    276 					}
    277 				} else if (event.button.button ==
    278 					   SDL_BUTTON_MIDDLE) {
    279 					SDL_Keymod mod = SDL_GetModState();
    280 					if (mod & SDL_KMOD_ALT) {
    281 						if (!editor_has_selection(ed)) {
    282 							float mx, my;
    283 							SDL_RenderCoordinatesFromWindow(
    284 							    renderer,
    285 							    event.button.x,
    286 							    event.button.y, &mx,
    287 							    &my);
    288 							editor_set_cursor_from_coords(
    289 							    ed, mx, my,
    290 							    *scroll_x,
    291 							    *scroll_y);
    292 							ed->selection_anchor =
    293 							    ed->cursor_idx;
    294 							editor_select_word(ed);
    295 						}
    296 						char *selection =
    297 						    editor_get_selection(ed);
    298 						if (selection) {
    299 							char sys_cmd[1024];
    300 							snprintf(
    301 							    sys_cmd,
    302 							    sizeof(sys_cmd),
    303 							    "xdg-open %s",
    304 							    selection);
    305 							unix_exec_command(
    306 							    sys_cmd, NULL);
    307 							free(selection);
    308 						}
    309 					} else {
    310 						/* Paste
    311 						 */
    312 						if (SDL_HasPrimarySelectionText()) {
    313 							char *text =
    314 							    SDL_GetPrimarySelectionText();
    315 							if (text) {
    316     								int start = ed->cursor_idx;
    317 								editor_insert_text(
    318 								    ed, text,
    319 								    false);
    320 								ed->selection_anchor = start;
    321 							}
    322 							SDL_free(text);
    323 						}
    324 					}
    325 				}
    326 				break;
    327 			case SDL_EVENT_MOUSE_BUTTON_UP:
    328 				if (event.button.button == SDL_BUTTON_LEFT) {
    329 					*is_dragging = false;
    330 				}
    331 				break;
    332 			case SDL_EVENT_MOUSE_MOTION:
    333 				if (*is_dragging) {
    334 					float mx, my;
    335 					SDL_RenderCoordinatesFromWindow(
    336 					    renderer, event.motion.x,
    337 					    event.motion.y, &mx, &my);
    338 					editor_set_cursor_from_coords(
    339 					    ed, mx, my, *scroll_x, *scroll_y);
    340 				}
    341 				break;
    342 			case SDL_EVENT_TEXT_INPUT:
    343 				if ((SDL_GetModState() &
    344 				     (SDL_KMOD_ALT | SDL_KMOD_CTRL))) {
    345 					break;
    346 				}
    347 				editor_insert_text(ed, event.text.text, true);
    348 				break;
    349 			}
    350 		} while (SDL_PollEvent(&event));
    351 
    352 		if (editor_has_selection(ed)) {
    353 			char *sel = editor_get_selection(ed);
    354 			if (sel) {
    355 				SDL_SetPrimarySelectionText(sel);
    356 				free(sel);
    357 			}
    358 		}
    359 	}
    360 }
    361 
    362 static void sig_quit(int sig) {
    363 	(void)sig;
    364 	SDL_Event ev;
    365 	memset(&ev, 0, sizeof(ev));
    366 	ev.type = SDL_EVENT_QUIT;
    367 	SDL_PushEvent(&ev);
    368 }
    369 
    370 static void handle_completion(int argc, char *argv[]) {
    371 	if (argc < 3) {
    372 		return;
    373 	}
    374 	
    375 	if (strcmp(argv[1], "completion") == 0 && strcmp(argv[2], "bash") == 0) {
    376 		// Handle bash completion
    377 		const char *current_word = getenv("COMP_WORDS");
    378 		const char *cursor_pos = getenv("COMP_POINT");
    379 		
    380 		if (!current_word || !cursor_pos) {
    381 			return;
    382 		}
    383 		
    384 		// Simple file/directory completion
    385 		printf("_filedir\n");
    386 		exit(0);
    387 	}
    388 }
    389 
    390 int main(int argc, char *argv[]) {
    391 	// Handle completion subcommand before initializing SDL
    392 	if (argc >= 2) {
    393 		handle_completion(argc, argv);
    394 	}
    395 	
    396 	// Otherwise wayland just wouldn't load.
    397 	SDL_SetHintWithPriority(SDL_HINT_VIDEO_DRIVER, "wayland",
    398 				SDL_HINT_OVERRIDE);
    399 
    400 	signal(SIGTERM, sig_quit);
    401 	signal(SIGINT, sig_quit);
    402 
    403 	SDL_Init(SDL_INIT_VIDEO);
    404 	TTF_Init();
    405 
    406 	SDL_Window *window = SDL_CreateWindow(
    407 	    "esc", 800, 600,
    408 	    SDL_WINDOW_RESIZABLE | SDL_WINDOW_HIGH_PIXEL_DENSITY);
    409 	SDL_Renderer *renderer = SDL_CreateRenderer(window, NULL);
    410 	SDL_SetRenderLogicalPresentation(renderer, 800, 600,
    411 					 SDL_LOGICAL_PRESENTATION_DISABLED);
    412 
    413 	TTF_Font *font = TTF_OpenFont("IosevkaTermSS13-Extended.ttf", 24.0f);
    414 	TTF_Font *bold_font =
    415 	    TTF_OpenFont("IosevkaTermSS13-ExtendedBold.ttf", 24.0f);
    416 	int char_width;
    417 	TTF_GetGlyphMetrics(font, 'A', NULL, NULL, NULL, NULL, &char_width);
    418 	float line_height = (float)TTF_GetFontHeight(font) * 1.5;
    419 	float cursor_height = (float)TTF_GetFontHeight(font);
    420 	Editor *ed = editor_create(char_width, line_height);
    421 	for (int i = 1; i < argc; i++) {
    422 		editor_open_file(ed, argv[i]);
    423 		editor_parse_ansi_codes(ed);
    424 	}
    425 	FuseIPC *ipc = fuse_ipc_start(ed);
    426 
    427 	RenderCtx ctx;
    428 	render_ctx_init(&ctx, renderer, font, bold_font, char_width,
    429 			line_height, cursor_height);
    430 
    431 	float scroll_x = 0.0f;
    432 	float scroll_y = 0.0f;
    433 	bool running = true;
    434 	bool is_dragging = false;
    435 
    436 	SelectionState sel = { .last_click_byte = -1, .stack_len = 0 };
    437 #ifdef HAVE_TREESITTER
    438 	sel.ts = (ed->files_count > 0 && ed->files[0].path)
    439 	             ? ts_state_create(ed->files[0].path) : NULL;
    440 #endif
    441 
    442 	SDL_StartTextInput(window);
    443 
    444 	while (running) {
    445 		handle_events(ed, renderer, &scroll_x, &scroll_y, line_height,
    446 			      &running, &is_dragging, &sel);
    447 
    448 		ctx.scroll_x = scroll_x;
    449 		ctx.scroll_y = scroll_y;
    450 		editor_lock(ed);
    451 		render_editor(&ctx, ed);
    452 		editor_unlock(ed);
    453 	}
    454 
    455 #ifdef HAVE_TREESITTER
    456 	ts_state_destroy(sel.ts);
    457 #endif
    458 	if (ipc)
    459 		fuse_ipc_stop(ipc);
    460 	editor_destroy(ed);
    461 	TTF_CloseFont(font);
    462 	TTF_CloseFont(bold_font);
    463 	SDL_DestroyRenderer(renderer);
    464 	SDL_DestroyWindow(window);
    465 	TTF_Quit();
    466 	SDL_Quit();
    467 	return 0;
    468 }
    469