├── .gitignore ├── script ├── link ├── compile-darwin ├── compile └── test ├── git-select-file.gif ├── .travis.yml ├── buffer_test_expected ├── util.h ├── textbuffer.h ├── menu_test.c ├── util.c ├── config.h ├── buffer.h ├── menu.h ├── terminal.h ├── buffer_test.c ├── Makefile ├── textbuffer.c ├── tmenu.1 ├── terminal.c ├── buffer.c ├── README.md ├── main.c └── menu.c /.gitignore: -------------------------------------------------------------------------------- 1 | /*.o 2 | /*_test 3 | /*_test_* 4 | /tmenu 5 | -------------------------------------------------------------------------------- /script/link: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | TARGET=$1; shift 3 | cc -o $TARGET "$@" 4 | -------------------------------------------------------------------------------- /git-select-file.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dhamidi/tmenu/HEAD/git-select-file.gif -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: c 2 | compiler: 3 | - gcc 4 | - clang 5 | script: 6 | - make 7 | - make test 8 | -------------------------------------------------------------------------------- /script/compile-darwin: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | cc -g -std=c99 -pedantic -Wall -Werror -D_DARWIN_C_SOURCE -c -o $2 $1 3 | -------------------------------------------------------------------------------- /buffer_test_expected: -------------------------------------------------------------------------------- 1 | 13 2 | hello, world 3 | hello, world 4 | hello, new world 5 | hell to, new world 6 | o, new world 7 | 8 | κάθαρσις 9 | -------------------------------------------------------------------------------- /script/compile: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | SYSTEM=$(uname -s | tr '[A-Z]' '[a-z]') 3 | COMPILE=${0%%/*}/compile-${SYSTEM} 4 | 5 | if [ -e $COMPILE ]; then 6 | exec $COMPILE $1 $2 7 | fi 8 | 9 | cc -g -std=c99 -pedantic -Wall -Werror -c -o $2 $1 10 | -------------------------------------------------------------------------------- /script/test: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | TEST_FILE_NAME=$1 4 | TEST_OUTPUT_ACTUAL=${1}_actual 5 | TEST_OUTPUT_EXPECTED=${1}_expected 6 | 7 | ./$1 > $TEST_OUTPUT_ACTUAL 8 | diff -u $TEST_OUTPUT_EXPECTED $TEST_OUTPUT_ACTUAL 9 | TEST_DIFFERENCES=$? 10 | 11 | if [ -n "$(command -v valgrind)" ]; then 12 | valgrind --error-exitcode=1 --leak-check=full --quiet ./$1 2>&1 >/dev/null 13 | TEST_VALGRIND=$? 14 | fi 15 | 16 | exit $(( TEST_DIFFERENCES + TEST_VALGRIND )) 17 | -------------------------------------------------------------------------------- /util.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014 Dario Hamidi 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with this program. If not, see . 16 | */ 17 | #define _POSIX_C_SOURCE 200809L 18 | #ifndef TMENU_UTIL_H 19 | #define TMENU_UTIL_H 20 | 21 | #include "config.h" 22 | #include 23 | 24 | void* must_malloc(size_t n); 25 | 26 | #endif 27 | -------------------------------------------------------------------------------- /textbuffer.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014 Dario Hamidi 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with this program. If not, see . 16 | */ 17 | #define _POSIX_C_SOURCE 200809L 18 | #ifndef TMENU_TEXTBUFFER_H 19 | #define TMENU_TEXTBUFFER_H 20 | 21 | #include "buffer.h" 22 | 23 | extern struct buffer_interface TextBuffer; 24 | 25 | #endif 26 | -------------------------------------------------------------------------------- /menu_test.c: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014 Dario Hamidi 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with this program. If not, see . 16 | */ 17 | #define _POSIX_C_SOURCE 200809L 18 | #include 19 | 20 | #include "menu.c" 21 | 22 | void test_menugrow_updates_capacity() { 23 | MENU menu = menunew(); 24 | size_t capacity_before_grow = menu->capacity; 25 | menugrow(menu); 26 | assert(menu->capacity == (capacity_before_grow + MENU_GROW_BY)); 27 | menudestroy(&menu); 28 | } 29 | 30 | int main(int argc, char** argv) { 31 | test_menugrow_updates_capacity(); 32 | } 33 | -------------------------------------------------------------------------------- /util.c: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014 Dario Hamidi 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with this program. If not, see . 16 | */ 17 | #include "util.h" 18 | 19 | #include "config.h" 20 | #include 21 | #include 22 | #include 23 | #include 24 | 25 | void* must_malloc(size_t n) { 26 | void* buffer = calloc(1, n); 27 | if (buffer == 0) { 28 | int error = errno; 29 | fprintf(stderr, "%s: failed to allocate buffer of size %ld: %s\n", 30 | PROGRAM_NAME, (long int)n, strerror(error)); 31 | abort(); 32 | } 33 | 34 | return buffer; 35 | } 36 | -------------------------------------------------------------------------------- /config.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014 Dario Hamidi 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with this program. If not, see . 16 | */ 17 | #ifndef PROGRAM_NAME 18 | #define PROGRAM_NAME "tmenu" 19 | #endif 20 | 21 | #ifndef BUFFER_GROW_BY 22 | #define BUFFER_GROW_BY 1024 23 | #endif 24 | 25 | #ifndef MENU_ITEM_MAX_SIZE 26 | #define MENU_ITEM_MAX_SIZE 2048 27 | #endif 28 | 29 | #ifndef MENU_MAX_ITEMS 30 | #define MENU_MAX_ITEMS 10000 31 | #endif 32 | 33 | #ifndef MENU_GROW_BY 34 | #define MENU_GROW_BY 1000 35 | #endif 36 | 37 | #ifndef MENU_DEFAULT_PROMPT 38 | #define MENU_DEFAULT_PROMPT ">>" 39 | #endif 40 | 41 | #ifndef MENU_DEFAULT_HEIGHT 42 | #define MENU_DEFAULT_HEIGHT 3 43 | #endif 44 | 45 | #ifndef MENU_DEFAULT_STATUS_LINE 46 | #define MENU_DEFAULT_STATUS_LINE 1 47 | #endif 48 | -------------------------------------------------------------------------------- /buffer.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014 Dario Hamidi 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with this program. If not, see . 16 | */ 17 | #define _POSIX_C_SOURCE 200809L 18 | #ifndef TMENU_BUFFER_H 19 | #define TMENU_BUFFER_H 20 | 21 | #include 22 | 23 | typedef struct buffer * BUFFER; 24 | 25 | extern struct buffer_interface { 26 | BUFFER (*new)(size_t); 27 | void (*destroy)(BUFFER*); 28 | size_t (*length)(BUFFER); 29 | void (*cput)(BUFFER, int); 30 | void (*sput)(BUFFER, const char*); 31 | char* (*string)(BUFFER, char**, size_t*); 32 | void (*forward)(BUFFER, int); 33 | void (*backward)(BUFFER, int); 34 | const char* (*before)(BUFFER); 35 | const char* (*after)(BUFFER); 36 | size_t (*point)(BUFFER); 37 | void (*delete)(BUFFER); 38 | void (*clear)(BUFFER); 39 | void (*delete_to_end)(BUFFER); 40 | void (*delete_to_beginning)(BUFFER); 41 | } Buffer; 42 | 43 | #endif 44 | -------------------------------------------------------------------------------- /menu.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014 Dario Hamidi 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with this program. If not, see . 16 | */ 17 | #define _POSIX_C_SOURCE 200809L 18 | #ifndef TMENU_MENU_H 19 | #define TMENU_MENU_H 20 | 21 | #include 22 | #include "textbuffer.h" 23 | #include "terminal.h" 24 | 25 | typedef struct menu * MENU; 26 | 27 | extern struct menu_interface { 28 | MENU (*new)(void); 29 | void (*destroy)(MENU*); 30 | 31 | void (*set_prompt)(MENU, const char*); 32 | void (*set_height)(MENU, int); 33 | void (*set_max_width)(MENU, int); 34 | void (*set_max_height)(MENU, int); 35 | void (*enable_status_line)(MENU, int); 36 | void (*add_item)(MENU, const char*); 37 | void (*select_next)(MENU); 38 | void (*select_prev)(MENU); 39 | const char* (*selection)(MENU); 40 | void (*display)(MENU, TERMINAL); 41 | void (*match)(MENU); 42 | BUFFER (*buffer)(MENU); 43 | } Menu; 44 | 45 | #endif 46 | -------------------------------------------------------------------------------- /terminal.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014 Dario Hamidi 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with this program. If not, see . 16 | */ 17 | #define _POSIX_C_SOURCE 200809L 18 | #ifndef TMENU_TERMINAL_H 19 | #define TMENU_TERMINAL_H 20 | 21 | #include 22 | 23 | typedef struct terminal * TERMINAL; 24 | 25 | enum terminal_error { 26 | TERMINAL_OK = 0, 27 | TERMINAL_GET_ATTR_FAIL, 28 | TERMINAL_SET_ATTR_FAIL, 29 | TERMINAL_UNKNOWN_MODE, 30 | }; 31 | typedef enum terminal_error TERMINAL_ERROR; 32 | 33 | extern struct terminal_interface { 34 | TERMINAL (*new)(const char*); 35 | void (*destroy)(TERMINAL*); 36 | 37 | TERMINAL_ERROR (*interactive_mode)(TERMINAL); 38 | TERMINAL_ERROR (*standard_mode)(TERMINAL); 39 | 40 | void (*left)(TERMINAL, int); 41 | void (*right)(TERMINAL, int); 42 | void (*up)(TERMINAL, int); 43 | void (*down)(TERMINAL, int); 44 | void (*erase)(TERMINAL, int); 45 | void (*col)(TERMINAL, int); 46 | void (*highlight)(TERMINAL,int); 47 | FILE* (*file)(TERMINAL); 48 | int (*fd)(TERMINAL); 49 | } Terminal; 50 | 51 | #endif 52 | -------------------------------------------------------------------------------- /buffer_test.c: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014 Dario Hamidi 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with this program. If not, see . 16 | */ 17 | #include "buffer.h" 18 | #include 19 | #include 20 | 21 | int main(int argc, char** argv) { 22 | BUFFER buffer = Buffer.new(1024); 23 | char* string = NULL; 24 | size_t string_len = 0; 25 | 26 | Buffer.sput(buffer, "hello, world"); 27 | printf("%ld\n", (long int)Buffer.point(buffer)); 28 | puts(Buffer.string(buffer, &string, &string_len)); 29 | 30 | Buffer.backward(buffer, 5); 31 | fputs(Buffer.before(buffer),stdout); 32 | fputs("", stdout); 33 | puts(Buffer.after(buffer)); 34 | 35 | Buffer.sput(buffer,"new "); 36 | puts(Buffer.string(buffer, &string, &string_len)); 37 | 38 | Buffer.backward(buffer, 100); 39 | Buffer.forward(buffer, 4); 40 | Buffer.sput(buffer," t"); 41 | puts(Buffer.string(buffer, &string, &string_len)); 42 | 43 | Buffer.delete_to_beginning(buffer); 44 | puts(Buffer.string(buffer, &string, &string_len)); 45 | 46 | Buffer.delete_to_end(buffer); 47 | puts(Buffer.string(buffer, &string, &string_len)); 48 | 49 | Buffer.sput(buffer, "κάθαρσις"); 50 | puts(Buffer.string(buffer, &string, &string_len)); 51 | 52 | Buffer.destroy(&buffer); 53 | 54 | free(string); 55 | return 0; 56 | } 57 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Copyright 2014 Dario Hamidi 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | .POSIX: 17 | 18 | PREFIX=/usr/local 19 | 20 | .c.o: script/compile 21 | script/compile $< $@ 22 | 23 | tmenu: main.o terminal.o textbuffer.o buffer.o menu.o util.o 24 | script/link $@ main.o terminal.o textbuffer.o buffer.o menu.o util.o 25 | 26 | install: tmenu 27 | mkdir -p $(PREFIX)/bin 28 | cp tmenu $(PREFIX)/bin/ 29 | chmod 755 $(PREFIX)/bin/tmenu 30 | mkdir -p $(PREFIX)/share/man/man1 31 | cp tmenu.1 $(PREFIX)/share/man/man1 32 | 33 | run: tmenu 34 | ./tmenu 35 | 36 | main.o: main.c script/compile config.h terminal.h textbuffer.h buffer.h menu.h 37 | 38 | terminal.o: terminal.c terminal.h script/compile 39 | 40 | util.o: util.h util.c script/compile 41 | 42 | textbuffer.o: textbuffer.h textbuffer.c buffer.h buffer.c 43 | 44 | buffer.o: util.o config.h script/compile buffer.h buffer.c 45 | 46 | buffer_test: buffer_test.o buffer.o util.o script/link 47 | script/link $@ buffer_test.o buffer.o util.o 48 | 49 | menu_test: menu_test.o util.o textbuffer.o buffer.o terminal.o script/link 50 | script/link $@ menu_test.o util.o textbuffer.o terminal.o buffer.o 51 | 52 | menu_test.o: menu.c 53 | 54 | menu.o: util.o config.h textbuffer.o buffer.o script/compile menu.h menu.c 55 | 56 | test: script/test buffer_test menu_test 57 | ./menu_test 58 | script/test buffer_test 59 | 60 | clean: 61 | rm -vf tmenu 62 | rm -vf ./*_test ./*.o 63 | rm -vf ./*_actual 64 | -------------------------------------------------------------------------------- /textbuffer.c: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014 Dario Hamidi 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with this program. If not, see . 16 | */ 17 | #include "textbuffer.h" 18 | 19 | static void bufbackward(BUFFER self, int n) { 20 | const char* before = Buffer.before(self); 21 | size_t before_len = Buffer.point(self) - 1; 22 | size_t pos = before_len - 1; 23 | 24 | while (n --> 0) { 25 | if ( (unsigned char)before[pos] <= 0x70) { 26 | // ASCII 27 | Buffer.backward(self, 1); 28 | } else { 29 | // UTF-8 30 | while ( (unsigned char)before[pos] >> 6 == 2) { 31 | pos--; 32 | } 33 | Buffer.backward(self, before_len - pos); 34 | } 35 | } 36 | } 37 | 38 | static void bufforward(BUFFER self, int n) { 39 | const char* after = Buffer.after(self); 40 | 41 | while (n --> 0) { 42 | Buffer.forward(self, 1); 43 | while ( (unsigned char)*(++after) >> 6 == 2) 44 | Buffer.forward(self, 1); 45 | } 46 | } 47 | 48 | static void bufdelete(BUFFER self) { 49 | const char* after = Buffer.after(self); 50 | 51 | Buffer.delete(self); 52 | while ( (unsigned char)*(++after) >> 6 == 2 ) 53 | Buffer.delete(self); 54 | } 55 | 56 | static void bufcput(BUFFER self, int c) { 57 | Buffer.cput(self, c); 58 | } 59 | 60 | static void bufsput(BUFFER self, const char* str) { 61 | Buffer.sput(self, str); 62 | } 63 | 64 | static BUFFER bufnew(size_t hint) { 65 | return Buffer.new(hint); 66 | } 67 | 68 | static void bufdestroy(BUFFER* self) { 69 | Buffer.destroy(self); 70 | } 71 | 72 | static char* bufstring(BUFFER self, char** out, size_t* len) { 73 | return Buffer.string(self, out, len); 74 | } 75 | 76 | static void bufclear(BUFFER self) { 77 | Buffer.clear(self); 78 | } 79 | 80 | static void bufdeletetoend(BUFFER self) { 81 | Buffer.delete_to_end(self); 82 | } 83 | 84 | static void bufdeletetobeginning(BUFFER self) { 85 | Buffer.delete_to_beginning(self); 86 | } 87 | 88 | static size_t bufpoint(BUFFER self) { 89 | return Buffer.point(self); 90 | } 91 | 92 | static const char* bufafter(BUFFER self) { 93 | return Buffer.after(self); 94 | } 95 | 96 | static const char* bufbefore(BUFFER self) { 97 | return Buffer.before(self); 98 | } 99 | 100 | static size_t buflen(BUFFER self) { 101 | return Buffer.length(self); 102 | } 103 | 104 | struct buffer_interface TextBuffer = { 105 | .new = bufnew, 106 | .destroy = bufdestroy, 107 | .length = buflen, 108 | .cput = bufcput, 109 | .sput = bufsput, 110 | .string = bufstring, 111 | .forward = bufforward, 112 | .backward = bufbackward, 113 | .delete = bufdelete, 114 | .clear = bufclear, 115 | .delete_to_end = bufdeletetoend, 116 | .delete_to_beginning = bufdeletetobeginning, 117 | .point = bufpoint, 118 | .after = bufafter, 119 | .before = bufbefore, 120 | }; 121 | -------------------------------------------------------------------------------- /tmenu.1: -------------------------------------------------------------------------------- 1 | .\" Copyright (C), 2014-2016 Dario Hamidi 2 | .\" You may distribute this file under the terms of the GNU Free 3 | .\" Documentation License. 4 | .\" 5 | .\" See http://liw.fi/manpages/ for how to edit this file. 6 | .TH tmenu 1 2014-06-02 7 | .SH NAME 8 | tmenu \- tty menu 9 | .SH SYNOPSIS 10 | .B tmenu 11 | [\fB\-q\fR] 12 | [\fB\-l\fR LINES] 13 | [\fB\-p\fR PROMPT] 14 | .SH DESCRIPTION 15 | .B tmenu 16 | is a dynamic menu for tty devices, which reads a list of 17 | newline\-separated items from stdin. When the user selects an item and 18 | presses Return, the selected item is printed to stdout. Entering text 19 | will narrow the list of items to items that contain the entered text. 20 | 21 | Patterns as recognized by \fBfnmatch(3)\fR are matched as well. The input 22 | string is surrounded with asterisks (\fB*\fR) to have the pattern match 23 | anywhere in the input string. 24 | 25 | Lines that are longer than the terminal is currently wide are truncated. 26 | When a line is truncated, a dollar sign ( 27 | .B $ 28 | ) is displayed in the rightmost column of that line. 29 | .SH OPTIONS 30 | .TP 31 | .BR \-l " " \fILINES\fR 32 | Set the height of the completion list in lines to \fILINES\fR. Defaults 33 | to 3. 34 | .TP 35 | .BR \-p " " \fIPROMPT\fR 36 | Set the prompt shown to the user to \fIPROMPT\fR. Defaults to \fI>>\fR. 37 | .TP 38 | .BR \-q 39 | Be quiet. When quiet, no status line is displayed. Defaults to showing 40 | the status line. 41 | .SH KEY BINDINGS 42 | .B tmenu 43 | uses the following key bindings for editing the text the user entered 44 | and manipulating the selection: 45 | .TP 46 | .BR Return ", " C\-j 47 | Output the currently selected item on stdout and exit. 48 | .TP 49 | .BR C\-n 50 | Select the next item in the list. 51 | .TP 52 | .BR C\-p 53 | Select the previous item in the list. 54 | .TP 55 | .BR C\-a 56 | Move the cursor to the beginning of the line. 57 | .TP 58 | .BR C\-e 59 | Move the cursor to the end of the line. 60 | .TP 61 | .BR C\-u 62 | Delete text until the beginning of the line. 63 | .TP 64 | .BR C\-k 65 | Delete text until the end of the line. 66 | .TP 67 | .BR C\-d 68 | Delete the character under the cursor. 69 | .TP 70 | .BR Backspace 71 | Delete the character before the cursor. 72 | .TP 73 | .BR C\-f 74 | Move the cursor forward by one character. 75 | .TP 76 | .BR C\-b 77 | Move the cursor backward by one character. 78 | .SH FILES 79 | .TP 80 | .IR /dev/tty 81 | .BR tmenu 82 | opens \fI/dev/tty\fR for displaying the menu to the user and reads user 83 | input from this file. Using \fIstdout\fR for display would make it 84 | impossible to use tmenu in pipes. 85 | .SH EXAMPLES 86 | .BR tmenu 87 | is most useful when used together with other programs. Here are some 88 | examples of possible uses: 89 | .TP 90 | .BR "Switching jobs in the shell" 91 | .nf 92 | .RS 93 | 94 | fg %$(jobs | tmenu | awk '{print substr($1,2,1)}') 95 | .RE 96 | .fi 97 | .TP 98 | .BR "Find a file in a git repository" 99 | .nf 100 | .RS 101 | 102 | $EDITOR "$(git ls-files | tmenu)" 103 | .RE 104 | .fi 105 | .TP 106 | .BR "Jump to favourite directory" 107 | .nf 108 | .RS 109 | 110 | cd "$(tmenu < ~/.bookmarks)" 111 | .RE 112 | .fi 113 | .SH "SEE ALSO" 114 | .BR dmenu (1) 115 | .SH BUGS 116 | .BR tmenu 117 | expects input to be encoded as UTF\-8. It will behave unexpectedly 118 | if any other encoding is used. In the future, user input will be 119 | converted to UTF\-8 using 120 | .BR iconv (3). 121 | 122 | .BR tmenu 123 | should handle the arrow keys, the HOME and the END key for navigating 124 | text. 125 | .SH AUTHOR 126 | Dario Hamidi 127 | -------------------------------------------------------------------------------- /terminal.c: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014 Dario Hamidi 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with this program. If not, see . 16 | */ 17 | #include "terminal.h" 18 | 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | 25 | #include "config.h" 26 | #include "util.h" 27 | 28 | struct terminal { 29 | FILE* file; 30 | int fd; 31 | }; 32 | 33 | static const char* dirs[] = { 34 | "\033[%dA", 35 | "\033[%dB", 36 | "\033[%dC", 37 | "\033[%dD", 38 | }; 39 | 40 | enum TERMINAL_DIR { 41 | TERMINAL_DIR_UP, 42 | TERMINAL_DIR_DOWN, 43 | TERMINAL_DIR_RIGHT, 44 | TERMINAL_DIR_LEFT, 45 | }; 46 | 47 | enum TERMINAL_MODE { 48 | TERMINAL_MODE_INTERACTIVE, 49 | TERMINAL_MODE_STANDARD, 50 | }; 51 | 52 | 53 | static TERMINAL termnew(const char* filename) { 54 | TERMINAL term = must_malloc(sizeof(*term)); 55 | int err = 0; 56 | 57 | term->file = fopen(filename, "r+"); 58 | err = errno; 59 | if (term->file == NULL) { 60 | fprintf(stderr, "%s:open:%s: %s\n", PROGRAM_NAME, filename, strerror(err)); 61 | abort(); 62 | } 63 | 64 | term->fd = fileno(term->file); 65 | 66 | 67 | return term; 68 | } 69 | 70 | static TERMINAL_ERROR termsetmode(TERMINAL self, enum TERMINAL_MODE mode) { 71 | struct termios terminal_settings = { 0 }; 72 | 73 | if ( tcgetattr(self->fd, &terminal_settings) == -1 ) { 74 | fprintf(stderr, "%s: failed to get terminal settings: %s\n", 75 | PROGRAM_NAME, strerror(errno)); 76 | return TERMINAL_GET_ATTR_FAIL; 77 | } 78 | 79 | switch (mode) { 80 | case TERMINAL_MODE_INTERACTIVE: 81 | terminal_settings.c_lflag &= ~(ICANON|ECHO); 82 | break; 83 | case TERMINAL_MODE_STANDARD: 84 | terminal_settings.c_lflag |= ICANON|ECHO; 85 | break; 86 | default: 87 | fprintf(stderr, "%s: unknown terminal mode: %X\n", 88 | PROGRAM_NAME, (int)mode); 89 | return TERMINAL_UNKNOWN_MODE; 90 | } 91 | 92 | if ( tcsetattr(self->fd, TCSANOW, &terminal_settings) == -1 ) { 93 | fprintf(stderr, "%s: failed to set terminal settings: %s\n", 94 | PROGRAM_NAME, strerror(errno)); 95 | return TERMINAL_SET_ATTR_FAIL; 96 | } 97 | 98 | return TERMINAL_OK; 99 | } 100 | 101 | static TERMINAL_ERROR terminteractivemode(TERMINAL self) { 102 | return termsetmode(self, TERMINAL_MODE_INTERACTIVE); 103 | } 104 | 105 | static TERMINAL_ERROR termstandardmode(TERMINAL self) { 106 | return termsetmode(self, TERMINAL_MODE_STANDARD); 107 | } 108 | 109 | static void termdestroy(TERMINAL* self) { 110 | TERMINAL term = *self; 111 | termsetmode(term, TERMINAL_MODE_STANDARD); 112 | fclose(term->file); 113 | free(term); 114 | *self = NULL; 115 | } 116 | 117 | static void mov(TERMINAL self, enum TERMINAL_DIR dir, int arg) { 118 | fprintf(self->file, dirs[dir], arg); 119 | } 120 | 121 | static void up(TERMINAL self, int arg) { 122 | mov(self, TERMINAL_DIR_UP, arg); 123 | } 124 | 125 | static void down(TERMINAL self, int arg) { 126 | mov(self, TERMINAL_DIR_DOWN, arg); 127 | } 128 | 129 | static void right(TERMINAL self, int arg) { 130 | mov(self, TERMINAL_DIR_RIGHT, arg); 131 | } 132 | 133 | static void left(TERMINAL self, int arg) { 134 | mov(self, TERMINAL_DIR_LEFT, arg); 135 | } 136 | 137 | static void erase(TERMINAL self, int arg) { 138 | if (arg >= 2) { 139 | fprintf(self->file, "\033[%dK", 2); 140 | } else if (arg <= 0) { 141 | fprintf(self->file, "\033[%dK", 0); 142 | } else { 143 | fprintf(self->file, "\033[%dK", 1); 144 | } 145 | } 146 | 147 | static void col(TERMINAL self, int x) { 148 | fprintf(self->file, "\033[%dG", x); 149 | } 150 | 151 | static void highlight(TERMINAL self, int on) { 152 | fprintf(self->file, "\033[%dm", on ? 7 : 0); 153 | } 154 | 155 | static int termfd(TERMINAL self) { 156 | return self->fd; 157 | } 158 | 159 | static FILE* termfile(TERMINAL self) { 160 | return self->file; 161 | } 162 | 163 | struct terminal_interface Terminal = { 164 | .new = termnew, 165 | .destroy = termdestroy, 166 | 167 | .left = left, 168 | .right = right, 169 | .up = up, 170 | .down = down, 171 | .erase = erase, 172 | .col = col, 173 | .highlight = highlight, 174 | 175 | .file = termfile, 176 | .fd = termfd, 177 | 178 | .interactive_mode = terminteractivemode, 179 | .standard_mode = termstandardmode, 180 | }; 181 | -------------------------------------------------------------------------------- /buffer.c: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014 Dario Hamidi 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with this program. If not, see . 16 | */ 17 | #include "buffer.h" 18 | 19 | #include "util.h" 20 | #include 21 | #include 22 | #include 23 | 24 | struct buffer { 25 | size_t capacity; 26 | size_t gapstart; 27 | size_t gapend; 28 | char* data; 29 | }; 30 | 31 | static BUFFER bufnew(size_t hint) { 32 | BUFFER buf = NULL; 33 | 34 | if (hint < 1024) { 35 | hint = 1024; 36 | } 37 | 38 | buf = must_malloc(sizeof(*buf)); 39 | 40 | buf->capacity = hint; 41 | buf->data = must_malloc(hint); 42 | buf->gapstart = 1; 43 | buf->gapend = hint - 1; 44 | 45 | return buf; 46 | } 47 | 48 | static const char* bufbefore(BUFFER self) { 49 | return self->data; 50 | } 51 | 52 | static const char* bufafter(BUFFER self) { 53 | return self->data + self->gapend - 1; 54 | } 55 | 56 | static void bufdestroy(BUFFER* self) { 57 | if (self == NULL) { 58 | return; 59 | } 60 | 61 | free( (*self)->data ); 62 | free( *self ); 63 | 64 | *self = NULL; 65 | } 66 | 67 | static int bufisfull(BUFFER self) { 68 | return (self->gapstart + 1) == self->gapend; 69 | } 70 | 71 | static void bufexpand(BUFFER self) { 72 | char *new_data; 73 | char *old_data = self->data; 74 | size_t new_capacity = self->capacity + BUFFER_GROW_BY; 75 | if (new_capacity <= self->capacity) { 76 | fprintf(stderr, "%s: cannot expand buffer: integer overflow\n", 77 | PROGRAM_NAME); 78 | abort(); 79 | } 80 | 81 | new_data = must_malloc(new_capacity); 82 | memcpy(new_data, bufbefore(self), self->gapstart); 83 | memcpy(new_data + (new_capacity - self->gapend), 84 | bufafter(self), 85 | self->capacity - self->gapend); 86 | 87 | self->data = new_data; 88 | self->gapend = new_capacity - self->gapend; 89 | 90 | free(old_data); 91 | } 92 | 93 | static void bufcput(BUFFER self, int c) { 94 | unsigned char byte = (unsigned char)c; 95 | if (bufisfull(self)) { 96 | bufexpand(self); 97 | } 98 | 99 | self->data[self->gapstart - 1] = byte; 100 | self->data[self->gapstart] = 0; 101 | self->gapstart++; 102 | } 103 | 104 | static void bufsput(BUFFER self, const char* str) { 105 | while (*str) { 106 | bufcput(self, *str++); 107 | } 108 | } 109 | 110 | static char* bufstring(BUFFER self, char** buf, size_t* bufsize) { 111 | size_t new_size = self->gapstart + (self->capacity - self->gapend); 112 | 113 | if ( (*bufsize) < new_size || (*buf) == NULL ) { 114 | *bufsize = new_size; 115 | free(*buf); 116 | *buf = must_malloc(new_size); 117 | } 118 | 119 | strcpy( (*buf), bufbefore(self) ); 120 | strcpy( (*buf) + self->gapstart - 1 , bufafter(self) ); 121 | 122 | return *buf; 123 | } 124 | 125 | static void bufforward(BUFFER self, int by) { 126 | while (by --> 0 && self->gapend < self->capacity - 1) { 127 | self->data[self->gapstart - 1] = self->data[self->gapend - 1]; 128 | self->data[self->gapend - 1] = 0; 129 | self->gapstart++; 130 | self->gapend++; 131 | } 132 | } 133 | 134 | static void bufbackward(BUFFER self, int by) { 135 | while (by --> 0 && self->gapstart > 1) { 136 | self->data[self->gapend - 2] = self->data[self->gapstart - 2]; 137 | self->data[self->gapstart - 2] = 0; 138 | self->gapstart--; 139 | self->gapend--; 140 | } 141 | } 142 | 143 | 144 | static size_t bufpoint(BUFFER self) { 145 | return self->gapstart; 146 | } 147 | 148 | static void bufdelete(BUFFER self) { 149 | if ( self->gapend < self->capacity - 1 ) { 150 | self->data[self->gapend - 1] = 0; 151 | self->gapend++; 152 | } 153 | } 154 | 155 | static void bufclear(BUFFER self) { 156 | memset(self->data, 0, self->capacity); 157 | self->gapstart = 1; 158 | self->gapend = self->capacity - 1; 159 | } 160 | 161 | static void bufdeletetoend(BUFFER self) { 162 | char* after = (char*)bufafter(self); 163 | memset(after, 0, strlen(after)); 164 | self->gapend = self->capacity - 1; 165 | } 166 | 167 | static void bufdeletetobeginning(BUFFER self) { 168 | char* before = (char*)bufbefore(self); 169 | memset(before, 0, strlen(before)); 170 | self->gapstart = 1; 171 | } 172 | 173 | static size_t buflen(BUFFER self) { 174 | return self->capacity - (self->gapend - self->gapstart); 175 | } 176 | 177 | struct buffer_interface Buffer = { 178 | .new = bufnew, 179 | .destroy = bufdestroy, 180 | .length = buflen, 181 | .cput = bufcput, 182 | .sput = bufsput, 183 | .string = bufstring, 184 | .forward = bufforward, 185 | .backward = bufbackward, 186 | .before = bufbefore, 187 | .after = bufafter, 188 | .point = bufpoint, 189 | .delete = bufdelete, 190 | .clear = bufclear, 191 | .delete_to_end = bufdeletetoend, 192 | .delete_to_beginning = bufdeletetobeginning, 193 | }; 194 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/dhamidi/tmenu.svg?branch=master)](https://travis-ci.org/dhamidi/tmenu) 2 | 3 | # Description 4 | 5 | ![git-select-file](git-select-file.gif) 6 | 7 | **tmenu** is a dynamic menu for tty devices, which reads a list of 8 | newline-separated items from stdin. When the user selects an item and 9 | presses Return, the selected item is printed to stdout. Entering text 10 | will narrow the list of items to items that contain the entered text. 11 | 12 | # Non/Features 13 | 14 | - written in (almost POSIX conforming) C99 15 | - emacs-like key bindings 16 | - does not use ncurses 17 | 18 | **tmenu** is *almost* conforming to 19 | [POSIX.1-2008](http://pubs.opengroup.org/onlinepubs/9699919799/). The 20 | only non-standard functionality currently used is: 21 | 22 | - the `TIOCGWINSZ` constant as `request` parameter when calling 23 | [ioctl](http://pubs.opengroup.org/onlinepubs/9699919799/functions/ioctl.html). 24 | This constant seems to be the least invasive deviation from the standard 25 | to query the current size of the terminal. 26 | 27 | - the `SIGWINCH` constant as `sig` parameter when calling `signal`. It 28 | is used for listening to resize events of the terminal window. The 29 | existence of `SIGWINCH` is not required by the standard. See 30 | [](http://pubs.opengroup.org/onlinepubs/9699919799/basedefs/signal.h.html). 31 | 32 | # Installation 33 | 34 | make 35 | sudo make install 36 | 37 | `make install` installs to `/usr/local` by default. If you want to 38 | install tmenu to a different location, set `PREFIX`: 39 | 40 | make install PREFIX=$HOME/local 41 | 42 | If you want to change compilation and linking flags, modify 43 | [script/compile](script/compile) and [script/link](script/link). Having 44 | the options in separate files causes `make` to recompile the program if 45 | options change. 46 | 47 | # Options 48 | 49 | - `-l LINES`: Set the height of the completion list in lines to LINES. Defaults to 3. 50 | - `-p PROMPT`: Set the prompt shown to the user to PROMPT. Defaults to `>>`. 51 | - `-q`: Be quiet. When quiet, no status line is displayed. Defaults to 52 | showing the status line. 53 | 54 | # Key Bindings 55 | 56 | - `Return`, `C-j`: Output the currently selected item on stdout and exit. 57 | - `C-n`: Select the next item in the list. 58 | - `C-p`: Select the previous item in the list. 59 | - `C-a`: Move the cursor to the beginning of the line. 60 | - `C-e`: Move the cursor to the end of the line. 61 | - `C-u`: Delete text until the beginning of the line. 62 | - `C-k`: Delete text until the end of the line. 63 | - `C-d`: Delete the character under the cursor. 64 | - `Backspace`: Delete the character before the cursor. 65 | - `C-f`: Move the cursor forward by one character. 66 | - `C-b`: Move the cursor backward by one character. 67 | 68 | # Uses 69 | 70 | ## Switch git branches 71 | 72 | Add an git alias with the following command to switch git branches using 73 | `tmenu`: 74 | 75 | git alias br "!git ls-remote -h . | awk '{print(substr($NF, 12))}' | tmenu | xargs git checkout" 76 | 77 | Then use `git br` to interactively switch the git branch. 78 | 79 | ## Switching between background jobs in the shell 80 | 81 | Most shells support job control (suspending tasks by pressing `C-z`, 82 | resuming it later using the `fg` command). The following shell function 83 | overrides the `fg` builtin to interactively select the job to resume. 84 | 85 | **Note**: This does not work when dash, or bash in POSIX mode, is used. 86 | In these cases `jobs` is executed in a subshell, always reporting no 87 | jobs. See 88 | [this bug](https://bugs.launchpad.net/ubuntu/+source/dash/+bug/243406). 89 | 90 | fg() { 91 | command fg %$(jobs | tmenu | xargs -I% expr match % '\[\([[:digit:]]\+\)\]') 92 | } 93 | 94 | ## Edit file in current git repository 95 | 96 | The following shell command install `git edit` as an alias for selecting 97 | a file from the current git repository and opening it with `$EDITOR`. 98 | 99 | git alias edit '!$EDITOR "$(git ls-files | tmenu)"' 100 | 101 | ## Managing per-project settings 102 | 103 | If you are frequently working on different projects, it can be useful to 104 | maintain different settings for your shell based on the project you are 105 | in. Add the following to `$HOME/.${SHELL}rc`: 106 | 107 | if [ -e ./.projectile.sh ]; then 108 | . ./.projectile.sh 109 | fi 110 | 111 | When a new shell is started, it executes the contents of 112 | `.projectile.sh` in the current directory (if such a file exists). 113 | 114 | Switching a project can then be done using the following shell function: 115 | 116 | projectile() { 117 | PROJECTILE=${PROJECTILE:-$HOME/projects} 118 | (cd $PROJECTILE/$(printf "%s\n" $PROJECTILE/* | 119 | xargs -L 1 basename | 120 | tmenu) 121 | $SHELL) 122 | } 123 | 124 | Set `PROJECTILE` to the directory containing your projects (default: 125 | `$HOME/projects`), then switch to a project using the `projectile` 126 | command. Place `.projectile.sh` files with the project specific shell 127 | settings into the project directories. Things that might be interesting 128 | to do: 129 | 130 | - `PATH` to contain the `bin` or `script` directory of the current 131 | project. This makes it easy to override certain shell commands. 132 | 133 | - Use `.projectile.sh` to set the version of the runtime used for the 134 | project (e.g. using [chruby](https://github.com/postmodern/chruby) or 135 | [virtualenv](http://virtualenv.org) for Python). 136 | 137 | - Change the prompt to highlight that you are working on a specific 138 | project now. 139 | 140 | - Setup a project-specific [tmux](https://github.com/Thomasadam/tmux) 141 | configuration 142 | 143 | # License 144 | 145 | Copyright 2014-2016 Dario Hamidi 146 | 147 | This program is free software: you can redistribute it and/or modify 148 | it under the terms of the GNU General Public License as published by 149 | the Free Software Foundation, either version 3 of the License, or 150 | (at your option) any later version. 151 | 152 | This program is distributed in the hope that it will be useful, 153 | but WITHOUT ANY WARRANTY; without even the implied warranty of 154 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 155 | GNU General Public License for more details. 156 | 157 | You should have received a copy of the GNU General Public License 158 | along with this program. If not, see . 159 | -------------------------------------------------------------------------------- /main.c: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014 Dario Hamidi 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with this program. If not, see . 16 | */ 17 | #define _POSIX_C_SOURCE 200809L 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | 28 | #include "config.h" 29 | #include "terminal.h" 30 | #include "buffer.h" 31 | #include "menu.h" 32 | 33 | enum key { 34 | KEY_CTRL_A = 0x1, 35 | KEY_CTRL_B = 0x2, 36 | KEY_CTRL_C = 0x3, 37 | KEY_CTRL_D = 0x4, 38 | KEY_CTRL_E = 0x5, 39 | KEY_CTRL_F = 0x6, 40 | KEY_CTRL_G = 0x7, 41 | KEY_CTRL_H = 0x8, 42 | KEY_CTRL_I = 0x9, 43 | KEY_CTRL_J = 0xa, 44 | KEY_CTRL_K = 0xb, 45 | KEY_CTRL_L = 0xc, 46 | KEY_CTRL_M = 0xd, 47 | KEY_CTRL_N = 0xe, 48 | KEY_CTRL_O = 0xf, 49 | KEY_CTRL_P = 0x10, 50 | KEY_CTRL_Q = 0x11, 51 | KEY_CTRL_R = 0x12, 52 | KEY_CTRL_S = 0x13, 53 | KEY_CTRL_T = 0x14, 54 | KEY_CTRL_U = 0x15, 55 | KEY_CTRL_V = 0x16, 56 | KEY_CTRL_X = 0x17, 57 | KEY_CTRL_Y = 0x18, 58 | KEY_CTRL_Z = 0x19, 59 | KEY_SPACE = 0x20, 60 | KEY_ENTER = 0xa, 61 | KEY_BACKSPACE = 0x7f, 62 | }; 63 | 64 | typedef int (*action_fn)(MENU); 65 | 66 | static int action_select_next(MENU menu) { 67 | Menu.select_next(menu); 68 | return 1; 69 | } 70 | 71 | static int action_select_prev(MENU menu) { 72 | Menu.select_prev(menu); 73 | return 1; 74 | } 75 | 76 | static int action_forward(MENU menu) { 77 | TextBuffer.forward(Menu.buffer(menu), 1); 78 | return 1; 79 | } 80 | 81 | static int action_backward(MENU menu) { 82 | TextBuffer.backward(Menu.buffer(menu), 1); 83 | return 1; 84 | } 85 | 86 | static int action_delete_char(MENU menu) { 87 | TextBuffer.delete(Menu.buffer(menu)); 88 | Menu.match(menu); 89 | return 1; 90 | } 91 | 92 | static int action_delete_char_before(MENU menu) { 93 | BUFFER buf = Menu.buffer(menu); 94 | size_t point_before = TextBuffer.point(buf); 95 | size_t point_after = point_before; 96 | 97 | action_backward(menu); 98 | 99 | point_after = TextBuffer.point(buf); 100 | 101 | while ( point_after++ < point_before ) 102 | TextBuffer.delete(Menu.buffer(menu)); 103 | 104 | Menu.match(menu); 105 | return 1; 106 | } 107 | 108 | static int action_delete_to_beginning(MENU menu) { 109 | TextBuffer.delete_to_beginning(Menu.buffer(menu)); 110 | Menu.match(menu); 111 | return 1; 112 | } 113 | 114 | static int action_delete_to_end(MENU menu) { 115 | TextBuffer.delete_to_end(Menu.buffer(menu)); 116 | Menu.match(menu); 117 | return 1; 118 | } 119 | 120 | static int action_beginning_of_line(MENU menu) { 121 | TextBuffer.backward(Menu.buffer(menu), 10000); 122 | return 1; 123 | } 124 | 125 | static int action_end_of_line(MENU menu) { 126 | TextBuffer.forward(Menu.buffer(menu), 10000); 127 | return 1; 128 | } 129 | 130 | static int action_confirm(MENU menu) { 131 | fprintf(stdout, "%s\n", Menu.selection(menu)); 132 | return 0; 133 | } 134 | 135 | static action_fn KEYMAP[256] = { 136 | [KEY_CTRL_P] = action_select_prev, 137 | [KEY_CTRL_N] = action_select_next, 138 | [KEY_CTRL_A] = action_beginning_of_line, 139 | [KEY_CTRL_E] = action_end_of_line, 140 | [KEY_CTRL_F] = action_forward, 141 | [KEY_CTRL_B] = action_backward, 142 | [KEY_CTRL_U] = action_delete_to_beginning, 143 | [KEY_CTRL_K] = action_delete_to_end, 144 | [KEY_CTRL_D] = action_delete_char, 145 | [KEY_ENTER] = action_confirm, 146 | [KEY_BACKSPACE] = action_delete_char_before, 147 | }; 148 | 149 | static int getitem(char* buf, size_t len, FILE* in) { 150 | size_t i = 0; 151 | int c; 152 | 153 | memset(buf, 0, len); 154 | 155 | for ( c = fgetc(in); c != EOF; c = fgetc(in) ) { 156 | if (c == '\n') { break; } 157 | 158 | if ( i < len - 1 ) { 159 | buf[i++] = c; 160 | } 161 | } 162 | 163 | return c != EOF; 164 | } 165 | 166 | /* global instances used for signal handling */ 167 | static MENU CURRENT_MENU = NULL; 168 | static TERMINAL CURRENT_TERMINAL = NULL; 169 | 170 | static void handle_interrupt(int signum) { 171 | Terminal.destroy(&CURRENT_TERMINAL); 172 | exit(0); 173 | } 174 | 175 | static void update_current_menu_size(int signum) { 176 | struct winsize winsize = { 0 }; 177 | int fd = Terminal.fd(CURRENT_TERMINAL); 178 | 179 | if (ioctl(fd, TIOCGWINSZ, &winsize) == -1) { 180 | perror(PROGRAM_NAME ":update_current_menu_size:ioctl"); 181 | return; 182 | } 183 | 184 | Menu.set_max_height(CURRENT_MENU, winsize.ws_row); 185 | Menu.set_max_width(CURRENT_MENU, winsize.ws_col); 186 | Menu.display(CURRENT_MENU, CURRENT_TERMINAL); 187 | } 188 | 189 | static void initsignals(struct sigaction *act ) { 190 | act->sa_handler = update_current_menu_size; 191 | act->sa_flags = SA_RESTART; 192 | sigemptyset(&act->sa_mask); 193 | if (sigaction(SIGWINCH, act, NULL) == -1) { 194 | perror(PROGRAM_NAME "initsignals:sigaction"); 195 | return; 196 | } 197 | 198 | act->sa_handler = handle_interrupt; 199 | if (sigaction(SIGINT, act, NULL) == -1) { 200 | perror(PROGRAM_NAME "initsignals:sigaction"); 201 | } 202 | } 203 | 204 | int main(int argc, char** argv) { 205 | char item[MENU_ITEM_MAX_SIZE] = { 0 }; 206 | char buf[8] = { 0 }; 207 | action_fn action = NULL; 208 | int opt, fd_in; 209 | struct sigaction sa; 210 | 211 | CURRENT_TERMINAL = Terminal.new("/dev/tty"); 212 | CURRENT_MENU = Menu.new(); 213 | 214 | fd_in = Terminal.fd(CURRENT_TERMINAL); 215 | 216 | while ( (opt = getopt(argc, argv, "p:l:q")) != -1 ) { 217 | switch (opt) { 218 | case 'p': 219 | Menu.set_prompt(CURRENT_MENU, optarg); 220 | break; 221 | case 'l': 222 | Menu.set_height(CURRENT_MENU, atoi(optarg)); 223 | break; 224 | case 'q': 225 | Menu.enable_status_line(CURRENT_MENU, 0); 226 | break; 227 | default: 228 | exit(EXIT_FAILURE); 229 | } 230 | } 231 | 232 | initsignals(&sa); 233 | update_current_menu_size(0); 234 | 235 | while ( getitem(item, MENU_ITEM_MAX_SIZE, stdin) ) { 236 | Menu.add_item(CURRENT_MENU, item); 237 | } 238 | 239 | if (Terminal.interactive_mode(CURRENT_TERMINAL) != TERMINAL_OK) { 240 | goto exit; 241 | } 242 | 243 | Menu.match(CURRENT_MENU); 244 | 245 | /* avoid clobbering the user's prompt */ 246 | /* fputs("\n", Terminal.file(CURRENT_TERMINAL)); */ 247 | 248 | Menu.display(CURRENT_MENU, CURRENT_TERMINAL); 249 | 250 | while ( read(fd_in, &buf, 8) != 0 ) { 251 | buf[7] = 0; 252 | action = KEYMAP[ (unsigned char)buf[0] ]; 253 | 254 | if (!action && (unsigned char)buf[0] >= KEY_SPACE) { 255 | TextBuffer.sput(Menu.buffer(CURRENT_MENU), buf); 256 | Menu.match(CURRENT_MENU); 257 | } else { 258 | if (action && ! action(CURRENT_MENU) ) { 259 | goto exit; 260 | } 261 | } 262 | 263 | Menu.display(CURRENT_MENU, CURRENT_TERMINAL); 264 | memset(&buf, 0, 8); 265 | } 266 | 267 | exit: 268 | Terminal.destroy(&CURRENT_TERMINAL); 269 | Menu.destroy(&CURRENT_MENU); 270 | 271 | return EXIT_SUCCESS; 272 | } 273 | -------------------------------------------------------------------------------- /menu.c: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014 Dario Hamidi 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with this program. If not, see . 16 | */ 17 | #define _POSIX_C_SOURCE 200809L 18 | #include "menu.h" 19 | 20 | #include "util.h" 21 | #include "config.h" 22 | #include 23 | #include 24 | #include 25 | #include 26 | 27 | struct menu { 28 | char** items; 29 | size_t capacity; 30 | size_t len; 31 | size_t* matches; 32 | size_t curmatch; 33 | size_t cursor; 34 | char* prompt; 35 | size_t prompt_len; 36 | size_t height; 37 | size_t max_height; 38 | size_t max_width; 39 | size_t y_offset; 40 | char* matchbuf; 41 | size_t matchbuf_len; 42 | BUFFER input; 43 | int status_line_enabled; 44 | }; 45 | 46 | static MENU menunew(void) { 47 | MENU menu = must_malloc(sizeof(*menu)); 48 | 49 | menu->capacity = MENU_ITEM_MAX_SIZE / 2; 50 | menu->len = 0; 51 | 52 | menu->items = must_malloc(sizeof(*menu->items) * menu->capacity); 53 | menu->matches = must_malloc(sizeof(*menu->matches) * menu->capacity); 54 | menu->cursor = 0; 55 | menu->prompt = strdup(MENU_DEFAULT_PROMPT); 56 | menu->prompt_len = strlen(MENU_DEFAULT_PROMPT); 57 | menu->height = MENU_DEFAULT_HEIGHT; 58 | 59 | menu->max_height = 0; 60 | menu->max_width = 0; 61 | 62 | menu->matchbuf = NULL; 63 | menu->matchbuf_len = 0; 64 | menu->input = TextBuffer.new(0); 65 | 66 | menu->status_line_enabled = MENU_DEFAULT_STATUS_LINE; 67 | 68 | return menu; 69 | } 70 | 71 | static void menudestroy(MENU* self) { 72 | size_t i; 73 | 74 | if (self == NULL) { 75 | return; 76 | } 77 | 78 | for (i = 0; i < (*self)->len; i++) { 79 | free( (*self)->items[i] ); 80 | } 81 | 82 | TextBuffer.destroy( & (*self)->input ); 83 | 84 | free( (*self)->items ); 85 | free( (*self)->matches ); 86 | free( (*self)->prompt ); 87 | free( (*self)->matchbuf ); 88 | free( *self ); 89 | 90 | *self = NULL; 91 | } 92 | 93 | static int canprint(MENU self) { 94 | return self->max_height - self->y_offset - 1; 95 | } 96 | 97 | static void prepare_matches(MENU self) { 98 | memset(self->matches, 0, sizeof(*self->matches) * self->len); 99 | TextBuffer.string(self->input, &self->matchbuf, &self->matchbuf_len); 100 | self->curmatch = 0; 101 | self->cursor = 0; 102 | } 103 | 104 | static const char* menuselection(MENU self) { 105 | if (self->len > 0) { 106 | return self->items[self->matches[self->cursor]]; 107 | } else { 108 | prepare_matches(self); 109 | return self->matchbuf; 110 | } 111 | } 112 | 113 | static void menusetprompt(MENU self, const char* prompt) { 114 | size_t newlen = strlen(prompt); 115 | size_t oldlen = strlen(self->prompt); 116 | 117 | if (oldlen >= newlen) { 118 | memset(self->prompt, 0, oldlen); 119 | strncpy(self->prompt, prompt, newlen + 1); /* trailing 0 byte */ 120 | } else { 121 | free(self->prompt); 122 | self->prompt = strdup(prompt); 123 | } 124 | 125 | self->prompt_len = newlen; 126 | } 127 | 128 | static void menusetheight(MENU self, int height) { 129 | if (height < 0) { 130 | height = height * -1; 131 | } 132 | 133 | self->height = height; 134 | } 135 | 136 | static int menugrow(MENU self) { 137 | size_t new_capacity = self->capacity + MENU_GROW_BY; 138 | size_t i; 139 | char** new_items = NULL; 140 | size_t* new_matches = NULL; 141 | 142 | if (self->capacity >= MENU_MAX_ITEMS) { 143 | fprintf(stderr, "%s: cannot hold more than %d items.\n", PROGRAM_NAME, MENU_MAX_ITEMS); 144 | return 0; 145 | } 146 | 147 | new_items = must_malloc(sizeof(*new_items) * new_capacity); 148 | new_matches = must_malloc(sizeof(*new_matches) * new_capacity); 149 | 150 | for (i = 0; i < self->len; i++) { 151 | new_items[i] = self->items[i]; 152 | new_matches[i] = self->matches[i]; 153 | } 154 | 155 | free( self->items ); 156 | free( self->matches ); 157 | 158 | self->capacity = new_capacity; 159 | self->items = new_items; 160 | self->matches = new_matches; 161 | 162 | return 1; 163 | } 164 | 165 | static void menuadditem(MENU self, const char* item) { 166 | if (self->len == self->capacity) { 167 | if (!menugrow(self)) { 168 | fprintf(stderr, "%s: ignoring item %s\n", PROGRAM_NAME, item); 169 | return; 170 | } 171 | } 172 | 173 | self->items[self->len++] = strdup(item); 174 | } 175 | 176 | static void menuselectnext(MENU self) { 177 | if (self->cursor < self->curmatch - 1) { 178 | self->cursor++; 179 | } 180 | } 181 | 182 | static void menuselectprev(MENU self) { 183 | if (self->cursor > 0) { 184 | self->cursor--; 185 | } 186 | } 187 | 188 | static void addmatch(MENU self, size_t index) { 189 | self->matches[self->curmatch++] = index; 190 | } 191 | 192 | static char* pattern(MENU self) { 193 | size_t pattern_len = 3 + self->matchbuf_len; 194 | char* pattern = must_malloc(pattern_len); 195 | snprintf(pattern, pattern_len, "*%s*", self->matchbuf); 196 | return pattern; 197 | } 198 | 199 | static void menumatch(MENU self) { 200 | size_t i = 0; 201 | prepare_matches(self); 202 | char* searchpattern = pattern(self); 203 | for (i = 0; i < self->len; i++) { 204 | if ( strstr(self->items[i], self->matchbuf) != NULL ) { 205 | addmatch(self, i); 206 | } else if ( fnmatch(searchpattern, self->items[i], 0) == 0 ) { 207 | addmatch(self, i); 208 | } 209 | } 210 | free( searchpattern ); 211 | } 212 | 213 | static BUFFER menubuffer(MENU self) { 214 | return self->input; 215 | } 216 | 217 | static void resetdisplay(MENU self, TERMINAL term) { 218 | size_t i; 219 | size_t n = 0; 220 | if (self->y_offset >= self->max_height) { 221 | n = self->max_height; 222 | } else { 223 | n = self->y_offset; 224 | }; 225 | 226 | for (i = 0; i < n; i++) { 227 | Terminal.erase(term, 2); 228 | Terminal.up(term, 1); 229 | } 230 | } 231 | 232 | static void displayprompt(MENU self, TERMINAL term) { 233 | const char* lbuffer = TextBuffer.before(self->input); 234 | const char* inputpos = TextBuffer.after(self->input); 235 | size_t utf8bytes = 1; 236 | size_t linelen = TextBuffer.length(self->input) + self->prompt_len + 1; 237 | FILE* out = Terminal.file(term); 238 | 239 | resetdisplay(self, term); 240 | Terminal.erase(term, 2); /* whole line */ 241 | Terminal.col(term, 0); 242 | 243 | fprintf(out, "%s %s", self->prompt, lbuffer); 244 | 245 | Terminal.highlight(term, 1); 246 | if ( inputpos[0] ) { 247 | fprintf(out, "%c", inputpos[0]); 248 | if ( (unsigned char)inputpos[0] > 0x70 ) { 249 | // UTF-8 250 | while ( (unsigned char)inputpos[utf8bytes] >> 6 == 2) { 251 | fprintf(out, "%c", inputpos[utf8bytes]); 252 | utf8bytes++; 253 | } 254 | } 255 | } else { 256 | fprintf(out, " "); 257 | } 258 | Terminal.highlight(term, 0); 259 | 260 | fprintf(out, "%s\n", inputpos + utf8bytes); 261 | self->y_offset = 1 + linelen / self->max_width; 262 | } 263 | 264 | static void displaymatch(MENU self, TERMINAL term, size_t i, int selected) { 265 | FILE* out = Terminal.file(term); 266 | const char* text = self->items[self->matches[i]]; 267 | size_t c; 268 | 269 | Terminal.erase(term, 2); Terminal.col(term, 0); 270 | 271 | if (i < self->curmatch) { 272 | if (selected) Terminal.highlight(term, 1); 273 | for (c = 1; text[c - 1] && canprint(self) > 1; c++) { 274 | fputc(text[c - 1], out); 275 | if ( (c+1) % self->max_width == 0) { 276 | fputc('$', out); 277 | break; 278 | } 279 | } 280 | if (selected) Terminal.highlight(term, 0); 281 | fputc('\n', out); 282 | } else { 283 | fprintf(out,"\n"); 284 | } 285 | } 286 | 287 | static size_t height_for_matches(MENU self) { 288 | size_t offset = 1 /* prompt */ 289 | + 1 /* trailing newline */ 290 | + (self->status_line_enabled != 0); 291 | 292 | if (self->height > self->max_height - offset) { 293 | return self->max_height - offset; 294 | } else { 295 | return self->height; 296 | } 297 | } 298 | 299 | static void displaymatches(MENU self, TERMINAL term) { 300 | size_t height = height_for_matches(self); 301 | size_t page = self->cursor / height; 302 | size_t item = self->cursor % height; 303 | size_t i; 304 | 305 | for (i = 0; i < height && canprint(self) > 1; i++) { 306 | displaymatch(self, term, (page * height) + i, i == item ); 307 | self->y_offset++; 308 | } 309 | } 310 | 311 | static void displayposition(MENU self, TERMINAL term) { 312 | FILE* out = Terminal.file(term); 313 | if (!canprint(self)) { return; } 314 | if (self->status_line_enabled) { 315 | Terminal.erase(term, 2); Terminal.col(term, 0); 316 | fprintf(out,"[%ld/%ld match(es)]\n", 317 | (long int)self->cursor + 1, (long int)self->curmatch); 318 | self->y_offset++; 319 | } 320 | } 321 | 322 | static void menudisplay(MENU self, TERMINAL term) { 323 | displayprompt(self, term); 324 | if (!canprint(self)) { return; } 325 | if (self->len > 0) { 326 | displaymatches(self, term); 327 | displayposition(self, term); 328 | } 329 | } 330 | 331 | static void menusetmaxwidth(MENU self, int width) { 332 | self->max_width = width; 333 | } 334 | 335 | static void menusetmaxheight(MENU self, int height) { 336 | self->max_height = height; 337 | } 338 | 339 | static void menuenablestatusline(MENU self, int enabled) { 340 | self->status_line_enabled = enabled; 341 | } 342 | 343 | struct menu_interface Menu = { 344 | .new = menunew, 345 | .destroy = menudestroy, 346 | .set_prompt = menusetprompt, 347 | .set_height = menusetheight, 348 | .add_item = menuadditem, 349 | .select_next = menuselectnext, 350 | .select_prev = menuselectprev, 351 | .selection = menuselection, 352 | .enable_status_line = menuenablestatusline, 353 | .display = menudisplay, 354 | .match = menumatch, 355 | .buffer = menubuffer, 356 | .set_max_width = menusetmaxwidth, 357 | .set_max_height = menusetmaxheight, 358 | }; 359 | --------------------------------------------------------------------------------