Skip to content

Instantly share code, notes, and snippets.

@FeepingCreature
Created May 20, 2021 19:30
Show Gist options
  • Save FeepingCreature/44e370fef18235140eb364b305177c0d to your computer and use it in GitHub Desktop.
Save FeepingCreature/44e370fef18235140eb364b305177c0d to your computer and use it in GitHub Desktop.
module hello;
macro import cx.macros.cimport;
import c_header("ctype.h");
import c_header("stdio.h");
import c_header("string.h");
import c_header("sys/ioctl.h");
import c_header("termios.h");
import c_header("time.h");
import c_header("unistd.h");
import helpers : itoa, print;
void main() {
mut Terminal term;
Buffer render(int rows, int cols) {
// print("render(" ~ rows.itoa ~ ", " ~ cols.itoa ~ ")");
auto buf = new Buffer(rows, cols);
buf.clear(Color.blue);
time_t rawtime;
tm timeinfo;
time(&rawtime);
localtime_r(&rawtime, &timeinfo);
string time = itoa_fill(2, timeinfo.tm_hour) ~ ":" ~ itoa_fill(2, timeinfo.tm_min) ~ ":" ~ itoa_fill(2, timeinfo.tm_sec);
// Title row
buf.go(0, 0)
.bg(Color.grey).fg(Color.red).print(" H")
.fg(Color.black).print("ello World").fillRow;
buf.fg(Color.lwhite).bg(Color.blue).printPrettyBox(1, 0, buf.rows - 2, buf.cols - 1);
buf.go(-1, 0)
.bg(Color.grey).fg(Color.black).print(" Welcome to TUI IRC Demo").printRight("[ " ~ time ~ " ] ");
buf.go(0, 0);
return buf;
}
term = new Terminal(&render);
// keyDebugLoop;
term.updateLoop;
}
void printPrettyBox(Buffer buf, int r0, int c0, int r1, int c1) {
buf.go(r0, c0).print("╔");
for (int i <- 0 .. c1 - c0 - 1) {
buf.print("═");
}
buf.print("╗");
for (int i <- r0 + 1 .. r1) {
buf.go(i, c0).print("║");
buf.go(i, c1).print("║");
}
buf.go(r1, c0).print("╚");
for (int i <- 0 .. c1 - c0 - 1) {
buf.print("═");
}
buf.print("╝");
}
string itoa_fill(int width, int value) {
mut string res = itoa(value);
while (res.length < width) res = "0" ~ res;
return res;
}
// TODO variadics
extern(C) int ioctl(int, int, void*);
struct Tile
{
string ch; // may be unicode, TODO int
Color fg, bg;
// TODO default gen
bool eq(Tile other) {
return ch == other.ch && fg == other.fg && bg == other.bg;
}
}
enum Color
{
black,
red,
green,
yellow,
blue,
magenta,
cyan,
white,
grey,
lred,
lgreen,
lyellow,
lblue,
lmagenta,
lcyan,
lwhite
}
class Buffer
{
int rows, cols;
Tile[] text;
// print state, todo factor out into renderer?
Color fg_, bg_;
int row, col;
this(this.rows, this.cols) {
this.fg_ = Color.lwhite;
this.bg_ = Color.black;
this.text = new Tile[](this.rows * this.cols);
clear(this.bg_);
}
Buffer print(string msg) {
// TODO cutoff
mut int i;
while (i < msg.length) {
auto chlen = msg[i .. $].utf8NextLength;
auto ch = msg[i .. i + chlen];
this.text[this.row * this.cols + this.col] = Tile(ch, this.fg_, this.bg_);
i += chlen;
this.col += 1;
}
return this;
}
Buffer printRight(string msg) {
// TODO cutoff
int start = this.row * this.cols;
for (int i <- this.col .. this.cols - msg.utf8Length) {
this.text[start + i] = Tile(" ", this.fg_, this.bg_);
this.col += 1;
}
mut int k;
for (int i <- 0 .. msg.utf8Length) {
auto chlen = msg[k .. $].utf8NextLength;
auto ch = msg[k .. k + chlen];
k += chlen;
this.text[start + this.col] = Tile(ch, this.fg_, this.bg_);
this.col += 1;
}
return this;
}
Buffer go(mut int row, mut int col) {
if (row < 0) row = this.rows - -row;
if (col < 0) col = this.cols - -col;
this.row = row;
this.col = col;
return this;
}
Buffer fg(Color fg) {
this.fg_ = fg;
return this;
}
Buffer bg(Color bg) {
this.bg_ = bg;
return this;
}
Buffer clear(Color bg) {
this.bg_ = bg;
for (int i <- 0 .. this.text.length) {
this.text[i] = Tile(" ", this.fg_, this.bg_);
}
return this;
}
Buffer fillRow() {
int base = this.row * this.cols + this.col;
for (int i <- 0 .. this.cols - this.col) {
this.text[base + i] = Tile(" ", this.fg_, this.bg_);
}
}
Tile[] line(int row) {
return this.text[this.cols * row .. this.cols * (row + 1)];
}
}
void puts(string msg) {
write(STDOUT_FILENO, msg.ptr, msg.length);
}
class Terminal
{
termios original;
Buffer currentScreenState;
Buffer delegate(int, int) render;
(int rows, int cols) getSize() {
mut winsize sz;
ioctl(0, TIOCGWINSZ, &sz);
return (sz.ws_row, sz.ws_col);
}
this(this.render) {
auto sz = getSize;
this.currentScreenState = new Buffer(sz.rows, sz.cols);
}
void update() {
auto sz = getSize;
auto newbuf = render(sz.rows, sz.cols);
// TODO compare tuples
if (newbuf.rows != currentScreenState.rows || newbuf.cols != currentScreenState.cols) {
repaint(newbuf);
return;
}
diffpaint(newbuf);
}
void updateLoop() {
enableRawMode;
mut char c;
while (c != "q"[0]) {
update;
c = 0;
read(STDIN_FILENO, &c, 1);
if (c == cast(char) 0) continue;
/*if (iscntrl(c)) {
print(itoa(c));
} else {
print(itoa(c) ~ " (" ~ c ~ ")");
}*/
}
disableRawMode;
}
void repaint(Buffer newBuf) {
puts("\x1B[2J");
mut Color fg, bg;
for (int i <- 0 .. newBuf.rows) {
auto line = newBuf.line(i);
for (int k <- 0 .. newBuf.cols) {
auto tile = line[k];
if (fg != tile.fg || bg != tile.bg) {
int translate(Color c) {
int i = cast(int) c;
if (i > 7) return 90 + i - 8;
return 30 + i;
}
puts("\x1B[" ~ itoa(translate(tile.fg)) ~ ";" ~ itoa(translate(tile.bg) + 10) ~ "m");
fg = tile.fg;
bg = tile.bg;
}
puts(tile.ch);
}
if (i != newBuf.rows - 1) puts("\n");
}
puts("\x1B[" ~ itoa(newBuf.row + 1) ~ ";" ~ itoa(newBuf.col + 1) ~ "H");
this.currentScreenState = newBuf;
}
void diffpaint(Buffer newBuf) {
mut Color fg, bg;
for (int i <- 0 .. newBuf.rows) {
auto oldLine = currentScreenState.line(i);
auto line = newBuf.line(i);
mut bool mustGoTo = true;
for (int k <- 0 .. newBuf.cols) {
auto tile = line[k];
auto oldTile = oldLine[k];
if (tile.eq(oldTile)) {
mustGoTo = true;
continue;
}
if (mustGoTo) {
puts("\x1B[" ~ itoa(i + 1) ~ ";" ~ itoa(k + 1) ~ "H");
mustGoTo = false;
}
if (fg != tile.fg || bg != tile.bg) {
int translate(Color c) {
int i = cast(int) c;
if (i > 7) return 90 + i - 8;
return 30 + i;
}
puts("\x1B[" ~ itoa(translate(tile.fg)) ~ ";" ~ itoa(translate(tile.bg) + 10) ~ "m");
fg = tile.fg;
bg = tile.bg;
}
puts(tile.ch);
}
}
puts("\x1B[" ~ itoa(newBuf.row + 1) ~ ";" ~ itoa(newBuf.col + 1) ~ "H");
// TODO why does this leak otherwise?
this.currentScreenState.text = [];
this.currentScreenState = newBuf;
}
void enableRawMode() {
tcgetattr(STDIN_FILENO, &this.original);
mut auto raw = this.original;
raw.c_lflag = raw.c_lflag & (ECHO | ICANON).bitflip;
// raw.c_cc[VMIN] = 0;
// raw.c_cc[VTIME] = 1;
raw.c_cc._6 = 0;
raw.c_cc._5 = 1;
tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw);
puts("\x1B[?1049h");
}
void disableRawMode() {
tcsetattr(STDIN_FILENO, TCSAFLUSH, &original);
puts("\x1B[?1049l");
}
}
int bitflip(int i) { return -i - 1; }
void keyDebugLoop() {
mut char c;
while (c != "q"[0]) {
c = 0;
read(STDIN_FILENO, &c, 1);
if (c == cast(char) 0) continue;
if (iscntrl(c)) {
print(itoa(c));
} else {
print(itoa(c) ~ " (" ~ c ~ ")");
}
}
}
// TODO std.uni, by-codepoint iteration
int utf8NextLength(string text)
{
// see https://en.wikipedia.org/wiki/UTF-8#FSS-UTF
if (text.length < 1) return 0;
int ch0 = text[0];
if (ch0 < 128) return 1;
assert(ch0 >= 192);
assert(text.length >= 2);
if (ch0 < 224) return 2;
assert(text.length >= 3);
if (ch0 < 240) return 3;
assert(text.length >= 4);
if (ch0 < 248) return 4;
assert(text.length >= 5);
if (ch0 < 252) return 5;
assert(text.length >= 6);
if (ch0 < 254) return 6;
assert(false);
}
int utf8Length(mut string text) {
mut int i;
while (text.length) {
i += 1;
text = text[text.utf8NextLength .. $];
}
return i;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment