esc
Externally Scriptable Editor
git clone git://mccd.space/esc
| Log | Files | Refs | README |
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