mirror of
https://github.com/marcusDenslow/FERRUM.git
synced 2026-10-25 15:48:50 +01:00
- Add comprehensive diff_viewer module with split-panel interface
- Replace simple `git diff` in `gg d` command with interactive viewer
- Implement two-mode navigation system:
* File list mode: navigate files with j/k, Enter to view
* Content mode: scroll diff content with j/k, ESC to return
- Support both tracked and untracked file diffs with proper handling
- Add color-coded diff display (green additions, red deletions)
- Include terminal raw mode handling for responsive keyboard input
- Add test files for diff viewer functionality
496 lines
16 KiB
C
496 lines
16 KiB
C
/**
|
|
* diff_viewer.c
|
|
* Interactive diff viewer implementation
|
|
*/
|
|
|
|
#include "diff_viewer.h"
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <unistd.h>
|
|
#include <sys/ioctl.h>
|
|
#include <termios.h>
|
|
#include <ctype.h>
|
|
|
|
/**
|
|
* Initialize the diff viewer
|
|
*/
|
|
int init_diff_viewer(DiffViewer *viewer) {
|
|
if (!viewer) return 0;
|
|
|
|
memset(viewer, 0, sizeof(DiffViewer));
|
|
viewer->selected_file = 0;
|
|
viewer->diff_scroll_offset = 0;
|
|
viewer->current_mode = MODE_FILE_LIST; // Start in file list mode
|
|
|
|
get_terminal_size(&viewer->terminal_width, &viewer->terminal_height);
|
|
viewer->file_panel_width = viewer->terminal_width * 0.3; // 30% of screen width
|
|
|
|
return 1;
|
|
}
|
|
|
|
/**
|
|
* Get terminal size
|
|
*/
|
|
void get_terminal_size(int *width, int *height) {
|
|
struct winsize w;
|
|
ioctl(STDOUT_FILENO, TIOCGWINSZ, &w);
|
|
*width = w.ws_col;
|
|
*height = w.ws_row;
|
|
}
|
|
|
|
/**
|
|
* Get list of changed files from git
|
|
*/
|
|
int get_changed_files(DiffViewer *viewer) {
|
|
if (!viewer) return 0;
|
|
|
|
FILE *fp = popen("git status --porcelain 2>/dev/null", "r");
|
|
if (!fp) return 0;
|
|
|
|
viewer->file_count = 0;
|
|
char line[512];
|
|
|
|
while (fgets(line, sizeof(line), fp) != NULL && viewer->file_count < MAX_FILES) {
|
|
// Remove newline
|
|
char *newline = strchr(line, '\n');
|
|
if (newline) *newline = '\0';
|
|
|
|
if (strlen(line) < 3) continue;
|
|
|
|
// Parse git status format: "XY filename"
|
|
char status = line[0];
|
|
if (status == ' ') status = line[1]; // Check second column if first is space
|
|
|
|
// Skip the status characters and space
|
|
char *filename = line + 3;
|
|
|
|
// Store file info
|
|
viewer->files[viewer->file_count].status = status;
|
|
strncpy(viewer->files[viewer->file_count].filename, filename, MAX_FILENAME_LEN - 1);
|
|
viewer->files[viewer->file_count].filename[MAX_FILENAME_LEN - 1] = '\0';
|
|
|
|
viewer->file_count++;
|
|
}
|
|
|
|
pclose(fp);
|
|
return viewer->file_count;
|
|
}
|
|
|
|
/**
|
|
* Check if a file is a new untracked file
|
|
*/
|
|
int is_new_file(const char *filename) {
|
|
char cmd[1024];
|
|
snprintf(cmd, sizeof(cmd), "git ls-files --error-unmatch \"%s\" 2>/dev/null", filename);
|
|
|
|
FILE *fp = popen(cmd, "r");
|
|
if (!fp) return 1; // Assume new if we can't check
|
|
|
|
char output[256];
|
|
int is_tracked = (fgets(output, sizeof(output), fp) != NULL);
|
|
pclose(fp);
|
|
|
|
return !is_tracked; // Return 1 if not tracked (new file)
|
|
}
|
|
|
|
/**
|
|
* Load content of a new file and show all lines as additions
|
|
*/
|
|
int load_new_file_content(DiffViewer *viewer, const char *filename) {
|
|
if (!viewer || !filename) return 0;
|
|
|
|
FILE *fp = fopen(filename, "r");
|
|
if (!fp) return 0;
|
|
|
|
viewer->diff_line_count = 0;
|
|
viewer->diff_scroll_offset = 0;
|
|
|
|
// Add header line
|
|
DiffLine *header = &viewer->diff_lines[viewer->diff_line_count++];
|
|
snprintf(header->line, sizeof(header->line), "@@ New file: %s @@", filename);
|
|
header->type = '@';
|
|
header->line_number_old = 0;
|
|
header->line_number_new = 0;
|
|
|
|
char line[1024];
|
|
int line_number = 1;
|
|
|
|
while (fgets(line, sizeof(line), fp) != NULL && viewer->diff_line_count < MAX_DIFF_LINES) {
|
|
// Remove newline
|
|
char *newline_pos = strchr(line, '\n');
|
|
if (newline_pos) *newline_pos = '\0';
|
|
|
|
DiffLine *diff_line = &viewer->diff_lines[viewer->diff_line_count];
|
|
|
|
// Format as addition
|
|
snprintf(diff_line->line, sizeof(diff_line->line), "+%s", line);
|
|
diff_line->type = '+';
|
|
diff_line->line_number_old = 0;
|
|
diff_line->line_number_new = line_number++;
|
|
|
|
viewer->diff_line_count++;
|
|
}
|
|
|
|
fclose(fp);
|
|
return viewer->diff_line_count;
|
|
}
|
|
|
|
/**
|
|
* Load diff for a specific file
|
|
*/
|
|
int load_file_diff(DiffViewer *viewer, const char *filename) {
|
|
if (!viewer || !filename) return 0;
|
|
|
|
// Check if this is a new untracked file
|
|
if (is_new_file(filename)) {
|
|
return load_new_file_content(viewer, filename);
|
|
}
|
|
|
|
char cmd[1024];
|
|
snprintf(cmd, sizeof(cmd), "git diff HEAD -- \"%s\" 2>/dev/null", filename);
|
|
|
|
FILE *fp = popen(cmd, "r");
|
|
if (!fp) return 0;
|
|
|
|
viewer->diff_line_count = 0;
|
|
viewer->diff_scroll_offset = 0;
|
|
|
|
char line[1024];
|
|
int old_line = 0, new_line = 0;
|
|
|
|
while (fgets(line, sizeof(line), fp) != NULL && viewer->diff_line_count < MAX_DIFF_LINES) {
|
|
// Remove newline
|
|
char *newline_pos = strchr(line, '\n');
|
|
if (newline_pos) *newline_pos = '\0';
|
|
|
|
DiffLine *diff_line = &viewer->diff_lines[viewer->diff_line_count];
|
|
|
|
// Parse diff line
|
|
if (line[0] == '@' && line[1] == '@') {
|
|
// Hunk header: @@-old_start,old_count +new_start,new_count@@
|
|
sscanf(line, "@@-%d,%*d +%d,%*d@@", &old_line, &new_line);
|
|
old_line--; // Will be incremented before use
|
|
new_line--;
|
|
|
|
strncpy(diff_line->line, line, sizeof(diff_line->line) - 1);
|
|
diff_line->line[sizeof(diff_line->line) - 1] = '\0';
|
|
diff_line->type = '@';
|
|
diff_line->line_number_old = old_line;
|
|
diff_line->line_number_new = new_line;
|
|
} else if (line[0] == '+') {
|
|
new_line++;
|
|
strncpy(diff_line->line, line, sizeof(diff_line->line) - 1);
|
|
diff_line->line[sizeof(diff_line->line) - 1] = '\0';
|
|
diff_line->type = '+';
|
|
diff_line->line_number_old = old_line;
|
|
diff_line->line_number_new = new_line;
|
|
} else if (line[0] == '-') {
|
|
old_line++;
|
|
strncpy(diff_line->line, line, sizeof(diff_line->line) - 1);
|
|
diff_line->line[sizeof(diff_line->line) - 1] = '\0';
|
|
diff_line->type = '-';
|
|
diff_line->line_number_old = old_line;
|
|
diff_line->line_number_new = new_line;
|
|
} else if (line[0] == ' ') {
|
|
old_line++;
|
|
new_line++;
|
|
strncpy(diff_line->line, line, sizeof(diff_line->line) - 1);
|
|
diff_line->line[sizeof(diff_line->line) - 1] = '\0';
|
|
diff_line->type = ' ';
|
|
diff_line->line_number_old = old_line;
|
|
diff_line->line_number_new = new_line;
|
|
} else {
|
|
// Skip other lines (file headers, etc.)
|
|
continue;
|
|
}
|
|
|
|
viewer->diff_line_count++;
|
|
}
|
|
|
|
pclose(fp);
|
|
|
|
// If no diff content but file exists, it might be a new file added to git
|
|
if (viewer->diff_line_count == 0) {
|
|
return load_new_file_content(viewer, filename);
|
|
}
|
|
|
|
return viewer->diff_line_count;
|
|
}
|
|
|
|
/**
|
|
* Render the diff viewer interface
|
|
*/
|
|
void render_diff_viewer(DiffViewer *viewer) {
|
|
if (!viewer) return;
|
|
|
|
// Clear screen
|
|
printf("\033[2J\033[H");
|
|
|
|
// Title bar
|
|
printf(ANSI_COLOR_CYAN "Git Diff Viewer" ANSI_COLOR_RESET);
|
|
if (viewer->current_mode == MODE_FILE_LIST) {
|
|
printf(" - Use ↑/↓ to navigate files, Enter to view, q to quit\n");
|
|
} else {
|
|
printf(" - Use ↑/↓ to scroll, ESC to return to file list, q to quit\n");
|
|
}
|
|
printf("─%.0*s\n", viewer->terminal_width - 1, "────────────────────────────────────────────────────────────────────────────────");
|
|
|
|
int start_row = 2;
|
|
int available_height = viewer->terminal_height - start_row - 1;
|
|
|
|
// Render file list (left panel)
|
|
for (int i = 0; i < available_height && i < viewer->file_count; i++) {
|
|
printf("\033[%d;1H", start_row + i + 1); // Position cursor
|
|
|
|
if (i == viewer->selected_file) {
|
|
if (viewer->current_mode == MODE_FILE_LIST) {
|
|
printf(ANSI_COLOR_CYAN "► " ANSI_COLOR_RESET);
|
|
} else {
|
|
printf(ANSI_COLOR_GREEN "► " ANSI_COLOR_RESET);
|
|
}
|
|
} else {
|
|
printf(" ");
|
|
}
|
|
|
|
// Status indicator
|
|
char status = viewer->files[i].status;
|
|
if (status == 'M') {
|
|
printf(ANSI_COLOR_YELLOW "M" ANSI_COLOR_RESET);
|
|
} else if (status == 'A') {
|
|
printf(ANSI_COLOR_GREEN "A" ANSI_COLOR_RESET);
|
|
} else if (status == 'D') {
|
|
printf(ANSI_COLOR_RED "D" ANSI_COLOR_RESET);
|
|
} else {
|
|
printf("%c", status);
|
|
}
|
|
|
|
printf(" ");
|
|
|
|
// Filename (truncated to fit panel)
|
|
char truncated_name[256];
|
|
int max_name_len = viewer->file_panel_width - 6;
|
|
if ((int)strlen(viewer->files[i].filename) > max_name_len) {
|
|
strncpy(truncated_name, viewer->files[i].filename, max_name_len - 3);
|
|
truncated_name[max_name_len - 3] = '\0';
|
|
strcat(truncated_name, "...");
|
|
} else {
|
|
strcpy(truncated_name, viewer->files[i].filename);
|
|
}
|
|
|
|
printf("%-*s", max_name_len, truncated_name);
|
|
|
|
// Vertical separator
|
|
printf("│");
|
|
}
|
|
|
|
// Render diff content (right panel)
|
|
if (viewer->current_mode == MODE_FILE_CONTENT &&
|
|
viewer->file_count > 0 && viewer->selected_file < viewer->file_count) {
|
|
int diff_panel_start = viewer->file_panel_width + 1;
|
|
int diff_panel_width = viewer->terminal_width - diff_panel_start;
|
|
|
|
for (int i = 0; i < available_height &&
|
|
(i + viewer->diff_scroll_offset) < viewer->diff_line_count; i++) {
|
|
|
|
int line_idx = i + viewer->diff_scroll_offset;
|
|
DiffLine *line = &viewer->diff_lines[line_idx];
|
|
|
|
printf("\033[%d;%dH", start_row + i + 1, diff_panel_start + 1);
|
|
|
|
// Color code based on line type
|
|
if (line->type == '+') {
|
|
printf(ANSI_COLOR_GREEN);
|
|
} else if (line->type == '-') {
|
|
printf(ANSI_COLOR_RED);
|
|
} else if (line->type == '@') {
|
|
printf(ANSI_COLOR_CYAN);
|
|
}
|
|
|
|
// Truncate line to fit panel
|
|
char display_line[1024];
|
|
int max_len = diff_panel_width - 1;
|
|
if ((int)strlen(line->line) > max_len) {
|
|
strncpy(display_line, line->line, max_len - 3);
|
|
display_line[max_len - 3] = '\0';
|
|
strcat(display_line, "...");
|
|
} else {
|
|
strcpy(display_line, line->line);
|
|
}
|
|
|
|
printf("%-*s", max_len, display_line);
|
|
printf(ANSI_COLOR_RESET);
|
|
}
|
|
} else if (viewer->current_mode == MODE_FILE_LIST) {
|
|
// Show help text in the right panel when in file list mode
|
|
int diff_panel_start = viewer->file_panel_width + 1;
|
|
printf("\033[%d;%dH", start_row + 2, diff_panel_start + 3);
|
|
printf(ANSI_COLOR_YELLOW "Select a file and press Enter to view its diff" ANSI_COLOR_RESET);
|
|
}
|
|
|
|
// Status line at bottom
|
|
printf("\033[%d;1H", viewer->terminal_height);
|
|
if (viewer->file_count > 0) {
|
|
if (viewer->current_mode == MODE_FILE_LIST) {
|
|
printf("File %d/%d: %s [File List Mode]",
|
|
viewer->selected_file + 1,
|
|
viewer->file_count,
|
|
viewer->files[viewer->selected_file].filename);
|
|
} else {
|
|
printf("File %d/%d: %s [Content Mode - Line %d/%d]",
|
|
viewer->selected_file + 1,
|
|
viewer->file_count,
|
|
viewer->files[viewer->selected_file].filename,
|
|
viewer->diff_scroll_offset + 1,
|
|
viewer->diff_line_count > 0 ? viewer->diff_line_count : 1);
|
|
}
|
|
} else {
|
|
printf("No changed files found");
|
|
}
|
|
|
|
fflush(stdout);
|
|
}
|
|
|
|
/**
|
|
* Handle keyboard input for navigation
|
|
*/
|
|
int handle_diff_input(DiffViewer *viewer, char key) {
|
|
if (!viewer) return 0;
|
|
|
|
int available_height = viewer->terminal_height - 3;
|
|
|
|
switch (key) {
|
|
case 'q':
|
|
case 'Q':
|
|
return 0; // Exit
|
|
|
|
case 27: // ESC
|
|
if (viewer->current_mode == MODE_FILE_CONTENT) {
|
|
viewer->current_mode = MODE_FILE_LIST;
|
|
} else {
|
|
return 0; // Exit if already in file list mode
|
|
}
|
|
break;
|
|
|
|
case 'k':
|
|
case 65: // Up arrow
|
|
if (viewer->current_mode == MODE_FILE_LIST) {
|
|
// Navigate file list
|
|
if (viewer->selected_file > 0) {
|
|
viewer->selected_file--;
|
|
}
|
|
} else {
|
|
// Scroll diff content up
|
|
if (viewer->diff_scroll_offset > 0) {
|
|
viewer->diff_scroll_offset--;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case 'j':
|
|
case 66: // Down arrow
|
|
if (viewer->current_mode == MODE_FILE_LIST) {
|
|
// Navigate file list
|
|
if (viewer->selected_file < viewer->file_count - 1) {
|
|
viewer->selected_file++;
|
|
}
|
|
} else {
|
|
// Scroll diff content down
|
|
if (viewer->diff_line_count > available_height &&
|
|
viewer->diff_scroll_offset < viewer->diff_line_count - available_height) {
|
|
viewer->diff_scroll_offset++;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case '\n':
|
|
case '\r':
|
|
if (viewer->current_mode == MODE_FILE_LIST && viewer->file_count > 0) {
|
|
// Enter file content mode and load diff
|
|
viewer->current_mode = MODE_FILE_CONTENT;
|
|
viewer->diff_scroll_offset = 0; // Reset scroll position
|
|
load_file_diff(viewer, viewer->files[viewer->selected_file].filename);
|
|
}
|
|
break;
|
|
}
|
|
|
|
return 1; // Continue
|
|
}
|
|
|
|
/**
|
|
* Set terminal to raw mode for interactive input
|
|
*/
|
|
void set_raw_mode(struct termios *orig_termios) {
|
|
tcgetattr(STDIN_FILENO, orig_termios);
|
|
|
|
struct termios raw = *orig_termios;
|
|
raw.c_lflag &= ~(ICANON | ECHO);
|
|
raw.c_cc[VTIME] = 0;
|
|
raw.c_cc[VMIN] = 1;
|
|
|
|
tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw);
|
|
}
|
|
|
|
/**
|
|
* Restore terminal to original mode
|
|
*/
|
|
void restore_terminal_mode(struct termios *orig_termios) {
|
|
tcsetattr(STDIN_FILENO, TCSAFLUSH, orig_termios);
|
|
}
|
|
|
|
/**
|
|
* Run the interactive diff viewer
|
|
*/
|
|
int run_diff_viewer(void) {
|
|
DiffViewer viewer;
|
|
struct termios orig_termios;
|
|
|
|
if (!init_diff_viewer(&viewer)) {
|
|
printf("Failed to initialize diff viewer\n");
|
|
return 1;
|
|
}
|
|
|
|
if (get_changed_files(&viewer) == 0) {
|
|
printf("No changed files found\n");
|
|
return 1;
|
|
}
|
|
|
|
// Don't load initial file diff - start in file list mode
|
|
|
|
set_raw_mode(&orig_termios);
|
|
|
|
int running = 1;
|
|
while (running) {
|
|
render_diff_viewer(&viewer);
|
|
|
|
char c = getchar();
|
|
|
|
// Handle escape sequences for arrow keys
|
|
if (c == 27) {
|
|
char seq[3];
|
|
if (read(STDIN_FILENO, &seq[0], 1) == 1 && seq[0] == '[') {
|
|
if (read(STDIN_FILENO, &seq[1], 1) == 1) {
|
|
running = handle_diff_input(&viewer, seq[1]);
|
|
}
|
|
} else {
|
|
running = handle_diff_input(&viewer, c);
|
|
}
|
|
} else {
|
|
running = handle_diff_input(&viewer, c);
|
|
}
|
|
}
|
|
|
|
restore_terminal_mode(&orig_termios);
|
|
printf("\033[2J\033[H"); // Clear screen
|
|
|
|
cleanup_diff_viewer(&viewer);
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Clean up diff viewer resources
|
|
*/
|
|
void cleanup_diff_viewer(DiffViewer *viewer) {
|
|
// Nothing to clean up for now, but good to have for future expansion
|
|
(void)viewer;
|
|
} |