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 }