feat: add CLI interface with command parsing and tests
Co-authored-by: aider (deepseek/deepseek-coder) <aider@aider.chat>
This commit is contained in:
143
cli.c
143
cli.c
@@ -0,0 +1,143 @@
|
|||||||
|
#include "cli.h"
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <ctype.h>
|
||||||
|
|
||||||
|
// Trim leading/trailing whitespace
|
||||||
|
static char *trim(char *str) {
|
||||||
|
char *end;
|
||||||
|
while (isspace((unsigned char)*str)) str++;
|
||||||
|
if (*str == 0) return str;
|
||||||
|
end = str + strlen(str) - 1;
|
||||||
|
while (end > str && isspace((unsigned char)*end)) end--;
|
||||||
|
*(end+1) = 0;
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse quantize mode from string
|
||||||
|
static QuantizeMode parse_quantize_mode(const char *str) {
|
||||||
|
if (strcasecmp(str, "off") == 0) return QUANTIZE_OFF;
|
||||||
|
if (strcasecmp(str, "beat") == 0) return QUANTIZE_BEAT;
|
||||||
|
if (strcasecmp(str, "bar") == 0) return QUANTIZE_BAR;
|
||||||
|
return QUANTIZE_OFF; // default
|
||||||
|
}
|
||||||
|
|
||||||
|
int cli_process_line(Engine *engine, const char *line) {
|
||||||
|
if (!engine || !line) return 1;
|
||||||
|
|
||||||
|
// Make a mutable copy
|
||||||
|
char buf[256];
|
||||||
|
strncpy(buf, line, sizeof(buf)-1);
|
||||||
|
buf[sizeof(buf)-1] = '\0';
|
||||||
|
|
||||||
|
char *trimmed = trim(buf);
|
||||||
|
if (trimmed[0] == '\0' || trimmed[0] == '#') return 1; // skip empty/comment
|
||||||
|
|
||||||
|
// Tokenize
|
||||||
|
char *token = strtok(trimmed, " \t");
|
||||||
|
if (!token) return 1;
|
||||||
|
|
||||||
|
if (strcasecmp(token, "quit") == 0 || strcasecmp(token, "exit") == 0) {
|
||||||
|
return 0; // stop loop
|
||||||
|
}
|
||||||
|
else if (strcasecmp(token, "help") == 0) {
|
||||||
|
printf("Commands:\n");
|
||||||
|
printf(" trigger clip <index> - Trigger a clip\n");
|
||||||
|
printf(" trigger scene <index> - Trigger a scene\n");
|
||||||
|
printf(" reset clip <index> - Reset a clip\n");
|
||||||
|
printf(" reset transport - Reset transport\n");
|
||||||
|
printf(" quantize off|beat|bar - Set quantize mode\n");
|
||||||
|
printf(" threshold <samples> - Set quantize threshold\n");
|
||||||
|
printf(" help - Show this help\n");
|
||||||
|
printf(" quit - Exit CLI\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
else if (strcasecmp(token, "trigger") == 0) {
|
||||||
|
char *sub = strtok(NULL, " \t");
|
||||||
|
if (!sub) {
|
||||||
|
printf("Usage: trigger clip|scene <index>\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
char *idx_str = strtok(NULL, " \t");
|
||||||
|
if (!idx_str) {
|
||||||
|
printf("Missing index\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
int idx = atoi(idx_str);
|
||||||
|
if (strcasecmp(sub, "clip") == 0) {
|
||||||
|
engine_trigger_clip(engine, idx);
|
||||||
|
} else if (strcasecmp(sub, "scene") == 0) {
|
||||||
|
engine_trigger_scene(engine, idx);
|
||||||
|
} else {
|
||||||
|
printf("Unknown trigger type: %s\n", sub);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (strcasecmp(token, "reset") == 0) {
|
||||||
|
char *sub = strtok(NULL, " \t");
|
||||||
|
if (!sub) {
|
||||||
|
printf("Usage: reset clip|transport\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (strcasecmp(sub, "clip") == 0) {
|
||||||
|
char *idx_str = strtok(NULL, " \t");
|
||||||
|
if (!idx_str) {
|
||||||
|
printf("Missing clip index\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
int idx = atoi(idx_str);
|
||||||
|
engine_reset_clip(engine, idx);
|
||||||
|
} else if (strcasecmp(sub, "transport") == 0) {
|
||||||
|
engine_reset_transport(engine);
|
||||||
|
} else {
|
||||||
|
printf("Unknown reset target: %s\n", sub);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (strcasecmp(token, "quantize") == 0) {
|
||||||
|
char *mode_str = strtok(NULL, " \t");
|
||||||
|
if (!mode_str) {
|
||||||
|
printf("Usage: quantize off|beat|bar\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
QuantizeMode mode = parse_quantize_mode(mode_str);
|
||||||
|
engine_set_quantize_mode(engine, mode);
|
||||||
|
}
|
||||||
|
else if (strcasecmp(token, "threshold") == 0) {
|
||||||
|
char *val_str = strtok(NULL, " \t");
|
||||||
|
if (!val_str) {
|
||||||
|
printf("Usage: threshold <samples>\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
jack_nframes_t samples = (jack_nframes_t)atol(val_str);
|
||||||
|
engine_set_quantize_threshold(engine, samples);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
printf("Unknown command: %s\n", token);
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void cli_run(Engine *engine) {
|
||||||
|
if (!engine) return;
|
||||||
|
|
||||||
|
printf("JACK Looper CLI\n");
|
||||||
|
printf("Type 'help' for commands, 'quit' to exit.\n\n");
|
||||||
|
|
||||||
|
char line[256];
|
||||||
|
while (1) {
|
||||||
|
printf("> ");
|
||||||
|
fflush(stdout);
|
||||||
|
|
||||||
|
if (!fgets(line, sizeof(line), stdin)) {
|
||||||
|
break; // EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove trailing newline
|
||||||
|
size_t len = strlen(line);
|
||||||
|
if (len > 0 && line[len-1] == '\n') line[len-1] = '\0';
|
||||||
|
|
||||||
|
if (!cli_process_line(engine, line)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
58
test_cli.c
58
test_cli.c
@@ -0,0 +1,58 @@
|
|||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <assert.h>
|
||||||
|
#include "engine.h"
|
||||||
|
#include "cli.h"
|
||||||
|
|
||||||
|
// Minimal test: just ensure parsing doesn't crash
|
||||||
|
static void test_cli_parse(void) {
|
||||||
|
printf("Test CLI parse... ");
|
||||||
|
|
||||||
|
// Create a minimal engine (no JACK needed for parsing)
|
||||||
|
Engine engine;
|
||||||
|
memset(&engine, 0, sizeof(engine));
|
||||||
|
engine.sample_rate = 48000;
|
||||||
|
engine.control_channel = 0;
|
||||||
|
engine.quantize_mode = QUANTIZE_OFF;
|
||||||
|
engine.quantize_threshold = 0;
|
||||||
|
engine.queued_triggers = NULL;
|
||||||
|
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 = NULL; // not needed for parsing
|
||||||
|
engine.clips[i].buffer_size = 0;
|
||||||
|
engine.clips[i].write_position = 0;
|
||||||
|
engine.clips[i].read_position = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test valid commands
|
||||||
|
assert(cli_process_line(&engine, "help") == 1);
|
||||||
|
assert(cli_process_line(&engine, "trigger clip 0") == 1);
|
||||||
|
assert(cli_process_line(&engine, "trigger scene 1") == 1);
|
||||||
|
assert(cli_process_line(&engine, "reset clip 2") == 1);
|
||||||
|
assert(cli_process_line(&engine, "reset transport") == 1);
|
||||||
|
assert(cli_process_line(&engine, "quantize beat") == 1);
|
||||||
|
assert(cli_process_line(&engine, "threshold 1000") == 1);
|
||||||
|
assert(cli_process_line(&engine, "quit") == 0);
|
||||||
|
|
||||||
|
// Test invalid commands (should not crash)
|
||||||
|
assert(cli_process_line(&engine, "") == 1);
|
||||||
|
assert(cli_process_line(&engine, "unknown") == 1);
|
||||||
|
assert(cli_process_line(&engine, "trigger") == 1);
|
||||||
|
assert(cli_process_line(&engine, "reset") == 1);
|
||||||
|
assert(cli_process_line(&engine, "quantize") == 1);
|
||||||
|
assert(cli_process_line(&engine, "threshold") == 1);
|
||||||
|
|
||||||
|
printf("PASSED\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void) {
|
||||||
|
printf("Running CLI tests...\n\n");
|
||||||
|
test_cli_parse();
|
||||||
|
printf("\nAll CLI tests passed!\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user