350 lines
10 KiB
C
350 lines
10 KiB
C
#include "tui.h"
|
|
#include <ncurses.h>
|
|
#include <string.h>
|
|
#include <stdlib.h>
|
|
#include <signal.h>
|
|
|
|
#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;
|
|
}
|
|
}
|
|
|
|
// 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((QuantizeMode)atomic_load(&g_engine->quantize_mode_atomic)),
|
|
(unsigned int)atomic_load(&g_engine->quantize_threshold_atomic),
|
|
atomic_load(&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();
|
|
}
|
|
|
|
// Handle command mode input (after pressing ':')
|
|
// Returns true if the application should quit
|
|
static bool handle_command_mode(void) {
|
|
char cmd_buffer[256];
|
|
int cmd_pos = 0;
|
|
memset(cmd_buffer, 0, sizeof(cmd_buffer));
|
|
|
|
// Save current nodelay state and force blocking input
|
|
int prev_nodelay = nodelay(stdscr, FALSE);
|
|
|
|
// Show command prompt
|
|
mvprintw(LINES - 1, 0, ":");
|
|
clrtoeol();
|
|
refresh();
|
|
|
|
while (1) {
|
|
int ch = getch();
|
|
// Do NOT break on ERR; getch() will block now
|
|
|
|
if (ch == '\n' || ch == '\r') {
|
|
// Execute command
|
|
cmd_buffer[cmd_pos] = '\0';
|
|
|
|
// Clear command line
|
|
mvprintw(LINES - 1, 0, " ");
|
|
refresh();
|
|
|
|
// Parse and execute command
|
|
if (strcmp(cmd_buffer, "q") == 0) {
|
|
// Restore previous nodelay state before returning
|
|
nodelay(stdscr, prev_nodelay);
|
|
return true; // Quit
|
|
}
|
|
// Add more commands here as needed
|
|
|
|
// Restore previous nodelay state before returning
|
|
nodelay(stdscr, prev_nodelay);
|
|
return false; // Don't quit
|
|
} else if (ch == 27) { // Escape - cancel command mode
|
|
mvprintw(LINES - 1, 0, " ");
|
|
refresh();
|
|
nodelay(stdscr, prev_nodelay);
|
|
return false;
|
|
} else if (ch == KEY_BACKSPACE || ch == 127) { // Backspace
|
|
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();
|
|
}
|
|
}
|
|
|
|
// Should never reach here, but restore just in case
|
|
nodelay(stdscr, prev_nodelay);
|
|
return false;
|
|
}
|
|
|
|
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 ':': {
|
|
bool should_quit = handle_command_mode();
|
|
if (should_quit) {
|
|
return;
|
|
}
|
|
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);
|
|
}
|