██████╗ █████╗ ██╗ █████╗ ████████╗██████╗ ██████╗ ████████╗██╗ ██╗██╗ ██╔══██╗██╔══██╗██║ ██╔══██╗╚══██╔══╝██╔══██╗██╔═══██╗ ╚══██╔══╝██║ ██║██║ ██████╔╝███████║██║ ███████║ ██║ ██████╔╝██║ ██║ ██║ ██║ ██║██║ ██╔══██╗██╔══██║██║ ██╔══██║ ██║ ██╔══██╗██║ ██║ ██║ ██║ ██║██║ ██████╔╝██║ ██║███████╗██║ ██║ ██║ ██║ ██║╚██████╔╝ ██║ ╚██████╔╝██║ ╚═════╝ ╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═════╝ ╚═╝
Play Balatro in your terminal. No modifications to game code.
A Rust reimplementation of the LOVE2D game engine that renders entirely in a terminal. It runs unmodified Balatro Lua code — the game thinks it's talking to a real LOVE2D runtime, but all graphics go through a software pixel buffer and are displayed via the Sixel graphics protocol (true pixels), Unicode octant characters (2×4 sub-pixels per cell), or half-block characters.
The result: a fully playable Balatro in your terminal.
- Rust 1.75+ with a C compiler (for vendored Lua 5.1)
- A copy of Balatro (
Balatro.exe) - A modern terminal with 24-bit color support (Windows Terminal 1.22+, WezTerm, iTerm2, kitty, Alacritty)
cargo build --releaseWindows tip: If your project is on OneDrive, use a separate build directory to avoid sync lag:
CARGO_TARGET_DIR="C:/tmp/balatro_build" cargo build --release # Git Bash$env:CARGO_TARGET_DIR="C:/tmp/balatro_build"; cargo build --release # PowerShell
Linux / macOS / Git Bash:
cargo run --release -- "path/to/Balatro.exe"Windows PowerShell:
cargo run --release -- "C:/Program Files (x86)/Steam/steamapps/common/Balatro/Balatro.exe"Sixel mode is used by default. Set
TUI_RENDER=octantorTUI_RENDER=halfblockto switch (see Configuration).
Pre-built binaries for Windows, Linux, and macOS are available on the Releases page.
./love-terminal "path/to/Balatro.exe" # Linux / macOS.\love-terminal.exe "path\to\Balatro.exe" # Windows PowerShell| Key | Action |
|---|---|
| Arrow keys | Navigate menus |
| Enter | Confirm / Play hand |
| Escape | Back / Cancel |
| E | Select / deselect cards |
| Tab | Switch between hand and jokers |
| Space | Sort hand |
Balatro's UI is designed for gamepad input. Keyboard keys are automatically mapped to
gamepadpressed/gamepadreleasedevents for full compatibility.
Balatro Lua code
│
▼
love.graphics.* API calls (Rust)
│
▼
Software pixel buffer + shader emulation
│
▼
Terminal output
┌──────────────────────────────────────┐
│ Sixel: true pixel graphics │ ← best quality
│ Octant: 2×4 sub-pixels/cell │ ← default
│ Half-block: ▀ fg/bg per cell │ ← fallback
└──────────────────────────────────────┘
The project embeds a Lua 5.1 VM via mlua and implements ~80 love.* API functions. All graphics pass through a software rasterizer — rectangles, ellipses, polygons, sprites, text — with anti-aliasing, bilinear filtering, and proper alpha blending.
Sixel mode renders actual pixels via the Sixel graphics protocol. The internal canvas matches the terminal's pixel area (e.g. 1080×600 on a 120×30 terminal), giving true pixel-level fidelity with 256-color quantization per frame. Octant mode (default) uses Unicode octant characters (🬀–🬻) to achieve 2×4 sub-pixel resolution per cell with gamma-correct downsampling. Half-block mode uses ▀ (U+2580) with fg=top/bg=bottom as a universal fallback.
balatro_port_tui/
├── love-terminal/ # Binary crate — entry point + game loop
│ └── src/
│ ├── main.rs # CLI parsing, path resolution
│ └── runner.rs # Terminal setup, frame loop, auto-input
├── love-api/ # Library crate — LOVE2D API implementation
│ └── src/
│ ├── lib.rs # LoveRuntime: Lua VM + module registration
│ ├── state.rs # SharedState (Arc<Mutex> fields)
│ ├── graphics.rs # love.graphics.* (3,400+ lines)
│ ├── filesystem.rs # love.filesystem + zip loader + require
│ ├── event.rs # love.event (keyboard/mouse → events)
│ ├── keyboard.rs # love.keyboard.isDown
│ ├── window.rs # love.window (terminal as display)
│ ├── timer.rs # love.timer (delta, FPS, sleep)
│ ├── system.rs # love.system (getOS → "Linux")
│ ├── stubs.rs # audio, thread, mouse, joystick stubs
│ └── lua_util.rs # Color parsing helpers
└── sprite-to-text/ # Library crate — pixel buffer + renderer
└── src/
├── pixel_buffer.rs # RGBA framebuffer (2,000+ lines)
└── renderer.rs # Downsampling → ratatui terminal output
~9,800 lines of Rust across 16 source files.
- Software rasterizer — rectangles, rounded rects, ellipses, polygons, thick lines, all anti-aliased
- Sprite rendering —
love.graphics.drawwith images, quads, sprite batches, bilinear filtering - Text rendering — TTF via fontdue, colored segments, word-wrapped
printf - Transform stack — translate, scale, rotate with matrix composition
- Canvas system — offscreen render targets with premultiplied alpha
- Stencil buffer — write + test operations for masked rendering
- Blend modes — alpha, replace, additive, multiply, premultiplied
Since there's no GPU, all shaders are emulated per-pixel in Rust:
| Shader | Effect |
|---|---|
| Background | Procedural swirl — 60×45 grid, paint distortion, 3-color mixing |
| CRT | Bloom extraction + Gaussian blur + contrast + vignette |
| Dissolve | Per-pixel noise threshold with burn-edge coloring |
| Foil | Radial + angular shimmer, silvery-blue metallic (from GLSL) |
| Holographic | HSL rainbow shift + hexagonal grid + noise field (from GLSL) |
| Polychrome | HSL hue rotation via animated noise, boosted saturation (from GLSL) |
| Negative Shine | 5-component sine wave shimmer with blue tint (from GLSL) |
| Gold Seal | Animated golden sine-wave highlight sweep (from GLSL) |
| Debuff | Desaturation + reddish tint + diagonal stripes (from GLSL) |
| Played | HSL desaturation + darkening |
| Flash / Shadow / Flame | Overlay effects (white flash, dark shadow, gradient flame) |
Balatro runs unmodified thanks to several compatibility tricks:
love.system.getOS()→"Linux"— skips Steam initializationrequire "luasteam"stub — returns{ init = function() return false end }require "bit"in Rust — Lua 5.1 lacks thebitlibrary that LuaJIT provides; implemented asbxor/band/bor/bnot/lshift/rshift- Custom
love.run()— Balatro returns a per-frame closure instead of using standard callbacks - Keyboard → Gamepad mapping — keys automatically fire
gamepadpressed/gamepadreleasedevents
All settings are controlled via environment variables. None are required — defaults work well for most setups.
| Variable | Default | Description |
|---|---|---|
TUI_RENDER |
sixel |
Rendering mode: sixel, octant, or halfblock |
TUI_PIXELBUDGET |
250000 |
Max canvas pixels for Sixel mode. Main FPS vs quality knob. Lower = faster, higher = sharper. See details below |
TUI_SCALE |
2 |
Canvas divider. For octant/halfblock: canvas = terminal_cells × scale. For Sixel: overrides PIXELBUDGET, canvas = sixel_target / scale |
TUI_CELL |
auto-detect | Cell pixel size for Sixel, format WxH (e.g. 9x20, 11x24). Use if the image doesn't fill the terminal or overflows |
TUI_COLS |
auto | Override terminal column count |
TUI_ROWS |
auto | Override terminal row count |
TUI_DEBUG |
off | Enable timing logs every 120 frames (set to any value to activate) |
TUI_AUTOPLAY |
off | Auto-play mode: progresses through title → blind select → play hand → shop automatically |
TUI_SNAPSHOT |
off | Save PPM screenshots of the pixel buffer every 200 frames (frames 200–12000) |
This is the most important performance setting for Sixel mode. It controls how many pixels the internal canvas has.
In Sixel mode, the Sixel target = terminal columns × 10 × terminal rows × 20 (VT340 virtual cell size). On a large terminal (e.g. 280×70) that's 2800×1400 = 3.92 million pixels — far too many for CPU rendering at 60 FPS.
The canvas is automatically downscaled so its total pixel count stays within the budget:
scale = sqrt(sixel_target_pixels / budget)
canvas = sixel_target / scale
| Budget | Canvas (at 280×70 term) | Quality | FPS (typical) |
|---|---|---|---|
100000 |
~450×225 | Low | 60+ |
250000 (default) |
~707×354 | Good | 50-60 |
500000 |
~1000×500 | High | 30-45 |
1000000 |
~1414×707 | Very high | 15-25 |
The canvas is drawn by Lua/shaders at this reduced resolution, then the Sixel encoder scales it up to the full terminal pixel area using nearest-neighbor interpolation. Lower budget = fewer pixels to shade = faster frames.
Octant/Half-block mode: canvas width = terminal columns × 2 × scale, canvas height = terminal rows × 4 × scale (octant) or rows × 2 × scale (halfblock). Default 2 gives good text readability. 3 is smoother but smaller text.
Sixel mode: if set, overrides PIXELBUDGET. Canvas = sixel_target / scale. E.g. TUI_SCALE=4 on a 2800×1400 target gives a 700×350 canvas.
Sixel mode needs to know how many pixels each terminal cell occupies. Auto-detection chain:
TUI_CELLenv var (if set, uses this directly)- Window pixel area via process tree walk (Windows Terminal)
- CSI 14 t terminal query (Linux/macOS)
- GetConsoleWindow + ConsoleFontEx (Windows legacy console)
- crossterm terminal size query
- DPI-based estimation (Windows — assumes Cascadia Mono at 9.6×20 per 96 DPI)
- Fallback: 9×20
If the rendered image is too small or too large for your terminal, set TUI_CELL manually. To find your cell size: divide your terminal window's pixel dimensions by column × row count.
| Mode | Resolution | Quality | Requirement |
|---|---|---|---|
sixel (default) |
Terminal pixel area (e.g. 1080×600) | Excellent | Terminal with Sixel support |
octant |
2×4 sub-pixels/cell (~560×280) | Good | Cascadia Code 2404.23+ |
halfblock |
▀ top/bottom per cell | Fair | Any terminal |
Sixel mode renders actual pixels via the Sixel graphics protocol, bypassing the 2-color-per-cell limitation. 256-color quantization per frame. Requires: Windows Terminal 1.22+, WezTerm, foot, mlterm, xterm.
Octant mode uses Unicode octant characters (🬀–🬻) for 2×4 sub-pixel resolution per cell with gamma-correct downsampling. Requires Cascadia Code 2404.23+ font.
Half-block mode uses ▀ (U+2580) with fg=top pixel, bg=bottom pixel. Works everywhere.
Linux / macOS / Git Bash:
cargo run --release -- "path/to/Balatro.exe" # Sixel (default)
TUI_CELL=11x24 cargo run --release -- "path/to/Balatro.exe" # Sixel + manual cell size
TUI_PIXELBUDGET=500000 cargo run --release -- "path/to/Balatro.exe" # Sixel + higher quality
TUI_PIXELBUDGET=100000 cargo run --release -- "path/to/Balatro.exe" # Sixel + max FPS
TUI_RENDER=octant cargo run --release -- "path/to/Balatro.exe" # Octant mode
TUI_RENDER=halfblock cargo run --release -- "path/to/Balatro.exe" # Half-block fallback
TUI_DEBUG=1 cargo run --release -- "path/to/Balatro.exe" # Show frame timing
TUI_AUTOPLAY=1 cargo run --release -- "path/to/Balatro.exe" # Auto-play demoWindows PowerShell:
cargo run --release -- "path/to/Balatro.exe" # Sixel (default)
$env:TUI_CELL="11x24"; cargo run --release -- "path/to/Balatro.exe" # Sixel + manual cell size
$env:TUI_PIXELBUDGET="500000"; cargo run --release -- "path/to/Balatro.exe" # Sixel + higher quality
$env:TUI_PIXELBUDGET="100000"; cargo run --release -- "path/to/Balatro.exe" # Sixel + max FPS
$env:TUI_RENDER="octant"; cargo run --release -- "path/to/Balatro.exe" # Octant mode
$env:TUI_RENDER="halfblock"; cargo run --release -- "path/to/Balatro.exe" # Half-block fallback
$env:TUI_DEBUG="1"; cargo run --release -- "path/to/Balatro.exe" # Show frame timing
$env:TUI_AUTOPLAY="1"; cargo run --release -- "path/to/Balatro.exe" # Auto-play demo- No audio —
love.audiois fully stubbed (play/stop/setVolume are no-ops) - No threading —
love.threadis stubbed - Mouse precision — mouse events map terminal coordinates to canvas, but precision is limited by cell size
- Shader approximations — CPU-based shader emulation is faithful but not pixel-identical to GPU GLSL
- Sixel color limit — Sixel mode quantizes each frame to 256 colors, which is sufficient for Balatro's art style but not lossless
- Cell size detection on Windows — Windows Terminal doesn't expose pixel dimensions; use
TUI_CELLenv var if the image doesn't fill the screen
| Crate | Purpose |
|---|---|
| mlua | Lua 5.1 VM embedding |
| ratatui | Terminal UI framework |
| crossterm | Cross-platform terminal I/O |
| fontdue | TTF font rasterization |
| zip | Reading game files from exe |
| flate2 | Deflate compress/decompress |
| parking_lot | Fast mutexes for shared state |
| anyhow | Error handling |
This project is licensed under the Apache License 2.0.
Disclaimer: Balatro is a game by LocalThunk. This project is an independent engine reimplementation for educational purposes — it does not include any game assets. You must own a legitimate copy of Balatro to use this software.


