diff --git a/.gitignore b/.gitignore index 066fa12..1cd68be 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ *.o build/ *.test -*.dSYM/ -nutshell \ No newline at end of file +*.dSYM/ \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..6736147 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,19 @@ +# Changelog + +All notable changes to Nutshell will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [0.0.4] - 2025-03-11 + +### Added +- Directory-level configuration with `.nutshell.json` files +- Configuration hierarchy: directory configs override user configs which override system configs +- Automatic config reloading when changing directories with `cd` +- Project-specific aliases and settings through directory configs +- Support for different themes per project + +### Fixed +- Memory leak in directory path traversal +- Config loading order to properly respect precedence rules \ No newline at end of file diff --git a/Makefile b/Makefile index 3649040..6d90a60 100644 --- a/Makefile +++ b/Makefile @@ -40,7 +40,7 @@ TEST_SRC = $(wildcard tests/*.c) TEST_OBJ = $(TEST_SRC:.c=.o) TEST_BINS = $(TEST_SRC:.c=.test) -.PHONY: all clean install install-user test test-pkg test-theme test-ai release uninstall uninstall-user +.PHONY: all clean install install-user test test-pkg test-theme test-ai test-config test-dirconfig release uninstall uninstall-user all: nutshell @@ -116,6 +116,16 @@ test-ai: tests/test_ai_integration.test tests/test_openai_commands.test tests/te @./tests/test_ai_shell_integration.test @echo "All AI tests completed!" +# Add a new target for config tests +test-config: tests/test_config.test + @echo "Running configuration system tests..." + @./tests/test_config.test + +# Add a target for directory config tests +test-dirconfig: tests/test_directory_config.test + @echo "Running directory config tests..." + @./tests/test_directory_config.test + # Update the test build rule to exclude main.o tests/%.test: tests/%.o $(filter-out src/core/main.o, $(OBJ)) $(CC) -o $@ $^ $(LDFLAGS) diff --git a/README.md b/README.md index 096b4df..01ad1f3 100644 --- a/README.md +++ b/README.md @@ -222,6 +222,40 @@ This will switch to the "minimal" theme. 3. Switch to your theme with `theme mytheme` +## Directory-level Configuration + +Nutshell now supports project-specific configurations through directory-level config files: + +### How it works + +- Nutshell searches for a `.nutshell.json` configuration file in the current directory +- If not found, it looks in parent directories until reaching your home directory +- Directory configs take precedence over user configs which take precedence over system configs +- Configurations are automatically reloaded when you change directories using `cd` + +### Creating a directory config + +Create a `.nutshell.json` file in your project directory: + +```json +{ + "theme": "minimal", + "aliases": { + "build": "make all", + "test": "make test", + "deploy": "scripts/deploy.sh" + }, + "packages": ["gitify"] +} +``` + +### Benefits + +- Project-specific aliases and settings +- Different themes for different projects +- Shared configurations for team projects (add `.nutshell.json` to version control) +- Hierarchical configuration (team settings in parent dir, personal tweaks in subdirs) + ## Debugging For development or troubleshooting, run the debug script: diff --git a/include/nutshell/config.h b/include/nutshell/config.h new file mode 100644 index 0000000..3a11988 --- /dev/null +++ b/include/nutshell/config.h @@ -0,0 +1,49 @@ +#ifndef NUTSHELL_CONFIG_H +#define NUTSHELL_CONFIG_H + +#include + +// Configuration structure to store settings +typedef struct { + char *theme; // Current theme name + char **enabled_packages; // Array of enabled package names + int package_count; // Number of enabled packages + char **aliases; // Array of custom aliases + char **alias_commands; // Array of commands for each alias + int alias_count; // Number of aliases + char **scripts; // Array of custom script paths + int script_count; // Number of custom scripts +} Config; + +// Global configuration +extern Config *global_config; + +// Configuration functions +void init_config_system(); +void cleanup_config_system(); + +// Load configuration from files (checks dir, user, system in that order) +bool load_config_files(); // Renamed from load_config to avoid conflict + +// Save current configuration to user config file +bool save_config(); + +// Update specific configuration settings +bool set_config_theme(const char *theme_name); +bool add_config_package(const char *package_name); +bool remove_config_package(const char *package_name); +bool add_config_alias(const char *alias_name, const char *command); +bool remove_config_alias(const char *alias_name); +bool add_config_script(const char *script_path); +bool remove_config_script(const char *script_path); + +// New functions for directory-level configuration +bool reload_directory_config(); +void cleanup_config_values(); + +// Get configuration settings +const char *get_config_theme(); +bool is_package_enabled(const char *package_name); +const char *get_alias_command(const char *alias_name); + +#endif // NUTSHELL_CONFIG_H diff --git a/scripts/debug_config.sh b/scripts/debug_config.sh new file mode 100644 index 0000000..e36b2a4 --- /dev/null +++ b/scripts/debug_config.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +# Set debug environment variables +export NUT_DEBUG=1 +export NUT_DEBUG_CONFIG=1 + +# Run the configuration test with debugging +make test-config + +# Or run the shell with debugging +# make && ./nutshell diff --git a/src/core/executor.c b/src/core/executor.c index 4604598..ad1cf33 100644 --- a/src/core/executor.c +++ b/src/core/executor.c @@ -3,6 +3,7 @@ #include #include +#include // Add this include for reload_directory_config #include #include #include @@ -71,7 +72,12 @@ void execute_command(ParsedCommand *cmd) { // Handle builtin commands without forking if (strcmp(cmd->args[0], "cd") == 0) { - if (cmd->args[1]) chdir(cmd->args[1]); + if (cmd->args[1]) { + if (chdir(cmd->args[1]) == 0) { + // Successfully changed directory, reload directory-specific config + reload_directory_config(); + } + } return; } diff --git a/src/core/shell.c b/src/core/shell.c index 51d89a4..4edc7e5 100644 --- a/src/core/shell.c +++ b/src/core/shell.c @@ -4,6 +4,7 @@ #include #include #include // Add this include to access AI functions +#include #include #include #include @@ -59,9 +60,38 @@ void shell_loop() { char *input; struct sigaction sa; + // Initialize the configuration system first + if (getenv("NUT_DEBUG")) { + DEBUG_LOG("Initializing configuration system"); + } + init_config_system(); + // Initialize the theme system + if (getenv("NUT_DEBUG")) { + DEBUG_LOG("Initializing theme system"); + } init_theme_system(); + // Load saved theme from config if available + const char *saved_theme = get_config_theme(); + if (saved_theme && current_theme && strcmp(current_theme->name, saved_theme) != 0) { + if (getenv("NUT_DEBUG")) { + DEBUG_LOG("Loading saved theme from config: %s", saved_theme); + } + Theme *theme = load_theme(saved_theme); + if (theme) { + if (current_theme) { + free_theme(current_theme); + } + current_theme = theme; + if (getenv("NUT_DEBUG")) { + DEBUG_LOG("Successfully loaded saved theme: %s", theme->name); + } + } else if (getenv("NUT_DEBUG")) { + DEBUG_LOG("Failed to load saved theme: %s", saved_theme); + } + } + // Initialize the AI shell integration init_ai_shell(); @@ -245,6 +275,9 @@ void shell_loop() { // Clean up theme system cleanup_theme_system(); + + // Clean up configuration system + cleanup_config_system(); } char *get_prompt() { diff --git a/src/utils/config.c b/src/utils/config.c new file mode 100644 index 0000000..63242cb --- /dev/null +++ b/src/utils/config.c @@ -0,0 +1,690 @@ +#define _POSIX_C_SOURCE 200809L +#define _GNU_SOURCE + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include // For PATH_MAX constant +#include // For dirname function +#include // Add this include for errno + +// Configuration debug macro +#define CONFIG_DEBUG(fmt, ...) \ + do { if (getenv("NUT_DEBUG_CONFIG")) fprintf(stderr, "CONFIG: " fmt "\n", ##__VA_ARGS__); } while(0) + +// Global configuration instance +Config *global_config = NULL; + +// Configuration file names +static const char *DIR_CONFIG_FILE = ".nutshell.json"; +static const char *USER_CONFIG_DIR = "/.nutshell"; +static const char *USER_CONFIG_FILE = "/.nutshell/config.json"; +static const char *SYSTEM_CONFIG_FILE = "/usr/local/nutshell/config.json"; + +// Initialize configuration system +void init_config_system() { + CONFIG_DEBUG("Initializing configuration system"); + + // Create empty configuration structure + global_config = calloc(1, sizeof(Config)); + if (!global_config) { + fprintf(stderr, "Error: Failed to initialize configuration system\n"); + return; + } + + // Ensure user config directory exists + char *home = getenv("HOME"); + if (home) { + char config_dir[512]; + snprintf(config_dir, sizeof(config_dir), "%s%s", home, USER_CONFIG_DIR); + + struct stat st = {0}; + if (stat(config_dir, &st) == -1) { + // Create directory if it doesn't exist + CONFIG_DEBUG("Creating user config directory %s", config_dir); + mkdir(config_dir, 0700); + } + } + + // Load configuration from files + load_config_files(); // Updated function name +} + +// Clean up configuration resources +void cleanup_config_system() { + if (!global_config) return; + + free(global_config->theme); + + for (int i = 0; i < global_config->package_count; i++) { + free(global_config->enabled_packages[i]); + } + free(global_config->enabled_packages); + + for (int i = 0; i < global_config->alias_count; i++) { + free(global_config->aliases[i]); + free(global_config->alias_commands[i]); + } + free(global_config->aliases); + free(global_config->alias_commands); + + for (int i = 0; i < global_config->script_count; i++) { + free(global_config->scripts[i]); + } + free(global_config->scripts); + + free(global_config); + global_config = NULL; +} + +// Load JSON file into configuration +static bool load_config_from_file(const char *path) { + CONFIG_DEBUG("Attempting to load config from: %s", path); + + // Check if file exists + struct stat st; + if (stat(path, &st) != 0) { + CONFIG_DEBUG("Config file not found: %s", path); + return false; + } + + // Open file + FILE *file = fopen(path, "r"); + if (!file) { + CONFIG_DEBUG("Failed to open config file: %s", path); + return false; + } + + // Read file contents + fseek(file, 0, SEEK_END); + long length = ftell(file); + fseek(file, 0, SEEK_SET); + + char *data = malloc(length + 1); + if (!data) { + CONFIG_DEBUG("Failed to allocate memory for config data"); + fclose(file); + return false; + } + + fread(data, 1, length, file); + data[length] = '\0'; + fclose(file); + + // Parse JSON + json_error_t error; + json_t *root = json_loads(data, 0, &error); + free(data); + + if (!root) { + CONFIG_DEBUG("JSON parse error: %s (line: %d, col: %d)", + error.text, error.line, error.column); + return false; + } + + // Extract theme setting + json_t *theme_json = json_object_get(root, "theme"); + if (json_is_string(theme_json)) { + free(global_config->theme); + global_config->theme = strdup(json_string_value(theme_json)); + CONFIG_DEBUG("Loaded theme: %s", global_config->theme); + } + + // Extract packages + json_t *packages_json = json_object_get(root, "packages"); + if (json_is_array(packages_json)) { + // Free existing packages + for (int i = 0; i < global_config->package_count; i++) { + free(global_config->enabled_packages[i]); + } + free(global_config->enabled_packages); + + // Allocate new packages array + size_t package_count = json_array_size(packages_json); + global_config->enabled_packages = calloc(package_count, sizeof(char*)); + global_config->package_count = package_count; + + // Load each package + for (size_t i = 0; i < package_count; i++) { + json_t *pkg = json_array_get(packages_json, i); + if (json_is_string(pkg)) { + global_config->enabled_packages[i] = strdup(json_string_value(pkg)); + CONFIG_DEBUG("Loaded package: %s", global_config->enabled_packages[i]); + } + } + } + + // Extract aliases + json_t *aliases_json = json_object_get(root, "aliases"); + if (json_is_object(aliases_json)) { + // Free existing aliases + for (int i = 0; i < global_config->alias_count; i++) { + free(global_config->aliases[i]); + free(global_config->alias_commands[i]); + } + free(global_config->aliases); + free(global_config->alias_commands); + + // Allocate new aliases array + size_t alias_count = json_object_size(aliases_json); + global_config->aliases = calloc(alias_count, sizeof(char*)); + global_config->alias_commands = calloc(alias_count, sizeof(char*)); + global_config->alias_count = alias_count; + + // Load each alias + const char *key; + json_t *value; + int i = 0; + + json_object_foreach(aliases_json, key, value) { + if (json_is_string(value)) { + global_config->aliases[i] = strdup(key); + global_config->alias_commands[i] = strdup(json_string_value(value)); + CONFIG_DEBUG("Loaded alias: %s -> %s", + global_config->aliases[i], global_config->alias_commands[i]); + i++; + } + } + } + + // Extract scripts + json_t *scripts_json = json_object_get(root, "scripts"); + if (json_is_array(scripts_json)) { + // Free existing scripts + for (int i = 0; i < global_config->script_count; i++) { + free(global_config->scripts[i]); + } + free(global_config->scripts); + + // Allocate new scripts array + size_t script_count = json_array_size(scripts_json); + global_config->scripts = calloc(script_count, sizeof(char*)); + global_config->script_count = script_count; + + // Load each script + for (size_t i = 0; i < script_count; i++) { + json_t *script = json_array_get(scripts_json, i); + if (json_is_string(script)) { + global_config->scripts[i] = strdup(json_string_value(script)); + CONFIG_DEBUG("Loaded script: %s", global_config->scripts[i]); + } + } + } + + json_decref(root); + CONFIG_DEBUG("Successfully loaded config from %s", path); + return true; +} + +// Check up the directory tree for config files +static bool find_directory_config(char *result_path, size_t max_size) { + char current_dir[PATH_MAX] = {0}; + char config_path[PATH_MAX] = {0}; + + // Get the absolute path of the current directory + if (!getcwd(current_dir, sizeof(current_dir))) { + CONFIG_DEBUG("Failed to get current directory"); + return false; + } + + CONFIG_DEBUG("Searching for directory config starting from: %s", current_dir); + CONFIG_DEBUG("Looking for file named: %s", DIR_CONFIG_FILE); + + // Start from the current directory and go up until root + char *dir_path = strdup(current_dir); + if (!dir_path) { + CONFIG_DEBUG("Failed to allocate memory for dir_path"); + return false; + } + + while (dir_path && strlen(dir_path) > 0) { + // Create the config file path + snprintf(config_path, sizeof(config_path), "%s/%s", dir_path, DIR_CONFIG_FILE); + CONFIG_DEBUG("Checking for config at: %s", config_path); + + // Check if config file exists + if (access(config_path, R_OK) == 0) { + CONFIG_DEBUG("Found directory config at: %s", config_path); + + // Check if we can actually open and read the file + FILE *check = fopen(config_path, "r"); + if (check) { + char buffer[256]; + size_t bytes_read = fread(buffer, 1, sizeof(buffer) - 1, check); + buffer[bytes_read] = '\0'; + fclose(check); + CONFIG_DEBUG("First %zu bytes of config: %.100s...", bytes_read, buffer); + } else { + CONFIG_DEBUG("WARNING: Found config but cannot open for reading: %s", strerror(errno)); + } + + strncpy(result_path, config_path, max_size); + free(dir_path); + return true; + } else { + CONFIG_DEBUG("Config file not found at: %s (%s)", config_path, strerror(errno)); + } + + // Go up one directory level - completely rewritten to avoid memory issues + char *parent_dir = strdup(dir_path); + if (!parent_dir) { + CONFIG_DEBUG("Failed to allocate memory for parent_dir"); + free(dir_path); + return false; + } + + // Use dirname() on the copy + char *dirname_result = dirname(parent_dir); + + // If we've reached the root directory + if (strcmp(dir_path, dirname_result) == 0 || + strcmp(dirname_result, "/") == 0) { + CONFIG_DEBUG("Reached root directory or can't go up further"); + free(parent_dir); + free(dir_path); + return false; + } + + // Replace current path with parent + free(dir_path); + dir_path = strdup(dirname_result); + free(parent_dir); + + if (!dir_path) { + CONFIG_DEBUG("Failed to allocate memory for new dir_path"); + return false; + } + } + + CONFIG_DEBUG("No directory config found in path"); + if (dir_path) { + free(dir_path); + } + return false; +} + +// Load configuration from files with improved directory hierarchy search +bool load_config_files() { + bool loaded_any = false; + + // Track which configuration sources were loaded + bool dir_loaded = false; + bool user_loaded = false; + bool system_loaded = false; + + // Load in reverse precedence order: system (lowest) -> user -> directory (highest) + + // Check for system config first (lowest precedence) + if (load_config_from_file(SYSTEM_CONFIG_FILE)) { + CONFIG_DEBUG("Loaded system config from: %s", SYSTEM_CONFIG_FILE); + system_loaded = true; + loaded_any = true; + } + + // Check for user config next (medium precedence) + char user_config[512]; + char *home = getenv("HOME"); + if (home) { + snprintf(user_config, sizeof(user_config), "%s%s", home, USER_CONFIG_FILE); + if (load_config_from_file(user_config)) { + CONFIG_DEBUG("Loaded user config from: %s", user_config); + user_loaded = true; + loaded_any = true; + } + } + + // Finally, try to find directory-specific config (highest precedence) + char dir_config_path[PATH_MAX] = {0}; + if (find_directory_config(dir_config_path, sizeof(dir_config_path))) { + if (load_config_from_file(dir_config_path)) { + CONFIG_DEBUG("Loaded directory-specific config from: %s", dir_config_path); + dir_loaded = true; + loaded_any = true; + + // Store the directory where we found the config + if (global_config) { + char *dir_name = strdup(dirname(dir_config_path)); + CONFIG_DEBUG("Setting active config directory to: %s", dir_name); + // Store this path for later reference + free(dir_name); // Free the temporary string + } + } + } + + CONFIG_DEBUG("Config loading summary: directory=%s, user=%s, system=%s", + dir_loaded ? "yes" : "no", + user_loaded ? "yes" : "no", + system_loaded ? "yes" : "no"); + + return loaded_any; +} + +// Force reload of configuration based on current directory +bool reload_directory_config() { + CONFIG_DEBUG("Reloading configuration for current directory"); + + // Temporarily store current theme to preserve it if not overridden + char *current_theme = global_config && global_config->theme ? + strdup(global_config->theme) : NULL; + + // Clear existing configuration but don't free the struct + cleanup_config_values(); + + // Reload configuration from files + bool result = load_config_files(); + + // If we had a theme before and no new theme was loaded, restore it + if (current_theme && global_config && !global_config->theme) { + global_config->theme = current_theme; + } else { + free(current_theme); + } + + return result; +} + +// Clean up only the values inside the configuration, not the struct itself +void cleanup_config_values() { + if (!global_config) return; + + free(global_config->theme); + global_config->theme = NULL; + + for (int i = 0; i < global_config->package_count; i++) { + free(global_config->enabled_packages[i]); + } + free(global_config->enabled_packages); + global_config->enabled_packages = NULL; + global_config->package_count = 0; + + for (int i = 0; i < global_config->alias_count; i++) { + free(global_config->aliases[i]); + free(global_config->alias_commands[i]); + } + free(global_config->aliases); + free(global_config->alias_commands); + global_config->aliases = NULL; + global_config->alias_commands = NULL; + global_config->alias_count = 0; + + for (int i = 0; i < global_config->script_count; i++) { + free(global_config->scripts[i]); + } + free(global_config->scripts); + global_config->scripts = NULL; + global_config->script_count = 0; +} + +// Save current configuration to user config file +bool save_config() { + if (!global_config) return false; + + CONFIG_DEBUG("Saving configuration"); + + char user_config[512]; + char *home = getenv("HOME"); + if (!home) { + CONFIG_DEBUG("HOME environment variable not set, can't save config"); + return false; + } + + snprintf(user_config, sizeof(user_config), "%s%s", home, USER_CONFIG_FILE); + + // Create JSON structure + json_t *root = json_object(); + + // Save theme + if (global_config->theme) { + json_object_set_new(root, "theme", json_string(global_config->theme)); + } + + // Save packages + if (global_config->package_count > 0) { + json_t *packages = json_array(); + for (int i = 0; i < global_config->package_count; i++) { + json_array_append_new(packages, json_string(global_config->enabled_packages[i])); + } + json_object_set_new(root, "packages", packages); + } + + // Save aliases + if (global_config->alias_count > 0) { + json_t *aliases = json_object(); + for (int i = 0; i < global_config->alias_count; i++) { + json_object_set_new(aliases, global_config->aliases[i], + json_string(global_config->alias_commands[i])); + } + json_object_set_new(root, "aliases", aliases); + } + + // Save scripts + if (global_config->script_count > 0) { + json_t *scripts = json_array(); + for (int i = 0; i < global_config->script_count; i++) { + json_array_append_new(scripts, json_string(global_config->scripts[i])); + } + json_object_set_new(root, "scripts", scripts); + } + + // Write JSON to file + char *json_str = json_dumps(root, JSON_INDENT(2)); + json_decref(root); + + if (!json_str) { + CONFIG_DEBUG("Failed to generate JSON"); + return false; + } + + FILE *file = fopen(user_config, "w"); + if (!file) { + CONFIG_DEBUG("Failed to open config file for writing: %s", user_config); + free(json_str); + return false; + } + + fputs(json_str, file); + fclose(file); + free(json_str); + + CONFIG_DEBUG("Configuration saved to %s", user_config); + return true; +} + +// Update theme in configuration +bool set_config_theme(const char *theme_name) { + if (!global_config || !theme_name) return false; + + CONFIG_DEBUG("Setting config theme to %s", theme_name); + + free(global_config->theme); + global_config->theme = strdup(theme_name); + + return save_config(); +} + +// Add package to configuration +bool add_config_package(const char *package_name) { + if (!global_config || !package_name) return false; + + CONFIG_DEBUG("Adding package: %s", package_name); + + // Check if package is already in config + for (int i = 0; i < global_config->package_count; i++) { + if (strcmp(global_config->enabled_packages[i], package_name) == 0) { + CONFIG_DEBUG("Package %s already exists in config", package_name); + return true; // Already exists + } + } + + CONFIG_DEBUG("Package count before adding: %d", global_config->package_count); + + // Add new package + char **new_packages = realloc(global_config->enabled_packages, + (global_config->package_count + 1) * sizeof(char*)); + + if (!new_packages) { + CONFIG_DEBUG("Failed to reallocate package array"); + return false; + } + + global_config->enabled_packages = new_packages; + global_config->enabled_packages[global_config->package_count] = strdup(package_name); + global_config->package_count++; + + CONFIG_DEBUG("Package count after adding: %d", global_config->package_count); + CONFIG_DEBUG("Added package: %s", global_config->enabled_packages[global_config->package_count-1]); + + return save_config(); +} + +// Remove package from configuration +bool remove_config_package(const char *package_name) { + if (!global_config || !package_name) return false; + + for (int i = 0; i < global_config->package_count; i++) { + if (strcmp(global_config->enabled_packages[i], package_name) == 0) { + // Found the package, remove it + free(global_config->enabled_packages[i]); + + // Shift remaining packages + for (int j = i; j < global_config->package_count - 1; j++) { + global_config->enabled_packages[j] = global_config->enabled_packages[j+1]; + } + + global_config->package_count--; + return save_config(); + } + } + + return false; // Package not found +} + +// Add alias to configuration +bool add_config_alias(const char *alias_name, const char *command) { + if (!global_config || !alias_name || !command) return false; + + // Check if alias already exists + for (int i = 0; i < global_config->alias_count; i++) { + if (strcmp(global_config->aliases[i], alias_name) == 0) { + // Update existing alias + free(global_config->alias_commands[i]); + global_config->alias_commands[i] = strdup(command); + return save_config(); + } + } + + // Add new alias + global_config->aliases = realloc(global_config->aliases, + (global_config->alias_count + 1) * sizeof(char*)); + global_config->alias_commands = realloc(global_config->alias_commands, + (global_config->alias_count + 1) * sizeof(char*)); + + global_config->aliases[global_config->alias_count] = strdup(alias_name); + global_config->alias_commands[global_config->alias_count] = strdup(command); + global_config->alias_count++; + + return save_config(); +} + +// Remove alias from configuration +bool remove_config_alias(const char *alias_name) { + if (!global_config || !alias_name) return false; + + for (int i = 0; i < global_config->alias_count; i++) { + if (strcmp(global_config->aliases[i], alias_name) == 0) { + // Found the alias, remove it + free(global_config->aliases[i]); + free(global_config->alias_commands[i]); + + // Shift remaining aliases + for (int j = i; j < global_config->alias_count - 1; j++) { + global_config->aliases[j] = global_config->aliases[j+1]; + global_config->alias_commands[j] = global_config->alias_commands[j+1]; + } + + global_config->alias_count--; + return save_config(); + } + } + + return false; // Alias not found +} + +// Add script to configuration +bool add_config_script(const char *script_path) { + if (!global_config || !script_path) return false; + + // Check if script already exists + for (int i = 0; i < global_config->script_count; i++) { + if (strcmp(global_config->scripts[i], script_path) == 0) { + return true; // Already exists + } + } + + // Add new script + global_config->scripts = realloc(global_config->scripts, + (global_config->script_count + 1) * sizeof(char*)); + global_config->scripts[global_config->script_count] = strdup(script_path); + global_config->script_count++; + + return save_config(); +} + +// Remove script from configuration +bool remove_config_script(const char *script_path) { + if (!global_config || !script_path) return false; + + for (int i = 0; i < global_config->script_count; i++) { + if (strcmp(global_config->scripts[i], script_path) == 0) { + // Found the script, remove it + free(global_config->scripts[i]); + + // Shift remaining scripts + for (int j = i; j < global_config->script_count - 1; j++) { + global_config->scripts[j] = global_config->scripts[j+1]; + } + + global_config->script_count--; + return save_config(); + } + } + + return false; // Script not found +} + +// Get theme from configuration +const char *get_config_theme() { + return global_config ? global_config->theme : NULL; +} + +// Check if package is enabled +bool is_package_enabled(const char *package_name) { + if (!global_config || !package_name) return false; + + for (int i = 0; i < global_config->package_count; i++) { + if (strcmp(global_config->enabled_packages[i], package_name) == 0) { + return true; + } + } + + return false; +} + +// Get alias command +const char *get_alias_command(const char *alias_name) { + if (!global_config || !alias_name) return NULL; + + for (int i = 0; i < global_config->alias_count; i++) { + if (strcmp(global_config->aliases[i], alias_name) == 0) { + return global_config->alias_commands[i]; + } + } + + return NULL; +} diff --git a/src/utils/theme.c b/src/utils/theme.c index 194fe02..f64ab91 100644 --- a/src/utils/theme.c +++ b/src/utils/theme.c @@ -9,6 +9,7 @@ #include #include #include // Add this include for struct stat, stat() function, and S_ISREG macro +#include // Add this helper macro at the top of the file #define THEME_DEBUG(fmt, ...) \ @@ -925,6 +926,13 @@ int theme_command(int argc, char **argv) { return 1; } + // Save theme selection to configuration + if (set_config_theme(theme_name)) { + THEME_DEBUG("Theme selection saved to config"); + } else { + THEME_DEBUG("Failed to save theme selection to config"); + } + printf("Theme changed to: %s\n", theme_name); return 0; } diff --git a/tests/test_config.c b/tests/test_config.c new file mode 100644 index 0000000..a246001 --- /dev/null +++ b/tests/test_config.c @@ -0,0 +1,327 @@ +#define _POSIX_C_SOURCE 200809L +#define _GNU_SOURCE + +#include +#include +#include +#include +#include +#include +#include +#include + +// Helper function to create a temporary config file for testing +static void create_test_config_file(const char *content) { + char *home = getenv("HOME"); + if (!home) { + printf("ERROR: HOME environment variable not set\n"); + return; + } + + char test_config_dir[512]; + snprintf(test_config_dir, sizeof(test_config_dir), "%s/.nutshell", home); + + struct stat st = {0}; + if (stat(test_config_dir, &st) == -1) { + mkdir(test_config_dir, 0700); + } + + char test_config_path[512]; + snprintf(test_config_path, sizeof(test_config_path), "%s/.nutshell/config.json", home); + + FILE *fp = fopen(test_config_path, "w"); + if (fp) { + fputs(content, fp); + fclose(fp); + printf("Created test config file: %s\n", test_config_path); + } else { + printf("ERROR: Failed to create test config file\n"); + } +} + +// Helper function to reset the config file to an empty state +static void create_empty_config_file() { + char *home = getenv("HOME"); + if (!home) { + printf("ERROR: HOME environment variable not set\n"); + return; + } + + char test_config_path[512]; + snprintf(test_config_path, sizeof(test_config_path), "%s/.nutshell/config.json", home); + + FILE *fp = fopen(test_config_path, "w"); + if (fp) { + // Write an empty JSON object + fputs("{\n}\n", fp); + fclose(fp); + printf("Created empty config file: %s\n", test_config_path); + } else { + printf("ERROR: Failed to create empty config file\n"); + } +} + +// Test basic initialization and cleanup +static void test_init_cleanup() { + printf("Testing config initialization and cleanup...\n"); + + // Initialize the config system + init_config_system(); + + // Verify that global_config is not NULL + assert(global_config != NULL); + + // Cleanup + cleanup_config_system(); + + // Verify that global_config is now NULL + assert(global_config == NULL); + + printf("Config initialization and cleanup test passed!\n"); +} + +// Test loading configuration from a file +static void test_load_config() { + printf("Testing config loading...\n"); + + // Create a test config file + const char *test_config = + "{\n" + " \"theme\": \"test_theme\",\n" + " \"packages\": [\"test_pkg1\", \"test_pkg2\"],\n" + " \"aliases\": {\n" + " \"ll\": \"ls -la\",\n" + " \"gs\": \"git status\"\n" + " },\n" + " \"scripts\": [\"script1.sh\", \"script2.sh\"]\n" + "}\n"; + + create_test_config_file(test_config); + + // Initialize and load the config + init_config_system(); + + // Verify loaded values + assert(global_config != NULL); + assert(global_config->theme != NULL); + assert(strcmp(global_config->theme, "test_theme") == 0); + + assert(global_config->package_count == 2); + assert(strcmp(global_config->enabled_packages[0], "test_pkg1") == 0); + assert(strcmp(global_config->enabled_packages[1], "test_pkg2") == 0); + + assert(global_config->alias_count == 2); + bool found_ll = false; + bool found_gs = false; + + for (int i = 0; i < global_config->alias_count; i++) { + if (strcmp(global_config->aliases[i], "ll") == 0) { + found_ll = true; + assert(strcmp(global_config->alias_commands[i], "ls -la") == 0); + } + if (strcmp(global_config->aliases[i], "gs") == 0) { + found_gs = true; + assert(strcmp(global_config->alias_commands[i], "git status") == 0); + } + } + + assert(found_ll); + assert(found_gs); + + assert(global_config->script_count == 2); + assert(strcmp(global_config->scripts[0], "script1.sh") == 0); + assert(strcmp(global_config->scripts[1], "script2.sh") == 0); + + cleanup_config_system(); + printf("Config loading test passed!\n"); +} + +// Test saving configuration to a file +static void test_save_config() { + printf("Testing config saving...\n"); + + // Initialize with empty config + init_config_system(); + printf("DEBUG: Initialized empty config\n"); + + // Set values + set_config_theme("saved_theme"); + printf("DEBUG: Set theme to 'saved_theme'\n"); + + printf("DEBUG: Before adding packages, count = %d\n", global_config->package_count); + add_config_package("saved_pkg1"); + printf("DEBUG: After adding 'saved_pkg1', count = %d\n", global_config->package_count); + add_config_package("saved_pkg2"); + printf("DEBUG: After adding 'saved_pkg2', count = %d\n", global_config->package_count); + + add_config_alias("st", "git status"); + add_config_alias("cl", "clear"); + add_config_script("/path/to/script.sh"); + + // Print current values for debugging + printf("DEBUG: Current config state:\n"); + printf("DEBUG: theme = '%s'\n", global_config->theme ? global_config->theme : "NULL"); + printf("DEBUG: package_count = %d\n", global_config->package_count); + + if (global_config->package_count > 0 && global_config->enabled_packages) { + for (int i = 0; i < global_config->package_count; i++) { + printf("DEBUG: package[%d] = '%s'\n", i, + global_config->enabled_packages[i] ? global_config->enabled_packages[i] : "NULL"); + } + } + + printf("DEBUG: alias_count = %d\n", global_config->alias_count); + printf("DEBUG: script_count = %d\n", global_config->script_count); + + // Don't verify exact counts yet, just verify theme was set correctly + assert(global_config->theme != NULL); + assert(strcmp(global_config->theme, "saved_theme") == 0); + + // Save should return true + printf("DEBUG: Saving config\n"); + bool save_result = save_config(); + printf("DEBUG: save_config() returned %s\n", save_result ? "true" : "false"); + assert(save_result == true); + + // Cleanup + printf("DEBUG: Cleaning up config\n"); + cleanup_config_system(); + + // Re-load and verify + printf("DEBUG: Re-initializing config to verify saved values\n"); + init_config_system(); + assert(global_config != NULL); + assert(global_config->theme != NULL); + assert(strcmp(global_config->theme, "saved_theme") == 0); + + // Verify packages - the test needs to be updated to check if they exist rather than exact count + printf("DEBUG: After reload: package_count = %d\n", global_config->package_count); + + bool found_pkg1 = false; + bool found_pkg2 = false; + + // Print and check each package + for (int i = 0; i < global_config->package_count; i++) { + printf("DEBUG: package[%d] = '%s'\n", i, + global_config->enabled_packages[i] ? global_config->enabled_packages[i] : "NULL"); + + if (global_config->enabled_packages[i]) { + if (strcmp(global_config->enabled_packages[i], "saved_pkg1") == 0) found_pkg1 = true; + if (strcmp(global_config->enabled_packages[i], "saved_pkg2") == 0) found_pkg2 = true; + } + } + + // Check that both packages were found, but don't rely on specific count + printf("DEBUG: found_pkg1 = %s, found_pkg2 = %s\n", + found_pkg1 ? "true" : "false", found_pkg2 ? "true" : "false"); + assert(found_pkg1); + assert(found_pkg2); + + // Verify aliases + bool found_st = false; + bool found_cl = false; + for (int i = 0; i < global_config->alias_count; i++) { + if (strcmp(global_config->aliases[i], "st") == 0) { + found_st = true; + assert(strcmp(global_config->alias_commands[i], "git status") == 0); + } + if (strcmp(global_config->aliases[i], "cl") == 0) { + found_cl = true; + assert(strcmp(global_config->alias_commands[i], "clear") == 0); + } + } + assert(found_st); + assert(found_cl); + + // Verify script - use variable to store expected script count + printf("DEBUG: After reload: script_count = %d\n", global_config->script_count); + + // Check for our specific script rather than count + bool found_script = false; + for (int i = 0; i < global_config->script_count; i++) { + printf("DEBUG: script[%d] = '%s'\n", i, global_config->scripts[i]); + if (strcmp(global_config->scripts[i], "/path/to/script.sh") == 0) { + found_script = true; + break; + } + } + + // Assert that our script was found + assert(found_script); + + cleanup_config_system(); + printf("Config saving test passed!\n"); +} + +// Test update functions +static void test_update_functions() { + printf("Testing config update functions...\n"); + + // Reset to an empty config file before starting this test + printf("DEBUG: Resetting to empty config file\n"); + create_empty_config_file(); + + // Initialize with empty config + init_config_system(); + + // Print initial state for debugging + printf("DEBUG: After initialization: script_count = %d\n", global_config->script_count); + + // Test theme setting and getting + set_config_theme("new_theme"); + assert(strcmp(get_config_theme(), "new_theme") == 0); + + // Test package functions + assert(is_package_enabled("test_pkg") == false); + add_config_package("test_pkg"); + assert(is_package_enabled("test_pkg") == true); + remove_config_package("test_pkg"); + assert(is_package_enabled("test_pkg") == false); + + // Test alias functions + assert(get_alias_command("ta") == NULL); + add_config_alias("ta", "touch all"); + assert(strcmp(get_alias_command("ta"), "touch all") == 0); + add_config_alias("ta", "touch any"); // Update existing + assert(strcmp(get_alias_command("ta"), "touch any") == 0); + remove_config_alias("ta"); + assert(get_alias_command("ta") == NULL); + + // Test script functions + assert(global_config->script_count == 0); + add_config_script("/test/script.sh"); + assert(global_config->script_count == 1); + add_config_script("/test/script.sh"); // Add again, should be ignored + assert(global_config->script_count == 1); + add_config_script("/test/script2.sh"); + assert(global_config->script_count == 2); + remove_config_script("/test/script.sh"); + assert(global_config->script_count == 1); + assert(strcmp(global_config->scripts[0], "/test/script2.sh") == 0); + + cleanup_config_system(); + printf("Config update functions test passed!\n"); +} + +int main() { + printf("Running configuration system tests...\n"); + + // Set debugging if needed + const char *debug_env = getenv("NUT_DEBUG"); + if (debug_env && strcmp(debug_env, "1") == 0) { + setenv("NUT_DEBUG_CONFIG", "1", 1); + printf("Config debugging enabled\n"); + } + + // Run tests + test_init_cleanup(); + test_load_config(); + test_save_config(); + + // Reset config file to empty state before running update functions test + create_empty_config_file(); + test_update_functions(); + + printf("All configuration system tests passed!\n"); + return 0; +} diff --git a/tests/test_directory_config.c b/tests/test_directory_config.c new file mode 100644 index 0000000..6081749 --- /dev/null +++ b/tests/test_directory_config.c @@ -0,0 +1,266 @@ +#define _POSIX_C_SOURCE 200809L +#define _GNU_SOURCE + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// Helper function to create a test directory structure with configs +static void create_test_directory_structure() { + // Create a temporary test directory structure + char *home = getenv("HOME"); + if (!home) { + printf("ERROR: HOME environment variable not set\n"); + return; + } + + char test_root[512]; + snprintf(test_root, sizeof(test_root), "%s/.nutshell/test_dirs", home); + + // Create directories + mkdir(test_root, 0755); + char parent_dir[512], child_dir[512], grandchild_dir[512]; + + snprintf(parent_dir, sizeof(parent_dir), "%s/parent", test_root); + mkdir(parent_dir, 0755); + + snprintf(child_dir, sizeof(child_dir), "%s/parent/child", test_root); + mkdir(child_dir, 0755); + + snprintf(grandchild_dir, sizeof(grandchild_dir), "%s/parent/child/grandchild", test_root); + mkdir(grandchild_dir, 0755); + + // Create config files - the filename must match exactly what find_directory_config looks for + char parent_config[512], child_config[512]; + snprintf(parent_config, sizeof(parent_config), "%s/.nutshell.json", parent_dir); + snprintf(child_config, sizeof(child_config), "%s/.nutshell.json", child_dir); + + printf("DEBUG: Creating parent config at: %s\n", parent_config); + + // Parent directory config + FILE *fp = fopen(parent_config, "w"); + if (fp) { + fprintf(fp, "{\n" + " \"theme\": \"parent_theme\",\n" + " \"packages\": [\"parent_pkg\"],\n" + " \"aliases\": {\n" + " \"parent_alias\": \"echo parent\"\n" + " }\n" + "}\n"); + fclose(fp); + printf("Created parent config: %s\n", parent_config); + } else { + printf("ERROR: Failed to create parent config at %s\n", parent_config); + perror("Reason"); + } + + printf("DEBUG: Creating child config at: %s\n", child_config); + + // Child directory config + fp = fopen(child_config, "w"); + if (fp) { + fprintf(fp, "{\n" + " \"theme\": \"child_theme\",\n" + " \"packages\": [\"child_pkg\"],\n" + " \"aliases\": {\n" + " \"child_alias\": \"echo child\"\n" + " }\n" + "}\n"); + fclose(fp); + printf("Created child config: %s\n", child_config); + } else { + printf("ERROR: Failed to create child config at %s\n", child_config); + perror("Reason"); + } + + // Verify files were created properly + struct stat st; + if (stat(parent_config, &st) == 0) { + printf("DEBUG: Parent config file exists and is %lld bytes\n", (long long)st.st_size); + } else { + printf("DEBUG: Parent config file does not exist!\n"); + } + + if (stat(child_config, &st) == 0) { + printf("DEBUG: Child config file exists and is %lld bytes\n", (long long)st.st_size); + } else { + printf("DEBUG: Child config file does not exist!\n"); + } +} + +// Helper function to back up and restore user's config +static void backup_user_config(bool restore) { + char *home = getenv("HOME"); + if (!home) return; + + char config_file[512], backup_file[512]; + snprintf(config_file, sizeof(config_file), "%s/.nutshell/config.json", home); + snprintf(backup_file, sizeof(backup_file), "%s/.nutshell/config.json.bak", home); + + if (restore) { + // Restore the backup + printf("DEBUG: Restoring user config from backup\n"); + struct stat st; + if (stat(backup_file, &st) == 0) { + // If backup exists, restore it + rename(backup_file, config_file); + } + } else { + // Create backup and remove config + printf("DEBUG: Backing up user config\n"); + struct stat st; + if (stat(config_file, &st) == 0) { + // If config exists, make backup + rename(config_file, backup_file); + } + } +} + +// Test loading directory-specific config +static void test_directory_config_loading() { + printf("Testing directory config loading...\n"); + + // Back up any existing user config + backup_user_config(false); + + // Create the test directory structure + create_test_directory_structure(); + + char *home = getenv("HOME"); + char test_root[512], parent_dir[512], child_dir[512], grandchild_dir[512]; + snprintf(test_root, sizeof(test_root), "%s/.nutshell/test_dirs", home); + snprintf(parent_dir, sizeof(parent_dir), "%s/parent", test_root); + snprintf(child_dir, sizeof(child_dir), "%s/parent/child", test_root); + snprintf(grandchild_dir, sizeof(grandchild_dir), "%s/parent/child/grandchild", test_root); + + // Save current directory + char cwd[512]; + getcwd(cwd, sizeof(cwd)); + + // Test parent directory config + printf("Testing in parent directory: %s\n", parent_dir); + + // Make sure the config file exists + char parent_config[512]; + snprintf(parent_config, sizeof(parent_config), "%s/.nutshell.json", parent_dir); + printf("DEBUG: Checking parent config file: %s\n", parent_config); + + FILE *check = fopen(parent_config, "r"); + if (check) { + char buffer[512] = {0}; + size_t bytes_read = fread(buffer, 1, sizeof(buffer) - 1, check); + fclose(check); + printf("DEBUG: Parent config content (%zu bytes):\n%s\n", bytes_read, buffer); + } else { + printf("DEBUG: Cannot open parent config for reading!\n"); + perror("Reason"); + } + + // Execute the actual test + if (chdir(parent_dir) != 0) { + printf("DEBUG: Failed to chdir to %s\n", parent_dir); + perror("chdir"); + return; + } + + printf("DEBUG: Current directory after chdir: "); + system("pwd"); + system("ls -la"); + + // Initialize with debug enabled + setenv("NUT_DEBUG_CONFIG", "1", 1); + init_config_system(); + + printf("DEBUG: After init_config_system()\n"); + + assert(global_config != NULL); + printf("DEBUG: global_config is not NULL\n"); + + // Check if theme was loaded + printf("DEBUG: global_config->theme = '%s'\n", + global_config->theme ? global_config->theme : "NULL"); + + // Make sure the theme is not NULL + assert(global_config->theme != NULL); + + // Check if it matches what we expect + assert(strcmp(global_config->theme, "parent_theme") == 0); + + // Verify the alias from parent config + const char *parent_alias = get_alias_command("parent_alias"); + assert(parent_alias != NULL); + assert(strcmp(parent_alias, "echo parent") == 0); + cleanup_config_system(); + + // Test child directory config + printf("Testing in child directory: %s\n", child_dir); + chdir(child_dir); + init_config_system(); + assert(global_config != NULL); + assert(global_config->theme != NULL); + assert(strcmp(global_config->theme, "child_theme") == 0); + + // Verify the alias from child config + const char *child_alias = get_alias_command("child_alias"); + assert(child_alias != NULL); + assert(strcmp(child_alias, "echo child") == 0); + cleanup_config_system(); + + // Test grandchild directory - should inherit from child + printf("Testing in grandchild directory (should inherit from child): %s\n", grandchild_dir); + chdir(grandchild_dir); + init_config_system(); + assert(global_config != NULL); + assert(global_config->theme != NULL); + assert(strcmp(global_config->theme, "child_theme") == 0); + cleanup_config_system(); + + // Test config reload on directory change + // Start in child directory + printf("Testing config reload on directory change\n"); + chdir(child_dir); + init_config_system(); + assert(global_config != NULL); + assert(strcmp(global_config->theme, "child_theme") == 0); + + // Change to parent directory and reload + chdir(parent_dir); + reload_directory_config(); + assert(strcmp(global_config->theme, "parent_theme") == 0); + + // Change to grandchild directory and reload + chdir(grandchild_dir); + reload_directory_config(); + assert(strcmp(global_config->theme, "child_theme") == 0); + + cleanup_config_system(); + + // Restore original directory and user config + chdir(cwd); + backup_user_config(true); + + printf("Directory config loading test passed!\n"); +} + +int main() { + printf("Running directory config tests...\n"); + + // Set debugging if needed + const char *debug_env = getenv("NUT_DEBUG"); + if (debug_env && strcmp(debug_env, "1") == 0) { + setenv("NUT_DEBUG_CONFIG", "1", 1); + printf("Config debugging enabled\n"); + } + + // Run tests + test_directory_config_loading(); + + printf("All directory config tests passed!\n"); + return 0; +} diff --git a/tests/test_theme.c b/tests/test_theme.c index 3bcab32..d8389e3 100644 --- a/tests/test_theme.c +++ b/tests/test_theme.c @@ -1,6 +1,7 @@ #include #include #include +#include // Add this include for configuration functions #include #include #include @@ -226,36 +227,50 @@ int test_get_prompt() { return 0; // Add return value } -// Test the theme command - change return type to int +// Test the theme command - update to handle config integration int test_theme_command() { printf("Testing theme command...\n"); + // Also initialize the configuration system + init_config_system(); + // Setup extern Theme *current_theme; current_theme = NULL; // Test listing themes char *args1[] = {"theme"}; + printf("DEBUG: Testing 'theme' command (list themes)\n"); int result = theme_command(1, args1); assert(result == 0); // Test setting theme char *args2[] = {"theme", "default"}; + printf("DEBUG: Testing 'theme default' command\n"); result = theme_command(2, args2); assert(result == 0); assert(current_theme != NULL); + printf("DEBUG: Current theme set to: %s\n", current_theme->name); assert(strcmp(current_theme->name, "default") == 0); + // Check if theme was saved to config + const char *saved_theme = get_config_theme(); + printf("DEBUG: Config saved theme: %s\n", saved_theme ? saved_theme : "NULL"); + assert(saved_theme != NULL); + assert(strcmp(saved_theme, "default") == 0); + // Test invalid theme char *args3[] = {"theme", "nonexistent_theme"}; + printf("DEBUG: Testing 'theme nonexistent_theme' command\n"); result = theme_command(2, args3); assert(result != 0); // Should fail // Cleanup cleanup_theme_system(); + cleanup_config_system(); printf("Theme command test passed!\n"); - return 0; // Add return value + return 0; } // Test segment command execution and output storage @@ -330,7 +345,8 @@ int main() { // Enable theme debug if NUT_DEBUG is set if (getenv("NUT_DEBUG")) { setenv("NUT_DEBUG_THEME", "1", 1); - printf("Theme debugging enabled\n"); + setenv("NUT_DEBUG_CONFIG", "1", 1); + printf("Theme and config debugging enabled\n"); } // Run the tests with proper return value checking