Repository navigation
Releases
v2.1.1
The bundle runs on Python 3.15 instead of 3.12. Nothing about the reading changes.
- The bundled Python is 3.15:
requires-pythonis>=3.15,<3.16and the bundle
script installs 3.15. The Rust side's Unicode tables are regenerated from it, Unicode
17.0 in place of 15.0, so both halves still agree on every character. - preshed is 3.0.15. With 3.0.13, spaCy's tokenizer raised
OverflowErroron 3.15 while
loadingen_core_web_sm, which broke every run. spacy, thinc, cymem, murmurhash, srsly,
blis and mlx move to their current patch releases with it. - A development checkout needs its
.venvrecreated:uv sync. - The bundle needs macOS 15 (Sequoia) or later, where 2.1.0 ran on 14. GitHub retired
themacos-14runner on 2 November 2026, and the runner's macOS is the floor of every
bundle built on it (mlx ships one wheel per macOS generation). The formula's
depends_on macos:moves to:sequoiawith it.
readaloud 2.1.0
The preferences are a directory of files, one per kind of input, picked by the file's
extension: a Markdown file is drawn by mdcat and read cell by cell without a flag, and a
Python file is read through its grammar by the shipped python.conf.
- The preferences are a directory,
~/.readaloud/:readaloud.conffor what applies
to everything,markdown.conffor Markdown, and a<language>.confper language
(python.conf,rust.conf), named after the grammar, with plain[config],
[pronunciations],[shapes],[parts]and[syntax]sections inside. The first run
of this version moves an existing~/.readaloud.confinto the directory, section by
section, deletes it and says so. The three shipped files live in
src/readaloud/configs/, where a language's file can be shared. [files]inreadaloud.confsays which configs read a file, by its extension:
.py .pyi = code python,.rs = code rust,.md = markdown. Each name is a layer, the
[config.code]-style sections inreadaloud.confplus a<name>.conf, laid over one
another in that order; the last names the grammar. Adding Rust is one line and a
rust.conf. Thecodelayer, every source file'sformat = codeand the words for the
operators, lives inreadaloud.conf.-mdand--markdownare gone (a hint says what to do).readaloud notes.mdreads
the file: a lone argument that names a file is that file, and.mdgets
markdown.conf, whose newrenderer = mdcatdraws it with
mdcat and reads its tables cell by cell, falling back to stripped text with a notice when
mdcat is not installed. A pipe or TEXT gets the same with--conf md;--conf NAME
reads any input with the configs of a.NAMEfile or the layer NAME.- A Python comment is read without its
#, and a triple-quoted string without its quotes:
[syntax]rules in the shippedpython.conf; a language's file carries its own markers.
A rule may say nothing, and the text it covers is then neither read nor reached by an
entry; a rule beats an entry of the same length, where a shape loses to one. The two
"triple quote" entries are gone from the shippedreadaloud.conf. - A comment leading a
def's body, which the grammar hangs on thedefitself rather
than in the body, is said before the body instead of being dropped by the rule. readaloud --tree app.pyprints a file's syntax tree, the node kinds and fields a
[syntax]rule can name, and the docs say how to change the reading of a piece of code:
find its kind in the tree, write the line, delete it to go back.- The
markdown = yes|nosetting of 2.0 isformat = markdown | code | plain: how plain
input is read.codeis what thecodelayer sets;plainis new, for text that is
neither Markdown nor code and should keep its*asterisks*. The first run rewrites an old
markdown = noasformat = code. - With
format = code, a source file's indented lines are cut into chunks of at most
charslike any paragraph, at line ends, instead of being read as one code block: a
class body no longer waits on one long synthesis. - In a source file, operators (
==,<=,->, the*of*args) are words of their
own, lit while they are spoken; before, their speech ran under the neighbouring word.
A rule's opening words light the token before the node, so "and returns type" lights
the->and "with a default of" the=. - With
format = code, each line is read as a sentence of its own, so the pause falls
where the line ends rather than wherever the voice feels like it: "return self dot path
dot stem", pause, "method for_file takes self". A line that runs on (a bracket left open,
a trailing comma or operator, a\) is not. The screen and the highlight are unchanged.
readaloud 2.0.0
The computational half of readaloud is now Rust, and a source file is read through its
syntax tree and coloured like an editor would.
A Rust core
- Everything that is computation moved out of Python into a compiled extension,
readaloud._core, built with PyO3 and maturin: terminal cell widths (wide CJK and
emoji), the pronunciation engine, word and sentence splitting and chunking, the
escape-sequence parser with its Markdown cleanup, and the alignment of Kokoro's
timestamps onto the spoken words. The Python that remains is the interface: the
config file, the document model, the TUI, playback. - Each port is checked against the Python it replaced, on random input as well as the
known cases, so a file reads exactly as it did. The Unicode tables are generated from
Python's own database, so the two sides never disagree about a character. - The Homebrew bundle ships the extension built on the macOS 14 runner with the pinned
Rust; nothing is compiled on your machine.
Source code, read and coloured through its grammar
- A
.pyfile is parsed with tree-sitter and said node by node the way you describe
it in a[syntax.py]section:def foo(bar: int, baz: str='') -> str:can be
"function foo takes bar type int and baz type string with a default of empty string
and returns type string", a|in a type annotation "or" and one between sets
"pipe",foo().bar"foo dot bar",__init__"dunder init". A rule names a node's
fields ($name,$parameters), the node in full ($0) or its parts ($*), and may
be confined to a context (type/binary_operator) or a field
(function_definition.return_type). Comments, strings and docstrings are untouched
unless a rule names them. See Syntax. - The file is coloured by the grammar's own highlights query, in the terminal's
colours: keywords, strings, comments, function and type names.highlight = noor
--no-colorleaves it plain. - Grammars are plug-ins. readaloud ships the tree-sitter runtime and no grammar; a
language's grammar is loaded at run time from itstree-sitter-<language>package
(prebuilt wheels, no compiler) or from a Neovimparser/directory. The Homebrew
install has Python's;uv tool install readaloud --with tree-sitter-rustadds Rust. - Each spoken word lights the word it came from: "takes" lights the first
parameter, "or" the type after the|, and a body of code keeps its lines. - A section can apply to one file type:
[config.py],[parts.py],[shapes.py],
[pronunciations.py]and[syntax.py]lay over the general ones for a file with
that suffix, andmarkdown = noreads it as written, so code keeps its#,*args
and underscores. See File types.
Shapes
- Shapes and parts live in the config file, with none built in: a
[shapes]line is
a regular expression and how to say what it matched,[parts]the named pieces the
patterns are built from. The shipped file readssettings.py:281-283as "settings dot
pie lines 281 to 283",localhost:8080as "local host port 8080", a URL with its
scheme, host, port and each slash of its path, and leaves a time as written. - Named shapes (
param: /pattern/ = wording) let a wording say a list of any
length:${params|param|,=and}says every match ofparamin the group, with "and"
for each comma. A shape may span 48 words and several lines. - What a shape or a rule says goes through the pronunciation entries and the patterns
once more, so.py = dot pieand/__(\w+)__/ = dunder $1apply inside them.
Config file: one change to make
- The settings section is
[config], not[readaloud]. A file that still says
[readaloud]is warned about and its settings fall back to their defaults until the
header is renamed. Everything else in an existing file reads as before. - The file a first run writes, and
--write-config, isreadaloud.confshipped as it
is, with one-line comments and the pronunciations in groups.
Other changes
follow_margindefaults to 5 rows: the spoken word stays about six rows from the top
in follow mode, with the text below it in view.- The README no longer suggests
mdcat --ansi notes.md | readaloud: a piped table reads
line by line;-mdreads it cell by cell. tests/fixtures/edge-cases.mdgathers the text that is hardest to read well, for
listening to by hand.
v1.2.3
v1.2.2
v1.2.1
v1.2.0
v1.1.0
Markdown with -md
readaloud -md notes.md shows a Markdown file the way mdcat --ansi draws it and reads its tables one cell at a time: row by row, left to right, with the highlight on the cell even where mdcat wraps it beside its neighbours. Piped through mdcat, a table's cells come out mixed together.
.and,step one cell, a click in a cell plays it, and follow mode keeps the whole row in view.- A short pause between cells and a longer one between rows, in the reader and in
--saveWAVs. - In a table, ticks and crosses are read as "yes" and "no", and arrows and key symbols by name (
⇧⌘is "shift, command"). - Footnotes are read. Badges are read by their alt text, without their URLs.
- A table readaloud cannot map is read line by line, with a notice.
-md needs mdcat on your PATH (brew install mdcat); nothing else in readaloud does.
Also
- Zero-width characters, non-ASCII spaces and private-use icons keep their width on screen, so padded rows line up with their rules.
- Startup notices (config warnings, media keys) take turns in the status bar instead of hiding behind "(+1 more)".
- The README gets the Apple silicon GPU right: MLX never used the Neural Engine.