From be3582bc136a07922aa7263622bf2ca6e261828d Mon Sep 17 00:00:00 2001 From: Loic Coenen Date: Fri, 1 May 2026 20:10:45 +0000 Subject: [PATCH] feat: implement undo/redo system with history tracking and tests Co-authored-by: aider (deepseek/deepseek-coder) --- engine.c | 259 ++++++++++++++++++++++++++++++++++- engine.h | 52 +++++++- test_tui.c | 385 +++++++++++++++++++++++++++++++++++++++++++++++++++++ tui.c | 14 ++ 4 files changed, 702 insertions(+), 8 deletions(-) diff --git a/engine.c b/engine.c index d9b5691..b02734a 100644 --- a/engine.c +++ b/engine.c @@ -333,6 +333,17 @@ void engine_process_commands(Engine *engine) { if (cmd.index < 0 || cmd.index >= MAX_CLIPS) break; Clip *clip = &engine->clips[cmd.index]; + // Record undo action + UndoAction action; + action.type = ACTION_TRIGGER_CLIP; + action.index = cmd.index; + action.value = 0; + action.previous_state = clip->state; + action.previous_buffer_size = clip->buffer_size; + action.previous_write_position = clip->write_position; + action.previous_read_position = clip->read_position; + engine_push_undo_action(engine, &action); + switch (clip->state) { case CLIP_EMPTY: clip->state = CLIP_RECORDING; @@ -359,6 +370,14 @@ void engine_process_commands(Engine *engine) { case CMD_TRIGGER_SCENE: { if (cmd.index < 0 || cmd.index >= MAX_SCENES) break; + + // Record undo action + UndoAction action; + action.type = ACTION_TRIGGER_SCENE; + action.index = cmd.index; + action.value = 0; + engine_push_undo_action(engine, &action); + for (int ch = 0; ch < MAX_CHANNELS; ch++) { int clip_idx = CLIP_INDEX(cmd.index, ch); Clip *clip = &engine->clips[clip_idx]; @@ -391,6 +410,18 @@ void engine_process_commands(Engine *engine) { case CMD_RESET_CLIP: { if (cmd.index < 0 || cmd.index >= MAX_CLIPS) break; Clip *clip = &engine->clips[cmd.index]; + + // Record undo action + UndoAction action; + action.type = ACTION_RESET_CLIP; + action.index = cmd.index; + action.value = 0; + action.previous_state = clip->state; + action.previous_buffer_size = clip->buffer_size; + action.previous_write_position = clip->write_position; + action.previous_read_position = clip->read_position; + engine_push_undo_action(engine, &action); + clip->state = CLIP_EMPTY; clip->buffer_size = 0; clip->write_position = 0; @@ -399,15 +430,51 @@ void engine_process_commands(Engine *engine) { break; } - case CMD_SET_QUANTIZE_MODE: - engine->quantize_mode = (QuantizeMode)cmd.index; - break; + case CMD_SET_QUANTIZE_MODE: { + QuantizeMode new_mode = (QuantizeMode)cmd.index; - case CMD_SET_QUANTIZE_THRESHOLD: - engine->quantize_threshold = cmd.value; - break; + // Record undo action + UndoAction action; + action.type = ACTION_SET_QUANTIZE_MODE; + action.index = (int)new_mode; + action.value = 0; + action.previous_quantize_mode = engine->quantize_mode; + engine_push_undo_action(engine, &action); + + engine->quantize_mode = new_mode; + atomic_store(&engine->quantize_mode_atomic, (int)new_mode); + break; + } + + case CMD_SET_QUANTIZE_THRESHOLD: { + jack_nframes_t new_threshold = cmd.value; + + // Record undo action + UndoAction action; + action.type = ACTION_SET_QUANTIZE_THRESHOLD; + action.index = 0; + action.value = new_threshold; + action.previous_quantize_threshold = engine->quantize_threshold; + engine_push_undo_action(engine, &action); + + engine->quantize_threshold = new_threshold; + atomic_store(&engine->quantize_threshold_atomic, new_threshold); + break; + } + + case CMD_RESET_TRANSPORT: { + // Record undo action + UndoAction action; + action.type = ACTION_RESET_TRANSPORT; + action.index = 0; + action.value = 0; + action.previous_rolling = engine->transport.rolling; + action.previous_clock_count = engine->transport.clock_count; + action.previous_beat_position = engine->transport.beat_position; + action.previous_bar_position = engine->transport.bar_position; + action.previous_sample_position = engine->transport.sample_position; + engine_push_undo_action(engine, &action); - case CMD_RESET_TRANSPORT: engine->transport.rolling = false; engine->transport.clock_count = 0; engine->transport.beat_position = 0; @@ -419,6 +486,16 @@ void engine_process_commands(Engine *engine) { atomic_store(&engine->transport_bar_position, 0); atomic_store(&engine->transport_sample_position, 0); break; + } + } + + case CMD_UNDO: + engine_undo(engine); + break; + + case CMD_REDO: + engine_redo(engine); + break; } read++; @@ -428,6 +505,159 @@ void engine_process_commands(Engine *engine) { atomic_store(&q->read_index, read); } +// Push an action to the undo history +void engine_push_undo_action(Engine *engine, UndoAction *action) { + if (!engine || !action) return; + + UndoHistory *history = &engine->undo_history; + + // If we've undone some actions, clear the redo history + if (history->redo_index > history->undo_index) { + history->redo_index = history->undo_index; + } + + // Add action at current undo position + int slot = history->undo_index % MAX_UNDO_HISTORY; + history->actions[slot] = *action; + history->undo_index++; + history->redo_index = history->undo_index; + + if (history->count < MAX_UNDO_HISTORY) { + history->count++; + } +} + +// Undo the last action +void engine_undo(Engine *engine) { + if (!engine) return; + + UndoHistory *history = &engine->undo_history; + if (history->undo_index <= 0) return; // Nothing to undo + + int slot = (history->undo_index - 1) % MAX_UNDO_HISTORY; + UndoAction *action = &history->actions[slot]; + + switch (action->type) { + case ACTION_TRIGGER_CLIP: { + int clip_idx = action->index; + engine->clips[clip_idx].state = action->previous_state; + engine->clips[clip_idx].buffer_size = action->previous_buffer_size; + engine->clips[clip_idx].write_position = action->previous_write_position; + engine->clips[clip_idx].read_position = action->previous_read_position; + break; + } + + case ACTION_TRIGGER_SCENE: { + int scene_idx = action->index; + for (int ch = 0; ch < MAX_CHANNELS; ch++) { + int clip_idx = CLIP_INDEX(scene_idx, ch); + engine->clips[clip_idx].state = CLIP_EMPTY; + } + break; + } + + case ACTION_RESET_CLIP: { + int clip_idx = action->index; + engine->clips[clip_idx].state = action->previous_state; + engine->clips[clip_idx].buffer_size = action->previous_buffer_size; + engine->clips[clip_idx].write_position = action->previous_write_position; + engine->clips[clip_idx].read_position = action->previous_read_position; + break; + } + + case ACTION_SET_QUANTIZE_MODE: { + engine->quantize_mode = action->previous_quantize_mode; + atomic_store(&engine->quantize_mode_atomic, (int)action->previous_quantize_mode); + break; + } + + case ACTION_SET_QUANTIZE_THRESHOLD: { + engine->quantize_threshold = action->previous_quantize_threshold; + atomic_store(&engine->quantize_threshold_atomic, action->previous_quantize_threshold); + break; + } + + case ACTION_RESET_TRANSPORT: { + engine->transport.rolling = action->previous_rolling; + engine->transport.clock_count = action->previous_clock_count; + engine->transport.beat_position = action->previous_beat_position; + engine->transport.bar_position = action->previous_bar_position; + engine->transport.sample_position = action->previous_sample_position; + atomic_store(&engine->transport_rolling, action->previous_rolling ? 1 : 0); + atomic_store(&engine->transport_clock_count, action->previous_clock_count); + atomic_store(&engine->transport_beat_position, action->previous_beat_position); + atomic_store(&engine->transport_bar_position, action->previous_bar_position); + atomic_store(&engine->transport_sample_position, action->previous_sample_position); + break; + } + } + + history->undo_index--; +} + +// Redo the last undone action +void engine_redo(Engine *engine) { + if (!engine) return; + + UndoHistory *history = &engine->undo_history; + if (history->redo_index <= history->undo_index) return; // Nothing to redo + + int slot = history->undo_index % MAX_UNDO_HISTORY; + UndoAction *action = &history->actions[slot]; + + switch (action->type) { + case ACTION_TRIGGER_CLIP: { + int clip_idx = action->index; + // Re-apply the trigger + engine_trigger_clip(engine, clip_idx); + engine_process_commands(engine); + break; + } + + case ACTION_TRIGGER_SCENE: { + int scene_idx = action->index; + engine_trigger_scene(engine, scene_idx); + engine_process_commands(engine); + break; + } + + case ACTION_RESET_CLIP: { + int clip_idx = action->index; + engine_reset_clip(engine, clip_idx); + engine_process_commands(engine); + break; + } + + case ACTION_SET_QUANTIZE_MODE: { + engine->quantize_mode = (QuantizeMode)action->index; + atomic_store(&engine->quantize_mode_atomic, action->index); + break; + } + + case ACTION_SET_QUANTIZE_THRESHOLD: { + engine->quantize_threshold = action->value; + atomic_store(&engine->quantize_threshold_atomic, action->value); + break; + } + + case ACTION_RESET_TRANSPORT: { + engine->transport.rolling = true; + engine->transport.clock_count = 0; + engine->transport.beat_position = 0; + engine->transport.bar_position = 0; + engine->transport.sample_position = 0; + atomic_store(&engine->transport_rolling, 1); + atomic_store(&engine->transport_clock_count, 0); + atomic_store(&engine->transport_beat_position, 0); + atomic_store(&engine->transport_bar_position, 0); + atomic_store(&engine->transport_sample_position, 0); + break; + } + } + + history->undo_index++; +} + int engine_init(Engine *engine, const char *client_name) { if (!engine || !client_name) return -1; @@ -438,6 +668,11 @@ int engine_init(Engine *engine, const char *client_name) { engine->quantize_threshold = 0; engine->queued_triggers = NULL; + // Initialize undo history + engine->undo_history.undo_index = 0; + engine->undo_history.redo_index = 0; + engine->undo_history.count = 0; + // Initialize command queue command_queue_init(&engine->command_queue); @@ -647,3 +882,13 @@ const char* quantize_mode_to_string(QuantizeMode mode) { default: return "Unknown"; } } + +void engine_undo_action(Engine *engine) { + if (!engine) return; + engine_submit_command(engine, CMD_UNDO, 0, 0); +} + +void engine_redo_action(Engine *engine) { + if (!engine) return; + engine_submit_command(engine, CMD_REDO, 0, 0); +} diff --git a/engine.h b/engine.h index 79c82c8..4954dfc 100644 --- a/engine.h +++ b/engine.h @@ -56,9 +56,49 @@ typedef enum { CMD_RESET_CLIP, CMD_SET_QUANTIZE_MODE, CMD_SET_QUANTIZE_THRESHOLD, - CMD_RESET_TRANSPORT + CMD_RESET_TRANSPORT, + CMD_UNDO, + CMD_REDO } CommandType; +// Undo/Redo action types +typedef enum { + ACTION_TRIGGER_CLIP, + ACTION_TRIGGER_SCENE, + ACTION_RESET_CLIP, + ACTION_SET_QUANTIZE_MODE, + ACTION_SET_QUANTIZE_THRESHOLD, + ACTION_RESET_TRANSPORT +} ActionType; + +// Undo/Redo action record +typedef struct { + ActionType type; + int index; // clip_index, scene_index, or mode value + jack_nframes_t value; // threshold value or other numeric param + ClipState previous_state; // For clip state changes + size_t previous_buffer_size; + size_t previous_write_position; + size_t previous_read_position; + bool previous_rolling; + uint32_t previous_clock_count; + uint32_t previous_beat_position; + uint32_t previous_bar_position; + uint32_t previous_sample_position; + QuantizeMode previous_quantize_mode; + jack_nframes_t previous_quantize_threshold; +} UndoAction; + +// Undo/Redo history +#define MAX_UNDO_HISTORY 256 + +typedef struct { + UndoAction actions[MAX_UNDO_HISTORY]; + int undo_index; // Points to next action to undo + int redo_index; // Points to next action to redo + int count; // Total actions in history +} UndoHistory; + typedef struct { CommandType type; int index; // clip_index, scene_index, or mode value @@ -115,6 +155,9 @@ typedef struct { atomic_uint quantize_threshold_atomic; bool running; + + // Undo/Redo + UndoHistory undo_history; } Engine; // Engine lifecycle @@ -150,4 +193,11 @@ const char* clip_state_to_string(ClipState state); uint8_t clip_state_to_velocity(ClipState state); const char* quantize_mode_to_string(QuantizeMode mode); +// Undo/Redo +void engine_undo(Engine *engine); +void engine_redo(Engine *engine); +void engine_push_undo_action(Engine *engine, UndoAction *action); +void engine_undo_action(Engine *engine); +void engine_redo_action(Engine *engine); + #endif // ENGINE_H diff --git a/test_tui.c b/test_tui.c index 23211e9..5e04eb3 100644 --- a/test_tui.c +++ b/test_tui.c @@ -1211,6 +1211,379 @@ void test_remark_existing_mark(void) { printf("PASSED\n"); } +// Test 53: Undo single clip trigger +void test_undo_single_trigger(void) { + printf("Test 53: Undo single clip trigger... "); + Engine *engine = create_test_engine(); + + // Start with empty clip + int clip_idx = 10; + assert(engine->clips[clip_idx].state == CLIP_EMPTY); + + // Trigger clip (empty -> recording) + engine_trigger_clip(engine, clip_idx); + engine_process_commands(engine); + assert(engine->clips[clip_idx].state == CLIP_RECORDING); + + // Undo: should go back to empty + engine_undo_action(engine); + engine_process_commands(engine); + assert(engine->clips[clip_idx].state == CLIP_EMPTY); + + printf("PASSED\n"); + destroy_test_engine(engine); +} + +// Test 54: Undo multiple clip triggers +void test_undo_multiple_triggers(void) { + printf("Test 54: Undo multiple clip triggers... "); + Engine *engine = create_test_engine(); + + int clip1 = 10, clip2 = 11, clip3 = 12; + + // Trigger three clips + engine_trigger_clip(engine, clip1); + engine_process_commands(engine); + assert(engine->clips[clip1].state == CLIP_RECORDING); + + engine_trigger_clip(engine, clip2); + engine_process_commands(engine); + assert(engine->clips[clip2].state == CLIP_RECORDING); + + engine_trigger_clip(engine, clip3); + engine_process_commands(engine); + assert(engine->clips[clip3].state == CLIP_RECORDING); + + // Undo last action: clip3 should go back to empty + engine_undo_action(engine); + engine_process_commands(engine); + assert(engine->clips[clip3].state == CLIP_EMPTY); + assert(engine->clips[clip2].state == CLIP_RECORDING); + assert(engine->clips[clip1].state == CLIP_RECORDING); + + // Undo again: clip2 should go back to empty + engine_undo_action(engine); + engine_process_commands(engine); + assert(engine->clips[clip2].state == CLIP_EMPTY); + assert(engine->clips[clip1].state == CLIP_RECORDING); + + printf("PASSED\n"); + destroy_test_engine(engine); +} + +// Test 55: Redo single undo +void test_redo_single_undo(void) { + printf("Test 55: Redo single undo... "); + Engine *engine = create_test_engine(); + + int clip_idx = 10; + + // Trigger clip + engine_trigger_clip(engine, clip_idx); + engine_process_commands(engine); + assert(engine->clips[clip_idx].state == CLIP_RECORDING); + + // Undo + engine_undo_action(engine); + engine_process_commands(engine); + assert(engine->clips[clip_idx].state == CLIP_EMPTY); + + // Redo: should go back to recording + engine_redo_action(engine); + engine_process_commands(engine); + assert(engine->clips[clip_idx].state == CLIP_RECORDING); + + printf("PASSED\n"); + destroy_test_engine(engine); +} + +// Test 56: Redo after multiple undos +void test_redo_multiple_undos(void) { + printf("Test 56: Redo after multiple undos... "); + Engine *engine = create_test_engine(); + + int clip1 = 10, clip2 = 11; + + // Trigger two clips + engine_trigger_clip(engine, clip1); + engine_process_commands(engine); + engine_trigger_clip(engine, clip2); + engine_process_commands(engine); + assert(engine->clips[clip1].state == CLIP_RECORDING); + assert(engine->clips[clip2].state == CLIP_RECORDING); + + // Undo twice + engine_undo_action(engine); + engine_process_commands(engine); + engine_undo_action(engine); + engine_process_commands(engine); + assert(engine->clips[clip1].state == CLIP_EMPTY); + assert(engine->clips[clip2].state == CLIP_EMPTY); + + // Redo twice + engine_redo_action(engine); + engine_process_commands(engine); + assert(engine->clips[clip1].state == CLIP_RECORDING); + assert(engine->clips[clip2].state == CLIP_EMPTY); + + engine_redo_action(engine); + engine_process_commands(engine); + assert(engine->clips[clip1].state == CLIP_RECORDING); + assert(engine->clips[clip2].state == CLIP_RECORDING); + + printf("PASSED\n"); + destroy_test_engine(engine); +} + +// Test 57: Undo scene trigger +void test_undo_scene_trigger(void) { + printf("Test 57: Undo scene trigger... "); + Engine *engine = create_test_engine(); + + int scene_idx = 3; + + // Trigger scene + engine_trigger_scene(engine, scene_idx); + engine_process_commands(engine); + + // All clips in scene should be recording + for (int ch = 0; ch < MAX_CHANNELS; ch++) { + int clip_idx = CLIP_INDEX(scene_idx, ch); + assert(engine->clips[clip_idx].state == CLIP_RECORDING); + } + + // Undo: all clips should go back to empty + engine_undo_action(engine); + engine_process_commands(engine); + for (int ch = 0; ch < MAX_CHANNELS; ch++) { + int clip_idx = CLIP_INDEX(scene_idx, ch); + assert(engine->clips[clip_idx].state == CLIP_EMPTY); + } + + printf("PASSED\n"); + destroy_test_engine(engine); +} + +// Test 58: Undo clip state cycle (empty -> recording -> looping -> stopped) +void test_undo_clip_state_cycle(void) { + printf("Test 58: Undo clip state cycle... "); + Engine *engine = create_test_engine(); + + int clip_idx = 10; + + // Cycle through all states + engine_trigger_clip(engine, clip_idx); // empty -> recording + engine_process_commands(engine); + assert(engine->clips[clip_idx].state == CLIP_RECORDING); + + engine_trigger_clip(engine, clip_idx); // recording -> looping + engine_process_commands(engine); + assert(engine->clips[clip_idx].state == CLIP_LOOPING); + + engine_trigger_clip(engine, clip_idx); // looping -> stopped + engine_process_commands(engine); + assert(engine->clips[clip_idx].state == CLIP_STOPPED); + + // Undo three times to go back to empty + engine_undo_action(engine); // stopped -> looping + engine_process_commands(engine); + assert(engine->clips[clip_idx].state == CLIP_LOOPING); + + engine_undo_action(engine); // looping -> recording + engine_process_commands(engine); + assert(engine->clips[clip_idx].state == CLIP_RECORDING); + + engine_undo_action(engine); // recording -> empty + engine_process_commands(engine); + assert(engine->clips[clip_idx].state == CLIP_EMPTY); + + printf("PASSED\n"); + destroy_test_engine(engine); +} + +// Test 59: Undo after new action clears redo history +void test_undo_clears_redo_on_new_action(void) { + printf("Test 59: Undo after new action clears redo history... "); + Engine *engine = create_test_engine(); + + int clip1 = 10, clip2 = 11; + + // Trigger clip1 + engine_trigger_clip(engine, clip1); + engine_process_commands(engine); + assert(engine->clips[clip1].state == CLIP_RECORDING); + + // Undo + engine_undo_action(engine); + engine_process_commands(engine); + assert(engine->clips[clip1].state == CLIP_EMPTY); + + // Redo should work + engine_redo_action(engine); + engine_process_commands(engine); + assert(engine->clips[clip1].state == CLIP_RECORDING); + + // Undo again + engine_undo_action(engine); + engine_process_commands(engine); + assert(engine->clips[clip1].state == CLIP_EMPTY); + + // Now do a new action (trigger clip2) + engine_trigger_clip(engine, clip2); + engine_process_commands(engine); + assert(engine->clips[clip2].state == CLIP_RECORDING); + + // Redo should NOT work now (redo history cleared) + engine_redo_action(engine); + engine_process_commands(engine); + assert(engine->clips[clip1].state == CLIP_EMPTY); // Should still be empty + + printf("PASSED\n"); + destroy_test_engine(engine); +} + +// Test 60: Undo reset clip +void test_undo_reset_clip(void) { + printf("Test 60: Undo reset clip... "); + Engine *engine = create_test_engine(); + + int clip_idx = 10; + + // Set up a looping clip with data + engine->clips[clip_idx].state = CLIP_LOOPING; + engine->clips[clip_idx].buffer_size = 100; + engine->clips[clip_idx].write_position = 100; + engine->clips[clip_idx].read_position = 50; + + // Reset the clip + engine_reset_clip(engine, clip_idx); + engine_process_commands(engine); + assert(engine->clips[clip_idx].state == CLIP_EMPTY); + assert(engine->clips[clip_idx].buffer_size == 0); + + // Undo: should restore clip to previous state + engine_undo_action(engine); + engine_process_commands(engine); + assert(engine->clips[clip_idx].state == CLIP_LOOPING); + assert(engine->clips[clip_idx].buffer_size == 100); + assert(engine->clips[clip_idx].write_position == 100); + assert(engine->clips[clip_idx].read_position == 50); + + printf("PASSED\n"); + destroy_test_engine(engine); +} + +// Test 61: Undo/redo with quantize mode change +void test_undo_quantize_change(void) { + printf("Test 61: Undo quantize mode change... "); + Engine *engine = create_test_engine(); + + assert(engine->quantize_mode == QUANTIZE_OFF); + + // Change quantize mode + engine_set_quantize_mode(engine, QUANTIZE_BEAT); + engine_process_commands(engine); + assert(engine->quantize_mode == QUANTIZE_BEAT); + + // Undo: should go back to OFF + engine_undo_action(engine); + engine_process_commands(engine); + assert(engine->quantize_mode == QUANTIZE_OFF); + + // Redo: should go back to BEAT + engine_redo_action(engine); + engine_process_commands(engine); + assert(engine->quantize_mode == QUANTIZE_BEAT); + + printf("PASSED\n"); + destroy_test_engine(engine); +} + +// Test 62: Undo/redo with threshold change +void test_undo_threshold_change(void) { + printf("Test 62: Undo threshold change... "); + Engine *engine = create_test_engine(); + + assert(engine->quantize_threshold == 0); + + // Change threshold + engine_set_quantize_threshold(engine, 1000); + engine_process_commands(engine); + assert(engine->quantize_threshold == 1000); + + // Undo: should go back to 0 + engine_undo_action(engine); + engine_process_commands(engine); + assert(engine->quantize_threshold == 0); + + // Redo: should go back to 1000 + engine_redo_action(engine); + engine_process_commands(engine); + assert(engine->quantize_threshold == 1000); + + printf("PASSED\n"); + destroy_test_engine(engine); +} + +// Test 63: Undo/redo with transport reset +void test_undo_transport_reset(void) { + printf("Test 63: Undo transport reset... "); + 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; + + // Reset transport + engine_reset_transport(engine); + engine_process_commands(engine); + assert(engine->transport.rolling == false); + assert(engine->transport.clock_count == 0); + + // Undo: should restore transport state + engine_undo_action(engine); + engine_process_commands(engine); + assert(engine->transport.rolling == true); + assert(engine->transport.clock_count == 100); + assert(engine->transport.beat_position == 2); + assert(engine->transport.bar_position == 5); + assert(engine->transport.sample_position == 10000); + + printf("PASSED\n"); + destroy_test_engine(engine); +} + +// Test 64: Undo/redo with paste operation +void test_undo_paste(void) { + printf("Test 64: Undo paste operation... "); + Engine *engine = create_test_engine(); + + int clip_idx = 27; + + // Simulate paste: trigger clip three times to go empty -> recording -> looping -> stopped + engine_trigger_clip(engine, clip_idx); + engine_trigger_clip(engine, clip_idx); + engine_trigger_clip(engine, clip_idx); + engine_process_commands(engine); + assert(engine->clips[clip_idx].state == CLIP_STOPPED); + + // Undo: should go back to empty + engine_undo_action(engine); + engine_process_commands(engine); + assert(engine->clips[clip_idx].state == CLIP_EMPTY); + + // Redo: should go back to stopped + engine_redo_action(engine); + engine_process_commands(engine); + assert(engine->clips[clip_idx].state == CLIP_STOPPED); + + printf("PASSED\n"); + destroy_test_engine(engine); +} + int main(void) { printf("Running TUI tests...\n\n"); @@ -1266,6 +1639,18 @@ int main(void) { test_visual_yank_then_escape(); test_multiple_marks(); test_remark_existing_mark(); + test_undo_single_trigger(); + test_undo_multiple_triggers(); + test_redo_single_undo(); + test_redo_multiple_undos(); + test_undo_scene_trigger(); + test_undo_clip_state_cycle(); + test_undo_clears_redo_on_new_action(); + test_undo_reset_clip(); + test_undo_quantize_change(); + test_undo_threshold_change(); + test_undo_transport_reset(); + test_undo_paste(); printf("\nAll TUI tests passed!\n"); return 0; diff --git a/tui.c b/tui.c index b5861d8..11a6eca 100644 --- a/tui.c +++ b/tui.c @@ -735,6 +735,20 @@ void tui_run(Engine *engine) { break; } + case 'u': { + // Undo + engine_undo_action(engine); + engine_process_commands(engine); + break; + } + + case 18: { // Ctrl+R (18 = 0x12) + // Redo + engine_redo_action(engine); + engine_process_commands(engine); + break; + } + case 27: // Escape key case 'Q': return;