From 4bdb4c8c5dc0115d7c570acaee0ca636bfe0d3ee Mon Sep 17 00:00:00 2001 From: Loic Coenen Date: Tue, 19 May 2026 17:04:15 +0000 Subject: [PATCH] feat: add fzf-based file/plugin/port selection and update build system --- client/makefile | 25 ++++++--- client/src/carla_host.c | 44 +++++++++++++++ client/src/log.c | 2 +- client/src/script.c | 100 +++++++++++++++++++++++++++++++++++ client/src/script.h | 2 + client/src/tui.c | 76 ++++++++++++++++++++++++++ client/src/tui.h | 3 ++ client/tests/test_tui_stub.c | 5 ++ orchestrator.c | 1 + tests/test_tui_stub.c | 32 +++++++++++ 10 files changed, 281 insertions(+), 9 deletions(-) create mode 100644 client/tests/test_tui_stub.c create mode 100644 tests/test_tui_stub.c diff --git a/client/makefile b/client/makefile index 0a060ea..5c1e7cc 100644 --- a/client/makefile +++ b/client/makefile @@ -1,5 +1,5 @@ CC = gcc -CFLAGS = -Wall -Wextra -Wpedantic -std=c11 -Isrc +CFLAGS = -Wall -Wextra -Wpedantic -std=gnu11 -Isrc -I../engine/src CARLA_INC = -I/usr/include/carla -I/usr/include/carla/includes CARLA_LIB = -L/usr/lib/carla -Wl,-rpath,/usr/lib/carla -lcarla_standalone2 @@ -22,20 +22,20 @@ all: looper-client test_status_parse looper-client: src/main.c src/tui.c $(PLUGINS_OBJ) $(CARLA_OBJ) $(CLIENT_CMD_OBJ) $(SCRIPT_OBJ) $(LOG_OBJ) $(CC) $(CFLAGS) $(CARLA_INC) -o $@ $^ $(CARLA_LIB) -ljack -lncurses -test_status_parse: tests/test_status_parse.c $(PLUGINS_OBJ) $(CARLA_OBJ) $(CLIENT_CMD_OBJ) - $(CC) $(CFLAGS) $(CARLA_INC) -o test_status_parse tests/test_status_parse.c src/tui.c $(PLUGINS_OBJ) $(CARLA_OBJ) $(CLIENT_CMD_OBJ) $(CARLA_LIB) -ljack -lncurses +test_status_parse: tests/test_status_parse.c $(PLUGINS_OBJ) $(CARLA_OBJ) $(CLIENT_CMD_OBJ) $(SCRIPT_OBJ) $(LOG_OBJ) + $(CC) $(CFLAGS) $(CARLA_INC) -o test_status_parse tests/test_status_parse.c src/tui.c $(PLUGINS_OBJ) $(CARLA_OBJ) $(CLIENT_CMD_OBJ) $(SCRIPT_OBJ) $(LOG_OBJ) $(CARLA_LIB) -ljack -lncurses # --- Plugin stubs (now real) --- $(PLUGINS_OBJ): src/plugins.c src/plugins.h $(CC) $(CFLAGS) $(CARLA_INC) -c -o $@ $< $(CARLA_OBJ): src/carla_host.c src/carla_host.h - $(CC) -Wall -Wextra -std=c11 -Isrc $(CARLA_INC) -c -o $@ $< + $(CC) -Wall -Wextra -std=gnu11 -Isrc -I../engine/src $(CARLA_INC) -c -o $@ $< CARLA_TEST_OBJ = src/carla_host_test.o $(CARLA_TEST_OBJ): src/carla_host.c src/carla_host.h - $(CC) -Wall -Wextra -std=c11 -Isrc $(CARLA_INC) -DTESTING -c -o $@ $< + $(CC) -Wall -Wextra -std=gnu11 -Isrc -I../engine/src $(CARLA_INC) -DTESTING -c -o $@ $< $(CLIENT_CMD_OBJ): src/client_cmd.c src/client_cmd.h $(CC) $(CFLAGS) $(CARLA_INC) -c -o $@ $< @@ -51,8 +51,17 @@ TEST_SCRIPT_OBJ = tests/test_script.o $(TEST_SCRIPT_OBJ): tests/test_script.c src/script.h $(CC) $(CFLAGS) -c -o $@ $< -$(TEST_SCRIPT_BIN): $(TEST_SCRIPT_OBJ) $(SCRIPT_OBJ) - $(CC) $(CFLAGS) -o $@ $^ +TEST_TUI_STUB_OBJ = tests/test_tui_stub.o + +tests/test_tui_stub.c: + mkdir -p tests + @printf '%s\n' '#include "tui.h"' '' 'char *tui_fzf_select(const char *const items[], size_t count, const char *prompt){(void)items;(void)count;(void)prompt;return NULL;}' '' 'void tui_cleanup(void){}' > $@ + +$(TEST_TUI_STUB_OBJ): tests/test_tui_stub.c + $(CC) $(CFLAGS) -c -o $@ $< + +$(TEST_SCRIPT_BIN): $(TEST_SCRIPT_OBJ) $(SCRIPT_OBJ) $(PLUGINS_OBJ) $(CLIENT_CMD_OBJ) $(CARLA_OBJ) $(TEST_TUI_STUB_OBJ) + $(CC) $(CFLAGS) $(CARLA_INC) -o $@ $^ $(CARLA_LIB) -ljack -lncurses $(LOG_OBJ): src/log.c $(CC) $(CFLAGS) -c -o $@ $< @@ -101,7 +110,7 @@ TEST_CARLA_MOCK_BIN = test_carla_host_mock CARLA_MOCK_OBJ = src/carla_host_mock.o $(CARLA_MOCK_OBJ): src/carla_host.c src/carla_host.h - $(CC) -Wall -Wextra -std=c11 -Isrc $(CARLA_INC) -DTESTING -DMOCK_JACK -c -o $@ $< + $(CC) -Wall -Wextra -std=gnu11 -Isrc -I../engine/src $(CARLA_INC) -DTESTING -DMOCK_JACK -c -o $@ $< $(TEST_CARLA_MOCK_BIN): tests/test_carla_host_mock.c $(CARLA_MOCK_OBJ) $(CC) $(CFLAGS) $(CARLA_INC) -DTESTING -DMOCK_JACK -o $@ $^ $(CARLA_LIB) -ljack diff --git a/client/src/carla_host.c b/client/src/carla_host.c index fdba62f..2c343e2 100644 --- a/client/src/carla_host.c +++ b/client/src/carla_host.c @@ -1,3 +1,5 @@ +#define _GNU_SOURCE +#include #include #include #include @@ -206,6 +208,48 @@ int carla_disconnect_plugin(int id) { return any ? 0 : -1; // return -1 if no connections were found (harmless) } +#ifdef MOCK_JACK +/* Mock: return a few fake port names */ +int carla_get_ports(const char *type, char ***ports, int *count) { + (void)type; + static const char *fake[] = {"system:capture_1", "system:capture_2", "system:playback_1", "system:playback_2"}; + *count = 4; + *ports = malloc(*count * sizeof(char*)); + if (!*ports) { *count = 0; return -1; } + for (int i = 0; i < *count; i++) + (*ports)[i] = strdup(fake[i]); + return 0; +} +#else +#include +int carla_get_ports(const char *type, char ***ports, int *count) { + (void)type; + if (!jack_client) { + *ports = NULL; + *count = 0; + return -1; + } + const char **jports = jack_get_ports(jack_client, NULL, NULL, 0); + if (!jports) { + *ports = NULL; + *count = 0; + return -1; + } + int n = 0; + while (jports[n]) n++; + *count = n; + *ports = malloc(n * sizeof(char*)); + if (!*ports) { + jack_free(jports); + return -1; + } + for (int i = 0; i < n; i++) + (*ports)[i] = strdup(jports[i]); + jack_free(jports); + return 0; +} +#endif + #ifdef TESTING int carla_test_connection_count(void) { return conn_count; diff --git a/client/src/log.c b/client/src/log.c index 1d88176..c74973c 100644 --- a/client/src/log.c +++ b/client/src/log.c @@ -1 +1 @@ -#include "../engine/src/log.c" +#include "../../engine/src/log.c" diff --git a/client/src/script.c b/client/src/script.c index 1d42e2b..722dc51 100644 --- a/client/src/script.c +++ b/client/src/script.c @@ -4,10 +4,16 @@ #include #include #include +/* Forward declarations for functions used from carla_host.c */ +int carla_load(const char *binary, const char *plugin_id, int *out_id); +int carla_get_ports(const char *type, char ***ports, int *count); +#include "tui.h" +#include #define MAX_NOTES 128 static char *note_actions[MAX_NOTES] = {0}; +char g_selected_port[256] = {0}; int script_load(const char *path) { FILE *fp = fopen(path, "r"); @@ -45,6 +51,100 @@ int script_load(const char *path) { return 0; } +int script_handle_fzf_command(const char *type) { + if (!type) return -1; + + if (strcmp(type, "sample") == 0) { + // Get sample directory from env or home + const char *dir = getenv("LOOPER_SAMPLE_DIR"); + if (!dir) dir = getenv("HOME"); + if (!dir) dir = "."; + char pattern[1024]; + snprintf(pattern, sizeof(pattern), "%s/**/*.wav", dir); + glob_t g; + if (glob(pattern, GLOB_TILDE, NULL, &g) != 0) { + // Try without wildcard + snprintf(pattern, sizeof(pattern), "%s/*.wav", dir); + if (glob(pattern, GLOB_TILDE, NULL, &g) != 0) + return -1; + } + const char **items = malloc((g.gl_pathc + 1) * sizeof(char*)); + if (!items) { globfree(&g); return -1; } + for (size_t i = 0; i < g.gl_pathc; i++) + items[i] = g.gl_pathv[i]; + items[g.gl_pathc] = NULL; + + char *selected = tui_fzf_select(items, g.gl_pathc, "Load sample: "); + if (selected) { + int out_id; + carla_load(selected, "", &out_id); + free(selected); + } + free(items); + globfree(&g); + return 0; + } + + if (strcmp(type, "plugin") == 0) { + // List .so files in common paths + const char *dirs[] = { + getenv("VST_PATH") ? getenv("VST_PATH") : "/usr/lib/vst", + getenv("HOME"), + NULL + }; + char **paths = NULL; + size_t count = 0; + for (int d = 0; dirs[d]; d++) { + char pattern[1024]; + snprintf(pattern, sizeof(pattern), "%s/**/*.so", dirs[d]); + glob_t g; + if (glob(pattern, GLOB_TILDE, NULL, &g) == 0) { + for (size_t i = 0; i < g.gl_pathc; i++) { + paths = realloc(paths, (count+1) * sizeof(char*)); + paths[count] = strdup(g.gl_pathv[i]); + count++; + } + globfree(&g); + } + } + if (count == 0) return -1; + const char **items = malloc(count * sizeof(char*)); + for (size_t i = 0; i < count; i++) items[i] = paths[i]; + + char *selected = tui_fzf_select(items, count, "Load plugin: "); + if (selected) { + int out_id; + carla_load(selected, "", &out_id); + free(selected); + } + for (size_t i = 0; i < count; i++) free(paths[i]); + free(paths); + free(items); + return 0; + } + + if (strcmp(type, "from") == 0 || strcmp(type, "to") == 0) { + char **ports; + int count; + // For "to" we need input ports (where output will go), for "from" we need output ports + if (carla_get_ports("audio", &ports, &count) != 0) + return -1; + + char *selected = tui_fzf_select((const char**)ports, count, + (strcmp(type,"to")==0) ? "Select plugin input port: " : "Select looper output port: "); + if (selected) { + strncpy(g_selected_port, selected, sizeof(g_selected_port)-1); + g_selected_port[sizeof(g_selected_port)-1] = '\0'; + free(selected); + } + for (int i = 0; i < count; i++) free(ports[i]); + free(ports); + return (selected ? 0 : -1); + } + + return -1; +} + void script_cleanup(void) { for (int i = 0; i < MAX_NOTES; i++) { free(note_actions[i]); diff --git a/client/src/script.h b/client/src/script.h index 8552c0c..2f387c6 100644 --- a/client/src/script.h +++ b/client/src/script.h @@ -4,5 +4,7 @@ int script_load(const char *path); void script_handle_note(int note); void script_cleanup(void); +int script_handle_fzf_command(const char *type); +extern char g_selected_port[256]; #endif diff --git a/client/src/tui.c b/client/src/tui.c index 5084bd6..36630ae 100644 --- a/client/src/tui.c +++ b/client/src/tui.c @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -350,6 +351,18 @@ void tui_run(void) { rack_mode = !rack_mode; rack_selected = 0; break; + case KEY_F(5): + script_handle_fzf_command("sample"); + draw_grid(); + break; + case KEY_F(6): + script_handle_fzf_command("plugin"); + draw_grid(); + break; + case KEY_F(7): + script_handle_fzf_command("from"); + draw_grid(); + break; case 27: case 'Q': if (rack_mode) { rack_mode = false; @@ -396,6 +409,69 @@ void tui_run(void) { } } +char* tui_fzf_select(const char *const items[], size_t count, const char *prompt) { + if (!items || count == 0) return NULL; + + // Save ncurses state + def_prog_mode(); + endwin(); + + // Build a temporary file with the items list + char tmpfile[] = "/tmp/tui_fzf_XXXXXX"; + int fd = mkstemp(tmpfile); + if (fd == -1) { + reset_prog_mode(); + refresh(); + return NULL; + } + FILE *tmp = fdopen(fd, "w"); + if (!tmp) { + close(fd); + unlink(tmpfile); + reset_prog_mode(); + refresh(); + return NULL; + } + for (size_t i = 0; i < count; i++) { + if (items[i]) + fprintf(tmp, "%s\n", items[i]); + } + fclose(tmp); + + // Build fzf command reading from the temporary file + char cmd[8192]; + snprintf(cmd, sizeof(cmd), + "fzf --prompt='%s' < %s", + prompt ? prompt : "Select: ", + tmpfile); + + FILE *result = popen(cmd, "r"); + if (!result) { + unlink(tmpfile); + reset_prog_mode(); + refresh(); + return NULL; + } + + char selected[4096] = {0}; + if (fgets(selected, sizeof(selected), result) != NULL) { + size_t len = strlen(selected); + if (len > 0 && selected[len-1] == '\n') + selected[len-1] = '\0'; + } + pclose(result); + unlink(tmpfile); + + // Restore ncurses + reset_prog_mode(); + refresh(); + + if (selected[0] == '\0') + return NULL; + + return strdup(selected); +} + void tui_cleanup(void) { if (yank_buffer.clip_indices) free(yank_buffer.clip_indices); /* free script note allocations */ diff --git a/client/src/tui.h b/client/src/tui.h index ebadcb3..18c1a71 100644 --- a/client/src/tui.h +++ b/client/src/tui.h @@ -1,9 +1,12 @@ #ifndef TUI_H #define TUI_H +#include + void tui_init(void); void tui_run(void); void tui_cleanup(void); int send_command(const char *cmd); +char* tui_fzf_select(const char *const items[], size_t count, const char *prompt); #endif diff --git a/client/tests/test_tui_stub.c b/client/tests/test_tui_stub.c new file mode 100644 index 0000000..ae9f822 --- /dev/null +++ b/client/tests/test_tui_stub.c @@ -0,0 +1,5 @@ +#include "tui.h" + +char *tui_fzf_select(const char *const items[], size_t count, const char *prompt){(void)items;(void)count;(void)prompt;return NULL;} + +void tui_cleanup(void){} diff --git a/orchestrator.c b/orchestrator.c index 4e3d588..bf37484 100644 --- a/orchestrator.c +++ b/orchestrator.c @@ -1,3 +1,4 @@ +#define _POSIX_C_SOURCE 199309L #include #include #include diff --git a/tests/test_tui_stub.c b/tests/test_tui_stub.c new file mode 100644 index 0000000..6cbe881 --- /dev/null +++ b/tests/test_tui_stub.c @@ -0,0 +1,32 @@ +/* Stub for tui functions used by script.c in test builds */ +#include +#include +#include +#include "tui.h" + +char *tui_fzf_select(const char *const items[], size_t count, const char *prompt) { + (void)items; + (void)count; + (void)prompt; + return NULL; +} + +void tui_cleanup(void) { + /* no operation */ +} +/* Stub for tui functions used by script.c in test builds */ +#include +#include +#include +#include "tui.h" + +char *tui_fzf_select(const char *const items[], size_t count, const char *prompt) { + (void)items; + (void)count; + (void)prompt; + return NULL; +} + +void tui_cleanup(void) { + /* no operation */ +}