Skip to content

Instantly share code, notes, and snippets.

@FeepingCreature
Created May 20, 2021 21:52
Show Gist options
  • Select an option

  • Save FeepingCreature/7e87f8db26e64e334f0ed3cd9cdf34f3 to your computer and use it in GitHub Desktop.

Select an option

Save FeepingCreature/7e87f8db26e64e334f0ed3cd9cdf34f3 to your computer and use it in GitHub Desktop.
module hello;
macro import cx.macros.assert;
macro import cx.macros.cimport;
import c_header("ctype.h");
import c_header("netdb.h");
import c_header("stdio.h");
import c_header("string.h");
import c_header("sys/errno.h");
import c_header("sys/ioctl.h");
import c_header("sys/socket.h");
import c_header("termios.h");
import c_header("time.h");
import c_header("unistd.h");
import helpers : itoa, print;
import std.string;
void main() {
/*Buffer render_(int rows, int cols) {
return new Buffer(rows, cols);
}
void enter_() { }
auto term = new Terminal(&render_, &enter_);
term.enableRawMode;
keyDebugLoop;
term.disableRawMode;
return;*/
string channel = "#lesswrong";
auto irc = new IRC;
irc.connect("irc.libera.chat", 6667);
irc.sendln("NICK feep[cx]");
irc.sendln("USER feep_cx * * :feep's cx tui client");
while (!irc.ready) {
irc.pump;
}
irc.sendln("JOIN " ~ channel);
mut Terminal term;
Buffer refresh(int rows, int cols) {
irc.pump;
// print("render(" ~ rows.itoa ~ ", " ~ cols.itoa ~ ")");
auto buf = new Buffer(rows, cols);
buf.clear(Color.blue);
with (buf) {
// title line
go(0, 0);
bg(Color.grey); fg(Color.red); print(" H");
fg(Color.black); print("ell World");
fillRow;
// chat box
fg(Color.lwhite); bg(Color.blue); buf.printPrettyBox(1, 0, buf.rows - 2, buf.cols - 1);
// status line
go(-1, 0);
bg(Color.grey); fg(Color.black);
print(" Welcome to TUI IRC Demo");
printRight("[ " ~ Time.now.local ~ " ] ");
mut int row;
int visibleMessages = min(cast(int) irc.messages.length, buf.rows - 4);
for (auto msg <- irc.messages[$ - visibleMessages .. $]) {
go(row + 2, 2);
// TODO line break
// TODO something about chaining after print is busted, figure out and fix
// I GOT IT, the chained calls evaluate the lhs twice!! TODO FIX
bg(Color.blue); fg(Color.black); print("[" ~ msg.time.local ~ "]");
skip(1); fg(Color.white); print("<");
bg(Color.grey); fg(Color.lwhite); print(msg.name); bg(Color.blue);
fg(Color.white); print(">");
skip(1); fg(Color.lwhite); print(msg.line);
row += 1;
}
// input line
go(-2, 2);
bg(Color.black); fg(Color.lwhite);
print("╡");
bg(Color.blue);
printRight("╞══╝");
fg(Color.lyellow);
go(-2, 3);
print(" ");
print(term.inputLine);
}
return buf;
}
void enter() {
string line = term.inputLine;
if (line == "/q" || line == "/quit") {
irc.sendln("QUIT");
term.running = false;
return;
}
if (line.startsWith("/me ")) {
string action = line[4 .. $];
irc.messages ~= (Time.now, "feep[cx]", "* feep[cx] " ~ action);
irc.sendln("PRIVMSG " ~ channel ~ " :\x01ACTION " ~ action ~ "\x01");
return;
}
irc.messages ~= (Time.now, "feep[cx]", line);
irc.sendln("PRIVMSG " ~ channel ~ " :" ~ line);
}
term = new Terminal(&refresh, &enter);
term.updateLoop;
}
class IRC
{
int sock;
string partialLineBuf;
(Time time, string name, string line)[] messages;
bool ready;
this() {
this.sock = socket(/*AF_INET*/PF_INET, SOCK_STREAM, 0);
}
void connect(string host, int port) {
auto address = new Address(host, port);
int ret = .connect(this.sock, address.info.ai_addr, address.info.ai_addrlen);
assert(ret == 0);
}
void process(string line) {
// puts("\x1B[?1049h");
// print(line);
if (line.startsWith("PING ")) {
sendln("PONG " ~ line[5 .. $]);
return;
}
if (line.find(" 376 ") != -1) {
// end of motd
this.ready = true;
return;
}
auto parts = line.split(" ");
string mask = parts[0];
string cmd = parts[1];
if (cmd == "PRIVMSG") {
string channel = parts[2];
mut string msg = parts[3 .. $].join(" ");
msg = msg[msg.find(":") + 1 .. $];
// print("<" ~ mask.user ~ "> " ~ msg);
messages ~= (Time.now, mask.user, msg);
}
// puts("\x1B[?1049l");
}
void flushLine() {
auto pos = this.partialLineBuf.find("\n");
assert(pos != -1);
mut string line = this.partialLineBuf[0 .. pos];
if (line[$ - 1] == "\r"[0]) line = line[0 .. $ - 1];
this.partialLineBuf = this.partialLineBuf[pos + 1 .. $];
process(line);
}
void sendln(string msg) {
send(msg);
send("\n");
}
void send(string msg) {
mut int sent;
while (sent < msg.length) {
int ret = cast(int) .send(this.sock, &msg[sent], msg.length - sent, 0);
assert(ret > 0);
sent += ret;
}
}
void pump() {
while (true) {
auto buf = new char[](1024);
int ret = cast(int) recv(this.sock, buf.ptr, cast(int) buf.length, /*MSG_DONTWAIT*/0x40);
if (ret == -1 && (cxruntime_errno() == EAGAIN/* || cxruntime_errno() == EWOULDBLOCK*/))
return;
assert(ret > 0);
this.partialLineBuf ~= buf[0 .. ret];
while (this.partialLineBuf.find("\n") != -1) {
flushLine;
}
}
}
}
string user(string mask) {
if (mask.startsWith(":") && mask.find("!") != -1) {
return mask[1 .. mask.find("!")];
}
return mask;
}
struct Time {
time_t t;
static Time now() {
Time t;
time(&t.t);
return t;
}
string local() {
tm timeinfo;
localtime_r(&t, &timeinfo);
return itoa_fill(2, timeinfo.tm_hour) ~ ":"
~ itoa_fill(2, timeinfo.tm_min) ~ ":"
~ itoa_fill(2, timeinfo.tm_sec);
}
}
extern(C) int cxruntime_errno();
class Address
{
addrinfo* info;
this(string name, int port) {
char* namep = name.toStringz;
char* portp = port.itoa.toStringz;
mut addrinfo hints;
hints.ai_family = /*AF_INET*/PF_INET;
hints.ai_socktype = SOCK_STREAM;
hints.ai_protocol = 0;
int ret = getaddrinfo(namep, portp, &hints, &this.info);
assert(ret == 0);
}
}
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 skip(int cols) {
this.col += cols;
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;
string inputLine;
Buffer delegate(int, int) render;
void delegate() enter;
bool running;
(int rows, int cols) getSize() {
mut winsize sz;
ioctl(0, TIOCGWINSZ, &sz);
return (sz.ws_row, sz.ws_col);
}
this(this.render, this.enter) {
auto sz = getSize;
this.currentScreenState = new Buffer(sz.rows, sz.cols);
this.running = true;
}
void update() {
auto sz = getSize;
auto newbuf = render(sz.rows, sz.cols);
paint(newbuf);
}
void updateLoop() {
enableRawMode;
mut char c;
while (running) {
update;
c = 0;
read(STDIN_FILENO, &c, 1);
if (c == cast(char) 0) continue;
if (c == cast(char) 10) {
enter();
inputLine = "";
continue;
}
if ((c == cast(char) 127 || c == cast(char) 8) && inputLine.length > 0) {
inputLine = inputLine[0 .. $ - 1];
continue;
}
if (!iscntrl(c)) {
inputLine ~= c;
}
}
disableRawMode;
}
void paint(Buffer newBuf) {
// TODO compare tuples
bool refresh = newBuf.rows != currentScreenState.rows || newBuf.cols != currentScreenState.cols;
mut Color fg, bg;
for (int i <- 0 .. newBuf.rows) {
auto line = newBuf.line(i);
mut bool mustGoTo = true;
for (int k <- 0 .. newBuf.cols) {
auto tile = line[k];
if (!refresh) {
auto oldLine = currentScreenState.line(i);
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 min(int a, int b) {
if (a < b) return a;
return b;
}
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