lchat

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

lchat.c (9440B)


      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 	struct pollfd pfd[3];
    156 	struct termios term;
    157 	struct slackline *sl = sl_init();
    158 	int fd = STDIN_FILENO;
    159 	int read_fd = 6;
    160 	int read_filter = -1;
    161 	int backend_sink = STDOUT_FILENO;
    162 	char c;
    163 	int ch;
    164 	bool empty_line = false;
    165 	bool bell_flag = true;
    166 	bool ucspi = false;
    167 	char *bell_file = ".bellmatch";
    168 	size_t history_len = 5;
    169 	char *prompt = read_file_line(".prompt");
    170 	char *title = read_file_line(".title");
    171 
    172 	if ((TERM = getenv("TERM")) == NULL)
    173 		TERM = "";
    174 
    175 	if (sl == NULL)
    176 		die("Failed to initialize slackline");
    177 
    178 	if (prompt == NULL)	/* set default prompt */
    179 		prompt = "> ";
    180 
    181 	size_t prompt_len = strlen(prompt);
    182 	size_t loverhang = 0;
    183 	char *dir = ".";
    184 	char *in_file = NULL;
    185 	char *out_file = NULL;
    186 
    187 	while ((ch = getopt(argc, argv, "an:i:eo:p:t:uhm:")) != -1) {
    188 		switch (ch) {
    189 		case 'a':
    190 			bell_flag = false;
    191 			break;
    192 		case 'n':
    193 			errno = 0;
    194 			history_len = strtoull(optarg, NULL, 0);
    195 			if (errno != 0)
    196 				die("strtoull:");
    197 			break;
    198 		case 'i':
    199 			if ((in_file = strdup(optarg)) == NULL)
    200 				die("strdup:");
    201 			break;
    202 		case 'e':
    203 			empty_line = true;
    204 			break;
    205 		case 'o':
    206 			if ((out_file = strdup(optarg)) == NULL)
    207 				die("strdup:");
    208 			break;
    209 		case 'p':
    210 			if ((prompt = strdup(optarg)) == NULL)
    211 				die("strdup:");
    212 			prompt_len = strlen(prompt);
    213 			break;
    214 		case 't':
    215 			if ((title = strdup(optarg)) == NULL)
    216 				die("strdup:");
    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 }