commit 08fb0af5028b902177b1feb940885d2adc802b64 Author: marcusDenslow Date: Mon Apr 28 12:35:52 2025 +0200 first commit diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..9e7e92d --- /dev/null +++ b/Makefile @@ -0,0 +1,22 @@ +CC = gcc +CFLAGS = -Wall -Wextra -g +LIBS = -lm + +# Get all .c files +SOURCES = $(wildcard *.c) +# Generate object file names from source file names +OBJECTS = $(SOURCES:.c=.o) +TARGET = shell + +all: $(TARGET) + +$(TARGET): $(OBJECTS) + $(CC) $(CFLAGS) -o $@ $^ $(LIBS) + +%.o: %.c + $(CC) $(CFLAGS) -c $< -o $@ + +clean: + rm -f $(OBJECTS) $(TARGET) + +.PHONY: all clean \ No newline at end of file diff --git a/aliases.c b/aliases.c new file mode 100644 index 0000000..2df9a76 --- /dev/null +++ b/aliases.c @@ -0,0 +1,488 @@ +/** + * aliases.c + * Implementation of shell alias functionality + */ + +#include "aliases.h" +#include "builtins.h" +#include +#include + +// Global variables for alias storage +AliasEntry *aliases = NULL; +int alias_count = 0; +int alias_capacity = 0; + +// Path to the aliases file +char aliases_file_path[PATH_MAX]; + +/** + * Initialize the alias system + */ +void init_aliases(void) { + // Set initial capacity + alias_capacity = 10; + aliases = (AliasEntry*)malloc(alias_capacity * sizeof(AliasEntry)); + + if (!aliases) { + fprintf(stderr, "lsh: allocation error in init_aliases\n"); + return; + } + + // Determine aliases file location - in user's home directory + char *home_dir = getenv("HOME"); + if (home_dir) { + snprintf(aliases_file_path, PATH_MAX, "%s/.lsh_aliases", home_dir); + } else { + // Fallback to current directory if HOME not available + strcpy(aliases_file_path, ".lsh_aliases"); + } + + // Load aliases from file + load_aliases(); +} + +/** + * Clean up the alias system + */ +void cleanup_aliases(void) { + if (!aliases) return; + + // Free all alias entries + for (int i = 0; i < alias_count; i++) { + free(aliases[i].name); + free(aliases[i].command); + } + + // Free the array + free(aliases); + aliases = NULL; + alias_count = 0; + alias_capacity = 0; +} + +/** + * Shutdown the alias system + */ +void shutdown_aliases(void) { + // Save aliases to file + save_aliases(); + + // Clean up + cleanup_aliases(); +} + +/** + * Load aliases from file + */ +int load_aliases(void) { + FILE *fp = fopen(aliases_file_path, "r"); + if (!fp) { + return 0; // File doesn't exist or can't be opened + } + + char line[LSH_RL_BUFSIZE]; + char name[LSH_RL_BUFSIZE / 2]; + char command[LSH_RL_BUFSIZE / 2]; + + while (fgets(line, sizeof(line), fp)) { + // Skip comments and empty lines + if (line[0] == '#' || line[0] == '\n') { + continue; + } + + // Parse alias definitions (name=command) + if (sscanf(line, "%s=%s", name, command) == 2) { + add_alias(name, command); + } + } + + fclose(fp); + return 1; +} + +/** + * Save aliases to file + */ +int save_aliases(void) { + FILE *fp = fopen(aliases_file_path, "w"); + if (!fp) { + fprintf(stderr, "lsh: error saving aliases to %s\n", aliases_file_path); + return 0; + } + + // Write file header + time_t t = time(NULL); + struct tm *tm_info = localtime(&t); + char timestamp[26]; + strftime(timestamp, sizeof(timestamp), "%Y-%m-%d %H:%M:%S", tm_info); + + fprintf(fp, "# LSH Aliases - Last updated: %s\n\n", timestamp); + + // Write each alias + for (int i = 0; i < alias_count; i++) { + fprintf(fp, "%s=%s\n", aliases[i].name, aliases[i].command); + } + + fclose(fp); + return 1; +} + +/** + * Add a new alias + */ +int add_alias(const char *name, const char *command) { + if (!name || !command) { + return 0; + } + + // Check if alias already exists, update it if it does + for (int i = 0; i < alias_count; i++) { + if (strcmp(aliases[i].name, name) == 0) { + // Update existing alias + free(aliases[i].command); + aliases[i].command = strdup(command); + return 1; + } + } + + // Check if we need to expand the array + if (alias_count >= alias_capacity) { + alias_capacity *= 2; + AliasEntry *new_aliases = (AliasEntry*)realloc(aliases, alias_capacity * sizeof(AliasEntry)); + if (!new_aliases) { + fprintf(stderr, "lsh: allocation error in add_alias\n"); + return 0; + } + aliases = new_aliases; + } + + // Add new alias + aliases[alias_count].name = strdup(name); + aliases[alias_count].command = strdup(command); + alias_count++; + + return 1; +} + +/** + * Remove an alias + */ +int remove_alias(const char *name) { + if (!name) { + return 0; + } + + for (int i = 0; i < alias_count; i++) { + if (strcmp(aliases[i].name, name) == 0) { + // Free memory for this alias + free(aliases[i].name); + free(aliases[i].command); + + // Shift remaining aliases down + for (int j = i; j < alias_count - 1; j++) { + aliases[j] = aliases[j + 1]; + } + + alias_count--; + return 1; + } + } + + return 0; // Alias not found +} + +/** + * Find an alias by name + */ +AliasEntry* find_alias(const char *name) { + for (int i = 0; i < alias_count; i++) { + if (strcmp(aliases[i].name, name) == 0) { + return &aliases[i]; + } + } + return NULL; +} + +/** + * Expand a command line by replacing aliases + */ +char* expand_aliases(const char *command) { + if (!command) { + return NULL; + } + + char *result = strdup(command); + if (!result) return NULL; + + // Copy the command for tokenization + char *cmd_copy = strdup(result); + if (!cmd_copy) { + free(result); + return NULL; + } + + // Extract the first word (command name) + char *token = strtok(cmd_copy, " \t"); + if (!token) { + free(cmd_copy); + return result; + } + + // Look up the alias + AliasEntry *alias = find_alias(token); + if (alias) { + // Get the args after the command + char *args = strchr(command, ' '); + + // Allocate new buffer for expanded command + free(result); + if (args) { + // Concatenate alias command and original args + result = malloc(strlen(alias->command) + strlen(args) + 1); + if (result) { + strcpy(result, alias->command); + strcat(result, args); + } + } else { + // No args, just use alias command + result = strdup(alias->command); + } + } + + free(cmd_copy); + return result; +} + +/** + * Command handler for the "alias" command + */ +int lsh_alias(char **args) { + // No arguments - list all aliases + if (args[1] == NULL) { + for (int i = 0; i < alias_count; i++) { + printf("alias %s='%s'\n", aliases[i].name, aliases[i].command); + } + return 1; + } + + // Pre-defined aliases + if (strcmp(args[1], "vim-mode") == 0) { + // Configure aliases for Vim-like operation + add_alias("v", "vim"); + + // Check for neovim first + FILE *test_nvim = popen("which nvim 2>/dev/null", "r"); + if (test_nvim) { + char buffer[128]; + if (fgets(buffer, sizeof(buffer), test_nvim) != NULL) { + // Neovim found, use it as preferred editor + pclose(test_nvim); + add_alias("e", "nvim"); + printf("Vim mode activated (using Neovim)\n"); + return 1; + } + pclose(test_nvim); + + // Check for regular vim + FILE *test_vim = popen("which vim 2>/dev/null", "r"); + if (test_vim) { + char buffer[128]; + if (fgets(buffer, sizeof(buffer), test_vim) != NULL) { + // Vim found + pclose(test_vim); + add_alias("e", "vim"); + printf("Vim mode activated\n"); + return 1; + } + pclose(test_vim); + } + } + + // Fallback to vi if neither vim nor neovim is found + add_alias("e", "vi"); + printf("Vim mode activated (using vi)\n"); + return 1; + } else if (strcmp(args[1], "emacs-mode") == 0) { + // Configure aliases for Emacs-like operation + FILE *test_emacs = popen("which emacs 2>/dev/null", "r"); + if (test_emacs) { + char buffer[128]; + if (fgets(buffer, sizeof(buffer), test_emacs) != NULL) { + pclose(test_emacs); + add_alias("e", "emacs -nw"); + printf("Emacs mode activated\n"); + return 1; + } + pclose(test_emacs); + } + + printf("Emacs not found. Emacs mode not activated.\n"); + return 1; + } + + // Format: alias name=command + char *equals = strchr(args[1], '='); + if (equals) { + // Split at equals sign + *equals = '\0'; + char *name = args[1]; + char *command = equals + 1; + + // Remove quotes around command if present + if (*command == '\'' || *command == '"') { + command++; + char *end = command + strlen(command) - 1; + if (*end == '\'' || *end == '"') { + *end = '\0'; + } + } + + add_alias(name, command); + printf("Alias added: %s='%s'\n", name, command); + } else if (args[2] != NULL) { + // Format: alias name command + add_alias(args[1], args[2]); + printf("Alias added: %s='%s'\n", args[1], args[2]); + } else { + // Just a name - show this specific alias + AliasEntry *alias = find_alias(args[1]); + if (alias) { + printf("alias %s='%s'\n", alias->name, alias->command); + } else { + printf("Alias '%s' not found\n", args[1]); + } + } + + // Save aliases to file + save_aliases(); + + return 1; +} + +/** + * Command handler for the "unalias" command + */ +int lsh_unalias(char **args) { + if (args[1] == NULL) { + printf("unalias: missing argument\n"); + return 1; + } + + if (remove_alias(args[1])) { + printf("Alias '%s' removed\n", args[1]); + + // Save aliases to file + save_aliases(); + } else { + printf("Alias '%s' not found\n", args[1]); + } + + return 1; +} + +/** + * Command handler to show all aliases + */ +int lsh_aliases(char **args) { + for (int i = 0; i < alias_count; i++) { + printf("alias %s='%s'\n", aliases[i].name, aliases[i].command); + } + return 1; +} + +/** + * Get alias names for tab completion + */ +char** get_alias_names(int *count) { + if (alias_count == 0 || !count) { + *count = 0; + return NULL; + } + + char **names = (char**)malloc(alias_count * sizeof(char*)); + if (!names) { + *count = 0; + return NULL; + } + + for (int i = 0; i < alias_count; i++) { + names[i] = strdup(aliases[i].name); + } + + *count = alias_count; + return names; +} + +/** + * Expand alias in args array + */ +char** expand_alias(char **args) { + if (!args || !args[0]) { + return NULL; + } + + // Check if the command is an alias + AliasEntry *alias = find_alias(args[0]); + if (!alias) { + return NULL; + } + + // Count tokens in alias command + char *alias_copy = strdup(alias->command); + if (!alias_copy) { + return NULL; + } + + char **alias_tokens = malloc(LSH_TOK_BUFSIZE * sizeof(char*)); + if (!alias_tokens) { + free(alias_copy); + return NULL; + } + + char *token, *rest = alias_copy; + int token_count = 0; + + while ((token = strtok_r(rest, " \t", &rest)) != NULL) { + alias_tokens[token_count++] = strdup(token); + + if (token_count >= LSH_TOK_BUFSIZE) { + fprintf(stderr, "lsh: alias expansion: too many tokens\n"); + break; + } + } + + free(alias_copy); + + // Count original arguments + int arg_count = 0; + while (args[arg_count] != NULL) { + arg_count++; + } + + // Allocate space for expanded command + char **expanded_args = malloc((token_count + arg_count) * sizeof(char*)); + if (!expanded_args) { + for (int i = 0; i < token_count; i++) { + free(alias_tokens[i]); + } + free(alias_tokens); + return NULL; + } + + // Copy alias tokens + for (int i = 0; i < token_count; i++) { + expanded_args[i] = alias_tokens[i]; + } + + // Copy remaining arguments (skip the command) + for (int i = 1; i < arg_count; i++) { + expanded_args[token_count + i - 1] = strdup(args[i]); + } + + // Null-terminate the array + expanded_args[token_count + arg_count - 1] = NULL; + + free(alias_tokens); + return expanded_args; +} \ No newline at end of file diff --git a/aliases.h b/aliases.h new file mode 100644 index 0000000..5423af8 --- /dev/null +++ b/aliases.h @@ -0,0 +1,63 @@ +/** + * aliases.h + * Definitions for shell alias functionality + */ + +#ifndef ALIASES_H +#define ALIASES_H + +#include "common.h" + +// Structure to represent an alias +typedef struct AliasEntry { + char *name; // Alias name (command to type) + char *command; // The command it expands to +} AliasEntry; + +// Initialize alias system +void init_aliases(void); + +// Clean up alias system +void cleanup_aliases(void); + +// Shutdown the alias system +void shutdown_aliases(void); + +// Load aliases from file +int load_aliases(void); + +// Save aliases to file +int save_aliases(void); + +// Add a new alias +int add_alias(const char *name, const char *command); + +// Remove an alias +int remove_alias(const char *name); + +// Find an alias by name +AliasEntry* find_alias(const char *name); + +// Expand a command line by replacing aliases +char* expand_aliases(const char *command); + +// Command handler for the "alias" command +int lsh_alias(char **args); + +// Command handler for the "unalias" command +int lsh_unalias(char **args); + +// Command handler to list all aliases +int lsh_aliases(char **args); + +// Get alias names for tab completion +char** get_alias_names(int *count); + +// Expand alias in args array +char** expand_alias(char **args); + +// Global variables for alias storage +extern AliasEntry *aliases; +extern int alias_count; + +#endif // ALIASES_H \ No newline at end of file diff --git a/aliases.o b/aliases.o new file mode 100644 index 0000000..a4beddd Binary files /dev/null and b/aliases.o differ diff --git a/autocorrect.c b/autocorrect.c new file mode 100644 index 0000000..c735d89 --- /dev/null +++ b/autocorrect.c @@ -0,0 +1,168 @@ +/** + * autocorrect.c + * Implementation of command auto-correction functionality + */ + +#include "autocorrect.h" +#include "builtins.h" + +/** + * Calculate Levenshtein distance between two strings + * Used to determine similarity between mistyped command and potential + * corrections + */ +int levenshtein_distance(const char *s1, const char *s2) { + int len1 = strlen(s1); + int len2 = strlen(s2); + + // Create a matrix to store distances + int **matrix = (int **)malloc((len1 + 1) * sizeof(int *)); + if (!matrix) return -1; + + for (int i = 0; i <= len1; i++) { + matrix[i] = (int *)malloc((len2 + 1) * sizeof(int)); + if (!matrix[i]) { + for (int j = 0; j < i; j++) { + free(matrix[j]); + } + free(matrix); + return -1; + } + } + + // Initialize the matrix + for (int i = 0; i <= len1; i++) { + matrix[i][0] = i; + } + for (int j = 0; j <= len2; j++) { + matrix[0][j] = j; + } + + // Fill the matrix + for (int i = 1; i <= len1; i++) { + for (int j = 1; j <= len2; j++) { + int cost = (s1[i - 1] == s2[j - 1]) ? 0 : 1; + matrix[i][j] = min3(matrix[i - 1][j] + 1, // deletion + matrix[i][j - 1] + 1, // insertion + matrix[i - 1][j - 1] + cost // substitution + ); + } + } + + // Get the result and free the matrix + int result = matrix[len1][len2]; + for (int i = 0; i <= len1; i++) { + free(matrix[i]); + } + free(matrix); + + return result; +} + +/** + * Helper function to find minimum of three integers + */ +int min3(int a, int b, int c) { + int min = a; + if (b < min) + min = b; + if (c < min) + min = c; + return min; +} + +/** + * Initialize the autocorrect system + */ +void init_autocorrect(void) { + // No initialization required currently +} + +/** + * Shutdown the autocorrect system + */ +void shutdown_autocorrect(void) { + // No cleanup required currently +} + +/** + * Check a command for possible corrections and suggest alternative + */ + +/** + * Check a command for possible corrections and suggest alternative + */ + +/** + * Check a command for possible corrections and suggest alternative + */ +char **check_for_corrections(char **args) { + if (!args || !args[0]) { + return NULL; + } + + const char *command = args[0]; + + // Skip very short commands or commands that start with ./ or / + if (strlen(command) < 3 || command[0] == '.' || command[0] == '/') { + return NULL; + } + + // Don't try to correct arguments, only the command itself + // Check if the command is valid, if it is, no correction needed + if (is_valid_command(command)) { + return NULL; + } + + // List of common commands to check against + const char *common_commands[] = { + "ls", "cd", "grep", "find", "cat", "mv", "cp", "rm", "mkdir", "rmdir", + "chmod", "chown", "ps", "top", "df", "du", "free", "mount", "umount", + "tar", "zip", "unzip", "ssh", "scp", "ping", "netstat", "ifconfig", + "route", "traceroute", "wget", "curl", "apt", "apt-get", "yum", "dnf", + "pacman", "git", "make", "gcc", "g++", "python", "python3", "node", + "npm", "vim", "nano", "history", "clear", "exit", "alias", "man", "help", + "touch", "echo", "pwd", "sudo", "shutdown", "reboot", NULL + }; + + // Check built-in commands too + int best_distance = 3; // Maximum edit distance to consider a correction + const char *best_match = NULL; + + // First check among built-in commands + for (int i = 0; i < lsh_num_builtins(); i++) { + int distance = levenshtein_distance(command, builtin_str[i]); + if (distance < best_distance) { + best_distance = distance; + best_match = builtin_str[i]; + } + } + + // Then check common commands + for (int i = 0; common_commands[i] != NULL; i++) { + int distance = levenshtein_distance(command, common_commands[i]); + if (distance < best_distance) { + best_distance = distance; + best_match = common_commands[i]; + } + } + + // If we found a good match, suggest it + if (best_match) { + printf("Command '%s' not found. Did you mean '%s'?\n", command, best_match); + // No longer waiting for input - just return NULL + } + + return NULL; +} + +/** + * Count the number of arguments in a NULL-terminated array + */ +int count_args(char **args) { + int count = 0; + while (args[count] != NULL) { + count++; + } + return count; +} diff --git a/autocorrect.h b/autocorrect.h new file mode 100644 index 0000000..9f31b7c --- /dev/null +++ b/autocorrect.h @@ -0,0 +1,50 @@ +/** + * autocorrect.h + * Header for command auto-correction functionality + */ + +#ifndef AUTOCORRECT_H +#define AUTOCORRECT_H + +#include "common.h" +#include "line_reader.h" +#include + +/** + * Calculate Levenshtein distance between two strings + * + * @param s1 First string + * @param s2 Second string + * @return Edit distance between the strings + */ +int levenshtein_distance(const char *s1, const char *s2); + +/** + * Helper function to find minimum of three integers + */ +int min3(int a, int b, int c); + +/** + * Initialize the autocorrect system + */ +void init_autocorrect(void); + +/** + * Shutdown the autocorrect system + */ +void shutdown_autocorrect(void); + +/** + * Check a command for possible corrections + * + * @param args Command arguments + * @return Corrected arguments array (must be freed by caller) or NULL if no correction + */ +char **check_for_corrections(char **args); + +/** + * Count the number of arguments in a NULL-terminated array + */ +int count_args(char **args); + +#endif // AUTOCORRECT_H \ No newline at end of file diff --git a/autocorrect.o b/autocorrect.o new file mode 100644 index 0000000..dfeb1d3 Binary files /dev/null and b/autocorrect.o differ diff --git a/bookmarks.c b/bookmarks.c new file mode 100644 index 0000000..1ee629e --- /dev/null +++ b/bookmarks.c @@ -0,0 +1,409 @@ +/** + * bookmarks.c + * Implementation of directory bookmarks functionality + */ + +#include "bookmarks.h" +#include +#include + +// Global variables for bookmark storage +BookmarkEntry *bookmarks = NULL; +int bookmark_count = 0; +int bookmark_capacity = 0; + +// Path to the bookmarks file +char bookmarks_file_path[PATH_MAX]; + +/** + * Initialize the bookmark system + */ +void init_bookmarks(void) { + // Set initial capacity + bookmark_capacity = 10; + bookmarks = + (BookmarkEntry *)malloc(bookmark_capacity * sizeof(BookmarkEntry)); + + if (!bookmarks) { + fprintf(stderr, "lsh: allocation error in init_bookmarks\n"); + return; + } + + // Determine bookmarks file location - in user's home directory + char *home_dir = getenv("HOME"); + if (home_dir) { + snprintf(bookmarks_file_path, PATH_MAX, "%s/.lsh_bookmarks", home_dir); + } else { + // Fallback to current directory if HOME not available + strcpy(bookmarks_file_path, ".lsh_bookmarks"); + } + + // Load bookmarks from file + load_bookmarks(); +} + +/** + * Clean up the bookmark system + */ +void cleanup_bookmarks(void) { + if (!bookmarks) + return; + + // Free all bookmark entries + for (int i = 0; i < bookmark_count; i++) { + free(bookmarks[i].name); + free(bookmarks[i].path); + } + + // Free the array + free(bookmarks); + bookmarks = NULL; + bookmark_count = 0; + bookmark_capacity = 0; +} + +/** + * Shutdown the bookmark system + */ +void shutdown_bookmarks(void) { + // Save bookmarks to file + save_bookmarks(); + + // Clean up + cleanup_bookmarks(); +} + +/** + * Load bookmarks from file + */ +int load_bookmarks(void) { + FILE *fp = fopen(bookmarks_file_path, "r"); + if (!fp) { + return 0; // File doesn't exist or can't be opened + } + + char line[LSH_RL_BUFSIZE]; + char name[LSH_RL_BUFSIZE / 2]; + char path[LSH_RL_BUFSIZE / 2]; + + while (fgets(line, sizeof(line), fp)) { + // Skip comments and empty lines + if (line[0] == '#' || line[0] == '\n') { + continue; + } + + // Try to parse using tab as delimiter first + char *tab = strchr(line, '\t'); + if (tab) { + // Split at tab + *tab = '\0'; + strncpy(name, line, sizeof(name) - 1); + name[sizeof(name) - 1] = '\0'; + strncpy(path, tab + 1, sizeof(path) - 1); + path[sizeof(path) - 1] = '\0'; + + // Remove newline if present + char *newline = strchr(path, '\n'); + if (newline) { + *newline = '\0'; + } + + add_bookmark(name, path); + } else { + // Parse in name=path format as fallback + if (sscanf(line, "%[^=]=%s", name, path) == 2) { + add_bookmark(name, path); + } + } + } + + fclose(fp); + return 1; +} + +/** + * Save bookmarks to file + */ +int save_bookmarks(void) { + FILE *fp = fopen(bookmarks_file_path, "w"); + if (!fp) { + fprintf(stderr, "lsh: error saving bookmarks to %s\n", bookmarks_file_path); + return 0; + } + + // Write file header + time_t t = time(NULL); + struct tm *tm_info = localtime(&t); + char timestamp[26]; + strftime(timestamp, sizeof(timestamp), "%Y-%m-%d %H:%M:%S", tm_info); + + fprintf(fp, "# LSH Bookmarks - Last updated: %s\n\n", timestamp); + + // Write each bookmark + for (int i = 0; i < bookmark_count; i++) { + fprintf(fp, "%s\t%s\n", bookmarks[i].name, bookmarks[i].path); + } + + fclose(fp); + return 1; +} + +/** + * Add a new bookmark + */ +int add_bookmark(const char *name, const char *path) { + if (!name || !path) { + return 0; + } + + // Check if bookmark already exists + for (int i = 0; i < bookmark_count; i++) { + if (strcmp(bookmarks[i].name, name) == 0) { + // Update existing bookmark + free(bookmarks[i].path); + bookmarks[i].path = strdup(path); + return 1; + } + } + + // Check if we need to expand the array + if (bookmark_count >= bookmark_capacity) { + bookmark_capacity *= 2; + BookmarkEntry *new_bookmarks = (BookmarkEntry *)realloc( + bookmarks, bookmark_capacity * sizeof(BookmarkEntry)); + if (!new_bookmarks) { + fprintf(stderr, "lsh: allocation error in add_bookmark\n"); + return 0; + } + bookmarks = new_bookmarks; + } + + // Add new bookmark + bookmarks[bookmark_count].name = strdup(name); + bookmarks[bookmark_count].path = strdup(path); + bookmark_count++; + + return 1; +} + +/** + * Remove a bookmark + */ +int remove_bookmark(const char *name) { + if (!name) { + return 0; + } + + for (int i = 0; i < bookmark_count; i++) { + if (strcmp(bookmarks[i].name, name) == 0) { + // Free memory for this bookmark + free(bookmarks[i].name); + free(bookmarks[i].path); + + // Shift remaining bookmarks down + for (int j = i; j < bookmark_count - 1; j++) { + bookmarks[j] = bookmarks[j + 1]; + } + + bookmark_count--; + return 1; + } + } + + return 0; // Bookmark not found +} + +/** + * Find a bookmark by name + */ +BookmarkEntry *find_bookmark(const char *name) { + for (int i = 0; i < bookmark_count; i++) { + if (strcmp(bookmarks[i].name, name) == 0) { + return &bookmarks[i]; + } + } + return NULL; +} + +/** + * Command handler for the "bookmark" command + */ +int lsh_bookmark(char **args) { + char cwd[PATH_MAX]; + + // No arguments - add bookmark for current directory + if (args[1] == NULL) { + printf("Usage: bookmark [path]\n"); + printf("If path is omitted, the current directory is used.\n"); + return 1; + } + + // Get current working directory if no path is specified + if (args[2] == NULL) { + if (getcwd(cwd, sizeof(cwd)) == NULL) { + perror("lsh: getcwd"); + return 1; + } + add_bookmark(args[1], cwd); + printf("Bookmark added: %s -> %s\n", args[1], cwd); + } else { + add_bookmark(args[1], args[2]); + printf("Bookmark added: %s -> %s\n", args[1], args[2]); + } + + // Save bookmarks to file + save_bookmarks(); + + return 1; +} + +/** + * Command handler for the "bookmarks" command + */ +int lsh_bookmarks(char **args) { + if (bookmark_count == 0) { + printf("No bookmarks defined.\n"); + printf("Use 'bookmark [path]' to add a bookmark.\n"); + return 1; + } + + // Check if we should use a pager + if (bookmark_count > 20) { + // Check for neovim first + FILE *test_nvim = popen("which nvim 2>/dev/null", "r"); + if (test_nvim) { + char buffer[128]; + if (fgets(buffer, sizeof(buffer), test_nvim) != NULL) { + pclose(test_nvim); + FILE *pager = popen("nvim -R -c 'set nonumber' -", "w"); + if (pager) { + fprintf(pager, "LSH Bookmarks:\n\n"); + for (int i = 0; i < bookmark_count; i++) { + fprintf(pager, " %s -> %s\n", bookmarks[i].name, bookmarks[i].path); + } + pclose(pager); + return 1; + } + } + pclose(test_nvim); + + // Try vim + FILE *test_vim = popen("which vim 2>/dev/null", "r"); + if (test_vim) { + char buffer[128]; + if (fgets(buffer, sizeof(buffer), test_vim) != NULL) { + pclose(test_vim); + FILE *pager = popen("vim -R -c 'set nonumber' -", "w"); + if (pager) { + fprintf(pager, "LSH Bookmarks:\n\n"); + for (int i = 0; i < bookmark_count; i++) { + fprintf(pager, " %s -> %s\n", bookmarks[i].name, bookmarks[i].path); + } + pclose(pager); + return 1; + } + } + pclose(test_vim); + } + } + } + + // Simple output if no pager available or bookmark count is small + printf("LSH Bookmarks:\n\n"); + + // Use ANSI escape codes for colored output + for (int i = 0; i < bookmark_count; i++) { + printf(" " ANSI_COLOR_GREEN "%s" ANSI_COLOR_RESET " -> %s\n", + bookmarks[i].name, bookmarks[i].path); + } + + return 1; +} + +/** + * Command handler for the "goto" command + */ +int lsh_goto(char **args) { + if (args[1] == NULL) { + printf("Usage: goto \n"); + return 1; + } + + BookmarkEntry *bookmark = find_bookmark(args[1]); + if (bookmark) { + if (chdir(bookmark->path) != 0) { + perror("lsh: chdir"); + return 1; + } + printf("Changed directory to: %s\n", bookmark->path); + } else { + printf("Bookmark '%s' not found.\n", args[1]); + } + + return 1; +} + +/** + * Command handler for the "unbookmark" command + */ +int lsh_unbookmark(char **args) { + if (args[1] == NULL) { + printf("Usage: unbookmark \n"); + return 1; + } + + if (remove_bookmark(args[1])) { + printf("Bookmark '%s' removed.\n", args[1]); + save_bookmarks(); + } else { + printf("Bookmark '%s' not found.\n", args[1]); + } + + return 1; +} + +/** + * Get bookmark names for tab completion + */ +char **get_bookmark_names(int *count) { + if (bookmark_count == 0 || !count) { + *count = 0; + return NULL; + } + + char **names = (char **)malloc(bookmark_count * sizeof(char *)); + if (!names) { + *count = 0; + return NULL; + } + + for (int i = 0; i < bookmark_count; i++) { + names[i] = strdup(bookmarks[i].name); + } + + *count = bookmark_count; + return names; +} + +/** + * Find a matching bookmark by partial name + */ +char *find_matching_bookmark(const char *partial_name) { + if (!partial_name || strlen(partial_name) == 0) { + return NULL; + } + + // If only one bookmark exists, return it if there's any match + if (bookmark_count == 1) { + return strdup(bookmarks[0].name); + } + + // Look for partial matches + for (int i = 0; i < bookmark_count; i++) { + if (strncasecmp(bookmarks[i].name, partial_name, strlen(partial_name)) == 0) { + return strdup(bookmarks[i].name); + } + } + + return NULL; +} \ No newline at end of file diff --git a/bookmarks.h b/bookmarks.h new file mode 100644 index 0000000..715e5fe --- /dev/null +++ b/bookmarks.h @@ -0,0 +1,57 @@ +/** + * bookmarks.h + * Functions for managing directory bookmarks + */ + +#ifndef BOOKMARKS_H +#define BOOKMARKS_H + +#include "common.h" + +// Structure to represent a bookmark +typedef struct { + char *name; // Bookmark name + char *path; // Directory path +} BookmarkEntry; + +// Initialize bookmark system +void init_bookmarks(void); + +// Clean up bookmark system +void cleanup_bookmarks(void); + +// Shutdown the bookmark system +void shutdown_bookmarks(void); + +// Load bookmarks from file +int load_bookmarks(void); + +// Save bookmarks to file +int save_bookmarks(void); + +// Add a new bookmark +int add_bookmark(const char *name, const char *path); + +// Remove a bookmark by name +int remove_bookmark(const char *name); + +// Find a bookmark by name +BookmarkEntry *find_bookmark(const char *name); + +// Command handlers +int lsh_bookmark(char **args); // Add a bookmark +int lsh_bookmarks(char **args); // List all bookmarks +int lsh_goto(char **args); // Jump to a bookmark +int lsh_unbookmark(char **args); // Remove a bookmark + +// Get bookmarked directories for tab completion +char **get_bookmark_names(int *count); + +// Find a matching bookmark by partial name +char *find_matching_bookmark(const char *partial_name); + +// Global bookmarks array +extern BookmarkEntry *bookmarks; +extern int bookmark_count; + +#endif // BOOKMARKS_H \ No newline at end of file diff --git a/bookmarks.o b/bookmarks.o new file mode 100644 index 0000000..8e5aa14 Binary files /dev/null and b/bookmarks.o differ diff --git a/builtins.c b/builtins.c new file mode 100644 index 0000000..0acb3c0 --- /dev/null +++ b/builtins.c @@ -0,0 +1,764 @@ +/** + * builtins.c + * Implementation of all built-in shell commands + */ + +#include "builtins.h" +#include "common.h" +#include "filters.h" +#include "fzf_native.h" +#include "git_integration.h" +#include "grep.h" +#include "persistent_history.h" +#include "structured_data.h" +#include "themes.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// History variables +HistoryEntry command_history[HISTORY_SIZE]; +int history_count = 0; +int history_index = 0; + +// String array of built-in command names +char *builtin_str[] = { + "cd", "help", "exit", "dir", "clear", + "mkdir", "rmdir", "del", "touch", "pwd", + "cat", "history", "copy", "move", "paste", + "ps", "news", "alias", "unalias", "aliases", + "bookmark", "bookmarks", "goto", "unbookmark", "focus_timer", + "weather", "grep", "grep-text", "ripgrep", "fzf", + "clip", "echo", "theme", "loc", "git_status", + "gg"}; + +// Array of function pointers to built-in command implementations +int (*builtin_func[])(char **) = { + &lsh_cd, &lsh_help, &lsh_exit, &lsh_dir, &lsh_clear, + &lsh_mkdir, &lsh_rmdir, &lsh_del, &lsh_touch, &lsh_pwd, + &lsh_cat, &lsh_history, &lsh_copy, &lsh_move, &lsh_paste, + &lsh_ps, &lsh_news, &lsh_alias, &lsh_unalias, &lsh_aliases, + &lsh_bookmark, &lsh_bookmarks, &lsh_goto, &lsh_unbookmark, &lsh_focus_timer, + &lsh_weather, &lsh_grep, &lsh_actual_grep, &lsh_ripgrep, &lsh_fzf_native, + &lsh_clip, &lsh_echo, &lsh_theme, &lsh_loc, &lsh_git_status, + &lsh_gg}; + +/** + * Set the console text color + */ +void set_color(int color) { + switch(color) { + case 0: printf(ANSI_COLOR_RESET); break; + case 1: printf(ANSI_COLOR_RED); break; + case 2: printf(ANSI_COLOR_GREEN); break; + case 3: printf(ANSI_COLOR_YELLOW); break; + case 4: printf(ANSI_COLOR_BLUE); break; + case 5: printf(ANSI_COLOR_MAGENTA); break; + case 6: printf(ANSI_COLOR_CYAN); break; + case 7: printf(ANSI_COLOR_WHITE); break; + default: printf(ANSI_COLOR_RESET); + } +} + +/** + * Reset console color to default + */ +void reset_color() { + printf(ANSI_COLOR_RESET); +} + +/** + * Get the number of built-in commands + */ +int lsh_num_builtins() { return sizeof(builtin_str) / sizeof(char *); } + +/** + * Add a command to the history + */ +void lsh_add_to_history(const char *command) { + // Don't add empty commands or duplicates of the last command + if (!command || *command == '\0' || + (history_count > 0 && + strcmp(command_history[history_index - 1].command, command) == 0)) { + return; + } + + // Free the oldest entry if we're overwriting it + if (history_count == HISTORY_SIZE) { + free(command_history[history_index].command); + } else { + history_count++; + } + + // Add the new command + command_history[history_index].command = strdup(command); + command_history[history_index].timestamp = time(NULL); + + // Update the index + history_index = (history_index + 1) % HISTORY_SIZE; +} + +/** + * Built-in command: cd + * Changes the current directory + */ +int lsh_cd(char **args) { + if (args[1] == NULL) { + // No argument provided, change to home directory + char *home_dir = getenv("HOME"); + if (home_dir == NULL) { + fprintf(stderr, "lsh: HOME environment variable not set\n"); + return 1; + } + if (chdir(home_dir) != 0) { + perror("lsh: cd"); + } + } else { + if (chdir(args[1]) != 0) { + perror("lsh: cd"); + } + } + return 1; +} + +/** + * Built-in command: help + * Displays help information + */ +int lsh_help(char **args) { + printf("LSH Shell - A lightweight shell with modern features\n"); + printf("Type a command and press Enter to execute it.\n"); + printf("The following built-in commands are available:\n\n"); + + // Sort commands alphabetically + char *sorted_commands[lsh_num_builtins()]; + for (int i = 0; i < lsh_num_builtins(); i++) { + sorted_commands[i] = builtin_str[i]; + } + + for (int i = 0; i < lsh_num_builtins() - 1; i++) { + for (int j = i + 1; j < lsh_num_builtins(); j++) { + if (strcmp(sorted_commands[i], sorted_commands[j]) > 0) { + char *temp = sorted_commands[i]; + sorted_commands[i] = sorted_commands[j]; + sorted_commands[j] = temp; + } + } + } + + // Print commands in columns + int columns = 4; + int rows = (lsh_num_builtins() + columns - 1) / columns; + for (int i = 0; i < rows; i++) { + for (int j = 0; j < columns; j++) { + int index = j * rows + i; + if (index < lsh_num_builtins()) { + printf("%-15s", sorted_commands[index]); + } + } + printf("\n"); + } + + printf("\nFor more information on specific commands, type 'help '\n"); + printf("Use tab completion for commands and file paths\n"); + printf("Use arrow keys to navigate command history\n"); + printf("Type a partial command followed by '?' for suggestions\n"); + + return 1; +} + +/** + * Built-in command: exit + * Exits the shell + */ +int lsh_exit(char **args) { return 0; } + +/** + * Built-in command: dir + * Lists files in the current directory + */ +int lsh_dir(char **args) { + DIR *dir; + struct dirent *entry; + struct stat file_stat; + char cwd[PATH_MAX]; + char file_path[PATH_MAX]; + int count = 0; + int detailed = 0; + + // Check if we should show detailed listing + if (args[1] != NULL && (strcmp(args[1], "-l") == 0 || strcmp(args[1], "--long") == 0)) { + detailed = 1; + } + + if (getcwd(cwd, sizeof(cwd)) == NULL) { + perror("lsh: getcwd"); + return 1; + } + + dir = opendir(cwd); + if (dir == NULL) { + perror("lsh: opendir"); + return 1; + } + + if (detailed) { + printf("Mode Size Modified Name\n"); + printf("----------------------------------------------------\n"); + } + + // First collect all entries + typedef struct { + char name[256]; + struct stat stat; + int is_dir; + } DirEntry; + + DirEntry *entries = malloc(1000 * sizeof(DirEntry)); // Arbitrary limit + int entry_count = 0; + + while ((entry = readdir(dir)) != NULL && entry_count < 1000) { + // Skip . and .. entries + if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) { + continue; + } + + snprintf(file_path, sizeof(file_path), "%s/%s", cwd, entry->d_name); + if (stat(file_path, &file_stat) == 0) { + strcpy(entries[entry_count].name, entry->d_name); + entries[entry_count].stat = file_stat; + entries[entry_count].is_dir = S_ISDIR(file_stat.st_mode); + entry_count++; + } + } + + closedir(dir); + + // Sort entries - directories first then files + for (int i = 0; i < entry_count - 1; i++) { + for (int j = i + 1; j < entry_count; j++) { + if (entries[i].is_dir < entries[j].is_dir || + (entries[i].is_dir == entries[j].is_dir && + strcmp(entries[i].name, entries[j].name) > 0)) { + DirEntry temp = entries[i]; + entries[i] = entries[j]; + entries[j] = temp; + } + } + } + + // Display entries + for (int i = 0; i < entry_count; i++) { + if (detailed) { + // Format mode (simplified) + char mode[11] = "----------"; + if (entries[i].is_dir) mode[0] = 'd'; + if (entries[i].stat.st_mode & S_IRUSR) mode[1] = 'r'; + if (entries[i].stat.st_mode & S_IWUSR) mode[2] = 'w'; + if (entries[i].stat.st_mode & S_IXUSR) mode[3] = 'x'; + if (entries[i].stat.st_mode & S_IRGRP) mode[4] = 'r'; + if (entries[i].stat.st_mode & S_IWGRP) mode[5] = 'w'; + if (entries[i].stat.st_mode & S_IXGRP) mode[6] = 'x'; + if (entries[i].stat.st_mode & S_IROTH) mode[7] = 'r'; + if (entries[i].stat.st_mode & S_IWOTH) mode[8] = 'w'; + if (entries[i].stat.st_mode & S_IXOTH) mode[9] = 'x'; + + // Format size + char size_str[20]; + if (entries[i].stat.st_size < 1024) { + snprintf(size_str, sizeof(size_str), "%d B", (int)entries[i].stat.st_size); + } else if (entries[i].stat.st_size < 1024 * 1024) { + snprintf(size_str, sizeof(size_str), "%.1f KB", + entries[i].stat.st_size / 1024.0); + } else { + snprintf(size_str, sizeof(size_str), "%.1f MB", + entries[i].stat.st_size / (1024.0 * 1024.0)); + } + + // Format time + char time_str[20]; + struct tm *tm_info = localtime(&entries[i].stat.st_mtime); + strftime(time_str, sizeof(time_str), "%Y-%m-%d %H:%M", tm_info); + + // Print entry with colors + printf("%s %-10s %-17s ", mode, size_str, time_str); + + if (entries[i].is_dir) { + printf(ANSI_COLOR_BLUE "%s" ANSI_COLOR_RESET "\n", entries[i].name); + } else if (entries[i].stat.st_mode & S_IXUSR) { + printf(ANSI_COLOR_GREEN "%s" ANSI_COLOR_RESET "\n", entries[i].name); + } else { + printf("%s\n", entries[i].name); + } + } else { + count++; + if (entries[i].is_dir) { + printf(ANSI_COLOR_BLUE "%-20s" ANSI_COLOR_RESET, entries[i].name); + } else if (entries[i].stat.st_mode & S_IXUSR) { + printf(ANSI_COLOR_GREEN "%-20s" ANSI_COLOR_RESET, entries[i].name); + } else { + printf("%-20s", entries[i].name); + } + + if (count % 4 == 0) { + printf("\n"); + } + } + } + + free(entries); + + if (!detailed && count % 4 != 0) { + printf("\n"); + } + + return 1; +} + +/** + * Built-in command: clear + * Clears the screen + */ +int lsh_clear(char **args) { + printf(ANSI_CLEAR_SCREEN); + printf(ANSI_CURSOR_HOME); + return 1; +} + +/** + * Built-in command: mkdir + * Creates a new directory + */ +int lsh_mkdir(char **args) { + if (args[1] == NULL) { + fprintf(stderr, "lsh: expected argument to \"mkdir\"\n"); + } else { + if (mkdir(args[1], 0755) != 0) { + perror("lsh: mkdir"); + } + } + return 1; +} + +/** + * Built-in command: rmdir + * Removes a directory + */ +int lsh_rmdir(char **args) { + if (args[1] == NULL) { + fprintf(stderr, "lsh: expected argument to \"rmdir\"\n"); + } else { + if (rmdir(args[1]) != 0) { + perror("lsh: rmdir"); + } + } + return 1; +} + +/** + * Built-in command: del + * Deletes a file + */ +int lsh_del(char **args) { + if (args[1] == NULL) { + fprintf(stderr, "lsh: expected argument to \"del\"\n"); + } else { + if (unlink(args[1]) != 0) { + perror("lsh: del"); + } + } + return 1; +} + +/** + * Built-in command: touch + * Creates a new file or updates timestamp of existing file + */ +int lsh_touch(char **args) { + if (args[1] == NULL) { + fprintf(stderr, "lsh: expected argument to \"touch\"\n"); + } else { + int fd = open(args[1], O_CREAT | O_WRONLY | O_APPEND, 0644); + if (fd == -1) { + perror("lsh: touch"); + } else { + close(fd); + } + } + return 1; +} + +/** + * Built-in command: pwd + * Prints the current working directory + */ +int lsh_pwd(char **args) { + char cwd[PATH_MAX]; + if (getcwd(cwd, sizeof(cwd)) != NULL) { + printf("%s\n", cwd); + } else { + perror("lsh: getcwd"); + } + return 1; +} + +/** + * Built-in command: cat + * Displays the contents of a file + */ +int lsh_cat(char **args) { + if (args[1] == NULL) { + fprintf(stderr, "lsh: expected argument to \"cat\"\n"); + return 1; + } + + FILE *fp = fopen(args[1], "r"); + if (fp == NULL) { + perror("lsh: cat"); + return 1; + } + + char buffer[1024]; + while (fgets(buffer, sizeof(buffer), fp) != NULL) { + printf("%s", buffer); + } + + fclose(fp); + return 1; +} + +/** + * Built-in command: history + * Displays command history + */ +int lsh_history(char **args) { + char time_str[20]; + struct tm *tm_info; + + printf("Command History:\n"); + printf("----------------\n"); + + // Display history in chronological order + for (int i = 0; i < history_count; i++) { + int idx = (history_index - history_count + i + HISTORY_SIZE) % HISTORY_SIZE; + tm_info = localtime(&command_history[idx].timestamp); + strftime(time_str, sizeof(time_str), "%Y-%m-%d %H:%M:%S", tm_info); + printf("%3d: [%s] %s\n", i + 1, time_str, command_history[idx].command); + } + + return 1; +} + +/** + * Built-in command: copy + * Copies a file + */ +int lsh_copy(char **args) { + if (args[1] == NULL || args[2] == NULL) { + fprintf(stderr, "lsh: expected source and destination arguments to \"copy\"\n"); + return 1; + } + + FILE *source = fopen(args[1], "rb"); + if (source == NULL) { + perror("lsh: copy (source)"); + return 1; + } + + FILE *dest = fopen(args[2], "wb"); + if (dest == NULL) { + fclose(source); + perror("lsh: copy (destination)"); + return 1; + } + + char buffer[4096]; + size_t bytes_read; + while ((bytes_read = fread(buffer, 1, sizeof(buffer), source)) > 0) { + fwrite(buffer, 1, bytes_read, dest); + } + + fclose(source); + fclose(dest); + printf("Copied %s to %s\n", args[1], args[2]); + return 1; +} + +/** + * Built-in command: move + * Moves a file + */ +int lsh_move(char **args) { + if (args[1] == NULL || args[2] == NULL) { + fprintf(stderr, "lsh: expected source and destination arguments to \"move\"\n"); + return 1; + } + + if (rename(args[1], args[2]) != 0) { + perror("lsh: move"); + return 1; + } + + printf("Moved %s to %s\n", args[1], args[2]); + return 1; +} + +/** + * Built-in command: paste + * Placeholder for paste functionality + */ +int lsh_paste(char **args) { + printf("Paste functionality not implemented yet\n"); + return 1; +} + +/** + * Built-in command: ps + * Lists running processes + */ +int lsh_ps(char **args) { + FILE *fp = popen("ps -ef", "r"); + if (fp == NULL) { + perror("lsh: ps"); + return 1; + } + + char buffer[1024]; + while (fgets(buffer, sizeof(buffer), fp) != NULL) { + printf("%s", buffer); + } + + pclose(fp); + return 1; +} + +/** + * Built-in command: news + * Placeholder for news functionality + */ +int lsh_news(char **args) { + printf("Fetching news feed...\n"); + printf("News functionality not fully implemented yet\n"); + return 1; +} + +/** + * Built-in command: clip + * Placeholder for clipboard functionality + */ +int lsh_clip(char **args) { + printf("Clipboard functionality not implemented yet\n"); + return 1; +} + +/** + * Built-in command: echo + * Echoes arguments to stdout + */ +int lsh_echo(char **args) { + if (args[1] == NULL) { + printf("\n"); + return 1; + } + + for (int i = 1; args[i] != NULL; i++) { + printf("%s", args[i]); + if (args[i + 1] != NULL) { + printf(" "); + } + } + printf("\n"); + return 1; +} + +/** + * Built-in command: loc + * Counts lines of code + */ +int lsh_loc(char **args) { + if (args[1] == NULL) { + fprintf(stderr, "lsh: expected file or directory argument to \"loc\"\n"); + return 1; + } + + struct stat st; + if (stat(args[1], &st) != 0) { + perror("lsh: loc"); + return 1; + } + + if (S_ISREG(st.st_mode)) { + // Count lines in a single file + FILE *file = fopen(args[1], "r"); + if (!file) { + perror("lsh: loc"); + return 1; + } + + int lines = 0, code_lines = 0, blank_lines = 0, comment_lines = 0; + char line[1024]; + int in_comment_block = 0; + + while (fgets(line, sizeof(line), file)) { + lines++; + char *trimmed = line; + while (*trimmed && isspace(*trimmed)) trimmed++; + + if (*trimmed == '\0') { + blank_lines++; + } else if (strncmp(trimmed, "//", 2) == 0) { + comment_lines++; + } else if (strncmp(trimmed, "/*", 2) == 0) { + comment_lines++; + in_comment_block = 1; + if (strstr(trimmed, "*/")) { + in_comment_block = 0; + } + } else if (in_comment_block) { + comment_lines++; + if (strstr(trimmed, "*/")) { + in_comment_block = 0; + } + } else { + code_lines++; + } + } + + fclose(file); + + printf("File: %s\n", args[1]); + printf("Total lines: %d\n", lines); + printf("Code lines: %d\n", code_lines); + printf("Comment lines: %d\n", comment_lines); + printf("Blank lines: %d\n", blank_lines); + + } else if (S_ISDIR(st.st_mode)) { + printf("Directory LOC counting not implemented yet\n"); + } else { + fprintf(stderr, "lsh: %s is not a file or directory\n", args[1]); + } + + return 1; +} + +/** + * Helper function to extract a string value from a JSON response + */ +char *extract_json_string(const char *json, const char *key) { + char search_key[100]; + sprintf(search_key, "\"%s\":", key); + + char *key_pos = strstr(json, search_key); + if (!key_pos) return NULL; + + key_pos += strlen(search_key); + + // Skip whitespace + while (*key_pos && isspace(*key_pos)) key_pos++; + + if (*key_pos != '"') return NULL; + + key_pos++; // Skip opening quote + + // Find closing quote + char *end = key_pos; + while (*end && *end != '"') { + if (*end == '\\' && *(end + 1)) { + end += 2; // Skip escaped character + } else { + end++; + } + } + + if (!*end) return NULL; + + // Allocate and copy the string value + int len = end - key_pos; + char *result = malloc(len + 1); + if (!result) return NULL; + + strncpy(result, key_pos, len); + result[len] = '\0'; + + return result; +} + +/** + * Built-in command: git_status + * Displays the Git status of the current repo + */ +int lsh_git_status(char **args) { + // Show current Git status + char *git_status = get_git_status(); + if (git_status) { + printf("Git Status: %s\n", git_status); + free(git_status); + } else { + printf("Not in a Git repository or Git not available\n"); + } + return 1; +} + +/** + * Built-in command: gg + * Quick access to Git commands + */ +int lsh_gg(char **args) { + if (args[1] == NULL) { + printf("Usage: gg \n"); + printf("Available commands:\n"); + printf(" s - status\n"); + printf(" c - commit\n"); + printf(" p - pull\n"); + printf(" ps - push\n"); + printf(" a - add .\n"); + printf(" l - log\n"); + printf(" d - diff\n"); + printf(" b - branch\n"); + printf(" ch - checkout\n"); + return 1; + } + + // Execute the appropriate git command based on shorthand + if (strcmp(args[1], "s") == 0) { + system("git status"); + } else if (strcmp(args[1], "c") == 0) { + if (args[2] != NULL) { + char command[1024]; + sprintf(command, "git commit -m \"%s\"", args[2]); + system(command); + } else { + system("git commit"); + } + } else if (strcmp(args[1], "p") == 0) { + system("git pull"); + } else if (strcmp(args[1], "ps") == 0) { + system("git push"); + } else if (strcmp(args[1], "a") == 0) { + system("git add ."); + } else if (strcmp(args[1], "l") == 0) { + system("git log --oneline -10"); + } else if (strcmp(args[1], "d") == 0) { + system("git diff"); + } else if (strcmp(args[1], "b") == 0) { + system("git branch"); + } else if (strcmp(args[1], "ch") == 0) { + if (args[2] != NULL) { + char command[1024]; + sprintf(command, "git checkout %s", args[2]); + system(command); + } else { + printf("Please specify a branch to checkout\n"); + } + } else { + printf("Unknown git command shorthand: %s\n", args[1]); + } + + return 1; +} \ No newline at end of file diff --git a/builtins.h b/builtins.h new file mode 100644 index 0000000..04101ad --- /dev/null +++ b/builtins.h @@ -0,0 +1,100 @@ +/** + * builtins.h + * Declarations for all built-in shell commands + */ + +#ifndef BUILTINS_H +#define BUILTINS_H + +#include "common.h" +#include + +// Syntax highlighting definitions using ANSI colors +#define COLOR_DEFAULT ANSI_COLOR_RESET +#define COLOR_KEYWORD ANSI_COLOR_CYAN +#define COLOR_STRING ANSI_COLOR_GREEN +#define COLOR_COMMENT "\x1b[90m" // Dark gray +#define COLOR_NUMBER ANSI_COLOR_MAGENTA +#define COLOR_PREPROCESSOR ANSI_COLOR_YELLOW +#define COLOR_IDENTIFIER "\x1b[97m" // Bright white + +// Add declarations for color functions +void set_color(int color); +void reset_color(); + +// History command implementation +#define HISTORY_SIZE 10 + +// New struct to hold history entries with timestamps +typedef struct { + char *command; + time_t timestamp; +} HistoryEntry; + +// History variables - made extern to be accessible from line_reader.c +extern HistoryEntry command_history[HISTORY_SIZE]; +extern int history_count; +extern int history_index; + +// Built-in command declarations +int lsh_cd(char **args); +int lsh_help(char **args); +int lsh_exit(char **args); +int lsh_dir(char **args); +int lsh_clear(char **args); +int lsh_mkdir(char **args); +int lsh_rmdir(char **args); +int lsh_del(char **args); +int lsh_touch(char **args); +int lsh_pwd(char **args); +int lsh_cat(char **args); +int lsh_history(char **args); +int lsh_copy(char **args); +int lsh_paste(char **args); +int lsh_move(char **args); +int lsh_ps(char **args); +int lsh_news(char **args); +// Alias command declarations - added for alias support +int lsh_alias(char **args); +int lsh_unalias(char **args); +int lsh_aliases(char **args); // New command to list all aliases +// Bookmark command declarations - added for bookmark support +int lsh_bookmark(char **args); +int lsh_bookmarks(char **args); +int lsh_goto(char **args); +int lsh_unbookmark(char **args); +int lsh_focus_timer(char **args); +int lsh_weather(char **args); +// Text search command +int lsh_grep(char **args); +int lsh_actual_grep(char **args); +int lsh_ripgrep(char **args); +int lsh_fzf_native(char **args); +int lsh_clip(char **args); +int lsh_echo(char **args); +int lsh_self_destruct(); +int lsh_theme(char **args); +int lsh_loc(char **args); +int lsh_git_status(char **args); +int lsh_gg(char **args); + +// Add command to history +void lsh_add_to_history(const char *command); + +// Get number of builtin commands +int lsh_num_builtins(void); + +// Expose the builtin command strings and function pointers +extern char *builtin_str[]; +extern int (*builtin_func[])(char **); + +/** + * Extract a string value from a JSON object + * + * @param json The JSON string to parse + * @param key The key to find + * @return Allocated string containing the value (caller must free) + */ +char *extract_json_string(const char *json, const char *key); + +#endif // BUILTINS_H \ No newline at end of file diff --git a/builtins.o b/builtins.o new file mode 100644 index 0000000..c8b5afb Binary files /dev/null and b/builtins.o differ diff --git a/common.h b/common.h new file mode 100644 index 0000000..f490181 --- /dev/null +++ b/common.h @@ -0,0 +1,115 @@ +/** + * common.h + * Common includes, defines and macros used throughout the shell + */ + +#ifndef COMMON_H +#define COMMON_H + +// Standard library includes +#include +#include +#include +#include // For isprint +#include // For file control +#include // For POSIX API (replaces direct.h) +#include +#include // For waitpid +#include // For stat (replaces fileapi.h) +#include // For terminal control +#include // For console dimensions +#include // For signal handling +#include // For time functions +#include // For PATH_MAX + +// Define PATH_MAX if not defined +#ifndef PATH_MAX +#define PATH_MAX 4096 +#endif + +// Common defines +#define LSH_RL_BUFSIZE 1024 +#define LSH_TOK_BUFSIZE 64 +#define LSH_TOK_DELIM " \t\r\n\a" + +// Terminal control and color codes (ANSI sequences) +#define ANSI_COLOR_RESET "\x1b[0m" +#define ANSI_COLOR_BLACK "\x1b[30m" +#define ANSI_COLOR_RED "\x1b[31m" +#define ANSI_COLOR_GREEN "\x1b[32m" +#define ANSI_COLOR_YELLOW "\x1b[33m" +#define ANSI_COLOR_BLUE "\x1b[34m" +#define ANSI_COLOR_MAGENTA "\x1b[35m" +#define ANSI_COLOR_CYAN "\x1b[36m" +#define ANSI_COLOR_WHITE "\x1b[37m" + +// Background colors +#define ANSI_BG_BLACK "\x1b[40m" +#define ANSI_BG_RED "\x1b[41m" +#define ANSI_BG_GREEN "\x1b[42m" +#define ANSI_BG_YELLOW "\x1b[43m" +#define ANSI_BG_BLUE "\x1b[44m" +#define ANSI_BG_MAGENTA "\x1b[45m" +#define ANSI_BG_CYAN "\x1b[46m" +#define ANSI_BG_WHITE "\x1b[47m" + +// Cursor movement and screen control +#define ANSI_CLEAR_SCREEN "\x1b[2J" +#define ANSI_CURSOR_HOME "\x1b[H" +#define ANSI_SAVE_CURSOR "\x1b[s" +#define ANSI_RESTORE_CURSOR "\x1b[u" + +// Key code definitions used by the line reader +#define KEY_BACKSPACE 127 // Different on Linux +#define KEY_TAB 9 +#define KEY_ENTER 10 // Linux: 10 (LF) or 13 (CR) +#define KEY_ESCAPE 27 + +// Special keys (Linux terminal uses escape sequences) +// These will need handling in the line reader +#define KEY_UP 1000 +#define KEY_DOWN 1001 +#define KEY_LEFT 1002 +#define KEY_RIGHT 1003 +#define KEY_SHIFT_ENTER 1010 + +// Typedefs for compatibility +typedef unsigned int UINT; +typedef unsigned short WORD; +typedef int BOOL; +typedef int HANDLE; +typedef int DWORD; + +// Define Windows constants +#define FALSE 0 +#define TRUE 1 + +// Struct to replace COORD +typedef struct { + short X; + short Y; +} COORD; + +// Struct to replace SMALL_RECT +typedef struct { + short Left; + short Top; + short Right; + short Bottom; +} SMALL_RECT; + +// Struct to replace CONSOLE_SCREEN_BUFFER_INFO +typedef struct { + COORD dwSize; + COORD dwCursorPosition; + WORD wAttributes; + SMALL_RECT srWindow; +} CONSOLE_SCREEN_BUFFER_INFO; + +// Struct to replace CONSOLE_CURSOR_INFO +typedef struct { + DWORD dwSize; + BOOL bVisible; +} CONSOLE_CURSOR_INFO; + +#endif // COMMON_H diff --git a/countdown_timer.c b/countdown_timer.c new file mode 100644 index 0000000..d238f2a --- /dev/null +++ b/countdown_timer.c @@ -0,0 +1,306 @@ +/** + * countdown_timer.c + * Implementation of a focus timer that integrates with the shell status bar + */ + +#include "countdown_timer.h" +#include "common.h" +#include "shell.h" +#include "themes.h" +#include +#include +#include +#include +#include +#include +#include +#include + +// Global timer state +static struct { + BOOL is_active; // Whether timer is currently running + time_t end_time; // When timer will expire (in seconds since epoch) + char display_text[64]; // Current timer display text + char session_name[128]; // Optional session name + pthread_t timer_thread; // Timer thread ID + BOOL should_exit; // Flag to signal thread exit + BOOL is_temporarily_hidden; // Flag to hide timer while running external programs +} timer_state = {FALSE, 0, "", "", 0, FALSE, FALSE}; + +/** + * Update timer display text based on remaining time + */ +static void update_timer_display(void) { + if (!timer_state.is_active) { + strcpy(timer_state.display_text, ""); + return; + } + + // Calculate remaining time + time_t now = time(NULL); + int seconds_left = (int)difftime(timer_state.end_time, now); + + if (seconds_left <= 0) { + // Timer has expired + if (strlen(timer_state.session_name) > 0) { + snprintf(timer_state.display_text, sizeof(timer_state.display_text), + "Session '%s' complete!", timer_state.session_name); + } else { + strcpy(timer_state.display_text, "Timer complete!"); + } + return; + } + + // Format time as MM:SS + int minutes = seconds_left / 60; + int seconds = seconds_left % 60; + + if (strlen(timer_state.session_name) > 0) { + snprintf(timer_state.display_text, sizeof(timer_state.display_text), + "%s: %02d:%02d remaining", timer_state.session_name, minutes, seconds); + } else { + snprintf(timer_state.display_text, sizeof(timer_state.display_text), + "Focus: %02d:%02d remaining", minutes, seconds); + } +} + +/** + * Timer thread function + */ +static void *timer_thread_func(void *param) { + while (!timer_state.should_exit) { + // Update timer display + update_timer_display(); + + // Check if timer has expired + time_t now = time(NULL); + if (difftime(timer_state.end_time, now) <= 0) { + // Play a bell sound + printf("\a"); + fflush(stdout); + + // Update status bar with completion message + update_timer_display(); + + // Keep thread running to display completion message + } + + // Sleep for 1 second + sleep(1); + } + + return NULL; +} + +/** + * Start a new countdown timer + */ +int start_countdown_timer(int seconds, const char *name) { + // Stop any existing timer + if (timer_state.is_active) { + stop_countdown_timer(); + } + + // Set timer parameters + timer_state.end_time = time(NULL) + seconds; + timer_state.is_active = TRUE; + timer_state.should_exit = FALSE; + timer_state.is_temporarily_hidden = FALSE; + + // Set optional session name + if (name && *name) { + strncpy(timer_state.session_name, name, sizeof(timer_state.session_name) - 1); + timer_state.session_name[sizeof(timer_state.session_name) - 1] = '\0'; + } else { + timer_state.session_name[0] = '\0'; + } + + // Initialize display text + update_timer_display(); + + // Create timer thread + int ret = pthread_create(&timer_state.timer_thread, NULL, timer_thread_func, NULL); + if (ret != 0) { + fprintf(stderr, "Failed to create timer thread: %s\n", strerror(ret)); + timer_state.is_active = FALSE; + return 0; + } + + // Detach thread so it cleans up automatically + pthread_detach(timer_state.timer_thread); + + return 1; +} + +/** + * Stop the current countdown timer + */ +void stop_countdown_timer() { + if (!timer_state.is_active) { + return; + } + + // Signal thread to exit + timer_state.should_exit = TRUE; + + // Wait for thread to terminate (with timeout) + // Use a more portable approach since pthread_timedjoin_np is GNU-specific + + // Set a timeout of 2 seconds + struct timespec timeout; + timeout.tv_sec = 2; + timeout.tv_nsec = 0; + + // Sleep for the timeout period - the thread will keep running + // and clean itself up since we detached it + nanosleep(&timeout, NULL); + + // Reset timer state + timer_state.is_active = FALSE; + timer_state.display_text[0] = '\0'; + timer_state.session_name[0] = '\0'; +} + +/** + * Check if a timer is currently active + */ +BOOL is_timer_active() { + return timer_state.is_active; +} + +/** + * Get the current timer display text + */ +const char *get_timer_display() { + if (timer_state.is_temporarily_hidden || !timer_state.is_active) { + return ""; + } + return timer_state.display_text; +} + +/** + * Temporarily hide the timer display + */ +void hide_timer_display(void) { + timer_state.is_temporarily_hidden = TRUE; +} + +/** + * Restore the timer display after it was hidden + */ +void show_timer_display(void) { + timer_state.is_temporarily_hidden = FALSE; +} + +/** + * Parse time string in format "XhYmZs" to seconds + */ +static int parse_time_string(const char *time_str) { + if (!time_str || !*time_str) { + return 0; + } + + int total_seconds = 0; + char *str = strdup(time_str); + if (!str) { + return 0; + } + + char *pos = str; + char *next_pos; + int value; + + while (*pos) { + // Skip non-digits + while (*pos && !isdigit(*pos)) { + pos++; + } + + if (!*pos) { + break; + } + + // Parse number + value = (int)strtol(pos, &next_pos, 10); + if (pos == next_pos) { + break; + } + + // Skip whitespace + pos = next_pos; + while (*pos && isspace(*pos)) { + pos++; + } + + // Check unit + if (*pos == 'h' || *pos == 'H') { + total_seconds += value * 3600; + } else if (*pos == 'm' || *pos == 'M') { + total_seconds += value * 60; + } else if (*pos == 's' || *pos == 'S') { + total_seconds += value; + } else { + // No unit specified, assume minutes + total_seconds += value * 60; + } + + // Move to next part + if (*pos) { + pos++; + } + } + + free(str); + return total_seconds; +} + +/** + * Command handler for the focus timer command + */ +int lsh_focus_timer(char **args) { + if (!args[1]) { + // No arguments - show help + printf("Usage: focus_timer [start|stop] [duration] [session name]\n"); + printf("Examples:\n"); + printf(" focus_timer start 25m \"Coding session\" # Start a 25-minute timer\n"); + printf(" focus_timer start 1h30m # Start a 1 hour 30 minute timer\n"); + printf(" focus_timer stop # Stop the current timer\n"); + printf("Current status: %s\n", + timer_state.is_active ? timer_state.display_text : "No active timer"); + return 1; + } + + if (strcmp(args[1], "stop") == 0) { + if (timer_state.is_active) { + printf("Stopping timer...\n"); + stop_countdown_timer(); + } else { + printf("No active timer to stop.\n"); + } + return 1; + } + + if (strcmp(args[1], "start") == 0) { + if (!args[2]) { + printf("Error: Duration required. Example: focus_timer start 25m\n"); + return 1; + } + + int seconds = parse_time_string(args[2]); + if (seconds <= 0) { + printf("Error: Invalid duration format. Examples: 25m, 1h30m, 90s\n"); + return 1; + } + + const char *session_name = args[3] ? args[3] : ""; + if (start_countdown_timer(seconds, session_name)) { + printf("Timer started: %s\n", timer_state.display_text); + } else { + printf("Failed to start timer.\n"); + } + return 1; + } + + printf("Unknown timer command: %s\n", args[1]); + return 1; +} \ No newline at end of file diff --git a/countdown_timer.h b/countdown_timer.h new file mode 100644 index 0000000..6db1d2d --- /dev/null +++ b/countdown_timer.h @@ -0,0 +1,57 @@ +/** + * countdown_timer.h + * Header for focus timer functionality + */ + +#ifndef COUNTDOWN_TIMER_H +#define COUNTDOWN_TIMER_H + +#include "common.h" + +/** + * Start a new countdown timer + * + * @param seconds Total duration in seconds + * @param name Optional name for the timer session + * @return 1 if successful, 0 on failure + */ +int start_countdown_timer(int seconds, const char *name); + +/** + * Stop the current countdown timer + */ +void stop_countdown_timer(); + +/** + * Check if a timer is currently active + * + * @return TRUE if timer is active, FALSE otherwise + */ +BOOL is_timer_active(); + +/** + * Get the current timer display text + * + * @return Pointer to the display text string + */ +const char *get_timer_display(); + +/** + * Temporarily hide the timer display (used when running external programs) + */ +void hide_timer_display(void); + +/** + * Restore the timer display after it was hidden + */ +void show_timer_display(void); + +/** + * Command handler for the "timer" command + * + * @param args Command arguments + * @return 1 to continue shell, 0 to exit + */ +int lsh_focus_timer(char **args); + +#endif // COUNTDOWN_TIMER_H diff --git a/countdown_timer.o b/countdown_timer.o new file mode 100644 index 0000000..eeb0f8a Binary files /dev/null and b/countdown_timer.o differ diff --git a/favorite_cities.c b/favorite_cities.c new file mode 100644 index 0000000..b289e73 --- /dev/null +++ b/favorite_cities.c @@ -0,0 +1,324 @@ +/** + * favorite_cities.c + * Implementation of favorite cities management for weather lookup + */ + +#include "favorite_cities.h" +#include "builtins.h" +#include "common.h" + +// Global variables for favorite cities storage +CityEntry *favorite_cities = NULL; +int favorite_city_count = 0; +int favorite_city_capacity = 0; + +// Path to the favorite cities file +char favorite_cities_file_path[PATH_MAX]; + +/** + * Initialize the favorite cities system + */ +void init_favorite_cities(void) { + // Set initial capacity + favorite_city_capacity = 10; + favorite_cities = + (CityEntry *)malloc(favorite_city_capacity * sizeof(CityEntry)); + + if (!favorite_cities) { + fprintf(stderr, "lsh: allocation error in init_favorite_cities\n"); + return; + } + + // Determine favorite cities file location - in user's home directory + char *home_dir = getenv("HOME"); + if (home_dir) { + snprintf(favorite_cities_file_path, PATH_MAX, "%s/.lsh_favorite_cities", + home_dir); + } else { + // Fallback to current directory if HOME not available + strcpy(favorite_cities_file_path, ".lsh_favorite_cities"); + } + + // Load favorite cities from file + load_favorite_cities(); + + // Add some default cities if none exist + if (favorite_city_count == 0) { + add_favorite_city("New York"); + add_favorite_city("London"); + add_favorite_city("Tokyo"); + add_favorite_city("Paris"); + add_favorite_city("Sydney"); + save_favorite_cities(); + } +} + +/** + * Clean up the favorite cities system + */ +void cleanup_favorite_cities(void) { + if (!favorite_cities) + return; + + free(favorite_cities); + favorite_cities = NULL; + favorite_city_count = 0; + favorite_city_capacity = 0; +} + +/** + * Shutdown favorite cities system - save and cleanup + */ +void shutdown_favorite_cities(void) { + // Save to file first + save_favorite_cities(); + + // Then clean up resources + cleanup_favorite_cities(); +} + +/** + * Load favorite cities from file + */ +int load_favorite_cities(void) { + FILE *file = fopen(favorite_cities_file_path, "r"); + if (!file) { + // File doesn't exist yet - not an error + return 1; + } + + // Clear existing favorite cities + favorite_city_count = 0; + + char line[128]; + int line_number = 0; + + while (fgets(line, sizeof(line), file)) { + line_number++; + + // Remove newline + size_t len = strlen(line); + if (len > 0 && line[len - 1] == '\n') { + line[len - 1] = '\0'; + len--; + } + + // Skip empty lines and comments + if (len == 0 || line[0] == '#') + continue; + + // Add the city + add_favorite_city(line); + } + + fclose(file); + return 1; +} + +/** + * Save favorite cities to file + */ +int save_favorite_cities(void) { + FILE *file = fopen(favorite_cities_file_path, "w"); + if (!file) { + fprintf(stderr, "lsh: error: could not save favorite cities to %s\n", + favorite_cities_file_path); + return 0; + } + + // Write header comment + fprintf(file, "# LSH favorite cities file\n"); + fprintf(file, + "# Each line represents a favorite city for weather lookups\n\n"); + + // Write each city + for (int i = 0; i < favorite_city_count; i++) { + fprintf(file, "%s\n", favorite_cities[i].name); + } + + fclose(file); + return 1; +} + +/** + * Add a new favorite city + */ +int add_favorite_city(const char *name) { + if (!name) + return 0; + + // Check if we need to resize the array + if (favorite_city_count >= favorite_city_capacity) { + favorite_city_capacity *= 2; + CityEntry *new_cities = (CityEntry *)realloc( + favorite_cities, favorite_city_capacity * sizeof(CityEntry)); + if (!new_cities) { + fprintf(stderr, "lsh: allocation error in add_favorite_city\n"); + return 0; + } + favorite_cities = new_cities; + } + + // Check if the city already exists + for (int i = 0; i < favorite_city_count; i++) { + if (strcasecmp(favorite_cities[i].name, name) == 0) { + // City already exists + return 1; + } + } + + // Add new city + strncpy(favorite_cities[favorite_city_count].name, name, + sizeof(favorite_cities[favorite_city_count].name) - 1); + favorite_cities[favorite_city_count] + .name[sizeof(favorite_cities[favorite_city_count].name) - 1] = '\0'; + favorite_city_count++; + + return 1; +} + +/** + * Remove a favorite city + */ +int remove_favorite_city(const char *name) { + if (!name) + return 0; + + for (int i = 0; i < favorite_city_count; i++) { + if (strcasecmp(favorite_cities[i].name, name) == 0) { + // Shift remaining cities + for (int j = i; j < favorite_city_count - 1; j++) { + strcpy(favorite_cities[j].name, favorite_cities[j + 1].name); + } + + favorite_city_count--; + return 1; + } + } + + return 0; // City not found +} + +/** + * Find a favorite city by name + */ +CityEntry *find_favorite_city(const char *name) { + if (!name) + return NULL; + + for (int i = 0; i < favorite_city_count; i++) { + if (strcasecmp(favorite_cities[i].name, name) == 0) { + return &favorite_cities[i]; + } + } + + return NULL; +} + +/** + * Get favorite city names for tab completion + */ +char **get_favorite_city_names(int *count) { + if (favorite_city_count == 0) { + *count = 0; + return NULL; + } + + char **names = (char **)malloc(favorite_city_count * sizeof(char *)); + if (!names) { + *count = 0; + return NULL; + } + + for (int i = 0; i < favorite_city_count; i++) { + names[i] = strdup(favorite_cities[i].name); + } + + *count = favorite_city_count; + return names; +} + +/** + * Command handler for the "cities" command + * Usage: + * cities add + * cities remove + * cities list + */ +int lsh_cities(char **args) { + if (args[1] == NULL) { + // No subcommand - show usage + printf("Usage: cities [arguments]\n"); + printf("Commands:\n"); + printf(" add Add a city to favorites\n"); + printf(" remove Remove a city from favorites\n"); + printf(" list List all favorite cities\n"); + return 1; + } + + // Handle subcommands + if (strcmp(args[1], "add") == 0) { + if (args[2] == NULL) { + printf("Usage: cities add \n"); + return 1; + } + + // Combine all arguments after "add" into a single city name + char city_name[256] = ""; + strncpy(city_name, args[2], sizeof(city_name) - 1); + + for (int i = 3; args[i] != NULL; i++) { + strncat(city_name, " ", sizeof(city_name) - strlen(city_name) - 1); + strncat(city_name, args[i], sizeof(city_name) - strlen(city_name) - 1); + } + + if (add_favorite_city(city_name)) { + save_favorite_cities(); + printf("Added '%s' to favorite cities\n", city_name); + } + + } else if (strcmp(args[1], "remove") == 0) { + if (args[2] == NULL) { + printf("Usage: cities remove \n"); + return 1; + } + + // Combine all arguments after "remove" into a single city name + char city_name[256] = ""; + strncpy(city_name, args[2], sizeof(city_name) - 1); + + for (int i = 3; args[i] != NULL; i++) { + strncat(city_name, " ", sizeof(city_name) - strlen(city_name) - 1); + strncat(city_name, args[i], sizeof(city_name) - strlen(city_name) - 1); + } + + if (remove_favorite_city(city_name)) { + save_favorite_cities(); + printf("Removed '%s' from favorite cities\n", city_name); + } else { + printf("City '%s' not found in favorites\n", city_name); + } + + } else if (strcmp(args[1], "list") == 0) { + // List all favorite cities + if (favorite_city_count == 0) { + printf("No favorite cities defined\n"); + return 1; + } + + printf("\nFavorite Cities:\n\n"); + + // Display cities in a nice format + for (int i = 0; i < favorite_city_count; i++) { + // Print city name with color + printf(ANSI_COLOR_CYAN " %s" ANSI_COLOR_RESET "\n", favorite_cities[i].name); + } + printf("\n"); + + } else { + printf("Unknown command: %s\n", args[1]); + printf("Available commands: add, remove, list\n"); + } + + return 1; +} diff --git a/favorite_cities.h b/favorite_cities.h new file mode 100644 index 0000000..1ff21d2 --- /dev/null +++ b/favorite_cities.h @@ -0,0 +1,50 @@ +/** + * favorite_cities.h + * Functions for managing favorite cities for weather lookups + */ + +#ifndef FAVORITE_CITIES_H +#define FAVORITE_CITIES_H + +#include "common.h" + +// Structure to represent a favorite city +typedef struct { + char name[128]; // City name +} CityEntry; + +// Initialize favorite cities system +void init_favorite_cities(void); + +// Clean up favorite cities system +void cleanup_favorite_cities(void); + +// Shutdown favorite cities system (save and cleanup) +void shutdown_favorite_cities(void); + +// Load favorite cities from file +int load_favorite_cities(void); + +// Save favorite cities to file +int save_favorite_cities(void); + +// Add a new favorite city +int add_favorite_city(const char *name); + +// Remove a favorite city +int remove_favorite_city(const char *name); + +// Find a favorite city by name +CityEntry *find_favorite_city(const char *name); + +// Get favorite city names for tab completion +char **get_favorite_city_names(int *count); + +// Command handlers +int lsh_cities(char **args); // Main cities command handler + +// Global favorite cities array +extern CityEntry *favorite_cities; +extern int favorite_city_count; + +#endif // FAVORITE_CITIES_H diff --git a/favorite_cities.o b/favorite_cities.o new file mode 100644 index 0000000..e1e60d6 Binary files /dev/null and b/favorite_cities.o differ diff --git a/filters.c b/filters.c new file mode 100644 index 0000000..8cc1e8b --- /dev/null +++ b/filters.c @@ -0,0 +1,510 @@ +/** + * filters.c + * Implementation of filter commands (where, sort-by, etc.) + */ + +#include "filters.h" +#include +#include // For strcasecmp and strncasecmp + +/** + * Filter a table based on a condition (e.g., where size > 10kb) + */ +TableData* lsh_where(TableData *input, char **args) { + if (!input || !args || !args[0]) { + fprintf(stderr, "lsh: where: missing arguments\n"); + fprintf(stderr, "Usage: ... | where FIELD OPERATOR VALUE\n"); + fprintf(stderr, " e.g.: ls | where size > 10kb\n"); + return NULL; + } + + // Parse filter condition (field, operator, value) + char *field = args[0]; + char *op = NULL; + char *value = NULL; + + // Find operator and value + if (args[1] != NULL) { + if (strcmp(args[1], ">") == 0 || + strcmp(args[1], "<") == 0 || + strcmp(args[1], "==") == 0 || + strcmp(args[1], ">=") == 0 || + strcmp(args[1], "<=") == 0) { + op = args[1]; + value = args[2]; + } + } + + if (!op || !value) { + fprintf(stderr, "lsh: where: invalid filter condition\n"); + fprintf(stderr, "Usage: ... | where FIELD OPERATOR VALUE\n"); + fprintf(stderr, " e.g.: ls | where size > 10kb\n"); + return NULL; + } + + // Find field index + int field_idx = -1; + for (int i = 0; i < input->header_count; i++) { + if (strcasecmp(input->headers[i], field) == 0) { + field_idx = i; + break; + } + } + + if (field_idx == -1) { + fprintf(stderr, "lsh: where: unknown field '%s'\n", field); + fprintf(stderr, "Available fields: "); + for (int i = 0; i < input->header_count; i++) { + fprintf(stderr, "%s%s", i > 0 ? ", " : "", input->headers[i]); + } + fprintf(stderr, "\n"); + return NULL; + } + + // Create new filtered table + return filter_table(input, field, op, value); +} + +/** + * Sort a table based on a column (e.g., sort-by size desc) + */ +TableData* lsh_sort_by(TableData *input, char **args) { + if (!input || !args || !args[0]) { + fprintf(stderr, "lsh: sort-by: missing arguments\n"); + fprintf(stderr, "Usage: ... | sort-by FIELD [asc|desc]\n"); + fprintf(stderr, " e.g.: ls | sort-by size desc\n"); + return NULL; + } + + // Parse sort field and direction + char *field = args[0]; + int descending = 0; + + // Check if direction is specified + if (args[1] != NULL) { + if (strcasecmp(args[1], "desc") == 0 || + strcasecmp(args[1], "descending") == 0) { + descending = 1; + } + } + + // Find field index + int field_idx = -1; + for (int i = 0; i < input->header_count; i++) { + if (strcasecmp(input->headers[i], field) == 0) { + field_idx = i; + break; + } + } + + if (field_idx == -1) { + fprintf(stderr, "lsh: sort-by: unknown field '%s'\n", field); + fprintf(stderr, "Available fields: "); + for (int i = 0; i < input->header_count; i++) { + fprintf(stderr, "%s%s", i > 0 ? ", " : "", input->headers[i]); + } + fprintf(stderr, "\n"); + return NULL; + } + + // Create a copy of the table + TableData *result = create_table(input->headers, input->header_count); + if (!result) { + return NULL; + } + + // Copy all rows to the new table + for (int i = 0; i < input->row_count; i++) { + DataValue *row_copy = (DataValue*)malloc(input->header_count * sizeof(DataValue)); + if (!row_copy) { + fprintf(stderr, "lsh: allocation error in sort_by\n"); + free_table(result); + return NULL; + } + + for (int j = 0; j < input->header_count; j++) { + row_copy[j] = copy_data_value(&input->rows[i][j]); + } + + add_table_row(result, row_copy); + } + + // Now sort the rows based on the specified column + // Bubble sort for simplicity (not efficient for large datasets) + for (int i = 0; i < result->row_count - 1; i++) { + for (int j = 0; j < result->row_count - i - 1; j++) { + int compare_result = 0; + + // Compare based on data type + if (result->rows[j][field_idx].type == TYPE_STRING || + result->rows[j][field_idx].type == TYPE_SIZE) { + // Handle special case for sizes (KB, MB, etc.) + if (strcasecmp(result->headers[field_idx], "Size") == 0 || + strcasecmp(result->headers[field_idx], "Memory") == 0) { + long size1 = extract_size_bytes(result->rows[j][field_idx].value.str_val); + long size2 = extract_size_bytes(result->rows[j+1][field_idx].value.str_val); + compare_result = (size1 > size2) ? 1 : (size1 < size2) ? -1 : 0; + } else { + // Regular string comparison + compare_result = strcasecmp( + result->rows[j][field_idx].value.str_val, + result->rows[j+1][field_idx].value.str_val + ); + } + } else if (result->rows[j][field_idx].type == TYPE_INT) { + int val1 = result->rows[j][field_idx].value.int_val; + int val2 = result->rows[j+1][field_idx].value.int_val; + compare_result = (val1 > val2) ? 1 : (val1 < val2) ? -1 : 0; + } else if (result->rows[j][field_idx].type == TYPE_FLOAT) { + float val1 = result->rows[j][field_idx].value.float_val; + float val2 = result->rows[j+1][field_idx].value.float_val; + compare_result = (val1 > val2) ? 1 : (val1 < val2) ? -1 : 0; + } + + // If descending, invert comparison result + if (descending) { + compare_result = -compare_result; + } + + // Swap if needed + if (compare_result > 0) { + DataValue *temp = result->rows[j]; + result->rows[j] = result->rows[j+1]; + result->rows[j+1] = temp; + } + } + } + + return result; +} + +/** + * Select specific columns from a table + * Supports both space-separated and comma-separated field names + */ +TableData* lsh_select(TableData *input, char **args) { + if (!input || !args || !args[0]) { + fprintf(stderr, "lsh: select: missing arguments\n"); + fprintf(stderr, "Usage: ... | select FIELD1 FIELD2 ...\n"); + fprintf(stderr, " e.g.: ls | select Name Size\n"); + return NULL; + } + + // Count how many fields we have + int num_fields = 0; + int arg_index = 0; + + // First pass: count fields + while (args[arg_index] != NULL) { + // Check if this argument contains commas + char *p = args[arg_index]; + int commas_in_arg = 0; + + while (*p) { + if (*p == ',') { + commas_in_arg++; + } + p++; + } + + if (commas_in_arg > 0) { + // Add number of comma-separated fields (commas + 1) + num_fields += commas_in_arg + 1; + } else { + // Just one field in this argument + num_fields++; + } + + arg_index++; + } + + // Allocate array for field indices + int *field_indices = (int*)malloc(num_fields * sizeof(int)); + if (!field_indices) { + fprintf(stderr, "lsh: allocation error in select\n"); + return NULL; + } + + // Second pass: process field names + int field_count = 0; + arg_index = 0; + + while (args[arg_index] != NULL && field_count < num_fields) { + // Make a copy since we'll modify it with strtok + char *arg_copy = strdup(args[arg_index]); + if (!arg_copy) { + fprintf(stderr, "lsh: allocation error in select\n"); + free(field_indices); + return NULL; + } + + // Check if this argument contains commas + if (strchr(arg_copy, ',')) { + // Handle comma-separated fields + char *field_token; + char *saveptr; + + field_token = strtok_r(arg_copy, ",", &saveptr); + while (field_token && field_count < num_fields) { + // Trim whitespace + while (isspace(*field_token)) field_token++; + char *end = field_token + strlen(field_token) - 1; + while (end > field_token && isspace(*end)) end--; + *(end + 1) = '\0'; + + // Find field index + int found = 0; + for (int i = 0; i < input->header_count; i++) { + if (strcasecmp(input->headers[i], field_token) == 0) { + field_indices[field_count++] = i; + found = 1; + break; + } + } + + if (!found) { + fprintf(stderr, "lsh: select: unknown field '%s'\n", field_token); + free(field_indices); + free(arg_copy); + return NULL; + } + + field_token = strtok_r(NULL, ",", &saveptr); + } + } else { + // This is a single field name (no commas) + char *field = arg_copy; + + // Trim whitespace + while (isspace(*field)) field++; + char *end = field + strlen(field) - 1; + while (end > field && isspace(*end)) end--; + *(end + 1) = '\0'; + + // Find field index + int found = 0; + for (int i = 0; i < input->header_count; i++) { + if (strcasecmp(input->headers[i], field) == 0) { + field_indices[field_count++] = i; + found = 1; + break; + } + } + + if (!found) { + fprintf(stderr, "lsh: select: unknown field '%s'\n", field); + free(arg_copy); + free(field_indices); + return NULL; + } + } + + free(arg_copy); + arg_index++; + } + + // Create new headers for result table + char **new_headers = (char**)malloc(field_count * sizeof(char*)); + if (!new_headers) { + fprintf(stderr, "lsh: allocation error in select\n"); + free(field_indices); + return NULL; + } + + for (int i = 0; i < field_count; i++) { + new_headers[i] = strdup(input->headers[field_indices[i]]); + } + + // Create result table with new headers + TableData *result = create_table(new_headers, field_count); + + // Free temporary header array + for (int i = 0; i < field_count; i++) { + free(new_headers[i]); + } + free(new_headers); + + if (!result) { + free(field_indices); + return NULL; + } + + // Copy rows with only the selected columns + for (int i = 0; i < input->row_count; i++) { + DataValue *row = (DataValue*)malloc(field_count * sizeof(DataValue)); + if (!row) { + fprintf(stderr, "lsh: allocation error in select\n"); + free_table(result); + free(field_indices); + return NULL; + } + + for (int j = 0; j < field_count; j++) { + row[j] = copy_data_value(&input->rows[i][field_indices[j]]); + } + + add_table_row(result, row); + } + + free(field_indices); + return result; +} + +/** + * Case-insensitive substring search (strcasestr equivalent for Windows) + */ +char *my_strcasestr(const char *haystack, const char *needle) { + if (!haystack || !needle) return NULL; + + size_t needle_len = strlen(needle); + if (needle_len == 0) return (char*)haystack; + + size_t haystack_len = strlen(haystack); + if (haystack_len < needle_len) return NULL; + + for (size_t i = 0; i <= haystack_len - needle_len; i++) { + if (strncasecmp(haystack + i, needle, needle_len) == 0) { + return (char*)(haystack + i); + } + } + + return NULL; +} + +/** + * Filter rows where a column contains a substring + */ +TableData* lsh_contains(TableData *input, char **args) { + if (!input || !args || !args[0] || !args[1]) { + fprintf(stderr, "lsh: contains: missing arguments\n"); + fprintf(stderr, "Usage: ... | contains FIELD VALUE\n"); + fprintf(stderr, " e.g.: ls | contains Name .exe\n"); + return NULL; + } + + // Parse arguments + char *field = args[0]; + char *value = args[1]; + + // Find field index + int field_idx = -1; + for (int i = 0; i < input->header_count; i++) { + if (strcasecmp(input->headers[i], field) == 0) { + field_idx = i; + break; + } + } + + if (field_idx == -1) { + fprintf(stderr, "lsh: contains: unknown field '%s'\n", field); + fprintf(stderr, "Available fields: "); + for (int i = 0; i < input->header_count; i++) { + fprintf(stderr, "%s%s", i > 0 ? ", " : "", input->headers[i]); + } + fprintf(stderr, "\n"); + return NULL; + } + + // Create result table with same headers + TableData *result = create_table(input->headers, input->header_count); + if (!result) { + return NULL; + } + + // Filter rows based on whether the specified column contains the substring + for (int i = 0; i < input->row_count; i++) { + int include_row = 0; + + // Only string fields can be checked with contains + if (input->rows[i][field_idx].type == TYPE_STRING || + input->rows[i][field_idx].type == TYPE_SIZE) { + char *cell_value = input->rows[i][field_idx].value.str_val; + + // Case-insensitive substring check + char *found = my_strcasestr(cell_value, value); + include_row = (found != NULL); + } + + if (include_row) { + // Create a copy of the row for the result table + DataValue *row_copy = (DataValue*)malloc(input->header_count * sizeof(DataValue)); + if (!row_copy) { + fprintf(stderr, "lsh: allocation error in contains\n"); + free_table(result); + return NULL; + } + + for (int j = 0; j < input->header_count; j++) { + row_copy[j] = copy_data_value(&input->rows[i][j]); + } + + add_table_row(result, row_copy); + } + } + + return result; +} + +/** + * Limit the number of rows displayed + */ +TableData* lsh_limit(TableData *input, char **args) { + if (!input || !args || !args[0]) { + fprintf(stderr, "lsh: limit: missing arguments\n"); + fprintf(stderr, "Usage: ... | limit N\n"); + fprintf(stderr, " e.g.: ls | sort-by Size desc | limit 5\n"); + return NULL; + } + + // Parse limit + int limit = atoi(args[0]); + if (limit <= 0) { + fprintf(stderr, "lsh: limit: invalid limit '%s', must be a positive number\n", args[0]); + return NULL; + } + + // Create result table with same headers + TableData *result = create_table(input->headers, input->header_count); + if (!result) { + return NULL; + } + + // Copy only the specified number of rows + int rows_to_copy = (limit < input->row_count) ? limit : input->row_count; + + for (int i = 0; i < rows_to_copy; i++) { + DataValue *row_copy = (DataValue*)malloc(input->header_count * sizeof(DataValue)); + if (!row_copy) { + fprintf(stderr, "lsh: allocation error in limit\n"); + free_table(result); + return NULL; + } + + for (int j = 0; j < input->header_count; j++) { + row_copy[j] = copy_data_value(&input->rows[i][j]); + } + + add_table_row(result, row_copy); + } + + return result; +} + +// Define the filter arrays here +char *filter_str[] = { + "where", + "sort-by", + "select", + "contains", + "limit" +}; + +TableData* (*filter_func[]) (TableData*, char**) = { + &lsh_where, + &lsh_sort_by, + &lsh_select, + &lsh_contains, + &lsh_limit +}; + +int filter_count = sizeof(filter_str) / sizeof(char*); diff --git a/filters.h b/filters.h new file mode 100644 index 0000000..512d2ca --- /dev/null +++ b/filters.h @@ -0,0 +1,66 @@ +/** + * filters.h + * Declarations for filter commands (where, sort-by, etc.) + */ + +#ifndef FILTERS_H +#define FILTERS_H + +#include "structured_data.h" + +/** + * Filter a table based on a condition (e.g., where size > 10kb) + * + * @param input The input table + * @param args Command arguments + * @return Filtered table or NULL on error + */ +TableData* lsh_where(TableData *input, char **args); + +/** + * Sort a table based on a column (e.g., sort-by size desc) + * + * @param input The input table + * @param args Command arguments + * @return Sorted table or NULL on error + */ +TableData* lsh_sort_by(TableData *input, char **args); + +/** + * Select specific columns from a table + * + * @param input The input table + * @param args Command arguments + * @return Table with selected columns or NULL on error + */ +TableData* lsh_select(TableData *input, char **args); + +/** + * Filter rows where a column contains a substring + * + * @param input The input table + * @param args Command arguments + * @return Filtered table or NULL on error + */ +TableData* lsh_contains(TableData *input, char **args); + +/** + * Limit the number of rows displayed + * + * @param input The input table + * @param args Command arguments + * @return Limited table or NULL on error + */ +TableData* lsh_limit(TableData *input, char **args); + +/** + * Case-insensitive substring search (strcasestr equivalent for Windows) + */ +char *my_strcasestr(const char *haystack, const char *needle); + +// Export the filter arrays for the shell +extern char *filter_str[]; +extern TableData* (*filter_func[]) (TableData*, char**); +extern int filter_count; + +#endif // FILTERS_H diff --git a/filters.o b/filters.o new file mode 100644 index 0000000..4c0ee9a Binary files /dev/null and b/filters.o differ diff --git a/fzf_native.c b/fzf_native.c new file mode 100644 index 0000000..42857dc --- /dev/null +++ b/fzf_native.c @@ -0,0 +1,571 @@ +/** + * fzf_native.c + * Wrapper for the native fzf command-line fuzzy finder + */ + +#include "fzf_native.h" +#include "common.h" +#include "line_reader.h" +#include "shell.h" +#include +#include +#include +#include // For stat + +/** + * Check if native fzf is installed on the system + * + * @return 1 if installed, 0 if not + */ +int is_fzf_installed(void) { + // Try to run fzf --version to check if it's installed + FILE *fp = popen("fzf --version 2>/dev/null", "r"); + if (fp == NULL) { + return 0; + } + + // Read output (if any) + char buffer[128]; + int has_output = (fgets(buffer, sizeof(buffer), fp) != NULL); + + // Close the process + pclose(fp); + + return has_output; +} + +/** + * Display instructions for installing fzf + */ +void show_fzf_install_instructions(void) { + printf("\nfzf is not installed on this system. To use this feature, install " + "fzf:\n\n"); + printf("Installation options:\n"); + printf("1. Using Git:\n"); + printf(" git clone --depth 1 https://github.com/junegunn/fzf.git ~/.fzf\n"); + printf(" ~/.fzf/install\n\n"); + printf("2. Using Chocolatey (Windows):\n"); + printf(" choco install fzf\n\n"); + printf("3. Using Scoop (Windows):\n"); + printf(" scoop install fzf\n\n"); + printf("4. Download prebuilt binary from: " + "https://github.com/junegunn/fzf/releases\n\n"); + printf("After installation, restart your shell.\n"); +} + +/** + * Run fzf with files from the current directory + * + * @param preview Enable preview window if 1 + * @param args Additional arguments for fzf + * @return Selected filename or NULL if canceled + */ +char *run_native_fzf_files(int preview, char **args) { + // Check if fzf is installed + if (!is_fzf_installed()) { + show_fzf_install_instructions(); + return NULL; + } + + // Build the command + char command[1024] = "find . -type f -not -path \"*/\.*\" -printf \"%P\\n\" | fzf"; + + // Add proper keybindings for both navigation and search toggle + strcat(command, " --bind=\"ctrl-j:down,ctrl-k:up,/:toggle-search\""); + + // Add preview if requested + if (preview) { + strcat(command, " --preview=\"cat {}\""); + } + + // Add any additional arguments + if (args && args[1] != NULL) { + int i = 1; + while (args[i] != NULL) { + strcat(command, " "); + + // If the argument contains spaces, quote it + if (strchr(args[i], ' ') != NULL) { + strcat(command, "\""); + strcat(command, args[i]); + strcat(command, "\""); + } else { + strcat(command, args[i]); + } + + i++; + } + } + + // Redirect output to a temporary file to capture selection + char tempfile[PATH_MAX]; + strcpy(tempfile, "/tmp/fzf_result.txt"); + + // Append output redirection to the command + strcat(command, " > "); + strcat(command, tempfile); + + // Run the command + int result = system(command); + + // Check if user canceled (fzf returns non-zero) + if (result != 0) { + remove(tempfile); // Clean up temp file even on cancel + return NULL; + } + + // Read the selected filename from the temporary file + FILE *fp = fopen(tempfile, "r"); + if (!fp) { + remove(tempfile); + return NULL; + } + + // Read the selected line + char *selected = (char *)malloc(PATH_MAX); + if (!selected) { + fclose(fp); + remove(tempfile); + return NULL; + } + + if (fgets(selected, PATH_MAX, fp) == NULL) { + fclose(fp); + remove(tempfile); + free(selected); + return NULL; + } + + // Remove newline if present + size_t len = strlen(selected); + if (len > 0 && selected[len - 1] == '\n') { + selected[len - 1] = '\0'; + } + + // Close the file and delete it + fclose(fp); + remove(tempfile); + + return selected; +} + +/** + * Run fzf with all files and directories from the current directory + * + * @param recursive Search recursively if 1 + * @param args Additional arguments for fzf + * @return Selected path or NULL if canceled + */ +char *run_native_fzf_all(int recursive, char **args) { + // Check if fzf is installed + if (!is_fzf_installed()) { + show_fzf_install_instructions(); + return NULL; + } + + // Build the command using find + char command[1024]; + + if (recursive) { + strcpy(command, "find . -not -path \"*/\.*\" -printf \"%P\\n\" | fzf"); + } else { + strcpy(command, "find . -maxdepth 1 -not -path \"*/\.*\" -printf \"%P\\n\" | fzf"); + } + + // Add proper keybindings for both navigation and search toggle + strcat(command, " --bind=\"ctrl-j:down,ctrl-k:up,/:toggle-search\""); + + // Add preview window showing file info or directory contents + strcat(command, " --preview=\"if [ -d {} ]; then ls -la {}; else bat " + "--color=always {} 2>/dev/null || cat {} 2>/dev/null; fi\""); + + // Add any additional arguments + if (args && args[1] != NULL) { + int i = 1; + while (args[i] != NULL) { + if (strcmp(args[i], "-r") != 0 && strcmp(args[i], "--recursive") != 0) { + strcat(command, " "); + + // If the argument contains spaces, quote it + if (strchr(args[i], ' ') != NULL) { + strcat(command, "\""); + strcat(command, args[i]); + strcat(command, "\""); + } else { + strcat(command, args[i]); + } + } + i++; + } + } + + // Redirect output to a temporary file to capture selection + char tempfile[PATH_MAX]; + strcpy(tempfile, "/tmp/fzf_result.txt"); + + // Append output redirection to the command + strcat(command, " > "); + strcat(command, tempfile); + + // Run the command + int result = system(command); + + // Check if user canceled (fzf returns non-zero) + if (result != 0) { + remove(tempfile); // Clean up temp file even on cancel + return NULL; + } + + // Read the selected path from the temporary file + FILE *fp = fopen(tempfile, "r"); + if (!fp) { + remove(tempfile); + return NULL; + } + + // Read the selected line + char *selected = (char *)malloc(PATH_MAX); + if (!selected) { + fclose(fp); + remove(tempfile); + return NULL; + } + + if (fgets(selected, PATH_MAX, fp) == NULL) { + fclose(fp); + remove(tempfile); + free(selected); + return NULL; + } + + // Remove newline if present + size_t len = strlen(selected); + if (len > 0 && selected[len - 1] == '\n') { + selected[len - 1] = '\0'; + } + + // Close the file and delete it + fclose(fp); + remove(tempfile); + + return selected; +} + +/** + * Run fzf with command history + * + * @return Selected command or NULL if canceled + */ +char *run_native_fzf_history(void) { + // Check if fzf is installed + if (!is_fzf_installed()) { + show_fzf_install_instructions(); + return NULL; + } + + // Create a temporary file with command history + char tempfile_in[PATH_MAX]; + strcpy(tempfile_in, "/tmp/fzf_history.txt"); + + FILE *fp_in = fopen(tempfile_in, "w"); + if (!fp_in) { + return NULL; + } + + // Write history to temporary file + int num_to_display = + (history_count < HISTORY_SIZE) ? history_count : HISTORY_SIZE; + int start_idx; + + if (history_count <= HISTORY_SIZE) { + // Haven't filled the buffer yet + start_idx = 0; + } else { + // History buffer is full, start from the oldest command + start_idx = history_index; // Next slot to overwrite contains oldest command + } + + for (int i = 0; i < num_to_display; i++) { + int idx = (start_idx + i) % HISTORY_SIZE; + if (command_history[idx].command) { + fprintf(fp_in, "%s\n", command_history[idx].command); + } + } + + fclose(fp_in); + + // Create a temporary file for the result + char tempfile_out[PATH_MAX]; + strcpy(tempfile_out, "/tmp/fzf_result.txt"); + + // Build the command + char command[1024]; + sprintf(command, + "cat \"%s\" | fzf --tac --no-sort " + "--bind=\"ctrl-j:down,ctrl-k:up,/:toggle-search\" > \"%s\"", + tempfile_in, tempfile_out); + + // Run the command + int result = system(command); + + // Delete the input file + remove(tempfile_in); + + // Check if user canceled + if (result != 0) { + remove(tempfile_out); // Clean up temp file even on cancel + return NULL; + } + + // Read the selected command from the output file + FILE *fp_out = fopen(tempfile_out, "r"); + if (!fp_out) { + remove(tempfile_out); + return NULL; + } + + // Read the selected line + char *selected = (char *)malloc(PATH_MAX); + if (!selected) { + fclose(fp_out); + remove(tempfile_out); + return NULL; + } + + if (fgets(selected, PATH_MAX, fp_out) == NULL) { + fclose(fp_out); + remove(tempfile_out); + free(selected); + return NULL; + } + + // Remove newline if present + size_t len = strlen(selected); + if (len > 0 && selected[len - 1] == '\n') { + selected[len - 1] = '\0'; + } + + // Close and delete the output file + fclose(fp_out); + remove(tempfile_out); + + return selected; +} + +/** + * Check if a specific editor is available + * + * @param editor Name of the editor executable + * @return 1 if available, 0 if not + */ +int is_editor_available(const char *editor) { + char command[256]; + snprintf(command, sizeof(command), "%s --version >/dev/null 2>&1", editor); + return (system(command) == 0); +} + +/** + * Open a file in the best available editor in the current terminal + * + * @param file_path Path to the file to open + * @param line_number Optional line number to jump to (0 to ignore) + * @return 1 if successful, 0 if failed + */ +int open_in_best_editor(const char *file_path, int line_number) { + char command[2048] = {0}; + int success = 0; + + // Try to detect available editors (in order of preference) + if (is_editor_available("nvim")) { + // Neovim is available - construct command with +line_number + if (line_number > 0) { + snprintf(command, sizeof(command), "nvim +%d \"%s\"", line_number, + file_path); + } else { + snprintf(command, sizeof(command), "nvim \"%s\"", file_path); + } + success = 1; + } else if (is_editor_available("vim")) { + // Vim is available + if (line_number > 0) { + snprintf(command, sizeof(command), "vim +%d \"%s\"", line_number, + file_path); + } else { + snprintf(command, sizeof(command), "vim \"%s\"", file_path); + } + success = 1; + } else if (is_editor_available("nano")) { + // Try nano if available + snprintf(command, sizeof(command), "nano \"%s\"", file_path); + success = 1; + } else if (is_editor_available("code")) { + // VSCode in the terminal if possible + if (line_number > 0) { + snprintf(command, sizeof(command), "code -g \"%s:%d\" -r", file_path, + line_number); + } else { + snprintf(command, sizeof(command), "code \"%s\" -r", file_path); + } + success = 1; + } else if (is_editor_available("notepad")) { + // Notepad as a last resort (will still open in a new window) + snprintf(command, sizeof(command), "notepad \"%s\"", file_path); + success = 1; + } + + if (success) { + // Clear the screen before launching the editor + system("clear"); + + // Execute the command directly in the current terminal + system(command); + + // Return directly to prompt without requiring key press + return 1; + } else { + // No suitable editor found + printf("No compatible editor (neovim, vim, nano or VSCode) found.\n"); + return 0; + } +} + +/** + * Command handler for the fzf command + * + * @param args Command arguments + * @return 1 to continue shell execution, 0 to exit shell + */ +int lsh_fzf_native(char **args) { + // Check if fzf is installed + if (!is_fzf_installed()) { + show_fzf_install_instructions(); + return 1; + } + + // Parse options + int recursive = 0; + int mode = 0; // 0 = all, 1 = files only, 2 = history + int no_open = 0; // Don't open files automatically if set + int arg_index = 1; + + if (args[arg_index] && strcmp(args[arg_index], "--help") == 0) { + printf("Usage: fzf [options] [pattern]\n"); + printf("Interactive fuzzy finder.\n"); + printf("Options:\n"); + printf(" -r, --recursive Search directories recursively\n"); + printf(" -f, --files Search only files (not directories)\n"); + printf(" -h, --history Search command history\n"); + printf(" --no-open Don't automatically open selected files\n"); + printf("\nControls:\n"); + printf(" Ctrl+j/Ctrl+k Move down/up (vim-style navigation)\n"); + printf(" Type directly To search (default mode)\n"); + printf(" / Toggle search mode (allows searching for 'j' " + "and 'k')\n"); + printf(" Enter Select item (and open file)\n"); + printf(" Ctrl+C/Esc Cancel\n"); + printf(" ? Toggle preview window\n"); + return 1; + } + + // Process options + while (args[arg_index] != NULL && args[arg_index][0] == '-') { + if (strcmp(args[arg_index], "-r") == 0 || + strcmp(args[arg_index], "--recursive") == 0) { + recursive = 1; + arg_index++; + } else if (strcmp(args[arg_index], "-f") == 0 || + strcmp(args[arg_index], "--files") == 0) { + mode = 1; // Files only + arg_index++; + } else if (strcmp(args[arg_index], "-h") == 0 || + strcmp(args[arg_index], "--history") == 0) { + mode = 2; // History + arg_index++; + } else if (strcmp(args[arg_index], "--no-open") == 0) { + no_open = 1; + arg_index++; + } else { + break; // Unknown option, treat as pattern + } + } + + // Run fzf based on the selected mode + char *result = NULL; + + if (mode == 2) { + // History mode + result = run_native_fzf_history(); + } else if (mode == 1) { + // Files only mode + result = run_native_fzf_files(1, args); + } else { + // All files and directories + result = run_native_fzf_all(recursive, args); + } + + // Handle the result + if (result) { + if (mode == 2) { + // Execute the selected command from history + printf("Executing: %s\n", result); + + // Parse and execute the command + char *cmd_copy = strdup(result); + if (cmd_copy) { + char **cmd_args = lsh_split_line(cmd_copy); + lsh_execute(cmd_args); + + // Clean up + free(cmd_args); + free(cmd_copy); + } + } else { + // File or directory selected + + // Check if it's a regular file (not a directory) + struct stat file_stat; + int stat_result = stat(result, &file_stat); + + if (stat_result == 0) { + int is_directory = S_ISDIR(file_stat.st_mode); + + if (is_directory) { + // It's a directory, just print it + printf("Selected directory: %s\n", result); + + // Optionally change to that directory + printf("Do you want to change to this directory? (y/n): "); + char response = getchar(); + // Clear input buffer + while (getchar() != '\n') + ; + + if (response == 'y' || response == 'Y') { + chdir(result); + printf("Changed directory to: %s\n", result); + } + } else { + // It's a file + printf("Selected file: %s\n", result); + + // Open in editor if allowed + if (!no_open) { + int success = open_in_best_editor(result, 0); + if (success) { + printf("File opened in editor.\n"); + } + } + } + } else { + // Could not determine file type, just print the path + printf("Selected: %s\n", result); + } + } + + free(result); + } else { + printf("No selection made.\n"); + } + + return 1; +} diff --git a/fzf_native.h b/fzf_native.h new file mode 100644 index 0000000..d893acd --- /dev/null +++ b/fzf_native.h @@ -0,0 +1,57 @@ +/** + * fzf_native.h + * Header for native fzf command-line fuzzy finder wrapper + */ + +#ifndef FZF_NATIVE_H +#define FZF_NATIVE_H + +#include "builtins.h" +#include "common.h" + +/** + * Check if native fzf is installed on the system + * + * @return 1 if installed, 0 if not + */ +int is_fzf_installed(void); + +/** + * Display instructions for installing fzf + */ +void show_fzf_install_instructions(void); + +/** + * Run fzf with files from the current directory + * + * @param preview Enable preview window if 1 + * @param args Additional arguments for fzf + * @return Selected filename or NULL if canceled + */ +char *run_native_fzf_files(int preview, char **args); + +/** + * Run fzf with all files and directories from the current directory + * + * @param recursive Search recursively if 1 + * @param args Additional arguments for fzf + * @return Selected path or NULL if canceled + */ +char *run_native_fzf_all(int recursive, char **args); + +/** + * Run fzf with command history + * + * @return Selected command or NULL if canceled + */ +char *run_native_fzf_history(void); + +/** + * Command handler for the fzf command + * + * @param args Command arguments + * @return 1 to continue shell execution, 0 to exit shell + */ +int lsh_fzf_native(char **args); + +#endif // FZF_NATIVE_H diff --git a/fzf_native.o b/fzf_native.o new file mode 100644 index 0000000..695dd29 Binary files /dev/null and b/fzf_native.o differ diff --git a/git_integration.c b/git_integration.c new file mode 100644 index 0000000..723af24 --- /dev/null +++ b/git_integration.c @@ -0,0 +1,154 @@ +/** + * git_integration.c + * Implementation of Git repository detection and information + */ + +#include "git_integration.h" +#include +#include +#include +#include + +/** + * Initialize Git integration + */ +void init_git_integration(void) { + // No initialization needed for Linux +} + +/** + * Check if the current directory is in a Git repository and get branch info + */ +int get_git_branch(char *branch_name, size_t buffer_size, int *is_dirty) { + char git_dir[PATH_MAX] = ""; + char cmd[PATH_MAX] = ""; + FILE *fp; + int status = 0; + + // Initialize output parameters + if (branch_name && buffer_size > 0) { + branch_name[0] = '\0'; + } + if (is_dirty) { + *is_dirty = 0; + } + + // First check if .git directory exists (faster than running git command) + strcpy(git_dir, ".git"); + struct stat st; + if (stat(git_dir, &st) != 0 || !S_ISDIR(st.st_mode)) { + // Try to find .git in parent directories + // (Note: this is more complex, simplified here) + return 0; + } + + // Get current branch + if (branch_name && buffer_size > 0) { + strcpy(cmd, "git rev-parse --abbrev-ref HEAD 2>/dev/null"); + fp = popen(cmd, "r"); + if (fp) { + if (fgets(branch_name, buffer_size, fp) != NULL) { + // Remove trailing newline + char *newline = strchr(branch_name, '\n'); + if (newline) { + *newline = '\0'; + } + status = 1; + } + pclose(fp); + } + } + + // Check if working directory is dirty + if (is_dirty && status) { + strcpy(cmd, "git status --porcelain 2>/dev/null"); + fp = popen(cmd, "r"); + if (fp) { + char ch; + if ((ch = fgetc(fp)) != EOF) { + *is_dirty = 1; + } + pclose(fp); + } + } + + return status; +} + +/** + * Get the name of the Git repository + */ +int get_git_repo_name(char *repo_name, size_t buffer_size) { + if (!repo_name || buffer_size == 0) { + return 0; + } + + repo_name[0] = '\0'; + + char cmd[PATH_MAX] = "git rev-parse --show-toplevel 2>/dev/null"; + FILE *fp = popen(cmd, "r"); + if (!fp) { + return 0; + } + + char path[PATH_MAX]; + if (fgets(path, sizeof(path), fp) == NULL) { + pclose(fp); + return 0; + } + pclose(fp); + + // Remove trailing newline + char *newline = strchr(path, '\n'); + if (newline) { + *newline = '\0'; + } + + // Extract the directory name from the full path + char *last_slash = strrchr(path, '/'); + if (last_slash) { + strncpy(repo_name, last_slash + 1, buffer_size - 1); + repo_name[buffer_size - 1] = '\0'; + return 1; + } + + return 0; +} + +/** + * Get Git status for the current repository + */ +char *get_git_status(void) { + char branch_name[100] = ""; + int is_dirty = 0; + char repo_name[100] = ""; + + // Check if we're in a Git repo and get branch info + if (!get_git_branch(branch_name, sizeof(branch_name), &is_dirty)) { + return NULL; // Not in a Git repo + } + + // Get repo name + get_git_repo_name(repo_name, sizeof(repo_name)); + + // Format status string + char *status = malloc(256); + if (!status) { + return NULL; + } + + if (strlen(repo_name) > 0) { + sprintf(status, "%s%s (%s%s%s)", + repo_name, + branch_name[0] ? ":" : "", + branch_name, + is_dirty ? " *" : "", + ""); + } else { + sprintf(status, "%s%s", + branch_name, + is_dirty ? " *" : ""); + } + + return status; +} \ No newline at end of file diff --git a/git_integration.h b/git_integration.h new file mode 100644 index 0000000..84453d1 --- /dev/null +++ b/git_integration.h @@ -0,0 +1,43 @@ +/** + * git_integration.h + * Functions for Git repository detection and information + */ + +#ifndef GIT_INTEGRATION_H +#define GIT_INTEGRATION_H + +#include "common.h" + +/** + * Initialize Git integration + */ +void init_git_integration(void); + +/** + * Check if the current directory is in a Git repository and get branch info + * + * @param branch_name Buffer to store branch name if found + * @param buffer_size Size of the branch_name buffer + * @param is_dirty Pointer to store dirty status flag (1 if repo has changes) + * @return 1 if in a Git repo, 0 otherwise + */ +int get_git_branch(char *branch_name, size_t buffer_size, int *is_dirty); + +/** + * Get the name of the Git repository + * + * @param repo_name Buffer to store repo name if found + * @param buffer_size Size of the repo_name buffer + * @return 1 if name was retrieved, 0 otherwise + */ +int get_git_repo_name(char *repo_name, size_t buffer_size); + +/** + * Get Git status for the current repository + * + * @return A string with the Git status or NULL if not in a repo + * (caller must free the returned string) + */ +char *get_git_status(void); + +#endif // GIT_INTEGRATION_H \ No newline at end of file diff --git a/git_integration.o b/git_integration.o new file mode 100644 index 0000000..98e2aa8 Binary files /dev/null and b/git_integration.o differ diff --git a/grep.c b/grep.c new file mode 100644 index 0000000..fca71ea --- /dev/null +++ b/grep.c @@ -0,0 +1,54 @@ +/** + * grep.c + * Simple implementation of text searching for Linux + */ + +#include "grep.h" +#include "builtins.h" +#include +#include +#include +#include +#include +#include +#include +#include + +/** + * Simple grep implementation + */ +int lsh_actual_grep(char **args) { + if (args[1] == NULL) { + printf("Usage: grep [pattern] [file...]\n"); + return 1; + } + + // Use the system's grep command + char command[1024] = "grep "; + + // Add the pattern (first argument) + strcat(command, "\""); + strcat(command, args[1]); + strcat(command, "\""); + + // Add any additional files specified + for (int i = 2; args[i] != NULL; i++) { + strcat(command, " "); + strcat(command, args[i]); + } + + // If no files specified, use all files in current directory + if (args[2] == NULL) { + strcat(command, " ."); + } + + // Execute the grep command + return system(command); +} + +/** + * Wrapper for grep + */ +int lsh_grep(char **args) { + return lsh_actual_grep(args); +} \ No newline at end of file diff --git a/grep.h b/grep.h new file mode 100644 index 0000000..2dcdda4 --- /dev/null +++ b/grep.h @@ -0,0 +1,28 @@ +/** + * grep.h + * Header for fast text searching in files with fuzzy matching capabilities + */ + +#ifndef GREP_H +#define GREP_H + +#include "common.h" + +/** + * Command handler for the "grep" command + * + * Usage: grep [options] pattern [file/directory] + * Options: + * -n, --line-numbers Show line numbers + * -i, --ignore-case Ignore case distinctions + * -r, --recursive Search directories recursively + * -f, --fuzzy Use fuzzy matching instead of exact + * --file Specify files/directories to search (otherwise searches + * current dir) + * + * @param args Command arguments + * @return 1 to continue shell execution, 0 to exit shell + */ +int lsh_grep(char **args); + +#endif // GREP_H diff --git a/grep.o b/grep.o new file mode 100644 index 0000000..75b6ceb Binary files /dev/null and b/grep.o differ diff --git a/line_reader.c b/line_reader.c new file mode 100644 index 0000000..d4ff2ba --- /dev/null +++ b/line_reader.c @@ -0,0 +1,1039 @@ +/** + * 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 "persistent_history.h" +#include "tab_complete.h" +#include "themes.h" +#include +#include +#include +#include +#include +#include +#include + +#ifndef PATH_MAX +#define PATH_MAX 4096 +#endif + +/** + * 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; + } + + // 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 + // We avoid modifying common.h by using local constants + #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) + // Note: These may vary widely between terminals + 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; +} + +/** + * 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; + int show_suggestions = 0; + + // Suggestion state + 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; + + // Define colors for suggestions + #define SUGGESTION_COLOR "\033[2;37m" // Dim white color for suggestions + #define COUNTER_COLOR "\033[0;36m" // Cyan color for the counter + #define RESET_COLOR "\033[0m" + + if (!buffer) { + fprintf(stderr, "lsh: allocation error\n"); + exit(EXIT_FAILURE); + } + + // Clear the buffer + buffer[0] = '\0'; + + // Display prompt + printf("\033[1;32m➜\033[0m "); + fflush(stdout); + + // Function to check if current input exactly matches current suggestion + int is_complete_match() { + if (!has_suggestion || suggestion_count == 0) { + return 0; // No suggestion to compare with + } + + // If we're typing a command (not an argument) + if (prefix_start == 0) { + // Check if buffer exactly matches the current suggestion + return (strcasecmp(buffer, suggestions[suggestion_index]) == 0); + } else { + // We're completing an argument + // Check if the part after the last space exactly matches the suggestion + char *arg_part = buffer + prefix_start; + return (strcasecmp(arg_part, suggestions[suggestion_index]) == 0); + } + } + + // Function to update and display suggestions + void update_suggestions() { + // 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) { + has_suggestion = 1; + suggestion_index = 0; + + // Create the full suggestion string that Enter would accept + 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'; + + // 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("\033[1;32m➜\033[0m %s", buffer); + fflush(stdout); + } else { + // Clear the current line + printf("\r\033[K"); + + // Display prompt and current text + printf("\033[1;32m➜\033[0m %s", current_text); + + // Display the suggestion part in dim color + printf("%s%s%s", SUGGESTION_COLOR, suggestion_text, RESET_COLOR); + + // Show counter for which suggestion we're on + if (suggestion_count > 1) { + printf(" %s(%d/%d)%s", COUNTER_COLOR, suggestion_index + 1, suggestion_count, RESET_COLOR); + + // Move cursor back to end of actual input (including the counter) + char counter_str[20]; + snprintf(counter_str, sizeof(counter_str), " (%d/%d)", suggestion_index + 1, suggestion_count); + int counter_len = strlen(counter_str); + int suggestion_len = strlen(suggestion_text); + + for (int i = 0; i < suggestion_len + counter_len; i++) { + printf("\b"); + } + } else { + // Just 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\033[1;32m➜\033[0m %s", buffer); + fflush(stdout); + } + } + + // Function to display the current suggestion (used after tab cycling) + void display_current_suggestion() { + if (has_suggestion && suggestion_count > 0) { + // Create the full suggestion string + 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'; + + // 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); + } + } + + // Clear the current line + printf("\r\033[K"); + + // Display prompt and current text + printf("\033[1;32m➜\033[0m %s", current_text); + + // Display the suggestion part in dim color + printf("%s%s%s", SUGGESTION_COLOR, suggestion_text, RESET_COLOR); + + // Show counter for which suggestion we're on + if (suggestion_count > 1) { + printf(" %s(%d/%d)%s", COUNTER_COLOR, suggestion_index + 1, suggestion_count, RESET_COLOR); + + // Move cursor back to end of actual input (including the counter) + char counter_str[20]; + snprintf(counter_str, sizeof(counter_str), " (%d/%d)", suggestion_index + 1, suggestion_count); + int counter_len = strlen(counter_str); + int suggestion_len = strlen(suggestion_text); + + for (int i = 0; i < suggestion_len + counter_len; i++) { + printf("\b"); + } + } else { + // Just 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); + } + } + + // Flag to track if we're in suggestion mode or execution mode + int suggestion_active = 1; // Start in suggestion mode + + while (1) { + c = read_key(); + + if (c == KEY_ENTER || c == '\n' || c == '\r') { + // Check if we have a visible suggestion (not an exact match) + if (has_suggestion && suggestion_count > 0) { + // Extract just the suggestion part to see if there's anything to complete + char suggestion_text[LSH_RL_BUFSIZE] = ""; + + 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'; + + if (strncasecmp(suggestions[suggestion_index], current_arg, strlen(current_arg)) == 0) { + strncpy(suggestion_text, + suggestions[suggestion_index] + strlen(current_arg), + sizeof(suggestion_text) - 1); + } + } else { + if (strncasecmp(suggestions[suggestion_index], buffer, strlen(buffer)) == 0) { + strncpy(suggestion_text, + suggestions[suggestion_index] + strlen(buffer), + sizeof(suggestion_text) - 1); + } + } + + // If there's a non-empty suggestion part and in suggestion mode, accept it + if (strlen(suggestion_text) > 0 && suggestion_active) { + // First Enter press with an active suggestion: Accept it without executing + strncpy(buffer, full_suggestion, bufsize - 1); + buffer[bufsize - 1] = '\0'; + position = strlen(buffer); + + // Redraw line with full accepted suggestion + printf("\r\033[K\033[1;32m➜\033[0m %s", buffer); + fflush(stdout); + + // Switch to execution mode for next Enter press + suggestion_active = 0; + + // Reset suggestion state since we've accepted it + has_suggestion = 0; + if (suggestions) { + for (int i = 0; i < suggestion_count; i++) { + if (suggestions[i]) free(suggestions[i]); + } + free(suggestions); + suggestions = NULL; + suggestion_count = 0; + } + } else { + // No visible suggestion or already accepted: Execute + buffer[position] = '\0'; + printf("\n"); + fflush(stdout); + break; + } + } else { + // No suggestion: Execute the command + buffer[position] = '\0'; + printf("\n"); + fflush(stdout); + break; + } + } else if (c == KEY_BACKSPACE || c == 127) { + // Handle backspace + if (position > 0) { + position--; + buffer[position] = '\0'; + + // Update suggestions after backspace + suggestion_active = 1; // Re-enter suggestion mode + update_suggestions(); + } + } else if (c == KEY_TAB) { + // If we have suggestions, cycle to the next one + suggestion_active = 1; // Ensure we're in suggestion mode + + if (suggestion_count == 1) { + // Only one suggestion - accept it but don't execute + strncpy(buffer, full_suggestion, bufsize - 1); + buffer[bufsize - 1] = '\0'; + position = strlen(buffer); + + // Redraw line with full accepted suggestion + printf("\r\033[K\033[1;32m➜\033[0m %s", buffer); + fflush(stdout); + + // Reset suggestion state to find new suggestions + update_suggestions(); + } else if (suggestion_count > 1) { + // Multiple suggestions - cycle to the next one + suggestion_index = (suggestion_index + 1) % suggestion_count; + display_current_suggestion(); + } + } else if (c == KEY_UP) { + // Navigate history upward + 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("\033[1;32m➜\033[0m %s", buffer); + fflush(stdout); + + // Update suggestions after loading history + suggestion_active = 1; // Re-enter suggestion mode + update_suggestions(); + } + } else if (c == KEY_DOWN) { + // Navigate history downward + 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("\033[1;32m➜\033[0m %s", buffer); + fflush(stdout); + + // Update suggestions after loading history + suggestion_active = 1; // Re-enter suggestion mode + update_suggestions(); + } else { + // At the end of history, clear the line + printf("\r\033[K\033[1;32m➜\033[0m "); + 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; + suggestion_active = 1; // Re-enter suggestion mode + } + } else if (c == '?') { + // Toggle showing command suggestions + show_suggestions = !show_suggestions; + + // Print the ? character + 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'; + printf("%c", c); + + if (show_suggestions) { + // Show command suggestions + printf("\nCommand suggestions:\n"); + + // Check history for similar commands + int suggestions_shown = 0; + char **matching_commands = get_matching_history_entries(buffer); + if (matching_commands) { + for (int i = 0; matching_commands[i] != NULL && suggestions_shown < 5; i++) { + printf(" %s\n", matching_commands[i]); + suggestions_shown++; + } + free_matching_entries(matching_commands); + } + + // Check built-in commands + for (int i = 0; i < lsh_num_builtins() && suggestions_shown < 5; i++) { + if (strncmp(buffer, builtin_str[i], position) == 0) { + printf(" %s\n", builtin_str[i]); + suggestions_shown++; + } + } + + // If we showed suggestions, redisplay the prompt and buffer + if (suggestions_shown > 0) { + printf("\033[1;32m➜\033[0m %s", buffer); + fflush(stdout); + } + + // Update suggestions after ? mode + suggestion_active = 1; // Re-enter suggestion mode + update_suggestions(); + } + } else if (c == KEY_RIGHT && has_suggestion) { + // Right arrow also accepts the current suggestion (without executing) + strncpy(buffer, full_suggestion, bufsize - 1); + buffer[bufsize - 1] = '\0'; + position = strlen(buffer); + + // Redraw line with full accepted suggestion + printf("\r\033[K\033[1;32m➜\033[0m %s", buffer); + fflush(stdout); + + // Reset suggestion state to find new suggestions + suggestion_active = 1; // Re-enter suggestion mode + update_suggestions(); + } 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'; + + // Update with the new character and show suggestions + suggestion_active = 1; // Re-enter suggestion mode when typing + update_suggestions(); + } + } + + // 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; +} diff --git a/line_reader.h b/line_reader.h new file mode 100644 index 0000000..cefe6ba --- /dev/null +++ b/line_reader.h @@ -0,0 +1,57 @@ +/** + * line_reader.h + * Functions for reading and parsing command lines + */ + +#ifndef LINE_READER_H +#define LINE_READER_H + +#include "common.h" + +/** + * 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); + +/** + * Read a key from the terminal + * + * Handles special keys and escape sequences + */ +int read_key(void); + +/** + * Read a line of input from the user with tab completion + * + * @return The line read from stdin (must be freed by caller) + */ +char *lsh_read_line(void); + +/** + * Split a line into tokens + * + * @param line The line to split + * @return An array of tokens (must be freed by caller) + */ +char **lsh_split_line(char *line); + +/** + * 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); + +/** + * 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); + +#endif // LINE_READER_H \ No newline at end of file diff --git a/line_reader.o b/line_reader.o new file mode 100644 index 0000000..d941ee2 Binary files /dev/null and b/line_reader.o differ diff --git a/main.c b/main.c new file mode 100644 index 0000000..34879f3 --- /dev/null +++ b/main.c @@ -0,0 +1,26 @@ +/** + * main.c + * Entry point for the LSH shell program + */ + +#include "common.h" +#include "shell.h" +#include + +/** + * Main entry point + */ + +int main(int argc, char **argv) { + // Set locale to support UTF-8 + setlocale(LC_ALL, "en_US.UTF-8"); + + // Setup terminal for proper UTF-8 handling + // This is Linux equivalent of SetConsoleOutputCP(65001) + printf("\033%%G"); // Tell terminal to use UTF-8 + + // Start the shell loop + lsh_loop(); + + return EXIT_SUCCESS; +} \ No newline at end of file diff --git a/main.o b/main.o new file mode 100644 index 0000000..1f5dd90 Binary files /dev/null and b/main.o differ diff --git a/mingw-get-setup.exe b/mingw-get-setup.exe new file mode 100644 index 0000000..0b28bc1 Binary files /dev/null and b/mingw-get-setup.exe differ diff --git a/persistent_history.c b/persistent_history.c new file mode 100644 index 0000000..fda5608 --- /dev/null +++ b/persistent_history.c @@ -0,0 +1,502 @@ +/** + * persistent_history.c + * Implementation of persistent command history and frequency tracking + */ + +#include "persistent_history.h" +#include +#include +#include +#include +#include +#include +#include +#include + +// Global variables for persistent history +static PersistentHistoryEntry *history_entries = NULL; +static int history_size = 0; +static int history_capacity = 0; +static int history_position = 0; + +// Global variables for frequency tracking +static CommandFrequency *command_frequencies = NULL; +static int frequency_count = 0; +static int frequency_capacity = 0; + +// Paths for history and frequency files +static char history_file_path[PATH_MAX]; +static char frequency_file_path[PATH_MAX]; + +/** + * Initialize the persistent history system + */ +void init_persistent_history(void) { + // Allocate initial history capacity + history_capacity = PERSISTENT_HISTORY_SIZE; + history_entries = (PersistentHistoryEntry *)malloc(history_capacity * sizeof(PersistentHistoryEntry)); + if (!history_entries) { + fprintf(stderr, "Failed to allocate memory for history entries\n"); + return; + } + + // Allocate initial frequency capacity + frequency_capacity = 100; + command_frequencies = (CommandFrequency *)malloc(frequency_capacity * sizeof(CommandFrequency)); + if (!command_frequencies) { + fprintf(stderr, "Failed to allocate memory for command frequencies\n"); + free(history_entries); + history_entries = NULL; + return; + } + + // Initialize history and frequency counters + history_size = 0; + history_position = -1; + frequency_count = 0; + + // Set up file paths + char *home_dir = getenv("HOME"); + if (home_dir) { + // Create .lsh directory if it doesn't exist + char lsh_dir[PATH_MAX]; + snprintf(lsh_dir, sizeof(lsh_dir), "%s/.lsh", home_dir); + + struct stat st = {0}; + if (stat(lsh_dir, &st) == -1) { + mkdir(lsh_dir, 0700); + } + + snprintf(history_file_path, sizeof(history_file_path), "%s/.lsh/history", home_dir); + snprintf(frequency_file_path, sizeof(frequency_file_path), "%s/.lsh/frequency", home_dir); + } else { + // Fallback to current directory + strcpy(history_file_path, ".lsh_history"); + strcpy(frequency_file_path, ".lsh_frequency"); + } + + // Load history and frequency data + load_history_from_file(); + load_frequencies_from_file(); +} + +/** + * Clean up persistent history resources + */ +void cleanup_persistent_history(void) { + if (history_entries) { + for (int i = 0; i < history_size; i++) { + free(history_entries[i].command); + } + free(history_entries); + history_entries = NULL; + } + + if (command_frequencies) { + for (int i = 0; i < frequency_count; i++) { + free(command_frequencies[i].command); + } + free(command_frequencies); + command_frequencies = NULL; + } + + history_size = 0; + history_capacity = 0; + frequency_count = 0; + frequency_capacity = 0; +} + +/** + * Shutdown persistent history + */ +void shutdown_persistent_history(void) { + save_history_to_file(); + save_frequencies_to_file(); + cleanup_persistent_history(); +} + +/** + * Add a command to the persistent history + */ +void add_to_history(const char *command) { + if (!command || !*command || !history_entries) { + return; // Skip empty commands + } + + // Check for duplicates (don't add the same command twice in a row) + if (history_size > 0 && strcmp(history_entries[history_size - 1].command, command) == 0) { + return; + } + + // Update command frequency + update_command_frequency(command); + + // Add to history + if (history_size >= history_capacity) { + // History is full, shift entries + free(history_entries[0].command); + memmove(&history_entries[0], &history_entries[1], + (history_capacity - 1) * sizeof(PersistentHistoryEntry)); + history_size = history_capacity - 1; + } + + // Add new entry + history_entries[history_size].command = strdup(command); + history_entries[history_size].timestamp = time(NULL); + history_size++; + + // Reset history position for navigation + history_position = -1; +} + +/** + * Update command frequency statistics + */ +void update_command_frequency(const char *command) { + if (!command || !*command || !command_frequencies) { + return; + } + + // Check if command exists in frequencies + for (int i = 0; i < frequency_count; i++) { + if (strcmp(command_frequencies[i].command, command) == 0) { + // Update existing entry + command_frequencies[i].count++; + return; + } + } + + // Add new frequency entry + if (frequency_count >= frequency_capacity) { + // Expand capacity + frequency_capacity *= 2; + CommandFrequency *new_freq = (CommandFrequency *)realloc( + command_frequencies, frequency_capacity * sizeof(CommandFrequency)); + if (!new_freq) { + fprintf(stderr, "Failed to allocate memory for command frequencies\n"); + return; + } + command_frequencies = new_freq; + } + + // Add new entry + command_frequencies[frequency_count].command = strdup(command); + command_frequencies[frequency_count].count = 1; + frequency_count++; +} + +/** + * Save history to file + */ +void save_history_to_file(void) { + if (!history_entries || history_size == 0) { + return; + } + + FILE *fp = fopen(history_file_path, "w"); + if (!fp) { + fprintf(stderr, "Failed to open history file for writing: %s\n", history_file_path); + return; + } + + // Write version and metadata + fprintf(fp, "# LSH Persistent History\n"); + fprintf(fp, "# Version: 1.0\n"); + fprintf(fp, "# Format: timestamp command\n\n"); + + // Write entries + for (int i = 0; i < history_size; i++) { + fprintf(fp, "%ld %s\n", (long)history_entries[i].timestamp, history_entries[i].command); + } + + fclose(fp); +} + +/** + * Load history from file + */ +void load_history_from_file(void) { + FILE *fp = fopen(history_file_path, "r"); + if (!fp) { + return; // File doesn't exist or can't be opened + } + + char line[1024]; + time_t timestamp; + char command[900]; // Allow space for timestamp and whitespace + + // Skip header lines + while (fgets(line, sizeof(line), fp) && line[0] == '#') { + // Skip header line + } + + // Clear history + for (int i = 0; i < history_size; i++) { + free(history_entries[i].command); + } + history_size = 0; + + // Read entries + while (fgets(line, sizeof(line), fp)) { + // Parse line: timestamp command + if (sscanf(line, "%ld %[^\n]", ×tamp, command) == 2) { + if (history_size < history_capacity) { + history_entries[history_size].command = strdup(command); + history_entries[history_size].timestamp = timestamp; + history_size++; + } + } + } + + fclose(fp); +} + +/** + * Save command frequencies to file + */ +void save_frequencies_to_file(void) { + if (!command_frequencies || frequency_count == 0) { + return; + } + + FILE *fp = fopen(frequency_file_path, "w"); + if (!fp) { + fprintf(stderr, "Failed to open frequency file for writing: %s\n", frequency_file_path); + return; + } + + // Write version and metadata + fprintf(fp, "# LSH Command Frequencies\n"); + fprintf(fp, "# Version: 1.0\n"); + fprintf(fp, "# Format: count command\n\n"); + + // Write entries + for (int i = 0; i < frequency_count; i++) { + fprintf(fp, "%d %s\n", command_frequencies[i].count, command_frequencies[i].command); + } + + fclose(fp); +} + +/** + * Load command frequencies from file + */ +void load_frequencies_from_file(void) { + FILE *fp = fopen(frequency_file_path, "r"); + if (!fp) { + return; // File doesn't exist or can't be opened + } + + char line[1024]; + int count; + char command[900]; // Allow space for count and whitespace + + // Skip header lines + while (fgets(line, sizeof(line), fp) && line[0] == '#') { + // Skip header line + } + + // Clear frequencies + for (int i = 0; i < frequency_count; i++) { + free(command_frequencies[i].command); + } + frequency_count = 0; + + // Read entries + while (fgets(line, sizeof(line), fp)) { + // Parse line: count command + if (sscanf(line, "%d %[^\n]", &count, command) == 2) { + if (frequency_count < frequency_capacity) { + command_frequencies[frequency_count].command = strdup(command); + command_frequencies[frequency_count].count = count; + frequency_count++; + } + } + } + + fclose(fp); +} + +/** + * Get the history entry at the specified index + */ +PersistentHistoryEntry *get_history_entry(int index) { + if (index < 0 || index >= history_size) { + return NULL; + } + return &history_entries[index]; +} + +/** + * Get the total number of history entries + */ +int get_history_count(void) { + return history_size; +} + +/** + * Find the best matching command based on frequency + */ +char *find_best_frequency_match(const char *prefix) { + if (!prefix || !*prefix || !command_frequencies) { + return NULL; + } + + int best_freq = 0; + int best_index = -1; + + // Find the most frequent command that starts with the prefix + for (int i = 0; i < frequency_count; i++) { + if (strncasecmp(command_frequencies[i].command, prefix, strlen(prefix)) == 0) { + if (command_frequencies[i].count > best_freq) { + best_freq = command_frequencies[i].count; + best_index = i; + } + } + } + + if (best_index >= 0) { + return strdup(command_frequencies[best_index].command); + } + + return NULL; +} + +/** + * Debug function to print frequency data + */ +void debug_print_frequencies(void) { + printf("Command Frequencies:\n"); + for (int i = 0; i < frequency_count; i++) { + printf("%3d: %s (%d)\n", i + 1, command_frequencies[i].command, command_frequencies[i].count); + } +} + +/** + * Case-insensitive substring search + */ +char *_stristr(const char *haystack, const char *needle) { + if (!haystack || !needle) + return NULL; + + char *h = strdup(haystack); + char *n = strdup(needle); + + if (!h || !n) { + if (h) free(h); + if (n) free(n); + return NULL; + } + + // Convert both strings to lowercase + for (char *p = h; *p; p++) + *p = tolower(*p); + for (char *p = n; *p; p++) + *p = tolower(*p); + + char *result = strstr(h, n); + char *original = NULL; + + // If found, map back to original string position + if (result) { + original = (char *)haystack + (result - h); + } + + free(h); + free(n); + return original; +} + +/** + * Get previous history entry for up-arrow key navigation + */ +char *get_previous_history_entry(int *position) { + if (!history_entries || history_size == 0) { + return NULL; + } + + if (*position < 0) { + // First time accessing history, start from the most recent entry + *position = history_size - 1; + } else if (*position > 0) { + // Move to previous entry + (*position)--; + } else { + // Already at the oldest entry, can't go back further + return history_entries[0].command; + } + + return history_entries[*position].command; +} + +/** + * Get next history entry for down-arrow key navigation + */ +char *get_next_history_entry(int *position) { + if (!history_entries || history_size == 0 || *position < 0) { + return NULL; + } + + if (*position < history_size - 1) { + // Move to next (more recent) entry + (*position)++; + return history_entries[*position].command; + } else { + // We've reached the end of history, return NULL to indicate + // user should get an empty prompt + *position = -1; + return NULL; + } +} + +/** + * Get matching history entries for tab completion + */ +char **get_matching_history_entries(const char *prefix) { + if (!prefix || !history_entries) { + return NULL; + } + + // Count matching entries + int matches = 0; + for (int i = 0; i < history_size; i++) { + if (strncasecmp(history_entries[i].command, prefix, strlen(prefix)) == 0) { + matches++; + } + } + + if (matches == 0) { + return NULL; + } + + // Allocate result array + char **result = (char **)malloc((matches + 1) * sizeof(char *)); + if (!result) { + return NULL; + } + + // Fill the array with matching commands + int count = 0; + for (int i = 0; i < history_size && count < matches; i++) { + if (strncasecmp(history_entries[i].command, prefix, strlen(prefix)) == 0) { + result[count++] = strdup(history_entries[i].command); + } + } + result[count] = NULL; + + return result; +} + +/** + * Free matching entries array + */ +void free_matching_entries(char **entries) { + if (!entries) { + return; + } + + for (int i = 0; entries[i] != NULL; i++) { + free(entries[i]); + } + free(entries); +} \ No newline at end of file diff --git a/persistent_history.h b/persistent_history.h new file mode 100644 index 0000000..1043095 --- /dev/null +++ b/persistent_history.h @@ -0,0 +1,85 @@ +/** + * persistent_history.h + * Functions for persistent command history and frequency tracking + */ + +#ifndef PERSISTENT_HISTORY_H +#define PERSISTENT_HISTORY_H + +#include "common.h" +#include "shell.h" +#include + +// Increase history size for persistent storage +#define PERSISTENT_HISTORY_SIZE 1000 + +// Structure for a history entry +typedef struct { + char *command; + time_t timestamp; +} PersistentHistoryEntry; + +// Structure for command frequency tracking +typedef struct { + char *command; + int count; +} CommandFrequency; + +// Initialize persistent history +void init_persistent_history(void); + +// Clean up persistent history +void cleanup_persistent_history(void); + +// Shutdown persistent history +void shutdown_persistent_history(void); + +// Add a command to persistent history +void add_to_history(const char *command); + +// Get command suggestions based on prefix and frequency +char **get_frequency_suggestions(const char *prefix, int *num_suggestions); + +// Save history to file +void save_history_to_file(void); + +// Load history from file +void load_history_from_file(void); + +// Update command frequency +void update_command_frequency(const char *command); + +// Save frequencies to file +void save_frequencies_to_file(void); + +// Load frequencies from file +void load_frequencies_from_file(void); + +// Get the history entry at specified index +PersistentHistoryEntry *get_history_entry(int index); + +// Get the total number of history entries +int get_history_count(void); + +// Find the best matching command based on frequency +char *find_best_frequency_match(const char *prefix); + +// Debug print frequency data +void debug_print_frequencies(void); + +// String search (case insensitive) +char *_stristr(const char *haystack, const char *needle); + +// Get previous history entry +char *get_previous_history_entry(int *position); + +// Get next history entry +char *get_next_history_entry(int *position); + +// Get matching history entries +char **get_matching_history_entries(const char *prefix); + +// Free matching entries +void free_matching_entries(char **entries); + +#endif // PERSISTENT_HISTORY_H \ No newline at end of file diff --git a/persistent_history.o b/persistent_history.o new file mode 100644 index 0000000..ed2c392 Binary files /dev/null and b/persistent_history.o differ diff --git a/ps_command.c b/ps_command.c new file mode 100644 index 0000000..bb37ef5 --- /dev/null +++ b/ps_command.c @@ -0,0 +1,113 @@ +/** + * ps_command.c + * Implementation of the 'ps' command for listing running processes + * Simple implementation for Linux + */ + +#include "builtins.h" +#include "common.h" +#include "structured_data.h" +#include +#include +#include + +/** + * Function to generate structured data for running processes + * This enables piping and filtering of process information + * + * @param args Command arguments + * @return TableData structure with process information + */ +TableData* lsh_ps_structured(char **args) { + // Define our table headers + char *headers[] = {"PID", "Name", "Memory", "Threads"}; + int header_count = 4; + + // Create our table + TableData *table = create_table(headers, header_count); + if (!table) { + fprintf(stderr, "lsh: allocation error in ps_structured\n"); + return NULL; + } + + // Use system's ps command to get process info + FILE *fp = popen("ps -e -o pid,comm,vsz,nlwp --no-headers", "r"); + if (!fp) { + fprintf(stderr, "lsh: failed to execute ps command\n"); + free_table(table); + return NULL; + } + + char line[1024]; + while (fgets(line, sizeof(line), fp)) { + // Parse the line + char name[256] = {0}; + unsigned long pid = 0, vsz = 0, threads = 0; + + sscanf(line, "%lu %255s %lu %lu", &pid, name, &vsz, &threads); + + // Create a new row for this process + DataValue *row = (DataValue*)malloc(header_count * sizeof(DataValue)); + if (!row) { + fprintf(stderr, "lsh: allocation error in ps_structured\n"); + free_table(table); + pclose(fp); + return NULL; + } + + // Set PID (as a string for compatibility) + char pidStr[20]; + sprintf(pidStr, "%lu", pid); + row[0].type = TYPE_STRING; + row[0].value.str_val = strdup(pidStr); + + // Set process name + row[1].type = TYPE_STRING; + row[1].value.str_val = strdup(name); + + // Format memory usage string (important for filtering) + char memoryString[32]; + if (vsz < 1024) { + sprintf(memoryString, "%lu B", vsz); + } else if (vsz < 1024 * 1024) { + sprintf(memoryString, "%.1f KB", vsz / 1024.0); + } else { + // Format as MB for consistency in filtering + sprintf(memoryString, "%.1f MB", vsz / (1024.0 * 1024.0)); + } + + row[2].type = TYPE_SIZE; // Use the special SIZE type for filtering + row[2].value.str_val = strdup(memoryString); + + // Set thread count + char threadStr[20]; + sprintf(threadStr, "%lu", threads); + row[3].type = TYPE_STRING; + row[3].value.str_val = strdup(threadStr); + + // Add the row to the table + add_table_row(table, row); + } + + pclose(fp); + return table; +} + +/** + * Implementation of the ps command to display process information + * Uses structured data to show processes in a formatted table + */ +int lsh_ps_fancy(char **args) { + // Create structured data table for processes + TableData *table = lsh_ps_structured(args); + + if (table) { + // Print the table using our nice formatting + print_table(table); + + // Free the table + free_table(table); + } + + return 1; +} \ No newline at end of file diff --git a/ps_command.o b/ps_command.o new file mode 100644 index 0000000..0b68cea Binary files /dev/null and b/ps_command.o differ diff --git a/rg_selection.txt b/rg_selection.txt new file mode 100644 index 0000000..e69de29 diff --git a/ripgrep.c b/ripgrep.c new file mode 100644 index 0000000..8967d5a --- /dev/null +++ b/ripgrep.c @@ -0,0 +1,746 @@ +/** + * ripgrep.c + * Interactive wrapper for the ripgrep (rg) command-line search tool + */ + +#include "ripgrep.h" +#include "common.h" +#include "line_reader.h" +#include "shell.h" // Add this for lsh_execute +#include +#include +#include +#include +#include // For PATH_MAX +#include // For access function +#include // For terminal control + +/** + * Check if ripgrep (rg) is installed on the system + * + * @return 1 if installed, 0 if not + */ +int is_rg_installed(void) { + // Try to run rg --version to check if it's installed + FILE *fp = popen("rg --version 2>/dev/null", "r"); + if (fp == NULL) { + return 0; + } + + // Read output (if any) + char buffer[128]; + int has_output = (fgets(buffer, sizeof(buffer), fp) != NULL); + + // Close the process + pclose(fp); + + return has_output; +} + +/** + * Display instructions for installing ripgrep + */ +void show_rg_install_instructions(void) { + printf("\nripgrep (rg) is not installed on this system. To use this feature, " + "install ripgrep:\n\n"); + printf("Installation options:\n"); + printf("1. Using package manager (Debian/Ubuntu):\n"); + printf(" sudo apt install ripgrep\n\n"); + printf("2. Using package manager (Fedora):\n"); + printf(" sudo dnf install ripgrep\n\n"); + printf("3. Using package manager (Arch Linux):\n"); + printf(" sudo pacman -S ripgrep\n\n"); + printf("4. Download prebuilt binary from: " + "https://github.com/BurntSushi/ripgrep/releases\n\n"); + printf("After installation, restart your shell.\n"); +} + +/** + * Check if a specific editor is available + * + * @param editor Name of the editor executable + * @return 1 if available, 0 if not + */ +int is_editor_available_for_rg(const char *editor) { + char command[256]; + snprintf(command, sizeof(command), "%s --version >/dev/null 2>&1", editor); + return (system(command) == 0); +} + +/** + * Open a file at a specific line in the best available editor + * + * @param file_path Path to the file to open + * @param line_number Line number to position cursor at + * @return 1 if successful, 0 if failed + */ +int rg_open_in_editor(const char *file_path, int line_number) { + char command[2048] = {0}; + int success = 0; + + // Try to detect available editors (in order of preference) + if (is_editor_available_for_rg("nvim")) { + // Neovim is available - construct command with +line_number + snprintf(command, sizeof(command), "nvim +%d \"%s\"", line_number, + file_path); + success = 1; + } else if (is_editor_available_for_rg("vim")) { + // Vim is available + snprintf(command, sizeof(command), "vim +%d \"%s\"", line_number, + file_path); + success = 1; + } else if (is_editor_available_for_rg("nano")) { + // Try nano if available + // Note: nano doesn't have perfect line number navigation + snprintf(command, sizeof(command), "nano +%d \"%s\"", line_number, + file_path); + success = 1; + } else if (is_editor_available_for_rg("code")) { + // VSCode with line number + snprintf(command, sizeof(command), "code -g \"%s:%d\" -r", file_path, + line_number); + success = 1; + } else if (is_editor_available_for_rg("gedit")) { + // gedit as last resort (limited line number support) + snprintf(command, sizeof(command), "gedit +%d \"%s\"", line_number, file_path); + success = 1; + } + + if (success) { + // Clear the screen before launching the editor + system("clear"); + + // Execute the command directly in the current terminal + int result = system(command); + success = (result == 0); + + return success; + } else { + // No suitable editor found + printf("No compatible editor (neovim, vim, nano, VSCode, gedit) found.\n"); + return 0; + } +} + +/** + * Parse a ripgrep result line to extract file path and line number + * + * @param result_line The result line from ripgrep (format: + * file:line:column:text) + * @param file_path Buffer to store extracted file path + * @param file_path_size Size of file_path buffer + * @param line_number Pointer to store extracted line number + * @return 1 if parsing successful, 0 if failed + */ +static int parse_rg_result(const char *result_line, char *file_path, + size_t file_path_size, int *line_number) { + // Ripgrep output format is: file:line:column:text + const char *first_colon = strchr(result_line, ':'); + if (!first_colon) + return 0; + + const char *second_colon = strchr(first_colon + 1, ':'); + if (!second_colon) + return 0; + + // Extract file path + size_t path_length = first_colon - result_line; + if (path_length >= file_path_size) + path_length = file_path_size - 1; + strncpy(file_path, result_line, path_length); + file_path[path_length] = '\0'; + + // Extract line number + char line_str[16] = {0}; + size_t line_str_length = second_colon - (first_colon + 1); + if (line_str_length >= sizeof(line_str)) + line_str_length = sizeof(line_str) - 1; + strncpy(line_str, first_colon + 1, line_str_length); + line_str[line_str_length] = '\0'; + + *line_number = atoi(line_str); + return 1; +} + +/** + * Run ripgrep with interactive filtering + * + * @param args Command arguments or NULL for interactive mode + * @return Selected filename/line or NULL if canceled + */ +char *run_interactive_ripgrep(char **args) { + // Check if ripgrep is installed + if (!is_rg_installed()) { + show_rg_install_instructions(); + return NULL; + } + + // Create temporary files for the search results + char temp_rg_results[PATH_MAX] = "/tmp/rg_results.txt"; + char temp_fzf_output[PATH_MAX] = "/tmp/rg_selected.txt"; + + // Build the initial command + char command[4096] = {0}; + + // Base ripgrep command with sensible defaults + strcpy(command, + "rg --line-number --column --no-heading --color=always --smart-case"); + + // Add any user-provided arguments + if (args && args[1] != NULL) { + int i = 1; + while (args[i] != NULL) { + strcat(command, " "); + + // If the argument contains spaces, quote it + if (strchr(args[i], ' ') != NULL) { + strcat(command, "\""); + strcat(command, args[i]); + strcat(command, "\""); + } else { + strcat(command, args[i]); + } + i++; + } + } else { + // If no search pattern provided, use empty string to search all files + // Will be filtered by fzf as user types + strcat(command, " \"\""); + } + + // Pipe to fzf for interactive selection with preview + strcat(command, " | fzf --ansi"); + + // Add keybindings for navigation + strcat(command, " --bind=\"ctrl-j:down,ctrl-k:up,/:toggle-search\""); + + // Add preview window showing file content with line highlighted + strcat(command, " --preview=\"bat --color=always --style=numbers " + "--highlight-line={2} {1}\""); + strcat(command, " --preview-window=+{2}-10"); + + // Add output redirection + strcat(command, " > "); + strcat(command, temp_fzf_output); + + // Run the command + printf("Starting interactive ripgrep search...\n"); + int result = system(command); + + // Check if user canceled (fzf returns non-zero) + if (result != 0) { + unlink(temp_fzf_output); + return NULL; + } + + // Read the selected result from the temporary file + FILE *fp = fopen(temp_fzf_output, "r"); + if (!fp) { + unlink(temp_fzf_output); + return NULL; + } + + // Read the selected line + char *selected = (char *)malloc(PATH_MAX * 2); + if (!selected) { + fclose(fp); + unlink(temp_fzf_output); + return NULL; + } + + if (fgets(selected, PATH_MAX * 2, fp) == NULL) { + fclose(fp); + unlink(temp_fzf_output); + free(selected); + return NULL; + } + + // Remove newline if present + size_t len = strlen(selected); + if (len > 0 && selected[len - 1] == '\n') { + selected[len - 1] = '\0'; + } + + // Close and delete the temporary file + fclose(fp); + unlink(temp_fzf_output); + + return selected; +} + +/** + * Run ripgrep with interactive editing capabilities for a custom Neovim-like + * experience + */ +void run_ripgrep_interactive_session(void) { + // Check if ripgrep and fzf are installed + if (!is_rg_installed()) { + show_rg_install_instructions(); + return; + } + + // Initialize the search query + char search_query[256] = ""; + char last_query[256] = ""; + + // Create a temporary file for results + char temp_results[PATH_MAX] = "/tmp/ripgrep_results.txt"; + + // Save original terminal settings to restore later + struct termios old_tio, new_tio; + tcgetattr(STDIN_FILENO, &old_tio); + + // Copy the old settings and modify for raw input + new_tio = old_tio; + new_tio.c_lflag &= ~(ICANON | ECHO); // Disable canonical mode and echo + new_tio.c_cc[VMIN] = 1; // Read one character at a time + new_tio.c_cc[VTIME] = 0; // No timeout + tcsetattr(STDIN_FILENO, TCSANOW, &new_tio); + + // Clear screen and show initial UI + system("clear"); + printf("--- Interactive Ripgrep Search ---\n"); + printf( + "Type to search | Ctrl+N/P: navigate | Enter: open | Ctrl+C: exit\n\n"); + printf("Search: "); + + int running = 1; + int selected_index = 0; + int results_count = 0; + char **results = NULL; + + // Main interaction loop + while (running) { + // Display the current search query + printf("\rSearch: %s", search_query); + fflush(stdout); + + // Check if the query has changed and we need to run ripgrep again + if (strcmp(search_query, last_query) != 0) { + // Free previous results if any + if (results) { + for (int i = 0; i < results_count; i++) { + free(results[i]); + } + free(results); + results = NULL; + results_count = 0; + } + + // Execute ripgrep with the current query if not empty + if (strlen(search_query) > 0) { + // Build ripgrep command + char command[4096]; + snprintf(command, sizeof(command), + "rg --line-number --column --no-heading --color=never " + "--smart-case \"%s\" > %s", + search_query, temp_results); + + system(command); + + // Read results from the temp file + FILE *fp = fopen(temp_results, "r"); + if (fp) { + // Count lines first + char line[1024]; + results_count = 0; + + while (fgets(line, sizeof(line), fp) != NULL) { + results_count++; + } + + // Allocate memory for results + rewind(fp); + results = (char **)malloc(results_count * sizeof(char *)); + + if (results) { + int i = 0; + while (i < results_count && fgets(line, sizeof(line), fp) != NULL) { + // Remove newline + size_t len = strlen(line); + if (len > 0 && line[len - 1] == '\n') { + line[len - 1] = '\0'; + } + + results[i] = strdup(line); + i++; + } + } + + fclose(fp); + } + } + + // Update last query + strcpy(last_query, search_query); + + // Reset selection to first result + selected_index = 0; + + // Clear the results display area + printf("\n\033[J"); // Clear from cursor to end of screen + + // Display results count + printf("\nFound %d matches\n\n", results_count); + + // Display results with selection + int max_display = 10; // Show at most 10 results at a time + for (int i = 0; i < results_count && i < max_display; i++) { + if (i == selected_index) { + printf("\033[7m> %s\033[0m\n", + results[i]); // Invert colors for selection + } else { + printf(" %s\n", results[i]); + } + } + } + + // Process user input + unsigned char c; + if (read(STDIN_FILENO, &c, 1) > 0) { + if (c == 3) { // Ctrl+C + running = 0; + } else if (c == 13 || c == 10) { // Enter or newline + // Open the selected file if there are results + if (results_count > 0 && selected_index < results_count) { + char file_path[PATH_MAX]; + int line_number; + + if (parse_rg_result(results[selected_index], file_path, + sizeof(file_path), &line_number)) { + rg_open_in_editor(file_path, line_number); + + // After returning from editor, refresh the UI + system("clear"); + printf("--- Interactive Ripgrep Search ---\n"); + printf("Type to search | Ctrl+N/P: navigate | Enter: open | Ctrl+C: " + "exit\n\n"); + printf("Search: %s\n\n", search_query); + + // Re-display results count + printf("Found %d matches\n\n", results_count); + + // Re-display results with selection + int max_display = 10; + for (int i = 0; i < results_count && i < max_display; i++) { + if (i == selected_index) { + printf("\033[7m> %s\033[0m\n", results[i]); + } else { + printf(" %s\n", results[i]); + } + } + } + } + } else if (c == 14) { // Ctrl+N + if (results_count > 0) { + selected_index = (selected_index + 1) % results_count; + + // Redraw results with new selection + printf("\033[%dA", + results_count > 10 + ? 10 + : results_count); // Move cursor up to results start + + int max_display = 10; + for (int i = 0; i < results_count && i < max_display; i++) { + if (i == selected_index) { + printf("\033[2K\033[7m> %s\033[0m\n", + results[i]); // Clear line and invert colors + } else { + printf("\033[2K %s\n", results[i]); // Clear line + } + } + } + } else if (c == 16) { // Ctrl+P + if (results_count > 0) { + selected_index = (selected_index - 1 + results_count) % results_count; + + // Redraw results with new selection + printf("\033[%dA", + results_count > 10 + ? 10 + : results_count); // Move cursor up to results start + + int max_display = 10; + for (int i = 0; i < results_count && i < max_display; i++) { + if (i == selected_index) { + printf("\033[2K\033[7m> %s\033[0m\n", + results[i]); // Clear line and invert colors + } else { + printf("\033[2K %s\n", results[i]); // Clear line + } + } + } + } else if (c == 127 || c == 8) { // Backspace (127 on Linux, 8 on some terminals) + // Remove last character from search query + size_t len = strlen(search_query); + if (len > 0) { + search_query[len - 1] = '\0'; + } + } else if (isprint(c)) { // Printable character + // Add character to search query + size_t len = strlen(search_query); + if (len < sizeof(search_query) - 1) { + search_query[len] = c; + search_query[len + 1] = '\0'; + } + } + } + } + + // Clean up + if (results) { + for (int i = 0; i < results_count; i++) { + free(results[i]); + } + free(results); + } + + unlink(temp_results); + + // Restore original terminal settings + tcsetattr(STDIN_FILENO, TCSANOW, &old_tio); + + // Final screen clear + system("clear"); +} + +/** + * Command handler for the ripgrep command + * + * @param args Command arguments + * @return 1 to continue shell execution, 0 to exit shell + */ +int lsh_ripgrep(char **args) { + // Check if ripgrep is installed + if (!is_rg_installed()) { + printf("Ripgrep (rg) is not installed. Falling back to custom " + "implementation.\n"); + printf("For better performance, consider installing ripgrep:\n"); + show_rg_install_instructions(); + printf("\nRunning with custom implementation...\n\n"); + + // Fall back to our custom implementation + run_ripgrep_interactive_session(); + return 1; + } + + // Also check if fzf is installed for the interactive UI + int fzf_available = is_fzf_installed(); // Reuse from fzf_native.h + + // Check for help flag + if (args[1] && + (strcmp(args[1], "--help") == 0 || strcmp(args[1], "-h") == 0)) { + printf("Usage: ripgrep [pattern] [options]\n"); + printf("Interactive code search using ripgrep (rg) with fzf.\n\n"); + printf("If called without arguments, launches fzf with ripgrep for " + "interactive searching.\n\n"); + printf("Options:\n"); + printf(" -t, --type [TYPE] Only search files matching TYPE (e.g., -t " + "cpp)\n"); + printf(" -i, --ignore-case Case insensitive search\n"); + printf(" -w, --word-regexp Only match whole words\n"); + printf(" -e, --regexp Treat pattern as a regular expression\n"); + printf(" -f, --fixed-strings Treat pattern as a literal string\n"); + printf(" -g, --glob [GLOB] Include/exclude files matching the glob\n"); + return 1; + } + + // If no arguments provided, use fzf with ripgrep + if (!args[1]) { + if (!fzf_available) { + printf("fzf is not installed. Falling back to custom implementation.\n"); + show_fzf_install_instructions(); + printf("\nRunning with custom implementation...\n\n"); + + // Fall back to custom implementation if fzf is not available + run_ripgrep_interactive_session(); + return 1; + } + + // Create a temporary file for the fzf preview script + char preview_script[PATH_MAX] = "/tmp/fzf_preview.sh"; + + // Create the preview script as a shell script with highlighting support + FILE *preview_fp = fopen(preview_script, "w"); + if (preview_fp) { + // Write a shell script that highlights searched content + fprintf(preview_fp, "#!/bin/bash\n"); + fprintf(preview_fp, "file=\"$1\"\n"); + fprintf(preview_fp, "line=\"$2\"\n"); + fprintf(preview_fp, "query=\"$3\"\n"); + fprintf(preview_fp, "if [ -z \"$query\" ]; then\n"); + // If no query, just show the file with line highlighting + fprintf(preview_fp, " bat --color=always --highlight-line \"$line\" \"$file\" 2>/dev/null || cat \"$file\"\n"); + fprintf(preview_fp, "else\n"); + // If query provided, highlight both the line and the search term + fprintf(preview_fp, " if grep -i \"$query\" \"$file\" >/dev/null 2>&1; then\n"); + fprintf(preview_fp, " rg --color=always --context 3 --line-number \"$query\" \"$file\" 2>/dev/null || "); + fprintf(preview_fp, "bat --color=always --highlight-line \"$line\" \"$file\" 2>/dev/null || cat \"$file\"\n"); + fprintf(preview_fp, " else\n"); + fprintf(preview_fp, " bat --color=always --highlight-line \"$line\" \"$file\" 2>/dev/null || cat \"$file\"\n"); + fprintf(preview_fp, " fi\n"); + fprintf(preview_fp, "fi\n"); + fclose(preview_fp); + + // Make the script executable + chmod(preview_script, 0755); + } + + // Use fzf with ripgrep to provide an interactive UI similar to Neovim + char command[4096] = {0}; + + // Use ripgrep with an empty pattern to search all text files + // and pipe to fzf for interactive filtering - now with fullscreen mode + snprintf( + command, sizeof(command), + "clear && rg --line-number --column --no-heading --color=always \"\" | " + "fzf --ansi --delimiter : " + "--preview \"%s {1} {2} {q}\" " + "--preview-window=right:60%%:wrap " + "--bind \"ctrl-j:down,ctrl-k:up,enter:accept\" " + "--border " + "--height=100%% > %s", + preview_script, "/tmp/rg_selection.txt"); + + // Execute the command + int result = system(command); + + // Check if user selected something (fzf returns non-zero on cancel) + if (result == 0) { + // Read the selected file/line from the temporary file + FILE *fp = fopen("/tmp/rg_selection.txt", "r"); + if (fp) { + char selected[1024]; + if (fgets(selected, sizeof(selected), fp)) { + // Remove newline if present + size_t len = strlen(selected); + if (len > 0 && selected[len - 1] == '\n') { + selected[len - 1] = '\0'; + } + + // Parse the selection to extract file path and line number + char file_path[PATH_MAX]; + int line_number; + + if (parse_rg_result(selected, file_path, sizeof(file_path), + &line_number)) { + printf("Opening %s at line %d\n", file_path, line_number); + rg_open_in_editor(file_path, line_number); + } + } + fclose(fp); + unlink("/tmp/rg_selection.txt"); // Clean up + } + } + + // Remove the preview script + unlink(preview_script); + + return 1; + } + + // If options are provided but no pattern, run regular ripgrep + if (args[1][0] == '-') { + // Build command to pass to system + char command[4096] = "rg"; + + // Add all arguments + for (int i = 1; args[i] != NULL; i++) { + strcat(command, " "); + + // If argument contains spaces, quote it + if (strchr(args[i], ' ') != NULL) { + strcat(command, "\""); + strcat(command, args[i]); + strcat(command, "\""); + } else { + strcat(command, args[i]); + } + } + + // Execute standard ripgrep command + system(command); + return 1; + } + + // If a pattern is provided, run ripgrep with fzf for selection + if (fzf_available) { + // Create a temporary file for the fzf preview script + char preview_script[PATH_MAX] = "/tmp/fzf_preview.sh"; + + // Create the preview script as a shell script with highlighting + FILE *preview_fp = fopen(preview_script, "w"); + if (preview_fp) { + // Write a shell script that highlights searched content + fprintf(preview_fp, "#!/bin/bash\n"); + fprintf(preview_fp, "file=\"$1\"\n"); + fprintf(preview_fp, "line=\"$2\"\n"); + fprintf(preview_fp, "search_term=\"%s\"\n", args[1]); // Use the search term provided + // Show file with both line highlighting and search term highlighting + fprintf(preview_fp, + "rg --color=always --context 3 --line-number \"%s\" \"$file\" 2>/dev/null || ", + args[1]); + fprintf(preview_fp, "bat --color=always --highlight-line \"$line\" \"$file\" 2>/dev/null || cat \"$file\"\n"); + fclose(preview_fp); + + // Make the script executable + chmod(preview_script, 0755); + } + + // Build command with fzf for better interaction + char command[4096] = {0}; + + // Now with better UI but using compatible options + snprintf( + command, sizeof(command), + "clear && rg --line-number --column --no-heading --color=always \"%s\" | " + "fzf --ansi --delimiter : " + "--preview \"%s {1} {2}\" " + "--preview-window=right:60%%:wrap " + "--bind \"ctrl-j:down,ctrl-k:up,enter:accept\" " + "--border " + "--height=100%% > %s", + args[1], preview_script, "/tmp/rg_selection.txt"); + + // Execute the command + int result = system(command); + + // Check if user selected something + if (result == 0) { + // Read the selected file/line from the temporary file + FILE *fp = fopen("/tmp/rg_selection.txt", "r"); + if (fp) { + char selected[1024]; + if (fgets(selected, sizeof(selected), fp)) { + // Remove newline if present + size_t len = strlen(selected); + if (len > 0 && selected[len - 1] == '\n') { + selected[len - 1] = '\0'; + } + + // Parse the selection to extract file path and line number + char file_path[PATH_MAX]; + int line_number; + + if (parse_rg_result(selected, file_path, sizeof(file_path), + &line_number)) { + printf("Opening %s at line %d\n", file_path, line_number); + rg_open_in_editor(file_path, line_number); + } + } + fclose(fp); + unlink("/tmp/rg_selection.txt"); // Clean up + } + } + + // Remove the preview script + unlink(preview_script); + } else { + // If fzf is not available, fall back to custom implementation + printf("fzf is not installed. Falling back to custom implementation.\n"); + run_ripgrep_interactive_session(); + } + + return 1; +} \ No newline at end of file diff --git a/ripgrep.h b/ripgrep.h new file mode 100644 index 0000000..72b5223 --- /dev/null +++ b/ripgrep.h @@ -0,0 +1,26 @@ +#ifndef RIPGREP_H +#define RIPGREP_H + +#include "builtins.h" +#include "common.h" +#include "fzf_native.h" + +int is_rg_installed(void); + +void show_rg_install_instructions(void); + +void show_fzf_install_instructions(void); + +char *run_interactive_ripgrep(char **args); + +int lsh_grep(char **args); + +int lsh_ripgrep(char **args); + +void run_ripgrep_interactive_session(void); + +int rg_open_in_editor(const char *file_path, int line_number); + +int is_editor_available_for_rg(const char *editor); + +#endif // RIPGREP_H diff --git a/ripgrep.o b/ripgrep.o new file mode 100644 index 0000000..9c852ef Binary files /dev/null and b/ripgrep.o differ diff --git a/shell b/shell new file mode 100755 index 0000000..cb5cca4 Binary files /dev/null and b/shell differ diff --git a/shell.bat b/shell.bat new file mode 100644 index 0000000..38a94d1 --- /dev/null +++ b/shell.bat @@ -0,0 +1,2 @@ +@echo off +"%~dp0shell.exe" %* diff --git a/shell.c b/shell.c new file mode 100644 index 0000000..1c42d46 --- /dev/null +++ b/shell.c @@ -0,0 +1,591 @@ +/** + * shell.c + * Implementation of core shell functionality + */ + +#include "shell.h" +#include "aliases.h" // Added for alias support +#include "autocorrect.h" +#include "bookmarks.h" // Added for bookmark support +#include "builtins.h" +#include "countdown_timer.h" +#include "favorite_cities.h" +#include "filters.h" +#include "git_integration.h" // Added for Git repository detection +#include "line_reader.h" +#include "persistent_history.h" +#include "structured_data.h" +#include "tab_complete.h" // Added for tab completion support +#include "themes.h" +#include +#include // Added for time functions +#include +#include +#include + +// Global variables for status bar +static int g_console_width = 80; +static int g_status_line = 0; +static WORD g_normal_attributes = 7; // Default white on black +static WORD g_status_attributes = 12; // Red +static BOOL g_status_bar_enabled = FALSE; // Flag to track if status bar is enabled +static struct termios g_orig_termios; // Original terminal settings + +/** + * Initialize terminal for raw mode + */ +int init_terminal(struct termios *orig_termios) { + int fd = STDIN_FILENO; + + if (!isatty(fd)) { + fprintf(stderr, "Not running in a terminal\n"); + return -1; + } + + // Get current terminal settings + if (tcgetattr(fd, orig_termios) == -1) { + perror("tcgetattr"); + return -1; + } + + // Create raw terminal settings (make a copy first) + struct termios raw = *orig_termios; + + // Disable canonical mode and echo, but preserve some control characters + raw.c_iflag &= ~(ICRNL | IXON); // Disable CTRL-S and CTRL-Q and CR to NL translation + raw.c_lflag &= ~(ECHO | ICANON | IEXTEN | ISIG); // Disable echo, canonical mode, and signal keys + raw.c_cc[VMIN] = 1; // Wait for at least one character + raw.c_cc[VTIME] = 0; // No timeout + + // Apply modified terminal settings + if (tcsetattr(fd, TCSAFLUSH, &raw) == -1) { + perror("tcsetattr"); + return -1; + } + + printf("\033[?25h"); // Ensure cursor is visible + + return fd; +} + +/** + * Restore terminal to original settings + */ +void restore_terminal(int fd, struct termios *orig_termios) { + // Restore cursor and terminal settings + printf("\033[?25h"); // Ensure cursor is visible + printf("\033c"); // Reset terminal state + + if (tcsetattr(fd, TCSAFLUSH, orig_termios) == -1) { + perror("tcsetattr"); + } +} + +/** + * Get console dimensions + */ +int get_console_dimensions(int fd, int *width, int *height) { + struct winsize ws; + + if (ioctl(fd, TIOCGWINSZ, &ws) == -1) { + perror("ioctl"); + return 0; + } + + *width = ws.ws_col; + *height = ws.ws_row; + + return 1; +} + +/** + * Temporarily hide the status bar before command execution + */ +void hide_status_bar(int fd) { + // Skip if status bar is not enabled + if (!g_status_bar_enabled) + return; + + int width, height; + if (!get_console_dimensions(fd, &width, &height)) { + return; + } + + // Save cursor position + printf(ANSI_SAVE_CURSOR); + + // Move to status bar line and clear it + printf("\033[%d;1H", height); + printf("\033[2K"); // Clear entire line + + // Restore cursor position + printf(ANSI_RESTORE_CURSOR); + fflush(stdout); +} + +/** + * This function scrolls the console buffer up one line to make room for the + * status bar when we're at the bottom of the screen. + */ +void ensure_status_bar_space(int fd) { + int width, height; + if (!get_console_dimensions(fd, &width, &height)) { + return; + } + + // First clear the status bar if it exists + printf(ANSI_SAVE_CURSOR); + + // Move to status bar line and clear it + printf("\033[%d;1H", height); + printf("\033[2K"); // Clear entire line + + // Add a blank line above status bar for spacing + printf("\033[%d;1H", height - 1); + printf("\033[2K"); // Clear entire line + + // Restore cursor position + printf(ANSI_RESTORE_CURSOR); + fflush(stdout); +} + +/** + * Initialize the status bar at the bottom of the screen + */ +int init_status_bar(int fd) { + int width, height; + if (!get_console_dimensions(fd, &width, &height)) { + return 0; + } + + g_console_width = width; + g_status_line = height; + g_status_bar_enabled = TRUE; + + // Clear status line + printf(ANSI_SAVE_CURSOR); + printf("\033[%d;1H", height); + printf("\033[2K"); // Clear entire line + printf(ANSI_RESTORE_CURSOR); + fflush(stdout); + + return 1; +} + +/** + * Check for console window resize and update status bar position + */ +void check_console_resize(int fd) { + if (!g_status_bar_enabled) + return; + + int width, height; + if (!get_console_dimensions(fd, &width, &height)) { + return; + } + + // Check if dimensions have changed + if (width != g_console_width || height != g_status_line) { + g_console_width = width; + g_status_line = height; + + // Redraw status bar at new position + hide_status_bar(fd); + } +} + +/** + * Update the status bar with Git information + */ +void update_status_bar(int fd, const char *git_info) { + if (!g_status_bar_enabled) + return; + + int width, height; + if (!get_console_dimensions(fd, &width, &height)) { + return; + } + + g_console_width = width; + g_status_line = height; + + // Get current time for status bar + time_t rawtime; + struct tm *timeinfo; + char time_buffer[10]; + + time(&rawtime); + timeinfo = localtime(&rawtime); + strftime(time_buffer, sizeof(time_buffer), "%H:%M:%S", timeinfo); + + // Get current directory for display + char cwd[LSH_RL_BUFSIZE]; + char parent_dir[LSH_RL_BUFSIZE / 2]; + char current_dir[LSH_RL_BUFSIZE / 2]; + + if (getcwd(cwd, sizeof(cwd)) != NULL) { + get_path_display(cwd, parent_dir, current_dir, LSH_RL_BUFSIZE / 2); + } else { + strcpy(parent_dir, "unknown"); + strcpy(current_dir, "dir"); + } + + printf(ANSI_SAVE_CURSOR); + + // Move cursor to status line + printf("\033[%d;1H", height); + + // Clear the status line + printf("\033[2K"); + + // Set status bar color (cyan background) + printf(ANSI_BG_CYAN ANSI_COLOR_BLACK); + + // Format: [time] parent_dir/current_dir [git_info] + printf(" %s ", time_buffer); + printf(" %s/%s ", parent_dir, current_dir); + + if (git_info != NULL && strlen(git_info) > 0) { + printf(" %s ", git_info); + } + + // Fill the rest of line with space to color the entire bar + printf("%*s", width - (int)strlen(time_buffer) - + (int)strlen(parent_dir) - (int)strlen(current_dir) - + (git_info ? (int)strlen(git_info) + 3 : 0) - 8, ""); + + // Reset color + printf(ANSI_COLOR_RESET); + + // Restore cursor position + printf(ANSI_RESTORE_CURSOR); + fflush(stdout); +} + +/** + * Get the name of the parent and current directories from a path + */ +void get_path_display(const char *cwd, char *parent_dir_name, + char *current_dir_name, size_t buf_size) { + char *last_slash = strrchr(cwd, '/'); + + if (last_slash != NULL) { + strncpy(current_dir_name, last_slash + 1, buf_size - 1); + current_dir_name[buf_size - 1] = '\0'; + + // If we're in root directory + if (last_slash == cwd) { + strncpy(parent_dir_name, "/", buf_size - 1); + parent_dir_name[buf_size - 1] = '\0'; + return; + } + + // Find the second-to-last slash for parent directory + char *parent_slash = last_slash; + while (parent_slash > cwd && *(parent_slash - 1) != '/') { + parent_slash--; + } + + if (parent_slash > cwd) { + size_t parent_len = parent_slash - cwd - 1; + if (parent_len >= buf_size) { + parent_len = buf_size - 1; + } + + // If parent is root + if (parent_len == 0) { + strncpy(parent_dir_name, "/", buf_size - 1); + } else { + strncpy(parent_dir_name, parent_slash - parent_len, parent_len); + parent_dir_name[parent_len] = '\0'; + } + } else { + strncpy(parent_dir_name, "/", buf_size - 1); + } + parent_dir_name[buf_size - 1] = '\0'; + } else { + // Fall back if path doesn't contain slashes + strncpy(current_dir_name, cwd, buf_size - 1); + strncpy(parent_dir_name, ".", buf_size - 1); + current_dir_name[buf_size - 1] = '\0'; + parent_dir_name[buf_size - 1] = '\0'; + } +} + +/** + * Launch an external program + */ +int lsh_launch(char **args) { + pid_t pid, wpid; + int status; + + // Fork a child process + pid = fork(); + + if (pid == 0) { + // Child process + if (execvp(args[0], args) == -1) { + perror("lsh"); + } + exit(EXIT_FAILURE); + } else if (pid < 0) { + // Error forking + perror("lsh"); + } else { + // Parent process + do { + wpid = waitpid(pid, &status, WUNTRACED); + } while (!WIFEXITED(status) && !WIFSIGNALED(status)); + } + + return 1; +} + +/** + * Execute a command + */ +int lsh_execute(char **args) { + if (args[0] == NULL) { + // An empty command was entered + return 1; + } + + // Check if it's a built-in command + for (int i = 0; i < lsh_num_builtins(); i++) { + if (strcmp(args[0], builtin_str[i]) == 0) { + return (*builtin_func[i])(args); + } + } + + // If not a built-in, check aliases and launch external + char **alias_expansion = expand_alias(args); + if (alias_expansion != NULL) { + int result = lsh_execute(alias_expansion); + // Free the memory allocated for alias expansion + for (int i = 0; alias_expansion[i] != NULL; i++) { + free(alias_expansion[i]); + } + free(alias_expansion); + return result; + } + + return lsh_launch(args); +} + +/** + * Execute a pipeline of commands + */ +int lsh_execute_piped(char ***commands) { + // Count the number of commands in the pipeline + int cmd_count = 0; + while (commands[cmd_count] != NULL) { + cmd_count++; + } + + if (cmd_count == 0) { + return 1; // Nothing to execute + } + + if (cmd_count == 1) { + // No piping needed + return lsh_execute(commands[0]); + } + + // Create pipes + int pipes[cmd_count - 1][2]; + for (int i = 0; i < cmd_count - 1; i++) { + if (pipe(pipes[i]) == -1) { + perror("pipe"); + return 1; + } + } + + // Create processes + pid_t pids[cmd_count]; + for (int i = 0; i < cmd_count; i++) { + pids[i] = fork(); + + if (pids[i] == -1) { + perror("fork"); + return 1; + } else if (pids[i] == 0) { + // Child process + + // Set up input from previous pipe + if (i > 0) { + if (dup2(pipes[i - 1][0], STDIN_FILENO) == -1) { + perror("dup2"); + exit(EXIT_FAILURE); + } + } + + // Set up output to next pipe + if (i < cmd_count - 1) { + if (dup2(pipes[i][1], STDOUT_FILENO) == -1) { + perror("dup2"); + exit(EXIT_FAILURE); + } + } + + // Close all pipe fds + for (int j = 0; j < cmd_count - 1; j++) { + close(pipes[j][0]); + close(pipes[j][1]); + } + + // Execute the command + if (execvp(commands[i][0], commands[i]) == -1) { + perror("execvp"); + exit(EXIT_FAILURE); + } + } + } + + // Parent process closes all pipe fds + for (int i = 0; i < cmd_count - 1; i++) { + close(pipes[i][0]); + close(pipes[i][1]); + } + + // Wait for all children + for (int i = 0; i < cmd_count; i++) { + int status; + waitpid(pids[i], &status, 0); + } + + return 1; +} + +/** + * Free memory for a command array from lsh_split_commands + */ +void free_commands(char ***commands) { + for (int i = 0; commands[i] != NULL; i++) { + for (int j = 0; commands[i][j] != NULL; j++) { + free(commands[i][j]); + } + free(commands[i]); + } + free(commands); +} + +/** + * Display a welcome banner + */ +void display_welcome_banner(void) { + printf(ANSI_COLOR_CYAN); + printf("╔════════════════════════════════════════════════════════════╗\n"); + printf("║ Welcome to the LSH Shell (Linux) ║\n"); + printf("║ ║\n"); + printf("║ Type 'help' to see available commands ║\n"); + printf("╚════════════════════════════════════════════════════════════╝\n"); + printf(ANSI_COLOR_RESET); +} + +/** + * Main shell loop + */ +void lsh_loop(void) { + char *line; + char **args; + char ***commands = NULL; + int status = 1; + char git_info[LSH_RL_BUFSIZE] = {0}; + int terminal_fd; + + // Initialize terminal for raw mode + terminal_fd = init_terminal(&g_orig_termios); + + // Initialize the status bar + init_status_bar(STDOUT_FILENO); + + // Initialize subsystems + init_aliases(); + init_bookmarks(); + init_tab_completion(); + init_persistent_history(); + init_favorite_cities(); + init_themes(); + init_autocorrect(); + init_git_integration(); + + // Display the welcome banner + display_welcome_banner(); + + do { + // Check for console resize + check_console_resize(STDOUT_FILENO); + + // Get Git status for current directory + // If git_status() returns null, git_info[0] will remain 0 + char *git_status_info = get_git_status(); + if (git_status_info != NULL) { + strncpy(git_info, git_status_info, LSH_RL_BUFSIZE - 1); + git_info[LSH_RL_BUFSIZE - 1] = '\0'; + free(git_status_info); + } else { + git_info[0] = '\0'; + } + + // Update status bar with Git information + update_status_bar(STDOUT_FILENO, git_info); + + // Get input from the user + line = lsh_read_line(); + + // Empty line check + if (line == NULL || line[0] == '\0') { + if (line != NULL) { + free(line); + } + continue; + } + + // Add line to persistent history + add_to_history(line); + + // Check for pipes and parse into multiple commands if present + if (strchr(line, '|') != NULL) { + commands = lsh_split_piped_line(line); + status = lsh_execute_piped(commands); + free_commands(commands); + } else { + // Normal command parsing + args = lsh_split_line(line); + + // Check for corrections before executing + char **corrected_args = check_for_corrections(args); + if (corrected_args != NULL) { + // Free the original args + for (int i = 0; args[i] != NULL; i++) { + free(args[i]); + } + free(args); + args = corrected_args; + } + + // Execute command + status = lsh_execute(args); + + // Free allocated memory + for (int i = 0; args[i] != NULL; i++) { + free(args[i]); + } + free(args); + } + + free(line); + } while (status); + + // Shutdown subsystems + shutdown_aliases(); + shutdown_bookmarks(); + shutdown_tab_completion(); + shutdown_persistent_history(); + shutdown_favorite_cities(); + shutdown_themes(); + shutdown_autocorrect(); + + // Restore terminal + restore_terminal(terminal_fd, &g_orig_termios); +} diff --git a/shell.exe b/shell.exe new file mode 100644 index 0000000..dd9f0a6 Binary files /dev/null and b/shell.exe differ diff --git a/shell.h b/shell.h new file mode 100644 index 0000000..91c1844 --- /dev/null +++ b/shell.h @@ -0,0 +1,125 @@ +/** + * shell.h + * Core shell functionality + */ + +#ifndef SHELL_H +#define SHELL_H + +#include "common.h" +#include "structured_data.h" + +/** + * Display a welcome banner with BBQ sauce invention time + */ +void display_welcome_banner(void); + +/** + * Execute a command + * + * @param args Null-terminated array of command arguments + * @return 1 to continue the shell, 0 to exit + */ +int lsh_execute(char **args); + +/** + * Execute a pipeline of commands + * + * @param commands Null-terminated array of command arrays + * @return 1 to continue the shell, 0 to exit + */ +int lsh_execute_piped(char ***commands); + +/** + * Launch an external program + * + * @param args Null-terminated array of command arguments + * @return Always returns 1 (continue the shell) + */ +int lsh_launch(char **args); + +/** + * Main shell loop + */ +void lsh_loop(void); + +/** + * Free memory for a command array from lsh_split_commands + */ +void free_commands(char ***commands); + +/** + * Initialize the status bar at the bottom of the screen + * + * @param fd File descriptor for terminal (STDOUT_FILENO) + * @return 1 if successful, 0 on failure + */ +int init_status_bar(int fd); + +/** + * Temporarily hide the status bar before command execution + * + * @param fd File descriptor for terminal (STDOUT_FILENO) + */ +void hide_status_bar(int fd); + +/** + * Ensures there's space for the status bar by scrolling if needed + * + * @param fd File descriptor for terminal (STDOUT_FILENO) + */ +void ensure_status_bar_space(int fd); + +/** + * Check for terminal window resize and update status bar position + * + * @param fd File descriptor for terminal (STDOUT_FILENO) + */ +void check_console_resize(int fd); + +/** + * Update the status bar with Git information + * + * @param fd File descriptor for terminal (STDOUT_FILENO) + * @param git_info The Git information to display in the status bar + */ +void update_status_bar(int fd, const char *git_info); + +/** + * Get the name of the parent and current directories from a path + * + * @param cwd The current working directory path + * @param parent_dir_name Buffer to store the parent directory name + * @param current_dir_name Buffer to store the current directory name + * @param buf_size Size of the buffers + */ +void get_path_display(const char *cwd, char *parent_dir_name, + char *current_dir_name, size_t buf_size); + +/** + * Initialize the terminal for raw mode + * + * @param orig_termios Original terminal settings to restore later + * @return File descriptor for terminal + */ +int init_terminal(struct termios *orig_termios); + +/** + * Restore terminal to original settings + * + * @param fd File descriptor for terminal + * @param orig_termios Original terminal settings + */ +void restore_terminal(int fd, struct termios *orig_termios); + +/** + * Get console dimensions + * + * @param fd File descriptor for terminal + * @param width Pointer to store width + * @param height Pointer to store height + * @return 1 on success, 0 on failure + */ +int get_console_dimensions(int fd, int *width, int *height); + +#endif // SHELL_H \ No newline at end of file diff --git a/shell.o b/shell.o new file mode 100644 index 0000000..3f5d685 Binary files /dev/null and b/shell.o differ diff --git a/structured_data.c b/structured_data.c new file mode 100644 index 0000000..3adb8ad --- /dev/null +++ b/structured_data.c @@ -0,0 +1,479 @@ +/** + * structured_data.c + * Implementation of structured data operations + */ + +#include "structured_data.h" +#include "builtins.h" // For set_color and reset_color functions +#include + +/** + * Create a new table with the given headers + */ +TableData* create_table(char **headers, int header_count) { + TableData *table = (TableData*)malloc(sizeof(TableData)); + if (!table) { + fprintf(stderr, "lsh: allocation error in create_table\n"); + return NULL; + } + + // Copy headers + table->headers = (char**)malloc(header_count * sizeof(char*)); + if (!table->headers) { + fprintf(stderr, "lsh: allocation error in create_table (headers)\n"); + free(table); + return NULL; + } + + for (int i = 0; i < header_count; i++) { + table->headers[i] = strdup(headers[i]); + } + + table->header_count = header_count; + table->row_count = 0; + table->row_capacity = 10; // Initial capacity for 10 rows + + // Allocate memory for rows + table->rows = (DataValue**)malloc(table->row_capacity * sizeof(DataValue*)); + if (!table->rows) { + fprintf(stderr, "lsh: allocation error in create_table (rows)\n"); + for (int i = 0; i < header_count; i++) { + free(table->headers[i]); + } + free(table->headers); + free(table); + return NULL; + } + + return table; +} + +/** + * Add a row to a table + */ +void add_table_row(TableData *table, DataValue *row) { + if (!table || !row) return; + + // Resize if needed + if (table->row_count >= table->row_capacity) { + table->row_capacity *= 2; + table->rows = (DataValue**)realloc(table->rows, table->row_capacity * sizeof(DataValue*)); + if (!table->rows) { + fprintf(stderr, "lsh: allocation error in add_table_row\n"); + return; + } + } + + // Add the row + table->rows[table->row_count] = row; + table->row_count++; +} + +/** + * Free all memory associated with a table + */ +void free_table(TableData *table) { + if (!table) return; + + // Free headers + for (int i = 0; i < table->header_count; i++) { + free(table->headers[i]); + } + free(table->headers); + + // Free rows + for (int i = 0; i < table->row_count; i++) { + for (int j = 0; j < table->header_count; j++) { + free_data_value(&table->rows[i][j]); + } + free(table->rows[i]); + } + free(table->rows); + + // Free table structure + free(table); +} + +/** + * Create a copy of a DataValue + */ +DataValue copy_data_value(const DataValue *src) { + DataValue dest; + dest.type = src->type; + dest.is_highlighted = src->is_highlighted; // Make sure we preserve the highlighting flag + + switch (src->type) { + case TYPE_STRING: + case TYPE_SIZE: + dest.value.str_val = strdup(src->value.str_val); + break; + case TYPE_INT: + dest.value.int_val = src->value.int_val; + break; + case TYPE_FLOAT: + dest.value.float_val = src->value.float_val; + break; + } + + return dest; +} + +/** + * Free a DataValue + */ +void free_data_value(DataValue *value) { + if (!value) return; + + if ((value->type == TYPE_STRING || value->type == TYPE_SIZE) && value->value.str_val) { + free(value->value.str_val); + value->value.str_val = NULL; + } +} + +/** + * Parse human-readable sizes (e.g., "10kb", "2.5MB") into bytes + */ +long parse_size(const char *size_str) { + char *unit; + double size = strtod(size_str, &unit); + + // Skip whitespace between number and unit + while (*unit && isspace(*unit)) unit++; + + // Handle case insensitive units + if (strcasecmp(unit, "kb") == 0 || strcasecmp(unit, "k") == 0) { + return (long)(size * 1024); + } else if (strcasecmp(unit, "mb") == 0 || strcasecmp(unit, "m") == 0) { + return (long)(size * 1024 * 1024); + } else if (strcasecmp(unit, "gb") == 0 || strcasecmp(unit, "g") == 0) { + return (long)(size * 1024 * 1024 * 1024); + } else if (strcasecmp(unit, "b") == 0 || *unit == '\0') { + return (long)size; + } + + // Try to handle cases like "2.5 KB" with a space + char num_str[32]; + char unit_str[8]; + if (sscanf(size_str, "%31s %7s", num_str, unit_str) == 2) { + double num = atof(num_str); + if (strcasecmp(unit_str, "kb") == 0 || strcasecmp(unit_str, "k") == 0) { + return (long)(num * 1024); + } else if (strcasecmp(unit_str, "mb") == 0 || strcasecmp(unit_str, "m") == 0) { + return (long)(num * 1024 * 1024); + } else if (strcasecmp(unit_str, "gb") == 0 || strcasecmp(unit_str, "g") == 0) { + return (long)(num * 1024 * 1024 * 1024); + } else if (strcasecmp(unit_str, "b") == 0) { + return (long)num; + } + } + + return (long)size; // Default to just the number if no unit matches +} + +/** + * Extract size in bytes from a formatted size string (e.g., "10.5 KB") + */ +long extract_size_bytes(const char *size_str) { + double size_val = 0; + char unit[8] = ""; + + // Try the format with float + space + unit (e.g., "10.5 MB") + if (sscanf(size_str, "%lf %7s", &size_val, unit) == 2) { + if (strcasecmp(unit, "B") == 0) { + return (long)size_val; + } else if (strcasecmp(unit, "KB") == 0) { + return (long)(size_val * 1024); + } else if (strcasecmp(unit, "MB") == 0) { + return (long)(size_val * 1024 * 1024); + } else if (strcasecmp(unit, "GB") == 0) { + return (long)(size_val * 1024 * 1024 * 1024); + } + } + + // Fallback to direct parsing if the format doesn't match + return parse_size(size_str); +} + +/** + * Filter a table based on a condition + */ +TableData* filter_table(TableData *input, char *field, char *op, char *value) { + if (!input || !field || !op || !value) { + return NULL; + } + + // Create a new table with the same headers + TableData *result = create_table(input->headers, input->header_count); + if (!result) { + return NULL; + } + + // Find field index + int field_idx = -1; + for (int i = 0; i < input->header_count; i++) { + if (strcasecmp(input->headers[i], field) == 0) { + field_idx = i; + break; + } + } + + if (field_idx == -1) { + fprintf(stderr, "filter_table: unknown field '%s'\n", field); + free_table(result); + return NULL; + } + + // Special handling for size field - parse human-readable sizes + // Modified to handle both "size" and "Memory" columns + int is_size_field = (strcasecmp(field, "size") == 0 || strcasecmp(field, "Memory") == 0); + long value_size = is_size_field ? parse_size(value) : 0; + + // Filter rows based on condition + for (int i = 0; i < input->row_count; i++) { + int include_row = 0; + + // Handle based on data type + if (input->rows[i][field_idx].type == TYPE_STRING || + input->rows[i][field_idx].type == TYPE_SIZE) { + char *row_value = input->rows[i][field_idx].value.str_val; + + // Special handling for size field with values like "10.5 KB" + if (is_size_field) { + long row_size = extract_size_bytes(row_value); + + // Compare sizes + if (strcmp(op, ">") == 0) { + include_row = (row_size > value_size); + } else if (strcmp(op, "<") == 0) { + include_row = (row_size < value_size); + } else if (strcmp(op, ">=") == 0) { + include_row = (row_size >= value_size); + } else if (strcmp(op, "<=") == 0) { + include_row = (row_size <= value_size); + } else if (strcmp(op, "==") == 0) { + include_row = (row_size == value_size); + } + } else { + // String comparison + int cmp = strcasecmp(row_value, value); + if (strcmp(op, ">") == 0) { + include_row = (cmp > 0); + } else if (strcmp(op, "<") == 0) { + include_row = (cmp < 0); + } else if (strcmp(op, "==") == 0) { + include_row = (cmp == 0); + } else if (strcmp(op, ">=") == 0) { + include_row = (cmp >= 0); + } else if (strcmp(op, "<=") == 0) { + include_row = (cmp <= 0); + } + } + } else if (input->rows[i][field_idx].type == TYPE_INT) { + int row_value = input->rows[i][field_idx].value.int_val; + int val = atoi(value); + + if (strcmp(op, ">") == 0) { + include_row = (row_value > val); + } else if (strcmp(op, "<") == 0) { + include_row = (row_value < val); + } else if (strcmp(op, ">=") == 0) { + include_row = (row_value >= val); + } else if (strcmp(op, "<=") == 0) { + include_row = (row_value <= val); + } else if (strcmp(op, "==") == 0) { + include_row = (row_value == val); + } + } else if (input->rows[i][field_idx].type == TYPE_FLOAT) { + float row_value = input->rows[i][field_idx].value.float_val; + float val = (float)atof(value); + + if (strcmp(op, ">") == 0) { + include_row = (row_value > val); + } else if (strcmp(op, "<") == 0) { + include_row = (row_value < val); + } else if (strcmp(op, ">=") == 0) { + include_row = (row_value >= val); + } else if (strcmp(op, "<=") == 0) { + include_row = (row_value <= val); + } else if (strcmp(op, "==") == 0) { + include_row = (row_value == val); + } + } + + if (include_row) { + // Create a copy of the row for the filtered table + DataValue *row_copy = (DataValue*)malloc(input->header_count * sizeof(DataValue)); + if (!row_copy) { + fprintf(stderr, "lsh: allocation error in filter_table\n"); + free_table(result); + return NULL; + } + + for (int j = 0; j < input->header_count; j++) { + row_copy[j] = copy_data_value(&input->rows[i][j]); + } + + add_table_row(result, row_copy); + } + } + + return result; +} + +/** + * Print a table to the console with nice formatting using Unicode box characters + * Linux version using ANSI escape sequences for colors + */ +void print_table(TableData *table) { + if (!table || table->row_count == 0) { + printf("(empty table)\n"); + return; + } + + // Calculate column widths + int *col_widths = (int*)malloc(table->header_count * sizeof(int)); + if (!col_widths) { + fprintf(stderr, "lsh: allocation error in print_table\n"); + return; + } + + // Initialize with header widths + for (int i = 0; i < table->header_count; i++) { + col_widths[i] = strlen(table->headers[i]); + } + + // Check cell widths + for (int i = 0; i < table->row_count; i++) { + for (int j = 0; j < table->header_count; j++) { + if (table->rows[i][j].type == TYPE_STRING || table->rows[i][j].type == TYPE_SIZE) { + int len = strlen(table->rows[i][j].value.str_val); + if (len > col_widths[j]) { + col_widths[j] = len; + } + } else if (table->rows[i][j].type == TYPE_INT) { + char buf[32]; + int len = snprintf(buf, sizeof(buf), "%d", table->rows[i][j].value.int_val); + if (len > col_widths[j]) { + col_widths[j] = len; + } + } else if (table->rows[i][j].type == TYPE_FLOAT) { + char buf[32]; + int len = snprintf(buf, sizeof(buf), "%.2f", table->rows[i][j].value.float_val); + if (len > col_widths[j]) { + col_widths[j] = len; + } + } + } + } + + // Add padding to column widths + for (int i = 0; i < table->header_count; i++) { + col_widths[i] += 4; // 2 spaces on each side + } + + // Calculate total table width + int totalWidth = 1; // Start with 1 for the left border + for (int i = 0; i < table->header_count; i++) { + totalWidth += col_widths[i] + 1; // Add column width and right border + } + + // Create a buffer big enough for one line of the table + char *lineBuffer = (char*)malloc(totalWidth * 4 + 1); // Unicode chars can be up to 4 bytes + if (!lineBuffer) { + fprintf(stderr, "lsh: allocation error in print_table\n"); + free(col_widths); + return; + } + + // Begin output with a newline + printf("\n"); + + // ---- Print top border ---- + lineBuffer[0] = '\0'; + strcat(lineBuffer, "┌"); // Top-left corner + for (int i = 0; i < table->header_count; i++) { + for (int j = 0; j < col_widths[i]; j++) { + strcat(lineBuffer, "─"); // Horizontal line + } + if (i < table->header_count - 1) { + strcat(lineBuffer, "┬"); // Top T-junction + } + } + strcat(lineBuffer, "┐"); // Top-right corner + printf("%s\n", lineBuffer); + + // ---- Print header row ---- + lineBuffer[0] = '\0'; + strcat(lineBuffer, "│"); // Vertical line + for (int i = 0; i < table->header_count; i++) { + char cellBuffer[256]; + snprintf(cellBuffer, sizeof(cellBuffer), " %-*s ", col_widths[i] - 2, table->headers[i]); + strcat(lineBuffer, cellBuffer); + strcat(lineBuffer, "│"); // Vertical line + } + printf("%s\n", lineBuffer); + + // ---- Print header/data separator ---- + lineBuffer[0] = '\0'; + strcat(lineBuffer, "├"); // Left T-junction + for (int i = 0; i < table->header_count; i++) { + for (int j = 0; j < col_widths[i]; j++) { + strcat(lineBuffer, "─"); // Horizontal line + } + if (i < table->header_count - 1) { + strcat(lineBuffer, "┼"); // Cross junction + } + } + strcat(lineBuffer, "┤"); // Right T-junction + printf("%s\n", lineBuffer); + + // Print data rows + for (int i = 0; i < table->row_count; i++) { + lineBuffer[0] = '\0'; + strcat(lineBuffer, "│"); // Left border + + for (int j = 0; j < table->header_count; j++) { + // Format cell content based on data type + char cellBuffer[256]; + + if (table->rows[i][j].type == TYPE_STRING || table->rows[i][j].type == TYPE_SIZE) { + snprintf(cellBuffer, sizeof(cellBuffer), " %-*s ", col_widths[j] - 2, table->rows[i][j].value.str_val); + } else if (table->rows[i][j].type == TYPE_INT) { + snprintf(cellBuffer, sizeof(cellBuffer), " %-*d ", col_widths[j] - 2, table->rows[i][j].value.int_val); + } else if (table->rows[i][j].type == TYPE_FLOAT) { + snprintf(cellBuffer, sizeof(cellBuffer), " %-*.2f ", col_widths[j] - 2, table->rows[i][j].value.float_val); + } + + // Apply color if the cell is highlighted + if (table->rows[i][j].is_highlighted) { + strcat(lineBuffer, ANSI_COLOR_GREEN); // Green text for highlighted cells + strcat(lineBuffer, cellBuffer); + strcat(lineBuffer, ANSI_COLOR_RESET); // Reset color + } else { + strcat(lineBuffer, cellBuffer); + } + + strcat(lineBuffer, "│"); // Right border of cell + } + + printf("%s\n", lineBuffer); + } + + // ---- Print bottom border ---- + lineBuffer[0] = '\0'; + strcat(lineBuffer, "└"); // Bottom-left corner + for (int i = 0; i < table->header_count; i++) { + for (int j = 0; j < col_widths[i]; j++) { + strcat(lineBuffer, "─"); // Horizontal line + } + if (i < table->header_count - 1) { + strcat(lineBuffer, "┴"); // Bottom T-junction + } + } + strcat(lineBuffer, "┘"); // Bottom-right corner + printf("%s\n\n", lineBuffer); + + // Clean up + free(lineBuffer); + free(col_widths); +} \ No newline at end of file diff --git a/structured_data.h b/structured_data.h new file mode 100644 index 0000000..15680f9 --- /dev/null +++ b/structured_data.h @@ -0,0 +1,70 @@ +/** + * structured_data.h + * Definitions for structured data representation and manipulation + */ + +#ifndef STRUCTURED_DATA_H +#define STRUCTURED_DATA_H + +#include "common.h" + +// Value types for structured data +typedef enum { + TYPE_STRING, + TYPE_INT, + TYPE_FLOAT, + TYPE_SIZE // Special type for file sizes with units +} ValueType; + +// A single data value that can be of different types +typedef struct { + ValueType type; + union { + char *str_val; + int int_val; + float float_val; + struct { + double value; + char unit[8]; + } size_val; + } value; + int is_highlighted; // Flag for highlighting in tables +} DataValue; + +// Table structure to hold rows and columns of data +typedef struct { + char **headers; // Column names + int header_count; // Number of columns + DataValue **rows; // Array of rows (each row is an array of DataValues) + int row_count; // Current number of rows + int row_capacity; // Allocated capacity for rows +} TableData; + +// Function to create a new table with the given headers +TableData* create_table(char **headers, int header_count); + +// Function to add a row to a table +void add_table_row(TableData *table, DataValue *row); + +// Function to free all memory associated with a table +void free_table(TableData *table); + +// Function to create a copy of a DataValue +DataValue copy_data_value(const DataValue *src); + +// Function to free a DataValue +void free_data_value(DataValue *value); + +// Function to filter a table based on a condition +TableData* filter_table(TableData *input, char *field, char *op, char *value); + +// Function to print a table to the console +void print_table(TableData *table); + +// Function to parse human-readable sizes +long parse_size(const char *size_str); + +// Function to extract size in bytes from a formatted size string +long extract_size_bytes(const char *size_str); + +#endif // STRUCTURED_DATA_H diff --git a/structured_data.o b/structured_data.o new file mode 100644 index 0000000..590580f Binary files /dev/null and b/structured_data.o differ diff --git a/tab_complete.c b/tab_complete.c new file mode 100644 index 0000000..3d3116a --- /dev/null +++ b/tab_complete.c @@ -0,0 +1,460 @@ +/** + * tab_complete.c + * Linux implementation of tab completion + */ + +#include "tab_complete.h" +#include "aliases.h" +#include "bookmarks.h" +#include "builtins.h" +#include "favorite_cities.h" +#include "themes.h" +#include +#include +#include +#include +#include +#include +#include + +// Predefined command argument types for better completion +static CommandArgInfo command_arg_info[] = { + {"cd", ARG_TYPE_DIRECTORY, "Change current directory"}, + {"ls", ARG_TYPE_DIRECTORY, "List directory contents"}, + {"cat", ARG_TYPE_FILE, "Display file contents"}, + {"rm", ARG_TYPE_FILE, "Remove file"}, + {"mkdir", ARG_TYPE_DIRECTORY, "Make directory"}, + {"rmdir", ARG_TYPE_DIRECTORY, "Remove directory"}, + {"cp", ARG_TYPE_FILE, "Copy file or directory"}, + {"mv", ARG_TYPE_BOTH, "Move file or directory"}, + {"grep", ARG_TYPE_FILE, "Search file contents"}, + {"less", ARG_TYPE_FILE, "View file contents"}, + {"more", ARG_TYPE_FILE, "View file contents"}, + {"find", ARG_TYPE_DIRECTORY, "Find files"}, + {"chmod", ARG_TYPE_FILE, "Change file permissions"}, + {"chown", ARG_TYPE_FILE, "Change file owner"}, + {"tar", ARG_TYPE_FILE, "Archive utility"}, + {"gzip", ARG_TYPE_FILE, "Compress files"}, + {"gunzip", ARG_TYPE_FILE, "Decompress files"}, + {"zip", ARG_TYPE_FILE, "Compress files"}, + {"unzip", ARG_TYPE_FILE, "Decompress files"}, + {"bash", ARG_TYPE_FILE, "Run bash script"}, + {"sh", ARG_TYPE_FILE, "Run shell script"}, + {"python", ARG_TYPE_FILE, "Run Python script"}, + {"perl", ARG_TYPE_FILE, "Run Perl script"}, + {"java", ARG_TYPE_FILE, "Run Java program"}, + {"gcc", ARG_TYPE_FILE, "C compiler"}, + {"make", ARG_TYPE_FILE, "Build utility"}, + {"diff", ARG_TYPE_FILE, "Compare files"}, + {"patch", ARG_TYPE_FILE, "Apply patch file"}, + {"man", ARG_TYPE_ANY, "Display manual page"}, + {"help", ARG_TYPE_ANY, "Display help"}, + {"alias", ARG_TYPE_ALIAS, "Define or list aliases"}, + {"unalias", ARG_TYPE_ALIAS, "Remove alias"}, + {"bookmark", ARG_TYPE_BOOKMARK, "Bookmark directories"}, + {"weather", ARG_TYPE_FAVORITE_CITY, "Weather information"}, + {"theme", ARG_TYPE_THEME, "Shell theme settings"}, + {NULL, ARG_TYPE_ANY, NULL} // End marker +}; + +// Global state +static CommandContext current_context; + +/** + * Initialize tab completion system + */ +void init_tab_completion(void) { + // Initialize context to defaults + memset(¤t_context, 0, sizeof(CommandContext)); +} + +/** + * Shutdown tab completion system + */ +void shutdown_tab_completion(void) { + // Nothing to clean up for now +} + +/** + * Get the type of argument expected for a command + */ +static ArgumentType get_argument_type(const char *cmd) { + if (!cmd || !*cmd) return ARG_TYPE_ANY; + + for (int i = 0; command_arg_info[i].command != NULL; i++) { + if (strcmp(command_arg_info[i].command, cmd) == 0) { + return command_arg_info[i].arg_type; + } + } + + return ARG_TYPE_ANY; +} + +/** + * Parse the command line to determine context + */ +static void parse_command_context(const char *buffer) { + // Reset the context + memset(¤t_context, 0, sizeof(CommandContext)); + + if (!buffer || !*buffer) return; + + // Make a copy of the buffer to tokenize + char buffer_copy[1024]; + strncpy(buffer_copy, buffer, sizeof(buffer_copy) - 1); + buffer_copy[sizeof(buffer_copy) - 1] = '\0'; + + // Tokenize the buffer + char *token = strtok(buffer_copy, " \t"); + int token_index = 0; + + // Extract the command if present + if (token) { + strncpy(current_context.filter_command, token, sizeof(current_context.filter_command) - 1); + current_context.filter_command[sizeof(current_context.filter_command) - 1] = '\0'; + token = strtok(NULL, " \t"); + token_index++; + } + + // Get the last token (current word being completed) + char *last_token = NULL; + while (token) { + last_token = token; + token_index++; + token = strtok(NULL, " \t"); + } + + if (last_token) { + strncpy(current_context.current_token, last_token, sizeof(current_context.current_token) - 1); + current_context.current_token[sizeof(current_context.current_token) - 1] = '\0'; + current_context.token_index = token_index - 1; // Adjust token index to be 0-based + } else { + // This handles the case where there's only one token (the command) + // In this case, we're completing the command itself + strncpy(current_context.current_token, current_context.filter_command, + sizeof(current_context.current_token) - 1); + current_context.current_token[sizeof(current_context.current_token) - 1] = '\0'; + current_context.token_index = 0; + } +} + +/** + * Find all files/directories that match the beginning of path + */ +static char *find_path_completions(const char *path) { + if (!path || !*path) return NULL; + + char dir_path[PATH_MAX] = "."; // Default to current directory + char name_prefix[PATH_MAX] = ""; + + // Split path into directory part and name prefix part + char *last_slash = strrchr(path, '/'); + if (last_slash) { + // Path contains a directory part + int dir_len = last_slash - path; + strncpy(dir_path, path, 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, path, sizeof(name_prefix) - 1); + name_prefix[sizeof(name_prefix) - 1] = '\0'; + } + + // Open the directory + DIR *dir = opendir(dir_path); + if (!dir) { + return NULL; + } + + // Find the first matching entry + struct dirent *entry; + char *completion = NULL; + int name_prefix_len = strlen(name_prefix); + + while ((entry = readdir(dir)) != NULL) { + // Skip "." and ".." if there's already a name prefix + if (name_prefix_len > 0 && (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0)) { + continue; + } + + // Check if this entry matches the name prefix + if (strncasecmp(entry->d_name, name_prefix, name_prefix_len) == 0) { + // Construct the completion + char full_path[PATH_MAX]; + + // Check if we're completing from root or relative + if (dir_path[0] == '/' && dir_path[1] == '\0') { + snprintf(full_path, sizeof(full_path), "/%s", entry->d_name); + } else if (strcmp(dir_path, ".") == 0) { + snprintf(full_path, sizeof(full_path), "%s", entry->d_name); + } else { + snprintf(full_path, sizeof(full_path), "%s/%s", dir_path, entry->d_name); + } + + // Check if it's a directory, append slash if it is + struct stat st; + char path_to_check[PATH_MAX]; + + if (full_path[0] == '/') { + // Absolute path + strncpy(path_to_check, full_path, sizeof(path_to_check) - 1); + } else { + // Relative path, construct with current directory + char cwd[PATH_MAX]; + if (getcwd(cwd, sizeof(cwd))) { + snprintf(path_to_check, sizeof(path_to_check), "%s/%s", cwd, full_path); + } else { + strncpy(path_to_check, full_path, sizeof(path_to_check) - 1); + } + } + + path_to_check[sizeof(path_to_check) - 1] = '\0'; + + if (stat(path_to_check, &st) == 0 && S_ISDIR(st.st_mode)) { + char *dir_completion = (char *)malloc(strlen(full_path) + 2); // +2 for slash and null + if (dir_completion) { + strcpy(dir_completion, full_path); + strcat(dir_completion, "/"); + completion = dir_completion; + } + } else { + completion = strdup(full_path); + } + + break; + } + } + + closedir(dir); + return completion; +} + +/** + * Get the completion for a command + */ +static char *complete_command(const char *prefix) { + if (!prefix || !*prefix) return NULL; + + // First check builtins + int builtin_count = lsh_num_builtins(); + + for (int i = 0; i < builtin_count; i++) { + if (strncasecmp(builtin_str[i], prefix, strlen(prefix)) == 0) { + return strdup(builtin_str[i]); + } + } + + // Then check for aliases + int alias_count; + char **aliases = get_alias_names(&alias_count); + + if (aliases) { + for (int i = 0; i < alias_count; i++) { + if (strncasecmp(aliases[i], prefix, strlen(prefix)) == 0) { + char *result = strdup(aliases[i]); + + // Free the alias names + for (int j = 0; j < alias_count; j++) { + free(aliases[j]); + } + free(aliases); + + return result; + } + } + + // Free the alias names if we didn't find a match + for (int i = 0; i < alias_count; i++) { + free(aliases[i]); + } + free(aliases); + } + + // Finally check for executables in PATH + char *path = getenv("PATH"); + if (!path) return NULL; + + char *path_copy = strdup(path); + if (!path_copy) return NULL; + + char *result = NULL; + char *dir = strtok(path_copy, ":"); + + while (dir && !result) { + DIR *dirp = opendir(dir); + if (dirp) { + struct dirent *entry; + while ((entry = readdir(dirp)) != NULL) { + if (strncasecmp(entry->d_name, prefix, strlen(prefix)) == 0) { + // Check if it's executable + char full_path[PATH_MAX]; + snprintf(full_path, sizeof(full_path), "%s/%s", dir, entry->d_name); + + struct stat st; + if (stat(full_path, &st) == 0 && (st.st_mode & S_IXUSR)) { + result = strdup(entry->d_name); + break; + } + } + } + closedir(dirp); + } + dir = strtok(NULL, ":"); + } + + free(path_copy); + return result; +} + +/** + * Get completion for the current context + */ +char *get_tab_completion(const char *buffer) { + if (!buffer) return NULL; + + // Parse the command context + parse_command_context(buffer); + + // If we're completing the first token, it's a command + if (current_context.token_index == 0) { + return complete_command(current_context.current_token); + } + + // For subsequent tokens, look at the command to determine what to complete + ArgumentType arg_type = get_argument_type(current_context.filter_command); + + switch (arg_type) { + case ARG_TYPE_FILE: + // Complete file names + return find_path_completions(current_context.current_token); + + case ARG_TYPE_DIRECTORY: + // Complete directory names + return find_path_completions(current_context.current_token); + + case ARG_TYPE_BOTH: + // Complete both files and directories + return find_path_completions(current_context.current_token); + + case ARG_TYPE_BOOKMARK: + // Complete bookmark names + if (current_context.current_token[0]) { + int bookmark_count; + char **bookmarks = get_bookmark_names(&bookmark_count); + + if (bookmarks) { + char *result = NULL; + for (int i = 0; i < bookmark_count; i++) { + if (strncasecmp(bookmarks[i], current_context.current_token, strlen(current_context.current_token)) == 0) { + result = strdup(bookmarks[i]); + break; + } + } + + // Free the bookmark names + for (int i = 0; i < bookmark_count; i++) { + free(bookmarks[i]); + } + free(bookmarks); + + return result; + } + } + break; + + case ARG_TYPE_ALIAS: + // Complete alias names + if (current_context.current_token[0]) { + int alias_count; + char **aliases = get_alias_names(&alias_count); + + if (aliases) { + char *result = NULL; + for (int i = 0; i < alias_count; i++) { + if (strncasecmp(aliases[i], current_context.current_token, strlen(current_context.current_token)) == 0) { + result = strdup(aliases[i]); + break; + } + } + + // Free the alias names + for (int i = 0; i < alias_count; i++) { + free(aliases[i]); + } + free(aliases); + + return result; + } + } + break; + + case ARG_TYPE_FAVORITE_CITY: + // Complete favorite city names + if (current_context.current_token[0]) { + int city_count; + char **cities = get_favorite_city_names(&city_count); + + if (cities) { + char *result = NULL; + for (int i = 0; i < city_count; i++) { + if (strncasecmp(cities[i], current_context.current_token, strlen(current_context.current_token)) == 0) { + result = strdup(cities[i]); + break; + } + } + + // Free the city names + for (int i = 0; i < city_count; i++) { + free(cities[i]); + } + free(cities); + + return result; + } + } + break; + + case ARG_TYPE_THEME: + // Complete theme names + if (current_context.current_token[0]) { + int theme_count; + char **themes = get_theme_names(&theme_count); + + if (themes) { + char *result = NULL; + for (int i = 0; i < theme_count; i++) { + if (strncasecmp(themes[i], current_context.current_token, strlen(current_context.current_token)) == 0) { + result = strdup(themes[i]); + break; + } + } + + // Free the theme names + for (int i = 0; i < theme_count; i++) { + free(themes[i]); + } + free(themes); + + return result; + } + } + break; + + case ARG_TYPE_ANY: + default: + // By default, complete file and directory names + return find_path_completions(current_context.current_token); + } + + return NULL; +} diff --git a/tab_complete.h b/tab_complete.h new file mode 100644 index 0000000..0e47b13 --- /dev/null +++ b/tab_complete.h @@ -0,0 +1,58 @@ +/** + * tab_complete.h + * Functions for tab completion and path suggestions + */ + +#ifndef TAB_COMPLETE_H +#define TAB_COMPLETE_H + +#include "common.h" + +/** + * Command context structure + */ +typedef struct { + int is_after_pipe; // Flag if cursor is right after a pipe + int is_filter_command; // Flag if current command is a filter command + char cmd_before_pipe[64]; // Command before the pipe + char filter_command[64]; // Current filter command + char current_token[1024]; // Current token being typed + int token_position; // Position within current token + int token_index; // Index of current token in command + int filter_arg_index; // Argument index for filter command + int has_current_field; // Flag if current field is set + int has_current_operator; // Flag if current operator is set + char current_field[64]; // Current field if known + char current_operator[16]; // Current operator if known +} CommandContext; + +typedef enum { + ARG_TYPE_ANY, // Any argument + ARG_TYPE_FILE, // Files only + ARG_TYPE_DIRECTORY, // Directories only + ARG_TYPE_BOOKMARK, // Bookmark names + ARG_TYPE_ALIAS, // Alias names + ARG_TYPE_BOTH, // Both files and directories + ARG_TYPE_FAVORITE_CITY, + ARG_TYPE_THEME, +} ArgumentType; + +/** + * Structure for command argument type information + */ +typedef struct { + char *command; // Command name + ArgumentType arg_type; // Expected argument type + char *description; // Optional description +} CommandArgInfo; + +// Main tab completion function +char *get_tab_completion(const char *buffer); + +// Initialize tab completion system +void init_tab_completion(void); + +// Shutdown tab completion system +void shutdown_tab_completion(void); + +#endif // TAB_COMPLETE_H diff --git a/tab_complete.o b/tab_complete.o new file mode 100644 index 0000000..f22a705 Binary files /dev/null and b/tab_complete.o differ diff --git a/themes.c b/themes.c new file mode 100644 index 0000000..696f7d3 --- /dev/null +++ b/themes.c @@ -0,0 +1,481 @@ +/** + * themes.c + * Implementation of theme system for shell appearance + */ + +#include "themes.h" +#include +#include + +// Define Windows colors for compatibility +#define FOREGROUND_BLUE 0x0001 +#define FOREGROUND_GREEN 0x0002 +#define FOREGROUND_RED 0x0004 +#define FOREGROUND_INTENSITY 0x0008 + +#define FOREGROUND_CYAN (FOREGROUND_GREEN | FOREGROUND_BLUE) +#define FOREGROUND_MAGENTA (FOREGROUND_RED | FOREGROUND_BLUE) + +// Current active theme +ShellTheme current_theme; + +// Define built-in themes +static ShellTheme default_theme = { + // Standard console colors + .PRIMARY_COLOR = + FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE, // White + .SECONDARY_COLOR = + FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE, // White (dimmer) + .ACCENT_COLOR = FOREGROUND_GREEN | FOREGROUND_INTENSITY, // Bright Green + .SUCCESS_COLOR = FOREGROUND_GREEN | FOREGROUND_INTENSITY, // Bright Green + .ERROR_COLOR = FOREGROUND_RED | FOREGROUND_INTENSITY, // Bright Red + .WARNING_COLOR = FOREGROUND_RED | FOREGROUND_GREEN | + FOREGROUND_INTENSITY, // Bright Yellow + + .HEADER_COLOR = FOREGROUND_GREEN | FOREGROUND_INTENSITY, // Bright Green + .STATUS_BAR_COLOR = 0, // Black + .STATUS_TEXT_COLOR = FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE | + FOREGROUND_INTENSITY, // Bright White + .PROMPT_COLOR = FOREGROUND_CYAN | FOREGROUND_INTENSITY, // Bright Cyan + + .DIRECTORY_COLOR = FOREGROUND_GREEN | FOREGROUND_INTENSITY, // Bright Green + .EXECUTABLE_COLOR = FOREGROUND_RED | FOREGROUND_GREEN | + FOREGROUND_INTENSITY, // Bright Yellow + .TEXT_FILE_COLOR = + FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE, // White + .IMAGE_FILE_COLOR = FOREGROUND_RED | FOREGROUND_BLUE | + FOREGROUND_INTENSITY, // Bright Magenta + .CODE_FILE_COLOR = FOREGROUND_BLUE | FOREGROUND_INTENSITY, // Bright Blue + .ARCHIVE_FILE_COLOR = FOREGROUND_RED | FOREGROUND_INTENSITY, // Bright Red + + .SYNTAX_KEYWORD = FOREGROUND_CYAN | FOREGROUND_INTENSITY, // Bright Cyan + .SYNTAX_STRING = FOREGROUND_GREEN | FOREGROUND_INTENSITY, // Bright Green + .SYNTAX_COMMENT = FOREGROUND_INTENSITY, // Gray + .SYNTAX_NUMBER = + FOREGROUND_MAGENTA | FOREGROUND_INTENSITY, // Bright Magenta + .SYNTAX_PREPROCESSOR = + FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_INTENSITY, // Yellow + + // ANSI colors for default theme (basic colors, not true full 24-bit colors) + .ANSI_BASE = "\033[37m", // White + .ANSI_SURFACE = "\033[37m", // White + .ANSI_OVERLAY = "\033[37m", // White + .ANSI_MUTED = "\033[37m", // White + .ANSI_SUBTLE = "\033[37m", // White + .ANSI_TEXT = "\033[97m", // Bright White + .ANSI_LOVE = "\033[91m", // Bright Red + .ANSI_GOLD = "\033[93m", // Bright Yellow + .ANSI_ROSE = "\033[95m", // Bright Magenta + .ANSI_PINE = "\033[92m", // Bright Green + .ANSI_FOAM = "\033[96m", // Bright Cyan + .ANSI_IRIS = "\033[94m", // Bright Blue + .ANSI_HIGHLIGHT = "\033[37m", // White + + // Don't use ANSI colors for default theme + .use_ansi_colors = FALSE, + + .name = "default"}; + +static ShellTheme rose_pine_theme = { + // Legacy console colors (for backward compatibility) + .PRIMARY_COLOR = + FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE, // White (base) + .SECONDARY_COLOR = + FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE, // Muted + .ACCENT_COLOR = FOREGROUND_BLUE, // Blue for git syntax + .SUCCESS_COLOR = FOREGROUND_GREEN, + .ERROR_COLOR = FOREGROUND_RED | FOREGROUND_INTENSITY, + .WARNING_COLOR = FOREGROUND_RED | FOREGROUND_GREEN, + + .HEADER_COLOR = FOREGROUND_RED | FOREGROUND_BLUE, + .STATUS_BAR_COLOR = 0, + .STATUS_TEXT_COLOR = FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE | + FOREGROUND_INTENSITY, + .PROMPT_COLOR = + FOREGROUND_RED | FOREGROUND_INTENSITY, // Bright red for repo name + + .DIRECTORY_COLOR = + FOREGROUND_RED | FOREGROUND_INTENSITY, // Bright pink for directory + + .EXECUTABLE_COLOR = FOREGROUND_RED | FOREGROUND_GREEN, + .TEXT_FILE_COLOR = FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE, + .IMAGE_FILE_COLOR = FOREGROUND_BLUE, + .CODE_FILE_COLOR = FOREGROUND_BLUE, + .ARCHIVE_FILE_COLOR = FOREGROUND_RED, + + .SYNTAX_KEYWORD = FOREGROUND_RED | FOREGROUND_BLUE, + .SYNTAX_STRING = FOREGROUND_GREEN, + .SYNTAX_COMMENT = FOREGROUND_RED | FOREGROUND_GREEN | 0x8, + .SYNTAX_NUMBER = FOREGROUND_RED, + .SYNTAX_PREPROCESSOR = FOREGROUND_RED | FOREGROUND_GREEN, + + // ANSI true color definitions - exact Rose Pine colors + .ANSI_BASE = "\033[38;2;25;23;36m", // Base color + .ANSI_SURFACE = "\033[38;2;31;29;46m", // Surface color + .ANSI_OVERLAY = "\033[38;2;38;35;58m", // Overlay color + .ANSI_MUTED = "\033[38;2;110;106;134m", // Muted color + .ANSI_SUBTLE = "\033[38;2;144;140;170m", // Subtle color + .ANSI_TEXT = "\033[38;2;224;222;244m", // Text color + .ANSI_LOVE = "\033[38;2;235;111;146m", // Love/Red color + .ANSI_GOLD = "\033[38;2;246;193;119m", // Gold color + // Updated Rose/pink color to a soft peach + .ANSI_ROSE = "\033[38;2;255;195;195m", // Soft Peach + .ANSI_PINE = "\033[38;2;49;116;143m", // Pine color + .ANSI_FOAM = "\033[38;2;156;207;216m", // Foam color + .ANSI_IRIS = "\033[38;2;196;167;231m", // Iris color + .ANSI_HIGHLIGHT = "\033[38;2;68;65;90m", // Highlight color + .ANSI_INVALID_COMMAND = "\033[38;2;205;120;120m", + + // Enable ANSI colors for Rose Pine theme + .use_ansi_colors = TRUE, + + .name = "rose-pine"}; + +static ShellTheme catppuccin_mocha_theme = { + // Legacy console colors (for backward compatibility) + .PRIMARY_COLOR = FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE | + FOREGROUND_INTENSITY, // Bright white for text + .SECONDARY_COLOR = + FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE, // White (dimmer) + .ACCENT_COLOR = + FOREGROUND_BLUE | FOREGROUND_INTENSITY, // Bright Blue for accent + .SUCCESS_COLOR = FOREGROUND_GREEN | FOREGROUND_INTENSITY, // Bright Green + .ERROR_COLOR = FOREGROUND_RED | FOREGROUND_INTENSITY, // Bright Red + .WARNING_COLOR = + FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_INTENSITY, // Yellow + + .HEADER_COLOR = FOREGROUND_RED | FOREGROUND_BLUE | + FOREGROUND_INTENSITY, // Mauve color for headers + .STATUS_BAR_COLOR = 0, // Black/dark background + .STATUS_TEXT_COLOR = FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE | + FOREGROUND_INTENSITY, // Bright White + .PROMPT_COLOR = FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE | + FOREGROUND_INTENSITY, // Blue text for prompt + + .DIRECTORY_COLOR = FOREGROUND_BLUE | FOREGROUND_GREEN | + FOREGROUND_INTENSITY, // Teal for directories + .EXECUTABLE_COLOR = FOREGROUND_RED | FOREGROUND_GREEN | + FOREGROUND_INTENSITY, // Yellow for executables + .TEXT_FILE_COLOR = FOREGROUND_RED | FOREGROUND_GREEN | + FOREGROUND_BLUE, // White for text files + .IMAGE_FILE_COLOR = FOREGROUND_RED | FOREGROUND_INTENSITY, // Red for images + .CODE_FILE_COLOR = FOREGROUND_BLUE | FOREGROUND_INTENSITY, // Blue for code + .ARCHIVE_FILE_COLOR = + FOREGROUND_GREEN | FOREGROUND_INTENSITY, // Green for archives + + .SYNTAX_KEYWORD = + FOREGROUND_BLUE | FOREGROUND_INTENSITY, // Blue for keywords + .SYNTAX_STRING = + FOREGROUND_GREEN | FOREGROUND_INTENSITY, // Green for strings + .SYNTAX_COMMENT = FOREGROUND_INTENSITY, // Gray for comments + .SYNTAX_NUMBER = FOREGROUND_RED | FOREGROUND_INTENSITY, // Red for numbers + .SYNTAX_PREPROCESSOR = FOREGROUND_GREEN | FOREGROUND_BLUE | + FOREGROUND_INTENSITY, // Cyan for preprocessor + + // ANSI true color definitions for Catppuccin Mocha + .ANSI_BASE = "\033[38;2;30;30;46m", // Base color #1e1e2e + .ANSI_SURFACE = "\033[38;2;49;50;68m", // Surface color #313244 + .ANSI_OVERLAY = "\033[38;2;69;71;90m", // Overlay color #45475a + .ANSI_MUTED = "\033[38;2;186;194;222m", // Muted color #bac2de (subtext1) + .ANSI_SUBTLE = "\033[38;2;166;173;200m", // Subtle color #a6adc8 (subtext0) + .ANSI_TEXT = "\033[38;2;205;214;244m", // Text color #cdd6f4 + .ANSI_LOVE = "\033[38;2;243;139;168m", // Love/Red color #f38ba8 + .ANSI_GOLD = "\033[38;2;249;226;175m", // Gold/Yellow color #f9e2af + .ANSI_ROSE = "\033[38;2;245;194;231m", // Pink color #f5c2e7 + .ANSI_PINE = "\033[38;2;166;227;161m", // Green color #a6e3a1 + .ANSI_FOAM = "\033[38;2;148;226;213m", // Teal color #94e2d5 + .ANSI_IRIS = "\033[38;2;203;166;247m", // Mauve/Purple color #cba6f7 + .ANSI_HIGHLIGHT = "\033[38;2;180;190;254m", // Lavender highlight #b4befe + + // Enable ANSI colors for Catppuccin Mocha theme + .use_ansi_colors = TRUE, + + .name = "catppuccin-mocha"}; + +// Path to the theme configuration file +char theme_config_path[PATH_MAX]; + +/** + * Initialize the theme system + */ +void init_theme_system(void) { + // Determine theme config file location in user's home directory + char *home_dir = getenv("HOME"); + if (home_dir) { + snprintf(theme_config_path, PATH_MAX, "%s/.lsh_theme", home_dir); + } else { + // Fallback to current directory if HOME not available + strcpy(theme_config_path, ".lsh_theme"); + } + + // Set default theme initially + memcpy(¤t_theme, &default_theme, sizeof(ShellTheme)); + + // Try to load theme from config file + FILE *config_file = fopen(theme_config_path, "r"); + if (config_file) { + char theme_name[32]; + if (fscanf(config_file, "theme=%31s", theme_name) == 1) { + // We found a theme name, try to load it + load_theme(theme_name); + } + fclose(config_file); + } else { + // Config file doesn't exist, create it with default theme + config_file = fopen(theme_config_path, "w"); + if (config_file) { + fprintf(config_file, "theme=default\n"); + fclose(config_file); + } + } +} + +/** + * Load a specific theme by name + * + * @param theme_name Name of the theme to load + * @return 1 if successful, 0 if theme not found + */ +int load_theme(const char *theme_name) { + if (strcmp(theme_name, "default") == 0) { + memcpy(¤t_theme, &default_theme, sizeof(ShellTheme)); + return 1; + } else if (strcmp(theme_name, "rose-pine") == 0) { + memcpy(¤t_theme, &rose_pine_theme, sizeof(ShellTheme)); + return 1; + } else if (strcmp(theme_name, "catppuccin-mocha") == 0) { + memcpy(¤t_theme, &catppuccin_mocha_theme, sizeof(ShellTheme)); + return 1; + } + + // Theme not found + fprintf(stderr, "Theme '%s' not found\n", theme_name); + return 0; +} + +/** + * Apply the current theme settings + * Called whenever theme-dependent UI elements need to be updated + */ +void apply_current_theme(void) { + // When called, the shell can update any on-screen elements that depend on + // theme colors. This function would be called when changing themes or at startup. + + // In Linux, we'll always use ANSI colors, as the terminal emulator handles this natively + if (current_theme.use_ansi_colors) { + // Use ANSI true color + printf("%s", current_theme.ANSI_TEXT); + } else { + // Use standard ANSI color approximation of the theme's primary color + // Linux terminals always support basic ANSI colors, so we'll map the Windows color attributes + // to the closest ANSI colors + + int fg = 37; // Default to white text + + if (current_theme.PRIMARY_COLOR & 0x01) fg = 34; // Blue + if (current_theme.PRIMARY_COLOR & 0x02) fg = 32; // Green + if (current_theme.PRIMARY_COLOR & 0x04) fg = 31; // Red + + // Combinations + if ((current_theme.PRIMARY_COLOR & 0x03) == 0x03) fg = 36; // Cyan (Green + Blue) + if ((current_theme.PRIMARY_COLOR & 0x05) == 0x05) fg = 35; // Magenta (Red + Blue) + if ((current_theme.PRIMARY_COLOR & 0x06) == 0x06) fg = 33; // Yellow (Red + Green) + if ((current_theme.PRIMARY_COLOR & 0x07) == 0x07) fg = 37; // White (Red + Green + Blue) + + // Bright variants + if (current_theme.PRIMARY_COLOR & 0x08) fg += 60; // Add intensity + + printf("\033[%dm", fg); // Set the color + } +} + +/** + * Get pointer to the current theme + * + * @return Pointer to the current theme structure + */ +const ShellTheme *get_current_theme(void) { return ¤t_theme; } + +/** + * List all available themes + */ +void list_available_themes(void) { + printf("Available themes:\n"); + printf(" default - Standard shell colors\n"); + printf( + " rose-pine - Soothing, warm color scheme with true color support\n"); + printf(" catppuccin-mocha - Deep dark theme with vibrant accent colors"); + + // Display which theme is currently active + printf("\nCurrent theme: %s", current_theme.name); + + // Display ANSI color status for current theme + if (current_theme.use_ansi_colors) { + printf(" (using true color)\n"); + } else { + printf(" (using standard console colors)\n"); + } +} + +/** + * Initialize themes at shell startup + */ +void init_themes(void) { + // Call the theme system initialization + init_theme_system(); + + // Apply the initial theme + apply_current_theme(); +} + +/** + * Shutdown theme system + */ +void shutdown_themes(void) { + // Reset terminal color to default + printf(ANSI_COLOR_RESET); + + // Currently, no other cleanup is needed for the theme system +} + +/** + * Command handler for the "theme" command + * + * @param args Command arguments + * @return 1 to continue shell execution, 0 to exit shell + */ +int lsh_theme(char **args) { + if (args[1] == NULL) { + // No arguments, show usage info and current theme + printf("Usage: theme [arguments]\n"); + printf("Commands:\n"); + printf(" list List available themes\n"); + printf(" set NAME Set the current theme to NAME\n"); + printf(" show Show current theme details\n"); + printf("\nCurrent theme: %s", current_theme.name); + + // Display ANSI color status for current theme + if (current_theme.use_ansi_colors) { + printf(" (using true color)\n"); + } else { + printf(" (using standard console colors)\n"); + } + + return 1; + } + + // Handle subcommands + if (strcmp(args[1], "list") == 0) { + list_available_themes(); + return 1; + } else if (strcmp(args[1], "set") == 0) { + if (args[2] == NULL) { + printf("Usage: theme set \n"); + printf("Try 'theme list' to see available themes\n"); + return 1; + } + + // Try to load the requested theme + if (load_theme(args[2])) { + // Update theme config file + FILE *config_file = fopen(theme_config_path, "w"); + if (config_file) { + fprintf(config_file, "theme=%s\n", args[2]); + fclose(config_file); + + // Apply the theme + apply_current_theme(); + + printf("Theme set to '%s'\n", args[2]); + + // Display ANSI color status for the selected theme + if (current_theme.use_ansi_colors) { + printf("This theme uses true color for better visual appearance.\n"); + } + } else { + fprintf(stderr, "Could not save theme setting\n"); + } + } + return 1; + } else if (strcmp(args[1], "show") == 0) { + printf("Current theme: %s\n", current_theme.name); + + // Display ANSI color status + if (current_theme.use_ansi_colors) { + printf("Using true color mode"); + + // Show a color demo if using ANSI colors + printf("\nColor sample:\n"); + printf("%sBase %sText %sLove %sGold %sRose %sPine %sFoam %sIris%s\n", + current_theme.ANSI_BASE, current_theme.ANSI_TEXT, + current_theme.ANSI_LOVE, current_theme.ANSI_GOLD, + current_theme.ANSI_ROSE, current_theme.ANSI_PINE, + current_theme.ANSI_FOAM, current_theme.ANSI_IRIS, + "\033[0m"); // Reset + } else { + printf("Using standard console colors\n"); + } + + return 1; + } else { + printf("Unknown theme command: %s\n", args[1]); + printf("Try 'theme list' or 'theme set '\n"); + return 1; + } + + return 1; +} +char **get_theme_names(int *theme_count) { + // Define the hardcoded themes + char *builtin_themes[] = {"default", "rose-pine", "catppuccin-mocha"}; + int builtin_count = sizeof(builtin_themes) / sizeof(builtin_themes[0]); + + // Try to open the theme config file to check for custom themes + FILE *config_file = fopen(theme_config_path, "r"); + char custom_theme[32]; + int has_custom_theme = 0; + + if (config_file) { + char theme_line[64]; + while (fgets(theme_line, sizeof(theme_line), config_file)) { + if (sscanf(theme_line, "theme=%31s", custom_theme) == 1) { + // Check if this is a custom theme (not one of our builtin themes) + int is_builtin = 0; + for (int i = 0; i < builtin_count; i++) { + if (strcmp(custom_theme, builtin_themes[i]) == 0) { + is_builtin = 1; + break; + } + } + + if (!is_builtin) { + has_custom_theme = 1; + break; + } + } + } + fclose(config_file); + } + + // Allocate space for themes (builtin + potential custom theme) + int total_themes = builtin_count + (has_custom_theme ? 1 : 0); + char **theme_names = (char **)malloc(total_themes * sizeof(char *)); + if (!theme_names) { + fprintf(stderr, "lsh: allocation error in get_theme_names\n"); + *theme_count = 0; + return NULL; + } + + // Copy builtin theme names + for (int i = 0; i < builtin_count; i++) { + theme_names[i] = strdup(builtin_themes[i]); + } + + // Add custom theme if found + if (has_custom_theme) { + theme_names[builtin_count] = strdup(custom_theme); + } + + *theme_count = total_themes; + return theme_names; +} diff --git a/themes.h b/themes.h new file mode 100644 index 0000000..c65bdf7 --- /dev/null +++ b/themes.h @@ -0,0 +1,92 @@ +#ifndef THEMES_H +#define THEMES_H + +#include "common.h" + +typedef struct { + // Standard console colors + WORD PRIMARY_COLOR; // Main text color + WORD SECONDARY_COLOR; // Secondary text color + WORD ACCENT_COLOR; // Color for highlights/important elements + WORD SUCCESS_COLOR; // Success messages + WORD ERROR_COLOR; // Error messages + WORD WARNING_COLOR; // Warning messages + + WORD INVALID_COMMAND_COLOR; // unrecognized command + + // UI element colors + WORD HEADER_COLOR; // Headers and titles + WORD STATUS_BAR_COLOR; // Status bar background + WORD STATUS_TEXT_COLOR; // Status bar text + WORD PROMPT_COLOR; // Command prompt + + // File system colors + WORD DIRECTORY_COLOR; // Directory names + WORD EXECUTABLE_COLOR; // Executable files + WORD TEXT_FILE_COLOR; // Text files + WORD IMAGE_FILE_COLOR; // Image files + WORD CODE_FILE_COLOR; // Code files (non-executable) + WORD ARCHIVE_FILE_COLOR; // Archive files + + // Syntax highlighting colors + WORD SYNTAX_KEYWORD; // Keywords in syntax highlighting + WORD SYNTAX_STRING; // Strings in syntax highlighting + WORD SYNTAX_COMMENT; // Comments in syntax highlighting + WORD SYNTAX_NUMBER; // Numbers in syntax highlighting + WORD SYNTAX_PREPROCESSOR; // Preprocessor in syntax highlighting + + // ANSI true color definitions (for modern terminals) + char ANSI_BASE[20]; // Base color + char ANSI_SURFACE[20]; // Surface color + char ANSI_OVERLAY[20]; // Overlay color + char ANSI_MUTED[20]; // Muted color + char ANSI_SUBTLE[20]; // Subtle color + char ANSI_TEXT[20]; // Text color + char ANSI_LOVE[20]; // Love/Red color + char ANSI_GOLD[20]; // Gold color + char ANSI_ROSE[20]; // Rose/pink color + char ANSI_PINE[20]; // Pine color + char ANSI_FOAM[20]; // Foam color + char ANSI_IRIS[20]; // Iris color + char ANSI_HIGHLIGHT[20]; // Highlight color + + char ANSI_INVALID_COMMAND[20]; + + // Flag to indicate if ANSI colors should be used + BOOL use_ansi_colors; + + // Name of the theme + char name[32]; +} ShellTheme; + +// Initialize theme system +void init_theme_system(void); + +// Shutdown theme system +void shutdown_themes(void); + +// Initialize themes for shell startup +void init_themes(void); + +// Load a theme by name +int load_theme(const char *theme_name); + +// Apply current theme settings +void apply_current_theme(void); + +// Get current theme +const ShellTheme *get_current_theme(void); + +// List available themes +void list_available_themes(void); + +// Get theme names for tab completion +char **get_theme_names(int *theme_count); + +// Command handler for "theme" command +int lsh_theme(char **args); + +// Current active theme +extern ShellTheme current_theme; + +#endif // THEMES_H \ No newline at end of file diff --git a/themes.o b/themes.o new file mode 100644 index 0000000..2d4a792 Binary files /dev/null and b/themes.o differ diff --git a/timer.c b/timer.c new file mode 100644 index 0000000..93d8f4a --- /dev/null +++ b/timer.c @@ -0,0 +1,97 @@ +/** + * timer.c + * Implements a command to measure execution time of other commands + */ + +#include "builtins.h" +#include "common.h" +#include "shell.h" +#include + +/** + * Format a time interval in milliseconds to a readable string + * + * @param ms Time in milliseconds + * @param buffer Buffer to store formatted time + * @param buffer_size Size of the buffer + */ +void format_time(double ms, char *buffer, size_t buffer_size) { + if (ms < 1.0) { + // Microseconds + snprintf(buffer, buffer_size, "%.2f μs", ms * 1000); + } else if (ms < 1000.0) { + // Milliseconds + snprintf(buffer, buffer_size, "%.2f ms", ms); + } else if (ms < 60000.0) { + // Seconds + snprintf(buffer, buffer_size, "%.2f s", ms / 1000); + } else { + // Minutes and seconds + int minutes = (int)(ms / 60000); + double seconds = (ms - minutes * 60000) / 1000; + snprintf(buffer, buffer_size, "%d min %.2f s", minutes, seconds); + } +} + +/** + * Implementation of the timer command + * Usage: timer COMMAND [ARGS...] + * + * @param args Command arguments + * @return 1 to continue, 0 to exit + */ +int lsh_timer(char **args) { + if (!args[1]) { + printf("timer: usage: timer COMMAND [ARGS...]\n"); + printf("Measures execution time of a command\n"); + return 1; + } + + // Create a new array for the command to execute (without "timer" prefix) + char **cmd_args = &args[1]; + + // Start timing + clock_t start_time = clock(); + + // Execute the command + int result; + if (strcmp(cmd_args[0], "cd") == 0 || strcmp(cmd_args[0], "exit") == 0 || + strcmp(cmd_args[0], "timer") == 0) { + // Handle built-in commands that might affect shell state + printf("timer: can't time built-in command: %s\n", cmd_args[0]); + result = 1; + } else { + // Execute the command normally + result = lsh_execute(cmd_args); + } + + // End timing + clock_t end_time = clock(); + + // Calculate execution time in milliseconds + double execution_time_ms = + ((double)(end_time - start_time) / CLOCKS_PER_SEC) * 1000; + + // Format the execution time + char time_str[64]; + format_time(execution_time_ms, time_str, sizeof(time_str)); + + // Print the execution time with color + printf("\n"); + printf(ANSI_COLOR_GREEN); // Green text for highlighted display + printf("╭───────────────────────────────╮\n"); + printf("│ Execution time: %-14s │\n", time_str); + printf("╰───────────────────────────────╯\n"); + printf(ANSI_COLOR_RESET); // Reset color + + return result; +} + +/** + * Implementation of the time command (alias for timer) + * Usage: time COMMAND [ARGS...] + * + * @param args Command arguments + * @return 1 to continue, 0 to exit + */ +int lsh_time(char **args) { return lsh_timer(args); } \ No newline at end of file diff --git a/timer.h b/timer.h new file mode 100644 index 0000000..412c851 --- /dev/null +++ b/timer.h @@ -0,0 +1,38 @@ +/** + * timer.h + * Header for command execution timer functionality + */ + +#ifndef TIMER_H +#define TIMER_H + +#include "common.h" + +/** + * Format a time interval in milliseconds to a readable string + * + * @param ms Time in milliseconds + * @param buffer Buffer to store formatted time + * @param buffer_size Size of the buffer + */ +void format_time(double ms, char *buffer, size_t buffer_size); + +/** + * Implementation of the timer command + * Usage: timer COMMAND [ARGS...] + * + * @param args Command arguments + * @return 1 to continue, 0 to exit + */ +int lsh_timer(char **args); + +/** + * Implementation of the time command (alias for timer) + * Usage: time COMMAND [ARGS...] + * + * @param args Command arguments + * @return 1 to continue, 0 to exit + */ +int lsh_time(char **args); + +#endif // TIMER_H diff --git a/timer.o b/timer.o new file mode 100644 index 0000000..8717e83 Binary files /dev/null and b/timer.o differ diff --git a/weather.c b/weather.c new file mode 100644 index 0000000..123f8d0 --- /dev/null +++ b/weather.c @@ -0,0 +1,75 @@ +/** + * weather.c + * Implementation of weather information fetching and display for Linux + * This is a simplified version that provides basic information about using curl/wget + * instead of implementing full HTTP functionality + */ + +#include "weather.h" +#include "favorite_cities.h" +#include +#include +#include +#include + +/** + * Command handler for the "weather" command + * In this Linux version, we'll provide instructions for using curl/wget + */ +int lsh_weather(char **args) { + printf("\n"); + printf("╭─────────────────────────────────────────────────────────────╮\n"); + printf("│ Weather Command (Linux Version) │\n"); + printf("├─────────────────────────────────────────────────────────────┤\n"); + printf("│ This is a simplified version of the weather command for │\n"); + printf("│ Linux. The full version would use libcurl for HTTP requests. │\n"); + printf("│ │\n"); + printf("│ To get weather information on Linux: │\n"); + printf("│ │\n"); + printf("│ 1. Using curl: │\n"); + printf("│ curl wttr.in/[city] │\n"); + printf("│ │\n"); + printf("│ 2. Using wget: │\n"); + printf("│ wget -O - wttr.in/[city] │\n"); + printf("│ │\n"); + printf("│ For example: │\n"); + printf("│ curl wttr.in/London │\n"); + printf("│ curl wttr.in/\"New York\" │\n"); + printf("│ │\n"); + printf("│ For a shorter format: │\n"); + printf("│ curl wttr.in/London?format=3 │\n"); + printf("│ │\n"); + printf("├─────────────────────────────────────────────────────────────┤\n"); + + // Check if a city was provided as an argument + if (args[1]) { + char command[256]; + // Use wttr.in for a simple demonstration + snprintf(command, sizeof(command), "curl -s wttr.in/%s?format=3", args[1]); + printf("│ Running: %-47s │\n", command); + printf("├─────────────────────────────────────────────────────────────┤\n"); + printf("│ Result: │\n"); + printf("│ │\n"); + + // Execute the command + FILE *fp = popen(command, "r"); + if (fp) { + char buffer[256]; + if (fgets(buffer, sizeof(buffer), fp)) { + // Print the result with padding + printf("│ %-53s │\n", buffer); + } else { + printf("│ No result returned │\n"); + } + pclose(fp); + } else { + printf("│ Error executing command │\n"); + } + + printf("│ │\n"); + } + + printf("╰─────────────────────────────────────────────────────────────╯\n\n"); + + return 1; +} \ No newline at end of file diff --git a/weather.h b/weather.h new file mode 100644 index 0000000..dccd1d2 --- /dev/null +++ b/weather.h @@ -0,0 +1,8 @@ +#ifndef WEATHER_H +#define WEATHER_H + +#include "common.h" + +int lsh_weather(char **args); + +#endif // WEATHER_H diff --git a/weather.o b/weather.o new file mode 100644 index 0000000..4b7d49d Binary files /dev/null and b/weather.o differ