xaizek / hstr (License: Apachev2) (since 2018-12-07)
Bash and Zsh shell history suggest box - easily view, navigate, search and manage your command history.
<root> / src / hstr.c (94555947162710395956c89e51b34b0a0f93415c) (9,381B) (mode 100644) [raw]
/*
 ============================================================================
 Name        : hstr.c
 Author      : Martin Dvorak
 Version     : 0.2
 Copyright   : Apache 2.0
 Description : Shell history completion utility
 ============================================================================
*/

#include <curses.h>
#include <fcntl.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <termios.h>
#include <unistd.h>
#include "include/hashset.h"

#define HSTR_VERSION "0.2"

#define LABEL_HISTORY " HISTORY "
#define LABEL_HELP "Type to filter history, use UP and DOWN arrow keys to choose an item using ENTER"
#define ENV_VAR_USER "USER"
#define ENV_VAR_HOME "HOME"
#define FILE_HISTORY ".bash_history"
#define SELECTION_CURSOR_IN_PROMPT -1

#define Y_OFFSET_PROMPT 0
#define Y_OFFSET_HELP 2
#define Y_OFFSET_HISTORY 3
#define Y_OFFSET_ITEMS 4

#define MIN(a,b) (((a)<(b))?(a):(b))
#define MAX(a,b) (((a)>(b))?(a):(b))

static char ** selection=NULL;
static int selectionSize=0;

int printPrompt(WINDOW *win) {
	char hostname[128];
	char *user = getenv(ENV_VAR_USER);
	int xoffset = 1;

	gethostname(hostname, sizeof hostname);
	mvwprintw(win, xoffset, Y_OFFSET_PROMPT, "%s@%s$ ", user, hostname);
	refresh();

	return xoffset+strlen(user)+1+strlen(hostname)+1;
}

void printHelpLabel(WINDOW *win) {
	mvwprintw(win, Y_OFFSET_HELP, 0, LABEL_HELP);
	refresh();
}

void printHistoryLabel(WINDOW *win) {
	char message[512];

	int width=getmaxx(win);

	strcpy(message, LABEL_HISTORY);
	width -= strlen(LABEL_HISTORY);
	int i;
	for (i=0; i < width; i++) {
		strcat(message, " ");
	}

	wattron(win, A_REVERSE);
	mvwprintw(win, Y_OFFSET_HISTORY, 0, message);
	wattroff(win, A_REVERSE);

	refresh();
}

int getMaxHistoryItems(WINDOW *win) {
	return (getmaxy(win)-(Y_OFFSET_ITEMS+2));
}

char *loadHistoryFile() {
	char *home = getenv(ENV_VAR_HOME);
	char *fileName=(char*)malloc(strlen(home)+1+strlen(FILE_HISTORY)+1);
	strcpy(fileName,home);
	strcat(fileName,"/");
	strcat(fileName,FILE_HISTORY);

	if(access(fileName, F_OK) != -1) {
		char *file_contents;
		long input_file_size;

		FILE *input_file = fopen(fileName, "rb");
		fseek(input_file, 0, SEEK_END);
		input_file_size = ftell(input_file);
		rewind(input_file);
		file_contents = malloc((input_file_size + 1) * (sizeof(char)));
		if(fread(file_contents, sizeof(char), input_file_size, input_file)==-1) {
			exit(EXIT_FAILURE);
		}
		fclose(input_file);
		file_contents[input_file_size] = 0;

		return file_contents;
	} else {
	    fprintf(stderr,"\nHistory file not found: %s\n",fileName);
	    exit(EXIT_FAILURE);
	}
}

int countHistoryLines(char *history) {
	int i = 0;
	char *p=strchr(history,'\n');
	while (p!=NULL) {
		i++;
		p=strchr(p+1,'\n');
	}
	return i;
}

char **tokenizeHistory(char *history, int lines) {
	char **tokens = malloc(sizeof(char*) * lines);

	int i = 0;
	char *pb=history, *pe;
	pe=strchr(history, '\n');
	while(pe!=NULL) {
		tokens[i]=pb;
		*pe=0;

		pb=pe+1;
		pe=strchr(pb, '\n');
		i++;
	}

	return tokens;
}

