fix: refactor TUI to use dispatcher pattern
Co-authored-by: aider (deepseek/deepseek-coder) <aider@aider.chat>
This commit is contained in:
390
tui.c
390
tui.c
@@ -22,6 +22,7 @@ enum {
|
||||
};
|
||||
|
||||
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;
|
||||
@@ -36,7 +37,6 @@ typedef enum {
|
||||
// Mark storage
|
||||
#define MAX_MARKS 26 // a-z
|
||||
static int marks[MAX_MARKS]; // stores clip_index for each mark
|
||||
static int mark_selected = -1; // -1 = no mark selected
|
||||
|
||||
// Visual mode state
|
||||
static int visual_start_row = 0;
|
||||
@@ -109,32 +109,16 @@ static int* get_selected_clips(int *count) {
|
||||
return clips;
|
||||
}
|
||||
|
||||
// Get all clip indices in a row
|
||||
static int* get_row_clips(int row, int *count) {
|
||||
*count = GRID_COLS;
|
||||
int *clips = (int *)malloc(*count * sizeof(int));
|
||||
if (!clips) {
|
||||
*count = 0;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
for (int c = 0; c < GRID_COLS; c++) {
|
||||
clips[c] = grid_to_clip_index(row, c);
|
||||
}
|
||||
|
||||
return clips;
|
||||
}
|
||||
|
||||
// Delete (reset) clips
|
||||
// Delete (reset) clips via dispatch
|
||||
static void delete_clips(int *clip_indices, int count) {
|
||||
for (int i = 0; i < count; i++) {
|
||||
engine_reset_clip(g_engine, clip_indices[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) {
|
||||
// Free previous yank buffer
|
||||
if (yank_buffer.clip_indices) {
|
||||
free(yank_buffer.clip_indices);
|
||||
}
|
||||
@@ -146,11 +130,10 @@ static void yank_clips(int *clip_indices, int count) {
|
||||
}
|
||||
}
|
||||
|
||||
// Paste clips (trigger them)
|
||||
// Paste clips (trigger them via dispatch)
|
||||
static void paste_clips(void) {
|
||||
if (!yank_buffer.clip_indices || yank_buffer.count == 0) return;
|
||||
|
||||
// Calculate offset from current position to first yanked clip
|
||||
int first_yanked_row = yank_buffer.clip_indices[0] / GRID_COLS;
|
||||
int first_yanked_col = yank_buffer.clip_indices[0] % GRID_COLS;
|
||||
int row_offset = selected_row - first_yanked_row;
|
||||
@@ -163,13 +146,13 @@ static void paste_clips(void) {
|
||||
int new_row = orig_row + row_offset;
|
||||
int new_col = orig_col + col_offset;
|
||||
|
||||
// Bounds check
|
||||
if (new_row >= 0 && new_row < GRID_ROWS && new_col >= 0 && new_col < GRID_COLS) {
|
||||
int new_clip_idx = grid_to_clip_index(new_row, new_col);
|
||||
// Trigger three times to go from empty -> recording -> looping -> stopped
|
||||
engine_trigger_clip(g_engine, new_clip_idx);
|
||||
engine_trigger_clip(g_engine, new_clip_idx);
|
||||
engine_trigger_clip(g_engine, new_clip_idx);
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -197,48 +180,47 @@ static void go_to_mark(char mark_char) {
|
||||
// Play next scene (next row)
|
||||
static void play_next_scene(void) {
|
||||
int next_row = (selected_row + 1) % GRID_ROWS;
|
||||
engine_trigger_scene(g_engine, next_row);
|
||||
Action action = { .type = ACTION_TRIGGER_SCENE, .data.trigger_scene = { .scene_index = next_row } };
|
||||
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;
|
||||
engine_trigger_scene(g_engine, prev_row);
|
||||
Action action = { .type = ACTION_TRIGGER_SCENE, .data.trigger_scene = { .scene_index = prev_row } };
|
||||
g_dispatch(action);
|
||||
selected_row = prev_row;
|
||||
}
|
||||
|
||||
// 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];
|
||||
AppState state = dispatcher_get_state();
|
||||
Clip *clip = &state.clips[clip_idx];
|
||||
|
||||
int y = row * CELL_HEIGHT + 1;
|
||||
int x = col * CELL_WIDTH + 1;
|
||||
|
||||
int color = state_to_color((ClipState)atomic_load(&clip->state));
|
||||
int color = state_to_color(clip->state);
|
||||
if (selected) {
|
||||
color = COLOR_SELECTED;
|
||||
} else if (current_mode == MODE_VISUAL && is_in_visual_selection(row, col)) {
|
||||
// Use a different highlight for visual selection (invert colors)
|
||||
color = COLOR_SELECTED; // Same as selected for now
|
||||
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 ((ClipState)atomic_load(&clip->state)) {
|
||||
switch (clip->state) {
|
||||
case CLIP_EMPTY: state_char = ' '; break;
|
||||
case CLIP_RECORDING: state_char = 'R'; break;
|
||||
case CLIP_LOOPING: state_char = 'L'; break;
|
||||
@@ -254,12 +236,12 @@ static void draw_cell(int row, int col, bool selected) {
|
||||
static void draw_grid(void) {
|
||||
clear();
|
||||
|
||||
// Draw title
|
||||
attron(A_BOLD);
|
||||
mvprintw(0, 0, "JACK Looper - 8x8 Clip Grid");
|
||||
attroff(A_BOLD);
|
||||
|
||||
// Draw cells
|
||||
AppState state = dispatcher_get_state();
|
||||
|
||||
for (int row = 0; row < GRID_ROWS; row++) {
|
||||
for (int col = 0; col < GRID_COLS; col++) {
|
||||
bool selected = (row == selected_row && col == selected_col);
|
||||
@@ -267,27 +249,21 @@ static void draw_grid(void) {
|
||||
}
|
||||
}
|
||||
|
||||
// Draw status bar
|
||||
int clip_idx = grid_to_clip_index(selected_row, selected_col);
|
||||
Clip *clip = &g_engine->clips[clip_idx];
|
||||
Clip *clip = &state.clips[clip_idx];
|
||||
|
||||
mvprintw(GRID_ROWS * CELL_HEIGHT + 1, 0,
|
||||
"Selected: Clip %d | State: %s | Buffer: %zu samples",
|
||||
clip_idx, clip_state_to_string((ClipState)atomic_load(&clip->state)),
|
||||
(size_t)atomic_load(&clip->buffer_size));
|
||||
|
||||
TransportState transport_state = (TransportState)atomic_load(&g_engine->transport->state_atomic);
|
||||
ClockSource clock_source = (ClockSource)atomic_load(&g_engine->transport->clock_source_atomic);
|
||||
clip_idx, clip_state_to_string(clip->state), clip->buffer_size);
|
||||
|
||||
mvprintw(GRID_ROWS * CELL_HEIGHT + 2, 0,
|
||||
"Transport: %s | Clock: %s | BPM: %.1f | Quantize: %s | Threshold: %u",
|
||||
transport_state_to_string(transport_state),
|
||||
clock_source_to_string(clock_source),
|
||||
(double)atomic_load(&g_engine->transport->bpm_atomic_raw) / 100.0,
|
||||
quantize_mode_to_string((QuantizeMode)atomic_load(&g_engine->quantize_mode_atomic)),
|
||||
(unsigned int)atomic_load(&g_engine->quantize_threshold_atomic));
|
||||
transport_state_to_string(state.transport_state),
|
||||
clock_source_to_string(state.clock_source),
|
||||
state.bpm,
|
||||
quantize_mode_to_string(state.quantize_mode),
|
||||
(unsigned int)state.quantize_threshold);
|
||||
|
||||
// Draw mode indicator
|
||||
const char *mode_str;
|
||||
switch (current_mode) {
|
||||
case MODE_NORMAL: mode_str = "NORMAL"; break;
|
||||
@@ -295,38 +271,23 @@ static void draw_grid(void) {
|
||||
case MODE_MOVE: mode_str = "MOVE"; break;
|
||||
default: mode_str = "?"; break;
|
||||
}
|
||||
mvprintw(GRID_ROWS * CELL_HEIGHT + 3, 0,
|
||||
"Mode: %s", mode_str);
|
||||
mvprintw(GRID_ROWS * CELL_HEIGHT + 3, 0, "Mode: %s", mode_str);
|
||||
|
||||
// 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,
|
||||
"Space - Play/Pause transport");
|
||||
mvprintw(GRID_ROWS * CELL_HEIGHT + 10, 0,
|
||||
"S - Stop transport");
|
||||
mvprintw(GRID_ROWS * CELL_HEIGHT + 11, 0,
|
||||
"C - Toggle clock source (Internal/MIDI)");
|
||||
mvprintw(GRID_ROWS * CELL_HEIGHT + 12, 0,
|
||||
"q - Toggle quantize mode (off/beat/bar)");
|
||||
mvprintw(GRID_ROWS * CELL_HEIGHT + 13, 0,
|
||||
"T - Set quantize threshold");
|
||||
mvprintw(GRID_ROWS * CELL_HEIGHT + 14, 0,
|
||||
"x - Reset transport position");
|
||||
mvprintw(GRID_ROWS * CELL_HEIGHT + 15, 0,
|
||||
"? - Toggle help");
|
||||
mvprintw(GRID_ROWS * CELL_HEIGHT + 16, 0,
|
||||
"Esc/q - Quit");
|
||||
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, "Space - Play/Pause transport");
|
||||
mvprintw(GRID_ROWS * CELL_HEIGHT + 10, 0, "S - Stop transport");
|
||||
mvprintw(GRID_ROWS * CELL_HEIGHT + 11, 0, "C - Toggle clock source (Internal/MIDI)");
|
||||
mvprintw(GRID_ROWS * CELL_HEIGHT + 12, 0, "q - Toggle quantize mode (off/beat/bar)");
|
||||
mvprintw(GRID_ROWS * CELL_HEIGHT + 13, 0, "T - Set quantize threshold");
|
||||
mvprintw(GRID_ROWS * CELL_HEIGHT + 14, 0, "x - Reset transport position");
|
||||
mvprintw(GRID_ROWS * CELL_HEIGHT + 15, 0, "? - Toggle help");
|
||||
mvprintw(GRID_ROWS * CELL_HEIGHT + 16, 0, "Esc/q - Quit");
|
||||
attroff(COLOR_PAIR(COLOR_HELP));
|
||||
}
|
||||
|
||||
@@ -334,42 +295,31 @@ static void draw_grid(void) {
|
||||
}
|
||||
|
||||
// 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
|
||||
return true;
|
||||
} else if (strncmp(cmd_buffer, "load ", 5) == 0) {
|
||||
// :load <clip_index> <filename>
|
||||
char *rest = cmd_buffer + 5;
|
||||
int clip_idx = atoi(rest);
|
||||
// Find filename after clip index
|
||||
char *filename = rest;
|
||||
while (*filename && *filename != ' ') filename++;
|
||||
if (*filename) {
|
||||
@@ -377,25 +327,26 @@ static bool handle_command_mode(void) {
|
||||
filename++;
|
||||
while (*filename == ' ') filename++;
|
||||
if (*filename) {
|
||||
// Submit load request via save/load queue
|
||||
save_load_queue_push(&g_engine->save_load_queue, REQ_LOAD_CLIP, clip_idx, 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) {
|
||||
// :save <clip_index>
|
||||
int clip_idx = atoi(cmd_buffer + 5);
|
||||
save_load_queue_push(&g_engine->save_load_queue, REQ_SAVE_CLIP, clip_idx, "");
|
||||
Action action = { .type = ACTION_SAVE_CLIP, .data.save_clip = { .clip_index = clip_idx } };
|
||||
g_dispatch(action);
|
||||
}
|
||||
|
||||
// Restore previous nodelay state before returning
|
||||
nodelay(stdscr, prev_nodelay);
|
||||
return false; // Don't quit
|
||||
} else if (ch == 27) { // Escape - cancel command mode
|
||||
return false;
|
||||
} else if (ch == 27) {
|
||||
mvprintw(LINES - 1, 0, " ");
|
||||
refresh();
|
||||
nodelay(stdscr, prev_nodelay);
|
||||
return false;
|
||||
} else if (ch == KEY_BACKSPACE || ch == 127) { // Backspace
|
||||
} else if (ch == KEY_BACKSPACE || ch == 127) {
|
||||
if (cmd_pos > 0) {
|
||||
cmd_pos--;
|
||||
cmd_buffer[cmd_pos] = '\0';
|
||||
@@ -410,76 +361,59 @@ static bool handle_command_mode(void) {
|
||||
}
|
||||
}
|
||||
|
||||
// Should never reach here, but restore just in case
|
||||
nodelay(stdscr, prev_nodelay);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Handle mouse events
|
||||
static void handle_mouse_event(MEVENT *event) {
|
||||
// Convert mouse position to grid coordinates
|
||||
int grid_row = (event->y - 1) / CELL_HEIGHT; // -1 for title row
|
||||
int grid_col = (event->x - 1) / CELL_WIDTH; // -1 for left margin
|
||||
int grid_row = (event->y - 1) / CELL_HEIGHT;
|
||||
int grid_col = (event->x - 1) / CELL_WIDTH;
|
||||
|
||||
// Bounds check
|
||||
if (grid_row < 0 || grid_row >= GRID_ROWS || grid_col < 0 || grid_col >= GRID_COLS) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event->bstate & BUTTON1_CLICKED) {
|
||||
// Left click: select and trigger clip
|
||||
selected_row = grid_row;
|
||||
selected_col = grid_col;
|
||||
|
||||
int clip_idx = grid_to_clip_index(selected_row, selected_col);
|
||||
engine_trigger_clip(g_engine, clip_idx);
|
||||
Action action = { .type = ACTION_TRIGGER_CLIP, .data.trigger_clip = { .clip_index = clip_idx } };
|
||||
g_dispatch(action);
|
||||
} else if (event->bstate & BUTTON3_CLICKED) {
|
||||
// Right click: select and reset clip
|
||||
selected_row = grid_row;
|
||||
selected_col = grid_col;
|
||||
|
||||
int clip_idx = grid_to_clip_index(selected_row, selected_col);
|
||||
engine_reset_clip(g_engine, clip_idx);
|
||||
Action action = { .type = ACTION_RESET_CLIP, .data.reset_clip = { .clip_index = clip_idx } };
|
||||
g_dispatch(action);
|
||||
} else if (event->bstate & BUTTON2_CLICKED) {
|
||||
// Middle click: select and trigger scene
|
||||
selected_row = grid_row;
|
||||
selected_col = grid_col;
|
||||
|
||||
engine_trigger_scene(g_engine, selected_row);
|
||||
Action action = { .type = ACTION_TRIGGER_SCENE, .data.trigger_scene = { .scene_index = selected_row } };
|
||||
g_dispatch(action);
|
||||
} else if (event->bstate & BUTTON1_DOUBLE_CLICKED) {
|
||||
// Double left click: select and trigger scene
|
||||
selected_row = grid_row;
|
||||
selected_col = grid_col;
|
||||
|
||||
engine_trigger_scene(g_engine, selected_row);
|
||||
Action action = { .type = ACTION_TRIGGER_SCENE, .data.trigger_scene = { .scene_index = selected_row } };
|
||||
g_dispatch(action);
|
||||
}
|
||||
}
|
||||
|
||||
static void handle_sigint(int sig) {
|
||||
(void)sig;
|
||||
tui_cleanup();
|
||||
_Exit(1);
|
||||
}
|
||||
|
||||
void tui_init(Engine *engine) {
|
||||
g_engine = engine;
|
||||
g_dispatch = engine->dispatch;
|
||||
|
||||
// Initialize ncurses
|
||||
initscr();
|
||||
cbreak();
|
||||
noecho();
|
||||
keypad(stdscr, TRUE);
|
||||
curs_set(0); // Hide cursor
|
||||
curs_set(0);
|
||||
|
||||
// Enable mouse events
|
||||
mousemask(BUTTON1_CLICKED | BUTTON3_CLICKED | BUTTON2_CLICKED | BUTTON1_DOUBLE_CLICKED, NULL);
|
||||
mouseinterval(10); // 10ms click interval
|
||||
mouseinterval(10);
|
||||
|
||||
// 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);
|
||||
@@ -493,8 +427,8 @@ void tui_run(Engine *engine) {
|
||||
if (!engine) return;
|
||||
|
||||
g_engine = engine;
|
||||
g_dispatch = engine->dispatch;
|
||||
|
||||
// Initialize marks
|
||||
for (int i = 0; i < MAX_MARKS; i++) {
|
||||
marks[i] = -1;
|
||||
}
|
||||
@@ -507,7 +441,6 @@ void tui_run(Engine *engine) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Handle mouse events
|
||||
if (ch == KEY_MOUSE) {
|
||||
MEVENT event;
|
||||
if (getmouse(&event) == OK) {
|
||||
@@ -517,65 +450,42 @@ void tui_run(Engine *engine) {
|
||||
}
|
||||
}
|
||||
|
||||
// Handle mode-specific input
|
||||
if (current_mode == MODE_MOVE) {
|
||||
switch (ch) {
|
||||
case 'h':
|
||||
case KEY_LEFT:
|
||||
case 'h': case KEY_LEFT:
|
||||
selected_col = (selected_col - 1 + GRID_COLS) % GRID_COLS;
|
||||
break;
|
||||
|
||||
case 'j':
|
||||
case KEY_DOWN:
|
||||
case 'j': case KEY_DOWN:
|
||||
selected_row = (selected_row + 1) % GRID_ROWS;
|
||||
break;
|
||||
|
||||
case 'k':
|
||||
case KEY_UP:
|
||||
case 'k': case KEY_UP:
|
||||
selected_row = (selected_row - 1 + GRID_ROWS) % GRID_ROWS;
|
||||
break;
|
||||
|
||||
case 'l':
|
||||
case KEY_RIGHT:
|
||||
case 'l': case KEY_RIGHT:
|
||||
selected_col = (selected_col + 1) % GRID_COLS;
|
||||
break;
|
||||
|
||||
case '\n':
|
||||
case '\r':
|
||||
case 27: // Escape
|
||||
case '\n': case '\r': case 27:
|
||||
current_mode = MODE_NORMAL;
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
draw_grid();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (current_mode == MODE_VISUAL) {
|
||||
switch (ch) {
|
||||
case 'h':
|
||||
case KEY_LEFT:
|
||||
case 'h': case KEY_LEFT:
|
||||
visual_end_col = (visual_end_col - 1 + GRID_COLS) % GRID_COLS;
|
||||
break;
|
||||
|
||||
case 'j':
|
||||
case KEY_DOWN:
|
||||
case 'j': case KEY_DOWN:
|
||||
visual_end_row = (visual_end_row + 1) % GRID_ROWS;
|
||||
break;
|
||||
|
||||
case 'k':
|
||||
case KEY_UP:
|
||||
case 'k': case KEY_UP:
|
||||
visual_end_row = (visual_end_row - 1 + GRID_ROWS) % GRID_ROWS;
|
||||
break;
|
||||
|
||||
case 'l':
|
||||
case KEY_RIGHT:
|
||||
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);
|
||||
@@ -586,7 +496,6 @@ void tui_run(Engine *engine) {
|
||||
current_mode = MODE_NORMAL;
|
||||
break;
|
||||
}
|
||||
|
||||
case 'y': {
|
||||
int count;
|
||||
int *clips = get_selected_clips(&count);
|
||||
@@ -597,135 +506,108 @@ void tui_run(Engine *engine) {
|
||||
current_mode = MODE_NORMAL;
|
||||
break;
|
||||
}
|
||||
|
||||
case 27: // Escape
|
||||
case 27:
|
||||
current_mode = MODE_NORMAL;
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
draw_grid();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Normal mode
|
||||
switch (ch) {
|
||||
case 'h':
|
||||
case KEY_LEFT:
|
||||
case 'h': case KEY_LEFT:
|
||||
selected_col = (selected_col - 1 + GRID_COLS) % GRID_COLS;
|
||||
break;
|
||||
|
||||
case 'j':
|
||||
case KEY_DOWN:
|
||||
case 'j': case KEY_DOWN:
|
||||
selected_row = (selected_row + 1) % GRID_ROWS;
|
||||
break;
|
||||
|
||||
case 'k':
|
||||
case KEY_UP:
|
||||
case 'k': case KEY_UP:
|
||||
selected_row = (selected_row - 1 + GRID_ROWS) % GRID_ROWS;
|
||||
break;
|
||||
|
||||
case 'l':
|
||||
case KEY_RIGHT:
|
||||
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);
|
||||
Action action = { .type = ACTION_TRIGGER_CLIP, .data.trigger_clip = { .clip_index = clip_idx } };
|
||||
g_dispatch(action);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'd': {
|
||||
// Delete (reset) current clip
|
||||
int clip_idx = grid_to_clip_index(selected_row, selected_col);
|
||||
engine_reset_clip(engine, clip_idx);
|
||||
Action action = { .type = ACTION_RESET_CLIP, .data.reset_clip = { .clip_index = clip_idx } };
|
||||
g_dispatch(action);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'y': {
|
||||
// Yank current clip
|
||||
int clip_idx = grid_to_clip_index(selected_row, selected_col);
|
||||
yank_clips(&clip_idx, 1);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'p': {
|
||||
// Paste
|
||||
case 'p':
|
||||
paste_clips();
|
||||
break;
|
||||
}
|
||||
|
||||
case 's': {
|
||||
// Trigger scene for current row
|
||||
engine_trigger_scene(engine, selected_row);
|
||||
Action action = { .type = ACTION_TRIGGER_SCENE, .data.trigger_scene = { .scene_index = selected_row } };
|
||||
g_dispatch(action);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'q': {
|
||||
// Cycle quantize mode
|
||||
AppState state = dispatcher_get_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 (engine->quantize_mode == modes[i]) {
|
||||
if (state.quantize_mode == modes[i]) {
|
||||
current = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
QuantizeMode next = modes[(current + 1) % num_modes];
|
||||
engine_set_quantize_mode(engine, next);
|
||||
Action action = { .type = ACTION_SET_QUANTIZE_MODE, .data.set_quantize_mode = { .mode = next } };
|
||||
g_dispatch(action);
|
||||
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);
|
||||
}
|
||||
AppState state = dispatcher_get_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 ' ': { // Space bar - toggle play/pause
|
||||
engine_transport_toggle_play(engine);
|
||||
case ' ': {
|
||||
Action action = { .type = ACTION_TRANSPORT_TOGGLE_PLAY };
|
||||
g_dispatch(action);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'S': { // Shift+S - stop transport
|
||||
engine_transport_stop(engine);
|
||||
case 'S': {
|
||||
Action action = { .type = ACTION_TRANSPORT_STOP };
|
||||
g_dispatch(action);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'C': { // Shift+C - toggle clock source
|
||||
ClockSource current = transport_get_clock_source(engine->transport);
|
||||
ClockSource next = (current == CLOCK_SOURCE_INTERNAL) ?
|
||||
case 'C': {
|
||||
AppState state = dispatcher_get_state();
|
||||
ClockSource next = (state.clock_source == CLOCK_SOURCE_INTERNAL) ?
|
||||
CLOCK_SOURCE_MIDI : CLOCK_SOURCE_INTERNAL;
|
||||
engine_set_clock_source(engine, next);
|
||||
Action action = { .type = ACTION_SET_CLOCK_SOURCE, .data.set_clock_source = { .source = next } };
|
||||
g_dispatch(action);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'x':
|
||||
engine_transport_stop(engine);
|
||||
case 'x': {
|
||||
Action action = { .type = ACTION_RESET_TRANSPORT };
|
||||
g_dispatch(action);
|
||||
break;
|
||||
|
||||
}
|
||||
case ':': {
|
||||
bool should_quit = handle_command_mode();
|
||||
if (should_quit) {
|
||||
return;
|
||||
}
|
||||
if (should_quit) return;
|
||||
break;
|
||||
}
|
||||
|
||||
case '?':
|
||||
show_help = !show_help;
|
||||
break;
|
||||
|
||||
case 'v': {
|
||||
// Enter visual mode
|
||||
current_mode = MODE_VISUAL;
|
||||
visual_start_row = selected_row;
|
||||
visual_start_col = selected_col;
|
||||
@@ -733,9 +615,7 @@ void tui_run(Engine *engine) {
|
||||
visual_end_col = selected_col;
|
||||
break;
|
||||
}
|
||||
|
||||
case 'V': {
|
||||
// Select entire line
|
||||
current_mode = MODE_VISUAL;
|
||||
visual_start_row = selected_row;
|
||||
visual_start_col = 0;
|
||||
@@ -743,62 +623,41 @@ void tui_run(Engine *engine) {
|
||||
visual_end_col = GRID_COLS - 1;
|
||||
break;
|
||||
}
|
||||
|
||||
case 'm': {
|
||||
// Enter move mode
|
||||
current_mode = MODE_MOVE;
|
||||
break;
|
||||
}
|
||||
|
||||
case 'N': {
|
||||
// Play next scene
|
||||
case 'N':
|
||||
play_next_scene();
|
||||
break;
|
||||
}
|
||||
|
||||
case 'P': {
|
||||
// Play previous scene
|
||||
case 'P':
|
||||
play_prev_scene();
|
||||
break;
|
||||
}
|
||||
|
||||
case '\'': {
|
||||
// Go to mark - wait for next character
|
||||
nodelay(stdscr, FALSE);
|
||||
int mark_ch = getch();
|
||||
nodelay(stdscr, TRUE);
|
||||
if (mark_ch != ERR) {
|
||||
go_to_mark((char)mark_ch);
|
||||
}
|
||||
if (mark_ch != ERR) go_to_mark((char)mark_ch);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'u': {
|
||||
// Undo
|
||||
engine_undo_action(engine);
|
||||
Action action = { .type = ACTION_UNDO };
|
||||
g_dispatch(action);
|
||||
break;
|
||||
}
|
||||
|
||||
case 18: { // Ctrl+R (18 = 0x12)
|
||||
// Redo
|
||||
engine_redo_action(engine);
|
||||
case 18: { // Ctrl+R
|
||||
Action action = { .type = ACTION_REDO };
|
||||
g_dispatch(action);
|
||||
break;
|
||||
}
|
||||
|
||||
case 27: // Escape key
|
||||
case 'Q':
|
||||
case 27: case 'Q':
|
||||
return;
|
||||
|
||||
default:
|
||||
// Check for mark setting (m<char>)
|
||||
if (ch == 'm') {
|
||||
// Wait for next character
|
||||
nodelay(stdscr, FALSE);
|
||||
int mark_ch = getch();
|
||||
nodelay(stdscr, TRUE);
|
||||
if (mark_ch != ERR) {
|
||||
set_mark((char)mark_ch);
|
||||
}
|
||||
if (mark_ch != ERR) set_mark((char)mark_ch);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -808,20 +667,13 @@ void tui_run(Engine *engine) {
|
||||
}
|
||||
|
||||
void tui_cleanup(void) {
|
||||
// Free yank buffer
|
||||
if (yank_buffer.clip_indices) {
|
||||
free(yank_buffer.clip_indices);
|
||||
yank_buffer.clip_indices = NULL;
|
||||
yank_buffer.count = 0;
|
||||
}
|
||||
|
||||
// Disable mouse
|
||||
mousemask(0, NULL);
|
||||
|
||||
// Restore terminal settings
|
||||
curs_set(1);
|
||||
endwin();
|
||||
|
||||
// Reset signal handler to default
|
||||
signal(SIGINT, SIG_DFL);
|
||||
}
|
||||
|
||||
4
tui.h
4
tui.h
@@ -2,6 +2,7 @@
|
||||
#define TUI_H
|
||||
|
||||
#include "engine.h"
|
||||
#include "dispatcher.h"
|
||||
|
||||
// Initialize TUI
|
||||
void tui_init(Engine *engine);
|
||||
@@ -12,7 +13,4 @@ void tui_run(Engine *engine);
|
||||
// Cleanup TUI
|
||||
void tui_cleanup(void);
|
||||
|
||||
// Handle command mode input (after pressing ':')
|
||||
void tui_handle_command_mode(Engine *engine);
|
||||
|
||||
#endif // TUI_H
|
||||
|
||||
Reference in New Issue
Block a user