#include "tui.h" #include #include #include #include #define GRID_ROWS 8 #define GRID_COLS 8 #define CELL_WIDTH 6 #define CELL_HEIGHT 3 // Color pairs enum { COLOR_EMPTY = 1, COLOR_RECORDING, COLOR_LOOPING, COLOR_STOPPED, COLOR_SELECTED, COLOR_HELP }; static Engine *g_engine = NULL; static int selected_row = 0; static int selected_col = 0; static bool show_help = false; // 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; } // Install signal handler for Ctrl+C signal(SIGINT, handle_sigint); } // Get clip index from grid position static int grid_to_clip_index(int row, int col) { return row * GRID_COLS + col; } // Draw a single cell static void draw_cell(int row, int col, bool selected) { int clip_idx = grid_to_clip_index(row, col); Clip *clip = &g_engine->clips[clip_idx]; int y = row * CELL_HEIGHT + 1; int x = col * CELL_WIDTH + 1; int color = state_to_color(clip->state); if (selected) { color = COLOR_SELECTED; } attron(COLOR_PAIR(color)); // Draw cell border for (int dy = 0; dy < CELL_HEIGHT; dy++) { for (int dx = 0; dx < CELL_WIDTH; dx++) { mvaddch(y + dy, x + dx, ' '); } } // Draw clip number mvprintw(y + 1, x + 1, "%2d", clip_idx); // Draw state indicator 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(); // Draw title attron(A_BOLD); mvprintw(0, 0, "JACK Looper - 8x8 Clip Grid"); attroff(A_BOLD); // Draw cells for (int row = 0; row < GRID_ROWS; row++) { for (int col = 0; col < GRID_COLS; col++) { bool selected = (row == selected_row && col == selected_col); draw_cell(row, col, selected); } } // Draw status bar int clip_idx = grid_to_clip_index(selected_row, selected_col); Clip *clip = &g_engine->clips[clip_idx]; mvprintw(GRID_ROWS * CELL_HEIGHT + 1, 0, "Selected: Clip %d | State: %s | Buffer: %zu samples", clip_idx, clip_state_to_string(clip->state), clip->buffer_size); mvprintw(GRID_ROWS * CELL_HEIGHT + 2, 0, "Quantize: %s | Threshold: %u | Transport: %s", quantize_mode_to_string(g_engine->quantize_mode), g_engine->quantize_threshold, g_engine->transport.rolling ? "Rolling" : "Stopped"); // Draw help if active if (show_help) { attron(COLOR_PAIR(COLOR_HELP)); mvprintw(GRID_ROWS * CELL_HEIGHT + 4, 0, "=== Help ==="); mvprintw(GRID_ROWS * CELL_HEIGHT + 5, 0, "h/j/k/l - Navigate grid (left/down/up/right)"); mvprintw(GRID_ROWS * CELL_HEIGHT + 6, 0, "t - Trigger selected clip"); mvprintw(GRID_ROWS * CELL_HEIGHT + 7, 0, "r - Reset selected clip"); mvprintw(GRID_ROWS * CELL_HEIGHT + 8, 0, "s - Trigger scene (current row)"); mvprintw(GRID_ROWS * CELL_HEIGHT + 9, 0, "q - Toggle quantize mode (off/beat/bar)"); mvprintw(GRID_ROWS * CELL_HEIGHT + 10, 0, "T - Set quantize threshold"); mvprintw(GRID_ROWS * CELL_HEIGHT + 11, 0, "x - Reset transport"); mvprintw(GRID_ROWS * CELL_HEIGHT + 12, 0, "? - Toggle help"); mvprintw(GRID_ROWS * CELL_HEIGHT + 13, 0, "Esc/q - Quit"); attroff(COLOR_PAIR(COLOR_HELP)); } refresh(); } static void handle_sigint(int sig) { (void)sig; tui_cleanup(); _exit(1); } void tui_init(Engine *engine) { g_engine = engine; // Initialize ncurses initscr(); cbreak(); noecho(); keypad(stdscr, TRUE); curs_set(0); // Hide cursor // Initialize colors if (has_colors()) { start_color(); // Define color pairs 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_YELLOW, 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; draw_grid(); while (1) { int ch = getch(); if (ch == ERR) { // getch returned error (e.g., signal interrupted) break; } switch (ch) { case 'h': case KEY_LEFT: selected_col = (selected_col - 1 + GRID_COLS) % GRID_COLS; break; case 'j': case KEY_DOWN: selected_row = (selected_row + 1) % GRID_ROWS; break; case 'k': case KEY_UP: selected_row = (selected_row - 1 + GRID_ROWS) % GRID_ROWS; break; case 'l': case KEY_RIGHT: selected_col = (selected_col + 1) % GRID_COLS; break; case 't': { int clip_idx = grid_to_clip_index(selected_row, selected_col); engine_trigger_clip(engine, clip_idx); break; } case 'r': { int clip_idx = grid_to_clip_index(selected_row, selected_col); engine_reset_clip(engine, clip_idx); break; } case 's': { // Trigger scene for current row engine_trigger_scene(engine, selected_row); break; } case 'q': { // Cycle quantize mode 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 (engine->quantize_mode == modes[i]) { current = i; break; } } QuantizeMode next = modes[(current + 1) % num_modes]; engine_set_quantize_mode(engine, next); break; } case 'T': { // Toggle threshold between 0 and 1000 if (engine->quantize_threshold == 0) { engine_set_quantize_threshold(engine, 1000); } else { engine_set_quantize_threshold(engine, 0); } break; } case 'x': engine_reset_transport(engine); break; case '?': show_help = !show_help; break; case 27: // Escape key case 'Q': return; default: break; } draw_grid(); } } void tui_cleanup(void) { // Restore terminal settings curs_set(1); endwin(); // Reset signal handler to default signal(SIGINT, SIG_DFL); }