Everything Quillo does, and how: the language, components, routes, the standard
library, databases, services, and running it. For what Quillo is and why, see the
README. For what it does not do, and what to do instead, see Possible or
not.
Quillo is a single binary, built with Cargo:
cargo build --release./target/release/quillo new hello # a program to start from, in hello/./target/release/quillo run app/main.qwe --watch # the sample app: http://127.0.0.1:8080./target/release/quillo check app.qwe # find mistakes without running./target/release/quillo build app.qwe # build its Rust (.qwer files) beforehand./target/release/quillo repl # try code line by line./target/release/quillo lsp # language server (for editors)./target/release/quillo dap # debugger (for editors)cargo test
This guide is for Quillo 0.8.0, and CHANGELOG.md says what it changed.
Features marked (since 0.7) came in 0.7.0. On an older release, read that release's guide:
https://github.com/quillolang/quillo/blob/v0.7.0/USERGUIDE.md or
https://github.com/quillolang/quillo/blob/v0.6.0/USERGUIDE.md.
quillo run checks the program, executes its top level and, if it declared routes or
server fns, serves them; a program with neither ends when its top level has run
(under --watch it is served all the same). The program is the first argument that is
not an option, wherever it stands (quillo run --watch main.qwe works); left out, or
given a folder, it is that folder's main.qwe, so quillo run in a program's folder
runs it. quillo app.qwe alone is quillo run app.qwe.
Options are words starting with -- that you add to the command line when you start the
program, and they hold for that run only; nothing is written to a file:
quillo run main.qwe --watch # while you work on itquillo run main.qwe --port 8081 --max-body 50MB # in production, on another port, larger uploads
An option's value follows it, or = (--port 8081, --port=8081); -w is the one
short option (-p is not --port); -h prints the usage; a mistyped option is told the
nearest one there is. quillo app, quillo serve and quillo fcgi take these options
too, but for a few each refuses (below). The options, and when to give them:
Option
What it does, and when to give it
--port N, --host H
where to listen (default $PORT or 8080, on 127.0.0.1). Give --port when 8080 is taken or a host names the port; --port 0 takes any free one (platform().server.port says which). Give --host 0.0.0.0 to be reached from other machines without a proxy in front; --host localhost is 127.0.0.1
--listen unix:PATH(since 0.7)
listen on a Unix socket instead of a TCP port, when a web server on the same machine passes requests on (on a shared server, so the neighbours cannot reach the port): made with mode 0600, removed when the server stops, and at most 100 bytes long (a short path relative to the folder quillo runs in). A $PORT that holds an absolute path (/home/u/app.sock, or unix:/home/u/app.sock) is taken the same way. Not with --split, --instances, --register or --join (see Quillo on a shared server)
--socket-mode 660(since 0.7)
with --listen: the socket's permissions, in octal, when the web server reaches it through the owner's group; the owner must keep read and write (600, the default)
--watch, -w
while you work on the program: reload when it or a used file changes (a symbolic link pointed elsewhere, to a file or a folder, counts, within a second), show errors on the page, and refresh open pages; a file the program served with file() that changes refreshes them too, and a stylesheet is swapped in them as they are, keeping what they show (for one another tool writes, such as Tailwind's). Started, or reloaded, while a file is broken or missing, it watches every file that load read or tried and the folders of those not there, so fixing or making one loads the program; each error is said once. Not in production
--reload
in production, for a program whose files change while it runs (a deploy that copies new files, a plugin editor): reload when files change, with no errors shown on pages and no page refreshes; with --instances, each instance loads the change, the first checking it, so one that does not check reaches none; --split passes it to its services
--env-file F
when the settings are not in the .env beside the program: a file kept outside the site's folder, or one file per place (--env-file prod.env); a path from the folder quillo runs in, which must exist
--policy FILE(since 0.7)
before a deploy, or on a server you share: narrow what the program may do, after /etc/quillo/policy.toml and before the quillo.policy beside it; may be given more than once (see The policy)
--no-build(since 0.7)
on a server with no Rust, or one that must never run cargo: load only libraries quillo build made, as .quillo/plugins.lock names them (see Rust)
--no-check
when the program is known to check (a deploy that ran quillo check already) and starting must be quick: skip the static checks
--max-body N
when visitors send larger files or bodies than 10MB: the largest request body to accept, a size such as 50MB (KB, MB and GB, in any case, are 1024-based) or bytes; default 10MB; a larger body gets 413 (see Uploads)
--max-compute 2s(since 0.7)
when a request that computes for long must not hold up the others: end one that computes this long without letting go, as a 500 (see Concurrency)
--workers N
requests that can wait at once (for a database, the network), each on a thread (default 64). Give fewer where an account limits its threads (shared hosting), more for many slow calls at once
--instances N
to use N cores on one machine: run the program N times over, one event loop each, each with its own variables (see Concurrency); not with --watch, --register or --split; more than the machine's cores is warned of
--trust-proxy 10.0.0.0/8(since 0.7)
when a proxy (limer, nginx, Apache) is in front: believe X-Forwarded-For from these addresses, so request.ip and the limits per client are the visitor's (see Live data)
--live-per-client N(since 0.7)
when one address is many visitors (an office behind one address): the pages one client may have waiting for live data (default 256)
--plain-http
when the site is served over plain HTTP on purpose, on a network you trust (a factory's kiosks at http://10.0.0.5:8080): cookies are not Secure, which browsers would drop there (see Cookies and sessions)
--allow-path DIR(since 0.7)
when a script used as a tool must read or write files outside its folder, or a deploy links a folder from elsewhere (uploads -> /srv/shared/uploads): read_file and write_file may use DIR too; any part of the program may then, routes too (see Paths and links)
-- a b(since 0.7)
for a script: after the program, what follows is the program's, and args() gives ["a", "b"]; before it, -- ends the options (quillo run -- a.qwe x). A stray word elsewhere is told it goes after -- (see Scripts as tools)
--with CMD
when a tool must run beside the program while you work (Tailwind's watcher): run a command (through sh -c, in its folder), its lines marked with its name, stopped with it however it ends, a Ctrl-C or SIGTERM while it loads, a panic and a SIGKILL too (more than once for several): --with "npx tailwindcss -i src/app.css -o public/app.css --watch"
--log F
the request log on stderr: text (the default), json (one object per line) for a log collector, or off when another layer logs requests (see Logs and metrics)
--metrics
when Prometheus (or another scraper) should read the server: answer GET /_quillo/metrics (see Logs and metrics)
--query-stats(since 0.7)
to find slow queries on a running server: time database queries for GET /_quillo/health/queries, the last 100, overall and per statement (on under --watch; see Stopping and health)
--describe
when a gateway imports the program's API: answer GET /_quillo/describe with it, as quillo docs writes it (see The program's API)
--no-view-cache
to measure, or to rule the cache out while looking for a bug: render views every time (see Cached views)
--no-jobs
on all but one of several servers that do not share a database, or on a copy that must not run them: run none of the program's scheduled jobs here (see Scheduled jobs)
--no-prefetch
when links must not be fetched before they are clicked (to measure, or a site whose GETs are counted): turn prefetching off (see Prefetching)
--base PATH
when a proxy serves the program under a path (/shop) and takes the path off before passing a request on: the program sees /x for /shop/x, and its links and redirects carry the prefix (/y is written /shop/y)
--split
to try the app as separate processes, as it will run deployed: run each service as a process of its own (see Services); with --port 0, the services are told the port the app was given
--scale NAME=N
with --split, for a service that needs more than one process: run service NAME as N copies (1 to 64), called in turn (see Services)
--service-url NAME=URL
when service NAME runs on another machine: where it is (also QUILLO_SERVICE_<NAME>, the name in capitals); several addresses, comma-separated, are taken in turn (see Services)
--as-service
to run a service on a machine of its own: serve the program as a service, its server fns answering other services' signed calls, not pages (see Services)
--service-name NAME
with --as-service, when the file is not named as callers call the service: the name it answers to; calls signed for another service are refused
--join URL
with --as-service and a --port, to add a copy of a service while the app runs: join the app at URL, signed with QUILLO_SERVICE_KEY when it is set (see Copies that join)
--join-as URL(since 0.7)
with --join, when the app must call the copy at another address than the one it connects from (https://billing-3.internal:8443); it must lead to where the copy joined from
--no-join, --allow-join A,B(since 0.7)
on the app, to take no copies that join, or only copies of these services
--register URL, --service S
to run several servers behind limer: join its pool at its admin address (the pool named S, else the file's name), and share live updates with its other servers; needs LIMER_ADMIN_SECRET (see Several servers)
--wt-port N
when the UDP port of the HTTP port's number is taken or not open: the UDP port pages connect to over WebTransport (see Channels)
--tls-cert F, --tls-key F
when WebTransport should present a real certificate, instead of one of its own; the files must exist, go together and be a pair (--tls-cert b.pem and --tls-key a.key are no pair: the key is not the certificate's); given twice, the last wins
--no-wt
when UDP is blocked between pages and the server: no WebTransport, so pages' channels and streams use a WebSocket at once
quillo new <folder>(since 0.7) writes a program to start from. By default it is the to-do app, the
eight files of app/: a database with a migration, a component, files served.
--minimal writes three: main.qwe, lib/greet.qwe and ui/page.qwe, a page, a
component and a function both sides use. Both get a .gitignore (*.db, *.db-wal,
*.db-shm, .env, .env.*, .quillo/), and --binary copies this quillo into
<folder>/.quillo/bin/quillo. It refuses a folder that is not empty (an empty one, or one
holding only .git, is used) and a name Windows refuses (con, aux, nul, prn,
com1 to com9, lpt1 to lpt9, any of <>:"|?*\, a trailing dot or space, a line
break). A new folder is written beside the target and moved in place at the end, so a
failed new leaves nothing half made, and it never writes an empty folder; an empty
folder that is there is built inside, so its parent need not be writable (a leftover of a
new whose process is gone is removed). It warns when
the folder is inside another program, and prints the next step:
quillo new hello --minimalcd hello && quillo run --watch
In VS Code, Quillo: New program (extension 0.14.0) asks for the to-do app or the minimal
one and for a folder, runs quillo new and opens the folder.
The other commands: quillo check (below, and Checking), quillo docs
(the API as OpenAPI 3, to stdout; see Services), quillo build (the Rust
files; see Rust), quillo fix(since 0.7) (the import lines a program's files need, and 0.6's lines turned around; see
Files of a program), quillo serve <folder>(since 0.7) (a folder of files as
they are, as a static host serves it; see Static files), quillo classes <file.qwe>(since 0.7) (the classes the program's markup writes, one a line; see HTML
literals), quillo fcgi <file.qwe>(since 0.7) (the program behind FastCGI, for
mod_fcgid or nginx's fastcgi_pass; see Quillo on a shared
server), quillo repl, quillo lsp, quillo dap,
quillo version (or --version, -V), which names the commit: from a source archive
it reads the COMMIT file git archive writes, else says (commit unknown). quillo <file.qwe> is quillo run <file.qwe>, options and all, whatever the case of its
extension (quillo MAIN.QWE). A misspelt command is told the nearest (unknown command `chek` (did you mean `check`?)), and a file of another extension given with no command is
told how to run it (to run the file: quillo run tool). An option given before the
command is told where it goes, in one line: quillo: put the command first: `quillo run --watch main.qwe` . -h and --help are read first, after any command (quillo run -h
with no program, quillo lsp --help), and print to stdout. quillo run given a file that is
not a program says what it is and how to serve it: page.html is HTML, not a Quillo program: serve its folder as it is with `quillo serve .` , and the same for Markdown
(with markdown_page), Rust (use "t.rs"), and .css, .js, .json, .txt, .xml,
.csv and .svg files (file()); a folder with no main.qwe but an index.html is
told quillo serve. check, docs, build and fix take one program, and refuse a second and an option
they do not have (check has --strict-null and --policy FILE). quillo check exits 1 when there is an error (warnings do not
fail it), quillo run 1 when the program does not load or the port cannot be taken, and
either 2 for a usage mistake (an unknown option, an option without its value, no program, --register without its
secret, options that do not go together, a --port outside 0 to 65535, a --tls-cert or
--tls-key file that is not there, a policy file that cannot be read or says something
a policy does not say, a .env that cannot be read). An option that takes a value refuses another option
in its place: --service-name --watch says needs a value, and --watch is an option. A stopped server exits 0 (see Stopping and
health).
Their options, given after the command as quillo run's are:
Command and option
What it does, and when to give it
quillo check --strict-null
to find where a value may be null and null does not work (see Checking); check only, not run
quillo check --policy FILE(since 0.7)
before a deploy to a server with a policy: check the program as it would run under it (see The policy)
quillo new --minimal, --binary
three files instead of the to-do app; a copy of this quillo in the new folder, for a machine that has none
quillo app --open PATH
the page the window opens on (/ by default); app takes run's options but --host, --register, --service, --port and --listen: its window asks the program directly, with no socket (see Desktop apps)
run's options but --split, --instances, --register and --join; --listen unix:PATH or --port when no FastCGI manager hands it a socket (see Quillo on a shared server)
a plugin's example program too; the plugins here the catalog has newer; what the catalog has; replace a file changed by hand; install elsewhere; another catalog (see Plugins from the catalog)
quillo repl -i
keep the greeting and prompts when the input comes from a pipe (see The REPL)
quillo build, docs, fix and classes take a program and no options.
The .env beside the program is read into its environment as it starts: KEY=value
lines (export allowed, # for comments, and # ends a bare value, so KEY= # set me
is "" while KEY=#fff is #fff; values in "..."
take \n, \t, \" and \\, and '...' is as written; a quoted value goes on to its
closing quote across lines, as a PEM key does, and a quote never closed skips its own line
only; a byte-order mark at the start is skipped;
a key is letters, digits and _, and a line that is not KEY=value, or whose value holds
a NUL byte, is skipped and said
at the start: quillo: .env:3: not KEY=value, skipped), for env("API_KEY") and for Quillo's own
settings (PORT, QUILLO_SECRET). Only a .env that is not there (or a folder of that
name) is nothing: one that cannot be read, is not UTF-8, or is a link to nothing stops
the start with exit 2, saying where and the ways out (quillo: cannot read .env: it is not UTF-8 text: byte 0xE9 at line 1, column 9 (save it as UTF-8); to start without it, remove it or give `--env-file /dev/null` ), as a missing --env-file does, and a service's own
.env stops its load the same way. A variable the environment already has wins, so a
.env left on a server cannot override what production sets. It is read once: a program
started with --reload does not read it again when its files change. Another file is
named when the program starts: quillo run main.qwe --env-file /etc/shop/prod.env. A service reads the .env beside its own file as
well, in one process and under --split alike: its values come before the app's file's
(the app's reach it too), and after the environment's. Keep .env out of version
control.
Everything Quillo reads from the environment:
Variable
PORT
the port, when --port is not given; an absolute path is a Unix socket to listen on, as --listen unix:
cargo test: where Chrome, PostgreSQL, MariaDB and limer are; show the test servers' output
The one other configuration is the policy, which only takes away: /etc/quillo/policy.toml,
--policy FILE and quillo.policy beside the program (see The policy).
HOST is not read (--host is), and RUST_LOG
and NO_COLOR do nothing. The program's paths (use, db.open, read_file, .env,
quillo.policy, quillo-system.db, .quillo/native) count from the program file's folder, whatever
folder quillo was started in.
quillo app runs a program in a native window instead of a browser tab: the
platform's own web view (WebKit on macOS and Linux, WebView2 on Windows) shows
its pages, and closing the window ends the program. It is built only on request:
cargo build --release --features app # Linux also needs libwebkit2gtk-4.1-dev and libgtk-3-dev./target/release/quillo app app/main.qwe --watch
The release downloads are built without it. It takes the options of run except
--host, --register and --service, reads the .env beside the program (or
--env-file) as run does, keeps --with commands going until the window closes, and
takes --open PATH, the page the window opens on
(quillo app main.qwe --open /admin; / by default). The program runs in the window's process with no
HTTP server: nothing listens, and nothing else on the machine can reach it. The window
asks for its pages through a scheme of its own, quillo://app/ (https://quillo.localhost/
on Windows), and the program answers each request as a server would, so routes,
sessions, server fns, uploads and live data work as in a browser (Phone apps, below,
says what differs). The origin is the same at every launch, so what a page keeps in
localStorage and IndexedDB is there at the next one. The web view
keeps no cookies for the scheme, so the program's are kept for it in
.quillo/app-cookies.json beside the program, and its session key in
.quillo/.quillo-secret unless QUILLO_SECRET is set: someone signed in stays signed in
from one launch to the next. --port and --listen are refused, as there is no socket to give them to. Links to other sites open in the
system's browser. Only macOS has run this way; Windows and Linux take the same code
through wry, not yet run.
Editor support: quillolang/editors has a VS Code extension (highlighting,
diagnostics as you type, quick fixes, hover, go to definition, completion, outline, snippets,
end tags added as you type a start tag's > or </, and a debugger: see Debugging),
which Cursor, Windsurf and VSCodium take as its .vsix (or from Open VSX, once a release is
published there). The same repository has a plugin for the JetBrains IDEs and Android Studio,
an extension for Zed, and support for Neovim, Helix, Emacs, Sublime Text and Vim, with Eclipse
and Visual Studio set up by hand. All but Visual Studio run quillo lsp, and all but Visual
Studio, Sublime Text and Vim quillo dap. Zed, Neovim, Helix and Emacs highlight
with tree-sitter-quillo, a tree-sitter grammar in the same repository; documentation sites can
use its Shiki, highlight.js and Prism definitions.
An element left open is reported at its start tag (`<li>` is never closed: add `</li>` before `</ul>` on line 5), with a quick fix that adds the end tag there. A class in markup
(<span.chip>, class="chip") goes to its rules with Go to Definition, in css(...), a
component's style or the CSS files under public/, and hover shows them; those classes
are offered after <span. and in class=". Find All References lists a class's rules and
the markup that writes it, from either side, and Rename renames it in both (a class put
together, card-${size}, is left as it is). A class no sheet has a rule for is marked: a
warning when a rule's class is close to it (no style sheet has a rule for `.chiip` (did you mean `chip`?), which quillo check says too), else a faint hint, as markup often
carries classes for its own sake. Neither is told when the program has no sheets of its
own or links one from another site. On a use line, the path goes to its file and
completes to the program's files, and the names taken from a .qwer file go to their
Rust functions, show their signatures (under the name after as) and complete to the
file's functions. Inside a call, as ( and , are typed and with Ctrl+Space, it shows the
parameters, the current one marked, and a line about the function: a builtin's own, a .qwer
function's /// comment, or the // comment above your function. Completion offers the
variables in scope at the cursor first. It runs the quillo it finds (its quillo.path
setting, the workspace's target/release, then the PATH), so stale errors on code the
build accepts come from an old binary: quillo --version names the commit, and
scripts/doctor.sh points out an old one. Other editors can run quillo lsp (hover,
go to definition (of names, and of classes to their CSS rules), references and rename
of classes (also from the CSS files under public/, when the client sends them), document symbols,
completion after ., <, </ and @ (and of classes after <tag. and in class="), diagnostics on
open, change and save, quick fixes as code actions, and quillo/closeTag: at a position
just after a start tag's > or after </, the text to insert there, or null; initializationOptions: { "strictNull": true } for the extra
warnings) and quillo dap directly. The language server reads the editor's text, not the
file on disk, and says diagnostics under every URI a file is open by, so a project opened
through a link gets them; it reads headers in any case (content-length), answers
-32002 to a request before initialize, -32600 after shutdown, and -32700 to a
body that is not JSON.
Examples, in quillolang/examples (cloned as ../quillo-examples in the commands here): hello.qwe (routes), orders.qwe
(payload-based routing and middleware), todo.qwe (server-rendered
app with a JSON API), notes.qwe (SQLite and a live component),
scroll.qwe (infinite scroll over 100,000 rows, rendering only a window),
compose.qwe (views and components built from each other),
layout.qwe (a page described as JSON, with a live editor),
spa.qwe (a single-page app: client-side navigation, deep links, 404s),
life.qwe (Conway's Game of Life, run by a timer in the browser),
storm.qwe (a dashboard under a storm of random changes, tested for leaks),
services (a shop that bills through a billing service, in one process or
split apart), and index.qwe, which mounts all of them in one server.
(since 0.7) A program runs inside an iOS or Android app: the runtime is linked into it and runs the
program in the app's process, with no HTTP server, as quillo app does on a desktop, so it
works offline and keeps its SQLite database in the app's container. mobile/ios/build.sh app --run builds the app around a program folder for the iOS simulator and opens it there, and
mobile/android/build.sh app --run a debug APK for an Android emulator or phone. The
app's name, id, version, build number, icon and first file (main = "mobile.qwe", by
default main/) come from an app.toml in the program's folder;
--device and --archive (iOS) and --release (Android, a signed APK and App Bundle)
make builds for the stores;
mobile/README.md says how, and what a phone build leaves out (compiled numbers,
WebTransport, .qwer plugins built at run time, anything that starts a process). The
app puts window.quilloNative on the program's pages for what a web view cannot reach:
js("quilloNative.location"), .haptic, .battery, .notify, .keepAwake,
.brightness, .share, .openSettings, .wait. Both apps answer .share as { shared, to } (on Android both null, as it cannot tell), on Android .brightness(null) gives the
screen back to the system (iOS does when the app leaves the screen), and a tel:, sms:, mailto: or maps link opens what the phone opens it with
(FEEDBACK28.md, Q-346). It has run on a real iPhone, served on 127.0.0.1 as it was then;
the scheme below has run on the iOS simulator and the Android emulator (pages, a server fn,
live data, a form with a file, and storage and a session kept from one launch to the next),
not yet on a real phone. Uploads to the stores are not
done yet, and HarmonyOS NEXT is planned (mobile/harmony/PLAN.md). platform() (see Standard library) tells a program
it is in a phone app, and on what. mobile/examples/fieldbook tries the phone's hardware
from a program.
Nothing listens. The web view gets its pages from the program through a scheme the app
answers: quillo://app/ on iOS (and in quillo app on macOS and Linux),
https://quillo.localhost/ on Android (and on Windows). So no other app on the phone, and
no page in its browser, can reach the program; iOS has no listening socket to take back
while the app is suspended; and the apps carry no exception for plain HTTP (iOS's
local-networking ATS key and Android's cleartext config are gone). The origin is the same
at every launch, so localStorage and IndexedDB keep what they had. An install made
before kept them under http://127.0.0.1:<port>, and they stay there: they are not copied
to the new origin (the program's database is the program's, and is not touched).
What a scheme cannot carry goes through the app's message bridge: on Android every request
with a body (its interceptor is given none), on WebKit a body that is a file, a Blob or a
FormData (it hands the scheme none of those, so the page turns them into bytes first), and
everywhere the connections live data and channels keep open. The shell's script, run before
every page, hands the runtime its way out when the runtime asks for it with an event, so
nothing is put on window. On Android, where every body goes through the bridge, it also
takes over the page's own fetch, so a script's own request with a body goes the same way:
the one global it changes. A form the scheme cannot carry (every post on
Android, a form with a file on WebKit) is sent through the bridge, and the page goes to its
answer, as after a post and a redirect. What the program sees is what a server sees, but:
The cookie jar is the app's. The web view keeps no cookies for a custom scheme, so the
program's are kept in .quillo/cookies.json in the app's data, with a session key
beside them, and a session lasts from one launch to the next. It keeps cookies as a
browser does, by name, path and domain (the longer path sent first), and one set with
no Path at the folder of the request that set it. document.cookie is
empty to the page's scripts.
The app goes only to http(s) addresses and its own pages: a location of another
scheme is not followed.
request.ip is "app".
Jobs, ticks (every on the server) and calls made later reach the program with no
connection to itself.
A stream route's answer arrives whole, when it ends: the shells hand an answer over in
one piece.
Service workers do not run on a custom scheme. Quillo uses none; a program's own will
not start.
Safari's and Chrome's inspectors work as before, at quillo://app/... or
https://quillo.localhost/....
mobile/README.md lists the calls an app makes into the runtime (quillo_open,
quillo_request, quillo_bridge and the rest).
Quillo files end in .qwe for ergonomics. Q, W and E sit side by side at the left end
of a QWERTY keyboard's top row, so the extension is one quick roll of the left hand's
little, ring and middle fingers, with no reaching and no looking down. You type it in
every quillo run app.qwe, every use "lib.qwe" and every new file name, so it should
be the easiest thing in the line to type.
It also reads as what Quillo is for: Quit Worrying about Everything. quillo check
finds mistakes before the program runs, text in pages is escaped, cross-site posts are
refused, sessions are signed, a payment sent twice is made once, and a transaction that
collides is tried again. What it does not take care of is listed in
Possible or not, so you know what is still yours to worry about.
Everything Quillo does, and how: the language, components, routes, the standard library, databases, services, and running it. For what Quillo is and why, see the README. For what it does not do, and what to do instead, see Possible or not.