Files
jack-looper/tui.c
Loic Coenen 8c816a0b46 fix: handle SIGINT to restore terminal raw mode on Ctrl+C
Co-authored-by: aider (deepseek/deepseek-coder) <aider@aider.chat>
2026-05-01 14:41:11 +00:00

282 lines
8.0 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;
}
// 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);
}