mirror of
https://github.com/marcusDenslow/FERRUM.git
synced 2026-10-25 16:58:49 +01:00
the suggestion that is currently "hovered" over in the suggestion list will be appended inline to the prompt. also hitting tab when only one match is found will now accept that suggestion instead of opening the menu added gg o flag to open the current repo in github if it is a git repo
1127 lines
34 KiB
C
1127 lines
34 KiB
C
/**
|
|
* line_reader.c
|
|
* Implementation of line reading and parsing with context-aware suggestions
|
|
*/
|
|
|
|
#include "line_reader.h"
|
|
#include "aliases.h"
|
|
#include "bookmarks.h" // Added for bookmark support
|
|
#include "builtins.h" // Added for history access
|
|
#include "common.h"
|
|
#include "git_integration.h"
|
|
#include "persistent_history.h"
|
|
#include "tab_complete.h"
|
|
#include "themes.h"
|
|
#include <termios.h>
|
|
#include <unistd.h>
|
|
#include <sys/stat.h>
|
|
#include <fcntl.h>
|
|
#include <dirent.h>
|
|
#include <limits.h>
|
|
#include <errno.h>
|
|
|
|
#ifndef PATH_MAX
|
|
#define PATH_MAX 4096
|
|
#endif
|
|
|
|
// Suggestion state - make these global
|
|
static int has_suggestion = 0;
|
|
static int suggestion_index = 0;
|
|
static char **suggestions = NULL;
|
|
static int suggestion_count = 0;
|
|
static char full_suggestion[LSH_RL_BUFSIZE] = {0};
|
|
static int prefix_start = 0;
|
|
static int menu_mode = 0;
|
|
static int menu_start_line = 0;
|
|
|
|
// Define colors for suggestions
|
|
#define SUGGESTION_COLOR "\033[2;37m" // Dim white color for suggestions
|
|
#define HIGHLIGHT_COLOR "\033[7;36m" // Highlighted background for selected item
|
|
#define NORMAL_COLOR "\033[0;36m" // Normal color for menu items
|
|
#define RESET_COLOR "\033[0m"
|
|
|
|
/**
|
|
* Check if a command is valid
|
|
*
|
|
* @param cmd The command to check
|
|
* @return 1 if valid, 0 if not
|
|
*/
|
|
int is_valid_command(const char *cmd) {
|
|
if (!cmd || cmd[0] == '\0') {
|
|
return 0; // Empty command is not valid
|
|
}
|
|
|
|
// Extract just the command part (before any spaces)
|
|
char command_part[LSH_RL_BUFSIZE];
|
|
int i = 0;
|
|
while (cmd[i] && !isspace(cmd[i]) && i < LSH_RL_BUFSIZE - 1) {
|
|
command_part[i] = cmd[i];
|
|
i++;
|
|
}
|
|
command_part[i] = '\0';
|
|
|
|
// Check built-in commands
|
|
for (int i = 0; i < lsh_num_builtins(); i++) {
|
|
if (strcasecmp(command_part, builtin_str[i]) == 0) {
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
// Check aliases
|
|
AliasEntry *alias = find_alias(command_part);
|
|
if (alias) {
|
|
return 1;
|
|
}
|
|
|
|
void generate_enhanced_prompt(char *prompt_buffer, size_t buffer_size) {
|
|
// Get current directory information
|
|
char cwd[PATH_MAX];
|
|
char parent_dir[PATH_MAX / 2];
|
|
char current_dir[PATH_MAX / 2];
|
|
|
|
if (getcwd(cwd, sizeof(cwd)) != NULL) {
|
|
get_path_display(cwd, parent_dir, current_dir, PATH_MAX / 2);
|
|
} else {
|
|
strcpy(parent_dir, "unknown");
|
|
strcpy(current_dir, "dir");
|
|
}
|
|
|
|
// Get Git information - modified to extract just the branch name
|
|
char git_display[LSH_RL_BUFSIZE] = {0};
|
|
char *git_status_info = get_git_status();
|
|
if (git_status_info != NULL) {
|
|
// Extract just the branch name from git status info
|
|
char branch_name[LSH_RL_BUFSIZE] = {0};
|
|
char *paren_open = strchr(git_status_info, '(');
|
|
char *paren_close = strchr(git_status_info, ')');
|
|
|
|
if (paren_open && paren_close && paren_close > paren_open) {
|
|
// Extract content between parentheses - should be the branch name
|
|
size_t branch_len = paren_close - paren_open - 1;
|
|
if (branch_len < sizeof(branch_name)) {
|
|
strncpy(branch_name, paren_open + 1, branch_len);
|
|
branch_name[branch_len] = '\0';
|
|
snprintf(git_display, sizeof(git_display), " \033[1;35mgit:(%s)\033[0m", branch_name);
|
|
} else {
|
|
// Fallback if branch name is too long
|
|
snprintf(git_display, sizeof(git_display), " \033[1;35mgit:(?)\033[0m");
|
|
}
|
|
} else {
|
|
// If we can't parse the branch name, just use the whole status
|
|
snprintf(git_display, sizeof(git_display), " \033[1;35mgit:(%s)\033[0m", git_status_info);
|
|
}
|
|
|
|
free(git_status_info);
|
|
}
|
|
|
|
// Format the prompt
|
|
snprintf(prompt_buffer, buffer_size, "\033[1;36m%s/%s\033[0m%s \033[1;31m✗\033[0m ",
|
|
parent_dir, current_dir, git_display);
|
|
}
|
|
|
|
// Check if it's an executable in PATH
|
|
// We'll use a simplified approach - check if the file exists and is executable
|
|
struct stat st;
|
|
char path_buffer[PATH_MAX];
|
|
|
|
// First check if it's an absolute or relative path
|
|
if (strchr(command_part, '/')) {
|
|
// Path contains slashes, check if file exists and is executable
|
|
if (stat(command_part, &st) == 0 && (st.st_mode & S_IXUSR)) {
|
|
return 1;
|
|
}
|
|
} else {
|
|
// Check in current directory
|
|
snprintf(path_buffer, PATH_MAX, "./%s", command_part);
|
|
if (stat(path_buffer, &st) == 0 && (st.st_mode & S_IXUSR)) {
|
|
return 1;
|
|
}
|
|
|
|
// Check in PATH directories
|
|
char *path_env = getenv("PATH");
|
|
if (!path_env) {
|
|
return 0; // No PATH defined
|
|
}
|
|
|
|
char *path_copy = strdup(path_env);
|
|
if (!path_copy) {
|
|
return 0; // Memory allocation error
|
|
}
|
|
|
|
char *token, *rest = path_copy;
|
|
while ((token = strtok_r(rest, ":", &rest))) {
|
|
// Construct full path to check
|
|
snprintf(path_buffer, PATH_MAX, "%s/%s", token, command_part);
|
|
if (stat(path_buffer, &st) == 0 && (st.st_mode & S_IXUSR)) {
|
|
free(path_copy);
|
|
return 1;
|
|
}
|
|
}
|
|
free(path_copy);
|
|
}
|
|
|
|
return 0; // Command not found
|
|
}
|
|
|
|
/**
|
|
* Read a key from the terminal
|
|
*
|
|
* Handles special keys and escape sequences
|
|
*/
|
|
int read_key(void) {
|
|
unsigned char c;
|
|
int nread;
|
|
char seq[6];
|
|
|
|
// Define local constants for special keys
|
|
#define LOCAL_KEY_SHIFT_ENTER 1010
|
|
|
|
// Read a character
|
|
while ((nread = read(STDIN_FILENO, &c, 1)) != 1) {
|
|
if (nread == -1 && errno != EAGAIN)
|
|
return -1;
|
|
}
|
|
|
|
// Handle carriage return (CR) as enter
|
|
if (c == 13) {
|
|
return KEY_ENTER;
|
|
}
|
|
|
|
// Handle escape sequences for arrow keys and other special keys
|
|
if (c == KEY_ESCAPE) {
|
|
// Read up to 5 additional chars
|
|
int i = 0;
|
|
fd_set readfds;
|
|
struct timeval timeout;
|
|
|
|
FD_ZERO(&readfds);
|
|
FD_SET(STDIN_FILENO, &readfds);
|
|
timeout.tv_sec = 0;
|
|
timeout.tv_usec = 50000; // 50ms timeout
|
|
|
|
// Try to read the sequence with timeout to avoid blocking
|
|
while (i < 5) {
|
|
int select_result = select(STDIN_FILENO + 1, &readfds, NULL, NULL, &timeout);
|
|
if (select_result <= 0) break; // Timeout or error
|
|
|
|
if (read(STDIN_FILENO, &seq[i], 1) != 1) break;
|
|
i++;
|
|
|
|
// Check for known sequences
|
|
if (i >= 2 && seq[0] == '[') {
|
|
switch (seq[1]) {
|
|
case 'A': return KEY_UP;
|
|
case 'B': return KEY_DOWN;
|
|
case 'C': return KEY_RIGHT;
|
|
case 'D': return KEY_LEFT;
|
|
}
|
|
}
|
|
|
|
// Check for Shift+Enter sequence (varies by terminal, try common forms)
|
|
if (i >= 3) {
|
|
// xterm/vte: ESC [ 13 ; 2 ~
|
|
if (seq[0] == '[' && seq[1] == '1' && seq[2] == '3' && i >= 5 && seq[3] == ';' && seq[4] == '2') {
|
|
return LOCAL_KEY_SHIFT_ENTER;
|
|
}
|
|
|
|
// Another common sequence for Shift+Enter in some terminals
|
|
if (seq[0] == 'O' && seq[1] == '2' && seq[2] == 'M') {
|
|
return LOCAL_KEY_SHIFT_ENTER;
|
|
}
|
|
}
|
|
}
|
|
|
|
return KEY_ESCAPE;
|
|
}
|
|
|
|
return c;
|
|
}
|
|
|
|
/**
|
|
* Update suggestions based on current input
|
|
*/
|
|
void update_suggestions(const char *buffer, int position) {
|
|
// Free previous suggestions if any
|
|
if (suggestions) {
|
|
for (int i = 0; i < suggestion_count; i++) {
|
|
if (suggestions[i]) free(suggestions[i]);
|
|
}
|
|
free(suggestions);
|
|
suggestions = NULL;
|
|
suggestion_count = 0;
|
|
}
|
|
|
|
has_suggestion = 0;
|
|
|
|
// Parse command line
|
|
prefix_start = 0;
|
|
|
|
// Find the last space to determine where the prefix starts
|
|
char *last_space = strrchr(buffer, ' ');
|
|
if (last_space) {
|
|
// We have a command and partial argument
|
|
prefix_start = (last_space - buffer) + 1;
|
|
|
|
// Skip additional spaces
|
|
while (buffer[prefix_start] == ' ' && buffer[prefix_start] != '\0') {
|
|
prefix_start++;
|
|
}
|
|
} else {
|
|
// Just a command, no arguments
|
|
prefix_start = 0;
|
|
}
|
|
|
|
// Build list of suggestions
|
|
suggestions = NULL;
|
|
suggestion_count = 0;
|
|
|
|
// Get command or file suggestions based on context
|
|
if (!last_space) {
|
|
// Command suggestions
|
|
// First count how many we'll need
|
|
for (int i = 0; i < lsh_num_builtins(); i++) {
|
|
if (strncasecmp(builtin_str[i], buffer, strlen(buffer)) == 0) {
|
|
suggestion_count++;
|
|
}
|
|
}
|
|
|
|
// Add alias count - rough estimate
|
|
int alias_count;
|
|
char **aliases = get_alias_names(&alias_count);
|
|
if (aliases) {
|
|
for (int i = 0; i < alias_count; i++) {
|
|
if (strncasecmp(aliases[i], buffer, strlen(buffer)) == 0) {
|
|
suggestion_count++;
|
|
}
|
|
}
|
|
|
|
// Free alias names, we'll get them again
|
|
for (int i = 0; i < alias_count; i++) {
|
|
free(aliases[i]);
|
|
}
|
|
free(aliases);
|
|
}
|
|
|
|
// Allocate suggestions array
|
|
if (suggestion_count > 0) {
|
|
suggestions = (char**)malloc(suggestion_count * sizeof(char*));
|
|
if (!suggestions) {
|
|
fprintf(stderr, "Memory allocation error\n");
|
|
suggestion_count = 0;
|
|
} else {
|
|
// Initialize all entries to NULL
|
|
for (int i = 0; i < suggestion_count; i++) {
|
|
suggestions[i] = NULL;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Now fill the suggestions array
|
|
int idx = 0;
|
|
|
|
// Add builtin commands
|
|
for (int i = 0; i < lsh_num_builtins() && idx < suggestion_count; i++) {
|
|
if (strncasecmp(builtin_str[i], buffer, strlen(buffer)) == 0) {
|
|
suggestions[idx++] = strdup(builtin_str[i]);
|
|
}
|
|
}
|
|
|
|
// Add aliases
|
|
aliases = get_alias_names(&alias_count);
|
|
if (aliases) {
|
|
for (int i = 0; i < alias_count && idx < suggestion_count; i++) {
|
|
if (strncasecmp(aliases[i], buffer, strlen(buffer)) == 0) {
|
|
suggestions[idx++] = strdup(aliases[i]);
|
|
}
|
|
}
|
|
|
|
// Free alias names
|
|
for (int i = 0; i < alias_count; i++) {
|
|
free(aliases[i]);
|
|
}
|
|
free(aliases);
|
|
}
|
|
|
|
// Update actual suggestion count in case we got fewer than expected
|
|
suggestion_count = idx;
|
|
} else {
|
|
// File/directory suggestions
|
|
char prefix[PATH_MAX] = "";
|
|
char dir_path[PATH_MAX] = ".";
|
|
char name_prefix[PATH_MAX] = "";
|
|
|
|
if (buffer[prefix_start] != '\0') {
|
|
strncpy(prefix, &buffer[prefix_start], sizeof(prefix) - 1);
|
|
prefix[sizeof(prefix) - 1] = '\0';
|
|
|
|
// Split path into directory part and name prefix part
|
|
char *last_slash = strrchr(prefix, '/');
|
|
if (last_slash) {
|
|
// Path contains a directory part
|
|
int dir_len = last_slash - prefix;
|
|
strncpy(dir_path, prefix, dir_len);
|
|
dir_path[dir_len] = '\0';
|
|
|
|
// Handle absolute path vs. relative path with subdirectories
|
|
if (dir_len == 0) {
|
|
// Handle case where path starts with '/'
|
|
strcpy(dir_path, "/");
|
|
}
|
|
|
|
// Extract the name prefix part (after the last slash)
|
|
strncpy(name_prefix, last_slash + 1, sizeof(name_prefix) - 1);
|
|
name_prefix[sizeof(name_prefix) - 1] = '\0';
|
|
} else {
|
|
// No directory part, just a name prefix
|
|
strncpy(name_prefix, prefix, sizeof(name_prefix) - 1);
|
|
name_prefix[sizeof(name_prefix) - 1] = '\0';
|
|
}
|
|
}
|
|
|
|
// Count matching files and directories
|
|
DIR *dir = opendir(dir_path);
|
|
if (dir) {
|
|
struct dirent *entry;
|
|
|
|
// First count the number of matching entries
|
|
while ((entry = readdir(dir)) != NULL) {
|
|
// Skip "." and ".." if there's a name prefix
|
|
if (name_prefix[0] != '\0' &&
|
|
(strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0)) {
|
|
continue;
|
|
}
|
|
|
|
if (strncasecmp(entry->d_name, name_prefix, strlen(name_prefix)) == 0) {
|
|
suggestion_count++;
|
|
}
|
|
}
|
|
|
|
rewinddir(dir);
|
|
|
|
// Allocate suggestions array
|
|
if (suggestion_count > 0) {
|
|
suggestions = (char**)malloc(suggestion_count * sizeof(char*));
|
|
if (!suggestions) {
|
|
fprintf(stderr, "Memory allocation error\n");
|
|
suggestion_count = 0;
|
|
} else {
|
|
// Initialize all entries to NULL
|
|
for (int i = 0; i < suggestion_count; i++) {
|
|
suggestions[i] = NULL;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fill the suggestions array
|
|
int idx = 0;
|
|
while ((entry = readdir(dir)) != NULL && idx < suggestion_count) {
|
|
// Skip "." and ".." if there's a name prefix
|
|
if (name_prefix[0] != '\0' &&
|
|
(strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0)) {
|
|
continue;
|
|
}
|
|
|
|
if (strncasecmp(entry->d_name, name_prefix, strlen(name_prefix)) == 0) {
|
|
// Create the full path suggestion
|
|
char full_path[PATH_MAX * 2];
|
|
|
|
if (strcmp(dir_path, ".") == 0) {
|
|
strncpy(full_path, entry->d_name, sizeof(full_path) - 1);
|
|
} else if (dir_path[0] == '/' && dir_path[1] == '\0') {
|
|
snprintf(full_path, sizeof(full_path) - 1, "/%s", entry->d_name);
|
|
} else {
|
|
snprintf(full_path, sizeof(full_path) - 1, "%s/%s", dir_path, entry->d_name);
|
|
}
|
|
|
|
// Check if it's a directory and add trailing slash if needed
|
|
struct stat st;
|
|
char check_path[PATH_MAX * 2];
|
|
|
|
if (dir_path[0] == '/') {
|
|
// Absolute path
|
|
snprintf(check_path, sizeof(check_path) - 1, "%s/%s", dir_path, entry->d_name);
|
|
} else {
|
|
// Relative path
|
|
snprintf(check_path, sizeof(check_path) - 1, "%s/%s", dir_path, entry->d_name);
|
|
}
|
|
|
|
if (stat(check_path, &st) == 0 && S_ISDIR(st.st_mode)) {
|
|
strncat(full_path, "/", sizeof(full_path) - strlen(full_path) - 1);
|
|
}
|
|
|
|
suggestions[idx++] = strdup(full_path);
|
|
}
|
|
}
|
|
|
|
// Update actual suggestion count
|
|
suggestion_count = idx;
|
|
|
|
closedir(dir);
|
|
}
|
|
}
|
|
|
|
// If we have suggestions, use the first one
|
|
if (suggestion_count > 0) {
|
|
// Skip showing "./" suggestions as they're often not useful
|
|
if (strcmp(suggestions[0], "./") == 0) {
|
|
has_suggestion = 0;
|
|
} else {
|
|
has_suggestion = 1;
|
|
suggestion_index = 0;
|
|
|
|
// Create the full suggestion string that would be accepted
|
|
if (prefix_start > 0) {
|
|
// We're completing an argument
|
|
strncpy(full_suggestion, buffer, prefix_start);
|
|
full_suggestion[prefix_start] = '\0';
|
|
strncat(full_suggestion, suggestions[suggestion_index],
|
|
sizeof(full_suggestion) - strlen(full_suggestion) - 1);
|
|
} else {
|
|
// We're completing a command
|
|
strncpy(full_suggestion, suggestions[suggestion_index], sizeof(full_suggestion) - 1);
|
|
}
|
|
full_suggestion[sizeof(full_suggestion) - 1] = '\0';
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Display a single inline suggestion (when not in menu mode)
|
|
*/
|
|
void display_inline_suggestion(const char *prompt_buffer, const char *buffer, int position) {
|
|
if (has_suggestion && suggestion_count > 0) {
|
|
// Determine what part should be in normal text and what part in suggestion color
|
|
char current_text[LSH_RL_BUFSIZE] = "";
|
|
char suggestion_text[LSH_RL_BUFSIZE] = "";
|
|
|
|
strncpy(current_text, buffer, position);
|
|
current_text[position] = '\0';
|
|
|
|
// Extract just the suggestion part (after what user typed)
|
|
int current_len = 0;
|
|
|
|
if (prefix_start > 0) {
|
|
char current_arg[LSH_RL_BUFSIZE] = "";
|
|
strncpy(current_arg, &buffer[prefix_start], position - prefix_start);
|
|
current_arg[position - prefix_start] = '\0';
|
|
current_len = strlen(current_arg);
|
|
|
|
if (current_len > 0) {
|
|
// Find where the current argument ends in the suggestion
|
|
char *suggestion_part = suggestions[suggestion_index];
|
|
if (strncasecmp(suggestion_part, current_arg, current_len) == 0) {
|
|
strncpy(suggestion_text, suggestion_part + current_len,
|
|
sizeof(suggestion_text) - 1);
|
|
}
|
|
} else {
|
|
// Use the entire suggestion
|
|
strncpy(suggestion_text, suggestions[suggestion_index],
|
|
sizeof(suggestion_text) - 1);
|
|
}
|
|
} else {
|
|
// Completing a command
|
|
if (strlen(buffer) <= strlen(suggestions[suggestion_index]) &&
|
|
strncasecmp(suggestions[suggestion_index], buffer, strlen(buffer)) == 0) {
|
|
strncpy(suggestion_text,
|
|
suggestions[suggestion_index] + strlen(buffer),
|
|
sizeof(suggestion_text) - 1);
|
|
}
|
|
}
|
|
|
|
// If there's no suggestion text (exact match), then don't show suggestion
|
|
if (strlen(suggestion_text) == 0) {
|
|
// Clear the current line
|
|
printf("\r\033[K");
|
|
|
|
// Display prompt and current text without suggestion
|
|
printf("%s%s", prompt_buffer, buffer);
|
|
fflush(stdout);
|
|
} else {
|
|
// Clear the current line
|
|
printf("\r\033[K");
|
|
|
|
// Display prompt and current text
|
|
printf("%s%s", prompt_buffer, current_text);
|
|
|
|
// Display the suggestion part in dim color
|
|
printf("%s%s%s", SUGGESTION_COLOR, suggestion_text, RESET_COLOR);
|
|
|
|
// Move cursor back to end of actual input
|
|
int suggestion_len = strlen(suggestion_text);
|
|
for (int i = 0; i < suggestion_len; i++) {
|
|
printf("\b");
|
|
}
|
|
|
|
fflush(stdout);
|
|
}
|
|
} else {
|
|
// No suggestions, just redraw the current line
|
|
printf("\r\033[K%s%s", prompt_buffer, buffer);
|
|
fflush(stdout);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Function to clear the menu
|
|
*/
|
|
void clear_menu() {
|
|
if (menu_start_line > 0) {
|
|
// Save current cursor position
|
|
printf("\033[s");
|
|
|
|
// Move cursor to where the menu starts (one line below input)
|
|
printf("\033[1B\r");
|
|
|
|
// Clear each line of the menu
|
|
for (int i = 0; i < menu_start_line; i++) {
|
|
printf("\033[K"); // Clear entire line
|
|
if (i < menu_start_line - 1) {
|
|
printf("\033[1B\r"); // Move down to next line if not the last
|
|
}
|
|
}
|
|
|
|
// Restore cursor position (back to input line)
|
|
printf("\033[u");
|
|
fflush(stdout);
|
|
|
|
menu_start_line = 0;
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Function to display menu of suggestions
|
|
*/
|
|
|
|
void display_menu(const char *prompt_buffer, const char *buffer, int position) {
|
|
if (!has_suggestion || suggestion_count == 0) {
|
|
// No suggestions to show
|
|
return;
|
|
}
|
|
|
|
// First, clear any existing menu
|
|
clear_menu();
|
|
|
|
// Save current cursor position (should be at end of input)
|
|
printf("\033[s");
|
|
|
|
// Calculate how many items to show
|
|
int show_count = suggestion_count;
|
|
if (show_count > 10) show_count = 10; // Max 10 items in menu
|
|
|
|
// Move to the beginning of the next line (below input)
|
|
printf("\n\r");
|
|
|
|
// Display each suggestion on a line
|
|
for (int i = 0; i < show_count; i++) {
|
|
if (i > 0) {
|
|
printf("\n\r"); // New line for each suggestion after the first
|
|
}
|
|
|
|
if (i == suggestion_index) {
|
|
// Highlight the selected item
|
|
printf("%s%s%s", HIGHLIGHT_COLOR, suggestions[i], RESET_COLOR);
|
|
} else {
|
|
// Normal color for non-selected items
|
|
printf("%s%s%s", NORMAL_COLOR, suggestions[i], RESET_COLOR);
|
|
}
|
|
}
|
|
|
|
// Store number of menu lines displayed
|
|
menu_start_line = show_count;
|
|
|
|
// Restore cursor to original position (back to input line)
|
|
printf("\033[u");
|
|
fflush(stdout);
|
|
}
|
|
|
|
|
|
/**
|
|
* Function to refresh the display - based on menu mode
|
|
*/
|
|
|
|
void refresh_display(const char *prompt_buffer, const char *buffer, int position) {
|
|
// Always clear any existing menu first
|
|
clear_menu();
|
|
|
|
// Always show inline suggestion - even in menu mode
|
|
display_inline_suggestion(prompt_buffer, buffer, position);
|
|
|
|
if (menu_mode) {
|
|
// Also show menu of options below the input line
|
|
display_menu(prompt_buffer, buffer, position);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Read a line of input from the user
|
|
*
|
|
* @return The line read from stdin
|
|
*/
|
|
char *lsh_read_line(void) {
|
|
int bufsize = LSH_RL_BUFSIZE;
|
|
int position = 0;
|
|
char *buffer = malloc(sizeof(char) * bufsize);
|
|
int c;
|
|
int history_position = -1;
|
|
|
|
// Prompt buffer for enhanced prompt
|
|
char prompt_buffer[LSH_RL_BUFSIZE];
|
|
|
|
// Reset menu mode
|
|
menu_mode = 0;
|
|
menu_start_line = 0;
|
|
|
|
if (!buffer) {
|
|
fprintf(stderr, "lsh: allocation error\n");
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
|
|
// Clear the buffer
|
|
buffer[0] = '\0';
|
|
|
|
// Generate enhanced prompt - INLINED CODE
|
|
{
|
|
// Get current directory information
|
|
char cwd[PATH_MAX];
|
|
char parent_dir[PATH_MAX / 2];
|
|
char current_dir[PATH_MAX / 2];
|
|
|
|
if (getcwd(cwd, sizeof(cwd)) != NULL) {
|
|
get_path_display(cwd, parent_dir, current_dir, PATH_MAX / 2);
|
|
} else {
|
|
strcpy(parent_dir, "unknown");
|
|
strcpy(current_dir, "dir");
|
|
}
|
|
|
|
// Get Git information
|
|
char git_display[LSH_RL_BUFSIZE] = {0};
|
|
char *git_status_info = get_git_status();
|
|
if (git_status_info != NULL) {
|
|
// Extract just the branch name from git status info
|
|
char branch_name[LSH_RL_BUFSIZE] = {0};
|
|
char *paren_open = strchr(git_status_info, '(');
|
|
char *paren_close = strchr(git_status_info, ')');
|
|
|
|
if (paren_open && paren_close && paren_close > paren_open) {
|
|
// Extract content between parentheses - should be the branch name
|
|
size_t branch_len = paren_close - paren_open - 1;
|
|
if (branch_len < sizeof(branch_name)) {
|
|
strncpy(branch_name, paren_open + 1, branch_len);
|
|
branch_name[branch_len] = '\0';
|
|
snprintf(git_display, sizeof(git_display), " \033[1;35mgit:(%s)\033[0m", branch_name);
|
|
} else {
|
|
// Fallback if branch name is too long
|
|
snprintf(git_display, sizeof(git_display), " \033[1;35mgit:(?)\033[0m");
|
|
}
|
|
} else {
|
|
// If we can't parse the branch name, just use the whole status
|
|
snprintf(git_display, sizeof(git_display), " \033[1;35mgit:(%s)\033[0m", git_status_info);
|
|
}
|
|
|
|
free(git_status_info);
|
|
}
|
|
|
|
// Format the prompt
|
|
snprintf(prompt_buffer, sizeof(prompt_buffer), "\033[1;36m%s/%s\033[0m%s \033[1;31m✗\033[0m ",
|
|
parent_dir, current_dir, git_display);
|
|
}
|
|
|
|
// Display prompt
|
|
printf("%s", prompt_buffer);
|
|
fflush(stdout);
|
|
|
|
// Initialize suggestions
|
|
update_suggestions(buffer, position);
|
|
|
|
while (1) {
|
|
c = read_key();
|
|
|
|
if (c == KEY_ENTER || c == '\n' || c == '\r') {
|
|
if (menu_mode) {
|
|
// In menu mode: accept the highlighted suggestion without executing
|
|
if (has_suggestion && suggestion_count > 0) {
|
|
// Update buffer with selected suggestion
|
|
if (prefix_start > 0) {
|
|
// We're completing an argument
|
|
char temp[LSH_RL_BUFSIZE];
|
|
strncpy(temp, buffer, prefix_start);
|
|
temp[prefix_start] = '\0';
|
|
strncat(temp, suggestions[suggestion_index],
|
|
LSH_RL_BUFSIZE - strlen(temp) - 1);
|
|
|
|
strncpy(buffer, temp, bufsize - 1);
|
|
} else {
|
|
// We're completing a command
|
|
strncpy(buffer, suggestions[suggestion_index], bufsize - 1);
|
|
}
|
|
buffer[bufsize - 1] = '\0';
|
|
position = strlen(buffer);
|
|
|
|
// Clear the menu
|
|
clear_menu();
|
|
menu_mode = 0;
|
|
|
|
// Redraw the line with accepted suggestion
|
|
printf("\r\033[K%s%s", prompt_buffer, buffer);
|
|
fflush(stdout);
|
|
|
|
// Don't immediately show a new suggestion after accepting one
|
|
// The user likely wants to execute the command first
|
|
// update_suggestions(buffer, position);
|
|
// display_inline_suggestion(prompt_buffer, buffer, position);
|
|
}
|
|
} else {
|
|
// Not in menu mode: execute the command as is
|
|
buffer[position] = '\0';
|
|
printf("\n");
|
|
fflush(stdout);
|
|
break;
|
|
}
|
|
} else if (c == KEY_ESCAPE) {
|
|
if (menu_mode) {
|
|
// Exit menu mode
|
|
menu_mode = 0;
|
|
clear_menu();
|
|
display_inline_suggestion(prompt_buffer, buffer, position);
|
|
}
|
|
} else if (c == KEY_BACKSPACE || c == 127) {
|
|
// Handle backspace
|
|
if (position > 0) {
|
|
position--;
|
|
buffer[position] = '\0';
|
|
|
|
// Exit menu mode when backspacing
|
|
if (menu_mode) {
|
|
menu_mode = 0;
|
|
clear_menu();
|
|
}
|
|
|
|
// Update suggestions after backspace
|
|
update_suggestions(buffer, position);
|
|
display_inline_suggestion(prompt_buffer, buffer, position);
|
|
}
|
|
} else if (c == KEY_TAB) {
|
|
if (menu_mode) {
|
|
// Already in menu mode: cycle to next suggestion
|
|
if (suggestion_count > 0) {
|
|
suggestion_index = (suggestion_index + 1) % suggestion_count;
|
|
|
|
// Update full_suggestion with the newly selected suggestion
|
|
if (prefix_start > 0) {
|
|
// We're completing an argument
|
|
strncpy(full_suggestion, buffer, prefix_start);
|
|
full_suggestion[prefix_start] = '\0';
|
|
strncat(full_suggestion, suggestions[suggestion_index],
|
|
sizeof(full_suggestion) - strlen(full_suggestion) - 1);
|
|
} else {
|
|
// We're completing a command
|
|
strncpy(full_suggestion, suggestions[suggestion_index], sizeof(full_suggestion) - 1);
|
|
}
|
|
full_suggestion[sizeof(full_suggestion) - 1] = '\0';
|
|
|
|
refresh_display(prompt_buffer, buffer, position);
|
|
}
|
|
} else {
|
|
// If there's only one suggestion, accept it directly
|
|
if (has_suggestion && suggestion_count == 1) {
|
|
// Update buffer with the single suggestion
|
|
if (prefix_start > 0) {
|
|
// We're completing an argument
|
|
char temp[LSH_RL_BUFSIZE];
|
|
strncpy(temp, buffer, prefix_start);
|
|
temp[prefix_start] = '\0';
|
|
strncat(temp, suggestions[suggestion_index],
|
|
LSH_RL_BUFSIZE - strlen(temp) - 1);
|
|
|
|
strncpy(buffer, temp, bufsize - 1);
|
|
} else {
|
|
// We're completing a command
|
|
strncpy(buffer, suggestions[suggestion_index], bufsize - 1);
|
|
}
|
|
buffer[bufsize - 1] = '\0';
|
|
position = strlen(buffer);
|
|
|
|
// Redraw the line with accepted suggestion
|
|
printf("\r\033[K%s%s", prompt_buffer, buffer);
|
|
fflush(stdout);
|
|
}
|
|
// Otherwise enter menu mode if we have multiple suggestions
|
|
else if (has_suggestion && suggestion_count > 1) {
|
|
menu_mode = 1;
|
|
suggestion_index = 0; // Start with first suggestion
|
|
refresh_display(prompt_buffer, buffer, position);
|
|
}
|
|
}
|
|
} else if (c == KEY_UP && menu_mode) {
|
|
// In menu mode, up arrow goes to previous suggestion
|
|
if (suggestion_count > 0) {
|
|
suggestion_index = (suggestion_index + suggestion_count - 1) % suggestion_count;
|
|
|
|
// Update full_suggestion with the newly selected suggestion
|
|
if (prefix_start > 0) {
|
|
// We're completing an argument
|
|
strncpy(full_suggestion, buffer, prefix_start);
|
|
full_suggestion[prefix_start] = '\0';
|
|
strncat(full_suggestion, suggestions[suggestion_index],
|
|
sizeof(full_suggestion) - strlen(full_suggestion) - 1);
|
|
} else {
|
|
// We're completing a command
|
|
strncpy(full_suggestion, suggestions[suggestion_index], sizeof(full_suggestion) - 1);
|
|
}
|
|
full_suggestion[sizeof(full_suggestion) - 1] = '\0';
|
|
|
|
refresh_display(prompt_buffer, buffer, position);
|
|
}
|
|
} else if (c == KEY_DOWN && menu_mode) {
|
|
// In menu mode, down arrow goes to next suggestion
|
|
if (suggestion_count > 0) {
|
|
suggestion_index = (suggestion_index + 1) % suggestion_count;
|
|
|
|
// Update full_suggestion with the newly selected suggestion
|
|
if (prefix_start > 0) {
|
|
// We're completing an argument
|
|
strncpy(full_suggestion, buffer, prefix_start);
|
|
full_suggestion[prefix_start] = '\0';
|
|
strncat(full_suggestion, suggestions[suggestion_index],
|
|
sizeof(full_suggestion) - strlen(full_suggestion) - 1);
|
|
} else {
|
|
// We're completing a command
|
|
strncpy(full_suggestion, suggestions[suggestion_index], sizeof(full_suggestion) - 1);
|
|
}
|
|
full_suggestion[sizeof(full_suggestion) - 1] = '\0';
|
|
|
|
refresh_display(prompt_buffer, buffer, position);
|
|
}
|
|
} else if (c == KEY_UP && !menu_mode) {
|
|
// Navigate history upward when not in menu mode
|
|
char *history_entry = get_previous_history_entry(&history_position);
|
|
if (history_entry) {
|
|
// Clear current line
|
|
printf("\r\033[K");
|
|
|
|
// Copy history entry to buffer
|
|
strncpy(buffer, history_entry, bufsize - 1);
|
|
buffer[bufsize - 1] = '\0';
|
|
position = strlen(buffer);
|
|
|
|
// Display the history entry and update suggestions
|
|
printf("%s%s", prompt_buffer, buffer);
|
|
fflush(stdout);
|
|
|
|
// Update suggestions after loading history
|
|
update_suggestions(buffer, position);
|
|
display_inline_suggestion(prompt_buffer, buffer, position);
|
|
}
|
|
} else if (c == KEY_DOWN && !menu_mode) {
|
|
// Navigate history downward when not in menu mode
|
|
char *history_entry = get_next_history_entry(&history_position);
|
|
if (history_entry) {
|
|
// Clear current line
|
|
printf("\r\033[K");
|
|
|
|
// Copy history entry to buffer
|
|
strncpy(buffer, history_entry, bufsize - 1);
|
|
buffer[bufsize - 1] = '\0';
|
|
position = strlen(buffer);
|
|
|
|
// Display the history entry and update suggestions
|
|
printf("%s%s", prompt_buffer, buffer);
|
|
fflush(stdout);
|
|
|
|
// Update suggestions after loading history
|
|
update_suggestions(buffer, position);
|
|
display_inline_suggestion(prompt_buffer, buffer, position);
|
|
} else {
|
|
// At the end of history, clear the line
|
|
printf("\r\033[K%s", prompt_buffer);
|
|
buffer[0] = '\0';
|
|
position = 0;
|
|
|
|
// Clear suggestions since we have an empty line
|
|
if (suggestions) {
|
|
for (int i = 0; i < suggestion_count; i++) {
|
|
if (suggestions[i]) free(suggestions[i]);
|
|
}
|
|
free(suggestions);
|
|
suggestions = NULL;
|
|
suggestion_count = 0;
|
|
}
|
|
has_suggestion = 0;
|
|
}
|
|
} else if (isprint(c)) {
|
|
// Regular character - add it to the buffer
|
|
if (position >= bufsize - 1) {
|
|
bufsize += LSH_RL_BUFSIZE;
|
|
char *new_buffer = realloc(buffer, bufsize);
|
|
if (!new_buffer) {
|
|
fprintf(stderr, "lsh: allocation error\n");
|
|
free(buffer);
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
buffer = new_buffer;
|
|
}
|
|
|
|
buffer[position] = c;
|
|
position++;
|
|
buffer[position] = '\0';
|
|
|
|
// Exit menu mode when typing
|
|
if (menu_mode) {
|
|
menu_mode = 0;
|
|
clear_menu();
|
|
}
|
|
|
|
// Update with the new character and show suggestions
|
|
update_suggestions(buffer, position);
|
|
display_inline_suggestion(prompt_buffer, buffer, position);
|
|
}
|
|
}
|
|
|
|
// Clean up suggestions
|
|
if (suggestions) {
|
|
for (int i = 0; i < suggestion_count; i++) {
|
|
if (suggestions[i]) free(suggestions[i]);
|
|
}
|
|
free(suggestions);
|
|
suggestions = NULL;
|
|
suggestion_count = 0;
|
|
}
|
|
has_suggestion = 0;
|
|
|
|
return buffer;
|
|
}
|
|
|
|
/**
|
|
* Split a line into tokens
|
|
*
|
|
* @param line The line to split
|
|
* @return NULL-terminated array of tokens
|
|
*/
|
|
char **lsh_split_line(char *line) {
|
|
int bufsize = LSH_TOK_BUFSIZE;
|
|
int position = 0;
|
|
char **tokens = malloc(bufsize * sizeof(char *));
|
|
char *token;
|
|
char *rest = line;
|
|
|
|
if (!tokens) {
|
|
fprintf(stderr, "lsh: allocation error\n");
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
|
|
// Handle quotes and ensure we don't split inside quoted strings
|
|
while ((token = parse_token(&rest)) != NULL) {
|
|
tokens[position] = token;
|
|
position++;
|
|
|
|
if (position >= bufsize) {
|
|
bufsize += LSH_TOK_BUFSIZE;
|
|
tokens = realloc(tokens, bufsize * sizeof(char *));
|
|
if (!tokens) {
|
|
fprintf(stderr, "lsh: allocation error\n");
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
}
|
|
}
|
|
|
|
tokens[position] = NULL;
|
|
return tokens;
|
|
}
|
|
|
|
/**
|
|
* Parse a token from a string, handling quoted strings
|
|
*
|
|
* @param str_ptr Pointer to the string to parse, will be updated
|
|
* @return The parsed token, or NULL if no more tokens
|
|
*/
|
|
char *parse_token(char **str_ptr) {
|
|
char *str = *str_ptr;
|
|
char *token_start;
|
|
|
|
// Skip leading whitespace
|
|
while (*str && isspace(*str)) {
|
|
str++;
|
|
}
|
|
|
|
if (*str == '\0') {
|
|
*str_ptr = str;
|
|
return NULL; // No more tokens
|
|
}
|
|
|
|
token_start = str;
|
|
|
|
if (*str == '"' || *str == '\'') {
|
|
// Handle quoted string
|
|
char quote = *str;
|
|
str++; // Skip the opening quote
|
|
token_start = str; // Token starts after the quote
|
|
|
|
// Find the closing quote
|
|
while (*str && *str != quote) {
|
|
str++;
|
|
}
|
|
|
|
if (*str == quote) {
|
|
// Found closing quote
|
|
*str = '\0'; // Terminate the token
|
|
str++; // Move past the closing quote
|
|
}
|
|
} else {
|
|
// Regular token (not quoted)
|
|
while (*str && !isspace(*str)) {
|
|
str++;
|
|
}
|
|
|
|
if (*str) {
|
|
*str = '\0'; // Terminate the token
|
|
str++; // Move past the terminator
|
|
}
|
|
}
|
|
|
|
*str_ptr = str;
|
|
return strdup(token_start);
|
|
}
|
|
|
|
/**
|
|
* Split a line with pipes into separate command arrays
|
|
*
|
|
* @param line The line to split
|
|
* @return NULL-terminated array of NULL-terminated token arrays
|
|
*/
|
|
char ***lsh_split_piped_line(char *line) {
|
|
int cmd_bufsize = LSH_TOK_BUFSIZE;
|
|
int cmd_position = 0;
|
|
char ***commands = malloc(cmd_bufsize * sizeof(char **));
|
|
char *cmd_str, *save_ptr;
|
|
|
|
if (!commands) {
|
|
fprintf(stderr, "lsh: allocation error\n");
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
|
|
// First split by pipe character
|
|
cmd_str = strtok_r(line, "|", &save_ptr);
|
|
|
|
while (cmd_str != NULL) {
|
|
// Allocate space for this command
|
|
commands[cmd_position] = lsh_split_line(cmd_str);
|
|
cmd_position++;
|
|
|
|
// Check if we need more space for commands
|
|
if (cmd_position >= cmd_bufsize) {
|
|
cmd_bufsize += LSH_TOK_BUFSIZE;
|
|
commands = realloc(commands, cmd_bufsize * sizeof(char **));
|
|
if (!commands) {
|
|
fprintf(stderr, "lsh: allocation error\n");
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
}
|
|
|
|
cmd_str = strtok_r(NULL, "|", &save_ptr);
|
|
}
|
|
|
|
commands[cmd_position] = NULL;
|
|
return commands;
|
|
}
|