My jam project this year is called Flowshell. It is a visual shell that jettisons the entire concept of the command line.
If you haven't yet, please watch the 10-minute video demo:
Tech
Flowshell is written in Go, using Raylib for the platform layer, and Clay for UI layout.
This is a bit of a hodgepodge, but a hodgepodge of components I like. I chose Go because I have experience doing process management in it, and it is overall my most-used language. There are also good Raylib bindings for Go. There are not great Clay bindings for Go, as far as I can tell. The first day of the jam was spent just setting up my own.
By day 3 I was convinced I had made a mistake and should have chosen other tech. In the end, though, I think I made the correct decisions in every way. Making my own Clay bindings was a very tedious up-front investment, but paid off hugely later on as I was able to customize every aspect of the Clay integration myself.
Raylib of course makes drawing various shapes extremely easy and I find it to be a great jamming tool. No complaints there.
Go is another matter. I think Go was the right choice, but only because of how familiar I already am with it. It certainly makes some code very pleasant, like reading and writing CSVs—a robust standard library helps a lot. But man, it is actually just the most painfully inexpressive language ever. Writing Clay elements is pain.
But! Because I controlled the whole Clay integration, I was able to add lots of nice utilities. I'll talk more about Clay later, but for this section, the important thing to note is that I was able to add more functions to my bindings layer that interact differently with C than the typical CLAY()
macros. I was also able to add type aliases with much terser names.
Node Implementation
In past node editor projects, I have gotten very lost in the implementation. All nodes have some things in common, but also many things that are different, and only sometimes is different data necessary...in the past I have tried using Go interfaces for this, but it has always been a strange disaster.
This time the megastruct/plex discourse finally sunk in, and I just made everything a big struct:
type Node struct { ID int Pos V2 Name string Pinned bool InputPorts []NodePort OutputPorts []NodePort Action NodeAction Valid bool Running bool done chan struct{} ResultAvailable bool Result NodeActionResult InputPortPositions []V2 OutputPortPositions []V2 DragRect rl.Rectangle } type NodeAction interface { UpdateAndValidate(n *Node) UI(n *Node) Run(n *Node) <-chan NodeActionResult // TODO: Cancellation! }
The only polymorphic part of this is Action NodeAction
, which is in fact a Go interface. But this makes sense; you need a few function pointers and possibly a bit of extra state to handle the different logic of each node, which boils down to editor logic, UI, and execution. Each thing really just mutates stuff on the node, which is to say it stores the results of the work so that you're not running most of it every frame like my past self (an insane person).
As a particular example of how my design philosophy has changed: in the past I would absolutely have had the input and output port types come from the interface somehow, from some kind of method. Now I just have fields...that you can mutate whenever you want, including in editor logic.
This made all the nodes in the system effortlessly polymorphic. Want to derive the output type from the input type? Just mutate OutputPorts[0].Type
in UpdateAndValidate()
. Want to have a variable number of input ports controllable by the user? Just modify InputPorts
in UI()
.
I cannot stress enough how simple everything is when you just stop doing fancy things with the type system and dump everything into a struct. It works!
Structured Data Implementation
Flowshell has a structured data format inspired by (stolen from) Nushell. Nushell has very good ideas and is also completely intolerable to use. The good part is the data: lists, records, and tables, which can gracefully accommodate many forms of structured data you encounter in a shell, such as CSV or JSON. One particularly nice thing is that you can easily convert between these formats using Nushell as the intermediary.
I designed a simple type system for this project, with this being the core:
type FlowTypeKind int const ( FSKindAny FlowTypeKind = iota // not valid for use on a FlowValue FSKindBytes FSKindInt64 FSKindFloat64 FSKindList FSKindRecord FSKindTable ) type FlowType struct { Kind FlowTypeKind ContainedType *FlowType // for lists and tables Fields []FlowField // for records } type FlowValue struct { Type *FlowType BytesValue []byte Int64Value int64 Float64Value float64 ListValue []FlowValue RecordValue []FlowValueField TableValue [][]FlowValueField } type FlowValueField struct { Name string Value FlowValue }
I opted to have just one data/string type (Bytes), one integer type (Int64), and one floating point type (Float64). The composite types are lists, records, and tables, like in Nushell.
This is pretty easy to work with, and I was able to make a simple type checking algorithm that could, say, tell me two list types contain different values. I'm sure this part of the project could absolutely explode in complexity, but it works well for now.
Sadly I did not have time during the jam to get the JSON conversion working. This would have opened up some fun demo opportunities, such as the ability to ingest data from a REST API, pluck out some values, build a table, and dump the result to CSV.
Is Clay any good?
Yes, extremely.
Despite the difficulty making bindings, and the horribly clunky initializer syntax of Go, Clay was very pleasant to use. It is very well designed and maps closely to how I have been trained to think about layout from my years as a web dev. That may sound like a dig, but I swear it's not—it basically just means that it does flexbox very well.
Clay had an answer for almost every situation I threw at it—which is quite something, given that I did a pretty wide variety of layouts for this project. The one big exception was the lack of a grid or table layout, and a seeming inability to measure the size of an element in isolation so I can just do it myself. I think it would be possible to work around, but more experimentation is needed.
The best thing about Clay's design is that it is very data-first. It leaves a pretty bad first impression because of how cumbersome it is to define an element, but after taking the time to build some utilities you like, it becomes much smoother:
// Before clay.CLAY(clay.ID("Foo"), clay.ElementDeclaration{ Layout: clay.LayoutConfig{ LayoutDirection: clay.TopToBottom, Sizing: clay.Sizing{ Width: clay.SizingFixed(24), Height: clay.SizingFixed(24), }, Padding: clay.Padding{Left: 16, Right: 16, Top: 8, Bottom: 8}, }, Border: clay.BorderElementConfig{ Width: clay.BorderWidth{Left: 1, Right: 1, Top: 1, Bottom: 1}, Color: Gray, }, }, func() { ... }) // After clay.CLAY(clay.ID("Foo"), clay.EL{ Layout: clay.LAY{ LayoutDirection: clay.TopToBottom, Sizing: WH(24, 24), Padding: PVH(S2, S3), }, Border: clay.B{Width: BA, Color: Gray}, }, func() { ... })
This kind of flexibility is genuinely fantastic. I found myself using my type aliases and utilities for probably 85% of all layouts, and it is the perfect opportunity to introduce variables for consistency (e.g. my S2
and S3
spacing sizes). This would be even better in literally any other language but Go because of how absurdly verbose Go's initializers are, even with my aliases.
One thing I find particularly effective is Clay's floating elements, which are absolute positioning on steroids. In CSS, the following layout would be a huge pain in the ass because of the need to position the bottom of the menu against the top of the text box:
Well, in Clay, the floating code is simply:
Floating: clay.FLOAT{ AttachTo: clay.AttachToParent, AttachPoints: clay.FloatingAttachPoints{ Parent: clay.AttachPointLeftTop, Element: clay.AttachPointLeftBottom, }, }
Finally, someone who actually understands layout.
Future work
I could probably spend a month just adding more and more nodes to the system. But, there are many more important things on the list if anyone (including myself) is ever to use this software for anything real:
- Saving / loading projects
- Undo / redo
- Zooming in and out
- Ability to drag and drop files out of Flowshell
- Better keyboard shortcuts / auto-wiring to selected node
As far as nodes, though, I need to add:
- Arithmetic operations (that work across composites)
- More aggregation functions (e.g. percentiles)
- Table operations (pick columns, get column as list, transpose)
- Record operations (get fields jq-style)
I dunno. There's probably 150 useful operations out there waiting to be implemented in the first month alone.
Will I continue working on this? Maybe. It is a hodgepodge of jam code. But I do like it and I would like to be able to use it in my day to day life.