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. The main difference is that statements can be read from a file - this is how Cellulator saves sheet state to disk.
There is currently only one type of statement in cellulator:
let CELL = EXPR
CELL is the address of a cell, e.g. A0
, GL3600
EXPR is any expression
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 references, cell ranges, 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 references evaluate to the value of another cell. The value returned by a cell reference will be updated if the expression contained by that cell changes.
Examples:
A0
GP359
crxp65535
Cell 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)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.
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 it's first operand if it is false, otherwise returns it's second operand.or
Returns it's first argument if not false, otherwise returns it's second operand.!
Logical not. Returns either 0 or 1 depending on the truthiness of it's 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'This is a string' # ' that has been concatenated'
'1: ' # A0
A0 # B0
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.