├── .gitignore ├── LICENSE ├── Makefile ├── README ├── TODO ├── config.def.h ├── config.mk ├── perf.sh ├── ptty.c ├── scroll.1 ├── scroll.c ├── up.log └── up.sh /.gitignore: -------------------------------------------------------------------------------- 1 | scroll 2 | ptty 3 | *.swp 4 | *.core 5 | config.h 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License (ISC) 2 | 3 | Copyright (c) 2020 Jan Klemkow 4 | Copyright (c) 2020 Jochen Sprickerhof 5 | 6 | Permission to use, copy, modify, and distribute this software for any 7 | purpose with or without fee is hereby granted, provided that the above 8 | copyright notice and this permission notice appear in all copies. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 11 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 12 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 13 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 14 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 15 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 16 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 17 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .POSIX: 2 | 3 | include config.mk 4 | 5 | all: scroll 6 | 7 | config.h: 8 | cp config.def.h config.h 9 | 10 | scroll: scroll.c config.h 11 | 12 | install: scroll 13 | mkdir -p $(DESTDIR)$(BINDIR) $(DESTDIR)$(MANDIR)/man1 14 | cp -f scroll $(DESTDIR)$(BINDIR) 15 | cp -f scroll.1 $(DESTDIR)$(MANDIR)/man1 16 | chmod 755 $(DESTDIR)$(BINDIR)/scroll 17 | 18 | uninstall: 19 | rm -f $(DESTDIR)$(BINDIR)/scroll $(DESTDIR)$(MANDIR)/man1/scroll.1 20 | 21 | test: scroll ptty 22 | # check usage 23 | if ./ptty ./scroll -h; then exit 1; fi 24 | # check exit passthrough of child 25 | if ! ./ptty ./scroll true; then exit 1; fi 26 | if ./ptty ./scroll false; then exit 1; fi 27 | ./up.sh 28 | 29 | clean: 30 | rm -f scroll ptty 31 | 32 | distclean: clean 33 | rm -f config.h scroll-$(VERSION).tar.gz 34 | 35 | dist: clean 36 | mkdir -p scroll-$(VERSION) 37 | cp -R README scroll.1 TODO Makefile config.mk config.def.h \ 38 | ptty.c scroll.c up.sh up.log \ 39 | scroll-$(VERSION) 40 | tar -cf - scroll-$(VERSION) | gzip > scroll-$(VERSION).tar.gz 41 | rm -rf scroll-$(VERSION) 42 | 43 | .c: 44 | $(CC) $(CFLAGS) $(CPPFLAGS) $(LDFLAGS) -o $@ $< -lutil 45 | 46 | .PHONY: all install test clean distclean dist 47 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | This program provides a scroll back buffer for a terminal like st(1). It 2 | should run on any Unix-like system. 3 | 4 | At the moment it is in an experimental state. Its not recommended for 5 | productive use. 6 | 7 | The initial version of this program is from Roberto E. Vargas Caballero: 8 | https://lists.suckless.org/dev/1703/31256.html 9 | 10 | What is the state of scroll? 11 | 12 | The project is faced with some hard facts, that our original plan is not doable 13 | as we thought in the fist place: 14 | 15 | 1. [crtl]+[e] is used in emacs mode (default) on the shell to jump to the end 16 | of the line. But, its also used so signal a scroll down mouse event from 17 | terminal emulators to the shell an other programs. 18 | 19 | - A workaround is to use vi mode in the shell. 20 | - Or to give up mouse support (default behavior) 21 | 22 | 2. scroll could not handle backward cursor jumps and editing of old lines 23 | properly. We just handle current line editing and switching between 24 | alternative screens (curses mode). For a proper end user experience we 25 | would need to write a completely new terminal emulator like screen or tmux. 26 | 27 | What is the performance impact of scroll? 28 | 29 | indirect OpenBSD 30 | ------------------------------- 31 | 0x 7.53 s 32 | 1x 10.10 s 33 | 2x 12.00 s 34 | 3x 13.73 s 35 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | * strlen function which is aware of unicode 2 | * handle wrapping lines in scrolling line count correctly 3 | * hotkey to dump buffer to file (like screen hardcopy) 4 | -------------------------------------------------------------------------------- /config.def.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Define ESC sequences to use for scroll events. 3 | * Use "cat -v" to figure out favorite key combination. 4 | * 5 | * lines is the number of lines scrolled up or down. 6 | * If lines is negative, it's the fraction of the terminal size. 7 | */ 8 | 9 | struct rule rules[] = { 10 | /* sequence event lines */ 11 | {"\033[5;2~", SCROLL_UP, -1}, /* [Shift] + [PageUP] */ 12 | {"\033[6;2~", SCROLL_DOWN, -1}, /* [Shift] + [PageDown] */ 13 | {"\031", SCROLL_UP, 1}, /* mouse wheel up */ 14 | {"\005", SCROLL_DOWN, 1}, /* mouse wheel Down */ 15 | {"k", SCROLL_UP, 1}, 16 | {"j", SCROLL_DOWN, 1}, 17 | {"u", SCROLL_UP, 20}, 18 | {"d", SCROLL_DOWN, 20}, 19 | }; 20 | -------------------------------------------------------------------------------- /config.mk: -------------------------------------------------------------------------------- 1 | # scroll version 2 | VERSION = 0.1 3 | 4 | # paths 5 | PREFIX = /usr/local 6 | BINDIR = $(PREFIX)/bin 7 | MANDIR = $(PREFIX)/share/man 8 | 9 | CPPFLAGS = -DVERSION=\"$(VERSION)\" -D_DEFAULT_SOURCE 10 | # if your system is not POSIX, add -std=c99 to CFLAGS 11 | CFLAGS = -Os 12 | LDFLAGS = -s 13 | -------------------------------------------------------------------------------- /perf.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -eu 4 | 5 | export POSIXLY_CORRECT=1 6 | num=1000000 7 | seq=seq 8 | 9 | if [ -x /usr/bin/jot ]; then 10 | seq=jot 11 | fi 12 | 13 | rm -f perf_*.log 14 | 15 | for i in `$seq 10`; do 16 | /usr/bin/time st -e $seq $num 2>>perf_0.log 17 | done 18 | 19 | for i in `$seq 10`; do 20 | /usr/bin/time st -e ./ptty $seq $num 2>>perf_1.log 21 | done 22 | 23 | for i in `$seq 10`; do 24 | /usr/bin/time st -e ./ptty ./ptty $seq $num 2>>perf_2.log 25 | done 26 | 27 | for i in `$seq 10`; do 28 | /usr/bin/time st -e ./ptty ./ptty ./ptty $seq $num 2>>perf_3.log 29 | done 30 | -------------------------------------------------------------------------------- /ptty.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | #if defined(__linux) 16 | #include 17 | #elif defined(__OpenBSD__) || defined(__NetBSD__) || defined(__APPLE__) 18 | #include 19 | #elif defined(__FreeBSD__) || defined(__DragonFly__) 20 | #include 21 | #endif 22 | 23 | void 24 | die(const char *fmt, ...) 25 | { 26 | va_list ap; 27 | va_start(ap, fmt); 28 | vfprintf(stderr, fmt, ap); 29 | va_end(ap); 30 | 31 | if (fmt[0] && fmt[strlen(fmt)-1] == ':') { 32 | fputc(' ', stderr); 33 | perror(NULL); 34 | } else { 35 | fputc('\n', stderr); 36 | } 37 | 38 | exit(EXIT_FAILURE); 39 | } 40 | 41 | void 42 | usage(void) 43 | { 44 | fputs("ptty [-C] [-c cols] [-r rows] cmd\n", stderr); 45 | exit(EXIT_FAILURE); 46 | } 47 | 48 | int 49 | main(int argc, char *argv[]) 50 | { 51 | struct winsize ws = {.ws_row = 25, .ws_col = 80, 0, 0}; 52 | int ch; 53 | bool closeflag = false; 54 | 55 | while ((ch = getopt(argc, argv, "c:r:Ch")) != -1) { 56 | switch (ch) { 57 | case 'c': /* cols */ 58 | ws.ws_col = strtoimax(optarg, NULL, 10); 59 | if (errno != 0) 60 | die("strtoimax: %s", optarg); 61 | break; 62 | case 'r': /* lines */ 63 | ws.ws_row = strtoimax(optarg, NULL, 10); 64 | if (errno != 0) 65 | die("strtoimax: %s", optarg); 66 | break; 67 | case 'C': 68 | closeflag = true; 69 | break; 70 | case 'h': 71 | default: 72 | usage(); 73 | } 74 | } 75 | argc -= optind; 76 | argv += optind; 77 | 78 | if (argc < 1) 79 | usage(); 80 | 81 | int mfd; 82 | pid_t child = forkpty(&mfd, NULL, NULL, &ws); 83 | switch (child) { 84 | case -1: 85 | die("forkpty"); 86 | case 0: /* child */ 87 | execvp(argv[0], argv); 88 | die("exec"); 89 | } 90 | 91 | /* parent */ 92 | 93 | if (closeflag && close(mfd) == -1) 94 | die("close:"); 95 | 96 | int pfds = 2; 97 | struct pollfd pfd[2] = { 98 | { STDIN_FILENO, POLLIN, 0}, 99 | { mfd, POLLIN, 0} 100 | }; 101 | 102 | for (;;) { 103 | char buf[BUFSIZ]; 104 | ssize_t n; 105 | int r; 106 | 107 | if ((r = poll(pfd, pfds, -1)) == -1) 108 | die("poll:"); 109 | 110 | if (pfd[0].revents & POLLIN) { 111 | if ((n = read(STDIN_FILENO, buf, sizeof buf)) == -1) 112 | die("read:"); 113 | if (n == 0) { 114 | pfd[0].fd = -1; 115 | if (close(mfd) == -1) 116 | die("close:"); 117 | break; 118 | } 119 | if (write(mfd, buf, n) == -1) 120 | die("write:"); 121 | } 122 | 123 | if (pfd[1].revents & POLLIN) { 124 | if ((n = read(mfd, buf, sizeof(buf)-1)) == -1) 125 | die("read:"); 126 | 127 | if (n == 0) break; 128 | 129 | buf[n] = '\0'; 130 | 131 | /* handle cursor position request */ 132 | if (strcmp("\033[6n", buf) == 0) { 133 | dprintf(mfd, "\033[25;1R"); 134 | continue; 135 | } 136 | 137 | if (write(STDOUT_FILENO, buf, n) == -1) 138 | die("write:"); 139 | } 140 | 141 | if (pfd[0].revents & POLLHUP) { 142 | pfd[0].fd = -1; 143 | if (close(mfd) == -1) 144 | die("close:"); 145 | break; 146 | } 147 | if (pfd[1].revents & POLLHUP) 148 | break; 149 | } 150 | 151 | int status; 152 | if (waitpid(child, &status, 0) != child) 153 | die("waitpid:"); 154 | 155 | return WEXITSTATUS(status); 156 | } 157 | -------------------------------------------------------------------------------- /scroll.1: -------------------------------------------------------------------------------- 1 | .\" 2 | .\" Copyright (c) 2020 Jan Klemkow 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 | .Dd April 9, 2020 17 | .Dt SCROLL 1 18 | .Os 19 | .Sh NAME 20 | .Nm scroll 21 | .Nd scrollback buffer 22 | .Sh SYNOPSIS 23 | .Nm 24 | .Op Fl Mh 25 | .Op Fl m Ar size 26 | .Op program Op arg ... 27 | .Sh DESCRIPTION 28 | The 29 | .Nm 30 | utility saves output lines from the child 31 | .Ar program 32 | to use them for scrollback. 33 | If 34 | .Ar program 35 | is not set, 36 | .Nm 37 | starts the users default shell. 38 | .Pp 39 | The options are as follows: 40 | .Bl -tag -width Ds 41 | .It Fl h 42 | Shows usage of 43 | .Nm . 44 | .It Fl M 45 | Set memory limit used for scrollbackbuffer to maximum. 46 | .It Fl m Ar size 47 | Set memory limit used for scrollbackbuffer to 48 | .Ar size . 49 | .El 50 | .Sh EXIT STATUS 51 | .Nm 52 | exits with the status code of its the child 53 | .Ar program . 54 | .Sh EXAMPLES 55 | .Nm st 56 | .Fl e 57 | .Nm scroll 58 | .Nm /bin/sh 59 | .Sh SEE ALSO 60 | .Xr screen 1 , 61 | .Xr st 1 , 62 | .Xr tmux 1 63 | .Sh AUTHORS 64 | .Nm 65 | was written by 66 | .An Jan Klemkow Aq Mt j.klemkow@wemelug.de 67 | and 68 | .An Jochen Sprickerhof Aq Mt git@jochen.sprickerhof.de . 69 | -------------------------------------------------------------------------------- /scroll.c: -------------------------------------------------------------------------------- 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 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | 27 | #if defined(__linux) 28 | #include 29 | #elif defined(__OpenBSD__) || defined(__NetBSD__) || defined(__APPLE__) 30 | #include 31 | #elif defined(__FreeBSD__) || defined(__DragonFly__) 32 | #include 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 | } 595 | -------------------------------------------------------------------------------- /up.log: -------------------------------------------------------------------------------- 1 | 1 2 | 2 3 | 3 4 | 4 5 | 5 6 | 6 7 | 7 8 | 8 9 | 9 10 | 10 11 | 11 12 | 12 13 | 13 14 | 14 15 | 15 16 | 16 17 | 17 18 | 18 19 | 19 20 | 20 21 | 21 22 | 22 23 | 23 24 | 24 25 | 25 26 | 26 27 | 27 28 | 28 29 | 29 30 | 30 31 | 31 32 | 32 33 | 33 34 | 34 35 | 35 36 | 36 37 | 37 38 | 38 39 | 39 40 | 40 41 | 41 42 | 42 43 | 43 44 | 44 45 | 45 46 | 46 47 | 47 48 | 48 49 | 49 50 | [?25l1 51 | 2 52 | 3 53 | 4 54 | 5 55 | 6 56 | 7 57 | 8 58 | 9 59 | 10 60 | 11 61 | 12 62 | 13 63 | 14 64 | 15 65 | 16 66 | 17 67 | 18 68 | 19 69 | 20 70 | 21 71 | 22 72 | 23 73 | 24 74 | 25 -------------------------------------------------------------------------------- /up.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -eu 4 | export POSIXLY_CORRECT=1 5 | 6 | i=1 7 | while test "$i" -lt 50; do 8 | echo "$i" 9 | i=$((i + 1)) 10 | done > tmp.log 11 | 12 | (sleep 1; printf '\033[5;2~'; sleep 1; ) \ 13 | | ./ptty ./scroll tail -fn 50 tmp.log > out.log 14 | 15 | cmp out.log up.log 16 | --------------------------------------------------------------------------------