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:

PlatformArchive
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.

OptionEffect
--checkReport files that would change (would reformat: PATH) and write nothing
--diffPrint a unified diff of what would change and write nothing
--stdin-filename PATHName the stdin buffer PATH in messages, so an editor's diagnostics point at a real file
--config PATHUse this config file instead of searching for .tuxedo.toml
-j, --jobs NNumber of threads (default: automatic)
-q, --quietOmit the per-file lines and the summary of a write run. Errors, and the summary of --check or --diff, still print
--no-ignoreDo not honour .gitignore
--astDump the Prism syntax tree of stdin, for debugging

Exit codes

CodeMeaning
0Nothing to do
1--check or --diff found a file that would change
2A 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"
KeyValuesDefaultEffect
excludelist of globs[]Paths a walk skips, on top of .gitignore
format.line_lengthinteger120Column a line is broken to fit
format.indent_sizeinteger2Spaces 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.