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}