Installation
Tuxedo is one native binary. Ruby projects usually get it through the tuxedo-r gem; anywhere else, download the binary itself. Either way, no Rust toolchain is needed.
With Bundler
group :development, :test do gem "tuxedo-r", require: false end
bundle install bundle exec tuxedo format --check .
With RubyGems
gem install tuxedo-r tuxedo format .
RubyGems picks the platform gem that matches your machine (for example tuxedo-r-x86_64-linux), which carries the precompiled binary. The gem needs Ruby 3.1 or newer and RubyGems 3.3.22 or newer; that is the Ruby that runs the tuxedo shim, not the Ruby syntax Tuxedo accepts. On Alpine and other musl hosts, an older RubyGems resolves the glibc gem, whose binary will not run: run gem update --system first.
Standalone binary
Every release on GitHub Releases has an archive per platform. Unpack it and put tuxedo somewhere on your PATH. Version 0.0.1 is built for one platform:
| Platform | Archive |
|---|---|
| Linux x86_64 (glibc) | tuxedo-x86_64-unknown-linux-gnu.tar.xz |
Tuxedo is not on crates.io: the name tuxedo there belongs to an unrelated crate, so cargo install tuxedo installs someone else's project. To build from source, clone the repository and run cargo build --release.
A platform without a prebuilt binary
On macOS, Windows, arm64 Linux and musl (Alpine), 0.0.2 has no prebuilt binary: gem install tuxedo-r succeeds, but running tuxedo reports that it cannot find the executable. Build Tuxedo from source, then set TUXEDO_INSTALL_DIR to the directory holding the tuxedo executable, and the gem's shim runs that one instead:
export TUXEDO_INSTALL_DIR=/path/to/tuxedo/target/release bundle exec tuxedo format --check .
Usage
tuxedo format . # rewrite files in place tuxedo format --check . # list files that would change; exit 1 if any tuxedo format --diff app/ # print a unified diff instead of writing cat a.rb | tuxedo format # format stdin to stdout
Tuxedo is at version 0.0.2: expect the layout, and the options that tune it, to keep moving between releases. Pin the version in your Gemfile.lock so a formatter upgrade is a deliberate commit, not a surprise in CI.
Command line
tuxedo format [OPTIONS] [PATHS]... is the whole interface. With no paths it reads one buffer from stdin and writes the result to stdout.
| Option | Effect |
|---|---|
--check | Report files that would change (would reformat: PATH) and write nothing |
--diff | Print a unified diff of what would change and write nothing |
--stdin-filename PATH | Name the stdin buffer PATH in messages, so an editor's diagnostics point at a real file |
--config PATH | Use this config file instead of searching for .tuxedo.toml |
-j, --jobs N | Number of threads (default: automatic) |
-q, --quiet | Omit the per-file lines and the summary of a write run. Errors, and the summary of --check or --diff, still print |
--no-ignore | Do not honour .gitignore |
--ast | Dump the Prism syntax tree of stdin, for debugging |
Exit codes
| Code | Meaning |
|---|---|
0 | Nothing to do |
1 | --check or --diff found a file that would change |
2 | A file could not be formatted: a syntax error, a changed meaning, an unreadable path or a failed write. A usage error also exits 2 |
A file that fails is left byte-identical.
What gets formatted
Given a directory, Tuxedo formats files ending in .rb, .rake, .gemspec or .ru, plus files named Rakefile or Gemfile. It skips hidden files, honours .gitignore (turn that off with --no-ignore), and skips whatever the config's exclude matches. A path named on the command line is formatted even if exclude matches it.
Configuration
Tuxedo reads .tuxedo.toml, found by walking up from the current directory, or the file named with --config. Every key is optional and an unknown one is an error. This is every key there is, with its default:
exclude = ["vendor/", "/db/schema.rb"] # skipped by format, on top of .gitignore
[format]
line_length = 120 # column a line is broken to fit
indent_size = 2 # spaces per indent level
hash_brace_space = "space" # "space" { a: 1 } or "no_space" {a: 1}
access_modifier_style = "indent" # "indent" or "outdent" for private/protected/public
quote_style = "double" # "double", "single" or "preserve"
| Key | Values | Default | Effect |
|---|---|---|---|
exclude | list of globs | [] | Paths a walk skips, on top of .gitignore |
format.line_length | integer | 120 | Column a line is broken to fit |
format.indent_size | integer | 2 | Spaces per indent level |
format.hash_brace_space | "space", "no_space" | "space" | { a: 1 } or {a: 1} |
format.access_modifier_style | "indent", "outdent" | "indent" | Where private, protected and public sit in a class body |
format.quote_style | "double", "single", "preserve" | "double" | The quote plain strings and quoted symbols are written with |
exclude
exclude takes gitignore-style globs, matched against the path from the directory the config file sits in: vendor/ matches at any depth, /db/schema.rb only at the top. --no-ignore turns .gitignore off but not exclude.
quote_style
Rewrites a plain string, a quoted symbol and a quoted hash key ('a': 1) to the chosen quote, and only when that loses nothing: a literal that the other quote would read as an escape or interpolation, or that holds that quote, keeps what was written. %q, %w, ?a, heredocs, and anything inside a #{} are never touched, and neither is an implicit concatenation whose parts would not all end up with the same quote. preserve rewrites nothing.
Monorepos
The config is found from the current working directory, not from the file being formatted. Run Tuxedo from the directory that holds the .tuxedo.toml you mean, or pass --config.
Turning the formatter off
# tuxedo:off and # tuxedo:on, each on a line of its own, copy the statements between them through exactly as written. It is for what a formatter cannot improve, such as a hand-aligned table:
# tuxedo:off MATRIX = [ [1, 0, 0], [0, 1, 0], [0, 0, 1] ] # tuxedo:on
A region with no on runs to the end of the body it opened in, and an off before the first statement holds the whole file. The markers go between statements. An on with no off before it, an off inside a region that is already off, and one in the middle of a literal (a hash, an argument list) are errors, reported like a syntax error, and the file is left untouched.
Continuous integration
With tuxedo-r in your Gemfile, a GitHub Actions job that fails when a file is not formatted:
jobs:
tuxedo:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ruby/setup-ruby@v1
with:
ruby-version: "3.3"
bundler-cache: true
- run: bundle exec tuxedo format --check .
Use --diff instead of --check to print what would change in the job log.
Editor integration
An editor that can pipe a buffer through a command can format with Tuxedo: tuxedo format --stdin-filename PATH reads the buffer on stdin and writes the formatted buffer to stdout.
Neovim, with conform.nvim
require("conform").setup({
formatters_by_ft = { ruby = { "tuxedo" } },
formatters = {
tuxedo = {
command = "tuxedo",
args = { "format", "--stdin-filename", "$FILENAME" },
stdin = true,
},
},
})
Emacs, with reformatter
(reformatter-define tuxedo-format :program "tuxedo" :args (list "format" "--stdin-filename" (or (buffer-file-name) "buffer.rb")))
VS Code
VS Code has no built-in hook for an external formatter; an extension such as Custom Local Formatters can run the same command.
Tuxedo and RuboCop
RuboCop is a reference, not the target. RuboCop is a linter, so it only ever adds line breaks. Tuxedo is a formatter and normalizes in both directions: given x = [1,\n 2], RuboCop block-expands it while Tuxedo collapses it to x = [1, 2]. Both are right by their own rules, so matching RuboCop byte-for-byte is not a goal.
Tuxedo only formats; RuboCop's non-layout cops are still yours to run. To run both, format first, then lint. A few RuboCop layout cops default to a style that Tuxedo's block indentation deliberately does not follow (for example Layout/ArgumentAlignment), so set those to their fixed-indentation styles. scripts/format-verify.rubocop.yml is the RuboCop layout configuration that Tuxedo's output satisfies.
Safety
Tuxedo never writes a file it cannot format faithfully. After formatting, it parses the output again and compares the syntax tree with the original's. If they differ, the file is refused, left exactly as it was, and the run exits 2.
The same check runs over real projects: on Rails, Thor, ruby/spec, Discourse and Homebrew (22,779 files) no file was refused, no file that parsed before failed to parse after, and formatting a second time changed nothing.
Ruby syntax
Tuxedo parses every file as the latest Ruby syntax Prism supports, whatever Ruby runs it. Code that relies on syntax removed in Ruby 3.4, such as keyword or block arguments in an index assignment (a[k: 1] = v), is rejected as a syntax error.