381 lines
13 KiB
C
381 lines
13 KiB
C
#include "tui.h"
|
||
#include <ncurses.h>
|
||
#include <string.h>
|
||
#include <stdlib.h>
|
||
#include <stdbool.h>
|
||
#include <unistd.h>
|
||
#include <fcntl.h>
|
||
#include <ctype.h>
|
||
#include <dirent.h>
|
||
#include <sys/stat.h>
|
||
#include <math.h>
|
||
#include "carla_host.h"
|
||
#include "client_cmd.h"
|
||
#include "plugins.h"
|
||
#include <CarlaHost.h>
|
||
|
||
/* ---------- 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;
|
||
size_t len = strlen(cmd);
|
||
int n = write(fd, cmd, len);
|
||
if (n == (int)len && cmd[len-1] != '\n')
|
||
write(fd, "\n", 1);
|
||
close(fd);
|
||
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 "?"; }
|
||
|
||
/* Grid dimensions */
|
||
#define GRID_ROWS 8
|
||
#define GRID_COLS 8
|
||
#define NUM_GRIDS 8
|
||
#define CELL_WIDTH 6
|
||
#define CELL_HEIGHT 3
|
||
|
||
/* status FIFO path */
|
||
#define STATUS_FIFO "/tmp/looper_status"
|
||
#define CMD_FIFO "/tmp/looper_cmd"
|
||
|
||
/* Per‑cell state array (indexed by row*GRID_COLS+col) */
|
||
typedef enum { STATE_IDLE, STATE_RECORD, STATE_LOOPING, STATE_PAUSED } ChannelState;
|
||
static ChannelState cell_state[GRID_ROWS * GRID_COLS];
|
||
|
||
/* Color pairs */
|
||
enum {
|
||
COLOR_EMPTY=1, COLOR_RECORDING, COLOR_LOOPING, COLOR_STOPPED,
|
||
COLOR_SELECTED, COLOR_HELP
|
||
};
|
||
|
||
static int selected_row = 0, selected_col = 0;
|
||
static int selected_grid = 0;
|
||
static bool show_help = false;
|
||
static bool rack_mode = false;
|
||
static int rack_selected = 0;
|
||
|
||
/* Visual mode, marks, yank buffer – keep but only local state */
|
||
static int marks[26];
|
||
typedef struct { int *clip_indices; int count; } YankBuffer;
|
||
static YankBuffer yank_buffer = {NULL, 0};
|
||
typedef enum { MODE_NORMAL, MODE_VISUAL, MODE_MOVE } UIMode;
|
||
static UIMode current_mode = MODE_NORMAL;
|
||
static int visual_start_row, visual_start_col, visual_end_row, visual_end_col;
|
||
|
||
/* Fuzzy search – keep struct but stub carla calls */
|
||
typedef struct {
|
||
char query[256]; int query_len, selected_index, num_results;
|
||
int result_indices[256]; bool active; char prompt[64];
|
||
void (*callback)(const char *);
|
||
const char **items; int num_items; bool free_items;
|
||
} FuzzySearch;
|
||
static FuzzySearch fuzzy_search = {0};
|
||
|
||
/* ---------- Parse status line from engine status FIFO ---------- */
|
||
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) {
|
||
if (sta >= 0 && sta <= 3) {
|
||
*state = (ChannelState)sta;
|
||
return true;
|
||
}
|
||
}
|
||
/* try text-based format */
|
||
char state_str[32];
|
||
if (sscanf(line, "CH=%d SC=%d STATE=%31s", ch, scene, state_str) != 3)
|
||
return false;
|
||
if (strcmp(state_str, "IDLE") == 0) { *state = STATE_IDLE; return true; }
|
||
if (strcmp(state_str, "RECORD") == 0) { *state = STATE_RECORD; return true; }
|
||
if (strcmp(state_str, "LOOPING") == 0) { *state = STATE_LOOPING; return true; }
|
||
if (strcmp(state_str, "PAUSED") == 0) { *state = STATE_PAUSED; return true; }
|
||
return false;
|
||
}
|
||
|
||
/* ---------- State to color (uses cell_state array) ---------- */
|
||
static int state_to_color(ChannelState s) {
|
||
switch (s) {
|
||
case STATE_IDLE: return COLOR_EMPTY;
|
||
case STATE_RECORD: return COLOR_RECORDING;
|
||
case STATE_LOOPING: return COLOR_LOOPING;
|
||
case STATE_PAUSED: return COLOR_STOPPED;
|
||
default: return COLOR_EMPTY;
|
||
}
|
||
}
|
||
|
||
/* ---------- Draw cell (no AppState) ---------- */
|
||
static void draw_cell(int grid, int row, int col, bool selected) {
|
||
int y = row * CELL_HEIGHT + 3;
|
||
int x = col * CELL_WIDTH + 1;
|
||
int idx = row * GRID_COLS + col;
|
||
ChannelState s = cell_state[idx];
|
||
int color = selected ? COLOR_SELECTED : state_to_color(s);
|
||
attron(COLOR_PAIR(color));
|
||
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);
|
||
attroff(COLOR_PAIR(color));
|
||
}
|
||
|
||
static void draw_rack(void) {
|
||
clear();
|
||
attron(A_BOLD);
|
||
mvprintw(0,0,"Rack View - Plugins");
|
||
attroff(A_BOLD);
|
||
CarlaHostHandle h = carla_get_handle();
|
||
if (!h) {
|
||
mvprintw(2,0,"Carla host not initialised");
|
||
refresh();
|
||
return;
|
||
}
|
||
uint32_t count = carla_get_current_plugin_count(h);
|
||
if (count == 0) {
|
||
mvprintw(2,0,"No plugins loaded");
|
||
refresh();
|
||
return;
|
||
}
|
||
for (uint32_t i=0; i<count; ++i) {
|
||
const CarlaPluginInfo *info = carla_get_plugin_info(h, i);
|
||
if (!info) continue;
|
||
if ((int)i == rack_selected)
|
||
attron(A_REVERSE);
|
||
mvprintw(2+i,0,"%u: %s", i, info->name ? info->name : "(unnamed)");
|
||
if ((int)i == rack_selected)
|
||
attroff(A_REVERSE);
|
||
}
|
||
mvprintw(2+count+1,0,"[B] bypass [D] delete [X] disconnect [R] grid [Esc] back");
|
||
refresh();
|
||
}
|
||
|
||
static void draw_grid(void) {
|
||
if (rack_mode) {
|
||
draw_rack();
|
||
return;
|
||
}
|
||
clear();
|
||
attron(A_BOLD);
|
||
mvprintw(0,0,"JACK Looper - Client (FIFO only)");
|
||
attroff(A_BOLD);
|
||
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",
|
||
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");
|
||
attroff(COLOR_PAIR(COLOR_HELP));
|
||
}
|
||
refresh();
|
||
}
|
||
|
||
/* ---------- TUI init ---------- */
|
||
void tui_init(void) {
|
||
initscr();
|
||
cbreak(); noecho(); keypad(stdscr, TRUE); curs_set(0);
|
||
if (!has_colors()) { endwin(); fprintf(stderr,"No colors\n"); exit(1); }
|
||
start_color();
|
||
init_pair(COLOR_EMPTY, COLOR_WHITE, COLOR_BLACK);
|
||
init_pair(COLOR_RECORDING, COLOR_RED, COLOR_BLACK);
|
||
init_pair(COLOR_LOOPING, COLOR_GREEN, COLOR_BLACK);
|
||
init_pair(COLOR_STOPPED, COLOR_BLUE, COLOR_BLACK);
|
||
init_pair(COLOR_SELECTED, COLOR_BLACK, COLOR_CYAN);
|
||
init_pair(COLOR_HELP, COLOR_CYAN, COLOR_BLACK);
|
||
for (int i=0;i<26;i++) marks[i] = -1;
|
||
/* initialise cell states to idle */
|
||
for (int i = 0; i < GRID_ROWS * GRID_COLS; i++)
|
||
cell_state[i] = STATE_IDLE;
|
||
/* open the JACK client used for Carla plugins */
|
||
carla_init_jack();
|
||
}
|
||
|
||
/* ---------- TUI run ---------- */
|
||
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;
|
||
}
|
||
}
|
||
close(fd);
|
||
}
|
||
|
||
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';
|
||
char *first = strtok(cmd_copy, " ");
|
||
if (first) {
|
||
if (strcmp(first, "rack") == 0) {
|
||
rack_mode = true;
|
||
rack_selected = 0;
|
||
} else if (strcmp(first, "grid") == 0) {
|
||
rack_mode = false;
|
||
}
|
||
}
|
||
int dummy_id;
|
||
handle_client_command(colon_buf, &dummy_id);
|
||
draw_grid();
|
||
continue;
|
||
} else if (chc == 27) {
|
||
colon_len = 0;
|
||
in_colon = false;
|
||
draw_grid();
|
||
continue;
|
||
} else if (chc == KEY_BACKSPACE || chc == 127) {
|
||
if (colon_len > 0) colon_len--;
|
||
} else if (chc >= 32 && chc < 127 && colon_len < 255) {
|
||
colon_buf[colon_len++] = chc;
|
||
}
|
||
mvprintw(LINES-1, 0, ":%s", colon_buf);
|
||
clrtoeol();
|
||
move(LINES-1, colon_len+1);
|
||
refresh();
|
||
continue;
|
||
}
|
||
|
||
int chc = getch();
|
||
if (chc == ':') {
|
||
in_colon = true;
|
||
colon_len = 0;
|
||
colon_buf[0] = '\0';
|
||
mvprintw(LINES-1, 0, ":");
|
||
clrtoeol();
|
||
move(LINES-1, 1);
|
||
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;
|
||
case 'k': case KEY_UP: selected_row = (selected_row-1+GRID_ROWS)%GRID_ROWS; break;
|
||
case 'l': case KEY_RIGHT: selected_col = (selected_col+1)%GRID_COLS; break;
|
||
case 't': {
|
||
char cmd[32];
|
||
snprintf(cmd, sizeof(cmd), "record %d\n", selected_col);
|
||
send_command(cmd);
|
||
break;
|
||
}
|
||
case 's':
|
||
send_command("scene_next\n");
|
||
break;
|
||
case 'S':
|
||
send_command("scene_prev\n");
|
||
break;
|
||
case 'd': case 'D':
|
||
send_command("stop\n");
|
||
break;
|
||
case 'a':
|
||
send_command("add\n");
|
||
break;
|
||
case 'A':
|
||
send_command("add_midi\n");
|
||
break;
|
||
case 'r':
|
||
send_command("remove\n");
|
||
break;
|
||
case 'b': {
|
||
char cmd[16];
|
||
snprintf(cmd, sizeof(cmd), "bind %d\n", selected_col);
|
||
send_command(cmd);
|
||
break;
|
||
}
|
||
case 'u':
|
||
send_command("unbind\n");
|
||
break;
|
||
case '?': show_help = !show_help; break;
|
||
case 'R':
|
||
rack_mode = !rack_mode;
|
||
rack_selected = 0;
|
||
break;
|
||
case 27: case 'Q':
|
||
if (rack_mode) {
|
||
rack_mode = false;
|
||
break;
|
||
}
|
||
return;
|
||
default:
|
||
if (rack_mode) {
|
||
switch (chc) {
|
||
case 'j': case KEY_DOWN:
|
||
{
|
||
CarlaHostHandle h = carla_get_handle();
|
||
uint32_t cnt = h ? carla_get_current_plugin_count(h) : 0;
|
||
if (cnt > 0) rack_selected = (rack_selected + 1) % cnt;
|
||
}
|
||
break;
|
||
case 'k': case KEY_UP:
|
||
{
|
||
CarlaHostHandle h = carla_get_handle();
|
||
uint32_t cnt = h ? carla_get_current_plugin_count(h) : 0;
|
||
if (cnt > 0) rack_selected = (rack_selected - 1 + cnt) % cnt;
|
||
}
|
||
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);
|
||
rack_selected = 0;
|
||
break;
|
||
case 'x': case 'X':
|
||
carla_disconnect_plugin(rack_selected);
|
||
mvprintw(LINES-1,0,"Disconnected plugin %d", rack_selected);
|
||
clrtoeol();
|
||
refresh();
|
||
napms(500);
|
||
break;
|
||
}
|
||
}
|
||
break;
|
||
}
|
||
draw_grid();
|
||
}
|
||
}
|
||
|
||
void tui_cleanup(void) {
|
||
if (yank_buffer.clip_indices) free(yank_buffer.clip_indices);
|
||
/* delete FIFOs */
|
||
unlink(STATUS_FIFO);
|
||
unlink(CMD_FIFO);
|
||
/* close the Carla JACK client */
|
||
carla_cleanup_jack();
|
||
curs_set(1); endwin();
|
||
}
|