From f3dde6b6684add7aa4582cf2542d35487a663889 Mon Sep 17 00:00:00 2001 From: Loic Coenen Date: Wed, 13 May 2026 17:55:59 +0000 Subject: [PATCH] move engine to engine/ --- client/src/tui.c | 1549 ++++++++++++++++ client/src/tui.h | 18 + {docs => engine/docs}/1-multichannel.md | 0 .../docs}/11-arbitrary-number-of-channels.md | 0 {docs => engine/docs}/12-command-architecture | 0 .../docs}/12-command-architecture.md | 0 {docs => engine/docs}/2-midi-looping.md | 0 .../4-implement-scene-switching-engine.md | 0 evaluation.md => engine/evaluation.md | 0 makefile => engine/makefile | 0 {src => engine/src}/channel.c | 0 {src => engine/src}/channel.h | 0 {src => engine/src}/command.h | 0 {src => engine/src}/looper.c | 0 {src => engine/src}/looper.h | 0 {src => engine/src}/main.c | 0 {src => engine/src}/midi.c | 0 {src => engine/src}/midi.h | 0 {src => engine/src}/pipe.c | 0 {src => engine/src}/pipe.h | 0 {src => engine/src}/queue.c | 0 {src => engine/src}/queue.h | 0 {tests => engine/tests}/integration.c | 0 {tests => engine/tests}/main.c | 0 {tests => engine/tests}/test_audio.c | 0 {tests => engine/tests}/test_channel.c | 0 {tests => engine/tests}/test_fifo.c | 0 {tests => engine/tests}/test_loop.c | 0 {tests => engine/tests}/test_save.c | 0 .../tests/test_tui.c | 0 {tests => engine/tests}/test_wav.c | 0 engine/tests/unit_tests_tui.c | 1574 +++++++++++++++++ src/channel.o | Bin 9640 -> 0 bytes src/looper.o | Bin 22328 -> 0 bytes src/main.o | Bin 9984 -> 0 bytes src/midi.o | Bin 8552 -> 0 bytes src/pipe.o | Bin 10560 -> 0 bytes src/queue.o | Bin 5952 -> 0 bytes 38 files changed, 3141 insertions(+) create mode 100644 client/src/tui.c create mode 100644 client/src/tui.h rename {docs => engine/docs}/1-multichannel.md (100%) rename {docs => engine/docs}/11-arbitrary-number-of-channels.md (100%) rename {docs => engine/docs}/12-command-architecture (100%) rename {docs => engine/docs}/12-command-architecture.md (100%) rename {docs => engine/docs}/2-midi-looping.md (100%) rename {docs => engine/docs}/4-implement-scene-switching-engine.md (100%) rename evaluation.md => engine/evaluation.md (100%) rename makefile => engine/makefile (100%) rename {src => engine/src}/channel.c (100%) rename {src => engine/src}/channel.h (100%) rename {src => engine/src}/command.h (100%) rename {src => engine/src}/looper.c (100%) rename {src => engine/src}/looper.h (100%) rename {src => engine/src}/main.c (100%) rename {src => engine/src}/midi.c (100%) rename {src => engine/src}/midi.h (100%) rename {src => engine/src}/pipe.c (100%) rename {src => engine/src}/pipe.h (100%) rename {src => engine/src}/queue.c (100%) rename {src => engine/src}/queue.h (100%) rename {tests => engine/tests}/integration.c (100%) rename {tests => engine/tests}/main.c (100%) rename {tests => engine/tests}/test_audio.c (100%) rename {tests => engine/tests}/test_channel.c (100%) rename {tests => engine/tests}/test_fifo.c (100%) rename {tests => engine/tests}/test_loop.c (100%) rename {tests => engine/tests}/test_save.c (100%) rename tests/unit_tests_tui.c => engine/tests/test_tui.c (100%) rename {tests => engine/tests}/test_wav.c (100%) create mode 100644 engine/tests/unit_tests_tui.c delete mode 100644 src/channel.o delete mode 100644 src/looper.o delete mode 100644 src/main.o delete mode 100644 src/midi.o delete mode 100644 src/pipe.o delete mode 100644 src/queue.o diff --git a/client/src/tui.c b/client/src/tui.c new file mode 100644 index 0000000..75558c8 --- /dev/null +++ b/client/src/tui.c @@ -0,0 +1,1549 @@ +#include "tui.h" +#include "wav_io.h" +#include "transport.h" +#include "carla.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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; + +// Grid of grids state +static int selected_grid = 0; // Which grid we're viewing (0-7) +static bool zoom_mode = false; // Whether we're in zoom mode + +// 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); + 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}; + +// 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 grid, int row, int col) { + return grid * GRID_ROWS * GRID_COLS + 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(selected_grid, 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_grid = yank_buffer.clip_indices[0] / (GRID_ROWS * GRID_COLS); + int first_yanked_row = (yank_buffer.clip_indices[0] % (GRID_ROWS * GRID_COLS)) / GRID_COLS; + int first_yanked_col = (yank_buffer.clip_indices[0] % (GRID_ROWS * GRID_COLS)) % GRID_COLS; + int grid_offset = selected_grid - first_yanked_grid; + 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_grid = yank_buffer.clip_indices[i] / (GRID_ROWS * GRID_COLS); + int remainder = yank_buffer.clip_indices[i] % (GRID_ROWS * GRID_COLS); + int orig_row = remainder / GRID_COLS; + int orig_col = remainder % GRID_COLS; + + int new_grid = orig_grid + grid_offset; + int new_row = orig_row + row_offset; + int new_col = orig_col + col_offset; + + if (new_grid >= 0 && new_grid < NUM_GRIDS && + new_row >= 0 && new_row < GRID_ROWS && + new_col >= 0 && new_col < GRID_COLS) { + int new_clip_idx = grid_to_clip_index(new_grid, 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_grid, 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 < NUM_GRIDS * GRID_ROWS * GRID_COLS) { + selected_grid = clip_idx / (GRID_ROWS * GRID_COLS); + int remainder = clip_idx % (GRID_ROWS * GRID_COLS); + selected_row = remainder / GRID_COLS; + selected_col = remainder % 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 + selected_grid * GRID_ROWS } }; + 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 + selected_grid * GRID_ROWS } }; + g_dispatch(action); + selected_row = prev_row; +} + +// Callback for adding a plugin via fuzzy search +static void rack_add_plugin_callback(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); + } +} + +// Callback for MIDI from selection +static void midi_from_callback(const char *selection) { + if (selection) { + printf("Selected MIDI input: %s\n", selection); + // In a real implementation, this would connect JACK ports + } +} + +// Callback for MIDI to selection +static void midi_to_callback(const char *selection) { + if (selection) { + printf("Selected MIDI output: %s\n", selection); + // In a real implementation, this would connect JACK ports + } +} + +// 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; + 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; + + // Create a temporary window for the border + WINDOW *win = newwin(height + 2, width + 2, start_y - 1, start_x - 1); + if (!win) return; + wclear(win); + box(win, 0, 0); + wrefresh(win); + delwin(win); + + // Draw title (inside the border) + attron(A_BOLD); + mvprintw(start_y, start_x, "%s", fuzzy_search.prompt); + attroff(A_BOLD); + + // Draw search box + mvprintw(start_y + 1, 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); + } + 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); + } + } + + // Draw help line + mvprintw(start_y + height + 2, start_x, "j/k: navigate, h/l: page, Enter: select, Esc: cancel"); +} + +// 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]; + 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; + } + 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; + 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) { + fuzzy_search.selected_index = fuzzy_search.num_results - 1; + } + } + return true; + } + case 'k': case KEY_UP: { + if (fuzzy_search.selected_index > 0) { + fuzzy_search.selected_index--; + } + return true; + } + case 'j': case KEY_DOWN: { + if (fuzzy_search.selected_index < fuzzy_search.num_results - 1) { + fuzzy_search.selected_index++; + } + return true; + } + case 'h': { + // Page up - move up by 10 items + fuzzy_search.selected_index -= 10; + if (fuzzy_search.selected_index < 0) { + fuzzy_search.selected_index = 0; + } + return true; + } + case 'l': { + // Page down - move down by 10 items + fuzzy_search.selected_index += 10; + if (fuzzy_search.selected_index >= fuzzy_search.num_results) { + fuzzy_search.selected_index = fuzzy_search.num_results - 1; + } + 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; + 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; + } + return true; + } + } +} + +// 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'; + 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; + fuzzy_search.items = items; + fuzzy_search.num_items = num_items; + fuzzy_search.free_items = free_items; + + // Initialize results with all items + fuzzy_search.num_results = 0; + 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(); + + attron(A_BOLD); + mvprintw(0, 0, "JACK Looper - Rack View (Channel %d)", rack_selected_channel); + attroff(A_BOLD); + + AppState state; + dispatcher_get_state(&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 (or page in fuzzy search)"); + mvprintw(y + 3, 0, "j/k - Navigate plugins (or items in fuzzy search)"); + 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(&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:", rack_add_plugin_callback); + 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 grid, int row, int col, bool selected) { + int clip_idx = grid_to_clip_index(grid, row, col); + AppState state; + dispatcher_get_state(&state); + + int y = row * CELL_HEIGHT + 3; // Offset by 2 for grid selector + int x = col * CELL_WIDTH + 1; + + int color; + if (state.show_midi_grid) { + color = state_to_color(state.midi_clips[clip_idx].state); + } else { + color = state_to_color(state.clips[clip_idx].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, ' '); + } + } + + if (state.show_midi_grid) { + MidiClip *mclip = &state.midi_clips[clip_idx]; + mvprintw(y + 1, x + 1, "%2d", clip_idx); + char state_char; + switch (mclip->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); + } else { + Clip *clip = &state.clips[clip_idx]; + 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(); + + AppState state; + dispatcher_get_state(&state); + + if (zoom_mode) { + // Draw grid selector overview + attron(A_BOLD); + mvprintw(0, 0, "JACK Looper - Grid Selector (h/j/k/l navigate, Enter select, z exit)"); + attroff(A_BOLD); + + // Draw mini grids + for (int g = 0; g < NUM_GRIDS; g++) { + int gx = (g % 4) * 20; + int gy = (g / 4) * 10 + 2; + + if (g == selected_grid) { + attron(A_REVERSE); + } + mvprintw(gy, gx, "Grid %d", g); + if (g == selected_grid) { + attroff(A_REVERSE); + } + + // Draw mini representation (4x4 sampling of the 8x8 grid) + for (int r = 0; r < 4; r++) { + for (int c = 0; c < 4; c++) { + int clip_idx = grid_to_clip_index(g, r * 2, c * 2); + AppState state; + dispatcher_get_state(&state); + Clip *clip = &state.clips[clip_idx]; + int color = state_to_color(clip->state); + attron(COLOR_PAIR(color)); + mvaddch(gy + 1 + r, gx + 1 + c, ' '); + attroff(COLOR_PAIR(color)); + } + } + } + + refresh(); + return; + } + + // Normal grid view + attron(A_BOLD); + if (state.show_midi_grid) { + mvprintw(0, 0, "JACK Looper - MIDI Grid %d", selected_grid); + } else { + mvprintw(0, 0, "JACK Looper - Audio Grid %d", selected_grid); + } + attroff(A_BOLD); + + // Draw grid selector bar at top + mvprintw(1, 0, "Grid: "); + for (int g = 0; g < NUM_GRIDS; g++) { + if (g == selected_grid) { + attron(A_REVERSE); + } + mvprintw(1, 6 + g * 4, "G%d", g); + if (g == selected_grid) { + attroff(A_REVERSE); + } + } + + 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(selected_grid, row, col, selected); + } + } + + int clip_idx = grid_to_clip_index(selected_grid, selected_row, selected_col); + + if (state.show_midi_grid) { + MidiClip *mclip = &state.midi_clips[clip_idx]; + mvprintw(GRID_ROWS * CELL_HEIGHT + 3, 0, + "Selected: Clip %d | Grid %d | State: %s | MIDI events: %d", + clip_idx, selected_grid, clip_state_to_string(mclip->state), mclip->event_count); + } else { + Clip *clip = &state.clips[clip_idx]; + mvprintw(GRID_ROWS * CELL_HEIGHT + 3, 0, + "Selected: Clip %d | Grid %d | State: %s | Buffer: %zu samples", + clip_idx, selected_grid, clip_state_to_string(clip->state), clip->buffer_size); + } + + mvprintw(GRID_ROWS * CELL_HEIGHT + 4, 0, + "Transport: %s | Clock: %s | BPM: %.1f | Quantize: %s | Grid: %s", + transport_state_to_string(state.transport_state), + clock_source_to_string(state.clock_source), + state.bpm, + quantize_mode_to_string(state.quantize_mode), + state.show_midi_grid ? "MIDI" : "AUDIO"); + + 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 + 5, 0, "Mode: %s", mode_str); + + if (show_help) { + attron(COLOR_PAIR(COLOR_HELP)); + mvprintw(GRID_ROWS * CELL_HEIGHT + 6, 0, "=== Help ==="); + mvprintw(GRID_ROWS * CELL_HEIGHT + 7, 0, "h/j/k/l - Navigate grid (left/down/up/right)"); + mvprintw(GRID_ROWS * CELL_HEIGHT + 8, 0, "t - Trigger selected clip"); + mvprintw(GRID_ROWS * CELL_HEIGHT + 9, 0, "r - Reset selected clip"); + mvprintw(GRID_ROWS * CELL_HEIGHT + 10, 0, "s - Trigger scene (current row)"); + mvprintw(GRID_ROWS * CELL_HEIGHT + 11, 0, "Space - Play/Pause transport"); + mvprintw(GRID_ROWS * CELL_HEIGHT + 12, 0, "S - Stop transport"); + mvprintw(GRID_ROWS * CELL_HEIGHT + 13, 0, "C - Toggle clock source (Internal/MIDI)"); + mvprintw(GRID_ROWS * CELL_HEIGHT + 14, 0, "q - Toggle quantize mode (off/beat/bar)"); + mvprintw(GRID_ROWS * CELL_HEIGHT + 15, 0, "T - Set quantize threshold"); + mvprintw(GRID_ROWS * CELL_HEIGHT + 16, 0, "x - Reset transport position"); + mvprintw(GRID_ROWS * CELL_HEIGHT + 17, 0, "z - Toggle grid selector (zoom mode)"); + mvprintw(GRID_ROWS * CELL_HEIGHT + 18, 0, "? - Toggle help"); + mvprintw(GRID_ROWS * CELL_HEIGHT + 19, 0, "Esc/q - Quit"); + mvprintw(GRID_ROWS * CELL_HEIGHT + 20, 0, "In fuzzy search: j/k navigate, h/l page, Enter select, Esc cancel"); + mvprintw(GRID_ROWS * CELL_HEIGHT + 21, 0, "L - Load sample into selected clip (fuzzy search)"); + mvprintw(GRID_ROWS * CELL_HEIGHT + 22, 0, "G - Toggle Audio/MIDI grid"); + 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:", midi_from_callback); + } 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); + 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) { + const char *filename = cmd_buffer + 5; + while (*filename == ' ') filename++; + if (*filename) { + Action action = { .type = ACTION_SAVE_PROJECT }; + strncpy(action.data.save_project.filename, filename, sizeof(action.data.save_project.filename) - 1); + action.data.save_project.filename[sizeof(action.data.save_project.filename) - 1] = '\0'; + g_dispatch(action); + } + } else if (strncmp(cmd_buffer, "load ", 5) == 0) { + const char *filename = cmd_buffer + 5; + while (*filename == ' ') filename++; + if (*filename) { + Action action = { .type = ACTION_LOAD_PROJECT }; + strncpy(action.data.load_project.filename, filename, sizeof(action.data.load_project.filename) - 1); + action.data.load_project.filename[sizeof(action.data.load_project.filename) - 1] = '\0'; + 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 - 3) / CELL_HEIGHT; // Offset for grid selector + 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_grid, selected_row, selected_col); + AppState s; + dispatcher_get_state(&s); + if (s.show_midi_grid) { + Action action = { .type = ACTION_MIDI_CLIP_TRIGGER, .data.midi_clip_trigger = { .clip_index = clip_idx } }; + g_dispatch(action); + } else { + 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_grid, selected_row, selected_col); + AppState s; + dispatcher_get_state(&s); + if (s.show_midi_grid) { + Action action = { .type = ACTION_MIDI_CLIP_RESET, .data.midi_clip_reset = { .clip_index = clip_idx } }; + g_dispatch(action); + } else { + 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 + selected_grid * GRID_ROWS } }; + 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 + selected_grid * GRID_ROWS } }; + g_dispatch(action); + } +} + +void tui_init(Engine *engine) { + g_engine = engine; + g_dispatch = engine->dispatch; + + initscr(); + if (!has_colors()) { + endwin(); + fprintf(stderr, "Terminal does not support colors\n"); + exit(1); + } + + cbreak(); + noecho(); + keypad(stdscr, TRUE); + curs_set(0); + + // Check terminal size + if (LINES < 20 || COLS < 40) { + endwin(); + fprintf(stderr, "Terminal too small (need at least 20x40)\n"); + exit(1); + } + + mousemask(BUTTON1_CLICKED | BUTTON3_CLICKED | BUTTON2_CLICKED | BUTTON1_DOUBLE_CLICKED, NULL); + mouseinterval(10); + + 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; + } + + // Carla is now initialized in main.c before dispatcher starts + 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; + } + } + + // Handle zoom mode navigation + if (zoom_mode) { + switch (ch) { + case 'h': case KEY_LEFT: + selected_grid = (selected_grid - 1 + NUM_GRIDS) % NUM_GRIDS; + break; + case 'l': case KEY_RIGHT: + selected_grid = (selected_grid + 1) % NUM_GRIDS; + break; + case 'j': case KEY_DOWN: + selected_grid = (selected_grid + 4) % NUM_GRIDS; // Move down a row (4 grids per row) + break; + case 'k': case KEY_UP: + selected_grid = (selected_grid - 4 + NUM_GRIDS) % NUM_GRIDS; // Move up a row + break; + case '\n': case '\r': + // Select this grid and exit zoom mode + zoom_mode = false; + break; + case 'z': + zoom_mode = false; + break; + case 27: case 'Q': + return; + } + 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) { + AppState s; + dispatcher_get_state(&s); + if (s.show_midi_grid) { + for (int i = 0; i < count; i++) { + Action action = { .type = ACTION_MIDI_CLIP_RESET, .data.midi_clip_reset = { .clip_index = clips[i] } }; + g_dispatch(action); + } + } else { + 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_grid, selected_row, selected_col); + AppState s; + dispatcher_get_state(&s); + if (s.show_midi_grid) { + Action action = { .type = ACTION_MIDI_CLIP_TRIGGER, .data.midi_clip_trigger = { .clip_index = clip_idx } }; + g_dispatch(action); + } else { + 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_grid, selected_row, selected_col); + AppState s; + dispatcher_get_state(&s); + if (s.show_midi_grid) { + Action action = { .type = ACTION_MIDI_CLIP_RESET, .data.midi_clip_reset = { .clip_index = clip_idx } }; + g_dispatch(action); + } else { + 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_grid, 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 + selected_grid * GRID_ROWS } }; + g_dispatch(action); + break; + } + case 'q': { + AppState state; + dispatcher_get_state(&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(&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(&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 'z': { + zoom_mode = !zoom_mode; + break; + } + case 'G': { + AppState s; + dispatcher_get_state(&s); + Action a = { .type = ACTION_SET_SHOW_MIDI_GRID, .data.set_show_midi_grid = { .show = !s.show_midi_grid } }; + g_dispatch(a); + break; + } + case 'L': { + // 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); + } + 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(&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(&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(); +} diff --git a/client/src/tui.h b/client/src/tui.h new file mode 100644 index 0000000..eef59b3 --- /dev/null +++ b/client/src/tui.h @@ -0,0 +1,18 @@ +#ifndef TUI_H +#define TUI_H + +#include "engine.h" +#include "dispatcher.h" + +#define NUM_GRIDS 8 + +// Initialize TUI +void tui_init(Engine *engine); + +// Run the TUI main loop +void tui_run(Engine *engine); + +// Cleanup TUI +void tui_cleanup(void); + +#endif // TUI_H diff --git a/docs/1-multichannel.md b/engine/docs/1-multichannel.md similarity index 100% rename from docs/1-multichannel.md rename to engine/docs/1-multichannel.md diff --git a/docs/11-arbitrary-number-of-channels.md b/engine/docs/11-arbitrary-number-of-channels.md similarity index 100% rename from docs/11-arbitrary-number-of-channels.md rename to engine/docs/11-arbitrary-number-of-channels.md diff --git a/docs/12-command-architecture b/engine/docs/12-command-architecture similarity index 100% rename from docs/12-command-architecture rename to engine/docs/12-command-architecture diff --git a/docs/12-command-architecture.md b/engine/docs/12-command-architecture.md similarity index 100% rename from docs/12-command-architecture.md rename to engine/docs/12-command-architecture.md diff --git a/docs/2-midi-looping.md b/engine/docs/2-midi-looping.md similarity index 100% rename from docs/2-midi-looping.md rename to engine/docs/2-midi-looping.md diff --git a/docs/4-implement-scene-switching-engine.md b/engine/docs/4-implement-scene-switching-engine.md similarity index 100% rename from docs/4-implement-scene-switching-engine.md rename to engine/docs/4-implement-scene-switching-engine.md diff --git a/evaluation.md b/engine/evaluation.md similarity index 100% rename from evaluation.md rename to engine/evaluation.md diff --git a/makefile b/engine/makefile similarity index 100% rename from makefile rename to engine/makefile diff --git a/src/channel.c b/engine/src/channel.c similarity index 100% rename from src/channel.c rename to engine/src/channel.c diff --git a/src/channel.h b/engine/src/channel.h similarity index 100% rename from src/channel.h rename to engine/src/channel.h diff --git a/src/command.h b/engine/src/command.h similarity index 100% rename from src/command.h rename to engine/src/command.h diff --git a/src/looper.c b/engine/src/looper.c similarity index 100% rename from src/looper.c rename to engine/src/looper.c diff --git a/src/looper.h b/engine/src/looper.h similarity index 100% rename from src/looper.h rename to engine/src/looper.h diff --git a/src/main.c b/engine/src/main.c similarity index 100% rename from src/main.c rename to engine/src/main.c diff --git a/src/midi.c b/engine/src/midi.c similarity index 100% rename from src/midi.c rename to engine/src/midi.c diff --git a/src/midi.h b/engine/src/midi.h similarity index 100% rename from src/midi.h rename to engine/src/midi.h diff --git a/src/pipe.c b/engine/src/pipe.c similarity index 100% rename from src/pipe.c rename to engine/src/pipe.c diff --git a/src/pipe.h b/engine/src/pipe.h similarity index 100% rename from src/pipe.h rename to engine/src/pipe.h diff --git a/src/queue.c b/engine/src/queue.c similarity index 100% rename from src/queue.c rename to engine/src/queue.c diff --git a/src/queue.h b/engine/src/queue.h similarity index 100% rename from src/queue.h rename to engine/src/queue.h diff --git a/tests/integration.c b/engine/tests/integration.c similarity index 100% rename from tests/integration.c rename to engine/tests/integration.c diff --git a/tests/main.c b/engine/tests/main.c similarity index 100% rename from tests/main.c rename to engine/tests/main.c diff --git a/tests/test_audio.c b/engine/tests/test_audio.c similarity index 100% rename from tests/test_audio.c rename to engine/tests/test_audio.c diff --git a/tests/test_channel.c b/engine/tests/test_channel.c similarity index 100% rename from tests/test_channel.c rename to engine/tests/test_channel.c diff --git a/tests/test_fifo.c b/engine/tests/test_fifo.c similarity index 100% rename from tests/test_fifo.c rename to engine/tests/test_fifo.c diff --git a/tests/test_loop.c b/engine/tests/test_loop.c similarity index 100% rename from tests/test_loop.c rename to engine/tests/test_loop.c diff --git a/tests/test_save.c b/engine/tests/test_save.c similarity index 100% rename from tests/test_save.c rename to engine/tests/test_save.c diff --git a/tests/unit_tests_tui.c b/engine/tests/test_tui.c similarity index 100% rename from tests/unit_tests_tui.c rename to engine/tests/test_tui.c diff --git a/tests/test_wav.c b/engine/tests/test_wav.c similarity index 100% rename from tests/test_wav.c rename to engine/tests/test_wav.c diff --git a/engine/tests/unit_tests_tui.c b/engine/tests/unit_tests_tui.c new file mode 100644 index 0000000..c2392d7 --- /dev/null +++ b/engine/tests/unit_tests_tui.c @@ -0,0 +1,1574 @@ +#include +#include +#include +#include +#include +#include "dispatcher.h" +#include "tui.h" + +// Test helper functions +static AppState* create_test_state(void) { + AppState *state = (AppState *)calloc(1, sizeof(AppState)); + assert(state != NULL); + + // Initialize clips + for (int i = 0; i < MAX_CLIPS; i++) { + state->clips[i].state = CLIP_EMPTY; + state->clips[i].buffer = (float *)calloc(MAX_BUFFER_SIZE, sizeof(float)); + assert(state->clips[i].buffer != NULL); + state->clips[i].buffer_size = 0; + state->clips[i].write_position = 0; + state->clips[i].read_position = 0; + } + + // Initialize transport + state->transport_state = TRANSPORT_STOPPED; + state->clock_source = CLOCK_SOURCE_INTERNAL; + state->bpm = 120.0; + state->samples_per_beat = (48000 * 60.0) / 120.0; + state->clock_count = 0; + state->beat_position = 0; + state->bar_position = 0; + state->sample_position = 0; + state->sample_accumulator = 0.0; + + // Initialize quantize + state->quantize_mode = QUANTIZE_OFF; + state->quantize_threshold = 0; + + // Initialize undo + state->undo.undo_index = 0; + state->undo.redo_index = 0; + state->undo.count = 0; + state->undo.current_batch_size = 0; + for (int i = 0; i < MAX_UNDO_HISTORY; i++) { + state->undo.prev_clip_indices[i] = -1; + state->undo.batch_sizes[i] = 0; + } + + // JACK info + state->sample_rate = 48000; + state->running = true; + + return state; +} + +static void destroy_test_state(AppState *state) { + if (state) { + for (int i = 0; i < MAX_CLIPS; i++) { + free(state->clips[i].buffer); + state->clips[i].buffer = NULL; + } + free(state); + } +} + +// Test 1: Grid to clip index mapping +void test_grid_to_clip_index(void) { + printf("Test 1: Grid to clip index mapping... "); + + // 8x8 grid should map to 64 clips + assert(0 * 8 + 0 == 0); // Top-left + assert(0 * 8 + 7 == 7); // Top-right + assert(7 * 8 + 0 == 56); // Bottom-left + assert(7 * 8 + 7 == 63); // Bottom-right + assert(3 * 8 + 4 == 28); // Middle + + printf("PASSED\n"); +} + +// Test 2: Trigger clip via grid position +void test_trigger_via_grid(void) { + printf("Test 2: Trigger clip via grid position... "); + AppState *state = create_test_state(); + + // Simulate pressing 't' on grid position (3, 4) = clip 28 + int clip_idx = 3 * 8 + 4; + Action action = { .type = ACTION_TRIGGER_CLIP, .data.trigger_clip.clip_index = clip_idx }; + reducer(state, action); + + assert(state->clips[clip_idx].state == CLIP_RECORDING); + + destroy_test_state(state); + printf("PASSED\n"); +} + +// Test 3: Reset clip via grid position +void test_reset_via_grid(void) { + printf("Test 3: Reset clip via grid position... "); + AppState *state = create_test_state(); + + // Set up a clip at grid position (1, 2) = clip 10 + int clip_idx = 1 * 8 + 2; + state->clips[clip_idx].state = CLIP_LOOPING; + state->clips[clip_idx].buffer_size = 100; + + // Simulate pressing 'r' + Action action = { .type = ACTION_RESET_CLIP, .data.reset_clip.clip_index = clip_idx }; + reducer(state, action); + + assert(state->clips[clip_idx].state == CLIP_EMPTY); + assert(state->clips[clip_idx].buffer_size == 0); + + destroy_test_state(state); + printf("PASSED\n"); +} + +// Test 4: Scene trigger via grid row +void test_scene_via_grid(void) { + printf("Test 4: Scene trigger via grid row... "); + AppState *state = create_test_state(); + + // Simulate pressing 's' on row 3 + int scene_index = 3; + Action action = { .type = ACTION_TRIGGER_SCENE, .data.trigger_scene.scene_index = scene_index }; + reducer(state, action); + + // All clips in scene 3 should be recording + for (int ch = 0; ch < MAX_CHANNELS; ch++) { + int clip_idx = CLIP_INDEX(scene_index, ch); + assert(state->clips[clip_idx].state == CLIP_RECORDING); + } + + destroy_test_state(state); + printf("PASSED\n"); +} + +// Test 5: Quantize mode cycling +void test_quantize_cycling(void) { + printf("Test 5: Quantize mode cycling... "); + AppState *state = create_test_state(); + + // Simulate pressing 'q' to cycle through modes + assert(state->quantize_mode == QUANTIZE_OFF); + + // Cycle: OFF -> BEAT + Action action = { .type = ACTION_SET_QUANTIZE_MODE, .data.set_quantize_mode.mode = QUANTIZE_BEAT }; + reducer(state, action); + assert(state->quantize_mode == QUANTIZE_BEAT); + + // Cycle: BEAT -> BAR + action.data.set_quantize_mode.mode = QUANTIZE_BAR; + reducer(state, action); + assert(state->quantize_mode == QUANTIZE_BAR); + + // Cycle: BAR -> OFF + action.data.set_quantize_mode.mode = QUANTIZE_OFF; + reducer(state, action); + assert(state->quantize_mode == QUANTIZE_OFF); + + destroy_test_state(state); + printf("PASSED\n"); +} + +// Test 6: Threshold toggling +void test_threshold_toggle(void) { + printf("Test 6: Threshold toggling... "); + AppState *state = create_test_state(); + + // Simulate pressing 'T' to toggle threshold + assert(state->quantize_threshold == 0); + + // Toggle to 1000 + Action action = { .type = ACTION_SET_QUANTIZE_THRESHOLD, .data.set_quantize_threshold.threshold = 1000 }; + reducer(state, action); + assert(state->quantize_threshold == 1000); + + // Toggle back to 0 + action.data.set_quantize_threshold.threshold = 0; + reducer(state, action); + assert(state->quantize_threshold == 0); + + destroy_test_state(state); + printf("PASSED\n"); +} + +// Test 7: Transport reset +void test_transport_reset_via_tui(void) { + printf("Test 7: Transport reset via TUI... "); + AppState *state = create_test_state(); + + // Set up transport state + state->transport_state = TRANSPORT_PLAYING; + state->clock_count = 100; + state->beat_position = 2; + state->bar_position = 5; + state->sample_position = 10000; + + // Simulate pressing 'x' + Action action = { .type = ACTION_TRANSPORT_STOP }; + reducer(state, action); + + assert(state->transport_state == TRANSPORT_STOPPED); + assert(state->clock_count == 0); + assert(state->beat_position == 0); + assert(state->bar_position == 0); + assert(state->sample_position == 0); + + destroy_test_state(state); + printf("PASSED\n"); +} + +// Test 8: Navigation wrapping +void test_navigation_wrapping(void) { + printf("Test 8: Navigation wrapping... "); + + // Test that navigation wraps around the grid + // Left from column 0 should go to column 7 + int col = 0; + col = (col - 1 + 8) % 8; + assert(col == 7); + + // Right from column 7 should go to column 0 + col = 7; + col = (col + 1) % 8; + assert(col == 0); + + // Up from row 0 should go to row 7 + int row = 0; + row = (row - 1 + 8) % 8; + assert(row == 7); + + // Down from row 7 should go to row 0 + row = 7; + row = (row + 1) % 8; + assert(row == 0); + + printf("PASSED\n"); +} + +// Test 9: Multiple clips in different states +void test_multiple_clip_states(void) { + printf("Test 9: Multiple clips in different states... "); + AppState *state = create_test_state(); + + // Set up clips in various states + state->clips[0].state = CLIP_EMPTY; + state->clips[1].state = CLIP_RECORDING; + state->clips[2].state = CLIP_LOOPING; + state->clips[3].state = CLIP_STOPPED; + + // Verify states + assert(state->clips[0].state == CLIP_EMPTY); + assert(state->clips[1].state == CLIP_RECORDING); + assert(state->clips[2].state == CLIP_LOOPING); + assert(state->clips[3].state == CLIP_STOPPED); + + destroy_test_state(state); + printf("PASSED\n"); +} + +// Test 10: Buffer size display +void test_buffer_size_display(void) { + printf("Test 10: Buffer size display... "); + AppState *state = create_test_state(); + + // Set up a clip with known buffer size + state->clips[5].state = CLIP_LOOPING; + state->clips[5].buffer_size = 48000; // 1 second at 48kHz + + // Verify buffer size + assert(state->clips[5].buffer_size == 48000); + + destroy_test_state(state); + printf("PASSED\n"); +} + +// Test 11: Help toggle +void test_help_toggle(void) { + printf("Test 11: Help toggle... "); + + // Test that help flag toggles correctly + bool show_help = false; + + show_help = !show_help; + assert(show_help == true); + + show_help = !show_help; + assert(show_help == false); + + printf("PASSED\n"); +} + +// Test 12: Escape key handling +void test_escape_handling(void) { + printf("Test 12: Escape key handling... "); + + // Test that escape key (27) is handled + int ch = 27; + assert(ch == 27); // Escape + + // Test that 'Q' is handled + ch = 'Q'; + assert(ch == 'Q'); + + printf("PASSED\n"); +} + +// Test 13: TUI init and cleanup (without ncurses) +void test_tui_init_cleanup(void) { + printf("Test 13: TUI init and cleanup... "); + AppState *state = create_test_state(); + + // Verify state is valid + assert(state->sample_rate == 48000); + assert(state->running == true);; + + destroy_test_state(state); + printf("PASSED (skipped ncurses init)\n"); +} + +// Test 14: State to color mapping +void test_state_to_color_mapping(void) { + printf("Test 14: State to color mapping... "); + + // Verify state values match expected color indices + assert(CLIP_EMPTY == 0); + assert(CLIP_RECORDING == 1); + assert(CLIP_LOOPING == 2); + assert(CLIP_STOPPED == 3); + + printf("PASSED\n"); +} + +// Test 15: Full grid coverage +void test_full_grid_coverage(void) { + printf("Test 15: Full grid coverage... "); + AppState *state = create_test_state(); + + // Trigger all 64 clips via grid positions + for (int row = 0; row < 8; row++) { + for (int col = 0; col < 8; col++) { + int clip_idx = row * 8 + col; + Action action = { .type = ACTION_TRIGGER_CLIP, .data.trigger_clip.clip_index = clip_idx }; + reducer(state, action); + assert(state->clips[clip_idx].state == CLIP_RECORDING); + } + } + + // Verify all clips are recording + for (int i = 0; i < 64; i++) { // Only check the 8x8 grid clips + assert(state->clips[i].state == CLIP_RECORDING); + } + + destroy_test_state(state); + printf("PASSED\n"); +} + +// Test 16: Scene trigger from each row +void test_scene_from_each_row(void) { + printf("Test 16: Scene trigger from each row... "); + AppState *state = create_test_state(); + + // Trigger scene from each row + for (int row = 0; row < 8; row++) { + Action action = { .type = ACTION_TRIGGER_SCENE, .data.trigger_scene.scene_index = row }; + reducer(state, action); + + // Verify all clips in this scene are recording + for (int ch = 0; ch < MAX_CHANNELS; ch++) { + int clip_idx = CLIP_INDEX(row, ch); + assert(state->clips[clip_idx].state == CLIP_RECORDING); + } + } + + destroy_test_state(state); + printf("PASSED\n"); +} + +// Test 17: Quantize mode cycle through all modes +void test_quantize_full_cycle(void) { + printf("Test 17: Quantize mode full cycle... "); + AppState *state = create_test_state(); + + // Cycle through all modes twice + for (int cycle = 0; cycle < 2; cycle++) { + Action action = { .type = ACTION_SET_QUANTIZE_MODE, .data.set_quantize_mode.mode = QUANTIZE_OFF }; + reducer(state, action); + assert(state->quantize_mode == QUANTIZE_OFF); + + action.data.set_quantize_mode.mode = QUANTIZE_BEAT; + reducer(state, action); + assert(state->quantize_mode == QUANTIZE_BEAT); + + action.data.set_quantize_mode.mode = QUANTIZE_BAR; + reducer(state, action); + assert(state->quantize_mode == QUANTIZE_BAR); + } + + destroy_test_state(state); + printf("PASSED\n"); +} + +// Test 18: Multiple threshold toggles +void test_multiple_threshold_toggles(void) { + printf("Test 18: Multiple threshold toggles... "); + AppState *state = create_test_state(); + + // Toggle threshold multiple times + for (int i = 0; i < 5; i++) { + if (state->quantize_threshold == 0) { + Action action = { .type = ACTION_SET_QUANTIZE_THRESHOLD, .data.set_quantize_threshold.threshold = 1000 }; + reducer(state, action); + assert(state->quantize_threshold == 1000); + } else { + Action action = { .type = ACTION_SET_QUANTIZE_THRESHOLD, .data.set_quantize_threshold.threshold = 0 }; + reducer(state, action); + assert(state->quantize_threshold == 0); + } + } + + destroy_test_state(state); + printf("PASSED\n"); +} + +// Test 19: Transport reset multiple times +void test_multiple_transport_resets(void) { + printf("Test 19: Multiple transport resets... "); + AppState *state = create_test_state(); + + // Reset transport multiple times + for (int i = 0; i < 5; i++) { + state->transport_state = TRANSPORT_PLAYING; + state->clock_count = 100 + i; + state->beat_position = i % 4; + state->bar_position = i; + state->sample_position = 10000 * i; + + Action action = { .type = ACTION_TRANSPORT_STOP }; + reducer(state, action); + + assert(state->transport_state == TRANSPORT_STOPPED); + assert(state->clock_count == 0); + assert(state->beat_position == 0); + assert(state->bar_position == 0); + assert(state->sample_position == 0); + } + + destroy_test_state(state); + printf("PASSED\n"); +} + +// Test 20: Navigation with arrow keys +void test_arrow_key_navigation(void) { + printf("Test 20: Arrow key navigation... "); + + // Test that arrow keys produce same results as hjkl + int row = 3, col = 4; + + // KEY_LEFT (same as 'h') + col = (col - 1 + 8) % 8; + assert(col == 3); + + // KEY_DOWN (same as 'j') + row = (row + 1) % 8; + assert(row == 4); + + // KEY_UP (same as 'k') + row = (row - 1 + 8) % 8; + assert(row == 3); + + // KEY_RIGHT (same as 'l') + col = (col + 1) % 8; + assert(col == 4); + + printf("PASSED\n"); +} + +// Test 21: Command mode parsing - quit command +void test_command_mode_quit(void) { + printf("Test 21: Command mode quit command... "); + + // Test that ":q" command is recognized + const char *cmd = "q"; + assert(strcmp(cmd, "q") == 0); + + printf("PASSED\n"); +} + +// Test 22: Command mode parsing - empty command +void test_command_mode_empty(void) { + printf("Test 22: Command mode empty command... "); + + // Test that empty command doesn't quit + const char *cmd = ""; + assert(strcmp(cmd, "q") != 0); + + printf("PASSED\n"); +} + +// Test 23: Command mode parsing - unknown command +void test_command_mode_unknown(void) { + printf("Test 23: Command mode unknown command... "); + + // Test that unknown commands don't quit + const char *cmd = "unknown"; + assert(strcmp(cmd, "q") != 0); + + printf("PASSED\n"); +} + +// Test 24: Command mode buffer overflow protection +void test_command_mode_buffer_overflow(void) { + printf("Test 24: Command mode buffer overflow protection... "); + + // Test that buffer doesn't overflow with long input + char cmd_buffer[256]; + int cmd_pos = 0; + memset(cmd_buffer, 0, sizeof(cmd_buffer)); + + // Simulate typing more characters than buffer can hold + for (int i = 0; i < 300; i++) { + if (cmd_pos < (int)sizeof(cmd_buffer) - 1) { + cmd_buffer[cmd_pos++] = 'a'; + } + } + cmd_buffer[cmd_pos] = '\0'; + + // Buffer should not overflow + assert(strlen(cmd_buffer) < sizeof(cmd_buffer)); + assert(cmd_pos <= (int)sizeof(cmd_buffer) - 1); + + printf("PASSED\n"); +} + +// Test 25: Command mode backspace handling +void test_command_mode_backspace(void) { + printf("Test 25: Command mode backspace handling... "); + + // Test backspace removes characters + char cmd_buffer[256]; + int cmd_pos = 0; + memset(cmd_buffer, 0, sizeof(cmd_buffer)); + + // Type "test" + cmd_buffer[cmd_pos++] = 't'; + cmd_buffer[cmd_pos++] = 'e'; + cmd_buffer[cmd_pos++] = 's'; + cmd_buffer[cmd_pos++] = 't'; + cmd_buffer[cmd_pos] = '\0'; + assert(strcmp(cmd_buffer, "test") == 0); + + // Backspace twice + cmd_pos--; + cmd_buffer[cmd_pos] = '\0'; + cmd_pos--; + cmd_buffer[cmd_pos] = '\0'; + assert(strcmp(cmd_buffer, "te") == 0); + + printf("PASSED\n"); +} + +// Test 26: Command mode escape cancels +void test_command_mode_escape(void) { + printf("Test 26: Command mode escape cancels... "); + + // Test that escape key (27) cancels command mode + int ch = 27; + assert(ch == 27); // Escape + + printf("PASSED\n"); +} + +// Test 27: Command mode enter executes +void test_command_mode_enter(void) { + printf("Test 27: Command mode enter executes... "); + + // Test that enter key executes command + int ch = '\n'; + assert(ch == '\n'); + + ch = '\r'; + assert(ch == '\r'); + + printf("PASSED\n"); +} + +// Test 28: Command mode colon triggers mode +void test_command_mode_colon(void) { + printf("Test 28: Command mode colon triggers mode... "); + + // Test that ':' character triggers command mode + char ch = ':'; + assert(ch == ':'); + + printf("PASSED\n"); +} + +// Test 29: Visual mode entry +void test_visual_mode_entry(void) { + printf("Test 29: Visual mode entry... "); + + // Simulate pressing 'v' to enter visual mode + int current_mode = 1; // MODE_VISUAL + int selected_row = 3, selected_col = 4; + int visual_start_row = 0, visual_start_col = 0; + int visual_end_row = 0, visual_end_col = 0; + + // Press 'v' + current_mode = 1; // MODE_VISUAL + visual_start_row = selected_row; + visual_start_col = selected_col; + visual_end_row = selected_row; + visual_end_col = selected_col; + + assert(current_mode == 1); + assert(visual_start_row == 3); + assert(visual_start_col == 4); + assert(visual_end_row == 3); + assert(visual_end_col == 4); + + printf("PASSED\n"); +} + +// Test 30: Visual mode selection expansion +void test_visual_mode_selection(void) { + printf("Test 30: Visual mode selection expansion... "); + + int visual_start_row = 2, visual_start_col = 2; + int visual_end_row = 2, visual_end_col = 2; + + // Move right + visual_end_col = (visual_end_col + 1) % 8; + assert(visual_end_col == 3); + + // Move down + visual_end_row = (visual_end_row + 1) % 8; + assert(visual_end_row == 3); + + // Check selection bounds + 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; + + assert(min_row == 2); + assert(max_row == 3); + assert(min_col == 2); + assert(max_col == 3); + + printf("PASSED\n"); +} + +// Test 31: Visual mode escape returns to normal +void test_visual_mode_escape(void) { + printf("Test 31: Visual mode escape returns to normal... "); + + int current_mode = 1; // MODE_VISUAL + + // Press Escape + current_mode = 0; // MODE_NORMAL + assert(current_mode == 0); + + printf("PASSED\n"); +} + +// Test 32: Visual line selection +void test_visual_line_selection(void) { + printf("Test 32: Visual line selection... "); + + int selected_row = 3; + int current_mode = 0; // MODE_NORMAL + int visual_start_row = 0, visual_start_col = 0; + int visual_end_row = 0, visual_end_col = 0; + + // Press 'V' + current_mode = 1; // MODE_VISUAL + visual_start_row = selected_row; + visual_start_col = 0; + visual_end_row = selected_row; + visual_end_col = 7; // GRID_COLS - 1 + + assert(current_mode == 1); + assert(visual_start_row == 3); + assert(visual_start_col == 0); + assert(visual_end_row == 3); + assert(visual_end_col == 7); + + printf("PASSED\n"); +} + +// Test 33: Move mode entry and navigation +void test_move_mode_navigation(void) { + printf("Test 33: Move mode navigation... "); + + int current_mode = 0; // MODE_NORMAL + int selected_row = 3, selected_col = 4; + + // Enter move mode + current_mode = 2; // MODE_MOVE + assert(current_mode == 2); + + // Move left + selected_col = (selected_col - 1 + 8) % 8; + assert(selected_col == 3); + + // Move down + selected_row = (selected_row + 1) % 8; + assert(selected_row == 4); + + // Move up + selected_row = (selected_row - 1 + 8) % 8; + assert(selected_row == 3); + + // Move right + selected_col = (selected_col + 1) % 8; + assert(selected_col == 4); + + printf("PASSED\n"); +} + +// Test 34: Move mode returns to normal on enter +void test_move_mode_enter(void) { + printf("Test 34: Move mode returns to normal on enter... "); + + int current_mode = 2; // MODE_MOVE + + // Press Enter + current_mode = 0; // MODE_NORMAL + assert(current_mode == 0); + + printf("PASSED\n"); +} + +// Test 35: Move mode returns to normal on escape +void test_move_mode_escape(void) { + printf("Test 35: Move mode returns to normal on escape... "); + + int current_mode = 2; // MODE_MOVE + + // Press Escape + current_mode = 0; // MODE_NORMAL + assert(current_mode == 0); + + printf("PASSED\n"); +} + +// Test 36: Delete (reset) single clip +void test_delete_single_clip(void) { + printf("Test 36: Delete (reset) single clip... "); + AppState *state = create_test_state(); + + // Set up a looping clip + int clip_idx = 10; + state->clips[clip_idx].state = CLIP_LOOPING; + state->clips[clip_idx].buffer_size = 100; + state->clips[clip_idx].write_position = 100; + state->clips[clip_idx].read_position = 50; + + // Simulate pressing 'd' on selected clip + Action action = { .type = ACTION_RESET_CLIP, .data.reset_clip.clip_index = clip_idx }; + reducer(state, action); + + assert(state->clips[clip_idx].state == CLIP_EMPTY); + assert(state->clips[clip_idx].buffer_size == 0); + assert(state->clips[clip_idx].write_position == 0); + assert(state->clips[clip_idx].read_position == 0); + + destroy_test_state(state); + printf("PASSED\n"); +} + +// Test 37: Delete (reset) multiple clips via visual selection +void test_delete_visual_selection(void) { + printf("Test 37: Delete visual selection... "); + AppState *state = create_test_state(); + + // Set up clips in a 2x2 selection area + int clips[] = {18, 19, 26, 27}; // rows 2-3, cols 2-3 + for (int i = 0; i < 4; i++) { + state->clips[clips[i]].state = CLIP_LOOPING; + state->clips[clips[i]].buffer_size = 100; + state->clips[clips[i]].write_position = 100; + state->clips[clips[i]].read_position = 50; + } + + // Simulate deleting the selection + for (int i = 0; i < 4; i++) { + Action action = { .type = ACTION_RESET_CLIP, .data.reset_clip.clip_index = clips[i] }; + reducer(state, action); + } + + for (int i = 0; i < 4; i++) { + assert(state->clips[clips[i]].state == CLIP_EMPTY); + assert(state->clips[clips[i]].buffer_size == 0); + } + + destroy_test_state(state); + printf("PASSED\n"); +} + +// Test 38: Yank single clip +void test_yank_single_clip(void) { + printf("Test 38: Yank single clip... "); + AppState *state = create_test_state(); + + // Set up a clip + int clip_idx = 15; + state->clips[clip_idx].state = CLIP_LOOPING; + state->clips[clip_idx].buffer_size = 100; + + // Simulate yanking the clip - should stop it (looping -> stopped) + Action action = { .type = ACTION_TRIGGER_CLIP, .data.trigger_clip.clip_index = clip_idx }; + reducer(state, action); + + assert(state->clips[clip_idx].state == CLIP_STOPPED); + + destroy_test_state(state); + printf("PASSED\n"); +} + +// Test 39: Yank multiple clips via visual selection +void test_yank_visual_selection(void) { + printf("Test 39: Yank visual selection... "); + AppState *state = create_test_state(); + + // Set up clips in a 2x2 selection + int clips[] = {18, 19, 26, 27}; + for (int i = 0; i < 4; i++) { + state->clips[clips[i]].state = CLIP_LOOPING; + state->clips[clips[i]].buffer_size = 100; + } + + // Simulate yanking the selection - should stop all clips + for (int i = 0; i < 4; i++) { + Action action = { .type = ACTION_TRIGGER_CLIP, .data.trigger_clip.clip_index = clips[i] }; + reducer(state, action); + } + + for (int i = 0; i < 4; i++) { + assert(state->clips[clips[i]].state == CLIP_STOPPED); + } + + destroy_test_state(state); + printf("PASSED\n"); +} + +// Test 40: Paste clips +void test_paste_clips(void) { + printf("Test 40: Paste clips... "); + AppState *state = create_test_state(); + + // Simulate yanking clip at position (1, 1) = clip 9 + int yank_buffer[] = {9}; + int selected_row = 3, selected_col = 3; // Paste at position (3, 3) = clip 27 + + // Calculate offset + int first_yanked_row = yank_buffer[0] / 8; + int first_yanked_col = yank_buffer[0] % 8; + int row_offset = selected_row - first_yanked_row; + int col_offset = selected_col - first_yanked_col; + + assert(first_yanked_row == 1); + assert(first_yanked_col == 1); + assert(row_offset == 2); + assert(col_offset == 2); + + // Simulate paste: trigger clip three times to go empty -> recording -> looping -> stopped + int new_row = first_yanked_row + row_offset; + int new_col = first_yanked_col + col_offset; + int new_clip_idx = new_row * 8 + new_col; + + assert(new_row == 3); + assert(new_col == 3); + assert(new_clip_idx == 27); + + Action action = { .type = ACTION_TRIGGER_CLIP, .data.trigger_clip.clip_index = new_clip_idx }; + reducer(state, action); + reducer(state, action); + reducer(state, action); + assert(state->clips[27].state == CLIP_STOPPED); + + destroy_test_state(state); + printf("PASSED\n"); +} + +// Test 41: Paste with bounds checking +void test_paste_bounds_checking(void) { + printf("Test 41: Paste bounds checking... "); + AppState *state = create_test_state(); + + // Yank clip at position (7, 7) = clip 63 (bottom-right) + int yank_buffer[] = {63}; + int selected_row = 0, selected_col = 0; // Paste at top-left + + // Calculate offset + int first_yanked_row = yank_buffer[0] / 8; + int first_yanked_col = yank_buffer[0] % 8; + int row_offset = selected_row - first_yanked_row; + int col_offset = selected_col - first_yanked_col; + + assert(row_offset == -7); + assert(col_offset == -7); + + // Simulate paste: new position should be (0, 0) = clip 0 + int new_row = first_yanked_row + row_offset; + int new_col = first_yanked_col + col_offset; + + assert(new_row == 0); + assert(new_col == 0); + + // Test out-of-bounds paste (should be clipped) + selected_row = 0; + selected_col = 0; + row_offset = selected_row - 0; + col_offset = selected_col - 0; + + // Yank clip at (0, 0) and try to paste at (0, 0) - should work + new_row = 0 + row_offset; + new_col = 0 + col_offset; + assert(new_row >= 0 && new_row < 8 && new_col >= 0 && new_col < 8); + + destroy_test_state(state); + printf("PASSED\n"); +} + +// Test 42: Mark setting +void test_mark_setting(void) { + printf("Test 42: Mark setting... "); + + int marks[26]; + for (int i = 0; i < 26; i++) { + marks[i] = -1; + } + + int selected_row = 3, selected_col = 4; + int clip_idx = selected_row * 8 + selected_col; + + // Set mark 'a' + char mark_char = 'a'; + int idx = mark_char - 'a'; + marks[idx] = clip_idx; + + assert(marks[0] == 28); // 3 * 8 + 4 = 28 + + // Set mark 'z' + mark_char = 'z'; + idx = mark_char - 'a'; + marks[idx] = clip_idx; + + assert(marks[25] == 28); + + printf("PASSED\n"); +} + +// Test 43: Go to mark +void test_go_to_mark(void) { + printf("Test 43: Go to mark... "); + + int marks[26]; + for (int i = 0; i < 26; i++) { + marks[i] = -1; + } + + // Set mark 'b' to clip 42 + marks[1] = 42; // 'b' - 'a' = 1 + + // Go to mark 'b' + char mark_char = 'b'; + int idx = mark_char - 'a'; + int clip_idx = marks[idx]; + + assert(clip_idx == 42); + + // Calculate row and col + int row = clip_idx / 8; + int col = clip_idx % 8; + assert(row == 5); + assert(col == 2); + + printf("PASSED\n"); +} + +// Test 44: Go to unset mark +void test_go_to_unset_mark(void) { + printf("Test 44: Go to unset mark... "); + + int marks[26]; + for (int i = 0; i < 26; i++) { + marks[i] = -1; + } + + // Try to go to unset mark 'c' + char mark_char = 'c'; + int idx = mark_char - 'a'; + int clip_idx = marks[idx]; + + assert(clip_idx == -1); // Mark not set + + printf("PASSED\n"); +} + +// Test 45: Play next scene +void test_play_next_scene(void) { + printf("Test 45: Play next scene... "); + AppState *state = create_test_state(); + + int selected_row = 3; + + // Play next scene + int next_row = (selected_row + 1) % 8; + Action action = { .type = ACTION_TRIGGER_SCENE, .data.trigger_scene.scene_index = next_row }; + reducer(state, action); + + assert(next_row == 4); + + // Verify clips in scene 4 are recording + for (int ch = 0; ch < MAX_CHANNELS; ch++) { + int clip_idx = CLIP_INDEX(4, ch); + assert(state->clips[clip_idx].state == CLIP_RECORDING); + } + + destroy_test_state(state); + printf("PASSED\n"); +} + +// Test 46: Play previous scene +void test_play_prev_scene(void) { + printf("Test 46: Play previous scene... "); + AppState *state = create_test_state(); + + int selected_row = 3; + + // Play previous scene + int prev_row = (selected_row - 1 + 8) % 8; + Action action = { .type = ACTION_TRIGGER_SCENE, .data.trigger_scene.scene_index = prev_row }; + reducer(state, action); + + assert(prev_row == 2); + + // Verify clips in scene 2 are recording + for (int ch = 0; ch < MAX_CHANNELS; ch++) { + int clip_idx = CLIP_INDEX(2, ch); + assert(state->clips[clip_idx].state == CLIP_RECORDING); + } + + destroy_test_state(state); + printf("PASSED\n"); +} + +// Test 47: Play next scene wraps around +void test_play_next_scene_wrap(void) { + printf("Test 47: Play next scene wraps around... "); + AppState *state = create_test_state(); + + int selected_row = 7; // Last row + + // Play next scene should wrap to row 0 + int next_row = (selected_row + 1) % 8; + assert(next_row == 0); + + Action action = { .type = ACTION_TRIGGER_SCENE, .data.trigger_scene.scene_index = next_row }; + reducer(state, action); + + for (int ch = 0; ch < MAX_CHANNELS; ch++) { + int clip_idx = CLIP_INDEX(0, ch); + assert(state->clips[clip_idx].state == CLIP_RECORDING); + } + + destroy_test_state(state); + printf("PASSED\n"); +} + +// Test 48: Play previous scene wraps around +void test_play_prev_scene_wrap(void) { + printf("Test 48: Play previous scene wraps around... "); + AppState *state = create_test_state(); + + int selected_row = 0; // First row + + // Play previous scene should wrap to row 7 + int prev_row = (selected_row - 1 + 8) % 8; + assert(prev_row == 7); + + Action action = { .type = ACTION_TRIGGER_SCENE, .data.trigger_scene.scene_index = prev_row }; + reducer(state, action); + + for (int ch = 0; ch < MAX_CHANNELS; ch++) { + int clip_idx = CLIP_INDEX(7, ch); + assert(state->clips[clip_idx].state == CLIP_RECORDING); + } + + destroy_test_state(state); + printf("PASSED\n"); +} + +// Test 49: Visual mode delete then escape +void test_visual_delete_then_escape(void) { + printf("Test 49: Visual mode delete then escape... "); + AppState *state = create_test_state(); + + // Set up clips in visual selection + int clips[] = {18, 19, 26, 27}; + for (int i = 0; i < 4; i++) { + state->clips[clips[i]].state = CLIP_LOOPING; + state->clips[clips[i]].buffer_size = 100; + } + + // Simulate visual mode delete + for (int i = 0; i < 4; i++) { + Action action = { .type = ACTION_RESET_CLIP, .data.reset_clip.clip_index = clips[i] }; + reducer(state, action); + } + + // Verify clips are reset + for (int i = 0; i < 4; i++) { + assert(state->clips[clips[i]].state == CLIP_EMPTY); + } + + // Simulate returning to normal mode + int current_mode = 0; // MODE_NORMAL + assert(current_mode == 0); + + destroy_test_state(state); + printf("PASSED\n"); +} + +// Test 50: Visual mode yank then escape +void test_visual_yank_then_escape(void) { + printf("Test 50: Visual mode yank then escape... "); + AppState *state = create_test_state(); + + // Set up clips in visual selection + int clips[] = {18, 19, 26, 27}; + for (int i = 0; i < 4; i++) { + state->clips[clips[i]].state = CLIP_LOOPING; + state->clips[clips[i]].buffer_size = 100; + } + + // Simulate yanking the selection - should stop all clips + for (int i = 0; i < 4; i++) { + Action action = { .type = ACTION_TRIGGER_CLIP, .data.trigger_clip.clip_index = clips[i] }; + reducer(state, action); + } + + // Verify clips are stopped + for (int i = 0; i < 4; i++) { + assert(state->clips[clips[i]].state == CLIP_STOPPED); + } + + // Simulate returning to normal mode + int current_mode = 0; // MODE_NORMAL + assert(current_mode == 0); + + destroy_test_state(state); + printf("PASSED\n"); +} + +// Test 51: Multiple marks +void test_multiple_marks(void) { + printf("Test 51: Multiple marks... "); + + int marks[26]; + for (int i = 0; i < 26; i++) { + marks[i] = -1; + } + + // Set multiple marks + marks[0] = 0; // 'a' = clip 0 + marks[1] = 63; // 'b' = clip 63 + marks[2] = 28; // 'c' = clip 28 + + assert(marks[0] == 0); + assert(marks[1] == 63); + assert(marks[2] == 28); + + // Go to mark 'b' + int clip_idx = marks[1]; + int row = clip_idx / 8; + int col = clip_idx % 8; + assert(row == 7); + assert(col == 7); + + printf("PASSED\n"); +} + +// Test 52: Re-mark existing mark +void test_remark_existing_mark(void) { + printf("Test 52: Re-mark existing mark... "); + + int marks[26]; + for (int i = 0; i < 26; i++) { + marks[i] = -1; + } + + // Set mark 'a' to clip 10 + marks[0] = 10; + assert(marks[0] == 10); + + // Re-mark 'a' to clip 42 + marks[0] = 42; + assert(marks[0] == 42); + + printf("PASSED\n"); +} + +// Test 53: Undo single clip trigger +void test_undo_single_trigger(void) { + printf("Test 53: Undo single clip trigger... "); + AppState *state = create_test_state(); + + // Start with empty clip + int clip_idx = 10; + assert(state->clips[clip_idx].state == CLIP_EMPTY); + + // Trigger clip (empty -> recording) + Action action = { .type = ACTION_TRIGGER_CLIP, .data.trigger_clip.clip_index = clip_idx }; + reducer(state, action); + assert(state->clips[clip_idx].state == CLIP_RECORDING); + + // Undo: should go back to empty + action.type = ACTION_UNDO; + reducer(state, action); + assert(state->clips[clip_idx].state == CLIP_EMPTY); + + printf("PASSED\n"); + destroy_test_state(state); +} + +// Test 54: Undo multiple clip triggers +void test_undo_multiple_triggers(void) { + printf("Test 54: Undo multiple clip triggers... "); + AppState *state = create_test_state(); + + int clip1 = 10, clip2 = 11, clip3 = 12; + + // Trigger three clips + Action action = { .type = ACTION_TRIGGER_CLIP, .data.trigger_clip.clip_index = clip1 }; + reducer(state, action); + assert(state->clips[clip1].state == CLIP_RECORDING); + + action.data.trigger_clip.clip_index = clip2; + reducer(state, action); + assert(state->clips[clip2].state == CLIP_RECORDING); + + action.data.trigger_clip.clip_index = clip3; + reducer(state, action); + assert(state->clips[clip3].state == CLIP_RECORDING); + + // Undo last action: clip3 should go back to empty + action.type = ACTION_UNDO; + reducer(state, action); + assert(state->clips[clip3].state == CLIP_EMPTY); + assert(state->clips[clip2].state == CLIP_RECORDING); + assert(state->clips[clip1].state == CLIP_RECORDING); + + // Undo again: clip2 should go back to empty + reducer(state, action); + assert(state->clips[clip2].state == CLIP_EMPTY); + assert(state->clips[clip1].state == CLIP_RECORDING); + + printf("PASSED\n"); + destroy_test_state(state); +} + +// Test 55: Redo single undo +void test_redo_single_undo(void) { + printf("Test 55: Redo single undo... "); + AppState *state = create_test_state(); + + int clip_idx = 10; + + // Trigger clip + Action action = { .type = ACTION_TRIGGER_CLIP, .data.trigger_clip.clip_index = clip_idx }; + reducer(state, action); + assert(state->clips[clip_idx].state == CLIP_RECORDING); + + // Undo + action.type = ACTION_UNDO; + reducer(state, action); + assert(state->clips[clip_idx].state == CLIP_EMPTY); + + // Redo: should go back to recording + action.type = ACTION_REDO; + reducer(state, action); + assert(state->clips[clip_idx].state == CLIP_RECORDING); + + printf("PASSED\n"); + destroy_test_state(state); +} + +// Test 56: Redo after multiple undos +void test_redo_multiple_undos(void) { + printf("Test 56: Redo after multiple undos... "); + AppState *state = create_test_state(); + + int clip1 = 10, clip2 = 11; + + // Trigger two clips + Action action = { .type = ACTION_TRIGGER_CLIP, .data.trigger_clip.clip_index = clip1 }; + reducer(state, action); + action.data.trigger_clip.clip_index = clip2; + reducer(state, action); + assert(state->clips[clip1].state == CLIP_RECORDING); + assert(state->clips[clip2].state == CLIP_RECORDING); + + // Undo twice + action.type = ACTION_UNDO; + reducer(state, action); + reducer(state, action); + assert(state->clips[clip1].state == CLIP_EMPTY); + assert(state->clips[clip2].state == CLIP_EMPTY); + + // Redo twice + action.type = ACTION_REDO; + reducer(state, action); + assert(state->clips[clip1].state == CLIP_RECORDING); + assert(state->clips[clip2].state == CLIP_EMPTY); + + reducer(state, action); + assert(state->clips[clip1].state == CLIP_RECORDING); + assert(state->clips[clip2].state == CLIP_RECORDING); + + printf("PASSED\n"); + destroy_test_state(state); +} + +// Test 57: Undo scene trigger +void test_undo_scene_trigger(void) { + printf("Test 57: Undo scene trigger... "); + AppState *state = create_test_state(); + + int scene_idx = 3; + + // Trigger scene + Action action = { .type = ACTION_TRIGGER_SCENE, .data.trigger_scene.scene_index = scene_idx }; + reducer(state, action); + + // All clips in scene should be recording + for (int ch = 0; ch < MAX_CHANNELS; ch++) { + int clip_idx = CLIP_INDEX(scene_idx, ch); + assert(state->clips[clip_idx].state == CLIP_RECORDING); + } + + // Undo: all clips should go back to empty + action.type = ACTION_UNDO; + reducer(state, action); + for (int ch = 0; ch < MAX_CHANNELS; ch++) { + int clip_idx = CLIP_INDEX(scene_idx, ch); + assert(state->clips[clip_idx].state == CLIP_EMPTY); + } + + printf("PASSED\n"); + destroy_test_state(state); +} + +// Test 58: Undo clip state cycle (empty -> recording -> looping -> stopped) +void test_undo_clip_state_cycle(void) { + printf("Test 58: Undo clip state cycle... "); + AppState *state = create_test_state(); + + int clip_idx = 10; + + // Cycle through all states + Action action = { .type = ACTION_TRIGGER_CLIP, .data.trigger_clip.clip_index = clip_idx }; + reducer(state, action); // empty -> recording + assert(state->clips[clip_idx].state == CLIP_RECORDING); + + reducer(state, action); // recording -> looping + assert(state->clips[clip_idx].state == CLIP_LOOPING); + + reducer(state, action); // looping -> stopped + assert(state->clips[clip_idx].state == CLIP_STOPPED); + + // Undo three times to go back to empty + action.type = ACTION_UNDO; + reducer(state, action); // stopped -> looping + assert(state->clips[clip_idx].state == CLIP_LOOPING); + + reducer(state, action); // looping -> recording + assert(state->clips[clip_idx].state == CLIP_RECORDING); + + reducer(state, action); // recording -> empty + assert(state->clips[clip_idx].state == CLIP_EMPTY); + + printf("PASSED\n"); + destroy_test_state(state); +} + +// Test 59: Undo after new action clears redo history +void test_undo_clears_redo_on_new_action(void) { + printf("Test 59: Undo after new action clears redo history... "); + AppState *state = create_test_state(); + + int clip1 = 10, clip2 = 11; + + // Trigger clip1 + Action action = { .type = ACTION_TRIGGER_CLIP, .data.trigger_clip.clip_index = clip1 }; + reducer(state, action); + assert(state->clips[clip1].state == CLIP_RECORDING); + + // Undo + action.type = ACTION_UNDO; + reducer(state, action); + assert(state->clips[clip1].state == CLIP_EMPTY); + + // Redo should work + action.type = ACTION_REDO; + reducer(state, action); + assert(state->clips[clip1].state == CLIP_RECORDING); + + // Undo again + action.type = ACTION_UNDO; + reducer(state, action); + assert(state->clips[clip1].state == CLIP_EMPTY); + + // Now do a new action (trigger clip2) + action.type = ACTION_TRIGGER_CLIP; + action.data.trigger_clip.clip_index = clip2; + reducer(state, action); + assert(state->clips[clip2].state == CLIP_RECORDING); + + // Redo should NOT work now (redo history cleared) + action.type = ACTION_REDO; + reducer(state, action); + assert(state->clips[clip1].state == CLIP_EMPTY); // Should still be empty + + printf("PASSED\n"); + destroy_test_state(state); +} + +// Test 60: Undo reset clip +void test_undo_reset_clip(void) { + printf("Test 60: Undo reset clip... "); + AppState *state = create_test_state(); + + int clip_idx = 10; + + // Set up a looping clip with data + state->clips[clip_idx].state = CLIP_LOOPING; + state->clips[clip_idx].buffer_size = 100; + state->clips[clip_idx].write_position = 100; + state->clips[clip_idx].read_position = 50; + + // Reset the clip + Action action = { .type = ACTION_RESET_CLIP, .data.reset_clip.clip_index = clip_idx }; + reducer(state, action); + assert(state->clips[clip_idx].state == CLIP_EMPTY); + assert(state->clips[clip_idx].buffer_size == 0); + + // Undo: should restore clip to previous state + action.type = ACTION_UNDO; + reducer(state, action); + assert(state->clips[clip_idx].state == CLIP_LOOPING); + assert(state->clips[clip_idx].buffer_size == 100); + assert(state->clips[clip_idx].write_position == 100); + assert(state->clips[clip_idx].read_position == 50); + + printf("PASSED\n"); + destroy_test_state(state); +} + + + + +// Test 64: Undo/redo with paste operation +void test_undo_paste(void) { + printf("Test 64: Undo paste operation... "); + AppState *state = create_test_state(); + + int clip_idx = 27; + + // Simulate paste: trigger clip three times to go empty -> recording -> looping -> stopped + Action action = { .type = ACTION_TRIGGER_CLIP, .data.trigger_clip.clip_index = clip_idx }; + reducer(state, action); + reducer(state, action); + reducer(state, action); + assert(state->clips[clip_idx].state == CLIP_STOPPED); + + // Undo: should go back to looping (undo last trigger: stopped -> looping) + action.type = ACTION_UNDO; + reducer(state, action); + assert(state->clips[clip_idx].state == CLIP_LOOPING); + + // Undo again: should go back to recording + reducer(state, action); + assert(state->clips[clip_idx].state == CLIP_RECORDING); + + // Undo again: should go back to empty + reducer(state, action); + assert(state->clips[clip_idx].state == CLIP_EMPTY); + + // Redo three times: should go back to stopped + action.type = ACTION_REDO; + reducer(state, action); + assert(state->clips[clip_idx].state == CLIP_RECORDING); + + reducer(state, action); + assert(state->clips[clip_idx].state == CLIP_LOOPING); + + reducer(state, action); + assert(state->clips[clip_idx].state == CLIP_STOPPED); + + printf("PASSED\n"); + destroy_test_state(state); +} + +int main(void) { + printf("Running TUI tests...\n\n"); + + test_grid_to_clip_index(); + test_trigger_via_grid(); + test_reset_via_grid(); + test_scene_via_grid(); + test_quantize_cycling(); + test_threshold_toggle(); + test_transport_reset_via_tui(); + test_navigation_wrapping(); + test_multiple_clip_states(); + test_buffer_size_display(); + test_help_toggle(); + test_escape_handling(); + test_tui_init_cleanup(); + test_state_to_color_mapping(); + test_full_grid_coverage(); + test_scene_from_each_row(); + test_quantize_full_cycle(); + test_multiple_threshold_toggles(); + test_multiple_transport_resets(); + test_arrow_key_navigation(); + test_command_mode_quit(); + test_command_mode_empty(); + test_command_mode_unknown(); + test_command_mode_buffer_overflow(); + test_command_mode_backspace(); + test_command_mode_escape(); + test_command_mode_enter(); + test_command_mode_colon(); + test_visual_mode_entry(); + test_visual_mode_selection(); + test_visual_mode_escape(); + test_visual_line_selection(); + test_move_mode_navigation(); + test_move_mode_enter(); + test_move_mode_escape(); + test_delete_single_clip(); + test_delete_visual_selection(); + test_yank_single_clip(); + test_yank_visual_selection(); + test_paste_clips(); + test_paste_bounds_checking(); + test_mark_setting(); + test_go_to_mark(); + test_go_to_unset_mark(); + test_play_next_scene(); + test_play_prev_scene(); + test_play_next_scene_wrap(); + test_play_prev_scene_wrap(); + test_visual_delete_then_escape(); + test_visual_yank_then_escape(); + test_multiple_marks(); + test_remark_existing_mark(); + test_undo_single_trigger(); + test_undo_multiple_triggers(); + test_redo_single_undo(); + test_redo_multiple_undos(); + test_undo_scene_trigger(); + test_undo_clip_state_cycle(); + test_undo_clears_redo_on_new_action(); + test_undo_reset_clip(); + test_undo_paste(); + + printf("\nAll TUI tests passed!\n"); + return 0; +} diff --git a/src/channel.o b/src/channel.o deleted file mode 100644 index 72477e157ecd441c4010799e2e0f8dd8e3ac1a48..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9640 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 diff --git a/src/main.o b/src/main.o deleted file mode 100644 index fd3175c3b29da921af0b722dc9e5c3f8d5431b84..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9984 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 UhufWfFiWmZ;e8v0^sW7W07Gp-0{{R3 diff --git a/src/midi.o b/src/midi.o deleted file mode 100644 index c89e0e577a535ed7cb554f0b0ddfd4aaa1c0c043..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8552 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 F{{Ru}yR!fQ diff --git a/src/pipe.o b/src/pipe.o deleted file mode 100644 index 7c189fbb2386cb354a72996558f0e8622aee2edf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10560 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�?-%!u*&(F2Oi@ zo=eBLBh+t)9GGxPQN$E-!04QTakL)^yZ9A+5j%v8MM7B|fl@+lG}}(ZO%M@wp==SG zunUQgC+vhVGK0zu<%WpESv&#xJF}fE%?=3>a+i@ZP#7X1TM*6#MSEE^PryY*$ofzc zVDeQCX>(%ue6E13S`Z`R+L5nT$53y0pdBs{+rulggP5S{a+j0&hcvcY+R_X*@>fkTQ64zj4{ z6W48t&wrm`ZaJZL{~iRwq~2j@*+?XQCMcgx)Le4_FQfOD*``u$903M)9VpnmX9 z0hLGfuO=C&-vMQiX%MR5xVvEs1If@hq>|dc%B#W`AeSj`y&rh_EL6T%LZzx9}{DJp)s63j#zUl@gV`2U1UI*S;fhonygJp(oQIZfuhZYqPwX&$^ W6CXcqGN-Tdhhh8CeFZtd%l|KeOvp|E diff --git a/src/queue.o b/src/queue.o deleted file mode 100644 index 216d259aba9b8a8812193a4444992b5ff00fac4d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5952 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