From ca3d69c5acc4b501af25a5819c679508ae42f081 Mon Sep 17 00:00:00 2001
From: Toni Spets <toni.spets@iki.fi>
Date: Mon, 25 Jul 2016 02:24:23 +0300
Subject: [PATCH] Simple implementation of less

Still TODO:

 * Read only up to terminal height of lines at startup
 * Continue reading up to EOF as the user scrolls down

This commit includes scr_* code to manage a text screen. It isn't
yet quite ready regarding UTF-8 but it reserves enough space to
hold any valid sequence.

Other upcoming user for scr_* would be vi.

NOTE: This version simplifies scr_* family of functions.
---
 lib/interestingtimes.c |  87 +++++++++++++++++++++++++++++++++++
 lib/lib.h              |   9 ++++
 toys/pending/less.c    | 121 +++++++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 217 insertions(+)
 create mode 100644 toys/pending/less.c

diff --git a/lib/interestingtimes.c b/lib/interestingtimes.c
index 62670cb..ee1e9b4 100644
--- a/lib/interestingtimes.c
+++ b/lib/interestingtimes.c
@@ -1,6 +1,7 @@
 /* interestingtimes.c - cursor control
  *
  * Copyright 2015 Rob Landley <rob@landley.net>
+ * Copyright 2016 Toni Spets <toni.spets@iki.fi>
  */
 
 #include "toys.h"
@@ -241,3 +242,89 @@ void tty_sigreset(int i)
   tty_reset();
   _exit(i ? 128+i : 0);
 }
+
+// at minimum, this allocates 16 kB of memory for buffers
+struct screen *scr_init(void)
+{
+  int i;
+  struct screen *scr = xzalloc(sizeof(struct screen));
+
+  scr->width = 80;
+  scr->height = 25;
+  terminal_probesize(&scr->width, &scr->height);
+
+  scr->buf[0] = xzalloc(sizeof(struct ptr_len) * scr->height);
+  scr->buf[1] = xzalloc(sizeof(struct ptr_len) * scr->height);
+
+  // allocate exactly two screenfuls of ASCII
+  for (i = 0; i < scr->height; i++) {
+    scr->buf[0][i].len = scr->width;
+    scr->buf[0][i].ptr = xzalloc(scr->buf[0][i].len);
+    scr->buf[1][i].len = scr->width;
+    scr->buf[1][i].ptr = xzalloc(scr->buf[1][i].len);
+  }
+
+  tty_esc("0m");
+  tty_esc("2J");
+  xset_terminal(1, 1, 0);
+  sigatexit(tty_sigreset);
+
+  return scr;
+}
+
+void scr_update(struct screen *scr)
+{
+  unsigned i, from_len, to_len;
+
+  tty_esc("s"); // save cursor
+
+  // diff and update screen
+  for (i = 0; i < scr->height; i++) {
+    struct ptr_len *from = &scr->buf[1][i];
+    struct ptr_len *to = &scr->buf[0][i];
+
+    // calculate real length of lines
+    for (from_len = 0; from_len < from->len && ((char *)from->ptr)[from_len]; from_len++);
+    for (to_len = 0; to_len < to->len && ((char *)to->ptr)[to_len]; to_len++);
+
+    if (to_len == from_len && memcmp(from->ptr, to->ptr, to_len) == 0)
+      continue;
+
+    printf("\033[%u;1H\033[2K%.*s", i + 1, to_len, (char *)to->ptr);
+  }
+
+  tty_esc("u"); // restore cursor
+
+  // swap buffers
+  void *tmp = scr->buf[0];
+  scr->buf[0] = scr->buf[1];
+  scr->buf[1] = tmp;
+
+  fflush(stdout);
+}
+
+// TODO: make ANSI escape and UTF-8 aware (x offset & total visible length)
+void scr_printf(struct screen *scr, unsigned x, unsigned y, const char *fmt, ...)
+{
+  va_list va;
+  int len;
+  struct ptr_len *l;
+
+  if (x >= scr->width || y >= scr->height) return;
+
+  l = &scr->buf[0][y];
+
+  va_start(va, fmt);
+  len = vsnprintf(l->ptr + x, l->len - x, fmt, va);
+  va_end(va);
+
+  // if we truncated, try again with enough space
+  if (len >= l->len) {
+    free(l->ptr);
+    l->len = len + 1;
+    l->ptr = xzalloc(l->len);
+    va_start(va, fmt);
+    vsnprintf(l->ptr + x, l->len - x, fmt, va);
+    va_end(va);
+  }
+}
diff --git a/lib/lib.h b/lib/lib.h
index f97940a..2e5dd0f 100644
--- a/lib/lib.h
+++ b/lib/lib.h
@@ -260,6 +260,15 @@ void tty_jump(int x, int y);
 void tty_reset(void);
 void tty_sigreset(int i);
 
