feat: add vim-like keyboard shortcuts with visual mode, marks, yank/paste, and scene navigation
Co-authored-by: aider (deepseek/deepseek-coder) <aider@aider.chat>
This commit is contained in:
363
tui.c
363
tui.c
@@ -24,6 +24,34 @@ static int selected_row = 0;
|
||||
static int selected_col = 0;
|
||||
static bool show_help = false;
|
||||
|
||||
// Modes
|
||||
typedef enum {
|
||||
MODE_NORMAL,
|
||||
MODE_VISUAL,
|
||||
MODE_MOVE
|
||||
} UIMode;
|
||||
|
||||
// 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;
|
||||
static int visual_start_col = 0;
|
||||
static int visual_end_row = 0;
|
||||
static int visual_end_col = 0;
|
||||
|
||||
// Yank buffer
|
||||
typedef struct {
|
||||
int *clip_indices;
|
||||
int count;
|
||||
} YankBuffer;
|
||||
static YankBuffer yank_buffer = {NULL, 0};
|
||||
|
||||
// Current mode
|
||||
static UIMode current_mode = MODE_NORMAL;
|
||||
|
||||
// Convert clip state to color pair
|
||||
static int state_to_color(ClipState state) {
|
||||
switch (state) {
|
||||
@@ -40,6 +68,145 @@ static int grid_to_clip_index(int row, int col) {
|
||||
return row * GRID_COLS + col;
|
||||
}
|
||||
|
||||
// Check if a cell is in the visual selection
|
||||
static bool is_in_visual_selection(int row, int col) {
|
||||
if (current_mode != MODE_VISUAL) return false;
|
||||
|
||||
int min_row = (visual_start_row < visual_end_row) ? visual_start_row : visual_end_row;
|
||||
int max_row = (visual_start_row > visual_end_row) ? visual_start_row : visual_end_row;
|
||||
int min_col = (visual_start_col < visual_end_col) ? visual_start_col : visual_end_col;
|
||||
int max_col = (visual_start_col > visual_end_col) ? visual_start_col : visual_end_col;
|
||||
|
||||
return (row >= min_row && row <= max_row && col >= min_col && col <= max_col);
|
||||
}
|
||||
|
||||
// Get all clip indices in the visual selection
|
||||
static int* get_selected_clips(int *count) {
|
||||
int min_row = (visual_start_row < visual_end_row) ? visual_start_row : visual_end_row;
|
||||
int max_row = (visual_start_row > visual_end_row) ? visual_start_row : visual_end_row;
|
||||
int min_col = (visual_start_col < visual_end_col) ? visual_start_col : visual_end_col;
|
||||
int max_col = (visual_start_col > visual_end_col) ? visual_start_col : visual_end_col;
|
||||
|
||||
int num_rows = max_row - min_row + 1;
|
||||
int num_cols = max_col - min_col + 1;
|
||||
*count = num_rows * num_cols;
|
||||
|
||||
int *clips = (int *)malloc(*count * sizeof(int));
|
||||
if (!clips) {
|
||||
*count = 0;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int idx = 0;
|
||||
for (int r = min_row; r <= max_row; r++) {
|
||||
for (int c = min_col; c <= max_col; c++) {
|
||||
clips[idx++] = grid_to_clip_index(r, c);
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
static void delete_clips(int *clip_indices, int count) {
|
||||
for (int i = 0; i < count; i++) {
|
||||
engine_reset_clip(g_engine, clip_indices[i]);
|
||||
}
|
||||
engine_process_commands(g_engine);
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
yank_buffer.clip_indices = (int *)malloc(count * sizeof(int));
|
||||
if (yank_buffer.clip_indices) {
|
||||
memcpy(yank_buffer.clip_indices, clip_indices, count * sizeof(int));
|
||||
yank_buffer.count = count;
|
||||
}
|
||||
}
|
||||
|
||||
// Paste clips (trigger them)
|
||||
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;
|
||||
int col_offset = selected_col - first_yanked_col;
|
||||
|
||||
for (int i = 0; i < yank_buffer.count; i++) {
|
||||
int orig_row = yank_buffer.clip_indices[i] / GRID_COLS;
|
||||
int orig_col = yank_buffer.clip_indices[i] % GRID_COLS;
|
||||
|
||||
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);
|
||||
engine_trigger_clip(g_engine, new_clip_idx);
|
||||
}
|
||||
}
|
||||
engine_process_commands(g_engine);
|
||||
}
|
||||
|
||||
// Set a mark at current position
|
||||
static void set_mark(char mark_char) {
|
||||
if (mark_char >= 'a' && mark_char <= 'z') {
|
||||
int idx = mark_char - 'a';
|
||||
marks[idx] = grid_to_clip_index(selected_row, selected_col);
|
||||
}
|
||||
}
|
||||
|
||||
// Go to a mark
|
||||
static void go_to_mark(char mark_char) {
|
||||
if (mark_char >= 'a' && mark_char <= 'z') {
|
||||
int idx = mark_char - 'a';
|
||||
int clip_idx = marks[idx];
|
||||
if (clip_idx >= 0 && clip_idx < GRID_ROWS * GRID_COLS) {
|
||||
selected_row = clip_idx / GRID_COLS;
|
||||
selected_col = clip_idx % GRID_COLS;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
engine_process_commands(g_engine);
|
||||
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);
|
||||
engine_process_commands(g_engine);
|
||||
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);
|
||||
@@ -51,6 +218,9 @@ static void draw_cell(int row, int col, bool selected) {
|
||||
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
|
||||
}
|
||||
|
||||
attron(COLOR_PAIR(color));
|
||||
@@ -109,6 +279,17 @@ static void draw_grid(void) {
|
||||
quantize_mode_to_string((QuantizeMode)atomic_load(&g_engine->quantize_mode_atomic)),
|
||||
(unsigned int)atomic_load(&g_engine->quantize_threshold_atomic));
|
||||
|
||||
// Draw mode indicator
|
||||
const char *mode_str;
|
||||
switch (current_mode) {
|
||||
case MODE_NORMAL: mode_str = "NORMAL"; break;
|
||||
case MODE_VISUAL: mode_str = "VISUAL"; break;
|
||||
case MODE_MOVE: mode_str = "MOVE"; break;
|
||||
default: mode_str = "?"; break;
|
||||
}
|
||||
mvprintw(GRID_ROWS * CELL_HEIGHT + 3, 0,
|
||||
"Mode: %s", mode_str);
|
||||
|
||||
// Draw help if active
|
||||
if (show_help) {
|
||||
attron(COLOR_PAIR(COLOR_HELP));
|
||||
@@ -236,15 +417,113 @@ void tui_run(Engine *engine) {
|
||||
|
||||
g_engine = engine;
|
||||
|
||||
// Initialize marks
|
||||
for (int i = 0; i < MAX_MARKS; i++) {
|
||||
marks[i] = -1;
|
||||
}
|
||||
|
||||
draw_grid();
|
||||
|
||||
while (1) {
|
||||
int ch = getch();
|
||||
if (ch == ERR) {
|
||||
// getch returned error (e.g., signal interrupted)
|
||||
break;
|
||||
}
|
||||
|
||||
// Handle mode-specific input
|
||||
if (current_mode == MODE_MOVE) {
|
||||
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 '\n':
|
||||
case '\r':
|
||||
case 27: // Escape
|
||||
current_mode = MODE_NORMAL;
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
draw_grid();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (current_mode == MODE_VISUAL) {
|
||||
switch (ch) {
|
||||
case 'h':
|
||||
case KEY_LEFT:
|
||||
visual_end_col = (visual_end_col - 1 + GRID_COLS) % GRID_COLS;
|
||||
break;
|
||||
|
||||
case 'j':
|
||||
case KEY_DOWN:
|
||||
visual_end_row = (visual_end_row + 1) % GRID_ROWS;
|
||||
break;
|
||||
|
||||
case 'k':
|
||||
case KEY_UP:
|
||||
visual_end_row = (visual_end_row - 1 + GRID_ROWS) % GRID_ROWS;
|
||||
break;
|
||||
|
||||
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);
|
||||
if (clips) {
|
||||
delete_clips(clips, count);
|
||||
free(clips);
|
||||
}
|
||||
current_mode = MODE_NORMAL;
|
||||
break;
|
||||
}
|
||||
|
||||
case 'y': {
|
||||
int count;
|
||||
int *clips = get_selected_clips(&count);
|
||||
if (clips) {
|
||||
yank_clips(clips, count);
|
||||
free(clips);
|
||||
}
|
||||
current_mode = MODE_NORMAL;
|
||||
break;
|
||||
}
|
||||
|
||||
case 27: // Escape
|
||||
current_mode = MODE_NORMAL;
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
draw_grid();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Normal mode
|
||||
switch (ch) {
|
||||
case 'h':
|
||||
case KEY_LEFT:
|
||||
@@ -273,13 +552,27 @@ void tui_run(Engine *engine) {
|
||||
break;
|
||||
}
|
||||
|
||||
case 'r': {
|
||||
case 'd': {
|
||||
// Delete (reset) current clip
|
||||
int clip_idx = grid_to_clip_index(selected_row, selected_col);
|
||||
engine_reset_clip(engine, clip_idx);
|
||||
engine_process_commands(engine);
|
||||
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
|
||||
paste_clips();
|
||||
break;
|
||||
}
|
||||
|
||||
case 's': {
|
||||
// Trigger scene for current row
|
||||
engine_trigger_scene(engine, selected_row);
|
||||
@@ -332,11 +625,70 @@ void tui_run(Engine *engine) {
|
||||
show_help = !show_help;
|
||||
break;
|
||||
|
||||
case 'v': {
|
||||
// Enter visual mode
|
||||
current_mode = MODE_VISUAL;
|
||||
visual_start_row = selected_row;
|
||||
visual_start_col = selected_col;
|
||||
visual_end_row = selected_row;
|
||||
visual_end_col = selected_col;
|
||||
break;
|
||||
}
|
||||
|
||||
case 'V': {
|
||||
// Select entire line
|
||||
current_mode = MODE_VISUAL;
|
||||
visual_start_row = selected_row;
|
||||
visual_start_col = 0;
|
||||
visual_end_row = selected_row;
|
||||
visual_end_col = GRID_COLS - 1;
|
||||
break;
|
||||
}
|
||||
|
||||
case 'm': {
|
||||
// Enter move mode
|
||||
current_mode = MODE_MOVE;
|
||||
break;
|
||||
}
|
||||
|
||||
case 'N': {
|
||||
// Play next scene
|
||||
play_next_scene();
|
||||
break;
|
||||
}
|
||||
|
||||
case 'P': {
|
||||
// Play previous scene
|
||||
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);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 27: // Escape key
|
||||
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);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -345,6 +697,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;
|
||||
}
|
||||
|
||||
// Restore terminal settings
|
||||
curs_set(1);
|
||||
endwin();
|
||||
|
||||
Reference in New Issue
Block a user