textselect

Interactively select lines and pipe it to a command
Log | Files | Refs | README | LICENSE

textselect.c (8129B)


      1 #include "arg.h"
      2 
      3 #include <errno.h>
      4 #include <fcntl.h>
      5 #include <ncurses.h>
      6 #include <stdio.h>
      7 #include <stdlib.h>
      8 #include <string.h>
      9 #include <sys/wait.h>
     10 #include <unistd.h>
     11 
     12 #define READBUFFER 1024
     13 #define BUFFERGROW 512
     14 #define PREFIX     16
     15 
     16 #define USAGE "Usage: %s [-hnsSv0] [-o output] <input> [command ...]\n"
     17 
     18 #define NORETURN  __attribute__((noreturn))
     19 #define MAX(a, b) ((a) > (b) ? (a) : (b))
     20 
     21 
     22 struct line {
     23 	char *content;
     24 	int   length;
     25 	bool  selected;
     26 };
     27 
     28 
     29 static char *argv0           = NULL;
     30 static bool  selected_invert = false;
     31 static bool  keep_empty      = false;
     32 static int   prefixlen       = 0;
     33 
     34 #include "config.h"
     35 
     36 static void die(const char *message) {
     37 	fprintf(stderr, "error: %s: %s\n", message, strerror(errno));
     38 	exit(EXIT_FAILURE);
     39 }
     40 
     41 static void help(void) {
     42 	fprintf(stderr,
     43 	        USAGE
     44 	        "Interactively select lines from a text file and optionally execute a command with the selected lines.\n"
     45 	        "\n"
     46 	        "Options:\n"
     47 	        "  -h              Display this help message and exit\n"
     48 	        "  -n              Keep empty lines which are not selectable\n"
     49 	        "  -o output       Specify an output file to save the selected lines\n"
     50 	        "  -s              Characters prepend to a selected line\n"
     51 	        "  -S              Characters prepend to a unselected line\n"
     52 	        "  -v              Invert the selection of lines\n"
     53 	        "  -0              Print selected lines delimited by a NUL-character\n"
     54 	        "\n"
     55 	        "Navigation and selection keys:\n"
     56 	        "  UP, LEFT        Move the cursor up\n"
     57 	        "  DOWN, RIGHT     Move the cursor down\n"
     58 	        "  v               Invert the selection of lines\n"
     59 	        "  SPACE           Select or deselect the current line\n"
     60 	        "  ENTER, q        Quit the selection interface\n"
     61 	        "\n"
     62 	        "Examples:\n"
     63 	        "  textselect -o output.txt input.txt\n"
     64 	        "  textselect input.txt sort\n",
     65 	        argv0);
     66 }
     67 
     68 static void usage(int exitcode) {
     69 	fprintf(stderr, USAGE, argv0);
     70 	exit(exitcode);
     71 }
     72 
     73 static void drawscreen(int height, int current_line, int head_line, struct line *lines, int lines_count) {
     74 	int width = getmaxx(stdscr);
     75 
     76 	werase(stdscr);
     77 	for (int i = 0; i < height; i++) {
     78 		if (i >= lines_count - head_line - 1) {
     79 			mvwprintw(stdscr, i, 0, "~");
     80 			continue;
     81 		}
     82 		if (lines[head_line + i].selected != selected_invert) {
     83 			mvwprintw(stdscr, i, 0, "%s", selected);
     84 			wattron(stdscr, A_BOLD);
     85 		} else {
     86 			mvwprintw(stdscr, i, 0, "%s", unselected);
     87 		}
     88 
     89 		if ((head_line + i) == current_line)
     90 			wattron(stdscr, A_REVERSE);
     91 
     92 		if (lines[head_line + i].length > width) {
     93 			mvwprintw(stdscr, i, prefixlen, "%.*s...", width - 3, lines[head_line + i].content);
     94 		} else {
     95 			mvwprintw(stdscr, i, prefixlen, "%s", lines[head_line + i].content);
     96 		}
     97 
     98 		wattroff(stdscr, A_REVERSE | A_BOLD);
     99 	}
    100 
    101 	wrefresh(stdscr);
    102 }
    103 
    104 static void handlescreen(struct line *lines, int lines_count) {
    105 	bool quit         = false;
    106 	int  height       = 0;
    107 	int  current_line = 0;
    108 	int  head_line    = 0;
    109 
    110 	initscr();
    111 	cbreak();
    112 	noecho();
    113 	keypad(stdscr, TRUE);
    114 
    115 	height = getmaxy(stdscr);
    116 	drawscreen(height, current_line, head_line, lines, lines_count);
    117 
    118 	while (!quit) {
    119 		height = getmaxy(stdscr);
    120 
    121 		switch (getch()) {
    122 			case KEY_UP:
    123 			case KEY_LEFT:
    124 				if (current_line > 0) {
    125 					current_line--;
    126 					if (current_line < head_line)
    127 						head_line--;
    128 				}
    129 				break;
    130 			case KEY_DOWN:
    131 			case KEY_RIGHT:
    132 				if (current_line < lines_count - 2) {
    133 					current_line++;
    134 					if (current_line >= head_line + height)
    135 						head_line++;
    136 				}
    137 				break;
    138 			case 'v':
    139 				selected_invert = !selected_invert;
    140 				break;
    141 			case ' ':
    142 				lines[current_line].selected = !lines[current_line].selected;
    143 				break;
    144 			case '\n':    // Use '\n' for ENTER key
    145 			case 'q':
    146 				quit = true;
    147 		}
    148 		drawscreen(height, current_line, head_line, lines, lines_count);
    149 	}
    150 
    151 	endwin();
    152 }
    153 
    154 static size_t loadfile(const char *filename, char **buffer, int *lines) {
    155 	static char readbuf[READBUFFER];
    156 	ssize_t     nread;
    157 	size_t      alloc = 0, size = 0;
    158 	int         fd;
    159 
    160 	*buffer = NULL;
    161 	*lines  = 1;
    162 
    163 	if ((fd = open(filename, O_RDONLY)) == -1)
    164 		die("unable to open input-file");
    165 
    166 	while ((nread = read(fd, readbuf, sizeof(readbuf))) > 0) {
    167 		for (ssize_t i = 0; i < nread; i++) {
    168 			if (size == alloc) {
    169 				if ((*buffer = realloc(*buffer, alloc += BUFFERGROW)) == NULL) {
    170 					die("unable to allocate buffer");
    171 				}
    172 			}
    173 
    174 			if (readbuf[i] == '\n') {
    175 				(*buffer)[size++] = '\0';
    176 				(*lines)++;
    177 			} else {
    178 				(*buffer)[size++] = readbuf[i];
    179 			}
    180 		}
    181 	}
    182 	(*buffer)[size++] = '\0';
    183 	(*lines)++;
    184 	close(fd);
    185 
    186 	return size;
    187 }
    188 
    189 static int splitbuffer(char *buffer, size_t size, int maxlines, struct line **lines) {
    190 	int count = 0, start = 0;
    191 
    192 	(*lines) = calloc(maxlines, sizeof(struct line));
    193 	if (*lines == NULL)
    194 		die("unable to allocate line-mapping");
    195 
    196 	(*lines)[count].content    = buffer;
    197 	(*lines)[count++].selected = false;
    198 	for (size_t i = 0; i < size; i++) {
    199 		if (buffer[i] == '\0' && (keep_empty || buffer[i - 1] != '\0')) {
    200 			(*lines)[count - 1].length = i - start;
    201 			(*lines)[count].content    = &buffer[i + 1];
    202 			(*lines)[count++].selected = false;
    203 			start                      = i + 1;
    204 		}
    205 	}
    206 	(*lines)[count - 1].length = size - start - 1;
    207 
    208 	return count;
    209 }
    210 
    211 static void printselected(int fd, bool print0, struct line *lines, int lines_count) {
    212 	for (int i = 0; i < lines_count; i++) {
    213 		if (lines[i].selected != selected_invert && *lines[i].content != '\0') {    // is selected AND it's not empty
    214 			write(fd, lines[i].content, lines[i].length);
    215 			write(fd, print0 ? "" : "\n", 1);
    216 		}
    217 	}
    218 }
    219 
    220 static pid_t runcommand(char **argv, int *destfd) {
    221 	int   pipefd[2];
    222 	pid_t pid;
    223 
    224 	if (pipe(pipefd) == -1)
    225 		die("unable to create pipe");
    226 
    227 	if ((pid = fork()) == -1)
    228 		die("unable to fork for child process");
    229 
    230 	if (pid == 0) {                       // Child process
    231 		close(pipefd[1]);                 // Close write end of the pipe
    232 		dup2(pipefd[0], STDIN_FILENO);    // Redirect stdin to read end of the pipe
    233 		execvp(argv[0], argv);
    234 		die("unable to execute child");    // If execvp fails
    235 	}
    236 
    237 	close(pipefd[0]);    // Close read end of the pipe
    238 
    239 	*destfd = pipefd[1];
    240 	return pid;
    241 }
    242 
    243 static void alignspace(char *text) {
    244 	for (int i = strlen(text); i < prefixlen; i++) {
    245 		text[i] = ' ';
    246 	}
    247 	text[prefixlen] = '\0';
    248 }
    249 
    250 int main(int argc, char *argv[]) {
    251 	char        *buffer, *input, *output = NULL;
    252 	bool         print0 = false;
    253 	int          lines_count, cmdfd;
    254 	struct line *lines;
    255 	size_t       buffer_size;
    256 
    257 	argv0 = argv[0];
    258 	ARGBEGIN
    259 	switch (OPT) {
    260 		case 'h':
    261 			help();
    262 			exit(0);
    263 		case 'v':
    264 			selected_invert = true;
    265 			break;
    266 		case 'n':
    267 			keep_empty = true;
    268 			break;
    269 		case 'o':
    270 			output = EARGF(usage(1));
    271 			break;
    272 		case '0':    // null
    273 			print0 = true;
    274 			break;
    275 		case 's':
    276 			strncpy(selected, EARGF(usage(1)), sizeof(selected));
    277 			selected[sizeof(selected) - 1] = '\0';
    278 			break;
    279 		case 'S':
    280 			strncpy(unselected, EARGF(usage(1)), sizeof(unselected));
    281 			unselected[sizeof(unselected) - 1] = '\0';
    282 			break;
    283 		default:
    284 			fprintf(stderr, "error: unknown option '-%c'\n", OPT);
    285 			usage(1);
    286 	}
    287 	ARGEND;
    288 
    289 	if (argc == 0) {
    290 		fprintf(stderr, "error: missing input\n");
    291 		usage(1);
    292 	}
    293 
    294 	input = argv[0];
    295 	SHIFT;
    296 
    297 	if (*selected || *unselected) {
    298 		int sellen = strlen(selected), unsellen = strlen(unselected);
    299 
    300 		prefixlen = MAX(sellen, unsellen) + 1;
    301 
    302 		alignspace(selected);
    303 		alignspace(unselected);
    304 	}
    305 
    306 	buffer_size = loadfile(input, &buffer, &lines_count);
    307 	lines_count = splitbuffer(buffer, buffer_size, lines_count, &lines);
    308 
    309 	handlescreen(lines, lines_count);
    310 
    311 	if (output != NULL) {
    312 		int fd;
    313 
    314 		fd = open(output, O_WRONLY | O_TRUNC | O_CREAT, 0664);
    315 		if (fd == -1)
    316 			die("unable to open output-file");
    317 
    318 		printselected(fd, print0, lines, lines_count);
    319 	}
    320 
    321 	if (argc == 0) {
    322 		printselected(STDOUT_FILENO, print0, lines, lines_count);
    323 	} else {
    324 		pid_t pid = runcommand(argv, &cmdfd);
    325 		printselected(cmdfd, print0, lines, lines_count);
    326 		close(cmdfd);
    327 		waitpid(pid, NULL, 0);
    328 	}
    329 
    330 	free(buffer);
    331 	free(lines);
    332 
    333 	return 0;
    334 }