lchat.c (9376B)
1 /* 2 * Copyright (c) 2015-2023 Jan Klemkow <j.klemkow@wemelug.de> 3 * Copyright (c) 2022-2023 Tom Schwindl <schwindl@posteo.de> 4 * 5 * Permission to use, copy, modify, and distribute this software for any 6 * purpose with or without fee is hereby granted, provided that the above 7 * copyright notice and this permission notice appear in all copies. 8 * 9 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 */ 17 18 #include <sys/ioctl.h> 19 20 #include <errno.h> 21 #include <fcntl.h> 22 #include <libgen.h> 23 #include <limits.h> 24 #include <poll.h> 25 #include <signal.h> 26 #include <stdbool.h> 27 #include <stdio.h> 28 #include <stdlib.h> 29 #include <string.h> 30 #include <termios.h> 31 #include <unistd.h> 32 33 #include "slackline.h" 34 #include "util.h" 35 36 #ifndef INFTIM 37 #define INFTIM -1 38 #endif 39 40 static struct termios origin_term; 41 static struct winsize winsize; 42 static char *TERM; 43 44 static void 45 sigwinch(int sig) 46 { 47 if (sig == SIGWINCH) 48 ioctl(STDOUT_FILENO, TIOCGWINSZ, &winsize); 49 } 50 51 static void 52 exit_handler(void) 53 { 54 /* reset terminal's window name */ 55 set_title(TERM, TERM); 56 57 if (tcsetattr(STDIN_FILENO, TCSANOW, &origin_term) == -1) 58 die("tcsetattr:"); 59 } 60 61 static char * 62 read_file_line(const char *file) 63 { 64 FILE *fh; 65 char buf[BUFSIZ]; 66 char *line = NULL; 67 char *nl = NULL; 68 69 if (access(file, R_OK) == -1) 70 return NULL; 71 72 if ((fh = fopen(file, "r")) == NULL) 73 die("fopen:"); 74 75 if (fgets(buf, sizeof buf, fh) == NULL) 76 die("fgets:"); 77 78 if (fclose(fh) == EOF) 79 die("fclose:"); 80 81 if ((nl = strchr(buf, '\n')) != NULL) /* delete new line */ 82 *nl = '\0'; 83 84 if ((line = strdup(buf)) == NULL) 85 die("strdup:"); 86 87 return line; 88 } 89 90 static void 91 line_output(struct slackline *sl, char *file) 92 { 93 int fd; 94 95 if ((fd = open(file, O_WRONLY|O_APPEND)) == -1) 96 die("open: %s:", file); 97 98 if (write(fd, sl->buf, sl->blen) == -1) 99 die("write:"); 100 101 if (close(fd) == -1) 102 die("close:"); 103 } 104 105 static void 106 fork_filter(int *read, int *write) 107 { 108 int fds_read[2]; /* .filter -> lchat */ 109 int fds_write[2]; /* lchat -> .filter */ 110 111 if (pipe(fds_read) == -1) 112 die("pipe:"); 113 if (pipe(fds_write) == -1) 114 die("pipe:"); 115 116 switch (fork()) { 117 case -1: 118 die("fork of .filter"); 119 break; 120 case 0: /* child */ 121 if (dup2(fds_read[1], STDOUT_FILENO) == -1) 122 die("dup2:"); 123 if (dup2(fds_write[0], STDIN_FILENO) == -1) 124 die("dup2:"); 125 126 if (close(fds_read[0]) == -1) 127 die("close:"); 128 if (close(fds_write[1]) == -1) 129 die("close:"); 130 131 execl("./.filter", "./.filter", NULL); 132 die("exec of .filter"); 133 } 134 135 /* parent */ 136 if (close(fds_read[1]) == -1) 137 die("close:"); 138 if (close(fds_write[0]) == -1) 139 die("close:"); 140 141 *read = fds_read[0]; 142 *write = fds_write[1]; 143 } 144 145 static void 146 usage(void) 147 { 148 die("lchat [-aeh] [-n lines] [-p prompt] [-t title] [-i in] [-o out]" 149 " [directory]"); 150 } 151 152 int 153 main(int argc, char *argv[]) 154 { 155 #ifdef __OpenBSD__ 156 if (pledge("stdio rpath wpath tty proc exec", NULL) == -1) 157 die("pledge:"); 158 #endif 159 struct pollfd pfd[3]; 160 struct termios term; 161 struct slackline *sl = sl_init(); 162 int fd = STDIN_FILENO; 163 int read_fd = 6; 164 int read_filter = -1; 165 int backend_sink = STDOUT_FILENO; 166 char c; 167 int ch; 168 bool empty_line = false; 169 bool bell_flag = true; 170 bool ucspi = false; 171 char *bell_file = ".bellmatch"; 172 size_t history_len = 5; 173 char *prompt = read_file_line(".prompt"); 174 char *title = read_file_line(".title"); 175 176 if ((TERM = getenv("TERM")) == NULL) 177 TERM = ""; 178 179 if (sl == NULL) 180 die("Failed to initialize slackline"); 181 182 if (prompt == NULL) /* set default prompt */ 183 prompt = "> "; 184 185 size_t prompt_len = strlen(prompt); 186 size_t loverhang = 0; 187 char *dir = "."; 188 char *in_file = NULL; 189 char *out_file = NULL; 190 191 while ((ch = getopt(argc, argv, "an:i:eo:p:t:uhm:")) != -1) { 192 switch (ch) { 193 case 'a': 194 bell_flag = false; 195 break; 196 case 'n': 197 errno = 0; 198 history_len = strtoull(optarg, NULL, 0); 199 if (errno != 0) 200 die("strtoull:"); 201 break; 202 case 'i': 203 in_file = optarg; 204 break; 205 case 'e': 206 empty_line = true; 207 break; 208 case 'o': 209 out_file = optarg; 210 break; 211 case 'p': 212 prompt = optarg; 213 prompt_len = strlen(prompt); 214 break; 215 case 't': 216 title = optarg; 217 break; 218 case 'u': 219 ucspi = true; 220 break; 221 case 'm': 222 if (strcmp(optarg, "emacs") == 0) 223 sl_mode(sl, SL_EMACS); 224 else 225 die("lchat: invalid mode"); 226 break; 227 case 'h': 228 default: 229 usage(); 230 /* NOTREACHED */ 231 } 232 } 233 argc -= optind; 234 argv += optind; 235 236 if (argc > 1) 237 usage(); 238 239 if (argc == 1) 240 if ((dir = strdup(argv[0])) == NULL) 241 die("strdup:"); 242 243 if (in_file == NULL) 244 if (asprintf(&in_file, "%s/in", dir) == -1) 245 die("asprintf:"); 246 247 if (out_file == NULL) 248 if (asprintf(&out_file, "%s/out", dir) == -1) 249 die("asprintf:"); 250 251 if (isatty(fd) == 0) 252 die("isatty:"); 253 254 /* set terminal's window title */ 255 if (title == NULL) { 256 char path[PATH_MAX]; 257 if (getcwd(path, sizeof path) == NULL) 258 die("getcwd:"); 259 if ((title = basename(path)) == NULL) 260 die("basename:"); 261 } 262 set_title(TERM, title); 263 264 /* prepare terminal reset on exit */ 265 if (tcgetattr(fd, &origin_term) == -1) 266 die("tcgetattr:"); 267 268 term = origin_term; 269 270 if (atexit(exit_handler) == -1) 271 die("atexit:"); 272 273 term.c_iflag &= ~(BRKINT|PARMRK|ISTRIP|INLCR|IGNCR|ICRNL|IXON); 274 term.c_lflag &= ~(ECHO|ICANON|IEXTEN); 275 term.c_cflag &= ~(CSIZE|PARENB); 276 term.c_cflag |= CS8; 277 term.c_cc[VMIN] = 1; 278 term.c_cc[VTIME] = 0; 279 280 if (tcsetattr(fd, TCSANOW, &term) == -1) 281 die("tcsetattr:"); 282 283 /* get the terminal size */ 284 sigwinch(SIGWINCH); 285 signal(SIGWINCH, sigwinch); 286 287 setbuf(stdin, NULL); 288 setbuf(stdout, NULL); 289 290 if (!ucspi) { 291 char tail_cmd[BUFSIZ]; 292 FILE *fh; 293 294 /* open external source */ 295 snprintf(tail_cmd, sizeof tail_cmd, "exec tail -n %zu -f %s", 296 history_len, out_file); 297 298 if ((fh = popen(tail_cmd, "r")) == NULL) 299 die("unable to open pipe to tail:"); 300 301 read_fd = fileno(fh); 302 } 303 304 int nfds = 2; 305 306 pfd[0].fd = fd; 307 pfd[0].events = POLLIN; 308 309 pfd[1].fd = read_fd; 310 pfd[1].events = POLLIN; 311 312 if (access(".filter", X_OK) == 0) { 313 fork_filter(&read_filter, &backend_sink); 314 315 pfd[2].fd = read_filter; 316 pfd[2].events = POLLIN; 317 318 nfds = 3; 319 } 320 321 /* print initial prompt */ 322 fputs(prompt, stdout); 323 324 for (;;) { 325 if (fflush(stdout) == EOF) 326 die("fflush:"); 327 328 errno = 0; 329 if (poll(pfd, nfds, INFTIM) == -1 && errno != EINTR) 330 die("poll:"); 331 332 /* moves cursor back after linewrap */ 333 if (loverhang > 0) { 334 fputs("\r\033[2K", stdout); /* cr + ... */ 335 printf("\033[%zuA", loverhang); /* x times UP */ 336 } 337 338 /* carriage return and erase the whole line */ 339 fputs("\r\033[2K", stdout); 340 341 /* handle keyboard intput */ 342 if (pfd[0].revents & POLLIN) { 343 ssize_t ret = read(fd, &c, sizeof c); 344 345 if (ret == -1) 346 die("read:"); 347 348 if (ret == 0) 349 return EXIT_SUCCESS; 350 351 switch (c) { 352 case 13: /* return */ 353 if (sl->rlen == 0 && empty_line == false) 354 goto out; 355 /* replace NUL-terminator with newline */ 356 sl->buf[sl->blen++] = '\n'; 357 if (ucspi) { 358 if (write(7, sl->buf, sl->blen) == -1) 359 die("write:"); 360 } else { 361 line_output(sl, in_file); 362 } 363 sl_reset(sl); 364 break; 365 case 12: /* ctrl+l -- clear screen, same as clear(1) */ 366 fputs("\x1b[2J\x1b[H", stdout); 367 break; 368 default: 369 if (sl_keystroke(sl, c) == -1) 370 die("sl_keystroke"); 371 } 372 } 373 374 /* handle backend error and its broken pipe */ 375 if (pfd[1].revents & POLLHUP) 376 break; 377 if (pfd[1].revents & POLLERR || pfd[1].revents & POLLNVAL) 378 die("backend error"); 379 380 /* handle backend input */ 381 if (pfd[1].revents & POLLIN) { 382 char buf[BUFSIZ]; 383 ssize_t n = read(pfd[1].fd, buf, sizeof buf); 384 if (n == 0) 385 die("backend exited"); 386 if (n == -1) 387 die("read:"); 388 if (write(backend_sink, buf, n) == -1) 389 die("write:"); 390 391 /* terminate the input buffer with NUL */ 392 buf[n == BUFSIZ ? n - 1 : n] = '\0'; 393 394 /* ring the bell on external input */ 395 if (bell_flag && bell_match(buf, bell_file)) 396 putchar('\a'); 397 } 398 399 /* handel optional .filter i/o */ 400 if (nfds > 2) { 401 /* handle .filter error and its broken pipe */ 402 if (pfd[2].revents & POLLHUP) 403 break; 404 if (pfd[2].revents & POLLERR || 405 pfd[2].revents & POLLNVAL) 406 die(".filter error"); 407 408 /* handle .filter output */ 409 if (pfd[2].revents & POLLIN) { 410 char buf[BUFSIZ]; 411 ssize_t n = read(pfd[2].fd, buf, sizeof buf); 412 if (n == 0) 413 die(".filter exited"); 414 if (n == -1) 415 die("read:"); 416 if (write(STDOUT_FILENO, buf, n) == -1) 417 die("write:"); 418 } 419 } 420 out: 421 /* show current input line */ 422 fputs(prompt, stdout); 423 fputs(sl->buf, stdout); 424 425 /* save amount of overhanging lines */ 426 loverhang = (prompt_len + sl->rlen) / winsize.ws_col; 427 428 /* correct line wrap handling */ 429 if ((prompt_len + sl->rlen) > 0 && 430 (prompt_len + sl->rlen) % winsize.ws_col == 0) 431 fputs("\n", stdout); 432 433 if (sl->rcur < sl->rlen) { /* move the cursor */ 434 putchar('\r'); 435 /* HACK: because \033[0C does the same as \033[1C */ 436 if (sl->rcur + prompt_len > 0) 437 printf("\033[%zuC", sl->rcur + prompt_len); 438 } 439 } 440 return EXIT_SUCCESS; 441 }