lchat.c - lchat - A line oriented chat front end for ii.
 (HTM) git clone git://git.suckless.org/lchat
 (DIR) Log
 (DIR) Files
 (DIR) Refs
 (DIR) README
       ---
       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 }