feat: add JACK port management, VU meter, fzf integration, and e2e tests
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
CC = gcc
|
||||
CFLAGS = -Wall -Wextra -Wpedantic -std=c11 -Isrc -I../engine/src
|
||||
CFLAGS = -Wall -Wextra -Wpedantic -std=c11 -Isrc -I../engine/src -fsanitize=address -fno-omit-frame-pointer
|
||||
|
||||
CARLA_INC = -I/usr/include/carla -I/usr/include/carla/includes
|
||||
CARLA_LIB = -L/usr/lib/carla -Wl,-rpath,/usr/lib/carla -lcarla_standalone2
|
||||
|
||||
@@ -20,22 +21,22 @@ TEST_INTEGRATION_BIN = test_integration
|
||||
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
|
||||
$(CC) $(CFLAGS) $(CARLA_INC) -fsanitize=address -o $@ $^ $(CARLA_LIB) -ljack -lncurses
|
||||
|
||||
test_status_parse: tests/test_status_parse.c $(PLUGINS_OBJ) $(CARLA_OBJ) $(CLIENT_CMD_OBJ) $(SCRIPT_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) $(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 +52,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 $@ $<
|
||||
@@ -84,7 +94,7 @@ TEST_CLIENT_OBJ = tests/test_client.o
|
||||
$(TEST_CLIENT_OBJ): tests/test_client.c src/tui.h
|
||||
$(CC) $(CFLAGS) $(CARLA_INC) -c -o $@ $<
|
||||
|
||||
$(TEST_CLIENT_BIN): $(TEST_CLIENT_OBJ) src/tui.c $(PLUGINS_OBJ) $(CARLA_OBJ) $(CLIENT_CMD_OBJ) $(SCRIPT_OBJ)
|
||||
$(TEST_CLIENT_BIN): $(TEST_CLIENT_OBJ) src/tui.c $(PLUGINS_OBJ) $(CARLA_OBJ) $(CLIENT_CMD_OBJ) $(SCRIPT_OBJ) $(LOG_OBJ)
|
||||
$(CC) $(CFLAGS) $(CARLA_INC) -o $@ $^ $(CARLA_LIB) -ljack -lncurses
|
||||
|
||||
# --- Carla host tests ---
|
||||
@@ -101,7 +111,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
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
#define _GNU_SOURCE
|
||||
#include <stdlib.h>
|
||||
#include <CarlaHost.h>
|
||||
#include <CarlaBackend.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include "carla_host.h"
|
||||
|
||||
@@ -55,6 +58,11 @@ int carla_init_jack(void) {
|
||||
jack_status_t status;
|
||||
jack_client = jack_client_open("looper-connector", JackNoStartServer, &status);
|
||||
// It's okay if jack_client is NULL; we still try Carla
|
||||
if (jack_client) {
|
||||
if (jack_activate(jack_client) != 0) {
|
||||
fprintf(stderr, "WARN: could not activate looper-connector JACK client\n");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// 2) Create the Carla host handle
|
||||
@@ -80,6 +88,28 @@ int carla_init_jack(void) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int carla_connect_direct(const char *source, const char *target) {
|
||||
if (!source || !target) return -1;
|
||||
if (!jack_client) return -1;
|
||||
int ret = jack_connect(jack_client, source, target);
|
||||
if (ret != 0) {
|
||||
fprintf(stderr, "JACK connect failed %s -> %s (ret=%d)\n", source, target, ret);
|
||||
return ret;
|
||||
}
|
||||
// Store the connection so get_connected_port can find it
|
||||
if (conn_count < MAX_CONNECTIONS) {
|
||||
strncpy(connections[conn_count].plugin_port, source,
|
||||
sizeof(connections[conn_count].plugin_port)-1);
|
||||
connections[conn_count].plugin_port[sizeof(connections[conn_count].plugin_port)-1] = '\0';
|
||||
strncpy(connections[conn_count].looper_port, target,
|
||||
sizeof(connections[conn_count].looper_port)-1);
|
||||
connections[conn_count].looper_port[sizeof(connections[conn_count].looper_port)-1] = '\0';
|
||||
connections[conn_count].plugin_id = -1; // direct connection
|
||||
conn_count++;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void carla_cleanup_jack(void) {
|
||||
if (handle != NULL) {
|
||||
carla_engine_close(handle);
|
||||
@@ -132,26 +162,35 @@ int carla_unload(int id) {
|
||||
|
||||
int carla_connect(int id, const char *port_name, const char *looper_port) {
|
||||
// Check that the plugin id is valid
|
||||
if (id < 0 || id >= plugin_count)
|
||||
if (id < 0 || id >= plugin_count) {
|
||||
fprintf(stderr, "CARLA_CONNECT: invalid plugin id %d (plugin_count=%d)\n", id, plugin_count);
|
||||
return -1;
|
||||
}
|
||||
if (!port_name || !looper_port) return -1;
|
||||
if (!jack_client) return -1;
|
||||
|
||||
fprintf(stderr, "CARLA_CONNECT: plugin_id=%d conn_count=%d port=%s looper=%s\n",
|
||||
id, conn_count, port_name, looper_port);
|
||||
// Real JACK port connection
|
||||
int ret = jack_connect(jack_client, looper_port, port_name);
|
||||
if (ret != 0) return -1;
|
||||
int ret = jack_connect(jack_client, port_name, looper_port);
|
||||
if (ret != 0) {
|
||||
fprintf(stderr, "CARLA_CONNECT: jack_connect(%s, %s) failed with %d\n", looper_port, port_name, ret);
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Store the connection so we can disconnect it later
|
||||
if (conn_count < MAX_CONNECTIONS) {
|
||||
connections[conn_count].plugin_id = id;
|
||||
strncpy(connections[conn_count].plugin_port, port_name,
|
||||
sizeof(connections[conn_count].plugin_port) - 1);
|
||||
connections[conn_count].plugin_port[sizeof(connections[conn_count].plugin_port) - 1] = '\0';
|
||||
strncpy(connections[conn_count].looper_port, looper_port,
|
||||
sizeof(connections[conn_count].looper_port) - 1);
|
||||
connections[conn_count].looper_port[sizeof(connections[conn_count].looper_port) - 1] = '\0';
|
||||
conn_count++;
|
||||
if (conn_count >= MAX_CONNECTIONS) {
|
||||
fprintf(stderr, "WARN: connection array full, refusing new connection\n");
|
||||
return -1;
|
||||
}
|
||||
connections[conn_count].plugin_id = id;
|
||||
strncpy(connections[conn_count].plugin_port, port_name,
|
||||
sizeof(connections[conn_count].plugin_port) - 1);
|
||||
connections[conn_count].plugin_port[sizeof(connections[conn_count].plugin_port) - 1] = '\0';
|
||||
strncpy(connections[conn_count].looper_port, looper_port,
|
||||
sizeof(connections[conn_count].looper_port) - 1);
|
||||
connections[conn_count].looper_port[sizeof(connections[conn_count].looper_port) - 1] = '\0';
|
||||
conn_count++;
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -206,6 +245,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 <jack/jack.h>
|
||||
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;
|
||||
@@ -230,3 +311,17 @@ int carla_test_add_connection(int plugin_id, const char *plugin_port, const char
|
||||
CarlaHostHandle carla_get_handle(void) {
|
||||
return handle;
|
||||
}
|
||||
|
||||
bool carla_get_connected_port(int channel, bool is_input, char *buf, size_t bufsize) {
|
||||
char needle[64];
|
||||
snprintf(needle, sizeof(needle), "ch%d%s", channel, is_input ? "in" : "out");
|
||||
for (int i = 0; i < conn_count; i++) {
|
||||
if (strstr(connections[i].looper_port, needle)) {
|
||||
strncpy(buf, connections[i].plugin_port, bufsize - 1);
|
||||
buf[bufsize - 1] = '\0';
|
||||
return true;
|
||||
}
|
||||
}
|
||||
buf[0] = '\0';
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
|
||||
/* All functions return -1 on error, 0 on success (except carla_load which returns 0 on success and sets *out_id) */
|
||||
|
||||
bool carla_get_connected_port(int channel, bool is_input, char *buf, size_t bufsize);
|
||||
|
||||
int carla_init_jack(void);
|
||||
void carla_cleanup_jack(void);
|
||||
|
||||
@@ -14,8 +16,9 @@ int carla_unload(int id);
|
||||
int carla_connect(int id, const char *port_name, const char *looper_port);
|
||||
int carla_disconnect(const char *from, const char *to);
|
||||
void carla_set_bypass(int id, bool bypass);
|
||||
int carla_connect_direct(const char *source, const char *target);
|
||||
int carla_get_ports(const char *type, char ***ports, int *count);
|
||||
|
||||
/* Get internal Carla host handle, may be NULL */
|
||||
int carla_disconnect_plugin(int id);
|
||||
CarlaHostHandle carla_get_handle(void);
|
||||
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
#include "client_cmd.h"
|
||||
#include "plugins.h"
|
||||
#include "carla_host.h"
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
static char from_port[256] = "";
|
||||
static char to_port[256] = "";
|
||||
char g_from_port[256] = "";
|
||||
char g_to_port[256] = "";
|
||||
char g_connect_error[512] = "";
|
||||
|
||||
const char* get_stored_from(void) { return from_port; }
|
||||
const char* get_stored_to(void) { return to_port; }
|
||||
const char* get_stored_from(void) { return g_from_port; }
|
||||
const char* get_stored_to(void) { return g_to_port; }
|
||||
|
||||
static int get_plugin_id_for_port(const char *port_spec) {
|
||||
// port_spec format: "plugin_id:port_name"
|
||||
@@ -34,18 +36,32 @@ int handle_client_command(const char *input, int *out_id) {
|
||||
if (strcmp(token, "from") == 0) {
|
||||
const char *port = strtok(NULL, " ");
|
||||
if (!port) return -1;
|
||||
strncpy(from_port, port, sizeof(from_port)-1);
|
||||
from_port[sizeof(from_port)-1] = '\0';
|
||||
return 0;
|
||||
int ret = carla_connect_direct(port, "looper:ch0in");
|
||||
if (ret == 0) {
|
||||
strncpy(g_from_port, port, sizeof(g_from_port)-1);
|
||||
g_from_port[sizeof(g_from_port)-1] = '\0';
|
||||
g_connect_error[0] = '\0';
|
||||
} else {
|
||||
snprintf(g_connect_error, sizeof(g_connect_error),
|
||||
"Failed: %s -> looper:ch0in (ret=%d)", port, ret);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
// --- to <port> ---
|
||||
if (strcmp(token, "to") == 0) {
|
||||
const char *port = strtok(NULL, " ");
|
||||
if (!port) return -1;
|
||||
strncpy(to_port, port, sizeof(to_port)-1);
|
||||
to_port[sizeof(to_port)-1] = '\0';
|
||||
return 0;
|
||||
int ret = carla_connect_direct("looper:ch0out", port);
|
||||
if (ret == 0) {
|
||||
strncpy(g_to_port, port, sizeof(g_to_port)-1);
|
||||
g_to_port[sizeof(g_to_port)-1] = '\0';
|
||||
g_connect_error[0] = '\0';
|
||||
} else {
|
||||
snprintf(g_connect_error, sizeof(g_connect_error),
|
||||
"Failed: looper:ch0out -> %s (ret=%d)", port, ret);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
// --- addplugin <path> ---
|
||||
@@ -58,12 +74,12 @@ int handle_client_command(const char *input, int *out_id) {
|
||||
if (ret == 0 && out_id) *out_id = id;
|
||||
|
||||
// auto-connect using stored :from/:to if set
|
||||
if (ret == 0 && from_port[0] && to_port[0]) {
|
||||
if (ret == 0 && g_from_port[0] && g_to_port[0]) {
|
||||
// parse plugin port name from stored from_port ("plugin_id:port_name")
|
||||
const char *colon = strchr(from_port, ':');
|
||||
const char *colon = strchr(g_from_port, ':');
|
||||
if (colon) {
|
||||
const char *pname = colon + 1;
|
||||
plugin_connect(id, pname, to_port);
|
||||
plugin_connect(id, pname, g_to_port);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,11 +91,11 @@ int handle_client_command(const char *input, int *out_id) {
|
||||
const char *from = strtok(NULL, " ");
|
||||
const char *to = strtok(NULL, " ");
|
||||
if (!from) {
|
||||
if (from_port[0]) from = from_port;
|
||||
if (g_from_port[0]) from = g_from_port;
|
||||
else return -1;
|
||||
}
|
||||
if (!to) {
|
||||
if (to_port[0]) to = to_port;
|
||||
if (g_to_port[0]) to = g_to_port;
|
||||
else return -1;
|
||||
}
|
||||
|
||||
@@ -99,11 +115,11 @@ int handle_client_command(const char *input, int *out_id) {
|
||||
const char *from = strtok(NULL, " ");
|
||||
const char *to = strtok(NULL, " ");
|
||||
if (!from) {
|
||||
if (from_port[0]) from = from_port;
|
||||
if (g_from_port[0]) from = g_from_port;
|
||||
else return -1;
|
||||
}
|
||||
if (!to) {
|
||||
if (to_port[0]) to = to_port;
|
||||
if (g_to_port[0]) to = g_to_port;
|
||||
else return -1;
|
||||
}
|
||||
return plugin_disconnect(from, to);
|
||||
|
||||
@@ -13,4 +13,8 @@ int handle_client_command(const char *input, int *out_id);
|
||||
const char* get_stored_from(void);
|
||||
const char* get_stored_to(void);
|
||||
|
||||
extern char g_connect_error[512];
|
||||
extern char g_from_port[256];
|
||||
extern char g_to_port[256];
|
||||
|
||||
#endif
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#include "carla_host.h"
|
||||
#include "log.h"
|
||||
#include "script.h"
|
||||
#include "tui.h"
|
||||
@@ -8,6 +9,10 @@
|
||||
int main(int argc, char *argv[]) {
|
||||
log_init();
|
||||
|
||||
if (carla_init_jack() != 0) {
|
||||
log_msg("Warning: could not initialise JACK connector client");
|
||||
}
|
||||
|
||||
const char *script_path = NULL;
|
||||
|
||||
if (argc > 2 && strcmp(argv[1], "-s") == 0) {
|
||||
|
||||
@@ -4,10 +4,16 @@
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
/* 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 <glob.h>
|
||||
|
||||
#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]);
|
||||
|
||||
@@ -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
|
||||
|
||||
373
client/src/tui.c
373
client/src/tui.c
@@ -1,51 +1,88 @@
|
||||
#define _POSIX_C_SOURCE 200809L
|
||||
#include "carla_host.h"
|
||||
#include "client_cmd.h"
|
||||
#include "log.h"
|
||||
#include "plugins.h"
|
||||
#include "script.h"
|
||||
#include "tui.h"
|
||||
#include <CarlaHost.h>
|
||||
#include <ctype.h>
|
||||
#include <dirent.h>
|
||||
#include <fcntl.h>
|
||||
#include <math.h>
|
||||
#include <ncurses.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/wait.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
/* ---------- engine alive indicator ---------- */
|
||||
static bool engine_running = false;
|
||||
static bool debug_mode = false;
|
||||
static int cmd_fifo_fd = -1;
|
||||
static int status_fifo_fd = -1;
|
||||
|
||||
int send_command(const char *cmd) {
|
||||
if (debug_mode) {
|
||||
if (debug_mode)
|
||||
fprintf(stderr, "DEBUG: send_command(%s)\n", cmd);
|
||||
|
||||
if (cmd_fifo_fd < 0) {
|
||||
const char *fifo_path = getenv("LOOPER_CMD_FIFO");
|
||||
if (!fifo_path) fifo_path = "/tmp/looper_cmd";
|
||||
cmd_fifo_fd = open(fifo_path, O_WRONLY);
|
||||
if (cmd_fifo_fd < 0) {
|
||||
perror("open cmd FIFO");
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
const char *fifo_path = getenv("LOOPER_CMD_FIFO");
|
||||
if (!fifo_path) fifo_path = "/tmp/looper_cmd";
|
||||
int fd = open(fifo_path, O_WRONLY | O_NONBLOCK);
|
||||
if (fd < 0) return -1;
|
||||
|
||||
size_t len = strlen(cmd);
|
||||
int n = write(fd, cmd, len);
|
||||
int n = write(cmd_fifo_fd, cmd, len);
|
||||
if (n == (int)len && cmd[len-1] != '\n')
|
||||
write(fd, "\n", 1);
|
||||
close(fd);
|
||||
write(cmd_fifo_fd, "\n", 1);
|
||||
return (n >= 0) ? 0 : -1;
|
||||
}
|
||||
|
||||
/* ---------- Stub functions (no engine) ---------- */
|
||||
// Clip states – dummy values used as placeholders
|
||||
static bool carla_resolve_channel_port(int channel, bool is_to, char *buf, size_t bufsize) {
|
||||
char **ports = NULL;
|
||||
int count = 0;
|
||||
if (carla_get_ports(NULL, &ports, &count) != 0) {
|
||||
return false;
|
||||
}
|
||||
char pattern[64];
|
||||
if (is_to) {
|
||||
snprintf(pattern, sizeof(pattern), "ch%dout", channel);
|
||||
} else {
|
||||
snprintf(pattern, sizeof(pattern), "ch%din", channel);
|
||||
}
|
||||
bool found = false;
|
||||
for (int i = 0; i < count && !found; i++) {
|
||||
if (strstr(ports[i], pattern)) {
|
||||
strncpy(buf, ports[i], bufsize - 1);
|
||||
buf[bufsize - 1] = '\0';
|
||||
found = true;
|
||||
}
|
||||
free(ports[i]);
|
||||
}
|
||||
free(ports);
|
||||
return found;
|
||||
}
|
||||
typedef enum { CLIP_EMPTY, CLIP_RECORDING, CLIP_LOOPING, CLIP_STOPPED } ClipState;
|
||||
static const char *clip_state_string(ClipState s) { (void)s; return "?"; }
|
||||
static const char *clip_state_string(ClipState s) {
|
||||
switch (s) {
|
||||
case CLIP_EMPTY: return " ";
|
||||
case CLIP_RECORDING: return "R";
|
||||
case CLIP_LOOPING: return "L";
|
||||
case CLIP_STOPPED: return "P";
|
||||
default: return "?";
|
||||
}
|
||||
}
|
||||
|
||||
/* Grid dimensions */
|
||||
#define GRID_ROWS 8
|
||||
#define GRID_COLS 8
|
||||
#define NUM_GRIDS 8
|
||||
#define CELL_WIDTH 6
|
||||
#define CELL_WIDTH 20
|
||||
#define CELL_HEIGHT 3
|
||||
|
||||
/* status FIFO path */
|
||||
@@ -87,6 +124,11 @@ typedef struct {
|
||||
static FuzzySearch fuzzy_search = {0};
|
||||
|
||||
/* ---------- Parse status line from engine status FIFO ---------- */
|
||||
static float vu_level[16] = {0.0f}; /* per‑channel RMS level (index = channel number) */
|
||||
|
||||
static bool parse_level_line(const char *line, int *ch, float *level) {
|
||||
return sscanf(line, "CH=%d LEVEL=%f", ch, level) == 2;
|
||||
}
|
||||
bool parse_status_line(const char *line, int *ch, int *scene, ChannelState *state) {
|
||||
int sta;
|
||||
if (sscanf(line, "CH=%d SC=%d STATE=%d", ch, scene, &sta) == 3) {
|
||||
@@ -128,7 +170,13 @@ static void draw_cell(int grid, int row, int col, bool selected) {
|
||||
for (int dy=0; dy<CELL_HEIGHT; dy++)
|
||||
for (int dx=0; dx<CELL_WIDTH; dx++)
|
||||
mvaddch(y+dy, x+dx, ' ');
|
||||
mvprintw(y+1, x+1, "%2d", grid*GRID_ROWS*GRID_COLS + row*GRID_COLS + col);
|
||||
int ch = grid * GRID_ROWS * GRID_COLS + row * GRID_COLS + col;
|
||||
const char state_char = (s == STATE_RECORD) ? 'R' :
|
||||
(s == STATE_LOOPING) ? 'L' :
|
||||
(s == STATE_PAUSED) ? 'P' : '.';
|
||||
mvprintw(y, x, "ch %2d", ch);
|
||||
mvaddch(y, x+5, state_char);
|
||||
|
||||
attroff(COLOR_PAIR(color));
|
||||
}
|
||||
|
||||
@@ -174,11 +222,62 @@ static void draw_grid(void) {
|
||||
for (int r=0; r<GRID_ROWS; r++)
|
||||
for (int c=0; c<GRID_COLS; c++)
|
||||
draw_cell(selected_grid, r, c, r==selected_row && c==selected_col);
|
||||
mvprintw(GRID_ROWS*CELL_HEIGHT+3, 0, "Selected: Grid %d, Row %d, Col %d",
|
||||
|
||||
/* ---------- Footer: per‑column input / output ---------- */
|
||||
int footer_y = GRID_ROWS * CELL_HEIGHT + 3;
|
||||
for (int c=0; c<GRID_COLS; c++) {
|
||||
int x = c * CELL_WIDTH + 1;
|
||||
int global_ch = selected_grid * GRID_ROWS * GRID_COLS + c;
|
||||
char input_buf[80], output_buf[80];
|
||||
bool has_input = carla_get_connected_port(global_ch, true, input_buf, sizeof(input_buf));
|
||||
bool has_output = carla_get_connected_port(global_ch, false, output_buf, sizeof(output_buf));
|
||||
if (global_ch == 0) {
|
||||
if (!has_input && g_from_port[0]) {
|
||||
strncpy(input_buf, g_from_port, sizeof(input_buf)-1);
|
||||
input_buf[sizeof(input_buf)-1] = '\0';
|
||||
has_input = true;
|
||||
}
|
||||
if (!has_output && g_to_port[0]) {
|
||||
strncpy(output_buf, g_to_port, sizeof(output_buf)-1);
|
||||
output_buf[sizeof(output_buf)-1] = '\0';
|
||||
has_output = true;
|
||||
}
|
||||
}
|
||||
char fallback[16];
|
||||
snprintf(fallback, sizeof(fallback), "ch%d", global_ch);
|
||||
mvprintw(footer_y, x, "i:%-20.20s", has_input ? input_buf : fallback);
|
||||
mvprintw(footer_y+1, x, "o:%-20.20s", has_output ? output_buf : fallback);
|
||||
}
|
||||
|
||||
/* VU meter line per channel */
|
||||
int vu_y = footer_y + 2;
|
||||
for (int c = 0; c < GRID_COLS; c++) {
|
||||
int x = c * CELL_WIDTH + 1;
|
||||
float level = vu_level[c];
|
||||
int bar_width = CELL_WIDTH - 2;
|
||||
int filled = (int)(level * bar_width);
|
||||
if (filled > bar_width) filled = bar_width;
|
||||
mvprintw(vu_y, x, "%*s", CELL_WIDTH, "");
|
||||
for (int i = 0; i < filled; i++) {
|
||||
char ch = (i < bar_width * 0.3f) ? '.' :
|
||||
(i < bar_width * 0.6f) ? 'x' : '#';
|
||||
mvaddch(vu_y, x + 1 + i, ch);
|
||||
}
|
||||
}
|
||||
|
||||
/* Display connection error if any */
|
||||
if (g_connect_error[0]) {
|
||||
attron(COLOR_PAIR(COLOR_RECORDING));
|
||||
mvprintw(vu_y + 1, 0, "ERROR: %-60s", g_connect_error);
|
||||
attroff(COLOR_PAIR(COLOR_RECORDING));
|
||||
g_connect_error[0] = '\0';
|
||||
}
|
||||
|
||||
mvprintw(vu_y + 2, 0, "Selected: Grid %d, Row %d, Col %d",
|
||||
selected_grid, selected_row, selected_col);
|
||||
if (show_help) {
|
||||
attron(COLOR_PAIR(COLOR_HELP));
|
||||
mvprintw(GRID_ROWS*CELL_HEIGHT+4, 0, "Help: h/j/k/l navigate, t record, d/D stop, s/S scene, a add, A add_midi, r remove, b bind, u unbind, R rack, ? help, Esc/Q quit");
|
||||
mvprintw(vu_y + 3, 0, "Help: h/j/k/l navigate, t record, d/D stop, s/S scene, a add, A add_midi, r remove, b bind, u unbind, R rack, ? help, Esc/Q quit");
|
||||
attroff(COLOR_PAIR(COLOR_HELP));
|
||||
}
|
||||
refresh();
|
||||
@@ -186,6 +285,7 @@ static void draw_grid(void) {
|
||||
|
||||
/* ---------- TUI init ---------- */
|
||||
void tui_init(void) {
|
||||
log_init();
|
||||
initscr();
|
||||
cbreak(); noecho(); keypad(stdscr, TRUE); curs_set(0);
|
||||
debug_mode = (getenv("LOOPER_DEBUG") != NULL);
|
||||
@@ -213,36 +313,42 @@ static char colon_buf[256];
|
||||
static int colon_len = 0;
|
||||
static bool in_colon = false;
|
||||
|
||||
void tui_run(void) {
|
||||
draw_grid();
|
||||
while (1) {
|
||||
/* read any available status lines */
|
||||
int fd = open(STATUS_FIFO, O_RDONLY | O_NONBLOCK);
|
||||
if (fd >= 0) {
|
||||
char buf[256];
|
||||
int n = read(fd, buf, sizeof(buf)-1);
|
||||
if (n > 0) {
|
||||
buf[n] = '\0';
|
||||
char *line = buf;
|
||||
while (*line) {
|
||||
char *nl = strchr(line, '\n');
|
||||
if (nl) *nl = '\0';
|
||||
int ch, sc;
|
||||
ChannelState st;
|
||||
if (parse_status_line(line, &ch, &sc, &st)) {
|
||||
if (ch >= 0 && ch < GRID_ROWS * GRID_COLS)
|
||||
cell_state[ch] = st;
|
||||
}
|
||||
if (nl) {
|
||||
*nl = '\n';
|
||||
line = nl + 1;
|
||||
} else break;
|
||||
static void tui_read_status(void) {
|
||||
if (status_fifo_fd < 0) {
|
||||
status_fifo_fd = open(STATUS_FIFO, O_RDONLY | O_NONBLOCK);
|
||||
if (status_fifo_fd < 0) return;
|
||||
}
|
||||
char buf[4096];
|
||||
int n = read(status_fifo_fd, buf, sizeof(buf)-1);
|
||||
if (n > 0) {
|
||||
buf[n] = '\0';
|
||||
char *line = buf;
|
||||
while (*line) {
|
||||
char *nl = strchr(line, '\n');
|
||||
if (nl) *nl = '\0';
|
||||
int ch, sc; ChannelState st; float level_val;
|
||||
if (parse_level_line(line, &ch, &level_val)) {
|
||||
if (ch >= 0 && ch < 16)
|
||||
vu_level[ch] = level_val;
|
||||
} else if (parse_status_line(line, &ch, &sc, &st)) {
|
||||
if (ch >= 0 && ch < GRID_COLS && sc >= 0 && sc < GRID_ROWS) {
|
||||
int idx = sc * GRID_COLS + ch;
|
||||
cell_state[idx] = st;
|
||||
}
|
||||
}
|
||||
close(fd);
|
||||
if (nl) { *nl = '\n'; line = nl + 1; } else break;
|
||||
}
|
||||
}
|
||||
/* keep fd open */
|
||||
}
|
||||
void tui_run(void) {
|
||||
draw_grid();
|
||||
nodelay(stdscr, TRUE); // non‑blocking input – getch returns ERR when no key is pressed
|
||||
while (1) {
|
||||
/* read status FIFO once per iteration – always */
|
||||
tui_read_status();
|
||||
|
||||
/* Check if engine is alive by testing existence of status FIFO */
|
||||
/* Check if engine is alive */
|
||||
engine_running = (access(STATUS_FIFO, F_OK) == 0);
|
||||
|
||||
/* read any available note events (for script macros) */
|
||||
@@ -267,13 +373,23 @@ void tui_run(void) {
|
||||
close(nfd);
|
||||
}
|
||||
|
||||
/* redraw grid (status may have changed – no extra key needed) */
|
||||
{
|
||||
struct timespec t1, t2;
|
||||
clock_gettime(CLOCK_MONOTONIC, &t1);
|
||||
draw_grid();
|
||||
clock_gettime(CLOCK_MONOTONIC, &t2);
|
||||
double ms = (t2.tv_sec - t1.tv_sec)*1000.0 + (t2.tv_nsec - t1.tv_nsec)/1000000.0;
|
||||
if (ms > 200) log_msg("SLOW draw_grid: %f ms", ms);
|
||||
}
|
||||
|
||||
int chc = getch();
|
||||
|
||||
if (in_colon) {
|
||||
int chc = getch();
|
||||
if (chc == '\n') {
|
||||
colon_buf[colon_len] = '\0';
|
||||
colon_len = 0;
|
||||
in_colon = false;
|
||||
// Check first token before calling handle_client_command
|
||||
char cmd_copy[256];
|
||||
strncpy(cmd_copy, colon_buf, sizeof(cmd_copy)-1);
|
||||
cmd_copy[sizeof(cmd_copy)-1] = '\0';
|
||||
@@ -284,6 +400,62 @@ void tui_run(void) {
|
||||
rack_selected = 0;
|
||||
} else if (strcmp(first, "grid") == 0) {
|
||||
rack_mode = false;
|
||||
} else if ((strcmp(first, "from") == 0 || strcmp(first, "to") == 0)) {
|
||||
char *potential_arg = strtok(NULL, " ");
|
||||
const char *port_name = NULL;
|
||||
if (potential_arg != NULL) {
|
||||
port_name = potential_arg;
|
||||
// Do NOT strip client prefix – keep full JACK port name
|
||||
} else {
|
||||
script_handle_fzf_command(first);
|
||||
if (g_selected_port[0] != '\0') {
|
||||
port_name = g_selected_port;
|
||||
}
|
||||
}
|
||||
/* port assignment happens only after successful connection below */
|
||||
if (port_name) {
|
||||
/* Resolve the looper port for the currently selected channel */
|
||||
char looper_port[256] = "";
|
||||
const bool is_to = (strcmp(first, "to") == 0);
|
||||
int channel = selected_col; // selected column = channel number
|
||||
bool found = carla_resolve_channel_port(channel, is_to, looper_port, sizeof(looper_port));
|
||||
if (!found) {
|
||||
/* Fallback to generic name with looper: prefix */
|
||||
if (is_to)
|
||||
snprintf(looper_port, sizeof(looper_port), "looper:ch%dout", channel);
|
||||
else
|
||||
snprintf(looper_port, sizeof(looper_port), "looper:ch%din", channel);
|
||||
}
|
||||
int ret;
|
||||
const char *src, *dst;
|
||||
if (is_to) {
|
||||
ret = carla_connect_direct(looper_port, port_name);
|
||||
src = looper_port;
|
||||
dst = port_name;
|
||||
} else {
|
||||
ret = carla_connect_direct(port_name, looper_port);
|
||||
src = port_name;
|
||||
dst = looper_port;
|
||||
}
|
||||
if (ret == 0) {
|
||||
if (is_to) {
|
||||
strncpy(g_to_port, port_name, sizeof(g_to_port)-1);
|
||||
g_to_port[sizeof(g_to_port)-1] = '\0';
|
||||
} else {
|
||||
strncpy(g_from_port, port_name, sizeof(g_from_port)-1);
|
||||
g_from_port[sizeof(g_from_port)-1] = '\0';
|
||||
}
|
||||
g_connect_error[0] = '\0';
|
||||
log_msg("Connected %s -> %s", src, dst);
|
||||
} else {
|
||||
snprintf(g_connect_error, sizeof(g_connect_error),
|
||||
"Failed: %s -> %s (ret=%d)", src, dst, ret);
|
||||
log_msg("Failed to connect %s -> %s (ret=%d)", src, dst, ret);
|
||||
}
|
||||
}
|
||||
if (!potential_arg) g_selected_port[0] = '\0';
|
||||
draw_grid();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
int dummy_id;
|
||||
@@ -304,10 +476,10 @@ void tui_run(void) {
|
||||
clrtoeol();
|
||||
move(LINES-1, colon_len+1);
|
||||
refresh();
|
||||
napms(50);
|
||||
continue;
|
||||
}
|
||||
|
||||
int chc = getch();
|
||||
if (chc == ':') {
|
||||
in_colon = true;
|
||||
colon_len = 0;
|
||||
@@ -318,6 +490,7 @@ void tui_run(void) {
|
||||
refresh();
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (chc) {
|
||||
case 'h': case KEY_LEFT: selected_col = (selected_col-1+GRID_COLS)%GRID_COLS; break;
|
||||
case 'j': case KEY_DOWN: selected_row = (selected_row+1)%GRID_ROWS; break;
|
||||
@@ -325,8 +498,20 @@ void tui_run(void) {
|
||||
case 'l': case KEY_RIGHT: selected_col = (selected_col+1)%GRID_COLS; break;
|
||||
case 't': {
|
||||
char cmd[32];
|
||||
log_msg("DIAG t pressed: selected_row=%d selected_col=%d", selected_row, selected_col);
|
||||
// First bind to the selected channel so engine knows which channel to operate on
|
||||
snprintf(cmd, sizeof(cmd), "bind %d\n", selected_col);
|
||||
send_command(cmd);
|
||||
log_msg("DIAG sent: %s", cmd);
|
||||
// Then set the scene for that channel
|
||||
snprintf(cmd, sizeof(cmd), "set_scene %d %d\n", selected_col, selected_row);
|
||||
send_command(cmd);
|
||||
log_msg("DIAG sent: %s", cmd);
|
||||
// Finally trigger record
|
||||
snprintf(cmd, sizeof(cmd), "record %d\n", selected_col);
|
||||
send_command(cmd);
|
||||
log_msg("DIAG sent: %s", cmd);
|
||||
// tui_read_status already called at top of loop
|
||||
break;
|
||||
}
|
||||
case 's':
|
||||
@@ -349,6 +534,8 @@ void tui_run(void) {
|
||||
break;
|
||||
case 'b': {
|
||||
char cmd[16];
|
||||
snprintf(cmd, sizeof(cmd), "set_scene %d %d\n", selected_col, selected_row);
|
||||
send_command(cmd);
|
||||
snprintf(cmd, sizeof(cmd), "bind %d\n", selected_col);
|
||||
send_command(cmd);
|
||||
break;
|
||||
@@ -361,12 +548,27 @@ 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;
|
||||
break;
|
||||
}
|
||||
return;
|
||||
case ERR:
|
||||
/* no key pressed – just continue the loop */
|
||||
break;
|
||||
default:
|
||||
if (rack_mode) {
|
||||
switch (chc) {
|
||||
@@ -386,7 +588,6 @@ void tui_run(void) {
|
||||
break;
|
||||
case 'b': case 'B':
|
||||
plugin_set_bypass(rack_selected, true);
|
||||
// toggle would be better, but for now just enable bypass
|
||||
break;
|
||||
case 'd': case 'D':
|
||||
plugin_unload(rack_selected);
|
||||
@@ -403,11 +604,81 @@ void tui_run(void) {
|
||||
}
|
||||
break;
|
||||
}
|
||||
draw_grid();
|
||||
napms(50); // avoid busy‑waste – grid redraws frequently enough
|
||||
}
|
||||
}
|
||||
|
||||
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 (cmd_fifo_fd >= 0) {
|
||||
close(cmd_fifo_fd);
|
||||
cmd_fifo_fd = -1;
|
||||
}
|
||||
if (status_fifo_fd >= 0) {
|
||||
close(status_fifo_fd);
|
||||
status_fifo_fd = -1;
|
||||
}
|
||||
if (yank_buffer.clip_indices) free(yank_buffer.clip_indices);
|
||||
/* free script note allocations */
|
||||
script_cleanup();
|
||||
@@ -419,3 +690,5 @@ void tui_cleanup(void) {
|
||||
carla_cleanup_jack();
|
||||
curs_set(1); endwin();
|
||||
}
|
||||
|
||||
extern char g_selected_port[256];
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
#ifndef TUI_H
|
||||
#define TUI_H
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
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
|
||||
|
||||
5
client/tests/test_tui_stub.c
Normal file
5
client/tests/test_tui_stub.c
Normal file
@@ -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){}
|
||||
Reference in New Issue
Block a user