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)
- Decimal place normalization (keep, strip trailing zeros, or pad to a fixed width)
- Scoping of number normalization: posting and
balanceamounts plus costs, price annotations andpricedirectives, or posting andbalanceamounts only - 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"
decimal_mode = "pad" # "keep", "minimal", or "pad"
decimal_places = 2 # fraction width used by "pad"
amount_scope = "all" # "all" or "amounts"
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) | Config file is the source of truth; settings are a fallback when absent |
# 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> |
2 |
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 |
--decimal-mode <MODE> |
keep |
Decimal places: keep, minimal (strip trailing zeros), or pad |
--decimal-places <N> |
2 |
Fraction width to pad to when --decimal-mode pad |
--amount-scope <SCOPE> |
all |
Which numbers --thousands and --decimal-mode reach: all (posting and balance amounts plus costs, price annotations and price directives), or amounts (posting and balance amounts only) |
--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",
decimal_mode="pad",
decimal_places=2,
amount_scope="all",
)
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,
2, // indent
70, // currency_column
75, // cost_column
0, // inline_comment_column
"keep", // thousands
"keep", // decimal_mode
2, // decimal_places
"all", // amount_scope
false, // spaces_in_braces
true, // fixed_cjk_width
"off", // sort
"keep", // sort_timeless
undefined, // sort_exclude
);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). The config file is the single source of truth: when one is present, the VSCode beanfmt.* settings are ignored entirely (keys the file omits fall back to built-in defaults), so format-on-save matches the CLI and CI. The VSCode settings only take effect when no config file is found.
Available settings:
| Setting | Default | Description |
|---|---|---|
beanfmt.indent |
2 |
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.decimalMode |
"keep" |
Decimal places: "keep", "minimal", or "pad" |
beanfmt.decimalPlaces |
2 |
Fraction width to pad to when decimalMode is "pad" |
beanfmt.amountScope |
"all" |
Which numbers get normalized: "all" (posting and balance amounts plus costs, price annotations and price directives), or "amounts" (posting and balance amounts only) |
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