From 8cb3b81fb32b750eda5482ae3948525142ccd4ea Mon Sep 17 00:00:00 2001 From: Eyal Erez Date: Wed, 20 Nov 2013 18:30:47 -0500 Subject: [PATCH] Adding MRU support Change-Id: I1ddd0e89199807aac1ce33627d34b33e09f48b68 --- .gitignore | 6 ++ Makefile | 21 +++++-- config.mk | 2 +- dmenu_do | 2 + dmenu_mru.c | 187 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ mru.c | 131 ++++++++++++++++++++++++++++++++++++++++++ mru.h | 78 +++++++++++++++++++++++++ 7 files changed, 421 insertions(+), 6 deletions(-) create mode 100644 .gitignore create mode 100755 dmenu_do create mode 100644 dmenu_mru.c create mode 100644 mru.c create mode 100644 mru.h diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5f0a940 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +config.h +dmenu +dmenu_mru +stest +*.o +*.tar.gz \ No newline at end of file diff --git a/Makefile b/Makefile index 0f7dfbd..3a4e1b5 100644 --- a/Makefile +++ b/Makefile @@ -3,10 +3,10 @@ include config.mk -SRC = dmenu.c draw.c stest.c +SRC = dmenu.c draw.c stest.c dmenu_mru.c mru.c OBJ = ${SRC:.c=.o} -all: options dmenu stest +all: options dmenu stest dmenu_mru options: @echo dmenu build options: @@ -14,6 +14,9 @@ options: @echo "LDFLAGS = ${LDFLAGS}" @echo "CC = ${CC}" +debug: CFLAGS += -g +debug: all + .c.o: @echo CC -c $< @${CC} -c $< ${CFLAGS} @@ -32,14 +35,18 @@ stest: stest.o @echo CC -o $@ @${CC} -o $@ stest.o ${LDFLAGS} +dmenu_mru: dmenu_mru.o mru.o + @echo CC -o $@ + @${CC} -o $@ dmenu_mru.o mru.o ${LDFLAGS} + clean: @echo cleaning - @rm -f dmenu stest ${OBJ} dmenu-${VERSION}.tar.gz + @rm -f dmenu stest dmenu_mru ${OBJ} dmenu-${VERSION}.tar.gz dist: clean @echo creating dist tarball @mkdir -p dmenu-${VERSION} - @cp LICENSE Makefile README config.mk dmenu.1 draw.h dmenu_path dmenu_run stest.1 ${SRC} dmenu-${VERSION} + @cp LICENSE Makefile README config.mk dmenu.1 draw.h mru.h dmenu_path dmenu_run dmenu_do stest.1 ${SRC} dmenu-${VERSION} @tar -cf dmenu-${VERSION}.tar dmenu-${VERSION} @gzip dmenu-${VERSION}.tar @rm -rf dmenu-${VERSION} @@ -47,10 +54,12 @@ dist: clean install: all @echo installing executables to ${DESTDIR}${PREFIX}/bin @mkdir -p ${DESTDIR}${PREFIX}/bin - @cp -f dmenu dmenu_path dmenu_run stest ${DESTDIR}${PREFIX}/bin + @cp -f dmenu dmenu_path dmenu_mru dmenu_run dmenu_do stest ${DESTDIR}${PREFIX}/bin @chmod 755 ${DESTDIR}${PREFIX}/bin/dmenu @chmod 755 ${DESTDIR}${PREFIX}/bin/dmenu_path + @chmod 755 ${DESTDIR}${PREFIX}/bin/dmenu_mru @chmod 755 ${DESTDIR}${PREFIX}/bin/dmenu_run + @chmod 755 ${DESTDIR}${PREFIX}/bin/dmenu_do @chmod 755 ${DESTDIR}${PREFIX}/bin/stest @echo installing manual pages to ${DESTDIR}${MANPREFIX}/man1 @mkdir -p ${DESTDIR}${MANPREFIX}/man1 @@ -63,7 +72,9 @@ uninstall: @echo removing executables from ${DESTDIR}${PREFIX}/bin @rm -f ${DESTDIR}${PREFIX}/bin/dmenu @rm -f ${DESTDIR}${PREFIX}/bin/dmenu_path + @rm -f ${DESTDIR}${PREFIX}/bin/dmenu_mru @rm -f ${DESTDIR}${PREFIX}/bin/dmenu_run + @rm -f ${DESTDIR}${PREFIX}/bin/dmenu_do @rm -f ${DESTDIR}${PREFIX}/bin/stest @echo removing manual page from ${DESTDIR}${MANPREFIX}/man1 @rm -f ${DESTDIR}${MANPREFIX}/man1/dmenu.1 diff --git a/config.mk b/config.mk index c0d466b..6d05765 100644 --- a/config.mk +++ b/config.mk @@ -19,7 +19,7 @@ LIBS = -L${X11LIB} -lX11 ${XINERAMALIBS} # flags CPPFLAGS = -D_BSD_SOURCE -D_POSIX_C_SOURCE=200809L -DVERSION=\"${VERSION}\" ${XINERAMAFLAGS} CFLAGS = -ansi -pedantic -Wall -Os ${INCS} ${CPPFLAGS} -LDFLAGS = -s ${LIBS} +LDFLAGS = ${LIBS} # compiler and linker CC = cc diff --git a/dmenu_do b/dmenu_do new file mode 100755 index 0000000..6bba84b --- /dev/null +++ b/dmenu_do @@ -0,0 +1,2 @@ +#!/bin/sh +exe=`dmenu_path | dmenu_mru | dmenu ${1+"$@"}` && exec `dmenu_mru $exe` diff --git a/dmenu_mru.c b/dmenu_mru.c new file mode 100644 index 0000000..724186d --- /dev/null +++ b/dmenu_mru.c @@ -0,0 +1,187 @@ +/* See LICENSE file for copyright and license details. */ +#include +#include +#include +#include +#include +#include +#include + +#include "mru.h" + + +#define HISTORY ".dmenu_hist" +#define MAX_HIST 1024 +#define MAX_COMMAND 1024 + +static void die(const char *s) { + fprintf(stderr, "dmenu_mru: %s\n", s); + exit(EXIT_FAILURE); +} + + +/** + * Read commands from stdin and return a double linked list of commands. + */ +ListEntry* read_commands() { + ListEntry* commands = list_create(); + + char line[1024]; + while (fgets(line, 1024, stdin) != NULL) { /* Read everything from stdin */ + size_t len = strlen(line) - 1; /* Strip out newline character */ + if (line[len] == '\n') + line[len] = '\0'; + if (len > 0) { /* Build list of commands */ + list_add(commands, line); + } + } + return commands; +} + +/** + * Read command history from file. + */ +ListEntry* read_history() { + ListEntry* history = list_create(); + FILE *hist; + char line [1024]; + + if(!(hist = fopen(HISTORY, "r"))) { /* Open file for reading */ + return history; /* If history file is not there, just return an empty list */ + } + + while (fgets(line, sizeof line, hist) != NULL) { /* Read a line from file */ + size_t len = strlen(line) - 1; /* Strip out newline character */ + if (line[len] == '\n') + line[len] = '\0'; + if (len > 0) { /* If it's non-empty, */ + list_add(history, line); /* add to list */ + } + } + fclose(hist); + return history; +} + +/** + * Write commands to history file. + */ +void write_history(ListEntry* history) { + FILE *hist; + int i = 0; + ListEntry* e = NULL; + if(!(hist = fopen(HISTORY, "w"))) /* Open file for writing */ + die("failed to write history"); + /* Write out commands to file Note that we are only going to write out the first MAX_HIST + commands */ + for(e = history->next; e != history && i < MAX_HIST; e = e->next, ++i) { + fprintf(hist, "%s\n", e->val); + } + fclose(hist); +} + +/** + * Move command to the top of the history list. + */ +void touch_history(ListEntry* history, char* command) { + ListEntry* e = NULL; + for(e = history->next; e != history; e = e->next) { + if (strcmp(command, e->val) == 0) { /* Find the command in history */ + list_remove(e); + break; /* History should be unique, no need to continue looking. */ + } + } + list_insert(history, command); /* Add command at the front */ +} + +/** + * Move command to the top of the MRU history file. + */ +void touch_command(char* command) { + ListEntry* history = read_history(); /* Read command history from file */ + touch_history(history, command); /* Move command to the top of history */ + write_history(history); /* Write command history back to file */ +} + +/** + * Read commands from stdin and output them in MRU order. + */ +void process_commands() { + ListEntry* e = NULL; + + ListEntry* history = read_history(); /* Read command history from file */ + ListEntry* commands = read_commands(); /* Read commands from stdin */ + HashEntry** commands_hash = hash_create(MAX_HIST, commands); /* Create hashmap for fast lookup */ + + /* First output history entries that are present in the commands hash */ + for(e = history->next; e != history; e = e->next) { + HashEntry* he = hash_get(commands_hash, MAX_HIST, e->val); + if (he != NULL) { + printf("%s\n", he->key); + list_remove(he->val); /* Remove entry from the commands list to make sure we don't output it + again later */ + } + } + + /* Then output the rest of the commands list */ + for(e = commands->next; e != commands; e = e->next) { + printf("%s\n", e->val); + } + + /* Free memory associated with the lists and hash */ + list_destroy(commands); + list_destroy(history); + hash_destroy(commands_hash, MAX_HIST); +} + +/** + * Join all elements of a string array starting at index seperated by sep and place into s. + */ +void join(int argc, char* argv[], int start, char sep, char* s, int size) { + int i = 0; + int len = 0; + for(i = start; i < argc; ++i) { + len = strlen(argv[i]); /* Calculate the string length */ + if (size < len + 1) + die("Command too long"); /* Make sure we don't overrun the buffer */ + strncpy(s, argv[i], len); /* Copy string */ + s[len] = sep; /* Replace end of string with sep */ + s += len + 1; /* Move pointer by the amount we added */ + size -= len + 1; /* Update size remaining */ + } + if (argc - start > 0) { + s[-1] = '\0'; /* NULL terminate the string for good measure */ + } +} + +/** + * This application has to modes of operation: + * + * 1. If no command-line arguments are passed, it will take * a list of via stdin, and move the most + * recently used to the front of the list. This is useful when piping the output of dmenu_path + * to dmenu. Command history is storied in ~/.dmenu_hist. + * 2. If command-line arguments are passed, the application will update the history file + * (~/.dmenu_hist) and move or add the command to the front of the file. + * + * Together these can be used to make dmenu sort most used commands for easy access (see dmenu_do for an example). + */ +int main(int argc, char* argv[]) { + const char *home; + if(!(home = getenv("HOME"))) /* Get user's home dir */ + die("no $HOME"); + if(chdir(home) < 0) /* Change to user's home dir so cache files are created here */ + die("chdir failed"); + + if (argc > 1) { + /* If a command is passed on the command line, then move this command to the top of the MRU + history */ + char* command = malloc(MAX_COMMAND * sizeof(char)); + join(argc, argv, 1, ' ', command, MAX_COMMAND); /* Join all parameters into a single string + (skip the first because it's the executable + name) */ + touch_command(command); /* Move command to the top of the MRU history */ + printf(command); /* Output command so it can be executed */ + } else { /* Othewise, process stdin and sort commands in MRU order */ + process_commands(); + } + return EXIT_SUCCESS; +} diff --git a/mru.c b/mru.c new file mode 100644 index 0000000..a122d7e --- /dev/null +++ b/mru.c @@ -0,0 +1,131 @@ +/* See LICENSE file for copyright and license details. */ +#include +#include +#include +#include +#include +#include +#include +#include "mru.h" + +/********/ +/* LIST */ +/********/ + +/** + * Free memory for one list entry. Null entries will be silently ignored. + */ +void _list_entry_destroy(ListEntry* e) { + if (e != NULL) { + free(e->val); /* Remember to free the value */ + free(e); + } +} + +ListEntry *list_create() { + ListEntry *e = (ListEntry*) malloc(sizeof(ListEntry*)); /* Allocate new entry */ + e->next = e; /* This is the root entry so next and previous are set to self */ + e->previous = e; + e->val = NULL; /* No value in the root entry, it will remain null */ + return e; +} + +void list_add(ListEntry *root, char *val) { + ListEntry *n = (ListEntry*) malloc(sizeof(ListEntry*)); /* Allocate new entry */ + n->val = strdup(val); /* Copy and set the value */ + n->next = root; /* Insert before root which is the last item */ + n->previous = root->previous; + root->previous->next = n; + root->previous = n; +} + +ListEntry *list_insert(ListEntry *e, char *val) { + ListEntry *n = (ListEntry*) malloc(sizeof(ListEntry*)); /* Allocate new entry */ + n->val = strdup(val); /* Copy and set the value */ + n->previous = e; + n->next = e->next; + e->next = n; + return n; +} + +void list_remove(ListEntry *e) { + ListEntry *p = e->previous; /* Get previous and next entries */ + ListEntry *n = e->next; + p->next = e->next; + n->previous = e->previous; + _list_entry_destroy(e); /* Remember to free memory */ +} + +void list_destroy(ListEntry *root) { + ListEntry* p = NULL; /* Save previous value for each iteration, since we can't free + memory for the current entry before moving to the next */ + ListEntry *e = NULL; + for(e = root->next; e != root; p = e, e = e->next) { + _list_entry_destroy(p); + } + + _list_entry_destroy(p); /* Remember to free the last element */ +} + +/*************/ +/* HASHTABLE */ +/*************/ + +/** + * Calculate hash value for the given string. + */ +unsigned _hash(char *s, unsigned size) { + unsigned hashval; + for (hashval = 0; *s != '\0'; ++s) + hashval = *s + 31 * hashval; + return hashval % size; +} + +HashEntry **hash_create(unsigned size, ListEntry *root) { + HashEntry **hashtable = (HashEntry**) malloc(size * sizeof(HashEntry*)); + ListEntry *e; + for(e = root->next; e != root; e = e->next) { + hash_put(hashtable, size, e->val, e); + } + return hashtable; +} + +void hash_destroy(HashEntry **hashtable, unsigned size) { + int i; + for (i = 0; i < size; ++i) { + if (hashtable[i] != NULL) { + free(hashtable[i]->key); + free(hashtable[i]); + } + } + free(hashtable); +} + +HashEntry *hash_get(HashEntry **hashtable, unsigned size, char *key) { + HashEntry *e = NULL; + for (e = hashtable[_hash(key, size)]; e != NULL; e = e->next) { + if (strcmp(key, e->key) == 0) + return e; + } + return NULL; +} + +HashEntry *hash_put(HashEntry **hashtable, unsigned size, char *key, ListEntry *val) { + HashEntry *e = NULL; + unsigned h; + if (key == NULL) { + return NULL; /* Will not add empty keys */ + } + e = hash_get(hashtable, size, key); + if (e != NULL) { /* Existing entry found. We'll just update it */ + e->val = val; + } else { /* Entry not found. Need a new one */ + e = (HashEntry*) malloc(sizeof(*e)); /* Allocate new Entry */ + e->key = strdup(key); /* Update entry's key */ + h = _hash(key, size); /* Calculate the hash value */ + e->next = hashtable[h]; /* Push this entry to the top of the list */ + e->val = val; + hashtable[h] = e; /* Add new entry */ + } + return e; +} diff --git a/mru.h b/mru.h new file mode 100644 index 0000000..fc2e895 --- /dev/null +++ b/mru.h @@ -0,0 +1,78 @@ +/* See LICENSE file for copyright and license details. */ +#ifndef MRU_H +#define MRU_H + +/********/ +/* LIST */ +/********/ + +/** + * Linked list entry + */ +typedef struct ListEntry { + struct ListEntry* next; /* Next entry in chain */ + struct ListEntry* previous; /* Previous entry in chain */ + char* val; /* Value */ +} ListEntry; + +/** + * Create empty list. + */ +ListEntry* list_create(); + +/** + * Destroy list and release all memory. + */ +void list_destroy(ListEntry* root); + +/** + * Add entry to double linked list. + */ +void list_add(ListEntry* root, char* val); + +/** + * Insert entry in a double linked list. The entry will be inserted after e. The new entry is + * returned. + */ +ListEntry* list_insert(ListEntry* e, char* val); + +/** + * Remove entry from a double linked list. Note that "e"'s memory will be freed. + */ +void list_remove(ListEntry* root); + +/*************/ +/* HASHTABLE */ +/*************/ + +/** + * Hashtable entry + */ +typedef struct HashEntry { + struct HashEntry* next; /* Next entry in chain */ + char* key; /* key */ + ListEntry* val; /* value */ +} HashEntry; + +/** + * Create hashtable and add all elements from the linked list. + */ +HashEntry** hash_create(unsigned size, ListEntry* head); + +/** + * Destroy hashtable. + */ +void hash_destroy(HashEntry** hashtable, unsigned size); + +/** + * Get entry. + */ +HashEntry* hash_get(HashEntry** hashtable, unsigned size, char* key); + +/** + * Add entry to hashmap. + */ +HashEntry* hash_put(HashEntry** hashtable, unsigned size, char* key, ListEntry* val); + + +#endif -- 1.8.4.4