Home

Awesome

Mesh Spreadsheet

Mesh is a data and code editor that feels like a spreadsheet.

This is Mesh v3. The formula language is ngn/k, and the backend logic is also written in ngn/k.

This is a very early release, and it may not receive any updates. See What needs work.

Sheets eval on each recalc, and those evals are not sandboxed! Take care with what you write in the formula bar.

Mesh v3 demo

Who is Mesh for?

Mesh is a spreadsheet program that fits into software's typical development and release workflows.

If you maintain load-bearing spreadsheets - files that are part of a processing pipeline - you might like Mesh.

What Mesh does

Usual spreadsheet things

table:+`existingColumn!1 2 3
table[`newColumn]:1+table`existingColumn

Unusual spreadsheet things

What Mesh doesn't do

Install

  1. Clone this repo.
  2. Get and build ngn/k.
  3. Make sure you have Python 3 and its websockets module installed. In Ubuntu, the latter is sudo apt install python3-websockets.
  4. Update the values in vars.py.
  5. Run server.py - this starts the backend.
  6. Start a second server to serve index.html. Try python3 -m http.server in the Mesh directory.
  7. Go to localhost:8000 in your browser.

Alternatively, you can skip steps 2-5 and run the backend server via Docker. Clone this repo, then in the repo's directory:

docker build -t mesh-spreadsheet .
docker run --publish 8765:8765 mesh-spreadsheet

You'll still need a way to access index.html per step 6 above, and so you'll need a copy of index.html available to serve (clone this repo again?). Maybe that can be done in a higher-level Docker Compose file.

How does Mesh work?

Mesh sheets are stored as ngn/k code in text files. The sheet is shown in your web browser, the client; it's connected via WebSockets to a backend server that does calculations and updates files on your disk.

When you change a cell:

If the data you're editing is stored in a file that lives outside the sheet, the server will instead:

Version 3 of Mesh updates cell calculation order when the sheet is written, not when it's run.

Cell meta-info

Mesh records information that's just for the Mesh app as a dictionary that appears just above the data's definition. The Mesh app can still see it in the parse tree and extract info from it, but because that name is immediately redefined by the next line, it doesn't affect the calculations.

Here's a list that's named amounts, located at G4, and formatted in AUD:

amounts:`number`loc!(`style`currency!("currency";"AUD");`G4)
amounts:1 2 3

Here's a read-write table connection to the file analysis.json:

B2:`path!,"analysis.json"
B2:`j?1:"analysis.json"

Here's a calculation that's exported to the file somePath.json:

B2:`exportTo!,"somePath.json"
B2:1 2 3 4

What needs work

Features

Packaging

Deep dive: editable dicts and tables

Sheets can store data in external files, or in the sheet source itself. This section talks about the latter.

Mesh provides UI handles for editing list literals: inserting, deleting, or in-place edits of elements of simple lists such as 1 2 3 and general lists such as (1;`sym). It can do this because the ngn/k AST has special representations of list literals, instead of just making them a function call. Mesh can just look at the first item in an AST list node to figure out what to do.

But this approach isn't clean for dicts and tables, since the AST represents them as calls of functions that can do things other than make a data structure. For example, ngn/k uses ! as make-dict and +! as make-table, but ! has overloads such as 'mod'. General lists are also represented in the AST as a function call, but that function is internal-only (5:), and it doesn't have any overloads, so Mesh knows that its arguments should be treated as list literal elements.

Conceivably Mesh could check the type and structure of the arguments to ! (and generate a UI handle if the args are editable), but it makes the backend code complex. If Mesh did go down this route, it might be good to require editable tables to use a composition ((+!)[`a;1 2]) rather than a simple function call (+![`a;1 2]): that way, the make-table function would appear as an easy-to-recognise single-node composition ((';+;!)) instead of being split across nodes in the AST.

Ideally, Mesh's formula language would:

  1. include special dict and table literal representations (ngn/k doesn't), that
  2. have unique AST representations.

Potentially those representations could literally be dict and table data structures. Then the backend could just edit them like we would in userland, and they'd also be much easier for Mesh to losslessly unparse: if dict literals just appeared in the AST as ! function calls, the unparser wouldn't know whether they intended a make-dict function call or a dict literal.

Ideally dict literals would be a list of key:value pairs, and table literals would be defined row-wise to flow with the portrait shape of a text file - even if that data was stored as columns behind the scenes.

Why the k family? Why not Python or JavaScript?

Officially, ngn/k is no longer supported. But Mesh could potentially be ported to other k dialects or other languages.

To work with Mesh's approach to spreadsheets, the language needs certain features:

  1. Its syntax should be simple and stable, so that it's (a) easy for Mesh to modify the AST and (b) fast to parse and unparse.
  2. Formulas for simple data processing, such as sums and table joins, should be short.
  3. It should have literals for lists, dicts, and tables. Those literals should have unique AST representations so that they are easy for Mesh to edit and can be losslessly round-tripped.

Bonus points if it has built-in serialisation formats (JSON, CSV) and is already on every machine or otherwise small enough to quickly download.

Thank you

Thanks to Arthur Whitney for inventing and refining k, to ngn for his implementation of k6, and to my family and friends for their support.