void allocSelection(int size) {
	selectionSize=size;
	if(selection!=NULL) {
		free(selection);
		selection=NULL;
	}
	if(size>0) {
		selection = malloc(size);
	}
}

int makeSelection(char* prefix, char **historyFileItems, int historyFileItemsCount, int maxSelectionCount) {
	allocSelection(sizeof(char*) * maxSelectionCount); // TODO realloc
	int i, selectionCount=0;

    HashSet set;
    hs_init(&set);

	for(i=0; i<historyFileItemsCount && selectionCount<maxSelectionCount; i++) {
		if(!hs_contains(&set, historyFileItems[i])) {
			if(prefix==NULL) {
				selection[selectionCount++]=historyFileItems[i];
				hs_add(&set, historyFileItems[i]);
			} else {
				if(historyFileItems[i]==strstr(historyFileItems[i], prefix)) {
					selection[selectionCount++]=historyFileItems[i];
					hs_add(&set, historyFileItems[i]);
				}
			}
		}
	}

	if(prefix!=NULL && selectionCount<maxSelectionCount) {
		for(i=0; i<historyFileItemsCount && selectionCount<maxSelectionCount; i++) {
			if(!hs_contains(&set, historyFileItems[i])) {
				char* substring = strstr(historyFileItems[i], prefix);
				if (substring != NULL && substring!=historyFileItems[i]) {
					selection[selectionCount++]=historyFileItems[i];
					hs_add(&set, historyFileItems[i]);
				}
			}
		}
	}

	selectionSize=selectionCount;
	return selectionCount;
}

char* printSelection(WINDOW *win, int maxHistoryItems, char *prefix, int historyFileItemsCount, char** historyFileItems) {
	char* result="";
	int selectionCount=makeSelection(prefix, historyFileItems, historyFileItemsCount, maxHistoryItems);
	if (selectionCount > 0) {
		result = selection[0];
	}

	int height=getMaxHistoryItems(win);
	int i;
	int y=Y_OFFSET_ITEMS;
	for (i = 0; i<height; ++i) {
		if(i<selectionSize) {
			mvwprintw(win, y++, 1, "%s", selection[i]);
			clrtoeol();
			if(prefix!=NULL) {
				wattron(win,A_BOLD);
				char *p=strstr(selection[i], prefix);
				mvwprintw(win, (y-1), 1+(p-selection[i]), "%s", prefix);
				wattroff(win,A_BOLD);
			}
		} else {
			mvwprintw(win, y++, 0, " ");
			clrtoeol();
		}
	}
	refresh();

	return result;
}

void highlightSelection(int selectionCursorPosition, int previousSelectionCursorPosition) {
	if(previousSelectionCursorPosition!=SELECTION_CURSOR_IN_PROMPT) {
		mvprintw(Y_OFFSET_ITEMS+previousSelectionCursorPosition, 0, " ");
	}
	if(selectionCursorPosition!=SELECTION_CURSOR_IN_PROMPT) {
		mvprintw(Y_OFFSET_ITEMS+selectionCursorPosition, 0, ">");
	}
}

