The file format
An .omx file is plain text: constants, functions and sheets, in any order.
A line that starts with # is a comment.
# Optional: the time zone of the file.
zone Europe/London
# A named value, and a function.
const TaxRate = 20%
fn WithTax(x) = x * (1 + TaxRate)
sheet Sales
# Optional: the type of a column.
Revenue : Decimal
# A cell that starts with = is a formula.
| Month | Revenue | Cost | Profit | Gross |
| ----- | ------- | ------------ | ------ | ----- |
| Jan | 10000 | 6000 | * | * |
| Feb | 12000 | = 7000 + 500 | * | * |
# A computed column: one formula, every row. The header row names it where
# it goes, and its cells are *.
Profit := Revenue - Cost
Gross := WithTax(Revenue)
Sheets
sheet Name, then a header row and data rows, written as a Markdown table: each line between outer pipes,| a | b |, cells separated by|, and a| --- | --- |line under the header row. A sheet is then a table wherever the file is shown as Markdown, a sheet of one column too.- The outer pipes and the
---line may be left out, as in older files:a | b. A header row that starts with|says the sheet has outer pipes. Without them, a|at the start or end of a row is an empty cell. - The app writes every sheet it changes as a Markdown table, lined up.
Name := expressionafter the rows is the formula of a computed column. The header row names the column, anywhere among the others, and each of its cells is*.- A computed column may be written as a
sequence,
Odd := 1, 3 ... Inf, which fills it from the terms given. - An empty cell is empty: there is no
null.
sheet Sales
| Month | Revenue | Profit | Cost | Tax |
| ----- | ------- | ------ | ---- | --- |
| Jan | 10000 | * | 6000 | * |
| Feb | 12000 | * | 7000 | * |
Profit := Revenue - Cost
Tax := Profit * 20%
Profit is the third column and Tax the fifth. A formula with no column
of its name in the header row is an error, and so is anything but * in a
cell of a computed column.
Types
Int, Decimal, Fraction, Percent, Num, Complex, Text, Bool,
Date, Time, DateTime.
A column with no Name : Type line takes its type from its cells.
| Type | Written |
|---|---|
Int |
42, 1_000_000 |
Decimal |
19.99 — exact |
Fraction |
1/7, 22/7, 2 3/4 — exact, shown as a fraction |
Percent |
20%, 12.5% — exact, shown as a percentage |
Num |
1.5e3, Inf, -Inf, NaN — floating point |
Text |
hi or "hi" (escape |) |
Bool |
true, false |
Date |
2025-01-31 |
Time |
09:30, 09:30:15 |
DateTime |
2025-01-31T09:30 |
Text written bare may hold Markdown: a **slow** start,
see the [lease](lease.pdf). The app shows it as that, and so does a
rendered document. Text in quotes is as it is:
"2 * 3 * 4". So is the text a formula makes, as it is written in quotes
there; a value read from a cell is Markdown or not as that cell is.
omasheet print shows every cell as it is written.
A Decimal, a Fraction and a Percent hold the same exact numbers, and a
column of one takes the others, showing them its own way: a Decimal column
shows 1/7 as 0.14285714285714285714…, and a Fraction column 0.25 as
1/4.
A Percent column takes any exact number and shows it as a percentage: 0.4
there is 40%, and so is = Profit / Revenue when it comes to two fifths.
A 20% in a Decimal or Fraction column is the number it stands for, 0.2
or 1/5.
A Fraction column shows every number as a fraction, and one of more than
one as a whole number and a part: 2.75 and 11/4 there are both 2 3/4.
A fraction may be written that way too, with a space: 2 3/4 is 11/4, and
the app keeps it in the file as 11/4.
See examples/types.omx for every type at work.
Dates and times are always written this way in the file. The app shows and accepts them in your locale’s form.
Constants, functions, zone
const Name = expressionfn Name(a, b) = expression— sees its parameters, constants and sheets, not the row it is called from. The comment above it is its description.zone Area/Citybefore the first sheet says which time zone the file’s date-times are in. Without it, they are in the zone of the machine.
Copyright (c) 2026 Stephen Roe. MIT licence.
Expressions (OMX)
Try any of these with omasheet eval [file.omx] '<expression>'.
Numbers
Arithmetic is exact: 1/3 + 1/6 is 0.5, 0.1 + 0.2 is 0.3, and integers
do not overflow. x.Num gives a floating-point Num.
No exact number is a division by zero, so that gives a Num: 1/0 is Inf,
-1/0 is -Inf and 0/0 is NaN. So does a function outside its
domain: sqrt(-1) is NaN and ln(0) is -Inf. They are written as they are shown,
Inf, -Inf and NaN, in a formula or a cell; the text is in quotes,
"Inf". A NaN is not equal to anything, itself included: of the
comparisons only != is true of one.
A number is shown in at most 23 characters, and a - before it. From
1e-15 up to 1e15 it is plain, 0.015625; smaller or larger it is in e
notation, 1e-43 and 1e+60, and so is one too wide to be plain. An exact
number in e notation keeps every digit it can, and where they do not all fit
they are cut and marked: 2 ** 64 is shown as 1.8446744073709551…e+19.
Only the display is cut, and omasheet eval --exact gives it in full.
A Decimal that does not end is cut and marked the same way: 1/3 is
shown as 0.33333333333333333333…. Pointing at it in the app shows more
places, and --exact gives it as a fraction, 1/3.
A Num is never rounded: it is shown with the fewest digits that give the
same Num back, 0.30000000000000004, and always fits.
A Percent is an exact number shown as so many in a hundred: 20% is 1/5.
It is rounded to two digits after the point, 41.67%, and --exact gives it
in full, as a fraction where it does not end, 125/3%.
A Fraction is shown as a fraction where it fits: 1/3, and one of more
than one as a whole number and a part, 11/4 as 2 3/4. One too wide is
shown in e notation, cut and marked. --exact and x.Text give it as it is
written in the file, 11/4.
Whole numbers and decimals divided give a Decimal, whether or not it
ends: 7 / 4 is 1.75 and 7 / 3 is 2.33333333333333333333…, held
exactly. Anything worked out from a Fraction is a Fraction:
x.Fraction * 2. To show a result as a fraction, declare its column
Fraction or write x.Fraction; for a decimal, write x.Decimal.
Numbers are shown the way Raku shows them:
| Type | Shown |
|---|---|
Int |
101; 1e+15 and 1.8446744073709551…e+19 from 1e15 |
Decimal |
1.01; 1e-43 below 1e-15 |
Fraction |
1/3, 2 3/4; 1.6129030457865418…e-01 when too wide |
Percent |
20%, 12.5%, 41.67% |
Num |
100, 1.5, 1.4142135623730951; 1e+21 or 1.5e-16 when very large or small; Inf, -Inf, NaN |
Complex |
each part as a Num: 3+4i, 1.5-2i, 0+1.4142135623730951i; both parts cut alike when too wide |
A Num that is a whole number looks like an Int, but it is still a Num.
A Decimal, a Fraction and a Percent are the same exact numbers shown
in different ways: 19.99, 2 3/4 and 20%. Adding, subtracting,
multiplying and dividing decimals gives a Decimal, and so does an average.
Each goes anywhere another does: a Decimal column shows 1/3 as
0.333…. A column of no declared type with a decimal in it that does not
end is a Fraction column, and shows every number in it as a fraction.
Percentages add up to a percentage:
20% + 5% is 25%, and so are their sum, average, least and greatest.
Anything else done with one gives the plain number: 100 * 20% is 20,
1 + 20% is 1.2, and 20% == 0.2 is true. To show a result as a
percentage, declare its column Percent or write x.Percent.
+ - * / **, == != < > <= >=, and or not, in, and a // b (b when a
is empty). if c then a else b.
Converting
The name of a type converts to it, written x.Int, x.Int() or Int(x). A
vector is converted value by value.
| Written | Gives |
|---|---|
x.Int |
a whole number: 19.99 gives 19, -19.99 gives -19 |
x.Decimal |
the same number as a decimal: (1/8).Decimal is 0.125, (1/3).Decimal is 0.333… |
x.Fraction |
the same number as a fraction: 2.75 gives 2 3/4; a Num as the decimal it is shown as |
x.Percent |
the same number as a percentage: 0.4 gives 40%, 5/12 gives 41.67% |
x.Num |
a floating-point number |
x.Complex |
a complex number from a real one |
x.Text |
any value as text, written the way a file writes it |
x.Bool |
false for zero, true for any other number |
x.Date, x.Time |
that half of a DateTime |
x.DateTime |
a Date at midnight |
Text is read the way a cell is: "42".Int is 42, "20%".Fraction is 1/5,
"2025-01-31".Date is that date, and "abc".Int is an error. true.Int is
1. To round rather than drop the fraction, use round(x); round(x, 0.01)
rounds to the nearest 0.01, and round(x, 100) to the nearest hundred.
Names
| Written | Means |
|---|---|
Sales |
the sheet |
Sales.Revenue |
the column, as a vector |
Revenue |
inside the sheet: this row’s value |
TaxRate |
a constant |
Selecting with [columns; rows]
| Written | Means |
|---|---|
Sales[Revenue; 1] |
one cell; positions count from 0 |
Sales[Revenue; -1] |
the last row |
Sales[; 0..2] |
rows 0 to 2, every column; 0..^2 leaves out the end |
Sales[; 0^..2] |
rows 1 to 2: ^.. leaves out the start, ^..^ both ends |
Sales[; ^2] |
the first two rows: ^n is 0..^n |
Sales[Revenue] |
the whole column: a missing or empty slot is all of it |
Sales[; Revenue > 11000] |
the rows where it is true |
Sales[Revenue; *-1] |
the row before this one; * is this row |
Sales[Revenue; 0..*] |
from the top down to this row |
Inside its own sheet the name can be dropped: [Revenue; *-1]. A row
outside the sheet is empty, so [Revenue; *-1] // 0 starts a running value.
Sequences with ...
A computed column may be written as a sequence, terms ... limit. The terms
given start the column and the rest follow a rule.
| Written | Gives |
|---|---|
1, 3 ... Inf |
1, 3, 5, 7, … — a constant difference |
1, 2, 4 ... Inf |
1, 2, 4, 8, … — a constant ratio |
5 ... Inf |
5, 6, 7, … — one term alone steps by one |
0, 1, $-1 + $-2 ... Inf |
0, 1, 1, 2, 3, 5, … — a rule of its own |
1, 3 ... 9 |
1, 3, 5, 7, 9, then empty |
"A", "B" ... Inf |
A, B, … Z, AA, AB, … |
"Monday", "Tuesday" ... Inf |
the days of the week, round and round |
"Jan", "Feb" ... Inf |
the months of the year |
"Jan-2026", "Apr-2026" ... Inf |
quarters, carrying into the year |
"AY" ... "BA" |
AY, AZ, BA, then empty |
2025-01-01, 2025-01-08 ... Inf |
every seventh day |
2025-01-01, 2025-02-01 ... Inf |
the first of each month |
2025-01-31, 2025-02-28 ... Inf |
the last day of each month |
$-1is the term before this one,$-2the one before that. The last term before...is the rule when it reads one, and another starting term when it does not.- With no rule the step is taken from the terms: their difference, or their ratio where three or more share one. Two terms are read as a difference, so a constant ratio needs three.
Inffills the column. Any other limit stops the sequence there and leaves the rows after it empty; a sequence that would step over its limit stops before it.- The days of the week and the months of the year are known by name,
written in full or in three letters, in any case: they go round, so
Sunday is followed by Monday and December by January. A month written
with its year,
Jan-2026orJanuary 2026, carries into the year instead. Every term must be written the same way, so"Mon", "Tuesday"is not one of these. - Any other text counts up the way letters and digits do, carrying at
Zand9, and only its last run of letters and digits moves:job-A9tojob-B0.succ(x)is that step on its own, and on a number it adds one. - Dates a fixed number of days apart step by days, and times and
date-times by seconds. Dates on the same day of each month step by
months instead, however many days that is, and the day they are counted
from is the largest one given:
2025-01-31, 2025-02-28 ... Infgoes on to the 31st of March, not back to its 28th. One date alone steps by a day. - A sequence fills a column: it cannot change how many rows the sheet has,
and
... Infhas no meaning anywhere else.
Durations
Next to a date, a plain number is days, and n.day, n.week, n.month
and n.year say which: Placed + 1.month, Due - 2.week. Only a plain
number takes a unit, so Placed.month is still the month of a date.
Months and years keep the day of the month, or the last day of a month too
short for it: 2025-01-31 + 1.month is 2025-02-28. That rounding does
not come back, so a sequence of month ends is better written with eom,
which gives the last day of a date’s month:
eom(2025-01-10), eom($-1 + 1.month) ... Inf.
Functions
Called as sum(x), x.sum() or x |> sum().
- Vectors:
sum avg min max count, of one vector or several taken together, with single values among them:sum([A; 0..3], [A; 7..9], 5). A block of cells counts every cell:sum([0..2; 0..3]). - Sheets:
filter(condition),select(columns) - Maths:
abs round floor ceil sqrt exp ln log10 log2 sin cos tan pi() e()and more - Counting up:
succ— a number plus one, or the next text,AZtoBA - Converting:
Int Decimal Fraction Percent Num Complex Text Bool Date Time DateTime - Dates:
today() now() year month day weekday hour minute second date time - Months:
add_months(x, n)— whatx + n.monthis — andeom(x) - Time zones:
to_zone("Asia/Tokyo") utc() local() offset() zone() - Your own:
fnin the file
The app lists them all under fn() (F1).
Dates and times
- A whole number is days next to a
Date, seconds next to aTimeorDateTime:2025-01-31 + 1is2025-02-01. - Two of a kind subtract to a number:
2025-03-01 - 2024-03-01is365. Date + Timeis aDateTime.- Across time zones, comparison and subtraction use the instant.
Copyright (c) 2026 Stephen Roe. MIT licence.
The app and the command line
The app
omasheet # a new file
omasheet file.omx # open a file, a workbook (.xlsx) or a CSV file
omasheet with no command opens the app. Needs Qt 6 (qt6-base, qt6-declarative). The text is the file: every edit
rewrites the text, and saving writes exactly that.
- One tab per sheet, plus the constants.
- Two sheets can be shown side by side, each in a window of its own.
- The box at the top shows the source of the current cell; the grid shows the value.
- Under its name a column shows its type: muted if it comes from the
cells, plain if it is declared, and after
:=in a formula column. Right-click a column’s header to Set type…, which starts on the type the column has now, or Clear type; a cell that does not fit a declared type is an error. - F3 shows the source of the whole file, to read or to edit. An edit is taken when the source is left, saved or closed, and undone as one step.
- Selecting several cells shows their count, sum and average in the footer.
-
Beside that box is the selection, written as a formula would write it:
Selected Shown One cell Sales[Revenue; 3]Cells of one column Sales[Revenue; 3..5]Cells of one row Sales[2..3; 3]A block of cells Sales[2..3; 3..5]A whole row, or rows Sales[; 3],Sales[; 3..5]A whole column, or columns Sales[Revenue],Sales[2..3]The whole sheet SalesSeveral columns are given by position, counting from 0.
- Cells that are copied stay highlighted through as many pastes and inserts as you like, until Esc or the next copy. Cells that are cut stay where they are, highlighted, until they are pasted.
- A number is shown in at most 23 characters: plain from
1e-15to1e15,19.99or1/3, and in e notation otherwise,1e+60. One with more digits than fit is cut and marked,1.8446744073709551…e+19, and aPercentis rounded,41.67%; pointing at it shows the value in full, as does pointing at any text cut off at the cell’s edge. A plain number of five digits or more is grouped in threes, and numbers follow your locale:12,345.678in Britain,12.345,678in Germany. The file does not: a number is typed as a file writes it,12345.678, or12_345.678to group it, and copying a cell copies it that way. - Dates and times follow your locale; the file stays ISO.
- A file in another time zone is shown on your clocks.
| Keys | |
|---|---|
| Ctrl+. | Context menu |
| Ctrl+? | Keyboard shortcuts |
| Ctrl++ / Ctrl+- | Zoom in / out (Ctrl+0 resets) |
| Ctrl+Z / Ctrl+Y | Undo / redo |
| Ctrl+Arrows | Go to end |
| Shift+Arrows | Select block |
| Ctrl+Alt+Left/Right | Previous / next sheet |
| Ctrl+C / X / V | Copy / Cut / Paste |
| Ctrl+Shift+V | Paste values: not formulas |
| Ctrl+Alt+V | Paste transposed: rows as columns |
| Ctrl+Enter | Insert row below (Shift: above) |
| Delete | Clear |
| Ctrl+Delete | Delete the selected rows, columns or sheet |
| Ctrl+O / S | Open / save |
| Ctrl+Shift+S | Save as |
| Ctrl+E | Export as a workbook, CSV or JSON |
| Ctrl+K | New constant |
| Ctrl+N | New sheet after this one |
| Ctrl+Shift+N | New window on this file |
| F1 | View functions |
| F2 | Edit |
| F3 | View source |
| F11 | Fullscreen |
Two sheets side by side
- Open the file and go to the first sheet.
- Right-click the tab of the second sheet and choose Open in new window. Or press Ctrl+Shift+N for a second window on the same sheet, and pick the other tab there.
- On Omarchy the new window tiles beside the first. Elsewhere, place the two windows next to each other yourself.
Both windows show the same file, so an edit in one shows in the other at once, and undo and save work from either. Each window has its own sheet, cursor and text size. Cells cut or copied in one can be pasted in the other.
Closing one window leaves the file open in the other; the last one asks about unsaved changes. Opening another file, or a new file, changes every window.
Starting the app twice on the same file is not the same thing: those are two separate copies, and the one saved last overwrites the other.
The command line
omasheet print file.omx # print the calculated file
omasheet eval '1/3 + 1/6' # one expression
omasheet eval file.omx 'Sales.Revenue.sum()'
omasheet lint file.omx # check without calculating
| Option | |
|---|---|
--locale |
show dates and times as this machine’s locale writes them |
--now 2025-01-31T09:30 |
fix what today() and now() give |
--zone Europe/London |
stand in for this machine’s time zone |
--exact |
show every number in full: 18446744073709551616, not 1.8446744073709551…e+19, and 125/3%, not 41.67% |
From the source tree, omasheet is cargo run --.
Copyright (c) 2026 Stephen Roe. MIT licence.
Workbooks, CSV, JSON and Markdown
The .omx file is the original. A workbook, a CSV file or JSON is a way in or out,
and each command says what it could not carry across.
Import and export are a helper, not a full conversion. A file and a workbook are built differently: named, typed sheets and exact numbers on one side, a grid of cells and floating point on the other. Those differences keep either direction from covering everything, so check what the command reports.
Import
omasheet import budget.xlsx -o budget.omx # a sheet for each worksheet
omasheet import sales.csv # one sheet, named after the file
omasheet import orders.json # a sheet for each list of rows
- The first row is the column names. A name that is not an identifier is
made one:
Unit PricebecomesUnitPrice. - A number is read exactly:
19.99is1999/100. A workbook holds every number as floating point, so it is read from the shortest decimal that gives the same number; CSV is read as it is written, every digit of it. - A column of dates, times, date-times or
true/falseis given its type. A column of numbers and text together has none: each cell is what it reads as. - A formula in a workbook is translated to OMX where OMX can say it: see Formulas. Where it cannot, the value is imported, and the cells are listed with the reason.
- Text that looks like a formula,
=SUM(A1:A3), stays text. - Text is imported as it is, not as Markdown:
*new*is written in quotes,"*new*", so its asterisks are kept.
JSON
JSON is read as values: a list of rows is a sheet named after the file, and an object of such lists is a sheet for each, named by its key.
{
"Sales": [
{"Month": "Jan", "Revenue": 10000, "Day": "2025-01-31"},
{"Month": "Feb", "Revenue": 12000, "Day": "2025-02-28"}
]
}
- A row is an object of values by column name. The columns are in the order they are first met, and one that a row does not have is empty there. A row may be a list of values instead, with the names as the first row.
- A number is read as it is written, every digit of it:
19.99is aDecimal, and1.5e3aNum.trueandfalseareBool, andnullis an empty cell. - JSON has no dates, so a string that is a date, a time or a date-time is
one. So is a string that is a
Complex,"3+4i", or"Inf","-Inf"or"NaN". Any other string is text, a number in quotes too:"42". - A list or an object inside a row is kept as text, and reported.
Formulas
A worksheet becomes a sheet whose first row is the header, so a cell of a formula becomes a column and a row:
| In the workbook, on row 3 | In the file |
|---|---|
=B3-C3 |
= Revenue - Cost |
=B2 |
= [Revenue; *-1], the row above |
=B$2 |
= [Revenue; 0], the first row whichever row asks |
=SUM(B2:B10), all the rows |
= Sales.Revenue.sum() |
=SUM(B$2:B3) |
= [Revenue; 0..*].sum(), a running total |
=Rates!B2*B3 |
= Rates[Rate; 0] * Revenue |
=IF(B3>C3,"up","down") |
= if Revenue > Cost then "up" else "down" |
- A column filled down with one formula becomes a computed column, with
*in its cells andProfit := Revenue - Costafter the rows. - These are translated:
+ - * / ^ %and the comparisons;SUM,AVERAGE,MIN,MAX,COUNT,COUNTA;IF,IFS,SWITCH,AND,OR,NOT;ROUND,INT,TRUNC,ABS,SIGN,SQRT,POWER,EXP,LN,LOG10,PIand the trigonometry;TODAY,NOW,YEAR,MONTH,DAY,HOUR,MINUTE,SECOND,WEEKDAY,DATEVALUE. - These are not, and the value is imported: joining text with
&, lookups such asVLOOKUP,DATE(y, m, d), names defined in the workbook, lists in{ }, and a cell in the header row or outside the sheet. - Every translation is checked. The file is worked out, and a formula that
does not give the value the workbook holds for its cell is dropped for
that value. The commonest cause is an empty cell: a workbook adds it up as
nothing, and in OMX what is worked out from an empty cell is empty.
TODAY()andNOW()are not checked, as they give another answer each day. - A workbook counts the days of the week from Sunday and OMX from Monday, so
WEEKDAY(A2)=1becomesweekday(Date) == 7. - A
SWITCHor anIFSwith nothing for when no case fits takes its last case for that.
Export
omasheet export budget.omx --xlsx # budget.xlsx, a worksheet for each sheet
omasheet export budget.omx --csv # budget.Sales.csv, budget.Summary.csv
omasheet export budget.omx --csv --sheet Sales -o sales.csv
omasheet export budget.omx --json # budget.json, every sheet
omasheet export budget.omx --json --sheet Sales # the rows of Sales alone
A workbook is written with the formulas of the file, and each with its calculated value, so it reads the same before anything works it out again. CSV and JSON hold the calculated values alone. Constants are not exported: a formula that reads one is written with what the constant is.
A sheet becomes a worksheet whose first row is the header, so a column and a row become a cell. Each formula is written for the row it is on:
In the file, on row 1 of Sales |
In the workbook |
|---|---|
Revenue - Cost |
=B3-C3 |
[Revenue; *-1], the row above |
=B2 |
[Revenue; 0], the first row |
=$B$2 |
Sales.Revenue.sum(), from another sheet |
=SUM('Sales'!$B$2:$B$13) |
sum([Revenue; 0..*]), a running total |
=SUM($B$2:B3) |
[Revenue; *-1] // 0 |
=IF(ISBLANK(B2),0,B2), and 0 on the first row |
Rates[Fee; Code == Sales[Code; *]] // 1 |
=IFERROR(INDEX('Rates'!$B$2:$B$9,MATCH(A3,'Rates'!$A$2:$A$9,0)),1) |
count(Orders[; Region == Name]) |
=COUNTIFS('Orders'!$F$2:$F$40,A3) |
if Revenue > Cost then "up" else "down" |
=IF(B3>C3,"up","down") |
- These are written:
+ - * / **and the comparisons;and,or,not,if;sum,avg,min,maxandcountof a column, a range of rows or a list;sum,avgandcountof the rows where columns equal values; a row found where one column equals a value;abs,round,floor,ceil,sqrt, the logarithms and the trigonometry;today,now,yeartosecond,weekday,dateandtime;x.Int, and a function the file defines. - These are not, and the value is written: a calculation on a whole column
at once,
Sales.Revenue * 2; rows picked by any other condition, by a list or by a range value;inand ranges as values;//after a calculation; text conversions; time zones; complex numbers. Each is reported with the columns it happened in. - A cell that comes to nothing, or that failed, is left empty, with no formula.
- A workbook counts a time in days and a file in seconds, so
At - 09:00is written=ROUND((C3-TIME(9,0,0))*86400,0). - A sheet exported alone with
--sheetcannot read another: those formulas are written as values. - A formula is not checked in a workbook, as nothing here works one out. It is exact in the file and floating point there, so the last digits may differ.
- Importing the workbook again brings back most formulas, but not all: the
import has no translation for
ISBLANKorIFERROR, and takes those cells as values.
| XLSX | CSV | |
|---|---|---|
Int, Decimal, Fraction |
the nearest floating-point number | in full; one that does not end, such as 1/3, as 0.3333333333333333 |
Percent |
that number, formatted as a percentage | 40%; one that does not end as 33.33333333333333% |
Date, Time, DateTime |
a date or time cell | 2025-01-31, 09:30, 2025-01-31T09:30 |
Complex |
text | 3+4i |
| a cell that failed | empty | empty |
JSON is written as it is read: an object holding each sheet by name as a
list of rows, a row to a line, and each row an object of values by column
name. With --sheet it is the rows of that sheet alone.
| JSON | |
|---|---|
Int, Decimal, Fraction |
a number, in full; one that does not end, such as 1/3, as 0.3333333333333333 |
Percent |
the number it stands for: 20% is 0.2 |
Num |
a number; Inf, -Inf and NaN as strings |
Text, Bool |
a string; true or false |
Date, Time, DateTime, Complex |
a string, as a file writes it: "2025-01-31", "3+4i" |
| an empty cell, or one that failed | null |
Anything written less exactly than the file holds it is reported, with the columns it happened in.
In the app
- Ctrl+O opens a workbook, a CSV file or JSON as well as an
.omxfile. It is imported as a new file, not yet saved. - Ctrl+E exports, and asks what as: every sheet as a workbook (X) or as JSON (J), or the sheet shown as CSV (C). It then asks where.
- What was changed, rounded or left behind is listed, and stays under
notesin the footer.
Markdown
omasheet render report.md # Markdown, with the values in place
omasheet render report.md --html -o report.html
A Markdown document can quote a file:
---
files: [budget.omx]
---
Revenue was {{ Sales.Revenue.sum() }}, best in {{ Sales[; Profit == Sales.Profit.max()].Month }}.
```omx
sheet Targets
| Month | Goal | Over |
| ----- | ---- | ---- |
| Jan | 4200 | * |
Over := Sales[Profit; *] - Goal
```
files:in the front matter names.omxfiles, from where the document is.files: budget.omxand a list of- budget.omxlines work too.- A fenced
omxblock is a file, shown as its sheets. The files and all the blocks of a document are one file, so each can read the others. {{ expression }}is replaced by its value. Several values read as a list,Feb, Mar; a sheet is shown as a Markdown table.{{in code is left alone, and so is\{{.- Text written bare in a cell may hold Markdown, and is read as that in the
document:
**bold**,[a link](there.md). Text in quotes, or made by a formula, is shown as it is. HTML in a cell is shown as text. - A mistake is reported at its line in the document, or in the file it is in, and nothing is written.
A tool that does not know Omasheet shows the block as code and the braces
as they are. examples/report.md is a worked
document, with what it renders as beside it.
Copyright (c) 2026 Stephen Roe. MIT licence.