esc
Externally Scriptable Editor
git clone git://mccd.space/esc
| Log | Files | Refs | README |
commit 11aba860b262389f612f61f8e6b04da294e3dad2 parent 945f65e2ddc9208567d9fa1cccf05cc765394727 Author: Marc Coquand <marc@coquand.email> Date: Thu, 19 Feb 2026 18:52:38 +0100 Basic unix facilities Diffstat:
| A | README.md | | | 16 | ++++++++++++++++ |
| M | editor.c | | | 15 | +++++++++++++++ |
| M | editor.h | | | 2 | ++ |
| M | main.c | | | 55 | +++++++++++++++++++++++++++++++++++++++++++++++++++++-- |
| A | unix_utils.c | | | 112 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | unix_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