esc

Externally Scriptable Editor

git clone git://mccd.space/esc

commit 11aba860b262389f612f61f8e6b04da294e3dad2
parent 945f65e2ddc9208567d9fa1cccf05cc765394727
Author: Marc Coquand <marc@coquand.email>
Date:   Thu, 19 Feb 2026 18:52:38 +0100

Basic unix facilities

Diffstat:
AREADME.md | 16++++++++++++++++
Meditor.c | 15+++++++++++++++
Meditor.h | 2++
Mmain.c | 55+++++++++++++++++++++++++++++++++++++++++++++++++++++--
Aunix_utils.c | 112+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aunix_utils.h | 13+++++++++++++
6 files changed, 211 insertions(+), 2 deletions(-)
diff --git a/README.md b/README.md
@@ -0,0 +1,16 @@
+# ei
+
+An externally extensible text editor
+
+## Dependencies
+
+- SDL3
+- Some menu set to `$MENU`
+- Notification daemon
+
+# Compiling
+
+```
+gcc main.c unix_utils.* editor.* -o minimal_editor $(pkg-config --cflags --libs sdl3 sdl3-ttf)
+```
+
diff --git a/editor.c b/editor.c
@@ -207,3 +207,18 @@ void editor_set_cursor_from_coords(Editor *ed, float mx, float my,
 	}
 	ed->cursor_idx = (int)(ptr - ed->buffer);
 }
+
+char* editor_get_selection(Editor *ed) {
+    if (ed->cursor_idx == ed->selection_anchor) return NULL;
+
+    int start = (ed->cursor_idx < ed->selection_anchor) ? ed->cursor_idx : ed->selection_anchor;
+    int end = (ed->cursor_idx > ed->selection_anchor) ? ed->cursor_idx : ed->selection_anchor;
+    int len = end - start;
+
+    char *result = malloc(len + 1);
+    if (!result) return NULL;
+    
+    memcpy(result, &ed->buffer[start], len);
+    result[len] = '\0';
+    return result;
+}
diff --git a/editor.h b/editor.h
@@ -42,4 +42,6 @@ void editor_clear_selection(Editor *ed);
 void editor_select_all(Editor *ed);
 void editor_goto_pos(Editor *ed, int pos);
 
+char* editor_get_selection(Editor *ed);
+
 #endif
diff --git a/main.c b/main.c
@@ -1,12 +1,15 @@
 #include "editor.h"
+#include "unix_utils.h"
 #include <SDL3/SDL.h>
 #include <SDL3/SDL_events.h>
 #include <SDL3/SDL_keycode.h>
 #include <SDL3/SDL_main.h>
 #include <SDL3_ttf/SDL_ttf.h>
 #include <stdbool.h>
+#include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
+#include <unistd.h>
 
 int main(int argc, char *argv[]) {
 	// Otherwise wayland just wouldn't load.
@@ -33,7 +36,9 @@ int main(int argc, char *argv[]) {
 	bool running = true;
 	bool is_dragging = false;
 
+	char *cmd;
 	SDL_StartTextInput(window);
+
 	bool is_selecting = false;
 	Editor *ed = editor_create(char_width, line_height);
 	ed->cursor_idx = 0;
@@ -74,15 +79,61 @@ int main(int argc, char *argv[]) {
 						editor_newline(ed);
 						editor_clear_selection(ed);
 						break;
+					case SDLK_U:
+						if (!(event.key.mod &
+						      SDL_KMOD_ALT))
+							break;
+						char *cmd =
+						    unix_select_command();
+						if (cmd) {
+							char *selection =
+							    editor_get_selection(
+								ed);
+							unix_run_notify(
+							    cmd, selection);
+							if (selection)
+								free(selection);
+							free(cmd);
+						}
+						break;
+					case SDLK_E:
+						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 &&
+							    strlen(output) >
+								0) {
+								editor_insert_text(
+								    ed, output,
+								    true);
+							}
+
+							if (output)
+								free(output);
+							if (selection)
+								free(selection);
+							free(cmd);
+						}
+						break;
 					case SDLK_A:
 						if (!(event.key.mod &
-						     SDL_KMOD_ALT))
+						      SDL_KMOD_ALT))
 							break;
 						editor_select_all(ed);
 						break;
 					case SDLK_B:
 						if (!(event.key.mod &
-						     SDL_KMOD_ALT) &&
+						      SDL_KMOD_ALT) &&
 						    !event.key.repeat)
 							break;
 					case SDLK_LEFT:
