| lchat.c - lchat - A line oriented chat front end for ii. | |
| git clone git://git.suckless.org/lchat | |
| Log | |
| Files | |
| Refs | |
| README | |
| --- | |
| lchat.c (9376B) | |
| --- | |
| 1 /* | |
| 2 * Copyright (c) 2015-2023 Jan Klemkow <[email protected]> | |
| 3 * Copyright (c) 2022-2023 Tom Schwindl <[email protected]> | |
| 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 WARRANT… | |
| 10 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF | |
| 11 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE F… | |
| 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 … | |
| 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 … | |
| 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… | |
| 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 time… | |
| 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) … | |
| 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 clea… | |
| 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 & POLLNVA… | |
| 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 … | |
| 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[1… | |
| 436 if (sl->rcur + prompt_len > 0) | |
| 437 printf("\033[%zuC", sl->rcur + prompt_le… | |
| 438 } | |
| 439 } | |
| 440 return EXIT_SUCCESS; | |
| 441 } |