mirror of
https://github.com/marcusDenslow/FERRUM.git
synced 2026-10-25 14:38:50 +01:00
1480 lines
56 KiB
C
1480 lines
56 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;
|
|
|
|
// Initial draw and main input loop
|
|
while (1) {
|
|
// Redraw title field with horizontal scrolling
|
|
werase(title_win);
|
|
box(title_win, 0, 0);
|
|
mvwprintw(title_win, 0, 2, " Title (Tab to switch, Enter to commit) ");
|
|
|
|
int visible_width = input_width - 4; // Account for borders
|
|
int title_len = strlen(title);
|
|
|
|
// 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
|
|
for (int i = display_start; i < display_end; i++) {
|
|
mvwaddch(title_win, 1, 1 + (i - display_start), title[i]);
|
|
}
|
|
|
|
// Position cursor at the end of visible text
|
|
int cursor_pos = title_len - title_scroll_offset;
|
|
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);
|
|
|
|
if (current_field == 0) {
|
|
wattron(title_win, A_REVERSE);
|
|
mvwchgat(title_win, 0, 2, 40, A_REVERSE, 0, NULL);
|
|
wattroff(title_win, A_REVERSE);
|
|
}
|
|
wrefresh(title_win);
|
|
|
|
// Redraw message field with vertical scrolling
|
|
werase(message_win);
|
|
box(message_win, 0, 0);
|
|
mvwprintw(message_win, 0, 2, " Message (Tab to switch, Enter for newline) ");
|
|
|
|
int visible_height = message_height - 2; // Account for borders
|
|
int message_visible_width = input_width - 3; // Account for borders
|
|
|
|
// Split message into lines for display
|
|
char display_lines[100][1024];
|
|
int line_count = 0;
|
|
int current_line_pos = 0;
|
|
|
|
display_lines[0][0] = '\0';
|
|
|
|
for (int i = 0; local_message[i] && line_count < 100; i++) {
|
|
if (local_message[i] == '\n') {
|
|
display_lines[line_count][current_line_pos] = '\0';
|
|
line_count++;
|
|
current_line_pos = 0;
|
|
if (line_count < 100) display_lines[line_count][0] = '\0';
|
|
} else {
|
|
if (current_line_pos >= message_visible_width - 1) {
|
|
// Word wrap
|
|
display_lines[line_count][current_line_pos] = '\0';
|
|
line_count++;
|
|
current_line_pos = 0;
|
|
if (line_count < 100) display_lines[line_count][0] = '\0';
|
|
}
|
|
if (line_count < 100) {
|
|
display_lines[line_count][current_line_pos] = local_message[i];
|
|
current_line_pos++;
|
|
}
|
|
}
|
|
}
|
|
if (current_line_pos > 0) line_count++;
|
|
|
|
// Display visible lines based on scroll offset
|
|
for (int i = 0; i < visible_height && (i + message_scroll_offset) < line_count; i++) {
|
|
int line_idx = i + message_scroll_offset;
|
|
mvwprintw(message_win, i + 1, 1, "%s", display_lines[line_idx]);
|
|
}
|
|
|
|
if (current_field == 1) {
|
|
wattron(message_win, A_REVERSE);
|
|
mvwchgat(message_win, 0, 2, 43, A_REVERSE, 0, NULL);
|
|
wattroff(message_win, A_REVERSE);
|
|
}
|
|
wrefresh(message_win);
|
|
|
|
// Enable input mode
|
|
noecho(); // Handle echo manually for better control
|
|
curs_set(1); // Show cursor
|
|
|
|
ch = getch();
|
|
if (ch == 27) break; // ESC to cancel
|
|
if (ch == '\t') {
|
|
// Switch between fields
|
|
current_field = !current_field;
|
|
} 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;
|
|
// Count current lines to see if we need to scroll
|
|
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; // Scroll down 2 lines
|
|
}
|
|
}
|
|
}
|
|
} 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;
|
|
}
|
|
}
|
|
} else {
|
|
int len = strlen(local_message);
|
|
if (len > 0) {
|
|
local_message[len - 1] = '\0';
|
|
}
|
|
}
|
|
} 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);
|
|
}
|
|
}
|
|
} else {
|
|
int len = strlen(local_message);
|
|
if (len < 2047) {
|
|
local_message[len] = ch;
|
|
local_message[len + 1] = '\0';
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Copy local message to output parameter
|
|
if (message && max_message_len > 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);
|
|
|
|
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
|
|
viewer->sync_status = SYNC_STATUS_PUSHING_APPEARING;
|
|
viewer->animation_frame = 0;
|
|
viewer->text_char_count = 0;
|
|
|
|
// 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;
|
|
}
|
|
|
|
/**
|
|
* 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 | 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_SYNCED_APPEARING && viewer->sync_status <= SYNC_STATUS_SYNCED_DISAPPEARING) {
|
|
// Show partial or full "Synced!" text
|
|
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';
|
|
}
|
|
|
|
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) {
|
|
wattron(viewer->status_bar_win, COLOR_PAIR(1)); // Green
|
|
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
|
|
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 2.6 seconds (52 frames)
|
|
if (viewer->animation_frame >= 52) {
|
|
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_SYNCED_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;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
if (get_ncurses_changed_files(&viewer) == 0) {
|
|
cleanup_ncurses_diff_viewer(&viewer);
|
|
printf("No changed files found\n");
|
|
return 1;
|
|
}
|
|
|
|
// 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 | 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();
|
|
} |