feat: add fuzzy search dialog for loading WAV samples

Co-authored-by: aider (deepseek/deepseek-coder) <aider@aider.chat>
This commit is contained in:
Loic Coenen
2026-05-03 11:30:25 +00:00
parent 70d38f7160
commit b6a106a86b

199
tui.c
View File

@@ -4,11 +4,17 @@
#include "carla.h"
#include <ncurses.h>
#include <string.h>
#include <strings.h>
#include <stdlib.h>
#include <signal.h>
#include <stdatomic.h>
#include <ctype.h>
#include <dirent.h>
#include <sys/stat.h>
#include <dirent.h>
#include <sys/stat.h>
#include <math.h>
#include <unistd.h>
#define CELL_WIDTH 6
#define CELL_HEIGHT 3
@@ -54,6 +60,9 @@ typedef struct {
bool active;
char prompt[64];
void (*callback)(const char *selection);
const char **items; // custom list of strings (NULL if using plugins)
int num_items; // number of items in custom list
bool free_items; // whether to free items when done
} FuzzySearch;
static FuzzySearch fuzzy_search = {0};
@@ -276,6 +285,60 @@ static void midi_to_callback(const char *selection) {
}
}
// List WAV files in a directory (returns malloc'd array of strings, count via *count)
static const char **list_wav_files(const char *dir, int *count) {
DIR *d = opendir(dir);
if (!d) {
*count = 0;
return NULL;
}
int capacity = 64;
const char **files = malloc(capacity * sizeof(char*));
int n = 0;
struct dirent *entry;
while ((entry = readdir(d)) != NULL) {
const char *name = entry->d_name;
size_t len = strlen(name);
if (len > 4 && strcasecmp(name + len - 4, ".wav") == 0) {
// Check if it's a regular file
char fullpath[1024];
snprintf(fullpath, sizeof(fullpath), "%s/%s", dir, name);
struct stat st;
if (stat(fullpath, &st) == 0 && S_ISREG(st.st_mode)) {
if (n >= capacity) {
capacity *= 2;
files = realloc(files, capacity * sizeof(char*));
}
files[n] = strdup(name);
n++;
}
}
}
closedir(d);
*count = n;
return files;
}
// Callback for loading a sample via fuzzy search
static void load_sample_callback(const char *selection) {
if (selection) {
// Build full path (assume samples directory)
char fullpath[1024];
const char *samples_dir = getenv("JACK_LOOPER_SAMPLES_DIR");
if (!samples_dir) samples_dir = ".";
snprintf(fullpath, sizeof(fullpath), "%s/%s", samples_dir, selection);
int clip_idx = grid_to_clip_index(selected_grid, selected_row, selected_col);
Action action = { .type = ACTION_LOAD_CLIP, .data.load_clip = { .clip_index = clip_idx } };
strncpy(action.data.load_clip.filename, fullpath, 255);
action.data.load_clip.filename[255] = '\0';
g_dispatch(action);
}
}
// Fuzzy matching function
static bool fuzzy_match(const char *pattern, const char *text) {
if (!pattern || !*pattern) return true;
@@ -320,10 +383,20 @@ static void draw_fuzzy_search(void) {
if (i == fuzzy_search.selected_index) {
attron(A_REVERSE);
}
int count;
const char **plugins = carla_get_available_plugins(&g_engine->carla_host, &count);
if (plugins && idx >= 0 && idx < count) {
mvprintw(start_y + 2 + i, start_x, "%s", plugins[idx]);
const char *item = NULL;
if (fuzzy_search.items) {
if (idx >= 0 && idx < fuzzy_search.num_items) {
item = fuzzy_search.items[idx];
}
} else {
int count;
const char **plugins = carla_get_available_plugins(&g_engine->carla_host, &count);
if (plugins && idx >= 0 && idx < count) {
item = plugins[idx];
}
}
if (item) {
mvprintw(start_y + 2 + i, start_x, "%s", item);
}
if (i == fuzzy_search.selected_index) {
attroff(A_REVERSE);
@@ -342,16 +415,46 @@ static bool handle_fuzzy_search(int ch) {
case '\n': case '\r': {
if (fuzzy_search.num_results > 0 && fuzzy_search.selected_index >= 0) {
int idx = fuzzy_search.result_indices[fuzzy_search.selected_index];
int count;
const char **plugins = carla_get_available_plugins(NULL, &count);
if (plugins && idx >= 0 && idx < count && fuzzy_search.callback) {
fuzzy_search.callback(plugins[idx]);
const char *selection = NULL;
if (fuzzy_search.items) {
if (idx >= 0 && idx < fuzzy_search.num_items) {
selection = fuzzy_search.items[idx];
}
} else {
int count;
const char **plugins = carla_get_available_plugins(NULL, &count);
if (plugins && idx >= 0 && idx < count) {
selection = plugins[idx];
}
}
if (selection && fuzzy_search.callback) {
fuzzy_search.callback(selection);
}
}
// Free custom items if needed
if (fuzzy_search.free_items && fuzzy_search.items) {
for (int i = 0; i < fuzzy_search.num_items; i++) {
free((void*)fuzzy_search.items[i]);
}
free(fuzzy_search.items);
fuzzy_search.items = NULL;
fuzzy_search.num_items = 0;
fuzzy_search.free_items = false;
}
fuzzy_search.active = false;
return true;
}
case 27: { // ESC
// Free custom items if needed
if (fuzzy_search.free_items && fuzzy_search.items) {
for (int i = 0; i < fuzzy_search.num_items; i++) {
free((void*)fuzzy_search.items[i]);
}
free(fuzzy_search.items);
fuzzy_search.items = NULL;
fuzzy_search.num_items = 0;
fuzzy_search.free_items = false;
}
fuzzy_search.active = false;
return true;
}
@@ -360,11 +463,19 @@ static bool handle_fuzzy_search(int ch) {
fuzzy_search.query[--fuzzy_search.query_len] = '\0';
// Update results
fuzzy_search.num_results = 0;
int count;
const char **plugins = carla_get_available_plugins(&g_engine->carla_host, &count);
for (int i = 0; i < count; i++) {
if (fuzzy_match(fuzzy_search.query, plugins[i])) {
fuzzy_search.result_indices[fuzzy_search.num_results++] = i;
if (fuzzy_search.items) {
for (int i = 0; i < fuzzy_search.num_items; i++) {
if (fuzzy_match(fuzzy_search.query, fuzzy_search.items[i])) {
fuzzy_search.result_indices[fuzzy_search.num_results++] = i;
}
}
} else {
int count;
const char **plugins = carla_get_available_plugins(&g_engine->carla_host, &count);
for (int i = 0; i < count; i++) {
if (fuzzy_match(fuzzy_search.query, plugins[i])) {
fuzzy_search.result_indices[fuzzy_search.num_results++] = i;
}
}
}
if (fuzzy_search.selected_index >= fuzzy_search.num_results) {
@@ -407,11 +518,19 @@ static bool handle_fuzzy_search(int ch) {
fuzzy_search.query[fuzzy_search.query_len] = '\0';
// Update results
fuzzy_search.num_results = 0;
int count;
const char **plugins = carla_get_available_plugins(&g_engine->carla_host, &count);
for (int i = 0; i < count; i++) {
if (fuzzy_match(fuzzy_search.query, plugins[i])) {
fuzzy_search.result_indices[fuzzy_search.num_results++] = i;
if (fuzzy_search.items) {
for (int i = 0; i < fuzzy_search.num_items; i++) {
if (fuzzy_match(fuzzy_search.query, fuzzy_search.items[i])) {
fuzzy_search.result_indices[fuzzy_search.num_results++] = i;
}
}
} else {
int count;
const char **plugins = carla_get_available_plugins(&g_engine->carla_host, &count);
for (int i = 0; i < count; i++) {
if (fuzzy_match(fuzzy_search.query, plugins[i])) {
fuzzy_search.result_indices[fuzzy_search.num_results++] = i;
}
}
}
fuzzy_search.selected_index = 0;
@@ -421,8 +540,9 @@ static bool handle_fuzzy_search(int ch) {
}
}
// Start fuzzy search
static void start_fuzzy_search(const char *prompt, void (*callback)(const char *)) {
// Start fuzzy search with custom items list (or NULL for plugins)
static void start_fuzzy_search_items(const char *prompt, void (*callback)(const char *),
const char **items, int num_items, bool free_items) {
fuzzy_search.active = true;
fuzzy_search.query_len = 0;
fuzzy_search.query[0] = '\0';
@@ -430,16 +550,30 @@ static void start_fuzzy_search(const char *prompt, void (*callback)(const char *
strncpy(fuzzy_search.prompt, prompt, sizeof(fuzzy_search.prompt) - 1);
fuzzy_search.prompt[sizeof(fuzzy_search.prompt) - 1] = '\0';
fuzzy_search.callback = callback;
fuzzy_search.items = items;
fuzzy_search.num_items = num_items;
fuzzy_search.free_items = free_items;
// Initialize results with all plugins
// Initialize results with all items
fuzzy_search.num_results = 0;
int count = 0;
carla_get_available_plugins(&g_engine->carla_host, &count);
for (int i = 0; i < count; i++) {
fuzzy_search.result_indices[fuzzy_search.num_results++] = i;
if (items) {
for (int i = 0; i < num_items; i++) {
fuzzy_search.result_indices[fuzzy_search.num_results++] = i;
}
} else {
int count = 0;
carla_get_available_plugins(&g_engine->carla_host, &count);
for (int i = 0; i < count; i++) {
fuzzy_search.result_indices[fuzzy_search.num_results++] = i;
}
}
}
// Start fuzzy search (using plugins)
static void start_fuzzy_search(const char *prompt, void (*callback)(const char *)) {
start_fuzzy_search_items(prompt, callback, NULL, 0, false);
}
// Draw rack view
static void draw_rack_view(void) {
clear();
@@ -811,6 +945,21 @@ static bool handle_command_mode(void) {
} else if (strcmp(cmd_buffer, "to") == 0) {
// :to - open fuzzy search for MIDI output destination
start_fuzzy_search("Select MIDI output destination:", midi_to_callback);
} else if (strcmp(cmd_buffer, "load") == 0) {
// Start fuzzy search for WAV files
const char *samples_dir = getenv("JACK_LOOPER_SAMPLES_DIR");
if (!samples_dir) samples_dir = ".";
int count;
const char **files = list_wav_files(samples_dir, &count);
if (files && count > 0) {
start_fuzzy_search_items("Load sample:", load_sample_callback,
files, count, true);
} else {
// No files found, show message
mvprintw(LINES - 1, 0, "No WAV files found in %s", samples_dir);
refresh();
napms(1500);
}
} else if (strncmp(cmd_buffer, "load ", 5) == 0) {
char *rest = cmd_buffer + 5;
int clip_idx = atoi(rest);