Files
ferrum-old/ncurses_diff_viewer.c
marcusDenslow 07cab0fc86 attempt at fixing unstaged files hanging around after commit, also some miner status changes to be more clear
this change is aiming to fix the files still appearing in unstaged files even though it was just comitted. also the Pushing command would say synced! when done, this was just a filler from the fetching feature. this now has a Pushed! status, the same has been added to pulling
2025-06-06 20:23:31 +02:00

1721 lines
66 KiB
C

/**
* ncurses_diff_viewer.c
* NCurses-based interactive diff viewer implementation
*/
#include "ncurses_diff_viewer.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include <time.h>
#include <locale.h>
/**
* 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;
}
/**
* 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;
}
/**
* 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);
}
/**
* 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);
}
/**
* 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)
}
/**
* 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;
}
// 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);
}
/**
* 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 (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
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;
}
/**
* 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;
}
}
/**
* 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;
}
// 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, ' ');
}
// 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
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;
}
} 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();
}
}
}
// 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 {
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;
}
/**
* 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) {
// 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;
}
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;
}
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;
}
return 0;
}
/**
* Render the file list window
*/
void render_file_list_window(NCursesDiffViewer *viewer) {
if (!viewer || !viewer->file_list_win) return;
// 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;
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));
}
}
}
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);
}
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, ' ');
}
}
// 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");
}
}
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: <space> | Stage All: a | Commit: c");
} else if (viewer->current_mode == NCURSES_MODE_COMMIT_LIST) {
strcpy(keybindings, "Push: P | Pull: p | 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, "");
}
} 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';
}
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);
}
// 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;
}
}
}
// 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
}
// 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;
}
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 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;
}
}
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;
}
}
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;
}
// 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 | 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 | 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
}
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);
}
}
endwin();
}