iomenu

interactive text selection menu
Log | Files | Refs | README | LICENSE

commit 13035ab96ecce34b052d703bebe4bc995ecc3920
parent 35d50bc1a88b26d1f57219a9f3f7b1a907c57498
Author: Josuah Demangeon⠠⠵ <mail@josuah.net>
Date:   Thu, 16 Mar 2017 22:26:25 +0100

on my way for cleaning this up

Diffstat:
A.gitignore | 1+
MLICENSE | 30++++++++++++------------------
MMakefile | 2+-
Miomenu.c | 479++++++++++++++++++++++++-------------------------------------------------------
4 files changed, 159 insertions(+), 353 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -0,0 +1 @@ +iomenu diff --git a/LICENSE b/LICENSE @@ -1,21 +1,15 @@ -MIT License +ISC Licence -Copyright (c) 2016 Josuah Demangeon⠠⠵ +Copyright (c) 2017 by Josuah Demangeon -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +THE SOFTWARE IS PROVIDED “AS IS” AND ISC DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY +SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/Makefile b/Makefile @@ -1,4 +1,4 @@ -CFLAGS = -std=c99 -pedantic -Wall -Wextra -g -static +CFLAGS = -std=c99 -Wpedantic -Wall -Wextra -g -static OBJ = ${SRC:.c=.o} all: clean iomenu diff --git a/iomenu.c b/iomenu.c @@ -9,64 +9,83 @@ #include <sys/ioctl.h> -/*--- constants --------------------------------------------------------------*/ - -#define LINE_SIZE 1024 #define OFFSET 5 #define CONTINUE 2 /* as opposed to EXIT_SUCCESS and EXIT_FAILURE */ - -/*--- macros -----------------------------------------------------------------*/ - #define CONTROL(char) (char ^ 0x40) #define MIN(X, Y) (((X) < (Y)) ? (X) : (Y)) #define MAX(X, Y) (((X) > (Y)) ? (X) : (Y)) -/*--- structures -------------------------------------------------------------*/ - typedef struct Line { char *text; /* sent as output and matched by input */ int match; /* whether it matches buffer's input */ } Line; -/*--- variables --------------------------------------------------------------*/ - Line **buffer; -int current, matching, total; -int opt_lines; -char *opt_prompt, *input; +size_t current = 0, matching = 0, total = 0; +char *input = ""; +int opt_lines = 30; +char *opt_prompt = ""; + +void +die(const char *s) +{ + /* tcsetattr(STDIN_FILENO, TCSANOW, &termio_old); */ + fprintf(stderr, "%s\n", s); + exit(EXIT_FAILURE); +} + + +struct termios +set_terminal(int tty_fd) +{ + struct termios termio_old; + struct termios termio_new; + + /* set the terminal to send one key at a time. */ + + /* get the terminal's state */ + if (tcgetattr(tty_fd, &termio_old) < 0) + die("Can not get terminal attributes with tcgetattr()."); + + /* create a new modified state by switching the binary flags */ + termio_new = termio_old; + termio_new.c_lflag &= ~(ICANON | ECHO | IGNBRK); + + /* apply this state to buffer[current] terminal now (TCSANOW) */ + tcsetattr(tty_fd, TCSANOW, &termio_new); + + return termio_old; +} -/*--- functions --------------------------------------------------------------*/ -/* - * Fill the buffer apropriately with the lines - */ void fill_buffer(void) { extern Line **buffer; - char s[LINE_SIZE]; - size_t size = 1; + char s[BUFSIZ]; + size_t size = 2 << 4; - buffer = malloc(sizeof(Line) * 2 << 4); + buffer = malloc(sizeof(Line) * size); input[0] = '\0'; total = matching = 1; /* read the file into an array of lines */ - for (; fgets(s, LINE_SIZE, stdin); total++, matching++) { + for (; fgets(s, BUFSIZ, stdin); total++, matching++) { if (total > size) { size *= 2; - if (!realloc(buffer, size * sizeof(Line))) + if (!realloc(buffer, sizeof(Line) * size)) die("realloc"); } + /* empty input match everything */ + buffer[total]->matches = 1; buffer[total]->text[strlen(s) - 1] = '\0'; - buffer[total]->match = 1; /* empty input match everything */ } } @@ -74,8 +93,6 @@ fill_buffer(void) void free_buffer(Line **buffer) { - Line *next = NULL; - for (; total > 0; total--) free(buffer[total - 1]->text); @@ -83,6 +100,17 @@ free_buffer(Line **buffer) } +int +line_matches(Line *line, char **tokv, size_t tokc) +{ + for (size_t i = 0; i < tokc; i++) + if (strstr(line->text, tokv[i]) != 0) + return 0; + + return 1; +} + + /* * If inc is 1, it will only check already matching lines. * If inc is 0, it will only check non-matching lines. @@ -105,131 +133,89 @@ filter_lines(int inc) /* match lines */ matching = 0; - for (int i = 0; i < total; i++) { + for (size_t i = 0; i < total; i++) { if (input[0] && strcmp(input, buffer[i]->text) == 0) { buffer[i]->match = 1; } else if ((inc && buffer[i]->match) || (!inc && !buffer[i]->match)) { - buffer[i]->match = match_line(buffer[i], tokv, tokc); + buffer[i]->match = line_matches(buffer[i], tokv, tokc); matching += buffer[i]->match; } } } -/* - * Return whecher the line matches every string from tokv. - */ int -match_line(Line *line, char **tokv, size_t tokc) -{ - for (int i = 0; i < tokc; i++) - if (!!strstr(buffer[i]->text, tokv[i])) - return 0; - - return 1; -} - - -/* - * Seek the previous matching line, or NULL if none matches. - */ -Line * matching_prev(int pos) { - for (; pos > 0 && !buffer[pos]->match; pos--); - return buffer[pos]; + for (size_t i = pos; i > 0; i--) + if (buffer[i]->match) + return i; + return pos; } -/* - * Seek the next matching line, or NULL if none matches. - */ -Line * -matching_next(int pos) +int +matching_next(size_t pos) { - for (; pos < total && !buffer[pos]->match; pos++); - return buffer[pos]; + for (size_t i = pos; i < total; i++) + if (buffer[i]->match) + return i; + + return pos; } -/* - * Print a line to stderr. - */ -void -draw_line(Line *line, const int cols) +int +matching_close(size_t pos) { - char output[LINE_SIZE] = "\033[K"; - int n = 0; - - if (opt_line_numbers) { - strcat(output, buffer[current] ? "\033[1;37m" : "\033[1;30m"); - sprintf(output + strlen(output), "%7d\033[m ", line->number); - } else { - strcat(output, buffer[current] ? "\033[1;31m > " : " "); - } - n += 8; + if (buffer[pos]->match) + return pos; - /* highlight buffer[current] line */ - if (buffer[current]) - strcat(output, "\033[1;33m"); + for (size_t i = 0; i + pos < total && i <= pos; i++) { + if (buffer[pos - i]->match) + return pos - i; - /* content */ - strncat(output, line->content, cols - n); - n += strlen(line->content); + if (buffer[pos + i]->match) + return pos + i; + } - /* MAX with '1' as \033[0C still move 1 to the right */ - sprintf(output + strlen(output), "\033[%dC", - MAX(1, 40 - n)); - n += MAX(1, 40 - n); - strcat(output, "\033[m\n"); + return pos; +} - fputs(output, stderr); +void +draw_line(size_t pos, const size_t cols) +{ + fprintf(stderr, pos == current ? "\033[1;31m%s" : "%s", buffer[pos]->text); } -/* - * Print all the lines from an array of pointer to lines. - * - * The total number oflines printed shall not excess 'count'. - */ void -draw_lines( int count, int cols) +draw_lines(size_t count, size_t cols) { - Line *line = buffer[current]; - int i = 0; - int j = 0; - - /* seek back from buffer[current] line to the first line to print */ - while (line && i < count - OFFSET) { - i = line->matches ? i + 1 : i; - line = line->prev; - } - line = line ? line : first; - - /* print up to count lines that match the input */ - while (line && j < count) { - if (line->matches) { - draw_line(line, line == buffer[current], cols); - j++; + size_t i; + for (i = MAX(current, 0); i < total && i < count;) { + if (buffer[i]->match) { + draw_line(i, cols); + i++; } - - line = line->next; } /* continue up to the end of the screen clearing it */ - for (; j < count; j++) - fputs("\r\033[K\n", stderr); + for (; i < count; i++) + fputs("\n\033[K", stderr); +} + + +void +draw_prompt(int cols) +{ + fprintf(stderr, "\r\033[K\033[1m%7s %s", opt_prompt, input); } -/* - * Update the screen interface and print all candidates. - * - * This also has to clear the previous lines. - */ void draw_screen( int tty_fd) { @@ -261,72 +247,49 @@ draw_clear(int lines) } -/* - * Print the prompt, before the input, with the number of candidates that - * match. - */ void -draw_prompt(int cols) +remove_word_input() { - size_t i; - int matching = matching; - int total = total; - - /* for the '/' separator between the numbers */ - cols--; - - /* number of digits */ - for (i = matching; i; i /= 10, cols--); - for (i = total; i; i /= 10, cols--); - cols -= !matching ? 1 : 0; /* 0 also has one digit*/ - - /* actual prompt */ - fprintf(stderr, "\r%-6s\033[K\033[1m>\033[m ", opt_prompt); - cols -= 2 + MAX(strlen(opt_prompt), 6); - - /* input without overflowing terminal width */ - for (i = 0; i < strlen(input) && cols > 0; cols--, i++) - fputc(input[i], stderr); + size_t len = strlen(input) - 1; - /* save the cursor position at the end of the input */ - fputs("\033[s", stderr); + for (int i = len; i >= 0 && isspace(input[i]); i--) + input[i] = '\0'; - /* grey */ - fputs("\033[1;30m", stderr); + len = strlen(input) - 1; + for (int i = len; i >= 0 && !isspace(input[i]); i--) + input[i] = '\0'; +} - /* go to the end of the line */ - fprintf(stderr, "\033[%dC", cols); - /* total match and line count at the end of the line */ - fprintf(stderr, "%d/%d", matching, total); +void +add_character(char key) +{ + size_t len = strlen(input); - /* restore cursor position at the end of the input */ - fputs("\033[m\033[u", stderr); + if (isprint(key)) { + input[len] = key; + input[len + 1] = '\0'; + } + filter_lines(1); + current = matching_close(current); } /* - * Listen for the user input and call the appropriate functions. + * Send the selection to stdout. */ -int -input_get(int tty_fd) +void +print_selection(int return_input) { - FILE *tty_fp = fopen("/dev/tty", "r"); - int exit_code; - - /* receive one character at a time from the terminal */ - struct termios termio_old = set_terminal(tty_fd); - - while ((exit_code = input_key(tty_fp)) == CONTINUE) - draw_screen(tty_fd); - - /* resets the terminal to the previous state. */ - tcsetattr(tty_fd, TCSANOW, &termio_old); + fputs("\r\033[K", stderr); - fclose(tty_fp); + if (return_input || !matching) { + puts(input); - return exit_code; + } else if (matching > 0) { + puts(buffer[current]->text); + } } @@ -341,7 +304,7 @@ input_key(FILE *tty_fp) char key = fgetc(tty_fp); if (key == '\n') { - action_print_selection(0); + print_selection(0); return EXIT_SUCCESS; } @@ -355,12 +318,10 @@ input_key(FILE *tty_fp) input[0] = '\0'; current = 0; filter_lines(0); - action_jump(1); - action_jump(-1); break; case CONTROL('W'): - action_remove_word_input(); + remove_word_input(); filter_lines(0); break; @@ -368,15 +329,15 @@ input_key(FILE *tty_fp) case CONTROL('H'): /* backspace */ input[strlen(input) - 1] = '\0'; filter_lines(0); - action_jump(0); + current = matching_close(current); break; case CONTROL('N'): - action_jump(1); + current = matching_next(current); break; case CONTROL('P'): - action_jump(-1); + matching_prev(current); break; case CONTROL('I'): /* tab */ @@ -386,52 +347,11 @@ input_key(FILE *tty_fp) case CONTROL('J'): case CONTROL('M'): /* enter */ - action_print_selection(0); - return EXIT_SUCCESS; - - case CONTROL('@'): /* ctrl + space */ - action_print_selection(1); + print_selection(0); return EXIT_SUCCESS; - case CONTROL('['): /* escape */ - switch (fgetc(tty_fp)) { - - case 'O': /* arrow keys */ - switch (fgetc(tty_fp)) { - - case 'A': /* up */ - action_jump(-1); - break; - - case 'B': /* Down */ - action_jump(1); - break; - } - break; - - case '[': /* page control */ - key = fgetc(tty_fp); - switch(fgetc(tty_fp)) { - - case '~': - switch (key) { - - case '5': /* page up */ - action_jump(-10); - break; - - case '6': /* page down */ - action_jump(10); - break; - } - break; - } - break; - } - break; - default: - action_add_character(key); + add_character(key); } return CONTINUE; @@ -439,129 +359,26 @@ input_key(FILE *tty_fp) /* - * Set the buffer[current] line to next/previous/any matching line. - */ -void -action_jump(int direction) -{ - Line * line = buffer[current]; - Line * result = line; - - if (direction == 0 && !buffer[current]->match) { - line = matching_next(current); - line = line ? line : matching_prev(current); - result = line ? line : result; - } - - for (; direction < 0 && line; direction++) { - line = matching_prev(line); - result = line ? line : result; - } - - for (; direction > 0 && line; direction--) { - line = matching_next(line); - result = line ? line : result; - } - - buffer[current] = result; -} - - -/* - * Remove the last word from the buffer's input - */ -void -action_remove_word_input() -{ - size_t len = strlen(input) - 1; - - for (int i = len; i >= 0 && isspace(input[i]); i--) - input[i] = '\0'; - - len = strlen(input) - 1; - for (int i = len; i >= 0 && !isspace(input[i]); i--) - input[i] = '\0'; -} - - -/* - * Add a character to the buffer input and filter lines again. - */ -void -action_add_character(char key) -{ - size_t len = strlen(input); - - if (isprint(key)) { - input[len] = key; - input[len + 1] = '\0'; - } - - filter_lines(1); - - action_jump(0); -} - - -/* - * Send the selection to stdout. - */ -void -action_print_selection(int return_input) -{ - fputs("\r\033[K", stderr); - - if (return_input || !matching) { - puts(input); - - } else if (matching > 0) { - puts(buffer[current]->text); - } -} - - -opt_line_numbers = 0; -opt_print_number = 0; -opt_lines = 30; -opt_prompt = ""; - - -/* - * Reset the terminal state and exit with error. - */ -void -die(const char *s) -{ - /* tcsetattr(STDIN_FILENO, TCSANOW, &termio_old); */ - fprintf(stderr, "%s\n", s); - exit(EXIT_FAILURE); -} - - -/* - * Set terminal to send one char at a time for interactive mode, and return the - * last terminal state. + * Listen for the user input and call the appropriate functions. */ -struct termios -set_terminal(int tty_fd) +int +input_get(int tty_fd) { - struct termios termio_old; - struct termios termio_new; + FILE *tty_fp = fopen("/dev/tty", "r"); + int exit_code; - /* set the terminal to send one key at a time. */ + /* receive one character at a time from the terminal */ + struct termios termio_old = set_terminal(tty_fd); - /* get the terminal's state */ - if (tcgetattr(tty_fd, &termio_old) < 0) - die("Can not get terminal attributes with tcgetattr()."); + while ((exit_code = input_key(tty_fp)) == CONTINUE) + draw_screen(tty_fd); - /* create a new modified state by switching the binary flags */ - termio_new = termio_old; - termio_new.c_lflag &= ~(ICANON | ECHO | IGNBRK); + /* resets the terminal to the previous state. */ + tcsetattr(tty_fd, TCSANOW, &termio_old); - /* apply this state to buffer[current] terminal now (TCSANOW) */ - tcsetattr(tty_fd, TCSANOW, &termio_new); + fclose(tty_fp); - return termio_old; + return exit_code; } @@ -578,8 +395,6 @@ int main(int argc, char *argv[]) { int i, exit_code, tty_fd = open("/dev/tty", O_RDWR); - Buffer *buffer = NULL; - Opt *opt = malloc(sizeof(Opt)); /* command line arguments */ @@ -588,9 +403,6 @@ main(int argc, char *argv[]) usage(); switch (argv[i][1]) { - case 'n': - opt_line_numbers = 1; - break; case 'l': if (sscanf(argv[++i], "%d", &opt_lines) <= 0) die("wrong number format after -l"); @@ -606,7 +418,7 @@ main(int argc, char *argv[]) } /* command line arguments */ - buffer = fill_buffer(); + fill_buffer(); /* set the interface */ draw_screen(tty_fd); @@ -618,7 +430,6 @@ main(int argc, char *argv[]) /* close files descriptors and pointers, and free memory */ close(tty_fd); - free(opt); free_buffer(buffer); return exit_code;