diff options
Diffstat (limited to 'src/history.c')
-rw-r--r-- | src/history.c | 224 |
1 files changed, 224 insertions, 0 deletions
diff --git a/src/history.c b/src/history.c new file mode 100644 index 0000000..8cb9eaf --- /dev/null +++ b/src/history.c @@ -0,0 +1,224 @@ +#include <errno.h> +#include <fcntl.h> +#include <libgen.h> +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/stat.h> +#include "history.h" +#include "log.h" +#include "xmalloc.h" + +static const char *default_state_dir = ".local/state"; +static const char *histfile_basename = "tofi-history"; + +[[nodiscard]] +static struct history history_create(void); +static bool mkdirp(const char *path); + +static char *get_histfile_path() { + char *histfile_name = NULL; + const char *state_path = getenv("XDG_STATE_HOME"); + if (state_path == NULL) { + const char *home = getenv("HOME"); + if (home == NULL) { + log_error("Couldn't retrieve HOME from environment.\n"); + return NULL; + } + size_t len = strlen(home) + 1 + + strlen(default_state_dir) + 1 + + strlen(histfile_basename) + 1; + histfile_name = xmalloc(len); + snprintf( + histfile_name, + len, + "%s/%s/%s", + home, + default_state_dir, + histfile_basename); + } else { + size_t len = strlen(state_path) + 1 + + strlen(histfile_basename) + 1; + histfile_name = xmalloc(len); + snprintf( + histfile_name, + len, + "%s/%s", + state_path, + histfile_basename); + } + return histfile_name; + +} + +struct history history_load() +{ + struct history vec = history_create(); + char *histfile_name = get_histfile_path(); + if (histfile_name == NULL) { + return vec; + } + + FILE *histfile = fopen(histfile_name, "rb"); + free(histfile_name); + + if (histfile == NULL) { + return vec; + } + + if (fseek(histfile, 0, SEEK_END) != 0) { + log_error("Error seeking in history file: %s.\n", strerror(errno)); + fclose(histfile); + return vec; + } + size_t len = ftell(histfile); + if (fseek(histfile, 0, SEEK_SET) != 0) { + log_error("Error seeking in history file: %s.\n", strerror(errno)); + fclose(histfile); + return vec; + } + + char *buf = xmalloc(len); + fread(buf, 1, len, histfile); + fclose(histfile); + + char *saveptr = NULL; + char *tok = strtok_r(buf, " ", &saveptr); + while (tok != NULL) { + size_t run_count = strtoull(tok, NULL, 10); + tok = strtok_r(NULL, "\n", &saveptr); + if (tok == NULL) { + break; + } + history_add(&vec, tok); + vec.buf[vec.count - 1].run_count = run_count; + tok = strtok_r(NULL, " ", &saveptr); + } + + free(buf); + return vec; +} + +void history_save(struct history *history) +{ + char *histfile_name = get_histfile_path(); + if (histfile_name == NULL) { + return; + } + + /* Create the path if necessary. */ + if (!mkdirp(histfile_name)) { + return; + } + + /* Use open rather than fopen to ensure the proper permissions. */ + int histfd = open(histfile_name, O_WRONLY | O_CREAT, 0600); + FILE *histfile = fdopen(histfd, "wb"); + if (histfile == NULL) { + return; + } + + for (size_t i = 0; i < history->count; i++) { + fprintf(histfile, "%zu %s\n", history->buf[i].run_count, history->buf[i].name); + } + + fclose(histfile); + free(histfile_name); +} + +struct history history_create(void) +{ + struct history vec = { + .count = 0, + .size = 16, + .buf = xcalloc(16, sizeof(struct program)) + }; + return vec; +} + +void history_destroy(struct history *restrict vec) +{ + for (size_t i = 0; i < vec->count; i++) { + free(vec->buf[i].name); + } + free(vec->buf); +} + +void history_add(struct history *restrict vec, const char *restrict str) +{ + /* + * If the program's already in our vector, just increment the count and + * move the program up if needed. + */ + for (size_t i = 0; i < vec->count; i++) { + if (!strcmp(vec->buf[i].name, str)) { + vec->buf[i].run_count++; + size_t count = vec->buf[i].run_count; + if (i > 0 && count <= vec->buf[i-1].run_count) { + return; + } + /* We need to move the program up the list */ + size_t j = i; + while (j > 0 && count > vec->buf[j-1].run_count) { + j--; + } + struct program tmp = vec->buf[i]; + memmove(&vec->buf[j+1], &vec->buf[j], (i - j) * sizeof(struct program)); + vec->buf[j] = tmp; + return; + } + } + + /* Otherwise add it to the end with a run count of 1 */ + if (vec->count == vec->size) { + vec->size *= 2; + vec->buf = xrealloc(vec->buf, vec->size * sizeof(vec->buf[0])); + } + vec->buf[vec->count].name = xstrdup(str); + vec->buf[vec->count].run_count = 1; + vec->count++; +} + +void history_remove(struct history *restrict vec, const char *restrict str) +{ + for (size_t i = 0; i < vec->count; i++) { + if (!strcmp(vec->buf[i].name, str)) { + free(vec->buf[i].name); + if (i < vec->count - 1) { + memmove(&vec->buf[i], &vec->buf[i+1], (vec->count - i) * sizeof(struct program)); + } + vec->count--; + return; + } + } +} + +bool mkdirp(const char *path) +{ + struct stat statbuf; + if (stat(path, &statbuf) == 0) { + /* If the history file exists, we don't need to do anything. */ + return true; + } + + /* + * Walk down the path, creating directories as we go. + * This works by repeatedly finding the next / in path, then calling + * mkdir() on the string up to that point. + */ + char *tmp = xstrdup(path); + char *cursor = tmp; + while ((cursor = strchr(cursor + 1, '/')) != NULL) { + *cursor = '\0'; + log_debug("Creating directory %s\n", tmp); + if (mkdir(tmp, 0700) != 0 && errno != EEXIST) { + log_error("Error creating history file path: %s.\n", strerror(errno)); + free(tmp); + return false; + } + *cursor = '/'; + } + free(tmp); + return true; +} |