util/textselect

textselect.c in master
Repositories | Summary | Log | Files | README.md | LICENSE

textselect.c (8129B) download


  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
 22struct line {
 23	char *content;
 24	int   length;
 25	bool  selected;
 26};
 27
 28
 29static char *argv0           = NULL;
 30static bool  selected_invert = false;
 31static bool  keep_empty      = false;
 32static int   prefixlen       = 0;
 33
 34#include "config.h"
 35
 36static void die(const char *message) {
 37	fprintf(stderr, "error: %s: %s\n", message, strerror(errno));
 38	exit(EXIT_FAILURE);
 39}
 40
 41static 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
 68static void usage(int exitcode) {
 69	fprintf(stderr, USAGE, argv0);
 70	exit(exitcode);
 71}
 72
 73static 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
104static 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
154static 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
189static 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
211static 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
220static 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
243static void alignspace(char *text) {
244	for (int i = strlen(text); i < prefixlen; i++) {
245		text[i] = ' ';
246	}
247	text[prefixlen] = '\0';
248}
249
250int 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}