diff --git a/another_test.txt b/another_test.txt new file mode 100644 index 0000000..7410249 --- /dev/null +++ b/another_test.txt @@ -0,0 +1 @@ +This is another test file for the diff viewer diff --git a/builtins.c b/builtins.c index 77899d3..399a877 100644 --- a/builtins.c +++ b/builtins.c @@ -5,6 +5,7 @@ #include "builtins.h" #include "common.h" +#include "diff_viewer.h" #include "filters.h" #include "fzf_native.h" #include "git_integration.h" @@ -1113,7 +1114,7 @@ int lsh_gg(char **args) { } else if (strcmp(args[1], "l") == 0) { system("git log --oneline -10"); } else if (strcmp(args[1], "d") == 0) { - system("git diff"); + run_diff_viewer(); } else if (strcmp(args[1], "b") == 0) { system("git branch"); } else if (strcmp(args[1], "ch") == 0) { diff --git a/builtins.o b/builtins.o index 65ec758..b9a76ae 100644 Binary files a/builtins.o and b/builtins.o differ diff --git a/diff_viewer.c b/diff_viewer.c new file mode 100644 index 0000000..da16c32 --- /dev/null +++ b/diff_viewer.c @@ -0,0 +1,496 @@ +/** + * diff_viewer.c + * Interactive diff viewer implementation + */ + +#include "diff_viewer.h" +#include +#include +#include +#include +#include +#include +#include + +/** + * 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; +} \ No newline at end of file diff --git a/diff_viewer.h b/diff_viewer.h new file mode 100644 index 0000000..a054834 --- /dev/null +++ b/diff_viewer.h @@ -0,0 +1,106 @@ +/** + * diff_viewer.h + * Interactive diff viewer for git changes + */ + +#ifndef DIFF_VIEWER_H +#define DIFF_VIEWER_H + +#include "common.h" +#include + +#define MAX_FILES 100 +#define MAX_FILENAME_LEN 256 +#define MAX_DIFF_LINES 1000 + +typedef struct { + char filename[MAX_FILENAME_LEN]; + char status; // 'M' = modified, 'A' = added, 'D' = deleted +} ChangedFile; + +typedef struct { + char line[512]; + char type; // '+' = addition, '-' = deletion, ' ' = context + int line_number_old; + int line_number_new; +} DiffLine; + +typedef enum { + MODE_FILE_LIST, + MODE_FILE_CONTENT +} ViewMode; + +typedef struct { + ChangedFile files[MAX_FILES]; + int file_count; + int selected_file; + DiffLine diff_lines[MAX_DIFF_LINES]; + int diff_line_count; + int diff_scroll_offset; + int terminal_width; + int terminal_height; + int file_panel_width; + ViewMode current_mode; +} DiffViewer; + +/** + * Initialize the diff viewer + */ +int init_diff_viewer(DiffViewer *viewer); + +/** + * Get list of changed files from git + */ +int get_changed_files(DiffViewer *viewer); + +/** + * Check if a file is a new untracked file + */ +int is_new_file(const char *filename); + +/** + * Load content of a new file and show all lines as additions + */ +int load_new_file_content(DiffViewer *viewer, const char *filename); + +/** + * Load diff for a specific file + */ +int load_file_diff(DiffViewer *viewer, const char *filename); + +/** + * Render the diff viewer interface + */ +void render_diff_viewer(DiffViewer *viewer); + +/** + * Handle keyboard input for navigation + */ +int handle_diff_input(DiffViewer *viewer, char key); + +/** + * Run the interactive diff viewer + */ +int run_diff_viewer(void); + +/** + * Clean up diff viewer resources + */ +void cleanup_diff_viewer(DiffViewer *viewer); + +/** + * Get terminal size + */ +void get_terminal_size(int *width, int *height); + +/** + * Set terminal to raw mode for interactive input + */ +void set_raw_mode(struct termios *orig_termios); + +/** + * Restore terminal to original mode + */ +void restore_terminal_mode(struct termios *orig_termios); + +#endif // DIFF_VIEWER_H \ No newline at end of file diff --git a/diff_viewer.o b/diff_viewer.o new file mode 100644 index 0000000..d228ea7 Binary files /dev/null and b/diff_viewer.o differ diff --git a/git_integration.o b/git_integration.o index 435fca0..df4ac64 100644 Binary files a/git_integration.o and b/git_integration.o differ diff --git a/line_reader.o b/line_reader.o index ce74f9b..39249f5 100644 Binary files a/line_reader.o and b/line_reader.o differ diff --git a/shell b/shell index ed2ba6c..7d8ffb3 100755 Binary files a/shell and b/shell differ diff --git a/shell.o b/shell.o index 6cb9719..80237fb 100644 Binary files a/shell.o and b/shell.o differ diff --git a/test.txt b/test.txt index 401006f..861fb62 100644 --- a/test.txt +++ b/test.txt @@ -3,3 +3,5 @@ This is a test file for the grep command It contains multiple lines Boyer-Moore algorithm testing the grep +This is a test change +Modified test.txt content diff --git a/test_file_for_diff.txt b/test_file_for_diff.txt new file mode 100644 index 0000000..bdb3efc --- /dev/null +++ b/test_file_for_diff.txt @@ -0,0 +1,6 @@ +// New test file +#include +int main() { + printf("Hello world\n"); + return 0; +}