From ae306fee97f90c0728f2a7c928b384e526208654 Mon Sep 17 00:00:00 2001 From: marcusDenslow Date: Fri, 6 Jun 2025 21:11:40 +0200 Subject: [PATCH] changing name --- ncurses_diff_viewer.c | 3587 ++++++++++++++++++++++------------------- ncurses_diff_viewer.o | Bin 68136 -> 68360 bytes shell | Bin 430816 -> 430584 bytes 3 files changed, 1890 insertions(+), 1697 deletions(-) diff --git a/ncurses_diff_viewer.c b/ncurses_diff_viewer.c index a82cc6d..b5210bf 100644 --- a/ncurses_diff_viewer.c +++ b/ncurses_diff_viewer.c @@ -4,1901 +4,2094 @@ */ #include "ncurses_diff_viewer.h" +#include #include #include #include -#include #include #include -#include +#include /** * Initialize the ncurses diff viewer */ int init_ncurses_diff_viewer(NCursesDiffViewer *viewer) { - if (!viewer) return 0; - - memset(viewer, 0, sizeof(NCursesDiffViewer)); - viewer->selected_file = 0; - viewer->file_scroll_offset = 0; - viewer->current_mode = NCURSES_MODE_FILE_LIST; - viewer->sync_status = SYNC_STATUS_IDLE; - viewer->spinner_frame = 0; - viewer->last_sync_time = time(NULL); - viewer->animation_frame = 0; - viewer->text_char_count = 0; - - // Set locale for Unicode support - setlocale(LC_ALL, ""); - - // Initialize ncurses - initscr(); - cbreak(); - noecho(); - keypad(stdscr, TRUE); - nodelay(stdscr, TRUE); // Make getch() non-blocking - curs_set(0); // Hide cursor to prevent flickering - - // Enable colors - if (has_colors()) { - start_color(); - init_pair(1, COLOR_GREEN, COLOR_BLACK); // Additions - init_pair(2, COLOR_RED, COLOR_BLACK); // Deletions - init_pair(3, COLOR_CYAN, COLOR_BLACK); // Headers - init_pair(4, COLOR_YELLOW, COLOR_BLACK); // Selected - init_pair(5, COLOR_WHITE, COLOR_BLUE); // Highlighted selection - } - - getmaxyx(stdscr, viewer->terminal_height, viewer->terminal_width); - viewer->file_panel_width = viewer->terminal_width * 0.4; // 40% of screen width - viewer->status_bar_height = viewer->terminal_height * 0.05; // 5% of screen height - if (viewer->status_bar_height < 1) viewer->status_bar_height = 1; // Minimum 1 line - - int available_height = viewer->terminal_height - 1 - viewer->status_bar_height; // Subtract top bar and status bar - viewer->file_panel_height = available_height * 0.6; // 60% of available height - viewer->commit_panel_height = available_height - viewer->file_panel_height - 1; // Rest of height - - // Position status bar right after the main content - int status_bar_y = 1 + available_height; - - // Create four windows - viewer->file_list_win = newwin(viewer->file_panel_height, viewer->file_panel_width, 1, 0); - viewer->commit_list_win = newwin(viewer->commit_panel_height, viewer->file_panel_width, - 1 + viewer->file_panel_height + 1, 0); - viewer->file_content_win = newwin(available_height, - viewer->terminal_width - viewer->file_panel_width - 1, - 1, viewer->file_panel_width + 1); - viewer->status_bar_win = newwin(viewer->status_bar_height, viewer->terminal_width, - status_bar_y, 0); - - if (!viewer->file_list_win || !viewer->file_content_win || !viewer->commit_list_win || !viewer->status_bar_win) { - cleanup_ncurses_diff_viewer(viewer); - return 0; - } - - return 1; + if (!viewer) + return 0; + + memset(viewer, 0, sizeof(NCursesDiffViewer)); + viewer->selected_file = 0; + viewer->file_scroll_offset = 0; + viewer->current_mode = NCURSES_MODE_FILE_LIST; + viewer->sync_status = SYNC_STATUS_IDLE; + viewer->spinner_frame = 0; + viewer->last_sync_time = time(NULL); + viewer->animation_frame = 0; + viewer->text_char_count = 0; + + // Set locale for Unicode support + setlocale(LC_ALL, ""); + + // Initialize ncurses + initscr(); + cbreak(); + noecho(); + keypad(stdscr, TRUE); + nodelay(stdscr, TRUE); // Make getch() non-blocking + curs_set(0); // Hide cursor to prevent flickering + + // Enable colors + if (has_colors()) { + start_color(); + init_pair(1, COLOR_GREEN, COLOR_BLACK); // Additions + init_pair(2, COLOR_RED, COLOR_BLACK); // Deletions + init_pair(3, COLOR_CYAN, COLOR_BLACK); // Headers + init_pair(4, COLOR_YELLOW, COLOR_BLACK); // Selected + init_pair(5, COLOR_WHITE, COLOR_BLUE); // Highlighted selection + } + + getmaxyx(stdscr, viewer->terminal_height, viewer->terminal_width); + viewer->file_panel_width = + viewer->terminal_width * 0.4; // 40% of screen width + viewer->status_bar_height = + viewer->terminal_height * 0.05; // 5% of screen height + if (viewer->status_bar_height < 1) + viewer->status_bar_height = 1; // Minimum 1 line + + int available_height = + viewer->terminal_height - 1 - + viewer->status_bar_height; // Subtract top bar and status bar + viewer->file_panel_height = available_height * 0.6; // 60% of available height + viewer->commit_panel_height = + available_height - viewer->file_panel_height - 1; // Rest of height + + // Position status bar right after the main content + int status_bar_y = 1 + available_height; + + // Create four windows + viewer->file_list_win = + newwin(viewer->file_panel_height, viewer->file_panel_width, 1, 0); + viewer->commit_list_win = + newwin(viewer->commit_panel_height, viewer->file_panel_width, + 1 + viewer->file_panel_height + 1, 0); + viewer->file_content_win = newwin( + available_height, viewer->terminal_width - viewer->file_panel_width - 1, + 1, viewer->file_panel_width + 1); + viewer->status_bar_win = newwin(viewer->status_bar_height, + viewer->terminal_width, status_bar_y, 0); + + if (!viewer->file_list_win || !viewer->file_content_win || + !viewer->commit_list_win || !viewer->status_bar_win) { + cleanup_ncurses_diff_viewer(viewer); + return 0; + } + + return 1; } /** * Get list of changed files from git */ int get_ncurses_changed_files(NCursesDiffViewer *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; - viewer->files[viewer->file_count].marked_for_commit = 0; // Not marked by default - 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; + 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; + viewer->files[viewer->file_count].marked_for_commit = + 0; // Not marked by default + 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; } /** * Create temporary file with current working version */ int create_temp_file_with_changes(const char *filename, char *temp_path) { - snprintf(temp_path, 256, "/tmp/shell_diff_current_%d", getpid()); - - char cmd[1024]; - snprintf(cmd, sizeof(cmd), "cp \"%s\" \"%s\" 2>/dev/null", filename, temp_path); - - return (system(cmd) == 0); + snprintf(temp_path, 256, "/tmp/shell_diff_current_%d", getpid()); + + char cmd[1024]; + snprintf(cmd, sizeof(cmd), "cp \"%s\" \"%s\" 2>/dev/null", filename, + temp_path); + + return (system(cmd) == 0); } /** * Create temporary file with git HEAD version */ int create_temp_file_git_version(const char *filename, char *temp_path) { - snprintf(temp_path, 256, "/tmp/shell_diff_git_%d", getpid()); - - char cmd[1024]; - snprintf(cmd, sizeof(cmd), "git show HEAD:\"%s\" > \"%s\" 2>/dev/null", filename, temp_path); - - return (system(cmd) == 0); + snprintf(temp_path, 256, "/tmp/shell_diff_git_%d", getpid()); + + char cmd[1024]; + snprintf(cmd, sizeof(cmd), "git show HEAD:\"%s\" > \"%s\" 2>/dev/null", + filename, temp_path); + + return (system(cmd) == 0); } /** * Check if a file is a new untracked file */ int is_ncurses_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) + 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 diff context with highlighting (like lazygit) */ int load_full_file_with_diff(NCursesDiffViewer *viewer, const char *filename) { - if (!viewer || !filename) return 0; - - viewer->file_line_count = 0; - viewer->file_scroll_offset = 0; - - // Check if this is a new file - if (is_ncurses_new_file(filename)) { - // For new files, show first 50 lines as additions - FILE *fp = fopen(filename, "r"); - if (!fp) return 0; - - char line[1024]; - int line_count = 0; - while (fgets(line, sizeof(line), fp) != NULL && - viewer->file_line_count < MAX_FULL_FILE_LINES && - line_count < 50) { - // Remove newline - char *newline_pos = strchr(line, '\n'); - if (newline_pos) *newline_pos = '\0'; - - NCursesFileLine *file_line = &viewer->file_lines[viewer->file_line_count]; - snprintf(file_line->line, sizeof(file_line->line), "+%s", line); - file_line->type = '+'; - file_line->is_diff_line = 1; - - viewer->file_line_count++; - line_count++; - } - - fclose(fp); - return viewer->file_line_count; + if (!viewer || !filename) + return 0; + + viewer->file_line_count = 0; + viewer->file_scroll_offset = 0; + + // Check if this is a new file + if (is_ncurses_new_file(filename)) { + // For new files, show first 50 lines as additions + FILE *fp = fopen(filename, "r"); + if (!fp) + return 0; + + char line[1024]; + int line_count = 0; + while (fgets(line, sizeof(line), fp) != NULL && + viewer->file_line_count < MAX_FULL_FILE_LINES && line_count < 50) { + // Remove newline + char *newline_pos = strchr(line, '\n'); + if (newline_pos) + *newline_pos = '\0'; + + NCursesFileLine *file_line = &viewer->file_lines[viewer->file_line_count]; + snprintf(file_line->line, sizeof(file_line->line), "+%s", line); + file_line->type = '+'; + file_line->is_diff_line = 1; + + viewer->file_line_count++; + line_count++; } - - // Use git diff to get only the changed context (like lazygit) - char cmd[1024]; - snprintf(cmd, sizeof(cmd), "git diff HEAD \"%s\" 2>/dev/null", filename); - - FILE *diff_fp = popen(cmd, "r"); - if (!diff_fp) return 0; - - char diff_line[1024]; - int found_changes = 0; - - while (fgets(diff_line, sizeof(diff_line), diff_fp) != NULL && - viewer->file_line_count < MAX_FULL_FILE_LINES) { - // Remove newline - char *newline_pos = strchr(diff_line, '\n'); - if (newline_pos) *newline_pos = '\0'; - - // Skip file headers - if (strncmp(diff_line, "diff --git", 10) == 0 || - strncmp(diff_line, "index ", 6) == 0 || - strncmp(diff_line, "--- ", 4) == 0 || - strncmp(diff_line, "+++ ", 4) == 0) { - continue; - } - - // Process hunk headers and content - if (diff_line[0] == '@' && diff_line[1] == '@') { - // Hunk header - NCursesFileLine *file_line = &viewer->file_lines[viewer->file_line_count]; - strncpy(file_line->line, diff_line, sizeof(file_line->line) - 1); - file_line->line[sizeof(file_line->line) - 1] = '\0'; - file_line->type = '@'; - file_line->is_diff_line = 0; - viewer->file_line_count++; - found_changes = 1; - } else if (diff_line[0] == '+') { - // Added line - NCursesFileLine *file_line = &viewer->file_lines[viewer->file_line_count]; - strncpy(file_line->line, diff_line, sizeof(file_line->line) - 1); - file_line->line[sizeof(file_line->line) - 1] = '\0'; - file_line->type = '+'; - file_line->is_diff_line = 1; - viewer->file_line_count++; - found_changes = 1; - } else if (diff_line[0] == '-') { - // Removed line - NCursesFileLine *file_line = &viewer->file_lines[viewer->file_line_count]; - strncpy(file_line->line, diff_line, sizeof(file_line->line) - 1); - file_line->line[sizeof(file_line->line) - 1] = '\0'; - file_line->type = '-'; - file_line->is_diff_line = 1; - viewer->file_line_count++; - found_changes = 1; - } else if (diff_line[0] == ' ') { - // Context line (unchanged) - NCursesFileLine *file_line = &viewer->file_lines[viewer->file_line_count]; - strncpy(file_line->line, diff_line, sizeof(file_line->line) - 1); - file_line->line[sizeof(file_line->line) - 1] = '\0'; - file_line->type = ' '; - file_line->is_diff_line = 0; - viewer->file_line_count++; - } - } - - pclose(diff_fp); - - // If no changes were found, show a message - if (!found_changes) { - NCursesFileLine *file_line = &viewer->file_lines[viewer->file_line_count]; - strncpy(file_line->line, "No changes in this file", sizeof(file_line->line) - 1); - file_line->line[sizeof(file_line->line) - 1] = '\0'; - file_line->type = ' '; - file_line->is_diff_line = 0; - viewer->file_line_count++; - } - + + fclose(fp); return viewer->file_line_count; + } + + // Use git diff to get only the changed context (like lazygit) + char cmd[1024]; + snprintf(cmd, sizeof(cmd), "git diff HEAD \"%s\" 2>/dev/null", filename); + + FILE *diff_fp = popen(cmd, "r"); + if (!diff_fp) + return 0; + + char diff_line[1024]; + int found_changes = 0; + + while (fgets(diff_line, sizeof(diff_line), diff_fp) != NULL && + viewer->file_line_count < MAX_FULL_FILE_LINES) { + // Remove newline + char *newline_pos = strchr(diff_line, '\n'); + if (newline_pos) + *newline_pos = '\0'; + + // Skip file headers + if (strncmp(diff_line, "diff --git", 10) == 0 || + strncmp(diff_line, "index ", 6) == 0 || + strncmp(diff_line, "--- ", 4) == 0 || + strncmp(diff_line, "+++ ", 4) == 0) { + continue; + } + + // Process hunk headers and content + if (diff_line[0] == '@' && diff_line[1] == '@') { + // Hunk header + NCursesFileLine *file_line = &viewer->file_lines[viewer->file_line_count]; + strncpy(file_line->line, diff_line, sizeof(file_line->line) - 1); + file_line->line[sizeof(file_line->line) - 1] = '\0'; + file_line->type = '@'; + file_line->is_diff_line = 0; + viewer->file_line_count++; + found_changes = 1; + } else if (diff_line[0] == '+') { + // Added line + NCursesFileLine *file_line = &viewer->file_lines[viewer->file_line_count]; + strncpy(file_line->line, diff_line, sizeof(file_line->line) - 1); + file_line->line[sizeof(file_line->line) - 1] = '\0'; + file_line->type = '+'; + file_line->is_diff_line = 1; + viewer->file_line_count++; + found_changes = 1; + } else if (diff_line[0] == '-') { + // Removed line + NCursesFileLine *file_line = &viewer->file_lines[viewer->file_line_count]; + strncpy(file_line->line, diff_line, sizeof(file_line->line) - 1); + file_line->line[sizeof(file_line->line) - 1] = '\0'; + file_line->type = '-'; + file_line->is_diff_line = 1; + viewer->file_line_count++; + found_changes = 1; + } else if (diff_line[0] == ' ') { + // Context line (unchanged) + NCursesFileLine *file_line = &viewer->file_lines[viewer->file_line_count]; + strncpy(file_line->line, diff_line, sizeof(file_line->line) - 1); + file_line->line[sizeof(file_line->line) - 1] = '\0'; + file_line->type = ' '; + file_line->is_diff_line = 0; + viewer->file_line_count++; + } + } + + pclose(diff_fp); + + // If no changes were found, show a message + if (!found_changes) { + NCursesFileLine *file_line = &viewer->file_lines[viewer->file_line_count]; + strncpy(file_line->line, "No changes in this file", + sizeof(file_line->line) - 1); + file_line->line[sizeof(file_line->line) - 1] = '\0'; + file_line->type = ' '; + file_line->is_diff_line = 0; + viewer->file_line_count++; + } + + return viewer->file_line_count; } /** * Draw a box with rounded corners */ void draw_rounded_box(WINDOW *win) { - if (!win) return; - - int height, width; - getmaxyx(win, height, width); - - // Draw horizontal lines - for (int x = 1; x < width - 1; x++) { - mvwaddch(win, 0, x, ACS_HLINE); - mvwaddch(win, height - 1, x, ACS_HLINE); - } - - // Draw vertical lines - for (int y = 1; y < height - 1; y++) { - mvwaddch(win, y, 0, ACS_VLINE); - mvwaddch(win, y, width - 1, ACS_VLINE); - } - - // Draw rounded corners - mvwaddch(win, 0, 0, ACS_ULCORNER); - mvwaddch(win, 0, width - 1, ACS_URCORNER); - mvwaddch(win, height - 1, 0, ACS_LLCORNER); - mvwaddch(win, height - 1, width - 1, ACS_LRCORNER); + if (!win) + return; + + int height, width; + getmaxyx(win, height, width); + + // Draw horizontal lines + for (int x = 1; x < width - 1; x++) { + mvwaddch(win, 0, x, ACS_HLINE); + mvwaddch(win, height - 1, x, ACS_HLINE); + } + + // Draw vertical lines + for (int y = 1; y < height - 1; y++) { + mvwaddch(win, y, 0, ACS_VLINE); + mvwaddch(win, y, width - 1, ACS_VLINE); + } + + // Draw rounded corners + mvwaddch(win, 0, 0, ACS_ULCORNER); + mvwaddch(win, 0, width - 1, ACS_URCORNER); + mvwaddch(win, height - 1, 0, ACS_LLCORNER); + mvwaddch(win, height - 1, width - 1, ACS_LRCORNER); } /** * Get commit history */ int get_commit_history(NCursesDiffViewer *viewer) { - if (!viewer) return 0; - - FILE *fp = popen("git log --oneline -20 --format=\"%h|%an|%s\" 2>/dev/null", "r"); - if (!fp) return 0; - - viewer->commit_count = 0; - char line[512]; - - // Get list of unpushed commits first - char unpushed_hashes[MAX_COMMITS][16]; - int unpushed_count = 0; - - FILE *unpushed_fp = popen("git log origin/HEAD..HEAD --format=\"%h\" 2>/dev/null", "r"); + if (!viewer) + return 0; + + FILE *fp = + popen("git log --oneline -20 --format=\"%h|%an|%s\" 2>/dev/null", "r"); + if (!fp) + return 0; + + viewer->commit_count = 0; + char line[512]; + + // Get list of unpushed commits first + char unpushed_hashes[MAX_COMMITS][16]; + int unpushed_count = 0; + + FILE *unpushed_fp = + popen("git log origin/HEAD..HEAD --format=\"%h\" 2>/dev/null", "r"); + if (unpushed_fp) { + while (fgets(line, sizeof(line), unpushed_fp) != NULL && + unpushed_count < MAX_COMMITS) { + char *newline = strchr(line, '\n'); + if (newline) + *newline = '\0'; + strncpy(unpushed_hashes[unpushed_count], line, + sizeof(unpushed_hashes[unpushed_count]) - 1); + unpushed_hashes[unpushed_count] + [sizeof(unpushed_hashes[unpushed_count]) - 1] = '\0'; + unpushed_count++; + } + pclose(unpushed_fp); + } + + // If origin/HEAD doesn't exist, try origin/main and origin/master + if (unpushed_count == 0) { + unpushed_fp = + popen("git log origin/main..HEAD --format=\"%h\" 2>/dev/null", "r"); if (unpushed_fp) { - while (fgets(line, sizeof(line), unpushed_fp) != NULL && unpushed_count < MAX_COMMITS) { - char *newline = strchr(line, '\n'); - if (newline) *newline = '\0'; - strncpy(unpushed_hashes[unpushed_count], line, sizeof(unpushed_hashes[unpushed_count]) - 1); - unpushed_hashes[unpushed_count][sizeof(unpushed_hashes[unpushed_count]) - 1] = '\0'; - unpushed_count++; - } - pclose(unpushed_fp); - } - - // If origin/HEAD doesn't exist, try origin/main and origin/master - if (unpushed_count == 0) { - unpushed_fp = popen("git log origin/main..HEAD --format=\"%h\" 2>/dev/null", "r"); - if (unpushed_fp) { - while (fgets(line, sizeof(line), unpushed_fp) != NULL && unpushed_count < MAX_COMMITS) { - char *newline = strchr(line, '\n'); - if (newline) *newline = '\0'; - strncpy(unpushed_hashes[unpushed_count], line, sizeof(unpushed_hashes[unpushed_count]) - 1); - unpushed_hashes[unpushed_count][sizeof(unpushed_hashes[unpushed_count]) - 1] = '\0'; - unpushed_count++; - } - pclose(unpushed_fp); - } - } - - if (unpushed_count == 0) { - unpushed_fp = popen("git log origin/master..HEAD --format=\"%h\" 2>/dev/null", "r"); - if (unpushed_fp) { - while (fgets(line, sizeof(line), unpushed_fp) != NULL && unpushed_count < MAX_COMMITS) { - char *newline = strchr(line, '\n'); - if (newline) *newline = '\0'; - strncpy(unpushed_hashes[unpushed_count], line, sizeof(unpushed_hashes[unpushed_count]) - 1); - unpushed_hashes[unpushed_count][sizeof(unpushed_hashes[unpushed_count]) - 1] = '\0'; - unpushed_count++; - } - pclose(unpushed_fp); - } - } - - while (fgets(line, sizeof(line), fp) != NULL && viewer->commit_count < MAX_COMMITS) { - // Remove newline + while (fgets(line, sizeof(line), unpushed_fp) != NULL && + unpushed_count < MAX_COMMITS) { char *newline = strchr(line, '\n'); - if (newline) *newline = '\0'; - - // Parse format: hash|author|title - char *hash = strtok(line, "|"); - char *author = strtok(NULL, "|"); - char *title = strtok(NULL, "|"); - - if (hash && author && title) { - // Store commit hash (first 7 chars) - strncpy(viewer->commits[viewer->commit_count].hash, hash, sizeof(viewer->commits[viewer->commit_count].hash) - 1); - viewer->commits[viewer->commit_count].hash[sizeof(viewer->commits[viewer->commit_count].hash) - 1] = '\0'; - - // Store first two letters of author name - viewer->commits[viewer->commit_count].author_initials[0] = author[0] ? author[0] : '?'; - viewer->commits[viewer->commit_count].author_initials[1] = author[1] ? author[1] : '?'; - viewer->commits[viewer->commit_count].author_initials[2] = '\0'; - - // Store title - strncpy(viewer->commits[viewer->commit_count].title, title, MAX_COMMIT_TITLE_LEN - 1); - viewer->commits[viewer->commit_count].title[MAX_COMMIT_TITLE_LEN - 1] = '\0'; - - // Check if this commit is in the unpushed list - viewer->commits[viewer->commit_count].is_pushed = 1; // Default to pushed - for (int i = 0; i < unpushed_count; i++) { - if (strcmp(viewer->commits[viewer->commit_count].hash, unpushed_hashes[i]) == 0) { - viewer->commits[viewer->commit_count].is_pushed = 0; - break; - } - } - - viewer->commit_count++; - } + if (newline) + *newline = '\0'; + strncpy(unpushed_hashes[unpushed_count], line, + sizeof(unpushed_hashes[unpushed_count]) - 1); + unpushed_hashes[unpushed_count] + [sizeof(unpushed_hashes[unpushed_count]) - 1] = '\0'; + unpushed_count++; + } + pclose(unpushed_fp); } - - pclose(fp); - return viewer->commit_count; + } + + if (unpushed_count == 0) { + unpushed_fp = + popen("git log origin/master..HEAD --format=\"%h\" 2>/dev/null", "r"); + if (unpushed_fp) { + while (fgets(line, sizeof(line), unpushed_fp) != NULL && + unpushed_count < MAX_COMMITS) { + char *newline = strchr(line, '\n'); + if (newline) + *newline = '\0'; + strncpy(unpushed_hashes[unpushed_count], line, + sizeof(unpushed_hashes[unpushed_count]) - 1); + unpushed_hashes[unpushed_count] + [sizeof(unpushed_hashes[unpushed_count]) - 1] = '\0'; + unpushed_count++; + } + pclose(unpushed_fp); + } + } + + while (fgets(line, sizeof(line), fp) != NULL && + viewer->commit_count < MAX_COMMITS) { + // Remove newline + char *newline = strchr(line, '\n'); + if (newline) + *newline = '\0'; + + // Parse format: hash|author|title + char *hash = strtok(line, "|"); + char *author = strtok(NULL, "|"); + char *title = strtok(NULL, "|"); + + if (hash && author && title) { + // Store commit hash (first 7 chars) + strncpy(viewer->commits[viewer->commit_count].hash, hash, + sizeof(viewer->commits[viewer->commit_count].hash) - 1); + viewer->commits[viewer->commit_count] + .hash[sizeof(viewer->commits[viewer->commit_count].hash) - 1] = '\0'; + + // Store first two letters of author name + viewer->commits[viewer->commit_count].author_initials[0] = + author[0] ? author[0] : '?'; + viewer->commits[viewer->commit_count].author_initials[1] = + author[1] ? author[1] : '?'; + viewer->commits[viewer->commit_count].author_initials[2] = '\0'; + + // Store title + strncpy(viewer->commits[viewer->commit_count].title, title, + MAX_COMMIT_TITLE_LEN - 1); + viewer->commits[viewer->commit_count].title[MAX_COMMIT_TITLE_LEN - 1] = + '\0'; + + // Check if this commit is in the unpushed list + viewer->commits[viewer->commit_count].is_pushed = 1; // Default to pushed + for (int i = 0; i < unpushed_count; i++) { + if (strcmp(viewer->commits[viewer->commit_count].hash, + unpushed_hashes[i]) == 0) { + viewer->commits[viewer->commit_count].is_pushed = 0; + break; + } + } + + viewer->commit_count++; + } + } + + pclose(fp); + return viewer->commit_count; } /** * Toggle file marking for commit */ void toggle_file_mark(NCursesDiffViewer *viewer, int file_index) { - if (!viewer || file_index < 0 || file_index >= viewer->file_count) return; - - viewer->files[file_index].marked_for_commit = !viewer->files[file_index].marked_for_commit; + if (!viewer || file_index < 0 || file_index >= viewer->file_count) + return; + + viewer->files[file_index].marked_for_commit = + !viewer->files[file_index].marked_for_commit; } /** * Mark all files for commit (or unmark if all are already marked) */ void mark_all_files(NCursesDiffViewer *viewer) { - if (!viewer) return; - - // Check if all files are already marked - int all_marked = 1; - for (int i = 0; i < viewer->file_count; i++) { - if (!viewer->files[i].marked_for_commit) { - all_marked = 0; - break; - } - } - - // If all are marked, unmark all. Otherwise, mark all - for (int i = 0; i < viewer->file_count; i++) { - viewer->files[i].marked_for_commit = all_marked ? 0 : 1; + if (!viewer) + return; + + // Check if all files are already marked + int all_marked = 1; + for (int i = 0; i < viewer->file_count; i++) { + if (!viewer->files[i].marked_for_commit) { + all_marked = 0; + break; } + } + + // If all are marked, unmark all. Otherwise, mark all + for (int i = 0; i < viewer->file_count; i++) { + viewer->files[i].marked_for_commit = all_marked ? 0 : 1; + } } /** * Get commit title and message input from user */ -int get_commit_title_input(char *title, int max_len, char *message, int max_message_len) { - if (!title) return 0; - - // Save current screen - WINDOW *saved_screen = dupwin(stdscr); - - // Calculate window dimensions - int input_width = COLS * 0.8; // 80% of screen width - int title_height = 3; - int message_height = 15; // Much taller message box - int start_x = COLS / 2 - input_width / 2; - int title_start_y = LINES / 2 - (title_height + message_height + 2) / 2; - int message_start_y = title_start_y + title_height + 1; - - // Create title and message windows directly (no outer dialog) - WINDOW *title_win = newwin(title_height, input_width, title_start_y, start_x); - WINDOW *message_win = newwin(message_height, input_width, message_start_y, start_x); - - if (!title_win || !message_win) { - if (saved_screen) delwin(saved_screen); - if (title_win) delwin(title_win); - if (message_win) delwin(message_win); - return 0; +int get_commit_title_input(char *title, int max_len, char *message, + int max_message_len) { + if (!title) + return 0; + + // Save current screen + WINDOW *saved_screen = dupwin(stdscr); + + // Calculate window dimensions + int input_width = COLS * 0.8; // 80% of screen width + int title_height = 3; + int message_height = 15; // Much taller message box + int start_x = COLS / 2 - input_width / 2; + int title_start_y = LINES / 2 - (title_height + message_height + 2) / 2; + int message_start_y = title_start_y + title_height + 1; + + // Create title and message windows directly (no outer dialog) + WINDOW *title_win = newwin(title_height, input_width, title_start_y, start_x); + WINDOW *message_win = + newwin(message_height, input_width, message_start_y, start_x); + + if (!title_win || !message_win) { + if (saved_screen) + delwin(saved_screen); + if (title_win) + delwin(title_win); + if (message_win) + delwin(message_win); + return 0; + } + + // Initialize message buffer + if (message) { + message[0] = '\0'; + } + + // Variables for input handling + char local_message[2048] = ""; + int current_field = 0; // 0 = title, 1 = message + int title_scroll_offset = 0; // For horizontal scrolling + int message_scroll_offset = 0; // For vertical scrolling + int ch; + + // Function to redraw title window + void redraw_title() { + werase(title_win); + box(title_win, 0, 0); + + int visible_width = input_width - 4; + int title_len = strlen(title); + + // CRITICAL: Clear the content area with spaces FIRST + for (int x = 1; x <= visible_width; x++) { + mvwaddch(title_win, 1, x, ' '); } - - // Initialize message buffer - if (message) { - message[0] = '\0'; + + // Calculate what part of the title to show + int display_start = title_scroll_offset; + int display_end = display_start + visible_width; + if (display_end > title_len) + display_end = title_len; + + // Show the visible portion of the title ON TOP of cleared spaces + for (int i = display_start; i < display_end; i++) { + mvwaddch(title_win, 1, 1 + (i - display_start), title[i]); } - - // Variables for input handling - char local_message[2048] = ""; - int current_field = 0; // 0 = title, 1 = message - int title_scroll_offset = 0; // For horizontal scrolling - int message_scroll_offset = 0; // For vertical scrolling - int ch; - - // Function to redraw title window - void redraw_title() { - werase(title_win); - box(title_win, 0, 0); - - int visible_width = input_width - 4; - int title_len = strlen(title); - - // CRITICAL: Clear the content area with spaces FIRST - for (int x = 1; x <= visible_width; x++) { - mvwaddch(title_win, 1, x, ' '); - } - - // Calculate what part of the title to show - int display_start = title_scroll_offset; - int display_end = display_start + visible_width; - if (display_end > title_len) display_end = title_len; - - // Show the visible portion of the title ON TOP of cleared spaces - for (int i = display_start; i < display_end; i++) { - mvwaddch(title_win, 1, 1 + (i - display_start), title[i]); - } - - // Highlight header if active - if (current_field == 0) { - wattron(title_win, COLOR_PAIR(4)); - mvwprintw(title_win, 0, 2, " Title (Tab to switch, Enter to commit) "); - wattroff(title_win, COLOR_PAIR(4)); - } else { - mvwprintw(title_win, 0, 2, " Title (Tab to switch, Enter to commit) "); - } - wrefresh(title_win); - } - - // Function to redraw message window - SIMPLE like title field - void redraw_message() { - werase(message_win); - box(message_win, 0, 0); - - int visible_height = message_height - 2; - int message_visible_width = input_width - 3; - - // Clear content area - for (int y = 1; y <= visible_height; y++) { - for (int x = 1; x <= message_visible_width; x++) { - mvwaddch(message_win, y, x, ' '); - } - } - - // SIMPLE: Just display the raw string, character by character - int y = 1, x = 1; - for (int i = 0; local_message[i] && y <= visible_height; i++) { - if (local_message[i] == '\n') { - y++; - x = 1; - } else { - if (x <= message_visible_width) { - mvwaddch(message_win, y, x, local_message[i]); - x++; - if (x > message_visible_width) { - y++; - x = 1; - } - } - } - } - - // Highlight header if active - if (current_field == 1) { - wattron(message_win, COLOR_PAIR(4)); - mvwprintw(message_win, 0, 2, " Message (Tab to switch, Enter for newline) "); - wattroff(message_win, COLOR_PAIR(4)); - } else { - mvwprintw(message_win, 0, 2, " Message (Tab to switch, Enter for newline) "); - } - wrefresh(message_win); - } - - // Initial draw - redraw_title(); - redraw_message(); - - // Position initial cursor + + // Highlight header if active if (current_field == 0) { - int cursor_pos = strlen(title) - title_scroll_offset; - int visible_width = input_width - 4; - if (cursor_pos > visible_width - 1) cursor_pos = visible_width - 1; - if (cursor_pos < 0) cursor_pos = 0; - wmove(title_win, 1, 1 + cursor_pos); - wrefresh(title_win); + wattron(title_win, COLOR_PAIR(4)); + mvwprintw(title_win, 0, 2, " Title (Tab to switch, Enter to commit) "); + wattroff(title_win, COLOR_PAIR(4)); + } else { + mvwprintw(title_win, 0, 2, " Title (Tab to switch, Enter to commit) "); } - - curs_set(1); - noecho(); - - // Main input loop - while (1) { - ch = getch(); - int redraw_needed = 0; - - if (ch == 27) { - // ESC to cancel - don't commit - if (message && max_message_len > 0) { - message[0] = '\0'; // Clear message - } - title[0] = '\0'; // Clear title - break; + wrefresh(title_win); + } + + // Function to redraw message window - SIMPLE like title field + void redraw_message() { + werase(message_win); + box(message_win, 0, 0); + + int visible_height = message_height - 2; + int message_visible_width = input_width - 3; + + // Clear content area + for (int y = 1; y <= visible_height; y++) { + for (int x = 1; x <= message_visible_width; x++) { + mvwaddch(message_win, y, x, ' '); + } + } + + // SIMPLE: Just display the raw string, character by character + int y = 1, x = 1; + for (int i = 0; local_message[i] && y <= visible_height; i++) { + if (local_message[i] == '\n') { + y++; + x = 1; + } else { + if (x <= message_visible_width) { + mvwaddch(message_win, y, x, local_message[i]); + x++; + if (x > message_visible_width) { + y++; + x = 1; + } } - - if (ch == '\t') { - // Switch between fields - current_field = !current_field; - redraw_needed = 1; - } else if (ch == '\n' || ch == '\r') { - if (current_field == 0) { - // Enter in title field - commit if title has content - if (strlen(title) > 0) { - break; - } - } else { - // Enter in message field - add newline - int len = strlen(local_message); - if (len < 2047) { - local_message[len] = '\n'; - local_message[len + 1] = '\0'; - - // Auto-scroll down if needed - int visible_height = message_height - 2; - int current_lines = 1; - for (int i = 0; local_message[i]; i++) { - if (local_message[i] == '\n') current_lines++; - } - if (current_lines > visible_height + message_scroll_offset) { - message_scroll_offset += 2; - } - redraw_message(); - } - } - } else if (ch == KEY_BACKSPACE || ch == 127 || ch == 8) { - // Handle backspace - if (current_field == 0) { - int len = strlen(title); - if (len > 0) { - title[len - 1] = '\0'; - - // Adjust scroll if needed - int visible_width = input_width - 4; - if (len - 1 <= title_scroll_offset) { - title_scroll_offset = (len - 1) - (visible_width - 5); - if (title_scroll_offset < 0) title_scroll_offset = 0; - } - redraw_title(); - } - } else { - int len = strlen(local_message); - if (len > 0) { - local_message[len - 1] = '\0'; - redraw_message(); - } - } - } else if (ch >= 32 && ch <= 126) { - // Regular character input - if (current_field == 0) { - int len = strlen(title); - if (len < max_len - 1) { - title[len] = ch; - title[len + 1] = '\0'; - - // Auto-scroll horizontally if needed - int visible_width = input_width - 4; - if (len + 1 > title_scroll_offset + visible_width - 5) { - title_scroll_offset = (len + 1) - (visible_width - 5); - } - redraw_title(); - } - } else { - int len = strlen(local_message); - if (len < 2047) { - local_message[len] = ch; - local_message[len + 1] = '\0'; - redraw_message(); - } - } + } + } + + // Highlight header if active + if (current_field == 1) { + wattron(message_win, COLOR_PAIR(4)); + mvwprintw(message_win, 0, 2, + " Message (Tab to switch, Enter for newline) "); + wattroff(message_win, COLOR_PAIR(4)); + } else { + mvwprintw(message_win, 0, 2, + " Message (Tab to switch, Enter for newline) "); + } + wrefresh(message_win); + } + + // Initial draw + redraw_title(); + redraw_message(); + + // Position initial cursor + if (current_field == 0) { + int cursor_pos = strlen(title) - title_scroll_offset; + int visible_width = input_width - 4; + if (cursor_pos > visible_width - 1) + cursor_pos = visible_width - 1; + if (cursor_pos < 0) + cursor_pos = 0; + wmove(title_win, 1, 1 + cursor_pos); + wrefresh(title_win); + } + + curs_set(1); + noecho(); + + // Main input loop + while (1) { + ch = getch(); + int redraw_needed = 0; + + if (ch == 27) { + // ESC to cancel - don't commit + if (message && max_message_len > 0) { + message[0] = '\0'; // Clear message + } + title[0] = '\0'; // Clear title + break; + } + + if (ch == '\t') { + // Switch between fields + current_field = !current_field; + redraw_needed = 1; + } else if (ch == '\n' || ch == '\r') { + if (current_field == 0) { + // Enter in title field - commit if title has content + if (strlen(title) > 0) { + break; } - - // Redraw if field switched - if (redraw_needed) { - redraw_title(); - redraw_message(); + } else { + // Enter in message field - add newline + int len = strlen(local_message); + if (len < 2047) { + local_message[len] = '\n'; + local_message[len + 1] = '\0'; + + // Auto-scroll down if needed + int visible_height = message_height - 2; + int current_lines = 1; + for (int i = 0; local_message[i]; i++) { + if (local_message[i] == '\n') + current_lines++; + } + if (current_lines > visible_height + message_scroll_offset) { + message_scroll_offset += 2; + } + redraw_message(); } - - // Position cursor in active field - if (current_field == 0) { - int cursor_pos = strlen(title) - title_scroll_offset; - int visible_width = input_width - 4; - if (cursor_pos > visible_width - 1) cursor_pos = visible_width - 1; - if (cursor_pos < 0) cursor_pos = 0; - wmove(title_win, 1, 1 + cursor_pos); - wrefresh(title_win); + } + } else if (ch == KEY_BACKSPACE || ch == 127 || ch == 8) { + // Handle backspace + if (current_field == 0) { + int len = strlen(title); + if (len > 0) { + title[len - 1] = '\0'; + + // Adjust scroll if needed + int visible_width = input_width - 4; + if (len - 1 <= title_scroll_offset) { + title_scroll_offset = (len - 1) - (visible_width - 5); + if (title_scroll_offset < 0) + title_scroll_offset = 0; + } + redraw_title(); + } + } else { + int len = strlen(local_message); + if (len > 0) { + local_message[len - 1] = '\0'; + redraw_message(); + } + } + } else if (ch >= 32 && ch <= 126) { + // Regular character input + if (current_field == 0) { + int len = strlen(title); + if (len < max_len - 1) { + title[len] = ch; + title[len + 1] = '\0'; + + // Auto-scroll horizontally if needed + int visible_width = input_width - 4; + if (len + 1 > title_scroll_offset + visible_width - 5) { + title_scroll_offset = (len + 1) - (visible_width - 5); + } + redraw_title(); + } + } else { + int len = strlen(local_message); + if (len < 2047) { + local_message[len] = ch; + local_message[len + 1] = '\0'; + redraw_message(); + } + } + } + + // Redraw if field switched + if (redraw_needed) { + redraw_title(); + redraw_message(); + } + + // Position cursor in active field + if (current_field == 0) { + int cursor_pos = strlen(title) - title_scroll_offset; + int visible_width = input_width - 4; + if (cursor_pos > visible_width - 1) + cursor_pos = visible_width - 1; + if (cursor_pos < 0) + cursor_pos = 0; + wmove(title_win, 1, 1 + cursor_pos); + wrefresh(title_win); + } else { + // Position cursor in message field - EXACTLY like title field + int message_len = strlen(local_message); + int message_visible_width = input_width - 3; + + // Simple cursor positioning - just count where we are + int y = 1, x = 1; + for (int i = 0; i < message_len; i++) { + if (local_message[i] == '\n') { + y++; + x = 1; } else { - // Position cursor in message field - EXACTLY like title field - int message_len = strlen(local_message); - int message_visible_width = input_width - 3; - - // Simple cursor positioning - just count where we are - int y = 1, x = 1; - for (int i = 0; i < message_len; i++) { - if (local_message[i] == '\n') { - y++; - x = 1; - } else { - x++; - if (x > message_visible_width) { - y++; - x = 1; - } - } - } - - // Position cursor at end of text - wmove(message_win, y, x); - wrefresh(message_win); + x++; + if (x > message_visible_width) { + y++; + x = 1; + } } + } + + // Position cursor at end of text + wmove(message_win, y, x); + wrefresh(message_win); } - - // Copy local message to output parameter only if not canceled - if (message && max_message_len > 0 && strlen(title) > 0) { - strncpy(message, local_message, max_message_len - 1); - message[max_message_len - 1] = '\0'; - } - - // Restore settings - curs_set(0); // Hide cursor - - // Clean up windows - delwin(title_win); - delwin(message_win); - - // Restore the screen - if (saved_screen) { - overwrite(saved_screen, stdscr); - delwin(saved_screen); - } - - // Force a complete redraw - clear(); - refresh(); - - return strlen(title) > 0 ? 1 : 0; + } + + // Copy local message to output parameter only if not canceled + if (message && max_message_len > 0 && strlen(title) > 0) { + strncpy(message, local_message, max_message_len - 1); + message[max_message_len - 1] = '\0'; + } + + // Restore settings + curs_set(0); // Hide cursor + + // Clean up windows + delwin(title_win); + delwin(message_win); + + // Restore the screen + if (saved_screen) { + overwrite(saved_screen, stdscr); + delwin(saved_screen); + } + + // Force a complete redraw + clear(); + refresh(); + + return strlen(title) > 0 ? 1 : 0; } /** * Commit marked files with title and message */ -int commit_marked_files(NCursesDiffViewer *viewer, const char *commit_title, const char *commit_message) { - if (!viewer || !commit_title || strlen(commit_title) == 0) return 0; - - // First, add marked files to git - for (int i = 0; i < viewer->file_count; i++) { - if (viewer->files[i].marked_for_commit) { - char cmd[1024]; - snprintf(cmd, sizeof(cmd), "git add \"%s\" 2>/dev/null >/dev/null", viewer->files[i].filename); - system(cmd); - } - } - - // Commit with the provided title and message - char commit_cmd[2048]; - if (commit_message && strlen(commit_message) > 0) { - snprintf(commit_cmd, sizeof(commit_cmd), "git commit -m \"%s\" -m \"%s\" 2>/dev/null >/dev/null", commit_title, commit_message); - } else { - snprintf(commit_cmd, sizeof(commit_cmd), "git commit -m \"%s\" 2>/dev/null >/dev/null", commit_title); - } - int result = system(commit_cmd); - - if (result == 0) { - // Small delay to ensure git has processed the commit - usleep(100000); // 100ms delay - - // Refresh file list and commit history - get_ncurses_changed_files(viewer); - get_commit_history(viewer); - - // Reset selection if no files remain - if (viewer->file_count == 0) { - viewer->selected_file = 0; - viewer->file_line_count = 0; - viewer->file_scroll_offset = 0; - } else if (viewer->selected_file >= viewer->file_count) { - viewer->selected_file = viewer->file_count - 1; - } - - // Reload the currently selected file's diff if files still exist - if (viewer->file_count > 0 && viewer->selected_file < viewer->file_count) { - load_full_file_with_diff(viewer, viewer->files[viewer->selected_file].filename); - } - - return 1; - } - +int commit_marked_files(NCursesDiffViewer *viewer, const char *commit_title, + const char *commit_message) { + if (!viewer || !commit_title || strlen(commit_title) == 0) return 0; + + // First, add marked files to git + for (int i = 0; i < viewer->file_count; i++) { + if (viewer->files[i].marked_for_commit) { + char cmd[1024]; + snprintf(cmd, sizeof(cmd), "git add \"%s\" 2>/dev/null >/dev/null", + viewer->files[i].filename); + system(cmd); + } + } + + // Commit with the provided title and message + char commit_cmd[2048]; + if (commit_message && strlen(commit_message) > 0) { + snprintf(commit_cmd, sizeof(commit_cmd), + "git commit -m \"%s\" -m \"%s\" 2>/dev/null >/dev/null", + commit_title, commit_message); + } else { + snprintf(commit_cmd, sizeof(commit_cmd), + "git commit -m \"%s\" 2>/dev/null >/dev/null", commit_title); + } + int result = system(commit_cmd); + + if (result == 0) { + // Small delay to ensure git has processed the commit + usleep(100000); // 100ms delay + + // Refresh file list and commit history + get_ncurses_changed_files(viewer); + get_commit_history(viewer); + + // Reset selection if no files remain + if (viewer->file_count == 0) { + viewer->selected_file = 0; + viewer->file_line_count = 0; + viewer->file_scroll_offset = 0; + } else if (viewer->selected_file >= viewer->file_count) { + viewer->selected_file = viewer->file_count - 1; + } + + // Reload the currently selected file's diff if files still exist + if (viewer->file_count > 0 && viewer->selected_file < viewer->file_count) { + load_full_file_with_diff(viewer, + viewer->files[viewer->selected_file].filename); + } + + return 1; + } + + return 0; } /** * Reset commit (soft) - undo commit but keep changes */ int reset_commit_soft(NCursesDiffViewer *viewer, int commit_index) { - if (!viewer || commit_index < 0 || commit_index >= viewer->commit_count) return 0; - - // Only allow reset of the most recent commit (index 0) - if (commit_index != 0) return 0; - - // Do soft reset of HEAD~1 - int result = system("git reset --soft HEAD~1 2>/dev/null >/dev/null"); - - if (result == 0) { - // Small delay to ensure git has processed the reset - usleep(100000); // 100ms delay - - // Refresh everything - get_ncurses_changed_files(viewer); - get_commit_history(viewer); - - // Reload current file if any files exist - if (viewer->file_count > 0 && viewer->selected_file < viewer->file_count) { - load_full_file_with_diff(viewer, viewer->files[viewer->selected_file].filename); - } - - return 1; - } - + if (!viewer || commit_index < 0 || commit_index >= viewer->commit_count) return 0; + + // Only allow reset of the most recent commit (index 0) + if (commit_index != 0) + return 0; + + // Do soft reset of HEAD~1 + int result = system("git reset --soft HEAD~1 2>/dev/null >/dev/null"); + + if (result == 0) { + // Small delay to ensure git has processed the reset + usleep(100000); // 100ms delay + + // Refresh everything + get_ncurses_changed_files(viewer); + get_commit_history(viewer); + + // Reload current file if any files exist + if (viewer->file_count > 0 && viewer->selected_file < viewer->file_count) { + load_full_file_with_diff(viewer, + viewer->files[viewer->selected_file].filename); + } + + return 1; + } + + return 0; } /** * Reset commit (hard) - undo commit and discard changes */ int reset_commit_hard(NCursesDiffViewer *viewer, int commit_index) { - if (!viewer || commit_index < 0 || commit_index >= viewer->commit_count) return 0; - - // Only allow reset of the most recent commit (index 0) - if (commit_index != 0) return 0; - - // Do hard reset of HEAD~1 - int result = system("git reset --hard HEAD~1 2>/dev/null >/dev/null"); - - if (result == 0) { - // Small delay to ensure git has processed the reset - usleep(100000); // 100ms delay - - // Refresh everything - get_ncurses_changed_files(viewer); - get_commit_history(viewer); - - // Reset file selection since changes are discarded - viewer->selected_file = 0; - viewer->file_line_count = 0; - viewer->file_scroll_offset = 0; - - // Reload current file if any files exist - if (viewer->file_count > 0 && viewer->selected_file < viewer->file_count) { - load_full_file_with_diff(viewer, viewer->files[viewer->selected_file].filename); - } - - return 1; - } - + if (!viewer || commit_index < 0 || commit_index >= viewer->commit_count) return 0; + + // Only allow reset of the most recent commit (index 0) + if (commit_index != 0) + return 0; + + // Do hard reset of HEAD~1 + int result = system("git reset --hard HEAD~1 2>/dev/null >/dev/null"); + + if (result == 0) { + // Small delay to ensure git has processed the reset + usleep(100000); // 100ms delay + + // Refresh everything + get_ncurses_changed_files(viewer); + get_commit_history(viewer); + + // Reset file selection since changes are discarded + viewer->selected_file = 0; + viewer->file_line_count = 0; + viewer->file_scroll_offset = 0; + + // Reload current file if any files exist + if (viewer->file_count > 0 && viewer->selected_file < viewer->file_count) { + load_full_file_with_diff(viewer, + viewer->files[viewer->selected_file].filename); + } + + return 1; + } + + return 0; } /** * Amend the most recent commit */ int amend_commit(NCursesDiffViewer *viewer) { - if (!viewer || viewer->commit_count == 0) return 0; - - // Get current commit message - char current_title[MAX_COMMIT_TITLE_LEN] = ""; - char current_message[2048] = ""; - - // Get the current commit message - FILE *fp = popen("git log -1 --pretty=format:%s 2>/dev/null", "r"); - if (fp) { - fgets(current_title, sizeof(current_title), fp); - pclose(fp); - } - - // Get the current commit body (if any) - fp = popen("git log -1 --pretty=format:%b 2>/dev/null", "r"); - if (fp) { - fgets(current_message, sizeof(current_message), fp); - pclose(fp); - } - - // Get new commit message from user (pre-filled with current message) - char new_title[MAX_COMMIT_TITLE_LEN]; - char new_message[2048]; - - strncpy(new_title, current_title, sizeof(new_title) - 1); - new_title[sizeof(new_title) - 1] = '\0'; - strncpy(new_message, current_message, sizeof(new_message) - 1); - new_message[sizeof(new_message) - 1] = '\0'; - - if (get_commit_title_input(new_title, MAX_COMMIT_TITLE_LEN, new_message, sizeof(new_message))) { - // Add any marked files first - for (int i = 0; i < viewer->file_count; i++) { - if (viewer->files[i].marked_for_commit) { - char cmd[1024]; - snprintf(cmd, sizeof(cmd), "git add \"%s\" 2>/dev/null >/dev/null", viewer->files[i].filename); - system(cmd); - } - } - - // Amend the commit with new message - char amend_cmd[2048]; - if (strlen(new_message) > 0) { - snprintf(amend_cmd, sizeof(amend_cmd), "git commit --amend -m \"%s\" -m \"%s\" 2>/dev/null >/dev/null", new_title, new_message); - } else { - snprintf(amend_cmd, sizeof(amend_cmd), "git commit --amend -m \"%s\" 2>/dev/null >/dev/null", new_title); - } - - int result = system(amend_cmd); - - if (result == 0) { - // Small delay to ensure git has processed the amend - usleep(100000); // 100ms delay - - // Refresh everything - get_ncurses_changed_files(viewer); - get_commit_history(viewer); - - // Reset selection if no files remain - if (viewer->file_count == 0) { - viewer->selected_file = 0; - viewer->file_line_count = 0; - viewer->file_scroll_offset = 0; - } else if (viewer->selected_file >= viewer->file_count) { - viewer->selected_file = viewer->file_count - 1; - } - - // Reload current file if any files exist - if (viewer->file_count > 0 && viewer->selected_file < viewer->file_count) { - load_full_file_with_diff(viewer, viewer->files[viewer->selected_file].filename); - } - - return 1; - } - } - + if (!viewer || viewer->commit_count == 0) return 0; + + // Get current commit message + char current_title[MAX_COMMIT_TITLE_LEN] = ""; + char current_message[2048] = ""; + + // Get the current commit message + FILE *fp = popen("git log -1 --pretty=format:%s 2>/dev/null", "r"); + if (fp) { + fgets(current_title, sizeof(current_title), fp); + pclose(fp); + } + + // Get the current commit body (if any) + fp = popen("git log -1 --pretty=format:%b 2>/dev/null", "r"); + if (fp) { + fgets(current_message, sizeof(current_message), fp); + pclose(fp); + } + + // Get new commit message from user (pre-filled with current message) + char new_title[MAX_COMMIT_TITLE_LEN]; + char new_message[2048]; + + strncpy(new_title, current_title, sizeof(new_title) - 1); + new_title[sizeof(new_title) - 1] = '\0'; + strncpy(new_message, current_message, sizeof(new_message) - 1); + new_message[sizeof(new_message) - 1] = '\0'; + + if (get_commit_title_input(new_title, MAX_COMMIT_TITLE_LEN, new_message, + sizeof(new_message))) { + // Add any marked files first + for (int i = 0; i < viewer->file_count; i++) { + if (viewer->files[i].marked_for_commit) { + char cmd[1024]; + snprintf(cmd, sizeof(cmd), "git add \"%s\" 2>/dev/null >/dev/null", + viewer->files[i].filename); + system(cmd); + } + } + + // Amend the commit with new message + char amend_cmd[2048]; + if (strlen(new_message) > 0) { + snprintf(amend_cmd, sizeof(amend_cmd), + "git commit --amend -m \"%s\" -m \"%s\" 2>/dev/null >/dev/null", + new_title, new_message); + } else { + snprintf(amend_cmd, sizeof(amend_cmd), + "git commit --amend -m \"%s\" 2>/dev/null >/dev/null", + new_title); + } + + int result = system(amend_cmd); + + if (result == 0) { + // Small delay to ensure git has processed the amend + usleep(100000); // 100ms delay + + // Refresh everything + get_ncurses_changed_files(viewer); + get_commit_history(viewer); + + // Reset selection if no files remain + if (viewer->file_count == 0) { + viewer->selected_file = 0; + viewer->file_line_count = 0; + viewer->file_scroll_offset = 0; + } else if (viewer->selected_file >= viewer->file_count) { + viewer->selected_file = viewer->file_count - 1; + } + + // Reload current file if any files exist + if (viewer->file_count > 0 && + viewer->selected_file < viewer->file_count) { + load_full_file_with_diff(viewer, + viewer->files[viewer->selected_file].filename); + } + + return 1; + } + } + + return 0; } /** * Push specific commit */ int push_commit(NCursesDiffViewer *viewer, int commit_index) { - if (!viewer || commit_index < 0 || commit_index >= viewer->commit_count) return 0; - - // Start pushing animation immediately - viewer->sync_status = SYNC_STATUS_PUSHING_APPEARING; - viewer->animation_frame = 0; - viewer->text_char_count = 0; - - // Render immediately to show the animation start - render_status_bar(viewer); - - // Do the actual push work - int result = system("git push origin 2>/dev/null >/dev/null"); - - if (result == 0) { - // Refresh commit history to get proper push status - get_commit_history(viewer); - return 1; - } - + if (!viewer || commit_index < 0 || commit_index >= viewer->commit_count) return 0; + + // Start pushing animation immediately + viewer->sync_status = SYNC_STATUS_PUSHING_APPEARING; + viewer->animation_frame = 0; + viewer->text_char_count = 0; + + // Render immediately to show the animation start + render_status_bar(viewer); + + // Do the actual push work + int result = system("git push origin 2>/dev/null >/dev/null"); + + if (result == 0) { + // Refresh commit history to get proper push status + get_commit_history(viewer); + return 1; + } + + return 0; } /** * Pull commits from remote */ int pull_commits(NCursesDiffViewer *viewer) { - if (!viewer) return 0; - - // Start pulling animation immediately - viewer->sync_status = SYNC_STATUS_PULLING_APPEARING; - viewer->animation_frame = 0; - viewer->text_char_count = 0; - - // Render immediately to show the animation start - render_status_bar(viewer); - - // Do the actual pull work - int result = system("git pull origin 2>/dev/null >/dev/null"); - - if (result == 0) { - // Refresh everything after pull - get_ncurses_changed_files(viewer); - get_commit_history(viewer); - - // Reset selection if no files remain - if (viewer->file_count == 0) { - viewer->selected_file = 0; - viewer->file_line_count = 0; - viewer->file_scroll_offset = 0; - } else if (viewer->selected_file >= viewer->file_count) { - viewer->selected_file = viewer->file_count - 1; - } - - // Reload current file if any - if (viewer->file_count > 0 && viewer->selected_file < viewer->file_count) { - load_full_file_with_diff(viewer, viewer->files[viewer->selected_file].filename); - } - - return 1; - } - + if (!viewer) return 0; + + // Start pulling animation immediately + viewer->sync_status = SYNC_STATUS_PULLING_APPEARING; + viewer->animation_frame = 0; + viewer->text_char_count = 0; + + // Render immediately to show the animation start + render_status_bar(viewer); + + // Do the actual pull work + int result = system("git pull origin 2>/dev/null >/dev/null"); + + if (result == 0) { + // Refresh everything after pull + get_ncurses_changed_files(viewer); + get_commit_history(viewer); + + // Reset selection if no files remain + if (viewer->file_count == 0) { + viewer->selected_file = 0; + viewer->file_line_count = 0; + viewer->file_scroll_offset = 0; + } else if (viewer->selected_file >= viewer->file_count) { + viewer->selected_file = viewer->file_count - 1; + } + + // Reload current file if any + if (viewer->file_count > 0 && viewer->selected_file < viewer->file_count) { + load_full_file_with_diff(viewer, + viewer->files[viewer->selected_file].filename); + } + + return 1; + } + + return 0; } /** * Render the file list window */ void render_file_list_window(NCursesDiffViewer *viewer) { - if (!viewer || !viewer->file_list_win) return; - - // CRITICAL: Clear the entire window first - werase(viewer->file_list_win); - - // Draw rounded border and title - draw_rounded_box(viewer->file_list_win); - mvwprintw(viewer->file_list_win, 0, 2, " 1. Files "); - - int max_files_visible = viewer->file_panel_height - 2; - - // CRITICAL: Clear the content area with spaces FIRST - for (int y = 1; y < viewer->file_panel_height - 1; y++) { - for (int x = 1; x < viewer->file_panel_width - 1; x++) { - mvwaddch(viewer->file_list_win, y, x, ' '); - } + if (!viewer || !viewer->file_list_win) + return; + + // CRITICAL: Clear the entire window first + werase(viewer->file_list_win); + + // Draw rounded border and title + draw_rounded_box(viewer->file_list_win); + mvwprintw(viewer->file_list_win, 0, 2, " 1. Files "); + + int max_files_visible = viewer->file_panel_height - 2; + + // CRITICAL: Clear the content area with spaces FIRST + for (int y = 1; y < viewer->file_panel_height - 1; y++) { + for (int x = 1; x < viewer->file_panel_width - 1; x++) { + mvwaddch(viewer->file_list_win, y, x, ' '); } - - for (int i = 0; i < max_files_visible; i++) { - int y = i + 1; - - // Skip if no more files - if (i >= viewer->file_count) continue; - - // Show selection indicator - if (i == viewer->selected_file) { - if (viewer->current_mode == NCURSES_MODE_FILE_LIST) { - wattron(viewer->file_list_win, COLOR_PAIR(5)); - mvwprintw(viewer->file_list_win, y, 1, ">"); - } else { - wattron(viewer->file_list_win, COLOR_PAIR(1)); - mvwprintw(viewer->file_list_win, y, 1, "*"); - } - } else { - mvwprintw(viewer->file_list_win, y, 1, " "); - } - - // Status indicator - char status = viewer->files[i].status; - if (status == 'M') { - wattron(viewer->file_list_win, COLOR_PAIR(4)); - mvwprintw(viewer->file_list_win, y, 2, "M"); - wattroff(viewer->file_list_win, COLOR_PAIR(4)); - } else if (status == 'A') { - wattron(viewer->file_list_win, COLOR_PAIR(1)); - mvwprintw(viewer->file_list_win, y, 2, "A"); - wattroff(viewer->file_list_win, COLOR_PAIR(1)); - } else if (status == 'D') { - wattron(viewer->file_list_win, COLOR_PAIR(2)); - mvwprintw(viewer->file_list_win, y, 2, "D"); - wattroff(viewer->file_list_win, COLOR_PAIR(2)); - } else { - mvwprintw(viewer->file_list_win, y, 2, "%c", status); - } - - // Filename (truncated to fit panel with "..") - int max_name_len = viewer->file_panel_width - 6; // Leave space for border - char truncated_name[256]; - if ((int)strlen(viewer->files[i].filename) > max_name_len) { - strncpy(truncated_name, viewer->files[i].filename, max_name_len - 2); - truncated_name[max_name_len - 2] = '\0'; - strcat(truncated_name, ".."); - } else { - strcpy(truncated_name, viewer->files[i].filename); - } - - // Color filename green if marked for commit - if (viewer->files[i].marked_for_commit) { - wattron(viewer->file_list_win, COLOR_PAIR(1)); - } - mvwprintw(viewer->file_list_win, y, 4, "%s", truncated_name); - if (viewer->files[i].marked_for_commit) { - wattroff(viewer->file_list_win, COLOR_PAIR(1)); - } - - if (i == viewer->selected_file) { - if (viewer->current_mode == NCURSES_MODE_FILE_LIST) { - wattroff(viewer->file_list_win, COLOR_PAIR(5)); - } else { - wattroff(viewer->file_list_win, COLOR_PAIR(1)); - } - } + } + + for (int i = 0; i < max_files_visible; i++) { + int y = i + 1; + + // Skip if no more files + if (i >= viewer->file_count) + continue; + + // Show selection indicator + if (i == viewer->selected_file) { + if (viewer->current_mode == NCURSES_MODE_FILE_LIST) { + wattron(viewer->file_list_win, COLOR_PAIR(5)); + mvwprintw(viewer->file_list_win, y, 1, ">"); + } else { + wattron(viewer->file_list_win, COLOR_PAIR(1)); + mvwprintw(viewer->file_list_win, y, 1, "*"); + } + } else { + mvwprintw(viewer->file_list_win, y, 1, " "); } - - wrefresh(viewer->file_list_win); + + // Status indicator + char status = viewer->files[i].status; + if (status == 'M') { + wattron(viewer->file_list_win, COLOR_PAIR(4)); + mvwprintw(viewer->file_list_win, y, 2, "M"); + wattroff(viewer->file_list_win, COLOR_PAIR(4)); + } else if (status == 'A') { + wattron(viewer->file_list_win, COLOR_PAIR(1)); + mvwprintw(viewer->file_list_win, y, 2, "A"); + wattroff(viewer->file_list_win, COLOR_PAIR(1)); + } else if (status == 'D') { + wattron(viewer->file_list_win, COLOR_PAIR(2)); + mvwprintw(viewer->file_list_win, y, 2, "D"); + wattroff(viewer->file_list_win, COLOR_PAIR(2)); + } else { + mvwprintw(viewer->file_list_win, y, 2, "%c", status); + } + + // Filename (truncated to fit panel with "..") + int max_name_len = viewer->file_panel_width - 6; // Leave space for border + char truncated_name[256]; + if ((int)strlen(viewer->files[i].filename) > max_name_len) { + strncpy(truncated_name, viewer->files[i].filename, max_name_len - 2); + truncated_name[max_name_len - 2] = '\0'; + strcat(truncated_name, ".."); + } else { + strcpy(truncated_name, viewer->files[i].filename); + } + + // Color filename green if marked for commit + if (viewer->files[i].marked_for_commit) { + wattron(viewer->file_list_win, COLOR_PAIR(1)); + } + mvwprintw(viewer->file_list_win, y, 4, "%s", truncated_name); + if (viewer->files[i].marked_for_commit) { + wattroff(viewer->file_list_win, COLOR_PAIR(1)); + } + + if (i == viewer->selected_file) { + if (viewer->current_mode == NCURSES_MODE_FILE_LIST) { + wattroff(viewer->file_list_win, COLOR_PAIR(5)); + } else { + wattroff(viewer->file_list_win, COLOR_PAIR(1)); + } + } + } + + wrefresh(viewer->file_list_win); } /** * Render the commit list window */ + void render_commit_list_window(NCursesDiffViewer *viewer) { - if (!viewer || !viewer->commit_list_win) return; - - // Draw rounded border and title - draw_rounded_box(viewer->commit_list_win); - mvwprintw(viewer->commit_list_win, 0, 2, " 3. Commits "); - - int max_commits_visible = viewer->commit_panel_height - 2; - - for (int i = 0; i < max_commits_visible; i++) { - int y = i + 1; - - // Skip if no more commits - if (i >= viewer->commit_count) continue; - - // Show selection indicator for commit list mode - if (i == viewer->selected_commit && viewer->current_mode == NCURSES_MODE_COMMIT_LIST) { - wattron(viewer->commit_list_win, COLOR_PAIR(5)); - mvwprintw(viewer->commit_list_win, y, 1, ">"); - wattroff(viewer->commit_list_win, COLOR_PAIR(5)); - } else { - mvwprintw(viewer->commit_list_win, y, 1, " "); - } - - // Show commit hash with color based on push status - if (viewer->commits[i].is_pushed) { - wattron(viewer->commit_list_win, COLOR_PAIR(1)); // Green for pushed - } else { - wattron(viewer->commit_list_win, COLOR_PAIR(2)); // Red for unpushed - } - mvwprintw(viewer->commit_list_win, y, 2, "%s", viewer->commits[i].hash); - if (viewer->commits[i].is_pushed) { - wattroff(viewer->commit_list_win, COLOR_PAIR(1)); - } else { - wattroff(viewer->commit_list_win, COLOR_PAIR(2)); - } - - // Show author initials with same color as hash - if (viewer->commits[i].is_pushed) { - wattron(viewer->commit_list_win, COLOR_PAIR(1)); // Green for pushed - } else { - wattron(viewer->commit_list_win, COLOR_PAIR(2)); // Red for unpushed - } - mvwprintw(viewer->commit_list_win, y, 10, "%s", viewer->commits[i].author_initials); - if (viewer->commits[i].is_pushed) { - wattroff(viewer->commit_list_win, COLOR_PAIR(1)); - } else { - wattroff(viewer->commit_list_win, COLOR_PAIR(2)); - } - - // Show commit title (always white, truncated to fit with "..") - int max_title_len = viewer->file_panel_width - 15; // Leave space for border - char truncated_title[256]; - if ((int)strlen(viewer->commits[i].title) > max_title_len) { - strncpy(truncated_title, viewer->commits[i].title, max_title_len - 2); - truncated_title[max_title_len - 2] = '\0'; - strcat(truncated_title, ".."); - } else { - strcpy(truncated_title, viewer->commits[i].title); - } - - mvwprintw(viewer->commit_list_win, y, 13, "%s", truncated_title); + if (!viewer || !viewer->commit_list_win) + return; + + // CRITICAL: Clear the entire window first + werase(viewer->commit_list_win); + + // Draw rounded border and title + draw_rounded_box(viewer->commit_list_win); + mvwprintw(viewer->commit_list_win, 0, 2, " 3. Commits "); + + int max_commits_visible = viewer->commit_panel_height - 2; + + // CRITICAL: Clear the content area with spaces FIRST + for (int y = 1; y < viewer->commit_panel_height - 1; y++) { + for (int x = 1; x < viewer->file_panel_width - 1; x++) { + mvwaddch(viewer->commit_list_win, y, x, ' '); } - - wrefresh(viewer->commit_list_win); + } + + for (int i = 0; i < max_commits_visible; i++) { + int y = i + 1; + + // Skip if no more commits + if (i >= viewer->commit_count) + continue; + + // Show selection indicator for commit list mode + if (i == viewer->selected_commit && + viewer->current_mode == NCURSES_MODE_COMMIT_LIST) { + wattron(viewer->commit_list_win, COLOR_PAIR(5)); + mvwprintw(viewer->commit_list_win, y, 1, ">"); + wattroff(viewer->commit_list_win, COLOR_PAIR(5)); + } else { + mvwprintw(viewer->commit_list_win, y, 1, " "); + } + + // Show commit hash with color based on push status + if (viewer->commits[i].is_pushed) { + wattron(viewer->commit_list_win, COLOR_PAIR(1)); // Green for pushed + } else { + wattron(viewer->commit_list_win, COLOR_PAIR(2)); // Red for unpushed + } + mvwprintw(viewer->commit_list_win, y, 2, "%s", viewer->commits[i].hash); + if (viewer->commits[i].is_pushed) { + wattroff(viewer->commit_list_win, COLOR_PAIR(1)); + } else { + wattroff(viewer->commit_list_win, COLOR_PAIR(2)); + } + + // Show author initials with same color as hash + if (viewer->commits[i].is_pushed) { + wattron(viewer->commit_list_win, COLOR_PAIR(1)); // Green for pushed + } else { + wattron(viewer->commit_list_win, COLOR_PAIR(2)); // Red for unpushed + } + mvwprintw(viewer->commit_list_win, y, 10, "%s", + viewer->commits[i].author_initials); + if (viewer->commits[i].is_pushed) { + wattroff(viewer->commit_list_win, COLOR_PAIR(1)); + } else { + wattroff(viewer->commit_list_win, COLOR_PAIR(2)); + } + + // Show commit title (always white, truncated to fit with "..") + int max_title_len = viewer->file_panel_width - 15; // Leave space for border + char truncated_title[256]; + if ((int)strlen(viewer->commits[i].title) > max_title_len) { + strncpy(truncated_title, viewer->commits[i].title, max_title_len - 2); + truncated_title[max_title_len - 2] = '\0'; + strcat(truncated_title, ".."); + } else { + strcpy(truncated_title, viewer->commits[i].title); + } + + mvwprintw(viewer->commit_list_win, y, 13, "%s", truncated_title); + } + + wrefresh(viewer->commit_list_win); } + /** * Render the file content window */ void render_file_content_window(NCursesDiffViewer *viewer) { - if (!viewer || !viewer->file_content_win) return; - - // Clear the window content area (but preserve borders) - int height, width; - getmaxyx(viewer->file_content_win, height, width); - for (int y = 1; y < height - 1; y++) { - wmove(viewer->file_content_win, y, 1); - for (int x = 1; x < width - 1; x++) { - waddch(viewer->file_content_win, ' '); - } + if (!viewer || !viewer->file_content_win) + return; + + // Clear the window content area (but preserve borders) + int height, width; + getmaxyx(viewer->file_content_win, height, width); + for (int y = 1; y < height - 1; y++) { + wmove(viewer->file_content_win, y, 1); + for (int x = 1; x < width - 1; x++) { + waddch(viewer->file_content_win, ' '); } - - // Draw rounded border - draw_rounded_box(viewer->file_content_win); - - if (viewer->file_count > 0 && viewer->selected_file < viewer->file_count) { - // Show file content (preview in list mode, scrollable in view mode) - if (viewer->current_mode == NCURSES_MODE_FILE_LIST) { - mvwprintw(viewer->file_content_win, 0, 2, " 2. %s (Preview) ", viewer->files[viewer->selected_file].filename); - } else { - mvwprintw(viewer->file_content_win, 0, 2, " 2. %s (Scrollable) ", viewer->files[viewer->selected_file].filename); - } - - int max_lines_visible = viewer->terminal_height - 4; - int content_width = viewer->terminal_width - viewer->file_panel_width - 3; - - for (int i = 0; i < max_lines_visible && - (i + viewer->file_scroll_offset) < viewer->file_line_count; i++) { - - int line_idx = i + viewer->file_scroll_offset; - NCursesFileLine *line = &viewer->file_lines[line_idx]; - - int y = i + 1; - - // Apply color based on line type - if (line->type == '+') { - wattron(viewer->file_content_win, COLOR_PAIR(1)); // Green for additions - } else if (line->type == '-') { - wattron(viewer->file_content_win, COLOR_PAIR(2)); // Red for deletions - } else if (line->type == '@') { - wattron(viewer->file_content_win, COLOR_PAIR(3)); // Cyan for hunk headers - } - - // Truncate line to fit window - char display_line[1024]; - if ((int)strlen(line->line) > content_width - 2) { - strncpy(display_line, line->line, content_width - 5); - display_line[content_width - 5] = '\0'; - strcat(display_line, "..."); - } else { - strcpy(display_line, line->line); - } - - mvwprintw(viewer->file_content_win, y, 1, "%s", display_line); - - // Reset color - if (line->type == '+') { - wattroff(viewer->file_content_win, COLOR_PAIR(1)); - } else if (line->type == '-') { - wattroff(viewer->file_content_win, COLOR_PAIR(2)); - } else if (line->type == '@') { - wattroff(viewer->file_content_win, COLOR_PAIR(3)); - } - } - - // Show scroll indicator - if (viewer->file_line_count > max_lines_visible) { - int scroll_pos = (viewer->file_scroll_offset * max_lines_visible) / viewer->file_line_count; - mvwprintw(viewer->file_content_win, scroll_pos + 1, content_width + 1, "â–ˆ"); - } - - // Show current position info only in file view mode - if (viewer->current_mode == NCURSES_MODE_FILE_VIEW) { - mvwprintw(viewer->file_content_win, max_lines_visible + 1, 1, - "Line %d-%d of %d", - viewer->file_scroll_offset + 1, - viewer->file_scroll_offset + max_lines_visible < viewer->file_line_count ? - viewer->file_scroll_offset + max_lines_visible : viewer->file_line_count, - viewer->file_line_count); - } else { - mvwprintw(viewer->file_content_win, max_lines_visible + 1, 1, - "Press Enter to enable scrolling"); - } + } + + // Draw rounded border + draw_rounded_box(viewer->file_content_win); + + if (viewer->file_count > 0 && viewer->selected_file < viewer->file_count) { + // Show file content (preview in list mode, scrollable in view mode) + if (viewer->current_mode == NCURSES_MODE_FILE_LIST) { + mvwprintw(viewer->file_content_win, 0, 2, " 2. %s (Preview) ", + viewer->files[viewer->selected_file].filename); + } else { + mvwprintw(viewer->file_content_win, 0, 2, " 2. %s (Scrollable) ", + viewer->files[viewer->selected_file].filename); } - - wrefresh(viewer->file_content_win); + + int max_lines_visible = viewer->terminal_height - 4; + int content_width = viewer->terminal_width - viewer->file_panel_width - 3; + + for (int i = 0; i < max_lines_visible && + (i + viewer->file_scroll_offset) < viewer->file_line_count; + i++) { + + int line_idx = i + viewer->file_scroll_offset; + NCursesFileLine *line = &viewer->file_lines[line_idx]; + + int y = i + 1; + + // Apply color based on line type + if (line->type == '+') { + wattron(viewer->file_content_win, COLOR_PAIR(1)); // Green for additions + } else if (line->type == '-') { + wattron(viewer->file_content_win, COLOR_PAIR(2)); // Red for deletions + } else if (line->type == '@') { + wattron(viewer->file_content_win, + COLOR_PAIR(3)); // Cyan for hunk headers + } + + // Truncate line to fit window + char display_line[1024]; + if ((int)strlen(line->line) > content_width - 2) { + strncpy(display_line, line->line, content_width - 5); + display_line[content_width - 5] = '\0'; + strcat(display_line, "..."); + } else { + strcpy(display_line, line->line); + } + + mvwprintw(viewer->file_content_win, y, 1, "%s", display_line); + + // Reset color + if (line->type == '+') { + wattroff(viewer->file_content_win, COLOR_PAIR(1)); + } else if (line->type == '-') { + wattroff(viewer->file_content_win, COLOR_PAIR(2)); + } else if (line->type == '@') { + wattroff(viewer->file_content_win, COLOR_PAIR(3)); + } + } + + // Show scroll indicator + if (viewer->file_line_count > max_lines_visible) { + int scroll_pos = (viewer->file_scroll_offset * max_lines_visible) / + viewer->file_line_count; + mvwprintw(viewer->file_content_win, scroll_pos + 1, content_width + 1, + "â–ˆ"); + } + + // Show current position info only in file view mode + if (viewer->current_mode == NCURSES_MODE_FILE_VIEW) { + mvwprintw(viewer->file_content_win, max_lines_visible + 1, 1, + "Line %d-%d of %d", viewer->file_scroll_offset + 1, + viewer->file_scroll_offset + max_lines_visible < + viewer->file_line_count + ? viewer->file_scroll_offset + max_lines_visible + : viewer->file_line_count, + viewer->file_line_count); + } else { + mvwprintw(viewer->file_content_win, max_lines_visible + 1, 1, + "Press Enter to enable scrolling"); + } + } + + wrefresh(viewer->file_content_win); } /** * Render the status bar */ void render_status_bar(NCursesDiffViewer *viewer) { - if (!viewer || !viewer->status_bar_win) return; - - // Clear status bar (no border) - werase(viewer->status_bar_win); - wbkgd(viewer->status_bar_win, COLOR_PAIR(3)); // Cyan background - - // Left side: Key bindings based on current mode - char keybindings[256] = ""; - if (viewer->current_mode == NCURSES_MODE_FILE_LIST) { - strcpy(keybindings, "Stage: | Stage All: a | Commit: c"); - } else if (viewer->current_mode == NCURSES_MODE_COMMIT_LIST) { - strcpy(keybindings, "Push: P | Pull: p | Reset: r/R | Amend: a | Nav: j/k"); - } else if (viewer->current_mode == NCURSES_MODE_FILE_VIEW) { - strcpy(keybindings, "Scroll: j/k | Page: Ctrl+U/D | Back: Esc"); + if (!viewer || !viewer->status_bar_win) + return; + + // Clear status bar (no border) + werase(viewer->status_bar_win); + wbkgd(viewer->status_bar_win, COLOR_PAIR(3)); // Cyan background + + // Left side: Key bindings based on current mode + char keybindings[256] = ""; + if (viewer->current_mode == NCURSES_MODE_FILE_LIST) { + strcpy(keybindings, "Stage: | Stage All: a | Commit: c"); + } else if (viewer->current_mode == NCURSES_MODE_COMMIT_LIST) { + strcpy(keybindings, "Push: P | Pull: p | Reset: r/R | Amend: a | Nav: j/k"); + } else if (viewer->current_mode == NCURSES_MODE_FILE_VIEW) { + strcpy(keybindings, "Scroll: j/k | Page: Ctrl+U/D | Back: Esc"); + } + + mvwprintw(viewer->status_bar_win, 0, 1, "%s", keybindings); + + // Right side: Sync status + char sync_text[64] = ""; + char *spinner_chars[] = {"|", "/", "-", "\\"}; + int spinner_idx = + viewer->spinner_frame % 4; // Change every frame (~50ms per character) + + if (viewer->sync_status == SYNC_STATUS_IDLE) { + // Show nothing when idle + strcpy(sync_text, ""); + } else if (viewer->sync_status >= SYNC_STATUS_SYNCING_APPEARING && + viewer->sync_status <= SYNC_STATUS_SYNCING_DISAPPEARING) { + // Show partial or full "Fetching" text with spinner + char full_text[] = "Fetching"; + int chars_to_show = viewer->text_char_count; + if (chars_to_show > 8) + chars_to_show = 8; // Max length of "Fetching" + if (chars_to_show < 0) + chars_to_show = 0; + + if (chars_to_show > 0) { + char partial_text[16]; + strncpy(partial_text, full_text, chars_to_show); + partial_text[chars_to_show] = '\0'; + + if (viewer->sync_status == SYNC_STATUS_SYNCING_VISIBLE) { + snprintf(sync_text, sizeof(sync_text), "%s %s", partial_text, + spinner_chars[spinner_idx]); + } else { + strcpy(sync_text, partial_text); + } + } else { + strcpy(sync_text, ""); } - - mvwprintw(viewer->status_bar_win, 0, 1, "%s", keybindings); - - // Right side: Sync status - char sync_text[64] = ""; - char *spinner_chars[] = {"|", "/", "-", "\\"}; - int spinner_idx = viewer->spinner_frame % 4; // Change every frame (~50ms per character) - - if (viewer->sync_status == SYNC_STATUS_IDLE) { - // Show nothing when idle - strcpy(sync_text, ""); - } else if (viewer->sync_status >= SYNC_STATUS_SYNCING_APPEARING && viewer->sync_status <= SYNC_STATUS_SYNCING_DISAPPEARING) { - // Show partial or full "Fetching" text with spinner - char full_text[] = "Fetching"; - int chars_to_show = viewer->text_char_count; - if (chars_to_show > 8) chars_to_show = 8; // Max length of "Fetching" - if (chars_to_show < 0) chars_to_show = 0; - - if (chars_to_show > 0) { - char partial_text[16]; - strncpy(partial_text, full_text, chars_to_show); - partial_text[chars_to_show] = '\0'; - - if (viewer->sync_status == SYNC_STATUS_SYNCING_VISIBLE) { - snprintf(sync_text, sizeof(sync_text), "%s %s", partial_text, spinner_chars[spinner_idx]); - } else { - strcpy(sync_text, partial_text); - } - } else { - strcpy(sync_text, ""); - } - } else if (viewer->sync_status >= SYNC_STATUS_PUSHING_APPEARING && viewer->sync_status <= SYNC_STATUS_PUSHING_DISAPPEARING) { - // Show partial or full "Pushing" text with spinner - char full_text[] = "Pushing"; - int chars_to_show = viewer->text_char_count; - if (chars_to_show > 7) chars_to_show = 7; // Max length of "Pushing" - if (chars_to_show < 0) chars_to_show = 0; - - if (chars_to_show > 0) { - char partial_text[16]; - strncpy(partial_text, full_text, chars_to_show); - partial_text[chars_to_show] = '\0'; - - if (viewer->sync_status == SYNC_STATUS_PUSHING_VISIBLE) { - snprintf(sync_text, sizeof(sync_text), "%s %s", partial_text, spinner_chars[spinner_idx]); - } else { - strcpy(sync_text, partial_text); - } - } else { - strcpy(sync_text, ""); - } - } else if (viewer->sync_status >= SYNC_STATUS_PULLING_APPEARING && viewer->sync_status <= SYNC_STATUS_PULLING_DISAPPEARING) { - // Show partial or full "Pulling" text with spinner - char full_text[] = "Pulling"; - int chars_to_show = viewer->text_char_count; - if (chars_to_show > 7) chars_to_show = 7; // Max length of "Pulling" - if (chars_to_show < 0) chars_to_show = 0; - - if (chars_to_show > 0) { - char partial_text[16]; - strncpy(partial_text, full_text, chars_to_show); - partial_text[chars_to_show] = '\0'; - - if (viewer->sync_status == SYNC_STATUS_PULLING_VISIBLE) { - snprintf(sync_text, sizeof(sync_text), "%s %s", partial_text, spinner_chars[spinner_idx]); - } else { - strcpy(sync_text, partial_text); - } - } else { - strcpy(sync_text, ""); - } - } else if (viewer->sync_status >= SYNC_STATUS_SYNCED_APPEARING && viewer->sync_status <= SYNC_STATUS_SYNCED_DISAPPEARING) { - // Show partial or full "Synced!" text (for fetching) - char full_text[] = "Synced!"; - int chars_to_show = viewer->text_char_count; - if (chars_to_show > 7) chars_to_show = 7; // Max length of "Synced!" - if (chars_to_show < 0) chars_to_show = 0; - - strncpy(sync_text, full_text, chars_to_show); - sync_text[chars_to_show] = '\0'; - } else if (viewer->sync_status >= SYNC_STATUS_PUSHED_APPEARING && viewer->sync_status <= SYNC_STATUS_PUSHED_DISAPPEARING) { - // Show partial or full "Pushed!" text - char full_text[] = "Pushed!"; - int chars_to_show = viewer->text_char_count; - if (chars_to_show > 7) chars_to_show = 7; // Max length of "Pushed!" - if (chars_to_show < 0) chars_to_show = 0; - - strncpy(sync_text, full_text, chars_to_show); - sync_text[chars_to_show] = '\0'; - } else if (viewer->sync_status >= SYNC_STATUS_PULLED_APPEARING && viewer->sync_status <= SYNC_STATUS_PULLED_DISAPPEARING) { - // Show partial or full "Pulled!" text - char full_text[] = "Pulled!"; - int chars_to_show = viewer->text_char_count; - if (chars_to_show > 7) chars_to_show = 7; // Max length of "Pulled!" - if (chars_to_show < 0) chars_to_show = 0; - - strncpy(sync_text, full_text, chars_to_show); - sync_text[chars_to_show] = '\0'; + } else if (viewer->sync_status >= SYNC_STATUS_PUSHING_APPEARING && + viewer->sync_status <= SYNC_STATUS_PUSHING_DISAPPEARING) { + // Show partial or full "Pushing" text with spinner + char full_text[] = "Pushing"; + int chars_to_show = viewer->text_char_count; + if (chars_to_show > 7) + chars_to_show = 7; // Max length of "Pushing" + if (chars_to_show < 0) + chars_to_show = 0; + + if (chars_to_show > 0) { + char partial_text[16]; + strncpy(partial_text, full_text, chars_to_show); + partial_text[chars_to_show] = '\0'; + + if (viewer->sync_status == SYNC_STATUS_PUSHING_VISIBLE) { + snprintf(sync_text, sizeof(sync_text), "%s %s", partial_text, + spinner_chars[spinner_idx]); + } else { + strcpy(sync_text, partial_text); + } + } else { + strcpy(sync_text, ""); } - - if (strlen(sync_text) > 0) { - int sync_text_pos = viewer->terminal_width - strlen(sync_text) - 1; // No border padding needed - - if (viewer->sync_status >= SYNC_STATUS_SYNCED_APPEARING && viewer->sync_status <= SYNC_STATUS_SYNCED_DISAPPEARING || - viewer->sync_status >= SYNC_STATUS_PUSHED_APPEARING && viewer->sync_status <= SYNC_STATUS_PUSHED_DISAPPEARING || - viewer->sync_status >= SYNC_STATUS_PULLED_APPEARING && viewer->sync_status <= SYNC_STATUS_PULLED_DISAPPEARING) { - wattron(viewer->status_bar_win, COLOR_PAIR(1)); // Green for success messages - mvwprintw(viewer->status_bar_win, 0, sync_text_pos, "%s", sync_text); - wattroff(viewer->status_bar_win, COLOR_PAIR(1)); - } else { - wattron(viewer->status_bar_win, COLOR_PAIR(4)); // Yellow for in-progress messages - mvwprintw(viewer->status_bar_win, 0, sync_text_pos, "%s", sync_text); - wattroff(viewer->status_bar_win, COLOR_PAIR(4)); - } + } else if (viewer->sync_status >= SYNC_STATUS_PULLING_APPEARING && + viewer->sync_status <= SYNC_STATUS_PULLING_DISAPPEARING) { + // Show partial or full "Pulling" text with spinner + char full_text[] = "Pulling"; + int chars_to_show = viewer->text_char_count; + if (chars_to_show > 7) + chars_to_show = 7; // Max length of "Pulling" + if (chars_to_show < 0) + chars_to_show = 0; + + if (chars_to_show > 0) { + char partial_text[16]; + strncpy(partial_text, full_text, chars_to_show); + partial_text[chars_to_show] = '\0'; + + if (viewer->sync_status == SYNC_STATUS_PULLING_VISIBLE) { + snprintf(sync_text, sizeof(sync_text), "%s %s", partial_text, + spinner_chars[spinner_idx]); + } else { + strcpy(sync_text, partial_text); + } + } else { + strcpy(sync_text, ""); } - - wrefresh(viewer->status_bar_win); - - // Ensure cursor stays hidden and positioned off-screen - move(viewer->terminal_height - 1, viewer->terminal_width - 1); - refresh(); + } else if (viewer->sync_status >= SYNC_STATUS_SYNCED_APPEARING && + viewer->sync_status <= SYNC_STATUS_SYNCED_DISAPPEARING) { + // Show partial or full "Synced!" text (for fetching) + char full_text[] = "Synced!"; + int chars_to_show = viewer->text_char_count; + if (chars_to_show > 7) + chars_to_show = 7; // Max length of "Synced!" + if (chars_to_show < 0) + chars_to_show = 0; + + strncpy(sync_text, full_text, chars_to_show); + sync_text[chars_to_show] = '\0'; + } else if (viewer->sync_status >= SYNC_STATUS_PUSHED_APPEARING && + viewer->sync_status <= SYNC_STATUS_PUSHED_DISAPPEARING) { + // Show partial or full "Pushed!" text + char full_text[] = "Pushed!"; + int chars_to_show = viewer->text_char_count; + if (chars_to_show > 7) + chars_to_show = 7; // Max length of "Pushed!" + if (chars_to_show < 0) + chars_to_show = 0; + + strncpy(sync_text, full_text, chars_to_show); + sync_text[chars_to_show] = '\0'; + } else if (viewer->sync_status >= SYNC_STATUS_PULLED_APPEARING && + viewer->sync_status <= SYNC_STATUS_PULLED_DISAPPEARING) { + // Show partial or full "Pulled!" text + char full_text[] = "Pulled!"; + int chars_to_show = viewer->text_char_count; + if (chars_to_show > 7) + chars_to_show = 7; // Max length of "Pulled!" + if (chars_to_show < 0) + chars_to_show = 0; + + strncpy(sync_text, full_text, chars_to_show); + sync_text[chars_to_show] = '\0'; + } + + if (strlen(sync_text) > 0) { + int sync_text_pos = viewer->terminal_width - strlen(sync_text) - + 1; // No border padding needed + + if (viewer->sync_status >= SYNC_STATUS_SYNCED_APPEARING && + viewer->sync_status <= SYNC_STATUS_SYNCED_DISAPPEARING || + viewer->sync_status >= SYNC_STATUS_PUSHED_APPEARING && + viewer->sync_status <= SYNC_STATUS_PUSHED_DISAPPEARING || + viewer->sync_status >= SYNC_STATUS_PULLED_APPEARING && + viewer->sync_status <= SYNC_STATUS_PULLED_DISAPPEARING) { + wattron(viewer->status_bar_win, + COLOR_PAIR(1)); // Green for success messages + mvwprintw(viewer->status_bar_win, 0, sync_text_pos, "%s", sync_text); + wattroff(viewer->status_bar_win, COLOR_PAIR(1)); + } else { + wattron(viewer->status_bar_win, + COLOR_PAIR(4)); // Yellow for in-progress messages + mvwprintw(viewer->status_bar_win, 0, sync_text_pos, "%s", sync_text); + wattroff(viewer->status_bar_win, COLOR_PAIR(4)); + } + } + + wrefresh(viewer->status_bar_win); + + // Ensure cursor stays hidden and positioned off-screen + move(viewer->terminal_height - 1, viewer->terminal_width - 1); + refresh(); } /** * Update sync status and check for new files */ void update_sync_status(NCursesDiffViewer *viewer) { - if (!viewer) return; - - time_t current_time = time(NULL); - - // Check if it's time to sync (every 30 seconds) - if (current_time - viewer->last_sync_time >= 30) { - viewer->sync_status = SYNC_STATUS_SYNCING_APPEARING; - viewer->last_sync_time = current_time; - viewer->animation_frame = 0; - viewer->text_char_count = 0; - - // Do the actual sync work immediately - get_ncurses_changed_files(viewer); - if (viewer->selected_file < viewer->file_count && viewer->file_count > 0) { - load_full_file_with_diff(viewer, viewer->files[viewer->selected_file].filename); - } - get_commit_history(viewer); + if (!viewer) + return; + + time_t current_time = time(NULL); + + // Check if it's time to sync (every 30 seconds) + if (current_time - viewer->last_sync_time >= 30) { + viewer->sync_status = SYNC_STATUS_SYNCING_APPEARING; + viewer->last_sync_time = current_time; + viewer->animation_frame = 0; + viewer->text_char_count = 0; + + // Do the actual sync work immediately + get_ncurses_changed_files(viewer); + if (viewer->selected_file < viewer->file_count && viewer->file_count > 0) { + load_full_file_with_diff(viewer, + viewer->files[viewer->selected_file].filename); } - - // Handle all animation states - if (viewer->sync_status != SYNC_STATUS_IDLE) { - viewer->animation_frame++; - - // Handle fetching animation (4 seconds total: appear + visible + disappear) - if (viewer->sync_status >= SYNC_STATUS_SYNCING_APPEARING && viewer->sync_status <= SYNC_STATUS_SYNCING_DISAPPEARING) { - if (viewer->sync_status == SYNC_STATUS_SYNCING_APPEARING) { - // Appearing: one character every 2 frames (0.1s) for "Fetching" (8 chars) - viewer->text_char_count = viewer->animation_frame / 2; - if (viewer->text_char_count >= 8) { - viewer->text_char_count = 8; - viewer->sync_status = SYNC_STATUS_SYNCING_VISIBLE; - viewer->animation_frame = 0; - } - } else if (viewer->sync_status == SYNC_STATUS_SYNCING_VISIBLE) { - // Visible with spinner for 2.4 seconds (48 frames) - if (viewer->animation_frame >= 48) { - viewer->sync_status = SYNC_STATUS_SYNCING_DISAPPEARING; - viewer->animation_frame = 0; - viewer->text_char_count = 8; - } - } else if (viewer->sync_status == SYNC_STATUS_SYNCING_DISAPPEARING) { - // Disappearing: remove one character every 2 frames (0.1s) - int chars_to_remove = viewer->animation_frame / 2; - viewer->text_char_count = 8 - chars_to_remove; - if (viewer->text_char_count <= 0) { - viewer->text_char_count = 0; - viewer->sync_status = SYNC_STATUS_SYNCED_APPEARING; - viewer->animation_frame = 0; - } - } + get_commit_history(viewer); + } + + // Handle all animation states + if (viewer->sync_status != SYNC_STATUS_IDLE) { + viewer->animation_frame++; + + // Handle fetching animation (4 seconds total: appear + visible + disappear) + if (viewer->sync_status >= SYNC_STATUS_SYNCING_APPEARING && + viewer->sync_status <= SYNC_STATUS_SYNCING_DISAPPEARING) { + if (viewer->sync_status == SYNC_STATUS_SYNCING_APPEARING) { + // Appearing: one character every 2 frames (0.1s) for "Fetching" (8 + // chars) + viewer->text_char_count = viewer->animation_frame / 2; + if (viewer->text_char_count >= 8) { + viewer->text_char_count = 8; + viewer->sync_status = SYNC_STATUS_SYNCING_VISIBLE; + viewer->animation_frame = 0; } - // Handle pushing animation (same 4 second pattern) - else if (viewer->sync_status >= SYNC_STATUS_PUSHING_APPEARING && viewer->sync_status <= SYNC_STATUS_PUSHING_DISAPPEARING) { - if (viewer->sync_status == SYNC_STATUS_PUSHING_APPEARING) { - // Appearing: one character every 2 frames (0.1s) for "Pushing" (7 chars) - viewer->text_char_count = viewer->animation_frame / 2; - if (viewer->text_char_count >= 7) { - viewer->text_char_count = 7; - viewer->sync_status = SYNC_STATUS_PUSHING_VISIBLE; - viewer->animation_frame = 0; - } - } else if (viewer->sync_status == SYNC_STATUS_PUSHING_VISIBLE) { - // Visible with spinner for 1.2 seconds (24 frames) - faster - if (viewer->animation_frame >= 24) { - viewer->sync_status = SYNC_STATUS_PUSHING_DISAPPEARING; - viewer->animation_frame = 0; - viewer->text_char_count = 7; - } - } else if (viewer->sync_status == SYNC_STATUS_PUSHING_DISAPPEARING) { - // Disappearing: remove one character every 2 frames (0.1s) - int chars_to_remove = viewer->animation_frame / 2; - viewer->text_char_count = 7 - chars_to_remove; - if (viewer->text_char_count <= 0) { - viewer->text_char_count = 0; - viewer->sync_status = SYNC_STATUS_PUSHED_APPEARING; - viewer->animation_frame = 0; - } - } + } else if (viewer->sync_status == SYNC_STATUS_SYNCING_VISIBLE) { + // Visible with spinner for 2.4 seconds (48 frames) + if (viewer->animation_frame >= 48) { + viewer->sync_status = SYNC_STATUS_SYNCING_DISAPPEARING; + viewer->animation_frame = 0; + viewer->text_char_count = 8; } - // Handle pulling animation (same pattern as pushing) - else if (viewer->sync_status >= SYNC_STATUS_PULLING_APPEARING && viewer->sync_status <= SYNC_STATUS_PULLING_DISAPPEARING) { - if (viewer->sync_status == SYNC_STATUS_PULLING_APPEARING) { - // Appearing: one character every 2 frames (0.1s) for "Pulling" (7 chars) - viewer->text_char_count = viewer->animation_frame / 2; - if (viewer->text_char_count >= 7) { - viewer->text_char_count = 7; - viewer->sync_status = SYNC_STATUS_PULLING_VISIBLE; - viewer->animation_frame = 0; - } - } else if (viewer->sync_status == SYNC_STATUS_PULLING_VISIBLE) { - // Visible with spinner for 1.2 seconds (24 frames) - faster - if (viewer->animation_frame >= 24) { - viewer->sync_status = SYNC_STATUS_PULLING_DISAPPEARING; - viewer->animation_frame = 0; - viewer->text_char_count = 7; - } - } else if (viewer->sync_status == SYNC_STATUS_PULLING_DISAPPEARING) { - // Disappearing: remove one character every 2 frames (0.1s) - int chars_to_remove = viewer->animation_frame / 2; - viewer->text_char_count = 7 - chars_to_remove; - if (viewer->text_char_count <= 0) { - viewer->text_char_count = 0; - viewer->sync_status = SYNC_STATUS_PULLED_APPEARING; - viewer->animation_frame = 0; - } - } - } - // Handle synced animation - else if (viewer->sync_status >= SYNC_STATUS_SYNCED_APPEARING && viewer->sync_status <= SYNC_STATUS_SYNCED_DISAPPEARING) { - if (viewer->sync_status == SYNC_STATUS_SYNCED_APPEARING) { - // Appearing: one character every 2 frames (0.1s) for "Synced!" (7 chars) - viewer->text_char_count = viewer->animation_frame / 2; - if (viewer->text_char_count >= 7) { - viewer->text_char_count = 7; - viewer->sync_status = SYNC_STATUS_SYNCED_VISIBLE; - viewer->animation_frame = 0; - } - } else if (viewer->sync_status == SYNC_STATUS_SYNCED_VISIBLE) { - // Visible for 3 seconds (60 frames) - if (viewer->animation_frame >= 60) { - viewer->sync_status = SYNC_STATUS_SYNCED_DISAPPEARING; - viewer->animation_frame = 0; - viewer->text_char_count = 7; - } - } else if (viewer->sync_status == SYNC_STATUS_SYNCED_DISAPPEARING) { - // Disappearing: remove one character every 2 frames (0.1s) - int chars_to_remove = viewer->animation_frame / 2; - viewer->text_char_count = 7 - chars_to_remove; - if (viewer->text_char_count <= 0) { - viewer->text_char_count = 0; - viewer->sync_status = SYNC_STATUS_IDLE; - } - } - } - // Handle pushed animation - else if (viewer->sync_status >= SYNC_STATUS_PUSHED_APPEARING && viewer->sync_status <= SYNC_STATUS_PUSHED_DISAPPEARING) { - if (viewer->sync_status == SYNC_STATUS_PUSHED_APPEARING) { - // Appearing: one character every 2 frames (0.1s) for "Pushed!" (7 chars) - viewer->text_char_count = viewer->animation_frame / 2; - if (viewer->text_char_count >= 7) { - viewer->text_char_count = 7; - viewer->sync_status = SYNC_STATUS_PUSHED_VISIBLE; - viewer->animation_frame = 0; - } - } else if (viewer->sync_status == SYNC_STATUS_PUSHED_VISIBLE) { - // Visible for 2 seconds (40 frames) - if (viewer->animation_frame >= 40) { - viewer->sync_status = SYNC_STATUS_PUSHED_DISAPPEARING; - viewer->animation_frame = 0; - viewer->text_char_count = 7; - } - } else if (viewer->sync_status == SYNC_STATUS_PUSHED_DISAPPEARING) { - // Disappearing: remove one character every 2 frames (0.1s) - int chars_to_remove = viewer->animation_frame / 2; - viewer->text_char_count = 7 - chars_to_remove; - if (viewer->text_char_count <= 0) { - viewer->text_char_count = 0; - viewer->sync_status = SYNC_STATUS_IDLE; - } - } - } - // Handle pulled animation - else if (viewer->sync_status >= SYNC_STATUS_PULLED_APPEARING && viewer->sync_status <= SYNC_STATUS_PULLED_DISAPPEARING) { - if (viewer->sync_status == SYNC_STATUS_PULLED_APPEARING) { - // Appearing: one character every 2 frames (0.1s) for "Pulled!" (7 chars) - viewer->text_char_count = viewer->animation_frame / 2; - if (viewer->text_char_count >= 7) { - viewer->text_char_count = 7; - viewer->sync_status = SYNC_STATUS_PULLED_VISIBLE; - viewer->animation_frame = 0; - } - } else if (viewer->sync_status == SYNC_STATUS_PULLED_VISIBLE) { - // Visible for 2 seconds (40 frames) - if (viewer->animation_frame >= 40) { - viewer->sync_status = SYNC_STATUS_PULLED_DISAPPEARING; - viewer->animation_frame = 0; - viewer->text_char_count = 7; - } - } else if (viewer->sync_status == SYNC_STATUS_PULLED_DISAPPEARING) { - // Disappearing: remove one character every 2 frames (0.1s) - int chars_to_remove = viewer->animation_frame / 2; - viewer->text_char_count = 7 - chars_to_remove; - if (viewer->text_char_count <= 0) { - viewer->text_char_count = 0; - viewer->sync_status = SYNC_STATUS_IDLE; - } - } + } else if (viewer->sync_status == SYNC_STATUS_SYNCING_DISAPPEARING) { + // Disappearing: remove one character every 2 frames (0.1s) + int chars_to_remove = viewer->animation_frame / 2; + viewer->text_char_count = 8 - chars_to_remove; + if (viewer->text_char_count <= 0) { + viewer->text_char_count = 0; + viewer->sync_status = SYNC_STATUS_SYNCED_APPEARING; + viewer->animation_frame = 0; } + } } - - // Always update spinner frame for spinner animation - viewer->spinner_frame++; - if (viewer->spinner_frame > 100) viewer->spinner_frame = 0; // Reset to prevent overflow + // Handle pushing animation (same 4 second pattern) + else if (viewer->sync_status >= SYNC_STATUS_PUSHING_APPEARING && + viewer->sync_status <= SYNC_STATUS_PUSHING_DISAPPEARING) { + if (viewer->sync_status == SYNC_STATUS_PUSHING_APPEARING) { + // Appearing: one character every 2 frames (0.1s) for "Pushing" (7 + // chars) + viewer->text_char_count = viewer->animation_frame / 2; + if (viewer->text_char_count >= 7) { + viewer->text_char_count = 7; + viewer->sync_status = SYNC_STATUS_PUSHING_VISIBLE; + viewer->animation_frame = 0; + } + } else if (viewer->sync_status == SYNC_STATUS_PUSHING_VISIBLE) { + // Visible with spinner for 1.2 seconds (24 frames) - faster + if (viewer->animation_frame >= 24) { + viewer->sync_status = SYNC_STATUS_PUSHING_DISAPPEARING; + viewer->animation_frame = 0; + viewer->text_char_count = 7; + } + } else if (viewer->sync_status == SYNC_STATUS_PUSHING_DISAPPEARING) { + // Disappearing: remove one character every 2 frames (0.1s) + int chars_to_remove = viewer->animation_frame / 2; + viewer->text_char_count = 7 - chars_to_remove; + if (viewer->text_char_count <= 0) { + viewer->text_char_count = 0; + viewer->sync_status = SYNC_STATUS_PUSHED_APPEARING; + viewer->animation_frame = 0; + } + } + } + // Handle pulling animation (same pattern as pushing) + else if (viewer->sync_status >= SYNC_STATUS_PULLING_APPEARING && + viewer->sync_status <= SYNC_STATUS_PULLING_DISAPPEARING) { + if (viewer->sync_status == SYNC_STATUS_PULLING_APPEARING) { + // Appearing: one character every 2 frames (0.1s) for "Pulling" (7 + // chars) + viewer->text_char_count = viewer->animation_frame / 2; + if (viewer->text_char_count >= 7) { + viewer->text_char_count = 7; + viewer->sync_status = SYNC_STATUS_PULLING_VISIBLE; + viewer->animation_frame = 0; + } + } else if (viewer->sync_status == SYNC_STATUS_PULLING_VISIBLE) { + // Visible with spinner for 1.2 seconds (24 frames) - faster + if (viewer->animation_frame >= 24) { + viewer->sync_status = SYNC_STATUS_PULLING_DISAPPEARING; + viewer->animation_frame = 0; + viewer->text_char_count = 7; + } + } else if (viewer->sync_status == SYNC_STATUS_PULLING_DISAPPEARING) { + // Disappearing: remove one character every 2 frames (0.1s) + int chars_to_remove = viewer->animation_frame / 2; + viewer->text_char_count = 7 - chars_to_remove; + if (viewer->text_char_count <= 0) { + viewer->text_char_count = 0; + viewer->sync_status = SYNC_STATUS_PULLED_APPEARING; + viewer->animation_frame = 0; + } + } + } + // Handle synced animation + else if (viewer->sync_status >= SYNC_STATUS_SYNCED_APPEARING && + viewer->sync_status <= SYNC_STATUS_SYNCED_DISAPPEARING) { + if (viewer->sync_status == SYNC_STATUS_SYNCED_APPEARING) { + // Appearing: one character every 2 frames (0.1s) for "Synced!" (7 + // chars) + viewer->text_char_count = viewer->animation_frame / 2; + if (viewer->text_char_count >= 7) { + viewer->text_char_count = 7; + viewer->sync_status = SYNC_STATUS_SYNCED_VISIBLE; + viewer->animation_frame = 0; + } + } else if (viewer->sync_status == SYNC_STATUS_SYNCED_VISIBLE) { + // Visible for 3 seconds (60 frames) + if (viewer->animation_frame >= 60) { + viewer->sync_status = SYNC_STATUS_SYNCED_DISAPPEARING; + viewer->animation_frame = 0; + viewer->text_char_count = 7; + } + } else if (viewer->sync_status == SYNC_STATUS_SYNCED_DISAPPEARING) { + // Disappearing: remove one character every 2 frames (0.1s) + int chars_to_remove = viewer->animation_frame / 2; + viewer->text_char_count = 7 - chars_to_remove; + if (viewer->text_char_count <= 0) { + viewer->text_char_count = 0; + viewer->sync_status = SYNC_STATUS_IDLE; + } + } + } + // Handle pushed animation + else if (viewer->sync_status >= SYNC_STATUS_PUSHED_APPEARING && + viewer->sync_status <= SYNC_STATUS_PUSHED_DISAPPEARING) { + if (viewer->sync_status == SYNC_STATUS_PUSHED_APPEARING) { + // Appearing: one character every 2 frames (0.1s) for "Pushed!" (7 + // chars) + viewer->text_char_count = viewer->animation_frame / 2; + if (viewer->text_char_count >= 7) { + viewer->text_char_count = 7; + viewer->sync_status = SYNC_STATUS_PUSHED_VISIBLE; + viewer->animation_frame = 0; + } + } else if (viewer->sync_status == SYNC_STATUS_PUSHED_VISIBLE) { + // Visible for 2 seconds (40 frames) + if (viewer->animation_frame >= 40) { + viewer->sync_status = SYNC_STATUS_PUSHED_DISAPPEARING; + viewer->animation_frame = 0; + viewer->text_char_count = 7; + } + } else if (viewer->sync_status == SYNC_STATUS_PUSHED_DISAPPEARING) { + // Disappearing: remove one character every 2 frames (0.1s) + int chars_to_remove = viewer->animation_frame / 2; + viewer->text_char_count = 7 - chars_to_remove; + if (viewer->text_char_count <= 0) { + viewer->text_char_count = 0; + viewer->sync_status = SYNC_STATUS_IDLE; + } + } + } + // Handle pulled animation + else if (viewer->sync_status >= SYNC_STATUS_PULLED_APPEARING && + viewer->sync_status <= SYNC_STATUS_PULLED_DISAPPEARING) { + if (viewer->sync_status == SYNC_STATUS_PULLED_APPEARING) { + // Appearing: one character every 2 frames (0.1s) for "Pulled!" (7 + // chars) + viewer->text_char_count = viewer->animation_frame / 2; + if (viewer->text_char_count >= 7) { + viewer->text_char_count = 7; + viewer->sync_status = SYNC_STATUS_PULLED_VISIBLE; + viewer->animation_frame = 0; + } + } else if (viewer->sync_status == SYNC_STATUS_PULLED_VISIBLE) { + // Visible for 2 seconds (40 frames) + if (viewer->animation_frame >= 40) { + viewer->sync_status = SYNC_STATUS_PULLED_DISAPPEARING; + viewer->animation_frame = 0; + viewer->text_char_count = 7; + } + } else if (viewer->sync_status == SYNC_STATUS_PULLED_DISAPPEARING) { + // Disappearing: remove one character every 2 frames (0.1s) + int chars_to_remove = viewer->animation_frame / 2; + viewer->text_char_count = 7 - chars_to_remove; + if (viewer->text_char_count <= 0) { + viewer->text_char_count = 0; + viewer->sync_status = SYNC_STATUS_IDLE; + } + } + } + } + + // Always update spinner frame for spinner animation + viewer->spinner_frame++; + if (viewer->spinner_frame > 100) + viewer->spinner_frame = 0; // Reset to prevent overflow } /** * Handle keyboard input for navigation */ int handle_ncurses_diff_input(NCursesDiffViewer *viewer, int key) { - if (!viewer) return 0; - - int max_lines_visible = viewer->terminal_height - 4; - - // Global quit commands - if (key == 'q' || key == 'Q') { - return 0; // Exit + if (!viewer) + return 0; + + int max_lines_visible = viewer->terminal_height - 4; + + // Global quit commands + if (key == 'q' || key == 'Q') { + return 0; // Exit + } + + // Global number key navigation + switch (key) { + case '1': + viewer->current_mode = NCURSES_MODE_FILE_LIST; + break; + case '2': + // Switch to file view mode and load selected file + if (viewer->file_count > 0 && viewer->selected_file < viewer->file_count) { + load_full_file_with_diff(viewer, + viewer->files[viewer->selected_file].filename); + viewer->current_mode = NCURSES_MODE_FILE_VIEW; } - - // Global number key navigation + break; + case '3': + viewer->current_mode = NCURSES_MODE_COMMIT_LIST; + break; + } + + if (viewer->current_mode == NCURSES_MODE_FILE_LIST) { + // File list mode navigation switch (key) { - case '1': - viewer->current_mode = NCURSES_MODE_FILE_LIST; - break; - case '2': - // Switch to file view mode and load selected file - if (viewer->file_count > 0 && viewer->selected_file < viewer->file_count) { - load_full_file_with_diff(viewer, viewer->files[viewer->selected_file].filename); - viewer->current_mode = NCURSES_MODE_FILE_VIEW; - } - break; - case '3': - viewer->current_mode = NCURSES_MODE_COMMIT_LIST; - break; + case 27: // ESC + return 0; // Exit from file list mode + + case KEY_UP: + case 'k': + if (viewer->selected_file > 0) { + viewer->selected_file--; + // Auto-preview the selected file + if (viewer->file_count > 0) { + load_full_file_with_diff( + viewer, viewer->files[viewer->selected_file].filename); + } + } + break; + + case KEY_DOWN: + case 'j': + if (viewer->selected_file < viewer->file_count - 1) { + viewer->selected_file++; + // Auto-preview the selected file + if (viewer->file_count > 0) { + load_full_file_with_diff( + viewer, viewer->files[viewer->selected_file].filename); + } + } + break; + + case ' ': // Space - toggle file marking + if (viewer->file_count > 0 && + viewer->selected_file < viewer->file_count) { + toggle_file_mark(viewer, viewer->selected_file); + } + break; + + case 'a': + case 'A': // Mark all files + mark_all_files(viewer); + break; + + case 'c': + case 'C': // Commit marked files + { + char commit_title[MAX_COMMIT_TITLE_LEN]; + char commit_message[2048]; + if (get_commit_title_input(commit_title, MAX_COMMIT_TITLE_LEN, + commit_message, sizeof(commit_message))) { + commit_marked_files(viewer, commit_title, commit_message); + } + } break; + + case '\t': // Tab - switch to commit list mode + viewer->current_mode = NCURSES_MODE_COMMIT_LIST; + break; + + case '\n': + case '\r': + case KEY_ENTER: + // Enter file view mode and load the selected file + if (viewer->file_count > 0 && + viewer->selected_file < viewer->file_count) { + load_full_file_with_diff(viewer, + viewer->files[viewer->selected_file].filename); + viewer->current_mode = NCURSES_MODE_FILE_VIEW; + } + break; } - - if (viewer->current_mode == NCURSES_MODE_FILE_LIST) { - // File list mode navigation - switch (key) { - case 27: // ESC - return 0; // Exit from file list mode - - case KEY_UP: - case 'k': - if (viewer->selected_file > 0) { - viewer->selected_file--; - // Auto-preview the selected file - if (viewer->file_count > 0) { - load_full_file_with_diff(viewer, viewer->files[viewer->selected_file].filename); - } - } - break; - - case KEY_DOWN: - case 'j': - if (viewer->selected_file < viewer->file_count - 1) { - viewer->selected_file++; - // Auto-preview the selected file - if (viewer->file_count > 0) { - load_full_file_with_diff(viewer, viewer->files[viewer->selected_file].filename); - } - } - break; - - case ' ': // Space - toggle file marking - if (viewer->file_count > 0 && viewer->selected_file < viewer->file_count) { - toggle_file_mark(viewer, viewer->selected_file); - } - break; - - case 'a': - case 'A': // Mark all files - mark_all_files(viewer); - break; - - case 'c': - case 'C': // Commit marked files - { - char commit_title[MAX_COMMIT_TITLE_LEN]; - char commit_message[2048]; - if (get_commit_title_input(commit_title, MAX_COMMIT_TITLE_LEN, commit_message, sizeof(commit_message))) { - commit_marked_files(viewer, commit_title, commit_message); - } - } - break; - - case '\t': // Tab - switch to commit list mode - viewer->current_mode = NCURSES_MODE_COMMIT_LIST; - break; - - case '\n': - case '\r': - case KEY_ENTER: - // Enter file view mode and load the selected file - if (viewer->file_count > 0 && viewer->selected_file < viewer->file_count) { - load_full_file_with_diff(viewer, viewer->files[viewer->selected_file].filename); - viewer->current_mode = NCURSES_MODE_FILE_VIEW; - } - break; + } else if (viewer->current_mode == NCURSES_MODE_FILE_VIEW) { + // File view mode navigation + switch (key) { + case 27: // ESC + viewer->current_mode = NCURSES_MODE_FILE_LIST; // Return to file list mode + break; + + case KEY_UP: + case 'k': + // Scroll content up + if (viewer->file_scroll_offset > 0) { + viewer->file_scroll_offset--; + } + break; + + case KEY_DOWN: + case 'j': + // Scroll content down + if (viewer->file_line_count > max_lines_visible && + viewer->file_scroll_offset < + viewer->file_line_count - max_lines_visible) { + viewer->file_scroll_offset++; + } + break; + + case 21: // Ctrl+U + // Scroll up 30 lines + viewer->file_scroll_offset -= 30; + if (viewer->file_scroll_offset < 0) { + viewer->file_scroll_offset = 0; + } + break; + + case 4: // Ctrl+D + // Scroll down 30 lines + if (viewer->file_line_count > max_lines_visible) { + viewer->file_scroll_offset += 30; + if (viewer->file_scroll_offset > + viewer->file_line_count - max_lines_visible) { + viewer->file_scroll_offset = + viewer->file_line_count - max_lines_visible; } - } else if (viewer->current_mode == NCURSES_MODE_FILE_VIEW) { - // File view mode navigation - switch (key) { - case 27: // ESC - viewer->current_mode = NCURSES_MODE_FILE_LIST; // Return to file list mode - break; - - case KEY_UP: - case 'k': - // Scroll content up - if (viewer->file_scroll_offset > 0) { - viewer->file_scroll_offset--; - } - break; - - case KEY_DOWN: - case 'j': - // Scroll content down - if (viewer->file_line_count > max_lines_visible && - viewer->file_scroll_offset < viewer->file_line_count - max_lines_visible) { - viewer->file_scroll_offset++; - } - break; - - case 21: // Ctrl+U - // Scroll up 30 lines - viewer->file_scroll_offset -= 30; - if (viewer->file_scroll_offset < 0) { - viewer->file_scroll_offset = 0; - } - break; - - case 4: // Ctrl+D - // Scroll down 30 lines - if (viewer->file_line_count > max_lines_visible) { - viewer->file_scroll_offset += 30; - if (viewer->file_scroll_offset > viewer->file_line_count - max_lines_visible) { - viewer->file_scroll_offset = viewer->file_line_count - max_lines_visible; - } - } - break; - - case KEY_NPAGE: // Page Down - case ' ': - // Scroll content down by page - if (viewer->file_line_count > max_lines_visible) { - viewer->file_scroll_offset += max_lines_visible; - if (viewer->file_scroll_offset > viewer->file_line_count - max_lines_visible) { - viewer->file_scroll_offset = viewer->file_line_count - max_lines_visible; - } - } - break; - - case KEY_PPAGE: // Page Up - // Scroll content up by page - viewer->file_scroll_offset -= max_lines_visible; - if (viewer->file_scroll_offset < 0) { - viewer->file_scroll_offset = 0; - } - break; - } - } else if (viewer->current_mode == NCURSES_MODE_COMMIT_LIST) { - // Commit list mode navigation - switch (key) { - case 27: // ESC - case '\t': // Tab - return to file list mode - viewer->current_mode = NCURSES_MODE_FILE_LIST; - break; - - case KEY_UP: - case 'k': - if (viewer->selected_commit > 0) { - viewer->selected_commit--; - } - break; - - case KEY_DOWN: - case 'j': - if (viewer->selected_commit < viewer->commit_count - 1) { - viewer->selected_commit++; - } - break; - - case 'P': // Push commit - if (viewer->commit_count > 0 && viewer->selected_commit < viewer->commit_count) { - push_commit(viewer, viewer->selected_commit); - } - break; - - case 'p': // Pull commits - pull_commits(viewer); - break; - - case 'r': // Reset (soft) - undo commit but keep changes - if (viewer->commit_count > 0 && viewer->selected_commit == 0) { - reset_commit_soft(viewer, viewer->selected_commit); - } - break; - - case 'R': // Reset (hard) - undo commit and discard changes - if (viewer->commit_count > 0 && viewer->selected_commit == 0) { - reset_commit_hard(viewer, viewer->selected_commit); - } - break; - - case 'a': - case 'A': // Amend most recent commit - if (viewer->commit_count > 0) { - amend_commit(viewer); - } - break; + } + break; + + case KEY_NPAGE: // Page Down + case ' ': + // Scroll content down by page + if (viewer->file_line_count > max_lines_visible) { + viewer->file_scroll_offset += max_lines_visible; + if (viewer->file_scroll_offset > + viewer->file_line_count - max_lines_visible) { + viewer->file_scroll_offset = + viewer->file_line_count - max_lines_visible; } + } + break; + + case KEY_PPAGE: // Page Up + // Scroll content up by page + viewer->file_scroll_offset -= max_lines_visible; + if (viewer->file_scroll_offset < 0) { + viewer->file_scroll_offset = 0; + } + break; } - - return 1; // Continue + } else if (viewer->current_mode == NCURSES_MODE_COMMIT_LIST) { + // Commit list mode navigation + switch (key) { + case 27: // ESC + case '\t': // Tab - return to file list mode + viewer->current_mode = NCURSES_MODE_FILE_LIST; + break; + + case KEY_UP: + case 'k': + if (viewer->selected_commit > 0) { + viewer->selected_commit--; + } + break; + + case KEY_DOWN: + case 'j': + if (viewer->selected_commit < viewer->commit_count - 1) { + viewer->selected_commit++; + } + break; + + case 'P': // Push commit + if (viewer->commit_count > 0 && + viewer->selected_commit < viewer->commit_count) { + push_commit(viewer, viewer->selected_commit); + } + break; + + case 'p': // Pull commits + pull_commits(viewer); + break; + + case 'r': // Reset (soft) - undo commit but keep changes + if (viewer->commit_count > 0 && viewer->selected_commit == 0) { + reset_commit_soft(viewer, viewer->selected_commit); + } + break; + + case 'R': // Reset (hard) - undo commit and discard changes + if (viewer->commit_count > 0 && viewer->selected_commit == 0) { + reset_commit_hard(viewer, viewer->selected_commit); + } + break; + + case 'a': + case 'A': // Amend most recent commit + if (viewer->commit_count > 0) { + amend_commit(viewer); + } + break; + } + } + + return 1; // Continue } /** * Run the ncurses diff viewer */ int run_ncurses_diff_viewer(void) { - NCursesDiffViewer viewer; - - if (!init_ncurses_diff_viewer(&viewer)) { - printf("Failed to initialize ncurses diff viewer\n"); - return 1; + NCursesDiffViewer viewer; + + if (!init_ncurses_diff_viewer(&viewer)) { + printf("Failed to initialize ncurses diff viewer\n"); + return 1; + } + + // Get changed files (can be 0, that's okay) + get_ncurses_changed_files(&viewer); + + // Load commit history + get_commit_history(&viewer); + + // Load the first file by default + if (viewer.file_count > 0) { + load_full_file_with_diff(&viewer, viewer.files[0].filename); + } + + // Initial display + attron(COLOR_PAIR(3)); + if (viewer.current_mode == NCURSES_MODE_FILE_LIST) { + mvprintw(0, 0, + "Git Diff Viewer: 1=files 2=view 3=commits | j/k=nav Space=mark " + "A=all C=commit P=push | q=quit"); + } else if (viewer.current_mode == NCURSES_MODE_FILE_VIEW) { + mvprintw(0, 0, + "Git Diff Viewer: 1=files 2=view 3=commits | j/k=scroll " + "Ctrl+U/D=30lines | q=quit"); + } else { + mvprintw(0, 0, + "Git Diff Viewer: 1=files 2=view 3=commits | j/k=nav P=push " + "p=pull r/R=reset a=amend | q=quit"); + } + attroff(COLOR_PAIR(3)); + refresh(); + + render_file_list_window(&viewer); + render_file_content_window(&viewer); + render_commit_list_window(&viewer); + render_status_bar(&viewer); + + // Main display loop + int running = 1; + NCursesViewMode last_mode = viewer.current_mode; + + while (running) { + // Only update title if mode changed + if (viewer.current_mode != last_mode) { + // Clear just the title line + move(0, 0); + clrtoeol(); + + attron(COLOR_PAIR(3)); + if (viewer.current_mode == NCURSES_MODE_FILE_LIST) { + mvprintw(0, 0, + "Git Diff Viewer: 1=files 2=view 3=commits | j/k=nav " + "Space=mark A=all C=commit P=push | q=quit"); + } else if (viewer.current_mode == NCURSES_MODE_FILE_VIEW) { + mvprintw(0, 0, + "Git Diff Viewer: 1=files 2=view 3=commits | j/k=scroll " + "Ctrl+U/D=30lines | q=quit"); + } else { + mvprintw(0, 0, + "Git Diff Viewer: 1=files 2=view 3=commits | j/k=nav P=push " + "p=pull r/R=reset a=amend | q=quit"); + } + attroff(COLOR_PAIR(3)); + refresh(); + last_mode = viewer.current_mode; } - - // Get changed files (can be 0, that's okay) - get_ncurses_changed_files(&viewer); - - // Load commit history - get_commit_history(&viewer); - - // Load the first file by default - if (viewer.file_count > 0) { - load_full_file_with_diff(&viewer, viewer.files[0].filename); - } - - // Initial display - attron(COLOR_PAIR(3)); - if (viewer.current_mode == NCURSES_MODE_FILE_LIST) { - mvprintw(0, 0, "Git Diff Viewer: 1=files 2=view 3=commits | j/k=nav Space=mark A=all C=commit P=push | q=quit"); - } else if (viewer.current_mode == NCURSES_MODE_FILE_VIEW) { - mvprintw(0, 0, "Git Diff Viewer: 1=files 2=view 3=commits | j/k=scroll Ctrl+U/D=30lines | q=quit"); - } else { - mvprintw(0, 0, "Git Diff Viewer: 1=files 2=view 3=commits | j/k=nav P=push p=pull r/R=reset a=amend | q=quit"); - } - attroff(COLOR_PAIR(3)); - refresh(); - + + // Update sync status and check for new files + update_sync_status(&viewer); + render_file_list_window(&viewer); render_file_content_window(&viewer); render_commit_list_window(&viewer); render_status_bar(&viewer); - - // Main display loop - int running = 1; - NCursesViewMode last_mode = viewer.current_mode; - - while (running) { - // Only update title if mode changed - if (viewer.current_mode != last_mode) { - // Clear just the title line - move(0, 0); - clrtoeol(); - - attron(COLOR_PAIR(3)); - if (viewer.current_mode == NCURSES_MODE_FILE_LIST) { - mvprintw(0, 0, "Git Diff Viewer: 1=files 2=view 3=commits | j/k=nav Space=mark A=all C=commit P=push | q=quit"); - } else if (viewer.current_mode == NCURSES_MODE_FILE_VIEW) { - mvprintw(0, 0, "Git Diff Viewer: 1=files 2=view 3=commits | j/k=scroll Ctrl+U/D=30lines | q=quit"); - } else { - mvprintw(0, 0, "Git Diff Viewer: 1=files 2=view 3=commits | j/k=nav P=push p=pull r/R=reset a=amend | q=quit"); - } - attroff(COLOR_PAIR(3)); - refresh(); - last_mode = viewer.current_mode; - } - - // Update sync status and check for new files - update_sync_status(&viewer); - - render_file_list_window(&viewer); - render_file_content_window(&viewer); - render_commit_list_window(&viewer); - render_status_bar(&viewer); - - // Keep cursor hidden - curs_set(0); - - int c = getch(); - if (c != ERR) { // Only process if a key was actually pressed - running = handle_ncurses_diff_input(&viewer, c); - } - - // Small delay to prevent excessive CPU usage and allow animations - usleep(50000); // 50ms delay + + // Keep cursor hidden + curs_set(0); + + int c = getch(); + if (c != ERR) { // Only process if a key was actually pressed + running = handle_ncurses_diff_input(&viewer, c); } - - cleanup_ncurses_diff_viewer(&viewer); - return 0; + + // Small delay to prevent excessive CPU usage and allow animations + usleep(50000); // 50ms delay + } + + cleanup_ncurses_diff_viewer(&viewer); + return 0; } /** * Clean up ncurses diff viewer resources */ void cleanup_ncurses_diff_viewer(NCursesDiffViewer *viewer) { - if (viewer) { - if (viewer->file_list_win) { - delwin(viewer->file_list_win); - } - if (viewer->file_content_win) { - delwin(viewer->file_content_win); - } - if (viewer->commit_list_win) { - delwin(viewer->commit_list_win); - } - if (viewer->status_bar_win) { - delwin(viewer->status_bar_win); - } + if (viewer) { + if (viewer->file_list_win) { + delwin(viewer->file_list_win); } - endwin(); + if (viewer->file_content_win) { + delwin(viewer->file_content_win); + } + if (viewer->commit_list_win) { + delwin(viewer->commit_list_win); + } + if (viewer->status_bar_win) { + delwin(viewer->status_bar_win); + } + } + endwin(); } diff --git a/ncurses_diff_viewer.o b/ncurses_diff_viewer.o index adf71941bfa85e2ce197c135ded075757256d9cc..107113b5c8d6eec8fc5c15ef1a02626e76fd0438 100644 GIT binary patch delta 14956 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 delta 42501 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%0A+=9X#j9_}%>Tn`{G=WFG7QQEq73oa(< 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

m*yl<^Tin`6@P}=&m`9;hX(bj&ZA@v9w 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 OEI-RNvF