aboutsummaryrefslogtreecommitdiff
path: root/src/libterm
diff options
context:
space:
mode:
Diffstat (limited to 'src/libterm')
-rw-r--r--src/libterm/term.c489
-rw-r--r--src/libterm/term.h270
-rw-r--r--src/libterm/window.c408
3 files changed, 1167 insertions, 0 deletions
diff --git a/src/libterm/term.c b/src/libterm/term.c
new file mode 100644
index 0000000..11591fc
--- /dev/null
+++ b/src/libterm/term.c
@@ -0,0 +1,489 @@
+#include "term.h"
+
+#include <signal.h>
+#include <sys/ioctl.h>
+
+struct ExtraInfo
+{
+ char *enteralt;
+ char *exitalt;
+
+ char *entermouse;
+ char *exitmouse;
+};
+
+static
+struct ExtraInfo vt200 =
+{
+ .enteralt = "\e[?1049h",
+ .exitalt = "\e[?1049l",
+
+ .entermouse = "\e[?1049h\e[?1006l",
+ .exitmouse = "\e[?1002l\e[?1006l",
+};
+
+static Term *sigwinchhead;
+
+// -----------------------------------------------------------------------
+// database lookup
+
+static
+char*
+tryinfostr(Term *t, enum unibi_string s)
+{
+ char *val = (char*)unibi_get_str(t->info, s);
+ /* TODO: provide fallbacks */
+ return val;
+}
+
+static
+char*
+guessinfostr(Term *t, enum unibi_string s, char *guess)
+{
+ char *val = (char*)unibi_get_str(t->info, s);
+ if (!val)
+ return guess;
+ return val;
+}
+
+static
+char*
+getinfostr(Term *t, enum unibi_string s)
+{
+ char *val = tryinfostr(t, s);
+ if (!val)
+ panicf("required term info string '%s' missing", unibi_name_str(s));
+
+ return val;
+}
+
+static
+char *
+tryextrastr(Term *t, char *name)
+{
+ const char *nm;
+ size_t max = unibi_count_ext_str(t->info);
+ for (size_t i = 0; i < max; i++) {
+ nm = unibi_get_ext_str_name(t->info, i);
+ if (nm && !strcmp(nm, name)) {
+ return (char *)nm;
+ }
+ }
+ return nil;
+}
+
+static
+char *
+guessextrastr(Term *t, char *name, char *guess)
+{
+ char *s;
+ if ((s = tryextrastr(t, name)))
+ return s;
+
+ return guess;
+}
+
+/* formats escape strings and writes to output */
+static void tfmt(Term *t, char *esc, int n, ...);
+static void tclear(Term *t);
+
+// -----------------------------------------------------------------------
+// exported term methods
+
+static
+char *
+ttmpbuf(Term *t, int len)
+{
+ if (t->tmp.len >= len)
+ return t->tmp.b;
+
+ /* TODO: error handling */
+ return (t->tmp.b = realloc(t->tmp.b, len));
+}
+
+void twrite(Term *t, long len, char *s);
+void tlistensigwinch(Term *t);
+
+Term*
+tmake(void)
+{
+ Term *t;
+
+ t = calloc(1, sizeof(*t));
+
+ /* meta data */
+ t->name = getenv("TERM");
+ t->info = unibi_from_term(t->name);
+ if (!t->info)
+ panicf("could not identify terminal");
+
+ t->fd = 1; // stdout
+ tlistensigwinch(t);
+
+ t->mode.mouse = 0;
+ t->mode.cursorvis = 1;
+ t->mode.altscreen = 0;
+
+ t->cap.colors = unibi_get_num(t->info, unibi_max_colors);
+ t->cap.bce = unibi_get_bool(t->info, unibi_back_color_erase);
+
+ /* initialize root window (get current size)*/
+ struct winsize ws = { 0 };
+ if (ioctl(t->fd, TIOCGWINSZ, &ws) == 1)
+ goto bad;
+
+ t->root = wmake(nil, 0, 0, ws.ws_col, ws.ws_row, 0);
+
+ t->root->curvis = 1;
+ t->root->blink = 0;
+
+ t->pen = (Pen){
+ .state = PenNormal,
+ .col = {.fg = -1, .bg = -1},
+ };
+
+ /* fill in output buffers */
+ t->buf.c = t->buf.b;
+ t->tmp.b = nil;
+ t->tmp.len = 0;
+
+ /* get all term info format strings */
+ t->esc.cup = getinfostr(t, unibi_cursor_address);
+ t->esc.vpa = tryinfostr(t, unibi_row_address);
+ t->esc.hpa = tryinfostr(t, unibi_column_address);
+ t->esc.cuu = getinfostr(t, unibi_parm_up_cursor);
+ t->esc.cuu1 = tryinfostr(t, unibi_cursor_up);
+ t->esc.cud = getinfostr(t, unibi_parm_down_cursor);
+ t->esc.cud1 = tryinfostr(t, unibi_cursor_down);
+ t->esc.cuf = getinfostr(t, unibi_parm_right_cursor);
+ t->esc.cuf1 = tryinfostr(t, unibi_cursor_right);
+ t->esc.cub = getinfostr(t, unibi_parm_left_cursor);
+ t->esc.cub1 = tryinfostr(t, unibi_cursor_left);
+ t->esc.ich = getinfostr(t, unibi_parm_ich);
+ t->esc.ich1 = tryinfostr(t, unibi_insert_character);
+ t->esc.dch = getinfostr(t, unibi_parm_dch);
+ t->esc.dch1 = tryinfostr(t, unibi_delete_character);
+ t->esc.il = getinfostr(t, unibi_parm_insert_line);
+ t->esc.il1 = tryinfostr(t, unibi_insert_line);
+ t->esc.dl = getinfostr(t, unibi_parm_delete_line);
+ t->esc.dl1 = tryinfostr(t, unibi_delete_line);
+ t->esc.ech = getinfostr(t, unibi_erase_chars);
+ t->esc.ed2 = getinfostr(t, unibi_clear_screen);
+ t->esc.stbm = getinfostr(t, unibi_change_scroll_region);
+ t->esc.sgr = getinfostr(t, unibi_set_attributes);
+ t->esc.sgr0 = getinfostr(t, unibi_exit_attribute_mode);
+ t->esc.sgr_i0 = tryinfostr(t, unibi_exit_italics_mode);
+ t->esc.sgr_i1 = tryinfostr(t, unibi_enter_italics_mode);
+ t->esc.sgr_fg = getinfostr(t, unibi_set_a_foreground);
+ t->esc.sgr_bg = getinfostr(t, unibi_set_a_background);
+ t->esc.sm_csr = getinfostr(t, unibi_cursor_normal);
+ t->esc.rm_csr = getinfostr(t, unibi_cursor_invisible);
+
+ /* extensions to terminfo */
+ t->esc.ext.rgbf = guessextrastr(t, "setrgbf", "\x1b[38;2;%p1%d;%p2%d;%p3%dm");
+ t->esc.ext.rgbb = guessextrastr(t, "setrgbb", "\x1b[48;2;%p1%d;%p2%d;%p3%dm");
+
+ return t;
+
+bad:
+ panicf("failed to initialize terminal instance");
+ free(t);
+ return nil;
+}
+
+void
+tfree(Term *t)
+{
+ if (t->mode.mouse)
+ twrite(t, 0, vt200.exitmouse);
+ if (!t->mode.cursorvis)
+ tfmt(t, t->esc.rm_csr, 0);
+ if (t->mode.altscreen)
+ twrite(t, 0, vt200.exitalt);
+
+ tfmt(t, t->esc.sgr0, 0);
+ tclear(t);
+ free(t);
+}
+
+/* handle resize events */
+void
+tresize(Term *t)
+{
+ if (t->fd == -1)
+ return;
+
+ struct winsize ws = { 0 };
+ if (ioctl(t->fd, TIOCGWINSZ, &ws) == 1)
+ return;
+
+ printf("[%d,%d]\n", ws.ws_col, ws.ws_row);
+ if (t->root->w != ws.ws_col || t->root->h != ws.ws_row)
+ wresize(t->root, ws.ws_col, ws.ws_row);
+}
+
+static
+void
+sigwinch(int num)
+{
+ Term *it;
+ for (it = sigwinchhead; it; it = it->link)
+ tresize(it);
+}
+
+void
+tlistensigwinch(Term *t)
+{
+ sigset_t new, old;
+ Term *it;
+
+ sigemptyset(&new);
+ sigaddset(&new, SIGWINCH);
+ sigprocmask(SIG_BLOCK, &new, &old);
+
+ if (!sigwinchhead) {
+ sigaction(SIGWINCH, &(struct sigaction){ .sa_handler = sigwinch }, nil);
+ sigwinchhead = t;
+ } else {
+ it = sigwinchhead;
+ while (it->link)
+ it = it->link;
+ it->link = t;
+ }
+
+ sigprocmask(SIG_SETMASK, &old, nil);
+}
+
+void
+tflush(Term *t)
+{
+ if (t->fd != -1)
+ write(t->fd, t->buf.b, t->buf.c - t->buf.b);
+
+ t->buf.c = t->buf.b;
+}
+
+void
+twrite(Term *t, long len, char *s)
+{
+ int n;
+ if (!len)
+ len = strlen(s);
+
+loop:
+ n = MIN(len, arrend(t->buf.b) - t->buf.c);
+ memcpy(t->buf.c, s, n);
+ t->buf.c += n;
+ len -= n;
+ if (len) {
+ tflush(t);
+ goto loop;
+ }
+}
+
+void
+tsetpen(Term *t, Pen new)
+{
+ int c;
+ ushort ic, in;
+ Pen cur = t->pen;
+ if (!memcmp(&new, &cur, sizeof(new)))
+ return;
+
+ /* attributes */
+ tfmt(t, t->esc.sgr, 9,
+ 0, /* standout */
+ new.state & PenUnderline,
+ new.state & PenReverse,
+ new.state & PenBlink,
+ new.state & PenDim,
+ new.state & PenBold,
+ new.state & PenInvis,
+ 0, /* protect */
+ 0); /* alt */
+
+ ic = cur.state & PenItalic;
+ in = new.state & PenItalic;
+ if (ic & ~in)
+ tfmt(t, t->esc.sgr_i0, 0);
+ else if (~ic & in)
+ tfmt(t, t->esc.sgr_i1, 0);
+
+ /* fg/bg color */
+ /* TODO: add a check for if the terminal supports true color */
+ /* TODO: deal w/ negative indices properly */
+ if (new.state & PenRGB) {
+ tfmt(t, t->esc.ext.rgbf, 3, new.rgb.fg.r, new.rgb.fg.g, new.rgb.fg.b);
+ tfmt(t, t->esc.ext.rgbb, 3, new.rgb.bg.r, new.rgb.bg.g, new.rgb.bg.b);
+ } else {
+ tfmt(t, t->esc.sgr_fg, 1, new.col.fg);
+ tfmt(t, t->esc.sgr_bg, 1, new.col.bg);
+ }
+
+ t->pen = new;
+}
+
+static
+void
+tfmt(Term *t, char *esc, int n, ...)
+{
+ int i;
+ long len;
+ va_list args;
+ unibi_var_t param[9];
+ char buf[64], *c = buf;
+
+ if (!esc)
+ panicf("no terminfo escape string given");
+
+ va_start(args, n);
+ for (i = 0; i < arrlen(param) && i < n; i++) {
+ param[i] = unibi_var_from_num(va_arg(args, int));
+ }
+ va_end(args);
+
+ len = unibi_run(esc, param, c, sizeof(buf));
+ if (len >= arrlen(buf)) {
+ c = ttmpbuf(t, len);
+ unibi_run(esc, param, c, len);
+ }
+
+ twrite(t, len, c);
+}
+
+/* absolute move */
+static
+int
+tgoto(Term *t, int row, int col)
+{
+ if (row != -1 && col != -1)
+ tfmt(t, t->esc.cup, 2, row, col);
+ else if (row != -1) {
+ if (!t->esc.vpa)
+ return 0;
+ tfmt(t, t->esc.vpa, 1, row);
+ } else if (col != -1) {
+ if (col == 0) {
+ twrite(t, 1, "\r");
+ return 1;
+ }
+ if (t->esc.hpa)
+ tfmt(t, t->esc.hpa, 1, col);
+ else if (t->esc.cuf) {
+ twrite(t, 1, "\r");
+ tfmt(t, t->esc.cuf, 1, col);
+ } else
+ return 0;
+ } else
+ return 0; /* unreachable */
+
+ return 1;
+}
+
+/* relative move */
+static
+void
+tjump(Term *t, int down, int right)
+{
+ if (down == 1 && t->esc.cud1)
+ tfmt(t, t->esc.cud1, 0);
+ else if (down == -1 && t->esc.cuu1)
+ tfmt(t, t->esc.cuu1, 0);
+ else if (down > 0)
+ tfmt(t, t->esc.cud, 1, down);
+ else if (down < 0)
+ tfmt(t, t->esc.cuu, 1, -down);
+
+ if (right == 1 && t->esc.cuf1)
+ tfmt(t, t->esc.cuf1, 0);
+ else if (right == -1 && t->esc.cub1)
+ tfmt (t, t->esc.cub1, 0);
+ else if (right > 0)
+ tfmt(t, t->esc.cuf, 1, right);
+ else if( right < 0)
+ tfmt(t, t->esc.cub, 1, -right);
+}
+
+static
+void
+tclear(Term *t)
+{
+ tfmt(t, t->esc.ed2, 0);
+}
+
+void
+tblit(Term *t, Window *win)
+{
+ int r, c, n, j;
+ Row *row;
+ char u[UTFmax+1] = {0};
+
+ j = 0;
+ tgoto(t, win->top, win->left);
+ for (r = 0; r < win->h; r++) {
+ row = win->row + r;
+ if (!row->dirty) {
+ j++;
+ continue;
+ }
+
+ if (j) {
+ tjump(t, j, 0);
+ j = 0;
+ }
+
+ for (c = 0; c < win->w; c++) {
+ tsetpen(t, row->cells[c].pen);
+ n = utf8·runetobyte(u, &row->cells[c].txt);
+ twrite(t, n, u);
+ }
+
+ row->dirty = 0;
+ }
+
+ tflush(t);
+}
+
+// -----------------------------------------------------------------------
+// testing
+
+int
+main()
+{
+ int i;
+ Term *t;
+ Window *win;
+
+ t = tmake();
+ win = t->root;
+ tclear(t);
+
+ win->pen = (Pen){
+ .state = PenNormal,
+ .col = {.fg=-1, .bg=-1},
+ };
+ for (i = 0; i < 2000; i++)
+ wputrune(win, 'a');
+
+ tblit(t, win);
+
+ win->cur.row = 10;
+ win->cur.col = 0;
+
+ win->pen = (Pen){
+ .state=PenNormal|PenRGB,
+ .rgb={.fg={200, 100, 100}, .bg={0, 0, 0} },
+ };
+
+ for (i = 0; i < 500; i++)
+ wputrune(win, 'b');
+
+ tblit(t, win);
+
+ sleep(5);
+ wscroll(win, 10);
+ tblit(t, win);
+ sleep(5);
+
+ tfree(t);
+}
diff --git a/src/libterm/term.h b/src/libterm/term.h
new file mode 100644
index 0000000..6bd2f6b
--- /dev/null
+++ b/src/libterm/term.h
@@ -0,0 +1,270 @@
+#pragma once
+
+#include <u.h>
+#include <libn.h>
+
+#include <termios.h>
+#include <unibilium.h>
+
+#define iota(x) 1 << (x)
+
+typedef struct RGB8 RGB8;
+typedef struct Pen Pen;
+
+typedef struct Dot Dot;
+typedef struct Cell Cell;
+typedef struct Row Row;
+typedef struct Buffer Buffer;
+typedef struct Window Window;
+
+typedef struct Node Node;
+typedef struct Key Key;
+typedef struct Input Input;
+
+typedef struct Term Term;
+
+struct RGB8
+{
+ uint8 r, g, b;
+};
+
+enum
+{
+ PenNormal = 0,
+ PenBold = iota(0),
+ PenDim = iota(1),
+ PenInvis = iota(2),
+ PenItalic = iota(3),
+ PenReverse = iota(4),
+ PenStrike = iota(5),
+ PenUnderline = iota(6),
+ PenBlink = iota(7),
+ /* ... */
+ PenRGB = iota(15),
+};
+
+struct Pen
+{
+ ushort state;
+ union {
+ /* 256 color (legacy) */
+ struct {
+ sshort fg : 8, bg : 8; /* 0 - 255 or COLOUR_DEFAULT */
+ } col;
+ /* true color (modern) */
+ struct {
+ RGB8 fg, bg;
+ } rgb;
+ };
+};
+
+/* outputs */
+struct Cell
+{
+ rune txt;
+ Pen pen;
+};
+
+struct Row
+{
+ Cell *cells;
+ uint dirty : 1;
+};
+
+struct Dot
+{
+ int row, col;
+};
+
+/*
+ * scroll.top & scroll.bot are pointers into the viewport.
+ *
+ * scroll back buffer
+ *
+ * scroll.buf->+----------------+-----+
+ * | | | ^ \
+ * | before | | | |
+ * current terminal content | viewport | | | |
+ * | | | |
+ * +----------------+-----+\ | | | s > scroll.above
+ * ^ | | i | \ | | i | c |
+ * | | | n | \ | | n | r |
+ * | | v | \ | | v | o |
+ * | | i | \ | | i | l /
+ * | buffer | s | >|<- scroll.index | s | l \
+ * h | | i | / | | i | |
+ * | | b | / | after | b | s > scroll.below
+ * | | l | / | viewport | l | i |
+ * v | | e | / | | e | z /
+ * +----------------+-----+/ | unused | | e
+ * <- maxw -> | scroll back | |
+ * <- w -> | buffer | | |
+ * | | | |
+ * | | | v
+ * scroll.buf + scroll.size->+----------------+-----+
+ * <- maxw ->
+ * <- w ->
+ */
+
+struct Buffer
+{
+ int w, h; /* dimension of buffer */
+ Pen pen; /* default attributes */
+ int maxw; /* allocated cells (maximal cols over time) */
+ Row *row; /* array of row pointers of size 'h' */
+ struct {
+ Row *buf;
+ Row *top;
+ Row *bot;
+ int size;
+ int index;
+ int above;
+ int below;
+ } scroll;
+ Dot cur, save; /* cursor position within buffer */
+};
+
+struct Window
+{
+ struct Buffer;
+ int top, left;
+ uchar curvis : 1;
+ uchar blink : 2;
+
+ Window *parent, *child, *link;
+};
+
+/* input */
+struct Key
+{
+ int type;
+ int mods;
+ uchar utf8[UTFmax+1];
+ union {
+ rune pt;
+ int num;
+ int sym;
+ char mouse[4];
+ } code;
+};
+
+struct KeyInfo
+{
+ int type;
+ int sym;
+ int modmask;
+ int modset;
+};
+
+struct Input
+{
+ int fd;
+ int flags;
+ int wait; /* in ms */
+
+ /* modifiers */
+ uchar closed : 1;
+ uchar started : 1;
+ uchar hasold : 1;
+
+ struct termios oldterm;
+
+ /* buffer */
+ struct {
+ long off;
+ uchar *b, *c, *e, bytes[256];
+ } rbuf;
+ struct {
+ uchar *s, bytes[256];
+ } ebuf;
+
+ /* key data */
+ Node *keys;
+ struct KeyInfo c0[32];
+};
+
+
+struct Term
+{
+ /* meta data */
+ char *name;
+ unibi_term *info;
+ struct {
+ uchar altscreen : 1;
+ uchar cursorvis : 1;
+ uchar mouse : 1;
+ } mode;
+ struct {
+ uchar bce : 1;
+ int colors;
+ } cap;
+
+ /* input capture */
+ Input input;
+
+ /* output display */
+ Window *root;
+ Pen pen;
+
+ /* raw text to pty */
+ int fd;
+ struct {
+ char *c, b[512];
+ } buf;
+
+ struct {
+ int len;
+ char *b;
+ } tmp;
+
+ /* info */
+ struct {
+ /* Positioning */
+ char *cup; // cursor_address
+ char *vpa; // row_address == vertical position absolute
+ char *hpa; // column_address = horizontal position absolute
+
+ /* Moving */
+ char *cuu; char *cuu1; // Cursor Up
+ char *cud; char *cud1; // Cursor Down
+ char *cuf; char *cuf1; // Cursor Forward == Right
+ char *cub; char *cub1; // Cursor Backward == Left
+
+ /* Editing */
+ char *ich; char *ich1; // Insert Character
+ char *dch; char *dch1; // Delete Character
+ char *il; char *il1; // Insert Line
+ char *dl; char *dl1; // Delete Line
+ char *ech; // Erase Character
+ char *ed2; // Erase Data 2 == Clear screen
+ char *stbm; // Set Top/Bottom Margins
+
+ /* formatting */
+ char *sgr; // Select Graphic Rendition
+ char *sgr0; // Exit Attribute Mode
+ char *sgr_i0, *sgr_i1; // SGR italic off/on
+ char *sgr_fg; // SGR foreground colour
+ char *sgr_bg; // SGR background colour
+
+ /* Mode setting/clearing */
+ char *sm_csr; char *rm_csr; // Set/reset mode: Cursor visible
+
+ /* augmentations to terminfo */
+ struct {
+ char *rgbf; // rgb foreground
+ char *rgbb; // rgb background
+ char *smxx; // strikethrough
+ char *smulx; // curly underline
+ } ext;
+ } esc;
+
+ Term *link;
+};
+
+/* functions */
+void tresize(Term *t);
+
+Window *wmake(Window *root, int top, int left, int w, int h, int scroll);
+void wresize(Window *root, int w, int h);
+void wputrune(Window *win, rune r);
+void wscroll(Window *win, int s);
diff --git a/src/libterm/window.c b/src/libterm/window.c
new file mode 100644
index 0000000..5d36c8b
--- /dev/null
+++ b/src/libterm/window.c
@@ -0,0 +1,408 @@
+#include "term.h"
+
+// -----------------------------------------------------------------------
+// buffers
+
+static
+void
+zero(Row *row, int start, int len)
+{
+ int i;
+ Cell cell = {
+ .txt = L' ',
+ .pen = {
+ .state = PenNormal,
+ .col.fg = -1,
+ .col.bg = -1,
+ },
+ };
+
+ for (i = start; i < len + start; i++)
+ row->cells[i] = cell;
+ row->dirty = 1;
+}
+
+static
+void
+roll(Row *start, Row *end, int count)
+{
+ int n = end - start;
+
+ /* enforce circularity */
+ count %= n;
+ if (count < 0)
+ count += n;
+
+ if (count) {
+ char buf[count * sizeof(Row)]; /* XXX: remove VLA */
+ memcpy(buf, start, count * sizeof(Row));
+ memmove(start, start + count, (n - count) * sizeof(Row));
+ memcpy(end - count, buf, count * sizeof(Row));
+
+ for (Row *row = start; row < end; row++)
+ row->dirty = 1;
+ }
+}
+
+/* buffer operations */
+static
+void
+bclear(Buffer *b)
+{
+ int i;
+ Cell cell = {
+ .txt = L' ',
+ .pen = {
+ .state = PenNormal,
+ .col.fg = -1,
+ .col.bg = -1,
+ },
+ };
+
+ for (i = 0; i < b->h; i++) {
+ Row *row = b->row + i;
+ for (int j = 0; j < b->w; j++) {
+ row->cells[j] = cell;
+ row->dirty = 1;
+ }
+ }
+}
+
+static
+void
+bfini(Buffer *b)
+{
+ int i;
+
+ for (i = 0; i < b->h; i++)
+ free(b->row[i].cells);
+
+ free(b->row);
+
+ if (b->scroll.size) {
+ for (i = 0; i < b->scroll.size; i++)
+ free(b->scroll.buf[i].cells);
+
+ free(b->scroll.buf);
+ }
+}
+
+static
+void
+bscroll(Buffer *b, int s)
+{
+ Row tmp;
+ int i, ssz = b->scroll.bot - b->scroll.top;
+
+ /* work in quanta of screen size */
+ if (s > ssz) {
+ bscroll(b, ssz);
+ bscroll(b, s - ssz);
+ return;
+ }
+ if (s < -ssz) {
+ bscroll(b, -ssz);
+ bscroll(b, s + ssz);
+ return;
+ }
+
+ b->scroll.above += s;
+ b->scroll.above = CLAMP(b->scroll.above, 0, b->scroll.size);
+
+ if (s > 0) {
+ if (b->scroll.size) {
+ for (i = 0; i < s; i++) {
+ tmp = b->scroll.top[i];
+ b->scroll.top[i] = b->scroll.buf[b->scroll.index];
+ b->scroll.buf[b->scroll.index] = tmp;
+
+ b->scroll.index++;
+ if (b->scroll.index == b->scroll.size)
+ b->scroll.index = 0;
+ }
+ } else
+ for (i = 0; i < s; i++)
+ zero(b->scroll.top+i, 0, b->maxw);
+ }
+
+ roll(b->scroll.top, b->scroll.bot, s);
+
+ if (s < 0) {
+ if (b->scroll.size) {
+ for (i = (-s) - 1; i >= 0; i--) {
+ b->scroll.index--;
+ if (b->scroll.index == -1)
+ b->scroll.index = b->scroll.size - 1;
+
+ tmp = b->scroll.top[i];
+
+ b->scroll.top[i] = b->scroll.buf[b->scroll.index];
+ b->scroll.buf[b->scroll.index] = tmp;
+ b->scroll.top[i].dirty = 1;
+ }
+ } else
+ for (i = (-s) - 1; i >= 0; i--)
+ zero(b->scroll.top+i, 0, b->maxw);
+ }
+}
+
+static
+void
+bresize(Buffer *b, int nrow, int ncol)
+{
+ int r, d;
+ Row *row = b->row;
+ Row *cur = row + b->cur.row;
+
+ if (b->h != nrow) {
+ /* scroll if we can */
+ if (cur >= row + nrow)
+ bscroll(b, b->cur.row - nrow + 1);
+ while (b->h > nrow) {
+ free(row[b->h - 1].cells);
+ b->h--;
+ }
+
+ row = realloc(row, sizeof(Row) * nrow);
+ }
+
+ if (b->maxw < ncol) {
+ /* expand each row */
+ for (r = 0; r < b->h; r++) {
+ row[r].cells = realloc(row[r].cells, sizeof(Cell) * ncol);
+ if (b->h < ncol)
+ zero(row + r, b->w, ncol - b->w);
+ row[r].dirty = 1;
+ }
+ /* expand the scroll buffer */
+ Row *sbuf = b->scroll.buf;
+ for (r = 0; r < b->scroll.size; r++) {
+ sbuf[r].cells = realloc(sbuf[r].cells, sizeof(Cell) * ncol);
+ if (b->w < ncol)
+ zero(sbuf + r, b->w, ncol - b->w);
+ }
+ b->maxw = b->w = ncol;
+ } else if (b->w != ncol) {
+ for (r = 0; r < b->h; r++)
+ row[r].dirty = 1;
+ b->w = ncol;
+ }
+
+ d = 0;
+ if (b->h < nrow) {
+ while (b->h < nrow) {
+ row[b->h].cells = calloc(b->maxw, sizeof(Cell));
+ zero(row + b->h, 0, b->maxw);
+ b->h++;
+ }
+
+ /* prepare for backfill */
+ if (cur >= b->scroll.bot - 1) {
+ d = b->row + nrow - cur - 1;
+ if (d > b->scroll.above)
+ d = b->scroll.above;
+ }
+ }
+
+ b->cur.row += row - b->row;
+ b->scroll.top = row;
+ b->scroll.bot = row + nrow;
+ b->row = row;
+
+ /* perform backfill */
+ if (d > 0) {
+ bscroll(b, -d);
+ b->cur.row += d;
+ }
+}
+
+static
+bool
+binit(Buffer *b, int cols, int rows, int scroll)
+{
+ int size;
+
+ b->pen.state = PenNormal;
+ b->pen.col.fg = b->pen.col.bg = -1;
+
+ size = MAX(scroll, 0);
+ if (size && !(b->scroll.buf = calloc(size, sizeof(Row))))
+ return false;
+
+ b->scroll.size = size;
+ bresize(b, rows, cols);
+
+ b->cur = (Dot){0};
+ b->save = b->cur;
+
+ return true;
+}
+
+static
+void
+bboundary(Buffer *b, Row **bs, Row **be, Row **as, Row **ae)
+{
+ if (bs)
+ *bs = nil;
+ if (be)
+ *be = nil;
+ if (as)
+ *as = nil;
+ if (ae)
+ *ae = nil;
+ if (!b->scroll.size)
+ return;
+
+ if (b->scroll.above) {
+ if (bs)
+ *bs = &b->scroll.buf[(b->scroll.index - b->scroll.above + b->scroll.size) % b->scroll.size];
+ if (be)
+ *be = &b->scroll.buf[(b->scroll.index-1 + b->scroll.size) % b->scroll.size];
+ }
+ if (b->scroll.below) {
+ if (as)
+ *as = &b->scroll.buf[b->scroll.index];
+ if (ae)
+ *ae = &b->scroll.buf[(b->scroll.index + b->scroll.below-1) % b->scroll.size];
+ }
+}
+
+static
+Row *
+browfirst(Buffer *b)
+{
+ Row *bstart;
+ if (!b->scroll.size || !b->scroll.above)
+ return b->row;
+ bboundary(b, &bstart, nil, nil, nil);
+ return bstart;
+}
+
+static
+Row *
+browlast(Buffer *b)
+{
+ Row *aend;
+ if (!b->scroll.size || !b->scroll.below)
+ return b->row + b->h - 1;
+ bboundary(b, nil, nil, nil, &aend);
+ return aend;
+}
+
+static
+Row *
+brownext(Buffer *b, Row *row)
+{
+ Row *before_start, *before_end, *after_start, *after_end;
+ Row *first = b->row, *last = b->row + b->h - 1;
+
+ if (!row)
+ return nil;
+
+ bboundary(b, &before_start, &before_end, &after_start, &after_end);
+
+ if (row >= first && row < last)
+ return ++row;
+ if (row == last)
+ return after_start;
+ if (row == before_end)
+ return first;
+ if (row == after_end)
+ return nil;
+ if (row == &b->scroll.buf[b->scroll.size - 1])
+ return b->scroll.buf;
+ return ++row;
+}
+
+static
+Row *
+bprevrow(Buffer *b, Row *row)
+{
+ Row *before_start, *before_end, *after_start, *after_end;
+ Row *first = b->row, *last = b->row + b->h - 1;
+
+ if (!row)
+ return nil;
+
+ bboundary(b, &before_start, &before_end, &after_start, &after_end);
+
+ if (row > first && row <= last)
+ return --row;
+ if (row == first)
+ return before_end;
+ if (row == before_start)
+ return nil;
+ if (row == after_start)
+ return last;
+ if (row == b->scroll.buf)
+ return &b->scroll.buf[b->scroll.size - 1];
+ return --row;
+}
+
+// -----------------------------------------------------------------------
+// windows
+
+Window *
+wmake(Window *root, int top, int left, int w, int h, int scroll)
+{
+ Window *child, *it;
+
+ child = calloc(1, sizeof(*child));
+ child->top = top;
+ child->left = left;
+ child->parent = root;
+ if (root) {
+ if (root->child) {
+ for (it = root->child; it->link != nil; it = it->link)
+ ;
+ it->link = child;
+ } else
+ root->child = child;
+
+ child->curvis = root->curvis;
+ child->blink = root->blink;
+ }
+
+ if (!binit((Buffer*)child, w, h, scroll)) {
+ free(child);
+ return nil;
+ }
+
+ return child;
+}
+
+void
+wfree(Window *win)
+{
+ free(win);
+}
+
+void
+wresize(Window *win, int w, int h)
+{
+ bresize((Buffer*)win, w, h);
+}
+
+/* TODO: more sophisticated damage tracking */
+void
+wputrune(Window *win, rune r)
+{
+ Row *row = win->row + win->cur.row;
+ Cell *cell = row->cells + win->cur.col;
+
+ cell->pen = win->pen;
+ cell->txt = r;
+
+ if (win->cur.col++ >= win->w) {
+ win->cur.col = 0;
+ if (win->cur.row++ >= win->h)
+ win->cur.row = win->h-1;
+ }
+ row->dirty = 1;
+}
+
+void
+wscroll(Window *win, int s)
+{
+ bscroll((Buffer*)win, s);
+}