diff --git a/unix_utils.c b/unix_utils.c
@@ -0,0 +1,112 @@
+#include "unix_utils.h"
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+char *unix_select_command(void) {
+	const char *menu_cmd = getenv("EI_MENU");
+	if (!menu_cmd)
+		menu_cmd = "wmenu";
+
+	// Build the PATH-searching pipeline
+	char menu_call[1024];
+	snprintf(
+	    menu_call, sizeof(menu_call),
+	    "echo $PATH | tr ':' '\\n' | xargs -I{} find {} -maxdepth 1 "
+	    "-executable -type f -printf \"%%f\\n\" 2>/dev/null | sort -u | %s",
+	    menu_cmd);
+
+	FILE *pipe = popen(menu_call, "r");
+	if (!pipe)
+		return NULL;
+
+	char *line = NULL;
+	size_t len = 0;
+	if (getline(&line, &len, pipe) != -1) {
+		line[strcspn(line, "\n")] = 0; // Strip newline
+	} else {
+		free(line);
+		line = NULL;
+	}
+	pclose(pipe);
+	return line;
+}
+
+char *unix_run_command(const char *command, const char *input) {
+	int pipe_in[2];
+	int pipe_out[2];
+
+	if (pipe(pipe_in) < 0 || pipe(pipe_out) < 0)
+		return NULL;
+
+	pid_t pid = fork();
+	if (pid == 0) {
+		dup2(pipe_in[0], STDIN_FILENO);
+		dup2(pipe_out[1], STDOUT_FILENO);
+		dup2(pipe_out[1], STDERR_FILENO);
+
+		close(pipe_in[0]);
+		close(pipe_in[1]);
+		close(pipe_out[0]);
+		close(pipe_out[1]);
+
+		execl("/bin/sh", "sh", "-c", command, NULL);
+		exit(1);
+	}
+
+	close(pipe_in[0]);
+	close(pipe_out[1]);
+
+	if (input && strlen(input) > 0) {
+		write(pipe_in[1], input, strlen(input));
+
+		if (input[strlen(input) - 1] != '\n') {
+			write(pipe_in[1], "\n", 1);
+		}
+	}
+	close(pipe_in[1]);
+
+	char *out_buf = NULL;
+	size_t out_size = 0;
+	FILE *mstream = open_memstream(&out_buf, &out_size);
+
+	char chunk[1024];
+	ssize_t n;
+	while ((n = read(pipe_out[0], chunk, sizeof(chunk))) > 0) {
+		fwrite(chunk, 1, n, mstream);
+	}
+
+	fclose(mstream);
+	close(pipe_out[0]);
+	waitpid(pid, NULL, 0);
+
+	// Remove trailing newlines
+	// ... This might not be needed
+	if (out_buf && out_size > 0) {
+		if (out_buf[out_size - 1] == '\n') {
+			out_buf[out_size - 1] = '\0';
+			if (out_size > 1 && out_buf[out_size - 2] == '\r') {
+				out_buf[out_size - 2] = '\0';
+			}
+		}
+	}
+
+	return out_buf;
+}
+
+void unix_run_notify(const char *command, const char *input) {
+    char *output = unix_run_command(command, input);
+    
+    if (!output) return;
+
+    if (fork() == 0) {
+        // Milisecond expire time
+        execlp("notify-send", "notify-send", "--expire-time=10000", "--app-name=ei", command, output, NULL);
+        
+        exit(1);
+    }
+
+    free(output);
+}
diff --git a/unix_utils.h b/unix_utils.h
@@ -0,0 +1,13 @@
+#ifndef UNIX_UTILS_H
+#define UNIX_UTILS_H
+
+// Returns a heap-allocated string of the command chosen via $MENU.
+// Returns NULL if cancelled or failed.
+char* unix_select_command(void);
+
+// Pipes 'input' into 'command' and returns the captured stdout.
+// Uses memstreams for easy string building.
+char *unix_run_command(const char *command, const char *input);
+
+void unix_run_notify(const char *command, const char *input);
+#endif