Uggly Documentation

Introduction

Uggly is a means to generate Terminal User Interfaces in a client-server architecture. Think of it as TUI over-the-wire (TUIOW). The client requests content from the server via gRPC protobuffers and the client handles rendering of that content. The server is sending "pages" of content one screen at a time. The protocol and page definitions take inspiration from CSS/HTML in that there are constructs such as DivBoxes, TextBlobs, Links, and Forms for example. It is opinionated in that only keyboard strokes are supported for link navigation.

There is a client compiled for Windows, Linux, and Mac. Servers can be written in any language that supports gRPC and protobuf (e.g, Go, Python, Java, etc.) and honors the uggly protocol. New clients could be written in other languages similar to the way that there are multiple web browsers today a la Firefox, Chrome, Edge, etc.


gif demo (44MB)
mp4 demo (11MB)

Windows Client running in Powershell terminal window connected to a python server displaying a dynamic table.


gif demo (55MB)
mp4 demo (7MB)

Windows Client running in Powershell terminal window connected to a Golang server handling a login workflow

Why

There is no doubt that GUI's are useful. For instance, there's no better way to interact with some systems than being able to see data in a tabular format. Today, if someone wants to write a GUI they have to use Web languages and view it in a browser or use desktop UI toolkits such as Qt, Swift, etc. Web apps are the most accessible because almost everyone already has a web browser but they're not always the best UX design and are often bloated and slow. Desktop apps are fast and highly optimized for their single-purpose (keyboard shortcuts are amazing) but less accessible because you have to download an app for every UI you want to use.

So if you're a developer and you're trying to share a UI with your users you're in a predicament. Do you put your time into a Web app or a Desktop app? Either of these options are non-trivial because despite their ubiquitous nature, they have steep learning curves and are often overkill for simple tasks.

Terminal User Interfaces have been around for a long time. They were quite common in the early days of desktop computing so they're nothing new or terribly novel. There's actually been a resurgence in TUI's over the past few years as people look for something simpler and more novel. The efficiencies that can be had when UI's are designed for use with a keyboard only are pretty incredible--never underestimate the power of human muscle memory. However, they're still essentially a desktop application--just like the Qt and Swift applications the users have to download the application when they want to use the UI. This makes them essentially a novelty because nobody is going to build any serious TUI because they would only have a limited number of users who would use it.

What if there was something that took some of the best of both worlds? The accessibility and flexibility of the browser and the efficiency of the TUI. This is what the uggly protocol is experimenting with. Offload the client side rendering of the TUI to the "browser" and let server developers focus on building "web apps" using concepts that they're familiar with (components, links, cookies, etc.). That way the end-users only have to download one client that lets them access any number of applications. Multi-platform support is built into the concept because every major operating system has a terminal.

There are no doubts that this will never really take off because right out of the gate it has no images, no mobile support and it's still highly unlikely that many people will opt to write a server-side TUI app instead of a web app. However, maybe making it a little bit easier could help make TUI's more ubiquitous. In any case, TUI's are a lot of fun to use and they can also be fun to write.

Try a Demo

  1. Download the client for your architecture here.
  2. Unpack the tarball.
  3. Then run the client in a terminal.
  4. You'll be presented with a blank screen. Try hitting (F1) and typing one of the following addresses in the address bar:
  5. You can explore those sites and if you want you can bookmark them by hitting (F7) so you don't have to type it again.

How it Works

It's kind of like a how a browser requests a page from a web server except that it's using a different protocol than HTML. They key difference is that there is no client side code (e.g., Javascript) and each screen that a user sees is generated on-demand by the server when the client makes the request. For, example, content that exceeds the size of the client's screen should be broken up into multiple pages by the server. Also, the server dictates what key-presses are available for link actions.

