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:
- Zig master
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
Cellulator is a modal program like vim. There are several modes in Cellulator:
- normal
- visual
- visual select
- command normal
- command insert
- command operator pending
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:
10000001_000_0001_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:
- 'This is a string'
- 'Double "quotes" inside of single quotes'
- "Single 'quotes' inside of double quotes"
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:
A0GP359crxp65535
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:&D20A0:D20Implicit coercion from cell literal to cell reference
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)
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) == 9Cellulator 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.
- unary
+Positive value (absolute value) - unary
-Negative value (* -1) - binary
+Addition - binary
-Subtraction *Multiplication/Division%Modulo division (remainder)(and)Grouping operators
The 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 equal
andReturns its first operand if it is false, otherwise returns its second operand.orReturns 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: ' # A0A0 # B0
- prefix
&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
- prefix
*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. - binary
: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:&D20A0:D20automatic reference coercion makes this work.*A0:D20prevent the automatic reference coercion of A0, and use the value stored at A0 as the top left anchor. For instance, iflet A0 = &C10then the expression*A0:D20would evaluate toC10: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.
@sumReturns the sum of its arguments@prodReturns the product of its arguments@avgReturns the average of its arguments.@minReturns the smallest argument.@maxReturns the largest argument.@countReturns the count of number variables.@countAllReturns the count of any type of variable.
The following builtin functions take one argument and coerce it to a number:
@sqrtReturns the square root of the given number.@roundRounds the given number to the nearest integer. If two integers are equally close, rounds away from zero.@floorReturns the largest integral value not greater than the given number.@ceilReturns 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.
@upperReturns the ASCII uppercase version of its argument as a string.@lowerReturns the ASCII lowercase version of its argument as a string.@lenReturns the number of grapheme clusters in the given string.
The following builtins are constants and do not take any arguments or parentheses:
@piArchimede's constant (n).@eEuler's number (e).
The following builtins take a single range as an argument:
@widthReturns the width of the given range.@heightReturns 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-9Set count0Set count if count is not zero, otherwise move cursor to the first populated cell on the current rowj,DownMove cursor downk,UpMove cursor uph,LeftMove cursor leftl,RightMove cursor rightC-fPage downC-bPage upC-dHalf page downC-uHalf page up:Enter command insert mode=Enter command insert mode, with text set tolet cellname =, where cellname is the cell under the cursoreEdit the expression of the current celldd,xDelete the cell under the cursorEscDismiss status message$Move cursor to the last populated cell on the current rowgcMove cursor to the count columngrMove cursor to the count rowggMove cursor to the first cell in the current columnG,geMove cursor to the last cell in the current columnwMove cursor to the next populated cellbMove cursor to the previous populated cellfIncrease decimal precision of the current columnFDecrease decimal precision of the current column+Increase width of current column if non-empty-Decrease width of current column if non-emptyaaFit column width to contentsuUndoURedo<Align text under cursor to the left>Align text under cursor to the right|Align text undor cursor to the centergnGo to the next sheetgpGo to the previous sheetC-wqClose the current sheetyyYank selected cellpPut yanked cells at cursor, copying expression exactlyPPut yanked cells at cursor, adjusting cell references in the expressionsicInsert count columns at the cursordcDelete count columns at the cursorirInsert count rows at the cursordrDelete count rows at the cursor
- All normal mode motions
Esc,C-[Enter normal moded,xDelete the cells in the given rangeoSwap cursor and anchorAlt-jMove selection down count timesAlt-kMove selection up count timesAlt-hMove selection left count timesAlt-lMove selection right count timesyyYank selected cells and enter normal mode
- All visual mode motions
ReturnWrite the selected range to the command bufferEscCancel select mode
EscEnter command normal modeReturn,C-m,C-jSubmit current command or completionBackspace,DelDelete the character before the cursor and move backwards oneC-p,UpPrevious commandC-n,DownNext commandC-a,HomeMove cursor to the beginning of the lineC-e,EndMove cursor to the end of the lineC-f,RightMove cursor forward one characterC-b,LeftMove cursor backward one characterC-wDelete the word before the cursorC-uDelete all text before the cursorC-kDelete all text after the cursorC-vEnter visual select modeC-p,<Up>History prevC-n,<Down>History next<Tab>Next completion<S-<Tab>>Previous completion
EscLeaves command mode without submitting command1-9Set count0Set count if count is not zero, otherwise move cursor to the first populated cell on the current rowh,LeftMove cursor left count timesl,RightMove cursor right count timesk,UpPrevious command count timesj,DownNext command count timesiEnter command insert modeIEnter command insert mode and move to the beginning of the lineaEnter command insert mode and move one character to the rightAEnter command insert mode and move to the end of the linesDelete the character under the cursor and enters command insert modeSDeletes all text and enters command insert modexDelete the character under the cursordEnter operator pending mode, with deletion as the operator actioncEnter operator pending mode, with change (delete and enter insert mode) as the operator actionDDeletes all text at and after the cursorCDeletes all text at and after the cursor, and enters command insert modewMoves cursor to the start of the next word count timesWMoves cursor to the start of the next WORD count timesbMoves cursor to the start of the previous word count timesBMoves cursor to the start of the previous WORD count timeseMoves cursor to the end of the next word count timesEMoves cursor to the end of the next WORD count timesM-eMoves cursor to the end of the previous word count timesM-EMoves cursor to the end of the previous WORD count times$,EndMove cursor to the end of the linek,<Up>History prevj,<Down>History next
Performs the given operation on the text delimited by the next motion
- All motions in command normal mode
iwInside wordawAround wordiWInside WORDaWAround 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 backticks
Cellulator 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.