Merge branch 'e2e' into integrate-fzf
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
CC = gcc
|
||||
CFLAGS = -Wall -Wextra -Wpedantic -std=gnu11 -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,7 +21,7 @@ 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) $(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
|
||||
@@ -93,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 ---
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include <stdlib.h>
|
||||
#include <CarlaHost.h>
|
||||
#include <CarlaBackend.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include "carla_host.h"
|
||||
|
||||
@@ -139,21 +140,25 @@ int carla_connect(int id, const char *port_name, const char *looper_port) {
|
||||
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;
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
|
||||
174
client/src/tui.c
174
client/src/tui.c
@@ -1,39 +1,67 @@
|
||||
#define _POSIX_C_SOURCE 200809L
|
||||
|
||||
#include "tui.h"
|
||||
#include <ncurses.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/wait.h>
|
||||
#include <fcntl.h>
|
||||
#include <ctype.h>
|
||||
#include <dirent.h>
|
||||
#include <sys/stat.h>
|
||||
#include <math.h>
|
||||
#include <time.h>
|
||||
|
||||
#include "carla_host.h"
|
||||
#include "client_cmd.h"
|
||||
#include "plugins.h"
|
||||
#include "script.h"
|
||||
#include <CarlaHost.h>
|
||||
#include "log.h"
|
||||
|
||||
/* ---------- engine alive indicator ---------- */
|
||||
static bool engine_running = false;
|
||||
static bool debug_mode = false;
|
||||
|
||||
/* Persistent FIFO fds – open once and reuse */
|
||||
static int cmd_fifo_fd = -1;
|
||||
static int status_fifo_fd = -1;
|
||||
|
||||
/* ---------- FIFO command helper ---------- */
|
||||
int send_command(const char *cmd) {
|
||||
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;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
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
|
||||
@@ -124,6 +152,17 @@ static void draw_cell(int grid, int row, int col, bool selected) {
|
||||
mvaddch(y+dy, x+dx, ' ');
|
||||
mvprintw(y+1, x+1, "%2d", grid*GRID_ROWS*GRID_COLS + row*GRID_COLS + col);
|
||||
attroff(COLOR_PAIR(color));
|
||||
|
||||
/* Draw state indicator character below the number, centered */
|
||||
const char state_char = (s == STATE_RECORD) ? 'R' :
|
||||
(s == STATE_LOOPING) ? 'L' :
|
||||
(s == STATE_PAUSED) ? 'P' : '.';
|
||||
int state_color = (s == STATE_RECORD) ? COLOR_RECORDING :
|
||||
(s == STATE_LOOPING) ? COLOR_LOOPING :
|
||||
(s == STATE_PAUSED) ? COLOR_STOPPED : COLOR_EMPTY;
|
||||
attron(COLOR_PAIR(state_color));
|
||||
mvaddch(y+2, x + CELL_WIDTH / 2, state_char);
|
||||
attroff(COLOR_PAIR(state_color));
|
||||
}
|
||||
|
||||
static void draw_rack(void) {
|
||||
@@ -163,7 +202,7 @@ static void draw_grid(void) {
|
||||
}
|
||||
clear();
|
||||
attron(A_BOLD);
|
||||
mvprintw(0,0,"JACK Looper - Client (FIFO only)");
|
||||
mvprintw(0,0,"JACK Looper - Client (FIFO only) %s", engine_running ? "[online]" : "[offline]");
|
||||
attroff(A_BOLD);
|
||||
for (int r=0; r<GRID_ROWS; r++)
|
||||
for (int c=0; c<GRID_COLS; c++)
|
||||
@@ -180,8 +219,10 @@ 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);
|
||||
if (!has_colors()) { endwin(); fprintf(stderr,"No colors\n"); exit(1); }
|
||||
start_color();
|
||||
init_pair(COLOR_EMPTY, COLOR_WHITE, COLOR_BLACK);
|
||||
@@ -206,34 +247,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;
|
||||
/* Read the status FIFO once and update cell_state array */
|
||||
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;
|
||||
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 */
|
||||
engine_running = (access(STATUS_FIFO, F_OK) == 0);
|
||||
|
||||
/* read any available note events (for script macros) */
|
||||
int nfd = open(NOTES_FIFO, O_RDONLY | O_NONBLOCK);
|
||||
@@ -257,13 +306,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';
|
||||
@@ -274,6 +333,14 @@ 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, " ");
|
||||
if (potential_arg == NULL) {
|
||||
// no argument: launch fzf
|
||||
script_handle_fzf_command(first);
|
||||
draw_grid();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
int dummy_id;
|
||||
@@ -294,10 +361,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;
|
||||
@@ -308,6 +375,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;
|
||||
@@ -315,8 +383,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':
|
||||
@@ -339,6 +419,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;
|
||||
@@ -369,6 +451,9 @@ void tui_run(void) {
|
||||
break;
|
||||
}
|
||||
return;
|
||||
case ERR:
|
||||
/* no key pressed – just continue the loop */
|
||||
break;
|
||||
default:
|
||||
if (rack_mode) {
|
||||
switch (chc) {
|
||||
@@ -388,7 +473,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);
|
||||
@@ -405,7 +489,7 @@ void tui_run(void) {
|
||||
}
|
||||
break;
|
||||
}
|
||||
draw_grid();
|
||||
napms(50); // avoid busy‑waste – grid redraws frequently enough
|
||||
}
|
||||
}
|
||||
|
||||
@@ -473,6 +557,14 @@ char* tui_fzf_select(const char *const items[], size_t count, const char *prompt
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
Reference in New Issue
Block a user