The below diagram may offer a better explanation: (I appologize to mobile users for wonky diagrams. Apparently monospace fonts are hard)



                               uggly Protocol Basic Concept (pseudocode)

      Client
    ┌-------------------------┐                            Server
    │ (generates PageRequest) │                          ┌------------------------------------------------┐
    │ ┌---------------------┐ │             ┌------------┤► (processes PageRequest                        │
    │ │PageRequest{         │ │             │            │          │          ▼                          │
    │ │ Name: home,         │ │             │            │          ▼                                     │
    │ │ Server: tui.app.com,│ │             │            │  (generates PageResponse)                      │
    │ │ Port: 8888}         │ │             │            │ ┌-------------------------------------------┐  │
    │ └---------------------┘ │  gRPC       │            │ │PageResponse{                              │  │
    │ GetPage(PageRequest)    ├-------------┘            │ │  Name: home,                              │  │
    └-------------------------┘                          │ │  DivBoxes: [div1=(x,y,height,width,color)]│  │
                                                         │ │  Elements: [                              │  │
    ┌------------------------------┐      gRPC           │ │     TextBlobs: [text1, text2],            │  │
    │  (processes PageResponse) ◄--┼---------------------┼-┤     Forms: [form1],                       │  │
    │             │                │                     │ │  ],                                       │  │
    │             ▼                │                     │ │  KeyStrokes: [                            │  │
    │  (renders components, sets   │                     │ │     "n" => page:news,                     │  │
    │   cookies, polls for         │                     │ │     "a" => page:about,                    │  │
    │  provided keystrokes)        │                     │ │  ],                                       │  │
    │                              │                     │ │  Cookies: [theme: "dark"],                │  │
    │  ┌------------------------┐  │                     │ │ }                                         │  │
    │  │┼----------------------┼│  │                     │ └-------------------------------------------┘  │
    │  ││                      ││  │                     │                                                │
    │  ││ ┌--┐ ┌---┐ ┌-------┐ ││  │                     └------------------------------------------------┘
    │  ││ └--┘ └---┘ └-------┘ ││  │
    │  ││                      ││  │
    │  │┼----------------------┼│  │
    │  └------------------------┘  │
    │                              │
    │  (key press generates new    │
    │   PageRequest, repeat        │
    │    process)                  │
    │                              │
    │                              │
    └------------------------------┘
Since there is already an existing client that you can use it may be more helpful to just talk about how one writes a server.

How to Build Servers

The only way to get more great TUI's is to have more people write more of them. I tried to make authoring servers easy and it can be a lot of fun once you get into it. There's something refreshing about using simple building blocks that are easy to understand that make for a very satisfying end product.

If you're familiar with gRPC then building servers should be fairly straightforward. Both the Python an Go starter guides from grpc.io were used to form the base of the following sections. There are also several example sites written in Golang and Python which will be linked to throughout the documentation. If you prefer to write servers in a different language then the uggly protobuf will be your only guide.

Hello-World Servers

If you're just getting started I would recommend writing in Python. The flexibility makes for much easier content generation and for server side you don't have to worry about the distribution and dependency problems.

For Python, start with the python sample hello world.

For Golang, start with the go sample hello world.

Generic Server Guide

The initial hurdle of writing uggly servers is understanding the core concepts and that is mostly language agnostic. This section will be all about understanding the basics, core design principles, and tips and tricks. All "code" in this section is pseudocode and is pointless to copy paste. You'll need to tweak it for whatever language you're writing your server in.

The heart of every server is the GetPage handler which accepts PageRequest and returns PageResponse. Let's take a look at both of these in detail.

PageRequest

Here's an excerpt from the uggly protocol docs of a PageRequest

A PageRequest contains the name of the desired page and some metadata about the cient height and width. The server can choose to ignore the width and height if it insists on statically sized content. Also, the server could generate a PageResponse saying something like "this server insists on a minimum height to view content" for example.

Field Type Label Description
name string The name of the page being requested
clientWidth int32 The width of the client at the time of the request
clientHeight int32 The height of the client at the time of the request
formData FormData repeated data from form submissions or could be used to send generic key value pairs
server string the intended server for the request
port string the intended port for the request
secure bool whether or not the connection should be secure (TLS)
sendCookies Cookie repeated Cookies that are intended to be sent from client to server
stream bool whether or not the request is for a page stream
Source: PageRequest

So from the server side this is what you'll be processing from a client in your GetPage function. The first thing to note is that a server only has one GetPage function that accepts PageRequest so if you want to serve more than one page you'll need to route requests to different handlers as demonstrated in this pseudocode:

func GetPage(pq PageRequest) (pr PageResponse) {
	if pq.Name == "foo" {
		return foo(pq) 
	}
	if pq.Name == "bar" {
		return bar(pq)
	}
}

func foo(PageRequest) (PageResponse) {
	// generate foo response
}

func bar(PageRequest) (PageResponse) {
	// generate bar response
}

The clientWidth and clientHeight fields may be of interest if you want to generate dynamically sized content.

func GetPage(pq PageRequest) (pr PageResponse) {
	pr.DivBoxes.Boxes.append(
		DivBox{
			Name: "main",
			Width: pq.ClientWidth / 2,
			Height: pq.ClientHeight / 2,
		}
	)
	pr.Elements.TextBlobs.append(
		TextBlob{
			Content: "lorem ipsum",
			Wrap: true,	
			DivBoxes: []string("main"),
		}
	)
	return pr
}

Alternatively, you could insist on a certain client size.

func GetPage(pq PageRequest) (pr PageResponse) {
	if pq.ClientWidth <= 500 {
		return clientTooSmall(pq)
	} else {
		return fixedSizeResponse(pq)
	}
}

func clientTooSmall(PageRequest) (PageResponse) {
	// generate response telling the user
	// their screen is too small
}

func fixedSizeResponse(pq PageRequest) (pr PageResponse) {
	pr.DivBoxes.Boxes.append(
		DivBox{
			Name: "main",
			Width: 499,
			Height: 100,
		}
	)
	pr.Elements.TextBlobs.append(
		TextBlob{
			Content: (lots of text)
			DivBoxes: []string("main"),
		}
	)
	return pr
}

You can almost always ignore the server, port, and secure parameters as those are mostly for the client side to establish a connection with the server. However, if you wanted you could reference them.

For now we'll skip over FormData and stream and talk about those later on.

PageResponse

Now that we've covered the PageRequest it's time to look into more detail at how we craft a response. Again, we'll refer to the documentation.

The PageResponse contains modular content that the server wishes the client to display in the form of DivBoxes, Elements, and Links.

Field Type Label Description
divBoxes DivBoxes divBoxes contains all of the divBoxes to be rendered for this page
elements Elements elements contains all of the non divBox elements to be rendered for this page
name string the name of the page to be rendered
keyStrokes KeyStroke repeated a list of keystrokes that the client should honor, these could be of type link, formActivation, or divScroll
setCookies Cookie repeated any cookies that the server is requesting the client to set for future requests
streamDelayMs int32 if the response is part of a stream then this is the time in milliseconds the client should wait before drawing the next request
Source: PageResponse

The PageResponse is the heart of the system. This is what the client renders to display pages, parse keystrokes, and activate forms. The server could have a list of hard coded PageResponse that it just returns to clients upon request or it could generate them dynamically based on changing conditions. Either way, there are two fundamental building blocks to every PageResponse -- the DivBoxes and the TextBlob which are stored under Elements in the response.

See an example of the use of DivBox and TextBlob in the below pseudocode with a pseudo rendering side by side:

Here we see two DivBoxes with two separate TextBlob for content. The boxes are placed so that there is buffer/padding between them.

func GetPage(pq PageRequest) (pr PageResponse) {
	pr.DivBoxes.Boxes.append(
		DivBox{
			Name: "main",
			Width: 10,
			Height: 10,
			StartX: 5,
			StartY: 5,
		}
	)
	pr.DivBoxes.Boxes.append(
		DivBox{
			Name: "main2",
			Width: 10,
			Height: 10,
			StartX: 20,
			StartY: 5,
		}
	)
	pr.Elements.TextBlobs.append(
		TextBlob{
			Content: "hello",
			DivBoxes: []string("main"),
		}
	)
	pr.Elements.TextBlobs.append(
		TextBlob{
			Content: "world",
			DivBoxes: []string("main2"),
		}
	)
	return pr
}


  ┌-----------------------------------------------------------┐
  │                                                           │
  │  ┌---------------------┐  ┌----------------------┐        │
  │  │ hello               │  │ world                │        │
  │  │                     │  │                      │        │
  │  │                     │  │                      │        │
  │  │                     │  │                      │        │
  │  │                     │  │                      │        │
  │  │                     │  │                      │        │
  │  │                     │  │                      │        │
  │  │                     │  │                      │        │
  │  └---------------------┘  └----------------------┘        │
  │                                                           │
  │                                                           │
  │                                                           │
  │                                                           │
  │                                                           │
  │                                                           │
  │                                                           │
  │                                                           │
  │                                                           │
  │                                                           │
  │                                                           │
  │                                                           │
  │                                                           │
  └-----------------------------------------------------------┘

Here we see two DivBox and a single TextBlob assigned to two different boxes. The Boxes are placed in such a way as there is no buffer.

func GetPage(pq PageRequest) (pr PageResponse) {
	pr.DivBoxes.Boxes.append(
		DivBox{
			Name: "main",
			Width: 10,
			Height: 10,
			StartX: 5,
			StartY: 5,
		}
	)
	pr.DivBoxes.Boxes.append(
		DivBox{
			Name: "main2",
			Width: 10,
			Height: 10,
			StartX: 10,
			StartY: 10,
		}
	)
	pr.Elements.TextBlobs.append(
		TextBlob{
			Content: "hello",
			DivBoxes: []string("main", "main2"),
		}
	)
	return pr
}


    ┌---------------------------------------┐
    │                                       │
    │  ┌---------------------┐              │
    │  │ hello               │              │
    │  │                     │              │
    │  │     ┌--------------- ------┐       │
    │  │     │ hello                │       │
    │  │     │                      │       │
    │  │     │                      │       │
    │  │     │                      │       │
    │  │     │                      │       │
    │  └-----┼                      │       │
    │        │                      │       │
    │        │                      │       │
    │        └----------------------┘       │
    │                                       │
    │                                       │
    │                                       │
    │                                       │
    │                                       │
    │                                       │
    │                                       │
    │                                       │
    │                                       │
    │                                       │
    └---------------------------------------┘

As you can see, each PageResponse is just a list of these basic elements.

KeyStrokes

The next topic that isn't exactly intuitive is that of the KeyStroke which can be included in any PageResponse as a list of actions that should be honored by the client. The following example shows how they can be used at a very basic level.

Here we have a server with two methods for returning a PageResponse, one for a page named foo and another for a page named bar. Each defines some content and also defines links that should be sent to the client when this response is generated. The client then loads those links whenever it renders the page and polls for them. When the user hits the keystroke it generates a new request (of type Link and requests that back to the server. Then bar sends back a link that can be used for requesting foo. Therefore, if the user sits there hitting f,b,f,b,f,b they'll just flip back and forth between the foo and bar pages.

func GetPage(pq PageRequest) (pr PageResponse) {
	if pr.Name == "foo" {
		return foo(pr)
	}
	if pr.Name == "bar" {
		return bar(pr)
	}
}

func foo(pq PageRequest) (pr PageResponse) {
	pr.DivBoxes.Boxes.append(
		DivBox{ Name: "main" }
	)
	pr.Elements.TextBlobs.append(
		TextBlob{
			Content: "foo: press b for bar",
			DivBoxes: []string("main"),
		}
	)
	pr.KeyStrokes.append(
		KeyStroke{
			KeyStroke: "b",
			Action: Link{	
				PageName: "bar",
			}
		}
	)
	return pr	
}

func bar(pq PageRequest) (pr PageResponse) {
	pr.DivBoxes.Boxes.append(
		DivBox{ Name: "main" }
	)
	pr.Elements.TextBlobs.append(
		TextBlob{
			Content: "bar: press f for foo",
			DivBoxes: []string("main"),
		}
	)
	pr.KeyStrokes.append(
		KeyStroke{
			KeyStroke: "f",
			Action: Link{	
				PageName: "foo",
			}
		}
	)
	return pr	
}

   ┌---------------------------------------┐
   │                                       │
   │    ┌------------------------------┐   │
   │    │ bar: press f for foo         │   │
   │    │                              │   │
   │    │                              │   │
   │    │                              │   │
   │    │                              │   │
   │    │                              │   │
   │    │                              │   │
   │    └------------------------------┘   │
   │                                       │
   │                                       │
   │                                       │
   └---------------------------------------┘


         (user presses 'f', a new PageRequest
          for 'foo' is created and sent to
          server)

   ┌---------------------------------------┐
   │                                       │
   │    ┌------------------------------┐   │
   │    │ foo: press b for bar         │   │
   │    │                              │   │
   │    │                              │   │
   │    │                              │   │
   │    │                              │   │
   │    │                              │   │
   │    │                              │   │
   │    └------------------------------┘   │
   │                                       │
   │                                       │
   │                                       │
   └---------------------------------------┘

So now that you understand some of the basic building blocks I think you can understand how it's possible to build rapid content navigation systems using only the keyboard.

TODO: Tutorial Sections for Forms, Streams, Cookies

How to Host Servers

You'll need some sort of compute for hosting as there's not many ways to host a gRPC server in a "serverless" fashion. Supposedly on GCP you can do it with Cloud Run but on traditional hosting or on AWS this means hosting on some sort of server or container. For AWS this is EC2, EKS, ECS, etc. AWS Application Load Balancers (ALB) support gRPC, so that's nice.

Docker is a good solution so you don't have to mess around with systemd wrappers, etc. The above demo sites (e.g., 'ugtps://tdo.bytester.net:443') are hosted on AWS ECS behind an ALB using the ECS native Blue/Green deployment with CodeDeploy. The actual hosting of the gRPC server was easy but the ECS bits with CodeDeploy were needlessly complex, IMO. Here is what the architecture looks like.


      Uggly Server Hosting on AWS ECS

                                 ┌--------------------------┐
                                 │ Route53                  │
                                 │                          │
                                 │  tdo.bytester.net        │
                                 └-----------┬--------------┘
                                             │
               ┌-----------------------------┼------------------------------┐
               │ EC2                         │                              │
               │           ┌-----------------▼----------------------┐       │
               │           │ ALB                                    │       │
               │           │     SSL                     SSL        │       │
               │           │   Listener                Listener     │       │
               │           │    TCP 443                TCP 1433     │       │
               │           │                                        │       │
               │           └--┬-----------------------------┬-------┘       │
               │              │                             │               │
               │  ┌-----------▼--------┐         ┌----------▼---------┐     │
               │  │ Target Group gRPC  │         │ Target Group gRPC  │     │
               │  │                    │         │                    │     │
               │  │                    │         │                    │     │
               │  └--------┬-----------┘         └-----------┬--------┘     │
               │           │                                 │              │
               └-----------┼---------------------------------┼--------------┘
                           │                                 │
                           │                                 │
                           │                                 │
        ┌------------------┼---------------------------------┼-------------------┐
        │   ECS Fargate    │                                 │                   │
        │                  │                                 │                   │
        │                  │                                 │                   │
        │        ┌---------▼------------┐            ┌-------▼--------------┐    │
        │        │ uggly-server-login   │            │ puggly-server        │    │
        │        │    (go container)    │            │  (python container)  │    │
        │        │      TCP 80          │            │    TCP 50051         │    │
        │        └----------------------┘            └----------------------┘    │
        │                                                                        │
        └------------------------------------------------------------------------┘