when grepping for commits, searching for author initials is now possible, and will be prioritized first. this means typing ma for marcus for example will give you all commits from marcus. if no hits are found on initials it will apply that grep to the name/titles. also some highlighing has been added to commit grep updated stash grep to include possiblility to grep for time and branches. this means you can filter for branches and times
7747 lines
238 KiB
C
7747 lines
238 KiB
C
/**
|
|
* ncurses_diff_viewer.c
|
|
* NCurses-based interactive diff viewer implementation
|
|
*/
|
|
|
|
#include "ncurses_diff_viewer.h"
|
|
#include "git_integration.h"
|
|
#include <ctype.h>
|
|
#include <locale.h>
|
|
#include <ncurses.h>
|
|
#include <signal.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/wait.h>
|
|
#include <time.h>
|
|
#include <unistd.h>
|
|
|
|
static volatile int terminal_resized = 0;
|
|
|
|
void handle_sigwinch(int sig) {
|
|
(void)sig;
|
|
terminal_resized = 1;
|
|
}
|
|
|
|
/**
|
|
* Handle terminal resize by recreating all windows
|
|
*/
|
|
void handle_terminal_resize(NCursesDiffViewer *viewer) {
|
|
if (!viewer)
|
|
return;
|
|
|
|
// Clean up old windows
|
|
if (viewer->file_list_win)
|
|
delwin(viewer->file_list_win);
|
|
if (viewer->branch_list_win)
|
|
delwin(viewer->branch_list_win);
|
|
if (viewer->commit_list_win)
|
|
delwin(viewer->commit_list_win);
|
|
if (viewer->stash_list_win)
|
|
delwin(viewer->stash_list_win);
|
|
if (viewer->file_content_win)
|
|
delwin(viewer->file_content_win);
|
|
if (viewer->status_bar_win)
|
|
delwin(viewer->status_bar_win);
|
|
|
|
// Clean up search windows during resize
|
|
cleanup_fuzzy_search(viewer);
|
|
cleanup_grep_search(viewer);
|
|
|
|
// Reinitialize ncurses with new terminal size
|
|
endwin();
|
|
refresh();
|
|
clear();
|
|
|
|
// Get new terminal dimensions
|
|
getmaxyx(stdscr, viewer->terminal_height, viewer->terminal_width);
|
|
viewer->file_panel_width = viewer->terminal_width * 0.4;
|
|
viewer->status_bar_height = viewer->terminal_height * 0.05;
|
|
if (viewer->status_bar_height < 1)
|
|
viewer->status_bar_height = 1;
|
|
|
|
int available_height =
|
|
viewer->terminal_height - 1 - viewer->status_bar_height;
|
|
viewer->file_panel_height = available_height * 0.3;
|
|
viewer->commit_panel_height = available_height * 0.3;
|
|
viewer->branch_panel_height = available_height * 0.2;
|
|
viewer->stash_panel_height = available_height - viewer->file_panel_height -
|
|
viewer->commit_panel_height -
|
|
viewer->branch_panel_height - 3;
|
|
|
|
int status_bar_y = 1 + available_height;
|
|
|
|
// Recreate all windows with new dimensions
|
|
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);
|
|
|
|
// Force complete redraw
|
|
terminal_resized = 0;
|
|
}
|
|
|
|
/**
|
|
* Initialize the ncurses diff viewer
|
|
*/
|
|
int init_ncurses_diff_viewer(NCursesDiffViewer *viewer) {
|
|
if (!viewer)
|
|
return 0;
|
|
|
|
memset(viewer, 0, sizeof(NCursesDiffViewer));
|
|
|
|
// Initialize fuzzy search
|
|
init_fuzzy_search(viewer);
|
|
|
|
// Initialize grep search
|
|
init_grep_search(viewer);
|
|
|
|
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;
|
|
viewer->pushing_branch_index = -1;
|
|
viewer->pulling_branch_index = -1;
|
|
viewer->branch_push_status = SYNC_STATUS_IDLE;
|
|
viewer->branch_pull_status = SYNC_STATUS_IDLE;
|
|
viewer->branch_animation_frame = 0;
|
|
viewer->branch_text_char_count = 0;
|
|
viewer->critical_operation_in_progress = 0;
|
|
|
|
viewer->staged_cursor_line = 0;
|
|
|
|
viewer->split_view_mode = 0;
|
|
viewer->staged_scroll_offset = 0;
|
|
viewer->active_pane = 0;
|
|
viewer->total_hunks = 0;
|
|
viewer->staged_line_count = 0;
|
|
memset(viewer->current_file_path, 0, sizeof(viewer->current_file_path));
|
|
|
|
// Initialize branch commits
|
|
viewer->branch_commit_count = 0;
|
|
viewer->branch_commits_scroll_offset = 0;
|
|
viewer->branch_commits_cursor_line = 0;
|
|
viewer->fetch_pid = -1;
|
|
viewer->fetch_in_progress = 0;
|
|
memset(viewer->current_branch_for_commits, 0,
|
|
sizeof(viewer->current_branch_for_commits));
|
|
|
|
// 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();
|
|
use_default_colors(); // Enable transparent background
|
|
init_pair(1, COLOR_GREEN, -1); // Additions
|
|
init_pair(2, COLOR_RED, -1); // Deletions
|
|
init_pair(3, COLOR_CYAN, -1); // Headers
|
|
init_pair(4, COLOR_YELLOW, -1); // Selected
|
|
init_pair(5, COLOR_BLACK,
|
|
COLOR_WHITE); // Highlighted selection - keep white background
|
|
init_pair(6, COLOR_MAGENTA, -1); // Orange-ish color for commit hash
|
|
|
|
// Gruvbox theme colors for commit info
|
|
init_pair(7, COLOR_CYAN, -1); // HEAD (blue-green)
|
|
init_pair(8, COLOR_GREEN, -1); // main branch (nice green)
|
|
init_pair(9, COLOR_RED, -1); // origin/* (red)
|
|
init_pair(10, COLOR_YELLOW, -1); // commit hash and arrows
|
|
}
|
|
|
|
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) {
|
|
char *newline = strchr(line, '\n');
|
|
if (newline)
|
|
*newline = '\0';
|
|
|
|
if (strlen(line) < 3)
|
|
continue;
|
|
|
|
char staged_status = line[0];
|
|
char unstaged_status = line[1];
|
|
|
|
char *filename = line + 3;
|
|
|
|
viewer->files[viewer->file_count].status =
|
|
(unstaged_status != ' ' ? unstaged_status : staged_status);
|
|
viewer->files[viewer->file_count].marked_for_commit = 0;
|
|
viewer->files[viewer->file_count].has_staged_changes =
|
|
(staged_status != ' ' && staged_status != '?') ? 1 : 0;
|
|
|
|
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 file with staging information
|
|
*/
|
|
|
|
int load_file_with_staging_info(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;
|
|
viewer->total_hunks = 0;
|
|
viewer->staged_line_count = 0;
|
|
viewer->staged_cursor_line = 0;
|
|
|
|
// Store current file path
|
|
strncpy(viewer->current_file_path, filename,
|
|
sizeof(viewer->current_file_path) - 1);
|
|
viewer->current_file_path[sizeof(viewer->current_file_path) - 1] = '\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;
|
|
int current_hunk = 0;
|
|
|
|
// Add fake hunk header for new files
|
|
NCursesFileLine *hunk_line = &viewer->file_lines[viewer->file_line_count];
|
|
snprintf(hunk_line->line, sizeof(hunk_line->line), "@@ -0,0 +1,%d @@", 50);
|
|
hunk_line->type = '@';
|
|
hunk_line->is_diff_line = 0;
|
|
hunk_line->hunk_id = current_hunk;
|
|
hunk_line->is_staged = 0;
|
|
hunk_line->line_number_old = 0;
|
|
hunk_line->line_number_new = 1;
|
|
hunk_line->is_context = 0;
|
|
viewer->file_line_count++;
|
|
|
|
while (fgets(line, sizeof(line), fp) != NULL &&
|
|
viewer->file_line_count < MAX_FULL_FILE_LINES && line_count < 50) {
|
|
|
|
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;
|
|
file_line->hunk_id = current_hunk;
|
|
file_line->is_staged = 0;
|
|
file_line->line_number_old = -1;
|
|
file_line->line_number_new = line_count + 1;
|
|
file_line->is_context = 0;
|
|
|
|
viewer->file_line_count++;
|
|
line_count++;
|
|
}
|
|
|
|
viewer->total_hunks = current_hunk + 1;
|
|
fclose(fp);
|
|
|
|
// For new files, also build staged view from what's actually staged
|
|
rebuild_staged_view_from_git(viewer);
|
|
return viewer->file_line_count;
|
|
}
|
|
|
|
// Use git diff to get unstaged changes (staging area vs working directory)
|
|
char cmd[1024];
|
|
snprintf(cmd, sizeof(cmd), "git diff -U5 \"%s\" 2>/dev/null", filename);
|
|
|
|
FILE *diff_fp = popen(cmd, "r");
|
|
if (!diff_fp)
|
|
return 0;
|
|
|
|
char diff_line[1024];
|
|
int current_hunk = -1;
|
|
int old_line_num = 0, new_line_num = 0;
|
|
|
|
while (fgets(diff_line, sizeof(diff_line), diff_fp) != NULL &&
|
|
viewer->file_line_count < MAX_FULL_FILE_LINES) {
|
|
|
|
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;
|
|
}
|
|
|
|
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->is_staged = 0;
|
|
|
|
// Process hunk headers and content
|
|
if (diff_line[0] == '@' && diff_line[1] == '@') {
|
|
// Parse hunk header: @@ -old_start,old_count +new_start,new_count @@
|
|
current_hunk++;
|
|
sscanf(diff_line, "@@ -%d,%*d +%d,%*d @@", &old_line_num, &new_line_num);
|
|
|
|
file_line->type = '@';
|
|
file_line->is_diff_line = 0;
|
|
file_line->hunk_id = current_hunk;
|
|
file_line->line_number_old = old_line_num;
|
|
file_line->line_number_new = new_line_num;
|
|
file_line->is_context = 0;
|
|
} else if (diff_line[0] == '+') {
|
|
file_line->type = '+';
|
|
file_line->is_diff_line = 1;
|
|
file_line->hunk_id = current_hunk;
|
|
file_line->line_number_old = -1;
|
|
file_line->line_number_new = new_line_num++;
|
|
file_line->is_context = 0;
|
|
} else if (diff_line[0] == '-') {
|
|
file_line->type = '-';
|
|
file_line->is_diff_line = 1;
|
|
file_line->hunk_id = current_hunk;
|
|
file_line->line_number_old = old_line_num++;
|
|
file_line->line_number_new = -1;
|
|
file_line->is_context = 0;
|
|
} else if (diff_line[0] == ' ') {
|
|
file_line->type = ' ';
|
|
file_line->is_diff_line = 0;
|
|
file_line->hunk_id = current_hunk;
|
|
file_line->line_number_old = old_line_num++;
|
|
file_line->line_number_new = new_line_num++;
|
|
file_line->is_context = 1;
|
|
} else {
|
|
continue;
|
|
}
|
|
|
|
viewer->file_line_count++;
|
|
}
|
|
|
|
viewer->total_hunks = current_hunk + 1;
|
|
pclose(diff_fp);
|
|
|
|
// Build staged view from what's actually in git's staging area
|
|
rebuild_staged_view_from_git(viewer);
|
|
|
|
return viewer->file_line_count;
|
|
}
|
|
|
|
/**
|
|
* Stage/unstage a single line by line index
|
|
*/
|
|
|
|
// this is a change
|
|
// this is another change
|
|
int stage_hunk_by_line(NCursesDiffViewer *viewer, int line_index) {
|
|
if (!viewer)
|
|
return 0;
|
|
|
|
if (viewer->active_pane == 0) {
|
|
// Unstaged pane - use line_index directly from file_lines
|
|
if (line_index < 0 || line_index >= viewer->file_line_count)
|
|
return 0;
|
|
|
|
NCursesFileLine *selected_line = &viewer->file_lines[line_index];
|
|
|
|
// Don't stage/unstage hunk headers or context lines
|
|
if (selected_line->type == '@' || selected_line->type == ' ')
|
|
return 0;
|
|
|
|
// Only stage actual diff lines (+ or -)
|
|
if (selected_line->type != '+' && selected_line->type != '-')
|
|
return 0;
|
|
|
|
// Toggle the staging state
|
|
selected_line->is_staged = !selected_line->is_staged;
|
|
|
|
} else {
|
|
// Staged pane - need to find corresponding line in file_lines
|
|
if (line_index < 0 || line_index >= viewer->staged_line_count)
|
|
return 0;
|
|
|
|
NCursesFileLine *staged_line = &viewer->staged_lines[line_index];
|
|
|
|
// Skip headers and context lines
|
|
if (staged_line->type == '@' || staged_line->type == ' ')
|
|
return 0;
|
|
|
|
// Only unstage actual diff lines (+ or -)
|
|
if (staged_line->type != '+' && staged_line->type != '-')
|
|
return 0;
|
|
|
|
// Find the corresponding line in file_lines and unstage it
|
|
for (int i = 0; i < viewer->file_line_count; i++) {
|
|
NCursesFileLine *orig_line = &viewer->file_lines[i];
|
|
|
|
// Match by content and type
|
|
if (orig_line->type == staged_line->type &&
|
|
strcmp(orig_line->line, staged_line->line) == 0 &&
|
|
orig_line->is_staged) {
|
|
|
|
orig_line->is_staged = 0; // Unstage it
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Rebuild staged view to reflect changes
|
|
rebuild_staged_view(viewer);
|
|
return 1;
|
|
}
|
|
|
|
/**
|
|
* Rebuild staged view with proper git patch format for individual lines
|
|
*/
|
|
void rebuild_staged_view(NCursesDiffViewer *viewer) {
|
|
if (!viewer)
|
|
return;
|
|
|
|
viewer->staged_line_count = 0;
|
|
|
|
// Check if we have any staged changes
|
|
int has_staged_changes = 0;
|
|
for (int i = 0; i < viewer->file_line_count; i++) {
|
|
if (viewer->file_lines[i].is_staged) {
|
|
has_staged_changes = 1;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!has_staged_changes) {
|
|
return;
|
|
}
|
|
|
|
// Add git diff header
|
|
NCursesFileLine *header = &viewer->staged_lines[viewer->staged_line_count++];
|
|
snprintf(header->line, sizeof(header->line), "diff --git a/%s b/%s",
|
|
viewer->current_file_path, viewer->current_file_path);
|
|
header->type = '@';
|
|
header->is_diff_line = 0;
|
|
header->is_staged = 1;
|
|
header->is_context = 0;
|
|
|
|
// Add index line
|
|
header = &viewer->staged_lines[viewer->staged_line_count++];
|
|
snprintf(header->line, sizeof(header->line), "index 13bdd0a..9abd450 100644");
|
|
header->type = '@';
|
|
header->is_diff_line = 0;
|
|
header->is_staged = 1;
|
|
header->is_context = 0;
|
|
|
|
// Add file headers
|
|
header = &viewer->staged_lines[viewer->staged_line_count++];
|
|
snprintf(header->line, sizeof(header->line), "--- a/%s",
|
|
viewer->current_file_path);
|
|
header->type = '@';
|
|
header->is_diff_line = 0;
|
|
header->is_staged = 1;
|
|
header->is_context = 0;
|
|
|
|
header = &viewer->staged_lines[viewer->staged_line_count++];
|
|
snprintf(header->line, sizeof(header->line), "+++ b/%s",
|
|
viewer->current_file_path);
|
|
header->type = '@';
|
|
header->is_diff_line = 0;
|
|
header->is_staged = 1;
|
|
header->is_context = 0;
|
|
|
|
// Process each hunk that has staged changes
|
|
for (int hunk = 0; hunk < viewer->total_hunks; hunk++) {
|
|
// Check if this hunk has staged changes
|
|
int hunk_has_staged = 0;
|
|
for (int i = 0; i < viewer->file_line_count; i++) {
|
|
if (viewer->file_lines[i].hunk_id == hunk &&
|
|
viewer->file_lines[i].is_staged) {
|
|
hunk_has_staged = 1;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!hunk_has_staged)
|
|
continue;
|
|
|
|
// Find the hunk boundaries
|
|
int hunk_start = -1, hunk_end = -1;
|
|
for (int i = 0; i < viewer->file_line_count; i++) {
|
|
if (viewer->file_lines[i].hunk_id == hunk) {
|
|
if (hunk_start == -1)
|
|
hunk_start = i;
|
|
hunk_end = i;
|
|
}
|
|
}
|
|
|
|
if (hunk_start == -1)
|
|
continue;
|
|
|
|
// Calculate new hunk header with adjusted line counts
|
|
int old_start = -1, new_start = -1;
|
|
int old_count = 0, new_count = 0;
|
|
|
|
// Find first line numbers
|
|
for (int i = hunk_start; i <= hunk_end; i++) {
|
|
NCursesFileLine *line = &viewer->file_lines[i];
|
|
if (line->type == '@') {
|
|
old_start = line->line_number_old;
|
|
new_start = line->line_number_new;
|
|
} else if (line->is_staged || line->is_context) {
|
|
if (line->type != '+')
|
|
old_count++;
|
|
if (line->type != '-')
|
|
new_count++;
|
|
}
|
|
}
|
|
|
|
// Add hunk header
|
|
NCursesFileLine *staged_header =
|
|
&viewer->staged_lines[viewer->staged_line_count++];
|
|
snprintf(staged_header->line, sizeof(staged_header->line),
|
|
"@@ -%d,%d +%d,%d @@", old_start, old_count, new_start, new_count);
|
|
staged_header->type = '@';
|
|
staged_header->is_diff_line = 0;
|
|
staged_header->is_staged = 1;
|
|
staged_header->is_context = 0;
|
|
|
|
// Add context lines before staged changes
|
|
for (int i = hunk_start; i <= hunk_end; i++) {
|
|
NCursesFileLine *line = &viewer->file_lines[i];
|
|
if (line->type == '@')
|
|
continue;
|
|
|
|
// Always include context lines and staged diff lines
|
|
if (line->is_context || line->is_staged) {
|
|
NCursesFileLine *staged_line =
|
|
&viewer->staged_lines[viewer->staged_line_count++];
|
|
*staged_line = *line;
|
|
staged_line->is_staged = 1;
|
|
|
|
if (viewer->staged_line_count >= MAX_FULL_FILE_LINES)
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (viewer->staged_line_count >= MAX_FULL_FILE_LINES)
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Rebuild staged view from what's actually staged in git
|
|
*/
|
|
void rebuild_staged_view_from_git(NCursesDiffViewer *viewer) {
|
|
if (!viewer)
|
|
return;
|
|
|
|
viewer->staged_line_count = 0;
|
|
|
|
// Get staged changes from git (HEAD vs staging area)
|
|
char cmd[1024];
|
|
snprintf(cmd, sizeof(cmd), "git diff --cached -U5 \"%s\" 2>/dev/null",
|
|
viewer->current_file_path);
|
|
|
|
FILE *diff_fp = popen(cmd, "r");
|
|
if (!diff_fp)
|
|
return;
|
|
|
|
char diff_line[1024];
|
|
int has_any_staged = 0;
|
|
|
|
// First pass: check if there are any staged changes
|
|
while (fgets(diff_line, sizeof(diff_line), diff_fp) != NULL) {
|
|
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;
|
|
}
|
|
|
|
if (strlen(diff_line) > 0) {
|
|
has_any_staged = 1;
|
|
break;
|
|
}
|
|
}
|
|
pclose(diff_fp);
|
|
|
|
if (!has_any_staged) {
|
|
return;
|
|
}
|
|
|
|
// Second pass: actually build the staged view
|
|
diff_fp = popen(cmd, "r");
|
|
if (!diff_fp)
|
|
return;
|
|
|
|
while (fgets(diff_line, sizeof(diff_line), diff_fp) != NULL &&
|
|
viewer->staged_line_count < MAX_FULL_FILE_LINES) {
|
|
|
|
char *newline_pos = strchr(diff_line, '\n');
|
|
if (newline_pos)
|
|
*newline_pos = '\0';
|
|
|
|
// Skip file headers for the first few lines, but include them for patch
|
|
// format
|
|
if (strncmp(diff_line, "diff --git", 10) == 0 ||
|
|
strncmp(diff_line, "index ", 6) == 0 ||
|
|
strncmp(diff_line, "--- ", 4) == 0 ||
|
|
strncmp(diff_line, "+++ ", 4) == 0) {
|
|
|
|
NCursesFileLine *staged_line =
|
|
&viewer->staged_lines[viewer->staged_line_count];
|
|
strncpy(staged_line->line, diff_line, sizeof(staged_line->line) - 1);
|
|
staged_line->line[sizeof(staged_line->line) - 1] = '\0';
|
|
staged_line->type = '@';
|
|
staged_line->is_diff_line = 0;
|
|
staged_line->is_staged = 1;
|
|
staged_line->is_context = 0;
|
|
viewer->staged_line_count++;
|
|
continue;
|
|
}
|
|
|
|
NCursesFileLine *staged_line =
|
|
&viewer->staged_lines[viewer->staged_line_count];
|
|
strncpy(staged_line->line, diff_line, sizeof(staged_line->line) - 1);
|
|
staged_line->line[sizeof(staged_line->line) - 1] = '\0';
|
|
staged_line->is_staged = 1;
|
|
|
|
// Set line type for proper coloring
|
|
if (diff_line[0] == '@' && diff_line[1] == '@') {
|
|
staged_line->type = '@';
|
|
staged_line->is_diff_line = 0;
|
|
staged_line->is_context = 0;
|
|
} else if (diff_line[0] == '+') {
|
|
staged_line->type = '+';
|
|
staged_line->is_diff_line = 1;
|
|
staged_line->is_context = 0;
|
|
} else if (diff_line[0] == '-') {
|
|
staged_line->type = '-';
|
|
staged_line->is_diff_line = 1;
|
|
staged_line->is_context = 0;
|
|
} else if (diff_line[0] == ' ') {
|
|
staged_line->type = ' ';
|
|
staged_line->is_diff_line = 0;
|
|
staged_line->is_context = 1;
|
|
} else {
|
|
staged_line->type = ' ';
|
|
staged_line->is_diff_line = 0;
|
|
staged_line->is_context = 0;
|
|
}
|
|
|
|
viewer->staged_line_count++;
|
|
}
|
|
|
|
pclose(diff_fp);
|
|
}
|
|
|
|
/**
|
|
* Apply staged changes using git
|
|
*/
|
|
|
|
int apply_staged_changes(NCursesDiffViewer *viewer) {
|
|
if (!viewer || viewer->staged_line_count == 0)
|
|
return 0;
|
|
|
|
char *patch_content = malloc(50000);
|
|
if (!patch_content)
|
|
return 0;
|
|
|
|
strcpy(patch_content, "");
|
|
for (int i = 0; i < viewer->staged_line_count; i++) {
|
|
strcat(patch_content, viewer->staged_lines[i].line);
|
|
strcat(patch_content, "\n");
|
|
}
|
|
|
|
char patch_filename[256];
|
|
snprintf(patch_filename, sizeof(patch_filename), "/tmp/lazygit-%d-%ld.patch",
|
|
getpid(), time(NULL));
|
|
|
|
FILE *patch_file = fopen(patch_filename, "w");
|
|
if (!patch_file) {
|
|
free(patch_content);
|
|
return 0;
|
|
}
|
|
|
|
fprintf(patch_file, "%s", patch_content);
|
|
fclose(patch_file);
|
|
free(patch_content);
|
|
|
|
char cmd[512];
|
|
snprintf(cmd, sizeof(cmd), "git apply --cached \"%s\" >/dev/null 2>&1",
|
|
patch_filename);
|
|
int result = system(cmd);
|
|
|
|
unlink(patch_filename);
|
|
|
|
if (result == 0) {
|
|
// Don't clear staging state - reload from git instead
|
|
get_ncurses_changed_files(viewer);
|
|
if (viewer->file_count > 0 && viewer->selected_file < viewer->file_count) {
|
|
load_file_with_staging_info(
|
|
viewer, viewer->files[viewer->selected_file].filename);
|
|
}
|
|
}
|
|
return (result == 0);
|
|
}
|
|
|
|
/**
|
|
* Unstage a specific line from the staged pane
|
|
*/
|
|
|
|
int unstage_line_from_git(NCursesDiffViewer *viewer, int staged_line_index) {
|
|
if (!viewer || staged_line_index < 0 ||
|
|
staged_line_index >= viewer->staged_line_count)
|
|
return 0;
|
|
|
|
NCursesFileLine *line = &viewer->staged_lines[staged_line_index];
|
|
|
|
// Skip headers and context lines
|
|
if (line->type == '@' || line->type == ' ')
|
|
return 0;
|
|
|
|
// Only unstage actual diff lines (+ or -)
|
|
if (line->type != '+' && line->type != '-')
|
|
return 0;
|
|
|
|
// For now, let's just reset the entire file and let user re-stage what they
|
|
// want This is simpler and more reliable than trying to create reverse
|
|
// patches
|
|
char reset_cmd[512];
|
|
snprintf(reset_cmd, sizeof(reset_cmd),
|
|
"git reset HEAD \"%s\" >/dev/null 2>&1", viewer->current_file_path);
|
|
int result = system(reset_cmd);
|
|
|
|
if (result == 0) {
|
|
// Reload after unstaging
|
|
get_ncurses_changed_files(viewer);
|
|
if (viewer->file_count > 0 && viewer->selected_file < viewer->file_count) {
|
|
load_file_with_staging_info(
|
|
viewer, viewer->files[viewer->selected_file].filename);
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Reset staged changes
|
|
*/
|
|
|
|
int reset_staged_changes(NCursesDiffViewer *viewer) {
|
|
if (!viewer)
|
|
return 0;
|
|
|
|
for (int i = 0; i < viewer->file_line_count; i++) {
|
|
viewer->file_lines[i].is_staged = 0;
|
|
}
|
|
|
|
viewer->staged_line_count = 0;
|
|
rebuild_staged_view(viewer);
|
|
|
|
return 1;
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
|
|
// 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 (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
|
|
|
|
for (int y = title_start_y; y < title_start_y + title_height; y++) {
|
|
move(y, start_x);
|
|
for (int x = 0; x < input_width; x++) {
|
|
addch(' ');
|
|
}
|
|
}
|
|
|
|
// Clear message window area
|
|
for (int y = message_start_y; y < message_start_y + message_height; y++) {
|
|
move(y, start_x);
|
|
for (int x = 0; x < input_width; x++) {
|
|
addch(' ');
|
|
}
|
|
}
|
|
|
|
// Clean up windows
|
|
delwin(title_win);
|
|
delwin(message_win);
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
// Write commit message to temp file
|
|
char temp_msg_file[256];
|
|
snprintf(temp_msg_file, sizeof(temp_msg_file), "/tmp/commit_msg_%d",
|
|
getpid());
|
|
|
|
FILE *msg_file = fopen(temp_msg_file, "w");
|
|
if (!msg_file)
|
|
return 0;
|
|
|
|
if (commit_message && strlen(commit_message) > 0) {
|
|
fprintf(msg_file, "%s\n\n%s", commit_title, commit_message);
|
|
} else {
|
|
fprintf(msg_file, "%s", commit_title);
|
|
}
|
|
fclose(msg_file);
|
|
|
|
char commit_cmd[512];
|
|
snprintf(commit_cmd, sizeof(commit_cmd),
|
|
"git commit -F \"%s\" 2>/dev/null >/dev/null", temp_msg_file);
|
|
int result = system(commit_cmd);
|
|
unlink(temp_msg_file);
|
|
|
|
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);
|
|
get_ncurses_git_branches(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_file_with_staging_info(
|
|
viewer, viewer->files[viewer->selected_file].filename);
|
|
}
|
|
|
|
werase(viewer->branch_list_win);
|
|
render_branch_list_window(viewer);
|
|
wrefresh(viewer->branch_list_win);
|
|
|
|
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_file_with_staging_info(
|
|
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_file_with_staging_info(
|
|
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);
|
|
}
|
|
}
|
|
|
|
// Write amended commit message to temp file
|
|
char temp_msg_file[256];
|
|
snprintf(temp_msg_file, sizeof(temp_msg_file), "/tmp/amend_msg_%d",
|
|
getpid());
|
|
|
|
FILE *msg_file = fopen(temp_msg_file, "w");
|
|
if (!msg_file)
|
|
return 0;
|
|
|
|
if (strlen(new_message) > 0) {
|
|
fprintf(msg_file, "%s\n\n%s", new_title, new_message);
|
|
} else {
|
|
fprintf(msg_file, "%s", new_title);
|
|
}
|
|
fclose(msg_file);
|
|
|
|
char amend_cmd[512];
|
|
snprintf(amend_cmd, sizeof(amend_cmd),
|
|
"git commit --amend -F \"%s\" 2>/dev/null >/dev/null",
|
|
temp_msg_file);
|
|
int result = system(amend_cmd);
|
|
unlink(temp_msg_file);
|
|
|
|
// this is a change i want
|
|
// this is a change i dont want
|
|
|
|
if (result == 0) {
|
|
// Small delay to ensure git has processed the amend
|
|
usleep(100000); // 100ms delay
|
|
|
|
// Refresh everything after ammending
|
|
get_ncurses_changed_files(viewer);
|
|
get_commit_history(viewer);
|
|
get_ncurses_git_branches(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_file_with_staging_info(
|
|
viewer, viewer->files[viewer->selected_file].filename);
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
int get_github_credentials(char *username, int username_len, char *token,
|
|
int token_len) {
|
|
if (!username || !token)
|
|
return 0;
|
|
|
|
int dialog_width = 60;
|
|
int dialog_height = 10;
|
|
int start_x = COLS / 2 - dialog_width / 2;
|
|
int start_y = LINES / 2 - dialog_height / 2;
|
|
|
|
WINDOW *cred_win = newwin(dialog_height, dialog_width, start_y, start_x);
|
|
if (!cred_win)
|
|
return 0;
|
|
|
|
char temp_username[256] = "";
|
|
char temp_token[256] = "";
|
|
int current_field = 0;
|
|
int username_pos = 0;
|
|
int token_pos = 0;
|
|
|
|
curs_set(1);
|
|
|
|
while (1) {
|
|
werase(cred_win);
|
|
box(cred_win, 0, 0);
|
|
|
|
mvwprintw(cred_win, 1, 2, "Github Authentication Required");
|
|
|
|
if (current_field == 0) {
|
|
wattron(cred_win, COLOR_PAIR(4));
|
|
mvwprintw(cred_win, 3, 2, "Username: %s", temp_username);
|
|
wattroff(cred_win, COLOR_PAIR(4));
|
|
} else {
|
|
mvwprintw(cred_win, 3, 2, "Username: %s", temp_username);
|
|
}
|
|
|
|
if (current_field == 1) {
|
|
wattron(cred_win, COLOR_PAIR(4));
|
|
mvwprintw(cred_win, 4, 2, "Token: ");
|
|
|
|
for (int i = 0; i < token_pos; i++) {
|
|
waddch(cred_win, '*');
|
|
}
|
|
wattroff(cred_win, COLOR_PAIR(4));
|
|
} else {
|
|
mvwprintw(cred_win, 4, 2, "Token: ");
|
|
for (int i = 0; i < token_pos; i++) {
|
|
waddch(cred_win, '*');
|
|
}
|
|
}
|
|
|
|
mvwprintw(cred_win, 6, 2, "Get token at: github.com/settings/tokens");
|
|
mvwprintw(cred_win, 7, 2,
|
|
"Tab: switch fields | Enter: confirm | Esc: cancel");
|
|
|
|
if (current_field == 0) {
|
|
wmove(cred_win, 3, 12 + username_pos);
|
|
} else {
|
|
wmove(cred_win, 4, 12 + token_pos);
|
|
}
|
|
|
|
wrefresh(cred_win);
|
|
|
|
int ch = getch();
|
|
|
|
switch (ch) {
|
|
case 27:
|
|
delwin(cred_win);
|
|
curs_set(0);
|
|
return 0;
|
|
|
|
case '\t':
|
|
current_field = (current_field + 1) % 2;
|
|
break;
|
|
|
|
case '\n':
|
|
case '\r':
|
|
if (strlen(temp_username) > 0 && strlen(temp_token) > 0) {
|
|
strncpy(username, temp_username, username_len - 1);
|
|
username[username_len - 1] = '\0';
|
|
strncpy(token, temp_token, token_len - 1);
|
|
token[token_len - 1] = '\0';
|
|
delwin(cred_win);
|
|
curs_set(0);
|
|
return 1;
|
|
}
|
|
break;
|
|
|
|
case KEY_BACKSPACE:
|
|
case 127:
|
|
case 8:
|
|
if (current_field == 0 && username_pos > 0) {
|
|
username_pos--;
|
|
temp_username[username_pos] = '\0';
|
|
} else if (current_field == 1 && token_pos > 0) {
|
|
token_pos--;
|
|
temp_token[token_pos] = '\0';
|
|
}
|
|
break;
|
|
|
|
default:
|
|
if (ch >= 32 && ch <= 126) {
|
|
if (current_field == 0 && username_pos < 256) {
|
|
temp_username[username_pos] = ch;
|
|
username_pos++;
|
|
temp_username[username_pos] = '\0';
|
|
} else if (current_field == 1 && token_pos < 256) {
|
|
temp_token[token_pos] = ch;
|
|
token_pos++;
|
|
temp_token[token_pos] = '\0';
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
int execute_git_with_auth(const char *base_cmd, const char *username,
|
|
const char *token) {
|
|
if (!base_cmd || !username || !token)
|
|
return 1;
|
|
|
|
char remote_url[1024] = "";
|
|
FILE *fp = popen("git config --get remote.origin.url 2>/dev/null", "r");
|
|
if (!fp)
|
|
return 1;
|
|
|
|
if (fgets(remote_url, sizeof(remote_url), fp) == NULL) {
|
|
pclose(fp);
|
|
return 1;
|
|
}
|
|
pclose(fp);
|
|
|
|
char *newline = strchr(remote_url, '\n');
|
|
if (newline)
|
|
*newline = '\0';
|
|
|
|
char auth_url[2048];
|
|
if (strstr(remote_url, "https://github.com/")) {
|
|
char *repo_part = remote_url + strlen("https://github.com/");
|
|
snprintf(auth_url, sizeof(auth_url), "https://%s:%s@github.com/%s",
|
|
username, token, repo_part);
|
|
} else if (strstr(remote_url, "git@github.com:")) {
|
|
char *repo_part = strchr(remote_url, ':') + 1;
|
|
if (strstr(repo_part, ".git")) {
|
|
*(strstr(repo_part, ".git")) = '\0';
|
|
}
|
|
snprintf(auth_url, sizeof(auth_url), "https://%s%s@github.com%s", username,
|
|
token, repo_part);
|
|
} else {
|
|
return 1;
|
|
}
|
|
|
|
char auth_cmd[4096];
|
|
snprintf(auth_cmd, sizeof(auth_cmd), "%s %s >/dev/null >&1", base_cmd,
|
|
auth_url);
|
|
|
|
return system(auth_cmd);
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
|
|
for (int i = 0; i < viewer->branch_count; i++) {
|
|
if (viewer->branches[i].status == 1) { // Current branch
|
|
viewer->pushing_branch_index = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Set branch-specific push status
|
|
viewer->branch_push_status = SYNC_STATUS_PUSHING_VISIBLE;
|
|
viewer->branch_animation_frame = 0;
|
|
viewer->branch_text_char_count = 7; // Show full "Pushing" immediately
|
|
|
|
// Force immediate branch window refresh to show "Pushing" before the blocking
|
|
// git operation
|
|
|
|
werase(viewer->branch_list_win);
|
|
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);
|
|
|
|
// Create a simple animated push with spinner updates
|
|
pid_t push_pid;
|
|
int result = 0;
|
|
|
|
// Try push first without authentication
|
|
char push_cmd[256];
|
|
if (is_diverged) {
|
|
strcpy(push_cmd, "git push --force-with-lease origin");
|
|
} else {
|
|
strcpy(push_cmd, "git push origin");
|
|
}
|
|
|
|
push_pid = fork();
|
|
if (push_pid == 0) {
|
|
// Child process: try normal push first
|
|
exit(system(is_diverged
|
|
? "git push --force-with-lease origin >/dev/null 2>&1"
|
|
: "git push origin >/dev/null 2>&1"));
|
|
}
|
|
|
|
// Parent process: wait and check if authentication is needed
|
|
if (push_pid > 0) {
|
|
int status;
|
|
int spinner_counter = 0;
|
|
|
|
while (waitpid(push_pid, &status, WNOHANG) == 0) {
|
|
// Update spinner animation
|
|
viewer->branch_animation_frame = spinner_counter;
|
|
spinner_counter = (spinner_counter + 1) % 40;
|
|
|
|
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);
|
|
|
|
usleep(100000); // 100ms delay
|
|
}
|
|
|
|
// Check if push failed (likely due to authentication)
|
|
if (WIFEXITED(status)) {
|
|
result = WEXITSTATUS(status);
|
|
} else {
|
|
result = 1;
|
|
}
|
|
|
|
// If push failed, try with authentication
|
|
if (result != 0) {
|
|
char username[256] = "";
|
|
char token[256] = "";
|
|
|
|
// Clear screen and get credentials
|
|
clear();
|
|
refresh();
|
|
|
|
if (get_github_credentials(username, sizeof(username), token,
|
|
sizeof(token))) {
|
|
// Try authenticated push
|
|
result = execute_git_with_auth(push_cmd, username, token);
|
|
|
|
// Clear credentials from memory for security
|
|
memset(username, 0, sizeof(username));
|
|
memset(token, 0, sizeof(token));
|
|
} else {
|
|
result = 1; // User cancelled
|
|
}
|
|
|
|
// Force complete screen refresh after credential dialog
|
|
clear();
|
|
refresh();
|
|
}
|
|
} else {
|
|
result = 1; // Fork failed
|
|
}
|
|
|
|
// Parent process: animate the spinner while push is happening
|
|
if (push_pid > 0) {
|
|
int status;
|
|
int spinner_counter = 0;
|
|
|
|
while (waitpid(push_pid, &status, WNOHANG) == 0) {
|
|
// Update spinner animation
|
|
viewer->branch_animation_frame = spinner_counter;
|
|
spinner_counter = (spinner_counter + 1) % 40;
|
|
|
|
// Refresh ALL windows, not just branch window
|
|
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);
|
|
|
|
// Small delay for animation timing
|
|
usleep(100000); // 100ms delay
|
|
}
|
|
|
|
// Get the exit status of the push command
|
|
if (WIFEXITED(status)) {
|
|
result = WEXITSTATUS(status);
|
|
} else {
|
|
result = 1; // Error
|
|
}
|
|
} else {
|
|
result = 1; // Fork failed
|
|
}
|
|
|
|
if (result == 0) {
|
|
// Immediately transition to "Pushed!" animation
|
|
viewer->sync_status = SYNC_STATUS_PUSHED_APPEARING;
|
|
viewer->animation_frame = 0;
|
|
viewer->text_char_count = 0;
|
|
|
|
// Set branch-specific pushed status
|
|
viewer->branch_push_status = SYNC_STATUS_PUSHED_APPEARING;
|
|
viewer->branch_animation_frame = 0;
|
|
viewer->branch_text_char_count = 0;
|
|
|
|
// Refresh commit history to get proper push status
|
|
get_commit_history(viewer);
|
|
get_ncurses_git_branches(viewer); // Add this line to refresh branch status
|
|
|
|
// Refresh both commit and branch panes
|
|
werase(viewer->commit_list_win);
|
|
render_commit_list_window(viewer);
|
|
wrefresh(viewer->commit_list_win);
|
|
|
|
werase(viewer->branch_list_win);
|
|
render_branch_list_window(viewer);
|
|
wrefresh(viewer->branch_list_win);
|
|
|
|
return 1;
|
|
} else {
|
|
// Push failed, show error
|
|
show_error_popup("Push failed. Check your network, credentials, or get a "
|
|
"Personal Access Token from github.com/settings/tokens");
|
|
viewer->sync_status = SYNC_STATUS_IDLE;
|
|
viewer->pushing_branch_index = -1;
|
|
viewer->branch_push_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_file_with_staging_info(
|
|
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);
|
|
}
|
|
|
|
// show staged indicator and filename
|
|
|
|
if (viewer->files[i].has_staged_changes) {
|
|
if (is_selected) {
|
|
wattroff(viewer->file_list_win, COLOR_PAIR(5));
|
|
}
|
|
|
|
wattron(viewer->file_list_win, COLOR_PAIR(1)); // green for staged
|
|
mvwprintw(viewer->file_list_win, y, 4, "5");
|
|
wattroff(viewer->file_list_win, COLOR_PAIR(1));
|
|
if (is_selected) {
|
|
wattron(viewer->file_list_win, COLOR_PAIR(5));
|
|
}
|
|
|
|
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, 5, " %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, 5, " %s", truncated_name);
|
|
}
|
|
} else {
|
|
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);
|
|
|
|
// Check if this commit is currently being viewed
|
|
int is_being_viewed = (i == viewer->selected_commit &&
|
|
viewer->current_mode == NCURSES_MODE_COMMIT_VIEW);
|
|
|
|
// Apply line highlight if selected
|
|
if (is_selected_commit) {
|
|
wattron(viewer->commit_list_win, COLOR_PAIR(5));
|
|
}
|
|
|
|
// Show view indicator (currently being viewed)
|
|
if (is_being_viewed) {
|
|
wattron(viewer->commit_list_win, COLOR_PAIR(1)); // Green for active view
|
|
mvwprintw(viewer->commit_list_win, y, 0, "*");
|
|
wattroff(viewer->commit_list_win, COLOR_PAIR(1));
|
|
} else {
|
|
mvwprintw(viewer->commit_list_win, y, 0, " ");
|
|
}
|
|
|
|
// 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));
|
|
}
|
|
|
|
// Color commit hash based on push status: yellow for pushed, red for
|
|
// unpushed
|
|
if (viewer->commits[i].is_pushed) {
|
|
wattron(viewer->commit_list_win,
|
|
COLOR_PAIR(4)); // Yellow for pushed commits
|
|
} else {
|
|
wattron(viewer->commit_list_win,
|
|
COLOR_PAIR(2)); // Red for unpushed commits
|
|
}
|
|
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(4));
|
|
} else {
|
|
wattroff(viewer->commit_list_win, COLOR_PAIR(2));
|
|
}
|
|
|
|
// Show author initials - use cyan for a subtle contrast
|
|
wattron(viewer->commit_list_win, COLOR_PAIR(3)); // Cyan for author initials
|
|
mvwprintw(viewer->commit_list_win, y, 10, "%s",
|
|
viewer->commits[i].author_initials);
|
|
wattroff(viewer->commit_list_win, COLOR_PAIR(3));
|
|
|
|
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 with split staging view
|
|
*/
|
|
void render_file_content_window(NCursesDiffViewer *viewer) {
|
|
if (!viewer || !viewer->file_content_win)
|
|
return;
|
|
|
|
int height, width;
|
|
getmaxyx(viewer->file_content_win, height, width);
|
|
|
|
// Clear the window
|
|
werase(viewer->file_content_win);
|
|
draw_rounded_box(viewer->file_content_win);
|
|
|
|
if (!viewer->split_view_mode) {
|
|
// Show preview content in list modes and view modes
|
|
if (viewer->current_mode == NCURSES_MODE_FILE_LIST ||
|
|
viewer->current_mode == NCURSES_MODE_COMMIT_LIST ||
|
|
viewer->current_mode == NCURSES_MODE_COMMIT_VIEW ||
|
|
viewer->current_mode == NCURSES_MODE_BRANCH_LIST ||
|
|
viewer->current_mode == NCURSES_MODE_BRANCH_VIEW ||
|
|
viewer->current_mode == NCURSES_MODE_STASH_LIST ||
|
|
viewer->current_mode == NCURSES_MODE_STASH_VIEW) {
|
|
|
|
// Add title based on current mode
|
|
const char* title = "";
|
|
switch (viewer->current_mode) {
|
|
case NCURSES_MODE_FILE_LIST:
|
|
title = " File Diff Preview ";
|
|
break;
|
|
case NCURSES_MODE_COMMIT_LIST:
|
|
title = " Commit Details ";
|
|
break;
|
|
case NCURSES_MODE_COMMIT_VIEW:
|
|
title = " Commit Diff ";
|
|
break;
|
|
case NCURSES_MODE_BRANCH_LIST:
|
|
title = " Branch Commits ";
|
|
break;
|
|
case NCURSES_MODE_BRANCH_VIEW:
|
|
title = " Branch Details ";
|
|
break;
|
|
case NCURSES_MODE_STASH_LIST:
|
|
title = " Stash Details ";
|
|
break;
|
|
case NCURSES_MODE_STASH_VIEW:
|
|
title = " Stash Diff ";
|
|
break;
|
|
default:
|
|
title = " Preview ";
|
|
break;
|
|
}
|
|
mvwprintw(viewer->file_content_win, 0, 2, "%s", title);
|
|
|
|
// Render preview content using the loaded file_lines
|
|
if (viewer->file_line_count > 0) {
|
|
int max_lines_visible = height - 2;
|
|
int display_count = 0;
|
|
|
|
for (int i = viewer->file_scroll_offset;
|
|
i < viewer->file_line_count && display_count < max_lines_visible;
|
|
i++) {
|
|
|
|
NCursesFileLine *line = &viewer->file_lines[i];
|
|
int is_cursor_line = (i == viewer->file_cursor_line);
|
|
|
|
// Calculate how many display lines this logical line will need
|
|
int line_height = calculate_wrapped_line_height(line->line, width - 4);
|
|
|
|
// Skip if this line would exceed remaining space
|
|
if (display_count + line_height > max_lines_visible) {
|
|
break;
|
|
}
|
|
|
|
int y = display_count + 1;
|
|
int color_pair = 0;
|
|
|
|
// Determine color based on line type
|
|
if (line->type == '@') {
|
|
color_pair = 3; // Cyan for hunk headers
|
|
} else if (line->type == '+') {
|
|
color_pair = 1; // Green for additions
|
|
} else if (line->type == '-') {
|
|
color_pair = 2; // Red for deletions
|
|
}
|
|
|
|
// Render the line with wrapping
|
|
int rows_used = render_wrapped_line(viewer->file_content_win, line->line,
|
|
y, 1, width, line_height, color_pair, is_cursor_line);
|
|
display_count += rows_used;
|
|
}
|
|
} else {
|
|
// No content to show
|
|
mvwprintw(viewer->file_content_win, height/2, (width - 15)/2, "No preview available");
|
|
}
|
|
}
|
|
|
|
wrefresh(viewer->file_content_win);
|
|
return;
|
|
}
|
|
|
|
// Split view mode for file staging
|
|
int split_line = height / 2;
|
|
int unstaged_height = split_line - 1;
|
|
int staged_height = height - split_line - 2;
|
|
|
|
// Draw split line
|
|
for (int x = 1; x < width - 1; x++) {
|
|
mvwaddch(viewer->file_content_win, split_line, x, ACS_HLINE);
|
|
}
|
|
|
|
// Render unstaged changes pane
|
|
if (viewer->active_pane == 0) {
|
|
wattron(viewer->file_content_win, COLOR_PAIR(4));
|
|
}
|
|
mvwprintw(viewer->file_content_win, 0, 2, " Unstaged changes ");
|
|
if (viewer->active_pane == 0) {
|
|
wattroff(viewer->file_content_win, COLOR_PAIR(4));
|
|
}
|
|
|
|
// Show unstaged lines with wrapping
|
|
int unstaged_display_count = 0;
|
|
for (int i = viewer->file_scroll_offset;
|
|
i < viewer->file_line_count &&
|
|
unstaged_display_count < unstaged_height - 1;
|
|
i++) {
|
|
|
|
NCursesFileLine *line = &viewer->file_lines[i];
|
|
int is_cursor_line = (i == viewer->file_cursor_line && viewer->active_pane == 0);
|
|
|
|
// Calculate how many display lines this logical line will need
|
|
int line_height = calculate_wrapped_line_height(line->line, width - 4);
|
|
|
|
// Skip if this line would exceed remaining space
|
|
if (unstaged_display_count + line_height > unstaged_height - 1) {
|
|
break;
|
|
}
|
|
|
|
int y = unstaged_display_count + 1;
|
|
int color_pair = 0;
|
|
|
|
// Determine color based on line type
|
|
if (line->type == '@') {
|
|
color_pair = 3; // Cyan for hunk headers
|
|
} else if (line->is_staged) {
|
|
color_pair = 3; // Dimmed for staged content
|
|
} else if (line->type == '+') {
|
|
color_pair = 1; // Green for additions
|
|
} else if (line->type == '-') {
|
|
color_pair = 2; // Red for deletions
|
|
}
|
|
|
|
// Handle staged indicator for diff lines
|
|
if (line->is_staged && (line->type == '+' || line->type == '-')) {
|
|
// First render the staged indicator
|
|
if (is_cursor_line) {
|
|
wattron(viewer->file_content_win, A_REVERSE);
|
|
}
|
|
wattron(viewer->file_content_win, COLOR_PAIR(1));
|
|
mvwaddch(viewer->file_content_win, y, 1, '*');
|
|
wattroff(viewer->file_content_win, COLOR_PAIR(1));
|
|
if (is_cursor_line) {
|
|
wattroff(viewer->file_content_win, A_REVERSE);
|
|
}
|
|
|
|
// Then render the line content starting from column 2, skipping first char
|
|
char line_without_prefix[1024];
|
|
strcpy(line_without_prefix, line->line + 1);
|
|
int rows_used = render_wrapped_line(viewer->file_content_win, line_without_prefix,
|
|
y, 2, width, line_height, color_pair, is_cursor_line);
|
|
unstaged_display_count += rows_used;
|
|
} else {
|
|
// Regular line rendering
|
|
int rows_used = render_wrapped_line(viewer->file_content_win, line->line,
|
|
y, 1, width, line_height, color_pair, is_cursor_line);
|
|
unstaged_display_count += rows_used;
|
|
}
|
|
}
|
|
|
|
// Render staged changes pane
|
|
if (viewer->active_pane == 1) {
|
|
wattron(viewer->file_content_win, COLOR_PAIR(1));
|
|
}
|
|
mvwprintw(viewer->file_content_win, split_line, 2, " Staged changes ");
|
|
if (viewer->active_pane == 1) {
|
|
wattroff(viewer->file_content_win, COLOR_PAIR(1));
|
|
}
|
|
|
|
// Show staged lines with proper git patch format and wrapping
|
|
int staged_display_count = 0;
|
|
for (int i = viewer->staged_scroll_offset;
|
|
i < viewer->staged_line_count &&
|
|
staged_display_count < staged_height - 1;
|
|
i++) {
|
|
|
|
NCursesFileLine *line = &viewer->staged_lines[i];
|
|
int is_cursor_line = (i == viewer->staged_cursor_line && viewer->active_pane == 1);
|
|
|
|
// Calculate how many display lines this logical line will need
|
|
int line_height = calculate_wrapped_line_height(line->line, width - 4);
|
|
|
|
// Skip if this line would exceed remaining space
|
|
if (staged_display_count + line_height > staged_height - 1) {
|
|
break;
|
|
}
|
|
|
|
int y = split_line + 1 + staged_display_count;
|
|
int color_pair = 0;
|
|
|
|
// Determine color based on line type
|
|
if (line->type == '+') {
|
|
color_pair = 1; // Green for additions
|
|
} else if (line->type == '-') {
|
|
color_pair = 2; // Red for deletions
|
|
} else if (line->type == '@') {
|
|
color_pair = 3; // Cyan for headers
|
|
}
|
|
|
|
// Render the line with wrapping
|
|
int rows_used = render_wrapped_line(viewer->file_content_win, line->line,
|
|
y, 1, width, line_height, color_pair, is_cursor_line);
|
|
staged_display_count += rows_used;
|
|
}
|
|
|
|
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, "View: Enter | 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");
|
|
} else if (viewer->current_mode == NCURSES_MODE_BRANCH_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 / 2) %
|
|
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 >= 30 &&
|
|
!viewer->critical_operation_in_progress && !viewer->fetch_in_progress) {
|
|
viewer->last_sync_time = current_time;
|
|
|
|
// Start background fetch instead of blocking
|
|
start_background_fetch(viewer);
|
|
return;
|
|
}
|
|
|
|
// Always check if background fetch is complete
|
|
check_background_fetch(viewer);
|
|
|
|
// Handle all animation states
|
|
if (viewer->sync_status != SYNC_STATUS_IDLE) {
|
|
viewer->animation_frame++;
|
|
|
|
// Handle fetching animation (4 seconds total: appear + visible + disappear)
|
|
if (viewer->sync_status >= SYNC_STATUS_SYNCING_APPEARING &&
|
|
viewer->sync_status <= SYNC_STATUS_SYNCING_DISAPPEARING) {
|
|
if (viewer->sync_status == SYNC_STATUS_SYNCING_APPEARING) {
|
|
// Appearing: one character every 2 frames (0.1s) for "Fetching" (8
|
|
// chars)
|
|
viewer->text_char_count = viewer->animation_frame / 2;
|
|
if (viewer->text_char_count >= 8) {
|
|
viewer->text_char_count = 8;
|
|
viewer->sync_status = SYNC_STATUS_SYNCING_VISIBLE;
|
|
viewer->animation_frame = 0;
|
|
}
|
|
} else if (viewer->sync_status == SYNC_STATUS_SYNCING_VISIBLE) {
|
|
// Visible with spinner for 2.4 seconds (48 frames)
|
|
if (viewer->animation_frame >= 48) {
|
|
viewer->sync_status = SYNC_STATUS_SYNCING_DISAPPEARING;
|
|
viewer->animation_frame = 0;
|
|
viewer->text_char_count = 8;
|
|
}
|
|
} else if (viewer->sync_status == SYNC_STATUS_SYNCING_DISAPPEARING) {
|
|
// Disappearing: remove one character every 2 frames (0.1s)
|
|
int chars_to_remove = viewer->animation_frame / 2;
|
|
viewer->text_char_count = 8 - chars_to_remove;
|
|
if (viewer->text_char_count <= 0) {
|
|
viewer->text_char_count = 0;
|
|
viewer->sync_status = SYNC_STATUS_SYNCED_APPEARING;
|
|
viewer->animation_frame = 0;
|
|
}
|
|
}
|
|
}
|
|
// Handle pushing animation (same 4 second pattern)
|
|
else if (viewer->sync_status >= SYNC_STATUS_PUSHING_APPEARING &&
|
|
viewer->sync_status <= SYNC_STATUS_PUSHING_DISAPPEARING) {
|
|
if (viewer->sync_status == SYNC_STATUS_PUSHING_APPEARING) {
|
|
// Appearing: one character every 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 branch-specific animations (only for completed states)
|
|
if (viewer->branch_push_status != SYNC_STATUS_IDLE ||
|
|
viewer->branch_pull_status != SYNC_STATUS_IDLE) {
|
|
viewer->branch_animation_frame++;
|
|
|
|
// Handle push animations (only "Pushed!" phase)
|
|
if (viewer->branch_push_status >= SYNC_STATUS_PUSHED_APPEARING &&
|
|
viewer->branch_push_status <= SYNC_STATUS_PUSHED_DISAPPEARING) {
|
|
if (viewer->branch_push_status == SYNC_STATUS_PUSHED_APPEARING) {
|
|
viewer->branch_text_char_count = viewer->branch_animation_frame;
|
|
if (viewer->branch_text_char_count >= 7) {
|
|
viewer->branch_text_char_count = 7;
|
|
viewer->branch_push_status = SYNC_STATUS_PUSHED_VISIBLE;
|
|
viewer->branch_animation_frame = 0;
|
|
}
|
|
} else if (viewer->branch_push_status == SYNC_STATUS_PUSHED_VISIBLE) {
|
|
if (viewer->branch_animation_frame >= 100) {
|
|
viewer->branch_push_status = SYNC_STATUS_PUSHED_DISAPPEARING;
|
|
viewer->branch_animation_frame = 0;
|
|
viewer->branch_text_char_count = 7;
|
|
}
|
|
} else if (viewer->branch_push_status ==
|
|
SYNC_STATUS_PUSHED_DISAPPEARING) {
|
|
int chars_to_remove = viewer->branch_animation_frame;
|
|
viewer->branch_text_char_count = 7 - chars_to_remove;
|
|
if (viewer->branch_text_char_count <= 0) {
|
|
viewer->branch_text_char_count = 0;
|
|
viewer->branch_push_status = SYNC_STATUS_IDLE;
|
|
viewer->pushing_branch_index = -1;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Handle pull animations (only "Pulled!" phase)
|
|
if (viewer->branch_pull_status >= SYNC_STATUS_PULLED_APPEARING &&
|
|
viewer->branch_pull_status <= SYNC_STATUS_PULLED_DISAPPEARING) {
|
|
if (viewer->branch_pull_status == SYNC_STATUS_PULLED_APPEARING) {
|
|
viewer->branch_text_char_count = viewer->branch_animation_frame / 2;
|
|
if (viewer->branch_text_char_count >= 7) {
|
|
viewer->branch_text_char_count = 7;
|
|
viewer->branch_pull_status = SYNC_STATUS_PULLED_VISIBLE;
|
|
viewer->branch_animation_frame = 0;
|
|
}
|
|
} else if (viewer->branch_pull_status == SYNC_STATUS_PULLED_VISIBLE) {
|
|
if (viewer->branch_animation_frame >= 40) {
|
|
viewer->branch_pull_status = SYNC_STATUS_PULLED_DISAPPEARING;
|
|
viewer->branch_animation_frame = 0;
|
|
viewer->branch_text_char_count = 7;
|
|
}
|
|
} else if (viewer->branch_pull_status ==
|
|
SYNC_STATUS_PULLED_DISAPPEARING) {
|
|
int chars_to_remove = viewer->branch_animation_frame / 2;
|
|
viewer->branch_text_char_count = 7 - chars_to_remove;
|
|
if (viewer->branch_text_char_count <= 0) {
|
|
viewer->branch_text_char_count = 0;
|
|
viewer->branch_pull_status = SYNC_STATUS_IDLE;
|
|
viewer->pulling_branch_index = -1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
int commit_staged_changes_only(NCursesDiffViewer *viewer,
|
|
const char *commit_title,
|
|
const char *commit_message) {
|
|
if (!viewer || !commit_title || strlen(commit_title) == 0)
|
|
return 0;
|
|
|
|
char temp_msg_file[256];
|
|
snprintf(temp_msg_file, sizeof(temp_msg_file), "/tmp/commit_msg_%d",
|
|
getpid());
|
|
|
|
FILE *msg_file = fopen(temp_msg_file, "w");
|
|
if (!msg_file)
|
|
return 0;
|
|
|
|
if (commit_message && strlen(commit_message) > 0) {
|
|
fprintf(msg_file, "%s\n\n%s", commit_title, commit_message);
|
|
} else {
|
|
fprintf(msg_file, "%s", commit_title);
|
|
}
|
|
fclose(msg_file);
|
|
|
|
char commit_cmd[512];
|
|
snprintf(commit_cmd, sizeof(commit_cmd),
|
|
"git commit -F \"%s\" 2>/dev/null >/dev/null", temp_msg_file);
|
|
int result = system(commit_cmd);
|
|
unlink(temp_msg_file);
|
|
|
|
if (result == 0) {
|
|
usleep(100000);
|
|
|
|
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;
|
|
}
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Handle keyboard input for navigation
|
|
*/
|
|
int handle_ncurses_diff_input(NCursesDiffViewer *viewer, int key) {
|
|
if (!viewer)
|
|
return 0;
|
|
|
|
// Handle fuzzy search input first if active
|
|
if (viewer->fuzzy_search_active) {
|
|
return handle_fuzzy_search_input(viewer, key);
|
|
}
|
|
|
|
// Handle grep search input if active
|
|
if (viewer->grep_search_active) {
|
|
return handle_grep_search_input(viewer, key);
|
|
}
|
|
|
|
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;
|
|
viewer->split_view_mode = 0; // Exit split view mode
|
|
// Load the selected file content when switching to file list mode
|
|
if (viewer->file_count > 0 && viewer->selected_file < viewer->file_count) {
|
|
load_file_with_staging_info(
|
|
viewer, viewer->files[viewer->selected_file].filename);
|
|
}
|
|
break;
|
|
case '2':
|
|
viewer->split_view_mode = 0; // Exit split view mode
|
|
// Switch to file view mode and load selected file
|
|
if (viewer->file_count > 0 && viewer->selected_file < viewer->file_count) {
|
|
load_file_with_staging_info(
|
|
viewer, viewer->files[viewer->selected_file].filename);
|
|
viewer->current_mode = NCURSES_MODE_FILE_VIEW;
|
|
}
|
|
break;
|
|
case '3':
|
|
viewer->current_mode = NCURSES_MODE_BRANCH_LIST;
|
|
viewer->split_view_mode = 0; // Exit split view mode
|
|
// Load commits for the currently selected branch when entering branch mode
|
|
if (viewer->branch_count > 0) {
|
|
load_branch_commits(viewer,
|
|
viewer->branches[viewer->selected_branch].name);
|
|
viewer->branch_commits_scroll_offset = 0;
|
|
}
|
|
break;
|
|
case '4':
|
|
viewer->current_mode = NCURSES_MODE_COMMIT_LIST;
|
|
viewer->split_view_mode = 0; // Exit split view mode
|
|
// 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;
|
|
viewer->split_view_mode = 0; // Exit split view mode
|
|
// 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--;
|
|
}
|
|
break;
|
|
|
|
case KEY_DOWN:
|
|
case 'j':
|
|
if (viewer->selected_file < viewer->file_count - 1) {
|
|
viewer->selected_file++;
|
|
}
|
|
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':
|
|
viewer->critical_operation_in_progress = 1;
|
|
create_ncurses_git_stash(viewer);
|
|
viewer->critical_operation_in_progress = 0;
|
|
break;
|
|
|
|
case 'c':
|
|
case 'C': // Commit marked files
|
|
{
|
|
char commit_title[MAX_COMMIT_TITLE_LEN] = "";
|
|
char commit_message[2048] = "";
|
|
viewer->critical_operation_in_progress = 2;
|
|
|
|
if (viewer->file_count > 0 &&
|
|
viewer->selected_file < viewer->file_count &&
|
|
viewer->files[viewer->selected_file].has_staged_changes) {
|
|
if (get_commit_title_input(commit_title, MAX_COMMIT_TITLE_LEN,
|
|
commit_message, sizeof(commit_message))) {
|
|
commit_staged_changes_only(viewer, commit_title, commit_message);
|
|
}
|
|
} else {
|
|
if (get_commit_title_input(commit_title, MAX_COMMIT_TITLE_LEN,
|
|
commit_message, sizeof(commit_message))) {
|
|
commit_marked_files(viewer, commit_title, commit_message);
|
|
}
|
|
}
|
|
viewer->critical_operation_in_progress = 0;
|
|
|
|
clear();
|
|
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);
|
|
|
|
} break;
|
|
|
|
case '\t': // Tab - switch to commit list mode
|
|
viewer->current_mode = NCURSES_MODE_COMMIT_LIST;
|
|
break;
|
|
|
|
case '/': // Forward slash - enter fuzzy search mode
|
|
enter_fuzzy_search_mode(viewer);
|
|
break;
|
|
|
|
case '\n':
|
|
case '\r':
|
|
case KEY_ENTER:
|
|
// Enter file view mode with staging and load the selected file
|
|
if (viewer->file_count > 0 &&
|
|
viewer->selected_file < viewer->file_count) {
|
|
load_file_with_staging_info(
|
|
viewer, viewer->files[viewer->selected_file].filename);
|
|
viewer->current_mode = NCURSES_MODE_FILE_VIEW;
|
|
viewer->split_view_mode = 1; // Enable split view for staging
|
|
viewer->active_pane = 0; // Start in unstaged pane
|
|
}
|
|
break;
|
|
}
|
|
} else if (viewer->current_mode == NCURSES_MODE_FILE_VIEW) {
|
|
// File view mode navigation
|
|
switch (key) {
|
|
case 27: // ESC
|
|
if (viewer->split_view_mode) {
|
|
viewer->split_view_mode = 0; // Exit split view
|
|
viewer->current_mode = NCURSES_MODE_FILE_LIST; // Return to file list
|
|
} else {
|
|
viewer->current_mode = NCURSES_MODE_FILE_LIST;
|
|
}
|
|
break;
|
|
|
|
case ' ': // Space - stage/unstage line
|
|
if (viewer->split_view_mode) {
|
|
if (viewer->active_pane == 0) {
|
|
stage_hunk_by_line(viewer, viewer->file_cursor_line);
|
|
} else {
|
|
// Unstage from staged pane
|
|
unstage_line_from_git(viewer, viewer->staged_cursor_line);
|
|
}
|
|
} else {
|
|
// Existing scroll logic for non-split view
|
|
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 '\t': // Tab - switch between unstaged and staged panes
|
|
if (viewer->split_view_mode) {
|
|
viewer->active_pane = !viewer->active_pane;
|
|
// Reset cursors when switching panes
|
|
if (viewer->active_pane == 0) {
|
|
viewer->file_cursor_line = 0;
|
|
} else {
|
|
viewer->staged_cursor_line = 0;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case 'a': // apply staged changes
|
|
if (viewer->split_view_mode) {
|
|
apply_staged_changes(viewer);
|
|
clear();
|
|
refresh();
|
|
}
|
|
break;
|
|
|
|
case 'r': // Reset staged changes
|
|
if (viewer->split_view_mode) {
|
|
reset_staged_changes(viewer);
|
|
}
|
|
break;
|
|
|
|
case KEY_UP:
|
|
case 'k':
|
|
if (viewer->split_view_mode) {
|
|
if (viewer->active_pane == 0) {
|
|
// Unstaged pane
|
|
move_cursor_smart_unstaged(viewer, -1);
|
|
} else {
|
|
// Staged pane
|
|
move_cursor_smart_staged(viewer, -1);
|
|
}
|
|
} else {
|
|
move_cursor_smart(viewer, -1);
|
|
}
|
|
break;
|
|
|
|
case KEY_DOWN:
|
|
case 'j':
|
|
if (viewer->split_view_mode) {
|
|
if (viewer->active_pane == 0) {
|
|
// Unstaged pane
|
|
move_cursor_smart_unstaged(viewer, 1);
|
|
} else {
|
|
// Staged pane
|
|
move_cursor_smart_staged(viewer, 1);
|
|
}
|
|
} else {
|
|
move_cursor_smart(viewer, 1);
|
|
}
|
|
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 with smart positioning
|
|
int target_cursor = viewer->file_cursor_line - max_lines_visible / 2;
|
|
if (target_cursor < 0) {
|
|
target_cursor = 0;
|
|
}
|
|
|
|
// Find the actual cursor position (skip empty lines like
|
|
// move_cursor_smart does)
|
|
int final_cursor = target_cursor;
|
|
int attempts = 0;
|
|
const int max_attempts = viewer->file_line_count;
|
|
|
|
// Look for non-empty line near target position
|
|
while (attempts < max_attempts &&
|
|
final_cursor < viewer->file_line_count) {
|
|
NCursesFileLine *line = &viewer->file_lines[final_cursor];
|
|
char *trimmed = line->line;
|
|
|
|
// Skip leading whitespace
|
|
while (*trimmed == ' ' || *trimmed == '\t') {
|
|
trimmed++;
|
|
}
|
|
|
|
// If line has content, stop here
|
|
if (*trimmed != '\0') {
|
|
break;
|
|
}
|
|
|
|
// Move down to find content, but don't go too far from target
|
|
final_cursor++;
|
|
attempts++;
|
|
|
|
// If we've moved too far down, go back to target and try going up
|
|
if (final_cursor > target_cursor + 5) {
|
|
final_cursor = target_cursor;
|
|
// Try going up from target
|
|
while (final_cursor > 0 && attempts < max_attempts) {
|
|
line = &viewer->file_lines[final_cursor];
|
|
trimmed = line->line;
|
|
while (*trimmed == ' ' || *trimmed == '\t') {
|
|
trimmed++;
|
|
}
|
|
if (*trimmed != '\0') {
|
|
break;
|
|
}
|
|
final_cursor--;
|
|
attempts++;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Ensure final cursor is in bounds
|
|
if (final_cursor < 0)
|
|
final_cursor = 0;
|
|
if (final_cursor >= viewer->file_line_count)
|
|
final_cursor = viewer->file_line_count - 1;
|
|
|
|
viewer->file_cursor_line = final_cursor;
|
|
|
|
// Adjust scroll based on FINAL cursor position
|
|
if (viewer->file_cursor_line < viewer->file_scroll_offset + 5) {
|
|
viewer->file_scroll_offset = viewer->file_cursor_line - 5;
|
|
if (viewer->file_scroll_offset < 0) {
|
|
viewer->file_scroll_offset = 0;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
case 4: // Ctrl+D
|
|
{
|
|
// Move cursor down half page with smart positioning
|
|
int target_cursor = viewer->file_cursor_line + max_lines_visible / 2;
|
|
if (target_cursor >= viewer->file_line_count) {
|
|
target_cursor = viewer->file_line_count - 1;
|
|
}
|
|
|
|
// Find the actual cursor position (skip empty lines like
|
|
// move_cursor_smart does)
|
|
int final_cursor = target_cursor;
|
|
int attempts = 0;
|
|
const int max_attempts = viewer->file_line_count;
|
|
|
|
// Look for non-empty line near target position
|
|
while (attempts < max_attempts && final_cursor >= 0) {
|
|
NCursesFileLine *line = &viewer->file_lines[final_cursor];
|
|
char *trimmed = line->line;
|
|
|
|
// Skip leading whitespace
|
|
while (*trimmed == ' ' || *trimmed == '\t') {
|
|
trimmed++;
|
|
}
|
|
|
|
// If line has content, stop here
|
|
if (*trimmed != '\0') {
|
|
break;
|
|
}
|
|
|
|
// Move up to find content, but don't go too far from target
|
|
final_cursor--;
|
|
attempts++;
|
|
|
|
// If we've moved too far up, go back to target and try going down
|
|
if (final_cursor < target_cursor - 5) {
|
|
final_cursor = target_cursor;
|
|
// Try going down from target
|
|
while (final_cursor < viewer->file_line_count - 1 &&
|
|
attempts < max_attempts) {
|
|
line = &viewer->file_lines[final_cursor];
|
|
trimmed = line->line;
|
|
while (*trimmed == ' ' || *trimmed == '\t') {
|
|
trimmed++;
|
|
}
|
|
if (*trimmed != '\0') {
|
|
break;
|
|
}
|
|
final_cursor++;
|
|
attempts++;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Ensure final cursor is in bounds
|
|
if (final_cursor < 0)
|
|
final_cursor = 0;
|
|
if (final_cursor >= viewer->file_line_count)
|
|
final_cursor = viewer->file_line_count - 1;
|
|
|
|
viewer->file_cursor_line = final_cursor;
|
|
|
|
// Adjust scroll based on FINAL cursor position
|
|
if (viewer->file_cursor_line >=
|
|
viewer->file_scroll_offset + max_lines_visible - 5) {
|
|
viewer->file_scroll_offset =
|
|
viewer->file_cursor_line - max_lines_visible + 5;
|
|
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
|
|
// 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;
|
|
viewer->split_view_mode = 0; // Exit split view mode
|
|
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) {
|
|
viewer->critical_operation_in_progress =
|
|
1; // Block fetching during push
|
|
// 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);
|
|
viewer->critical_operation_in_progress = 0; // Re-enable fetching
|
|
}
|
|
break;
|
|
|
|
case 'r':
|
|
if (viewer->commit_count > 0 && viewer->selected_commit == 0) {
|
|
viewer->critical_operation_in_progress = 1;
|
|
reset_commit_soft(viewer, viewer->selected_commit);
|
|
viewer->critical_operation_in_progress = 0;
|
|
}
|
|
break;
|
|
|
|
case 'R': // Reset (hard) - undo commit and discard changes
|
|
if (viewer->commit_count > 0 && viewer->selected_commit == 0) {
|
|
viewer->critical_operation_in_progress = 1;
|
|
reset_commit_hard(viewer, viewer->selected_commit);
|
|
viewer->critical_operation_in_progress = 0;
|
|
}
|
|
break;
|
|
|
|
case 'a':
|
|
case 'A': // Amend most recent commit
|
|
if (viewer->commit_count > 0) {
|
|
viewer->critical_operation_in_progress = 1;
|
|
amend_commit(viewer);
|
|
viewer->critical_operation_in_progress = 0;
|
|
|
|
// Force complete screen refresh after amend dialog
|
|
clear();
|
|
refresh();
|
|
|
|
// Redraw all windows immediately
|
|
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);
|
|
}
|
|
break;
|
|
|
|
case '/': // Forward slash - enter grep search mode
|
|
enter_grep_search_mode(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;
|
|
viewer->split_view_mode = 0; // Exit split view mode
|
|
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) {
|
|
viewer->critical_operation_in_progress = 1;
|
|
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_file_with_staging_info(
|
|
viewer, viewer->files[viewer->selected_file].filename);
|
|
}
|
|
}
|
|
viewer->critical_operation_in_progress = 0;
|
|
}
|
|
break;
|
|
|
|
case 'g':
|
|
case 'G': // Pop stash (applies and removes from list)
|
|
if (viewer->stash_count > 0 &&
|
|
viewer->selected_stash < viewer->stash_count) {
|
|
viewer->critical_operation_in_progress = 1;
|
|
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_file_with_staging_info(
|
|
viewer, viewer->files[viewer->selected_file].filename);
|
|
}
|
|
}
|
|
viewer->critical_operation_in_progress = 0;
|
|
}
|
|
break;
|
|
|
|
case 'd':
|
|
case 'D': // Drop stash (removes without applying)
|
|
if (viewer->stash_count > 0 &&
|
|
viewer->selected_stash < viewer->stash_count) {
|
|
viewer->critical_operation_in_progress = 1;
|
|
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;
|
|
}
|
|
}
|
|
viewer->critical_operation_in_progress = 0;
|
|
}
|
|
break;
|
|
|
|
case '/': // Forward slash - enter grep search mode
|
|
enter_grep_search_mode(viewer);
|
|
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;
|
|
viewer->split_view_mode = 0; // Exit split view mode
|
|
break;
|
|
|
|
case KEY_UP:
|
|
case 'k':
|
|
if (viewer->selected_branch > 0) {
|
|
viewer->selected_branch--;
|
|
// Load commits for the newly selected branch and reset scroll
|
|
if (viewer->branch_count > 0) {
|
|
load_branch_commits(viewer,
|
|
viewer->branches[viewer->selected_branch].name);
|
|
viewer->branch_commits_scroll_offset = 0;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case KEY_DOWN:
|
|
case 'j':
|
|
if (viewer->selected_branch < viewer->branch_count - 1) {
|
|
viewer->selected_branch++;
|
|
// Load commits for the newly selected branch and reset scroll
|
|
if (viewer->branch_count > 0) {
|
|
load_branch_commits(viewer,
|
|
viewer->branches[viewer->selected_branch].name);
|
|
viewer->branch_commits_scroll_offset = 0;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case '\n':
|
|
case '\r':
|
|
case KEY_ENTER:
|
|
// Enter - Switch to branch view mode to scroll commits
|
|
if (viewer->branch_count > 0 &&
|
|
viewer->selected_branch < viewer->branch_count) {
|
|
// Load commits and parse them for navigation
|
|
load_branch_commits(viewer,
|
|
viewer->branches[viewer->selected_branch].name);
|
|
parse_branch_commits_to_lines(viewer);
|
|
viewer->current_mode = NCURSES_MODE_BRANCH_VIEW;
|
|
}
|
|
break;
|
|
|
|
case 'c': // c - Checkout selected branch
|
|
if (viewer->branch_count > 0 &&
|
|
viewer->selected_branch < viewer->branch_count) {
|
|
viewer->critical_operation_in_progress = 1;
|
|
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_file_with_staging_info(
|
|
viewer, viewer->files[viewer->selected_file].filename);
|
|
}
|
|
}
|
|
viewer->critical_operation_in_progress = 0;
|
|
|
|
// Force complete screen refresh after checkout
|
|
clear();
|
|
refresh();
|
|
}
|
|
break;
|
|
|
|
case 'n': // New branch
|
|
{
|
|
viewer->critical_operation_in_progress = 1;
|
|
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_file_with_staging_info(
|
|
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();
|
|
viewer->critical_operation_in_progress = 0;
|
|
} break;
|
|
|
|
case 'd': // Delete branch
|
|
if (viewer->branch_count > 0 &&
|
|
viewer->selected_branch < viewer->branch_count) {
|
|
viewer->critical_operation_in_progress = 1;
|
|
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();
|
|
viewer->critical_operation_in_progress = 0;
|
|
}
|
|
break;
|
|
|
|
case 'r': // Rename branch
|
|
if (viewer->branch_count > 0 &&
|
|
viewer->selected_branch < viewer->branch_count) {
|
|
viewer->critical_operation_in_progress = 1;
|
|
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();
|
|
viewer->critical_operation_in_progress = 0;
|
|
}
|
|
break;
|
|
|
|
case 'p':
|
|
if (viewer->branch_count > 0 &&
|
|
viewer->selected_branch < viewer->branch_count) {
|
|
viewer->critical_operation_in_progress = 1;
|
|
const char *branch_name =
|
|
viewer->branches[viewer->selected_branch].name;
|
|
|
|
if (viewer->branches[viewer->selected_branch].commits_behind > 0) {
|
|
// Start pulling animation immediately
|
|
viewer->sync_status = SYNC_STATUS_PULLING_APPEARING;
|
|
viewer->animation_frame = 0;
|
|
viewer->text_char_count = 0;
|
|
|
|
// Set branch-specific pull status
|
|
viewer->pulling_branch_index = viewer->selected_branch;
|
|
viewer->branch_pull_status = SYNC_STATUS_PULLING_VISIBLE;
|
|
viewer->branch_animation_frame = 0;
|
|
viewer->branch_text_char_count = 7; // Show full "Pulling" immediately
|
|
|
|
// Force immediate branch window refresh to show "Pulling" before the
|
|
// blocking git operation
|
|
werase(viewer->branch_list_win);
|
|
render_branch_list_window(viewer);
|
|
wrefresh(viewer->branch_list_win);
|
|
|
|
// Create a simple animated pull with spinner updates
|
|
pid_t pull_pid = fork();
|
|
int result = 0;
|
|
|
|
if (pull_pid == 0) {
|
|
// Child process: do the actual pull
|
|
exit(system("git pull 2>/dev/null >/dev/null"));
|
|
}
|
|
|
|
// Parent process: animate the spinner while pull is happening
|
|
if (pull_pid > 0) {
|
|
int status;
|
|
int spinner_counter = 0;
|
|
|
|
while (waitpid(pull_pid, &status, WNOHANG) == 0) {
|
|
// Update spinner animation
|
|
viewer->branch_animation_frame = spinner_counter;
|
|
spinner_counter =
|
|
(spinner_counter + 1) % 40; // Cycle every 40 iterations
|
|
|
|
// Refresh the branch window to show spinning animation
|
|
werase(viewer->branch_list_win);
|
|
render_branch_list_window(viewer);
|
|
wrefresh(viewer->branch_list_win);
|
|
|
|
// Small delay for animation timing
|
|
usleep(100000); // 100ms delay
|
|
}
|
|
|
|
// Get the exit status of the pull command
|
|
if (WIFEXITED(status)) {
|
|
result = WEXITSTATUS(status);
|
|
} else {
|
|
result = 1; // Error
|
|
}
|
|
} else {
|
|
result = 1; // Fork failed
|
|
}
|
|
|
|
if (result == 0) {
|
|
// Reset branch animation completely
|
|
viewer->branch_pull_status = SYNC_STATUS_PULLED_APPEARING;
|
|
viewer->branch_animation_frame = 0;
|
|
viewer->branch_text_char_count = 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_file_with_staging_info(
|
|
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;
|
|
viewer->pulling_branch_index = -1;
|
|
viewer->branch_pull_status = SYNC_STATUS_IDLE;
|
|
}
|
|
|
|
} else {
|
|
show_error_popup("No commits to pull from remote");
|
|
}
|
|
viewer->critical_operation_in_progress = 0;
|
|
}
|
|
break;
|
|
|
|
case '/': // Forward slash - enter grep search mode
|
|
enter_grep_search_mode(viewer);
|
|
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
|
|
viewer->split_view_mode = 0; // Exit split view mode
|
|
break;
|
|
|
|
case KEY_UP:
|
|
case 'k':
|
|
// Move cursor up while skipping empty lines
|
|
move_cursor_smart(viewer, -1);
|
|
break;
|
|
|
|
case KEY_DOWN:
|
|
case 'j':
|
|
// Move cursor down while skipping empty lines
|
|
move_cursor_smart(viewer, 1);
|
|
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
|
|
viewer->split_view_mode = 0; // Exit split view mode
|
|
break;
|
|
|
|
case KEY_UP:
|
|
case 'k':
|
|
// Move cursor up while skipping empty lines
|
|
move_cursor_smart(viewer, -1);
|
|
break;
|
|
|
|
case KEY_DOWN:
|
|
case 'j':
|
|
// Move cursor down while skipping empty lines
|
|
move_cursor_smart(viewer, 1);
|
|
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_BRANCH_VIEW) {
|
|
// Branch view mode navigation (same as file view)
|
|
switch (key) {
|
|
case 27: // ESC
|
|
viewer->current_mode =
|
|
NCURSES_MODE_BRANCH_LIST; // Return to branch list mode
|
|
viewer->split_view_mode = 0; // Exit split view mode
|
|
break;
|
|
|
|
case KEY_UP:
|
|
case 'k':
|
|
// Move cursor up while skipping empty lines
|
|
move_cursor_smart(viewer, -1);
|
|
break;
|
|
|
|
case KEY_DOWN:
|
|
case 'j':
|
|
// Move cursor down while skipping empty lines
|
|
move_cursor_smart(viewer, 1);
|
|
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;
|
|
}
|
|
signal(SIGWINCH, handle_sigwinch);
|
|
|
|
// 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);
|
|
|
|
// Initial preview will be handled by update_preview_for_current_selection
|
|
|
|
// 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);
|
|
render_fuzzy_search(&viewer);
|
|
render_grep_search(&viewer);
|
|
|
|
// Main display loop
|
|
int running = 1;
|
|
NCursesViewMode last_mode = viewer.current_mode;
|
|
|
|
while (running) {
|
|
|
|
if (terminal_resized) {
|
|
handle_terminal_resize(&viewer);
|
|
}
|
|
|
|
// 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);
|
|
|
|
// Update preview based on current selection
|
|
update_preview_for_current_selection(&viewer);
|
|
|
|
// Skip main window rendering if fuzzy or grep search is active to prevent flickering
|
|
if (!viewer.fuzzy_search_active && !viewer.grep_search_active) {
|
|
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);
|
|
}
|
|
|
|
// Always render search overlays (they handle their own visibility)
|
|
render_fuzzy_search(&viewer);
|
|
render_grep_search(&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;
|
|
|
|
// Note: We now do fetching in the background to avoid UI freezes
|
|
|
|
// 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_file_with_staging_info(
|
|
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);
|
|
|
|
// Check if this stash is currently being viewed
|
|
int is_being_viewed = (i == viewer->selected_stash &&
|
|
viewer->current_mode == NCURSES_MODE_STASH_VIEW);
|
|
|
|
// Apply line highlight if selected
|
|
if (is_selected_stash) {
|
|
wattron(viewer->stash_list_win, COLOR_PAIR(5));
|
|
}
|
|
|
|
// Show view indicator (currently being viewed)
|
|
if (is_being_viewed) {
|
|
wattron(viewer->stash_list_win, COLOR_PAIR(1)); // Green for active view
|
|
mvwprintw(viewer->stash_list_win, y, 0, "*");
|
|
wattroff(viewer->stash_list_win, COLOR_PAIR(1));
|
|
} else {
|
|
mvwprintw(viewer->stash_list_win, y, 0, " ");
|
|
}
|
|
|
|
// 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 time part and white message part
|
|
if (is_selected_stash) {
|
|
wattroff(viewer->stash_list_win, COLOR_PAIR(5));
|
|
}
|
|
|
|
// Find the colon after "On [branch]:"
|
|
char *colon_pos = strstr(truncated_stash, ": ");
|
|
if (colon_pos) {
|
|
// Split at the colon
|
|
int time_part_len = colon_pos - truncated_stash + 1; // Include the ":"
|
|
|
|
// Find where " On " starts to insert space before it for single digits
|
|
char *on_pos = strstr(truncated_stash, " On ");
|
|
|
|
if (on_pos) {
|
|
// Check if time is single digit (e.g., "6h" or "4m")
|
|
int is_single_digit = 0;
|
|
int time_len = on_pos - truncated_stash;
|
|
if (time_len == 2) { // Single digit + letter (e.g., "6h", "4m")
|
|
char first_char = truncated_stash[0];
|
|
char second_char = truncated_stash[1];
|
|
if (first_char >= '1' && first_char <= '9' &&
|
|
(second_char == 'h' || second_char == 'm' ||
|
|
second_char == 'd' || second_char == 'w')) {
|
|
is_single_digit = 1;
|
|
}
|
|
}
|
|
|
|
// Print time part in yellow
|
|
wattron(viewer->stash_list_win, COLOR_PAIR(4));
|
|
mvwprintw(viewer->stash_list_win, y, 2, "%.*s", time_len,
|
|
truncated_stash);
|
|
|
|
// Add extra space for single digit times
|
|
if (is_single_digit) {
|
|
mvwprintw(viewer->stash_list_win, y, 2 + time_len, " ");
|
|
time_len++;
|
|
}
|
|
|
|
// Print " On [branch]:" part in yellow
|
|
mvwprintw(viewer->stash_list_win, y, 2 + time_len, "%.*s",
|
|
(int)(colon_pos - on_pos + 1), on_pos);
|
|
wattroff(viewer->stash_list_win, COLOR_PAIR(4));
|
|
|
|
// Print message part in normal color (white)
|
|
mvwprintw(viewer->stash_list_win, y,
|
|
2 + time_part_len + (is_single_digit ? 1 : 0), "%s",
|
|
colon_pos + 1);
|
|
} else {
|
|
// Fallback: print entire yellow part if no " On " found
|
|
wattron(viewer->stash_list_win, COLOR_PAIR(4));
|
|
mvwprintw(viewer->stash_list_win, y, 2, "%.*s", time_part_len,
|
|
truncated_stash);
|
|
wattroff(viewer->stash_list_win, COLOR_PAIR(4));
|
|
|
|
// Print message part in normal color (white)
|
|
mvwprintw(viewer->stash_list_win, y, 2 + time_part_len, "%s",
|
|
colon_pos + 1);
|
|
}
|
|
} else {
|
|
// Fallback: print entire string in yellow if no colon found
|
|
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));
|
|
}
|
|
}
|
|
}
|
|
|
|
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));
|
|
}
|
|
}
|
|
|
|
// Add push/pull status indicators next to the branch
|
|
char branch_sync_text[32] = "";
|
|
char *spinner_chars[] = {"|", "/", "-", "\\"};
|
|
int spinner_idx = (viewer->branch_animation_frame / 1) % 4;
|
|
|
|
// Check if this branch is being pushed
|
|
if (i == viewer->pushing_branch_index) {
|
|
if (viewer->branch_push_status >= SYNC_STATUS_PUSHING_APPEARING &&
|
|
viewer->branch_push_status <= SYNC_STATUS_PUSHING_DISAPPEARING) {
|
|
if (viewer->branch_push_status == SYNC_STATUS_PUSHING_VISIBLE) {
|
|
snprintf(branch_sync_text, sizeof(branch_sync_text), " Pushing %s",
|
|
spinner_chars[spinner_idx]);
|
|
} else {
|
|
char partial_text[16] = "";
|
|
int chars_to_show = viewer->branch_text_char_count;
|
|
if (chars_to_show > 7)
|
|
chars_to_show = 7;
|
|
if (chars_to_show > 0) {
|
|
strncpy(partial_text, "Pushing", chars_to_show);
|
|
partial_text[chars_to_show] = '\0';
|
|
snprintf(branch_sync_text, sizeof(branch_sync_text), " %s",
|
|
partial_text);
|
|
}
|
|
}
|
|
} else if (viewer->branch_push_status >= SYNC_STATUS_PUSHED_APPEARING &&
|
|
viewer->branch_push_status <=
|
|
SYNC_STATUS_PUSHED_DISAPPEARING) {
|
|
char partial_text[16] = "";
|
|
int chars_to_show = viewer->branch_text_char_count;
|
|
if (chars_to_show > 7)
|
|
chars_to_show = 7;
|
|
if (chars_to_show > 0) {
|
|
strncpy(partial_text, "Pushed!", chars_to_show);
|
|
partial_text[chars_to_show] = '\0';
|
|
snprintf(branch_sync_text, sizeof(branch_sync_text), " %s",
|
|
partial_text);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check if this branch is being pulled
|
|
if (i == viewer->pulling_branch_index) {
|
|
if (viewer->branch_pull_status >= SYNC_STATUS_PULLING_APPEARING &&
|
|
viewer->branch_pull_status <= SYNC_STATUS_PULLING_DISAPPEARING) {
|
|
if (viewer->branch_pull_status == SYNC_STATUS_PULLING_VISIBLE) {
|
|
snprintf(branch_sync_text, sizeof(branch_sync_text), " Pulling %s",
|
|
spinner_chars[spinner_idx]);
|
|
} else {
|
|
char partial_text[16] = "";
|
|
int chars_to_show = viewer->branch_text_char_count;
|
|
if (chars_to_show > 7)
|
|
chars_to_show = 7;
|
|
if (chars_to_show > 0) {
|
|
strncpy(partial_text, "Pulling", chars_to_show);
|
|
partial_text[chars_to_show] = '\0';
|
|
snprintf(branch_sync_text, sizeof(branch_sync_text), " %s",
|
|
partial_text);
|
|
}
|
|
}
|
|
} else if (viewer->branch_pull_status >= SYNC_STATUS_PULLED_APPEARING &&
|
|
viewer->branch_pull_status <=
|
|
SYNC_STATUS_PULLED_DISAPPEARING) {
|
|
char partial_text[16] = "";
|
|
int chars_to_show = viewer->branch_text_char_count;
|
|
if (chars_to_show > 7)
|
|
chars_to_show = 7;
|
|
if (chars_to_show > 0) {
|
|
strncpy(partial_text, "Pulled!", chars_to_show);
|
|
partial_text[chars_to_show] = '\0';
|
|
snprintf(branch_sync_text, sizeof(branch_sync_text), " %s",
|
|
partial_text);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Display the branch sync status
|
|
if (strlen(branch_sync_text) > 0) {
|
|
if (is_selected_branch) {
|
|
wattroff(viewer->branch_list_win, COLOR_PAIR(5));
|
|
}
|
|
|
|
wattron(viewer->branch_list_win,
|
|
COLOR_PAIR(4)); // Yellow for sync status
|
|
mvwprintw(viewer->branch_list_win, y,
|
|
2 + strlen(display_branch) + strlen(status_indicator), "%s",
|
|
branch_sync_text);
|
|
wattroff(viewer->branch_list_win, COLOR_PAIR(4));
|
|
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* Load commits for a specific branch for the hover preview
|
|
*/
|
|
int load_branch_commits(NCursesDiffViewer *viewer, const char *branch_name) {
|
|
if (!viewer || !branch_name) {
|
|
return 0;
|
|
}
|
|
|
|
// Only load if it's a different branch than currently loaded
|
|
if (strcmp(viewer->current_branch_for_commits, branch_name) == 0) {
|
|
return viewer->branch_commit_count; // Already loaded
|
|
}
|
|
|
|
viewer->branch_commit_count =
|
|
get_branch_commits(branch_name, viewer->branch_commits, MAX_COMMITS);
|
|
|
|
strncpy(viewer->current_branch_for_commits, branch_name,
|
|
sizeof(viewer->current_branch_for_commits) - 1);
|
|
viewer
|
|
->current_branch_for_commits[sizeof(viewer->current_branch_for_commits) -
|
|
1] = '\0';
|
|
|
|
return viewer->branch_commit_count;
|
|
}
|
|
|
|
/**
|
|
* Parse branch commits into navigable lines for branch view mode
|
|
*/
|
|
int parse_branch_commits_to_lines(NCursesDiffViewer *viewer) {
|
|
if (!viewer || viewer->branch_commit_count == 0) {
|
|
return 0;
|
|
}
|
|
|
|
viewer->file_line_count = 0;
|
|
viewer->file_scroll_offset = 0;
|
|
viewer->file_cursor_line = 0;
|
|
|
|
// Parse each commit into file_lines for navigation
|
|
for (int commit_idx = 0; commit_idx < viewer->branch_commit_count &&
|
|
viewer->file_line_count < MAX_FULL_FILE_LINES;
|
|
commit_idx++) {
|
|
|
|
char *commit_text = viewer->branch_commits[commit_idx];
|
|
char *line_start = commit_text;
|
|
char *line_end;
|
|
|
|
// Parse each line of the commit
|
|
while ((line_end = strchr(line_start, '\n')) != NULL &&
|
|
viewer->file_line_count < MAX_FULL_FILE_LINES) {
|
|
*line_end = '\0'; // Temporarily null-terminate
|
|
|
|
NCursesFileLine *file_line = &viewer->file_lines[viewer->file_line_count];
|
|
strncpy(file_line->line, line_start, sizeof(file_line->line) - 1);
|
|
file_line->line[sizeof(file_line->line) - 1] = '\0';
|
|
|
|
// Set line type for appropriate coloring
|
|
if (strncmp(line_start, "commit ", 7) == 0) {
|
|
file_line->type = 'h'; // Commit header
|
|
} else if (strncmp(line_start, "Author:", 7) == 0 ||
|
|
strncmp(line_start, "Date:", 5) == 0) {
|
|
file_line->type = 'i'; // Info line
|
|
} else {
|
|
file_line->type = ' '; // Regular line
|
|
}
|
|
|
|
file_line->is_diff_line = 0;
|
|
viewer->file_line_count++;
|
|
|
|
*line_end = '\n'; // Restore newline
|
|
line_start = line_end + 1;
|
|
}
|
|
|
|
// Handle last line if no newline at end
|
|
if (strlen(line_start) > 0 &&
|
|
viewer->file_line_count < MAX_FULL_FILE_LINES) {
|
|
NCursesFileLine *file_line = &viewer->file_lines[viewer->file_line_count];
|
|
strncpy(file_line->line, line_start, 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++;
|
|
}
|
|
|
|
// Add spacing between commits
|
|
if (viewer->file_line_count < MAX_FULL_FILE_LINES) {
|
|
NCursesFileLine *empty_line =
|
|
&viewer->file_lines[viewer->file_line_count];
|
|
strcpy(empty_line->line, "");
|
|
empty_line->type = ' ';
|
|
empty_line->is_diff_line = 0;
|
|
viewer->file_line_count++;
|
|
}
|
|
}
|
|
|
|
return viewer->file_line_count;
|
|
}
|
|
|
|
/**
|
|
* Start background fetch process
|
|
*/
|
|
void start_background_fetch(NCursesDiffViewer *viewer) {
|
|
if (!viewer || viewer->fetch_in_progress ||
|
|
viewer->critical_operation_in_progress) {
|
|
return;
|
|
}
|
|
|
|
viewer->fetch_pid = fork();
|
|
if (viewer->fetch_pid == 0) {
|
|
// Child process: do the fetch
|
|
system("git fetch --all --quiet >/dev/null 2>&1");
|
|
exit(0);
|
|
} else if (viewer->fetch_pid > 0) {
|
|
// Parent process: mark fetch as in progress
|
|
viewer->fetch_in_progress = 1;
|
|
viewer->sync_status = SYNC_STATUS_SYNCING_APPEARING;
|
|
viewer->animation_frame = 0;
|
|
viewer->text_char_count = 0;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if background fetch is complete and update UI accordingly
|
|
*/
|
|
void check_background_fetch(NCursesDiffViewer *viewer) {
|
|
if (!viewer || !viewer->fetch_in_progress) {
|
|
return;
|
|
}
|
|
|
|
int status;
|
|
pid_t result = waitpid(viewer->fetch_pid, &status, WNOHANG);
|
|
|
|
if (result == viewer->fetch_pid) {
|
|
// Fetch completed
|
|
viewer->fetch_in_progress = 0;
|
|
viewer->fetch_pid = -1;
|
|
|
|
// Preserve current positions during refresh
|
|
int preserved_file_scroll = viewer->file_scroll_offset;
|
|
int preserved_file_cursor = viewer->file_cursor_line;
|
|
int preserved_selected_file = viewer->selected_file;
|
|
|
|
// Update only the necessary data without blocking UI
|
|
get_ncurses_changed_files(viewer);
|
|
get_commit_history(viewer);
|
|
get_ncurses_git_branches(viewer);
|
|
|
|
// Restore file selection if still valid
|
|
if (preserved_selected_file < viewer->file_count) {
|
|
viewer->selected_file = preserved_selected_file;
|
|
|
|
// Reload current file if in file mode and restore position
|
|
if ((viewer->current_mode == NCURSES_MODE_FILE_LIST ||
|
|
viewer->current_mode == NCURSES_MODE_FILE_VIEW) &&
|
|
viewer->file_count > 0) {
|
|
load_file_with_staging_info(
|
|
viewer, viewer->files[viewer->selected_file].filename);
|
|
|
|
// Restore scroll position if still valid
|
|
if (preserved_file_cursor < viewer->file_line_count) {
|
|
viewer->file_cursor_line = preserved_file_cursor;
|
|
}
|
|
if (preserved_file_scroll < viewer->file_line_count) {
|
|
viewer->file_scroll_offset = preserved_file_scroll;
|
|
}
|
|
}
|
|
}
|
|
|
|
// If we're in branch mode and have commits loaded, refresh them
|
|
if (viewer->current_mode == NCURSES_MODE_BRANCH_LIST ||
|
|
viewer->current_mode == NCURSES_MODE_BRANCH_VIEW) {
|
|
if (viewer->branch_count > 0 &&
|
|
strlen(viewer->current_branch_for_commits) > 0) {
|
|
load_branch_commits(viewer, viewer->current_branch_for_commits);
|
|
if (viewer->current_mode == NCURSES_MODE_BRANCH_VIEW) {
|
|
// Preserve cursor position in branch view too
|
|
int prev_cursor = viewer->file_cursor_line;
|
|
int prev_scroll = viewer->file_scroll_offset;
|
|
parse_branch_commits_to_lines(viewer);
|
|
if (prev_cursor < viewer->file_line_count) {
|
|
viewer->file_cursor_line = prev_cursor;
|
|
}
|
|
if (prev_scroll < viewer->file_line_count) {
|
|
viewer->file_scroll_offset = prev_scroll;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Show completion status briefly
|
|
viewer->sync_status = SYNC_STATUS_SYNCED_APPEARING;
|
|
viewer->animation_frame = 0;
|
|
viewer->text_char_count = 0;
|
|
} else if (result == -1) {
|
|
// Error occurred
|
|
viewer->fetch_in_progress = 0;
|
|
viewer->fetch_pid = -1;
|
|
viewer->sync_status = SYNC_STATUS_IDLE;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update preview based on current mode and selection
|
|
*/
|
|
void update_preview_for_current_selection(NCursesDiffViewer *viewer) {
|
|
if (!viewer) return;
|
|
|
|
static NCursesViewMode last_mode = (NCursesViewMode)-1;
|
|
static int last_file = -1;
|
|
static int last_commit = -1;
|
|
static int last_branch = -1;
|
|
static int last_stash = -1;
|
|
|
|
int needs_update = 0;
|
|
|
|
if (viewer->current_mode == NCURSES_MODE_FILE_LIST) {
|
|
if (last_mode != viewer->current_mode || last_file != viewer->selected_file) {
|
|
if (viewer->file_count > 0 && viewer->selected_file < viewer->file_count) {
|
|
// Load diff preview instead of raw file content
|
|
load_file_with_staging_info(viewer, viewer->files[viewer->selected_file].filename);
|
|
}
|
|
last_file = viewer->selected_file;
|
|
needs_update = 1;
|
|
}
|
|
} else if (viewer->current_mode == NCURSES_MODE_COMMIT_LIST) {
|
|
if (last_mode != viewer->current_mode || last_commit != viewer->selected_commit) {
|
|
if (viewer->commit_count > 0 && viewer->selected_commit < viewer->commit_count) {
|
|
load_commit_for_viewing(viewer, viewer->commits[viewer->selected_commit].hash);
|
|
}
|
|
last_commit = viewer->selected_commit;
|
|
needs_update = 1;
|
|
}
|
|
} else if (viewer->current_mode == NCURSES_MODE_BRANCH_LIST) {
|
|
if (last_mode != viewer->current_mode || last_branch != viewer->selected_branch) {
|
|
if (viewer->branch_count > 0 && viewer->selected_branch < viewer->branch_count) {
|
|
load_branch_commits(viewer, viewer->branches[viewer->selected_branch].name);
|
|
parse_branch_commits_to_lines(viewer); // Convert to file_lines for preview display
|
|
}
|
|
last_branch = viewer->selected_branch;
|
|
needs_update = 1;
|
|
}
|
|
} else if (viewer->current_mode == NCURSES_MODE_STASH_LIST) {
|
|
if (last_mode != viewer->current_mode || last_stash != viewer->selected_stash) {
|
|
if (viewer->stash_count > 0 && viewer->selected_stash < viewer->stash_count) {
|
|
load_stash_for_viewing(viewer, viewer->selected_stash);
|
|
}
|
|
last_stash = viewer->selected_stash;
|
|
needs_update = 1;
|
|
}
|
|
}
|
|
|
|
if (needs_update) {
|
|
last_mode = viewer->current_mode;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Load file preview showing the top of the file content
|
|
*/
|
|
int load_file_preview(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;
|
|
|
|
FILE *fp = fopen(filename, "r");
|
|
if (!fp) {
|
|
return 0;
|
|
}
|
|
|
|
char line[1024];
|
|
int line_count = 0;
|
|
|
|
// Load first 50 lines of the file for preview
|
|
while (fgets(line, sizeof(line), fp) && line_count < 50 &&
|
|
line_count < MAX_FULL_FILE_LINES) {
|
|
// Remove trailing newline
|
|
int len = strlen(line);
|
|
if (len > 0 && line[len - 1] == '\n') {
|
|
line[len - 1] = '\0';
|
|
}
|
|
|
|
// Store the line as a context line (no diff markings)
|
|
strncpy(viewer->file_lines[line_count].line, line,
|
|
sizeof(viewer->file_lines[line_count].line) - 1);
|
|
viewer->file_lines[line_count].line[sizeof(viewer->file_lines[line_count].line) - 1] = '\0';
|
|
viewer->file_lines[line_count].type = ' '; // Context line
|
|
viewer->file_lines[line_count].is_diff_line = 0;
|
|
viewer->file_lines[line_count].is_staged = 0;
|
|
viewer->file_lines[line_count].hunk_id = 0;
|
|
viewer->file_lines[line_count].line_number_old = line_count + 1;
|
|
viewer->file_lines[line_count].line_number_new = line_count + 1;
|
|
viewer->file_lines[line_count].is_context = 1;
|
|
|
|
line_count++;
|
|
}
|
|
|
|
fclose(fp);
|
|
viewer->file_line_count = line_count;
|
|
|
|
// Reset staged content since this is just a preview
|
|
viewer->staged_line_count = 0;
|
|
viewer->staged_cursor_line = 0;
|
|
viewer->staged_scroll_offset = 0;
|
|
|
|
return 1;
|
|
}
|
|
|
|
/**
|
|
* Helper function to wrap a long line into multiple display lines
|
|
* Returns the number of wrapped lines created
|
|
*/
|
|
int wrap_line_to_width(const char *input_line, char wrapped_lines[][1024], int max_lines, int width) {
|
|
int input_len = strlen(input_line);
|
|
int line_count = 0;
|
|
int input_pos = 0;
|
|
|
|
// If line fits in width, just copy it
|
|
if (input_len <= width) {
|
|
strcpy(wrapped_lines[0], input_line);
|
|
return 1;
|
|
}
|
|
|
|
while (input_pos < input_len && line_count < max_lines - 1) {
|
|
int chars_to_copy = width;
|
|
|
|
// Don't exceed remaining input length
|
|
if (input_pos + chars_to_copy > input_len) {
|
|
chars_to_copy = input_len - input_pos;
|
|
}
|
|
|
|
// Copy the segment
|
|
strncpy(wrapped_lines[line_count], input_line + input_pos, chars_to_copy);
|
|
wrapped_lines[line_count][chars_to_copy] = '\0';
|
|
|
|
input_pos += chars_to_copy;
|
|
line_count++;
|
|
}
|
|
|
|
return line_count;
|
|
}
|
|
|
|
/**
|
|
* Render a single line with proper wrapping and coloring
|
|
* Returns the number of display rows used
|
|
*/
|
|
int render_wrapped_line(WINDOW *win, const char *line, int start_y, int start_x,
|
|
int width, int max_rows, int color_pair, int reverse) {
|
|
char wrapped_lines[10][1024]; // Support up to 10 wrapped lines per original line
|
|
int wrap_count = wrap_line_to_width(line, wrapped_lines, 10, width - start_x);
|
|
|
|
int rows_used = 0;
|
|
for (int i = 0; i < wrap_count && rows_used < max_rows; i++) {
|
|
if (reverse) {
|
|
wattron(win, A_REVERSE);
|
|
}
|
|
if (color_pair > 0) {
|
|
wattron(win, COLOR_PAIR(color_pair));
|
|
}
|
|
|
|
mvwprintw(win, start_y + rows_used, start_x, "%s", wrapped_lines[i]);
|
|
|
|
if (color_pair > 0) {
|
|
wattroff(win, COLOR_PAIR(color_pair));
|
|
}
|
|
if (reverse) {
|
|
wattroff(win, A_REVERSE);
|
|
}
|
|
|
|
rows_used++;
|
|
}
|
|
|
|
return rows_used;
|
|
}
|
|
|
|
/**
|
|
* Calculate how many display lines a wrapped line will take
|
|
*/
|
|
int calculate_wrapped_line_height(const char *line, int width) {
|
|
int line_len = strlen(line);
|
|
if (line_len <= width) {
|
|
return 1;
|
|
}
|
|
return (line_len + width - 1) / width; // Ceiling division
|
|
}
|
|
|
|
/**
|
|
* Move cursor up/down while skipping empty lines
|
|
* direction: -1 for up, 1 for down
|
|
*/
|
|
void move_cursor_smart(NCursesDiffViewer *viewer, int direction) {
|
|
if (!viewer || viewer->file_line_count == 0) {
|
|
return;
|
|
}
|
|
|
|
int original_cursor = viewer->file_cursor_line;
|
|
int new_cursor = viewer->file_cursor_line;
|
|
int attempts = 0;
|
|
const int max_attempts = viewer->file_line_count; // Prevent infinite loops
|
|
|
|
do {
|
|
new_cursor += direction;
|
|
attempts++;
|
|
|
|
// Bounds checking
|
|
if (new_cursor < 0) {
|
|
new_cursor = 0;
|
|
break;
|
|
}
|
|
if (new_cursor >= viewer->file_line_count) {
|
|
new_cursor = viewer->file_line_count - 1;
|
|
break;
|
|
}
|
|
|
|
// Check if current line is empty or just whitespace
|
|
NCursesFileLine *line = &viewer->file_lines[new_cursor];
|
|
char *trimmed = line->line;
|
|
|
|
// Skip leading whitespace
|
|
while (*trimmed == ' ' || *trimmed == '\t') {
|
|
trimmed++;
|
|
}
|
|
|
|
// If line has content after trimming, or we've tried too many times, stop
|
|
// here
|
|
if (*trimmed != '\0' || attempts >= max_attempts) {
|
|
break;
|
|
}
|
|
|
|
} while (attempts < max_attempts);
|
|
|
|
int cursor_movement = new_cursor - original_cursor;
|
|
|
|
viewer->file_cursor_line = new_cursor;
|
|
|
|
int height, width;
|
|
getmaxyx(viewer->file_content_win, height, width);
|
|
int max_lines_visible = height - 2;
|
|
|
|
if (direction == -1) {
|
|
int cursor_display_pos =
|
|
viewer->file_cursor_line - viewer->file_scroll_offset;
|
|
|
|
if (cursor_display_pos < 3) {
|
|
int scroll_adjustment = 3 - cursor_display_pos;
|
|
viewer->file_scroll_offset -= scroll_adjustment;
|
|
|
|
if (viewer->file_scroll_offset < 0) {
|
|
viewer->file_scroll_offset = 0;
|
|
}
|
|
}
|
|
} else {
|
|
int cursor_display_pos =
|
|
viewer->file_cursor_line - viewer->file_scroll_offset;
|
|
|
|
if (cursor_display_pos >= max_lines_visible - 3) {
|
|
int scroll_adjustment = cursor_display_pos - (max_lines_visible - 4);
|
|
viewer->file_scroll_offset += scroll_adjustment;
|
|
|
|
int max_scroll = viewer->file_line_count - max_lines_visible;
|
|
if (max_scroll < 0)
|
|
max_scroll = 0;
|
|
if (viewer->file_scroll_offset > max_scroll) {
|
|
viewer->file_scroll_offset = max_scroll;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Move cursor up/down in unstaged pane with proper scrolling
|
|
* direction: -1 for up, 1 for down
|
|
*/
|
|
void move_cursor_smart_unstaged(NCursesDiffViewer *viewer, int direction) {
|
|
if (!viewer || viewer->file_line_count == 0) {
|
|
return;
|
|
}
|
|
|
|
int original_cursor = viewer->file_cursor_line;
|
|
int new_cursor = viewer->file_cursor_line;
|
|
int attempts = 0;
|
|
const int max_attempts = viewer->file_line_count;
|
|
|
|
do {
|
|
new_cursor += direction;
|
|
attempts++;
|
|
|
|
if (new_cursor < 0) {
|
|
new_cursor = 0;
|
|
break;
|
|
}
|
|
if (new_cursor >= viewer->file_line_count) {
|
|
new_cursor = viewer->file_line_count - 1;
|
|
break;
|
|
}
|
|
|
|
NCursesFileLine *line = &viewer->file_lines[new_cursor];
|
|
char *trimmed = line->line;
|
|
|
|
while (*trimmed == ' ' || *trimmed == '\t') {
|
|
trimmed++;
|
|
}
|
|
|
|
if (*trimmed != '\0' || attempts >= max_attempts) {
|
|
break;
|
|
}
|
|
|
|
} while (attempts < max_attempts);
|
|
|
|
viewer->file_cursor_line = new_cursor;
|
|
|
|
int height, width;
|
|
getmaxyx(viewer->file_content_win, height, width);
|
|
int split_line = height / 2;
|
|
int unstaged_height = split_line - 1;
|
|
|
|
// Calculate display positions accounting for wrapped lines
|
|
int cursor_display_rows = 0;
|
|
int scroll_display_rows = 0;
|
|
|
|
// Count display rows from scroll offset to cursor
|
|
for (int i = viewer->file_scroll_offset; i <= viewer->file_cursor_line && i < viewer->file_line_count; i++) {
|
|
int line_height = calculate_wrapped_line_height(viewer->file_lines[i].line, width - 4);
|
|
if (i < viewer->file_cursor_line) {
|
|
cursor_display_rows += line_height;
|
|
}
|
|
if (i >= viewer->file_scroll_offset) {
|
|
scroll_display_rows += line_height;
|
|
}
|
|
}
|
|
|
|
if (direction == -1) {
|
|
// Moving up - ensure cursor stays visible at top
|
|
if (cursor_display_rows < 2) {
|
|
// Need to scroll up to make cursor visible
|
|
int target_rows = 2;
|
|
int new_scroll_offset = viewer->file_cursor_line;
|
|
int accumulated_rows = calculate_wrapped_line_height(viewer->file_lines[viewer->file_cursor_line].line, width - 4);
|
|
|
|
while (new_scroll_offset > 0 && accumulated_rows < target_rows) {
|
|
new_scroll_offset--;
|
|
accumulated_rows += calculate_wrapped_line_height(viewer->file_lines[new_scroll_offset].line, width - 4);
|
|
}
|
|
|
|
viewer->file_scroll_offset = new_scroll_offset;
|
|
if (viewer->file_scroll_offset < 0) {
|
|
viewer->file_scroll_offset = 0;
|
|
}
|
|
}
|
|
} else {
|
|
// Moving down - ensure cursor stays visible at bottom
|
|
if (cursor_display_rows >= unstaged_height - 2) {
|
|
// Need to scroll down to make cursor visible
|
|
int target_remaining_rows = unstaged_height - 3;
|
|
int new_scroll_offset = viewer->file_cursor_line;
|
|
int accumulated_rows = calculate_wrapped_line_height(viewer->file_lines[viewer->file_cursor_line].line, width - 4);
|
|
|
|
while (new_scroll_offset > viewer->file_scroll_offset && accumulated_rows > target_remaining_rows) {
|
|
new_scroll_offset--;
|
|
accumulated_rows -= calculate_wrapped_line_height(viewer->file_lines[new_scroll_offset].line, width - 4);
|
|
}
|
|
|
|
if (new_scroll_offset > viewer->file_scroll_offset) {
|
|
viewer->file_scroll_offset = new_scroll_offset;
|
|
}
|
|
|
|
int max_scroll = viewer->file_line_count - 1;
|
|
if (viewer->file_scroll_offset > max_scroll) {
|
|
viewer->file_scroll_offset = max_scroll;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Move cursor up/down in staged pane with proper scrolling
|
|
* direction: -1 for up, 1 for down
|
|
*/
|
|
void move_cursor_smart_staged(NCursesDiffViewer *viewer, int direction) {
|
|
if (!viewer || viewer->staged_line_count == 0) {
|
|
return;
|
|
}
|
|
|
|
int height, width;
|
|
getmaxyx(viewer->file_content_win, height, width);
|
|
int split_line = height / 2;
|
|
int staged_height = height - split_line - 2;
|
|
|
|
if (direction == -1) {
|
|
if (viewer->staged_cursor_line > 0) {
|
|
viewer->staged_cursor_line--;
|
|
|
|
// Calculate display positions accounting for wrapped lines
|
|
int cursor_display_rows = 0;
|
|
|
|
// Count display rows from scroll offset to cursor
|
|
for (int i = viewer->staged_scroll_offset; i < viewer->staged_cursor_line && i < viewer->staged_line_count; i++) {
|
|
cursor_display_rows += calculate_wrapped_line_height(viewer->staged_lines[i].line, width - 4);
|
|
}
|
|
|
|
if (cursor_display_rows < 1) {
|
|
// Need to scroll up to make cursor visible
|
|
int target_rows = 1;
|
|
int new_scroll_offset = viewer->staged_cursor_line;
|
|
int accumulated_rows = calculate_wrapped_line_height(viewer->staged_lines[viewer->staged_cursor_line].line, width - 4);
|
|
|
|
while (new_scroll_offset > 0 && accumulated_rows < target_rows) {
|
|
new_scroll_offset--;
|
|
accumulated_rows += calculate_wrapped_line_height(viewer->staged_lines[new_scroll_offset].line, width - 4);
|
|
}
|
|
|
|
viewer->staged_scroll_offset = new_scroll_offset;
|
|
if (viewer->staged_scroll_offset < 0) {
|
|
viewer->staged_scroll_offset = 0;
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
if (viewer->staged_cursor_line < viewer->staged_line_count - 1) {
|
|
viewer->staged_cursor_line++;
|
|
|
|
// Calculate display positions accounting for wrapped lines
|
|
int cursor_display_rows = 0;
|
|
|
|
// Count display rows from scroll offset to cursor
|
|
for (int i = viewer->staged_scroll_offset; i < viewer->staged_cursor_line && i < viewer->staged_line_count; i++) {
|
|
cursor_display_rows += calculate_wrapped_line_height(viewer->staged_lines[i].line, width - 4);
|
|
}
|
|
|
|
if (cursor_display_rows >= staged_height - 1) {
|
|
// Need to scroll down to make cursor visible
|
|
int target_remaining_rows = staged_height - 2;
|
|
int new_scroll_offset = viewer->staged_cursor_line;
|
|
int accumulated_rows = calculate_wrapped_line_height(viewer->staged_lines[viewer->staged_cursor_line].line, width - 4);
|
|
|
|
while (new_scroll_offset > viewer->staged_scroll_offset && accumulated_rows > target_remaining_rows) {
|
|
new_scroll_offset--;
|
|
accumulated_rows -= calculate_wrapped_line_height(viewer->staged_lines[new_scroll_offset].line, width - 4);
|
|
}
|
|
|
|
if (new_scroll_offset > viewer->staged_scroll_offset) {
|
|
viewer->staged_scroll_offset = new_scroll_offset;
|
|
}
|
|
|
|
int max_scroll = viewer->staged_line_count - 1;
|
|
if (viewer->staged_scroll_offset > max_scroll) {
|
|
viewer->staged_scroll_offset = max_scroll;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Clean up ncurses diff viewer resources
|
|
*/
|
|
void cleanup_ncurses_diff_viewer(NCursesDiffViewer *viewer) {
|
|
if (viewer) {
|
|
// Clean up any background fetch process
|
|
if (viewer->fetch_in_progress && viewer->fetch_pid > 0) {
|
|
kill(viewer->fetch_pid, SIGTERM);
|
|
waitpid(viewer->fetch_pid, NULL, 0);
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
// Clean up fuzzy search windows
|
|
cleanup_fuzzy_search(viewer);
|
|
|
|
// Clean up grep search windows
|
|
cleanup_grep_search(viewer);
|
|
}
|
|
endwin();
|
|
}
|
|
|
|
/**
|
|
* Initialize fuzzy search state
|
|
*/
|
|
void init_fuzzy_search(NCursesDiffViewer *viewer) {
|
|
if (!viewer) return;
|
|
|
|
viewer->fuzzy_search_active = 0;
|
|
viewer->fuzzy_search_query[0] = '\0';
|
|
viewer->fuzzy_search_query_len = 0;
|
|
viewer->fuzzy_filtered_count = 0;
|
|
viewer->fuzzy_selected_index = 0;
|
|
viewer->fuzzy_scroll_offset = 0;
|
|
viewer->fuzzy_input_win = NULL;
|
|
viewer->fuzzy_list_win = NULL;
|
|
|
|
// Initialize state tracking
|
|
viewer->fuzzy_needs_full_redraw = 0;
|
|
viewer->fuzzy_needs_input_redraw = 0;
|
|
viewer->fuzzy_needs_list_redraw = 0;
|
|
viewer->fuzzy_last_query[0] = '\0';
|
|
viewer->fuzzy_last_selected = -1;
|
|
viewer->fuzzy_last_scroll = -1;
|
|
viewer->fuzzy_last_filtered_count = -1;
|
|
}
|
|
|
|
/**
|
|
* Cleanup fuzzy search windows
|
|
*/
|
|
void cleanup_fuzzy_search(NCursesDiffViewer *viewer) {
|
|
if (!viewer) return;
|
|
|
|
if (viewer->fuzzy_input_win) {
|
|
delwin(viewer->fuzzy_input_win);
|
|
viewer->fuzzy_input_win = NULL;
|
|
}
|
|
if (viewer->fuzzy_list_win) {
|
|
delwin(viewer->fuzzy_list_win);
|
|
viewer->fuzzy_list_win = NULL;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Calculate fuzzy match score - higher score is better match
|
|
* Returns 0 if no match, positive score if match
|
|
*/
|
|
int calculate_fuzzy_score(const char *pattern, const char *filename) {
|
|
if (!pattern || !filename) return 0;
|
|
if (strlen(pattern) == 0) return 1000; // Empty pattern matches everything with high score
|
|
|
|
int pattern_len = strlen(pattern);
|
|
int filename_len = strlen(filename);
|
|
|
|
if (pattern_len > filename_len) return 0; // Pattern longer than filename
|
|
|
|
int score = 0;
|
|
int pattern_pos = 0;
|
|
int consecutive_matches = 0;
|
|
int first_char_bonus = 0;
|
|
|
|
for (int i = 0; i < filename_len && pattern_pos < pattern_len; i++) {
|
|
char p_char = tolower(pattern[pattern_pos]);
|
|
char f_char = tolower(filename[i]);
|
|
|
|
if (p_char == f_char) {
|
|
// Base score for character match
|
|
score += 1;
|
|
|
|
// Bonus for consecutive matches (prioritizes contiguous substrings)
|
|
consecutive_matches++;
|
|
score += consecutive_matches * 5;
|
|
|
|
// Bonus for matching at word boundaries
|
|
if (i == 0 || filename[i-1] == '/' || filename[i-1] == '_' || filename[i-1] == '-' || filename[i-1] == '.') {
|
|
score += 15;
|
|
}
|
|
|
|
// Bonus for matching first character
|
|
if (pattern_pos == 0) {
|
|
if (i == 0) {
|
|
first_char_bonus = 50; // Very high bonus for first char match
|
|
} else {
|
|
// Find start of filename (after last /)
|
|
const char *basename = strrchr(filename, '/');
|
|
basename = basename ? basename + 1 : filename;
|
|
if (filename + i == basename) {
|
|
first_char_bonus = 30; // High bonus for basename first char
|
|
}
|
|
}
|
|
}
|
|
|
|
// Bonus for exact position match (same position in pattern and filename)
|
|
if (i == pattern_pos) {
|
|
score += 10;
|
|
}
|
|
|
|
pattern_pos++;
|
|
} else {
|
|
// Reset consecutive matches
|
|
consecutive_matches = 0;
|
|
}
|
|
}
|
|
|
|
// Must match all pattern characters
|
|
if (pattern_pos < pattern_len) return 0;
|
|
|
|
// Apply first character bonus
|
|
score += first_char_bonus;
|
|
|
|
// Bonus for shorter filenames (prefer more specific matches)
|
|
score += (100 - filename_len);
|
|
|
|
// Bonus for fewer unmatched characters
|
|
int unmatched_chars = filename_len - pattern_len;
|
|
score += (50 - unmatched_chars);
|
|
|
|
return score;
|
|
}
|
|
|
|
/**
|
|
* Compare function for sorting scored files (higher scores first)
|
|
*/
|
|
int compare_scored_files(const void *a, const void *b) {
|
|
const struct {int file_index; int score;} *file_a = a;
|
|
const struct {int file_index; int score;} *file_b = b;
|
|
return file_b->score - file_a->score; // Descending order (higher scores first)
|
|
}
|
|
|
|
/**
|
|
* Update fuzzy search filter based on current query
|
|
*/
|
|
void update_fuzzy_filter(NCursesDiffViewer *viewer) {
|
|
if (!viewer) return;
|
|
|
|
viewer->fuzzy_filtered_count = 0;
|
|
viewer->fuzzy_selected_index = 0;
|
|
viewer->fuzzy_scroll_offset = 0;
|
|
|
|
// Calculate scores for all files and collect matches
|
|
for (int i = 0; i < viewer->file_count && viewer->fuzzy_filtered_count < MAX_FILES; i++) {
|
|
int score = calculate_fuzzy_score(viewer->fuzzy_search_query, viewer->files[i].filename);
|
|
if (score > 0) {
|
|
viewer->fuzzy_scored_files[viewer->fuzzy_filtered_count].file_index = i;
|
|
viewer->fuzzy_scored_files[viewer->fuzzy_filtered_count].score = score;
|
|
viewer->fuzzy_filtered_count++;
|
|
}
|
|
}
|
|
|
|
// Sort results by score (highest first)
|
|
if (viewer->fuzzy_filtered_count > 1) {
|
|
qsort(viewer->fuzzy_scored_files, viewer->fuzzy_filtered_count,
|
|
sizeof(viewer->fuzzy_scored_files[0]), compare_scored_files);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Enter fuzzy search mode
|
|
*/
|
|
void enter_fuzzy_search_mode(NCursesDiffViewer *viewer) {
|
|
if (!viewer || viewer->current_mode != NCURSES_MODE_FILE_LIST) return;
|
|
|
|
viewer->fuzzy_search_active = 1;
|
|
viewer->fuzzy_search_query[0] = '\0';
|
|
viewer->fuzzy_search_query_len = 0;
|
|
|
|
// Create fuzzy search windows
|
|
int input_height = 3;
|
|
int list_height = viewer->terminal_height - input_height - 4;
|
|
int width = viewer->terminal_width - 4;
|
|
int start_y = (viewer->terminal_height - input_height - list_height) / 2;
|
|
int start_x = 2;
|
|
|
|
viewer->fuzzy_input_win = newwin(input_height, width, start_y, start_x);
|
|
viewer->fuzzy_list_win = newwin(list_height, width, start_y + input_height, start_x);
|
|
|
|
if (viewer->fuzzy_input_win) {
|
|
box(viewer->fuzzy_input_win, 0, 0);
|
|
mvwprintw(viewer->fuzzy_input_win, 0, 2, " Fuzzy File Search ");
|
|
}
|
|
|
|
if (viewer->fuzzy_list_win) {
|
|
box(viewer->fuzzy_list_win, 0, 0);
|
|
}
|
|
|
|
// Initialize with all files
|
|
update_fuzzy_filter(viewer);
|
|
|
|
// Force initial full redraw
|
|
viewer->fuzzy_needs_full_redraw = 1;
|
|
}
|
|
|
|
/**
|
|
* Exit fuzzy search mode
|
|
*/
|
|
void exit_fuzzy_search_mode(NCursesDiffViewer *viewer) {
|
|
if (!viewer) return;
|
|
|
|
viewer->fuzzy_search_active = 0;
|
|
cleanup_fuzzy_search(viewer);
|
|
|
|
// Force redraw of main windows next time
|
|
touchwin(stdscr);
|
|
refresh();
|
|
}
|
|
|
|
/**
|
|
* Render fuzzy search input field only
|
|
*/
|
|
void render_fuzzy_input(NCursesDiffViewer *viewer) {
|
|
if (!viewer->fuzzy_input_win) return;
|
|
|
|
// Clear only the content area, not the border
|
|
for (int y = 1; y < getmaxy(viewer->fuzzy_input_win) - 1; y++) {
|
|
for (int x = 1; x < getmaxx(viewer->fuzzy_input_win) - 1; x++) {
|
|
mvwaddch(viewer->fuzzy_input_win, y, x, ' ');
|
|
}
|
|
}
|
|
|
|
// Show current query
|
|
mvwprintw(viewer->fuzzy_input_win, 1, 2, "> %s", viewer->fuzzy_search_query);
|
|
|
|
// Show cursor
|
|
int cursor_x = 4 + viewer->fuzzy_search_query_len;
|
|
if (cursor_x < getmaxx(viewer->fuzzy_input_win) - 1) {
|
|
mvwaddch(viewer->fuzzy_input_win, 1, cursor_x, '_');
|
|
}
|
|
|
|
wrefresh(viewer->fuzzy_input_win);
|
|
}
|
|
|
|
/**
|
|
* Render fuzzy search file list content only (no borders)
|
|
*/
|
|
void render_fuzzy_list_content(NCursesDiffViewer *viewer) {
|
|
if (!viewer->fuzzy_list_win) return;
|
|
|
|
int list_height = getmaxy(viewer->fuzzy_list_win) - 2;
|
|
int list_width = getmaxx(viewer->fuzzy_list_win) - 4;
|
|
|
|
// Clear only the content area, not the border
|
|
for (int y = 1; y <= list_height; y++) {
|
|
for (int x = 1; x < getmaxx(viewer->fuzzy_list_win) - 1; x++) {
|
|
mvwaddch(viewer->fuzzy_list_win, y, x, ' ');
|
|
}
|
|
}
|
|
|
|
// Show filtered results
|
|
for (int i = 0; i < viewer->fuzzy_filtered_count && i < list_height; i++) {
|
|
int display_index = i + viewer->fuzzy_scroll_offset;
|
|
if (display_index >= viewer->fuzzy_filtered_count) break;
|
|
|
|
int file_index = viewer->fuzzy_scored_files[display_index].file_index;
|
|
char *filename = viewer->files[file_index].filename;
|
|
char status = viewer->files[file_index].status;
|
|
|
|
// Highlight selected item
|
|
if (display_index == viewer->fuzzy_selected_index) {
|
|
wattron(viewer->fuzzy_list_win, A_REVERSE);
|
|
}
|
|
|
|
// Show status character and filename
|
|
mvwprintw(viewer->fuzzy_list_win, i + 1, 2, "%c %-*.*s",
|
|
status, list_width - 3, list_width - 3, filename);
|
|
|
|
if (display_index == viewer->fuzzy_selected_index) {
|
|
wattroff(viewer->fuzzy_list_win, A_REVERSE);
|
|
}
|
|
}
|
|
|
|
// Show result count in top border area
|
|
if (viewer->fuzzy_filtered_count > 0) {
|
|
mvwprintw(viewer->fuzzy_list_win, 0, getmaxx(viewer->fuzzy_list_win) - 15,
|
|
" %d/%d ", viewer->fuzzy_selected_index + 1, viewer->fuzzy_filtered_count);
|
|
} else {
|
|
mvwprintw(viewer->fuzzy_list_win, 0, getmaxx(viewer->fuzzy_list_win) - 10, " 0/0 ");
|
|
}
|
|
|
|
wrefresh(viewer->fuzzy_list_win);
|
|
}
|
|
|
|
/**
|
|
* Create fuzzy search windows with borders (one-time setup)
|
|
*/
|
|
void create_fuzzy_windows_with_borders(NCursesDiffViewer *viewer) {
|
|
if (!viewer->fuzzy_input_win || !viewer->fuzzy_list_win) return;
|
|
|
|
// Draw input window border and title
|
|
wclear(viewer->fuzzy_input_win);
|
|
box(viewer->fuzzy_input_win, 0, 0);
|
|
mvwprintw(viewer->fuzzy_input_win, 0, 2, " Fuzzy File Search ");
|
|
wrefresh(viewer->fuzzy_input_win);
|
|
|
|
// Draw list window border
|
|
wclear(viewer->fuzzy_list_win);
|
|
box(viewer->fuzzy_list_win, 0, 0);
|
|
wrefresh(viewer->fuzzy_list_win);
|
|
}
|
|
|
|
/**
|
|
* Main fuzzy search render function with granular updates
|
|
*/
|
|
void render_fuzzy_search(NCursesDiffViewer *viewer) {
|
|
if (!viewer || !viewer->fuzzy_search_active) return;
|
|
if (!viewer->fuzzy_input_win || !viewer->fuzzy_list_win) return;
|
|
|
|
// Check what specifically needs to be redrawn
|
|
int query_changed = strcmp(viewer->fuzzy_search_query, viewer->fuzzy_last_query) != 0;
|
|
int selection_changed = viewer->fuzzy_selected_index != viewer->fuzzy_last_selected;
|
|
int scroll_changed = viewer->fuzzy_scroll_offset != viewer->fuzzy_last_scroll;
|
|
int results_changed = viewer->fuzzy_filtered_count != viewer->fuzzy_last_filtered_count;
|
|
|
|
// Full redraw needed on first render
|
|
if (viewer->fuzzy_needs_full_redraw) {
|
|
create_fuzzy_windows_with_borders(viewer);
|
|
render_fuzzy_input(viewer);
|
|
render_fuzzy_list_content(viewer);
|
|
viewer->fuzzy_needs_full_redraw = 0;
|
|
}
|
|
// Redraw input field if query changed
|
|
else if (query_changed || viewer->fuzzy_needs_input_redraw) {
|
|
render_fuzzy_input(viewer);
|
|
viewer->fuzzy_needs_input_redraw = 0;
|
|
}
|
|
|
|
// Redraw file list if results, selection, or scroll changed
|
|
if (results_changed || selection_changed || scroll_changed || viewer->fuzzy_needs_list_redraw) {
|
|
render_fuzzy_list_content(viewer);
|
|
viewer->fuzzy_needs_list_redraw = 0;
|
|
}
|
|
|
|
// Update last rendered state
|
|
strcpy(viewer->fuzzy_last_query, viewer->fuzzy_search_query);
|
|
viewer->fuzzy_last_selected = viewer->fuzzy_selected_index;
|
|
viewer->fuzzy_last_scroll = viewer->fuzzy_scroll_offset;
|
|
viewer->fuzzy_last_filtered_count = viewer->fuzzy_filtered_count;
|
|
}
|
|
|
|
/**
|
|
* Handle fuzzy search input
|
|
*/
|
|
int handle_fuzzy_search_input(NCursesDiffViewer *viewer, int key) {
|
|
if (!viewer || !viewer->fuzzy_search_active) return 0;
|
|
|
|
switch (key) {
|
|
case 27: // ESC
|
|
exit_fuzzy_search_mode(viewer);
|
|
return 1;
|
|
|
|
case KEY_ENTER:
|
|
case '\n':
|
|
case '\r':
|
|
if (viewer->fuzzy_filtered_count > 0) {
|
|
select_fuzzy_file(viewer);
|
|
exit_fuzzy_search_mode(viewer);
|
|
}
|
|
return 1;
|
|
|
|
case KEY_UP:
|
|
if (viewer->fuzzy_selected_index > 0) {
|
|
viewer->fuzzy_selected_index--;
|
|
// Adjust scroll if needed
|
|
if (viewer->fuzzy_selected_index < viewer->fuzzy_scroll_offset) {
|
|
viewer->fuzzy_scroll_offset = viewer->fuzzy_selected_index;
|
|
}
|
|
}
|
|
return 1;
|
|
|
|
case KEY_DOWN:
|
|
if (viewer->fuzzy_selected_index < viewer->fuzzy_filtered_count - 1) {
|
|
viewer->fuzzy_selected_index++;
|
|
// Adjust scroll if needed
|
|
int list_height = getmaxy(viewer->fuzzy_list_win) - 2;
|
|
if (viewer->fuzzy_selected_index >= viewer->fuzzy_scroll_offset + list_height) {
|
|
viewer->fuzzy_scroll_offset = viewer->fuzzy_selected_index - list_height + 1;
|
|
}
|
|
}
|
|
return 1;
|
|
|
|
case KEY_BACKSPACE:
|
|
case 127:
|
|
case '\b':
|
|
if (viewer->fuzzy_search_query_len > 0) {
|
|
viewer->fuzzy_search_query_len--;
|
|
viewer->fuzzy_search_query[viewer->fuzzy_search_query_len] = '\0';
|
|
update_fuzzy_filter(viewer);
|
|
}
|
|
return 1;
|
|
|
|
default:
|
|
// Add printable characters to search query
|
|
if (key >= 32 && key <= 126 && viewer->fuzzy_search_query_len < 255) {
|
|
viewer->fuzzy_search_query[viewer->fuzzy_search_query_len++] = key;
|
|
viewer->fuzzy_search_query[viewer->fuzzy_search_query_len] = '\0';
|
|
update_fuzzy_filter(viewer);
|
|
}
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Select file from fuzzy search results
|
|
*/
|
|
void select_fuzzy_file(NCursesDiffViewer *viewer) {
|
|
if (!viewer || viewer->fuzzy_filtered_count == 0) return;
|
|
|
|
// Get the actual file index from scored results
|
|
int file_index = viewer->fuzzy_scored_files[viewer->fuzzy_selected_index].file_index;
|
|
|
|
// Update the main file list selection to point to this file
|
|
viewer->selected_file = file_index;
|
|
|
|
// Scroll the main file list to show the selected file
|
|
int file_panel_height = getmaxy(viewer->file_list_win) - 2;
|
|
if (viewer->selected_file < viewer->file_count) {
|
|
// Ensure the selected file is visible in the main panel
|
|
if (viewer->selected_file < file_panel_height / 2) {
|
|
// If near the top, scroll to top
|
|
// No action needed as the file list starts from 0
|
|
} else if (viewer->selected_file >= viewer->file_count - file_panel_height / 2) {
|
|
// If near the bottom, show as much as possible
|
|
// No action needed as we'll handle this in rendering
|
|
}
|
|
}
|
|
|
|
// Load the selected file with split staging view (same as pressing Enter)
|
|
if (viewer->selected_file >= 0 && viewer->selected_file < viewer->file_count) {
|
|
const char *filename = viewer->files[viewer->selected_file].filename;
|
|
|
|
// Load file with staging information
|
|
load_file_with_staging_info(viewer, filename);
|
|
|
|
// Switch to split view mode
|
|
viewer->split_view_mode = 1;
|
|
viewer->current_mode = NCURSES_MODE_FILE_VIEW;
|
|
viewer->active_pane = 0; // Start with unstaged pane active
|
|
|
|
// Store current file path
|
|
strncpy(viewer->current_file_path, filename, sizeof(viewer->current_file_path) - 1);
|
|
viewer->current_file_path[sizeof(viewer->current_file_path) - 1] = '\0';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Initialize grep search state
|
|
*/
|
|
void init_grep_search(NCursesDiffViewer *viewer) {
|
|
if (!viewer) return;
|
|
|
|
viewer->grep_search_active = 0;
|
|
viewer->grep_search_mode = NCURSES_MODE_FILE_LIST;
|
|
viewer->grep_search_query[0] = '\0';
|
|
viewer->grep_search_query_len = 0;
|
|
viewer->grep_filtered_count = 0;
|
|
viewer->grep_selected_index = 0;
|
|
viewer->grep_scroll_offset = 0;
|
|
viewer->grep_input_win = NULL;
|
|
viewer->grep_list_win = NULL;
|
|
|
|
// Initialize state tracking
|
|
viewer->grep_needs_full_redraw = 0;
|
|
viewer->grep_needs_input_redraw = 0;
|
|
viewer->grep_needs_list_redraw = 0;
|
|
viewer->grep_last_query[0] = '\0';
|
|
viewer->grep_last_selected = -1;
|
|
viewer->grep_last_scroll = -1;
|
|
viewer->grep_last_filtered_count = -1;
|
|
}
|
|
|
|
/**
|
|
* Cleanup grep search windows
|
|
*/
|
|
void cleanup_grep_search(NCursesDiffViewer *viewer) {
|
|
if (!viewer) return;
|
|
|
|
if (viewer->grep_input_win) {
|
|
delwin(viewer->grep_input_win);
|
|
viewer->grep_input_win = NULL;
|
|
}
|
|
if (viewer->grep_list_win) {
|
|
delwin(viewer->grep_list_win);
|
|
viewer->grep_list_win = NULL;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Extract branch name from stash info
|
|
*/
|
|
void extract_branch_from_stash(const char *stash_info, char *branch_name, int max_len) {
|
|
if (!stash_info || !branch_name || max_len <= 0) return;
|
|
|
|
branch_name[0] = '\0';
|
|
|
|
// Look for "On " pattern in stash info
|
|
const char *on_pattern = "On ";
|
|
const char *on_pos = strstr(stash_info, on_pattern);
|
|
|
|
if (on_pos) {
|
|
const char *branch_start = on_pos + strlen(on_pattern);
|
|
const char *branch_end = strchr(branch_start, ':');
|
|
|
|
if (branch_end && branch_end > branch_start) {
|
|
int branch_len = branch_end - branch_start;
|
|
if (branch_len < max_len) {
|
|
strncpy(branch_name, branch_start, branch_len);
|
|
branch_name[branch_len] = '\0';
|
|
|
|
// Trim trailing spaces
|
|
while (branch_len > 0 && branch_name[branch_len - 1] == ' ') {
|
|
branch_name[--branch_len] = '\0';
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Calculate grep match score for text content
|
|
*/
|
|
int calculate_grep_score(const char *pattern, const char *text) {
|
|
if (!pattern || !text) return 0;
|
|
if (strlen(pattern) == 0) return 1000; // Empty pattern matches everything
|
|
|
|
int pattern_len = strlen(pattern);
|
|
int text_len = strlen(text);
|
|
|
|
if (pattern_len > text_len) return 0;
|
|
|
|
int score = 0;
|
|
int pattern_pos = 0;
|
|
int consecutive_matches = 0;
|
|
int first_char_bonus = 0;
|
|
|
|
for (int i = 0; i < text_len && pattern_pos < pattern_len; i++) {
|
|
char p_char = tolower(pattern[pattern_pos]);
|
|
char t_char = tolower(text[i]);
|
|
|
|
if (p_char == t_char) {
|
|
// Base score for character match
|
|
score += 1;
|
|
|
|
// Bonus for consecutive matches
|
|
consecutive_matches++;
|
|
score += consecutive_matches * 3;
|
|
|
|
// Bonus for matching at word boundaries
|
|
if (i == 0 || text[i-1] == ' ' || text[i-1] == '-' || text[i-1] == '_') {
|
|
score += 10;
|
|
}
|
|
|
|
// Bonus for matching first character
|
|
if (pattern_pos == 0 && i == 0) {
|
|
first_char_bonus = 30;
|
|
}
|
|
|
|
// Bonus for exact position match
|
|
if (i == pattern_pos) {
|
|
score += 5;
|
|
}
|
|
|
|
pattern_pos++;
|
|
} else {
|
|
consecutive_matches = 0;
|
|
}
|
|
}
|
|
|
|
// Must match all pattern characters
|
|
if (pattern_pos < pattern_len) return 0;
|
|
|
|
score += first_char_bonus;
|
|
score += (50 - text_len); // Prefer shorter text
|
|
score += (30 - (text_len - pattern_len)); // Fewer unmatched chars
|
|
|
|
return score;
|
|
}
|
|
|
|
/**
|
|
* Compare function for sorting grep scored items
|
|
*/
|
|
int compare_grep_scored_items(const void *a, const void *b) {
|
|
const struct {int item_index; int score;} *item_a = a;
|
|
const struct {int item_index; int score;} *item_b = b;
|
|
return item_b->score - item_a->score; // Descending order
|
|
}
|
|
|
|
/**
|
|
* Update grep search filter based on current query and mode
|
|
*/
|
|
void update_grep_filter(NCursesDiffViewer *viewer) {
|
|
if (!viewer) return;
|
|
|
|
viewer->grep_filtered_count = 0;
|
|
viewer->grep_selected_index = 0;
|
|
viewer->grep_scroll_offset = 0;
|
|
|
|
// Search different data based on current mode
|
|
switch (viewer->grep_search_mode) {
|
|
case NCURSES_MODE_COMMIT_LIST:
|
|
// Search commit titles and author initials
|
|
for (int i = 0; i < viewer->commit_count && viewer->grep_filtered_count < MAX_COMMITS; i++) {
|
|
// Try matching against title first
|
|
int title_score = calculate_grep_score(viewer->grep_search_query, viewer->commits[i].title);
|
|
|
|
// Try matching against author initials
|
|
int author_score = calculate_grep_score(viewer->grep_search_query, viewer->commits[i].author_initials);
|
|
|
|
// Use the higher score (prefer title matches over author matches)
|
|
int score = (title_score > author_score) ? title_score : author_score;
|
|
|
|
if (score > 0) {
|
|
viewer->grep_scored_items[viewer->grep_filtered_count].item_index = i;
|
|
viewer->grep_scored_items[viewer->grep_filtered_count].score = score;
|
|
viewer->grep_filtered_count++;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case NCURSES_MODE_STASH_LIST:
|
|
// Search stash info and branch names
|
|
for (int i = 0; i < viewer->stash_count && viewer->grep_filtered_count < MAX_COMMITS; i++) {
|
|
// Try matching against full stash info first
|
|
int stash_score = calculate_grep_score(viewer->grep_search_query, viewer->stashes[i].stash_info);
|
|
|
|
// Extract and try matching against branch name
|
|
char branch_name[64];
|
|
extract_branch_from_stash(viewer->stashes[i].stash_info, branch_name, sizeof(branch_name));
|
|
int branch_score = 0;
|
|
if (strlen(branch_name) > 0) {
|
|
branch_score = calculate_grep_score(viewer->grep_search_query, branch_name);
|
|
}
|
|
|
|
// Use the higher score (prefer full stash info matches over branch-only matches)
|
|
int score = (stash_score > branch_score) ? stash_score : branch_score;
|
|
|
|
if (score > 0) {
|
|
viewer->grep_scored_items[viewer->grep_filtered_count].item_index = i;
|
|
viewer->grep_scored_items[viewer->grep_filtered_count].score = score;
|
|
viewer->grep_filtered_count++;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case NCURSES_MODE_BRANCH_LIST:
|
|
// Search branch names
|
|
for (int i = 0; i < viewer->branch_count && viewer->grep_filtered_count < MAX_COMMITS; i++) {
|
|
int score = calculate_grep_score(viewer->grep_search_query, viewer->branches[i].name);
|
|
if (score > 0) {
|
|
viewer->grep_scored_items[viewer->grep_filtered_count].item_index = i;
|
|
viewer->grep_scored_items[viewer->grep_filtered_count].score = score;
|
|
viewer->grep_filtered_count++;
|
|
}
|
|
}
|
|
break;
|
|
|
|
default:
|
|
// No grep search for other modes
|
|
break;
|
|
}
|
|
|
|
// Sort results by score
|
|
if (viewer->grep_filtered_count > 1) {
|
|
qsort(viewer->grep_scored_items, viewer->grep_filtered_count,
|
|
sizeof(viewer->grep_scored_items[0]), compare_grep_scored_items);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Enter grep search mode for current view mode
|
|
*/
|
|
void enter_grep_search_mode(NCursesDiffViewer *viewer) {
|
|
if (!viewer) return;
|
|
|
|
// Only allow grep search in specific modes
|
|
if (viewer->current_mode != NCURSES_MODE_COMMIT_LIST &&
|
|
viewer->current_mode != NCURSES_MODE_STASH_LIST &&
|
|
viewer->current_mode != NCURSES_MODE_BRANCH_LIST) {
|
|
return;
|
|
}
|
|
|
|
viewer->grep_search_active = 1;
|
|
viewer->grep_search_mode = viewer->current_mode;
|
|
viewer->grep_search_query[0] = '\0';
|
|
viewer->grep_search_query_len = 0;
|
|
|
|
// Create grep search windows
|
|
int input_height = 3;
|
|
int list_height = viewer->terminal_height - input_height - 4;
|
|
int width = viewer->terminal_width - 4;
|
|
int start_y = (viewer->terminal_height - input_height - list_height) / 2;
|
|
int start_x = 2;
|
|
|
|
viewer->grep_input_win = newwin(input_height, width, start_y, start_x);
|
|
viewer->grep_list_win = newwin(list_height, width, start_y + input_height, start_x);
|
|
|
|
// Initialize with all items
|
|
update_grep_filter(viewer);
|
|
|
|
// Force initial full redraw
|
|
viewer->grep_needs_full_redraw = 1;
|
|
}
|
|
|
|
/**
|
|
* Exit grep search mode
|
|
*/
|
|
void exit_grep_search_mode(NCursesDiffViewer *viewer) {
|
|
if (!viewer) return;
|
|
|
|
viewer->grep_search_active = 0;
|
|
cleanup_grep_search(viewer);
|
|
|
|
// Force redraw of main windows next time
|
|
touchwin(stdscr);
|
|
refresh();
|
|
}
|
|
|
|
/**
|
|
* Render grep search input field only
|
|
*/
|
|
void render_grep_input(NCursesDiffViewer *viewer) {
|
|
if (!viewer->grep_input_win) return;
|
|
|
|
// Clear only the content area, not the border
|
|
for (int y = 1; y < getmaxy(viewer->grep_input_win) - 1; y++) {
|
|
for (int x = 1; x < getmaxx(viewer->grep_input_win) - 1; x++) {
|
|
mvwaddch(viewer->grep_input_win, y, x, ' ');
|
|
}
|
|
}
|
|
|
|
// Show current query
|
|
mvwprintw(viewer->grep_input_win, 1, 2, "> %s", viewer->grep_search_query);
|
|
|
|
// Show cursor
|
|
int cursor_x = 4 + viewer->grep_search_query_len;
|
|
if (cursor_x < getmaxx(viewer->grep_input_win) - 1) {
|
|
mvwaddch(viewer->grep_input_win, 1, cursor_x, '_');
|
|
}
|
|
|
|
wrefresh(viewer->grep_input_win);
|
|
}
|
|
|
|
/**
|
|
* Render grep search list content only (no borders)
|
|
*/
|
|
void render_grep_list_content(NCursesDiffViewer *viewer) {
|
|
if (!viewer->grep_list_win) return;
|
|
|
|
int list_height = getmaxy(viewer->grep_list_win) - 2;
|
|
int list_width = getmaxx(viewer->grep_list_win) - 4;
|
|
|
|
// Clear only the content area, not the border
|
|
for (int y = 1; y <= list_height; y++) {
|
|
for (int x = 1; x < getmaxx(viewer->grep_list_win) - 1; x++) {
|
|
mvwaddch(viewer->grep_list_win, y, x, ' ');
|
|
}
|
|
}
|
|
|
|
// Show filtered results
|
|
for (int i = 0; i < viewer->grep_filtered_count && i < list_height; i++) {
|
|
int display_index = i + viewer->grep_scroll_offset;
|
|
if (display_index >= viewer->grep_filtered_count) break;
|
|
|
|
int item_index = viewer->grep_scored_items[display_index].item_index;
|
|
char display_text[512] = "";
|
|
|
|
// Get display text based on mode
|
|
switch (viewer->grep_search_mode) {
|
|
case NCURSES_MODE_COMMIT_LIST:
|
|
// For commits, we'll handle colors separately
|
|
break;
|
|
case NCURSES_MODE_STASH_LIST:
|
|
// For stashes, we'll handle special formatting to highlight branch
|
|
break;
|
|
case NCURSES_MODE_BRANCH_LIST:
|
|
strncpy(display_text, viewer->branches[item_index].name, sizeof(display_text) - 1);
|
|
break;
|
|
default:
|
|
strcpy(display_text, "Unknown");
|
|
break;
|
|
}
|
|
display_text[sizeof(display_text) - 1] = '\0';
|
|
|
|
// Highlight selected item
|
|
if (display_index == viewer->grep_selected_index) {
|
|
wattron(viewer->grep_list_win, A_REVERSE);
|
|
}
|
|
|
|
// Render based on mode with appropriate colors
|
|
if (viewer->grep_search_mode == NCURSES_MODE_COMMIT_LIST) {
|
|
// Render commit with colors: hash (yellow) + author initials (green) + title
|
|
int x_pos = 2;
|
|
|
|
// Hash in yellow (COLOR_PAIR(10))
|
|
wattron(viewer->grep_list_win, COLOR_PAIR(10));
|
|
mvwprintw(viewer->grep_list_win, i + 1, x_pos, "%s", viewer->commits[item_index].hash);
|
|
wattroff(viewer->grep_list_win, COLOR_PAIR(10));
|
|
x_pos += strlen(viewer->commits[item_index].hash) + 1;
|
|
|
|
// Author initials in green (COLOR_PAIR(8))
|
|
wattron(viewer->grep_list_win, COLOR_PAIR(8));
|
|
mvwprintw(viewer->grep_list_win, i + 1, x_pos, "%s", viewer->commits[item_index].author_initials);
|
|
wattroff(viewer->grep_list_win, COLOR_PAIR(8));
|
|
x_pos += strlen(viewer->commits[item_index].author_initials) + 1;
|
|
|
|
// Title in default color
|
|
int remaining_width = list_width - (x_pos - 2);
|
|
if (remaining_width > 0) {
|
|
mvwprintw(viewer->grep_list_win, i + 1, x_pos, "%-*.*s",
|
|
remaining_width, remaining_width, viewer->commits[item_index].title);
|
|
}
|
|
} else if (viewer->grep_search_mode == NCURSES_MODE_STASH_LIST) {
|
|
// Render stash with branch name highlighted
|
|
const char *stash_info = viewer->stashes[item_index].stash_info;
|
|
char branch_name[64];
|
|
extract_branch_from_stash(stash_info, branch_name, sizeof(branch_name));
|
|
|
|
// Find the "On branch_name:" part to highlight
|
|
char on_pattern[80];
|
|
snprintf(on_pattern, sizeof(on_pattern), "On %s:", branch_name);
|
|
const char *on_pos = strstr(stash_info, on_pattern);
|
|
|
|
if (on_pos && strlen(branch_name) > 0) {
|
|
int x_pos = 2;
|
|
|
|
// Render text before "On"
|
|
int before_len = on_pos - stash_info;
|
|
if (before_len > 0) {
|
|
mvwprintw(viewer->grep_list_win, i + 1, x_pos, "%.*s", before_len, stash_info);
|
|
x_pos += before_len;
|
|
}
|
|
|
|
// Render "On " in default color
|
|
mvwprintw(viewer->grep_list_win, i + 1, x_pos, "On ");
|
|
x_pos += 3;
|
|
|
|
// Render branch name in green (COLOR_PAIR(8))
|
|
wattron(viewer->grep_list_win, COLOR_PAIR(8));
|
|
mvwprintw(viewer->grep_list_win, i + 1, x_pos, "%s", branch_name);
|
|
wattroff(viewer->grep_list_win, COLOR_PAIR(8));
|
|
x_pos += strlen(branch_name);
|
|
|
|
// Render remaining text after branch name
|
|
const char *after_pos = on_pos + strlen(on_pattern);
|
|
int remaining_width = list_width - (x_pos - 2);
|
|
if (remaining_width > 0 && *after_pos) {
|
|
mvwprintw(viewer->grep_list_win, i + 1, x_pos, ":%-*.*s",
|
|
remaining_width - 1, remaining_width - 1, after_pos);
|
|
}
|
|
} else {
|
|
// Fallback: render normally if no branch pattern found
|
|
mvwprintw(viewer->grep_list_win, i + 1, 2, "%-*.*s",
|
|
list_width - 2, list_width - 2, stash_info);
|
|
}
|
|
} else {
|
|
// Show item text for other modes (branches)
|
|
mvwprintw(viewer->grep_list_win, i + 1, 2, "%-*.*s",
|
|
list_width - 2, list_width - 2, display_text);
|
|
}
|
|
|
|
if (display_index == viewer->grep_selected_index) {
|
|
wattroff(viewer->grep_list_win, A_REVERSE);
|
|
}
|
|
}
|
|
|
|
// Show result count
|
|
if (viewer->grep_filtered_count > 0) {
|
|
mvwprintw(viewer->grep_list_win, 0, getmaxx(viewer->grep_list_win) - 15,
|
|
" %d/%d ", viewer->grep_selected_index + 1, viewer->grep_filtered_count);
|
|
} else {
|
|
mvwprintw(viewer->grep_list_win, 0, getmaxx(viewer->grep_list_win) - 10, " 0/0 ");
|
|
}
|
|
|
|
wrefresh(viewer->grep_list_win);
|
|
}
|
|
|
|
/**
|
|
* Create grep search windows with borders (one-time setup)
|
|
*/
|
|
void create_grep_windows_with_borders(NCursesDiffViewer *viewer) {
|
|
if (!viewer->grep_input_win || !viewer->grep_list_win) return;
|
|
|
|
// Get mode name for title
|
|
const char *mode_name = "Search";
|
|
switch (viewer->grep_search_mode) {
|
|
case NCURSES_MODE_COMMIT_LIST:
|
|
mode_name = "Commit Grep";
|
|
break;
|
|
case NCURSES_MODE_STASH_LIST:
|
|
mode_name = "Stash Grep";
|
|
break;
|
|
case NCURSES_MODE_BRANCH_LIST:
|
|
mode_name = "Branch Grep";
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
// Draw input window border and title
|
|
wclear(viewer->grep_input_win);
|
|
box(viewer->grep_input_win, 0, 0);
|
|
mvwprintw(viewer->grep_input_win, 0, 2, " %s ", mode_name);
|
|
wrefresh(viewer->grep_input_win);
|
|
|
|
// Draw list window border
|
|
wclear(viewer->grep_list_win);
|
|
box(viewer->grep_list_win, 0, 0);
|
|
wrefresh(viewer->grep_list_win);
|
|
}
|
|
|
|
/**
|
|
* Main grep search render function with granular updates
|
|
*/
|
|
void render_grep_search(NCursesDiffViewer *viewer) {
|
|
if (!viewer || !viewer->grep_search_active) return;
|
|
if (!viewer->grep_input_win || !viewer->grep_list_win) return;
|
|
|
|
// Check what specifically needs to be redrawn
|
|
int query_changed = strcmp(viewer->grep_search_query, viewer->grep_last_query) != 0;
|
|
int selection_changed = viewer->grep_selected_index != viewer->grep_last_selected;
|
|
int scroll_changed = viewer->grep_scroll_offset != viewer->grep_last_scroll;
|
|
int results_changed = viewer->grep_filtered_count != viewer->grep_last_filtered_count;
|
|
|
|
// Full redraw needed on first render
|
|
if (viewer->grep_needs_full_redraw) {
|
|
create_grep_windows_with_borders(viewer);
|
|
render_grep_input(viewer);
|
|
render_grep_list_content(viewer);
|
|
viewer->grep_needs_full_redraw = 0;
|
|
}
|
|
// Redraw input field if query changed
|
|
else if (query_changed || viewer->grep_needs_input_redraw) {
|
|
render_grep_input(viewer);
|
|
viewer->grep_needs_input_redraw = 0;
|
|
}
|
|
|
|
// Redraw list if results, selection, or scroll changed
|
|
if (results_changed || selection_changed || scroll_changed || viewer->grep_needs_list_redraw) {
|
|
render_grep_list_content(viewer);
|
|
viewer->grep_needs_list_redraw = 0;
|
|
}
|
|
|
|
// Update last rendered state
|
|
strcpy(viewer->grep_last_query, viewer->grep_search_query);
|
|
viewer->grep_last_selected = viewer->grep_selected_index;
|
|
viewer->grep_last_scroll = viewer->grep_scroll_offset;
|
|
viewer->grep_last_filtered_count = viewer->grep_filtered_count;
|
|
}
|
|
|
|
/**
|
|
* Handle grep search input
|
|
*/
|
|
int handle_grep_search_input(NCursesDiffViewer *viewer, int key) {
|
|
if (!viewer || !viewer->grep_search_active) return 0;
|
|
|
|
switch (key) {
|
|
case 27: // ESC
|
|
exit_grep_search_mode(viewer);
|
|
return 1;
|
|
|
|
case KEY_ENTER:
|
|
case '\n':
|
|
case '\r':
|
|
if (viewer->grep_filtered_count > 0) {
|
|
select_grep_item(viewer);
|
|
exit_grep_search_mode(viewer);
|
|
}
|
|
return 1;
|
|
|
|
case KEY_UP:
|
|
if (viewer->grep_selected_index > 0) {
|
|
viewer->grep_selected_index--;
|
|
// Adjust scroll if needed
|
|
if (viewer->grep_selected_index < viewer->grep_scroll_offset) {
|
|
viewer->grep_scroll_offset = viewer->grep_selected_index;
|
|
}
|
|
}
|
|
return 1;
|
|
|
|
case KEY_DOWN:
|
|
if (viewer->grep_selected_index < viewer->grep_filtered_count - 1) {
|
|
viewer->grep_selected_index++;
|
|
// Adjust scroll if needed
|
|
int list_height = getmaxy(viewer->grep_list_win) - 2;
|
|
if (viewer->grep_selected_index >= viewer->grep_scroll_offset + list_height) {
|
|
viewer->grep_scroll_offset = viewer->grep_selected_index - list_height + 1;
|
|
}
|
|
}
|
|
return 1;
|
|
|
|
case KEY_BACKSPACE:
|
|
case 127:
|
|
case '\b':
|
|
if (viewer->grep_search_query_len > 0) {
|
|
viewer->grep_search_query_len--;
|
|
viewer->grep_search_query[viewer->grep_search_query_len] = '\0';
|
|
update_grep_filter(viewer);
|
|
}
|
|
return 1;
|
|
|
|
default:
|
|
// Add printable characters to search query
|
|
if (key >= 32 && key <= 126 && viewer->grep_search_query_len < 255) {
|
|
viewer->grep_search_query[viewer->grep_search_query_len++] = key;
|
|
viewer->grep_search_query[viewer->grep_search_query_len] = '\0';
|
|
update_grep_filter(viewer);
|
|
}
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Select item from grep search results
|
|
*/
|
|
void select_grep_item(NCursesDiffViewer *viewer) {
|
|
if (!viewer || viewer->grep_filtered_count == 0) return;
|
|
|
|
// Get the actual item index from scored results
|
|
int item_index = viewer->grep_scored_items[viewer->grep_selected_index].item_index;
|
|
|
|
// Update the main selection based on mode
|
|
switch (viewer->grep_search_mode) {
|
|
case NCURSES_MODE_COMMIT_LIST:
|
|
viewer->selected_commit = item_index;
|
|
viewer->current_mode = NCURSES_MODE_COMMIT_VIEW;
|
|
// Load the selected commit for viewing
|
|
if (item_index >= 0 && item_index < viewer->commit_count) {
|
|
load_commit_for_viewing(viewer, viewer->commits[item_index].hash);
|
|
}
|
|
break;
|
|
|
|
case NCURSES_MODE_STASH_LIST:
|
|
viewer->selected_stash = item_index;
|
|
viewer->current_mode = NCURSES_MODE_STASH_VIEW;
|
|
// Load the selected stash for viewing
|
|
if (item_index >= 0 && item_index < viewer->stash_count) {
|
|
load_stash_for_viewing(viewer, item_index);
|
|
}
|
|
break;
|
|
|
|
case NCURSES_MODE_BRANCH_LIST:
|
|
viewer->selected_branch = item_index;
|
|
viewer->current_mode = NCURSES_MODE_BRANCH_VIEW;
|
|
// Load commits for the selected branch
|
|
if (item_index >= 0 && item_index < viewer->branch_count) {
|
|
load_branch_commits(viewer, viewer->branches[item_index].name);
|
|
parse_branch_commits_to_lines(viewer);
|
|
}
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
}
|