lchat

A line oriented chat front end for ii.
git clone git://git.suckless.org/lchat
Log | Files | Refs | README

lchat.c (10289B)


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