mirror of
https://github.com/marcusDenslow/FERRUM.git
synced 2026-10-25 20:28:51 +01:00
stats is a command that will show you which command you have used the most, and predicts your next command based on freciency
503 lines
13 KiB
C
503 lines
13 KiB
C
/**
|
|
* persistent_history.c
|
|
* Implementation of persistent command history and frequency tracking
|
|
*/
|
|
|
|
#include "persistent_history.h"
|
|
#include <sys/stat.h>
|
|
#include <dirent.h>
|
|
#include <errno.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <time.h>
|
|
#include <unistd.h>
|
|
#include <ctype.h>
|
|
|
|
// Global variables for persistent history
|
|
PersistentHistoryEntry *history_entries = NULL;
|
|
int history_size = 0;
|
|
static int history_capacity = 0;
|
|
static int history_position = 0;
|
|
|
|
// Global variables for frequency tracking
|
|
CommandFrequency *command_frequencies = NULL;
|
|
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);
|
|
} |