efjimm/Cellulator
Spreadsheet program written in Zig
master
master
master
Cellulator is a terminal based, vim-like spreadsheet calculator.
Cellulator primarily targets Linux. MacOS and FreeBSD 0.14 targets compile successfully but are otherwise untested.
Requirements:
Clone the repo and run zig build -Doptimize=ReleaseSafe
to build the project. The resulting
binary will be in zig-out/bin by default.
Run zig build --summary new test
to run the tests.
Cellulator is currently in early development. Expect missing features. If you actually intend on using Cellulator, build it in ReleaseSafe mode to catch any latent bugs.
The maximum sheet size is $2^{32}$ rows by $2^{32}$ columns.
Cellulator is a modal program like vim. There are several modes in Cellulator:
Normal mode allows you to move around the sheet using vim-like motions and perform various operations. Visual mode is used for performing operations on a range of cells interactively.
Command modes are for editing the text in the command line. vim-style text editing is available for editing the command line buffer, hence the multiple command modes.
Cellulator differentiates between statements and commands. They can both be entered via the command line. Commands are similar to vim commands, providing a command line experience to interact with the application. Statements use an expressive, functional language and are used for setting cell expressions. The reason for the separation is due to the difference in usage between statements and commands - having to surround your filepath in quotes to save would be annoying, and allowing strings without quotes in the expression language would be even worse.
There are currently two types of statements in cellulator:
let CELL = EXPR
EXPR
CELL is the address of a cell, e.g. A0
, GL3600
EXPR is any expression
The let
statement assigns the expression a cell.
A lone expression will print the result of evaluating the expression to the command line.
Commands can be entered via placing a colon character as the first character of a command. Pressing ':' in normal mode will do this automatically. What follows is a list of currently implemented commands. Values surrounded in {} are optional.
A help dialogue for commands can be displayed by passing a -h
flag to the command anywhere in
the argument list. Passing the -h
flag will only display the help dialogue and will not run the
command.
Currently implemented commands are:
:w
:e
:q
:q!
:fill
:fill-expr
:bw
:be
:undo
:redo
:delete
:delete-cols
:delete-rows
:insert-cols
:insert-rows
:text-align
:set
:unset
:yank
:put
:p
:put-adjust
:pa
:sheet-close
:sheet-close!
:sc
:sc!
:sheet-rename
:go
Type the command name followed by -h
to see usage information for each command.
Expressions consist of number/string literals, cell literals, builtins, and operators. They can be
used on the right-hand side of the =
in a let
statement.
Number literals consist of a string of ASCII digit characters (0-9) and underscores, with at most one decimal point. Underscores are ignored and are only used for visual separation of digits. Underscores are not preserved when assigned to cells.
Examples:
1000000
1_000_000
1_234_567.000_089
String literals consist of arbitrary text surrounded by single or double quotes. There is currently no way to escape quotes inside of quotes.
Examples:
Cell literals evaluate to the value of a cell, or to a cell reference if used in a context that
requires a cell reference. This behaviour is called automatic reference coercion. For example,
the binary :
and prefix *
operators require cell references as operands, so cell literals passed
to these operators will be automatically coerced to a cell reference. Automatic reference coercion
can be prevented by dereferencing a cell literal, which will always yield the cell's value
regardless of context. Automatic reference coercion only happens for cell literals.
The value returned by a cell literal will be updated if the expression contained by that cell changes.
Examples:
A0
GP359
crxp65535
Cell references and ranges are first class values in Cellulator.
A cell reference is a reference to a cell, rather than the cell's value. Evaluating a cell reference
does not evaluate the cell. Cell references can be created via the reference-of operator &
, in
addition to implicit conversions from cell literals. Cell references can be dereferenced with the
dereference operator *
(prefix.)
Examples:
&A0
&ZZ200
Cell ranges represent all cells in the inclusive rectangular area between two positions. They are
created by the range operator :
. This operator takes two cell references as its operands.
Examples:
&A0:&D20
A0:D20
Implicit coercion from cell literal to cell referenceCell ranges represent all cells in the inclusive square area between two positions. Cell ranges can
only be used in builtin functions. They are defined as two cell references separated with a colon
:
character.
Examples:
A0:B0
(Contains 2 cells)A0:A0
(Contains 1 cell)D6:E3
(Contains 8 cells)Functions are first class values in Cellulator. They can capture values from outer functions in a
closure. They can be assigned to cells like any other value. A function is defined with the syntax
|ARGS| BODY
.
Examples:
-- Function with no arguments returning 2
|| 2
-- Function with no arguments returning 3, immediately invoked
(|| 3)()
-- Function with 1 argument
|x| x * x
-- Function with 3 arguments
|x, y, z| x * y + z
-- Function that returns a new function, capturing the argument
let a0 = |x| |y| x + y
a0(3)(4) == 7
-- b0 is now a function that takes one argument and adds 5 to it
let b0 = a0(5)
let c0 = b0(10)
c0 == 15
-- Function that takes another function as an argument
let a0 = |f| f(3)
a0(|x| x * x) == 9
Cellulator has logical and equality operators but does not have a boolean data type. Instead, values have truthiness. An empty cell or the number zero is interpreted as false, and anything else is interpreted as true.
Volatile expressions are updated on every recalculation. Volatile expressions are created by
accesses through a dynamic cell references or range. For example, if a cell's expression was
**A0 + 2
then that cell would have to be marked volatile, as it would need to be updated whenever
the cell referenced by A0 changes.
Note that only accesses through a dynamic range are volatile. The builtin function @width
for
example does not access through its argument, which means you can pass a dynamic range without
making the expression volatile. Certain builtin functions will automatically dereference any
reference arguments they receive, but will only dereference one level. As such, the arguments to
these functions are a reference context and cell literals passed to them will undergo automatic
reference coercion. Because the function only dereferences once, if the cell value is a reference it
will not be dereferenced further. This prevents innocuous looking function invocations from making
volatile accesses without explicit opt-in by using the * operator on the cell literal argument.
The following is a list of all operators that return number values. They try to convert non-number operands (e.g. strings) to numbers. Strings that cannot be converted to numbers will return an InvalidCoercion error.
+
Positive value (absolute value)-
Negative value (* -1)+
Addition-
Subtraction*
Multiplication/
Division%
Modulo division (remainder)(
and )
Grouping operatorsThe following operators return 0 for false and 1 for true:
>
Greater than<
Less than>=
Greater than or equal to<=
Less than or equal to==
Equal!=
Not equaland
Returns its first operand if it is false, otherwise returns its second operand.or
Returns its first argument if not false, otherwise returns its second operand.!
Logical not. Returns either 0 or 1 depending on the truthiness of its operand.Note that due to the and
/or
operators returning their arguments instead of a true/false value
they can be used like conditionals. For example, A0 > B0 and "Greater" or "Not greater"
will
evaluate to "Greater"
when A0 > B0 and "Not greater" otherwise.
The following is a list of operators that return string values. They try to convert non-string operands to strings. Converting a number to a string never fails outside of OOM situations.
#
Concatenates the strings on the left and right. Examples:'This is a string' # ' that has been concatenated'
'1: ' # A0
A0 # B0
&
Reference-of operator. Coerces a cell literal to a cell reference. This operator is
not usually necessary due to automatic reference coercion. Examples:&a0
&ZZ20
*
Dereference operator. Coerces a cell reference to the value of the cell. This operator
can be used on a cell literal to prevent automatic reference coercion. This works because this
operator takes a reference, so using it on a cell literal will automatically coerce that literal
to a reference and then dereference that, resulting in the cell's value.:
Range operator. Takes cell references as operands and returns a range whose top left
and bottom right points are anchored on the given cell references. Examples:&A0:&D20
A0:D20
automatic reference coercion makes this work.*A0:D20
prevent the automatic reference coercion of A0, and use the value stored at A0 as the
top left anchor. For instance, if let A0 = &C10
then the expression *A0:D20
would evaluate
to C10:D20
.There are two types of builtins: functions and constants. Builtin functions are used in the
format @builtin_name(argument_1, argument_2, argument_3, ...)
. Different builtin functions take
and return different types and numbers of arguments. Builtin constants are used in the format
@builtin_name
.
The following builtin functions take an arbitrary number of arguments and coerce them to numbers. They may also take ranges as arguments.
@sum
Returns the sum of its arguments@prod
Returns the product of its arguments@avg
Returns the average of its arguments.@min
Returns the smallest argument.@max
Returns the largest argument.@count
Returns the count of number variables.@countAll
Returns the count of any type of variable.The following builtin functions take one argument and coerce it to a number:
@sqrt
Returns the square root of the given number.@round
Rounds the given number to the nearest integer. If two integers are equally close, rounds away from zero.@floor
Returns the largest integral value not greater than the given number.@ceil
Returns the smallest integral value not less than the given number.@log(base, x)
Returns the logarithm of x for the provided base.The following builtins take one argument and coerce it to a string. They may not take a range as an argument.
@upper
Returns the ASCII uppercase version of its argument as a string.@lower
Returns the ASCII lowercase version of its argument as a string.@len
Returns the number of grapheme clusters in the given string.The following builtins are constants and do not take any arguments or parentheses:
@pi
Archimede's constant (n).@e
Euler's number (e).The following builtins take a single range as an argument:
@width
Returns the width of the given range.@height
Returns the heigh of the given range.Motions in command normal and command operator pending modes can be prefixed by a number, which will repeat the following motion that many times. This does not currently work for any of the inside or around motions.
1-9
Set count0
Set count if count is not zero, otherwise move cursor to the first populated cell on the
current rowj
, Down
Move cursor downk
, Up
Move cursor uph
, Left
Move cursor leftl
, Right
Move cursor rightC-f
Page downC-b
Page upC-d
Half page downC-u
Half page up:
Enter command insert mode=
Enter command insert mode, with text set to let cellname =
, where cellname is the cell
under the cursore
Edit the expression of the current celldd
, x
Delete the cell under the cursorEsc
Dismiss status message$
Move cursor to the last populated cell on the current rowgc
Move cursor to the count columngr
Move cursor to the count rowgg
Move cursor to the first cell in the current columnG
, ge
Move cursor to the last cell in the current columnw
Move cursor to the next populated cellb
Move cursor to the previous populated cellf
Increase decimal precision of the current columnF
Decrease decimal precision of the current column+
Increase width of current column if non-empty-
Decrease width of current column if non-emptyaa
Fit column width to contentsu
UndoU
Redo<
Align text under cursor to the left>
Align text under cursor to the right|
Align text undor cursor to the centergn
Go to the next sheetgp
Go to the previous sheetC-wq
Close the current sheetyy
Yank selected cellp
Put yanked cells at cursor, copying expression exactlyP
Put yanked cells at cursor, adjusting cell references in the expressionsic
Insert count columns at the cursordc
Delete count columns at the cursorir
Insert count rows at the cursordr
Delete count rows at the cursorEsc
, C-[
Enter normal moded
, x
Delete the cells in the given rangeo
Swap cursor and anchorAlt-j
Move selection down count timesAlt-k
Move selection up count timesAlt-h
Move selection left count timesAlt-l
Move selection right count timesyy
Yank selected cells and enter normal modeReturn
Write the selected range to the command bufferEsc
Cancel select modeEsc
Enter command normal modeReturn
, C-m
, C-j
Submit current command or completionBackspace
, Del
Delete the character before the cursor and move backwards oneC-p
, Up
Previous commandC-n
, Down
Next commandC-a
, Home
Move cursor to the beginning of the lineC-e
, End
Move cursor to the end of the lineC-f
, Right
Move cursor forward one characterC-b
, Left
Move cursor backward one characterC-w
Delete the word before the cursorC-u
Delete all text before the cursorC-k
Delete all text after the cursorC-v
Enter visual select modeC-p
, <Up>
History prevC-n
, <Down>
History next<Tab>
Next completion<S-<Tab>>
Previous completionEsc
Leaves command mode without submitting command1-9
Set count0
Set count if count is not zero, otherwise move cursor to the first populated cell on the
current rowh
, Left
Move cursor left count timesl
, Right
Move cursor right count timesk
, Up
Previous command count timesj
, Down
Next command count timesi
Enter command insert modeI
Enter command insert mode and move to the beginning of the linea
Enter command insert mode and move one character to the rightA
Enter command insert mode and move to the end of the lines
Delete the character under the cursor and enters command insert modeS
Deletes all text and enters command insert modex
Delete the character under the cursord
Enter operator pending mode, with deletion as the operator actionc
Enter operator pending mode, with change (delete and enter insert mode) as the operator actionD
Deletes all text at and after the cursorC
Deletes all text at and after the cursor, and enters command insert modew
Moves cursor to the start of the next word count timesW
Moves cursor to the start of the next WORD count timesb
Moves cursor to the start of the previous word count timesB
Moves cursor to the start of the previous WORD count timese
Moves cursor to the end of the next word count timesE
Moves cursor to the end of the next WORD count timesM-e
Moves cursor to the end of the previous word count timesM-E
Moves cursor to the end of the previous WORD count times$
, End
Move cursor to the end of the linek
, <Up>
History prevj
, <Down>
History nextPerforms the given operation on the text delimited by the next motion
iw
Inside wordaw
Around wordiW
Inside WORDaW
Around WORDi(
, i)
Inside parenthesesa(
, a)
Around parenthesesi[
, i]
Inside bracketsa[
, a]
Around bracketsi{
, i}
Inside bracesa{
, a}
Around bracesi<
, i>
Inside angle bracketsa<
, a>
Around angle bracketsi"
Inside double quotesa"
Around double quotesi'
Inside single quotesa'
Around single quotesi`
Inside backticksa`
Around backticksCellulator integrates Lua for scripting and configuration purposes. Currently the API is a very small proof of concept and not at all stable.
At startup Cellulator runs the Lua file at $XDG_CONFIG_HOME/cellulator/init.lua
. A global variable
zc
is exposed which provides functionality to interact with Cellulator.
Here is an example init.lua
:
zc.events:subscribe('Start', function()
-- Set theme on startup
zc:command('set theme mytheme')
end)
zc.events:subscribe('SetCell', function(pos)
-- Move the cursor down one
zc:set_cursor{ x = pos.x, y = pos.y + 1 }
-- Set the cell directly to the right to (expr) * 2
zc:set_cell({ x = pos.x + 1, y = pos.y }, '(' .. expr .. ') * 2')
end)
zc.events:subscribe('UpdateFilePath', function(sheet_name, new_path)
-- Display a status message when we open a new file
zc:status{'Opened file "' .. new_path .. '" in ' .. sheet_name}
end)
Cellulator has a mechanism for emitting events, which Lua code can register handlers for. Lua code can also register and emit its own events.
When a theme is set using the :set THEME_NAME
command, the corresponding theme file at
${XDG_CONFIG_HOME}/cellulator/themes/terminal/THEME_NAME.lua
is loaded. For the terminal UI, theme
files are lua files that when executed return a table containing the theme definition. The keys of
the returned table correspond to a specific UI element to be styled. Omitted keys are left at their
default value.
Here is an (extremely ugly) example theme:
local bg = '#ff0000'
local fg = '#c0c5ce'
local high_bg = '#6984ae'
local high_fg = '#292d36'
local high = { fg = high_fg, bg = high_bg }
local plain= { fg = fg, bg = bg }
return {
filepath = { fg = fg, bg = bg, attrs = { 'bold', 'underline' } },
status_line = { fg = '#00ff00', bg = '#0000ff' },
command_line = { fg = '#00ff00', bg = '#0000ff' },
expression = { fg = '#00ff00', bg = bg },
column_heading_unselected = plain,
column_heading_selected = high,
row_heading_unselected = plain,
row_heading_selected = high,
cell_blank_selected = high,
cell_blank_unselected = plain,
cell_number_selected = high,
cell_number_unselected = plain,
cell_text_selected = high,
cell_text_unselected = plain,
cell_error_selected = high,
cell_error_unselected = plain,
selected_sheet = high,
unselected_sheet = plain,
}
See src/Tui.zig
for a list of valid element names.