char* selectionLoop(char **historyFileItems, int historyFileItemsCount) {
	initscr();
	if (has_colors() == FALSE) {
		endwin();
		printf("Your terminal does not support color\n");
		exit(1);
	}

	start_color();
	init_pair(1, COLOR_WHITE, COLOR_BLACK);
	attron(COLOR_PAIR(1));
	printHistoryLabel(stdscr);
	printHelpLabel(stdscr);
	printSelection(stdscr, getMaxHistoryItems(stdscr), NULL, historyFileItemsCount, historyFileItems);
	int basex = printPrompt(stdscr);
	int x = basex;
	attroff(COLOR_PAIR(1));

	int selectionCursorPosition=SELECTION_CURSOR_IN_PROMPT;
	int previousSelectionCursorPosition=SELECTION_CURSOR_IN_PROMPT;

	int y = 1, c, maxHistoryItems;
	bool done = FALSE;
	char prefix[500]="";
	char* result="";
	while (!done) {
		maxHistoryItems=getMaxHistoryItems(stdscr);

		noecho();
		c = wgetch(stdscr);
        //mvprintw(Y_OFFSET_HELP, 0, "Key pressed is = %4d Hopefully it can be printed as '%c'", c, c);
		echo();

		switch (c) {
		case 91:
			// TODO 91 killed > debug to determine how to distinguish \e and [
	        //mvprintw(Y_OFFSET_HELP, 0, "91 killed");
			break;
		case KEY_BACKSPACE:
		case 127:
			if(strlen(prefix)>0) {
				prefix[strlen(prefix)-1]=0;
				x--;
				wattron(stdscr,A_BOLD);
				mvprintw(y, basex, "%s", prefix);
				wattroff(stdscr,A_BOLD);
				clrtoeol();
			}

			if(strlen(prefix)>0) {
				makeSelection(prefix, historyFileItems, historyFileItemsCount, maxHistoryItems);
			} else {
				makeSelection(NULL, historyFileItems, historyFileItemsCount, maxHistoryItems);
			}
			result = printSelection(stdscr, maxHistoryItems, prefix, historyFileItemsCount, historyFileItems);
			break;
		case KEY_UP:
		case 65:
			if(selectionCursorPosition>SELECTION_CURSOR_IN_PROMPT) {
				previousSelectionCursorPosition=selectionCursorPosition;
				selectionCursorPosition--;
			} else {
				previousSelectionCursorPosition=SELECTION_CURSOR_IN_PROMPT;
			}
			highlightSelection(selectionCursorPosition, previousSelectionCursorPosition);
			break;
		case KEY_DOWN:
		case 66:
			previousSelectionCursorPosition=selectionCursorPosition;
			if((selectionCursorPosition+1)<selectionSize) {
				selectionCursorPosition++;
			} else {
				selectionCursorPosition=0;
			}
			highlightSelection(selectionCursorPosition, previousSelectionCursorPosition);
			break;
		case 10:
			if(selectionCursorPosition!=SELECTION_CURSOR_IN_PROMPT) {
		        mvprintw(Y_OFFSET_HELP, 0, "EXIT: %d %d   ",selectionCursorPosition, selectionSize);
				result=selection[selectionCursorPosition];
				allocSelection(0);
			}
			done = TRUE;
			break;
		default:
			if(c!=27) {
				strcat(prefix, (char*)(&c));
				wattron(stdscr,A_BOLD);
				mvprintw(y, basex, "%s", prefix);
				wattroff(stdscr,A_BOLD);
				clrtoeol();

				result = printSelection(stdscr, maxHistoryItems, prefix, historyFileItemsCount, historyFileItems);
			}
			break;
		}
	}
	endwin();

	return result;
}

void tiocsti() {
	char buf[] = "cmd";
	int i;
	for (i = 0; i < sizeof buf - 1; i++) {
		ioctl(0, TIOCSTI, &buf[i]);
	}
}

void fillTerminalInput(char* cmd){
	size_t size = strlen(cmd);
	int i;
	char *c;
	for (i = 0; i < size; i++) {
		// terminal I/O control, simulate terminal input
		c=(cmd+i);
		ioctl(0, TIOCSTI, c);
	}
	printf("\n");
}

void reverseCharPointerArray(char **array, int length) {
	int i;
	char * temp;
    for (i=0; i<length/2; i++) {
        temp = array[i];
        array[i] = array[length-i-1];
        array[length-i-1] = temp;
    }
}

void hstr() {
	char *historyAsString = loadHistoryFile(FILE_HISTORY);
	int itemsCount = countHistoryLines(historyAsString);
	char** items = tokenizeHistory(historyAsString, itemsCount);
	reverseCharPointerArray(items, itemsCount);
	char* command = selectionLoop(items, itemsCount);
	fillTerminalInput(command);
	free(historyAsString);
	free(items);
}

int main(int argc, char *argv[]) {
	hstr();
	return EXIT_SUCCESS;
}

Hints

Before first commit, do not forget to setup your git environment:
git config --global user.name "your_name_here"
git config --global user.email "your@email_here"

Clone this repository using HTTP(S):
git clone https://code.reversed.top/user/xaizek/hstr

Clone this repository using ssh (do not forget to upload a key first):
git clone ssh://rocketgit@code.reversed.top/user/xaizek/hstr

You are allowed to anonymously push to this repository.
This means that your pushed commits will automatically be transformed into a pull request:
... clone the repository ...
... make some changes and some commits ...
git push origin master