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 } |