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 }