A fast beancount file formatter with CJK double-width character support and smart date-based sorting.
- Column-aligned currencies and costs with CJK-aware display width
- Thousands separator normalization (add, remove, or keep)
- Brace spacing control for cost annotations
- Smart date-based sorting (asc/desc) with
timemetadata intra-day ordering, timeless entry positioning, and directive-type sort barriers - Recursive formatting of
included files (with glob support) - Multi-platform: CLI, Python library, WASM module, and VSCode extension
cargo install beanfmtpip install beanfmtInstall from the VSCode Marketplace, or from the command line:
code --install-extension cyrus-x.beanfmtBeanfmt supports TOML configuration files with a three-layer merge priority (low → high):
- Built-in defaults
- Global config:
$XDG_CONFIG_HOME/beanfmt/config.toml(defaults to~/.config/beanfmt/config.toml) - Project config:
.beanfmt.tomlorbeanfmt.toml(searched upward from the current directory) - CLI arguments (highest priority)
Example .beanfmt.toml:
indent = 2
currency_column = 60
cost_column = 65
inline_comment_column = 0 # column to align inline comments to; 0 disables
thousands = "add"
spaces_in_braces = true
fixed_cjk_width = true
sort = "asc" # "asc", "desc", "off", or true/false
sort_timeless = "keep" # "begin", "end", or "keep"
sort_exclude = ["open", "close"] # excluded types act as sort barriersAll fields are optional. Unspecified fields inherit from the next lower priority layer. Use --no-config to skip all configuration file loading. See beanfmt.toml for a full reference with comments.
Config file support varies by target:
| Target | Global config | Project config | Notes |
|---|---|---|---|
| CLI | Auto | Auto | --no-config to disable |
| Python | No | Opt-in via config=True |
load_project_config() for manual loading |
| VSCode | No (user settings serve this role) | Auto (within workspace) | Explicit settings override config file |
# Format from stdin
cat ledger.beancount | beanfmt
# Format a file (print to stdout)
beanfmt ledger.beancount
# Format in-place
beanfmt -w ledger.beancount
# Recursively format all included files in-place
beanfmt --recursive -w ledger.beancount
# Custom alignment columns
beanfmt --currency-column 60 --cost-column 65 ledger.beancount
# Add thousands separators and sort by date
beanfmt --thousands add --sort ledger.beancount| Flag | Default | Description |
|---|---|---|
--indent <N> |
4 |
Number of spaces for indentation |
--currency-column <N> |
70 |
Target column for currency alignment |
--cost-column <N> |
75 |
Target column for cost/price alignment |
--inline-comment-column <N> |
0 |
Target column to align inline comments (;) to; 0 disables alignment |
--thousands <MODE> |
keep |
Thousands separator: add, remove, or keep |
--spaces-in-braces / --no-spaces-in-braces |
off | Add spaces inside cost braces { ... } |
--fixed-cjk-width / --no-fixed-cjk-width |
on | CJK double-width alignment |
--sort [MODE] / --no-sort |
off |
Sort entries by date: asc (default if bare --sort), desc, off |
--sort-timeless <POS> |
begin |
Where to place timeless entries within a day: begin, end |
--sort-exclude <TYPES> |
(none) | Comma-separated directive types to exclude from sorting; excluded directives act as sort barriers. Values: transaction, balance, open, close, price, pad, note, document, event, custom, query, commodity |
--recursive |
off | Follow and format included files |
-w, --write |
off | Write output back to file (in-place) |
--no-config |
off | Skip loading configuration files |
import beanfmt
# Format a string
output = beanfmt.format(source, currency_column=60, sort=True)
# Format a file
output = beanfmt.format_file("ledger.beancount")
# Format with project config (.beanfmt.toml auto-discovery)
output = beanfmt.format_file("ledger.beancount", config=True)
# Format with a specific config file
output = beanfmt.format_file("ledger.beancount", config="/path/to/.beanfmt.toml")
# Config + kwargs override (kwargs win over config file values)
output = beanfmt.format_file("ledger.beancount", config=True, indent=8)
# Load and inspect project config
opts = beanfmt.load_project_config("/path/to/project/")
opts = beanfmt.parse_config('indent = 2\ncurrency_column = 80\n')
# Reusable options
opts = beanfmt.Options(currency_column=60, thousands_separator="add")
output = beanfmt.format(source, options=opts)
# Recursive formatting — returns list of (path, content) tuples
results = beanfmt.format_recursive("ledger.beancount", config=True)The config parameter on format_file and format_recursive accepts None (default, no config loading), True (auto-discover .beanfmt.toml from the file's directory upward), False (same as None), or a string path to a specific config file. Individual kwargs always override config file values. The config and options parameters are mutually exclusive.
import { format, format_default } from "beanfmt";
// Format with default options
const output = format_default(source);
// Format with full options
const output = format(source, 4, 70, 75, "keep", false, true, "off", "begin", undefined);Install the extension, then configure in settings.json:
The extension automatically reads .beanfmt.toml or beanfmt.toml from the workspace (searching from the file's directory up to the workspace root). Explicit VSCode settings override config file values; unset settings fall back to the config file, then to built-in defaults.
Available settings:
| Setting | Default | Description |
|---|---|---|
beanfmt.indent |
4 |
Number of spaces for indentation |
beanfmt.currencyColumn |
70 |
Currency alignment column |
beanfmt.costColumn |
75 |
Cost/price alignment column |
beanfmt.thousandsSeparator |
"keep" |
"add", "remove", or "keep" |
beanfmt.spacesInBraces |
false |
Spaces inside cost braces |
beanfmt.fixedCJKWidth |
true |
CJK double-width alignment |
beanfmt.sort |
"off" |
Sort entries by date: "asc", "desc", "off" |
beanfmt.sortTimeless |
"keep" |
Timeless entry position within a day: "begin", "end", "keep" |
beanfmt.sortExclude |
[] |
Directive types to exclude from sorting (act as sort barriers) |
Beanfmt is a Rust workspace with four build targets — library, CLI, Python (PyO3/maturin), and WASM (wasm-bindgen) — plus a VSCode extension.
- Rust toolchain (stable) and
just— required for all targets uv— Python extension (drives maturin)wasm-pack— WASM modulebun— VSCode extension
You only need the extra tools for the targets you actually build.
Common tasks are wrapped in just recipes:
just # List all recipes
just build # Build the library
just build-cli # Build the CLI binary
just build-python # Build the Python extension (uv + maturin)
just build-wasm # Build the WASM module
just build-vscode # Build the VSCode extension (WASM + TypeScript)
just test # Run all tests
just clippy # Run clippy lints
just fmt # Format code
just check # fmt-check + clippy + testOr build individual targets directly with Cargo:
cargo install --path . # CLI
maturin develop --features python # Python (requires maturin)Note: the
pythonandwasmfeatures are mutually exclusive.
MIT