From 4573eb0201f0aff33a7f98afe8c9809daafc60ae Mon Sep 17 00:00:00 2001 From: Loic Coenen Date: Fri, 1 May 2026 09:48:59 +0000 Subject: [PATCH] feat: add ncurses-based TUI frontend with 8x8 clip grid and keyboard controls Co-authored-by: aider (deepseek/deepseek-coder) --- Let's craft the blocks.tui.h | 15 ++ main.c | 11 +- makefile | 22 +- test_tui.c | 474 +++++++++++++++++++++++++++++++++++ tui.c | 264 +++++++++++++++++++ 5 files changed, 775 insertions(+), 11 deletions(-) create mode 100644 Let's craft the blocks.tui.h diff --git a/Let's craft the blocks.tui.h b/Let's craft the blocks.tui.h new file mode 100644 index 0000000..9e92cc9 --- /dev/null +++ b/Let's craft the blocks.tui.h @@ -0,0 +1,15 @@ +#ifndef TUI_H +#define TUI_H + +#include "engine.h" + +// 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/main.c b/main.c index 606390e..76f8838 100644 --- a/main.c +++ b/main.c @@ -3,6 +3,7 @@ #include #include #include "engine.h" +#include "tui.h" static Engine engine; static volatile int keep_running = 1; @@ -71,13 +72,13 @@ int main(int argc, char *argv[]) { printf("Sample rate: %u Hz\n", engine.sample_rate); printf("Press Ctrl+C to stop\n\n"); - // Main loop - while (keep_running) { - sleep(1); - } + // Initialize and run TUI + tui_init(&engine); + tui_run(&engine); + tui_cleanup(); // Cleanup - printf("\nShutting down...\n"); + printf("\nShutting down...\n"); // This line may not be reached if tui_run returns engine_stop(&engine); engine_cleanup(&engine); diff --git a/makefile b/makefile index 47aeec9..73af288 100644 --- a/makefile +++ b/makefile @@ -1,28 +1,38 @@ CC = gcc CFLAGS = -Wall -Wextra -std=c99 -D_POSIX_C_SOURCE=200809L -LDFLAGS = -ljack -lm +LDFLAGS = -ljack -lm -lncurses -all: jack-looper test_engine +all: jack-looper test_engine test_tui -jack-looper: main.o engine.o +jack-looper: main.o engine.o tui.o $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) test_engine: test_engine.o engine.o $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) -main.o: main.c engine.h +test_tui: test_tui.o engine.o + $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) + +main.o: main.c engine.h tui.h $(CC) $(CFLAGS) -c -o $@ $< engine.o: engine.c engine.h $(CC) $(CFLAGS) -c -o $@ $< +tui.o: tui.c tui.h engine.h + $(CC) $(CFLAGS) -c -o $@ $< + test_engine.o: test_engine.c engine.h $(CC) $(CFLAGS) -c -o $@ $< +test_tui.o: test_tui.c engine.h tui.h + $(CC) $(CFLAGS) -c -o $@ $< + .PHONY: all clean test clean: - rm -f *.o jack-looper test_engine + rm -f *.o jack-looper test_engine test_tui -test: test_engine +test: test_engine test_tui ./test_engine + ./test_tui diff --git a/test_tui.c b/test_tui.c index e69de29..91a8688 100644 --- a/test_tui.c +++ b/test_tui.c @@ -0,0 +1,474 @@ +#include +#include +#include +#include +#include "engine.h" +#include "tui.h" + +// Test helper +static Engine *create_test_engine(void) { + Engine *engine = (Engine *)calloc(1, sizeof(Engine)); + assert(engine != NULL); + + engine->control_channel = 0; + engine->sample_rate = 48000; + engine->quantize_mode = QUANTIZE_OFF; + engine->quantize_threshold = 0; + engine->queued_triggers = NULL; + + // Initialize transport + engine->transport.rolling = false; + engine->transport.clock_count = 0; + engine->transport.beat_position = 0; + engine->transport.bar_position = 0; + engine->transport.sample_position = 0; + + for (int i = 0; i < MAX_CLIPS; i++) { + engine->clips[i].state = CLIP_EMPTY; + engine->clips[i].buffer = (float *)calloc(MAX_BUFFER_SIZE, sizeof(float)); + assert(engine->clips[i].buffer != NULL); + engine->clips[i].buffer_size = 0; + engine->clips[i].write_position = 0; + engine->clips[i].read_position = 0; + } + + return engine; +} + +static void destroy_test_engine(Engine *engine) { + if (engine) { + QueuedTrigger *qt = engine->queued_triggers; + while (qt) { + QueuedTrigger *next = qt->next; + free(qt); + qt = next; + } + + for (int i = 0; i < MAX_CLIPS; i++) { + free(engine->clips[i].buffer); + } + free(engine); + } +} + +// 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... "); + Engine *engine = create_test_engine(); + + // Simulate pressing 't' on grid position (3, 4) = clip 28 + int clip_idx = 3 * 8 + 4; + engine_trigger_clip(engine, clip_idx); + assert(engine->clips[clip_idx].state == CLIP_RECORDING); + + destroy_test_engine(engine); + printf("PASSED\n"); +} + +// Test 3: Reset clip via grid position +void test_reset_via_grid(void) { + printf("Test 3: Reset clip via grid position... "); + Engine *engine = create_test_engine(); + + // Set up a clip at grid position (1, 2) = clip 10 + int clip_idx = 1 * 8 + 2; + engine->clips[clip_idx].state = CLIP_LOOPING; + engine->clips[clip_idx].buffer_size = 100; + + // Simulate pressing 'r' + engine_reset_clip(engine, clip_idx); + assert(engine->clips[clip_idx].state == CLIP_EMPTY); + assert(engine->clips[clip_idx].buffer_size == 0); + + destroy_test_engine(engine); + printf("PASSED\n"); +} + +// Test 4: Scene trigger via grid row +void test_scene_via_grid(void) { + printf("Test 4: Scene trigger via grid row... "); + Engine *engine = create_test_engine(); + + // Simulate pressing 's' on row 3 + int scene_index = 3; + engine_trigger_scene(engine, scene_index); + + // 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(engine->clips[clip_idx].state == CLIP_RECORDING); + } + + destroy_test_engine(engine); + printf("PASSED\n"); +} + +// Test 5: Quantize mode cycling +void test_quantize_cycling(void) { + printf("Test 5: Quantize mode cycling... "); + Engine *engine = create_test_engine(); + + // Simulate pressing 'q' to cycle through modes + assert(engine->quantize_mode == QUANTIZE_OFF); + + // Cycle: OFF -> BEAT + engine_set_quantize_mode(engine, QUANTIZE_BEAT); + assert(engine->quantize_mode == QUANTIZE_BEAT); + + // Cycle: BEAT -> BAR + engine_set_quantize_mode(engine, QUANTIZE_BAR); + assert(engine->quantize_mode == QUANTIZE_BAR); + + // Cycle: BAR -> OFF + engine_set_quantize_mode(engine, QUANTIZE_OFF); + assert(engine->quantize_mode == QUANTIZE_OFF); + + destroy_test_engine(engine); + printf("PASSED\n"); +} + +// Test 6: Threshold toggling +void test_threshold_toggle(void) { + printf("Test 6: Threshold toggling... "); + Engine *engine = create_test_engine(); + + // Simulate pressing 'T' to toggle threshold + assert(engine->quantize_threshold == 0); + + // Toggle to 1000 + engine_set_quantize_threshold(engine, 1000); + assert(engine->quantize_threshold == 1000); + + // Toggle back to 0 + engine_set_quantize_threshold(engine, 0); + assert(engine->quantize_threshold == 0); + + destroy_test_engine(engine); + printf("PASSED\n"); +} + +// Test 7: Transport reset +void test_transport_reset_via_tui(void) { + printf("Test 7: Transport reset via TUI... "); + Engine *engine = create_test_engine(); + + // Set up transport state + engine->transport.rolling = true; + engine->transport.clock_count = 100; + engine->transport.beat_position = 2; + engine->transport.bar_position = 5; + engine->transport.sample_position = 10000; + + // Simulate pressing 'x' + engine_reset_transport(engine); + + assert(engine->transport.rolling == false); + assert(engine->transport.clock_count == 0); + assert(engine->transport.beat_position == 0); + assert(engine->transport.bar_position == 0); + assert(engine->transport.sample_position == 0); + + destroy_test_engine(engine); + 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... "); + Engine *engine = create_test_engine(); + + // Set up clips in various states + engine->clips[0].state = CLIP_EMPTY; + engine->clips[1].state = CLIP_RECORDING; + engine->clips[2].state = CLIP_LOOPING; + engine->clips[3].state = CLIP_STOPPED; + + // Verify states + assert(engine->clips[0].state == CLIP_EMPTY); + assert(engine->clips[1].state == CLIP_RECORDING); + assert(engine->clips[2].state == CLIP_LOOPING); + assert(engine->clips[3].state == CLIP_STOPPED); + + destroy_test_engine(engine); + printf("PASSED\n"); +} + +// Test 10: Buffer size display +void test_buffer_size_display(void) { + printf("Test 10: Buffer size display... "); + Engine *engine = create_test_engine(); + + // Set up a clip with known buffer size + engine->clips[5].state = CLIP_LOOPING; + engine->clips[5].buffer_size = 48000; // 1 second at 48kHz + + // Verify buffer size + assert(engine->clips[5].buffer_size == 48000); + + destroy_test_engine(engine); + 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... "); + Engine *engine = create_test_engine(); + + // We can't actually call tui_init/tui_cleanup in test environment + // but we can verify the engine is valid + assert(engine != NULL); + assert(engine->sample_rate == 48000); + + destroy_test_engine(engine); + 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... "); + Engine *engine = create_test_engine(); + + // 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; + engine_trigger_clip(engine, clip_idx); + assert(engine->clips[clip_idx].state == CLIP_RECORDING); + } + } + + // Verify all clips are recording + for (int i = 0; i < MAX_CLIPS; i++) { + assert(engine->clips[i].state == CLIP_RECORDING); + } + + destroy_test_engine(engine); + 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... "); + Engine *engine = create_test_engine(); + + // Trigger scene from each row + for (int row = 0; row < 8; row++) { + engine_trigger_scene(engine, row); + + // Verify all clips in this scene are recording + for (int ch = 0; ch < MAX_CHANNELS; ch++) { + int clip_idx = CLIP_INDEX(row, ch); + assert(engine->clips[clip_idx].state == CLIP_RECORDING); + } + } + + destroy_test_engine(engine); + printf("PASSED\n"); +} + +// Test 17: Quantize mode cycle through all modes +void test_quantize_full_cycle(void) { + printf("Test 17: Quantize mode full cycle... "); + Engine *engine = create_test_engine(); + + // Cycle through all modes twice + for (int cycle = 0; cycle < 2; cycle++) { + engine_set_quantize_mode(engine, QUANTIZE_OFF); + assert(engine->quantize_mode == QUANTIZE_OFF); + + engine_set_quantize_mode(engine, QUANTIZE_BEAT); + assert(engine->quantize_mode == QUANTIZE_BEAT); + + engine_set_quantize_mode(engine, QUANTIZE_BAR); + assert(engine->quantize_mode == QUANTIZE_BAR); + } + + destroy_test_engine(engine); + printf("PASSED\n"); +} + +// Test 18: Multiple threshold toggles +void test_multiple_threshold_toggles(void) { + printf("Test 18: Multiple threshold toggles... "); + Engine *engine = create_test_engine(); + + // Toggle threshold multiple times + for (int i = 0; i < 5; i++) { + if (engine->quantize_threshold == 0) { + engine_set_quantize_threshold(engine, 1000); + assert(engine->quantize_threshold == 1000); + } else { + engine_set_quantize_threshold(engine, 0); + assert(engine->quantize_threshold == 0); + } + } + + destroy_test_engine(engine); + printf("PASSED\n"); +} + +// Test 19: Transport reset multiple times +void test_multiple_transport_resets(void) { + printf("Test 19: Multiple transport resets... "); + Engine *engine = create_test_engine(); + + // Reset transport multiple times + for (int i = 0; i < 5; i++) { + engine->transport.rolling = true; + engine->transport.clock_count = 100 + i; + engine->transport.beat_position = i % 4; + engine->transport.bar_position = i; + engine->transport.sample_position = 10000 * i; + + engine_reset_transport(engine); + + assert(engine->transport.rolling == false); + assert(engine->transport.clock_count == 0); + assert(engine->transport.beat_position == 0); + assert(engine->transport.bar_position == 0); + assert(engine->transport.sample_position == 0); + } + + destroy_test_engine(engine); + 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"); +} + +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(); + + printf("\nAll TUI tests passed!\n"); + return 0; +} diff --git a/tui.c b/tui.c index e69de29..f0868b5 100644 --- a/tui.c +++ b/tui.c @@ -0,0 +1,264 @@ +#include "tui.h" +#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; + } +} + +// 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(); +} + +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(); + + 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(); +}