iomenu

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

commit d0e21509afe9e6ebf8e3f0e75ce31d9799905c7a
Author: Josuah Demangeon⠠⠵ <mail@josuah.net>
Date:   Sat, 11 Mar 2017 11:18:03 +0100

import from repo.or.cz (sadly)

Diffstat:
ALICENSE | 21+++++++++++++++++++++
AMakefile | 22++++++++++++++++++++++
AREADME | 90+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
ATODO | 8++++++++
Abuffer.c | 209+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adraw.c | 176+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainput.c | 237+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aio-abduco | 15+++++++++++++++
Aio-files | 58++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aio-grep | 16++++++++++++++++
Aio-man | 9+++++++++
Aio-mblaze | 16++++++++++++++++
Aio-run | 100+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aio-setfont | 11+++++++++++
Aiomenu.1 | 77+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Amain.c | 91+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Amain.h | 95+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Anohup.out | 92+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Autil.c | 73+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
19 files changed, 1416 insertions(+), 0 deletions(-)

diff --git a/LICENSE b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2016 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: + +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. diff --git a/Makefile b/Makefile @@ -0,0 +1,22 @@ +CFLAGS = -std=c89 -pedantic -Wall -Wextra -g -static +SRC = main.c buffer.c util.c draw.c input.c +OBJ = ${SRC:.c=.o} + +MANPREFIX = $(PREFIX) + +all: clean iomenu + +.c.o: + ${CC} -c ${CFLAGS} $< + +iomenu: ${OBJ} + ${CC} -o $@ ${OBJ} ${LDFLAGS} + rm -f *.o + +clean: + rm -f iomenu ${OBJ} + +install: iomenu + mkdir -p $(PREFIX)/bin $(MANPREFIX)/man/man1 + cp *.1 $(MANPREFIX)/man/man1/ + cp iomenu io-* $(PREFIX)/bin/ diff --git a/README b/README @@ -0,0 +1,90 @@ +iomenu - Filter lines from stdin with an interactive menu + + . _ __ _ __ + | (_) ||| (/_ | | |_| + +________________________________________________________________________________ + + iomenu is a terminal tool to interactively select lines from stdin, and + print them out to the standard output. + + You can use scripts made for dmenu [1], as iomenu mostly the same way. + + Thanks to the authors of dmenu [1], sandy [2], vis-menu[3], pep[4], ... + that taught me C by writing some. + + +Getting started +________________________________________________________________________________ + + You can install iomenu by running: + + """ + make install + """ + + You can optionnaly set a "PREFIX" variable to set the path to + install to: + + """ + make PREFIX="$HOME/bin" install + """ + + All you need to build it is a C compiler: It is plain C89 source + code without external dependencies. + + All usage details are written in the man page, "iomenu.1". + + +Examples +________________________________________________________________________________ + + +Open a bookmark from a list in a text file + + """ + iomenu < bookmarks-urls.txt | xargs firefox + """ + + +Go to a subdirectory + + """ + cd "$(find . -type d | iomenu)" + """ + + +Edit a file located in ~ + + """ + $EDITOR "$(find -type f | iomenu)" + """ + + +Play an audio file + + """ + mplayer "$(find ~/Music | iomenu)" + """ + + +Select a background job to attach to + + """ + fg "%$(jobs | iomenu | cut -c 2)" + """ + + +Filter "ps" output and print a process ID + + """ + { printf '#'; ps ax; } | iomenu -s '#' | sed -r 's/ *([0-9]*).*/\1/' + """ + + +________________________________________________________________________________ + +1 http://git.suckless.org/dmenu/tree/dmenu.c +2 http://git.suckless.org/sandy/tree/sandy.c +3 http://github.com/martanne/vis/blob/master/vis-menu.c +4 http://github.com/charles-l/pep/blob/master/pep.c diff --git a/TODO b/TODO @@ -0,0 +1,8 @@ +- Check return values for every function that may fail. + +- Add support for a default input string (when I will need it or if + someone ask for it). + +- Fix the input shifting the line count by 1. + +- Case insensitive match. diff --git a/buffer.c b/buffer.c @@ -0,0 +1,209 @@ +#include <ctype.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include "main.h" + + +/* + * Fill the buffer apropriately with the lines and headers. + */ +Buffer * +fill_buffer(char *separator) +{ + /* fill buffer with string */ + char s[LINE_SIZE]; + Buffer *buffer = malloc(sizeof(Buffer)); + FILE *fp = stdin; + int l; + + if (!fp) + die("Can not open file for reading."); + + buffer->input[0] = '\0'; + buffer->total = buffer->matching = 1; + + /* empty line in case no line come from stdin */ + buffer->first = buffer->current = malloc(sizeof(Line)); + buffer->first->content = buffer->first->comment = ""; + buffer->first->next = buffer->first->prev = NULL; + buffer->last = NULL; + + /* read the file into a doubly linked list of lines */ + for (l = 1; fgets(s, LINE_SIZE, fp); buffer->total++, l++) { + buffer->last = add_line(buffer, l, s, separator, buffer->last); + + l = buffer->last->header ? 0 : l; + } + + /* prevent initial current line to be a header */ + buffer->current = buffer->first; + while (buffer->current->next && buffer->current->header) + buffer->current = buffer->current->next; + + return buffer; +} + + +/* + * Add a line to the end of the current buffer. + */ +Line * +add_line(Buffer *buffer, int number, char *s, char *separator, Line *prev) +{ + /* allocate new line */ + buffer->last = new_line(s, separator); + buffer->last->number = number; + buffer->last->matches = 1; /* matches by default */ + buffer->matching++; + + /* interlink with previous line if exists */ + if (prev) { + prev->next = buffer->last; + buffer->last->prev = prev; + } else { + buffer->first = buffer->last; + } + + return buffer->last; +} + + +/* + * Parse the line content to determine if it is a header and identify the + * separator if any. + */ +Line * +new_line(char *s, char *separator) +{ + Line *line = malloc(sizeof(Line)); + char *sep = separator ? strstr(s, separator) : NULL; + int pos = sep ? (int) (sep - s) : (int) strlen(s) - 1; + + /* header is when separator is the first character of the line */ + line->header = (sep == s); + + /* strip trailing newline */ + s[strlen(s) - 1] = '\0'; + + /* fill line->content */ + line->content = malloc((pos + 1) * sizeof(char)); + strncpy(line->content, s, pos); + + /* fill line->comment */ + line->comment = malloc((strlen(s) - pos) * sizeof(char)); + if (sep) { + strcpy(line->comment, s + pos + strlen(separator)); + } + + /* strip trailing whitespaces from line->content */ + for (pos--; pos > 0 && isspace(line->content[pos]); pos--) + line->content[pos] = '\0'; + + /* strip leading whitespaces from line->comment */ + for (pos = 0; isspace(line->comment[pos]); pos++); + line->comment += pos; + + return line; +} + + +/* + * Free the buffer, also recursing the doubly linked list. + */ +void +free_buffer(Buffer *buffer) +{ + Line *next = NULL; + + while (buffer->first) { + next = buffer->first->next; + + free(buffer->first); + + buffer->first = next; + } + + free(buffer); +} + + +/* + * Set the line->matching state according to the return value of match_line, + * and buffer->matching to number of matching candidates. + * + * The incremental parameter sets whether check already matching or + * non-matching lines only. This is for performance concerns. + */ +void +filter_lines(Buffer *buffer, int inc) +{ + Line *line = buffer->first; + char **tokv = NULL; + char *s, buf[sizeof buffer->input]; + size_t n = 0, tokc = 0; + + /* tokenize input from space characters, this comes from dmenu */ + strcpy(buf, buffer->input); + for (s = strtok(buf, " "); s; s = strtok(NULL, " ")) { + if (++tokc > n && !(tokv = realloc(tokv, ++n * sizeof(*tokv)))) + die("cannot realloc memory for tokv\n"); + + tokv[tokc - 1] = s; + } + + /* match lines */ + buffer->matching = 0; + while (line) { + if (buffer->input[0] && !strcmp(buffer->input, line->content)) { + line->matches = 1; + buffer->current = line; + } else if ((inc && line->matches) || (!inc && !line->matches)) { + line->matches = match_line(line, tokv, tokc); + buffer->matching += line->header ? 0 : line->matches; + } + + line = line->next; + } +} + + +/* + * Return whecher the line matches every string from tokv. + */ +int +match_line(Line *line, char **tokv, size_t tokc) +{ + size_t i, match = 1, offset = 0; + + if (line->header) + return 1; + + for (i = 0; i < tokc && match; i++) + match = !!strstr(line->content + offset, tokv[i]); + + return match; +} + + +/* + * Seek the previous matching line, or NULL if none matches. + */ +Line * +matching_prev(Line *line) +{ + while ((line = line->prev) && (!line->matches || line->header)); + return line; +} + + +/* + * Seek the next matching line, or NULL if none matches. + */ +Line * +matching_next(Line *line) +{ + while ((line = line->next) && (!line->matches || line->header)); + return line; +} diff --git a/draw.c b/draw.c @@ -0,0 +1,176 @@ +#include <stdlib.h> +#include <string.h> +#include <stdio.h> +#include <sys/ioctl.h> + +#include "main.h" + + +/* + * Print a line to stderr. + */ +void +draw_line(Line *line, int current, const int cols, Opt *opt) +{ + char *content = expand_tabs(line->content); + char *comment = expand_tabs(line->comment); + char output[LINE_SIZE * sizeof(char)] = "\033[K"; + int n = 0; + + if (opt->line_numbers && !line->header) { + strcat(output, current ? "\033[1;37m" : "\033[1;30m"); + sprintf(output + strlen(output), "%7d\033[m ", line->number); + } else { + strcat(output, current ? "\033[1;31m > " : " "); + } + n += 8; + + + /* highlight current line */ + if (current) + strcat(output, "\033[1;33m"); + + /* content */ + strncat(output, content, cols - n); + n += strlen(content); + + /* align comment */ + if (!line->header && line->comment[0] != '\0') { + /* 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); + } else if (line->header) + + /* comment */ + strcat(output, "\033[1;30m"); + strncat(output, comment, cols - n); + n += strlen(comment); + + strcat(output, "\033[m\n"); + + fputs(output, stderr); + + free(content); + free(comment); +} + + +/* + * Print all the lines from an array of pointer to lines. + * + * The total number oflines printed shall not excess 'count'. + */ +void +draw_lines(Buffer *buffer, int count, int cols, Opt *opt) +{ + Line *line = buffer->current; + int i = 0; + int j = 0; + + /* seek back from 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 : buffer->first; + + /* print up to count lines that match the input */ + while (line && j < count) { + if (line->matches) { + draw_line(line, line == buffer->current, cols, opt); + j++; + } + + line = line->next; + } + + /* continue up to the end of the screen clearing it */ + for (; j < count; j++) + fputs("\r\033[K\n", stderr); +} + + +/* + * Update the screen interface and print all candidates. + * + * This also has to clear the previous lines. + */ +void +draw_screen(Buffer *buffer, int tty_fd, Opt *opt) +{ + struct winsize w; + int count; + + if (ioctl(tty_fd, TIOCGWINSZ, &w) < 0) + die("could not get terminal size"); + + count = MIN(opt->lines, w.ws_row - 2); + + fputs("\n", stderr); + draw_lines(buffer, count, w.ws_col, opt); + + /* go up to the prompt position and update it */ + fprintf(stderr, "\033[%dA", count + 1); + draw_prompt(buffer, w.ws_col, opt); +} + + +void +draw_clear(int lines) +{ + int i; + + for (i = 0; i < lines + 1; i++) + fputs("\r\033[K\n", stderr); + fprintf(stderr, "\033[%dA", lines + 1); +} + + +/* + * Print the prompt, before the input, with the number of candidates that + * match. + */ +void +draw_prompt(Buffer *buffer, int cols, Opt *opt) +{ + size_t i; + int matching = buffer->matching; + int total = buffer->total; + char *input = expand_tabs(buffer->input); + char *suggest = expand_tabs(buffer->current->content); + + /* 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); + + /* save the cursor position at the end of the input */ + fputs("\033[s", stderr); + + /* grey */ + fputs("\033[1;30m", stderr); + + /* 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); + + /* restore cursor position at the end of the input */ + fputs("\033[m\033[u", stderr); + + free(input); + free(suggest); +} diff --git a/input.c b/input.c @@ -0,0 +1,237 @@ +#include <ctype.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <termios.h> + +#include "main.h" + + +/* + * Listen for the user input and call the appropriate functions. + */ +int +input_get(Buffer *buffer, int tty_fd, Opt *opt) +{ + 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); + + /* get input char by char from the keyboard. */ + while ((exit_code = input_key(tty_fp, buffer, opt)) == CONTINUE) + draw_screen(buffer, tty_fd, opt); + + /* resets the terminal to the previous state. */ + tcsetattr(tty_fd, TCSANOW, &termio_old); + + fclose(tty_fp); + + return exit_code; +} + + +/* + * Perform action associated with key + */ +int +input_key(FILE *tty_fp, Buffer *buffer, Opt *opt) +{ + char key = fgetc(tty_fp); + + if (key == opt->validate_key) { + action_print_selection(buffer, 0, opt); + return EXIT_SUCCESS; + } + + switch (key) { + + case CONTROL('C'): + draw_clear(opt->lines); + return EXIT_FAILURE; + + case CONTROL('U'): + buffer->input[0] = '\0'; + buffer->current = buffer->first; + filter_lines(buffer, 0); + action_jump(buffer, 1); + action_jump(buffer, -1); + break; + + case CONTROL('W'): + action_remove_word_input(buffer); + filter_lines(buffer, 0); + break; + + case 127: + case CONTROL('H'): /* backspace */ + buffer->input[strlen(buffer->input) - 1] = '\0'; + filter_lines(buffer, 0); + action_jump(buffer, 0); + break; + + case CONTROL('N'): + action_jump(buffer, 1); + break; + + case CONTROL('P'): + action_jump(buffer, -1); + break; + + case CONTROL('I'): /* tab */ + strcpy(buffer->input, buffer->current->content); + filter_lines(buffer, 1); + break; + + case CONTROL('J'): + case CONTROL('M'): /* enter */ + action_print_selection(buffer, 0, opt); + return EXIT_SUCCESS; + + case CONTROL('@'): /* ctrl + space */ + action_print_selection(buffer, 1, opt); + return EXIT_SUCCESS; + + case CONTROL('['): /* escape */ + switch (fgetc(tty_fp)) { + + case 'O': /* arrow keys */ + switch (fgetc(tty_fp)) { + + case 'A': /* up */ + action_jump(buffer, -1); + break; + + case 'B': /* Down */ + action_jump(buffer, 1); + break; + } + break; + + case '[': /* page control */ + key = fgetc(tty_fp); + switch(fgetc(tty_fp)) { + + case '~': + switch (key) { + + case '5': /* page up */ + action_jump(buffer, -10); + break; + + case '6': /* page down */ + action_jump(buffer, 10); + break; + } + break; + } + break; + } + break; + + default: + action_add_character(buffer, key); + } + + return CONTINUE; +} + + +/* + * Set the current line to next/previous/any matching line. + */ +void +action_jump(Buffer *buffer, int direction) +{ + Line * line = buffer->current; + Line * result = line; + + if (direction == 0 && !buffer->current->matches) { + line = matching_next(buffer->current); + line = line ? line : matching_prev(buffer->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(Buffer *buffer) +{ + size_t length = strlen(buffer->input) - 1; + int i; + + for (i = length; i >= 0 && isspace(buffer->input[i]); i--) + buffer->input[i] = '\0'; + + length = strlen(buffer->input) - 1; + for (i = length; i >= 0 && !isspace(buffer->input[i]); i--) + buffer->input[i] = '\0'; +} + + +/* + * Add a character to the buffer input and filter lines again. + */ +void +action_add_character(Buffer *buffer, char key) +{ + size_t length = strlen(buffer->input); + + if (isprint(key)) { + buffer->input[length] = key; + buffer->input[length + 1] = '\0'; + } + + filter_lines(buffer, 1); + + action_jump(buffer, 0); +} + + +/* + * Send the selection to stdout. + */ +void +action_print_selection(Buffer *buffer, int return_input, Opt *opt) +{ + Line *line = NULL; + + fputs("\r\033[K", stderr); + + if (opt->print_header) { + for (line = buffer->current; line; line = line->prev) { + if (line->header) { + fputs(line->comment, stdout); + break; + } + } + fputc((int) '\t', stdout); + } + + if (opt->print_number) { + if (buffer->matching > 0) + printf("%d\n", buffer->current->number); + + } else if (return_input || !buffer->matching) { + puts(buffer->input); + + } else if (buffer->matching > 0) { + puts(buffer->current->content); + } +} diff --git a/io-abduco b/io-abduco @@ -0,0 +1,15 @@ +# Prompt for an abduco session to attach to + +if [ "$ABDUCO" ] +then + printf 'session already active: %s\n' "$ABDUCO" + exit 1 +fi + +name="$(printf '#%s' "$( + abduco | sed -r 's/(.*)\t(.*)/\2 # \1/' +)" | iomenu -s '#')" + +[ "$SSH_CLIENT$SSH_TTY$SSH_CONNECTION" ] && e='^\' || e='^Z' + +TERM=screen ABDUCO="$name" exec abduco -e "$e" -A "$name" "$SHELL" diff --git a/io-files b/io-files @@ -0,0 +1,58 @@ +# Prompt a file to open in PAGER, with an history. In less(1), 'v' to edit. + + +CACHE="${XDG_CACHE_HOME:-$HOME/.cache}" + + +path() +( + if [ "$1" ] + then + printf '%s\n' "$(cd "${1%/*}"; pwd)/${1##*/}" + else + { + printf '#\n# Recent files\n' + [ -f "$CACHE/iomenu/files" ] && + tac "$CACHE/iomenu/files" + + printf '#\n# Current directory\n' + find "$PWD" -maxdepth 1 -type f + + printf '#\n# All files\n' + find ~ -type f ! -path '*/.cache/*' ! -path '*/.git/*' + + } | sed "s|$HOME|~|" | iomenu -l 256 -s '#' | sed "s|~|$HOME|" + + fi | tee -a "$CACHE/iomenu/files" +) + + +history() +( + sort "$CACHE/iomenu/files" | uniq -d | while IFS='' read -r f + do + printf '%s\n' "$( + grep -Fxv "$f" "$CACHE/iomenu/files" + )" "$f" > "$CACHE/iomenu/files" + done + + printf '%s\n' "$(tail "$CACHE/iomenu/files")" > "$CACHE/iomenu/files" +) + + +main() +( + mkdir -p "$CACHE/iomenu" + + file="$(path "$1")" + + # terminal name + printf '\033]0;%s\007' "$(printf %s "$file" | sed "s|$HOME|~|")" + + history + + [ "$file" ] && [ -d "${file%/*}" ] && exec $EDITOR "$file" +) + + +main "$@" diff --git a/io-grep b/io-grep @@ -0,0 +1,16 @@ +directory="$( + cd "$HOME" + find . -type d ! -path '*/.git/*' ! -name '.git' | + sed 's/^./~/' | iomenu -l 256 +)" + +directory="$HOME${directory#\~}" + +grep -rL '\x00' "$directory" | while IFS='' read -r path +do + printf '#io-grep %s\n' "~${path#$HOME}" + cat "$path" +done | iomenu -s '#io-grep' -H -N -l 256 | { + IFS=' ' read -r path line + exec $EDITOR +"$line"g "$HOME${path#\~}" +} diff --git a/io-man b/io-man @@ -0,0 +1,9 @@ +# prompt a man page to open + +man "$( + IFS=':' + find $(manpath -q) ! -type d | + sed -r 's/.*\/(.*).[0-9](.gz)?$/\1/' | + sort -u | + iomenu +)" diff --git a/io-mblaze b/io-mblaze @@ -0,0 +1,16 @@ +T=' ' +choice="$( + mdirs "${MAIL%/*}" | while IFS='' read -r dir + do + printf '#\n# %s\n' "${dir##*/}" + + mlist "$dir" | mpick :u | msort -d | mthread | + mscan -f '%D %24f %u%t%2i%120S' + + done | iomenu -N -H -s '#' -l 255 +)" + +[ "$choice" ] || exit 0 + +mlist "${MAIL%/*}/${choice%%$T*}" | mpick :u | msort -d | mthread | +sed -n "${choice#*$T}p" | mshow | $PAGER diff --git a/io-run b/io-run @@ -0,0 +1,100 @@ +# Prompt for a programs to run + + +CACHE="${XDG_CACHE_HOME:-$HOME/.cache}" + + +usage() +{ + printf 'Usage: %s [cmd [args...] [+]] + +cmd do not prompt for a command and run cmd right away +args do not prompt for arguments neither and use arg ++ if present after the arguments, prompt for a path\n' "${0##*/}" +} + + +# +# Update the cache and get the command to run. +# +update_cache() +( + IFS=':' u=0 + + for dir in $PATH + do + [ "$CACHE/dmenu_run" -ot "$dir" ] && u=1 + done + + [ "$u" -eq 1 ] && find -L $PATH -type f -exec test -x {} \; -print | + sed 's|.*/||' | sort -u > "$CACHE/dmenu_run" +) + + +# +# Prompt for options for a given command and log it to an history file +# +get_options() +( + local command="$1" + + printf '%s ' "$command" >> "$CACHE/iomenu/run" + + while read -r cmd opt + do + [ "$command" = "$cmd" ] && printf '%s\n' "$opt" + done < "$CACHE/iomenu/run" | + iomenu -p "$command" | tee -a "$CACHE/iomenu/run" + + sort -u "$CACHE/iomenu/run" -o "$CACHE/iomenu/run" +) + + +# +# Prompt for a file path in $HOME and print it. +# +get_path() +( + find "$HOME" ! -path "$CACHE" ! -path '*/.git/*' | + sed -r "s/.{${#HOME}}/~/" | iomenu -l 256 | sed 's/^~//' +) + + +# +# Get the options according to the command and run it +# +run() +( + command="${1:-$(iomenu -l 256 -s '#' < "$CACHE/dmenu_run")}" + + [ -z "$command" ] && exit 1 + + options="$(get_options "$command")" + + if [ "$options" ] && [ -z "${options%%*+}" ] + then + path="$(get_path)" options="${options%+}" + fi + + if [ "$path" ] + then exec $command $options "$path" + else exec $command $options + fi +) + + +main() +( + mkdir -p "$CACHE/iomenu" + + if [ $# -gt 0 ] && [ -z "${1##-*}" ] + then + usage + else + update_cache + run "$@" + fi +) + + +main "$@" diff --git a/io-setfont b/io-setfont @@ -0,0 +1,11 @@ +setfont "$( + find /usr/share ~ -type d -name consolefonts | while IFS='' read -r path + do + cd "$path" || exit 1 + + fonts="$(find . -type f | cut -c 3-)" + + [ "$fonts" ] && printf '#\n# %s\n%s\n' "$path" "$fonts" + + done | iomenu -l 256 -s '#' -H | sed 's/\t/\//' +)" diff --git a/iomenu.1 b/iomenu.1 @@ -0,0 +1,77 @@ +.Dd $Mdocdate: October 16 2016 $ +.Dt IOMENU 1 +.Os +.Sh NAME +.Nm iomenu +.Op Fl nNHksl +. +. +.Sh DESCRIPTION +. +The +.Nm +utility filters lines form stdin interactively with the keyboard, and print +the selected line to stdout. +.Pp +Lower case switches are for the interface, uppercase switches are for +input/output. +.Bl -tag +.It Fl n +Display line numbers in interface. +. +.It Fl N +Return the line number rather than the match. +. +.It Fl H +Return the current header that the selection belongs to in addition to the +match, delimited by a tab. +. +.It Fl k Cm key +Key to use to validate current selection in addition to Enter. +. +.It Fl s Cm separator +Character separating the content from the comments. Every character after +the separator will be considered as comment and will be grayed and aligned +in the interface. +.Pp +If a separator is at the beginning of a line (without leading space), the +line is considered as a section header, and it will always be displayed +regardless if it matches or not. +. +.It Fl l Cm lines +Number of lines to display at once. Default is 30. +.El +. +. +.Sh KEYBINDINGS +. +.Bl -tag +.It Cm ^M, ^J, Enter +Print the matched line to stdout and exit. +. +.It Cm ^@, ^Space +Print the content of the input rather than the matched line to stdout and exit. +. +.It Cm ^P / ^N, Up / Down +Navigate to the previous / next line. +. +.It Cm PageUp / PageDown +Navigate 10 lines up / down. +. +.It Cm ^I, Tab +Set input to the currently highlighted candidate, then cycle through candidate +list. +. +.It Cm ^H, Backspace +Delete one char backward, but if there is no char +backward, it should return an error code of 1. +. +.It Cm ^C +Cancel, and make filter return the error code of 1. +. +.It Cm ^W +Deletes the last entered word. +. +.It Cm ^U +Deletes the entire input and jump to the first line. +.El diff --git a/main.c b/main.c @@ -0,0 +1,91 @@ +#include <ctype.h> +#include <fcntl.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/ioctl.h> +#include <termios.h> +#include <unistd.h> + +#include "main.h" + + +void +usage(void) +{ + fputs("usage: iomenu [-n] [-N] [-k key] [-s separator] ", stderr); + fputs("[-p prompt] [-l lines]\n", stderr); + + exit(EXIT_FAILURE); +} + + +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)); + + opt->line_numbers = 0; + opt->print_number = 0; + opt->validate_key = CONTROL('M'); + opt->separator = NULL; + opt->lines = 30; + opt->prompt = ""; + + /* command line arguments */ + for (i = 1; i < argc; i++) { + if (argv[i][0] != '-' || strlen(argv[i]) != 2) + usage(); + + switch (argv[i][1]) { + case 'n': + opt->line_numbers = 1; + break; + case 'N': + opt->print_number = 1; + opt->line_numbers = 1; + break; + case 'H': + opt->print_header = 1; + break; + case 'k': + opt->validate_key = (argv[++i][0] == '^') ? + CONTROL(toupper(argv[i][1])): argv[i][0]; + break; + case 's': + opt->separator = argv[++i]; + break; + case 'l': + if (sscanf(argv[++i], "%d", &opt->lines) <= 0) + die("wrong number format after -l"); + break; + case 'p': + if (++i >= argc) + die("wrong string format after -p"); + opt->prompt = argv[i]; + break; + default: + usage(); + } + } + + /* command line arguments */ + buffer = fill_buffer(opt->separator); + + /* set the interface */ + draw_screen(buffer, tty_fd, opt); + + /* listen and interact to input */ + exit_code = input_get(buffer, tty_fd, opt); + + draw_clear(opt->lines); + + /* close files descriptors and pointers, and free memory */ + close(tty_fd); + free(opt); + free_buffer(buffer); + + return exit_code; +} diff --git a/main.h b/main.h @@ -0,0 +1,95 @@ +#define LINE_SIZE 1024 +#define OFFSET 5 +#define CONTINUE 2 /* as opposed to EXIT_SUCCESS and EXIT_FAILURE */ + +#define CONTROL(char) (char ^ 0x40) +#define MIN(X, Y) (((X) < (Y)) ? (X) : (Y)) +#define MAX(X, Y) (((X) > (Y)) ? (X) : (Y)) + + +/* + * Options from the command line, to pass to each function that need some + */ +typedef struct Opt { + int line_numbers; + int print_number; + int print_header; + char validate_key; + char *separator; + int lines; + char *prompt; +} Opt; + + +/* + * Line coming from stdin, wrapped in a header. + */ +typedef struct Line { + char *content; /* sent as output and matched by input */ + char *comment; /* displayed at the right of the content */ + + int number; /* set here as order will not change */ + int matches; /* whether it matches buffer's input */ + int header; /* whether the line is a header */ + + struct Line *prev; /* doubly linked list structure */ + struct Line *next; +} Line; + + +/* + * Buffer containing a doubly linked list of headers + */ +typedef struct Buffer { + int total; /* total number of line in buffer */ + int matching; /* number lines matching the input */ + + char input[LINE_SIZE]; /* string from user's keyboard */ + + Line *current; /* selected line, highlighted */ + Line *first; /* boundaries of the linked list */ + Line *last; +} Buffer; + + +/* main */ + +void usage(void); + + +/* buffer */ + +Buffer * fill_buffer(char *); +void free_buffer(Buffer *); +Line * add_line(Buffer *, int, char *, char *, Line *); +Line * new_line(char *, char *); +Line * matching_next(Line *); +Line * matching_prev(Line *); +int match_line(Line *, char **, size_t); +void filter_lines(Buffer *, int); + + +/* draw */ + +void draw_screen(Buffer *, int, Opt *); +void draw_clear(int); +void draw_line(Line *, int, int, Opt *); +void draw_lines(Buffer *, int, int, Opt *); +void draw_prompt(Buffer *, int, Opt *); + + +/* input */ + +int input_get(Buffer *, int, Opt *); +int input_key(FILE *, Buffer *, Opt *); +void action_jump(Buffer *, int); +void action_print_selection(Buffer *,int, Opt *); +void action_remove_word_input(Buffer *); +void action_add_character(Buffer *, char); + + +/* util */ + +void die(const char *); +struct termios set_terminal(int); +char * expand_tabs(char *); diff --git a/nohup.out b/nohup.out @@ -0,0 +1,92 @@ +build: Installing tmux +checking for a BSD-compatible install... /usr/bin/install -c +checking whether build environment is sane... yes +checking for a thread-safe mkdir -p... /bin/mkdir -p +checking for gawk... no +checking for mawk... mawk +checking whether make sets $(MAKE)... yes +checking whether make supports nested variables... yes +checking build system type... x86_64-unknown-linux-gnu +checking host system type... x86_64-unknown-linux-gnu +checking for gcc... gcc +checking whether the C compiler works... yes +checking for C compiler default output file name... a.out +checking for suffix of executables... +checking whether we are cross compiling... no +checking for suffix of object files... o +checking whether we are using the GNU C compiler... yes +checking whether gcc accepts -g... yes +checking for gcc option to accept ISO C89... none needed +checking whether gcc understands -c and -o together... yes +checking for style of include used by make... GNU +checking dependency style of gcc... gcc3 +checking how to run the C preprocessor... gcc -E +checking for grep that handles long lines and -e... /bin/grep +checking for egrep... /bin/grep -E +checking for pkg-config... /usr/bin/pkg-config +checking pkg-config is at least version 0.9.0... yes +checking for glibc... yes +checking for ANSI C header files... yes +checking for sys/types.h... yes +checking for sys/stat.h... yes +checking for stdlib.h... yes +checking for string.h... yes +checking for memory.h... yes +checking for strings.h... yes +checking for inttypes.h... yes +checking for stdint.h... yes +checking for unistd.h... yes +checking bitstring.h usability... no +checking bitstring.h presence... no +checking for bitstring.h... no +checking dirent.h usability... yes +checking dirent.h presence... yes +checking for dirent.h... yes +checking fcntl.h usability... yes +checking fcntl.h presence... yes +checking for fcntl.h... yes +checking for inttypes.h... (cached) yes +checking libutil.h usability... no +checking libutil.h presence... no +checking for libutil.h... no +checking ndir.h usability... no +checking ndir.h presence... no +checking for ndir.h... no +checking paths.h usability... yes +checking paths.h presence... yes +checking for paths.h... yes +checking pty.h usability... yes +checking pty.h presence... yes +checking for pty.h... yes +checking for stdint.h... (cached) yes +checking sys/dir.h usability... yes +checking sys/dir.h presence... yes +checking for sys/dir.h... yes +checking sys/ndir.h usability... no +checking sys/ndir.h presence... no +checking for sys/ndir.h... no +checking sys/tree.h usability... no +checking sys/tree.h presence... no +checking for sys/tree.h... no +checking term.h usability... no +checking term.h presence... no +checking for term.h... no +checking util.h usability... no +checking util.h presence... no +checking for util.h... no +checking for library containing flock... none required +checking for dirfd... yes +checking for flock... yes +checking for prctl... yes +checking for sysconf... yes +checking for cfmakeraw... yes +checking for library containing clock_gettime... none required +checking for LIBEVENT... no +checking for library containing event_init... no +checking event.h usability... no +checking event.h presence... no +checking for event.h... no +configure: error: "libevent not found" +make: *** No rule to make target 'install'. Stop. +build: Updating index in /home/josuah/.local/tmux +build: Removing broken links from /home/josuah/.local diff --git a/util.c b/util.c @@ -0,0 +1,73 @@ +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <termios.h> +#include <unistd.h> + +#include "main.h" + + +/* + * 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. + */ +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 current terminal now (TCSANOW) */ + tcsetattr(tty_fd, TCSANOW, &termio_new); + + return termio_old; +} + + +/* + * Replace tab as a multiple of 8 spaces in a line. + * + * Allocates memory. + */ +char * +expand_tabs(char *line) +{ + size_t i, n; + char *converted = malloc(sizeof(char) * (strlen(line) * 8 + 1)); + + for (i = 0, n = 0; i < strlen(line); i++, n++) { + if (line[i] == '\t') { + for (; n == 0 || n % 8 != 0; n++) + converted[n] = ' '; + n--; + } else { + converted[n] = line[i]; + } + } + + converted[n] = '\0'; + + return converted; +}