#include "tui.h" #include "wav_io.h" #include "transport.h" #include "carla.h" #include #include #include #include #include #include #define GRID_ROWS 8 #define GRID_COLS 8 #define CELL_WIDTH 6 #define CELL_HEIGHT 3 // Color pairs enum { COLOR_EMPTY = 1, COLOR_RECORDING, COLOR_LOOPING, COLOR_STOPPED, COLOR_SELECTED, COLOR_HELP }; static Engine *g_engine = NULL; static DispatchFn g_dispatch = NULL; static int selected_row = 0; static int selected_col = 0; static bool show_help = false; // View modes typedef enum { VIEW_GRID, VIEW_RACK } UIView; static UIView current_view = VIEW_GRID; static int rack_selected_channel = 0; static int rack_selected_plugin = -1; static int rack_selected_param = -1; // Fuzzy search state typedef struct { char query[256]; int query_len; int selected_index; int num_results; int result_indices[256]; bool active; char prompt[64]; void (*callback)(const char *selection); } FuzzySearch; static FuzzySearch fuzzy_search = {0}; // Modes typedef enum { MODE_NORMAL, MODE_VISUAL, MODE_MOVE } UIMode; // Mark storage #define MAX_MARKS 26 // a-z static int marks[MAX_MARKS]; // stores clip_index for each mark // Visual mode state static int visual_start_row = 0; static int visual_start_col = 0; static int visual_end_row = 0; static int visual_end_col = 0; // Yank buffer typedef struct { int *clip_indices; int count; } YankBuffer; static YankBuffer yank_buffer = {NULL, 0}; // Current mode static UIMode current_mode = MODE_NORMAL; // Convert clip state to color pair static int state_to_color(ClipState state) { switch (state) { case CLIP_EMPTY: return COLOR_EMPTY; case CLIP_RECORDING: return COLOR_RECORDING; case CLIP_LOOPING: return COLOR_LOOPING; case CLIP_STOPPED: return COLOR_STOPPED; default: return COLOR_EMPTY; } } // Get clip index from grid position static int grid_to_clip_index(int row, int col) { return row * GRID_COLS + col; } // Check if a cell is in the visual selection static bool is_in_visual_selection(int row, int col) { if (current_mode != MODE_VISUAL) return false; int min_row = (visual_start_row < visual_end_row) ? visual_start_row : visual_end_row; int max_row = (visual_start_row > visual_end_row) ? visual_start_row : visual_end_row; int min_col = (visual_start_col < visual_end_col) ? visual_start_col : visual_end_col; int max_col = (visual_start_col > visual_end_col) ? visual_start_col : visual_end_col; return (row >= min_row && row <= max_row && col >= min_col && col <= max_col); } // Get all clip indices in the visual selection static int* get_selected_clips(int *count) { int min_row = (visual_start_row < visual_end_row) ? visual_start_row : visual_end_row; int max_row = (visual_start_row > visual_end_row) ? visual_start_row : visual_end_row; int min_col = (visual_start_col < visual_end_col) ? visual_start_col : visual_end_col; int max_col = (visual_start_col > visual_end_col) ? visual_start_col : visual_end_col; int num_rows = max_row - min_row + 1; int num_cols = max_col - min_col + 1; *count = num_rows * num_cols; int *clips = (int *)malloc(*count * sizeof(int)); if (!clips) { *count = 0; return NULL; } int idx = 0; for (int r = min_row; r <= max_row; r++) { for (int c = min_col; c <= max_col; c++) { clips[idx++] = grid_to_clip_index(r, c); } } return clips; } // Delete (reset) clips via dispatch static void delete_clips(int *clip_indices, int count) { for (int i = 0; i < count; i++) { Action action = { .type = ACTION_RESET_CLIP, .data.reset_clip = { .clip_index = clip_indices[i] } }; g_dispatch(action); } } // Yank clips (store their indices) static void yank_clips(int *clip_indices, int count) { if (yank_buffer.clip_indices) { free(yank_buffer.clip_indices); } yank_buffer.clip_indices = (int *)malloc(count * sizeof(int)); if (yank_buffer.clip_indices) { memcpy(yank_buffer.clip_indices, clip_indices, count * sizeof(int)); yank_buffer.count = count; } } // Paste clips (trigger them via dispatch) static void paste_clips(void) { if (!yank_buffer.clip_indices || yank_buffer.count == 0) return; int first_yanked_row = yank_buffer.clip_indices[0] / GRID_COLS; int first_yanked_col = yank_buffer.clip_indices[0] % GRID_COLS; int row_offset = selected_row - first_yanked_row; int col_offset = selected_col - first_yanked_col; for (int i = 0; i < yank_buffer.count; i++) { int orig_row = yank_buffer.clip_indices[i] / GRID_COLS; int orig_col = yank_buffer.clip_indices[i] % GRID_COLS; int new_row = orig_row + row_offset; int new_col = orig_col + col_offset; if (new_row >= 0 && new_row < GRID_ROWS && new_col >= 0 && new_col < GRID_COLS) { int new_clip_idx = grid_to_clip_index(new_row, new_col); // Trigger three times to cycle: empty -> recording -> looping -> stopped for (int j = 0; j < 3; j++) { Action action = { .type = ACTION_TRIGGER_CLIP, .data.trigger_clip = { .clip_index = new_clip_idx } }; g_dispatch(action); } } } } // Set a mark at current position static void set_mark(char mark_char) { if (mark_char >= 'a' && mark_char <= 'z') { int idx = mark_char - 'a'; marks[idx] = grid_to_clip_index(selected_row, selected_col); } } // Go to a mark static void go_to_mark(char mark_char) { if (mark_char >= 'a' && mark_char <= 'z') { int idx = mark_char - 'a'; int clip_idx = marks[idx]; if (clip_idx >= 0 && clip_idx < GRID_ROWS * GRID_COLS) { selected_row = clip_idx / GRID_COLS; selected_col = clip_idx % GRID_COLS; } } } // Play next scene (next row) static void play_next_scene(void) { int next_row = (selected_row + 1) % GRID_ROWS; Action action = { .type = ACTION_TRIGGER_SCENE, .data.trigger_scene = { .scene_index = next_row } }; g_dispatch(action); selected_row = next_row; } // Play previous scene (previous row) static void play_prev_scene(void) { int prev_row = (selected_row - 1 + GRID_ROWS) % GRID_ROWS; Action action = { .type = ACTION_TRIGGER_SCENE, .data.trigger_scene = { .scene_index = prev_row } }; g_dispatch(action); selected_row = prev_row; } // Fuzzy matching function static bool fuzzy_match(const char *pattern, const char *text) { if (!pattern || !*pattern) return true; if (!text) return false; while (*pattern) { char p = *pattern; while (*text && tolower((unsigned char)*text) != tolower((unsigned char)p)) text++; if (!*text) return false; text++; pattern++; } return true; } // Draw fuzzy search static void draw_fuzzy_search(void) { int start_y = LINES / 3; int start_x = COLS / 4; int width = COLS / 2; int height = LINES / 3; // Draw border for (int y = start_y - 1; y <= start_y + height; y++) { for (int x = start_x - 1; x <= start_x + width; x++) { mvaddch(y, x, ' '); } } // Draw title attron(A_BOLD); mvprintw(start_y - 1, start_x, "%s", fuzzy_search.prompt); attroff(A_BOLD); // Draw search box mvprintw(start_y, start_x, "> %s", fuzzy_search.query); // Draw results for (int i = 0; i < height - 2 && i < fuzzy_search.num_results; i++) { int idx = fuzzy_search.result_indices[i]; if (i == fuzzy_search.selected_index) { attron(A_REVERSE); } int count; const char **plugins = carla_get_available_plugins(NULL, &count); if (plugins && idx >= 0 && idx < count) { mvprintw(start_y + 1 + i, start_x, "%s", plugins[idx]); } if (i == fuzzy_search.selected_index) { attroff(A_REVERSE); } } } // Handle fuzzy search input static bool handle_fuzzy_search(int ch) { if (!fuzzy_search.active) return false; switch (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]); } } fuzzy_search.active = false; return true; } case 27: { // ESC fuzzy_search.active = false; return true; } case KEY_BACKSPACE: case 127: { if (fuzzy_search.query_len > 0) { fuzzy_search.query[--fuzzy_search.query_len] = '\0'; // Update results fuzzy_search.num_results = 0; int count; const char **plugins = carla_get_available_plugins(NULL, &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) { fuzzy_search.selected_index = fuzzy_search.num_results - 1; } } return true; } case KEY_UP: { if (fuzzy_search.selected_index > 0) { fuzzy_search.selected_index--; } return true; } case KEY_DOWN: { if (fuzzy_search.selected_index < fuzzy_search.num_results - 1) { fuzzy_search.selected_index++; } return true; } default: { if (ch >= 32 && ch <= 126 && fuzzy_search.query_len < 255) { fuzzy_search.query[fuzzy_search.query_len++] = (char)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(NULL, &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; } return true; } } } // Start fuzzy search static void start_fuzzy_search(const char *prompt, void (*callback)(const char *)) { fuzzy_search.active = true; fuzzy_search.query_len = 0; fuzzy_search.query[0] = '\0'; fuzzy_search.selected_index = 0; strncpy(fuzzy_search.prompt, prompt, sizeof(fuzzy_search.prompt) - 1); fuzzy_search.prompt[sizeof(fuzzy_search.prompt) - 1] = '\0'; fuzzy_search.callback = callback; // Initialize results with all plugins fuzzy_search.num_results = 0; int count; const char **plugins = carla_get_available_plugins(NULL, &count); for (int i = 0; i < count; i++) { fuzzy_search.result_indices[fuzzy_search.num_results++] = i; } } // Draw rack view static void draw_rack_view(void) { clear(); attron(A_BOLD); mvprintw(0, 0, "JACK Looper - Rack View (Channel %d)", rack_selected_channel); attroff(A_BOLD); AppState state = dispatcher_get_state(); ChannelRack *rack = &state.carla_host.channel_racks[rack_selected_channel]; // Draw channel selector mvprintw(1, 0, "Channel: "); for (int ch = 0; ch < 8; ch++) { if (ch == rack_selected_channel) { attron(A_REVERSE); } mvprintw(1, 10 + ch * 4, "Ch%d", ch); if (ch == rack_selected_channel) { attroff(A_REVERSE); } } // Draw volume mvprintw(2, 0, "Volume: %.2f [ - ] to decrease, [ = ] to increase", rack->volume); // Draw bypass toggle mvprintw(3, 0, "Bypass: %s [b] to toggle", rack->bypassed ? "ON" : "OFF"); // Draw plugin chain mvprintw(4, 0, "=== Plugin Chain ==="); int y = 5; for (int p = 0; p < rack->num_plugins; p++) { PluginInfo *plugin = &rack->plugins[p]; if (p == rack_selected_plugin) { attron(A_REVERSE); } mvprintw(y++, 2, "[%d] %s", p, plugin->name); if (p == rack_selected_plugin) { attroff(A_REVERSE); } // Draw parameters if this plugin is selected if (p == rack_selected_plugin) { for (int param = 0; param < plugin->num_parameters; param++) { if (param == rack_selected_param) { attron(A_REVERSE); } mvprintw(y++, 4, "%s: %.3f", plugin->parameter_names[param], plugin->parameters[param]); if (param == rack_selected_param) { attroff(A_REVERSE); } } } } // Draw controls help mvprintw(y + 1, 0, "=== Controls ==="); mvprintw(y + 2, 0, "h/l - Previous/Next channel"); mvprintw(y + 3, 0, "j/k - Navigate plugins"); mvprintw(y + 4, 0, "a - Add plugin to rack"); mvprintw(y + 5, 0, "d - Remove selected plugin"); mvprintw(y + 6, 0, "b - Toggle bypass"); mvprintw(y + 7, 0, "-/= - Decrease/Increase volume"); mvprintw(y + 8, 0, "Enter - Select parameter to edit"); mvprintw(y + 9, 0, "Tab - Switch to grid view"); mvprintw(y + 10, 0, "q/Esc - Quit"); refresh(); } // Handle rack view input static bool handle_rack_view(int ch) { AppState state = dispatcher_get_state(); ChannelRack *rack = &state.carla_host.channel_racks[rack_selected_channel]; switch (ch) { case 'h': case KEY_LEFT: rack_selected_channel = (rack_selected_channel - 1 + 8) % 8; rack_selected_plugin = -1; rack_selected_param = -1; break; case 'l': case KEY_RIGHT: rack_selected_channel = (rack_selected_channel + 1) % 8; rack_selected_plugin = -1; rack_selected_param = -1; break; case 'j': case KEY_DOWN: if (rack_selected_plugin < rack->num_plugins - 1) { rack_selected_plugin++; rack_selected_param = -1; } break; case 'k': case KEY_UP: if (rack_selected_plugin > 0) { rack_selected_plugin--; rack_selected_param = -1; } else if (rack_selected_plugin == 0) { rack_selected_plugin = -1; } break; case 'a': { // Start fuzzy search for plugin selection start_fuzzy_search("Add plugin:", [](const char *selection) { if (selection) { // Parse URI from selection (format: "uri - name") char uri[512]; const char *space = strchr(selection, ' '); if (space) { size_t len = space - selection; strncpy(uri, selection, len); uri[len] = '\0'; } else { strncpy(uri, selection, sizeof(uri) - 1); } Action action = { .type = ACTION_RACK_ADD_PLUGIN, .data.rack_add_plugin = { .channel = rack_selected_channel, .type = PLUGIN_TYPE_INTERNAL } }; strncpy(action.data.rack_add_plugin.uri, uri, sizeof(action.data.rack_add_plugin.uri) - 1); action.data.rack_add_plugin.uri[sizeof(action.data.rack_add_plugin.uri) - 1] = '\0'; g_dispatch(action); } }); break; } case 'd': { if (rack_selected_plugin >= 0 && rack_selected_plugin < rack->num_plugins) { Action action = { .type = ACTION_RACK_REMOVE_PLUGIN, .data.rack_remove_plugin = { .channel = rack_selected_channel, .plugin_index = rack_selected_plugin } }; g_dispatch(action); rack_selected_plugin = -1; rack_selected_param = -1; } break; } case 'b': { Action action = { .type = ACTION_RACK_BYPASS, .data.rack_bypass = { .channel = rack_selected_channel, .bypass = !rack->bypassed } }; g_dispatch(action); break; } case '-': case '_': { float new_vol = fmaxf(0.0f, rack->volume - 0.1f); Action action = { .type = ACTION_RACK_SET_VOLUME, .data.rack_set_volume = { .channel = rack_selected_channel, .volume = new_vol } }; g_dispatch(action); break; } case '=': case '+': { float new_vol = fminf(2.0f, rack->volume + 0.1f); Action action = { .type = ACTION_RACK_SET_VOLUME, .data.rack_set_volume = { .channel = rack_selected_channel, .volume = new_vol } }; g_dispatch(action); break; } case '\t': { current_view = VIEW_GRID; break; } case 'q': case 27: return true; // Quit } return false; } // Draw a single cell static void draw_cell(int row, int col, bool selected) { int clip_idx = grid_to_clip_index(row, col); AppState state = dispatcher_get_state(); Clip *clip = &state.clips[clip_idx]; int y = row * CELL_HEIGHT + 1; int x = col * CELL_WIDTH + 1; int color = state_to_color(clip->state); if (selected) { color = COLOR_SELECTED; } else if (current_mode == MODE_VISUAL && is_in_visual_selection(row, col)) { color = COLOR_SELECTED; } 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", clip_idx); char state_char; switch (clip->state) { case CLIP_EMPTY: state_char = ' '; break; case CLIP_RECORDING: state_char = 'R'; break; case CLIP_LOOPING: state_char = 'L'; break; case CLIP_STOPPED: state_char = 'S'; break; default: state_char = '?'; break; } mvaddch(y + 1, x + 4, state_char); attroff(COLOR_PAIR(color)); } // Draw the entire grid static void draw_grid(void) { clear(); attron(A_BOLD); mvprintw(0, 0, "JACK Looper - 8x8 Clip Grid"); attroff(A_BOLD); AppState state = dispatcher_get_state(); for (int row = 0; row < GRID_ROWS; row++) { for (int col = 0; col < GRID_COLS; col++) { bool selected = (row == selected_row && col == selected_col); draw_cell(row, col, selected); } } int clip_idx = grid_to_clip_index(selected_row, selected_col); Clip *clip = &state.clips[clip_idx]; mvprintw(GRID_ROWS * CELL_HEIGHT + 1, 0, "Selected: Clip %d | State: %s | Buffer: %zu samples", clip_idx, clip_state_to_string(clip->state), clip->buffer_size); mvprintw(GRID_ROWS * CELL_HEIGHT + 2, 0, "Transport: %s | Clock: %s | BPM: %.1f | Quantize: %s | Threshold: %u", transport_state_to_string(state.transport_state), clock_source_to_string(state.clock_source), state.bpm, quantize_mode_to_string(state.quantize_mode), (unsigned int)state.quantize_threshold); const char *mode_str; switch (current_mode) { case MODE_NORMAL: mode_str = "NORMAL"; break; case MODE_VISUAL: mode_str = "VISUAL"; break; case MODE_MOVE: mode_str = "MOVE"; break; default: mode_str = "?"; break; } mvprintw(GRID_ROWS * CELL_HEIGHT + 3, 0, "Mode: %s", mode_str); if (show_help) { attron(COLOR_PAIR(COLOR_HELP)); mvprintw(GRID_ROWS * CELL_HEIGHT + 4, 0, "=== Help ==="); mvprintw(GRID_ROWS * CELL_HEIGHT + 5, 0, "h/j/k/l - Navigate grid (left/down/up/right)"); mvprintw(GRID_ROWS * CELL_HEIGHT + 6, 0, "t - Trigger selected clip"); mvprintw(GRID_ROWS * CELL_HEIGHT + 7, 0, "r - Reset selected clip"); mvprintw(GRID_ROWS * CELL_HEIGHT + 8, 0, "s - Trigger scene (current row)"); mvprintw(GRID_ROWS * CELL_HEIGHT + 9, 0, "Space - Play/Pause transport"); mvprintw(GRID_ROWS * CELL_HEIGHT + 10, 0, "S - Stop transport"); mvprintw(GRID_ROWS * CELL_HEIGHT + 11, 0, "C - Toggle clock source (Internal/MIDI)"); mvprintw(GRID_ROWS * CELL_HEIGHT + 12, 0, "q - Toggle quantize mode (off/beat/bar)"); mvprintw(GRID_ROWS * CELL_HEIGHT + 13, 0, "T - Set quantize threshold"); mvprintw(GRID_ROWS * CELL_HEIGHT + 14, 0, "x - Reset transport position"); mvprintw(GRID_ROWS * CELL_HEIGHT + 15, 0, "? - Toggle help"); mvprintw(GRID_ROWS * CELL_HEIGHT + 16, 0, "Esc/q - Quit"); attroff(COLOR_PAIR(COLOR_HELP)); } refresh(); } // Handle command mode input (after pressing ':') static bool handle_command_mode(void) { char cmd_buffer[256]; int cmd_pos = 0; memset(cmd_buffer, 0, sizeof(cmd_buffer)); int prev_nodelay = nodelay(stdscr, FALSE); mvprintw(LINES - 1, 0, ":"); clrtoeol(); refresh(); while (1) { int ch = getch(); if (ch == '\n' || ch == '\r') { cmd_buffer[cmd_pos] = '\0'; mvprintw(LINES - 1, 0, " "); refresh(); if (strcmp(cmd_buffer, "q") == 0) { nodelay(stdscr, prev_nodelay); return true; } else if (strcmp(cmd_buffer, "view rack") == 0) { current_view = VIEW_RACK; rack_selected_channel = selected_row; rack_selected_plugin = -1; rack_selected_param = -1; } else if (strncmp(cmd_buffer, "rack ", 5) == 0) { // :rack - add plugin to selected channel's rack const char *plugin_name = cmd_buffer + 5; Action action = { .type = ACTION_RACK_ADD_PLUGIN, .data.rack_add_plugin = { .channel = selected_row, .type = PLUGIN_TYPE_INTERNAL } }; strncpy(action.data.rack_add_plugin.uri, plugin_name, sizeof(action.data.rack_add_plugin.uri) - 1); action.data.rack_add_plugin.uri[sizeof(action.data.rack_add_plugin.uri) - 1] = '\0'; g_dispatch(action); } else if (strcmp(cmd_buffer, "from") == 0) { // :from - open fuzzy search for MIDI input source start_fuzzy_search("Select MIDI input source:", [](const char *selection) { if (selection) { printf("Selected MIDI input: %s\n", selection); // In a real implementation, this would connect JACK ports } }); } else if (strcmp(cmd_buffer, "to") == 0) { // :to - open fuzzy search for MIDI output destination start_fuzzy_search("Select MIDI output destination:", [](const char *selection) { if (selection) { printf("Selected MIDI output: %s\n", selection); // In a real implementation, this would connect JACK ports } }); } else if (strncmp(cmd_buffer, "load ", 5) == 0) { char *rest = cmd_buffer + 5; int clip_idx = atoi(rest); char *filename = rest; while (*filename && *filename != ' ') filename++; if (*filename) { *filename = '\0'; filename++; while (*filename == ' ') filename++; if (*filename) { Action action = { .type = ACTION_LOAD_CLIP, .data.load_clip = { .clip_index = clip_idx } }; strncpy(action.data.load_clip.filename, filename, 255); action.data.load_clip.filename[255] = '\0'; g_dispatch(action); } } } else if (strncmp(cmd_buffer, "save ", 5) == 0) { int clip_idx = atoi(cmd_buffer + 5); Action action = { .type = ACTION_SAVE_CLIP, .data.save_clip = { .clip_index = clip_idx } }; g_dispatch(action); } nodelay(stdscr, prev_nodelay); return false; } else if (ch == 27) { mvprintw(LINES - 1, 0, " "); refresh(); nodelay(stdscr, prev_nodelay); return false; } else if (ch == KEY_BACKSPACE || ch == 127) { if (cmd_pos > 0) { cmd_pos--; cmd_buffer[cmd_pos] = '\0'; mvprintw(LINES - 1, 0, ":%s ", cmd_buffer); refresh(); } } else if (cmd_pos < (int)sizeof(cmd_buffer) - 1) { cmd_buffer[cmd_pos++] = (char)ch; cmd_buffer[cmd_pos] = '\0'; mvprintw(LINES - 1, 0, ":%s", cmd_buffer); refresh(); } } nodelay(stdscr, prev_nodelay); return false; } // Handle mouse events static void handle_mouse_event(MEVENT *event) { int grid_row = (event->y - 1) / CELL_HEIGHT; int grid_col = (event->x - 1) / CELL_WIDTH; if (grid_row < 0 || grid_row >= GRID_ROWS || grid_col < 0 || grid_col >= GRID_COLS) { return; } if (event->bstate & BUTTON1_CLICKED) { selected_row = grid_row; selected_col = grid_col; int clip_idx = grid_to_clip_index(selected_row, selected_col); Action action = { .type = ACTION_TRIGGER_CLIP, .data.trigger_clip = { .clip_index = clip_idx } }; g_dispatch(action); } else if (event->bstate & BUTTON3_CLICKED) { selected_row = grid_row; selected_col = grid_col; int clip_idx = grid_to_clip_index(selected_row, selected_col); Action action = { .type = ACTION_RESET_CLIP, .data.reset_clip = { .clip_index = clip_idx } }; g_dispatch(action); } else if (event->bstate & BUTTON2_CLICKED) { selected_row = grid_row; selected_col = grid_col; Action action = { .type = ACTION_TRIGGER_SCENE, .data.trigger_scene = { .scene_index = selected_row } }; g_dispatch(action); } else if (event->bstate & BUTTON1_DOUBLE_CLICKED) { selected_row = grid_row; selected_col = grid_col; Action action = { .type = ACTION_TRIGGER_SCENE, .data.trigger_scene = { .scene_index = selected_row } }; g_dispatch(action); } } void tui_init(Engine *engine) { g_engine = engine; g_dispatch = engine->dispatch; initscr(); cbreak(); noecho(); keypad(stdscr, TRUE); curs_set(0); mousemask(BUTTON1_CLICKED | BUTTON3_CLICKED | BUTTON2_CLICKED | BUTTON1_DOUBLE_CLICKED, NULL); mouseinterval(10); if (has_colors()) { 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); } } void tui_run(Engine *engine) { if (!engine) return; g_engine = engine; g_dispatch = engine->dispatch; for (int i = 0; i < MAX_MARKS; i++) { marks[i] = -1; } // Initialize Carla AppState init_state = dispatcher_get_state(); carla_init(&init_state.carla_host, engine->client); carla_scan_plugins(&init_state.carla_host); draw_grid(); while (1) { // Handle fuzzy search first if (fuzzy_search.active) { draw_fuzzy_search(); int ch = getch(); if (handle_fuzzy_search(ch)) { // Fuzzy search closed, redraw current view if (current_view == VIEW_GRID) { draw_grid(); } else { draw_rack_view(); } } else { draw_fuzzy_search(); } continue; } // Handle current view if (current_view == VIEW_RACK) { draw_rack_view(); int ch = getch(); if (handle_rack_view(ch)) { return; // Quit } continue; } // Grid view (existing code) int ch = getch(); if (ch == ERR) { break; } if (ch == KEY_MOUSE) { MEVENT event; if (getmouse(&event) == OK) { handle_mouse_event(&event); draw_grid(); continue; } } if (current_mode == MODE_MOVE) { switch (ch) { 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 '\n': case '\r': case 27: current_mode = MODE_NORMAL; break; } draw_grid(); continue; } if (current_mode == MODE_VISUAL) { switch (ch) { case 'h': case KEY_LEFT: visual_end_col = (visual_end_col - 1 + GRID_COLS) % GRID_COLS; break; case 'j': case KEY_DOWN: visual_end_row = (visual_end_row + 1) % GRID_ROWS; break; case 'k': case KEY_UP: visual_end_row = (visual_end_row - 1 + GRID_ROWS) % GRID_ROWS; break; case 'l': case KEY_RIGHT: visual_end_col = (visual_end_col + 1) % GRID_COLS; break; case 'd': { int count; int *clips = get_selected_clips(&count); if (clips) { delete_clips(clips, count); free(clips); } current_mode = MODE_NORMAL; break; } case 'y': { int count; int *clips = get_selected_clips(&count); if (clips) { yank_clips(clips, count); free(clips); } current_mode = MODE_NORMAL; break; } case 27: current_mode = MODE_NORMAL; break; } draw_grid(); continue; } // Normal mode switch (ch) { 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': { int clip_idx = grid_to_clip_index(selected_row, selected_col); Action action = { .type = ACTION_TRIGGER_CLIP, .data.trigger_clip = { .clip_index = clip_idx } }; g_dispatch(action); break; } case 'd': { int clip_idx = grid_to_clip_index(selected_row, selected_col); Action action = { .type = ACTION_RESET_CLIP, .data.reset_clip = { .clip_index = clip_idx } }; g_dispatch(action); break; } case 'y': { int clip_idx = grid_to_clip_index(selected_row, selected_col); yank_clips(&clip_idx, 1); break; } case 'p': paste_clips(); break; case 's': { Action action = { .type = ACTION_TRIGGER_SCENE, .data.trigger_scene = { .scene_index = selected_row } }; g_dispatch(action); break; } case 'q': { AppState state = dispatcher_get_state(); QuantizeMode modes[] = {QUANTIZE_OFF, QUANTIZE_BEAT, QUANTIZE_BAR}; int num_modes = sizeof(modes) / sizeof(modes[0]); int current = 0; for (int i = 0; i < num_modes; i++) { if (state.quantize_mode == modes[i]) { current = i; break; } } QuantizeMode next = modes[(current + 1) % num_modes]; Action action = { .type = ACTION_SET_QUANTIZE_MODE, .data.set_quantize_mode = { .mode = next } }; g_dispatch(action); break; } case 'T': { AppState state = dispatcher_get_state(); jack_nframes_t new_threshold = (state.quantize_threshold == 0) ? 1000 : 0; Action action = { .type = ACTION_SET_QUANTIZE_THRESHOLD, .data.set_quantize_threshold = { .threshold = new_threshold } }; g_dispatch(action); break; } case ' ': { Action action = { .type = ACTION_TRANSPORT_TOGGLE_PLAY }; g_dispatch(action); break; } case 'S': { Action action = { .type = ACTION_TRANSPORT_STOP }; g_dispatch(action); break; } case 'C': { AppState state = dispatcher_get_state(); ClockSource next = (state.clock_source == CLOCK_SOURCE_INTERNAL) ? CLOCK_SOURCE_MIDI : CLOCK_SOURCE_INTERNAL; Action action = { .type = ACTION_SET_CLOCK_SOURCE, .data.set_clock_source = { .source = next } }; g_dispatch(action); break; } case 'x': { Action action = { .type = ACTION_RESET_TRANSPORT }; g_dispatch(action); break; } case ':': { bool should_quit = handle_command_mode(); if (should_quit) return; break; } case '?': show_help = !show_help; break; case 'v': { current_mode = MODE_VISUAL; visual_start_row = selected_row; visual_start_col = selected_col; visual_end_row = selected_row; visual_end_col = selected_col; break; } case 'V': { current_mode = MODE_VISUAL; visual_start_row = selected_row; visual_start_col = 0; visual_end_row = selected_row; visual_end_col = GRID_COLS - 1; break; } case 'm': { current_mode = MODE_MOVE; break; } case 'N': play_next_scene(); break; case 'P': play_prev_scene(); break; case '\'': { nodelay(stdscr, FALSE); int mark_ch = getch(); nodelay(stdscr, TRUE); if (mark_ch != ERR) go_to_mark((char)mark_ch); break; } case 'u': { Action action = { .type = ACTION_UNDO }; g_dispatch(action); break; } case 18: { // Ctrl+R Action action = { .type = ACTION_REDO }; g_dispatch(action); break; } case '-': case '_': { // Decrease volume of selected channel AppState state = dispatcher_get_state(); float current_vol = carla_get_channel_volume(&state.carla_host, selected_row); float new_vol = fmaxf(0.0f, current_vol - 0.1f); Action action = { .type = ACTION_RACK_SET_VOLUME, .data.rack_set_volume = { .channel = selected_row, .volume = new_vol } }; g_dispatch(action); break; } case '=': case '+': { // Increase volume of selected channel AppState state = dispatcher_get_state(); float current_vol = carla_get_channel_volume(&state.carla_host, selected_row); float new_vol = fminf(2.0f, current_vol + 0.1f); Action action = { .type = ACTION_RACK_SET_VOLUME, .data.rack_set_volume = { .channel = selected_row, .volume = new_vol } }; g_dispatch(action); break; } case '\t': { // Switch to rack view current_view = VIEW_RACK; rack_selected_channel = selected_row; rack_selected_plugin = -1; rack_selected_param = -1; break; } case 27: case 'Q': return; default: if (ch == 'm') { nodelay(stdscr, FALSE); int mark_ch = getch(); nodelay(stdscr, TRUE); if (mark_ch != ERR) set_mark((char)mark_ch); } break; } draw_grid(); } } void tui_cleanup(void) { if (yank_buffer.clip_indices) { free(yank_buffer.clip_indices); yank_buffer.clip_indices = NULL; yank_buffer.count = 0; } mousemask(0, NULL); curs_set(1); endwin(); }