mirror of
https://github.com/marcusDenslow/FERRUM.git
synced 2026-10-25 12:22:24 +01:00
the ahead and behind wasnt working in the branch view, also pulling didnt work so the entire feature was broken, this is now fixed and works as expected atleast for the small test case i gave it
4677 lines
144 KiB
C
4677 lines
144 KiB
C
/**
|
|
* ncurses_diff_viewer.c
|
|
* NCurses-based interactive diff viewer implementation
|
|
*/
|
|
|
|
#include "ncurses_diff_viewer.h"
|
|
#include "git_integration.h"
|
|
#include <locale.h>
|
|
#include <ncurses.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/stat.h>
|
|
#include <time.h>
|
|
#include <unistd.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->file_cursor_line = 0;
|
|
viewer->selected_stash = 0;
|
|
viewer->selected_branch = 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_BLACK,
|
|
COLOR_WHITE); // Highlighted selection - low opacity line highlight
|
|
init_pair(6, COLOR_MAGENTA,
|
|
COLOR_BLACK); // Orange-ish color for commit hash
|
|
}
|
|
|
|
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
|
|
|
|
// Distribute height among 4 panels: file, commit, branch, stash
|
|
viewer->file_panel_height = available_height * 0.3; // 30%
|
|
viewer->commit_panel_height = available_height * 0.3; // 30%
|
|
viewer->branch_panel_height = available_height * 0.2; // 20%
|
|
viewer->stash_panel_height = available_height - viewer->file_panel_height -
|
|
viewer->commit_panel_height -
|
|
viewer->branch_panel_height -
|
|
3; // Rest minus separators
|
|
|
|
// Position status bar right after the main content
|
|
int status_bar_y = 1 + available_height;
|
|
|
|
// Create six windows: file_list, branch_list, commit_list, stash_list,
|
|
// file_content, status_bar
|
|
viewer->file_list_win =
|
|
newwin(viewer->file_panel_height, viewer->file_panel_width, 1, 0);
|
|
viewer->branch_list_win =
|
|
newwin(viewer->branch_panel_height, viewer->file_panel_width,
|
|
1 + viewer->file_panel_height + 1, 0);
|
|
viewer->commit_list_win = newwin(
|
|
viewer->commit_panel_height, viewer->file_panel_width,
|
|
1 + viewer->file_panel_height + 1 + viewer->branch_panel_height + 1, 0);
|
|
viewer->stash_list_win =
|
|
newwin(viewer->stash_panel_height, viewer->file_panel_width,
|
|
1 + viewer->file_panel_height + 1 + viewer->branch_panel_height +
|
|
1 + viewer->commit_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->stash_list_win ||
|
|
!viewer->branch_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;
|
|
viewer->file_cursor_line = 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;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Show confirmation dialog for diverged branch
|
|
* Returns 1 for force push, 0 for cancel
|
|
*/
|
|
int show_diverged_branch_dialog(int commits_ahead, int commits_behind) {
|
|
// Save current screen
|
|
WINDOW *saved_screen = dupwin(stdscr);
|
|
|
|
// Calculate window dimensions
|
|
int dialog_width = 60;
|
|
int dialog_height = 8;
|
|
int start_x = COLS / 2 - dialog_width / 2;
|
|
int start_y = LINES / 2 - dialog_height / 2;
|
|
|
|
// Create dialog window
|
|
WINDOW *dialog_win = newwin(dialog_height, dialog_width, start_y, start_x);
|
|
if (!dialog_win) {
|
|
if (saved_screen)
|
|
delwin(saved_screen);
|
|
return 0;
|
|
}
|
|
|
|
// Draw dialog
|
|
wattron(dialog_win, COLOR_PAIR(3)); // Red for warning
|
|
box(dialog_win, 0, 0);
|
|
|
|
// Title
|
|
mvwprintw(dialog_win, 1, 2, "Branch has diverged!");
|
|
|
|
// Message
|
|
mvwprintw(dialog_win, 3, 2, "Local: %d commit(s) ahead", commits_ahead);
|
|
mvwprintw(dialog_win, 4, 2, "Remote: %d commit(s) ahead", commits_behind);
|
|
|
|
// Instructions
|
|
mvwprintw(dialog_win, 6, 2, "Force push anyway? (y/N):");
|
|
wattroff(dialog_win, COLOR_PAIR(3));
|
|
|
|
wrefresh(dialog_win);
|
|
|
|
// Get user input
|
|
int ch;
|
|
int result = 0;
|
|
|
|
while ((ch = wgetch(dialog_win)) != ERR) {
|
|
if (ch == 'y' || ch == 'Y') {
|
|
result = 1;
|
|
break;
|
|
} else if (ch == 'n' || ch == 'N' || ch == 27 ||
|
|
ch == 'q') { // ESC or q or n
|
|
result = 0;
|
|
break;
|
|
} else if (ch == '\n' || ch == '\r') {
|
|
result = 0; // Default to no on Enter
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Cleanup
|
|
delwin(dialog_win);
|
|
if (saved_screen) {
|
|
touchwin(saved_screen);
|
|
wrefresh(saved_screen);
|
|
delwin(saved_screen);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Show confirmation dialog requiring user to type "yes"
|
|
* Returns 1 if confirmed, 0 if cancelled
|
|
*/
|
|
int show_reset_confirmation_dialog(void) {
|
|
// Save current screen
|
|
WINDOW *saved_screen = dupwin(stdscr);
|
|
|
|
// Calculate window dimensions
|
|
int dialog_width = 60;
|
|
int dialog_height = 10;
|
|
int start_x = COLS / 2 - dialog_width / 2;
|
|
int start_y = LINES / 2 - dialog_height / 2;
|
|
|
|
// Create dialog window
|
|
WINDOW *dialog_win = newwin(dialog_height, dialog_width, start_y, start_x);
|
|
if (!dialog_win) {
|
|
if (saved_screen)
|
|
delwin(saved_screen);
|
|
return 0;
|
|
}
|
|
|
|
char input_buffer[10] = "";
|
|
int input_pos = 0;
|
|
|
|
while (1) {
|
|
// Clear and redraw dialog
|
|
werase(dialog_win);
|
|
wattron(dialog_win, COLOR_PAIR(3)); // Red for warning
|
|
box(dialog_win, 0, 0);
|
|
|
|
// Title
|
|
mvwprintw(dialog_win, 1, 2, "HARD RESET WARNING!");
|
|
|
|
// Warning message
|
|
mvwprintw(dialog_win, 3, 2, "This will permanently delete the most recent");
|
|
mvwprintw(dialog_win, 4, 2, "commit and ALL uncommitted changes!");
|
|
|
|
// Instructions
|
|
mvwprintw(dialog_win, 6, 2, "Type 'yes' to confirm or ESC to cancel:");
|
|
|
|
// Input field
|
|
mvwprintw(dialog_win, 7, 2, "> %s", input_buffer);
|
|
|
|
wattroff(dialog_win, COLOR_PAIR(3));
|
|
wrefresh(dialog_win);
|
|
|
|
// Position cursor
|
|
wmove(dialog_win, 7, 4 + strlen(input_buffer));
|
|
|
|
// Get user input
|
|
int ch = wgetch(dialog_win);
|
|
|
|
if (ch == 27 || ch == 'q') { // ESC or q
|
|
break;
|
|
} else if (ch == '\n' || ch == '\r') {
|
|
// Check if input is "yes" (case insensitive)
|
|
if (strcasecmp(input_buffer, "yes") == 0) {
|
|
// Cleanup and return confirmed
|
|
delwin(dialog_win);
|
|
if (saved_screen) {
|
|
touchwin(saved_screen);
|
|
wrefresh(saved_screen);
|
|
delwin(saved_screen);
|
|
}
|
|
return 1;
|
|
} else {
|
|
// Wrong input, clear buffer
|
|
input_buffer[0] = '\0';
|
|
input_pos = 0;
|
|
}
|
|
} else if (ch == KEY_BACKSPACE || ch == 127 || ch == 8) {
|
|
// Backspace
|
|
if (input_pos > 0) {
|
|
input_pos--;
|
|
input_buffer[input_pos] = '\0';
|
|
}
|
|
} else if (ch >= 32 && ch <= 126 && input_pos < 8) {
|
|
// Regular character
|
|
input_buffer[input_pos] = ch;
|
|
input_pos++;
|
|
input_buffer[input_pos] = '\0';
|
|
}
|
|
}
|
|
|
|
// Cleanup and return cancelled
|
|
delwin(dialog_win);
|
|
if (saved_screen) {
|
|
touchwin(saved_screen);
|
|
wrefresh(saved_screen);
|
|
delwin(saved_screen);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* 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) {
|
|
// Small delay to ensure git has processed the commit
|
|
usleep(100000); // 100ms delay
|
|
|
|
// Refresh file list and commit history
|
|
get_ncurses_changed_files(viewer);
|
|
get_commit_history(viewer);
|
|
|
|
// Reset selection if no files remain
|
|
if (viewer->file_count == 0) {
|
|
viewer->selected_file = 0;
|
|
viewer->file_line_count = 0;
|
|
viewer->file_scroll_offset = 0;
|
|
} else if (viewer->selected_file >= viewer->file_count) {
|
|
viewer->selected_file = viewer->file_count - 1;
|
|
}
|
|
|
|
// Reload the currently selected file's diff if files still exist
|
|
if (viewer->file_count > 0 && viewer->selected_file < viewer->file_count) {
|
|
load_full_file_with_diff(viewer,
|
|
viewer->files[viewer->selected_file].filename);
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Reset commit (soft) - undo commit but keep changes
|
|
*/
|
|
int reset_commit_soft(NCursesDiffViewer *viewer, int commit_index) {
|
|
if (!viewer || commit_index < 0 || commit_index >= viewer->commit_count)
|
|
return 0;
|
|
|
|
// Only allow reset of the most recent commit (index 0)
|
|
if (commit_index != 0)
|
|
return 0;
|
|
|
|
// Do soft reset of HEAD~1
|
|
int result = system("git reset --soft HEAD~1 2>/dev/null >/dev/null");
|
|
|
|
if (result == 0) {
|
|
// Small delay to ensure git has processed the reset
|
|
usleep(100000); // 100ms delay
|
|
|
|
// Refresh everything
|
|
get_ncurses_changed_files(viewer);
|
|
get_commit_history(viewer);
|
|
|
|
// Reload current file if any files exist
|
|
if (viewer->file_count > 0 && viewer->selected_file < viewer->file_count) {
|
|
load_full_file_with_diff(viewer,
|
|
viewer->files[viewer->selected_file].filename);
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Reset commit (hard) - undo commit and discard changes
|
|
*/
|
|
int reset_commit_hard(NCursesDiffViewer *viewer, int commit_index) {
|
|
if (!viewer || commit_index < 0 || commit_index >= viewer->commit_count)
|
|
return 0;
|
|
|
|
// Only allow reset of the most recent commit (index 0)
|
|
if (commit_index != 0)
|
|
return 0;
|
|
|
|
// Show confirmation dialog
|
|
if (!show_reset_confirmation_dialog()) {
|
|
return 0; // User cancelled
|
|
}
|
|
|
|
// Do hard reset of HEAD~1
|
|
int result = system("git reset --hard HEAD~1 2>/dev/null >/dev/null");
|
|
|
|
if (result == 0) {
|
|
// Small delay to ensure git has processed the reset
|
|
usleep(100000); // 100ms delay
|
|
|
|
// Refresh everything
|
|
get_ncurses_changed_files(viewer);
|
|
get_commit_history(viewer);
|
|
|
|
// Reset file selection since changes are discarded
|
|
viewer->selected_file = 0;
|
|
viewer->file_line_count = 0;
|
|
viewer->file_scroll_offset = 0;
|
|
|
|
// Reload current file if any files exist
|
|
if (viewer->file_count > 0 && viewer->selected_file < viewer->file_count) {
|
|
load_full_file_with_diff(viewer,
|
|
viewer->files[viewer->selected_file].filename);
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Amend the most recent commit
|
|
*/
|
|
int amend_commit(NCursesDiffViewer *viewer) {
|
|
if (!viewer || viewer->commit_count == 0)
|
|
return 0;
|
|
|
|
// Get current commit message
|
|
char current_title[MAX_COMMIT_TITLE_LEN] = "";
|
|
char current_message[2048] = "";
|
|
|
|
// Get the current commit message
|
|
FILE *fp = popen("git log -1 --pretty=format:%s 2>/dev/null", "r");
|
|
if (fp) {
|
|
fgets(current_title, sizeof(current_title), fp);
|
|
pclose(fp);
|
|
}
|
|
|
|
// Get the current commit body (if any)
|
|
fp = popen("git log -1 --pretty=format:%b 2>/dev/null", "r");
|
|
if (fp) {
|
|
fgets(current_message, sizeof(current_message), fp);
|
|
pclose(fp);
|
|
}
|
|
|
|
// Get new commit message from user (pre-filled with current message)
|
|
char new_title[MAX_COMMIT_TITLE_LEN];
|
|
char new_message[2048];
|
|
|
|
strncpy(new_title, current_title, sizeof(new_title) - 1);
|
|
new_title[sizeof(new_title) - 1] = '\0';
|
|
strncpy(new_message, current_message, sizeof(new_message) - 1);
|
|
new_message[sizeof(new_message) - 1] = '\0';
|
|
|
|
if (get_commit_title_input(new_title, MAX_COMMIT_TITLE_LEN, new_message,
|
|
sizeof(new_message))) {
|
|
// Add any marked files first
|
|
for (int i = 0; i < viewer->file_count; i++) {
|
|
if (viewer->files[i].marked_for_commit) {
|
|
char cmd[1024];
|
|
snprintf(cmd, sizeof(cmd), "git add \"%s\" 2>/dev/null >/dev/null",
|
|
viewer->files[i].filename);
|
|
system(cmd);
|
|
}
|
|
}
|
|
|
|
// Amend the commit with new message
|
|
char amend_cmd[2048];
|
|
if (strlen(new_message) > 0) {
|
|
snprintf(amend_cmd, sizeof(amend_cmd),
|
|
"git commit --amend -m \"%s\" -m \"%s\" 2>/dev/null >/dev/null",
|
|
new_title, new_message);
|
|
} else {
|
|
snprintf(amend_cmd, sizeof(amend_cmd),
|
|
"git commit --amend -m \"%s\" 2>/dev/null >/dev/null",
|
|
new_title);
|
|
}
|
|
|
|
int result = system(amend_cmd);
|
|
|
|
if (result == 0) {
|
|
// Small delay to ensure git has processed the amend
|
|
usleep(100000); // 100ms delay
|
|
|
|
// Refresh everything
|
|
get_ncurses_changed_files(viewer);
|
|
get_commit_history(viewer);
|
|
|
|
// Reset selection if no files remain
|
|
if (viewer->file_count == 0) {
|
|
viewer->selected_file = 0;
|
|
viewer->file_line_count = 0;
|
|
viewer->file_scroll_offset = 0;
|
|
} else if (viewer->selected_file >= viewer->file_count) {
|
|
viewer->selected_file = viewer->file_count - 1;
|
|
}
|
|
|
|
// Reload current file if any files exist
|
|
if (viewer->file_count > 0 &&
|
|
viewer->selected_file < viewer->file_count) {
|
|
load_full_file_with_diff(viewer,
|
|
viewer->files[viewer->selected_file].filename);
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
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;
|
|
|
|
// Check if current branch has upstream
|
|
char current_branch[256];
|
|
if (!get_current_branch_name(current_branch, sizeof(current_branch))) {
|
|
show_error_popup("Failed to get current branch name");
|
|
viewer->sync_status = SYNC_STATUS_IDLE;
|
|
return 0;
|
|
}
|
|
|
|
if (!branch_has_upstream(current_branch)) {
|
|
// Show upstream selection dialog
|
|
char upstream_selection[512];
|
|
if (show_upstream_selection_dialog(current_branch, upstream_selection,
|
|
sizeof(upstream_selection))) {
|
|
// User selected an upstream, set it up
|
|
char cmd[1024];
|
|
snprintf(cmd, sizeof(cmd), "git push --set-upstream %s >/dev/null 2>&1",
|
|
upstream_selection);
|
|
|
|
int result = system(cmd);
|
|
if (result == 0) {
|
|
// Upstream set successfully, show success and refresh
|
|
viewer->sync_status = SYNC_STATUS_PUSHED_APPEARING;
|
|
viewer->animation_frame = 0;
|
|
viewer->text_char_count = 0;
|
|
get_commit_history(viewer);
|
|
|
|
// Only refresh the commit pane
|
|
werase(viewer->commit_list_win);
|
|
render_commit_list_window(viewer);
|
|
wrefresh(viewer->commit_list_win);
|
|
|
|
return 1;
|
|
} else {
|
|
show_error_popup(
|
|
"Failed to set upstream and push. Check your connection.");
|
|
}
|
|
}
|
|
|
|
viewer->sync_status = SYNC_STATUS_IDLE;
|
|
return 0;
|
|
}
|
|
|
|
// Check for branch divergence first
|
|
int commits_ahead = 0;
|
|
int commits_behind = 0;
|
|
int is_diverged = check_branch_divergence(&commits_ahead, &commits_behind);
|
|
|
|
// If diverged, show confirmation dialog
|
|
if (is_diverged) {
|
|
if (!show_diverged_branch_dialog(commits_ahead, commits_behind)) {
|
|
// User cancelled
|
|
viewer->sync_status = SYNC_STATUS_IDLE;
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
// Animation already started by key handler for immediate feedback
|
|
|
|
// Do the actual push work
|
|
int result;
|
|
if (is_diverged) {
|
|
// Use force push with lease for safety
|
|
result = system("git push --force-with-lease origin >/dev/null 2>&1");
|
|
} else {
|
|
// Normal push
|
|
result = system("git push origin >/dev/null 2>&1");
|
|
}
|
|
|
|
if (result == 0) {
|
|
// Immediately transition to "Pushed!" animation
|
|
viewer->sync_status = SYNC_STATUS_PUSHED_APPEARING;
|
|
viewer->animation_frame = 0;
|
|
viewer->text_char_count = 0;
|
|
|
|
// Refresh commit history to get proper push status
|
|
get_commit_history(viewer);
|
|
|
|
// Only refresh the commit pane
|
|
werase(viewer->commit_list_win);
|
|
render_commit_list_window(viewer);
|
|
wrefresh(viewer->commit_list_win);
|
|
|
|
return 1;
|
|
} else {
|
|
// Push failed, show error
|
|
show_error_popup(
|
|
"Push failed. Check your network connection and authentication.");
|
|
viewer->sync_status = SYNC_STATUS_IDLE;
|
|
}
|
|
|
|
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;
|
|
|
|
// CRITICAL: Clear the entire window first
|
|
werase(viewer->file_list_win);
|
|
|
|
// Draw rounded border and title
|
|
draw_rounded_box(viewer->file_list_win);
|
|
mvwprintw(viewer->file_list_win, 0, 2, " 1. Files ");
|
|
|
|
int max_files_visible = viewer->file_panel_height - 2;
|
|
|
|
// CRITICAL: Clear the content area with spaces FIRST
|
|
for (int y = 1; y < viewer->file_panel_height - 1; y++) {
|
|
for (int x = 1; x < viewer->file_panel_width - 1; x++) {
|
|
mvwaddch(viewer->file_list_win, y, x, ' ');
|
|
}
|
|
}
|
|
|
|
for (int i = 0; i < max_files_visible; i++) {
|
|
int y = i + 1;
|
|
|
|
// Skip if no more files
|
|
if (i >= viewer->file_count)
|
|
continue;
|
|
|
|
// Check if this line should be highlighted
|
|
int is_selected = (i == viewer->selected_file &&
|
|
viewer->current_mode == NCURSES_MODE_FILE_LIST);
|
|
int is_marked = (i == viewer->selected_file &&
|
|
viewer->current_mode != NCURSES_MODE_FILE_LIST);
|
|
|
|
// Apply line highlight if selected
|
|
if (is_selected) {
|
|
wattron(viewer->file_list_win, COLOR_PAIR(5));
|
|
}
|
|
|
|
// Show selection indicator
|
|
if (is_selected) {
|
|
mvwprintw(viewer->file_list_win, y, 1, ">");
|
|
} else if (is_marked) {
|
|
wattron(viewer->file_list_win, COLOR_PAIR(1));
|
|
mvwprintw(viewer->file_list_win, y, 1, "*");
|
|
wattroff(viewer->file_list_win, COLOR_PAIR(1));
|
|
} else {
|
|
mvwprintw(viewer->file_list_win, y, 1, " ");
|
|
}
|
|
|
|
// Status indicator - turn off highlight temporarily for colored status
|
|
if (is_selected) {
|
|
wattroff(viewer->file_list_win, COLOR_PAIR(5));
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
// Restore highlight if it was on
|
|
if (is_selected) {
|
|
wattron(viewer->file_list_win, COLOR_PAIR(5));
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
|
|
// Check if marked for commit and apply green color
|
|
if (viewer->files[i].marked_for_commit) {
|
|
if (is_selected) {
|
|
wattroff(viewer->file_list_win, COLOR_PAIR(5));
|
|
}
|
|
wattron(viewer->file_list_win, COLOR_PAIR(1));
|
|
mvwprintw(viewer->file_list_win, y, 4, "%s", truncated_name);
|
|
wattroff(viewer->file_list_win, COLOR_PAIR(1));
|
|
if (is_selected) {
|
|
wattron(viewer->file_list_win, COLOR_PAIR(5));
|
|
}
|
|
} else {
|
|
mvwprintw(viewer->file_list_win, y, 4, "%s", truncated_name);
|
|
}
|
|
|
|
// Turn off line highlight if it was applied
|
|
if (is_selected) {
|
|
wattroff(viewer->file_list_win, COLOR_PAIR(5));
|
|
}
|
|
}
|
|
|
|
wrefresh(viewer->file_list_win);
|
|
}
|
|
|
|
/**
|
|
* Render the commit list window
|
|
*/
|
|
|
|
void render_commit_list_window(NCursesDiffViewer *viewer) {
|
|
if (!viewer || !viewer->commit_list_win)
|
|
return;
|
|
|
|
// CRITICAL: Clear the entire window first
|
|
werase(viewer->commit_list_win);
|
|
|
|
// Draw rounded border and title
|
|
draw_rounded_box(viewer->commit_list_win);
|
|
mvwprintw(viewer->commit_list_win, 0, 2, " 4. Commits ");
|
|
|
|
int max_commits_visible = viewer->commit_panel_height - 2;
|
|
|
|
// CRITICAL: Clear the content area with spaces FIRST
|
|
for (int y = 1; y < viewer->commit_panel_height - 1; y++) {
|
|
for (int x = 1; x < viewer->file_panel_width - 1; x++) {
|
|
mvwaddch(viewer->commit_list_win, y, x, ' ');
|
|
}
|
|
}
|
|
|
|
for (int i = 0; i < max_commits_visible; i++) {
|
|
int y = i + 1;
|
|
|
|
// Skip if no more commits
|
|
if (i >= viewer->commit_count)
|
|
continue;
|
|
|
|
// Check if this commit line should be highlighted
|
|
int is_selected_commit = (i == viewer->selected_commit &&
|
|
viewer->current_mode == NCURSES_MODE_COMMIT_LIST);
|
|
|
|
// Apply line highlight if selected
|
|
if (is_selected_commit) {
|
|
wattron(viewer->commit_list_win, COLOR_PAIR(5));
|
|
}
|
|
|
|
// Show selection indicator
|
|
if (is_selected_commit) {
|
|
mvwprintw(viewer->commit_list_win, y, 1, ">");
|
|
} else {
|
|
mvwprintw(viewer->commit_list_win, y, 1, " ");
|
|
}
|
|
|
|
// Show commit hash with color based on push status
|
|
if (is_selected_commit) {
|
|
wattroff(viewer->commit_list_win, COLOR_PAIR(5));
|
|
}
|
|
|
|
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));
|
|
}
|
|
|
|
if (is_selected_commit) {
|
|
wattron(viewer->commit_list_win, COLOR_PAIR(5));
|
|
}
|
|
|
|
// 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);
|
|
|
|
// Turn off selection highlighting if this was the selected commit
|
|
if (is_selected_commit) {
|
|
wattroff(viewer->commit_list_win, COLOR_PAIR(5));
|
|
}
|
|
}
|
|
|
|
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);
|
|
|
|
// Determine what content to show based on current mode
|
|
if (viewer->current_mode == NCURSES_MODE_COMMIT_LIST ||
|
|
viewer->current_mode == NCURSES_MODE_COMMIT_VIEW) {
|
|
// Show commit info
|
|
if (viewer->commit_count > 0 &&
|
|
viewer->selected_commit < viewer->commit_count) {
|
|
if (viewer->current_mode == NCURSES_MODE_COMMIT_LIST) {
|
|
mvwprintw(viewer->file_content_win, 0, 2, " 2. Commit %s (Preview) ",
|
|
viewer->commits[viewer->selected_commit].hash);
|
|
} else {
|
|
mvwprintw(viewer->file_content_win, 0, 2, " 2. Commit %s (Scrollable) ",
|
|
viewer->commits[viewer->selected_commit].hash);
|
|
}
|
|
} else {
|
|
mvwprintw(viewer->file_content_win, 0, 2, " 2. Commit View ");
|
|
}
|
|
} else if (viewer->current_mode == NCURSES_MODE_STASH_LIST ||
|
|
viewer->current_mode == NCURSES_MODE_STASH_VIEW) {
|
|
// Show stash info
|
|
if (viewer->stash_count > 0 &&
|
|
viewer->selected_stash < viewer->stash_count) {
|
|
if (viewer->current_mode == NCURSES_MODE_STASH_LIST) {
|
|
mvwprintw(viewer->file_content_win, 0, 2, " 2. Stash@{%d} (Preview) ",
|
|
viewer->selected_stash);
|
|
} else {
|
|
mvwprintw(viewer->file_content_win, 0, 2,
|
|
" 2. Stash@{%d} (Scrollable) ", viewer->selected_stash);
|
|
}
|
|
} else {
|
|
mvwprintw(viewer->file_content_win, 0, 2, " 2. Stash View ");
|
|
}
|
|
} else 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);
|
|
}
|
|
} else {
|
|
mvwprintw(viewer->file_content_win, 0, 2, " 2. Content View ");
|
|
}
|
|
|
|
// Show content if available
|
|
if (viewer->file_line_count > 0) {
|
|
|
|
int height, width;
|
|
getmaxyx(viewer->file_content_win, height, width);
|
|
int max_lines_visible = height - 2;
|
|
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;
|
|
int is_cursor_line = (line_idx == viewer->file_cursor_line);
|
|
|
|
// Apply line highlighting for cursor line
|
|
if (is_cursor_line) {
|
|
wattron(viewer->file_content_win, A_REVERSE);
|
|
}
|
|
|
|
// Handle commit header coloring
|
|
if (line->type == 'h' && viewer->current_mode == NCURSES_MODE_FILE_VIEW) {
|
|
// Commit header line with special coloring for hash and branches
|
|
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);
|
|
}
|
|
|
|
// Print character by character with appropriate colors
|
|
int x = 1;
|
|
for (int j = 0; display_line[j] != '\0' && x < content_width; j++) {
|
|
char c = display_line[j];
|
|
|
|
// Look for specific patterns to color
|
|
if (strstr(&display_line[j], "commit ") == &display_line[j]) {
|
|
// Color "commit" in cyan
|
|
wattron(viewer->file_content_win, COLOR_PAIR(3));
|
|
mvwprintw(viewer->file_content_win, y, x, "commit ");
|
|
wattroff(viewer->file_content_win, COLOR_PAIR(3));
|
|
x += 7;
|
|
j += 6; // Skip "commit"
|
|
|
|
// Now check if the next characters are a commit hash (40 hex chars)
|
|
if (j + 1 < (int)strlen(display_line) &&
|
|
display_line[j + 1] != '\0') {
|
|
int hash_start = j + 1;
|
|
int hash_len = 0;
|
|
// Count consecutive hex characters
|
|
while (hash_start + hash_len < (int)strlen(display_line) &&
|
|
hash_len < 40 &&
|
|
((display_line[hash_start + hash_len] >= '0' &&
|
|
display_line[hash_start + hash_len] <= '9') ||
|
|
(display_line[hash_start + hash_len] >= 'a' &&
|
|
display_line[hash_start + hash_len] <= 'f') ||
|
|
(display_line[hash_start + hash_len] >= 'A' &&
|
|
display_line[hash_start + hash_len] <= 'F'))) {
|
|
hash_len++;
|
|
}
|
|
|
|
// If we found a hash (typically 40 chars, but could be shorter),
|
|
// color it orange
|
|
if (hash_len >= 7) { // At least 7 characters for short hash
|
|
wattron(viewer->file_content_win, COLOR_PAIR(6));
|
|
for (int h = 0; h < hash_len; h++) {
|
|
mvwaddch(viewer->file_content_win, y, x,
|
|
display_line[hash_start + h]);
|
|
x++;
|
|
}
|
|
wattroff(viewer->file_content_win, COLOR_PAIR(6));
|
|
j += hash_len; // Skip the hash characters
|
|
}
|
|
}
|
|
} else if (strstr(&display_line[j], "origin/main") ==
|
|
&display_line[j]) {
|
|
// Color "origin/main" in red
|
|
wattron(viewer->file_content_win, COLOR_PAIR(2));
|
|
mvwprintw(viewer->file_content_win, y, x, "origin/main");
|
|
wattroff(viewer->file_content_win, COLOR_PAIR(2));
|
|
x += 11;
|
|
j += 10; // Skip "origin/main"
|
|
} else if (strstr(&display_line[j], "origin/HEAD") ==
|
|
&display_line[j]) {
|
|
// Color "origin/HEAD" in red
|
|
wattron(viewer->file_content_win, COLOR_PAIR(2));
|
|
mvwprintw(viewer->file_content_win, y, x, "origin/HEAD");
|
|
wattroff(viewer->file_content_win, COLOR_PAIR(2));
|
|
x += 11;
|
|
j += 10; // Skip "origin/HEAD"
|
|
} else if (strstr(&display_line[j], "main") == &display_line[j]) {
|
|
// Color "main" in green (but only if not part of origin/main)
|
|
// Check that it's not preceded by "origin/"
|
|
int is_standalone_main = 1;
|
|
if (j >= 7 && strncmp(&display_line[j - 7], "origin/", 7) == 0) {
|
|
is_standalone_main = 0;
|
|
}
|
|
|
|
if (is_standalone_main) {
|
|
wattron(viewer->file_content_win, COLOR_PAIR(1));
|
|
mvwprintw(viewer->file_content_win, y, x, "main");
|
|
wattroff(viewer->file_content_win, COLOR_PAIR(1));
|
|
x += 4;
|
|
j += 3; // Skip "main"
|
|
} else {
|
|
mvwaddch(viewer->file_content_win, y, x, c);
|
|
x++;
|
|
}
|
|
} else if (strstr(&display_line[j], "HEAD") == &display_line[j]) {
|
|
// Color "HEAD" in yellow
|
|
wattron(viewer->file_content_win, COLOR_PAIR(4));
|
|
mvwprintw(viewer->file_content_win, y, x, "HEAD");
|
|
wattroff(viewer->file_content_win, COLOR_PAIR(4));
|
|
x += 4;
|
|
j += 3; // Skip "HEAD"
|
|
} else if (c == '(' || c == ')' || c == ',' ||
|
|
strstr(&display_line[j], "->") == &display_line[j]) {
|
|
// Color branch separators in cyan
|
|
wattron(viewer->file_content_win, COLOR_PAIR(3));
|
|
if (strstr(&display_line[j], "->") == &display_line[j]) {
|
|
mvwprintw(viewer->file_content_win, y, x, "->");
|
|
x += 2;
|
|
j += 1; // Will be incremented by loop
|
|
} else {
|
|
mvwaddch(viewer->file_content_win, y, x, c);
|
|
x++;
|
|
}
|
|
wattroff(viewer->file_content_win, COLOR_PAIR(3));
|
|
} else {
|
|
mvwaddch(viewer->file_content_win, y, x, c);
|
|
x++;
|
|
}
|
|
}
|
|
} else if (line->type == 'h') {
|
|
// In commit/stash view, show header lines as plain white text
|
|
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);
|
|
|
|
// For cursor line on blank lines, fill with spaces to show highlighting
|
|
if (is_cursor_line && strlen(display_line) == 0) {
|
|
for (int x = 1; x < content_width; x++) {
|
|
mvwaddch(viewer->file_content_win, y, x, ' ');
|
|
}
|
|
}
|
|
} else if (line->type == 'i' &&
|
|
viewer->current_mode == NCURSES_MODE_FILE_VIEW) {
|
|
// Commit info lines (Author, Date) in white with colored labels - only
|
|
// in file view
|
|
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);
|
|
}
|
|
|
|
// Color the label part
|
|
if (strncmp(display_line, "Author: ", 8) == 0) {
|
|
wattron(viewer->file_content_win, COLOR_PAIR(3));
|
|
mvwprintw(viewer->file_content_win, y, 1, "Author: ");
|
|
wattroff(viewer->file_content_win, COLOR_PAIR(3));
|
|
mvwprintw(viewer->file_content_win, y, 9, "%s", &display_line[8]);
|
|
} else if (strncmp(display_line, "Date: ", 6) == 0) {
|
|
wattron(viewer->file_content_win, COLOR_PAIR(3));
|
|
mvwprintw(viewer->file_content_win, y, 1, "Date: ");
|
|
wattroff(viewer->file_content_win, COLOR_PAIR(3));
|
|
mvwprintw(viewer->file_content_win, y, 7, "%s", &display_line[6]);
|
|
} else {
|
|
mvwprintw(viewer->file_content_win, y, 1, "%s", display_line);
|
|
}
|
|
} else if (line->type == 'i') {
|
|
// In commit/stash view, just show plain white text
|
|
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);
|
|
|
|
// For cursor line on blank lines, fill with spaces to show highlighting
|
|
if (is_cursor_line && strlen(display_line) == 0) {
|
|
for (int x = 1; x < content_width; x++) {
|
|
mvwaddch(viewer->file_content_win, y, x, ' ');
|
|
}
|
|
}
|
|
} else if (line->type == 's') {
|
|
// File statistics line - need to color + and - characters and byte
|
|
// changes
|
|
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);
|
|
}
|
|
|
|
// Print the line character by character with appropriate colors
|
|
int x = 1;
|
|
for (int j = 0; display_line[j] != '\0' && x < content_width; j++) {
|
|
char c = display_line[j];
|
|
|
|
// Color + characters green, - characters red
|
|
if (c == '+') {
|
|
wattron(viewer->file_content_win, COLOR_PAIR(1)); // Green
|
|
mvwaddch(viewer->file_content_win, y, x, c);
|
|
wattroff(viewer->file_content_win, COLOR_PAIR(1));
|
|
} else if (c == '-') {
|
|
wattron(viewer->file_content_win, COLOR_PAIR(2)); // Red
|
|
mvwaddch(viewer->file_content_win, y, x, c);
|
|
wattroff(viewer->file_content_win, COLOR_PAIR(2));
|
|
} else if (strstr(&display_line[j], "Bin ") == &display_line[j]) {
|
|
// Handle binary file byte changes
|
|
char *arrow = strstr(&display_line[j], " -> ");
|
|
if (arrow) {
|
|
// Print "Bin " normally
|
|
mvwprintw(viewer->file_content_win, y, x, "Bin ");
|
|
x += 4;
|
|
j += 3; // Skip "Bin"
|
|
|
|
// Print bytes before arrow in red
|
|
wattron(viewer->file_content_win, COLOR_PAIR(2));
|
|
while (display_line[j] != '\0' && &display_line[j] < arrow) {
|
|
mvwaddch(viewer->file_content_win, y, x, display_line[j]);
|
|
x++;
|
|
j++;
|
|
}
|
|
wattroff(viewer->file_content_win, COLOR_PAIR(2));
|
|
|
|
// Print arrow normally
|
|
mvwprintw(viewer->file_content_win, y, x, " -> ");
|
|
x += 4;
|
|
j += 3; // Skip " -> "
|
|
|
|
// Print bytes after arrow in green
|
|
wattron(viewer->file_content_win, COLOR_PAIR(1));
|
|
while (display_line[j] != '\0' && display_line[j] != ' ') {
|
|
mvwaddch(viewer->file_content_win, y, x, display_line[j]);
|
|
x++;
|
|
j++;
|
|
}
|
|
wattroff(viewer->file_content_win, COLOR_PAIR(1));
|
|
j--; // Adjust for loop increment
|
|
} else {
|
|
mvwaddch(viewer->file_content_win, y, x, c);
|
|
}
|
|
} else {
|
|
mvwaddch(viewer->file_content_win, y, x, c);
|
|
}
|
|
x++;
|
|
}
|
|
} else {
|
|
// Regular line coloring
|
|
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);
|
|
|
|
// For cursor line on blank lines, fill with spaces to show highlighting
|
|
if (is_cursor_line && strlen(display_line) == 0) {
|
|
for (int x = 1; x < content_width; x++) {
|
|
mvwaddch(viewer->file_content_win, y, x, ' ');
|
|
}
|
|
}
|
|
|
|
// 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));
|
|
}
|
|
}
|
|
|
|
// Turn off line highlighting if it was applied
|
|
if (is_cursor_line) {
|
|
wattroff(viewer->file_content_win, A_REVERSE);
|
|
}
|
|
}
|
|
|
|
// 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 | Stash: s | Commit: c");
|
|
} else if (viewer->current_mode == NCURSES_MODE_COMMIT_LIST) {
|
|
strcpy(keybindings, "Push: P | Pull: p | Reset: r/R | Amend: a | Nav: j/k");
|
|
} else if (viewer->current_mode == NCURSES_MODE_STASH_LIST) {
|
|
strcpy(keybindings, "Apply: <space> | Pop: g | Drop: d | Nav: j/k");
|
|
} else if (viewer->current_mode == NCURSES_MODE_BRANCH_LIST) {
|
|
strcpy(keybindings,
|
|
"Checkout: c | New: n | Rename: r | Delete: d | 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");
|
|
} else if (viewer->current_mode == NCURSES_MODE_COMMIT_VIEW) {
|
|
strcpy(keybindings, "Scroll: j/k | Page: Ctrl+U/D | Back: Esc");
|
|
} else if (viewer->current_mode == NCURSES_MODE_STASH_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 / 1) %
|
|
4; // Change every frame (~20ms 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 >= 60) {
|
|
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 - fetch remote changes quietly
|
|
system("git fetch --all --quiet >/dev/null 2>&1");
|
|
get_ncurses_changed_files(viewer);
|
|
|
|
// Only reload file diff if we're actually in a file-related mode
|
|
if ((viewer->current_mode == NCURSES_MODE_FILE_LIST ||
|
|
viewer->current_mode == NCURSES_MODE_FILE_VIEW) &&
|
|
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);
|
|
get_ncurses_git_branches(viewer); // This will now show updated branch status
|
|
|
|
// If we're in commit view mode, reload the current commit
|
|
if (viewer->current_mode == NCURSES_MODE_COMMIT_VIEW &&
|
|
viewer->commit_count > 0 &&
|
|
viewer->selected_commit < viewer->commit_count) {
|
|
load_commit_for_viewing(viewer,
|
|
viewer->commits[viewer->selected_commit].hash);
|
|
}
|
|
|
|
// If we're in stash view mode, reload the current stash
|
|
if (viewer->current_mode == NCURSES_MODE_STASH_VIEW &&
|
|
viewer->stash_count > 0 &&
|
|
viewer->selected_stash < viewer->stash_count) {
|
|
load_stash_for_viewing(viewer, viewer->selected_stash);
|
|
}
|
|
}
|
|
|
|
// 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 frame (0.05s) for "Pushing" (7
|
|
// chars)
|
|
viewer->text_char_count = viewer->animation_frame;
|
|
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 - keep spinning until push_commit changes status
|
|
// Don't auto-transition, let push_commit function handle the transition
|
|
// This allows the spinner to keep going during the blocking git push
|
|
} else if (viewer->sync_status == SYNC_STATUS_PUSHING_DISAPPEARING) {
|
|
// Disappearing: remove one character every frame (0.05s)
|
|
int chars_to_remove = viewer->animation_frame;
|
|
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 frame (0.05s) for "Pushed!" (7
|
|
// chars)
|
|
viewer->text_char_count = viewer->animation_frame;
|
|
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 (100 frames at 20ms)
|
|
if (viewer->animation_frame >= 100) {
|
|
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 frame (0.05s)
|
|
int chars_to_remove = viewer->animation_frame;
|
|
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;
|
|
// Load the selected file content when switching to file list mode
|
|
if (viewer->file_count > 0 && viewer->selected_file < viewer->file_count) {
|
|
load_full_file_with_diff(viewer,
|
|
viewer->files[viewer->selected_file].filename);
|
|
}
|
|
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_BRANCH_LIST;
|
|
break;
|
|
case '4':
|
|
viewer->current_mode = NCURSES_MODE_COMMIT_LIST;
|
|
// Auto-preview the selected commit
|
|
if (viewer->commit_count > 0) {
|
|
load_commit_for_viewing(viewer,
|
|
viewer->commits[viewer->selected_commit].hash);
|
|
}
|
|
break;
|
|
case '5':
|
|
viewer->current_mode = NCURSES_MODE_STASH_LIST;
|
|
// Auto-preview the selected stash
|
|
if (viewer->stash_count > 0) {
|
|
load_stash_for_viewing(viewer, viewer->selected_stash);
|
|
}
|
|
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 's':
|
|
case 'S':
|
|
create_ncurses_git_stash(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':
|
|
// Move cursor up (vim-style)
|
|
if (viewer->file_cursor_line > 0) {
|
|
viewer->file_cursor_line--;
|
|
// Scroll up if cursor gets within 3 lines of top
|
|
if (viewer->file_cursor_line < viewer->file_scroll_offset + 3 &&
|
|
viewer->file_scroll_offset > 0) {
|
|
viewer->file_scroll_offset--;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case KEY_DOWN:
|
|
case 'j':
|
|
// Move cursor down (vim-style)
|
|
if (viewer->file_cursor_line < viewer->file_line_count - 1) {
|
|
viewer->file_cursor_line++;
|
|
// Scroll down if cursor gets within 3 lines of bottom
|
|
if (viewer->file_cursor_line >=
|
|
viewer->file_scroll_offset + max_lines_visible - 3 &&
|
|
viewer->file_scroll_offset <
|
|
viewer->file_line_count - max_lines_visible) {
|
|
viewer->file_scroll_offset++;
|
|
}
|
|
}
|
|
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;
|
|
|
|
case 21: // Ctrl+U
|
|
// Move cursor up half page
|
|
viewer->file_cursor_line -= max_lines_visible / 2;
|
|
if (viewer->file_cursor_line < 0) {
|
|
viewer->file_cursor_line = 0;
|
|
}
|
|
// Adjust scroll to keep cursor visible with padding
|
|
if (viewer->file_cursor_line < viewer->file_scroll_offset + 3) {
|
|
viewer->file_scroll_offset = viewer->file_cursor_line - 3;
|
|
if (viewer->file_scroll_offset < 0) {
|
|
viewer->file_scroll_offset = 0;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case 4: // Ctrl+D
|
|
// Move cursor down half page
|
|
viewer->file_cursor_line += max_lines_visible / 2;
|
|
if (viewer->file_cursor_line >= viewer->file_line_count) {
|
|
viewer->file_cursor_line = viewer->file_line_count - 1;
|
|
}
|
|
// Adjust scroll to keep cursor visible with padding
|
|
if (viewer->file_cursor_line >=
|
|
viewer->file_scroll_offset + max_lines_visible - 3) {
|
|
viewer->file_scroll_offset =
|
|
viewer->file_cursor_line - max_lines_visible + 4;
|
|
if (viewer->file_scroll_offset >
|
|
viewer->file_line_count - max_lines_visible) {
|
|
viewer->file_scroll_offset =
|
|
viewer->file_line_count - max_lines_visible;
|
|
}
|
|
if (viewer->file_scroll_offset < 0) {
|
|
viewer->file_scroll_offset = 0;
|
|
}
|
|
}
|
|
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;
|
|
}
|
|
} 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--;
|
|
// Auto-preview the selected commit
|
|
if (viewer->commit_count > 0) {
|
|
load_commit_for_viewing(
|
|
viewer, viewer->commits[viewer->selected_commit].hash);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case KEY_DOWN:
|
|
case 'j':
|
|
if (viewer->selected_commit < viewer->commit_count - 1) {
|
|
viewer->selected_commit++;
|
|
// Auto-preview the selected commit
|
|
if (viewer->commit_count > 0) {
|
|
load_commit_for_viewing(
|
|
viewer, viewer->commits[viewer->selected_commit].hash);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case '\n':
|
|
case '\r':
|
|
case KEY_ENTER:
|
|
// Enter commit view mode
|
|
if (viewer->commit_count > 0 &&
|
|
viewer->selected_commit < viewer->commit_count) {
|
|
load_commit_for_viewing(viewer,
|
|
viewer->commits[viewer->selected_commit].hash);
|
|
viewer->current_mode = NCURSES_MODE_COMMIT_VIEW;
|
|
}
|
|
break;
|
|
|
|
case 'P': // Push commit
|
|
if (viewer->commit_count > 0 &&
|
|
viewer->selected_commit < viewer->commit_count) {
|
|
// Show immediate feedback
|
|
viewer->sync_status = SYNC_STATUS_PUSHING_VISIBLE;
|
|
viewer->animation_frame = 0;
|
|
viewer->text_char_count = 7; // Show full "Pushing" immediately
|
|
// Force a quick render to show "Pushing!" immediately
|
|
render_status_bar(viewer);
|
|
wrefresh(viewer->status_bar_win);
|
|
// Now do the actual push
|
|
push_commit(viewer, viewer->selected_commit);
|
|
}
|
|
break;
|
|
|
|
case 'r': // Reset (soft) - undo commit but keep changes
|
|
if (viewer->commit_count > 0 && viewer->selected_commit == 0) {
|
|
reset_commit_soft(viewer, viewer->selected_commit);
|
|
}
|
|
break;
|
|
|
|
case 'R': // Reset (hard) - undo commit and discard changes
|
|
if (viewer->commit_count > 0 && viewer->selected_commit == 0) {
|
|
reset_commit_hard(viewer, viewer->selected_commit);
|
|
}
|
|
break;
|
|
|
|
case 'a':
|
|
case 'A': // Amend most recent commit
|
|
if (viewer->commit_count > 0) {
|
|
amend_commit(viewer);
|
|
}
|
|
break;
|
|
}
|
|
} else if (viewer->current_mode == NCURSES_MODE_STASH_LIST) {
|
|
// Stash 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_stash > 0) {
|
|
viewer->selected_stash--;
|
|
// Auto-preview the selected stash
|
|
if (viewer->stash_count > 0) {
|
|
load_stash_for_viewing(viewer, viewer->selected_stash);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case KEY_DOWN:
|
|
case 'j':
|
|
if (viewer->selected_stash < viewer->stash_count - 1) {
|
|
viewer->selected_stash++;
|
|
// Auto-preview the selected stash
|
|
if (viewer->stash_count > 0) {
|
|
load_stash_for_viewing(viewer, viewer->selected_stash);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case '\n':
|
|
case '\r':
|
|
case KEY_ENTER:
|
|
// Enter stash view mode
|
|
if (viewer->stash_count > 0 &&
|
|
viewer->selected_stash < viewer->stash_count) {
|
|
load_stash_for_viewing(viewer, viewer->selected_stash);
|
|
viewer->current_mode = NCURSES_MODE_STASH_VIEW;
|
|
}
|
|
break;
|
|
|
|
case ' ': // Space - Apply stash (keeps stash in list)
|
|
if (viewer->stash_count > 0 &&
|
|
viewer->selected_stash < viewer->stash_count) {
|
|
if (apply_git_stash(viewer->selected_stash)) {
|
|
// Refresh everything after applying stash
|
|
get_ncurses_changed_files(viewer);
|
|
get_commit_history(viewer);
|
|
|
|
// Reset file selection if no files remain
|
|
if (viewer->file_count == 0) {
|
|
viewer->selected_file = 0;
|
|
viewer->file_line_count = 0;
|
|
viewer->file_scroll_offset = 0;
|
|
} else if (viewer->selected_file >= viewer->file_count) {
|
|
viewer->selected_file = viewer->file_count - 1;
|
|
}
|
|
|
|
// Reload current file if any files exist
|
|
if (viewer->file_count > 0 &&
|
|
viewer->selected_file < viewer->file_count) {
|
|
load_full_file_with_diff(
|
|
viewer, viewer->files[viewer->selected_file].filename);
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
|
|
case 'g':
|
|
case 'G': // Pop stash (applies and removes from list)
|
|
if (viewer->stash_count > 0 &&
|
|
viewer->selected_stash < viewer->stash_count) {
|
|
if (pop_git_stash(viewer->selected_stash)) {
|
|
// Refresh everything after popping stash
|
|
get_ncurses_changed_files(viewer);
|
|
get_ncurses_git_stashes(viewer);
|
|
get_commit_history(viewer);
|
|
|
|
// Adjust selected stash if needed
|
|
if (viewer->selected_stash >= viewer->stash_count &&
|
|
viewer->stash_count > 0) {
|
|
viewer->selected_stash = viewer->stash_count - 1;
|
|
}
|
|
|
|
// Reset file selection if no files remain
|
|
if (viewer->file_count == 0) {
|
|
viewer->selected_file = 0;
|
|
viewer->file_line_count = 0;
|
|
viewer->file_scroll_offset = 0;
|
|
} else if (viewer->selected_file >= viewer->file_count) {
|
|
viewer->selected_file = viewer->file_count - 1;
|
|
}
|
|
|
|
// Reload current file if any files exist
|
|
if (viewer->file_count > 0 &&
|
|
viewer->selected_file < viewer->file_count) {
|
|
load_full_file_with_diff(
|
|
viewer, viewer->files[viewer->selected_file].filename);
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
|
|
case 'd':
|
|
case 'D': // Drop stash (removes without applying)
|
|
if (viewer->stash_count > 0 &&
|
|
viewer->selected_stash < viewer->stash_count) {
|
|
if (drop_git_stash(viewer->selected_stash)) {
|
|
// Refresh stash list after dropping
|
|
get_ncurses_git_stashes(viewer);
|
|
|
|
// Adjust selected stash if needed
|
|
if (viewer->selected_stash >= viewer->stash_count &&
|
|
viewer->stash_count > 0) {
|
|
viewer->selected_stash = viewer->stash_count - 1;
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
} else if (viewer->current_mode == NCURSES_MODE_BRANCH_LIST) {
|
|
// Branch 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_branch > 0) {
|
|
viewer->selected_branch--;
|
|
}
|
|
break;
|
|
|
|
case KEY_DOWN:
|
|
case 'j':
|
|
if (viewer->selected_branch < viewer->branch_count - 1) {
|
|
viewer->selected_branch++;
|
|
}
|
|
break;
|
|
|
|
case '\n':
|
|
case '\r':
|
|
case KEY_ENTER:
|
|
// Enter - Switch to selected branch (same as space)
|
|
if (viewer->branch_count > 0 &&
|
|
viewer->selected_branch < viewer->branch_count) {
|
|
char cmd[512];
|
|
snprintf(cmd, sizeof(cmd), "git checkout \"%s\" >/dev/null 2>&1",
|
|
viewer->branches[viewer->selected_branch].name);
|
|
|
|
if (system(cmd) == 0) {
|
|
// Refresh everything after branch switch
|
|
get_ncurses_changed_files(viewer);
|
|
get_commit_history(viewer);
|
|
get_ncurses_git_branches(
|
|
viewer); // Refresh branch list to update current branch
|
|
|
|
// Reset file selection if no files remain
|
|
if (viewer->file_count == 0) {
|
|
viewer->selected_file = 0;
|
|
viewer->file_line_count = 0;
|
|
viewer->file_scroll_offset = 0;
|
|
} else if (viewer->selected_file >= viewer->file_count) {
|
|
viewer->selected_file = viewer->file_count - 1;
|
|
}
|
|
|
|
// Reload current file if any files exist
|
|
if (viewer->file_count > 0 &&
|
|
viewer->selected_file < viewer->file_count) {
|
|
load_full_file_with_diff(
|
|
viewer, viewer->files[viewer->selected_file].filename);
|
|
}
|
|
}
|
|
|
|
// Force complete screen refresh after checkout
|
|
clear();
|
|
refresh();
|
|
}
|
|
break;
|
|
|
|
case 'c': // c - Checkout selected branch
|
|
if (viewer->branch_count > 0 &&
|
|
viewer->selected_branch < viewer->branch_count) {
|
|
char cmd[512];
|
|
snprintf(cmd, sizeof(cmd), "git checkout \"%s\" >/dev/null 2>&1",
|
|
viewer->branches[viewer->selected_branch].name);
|
|
|
|
if (system(cmd) == 0) {
|
|
// Refresh everything after branch switch
|
|
get_ncurses_changed_files(viewer);
|
|
get_commit_history(viewer);
|
|
get_ncurses_git_branches(
|
|
viewer); // Refresh branch list to update current branch
|
|
|
|
// Reset file selection if no files remain
|
|
if (viewer->file_count == 0) {
|
|
viewer->selected_file = 0;
|
|
viewer->file_line_count = 0;
|
|
viewer->file_scroll_offset = 0;
|
|
} else if (viewer->selected_file >= viewer->file_count) {
|
|
viewer->selected_file = viewer->file_count - 1;
|
|
}
|
|
|
|
// Reload current file if any files exist
|
|
if (viewer->file_count > 0 &&
|
|
viewer->selected_file < viewer->file_count) {
|
|
load_full_file_with_diff(
|
|
viewer, viewer->files[viewer->selected_file].filename);
|
|
}
|
|
}
|
|
|
|
// Force complete screen refresh after checkout
|
|
clear();
|
|
refresh();
|
|
}
|
|
break;
|
|
|
|
case 'n': // New branch
|
|
{
|
|
char new_branch_name[256];
|
|
if (get_branch_name_input(new_branch_name, sizeof(new_branch_name))) {
|
|
if (create_git_branch(new_branch_name)) {
|
|
// Refresh everything after creating new branch
|
|
get_ncurses_changed_files(viewer);
|
|
get_commit_history(viewer);
|
|
get_ncurses_git_branches(viewer);
|
|
|
|
// Find and select the new branch (need to look for the cleaned name
|
|
// with dashes)
|
|
char clean_branch_name[256];
|
|
strncpy(clean_branch_name, new_branch_name,
|
|
sizeof(clean_branch_name) - 1);
|
|
clean_branch_name[sizeof(clean_branch_name) - 1] = '\0';
|
|
for (int j = 0; clean_branch_name[j] != '\0'; j++) {
|
|
if (clean_branch_name[j] == ' ') {
|
|
clean_branch_name[j] = '-';
|
|
}
|
|
}
|
|
|
|
for (int i = 0; i < viewer->branch_count; i++) {
|
|
if (strcmp(viewer->branches[i].name, clean_branch_name) == 0) {
|
|
viewer->selected_branch = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Reset file selection if no files remain
|
|
if (viewer->file_count == 0) {
|
|
viewer->selected_file = 0;
|
|
viewer->file_line_count = 0;
|
|
viewer->file_scroll_offset = 0;
|
|
} else if (viewer->selected_file >= viewer->file_count) {
|
|
viewer->selected_file = viewer->file_count - 1;
|
|
}
|
|
|
|
// Reload current file if any files exist
|
|
if (viewer->file_count > 0 &&
|
|
viewer->selected_file < viewer->file_count) {
|
|
load_full_file_with_diff(
|
|
viewer, viewer->files[viewer->selected_file].filename);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Force immediate branch window update
|
|
werase(viewer->branch_list_win);
|
|
render_branch_list_window(viewer);
|
|
wrefresh(viewer->branch_list_win);
|
|
|
|
// Clear screen artifacts from dialog
|
|
clear();
|
|
refresh();
|
|
} break;
|
|
|
|
case 'd': // Delete branch
|
|
if (viewer->branch_count > 0 &&
|
|
viewer->selected_branch < viewer->branch_count) {
|
|
const char *branch_to_delete =
|
|
viewer->branches[viewer->selected_branch].name;
|
|
|
|
// Don't allow deleting the current branch
|
|
if (viewer->branches[viewer->selected_branch].status == 1) {
|
|
show_error_popup("Cannot delete current branch!");
|
|
break;
|
|
}
|
|
|
|
DeleteBranchOption option = show_delete_branch_dialog(branch_to_delete);
|
|
if (option != DELETE_CANCEL) {
|
|
if (delete_git_branch(branch_to_delete, option)) {
|
|
// Refresh branch list after deletion
|
|
get_ncurses_git_branches(viewer);
|
|
|
|
// Adjust selection if needed
|
|
if (viewer->selected_branch >= viewer->branch_count &&
|
|
viewer->branch_count > 0) {
|
|
viewer->selected_branch = viewer->branch_count - 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Force immediate branch window update
|
|
werase(viewer->branch_list_win);
|
|
render_branch_list_window(viewer);
|
|
wrefresh(viewer->branch_list_win);
|
|
|
|
// Clear screen artifacts from dialog
|
|
clear();
|
|
refresh();
|
|
}
|
|
break;
|
|
|
|
case 'r': // Rename branch
|
|
if (viewer->branch_count > 0 &&
|
|
viewer->selected_branch < viewer->branch_count) {
|
|
const char *current_name =
|
|
viewer->branches[viewer->selected_branch].name;
|
|
char new_name[256];
|
|
|
|
if (get_rename_branch_input(current_name, new_name, sizeof(new_name))) {
|
|
if (rename_git_branch(current_name, new_name)) {
|
|
// Refresh branch list after rename
|
|
get_ncurses_git_branches(viewer);
|
|
|
|
// Find and select the renamed branch
|
|
for (int i = 0; i < viewer->branch_count; i++) {
|
|
if (strcmp(viewer->branches[i].name, new_name) == 0) {
|
|
viewer->selected_branch = i;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Force immediate branch window update
|
|
werase(viewer->branch_list_win);
|
|
render_branch_list_window(viewer);
|
|
wrefresh(viewer->branch_list_win);
|
|
|
|
// Clear screen artifacts from dialog
|
|
clear();
|
|
refresh();
|
|
}
|
|
break;
|
|
|
|
case 'p':
|
|
if (viewer->branch_count > 0 &&
|
|
viewer->selected_branch < viewer->branch_count) {
|
|
const char *branch_name =
|
|
viewer->branches[viewer->selected_branch].name;
|
|
|
|
if (viewer->branches[viewer->selected_branch].commits_behind > 0) {
|
|
viewer->sync_status = SYNC_STATUS_PULLING_APPEARING;
|
|
viewer->animation_frame = 0;
|
|
viewer->text_char_count = 0;
|
|
|
|
render_status_bar(viewer);
|
|
|
|
char pull_cmd[512];
|
|
snprintf(pull_cmd, sizeof(pull_cmd),
|
|
"git pull origin %s 2>/dev/null >/dev/null", branch_name);
|
|
int result = system(pull_cmd);
|
|
|
|
if (result == 0) {
|
|
get_ncurses_changed_files(viewer);
|
|
get_commit_history(viewer);
|
|
get_ncurses_git_branches(viewer);
|
|
|
|
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;
|
|
}
|
|
|
|
if (viewer->file_count > 0 &&
|
|
viewer->selected_file < viewer->file_count) {
|
|
load_full_file_with_diff(
|
|
viewer, viewer->files[viewer->selected_file].filename);
|
|
}
|
|
|
|
viewer->sync_status = SYNC_STATUS_PULLED_APPEARING;
|
|
viewer->animation_frame = 0;
|
|
viewer->text_char_count = 0;
|
|
} else {
|
|
show_error_popup("Pull failed. Check your network connection.");
|
|
viewer->sync_status = SYNC_STATUS_IDLE;
|
|
}
|
|
} else {
|
|
show_error_popup("No commits to pull from remote");
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
} else if (viewer->current_mode == NCURSES_MODE_COMMIT_VIEW) {
|
|
// Commit view mode navigation (same as file view)
|
|
switch (key) {
|
|
case 27: // ESC
|
|
viewer->current_mode =
|
|
NCURSES_MODE_COMMIT_LIST; // Return to commit list mode
|
|
break;
|
|
|
|
case KEY_UP:
|
|
case 'k':
|
|
// Move cursor up (vim-style)
|
|
if (viewer->file_cursor_line > 0) {
|
|
viewer->file_cursor_line--;
|
|
// Scroll up if cursor gets within 3 lines of top
|
|
if (viewer->file_cursor_line < viewer->file_scroll_offset + 3 &&
|
|
viewer->file_scroll_offset > 0) {
|
|
viewer->file_scroll_offset--;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case KEY_DOWN:
|
|
case 'j':
|
|
// Move cursor down (vim-style)
|
|
if (viewer->file_cursor_line < viewer->file_line_count - 1) {
|
|
viewer->file_cursor_line++;
|
|
// Scroll down if cursor gets within 3 lines of bottom
|
|
if (viewer->file_cursor_line >=
|
|
viewer->file_scroll_offset + max_lines_visible - 3 &&
|
|
viewer->file_scroll_offset <
|
|
viewer->file_line_count - max_lines_visible) {
|
|
viewer->file_scroll_offset++;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case 21: // Ctrl+U
|
|
// Move cursor up half page
|
|
viewer->file_cursor_line -= max_lines_visible / 2;
|
|
if (viewer->file_cursor_line < 0) {
|
|
viewer->file_cursor_line = 0;
|
|
}
|
|
// Adjust scroll to keep cursor visible with padding
|
|
if (viewer->file_cursor_line < viewer->file_scroll_offset + 3) {
|
|
viewer->file_scroll_offset = viewer->file_cursor_line - 3;
|
|
if (viewer->file_scroll_offset < 0) {
|
|
viewer->file_scroll_offset = 0;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case 4: // Ctrl+D
|
|
// Move cursor down half page
|
|
viewer->file_cursor_line += max_lines_visible / 2;
|
|
if (viewer->file_cursor_line >= viewer->file_line_count) {
|
|
viewer->file_cursor_line = viewer->file_line_count - 1;
|
|
}
|
|
// Adjust scroll to keep cursor visible with padding
|
|
if (viewer->file_cursor_line >=
|
|
viewer->file_scroll_offset + max_lines_visible - 3) {
|
|
viewer->file_scroll_offset =
|
|
viewer->file_cursor_line - max_lines_visible + 4;
|
|
if (viewer->file_scroll_offset >
|
|
viewer->file_line_count - max_lines_visible) {
|
|
viewer->file_scroll_offset =
|
|
viewer->file_line_count - max_lines_visible;
|
|
}
|
|
if (viewer->file_scroll_offset < 0) {
|
|
viewer->file_scroll_offset = 0;
|
|
}
|
|
}
|
|
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_STASH_VIEW) {
|
|
// Stash view mode navigation (same as file view)
|
|
switch (key) {
|
|
case 27: // ESC
|
|
viewer->current_mode =
|
|
NCURSES_MODE_STASH_LIST; // Return to stash list mode
|
|
break;
|
|
|
|
case KEY_UP:
|
|
case 'k':
|
|
// Move cursor up (vim-style)
|
|
if (viewer->file_cursor_line > 0) {
|
|
viewer->file_cursor_line--;
|
|
// Scroll up if cursor gets within 3 lines of top
|
|
if (viewer->file_cursor_line < viewer->file_scroll_offset + 3 &&
|
|
viewer->file_scroll_offset > 0) {
|
|
viewer->file_scroll_offset--;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case KEY_DOWN:
|
|
case 'j':
|
|
// Move cursor down (vim-style)
|
|
if (viewer->file_cursor_line < viewer->file_line_count - 1) {
|
|
viewer->file_cursor_line++;
|
|
// Scroll down if cursor gets within 3 lines of bottom
|
|
if (viewer->file_cursor_line >=
|
|
viewer->file_scroll_offset + max_lines_visible - 3 &&
|
|
viewer->file_scroll_offset <
|
|
viewer->file_line_count - max_lines_visible) {
|
|
viewer->file_scroll_offset++;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case 21: // Ctrl+U
|
|
// Move cursor up half page
|
|
viewer->file_cursor_line -= max_lines_visible / 2;
|
|
if (viewer->file_cursor_line < 0) {
|
|
viewer->file_cursor_line = 0;
|
|
}
|
|
// Adjust scroll to keep cursor visible with padding
|
|
if (viewer->file_cursor_line < viewer->file_scroll_offset + 3) {
|
|
viewer->file_scroll_offset = viewer->file_cursor_line - 3;
|
|
if (viewer->file_scroll_offset < 0) {
|
|
viewer->file_scroll_offset = 0;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case 4: // Ctrl+D
|
|
// Move cursor down half page
|
|
viewer->file_cursor_line += max_lines_visible / 2;
|
|
if (viewer->file_cursor_line >= viewer->file_line_count) {
|
|
viewer->file_cursor_line = viewer->file_line_count - 1;
|
|
}
|
|
// Adjust scroll to keep cursor visible with padding
|
|
if (viewer->file_cursor_line >=
|
|
viewer->file_scroll_offset + max_lines_visible - 3) {
|
|
viewer->file_scroll_offset =
|
|
viewer->file_cursor_line - max_lines_visible + 4;
|
|
if (viewer->file_scroll_offset >
|
|
viewer->file_line_count - max_lines_visible) {
|
|
viewer->file_scroll_offset =
|
|
viewer->file_line_count - max_lines_visible;
|
|
}
|
|
if (viewer->file_scroll_offset < 0) {
|
|
viewer->file_scroll_offset = 0;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case KEY_NPAGE: // Page Down
|
|
case ' ':
|
|
// Scroll content down by page
|
|
if (viewer->file_line_count > max_lines_visible) {
|
|
viewer->file_scroll_offset += max_lines_visible;
|
|
if (viewer->file_scroll_offset >
|
|
viewer->file_line_count - max_lines_visible) {
|
|
viewer->file_scroll_offset =
|
|
viewer->file_line_count - max_lines_visible;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case KEY_PPAGE: // Page Up
|
|
// Scroll content up by page
|
|
viewer->file_scroll_offset -= max_lines_visible;
|
|
if (viewer->file_scroll_offset < 0) {
|
|
viewer->file_scroll_offset = 0;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
return 1; // Continue
|
|
}
|
|
|
|
/**
|
|
* 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);
|
|
|
|
// get stashes
|
|
get_ncurses_git_stashes(&viewer);
|
|
|
|
// get branches
|
|
get_ncurses_git_branches(&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);
|
|
}
|
|
|
|
// Load initial commit preview if available
|
|
if (viewer.commit_count > 0) {
|
|
load_commit_for_viewing(&viewer, viewer.commits[0].hash);
|
|
}
|
|
|
|
// Load initial stash preview if available
|
|
if (viewer.stash_count > 0) {
|
|
load_stash_for_viewing(&viewer, 0);
|
|
}
|
|
|
|
// 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=branches 4=commits 5=stashes | "
|
|
"j/k=nav "
|
|
"Space=mark "
|
|
"A=all S=stash 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=branches 4=commits 5=stashes | "
|
|
"j/k=scroll "
|
|
"Ctrl+U/D=30lines | q=quit");
|
|
} else {
|
|
mvprintw(0, 0,
|
|
"Git Diff Viewer: 1=files 2=view 3=branches 4=commits 5=stashes | "
|
|
"j/k=nav P=push "
|
|
"r/R=reset a=amend | q=quit");
|
|
}
|
|
|
|
attroff(COLOR_PAIR(3));
|
|
refresh();
|
|
|
|
render_file_list_window(&viewer);
|
|
render_file_content_window(&viewer);
|
|
render_commit_list_window(&viewer);
|
|
render_branch_list_window(&viewer);
|
|
render_stash_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=branches 4=commits "
|
|
"5=stashes | j/k=nav "
|
|
"Space=mark A=all S=stash 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=branches 4=commits "
|
|
"5=stashes | j/k=scroll "
|
|
"Ctrl+U/D=30lines | q=quit");
|
|
} else {
|
|
mvprintw(
|
|
0, 0,
|
|
"Git Diff Viewer: 1=files 2=view 3=branches 4=commits 5=stashes | "
|
|
"j/k=nav P=push "
|
|
"r/R=reset a=amend | q=quit");
|
|
}
|
|
attroff(COLOR_PAIR(3));
|
|
refresh();
|
|
last_mode = viewer.current_mode;
|
|
}
|
|
// Update sync status and check for new files
|
|
update_sync_status(&viewer);
|
|
|
|
render_file_list_window(&viewer);
|
|
render_file_content_window(&viewer);
|
|
render_commit_list_window(&viewer);
|
|
render_branch_list_window(&viewer);
|
|
render_stash_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(20000); // 20ms delay
|
|
}
|
|
|
|
cleanup_ncurses_diff_viewer(&viewer);
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Get list of git stashes
|
|
*/
|
|
|
|
/**
|
|
* Get list of git branches
|
|
*/
|
|
|
|
int get_ncurses_git_branches(NCursesDiffViewer *viewer) {
|
|
if (!viewer)
|
|
return 0;
|
|
|
|
viewer->branch_count = 0;
|
|
|
|
// First, fetch from all remotes to get latest status (like lazygit does)
|
|
system("git fetch --all --quiet >/dev/null 2>&1");
|
|
|
|
// Use git branch (without -a) to only show local branches
|
|
FILE *fp = popen("git branch 2>/dev/null", "r");
|
|
if (!fp) {
|
|
return 0;
|
|
}
|
|
|
|
char line[512];
|
|
while (fgets(line, sizeof(line), fp) && viewer->branch_count < MAX_BRANCHES) {
|
|
// Remove newline
|
|
line[strcspn(line, "\n")] = 0;
|
|
|
|
// Skip empty lines
|
|
if (strlen(line) == 0) continue;
|
|
|
|
// Check if this is the current branch (starts with *)
|
|
int is_current = 0;
|
|
char *branch_name = line;
|
|
|
|
// Skip leading spaces
|
|
while (*branch_name == ' ') branch_name++;
|
|
|
|
if (*branch_name == '*') {
|
|
is_current = 1;
|
|
branch_name++; // Skip the *
|
|
while (*branch_name == ' ') branch_name++; // Skip spaces after *
|
|
}
|
|
|
|
// Skip any lines that contain "->" (symbolic references like origin/HEAD -> origin/main)
|
|
if (strstr(branch_name, "->") != NULL) {
|
|
continue;
|
|
}
|
|
|
|
// Skip remote branches (shouldn't appear with git branch, but just in case)
|
|
if (strncmp(branch_name, "remotes/", 8) == 0) {
|
|
continue;
|
|
}
|
|
|
|
// Copy branch name
|
|
strncpy(viewer->branches[viewer->branch_count].name, branch_name,
|
|
MAX_BRANCHNAME_LEN - 1);
|
|
viewer->branches[viewer->branch_count].name[MAX_BRANCHNAME_LEN - 1] = '\0';
|
|
viewer->branches[viewer->branch_count].status = is_current;
|
|
|
|
// Initialize ahead/behind counts
|
|
viewer->branches[viewer->branch_count].commits_ahead = 0;
|
|
viewer->branches[viewer->branch_count].commits_behind = 0;
|
|
|
|
// Get ahead/behind status using the exact method lazygit uses
|
|
// First check if remote branch exists
|
|
char remote_exists_cmd[512];
|
|
snprintf(remote_exists_cmd, sizeof(remote_exists_cmd),
|
|
"git show-ref --verify --quiet refs/remotes/origin/%s", branch_name);
|
|
|
|
if (system(remote_exists_cmd) == 0) {
|
|
// Remote branch exists, now get ahead/behind counts
|
|
|
|
// Get commits behind (how many commits remote has that we don't)
|
|
char behind_cmd[512];
|
|
snprintf(behind_cmd, sizeof(behind_cmd),
|
|
"git rev-list --count %s..origin/%s 2>/dev/null", branch_name, branch_name);
|
|
|
|
FILE *behind_fp = popen(behind_cmd, "r");
|
|
if (behind_fp) {
|
|
char behind_count[32];
|
|
if (fgets(behind_count, sizeof(behind_count), behind_fp) != NULL) {
|
|
viewer->branches[viewer->branch_count].commits_behind = atoi(behind_count);
|
|
}
|
|
pclose(behind_fp);
|
|
}
|
|
|
|
// Get commits ahead (how many commits we have that remote doesn't)
|
|
char ahead_cmd[512];
|
|
snprintf(ahead_cmd, sizeof(ahead_cmd),
|
|
"git rev-list --count origin/%s..%s 2>/dev/null", branch_name, branch_name);
|
|
|
|
FILE *ahead_fp = popen(ahead_cmd, "r");
|
|
if (ahead_fp) {
|
|
char ahead_count[32];
|
|
if (fgets(ahead_count, sizeof(ahead_count), ahead_fp) != NULL) {
|
|
viewer->branches[viewer->branch_count].commits_ahead = atoi(ahead_count);
|
|
}
|
|
pclose(ahead_fp);
|
|
}
|
|
}
|
|
|
|
viewer->branch_count++;
|
|
}
|
|
|
|
pclose(fp);
|
|
return 1;
|
|
}
|
|
|
|
/**
|
|
* Get branch name input from user for new branch creation
|
|
*/
|
|
int get_branch_name_input(char *branch_name, int max_len) {
|
|
if (!branch_name || max_len <= 0)
|
|
return 0;
|
|
|
|
// Create input window
|
|
int win_height = 7;
|
|
int win_width = 60;
|
|
int start_y = (LINES - win_height) / 2;
|
|
int start_x = (COLS - win_width) / 2;
|
|
|
|
WINDOW *input_win = newwin(win_height, win_width, start_y, start_x);
|
|
if (!input_win)
|
|
return 0;
|
|
|
|
char input[256] = {0};
|
|
int input_pos = 0;
|
|
int result = 0;
|
|
|
|
// Enable echo and cursor for input
|
|
echo();
|
|
curs_set(1);
|
|
keypad(input_win, TRUE);
|
|
|
|
while (1) {
|
|
// Redraw window
|
|
werase(input_win);
|
|
box(input_win, 0, 0);
|
|
mvwprintw(input_win, 0, 2, " Create New Branch ");
|
|
mvwprintw(input_win, 2, 2, "Branch name:");
|
|
mvwprintw(input_win, 5, 2, "Enter: create | Esc: cancel");
|
|
|
|
// Show current input
|
|
mvwprintw(input_win, 3, 2, "> %s", input);
|
|
|
|
// Position cursor at end of input
|
|
wmove(input_win, 3, 4 + input_pos);
|
|
wrefresh(input_win);
|
|
|
|
int ch = wgetch(input_win);
|
|
|
|
switch (ch) {
|
|
case 27: // ESC
|
|
result = 0;
|
|
goto cleanup;
|
|
|
|
case '\n':
|
|
case '\r':
|
|
case KEY_ENTER:
|
|
if (input_pos > 0) {
|
|
strncpy(branch_name, input, max_len - 1);
|
|
branch_name[max_len - 1] = '\0';
|
|
result = 1;
|
|
}
|
|
goto cleanup;
|
|
|
|
case KEY_BACKSPACE:
|
|
case 127:
|
|
case '\b':
|
|
if (input_pos > 0) {
|
|
input_pos--;
|
|
input[input_pos] = '\0';
|
|
}
|
|
break;
|
|
|
|
default:
|
|
// Add printable characters
|
|
if (ch >= 32 && ch <= 126 && input_pos < max_len - 1) {
|
|
input[input_pos] = ch;
|
|
input_pos++;
|
|
input[input_pos] = '\0';
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
cleanup:
|
|
// Restore ncurses settings
|
|
noecho();
|
|
curs_set(0);
|
|
delwin(input_win);
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Create a new git branch
|
|
*/
|
|
int create_git_branch(const char *branch_name) {
|
|
if (!branch_name || strlen(branch_name) == 0)
|
|
return 0;
|
|
|
|
// Clean branch name: replace spaces with dashes
|
|
char clean_branch_name[256];
|
|
strncpy(clean_branch_name, branch_name, sizeof(clean_branch_name) - 1);
|
|
clean_branch_name[sizeof(clean_branch_name) - 1] = '\0';
|
|
|
|
// Replace spaces with dashes
|
|
for (int i = 0; clean_branch_name[i] != '\0'; i++) {
|
|
if (clean_branch_name[i] == ' ') {
|
|
clean_branch_name[i] = '-';
|
|
}
|
|
}
|
|
|
|
char cmd[512];
|
|
snprintf(cmd, sizeof(cmd), "git checkout -b \"%s\" >/dev/null 2>&1",
|
|
clean_branch_name);
|
|
|
|
return (system(cmd) == 0);
|
|
}
|
|
|
|
/**
|
|
* Get new branch name input from user for branch renaming
|
|
*/
|
|
int get_rename_branch_input(const char *current_name, char *new_name,
|
|
int max_len) {
|
|
if (!current_name || !new_name || max_len <= 0)
|
|
return 0;
|
|
|
|
// Create input window
|
|
int win_height = 8;
|
|
int win_width = 60;
|
|
int start_y = (LINES - win_height) / 2;
|
|
int start_x = (COLS - win_width) / 2;
|
|
|
|
WINDOW *input_win = newwin(win_height, win_width, start_y, start_x);
|
|
if (!input_win)
|
|
return 0;
|
|
|
|
char input[256] = {0};
|
|
int input_pos = 0;
|
|
int result = 0;
|
|
|
|
// Pre-fill with current name
|
|
strncpy(input, current_name, sizeof(input) - 1);
|
|
input_pos = strlen(input);
|
|
|
|
// Enable echo and cursor for input
|
|
echo();
|
|
curs_set(1);
|
|
keypad(input_win, TRUE);
|
|
|
|
while (1) {
|
|
// Redraw window
|
|
werase(input_win);
|
|
box(input_win, 0, 0);
|
|
mvwprintw(input_win, 0, 2, " Rename Branch ");
|
|
mvwprintw(input_win, 2, 2, "Current: %s", current_name);
|
|
mvwprintw(input_win, 3, 2, "New name:");
|
|
mvwprintw(input_win, 6, 2, "Enter: rename | Esc: cancel");
|
|
|
|
// Show current input
|
|
mvwprintw(input_win, 4, 2, "> %s", input);
|
|
|
|
// Position cursor at end of input
|
|
wmove(input_win, 4, 4 + input_pos);
|
|
wrefresh(input_win);
|
|
|
|
int ch = wgetch(input_win);
|
|
|
|
switch (ch) {
|
|
case 27: // ESC
|
|
result = 0;
|
|
goto cleanup_rename;
|
|
|
|
case '\n':
|
|
case '\r':
|
|
case KEY_ENTER:
|
|
if (input_pos > 0 && strcmp(input, current_name) != 0) {
|
|
strncpy(new_name, input, max_len - 1);
|
|
new_name[max_len - 1] = '\0';
|
|
result = 1;
|
|
}
|
|
goto cleanup_rename;
|
|
|
|
case KEY_BACKSPACE:
|
|
case 127:
|
|
case '\b':
|
|
if (input_pos > 0) {
|
|
input_pos--;
|
|
input[input_pos] = '\0';
|
|
}
|
|
break;
|
|
|
|
default:
|
|
// Add printable characters
|
|
if (ch >= 32 && ch <= 126 && input_pos < max_len - 1) {
|
|
input[input_pos] = ch;
|
|
input_pos++;
|
|
input[input_pos] = '\0';
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
cleanup_rename:
|
|
// Restore ncurses settings
|
|
noecho();
|
|
curs_set(0);
|
|
delwin(input_win);
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Rename a git branch
|
|
*/
|
|
int rename_git_branch(const char *old_name, const char *new_name) {
|
|
if (!old_name || !new_name || strlen(old_name) == 0 || strlen(new_name) == 0)
|
|
return 0;
|
|
|
|
char cmd[512];
|
|
snprintf(cmd, sizeof(cmd), "git branch -m \"%s\" \"%s\" >/dev/null 2>&1",
|
|
old_name, new_name);
|
|
|
|
return (system(cmd) == 0);
|
|
}
|
|
|
|
/**
|
|
* Show delete branch confirmation dialog
|
|
*/
|
|
int show_delete_branch_dialog(const char *branch_name) {
|
|
if (!branch_name)
|
|
return DELETE_CANCEL;
|
|
|
|
// Create dialog window
|
|
int win_height = 8;
|
|
int win_width = 50;
|
|
int start_y = (LINES - win_height) / 2;
|
|
int start_x = (COLS - win_width) / 2;
|
|
|
|
WINDOW *dialog_win = newwin(win_height, win_width, start_y, start_x);
|
|
if (!dialog_win)
|
|
return DELETE_CANCEL;
|
|
|
|
int selected_option = DELETE_LOCAL;
|
|
const char *options[] = {"Delete local (l)", "Delete remote (r)",
|
|
"Delete both (b)"};
|
|
|
|
while (1) {
|
|
werase(dialog_win);
|
|
box(dialog_win, 0, 0);
|
|
mvwprintw(dialog_win, 0, 2, " Delete Branch ");
|
|
mvwprintw(dialog_win, 2, 2, "Branch: %s", branch_name);
|
|
|
|
// Draw options
|
|
for (int i = 0; i < 3; i++) {
|
|
int y = 3 + i;
|
|
if (i == selected_option) {
|
|
wattron(dialog_win, COLOR_PAIR(5)); // Highlight selected
|
|
mvwprintw(dialog_win, y, 2, "> %s", options[i]);
|
|
wattroff(dialog_win, COLOR_PAIR(5));
|
|
} else {
|
|
mvwprintw(dialog_win, y, 2, " %s", options[i]);
|
|
}
|
|
}
|
|
|
|
mvwprintw(dialog_win, 6, 2, "Enter: select | Esc: cancel");
|
|
wrefresh(dialog_win);
|
|
|
|
int key = getch();
|
|
switch (key) {
|
|
case 27: // ESC
|
|
delwin(dialog_win);
|
|
return DELETE_CANCEL;
|
|
|
|
case 'l':
|
|
delwin(dialog_win);
|
|
return DELETE_LOCAL;
|
|
|
|
case 'r':
|
|
delwin(dialog_win);
|
|
return DELETE_REMOTE;
|
|
|
|
case 'b':
|
|
delwin(dialog_win);
|
|
return DELETE_BOTH;
|
|
|
|
case KEY_UP:
|
|
case 'k':
|
|
if (selected_option > 0) {
|
|
selected_option--;
|
|
}
|
|
break;
|
|
|
|
case KEY_DOWN:
|
|
case 'j':
|
|
if (selected_option < 2) {
|
|
selected_option++;
|
|
}
|
|
break;
|
|
|
|
case '\n':
|
|
case '\r':
|
|
case KEY_ENTER:
|
|
delwin(dialog_win);
|
|
return selected_option;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Show universal error popup
|
|
*/
|
|
void show_error_popup(const char *error_message) {
|
|
if (!error_message)
|
|
return;
|
|
|
|
int max_y, max_x;
|
|
getmaxyx(stdscr, max_y, max_x);
|
|
|
|
int popup_height = 5;
|
|
int popup_width = strlen(error_message) + 6;
|
|
if (popup_width > max_x - 4) {
|
|
popup_width = max_x - 4;
|
|
}
|
|
|
|
int start_y = (max_y - popup_height) / 2;
|
|
int start_x = (max_x - popup_width) / 2;
|
|
|
|
WINDOW *popup_win = newwin(popup_height, popup_width, start_y, start_x);
|
|
|
|
wattron(popup_win, COLOR_PAIR(1));
|
|
box(popup_win, 0, 0);
|
|
|
|
mvwprintw(popup_win, 1, 2, "Error:");
|
|
mvwprintw(popup_win, 2, 2, "%.*s", popup_width - 4, error_message);
|
|
mvwprintw(popup_win, 3, 2, "Press any key to continue...");
|
|
|
|
wattroff(popup_win, COLOR_PAIR(1));
|
|
wrefresh(popup_win);
|
|
|
|
// Wait for user input
|
|
getch();
|
|
|
|
delwin(popup_win);
|
|
clear();
|
|
refresh();
|
|
}
|
|
|
|
/**
|
|
* Get available git remotes
|
|
*/
|
|
int get_git_remotes(char remotes[][256], int max_remotes) {
|
|
FILE *fp = popen("git remote 2>/dev/null", "r");
|
|
if (!fp)
|
|
return 0;
|
|
|
|
int count = 0;
|
|
char line[256];
|
|
|
|
while (fgets(line, sizeof(line), fp) && count < max_remotes) {
|
|
// Remove trailing newline
|
|
int len = strlen(line);
|
|
if (len > 0 && line[len - 1] == '\n') {
|
|
line[len - 1] = '\0';
|
|
}
|
|
|
|
if (strlen(line) > 0) {
|
|
strncpy(remotes[count], line, 255);
|
|
remotes[count][255] = '\0';
|
|
count++;
|
|
}
|
|
}
|
|
|
|
pclose(fp);
|
|
return count;
|
|
}
|
|
|
|
/**
|
|
* Show upstream selection dialog
|
|
*/
|
|
int show_upstream_selection_dialog(const char *branch_name,
|
|
char *upstream_result, int max_len) {
|
|
if (!branch_name || !upstream_result)
|
|
return 0;
|
|
|
|
int max_y, max_x;
|
|
getmaxyx(stdscr, max_y, max_x);
|
|
|
|
int dialog_height = 12;
|
|
int dialog_width = 60;
|
|
int start_y = (max_y - dialog_height) / 2;
|
|
int start_x = (max_x - dialog_width) / 2;
|
|
|
|
WINDOW *dialog_win = newwin(dialog_height, dialog_width, start_y, start_x);
|
|
|
|
// Get available remotes
|
|
char remotes[10][256];
|
|
int remote_count = get_git_remotes(remotes, 10);
|
|
|
|
char input_buffer[256] = "";
|
|
int cursor_pos = 0;
|
|
int selected_suggestion = 0;
|
|
|
|
// Default suggestion
|
|
if (remote_count > 0) {
|
|
snprintf(input_buffer, sizeof(input_buffer), "%s %s", remotes[0],
|
|
branch_name);
|
|
cursor_pos = strlen(input_buffer);
|
|
}
|
|
|
|
while (1) {
|
|
werase(dialog_win);
|
|
box(dialog_win, 0, 0);
|
|
|
|
// Title
|
|
mvwprintw(dialog_win, 1, 2, "Set Upstream Branch");
|
|
mvwprintw(dialog_win, 2, 2, "Enter upstream as <remote> <branchname>");
|
|
|
|
// Input field
|
|
mvwprintw(dialog_win, 4, 2, "Upstream: %s", input_buffer);
|
|
|
|
// Suggestions header
|
|
mvwprintw(dialog_win, 6, 2, "Suggestions (press <tab> to focus):");
|
|
|
|
// Show suggestions
|
|
for (int i = 0; i < remote_count && i < 3; i++) {
|
|
char suggestion[256];
|
|
snprintf(suggestion, sizeof(suggestion), "%s %s", remotes[i],
|
|
branch_name);
|
|
|
|
if (i == selected_suggestion) {
|
|
wattron(dialog_win, A_REVERSE);
|
|
}
|
|
mvwprintw(dialog_win, 7 + i, 4, "%s", suggestion);
|
|
if (i == selected_suggestion) {
|
|
wattroff(dialog_win, A_REVERSE);
|
|
}
|
|
}
|
|
|
|
// Instructions
|
|
mvwprintw(dialog_win, dialog_height - 2, 2, "Enter: Set | Esc: Cancel");
|
|
|
|
wrefresh(dialog_win);
|
|
|
|
int key = getch();
|
|
|
|
switch (key) {
|
|
case 27: // ESC
|
|
delwin(dialog_win);
|
|
return 0;
|
|
|
|
case '\n':
|
|
case '\r':
|
|
case KEY_ENTER:
|
|
if (strlen(input_buffer) > 0) {
|
|
strncpy(upstream_result, input_buffer, max_len - 1);
|
|
upstream_result[max_len - 1] = '\0';
|
|
delwin(dialog_win);
|
|
return 1;
|
|
}
|
|
break;
|
|
|
|
case '\t':
|
|
// Tab to select suggestion
|
|
if (remote_count > 0 && selected_suggestion < remote_count) {
|
|
snprintf(input_buffer, sizeof(input_buffer), "%s %s",
|
|
remotes[selected_suggestion], branch_name);
|
|
cursor_pos = strlen(input_buffer);
|
|
}
|
|
break;
|
|
|
|
case KEY_UP:
|
|
if (selected_suggestion > 0) {
|
|
selected_suggestion--;
|
|
}
|
|
break;
|
|
|
|
case KEY_DOWN:
|
|
if (selected_suggestion < remote_count - 1) {
|
|
selected_suggestion++;
|
|
}
|
|
break;
|
|
|
|
case KEY_BACKSPACE:
|
|
case 127:
|
|
if (cursor_pos > 0) {
|
|
cursor_pos--;
|
|
input_buffer[cursor_pos] = '\0';
|
|
}
|
|
break;
|
|
|
|
default:
|
|
if (isprint(key) && cursor_pos < (int)sizeof(input_buffer) - 1) {
|
|
input_buffer[cursor_pos] = key;
|
|
cursor_pos++;
|
|
input_buffer[cursor_pos] = '\0';
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
delwin(dialog_win);
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Get current git branch name
|
|
*/
|
|
int get_current_branch_name(char *branch_name, int max_len) {
|
|
if (!branch_name)
|
|
return 0;
|
|
|
|
FILE *fp = popen("git rev-parse --abbrev-ref HEAD 2>/dev/null", "r");
|
|
if (!fp)
|
|
return 0;
|
|
|
|
if (fgets(branch_name, max_len, fp) != NULL) {
|
|
// Remove trailing newline
|
|
int len = strlen(branch_name);
|
|
if (len > 0 && branch_name[len - 1] == '\n') {
|
|
branch_name[len - 1] = '\0';
|
|
}
|
|
pclose(fp);
|
|
return 1;
|
|
}
|
|
|
|
pclose(fp);
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Check if a branch has an upstream
|
|
*/
|
|
int branch_has_upstream(const char *branch_name) {
|
|
if (!branch_name)
|
|
return 0;
|
|
|
|
char cmd[512];
|
|
snprintf(cmd, sizeof(cmd),
|
|
"git rev-parse --abbrev-ref \"%s@{upstream}\" >/dev/null 2>&1",
|
|
branch_name);
|
|
return (system(cmd) == 0);
|
|
}
|
|
|
|
/**
|
|
* Delete git branch based on option
|
|
*/
|
|
int delete_git_branch(const char *branch_name, DeleteBranchOption option) {
|
|
if (!branch_name || option == DELETE_CANCEL)
|
|
return 0;
|
|
|
|
// Check for upstream before attempting remote deletion
|
|
if (option == DELETE_REMOTE || option == DELETE_BOTH) {
|
|
if (!branch_has_upstream(branch_name)) {
|
|
// Show error message for branches without upstream
|
|
show_error_popup("The selected branch has no upstream (tip: delete the "
|
|
"branch locally)");
|
|
return 0; // Don't proceed with deletion
|
|
}
|
|
}
|
|
|
|
char cmd[512];
|
|
int success = 1;
|
|
|
|
switch (option) {
|
|
case DELETE_LOCAL:
|
|
snprintf(cmd, sizeof(cmd), "git branch -D \"%s\" >/dev/null 2>&1",
|
|
branch_name);
|
|
success = (system(cmd) == 0);
|
|
break;
|
|
|
|
case DELETE_REMOTE:
|
|
snprintf(cmd, sizeof(cmd),
|
|
"git push origin --delete \"%s\" >/dev/null 2>&1", branch_name);
|
|
success = (system(cmd) == 0);
|
|
break;
|
|
|
|
case DELETE_BOTH:
|
|
// Delete local first
|
|
snprintf(cmd, sizeof(cmd), "git branch -D \"%s\" >/dev/null 2>&1",
|
|
branch_name);
|
|
success = (system(cmd) == 0);
|
|
|
|
// Then delete remote
|
|
if (success) {
|
|
snprintf(cmd, sizeof(cmd),
|
|
"git push origin --delete \"%s\" >/dev/null 2>&1", branch_name);
|
|
success = (system(cmd) == 0);
|
|
}
|
|
break;
|
|
|
|
case DELETE_CANCEL:
|
|
default:
|
|
return 0;
|
|
}
|
|
|
|
return success;
|
|
}
|
|
|
|
int get_ncurses_git_stashes(NCursesDiffViewer *viewer) {
|
|
if (!viewer)
|
|
return 0;
|
|
|
|
char stash_lines[MAX_STASHES][512];
|
|
viewer->stash_count = get_git_stashes(stash_lines, MAX_STASHES);
|
|
|
|
for (int i = 0; i < viewer->stash_count; i++) {
|
|
strncpy(viewer->stashes[i].stash_info, stash_lines[i], 511);
|
|
viewer->stashes[i].stash_info[511] = '\0';
|
|
}
|
|
|
|
return viewer->stash_count;
|
|
}
|
|
|
|
/**
|
|
* Get stash name input from user
|
|
*/
|
|
int get_stash_name_input(char *stash_name, int max_len) {
|
|
if (!stash_name)
|
|
return 0;
|
|
|
|
// Save current screen
|
|
WINDOW *saved_screen = dupwin(stdscr);
|
|
|
|
// Calculate window dimensions
|
|
int input_width = COLS * 0.6; // 60% of screen width
|
|
int input_height = 3;
|
|
int start_x = COLS / 2 - input_width / 2;
|
|
int start_y = LINES / 2 - input_height / 2;
|
|
|
|
// Create input window
|
|
WINDOW *input_win = newwin(input_height, input_width, start_y, start_x);
|
|
|
|
if (!input_win) {
|
|
if (saved_screen)
|
|
delwin(saved_screen);
|
|
return 0;
|
|
}
|
|
|
|
// Variables for input handling
|
|
int input_scroll_offset = 0; // For horizontal scrolling
|
|
int ch;
|
|
|
|
// Function to redraw input window
|
|
void redraw_input() {
|
|
werase(input_win);
|
|
box(input_win, 0, 0);
|
|
|
|
int visible_width = input_width - 4;
|
|
int name_len = strlen(stash_name);
|
|
|
|
// Clear the content area with spaces FIRST
|
|
for (int x = 1; x <= visible_width; x++) {
|
|
mvwaddch(input_win, 1, x, ' ');
|
|
}
|
|
|
|
// Calculate what part of the name to show
|
|
int display_start = input_scroll_offset;
|
|
int display_end = display_start + visible_width;
|
|
if (display_end > name_len)
|
|
display_end = name_len;
|
|
|
|
// Show the visible portion of the name ON TOP of cleared spaces
|
|
for (int i = display_start; i < display_end; i++) {
|
|
mvwaddch(input_win, 1, 1 + (i - display_start), stash_name[i]);
|
|
}
|
|
|
|
// Header
|
|
wattron(input_win, COLOR_PAIR(4));
|
|
mvwprintw(input_win, 0, 2,
|
|
" Enter stash name (ESC to cancel, Enter to confirm) ");
|
|
wattroff(input_win, COLOR_PAIR(4));
|
|
|
|
wrefresh(input_win);
|
|
}
|
|
|
|
// Initial draw
|
|
redraw_input();
|
|
|
|
// Position initial cursor
|
|
int cursor_pos = strlen(stash_name) - input_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(input_win, 1, 1 + cursor_pos);
|
|
wrefresh(input_win);
|
|
|
|
curs_set(1);
|
|
noecho();
|
|
|
|
// Main input loop
|
|
while (1) {
|
|
ch = getch();
|
|
|
|
if (ch == 27) {
|
|
// ESC to cancel
|
|
stash_name[0] = '\0'; // Clear name
|
|
break;
|
|
}
|
|
|
|
if (ch == '\n' || ch == '\r') {
|
|
// Enter to confirm - accept if name has content
|
|
if (strlen(stash_name) > 0) {
|
|
break;
|
|
}
|
|
} else if (ch == KEY_BACKSPACE || ch == 127 || ch == 8) {
|
|
// Handle backspace
|
|
int len = strlen(stash_name);
|
|
if (len > 0) {
|
|
stash_name[len - 1] = '\0';
|
|
|
|
// Adjust scroll if needed
|
|
int visible_width = input_width - 4;
|
|
if (len - 1 <= input_scroll_offset) {
|
|
input_scroll_offset = (len - 1) - (visible_width - 5);
|
|
if (input_scroll_offset < 0)
|
|
input_scroll_offset = 0;
|
|
}
|
|
redraw_input();
|
|
}
|
|
} else if (ch >= 32 && ch <= 126) {
|
|
// Regular character input
|
|
int len = strlen(stash_name);
|
|
if (len < max_len - 1) {
|
|
stash_name[len] = ch;
|
|
stash_name[len + 1] = '\0';
|
|
|
|
// Auto-scroll horizontally if needed
|
|
int visible_width = input_width - 4;
|
|
if (len + 1 > input_scroll_offset + visible_width - 5) {
|
|
input_scroll_offset = (len + 1) - (visible_width - 5);
|
|
}
|
|
redraw_input();
|
|
}
|
|
}
|
|
|
|
// Position cursor
|
|
int cursor_pos = strlen(stash_name) - input_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(input_win, 1, 1 + cursor_pos);
|
|
wrefresh(input_win);
|
|
}
|
|
|
|
// Restore settings
|
|
curs_set(0); // Hide cursor
|
|
|
|
// Clean up window
|
|
delwin(input_win);
|
|
|
|
// Restore the screen
|
|
if (saved_screen) {
|
|
overwrite(saved_screen, stdscr);
|
|
delwin(saved_screen);
|
|
}
|
|
|
|
// Force a complete redraw
|
|
clear();
|
|
refresh();
|
|
|
|
return strlen(stash_name) > 0 ? 1 : 0;
|
|
}
|
|
|
|
/**
|
|
* Create a new git stash
|
|
*/
|
|
int create_ncurses_git_stash(NCursesDiffViewer *viewer) {
|
|
if (!viewer)
|
|
return 0;
|
|
|
|
// Get stash name from user
|
|
char stash_name[256] = "";
|
|
if (!get_stash_name_input(stash_name, sizeof(stash_name))) {
|
|
return 0; // User cancelled
|
|
}
|
|
|
|
int result = create_git_stash_with_name(stash_name);
|
|
|
|
if (result) {
|
|
// Refresh everything after creating stash
|
|
get_ncurses_changed_files(viewer);
|
|
get_ncurses_git_stashes(viewer);
|
|
get_commit_history(viewer);
|
|
|
|
// Reset file selection since changes are stashed
|
|
viewer->selected_file = 0;
|
|
viewer->file_line_count = 0;
|
|
viewer->file_scroll_offset = 0;
|
|
|
|
// Reload current file if any files still exist
|
|
if (viewer->file_count > 0 && viewer->selected_file < viewer->file_count) {
|
|
load_full_file_with_diff(viewer,
|
|
viewer->files[viewer->selected_file].filename);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Render the stash list window
|
|
*/
|
|
void render_stash_list_window(NCursesDiffViewer *viewer) {
|
|
if (!viewer || !viewer->stash_list_win)
|
|
return;
|
|
|
|
// Clear the entire window first
|
|
werase(viewer->stash_list_win);
|
|
|
|
// Draw rounded border and title
|
|
draw_rounded_box(viewer->stash_list_win);
|
|
mvwprintw(viewer->stash_list_win, 0, 2, " 5. Stashes ");
|
|
|
|
int max_stashes_visible = viewer->stash_panel_height - 2;
|
|
|
|
// Clear the content area with spaces
|
|
for (int y = 1; y < viewer->stash_panel_height - 1; y++) {
|
|
for (int x = 1; x < viewer->file_panel_width - 1; x++) {
|
|
mvwaddch(viewer->stash_list_win, y, x, ' ');
|
|
}
|
|
}
|
|
|
|
if (viewer->stash_count == 0) {
|
|
// Show "No stashes" message
|
|
mvwprintw(viewer->stash_list_win, 1, 2, "No stashes available");
|
|
} else {
|
|
for (int i = 0; i < max_stashes_visible && i < viewer->stash_count; i++) {
|
|
int y = i + 1;
|
|
|
|
// Check if this stash line should be highlighted
|
|
int is_selected_stash = (i == viewer->selected_stash &&
|
|
viewer->current_mode == NCURSES_MODE_STASH_LIST);
|
|
|
|
// Apply line highlight if selected
|
|
if (is_selected_stash) {
|
|
wattron(viewer->stash_list_win, COLOR_PAIR(5));
|
|
}
|
|
|
|
// Show selection indicator
|
|
if (is_selected_stash) {
|
|
mvwprintw(viewer->stash_list_win, y, 1, ">");
|
|
} else {
|
|
mvwprintw(viewer->stash_list_win, y, 1, " ");
|
|
}
|
|
|
|
// Show stash info (truncated to fit panel)
|
|
int max_stash_len = viewer->file_panel_width - 4;
|
|
char truncated_stash[256];
|
|
if ((int)strlen(viewer->stashes[i].stash_info) > max_stash_len) {
|
|
strncpy(truncated_stash, viewer->stashes[i].stash_info,
|
|
max_stash_len - 2);
|
|
truncated_stash[max_stash_len - 2] = '\0';
|
|
strcat(truncated_stash, "..");
|
|
} else {
|
|
strcpy(truncated_stash, viewer->stashes[i].stash_info);
|
|
}
|
|
|
|
// Show stash info with yellow color
|
|
if (is_selected_stash) {
|
|
wattroff(viewer->stash_list_win, COLOR_PAIR(5));
|
|
}
|
|
|
|
wattron(viewer->stash_list_win, COLOR_PAIR(4));
|
|
mvwprintw(viewer->stash_list_win, y, 2, "%s", truncated_stash);
|
|
wattroff(viewer->stash_list_win, COLOR_PAIR(4));
|
|
|
|
if (is_selected_stash) {
|
|
wattron(viewer->stash_list_win, COLOR_PAIR(5));
|
|
}
|
|
|
|
// Turn off selection highlighting if this was the selected stash
|
|
if (is_selected_stash) {
|
|
wattroff(viewer->stash_list_win, COLOR_PAIR(5));
|
|
}
|
|
}
|
|
}
|
|
|
|
wrefresh(viewer->stash_list_win);
|
|
}
|
|
|
|
void render_branch_list_window(NCursesDiffViewer *viewer) {
|
|
if (!viewer || !viewer->branch_list_win)
|
|
return;
|
|
|
|
werase(viewer->branch_list_win);
|
|
|
|
draw_rounded_box(viewer->branch_list_win);
|
|
mvwprintw(viewer->branch_list_win, 0, 2, " 3. Branches ");
|
|
|
|
int max_branches_visible = viewer->branch_panel_height - 2;
|
|
|
|
for (int y = 1; y < viewer->branch_panel_height - 1; y++) {
|
|
for (int x = 1; x < viewer->file_panel_width - 1; x++) {
|
|
mvwaddch(viewer->branch_list_win, y, x, ' ');
|
|
}
|
|
}
|
|
|
|
if (viewer->branch_count == 0) {
|
|
mvwprintw(viewer->branch_list_win, 1, 2, "No branches available");
|
|
} else {
|
|
for (int i = 0; i < max_branches_visible && i < viewer->branch_count; i++) {
|
|
int y = i + 1;
|
|
|
|
int is_selected_branch =
|
|
(i == viewer->selected_branch &&
|
|
viewer->current_mode == NCURSES_MODE_BRANCH_LIST);
|
|
int is_current_branch =
|
|
viewer->branches[i].status; // status = 1 for current branch
|
|
|
|
if (is_selected_branch) {
|
|
wattron(viewer->branch_list_win, COLOR_PAIR(5));
|
|
}
|
|
|
|
// Show selection arrow
|
|
if (is_selected_branch) {
|
|
mvwprintw(viewer->branch_list_win, y, 1, ">");
|
|
} else {
|
|
mvwprintw(viewer->branch_list_win, y, 1, " ");
|
|
}
|
|
|
|
// Prepare branch display with asterisk for current branch and status
|
|
int max_branch_len = viewer->file_panel_width -
|
|
15; // Leave more space for status indicators
|
|
char display_branch[300];
|
|
char status_indicator[50] = "";
|
|
|
|
// Create status indicator
|
|
if (viewer->branches[i].commits_ahead > 0 &&
|
|
viewer->branches[i].commits_behind > 0) {
|
|
snprintf(status_indicator, sizeof(status_indicator), " %d↑%d↓",
|
|
viewer->branches[i].commits_ahead,
|
|
viewer->branches[i].commits_behind);
|
|
} else if (viewer->branches[i].commits_ahead > 0) {
|
|
snprintf(status_indicator, sizeof(status_indicator), " %d↑",
|
|
viewer->branches[i].commits_ahead);
|
|
} else if (viewer->branches[i].commits_behind > 0) {
|
|
snprintf(status_indicator, sizeof(status_indicator), " %d↓",
|
|
viewer->branches[i].commits_behind);
|
|
}
|
|
|
|
if (is_current_branch) {
|
|
snprintf(display_branch, sizeof(display_branch), "* %s",
|
|
viewer->branches[i].name);
|
|
} else {
|
|
snprintf(display_branch, sizeof(display_branch), " %s",
|
|
viewer->branches[i].name);
|
|
}
|
|
|
|
// Truncate branch name if too long
|
|
if ((int)strlen(display_branch) > max_branch_len) {
|
|
display_branch[max_branch_len - 2] = '.';
|
|
display_branch[max_branch_len - 1] = '.';
|
|
display_branch[max_branch_len] = '\0';
|
|
}
|
|
|
|
// Color the branch name
|
|
if (is_current_branch) {
|
|
wattron(viewer->branch_list_win,
|
|
COLOR_PAIR(1)); // Green for current branch
|
|
} else {
|
|
wattron(viewer->branch_list_win,
|
|
COLOR_PAIR(4)); // Yellow for other branches
|
|
}
|
|
|
|
mvwprintw(viewer->branch_list_win, y, 2, "%s", display_branch);
|
|
|
|
if (is_current_branch) {
|
|
wattroff(viewer->branch_list_win, COLOR_PAIR(1));
|
|
} else {
|
|
wattroff(viewer->branch_list_win, COLOR_PAIR(4));
|
|
}
|
|
|
|
// Add status indicator with appropriate colors
|
|
if (strlen(status_indicator) > 0) {
|
|
if (is_selected_branch) {
|
|
wattroff(viewer->branch_list_win, COLOR_PAIR(5));
|
|
}
|
|
|
|
if (viewer->branches[i].commits_behind > 0) {
|
|
wattron(viewer->branch_list_win, COLOR_PAIR(2)); // Red for behind
|
|
} else {
|
|
wattron(viewer->branch_list_win, COLOR_PAIR(1)); // Green for ahead
|
|
}
|
|
|
|
mvwprintw(viewer->branch_list_win, y, 2 + strlen(display_branch), "%s",
|
|
status_indicator);
|
|
|
|
if (viewer->branches[i].commits_behind > 0) {
|
|
wattroff(viewer->branch_list_win, COLOR_PAIR(2));
|
|
} else {
|
|
wattroff(viewer->branch_list_win, COLOR_PAIR(1));
|
|
}
|
|
|
|
if (is_selected_branch) {
|
|
wattron(viewer->branch_list_win, COLOR_PAIR(5));
|
|
}
|
|
}
|
|
|
|
if (is_selected_branch) {
|
|
wattroff(viewer->branch_list_win, COLOR_PAIR(5));
|
|
}
|
|
}
|
|
}
|
|
wrefresh(viewer->branch_list_win);
|
|
}
|
|
|
|
/**
|
|
* Parse and load content lines from text with diff highlighting
|
|
*/
|
|
int parse_content_lines(NCursesDiffViewer *viewer, const char *content) {
|
|
if (!viewer || !content) {
|
|
return 0;
|
|
}
|
|
|
|
viewer->file_line_count = 0;
|
|
viewer->file_scroll_offset = 0;
|
|
viewer->file_cursor_line = 0;
|
|
|
|
// Parse line by line, preserving empty lines
|
|
const char *line_start = content;
|
|
const char *line_end;
|
|
|
|
while (*line_start && viewer->file_line_count < MAX_FULL_FILE_LINES) {
|
|
NCursesFileLine *file_line = &viewer->file_lines[viewer->file_line_count];
|
|
|
|
// Find end of current line
|
|
line_end = strchr(line_start, '\n');
|
|
if (!line_end) {
|
|
line_end = line_start + strlen(line_start);
|
|
}
|
|
|
|
// Calculate line length
|
|
size_t line_len = line_end - line_start;
|
|
|
|
// Copy line content (truncate if too long)
|
|
size_t copy_len = (line_len < sizeof(file_line->line) - 1)
|
|
? line_len
|
|
: sizeof(file_line->line) - 1;
|
|
strncpy(file_line->line, line_start, copy_len);
|
|
file_line->line[copy_len] = '\0';
|
|
|
|
// Determine line type for syntax highlighting
|
|
if (line_len == 0) {
|
|
file_line->type = ' '; // Empty line
|
|
} else if (strncmp(file_line->line, "diff --git", 10) == 0 ||
|
|
strncmp(file_line->line, "index ", 6) == 0 ||
|
|
strncmp(file_line->line, "--- ", 4) == 0 ||
|
|
strncmp(file_line->line, "+++ ", 4) == 0) {
|
|
file_line->type = '@'; // Use @ for headers
|
|
} else if (line_len > 1 && file_line->line[0] == '@' &&
|
|
file_line->line[1] == '@') {
|
|
file_line->type = '@'; // Hunk headers
|
|
} else if (line_len > 0 && file_line->line[0] == '+') {
|
|
file_line->type = '+'; // Added lines
|
|
} else if (line_len > 0 && file_line->line[0] == '-') {
|
|
file_line->type = '-'; // Removed lines
|
|
} else if (strstr(file_line->line, " | ") &&
|
|
(strstr(file_line->line, "+") || strstr(file_line->line, "-") ||
|
|
strstr(file_line->line, "Bin"))) {
|
|
file_line->type = 's'; // File statistics lines (special type)
|
|
} else if (strstr(file_line->line, " files changed") ||
|
|
strstr(file_line->line, " insertions") ||
|
|
strstr(file_line->line, " deletions")) {
|
|
file_line->type = 's'; // Summary statistics lines
|
|
} else if (strncmp(file_line->line, "commit ", 7) == 0) {
|
|
file_line->type = 'h'; // Commit header
|
|
} else if (strncmp(file_line->line, "Author: ", 8) == 0 ||
|
|
strncmp(file_line->line, "Date: ", 6) == 0) {
|
|
file_line->type = 'i'; // Commit info lines
|
|
} else {
|
|
file_line->type = ' '; // Normal/context lines
|
|
}
|
|
|
|
file_line->is_diff_line = (file_line->type != ' ') ? 1 : 0;
|
|
|
|
viewer->file_line_count++;
|
|
|
|
// Move to next line
|
|
if (*line_end == '\n') {
|
|
line_start = line_end + 1;
|
|
} else {
|
|
break; // End of content
|
|
}
|
|
}
|
|
return viewer->file_line_count;
|
|
}
|
|
|
|
/**
|
|
* Load commit details for viewing
|
|
*/
|
|
int load_commit_for_viewing(NCursesDiffViewer *viewer,
|
|
const char *commit_hash) {
|
|
if (!viewer || !commit_hash) {
|
|
return 0;
|
|
}
|
|
|
|
char *commit_content = malloc(50000); // Large buffer for commit details
|
|
if (!commit_content) {
|
|
return 0;
|
|
}
|
|
|
|
if (get_commit_details(commit_hash, commit_content, 50000)) {
|
|
int result = parse_content_lines(viewer, commit_content);
|
|
free(commit_content);
|
|
return result;
|
|
}
|
|
|
|
free(commit_content);
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Load stash details for viewing
|
|
*/
|
|
int load_stash_for_viewing(NCursesDiffViewer *viewer, int stash_index) {
|
|
if (!viewer || stash_index < 0) {
|
|
return 0;
|
|
}
|
|
|
|
char *stash_content = malloc(50000); // Large buffer for stash details
|
|
if (!stash_content) {
|
|
return 0;
|
|
}
|
|
|
|
if (get_stash_diff(stash_index, stash_content, 50000)) {
|
|
int result = parse_content_lines(viewer, stash_content);
|
|
free(stash_content);
|
|
return result;
|
|
}
|
|
|
|
free(stash_content);
|
|
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->stash_list_win) {
|
|
delwin(viewer->stash_list_win);
|
|
}
|
|
if (viewer->branch_list_win) {
|
|
delwin(viewer->branch_list_win);
|
|
}
|
|
if (viewer->status_bar_win) {
|
|
delwin(viewer->status_bar_win);
|
|
}
|
|
}
|
|
endwin();
|
|
}
|