scroll.c - scroll - scrollbackbuffer program for st
(HTM) git clone git://git.suckless.org/scroll
(DIR) Log
(DIR) Files
(DIR) Refs
(DIR) README
(DIR) LICENSE
---
scroll.c (12817B)
---
1 /*
2 * Based on an example code from Roberto E. Vargas Caballero.
3 *
4 * See LICENSE file for copyright and license details.
5 */
6
7 #include <sys/types.h>
8 #include <sys/ioctl.h>
9 #include <sys/wait.h>
10 #include <sys/queue.h>
11 #include <sys/resource.h>
12
13 #include <assert.h>
14 #include <errno.h>
15 #include <fcntl.h>
16 #include <poll.h>
17 #include <pwd.h>
18 #include <signal.h>
19 #include <stdarg.h>
20 #include <stdbool.h>
21 #include <stdio.h>
22 #include <stdlib.h>
23 #include <string.h>
24 #include <termios.h>
25 #include <unistd.h>
26
27 #if defined(__linux)
28 #include <pty.h>
29 #elif defined(__OpenBSD__) || defined(__NetBSD__) || defined(__APPLE__)
30 #include <util.h>
31 #elif defined(__FreeBSD__) || defined(__DragonFly__)
32 #include <libutil.h>
33 #endif
34
35 #define LENGTH(X) (sizeof (X) / sizeof ((X)[0]))
36
37 const char *argv0;
38
39 TAILQ_HEAD(tailhead, line) head;
40
41 struct line {
42 TAILQ_ENTRY(line) entries;
43 size_t size;
44 size_t len;
45 char *buf;
46 } *bottom;
47
48 pid_t child;
49 int mfd;
50 struct termios dfl;
51 struct winsize ws;
52 static bool altscreen = false; /* is alternative screen active */
53 static bool doredraw = false; /* redraw upon sigwinch */
54
55 struct rule {
56 const char *seq;
57 enum {SCROLL_UP, SCROLL_DOWN} event;
58 short lines;
59 };
60
61 #include "config.h"
62
63 void
64 die(const char *fmt, ...)
65 {
66 va_list ap;
67 va_start(ap, fmt);
68 vfprintf(stderr, fmt, ap);
69 va_end(ap);
70
71 if (fmt[0] && fmt[strlen(fmt)-1] == ':') {
72 fputc(' ', stderr);
73 perror(NULL);
74 } else {
75 fputc('\n', stderr);
76 }
77
78 exit(EXIT_FAILURE);
79 }
80
81 void
82 sigwinch(int sig)
83 {
84 assert(sig == SIGWINCH);
85
86 if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == -1)
87 die("ioctl:");
88 if (ioctl(mfd, TIOCSWINSZ, &ws) == -1) {
89 if (errno == EBADF) /* child already exited */
90 return;
91 die("ioctl:");
92 }
93 kill(-child, SIGWINCH);
94 doredraw = true;
95 }
96
97 void
98 reset(void)
99 {
100 if (tcsetattr(STDIN_FILENO, TCSANOW, &dfl) == -1)
101 die("tcsetattr:");
102 }
103
104 /* error avoiding remalloc */
105 void *
106 earealloc(void *ptr, size_t size)
107 {
108 void *mem;
109
110 while ((mem = realloc(ptr, size)) == NULL) {
111 struct line *line = TAILQ_LAST(&head, tailhead);
112
113 if (line == NULL)
114 die("realloc:");
115
116 TAILQ_REMOVE(&head, line, entries);
117 free(line->buf);
118 free(line);
119 }
120
121 return mem;
122 }
123
124 /* Count string length w/o ansi esc sequences. */
125 size_t
126 strelen(const char *buf, size_t size)
127 {
128 enum {CHAR, BREK, ESC} state = CHAR;
129 size_t len = 0;
130
131 for (size_t i = 0; i < size; i++) {
132 char c = buf[i];
133
134 switch (state) {
135 case CHAR:
136 if (c == '\033')
137 state = BREK;
138 else
139 len++;
140 break;
141 case BREK:
142 if (c == '[') {
143 state = ESC;
144 } else {
145 state = CHAR;
146 len++;
147 }
148 break;
149 case ESC:
150 if (c >= 64 && c <= 126)
151 state = CHAR;
152 break;
153 }
154 }
155
156 return len;
157 }
158
159 /* detect alternative screen switching and clear screen */
160 bool
161 skipesc(char c)
162 {
163 static enum {CHAR, BREK, ESC} state = CHAR;
164 static char buf[BUFSIZ];
165 static size_t i = 0;
166
167 switch (state) {
168 case CHAR:
169 if (c == '\033')
170 state = BREK;
171 break;
172 case BREK:
173 if (c == '[')
174 state = ESC;
175 else
176 state = CHAR;
177 break;
178 case ESC:
179 buf[i++] = c;
180 if (i == sizeof buf) {
181 /* TODO: find a better way to handle this situation */
182 state = CHAR;
183 i = 0;
184 } else if (c >= 64 && c <= 126) {
185 state = CHAR;
186 buf[i] = '\0';
187 i = 0;
188
189 /* esc seq. enable alternative screen */
190 if (strcmp(buf, "?1049h") == 0 ||
191 strcmp(buf, "?1047h") == 0 ||
192 strcmp(buf, "?47h" ) == 0)
193 altscreen = true;
194
195 /* esc seq. disable alternative screen */
196 if (strcmp(buf, "?1049l") == 0 ||
197 strcmp(buf, "?1047l") == 0 ||
198 strcmp(buf, "?47l" ) == 0)
199 altscreen = false;
200
201 /* don't save cursor move or clear screen */
202 /* esc sequences to log */
203 switch (c) {
204 case 'A':
205 case 'B':
206 case 'C':
207 case 'D':
208 case 'H':
209 case 'J':
210 case 'K':
211 case 'f':
212 return true;
213 }
214 }
215 break;
216 }
217
218 return altscreen;
219 }
220
221 void
222 getcursorposition(int *x, int *y)
223 {
224 char input[BUFSIZ];
225 ssize_t n;
226
227 if (write(STDOUT_FILENO, "\033[6n", 4) == -1)
228 die("requesting cursor position");
229
230 do {
231 if ((n = read(STDIN_FILENO, input, sizeof(input)-1)) == -1)
232 die("reading cursor position");
233 input[n] = '\0';
234 } while (sscanf(input, "\033[%d;%dR", y, x) != 2);
235
236 if (*x <= 0 || *y <= 0)
237 die("invalid cursor position: x=%d y=%d", *x, *y);
238 }
239
240 void
241 addline(char *buf, size_t size)
242 {
243 struct line *line = earealloc(NULL, sizeof *line);
244
245 line->size = size;
246 line->len = strelen(buf, size);
247 line->buf = earealloc(NULL, size);
248 memcpy(line->buf, buf, size);
249
250 TAILQ_INSERT_HEAD(&head, line, entries);
251 }
252
253 void
254 redraw()
255 {
256 int rows = 0, x, y;
257
258 if (bottom == NULL)
259 return;
260
261 getcursorposition(&x, &y);
262
263 if (y < ws.ws_row-1)
264 y--;
265
266 /* wind back bottom pointer by shown history */
267 for (; bottom != NULL && TAILQ_NEXT(bottom, entries) != NULL &&
268 rows < y - 1; rows++)
269 bottom = TAILQ_NEXT(bottom, entries);
270
271 /* clear screen */
272 dprintf(STDOUT_FILENO, "\033[2J");
273 /* set cursor position to upper left corner */
274 write(STDOUT_FILENO, "\033[0;0H", 6);
275
276 /* remove newline of first line as we are at 0,0 already */
277 if (bottom->size > 0 && bottom->buf[0] == '\n')
278 write(STDOUT_FILENO, bottom->buf + 1, bottom->size - 1);
279 else
280 write(STDOUT_FILENO, bottom->buf, bottom->size);
281
282 for (rows = ws.ws_row; rows > 0 &&
283 TAILQ_PREV(bottom, tailhead, entries) != NULL; rows--) {
284 bottom = TAILQ_PREV(bottom, tailhead, entries);
285 write(STDOUT_FILENO, bottom->buf, bottom->size);
286 }
287
288 if (bottom == TAILQ_FIRST(&head)) {
289 /* add new line in front of the shell prompt */
290 write(STDOUT_FILENO, "\n", 1);
291 write(STDOUT_FILENO, "\033[?25h", 6); /* show cursor */
292 } else
293 bottom = TAILQ_NEXT(bottom, entries);
294 }
295
296 void
297 scrollup(int n)
298 {
299 int rows = 2, x, y, extra = 0;
300 struct line *scrollend = bottom;
301
302 if (bottom == NULL)
303 return;
304
305 getcursorposition(&x, &y);
306
307 if (n < 0) /* scroll by fraction of ws.ws_row, but at least one line */
308 n = ws.ws_row > (-n) ? ws.ws_row / (-n) : 1;
309
310 /* wind back scrollend pointer by the current screen */
311 while (rows < y && TAILQ_NEXT(scrollend, entries) != NULL) {
312 scrollend = TAILQ_NEXT(scrollend, entries);
313 rows += (scrollend->len - 1) / ws.ws_col + 1;
314 }
315
316 if (rows <= 0)
317 return;
318
319 /* wind back scrollend pointer n lines */
320 for (rows = 0; rows + extra < n &&
321 TAILQ_NEXT(scrollend, entries) != NULL; rows++) {
322 scrollend = TAILQ_NEXT(scrollend, entries);
323 extra += (scrollend->len - 1) / ws.ws_col;
324 }
325
326 /* move the text in terminal rows lines down */
327 dprintf(STDOUT_FILENO, "\033[%dT", n);
328 /* set cursor position to upper left corner */
329 write(STDOUT_FILENO, "\033[0;0H", 6);
330 /* hide cursor */
331 write(STDOUT_FILENO, "\033[?25l", 6);
332
333 /* remove newline of first line as we are at 0,0 already */
334 if (scrollend->size > 0 && scrollend->buf[0] == '\n')
335 write(STDOUT_FILENO, scrollend->buf + 1, scrollend->size - 1);
336 else
337 write(STDOUT_FILENO, scrollend->buf, scrollend->size);
338 if (y + n >= ws.ws_row)
339 bottom = TAILQ_NEXT(bottom, entries);
340
341 /* print rows lines and move bottom forward to the new screen bottom */
342 for (; rows > 1; rows--) {
343 scrollend = TAILQ_PREV(scrollend, tailhead, entries);
344 if (y + n >= ws.ws_row)
345 bottom = TAILQ_NEXT(bottom, entries);
346 write(STDOUT_FILENO, scrollend->buf, scrollend->size);
347 }
348 /* move cursor from line n to the old bottom position */
349 if (y + n < ws.ws_row) {
350 dprintf(STDOUT_FILENO, "\033[%d;%dH", y + n, x);
351 write(STDOUT_FILENO, "\033[?25h", 6); /* show cursor */
352 } else
353 dprintf(STDOUT_FILENO, "\033[%d;0H", ws.ws_row);
354 }
355
356 void
357 scrolldown(char *buf, size_t size, int n)
358 {
359 if (bottom == NULL || bottom == TAILQ_FIRST(&head))
360 return;
361
362 if (n < 0) /* scroll by fraction of ws.ws_row, but at least one line */
363 n = ws.ws_row > (-n) ? ws.ws_row / (-n) : 1;
364
365 bottom = TAILQ_PREV(bottom, tailhead, entries);
366 /* print n lines */
367 while (n > 0 && bottom != NULL && bottom != TAILQ_FIRST(&head)) {
368 bottom = TAILQ_PREV(bottom, tailhead, entries);
369 write(STDOUT_FILENO, bottom->buf, bottom->size);
370 n -= (bottom->len - 1) / ws.ws_col + 1;
371 }
372 if (n > 0 && bottom == TAILQ_FIRST(&head)) {
373 write(STDOUT_FILENO, "\033[?25h", 6); /* show cursor */
374 write(STDOUT_FILENO, buf, size);
375 } else if (bottom != NULL)
376 bottom = TAILQ_NEXT(bottom, entries);
377 }
378
379 void
380 jumpdown(char *buf, size_t size)
381 {
382 int rows = ws.ws_row;
383
384 /* wind back by one page starting from the latest line */
385 bottom = TAILQ_FIRST(&head);
386 for (; TAILQ_NEXT(bottom, entries) != NULL && rows > 0; rows--)
387 bottom = TAILQ_NEXT(bottom, entries);
388
389 scrolldown(buf, size, ws.ws_row);
390 }
391
392 void
393 usage(void) {
394 die("usage: %s [-Mvh] [-m mem] [program]", argv0);
395 }
396
397 int
398 main(int argc, char *argv[])
399 {
400 int ch;
401 struct rlimit rlimit;
402
403 argv0 = argv[0];
404
405 if (getrlimit(RLIMIT_DATA, &rlimit) == -1)
406 die("getrlimit");
407
408 const char *optstring = "Mm:vh";
409 while ((ch = getopt(argc, argv, optstring)) != -1) {
410 switch (ch) {
411 case 'M':
412 rlimit.rlim_cur = rlimit.rlim_max;
413 break;
414 case 'm':
415 rlimit.rlim_cur = strtoull(optarg, NULL, 0);
416 if (errno != 0)
417 die("strtoull: %s", optarg);
418 break;
419 case 'v':
420 die("%s " VERSION, argv0);
421 break;
422 case 'h':
423 default:
424 usage();
425 }
426 }
427 argc -= optind;
428 argv += optind;
429
430 TAILQ_INIT(&head);
431
432 if (isatty(STDIN_FILENO) == 0 || isatty(STDOUT_FILENO) == 0)
433 die("parent it not a tty");
434
435 /* save terminal settings for resetting after exit */
436 if (tcgetattr(STDIN_FILENO, &dfl) == -1)
437 die("tcgetattr:");
438 if (atexit(reset))
439 die("atexit:");
440
441 /* get window size of the terminal */
442 if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == -1)
443 die("ioctl:");
444
445 child = forkpty(&mfd, NULL, &dfl, &ws);
446 if (child == -1)
447 die("forkpty:");
448 if (child == 0) { /* child */
449 if (argc >= 1) {
450 execvp(argv[0], argv);
451 } else {
452 struct passwd *passwd = getpwuid(getuid());
453 if (passwd == NULL)
454 die("getpwid:");
455 execlp(passwd->pw_shell, passwd->pw_shell, NULL);
456 }
457
458 perror("execvp");
459 _exit(127);
460 }
461
462 /* set maximum memory size for scrollback buffer */
463 if (setrlimit(RLIMIT_DATA, &rlimit) == -1)
464 die("setrlimit:");
465
466 #ifdef __OpenBSD__
467 if (pledge("stdio tty proc", NULL) == -1)
468 die("pledge:");
469 #endif
470
471 if (signal(SIGWINCH, sigwinch) == SIG_ERR)
472 die("signal:");
473
474 struct termios new = dfl;
475 cfmakeraw(&new);
476 new.c_cc[VMIN ] = 1; /* return read if at least one byte in buffer */
477 new.c_cc[VTIME] = 0; /* no polling time for read from terminal */
478 if (tcsetattr(STDIN_FILENO, TCSANOW, &new) == -1)
479 die("tcsetattr:");
480
481 size_t size = BUFSIZ, len = 0, pos = 0;
482 char *buf = calloc(size, sizeof *buf);
483 if (buf == NULL)
484 die("calloc:");
485
486 struct pollfd pfd[2] = {
487 {STDIN_FILENO, POLLIN, 0},
488 {mfd, POLLIN, 0}
489 };
490
491 for (;;) {
492 char input[BUFSIZ];
493
494 if (poll(pfd, LENGTH(pfd), -1) == -1 && errno != EINTR)
495 die("poll:");
496
497 if (doredraw) {
498 redraw();
499 doredraw = false;
500 }
501
502 if (pfd[0].revents & POLLHUP || pfd[1].revents & POLLHUP)
503 break;
504
505 if (pfd[0].revents & POLLIN) {
506 ssize_t n = read(STDIN_FILENO, input, sizeof(input)-1);
507
508 if (n == -1 && errno != EINTR)
509 die("read:");
510 if (n == 0)
511 break;
512
513 input[n] = '\0';
514
515 if (altscreen)
516 goto noevent;
517
518 for (size_t i = 0; i < LENGTH(rules); i++) {
519 if (strncmp(rules[i].seq, input,
520 strlen(rules[i].seq)) == 0) {
521 if (rules[i].event == SCROLL_UP)
522 scrollup(rules[i].lines);
523 if (rules[i].event == SCROLL_DOWN)
524 scrolldown(buf, len,
525 rules[i].lines);
526 goto out;
527 }
528 }
529 noevent:
530 if (write(mfd, input, n) == -1)
531 die("write:");
532
533 if (bottom != TAILQ_FIRST(&head))
534 jumpdown(buf, len);
535 }
536 out:
537 if (pfd[1].revents & POLLIN) {
538 ssize_t n = read(mfd, input, sizeof(input)-1);
539
540 if (n == -1 && errno != EINTR)
541 die("read:");
542 if (n == 0) /* on exit of child we continue here */
543 continue; /* let signal handler catch SIGCHLD */
544
545 input[n] = '\0';
546
547 /* don't print child output while scrolling */
548 if (bottom == TAILQ_FIRST(&head))
549 if (write(STDOUT_FILENO, input, n) == -1)
550 die("write:");
551
552 /* iterate over the input buffer */
553 for (char *c = input; n-- > 0; c++) {
554 /* don't save alternative screen and */
555 /* clear screen esc sequences to scrollback */
556 if (skipesc(*c))
557 continue;
558
559 if (*c == '\n') {
560 addline(buf, len);
561 /* only advance bottom if scroll is */
562 /* at the end of the scroll back */
563 if (bottom == NULL ||
564 TAILQ_PREV(bottom, tailhead,
565 entries) == TAILQ_FIRST(&head))
566 bottom = TAILQ_FIRST(&head);
567
568 memset(buf, 0, size);
569 len = pos = 0;
570 buf[pos++] = '\r';
571 } else if (*c == '\r') {
572 pos = 0;
573 continue;
574 }
575 buf[pos++] = *c;
576 if (pos > len)
577 len = pos;
578 if (len == size) {
579 size *= 2;
580 buf = earealloc(buf, size);
581 }
582 }
583 }
584 }
585
586 if (close(mfd) == -1)
587 die("close:");
588
589 int status;
590 if (waitpid(child, &status, 0) == -1)
591 die("waitpid:");
592
593 return WEXITSTATUS(status);
594 }