+struct screen {
+  unsigned width;
+  unsigned height;
+  struct ptr_len *buf[2];
+};
+struct screen *scr_init(void);
+void scr_update(struct screen *scr);
+void scr_printf(struct screen *scr, unsigned x, unsigned y, const char *fmt, ...);
+
 // net.c
 int xsocket(int domain, int type, int protocol);
 void xsetsockopt(int fd, int level, int opt, void *val, socklen_t len);
diff --git a/toys/pending/less.c b/toys/pending/less.c
new file mode 100644
index 0000000..55f0eca
--- /dev/null
+++ b/toys/pending/less.c
@@ -0,0 +1,121 @@
+/* less.c - opposite of more
+ *
+ * Copyright 2016 Toni Spets <toni.spets@iki.fi>
+ *
+ * No standard.
+
+USE_LESS(NEWTOY(less, "<1>1", TOYFLAG_USR|TOYFLAG_BIN))
+
+config LESS
+  bool "less"
+  default n
+  help
+    usage: less FILE
+*/
+
+#define FOR_less
+#include "toys.h"
+
+GLOBALS(
+  struct linestack *ls;
+  struct screen *scr;
+
+  // view offset
+  int x_off;
+  int y_off;
+  int y_max;
+)
+
+static void redraw(void)
+{
+  unsigned y;
+
+  for (y = 0; y < TT.scr->height - 1; y++) {
+    if (y + TT.y_off < TT.ls->len) {
+      if (TT.x_off < TT.ls->idx[y + TT.y_off].len) {
+        scr_printf(TT.scr, 0, y, "%.*s",
+                   TT.ls->idx[y + TT.y_off].len - TT.x_off,
+                   (char *)TT.ls->idx[y + TT.y_off].ptr + TT.x_off);
+      } else {
+        scr_printf(TT.scr, 0, y, "");
+      }
+    } else {
+      scr_printf(TT.scr, 0, y, "~");
+    }
+  }
+
+  scr_printf(TT.scr, 0, TT.scr->height - 1, toybuf);
+  scr_update(TT.scr);
+}
+
+void less_main(void)
+{
+  char scratch[16];
+  unsigned run = 1;
+
+  if (!isatty(0) || !isatty(1))
+    error_exit("no tty");
+
+  if (!(TT.ls = linestack_load(*toys.optargs)))
+    TT.ls = xzalloc(sizeof(struct linestack));
+
+  TT.scr = scr_init();
+
+  TT.y_max = TT.ls->len - TT.scr->height + 1;
+  if (TT.y_max < 0) TT.y_max = 0;
+
+  sprintf(toybuf, "%s%s", *toys.optargs, (TT.y_off == TT.y_max ? " (END)" : ""));
+
+  *scratch = 0;
+  do {
+    int key;
+
+    printf("\033[%u;%luH", TT.scr->height, strlen(toybuf) + 1);
+    redraw();
+
+    key = scan_key(scratch, -1);
+
+    sprintf(toybuf, ":");
+
+    switch (key) {
+      case 'q':   run = 0; break;
+      case 0x100: // arrow up
+      case 'k':   if (TT.y_off > 0) TT.y_off--; break;
+      case 0x101: // arrow down
+      case 0xD:   // return
+      case 'j':   if (TT.y_off + TT.scr->height <= TT.ls->len) TT.y_off++; break;
+      case 0x102: // arrow right
+      case 'l':   TT.x_off += TT.scr->width; break;
+      case 0x103: // arrow left
+      case 'h':   if (TT.x_off > 0) TT.x_off -= TT.scr->width; break;
+      case 0x15:  // ^U
+        if (TT.y_off - (int)(TT.scr->height / 2) >= 0) TT.y_off -= TT.scr->height / 2;
+        else TT.y_off = 0;
+        break;
+      case 0x4:   // ^D
+        if (TT.y_off + (TT.scr->height / 2) <= TT.y_max) TT.y_off += TT.scr->height / 2;
+        else TT.y_off = TT.y_max;
+        break;
+      case 0x2:   // ^B
+      case 0x104: // pg up
+        if (TT.y_off - (int)TT.scr->height >= 0) TT.y_off -= TT.scr->height;
+        else TT.y_off = 0;
+        break;
+      case 0x6:   // ^F
+      case 0x20:  // space
+      case 0x105: // pg dn
+        if (TT.y_off + TT.scr->height <= TT.y_max) TT.y_off += TT.scr->height;
+        else TT.y_off = TT.y_max;
+        break;
+      default:
+        if (CFG_TOYBOX_DEBUG)
+          sprintf(toybuf, "Unhandled key %d (%Xh)", key, key);
+    }
+
+    if (TT.y_off == TT.y_max)
+      sprintf(toybuf, "(END)");
+
+  } while (run);
+
+  tty_reset();
+}
-- 
2.7.4
