# Collaborative ASCII Drawing With Telnet
*by [@bwasti](https://twitter.com/bwasti)*
****

If the server isn't swamped, you can try it out (hold shift to erase, arrow keys to move):

```
telnet bram.town
```
If you're on a newer mac, you may need to `brew install telnet`.
It doesn't come by default these days...

![](https://i.imgur.com/QfIJWob.gif)

The full code listing can be found [here](https://github.com/bwasti/bram.town/blob/main/server.ts).
I run it with `bun server.ts`.

****

### User Input

For details, check out the xterm docs: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html

We can tell xterm terminals to start
sending mouse information by sending
connected users an ANSI sequence like this:

```
socket.write("\x1b[?1003;1006;1015h");
```
This concatenates 3 different parameters,
which we can find in the docs:

![](https://i.imgur.com/K1qmfPb.png)

I found the `EXT_MODE` parameters to be particularly important,
as they can cause the *client* to crash if not set correctly
(when using a macOS version at least).

This was a nightmare to debug, because the errors looked like this:

![](https://i.imgur.com/k5MH6vO.png)

and they only happened... sometimes!
When moving the cursor beyond ~90 or so characters
to the right, the client would crash but not disconnect.

But this wasn't consistent.
And that was because I was playing with a
really cool project for inspiration:

**mapscii.me!**

![](https://i.imgur.com/RbjbedX.gif)

and every time I used mapscii,
my terminal was correctly set to the extended mode.

Because terminals are state based and can be manipulated
by the characters they print.  Neat.

I guess we should clean up when people leave...

```
user.socket.write("\x1b[?1003;1006;1015l");
```

### Rendering Output

But just getting user input isn't enough,
we also need to "render" the output.

This happens to be a lot easier, and ChatGPT wrote that code for me:

First, move the cursor to the top left corner:
```
user.socket.write("\x1b[H");
```
Then, dump the correctly sized contents and hide the cursor:

```
user.socket.write(screenString + "\x1b[?25l");
```

Done.

Although I didn't explain "correctly sized contents" at all.
This turns out to be another tricky mess of hard to understand control code sequences.
Luckily for me, ChatGPT actually *did* understand this stuff,
so I didn't have to write/debug that part of the code.

Getting the user window size is done with telnet's NAWS option.
NAWS stands for "Negotiate About Window Size."
The server requests NAWS by outputting a series of telnet protocol bytes:

```
socket.write(Buffer.from([IAC, DO, NAWS]));
```

and the client will respond if it can handle such a request
as well as the data associated with it.
I assume it can and just read the data:

```
if (data.length >= 5) {
  user.width = (data[0] << 8) + data[1];
  user.height = (data[2] << 8) + data[3];
  //scheduleRender(user);
}
```

You can read more about this stuff here: http://www.pcmicro.com/netfoss/telnet.html
****

### Features

Once I got this working I added some random features,
but I'm hoping to add more.
Drawing and panning around a global canvas with multiple users
is kind of a litmus test for these things, so I just did that.

You can move around with WASD or arrow keys.

You can click and drag to draw pixels.
These can be "layered" by repeated drawing to get a typical "ASCII art" effect.
If you hold shift while dragging around, you erase pixels.

Your cursor is represented by a little circle and everyone can see it.

---

if you'd like to follow me [@bwasti](https://twitter.com/bwasti) :^}