# Ruby Development Windows Native Setup
Adding Ruby development on top of a Windows 11 machine already configured per [[General_Development_Windows_Native_Setup]]. Most of the stack carries over — Windows Terminal, PowerShell 7, Starship, Git, GitHub CLI, VS Code, and Claude Code all work identically. This guide only covers the Ruby-specific additions, and the same commands work on both **x64 (Intel/AMD)** and **ARM64 (Snapdragon, Surface Pro 11, Dev Kit 2023)**, with a small number of ARM64 caveats called out where they matter. It's the Windows-native counterpart to [[Ruby_Development_Ubuntu_Setup]] and [[Ruby_Development_Mac_Tahoe_Setup]].
> [!tip] AI assistance is optional
> The reference to Claude Code above assumes you completed Part 7 of the General guide. If you skipped it or use a different AI tool, this guide's instructions still work — the language toolchain doesn't depend on any AI being present.
> [!info] Coexistence
> C, Python, Ruby, Go, and Rust can all live on the same Windows 11 system without interfering. Each language's toolchain installs to its own prefix: `uv` under `%USERPROFILE%\.local\share\uv\`, `rv` under `%USERPROFILE%\.cargo\bin\` + `%LOCALAPPDATA%\rv\`, Ruby itself under `C:\Ruby34-x64\` (or `C:\Ruby34-arm64\`) via RubyInstaller, Go under `%USERPROFILE%\go\`, Rust under `%USERPROFILE%\.rustup\` and `%USERPROFILE%\.cargo\`, C tooling under `C:\Program Files\Microsoft Visual Studio\`. No conflicts.
> [!warning] Ruby on Windows is the trickiest of the five
> Ruby has historically been a second-class citizen on Windows because so much of the gem ecosystem assumes a Unix-like environment. The modern story (RubyInstaller + the MSYS2 Devkit) makes this work cleanly on **x64**, and works on **ARM64** with a handful of caveats. Read the ARM64 callouts in each section before installing.
---
## The two-tool story: RubyInstaller + rv
Unlike Linux and macOS — where `rv` handles *both* version management *and* the Ruby runtime download in a single tool — Windows needs **two tools**:
| Tool | Role on Windows | Why |
|------|-----------------|-----|
| **RubyInstaller** | Provides the actual Ruby runtime (`ruby.exe`), the MSYS2/MinGW dev toolchain, and standard library | The only source of precompiled, native, Devkit-bundled Ruby for Windows |
| **rv** | Version manager that switches between RubyInstaller-installed rubies | Lets you keep 3.3 and 3.4 side-by-side and pin one per project; `rv install` itself does **not** publish Ruby binaries for Windows |
Why the split? RubyInstaller has been the canonical Windows Ruby distribution for over a decade and is the only project that builds Ruby binaries that include the **MSYS2 Devkit** — the Unix-like GNU toolchain that lets gems with C extensions (nokogiri, pg, redcarpet, sqlite3, ffi, and dozens more) compile on install. `rv` is a fantastic version manager (a direct analog of `uv` for Python), but as of June 2026 its Windows builds don't yet ship precompiled rubies via `rv install`. The pragmatic Windows setup is therefore: **install each Ruby version via its own RubyInstaller MSI, then use `rv` to switch between them**.
> [!info] Single-version users can skip rv
> If you only ever need one Ruby version on this machine, RubyInstaller alone is sufficient — the installer puts `ruby.exe` on `PATH`, and that's the end of it. Install `rv` only when you need to juggle multiple Ruby versions or pin a specific version per project.
The 2026 Windows Ruby landscape, for context:
| Tool | Best for | Notes |
|------|----------|-------|
| **RubyInstaller + rv** | The 2026 recommendation | Native Windows runtime + fast Rust-based switcher |
| **RubyInstaller alone** | Single-version users | Simplest possible setup |
| **uru** | Multi-version, older | Pre-`rv` standard, still works, less active |
| **pik** | Don't bother | Long-abandoned |
| **WSL2 + rv** | If you don't need native Windows Ruby | See [[WSL2 Windows Development Setup]] — the Ubuntu Ruby guide applies inside WSL |
| **`mise`** | Multi-language users | Has Windows support; good if you also run Python/Node/Go through it |
---
## Install Ruby via RubyInstaller
Use winget — it pulls the current RubyInstaller MSI with Devkit bundled, and resolves the right binary for your CPU:
```powershell
winget install -e --id RubyInstallerTeam.RubyWithDevKit.3.4
```
The `RubyWithDevKit` package matters: there's also a plain `Ruby` package without Devkit, but **always pick the WithDevKit variant** — without it, most non-trivial gems will fail to install the moment they try to compile a C extension.
> [!info] Native ARM64 RubyInstaller landed in 3.4.1
> The RubyInstaller project added **native ARM64 builds in version 3.4.1** (early 2025). winget on an ARM64 host resolves to the ARM64 MSI automatically. On older Snapdragon machines that still run 3.3 or earlier, Ruby works under the x64-on-ARM64 emulation layer, but you lose the performance benefit of native ARM64 — always prefer 3.4.1+.
Open a **new** PowerShell window after install (so the new `PATH` entries are visible), then verify:
```powershell
ruby -v
# ruby 3.4.x (...) [x64-mingw-ucrt] on x64
# ruby 3.4.x (...) [arm64-mingw-ucrt] on ARM64
gem -v
# 3.5.x or newer
(Get-Command ruby).Source
# C:\Ruby34-x64\bin\ruby.exe (or C:\Ruby34-arm64\bin\ruby.exe)
```
The `mingw-ucrt` suffix in the platform string is important — it's the modern UCRT-based MSYS2 build that the gem ecosystem has standardized on since Ruby 3.1. The older `mingw32` platform from Ruby 2.x is gone.
### Run the post-install: `ridk install`
The MSI installs Ruby itself, but the Unix-like build tools needed for compiling gem C extensions arrive via a second step. **This is the single most important command in this guide** — skip it and roughly half of all popular gems will fail at `bundle install`:
```powershell
ridk install
```
`ridk` ("Ruby Installer Development Kit") is a small CLI installed alongside Ruby that downloads and configures MSYS2 (the Windows port of the GNU userland and toolchain). When run interactively it shows a menu:
```
1 - MSYS2 base installation
2 - MSYS2 system update (optional)
3 - MSYS2 and MINGW development toolchain
```
Pick **`1 2 3`** to do everything:
```powershell
ridk install 1 2 3
```
This downloads MSYS2 (~500 MB), updates its package database, and installs the MinGW toolchain (gcc, make, autoconf, m4, pkg-config, etc.). It lands in `C:\Ruby34-x64\msys64\` (or the ARM64 equivalent) so it doesn't pollute your global PATH outside the Ruby shell.
Verify the toolchain works:
```powershell
ridk version
# Should print MSYS2 and MinGW versions
ridk exec gcc --version
# Should print a gcc version
```
> [!warning] ARM64 caveat — MSYS2 is not fully ported
> As of mid-2026, the MSYS2 project's **Unix-tools layer (autoconf, m4, libtool) is not yet fully native on ARM64**. `ridk install` on an ARM64 host installs what's available, but gems that lean heavily on autoconf-style build scripts can fail. Concretely:
>
> - Pure-Ruby gems → fine
> - Gems with simple C extensions (e.g., `bcrypt`, `redcarpet`, `msgpack`) → fine, the MinGW gcc is native ARM64
> - Gems that vendor an autotools build (older `nokogiri` versions, `pg` built against libpq from source, some image-processing gems) → may fail
>
> Workarounds, in order of preference:
> 1. **Use precompiled gem binaries** — many of the historically tricky gems (`nokogiri`, `sqlite3`, `pg`, `ffi`) now ship `arm64-mingw-ucrt` precompiled binaries on RubyGems. `bundle install` picks these up automatically if the platform is in your lockfile (see the Bundler section below).
> 2. **Use a pure-Ruby alternative** — for example, `rexml` instead of `nokogiri` for simple XML, or the `mini_sqlite3` gem.
> 3. **Build under WSL2** — for one-off gems that won't cooperate, install them inside WSL2 Ubuntu and copy the resulting `.so`. Rarely needed.
> 4. **Wait for MSYS2's ARM64 port to complete** — active work, expected to land in full through 2026.
### Install `rv` as the version switcher
Skip this section if you only need one Ruby version.
`rv` ships in winget:
```powershell
winget install -e --id Spinel.rv
```
If winget doesn't have it in your channel yet, install via Cargo (which you have if you've already done [[Rust_Development_Windows_Native_Setup]]):
```powershell
cargo install rv-cli
```
Either way, `rv.exe` lands in `%USERPROFILE%\.cargo\bin\` (cargo install) or in Program Files (winget). Open a new PowerShell window and verify — but **use `rvw`, not `rv`**:
```powershell
rvw --version
```
> [!warning] `rv` is a built-in PowerShell alias for `Remove-Variable`
> Typing `rv` in PowerShell does **not** invoke the Ruby version manager — it removes a PowerShell variable. The `rvw` shim (which the installer adds alongside `rv`) is the PowerShell-safe wrapper. Use `rvw` everywhere this guide says `rv`. If you prefer `rv`, override the built-in alias in your `$PROFILE`:
> ```powershell
> # Force rv to mean the Ruby version manager, not Remove-Variable
> Remove-Item Alias:rv -Force -ErrorAction SilentlyContinue
> Set-Alias -Name rv -Value rvw -Option AllScope
> ```
> This works, but `rvw` is what every Windows Ruby tutorial and bug report uses — sticking with `rvw` makes copy-paste from the internet less surprising.
### Tell rv about your RubyInstaller-installed rubies
`rv` doesn't auto-discover RubyInstaller installs. Register them once:
```powershell
# List what rv knows about so far
rvw list
# (empty)
# Discover and register the RubyInstaller installs in C:\
rvw discover
rvw list
# 3.4.1 C:\Ruby34-x64\bin\ruby.exe (pinned)
```
If `rvw discover` doesn't find your install (e.g., you put Ruby in a non-default location), point at it manually:
```powershell
rvw add 3.4.1 C:\Ruby34-x64\bin\ruby.exe
```
### Install a second Ruby version
When you need a second version, install it via RubyInstaller (not `rvw install`) and then register it:
```powershell
# Install Ruby 3.3 alongside 3.4
winget install -e --id RubyInstallerTeam.RubyWithDevKit.3.3
ridk install 1 2 3 # answer "Y" to use the existing MSYS2 if asked
# Register with rv
rvw discover
rvw list
# 3.4.1 C:\Ruby34-x64\bin\ruby.exe
# 3.3.6 C:\Ruby33-x64\bin\ruby.exe
```
### Pin a default
```powershell
rvw pin 3.4 --global
ruby -v
# Now uses 3.4
```
Inside a project directory, pin per-project — this writes a `.ruby-version` file that other tools (Bundler, your editor, CI) also respect:
```powershell
cd $HOME\projects\some-project
rvw pin 3.3
type .ruby-version
# 3.3.6
```
---
## Bundler — the dependency manager
Bundler is Ruby's equivalent of `pip` + `venv` combined. It ships with modern Ruby (no install needed), but upgrade once:
```powershell
gem update --system
gem install bundler
bundle --version
```
Configure Bundler to install gems into a project-local `vendor\bundle\` directory by default (keeps project environments isolated, similar to Python venvs):
```powershell
bundle config set --global path 'vendor/bundle'
```
> [!info] Why forward slashes in `'vendor/bundle'`?
> Bundler stores its path config in YAML and normalizes separators internally — both `vendor/bundle` and `vendor\bundle` work on Windows. Stick with forward slashes for consistency with the Linux/macOS guides; the file `bundle install` actually creates uses Windows-native paths regardless.
> [!info] Why project-local gems?
> Without this config, `bundle install` installs gems globally for the current Ruby version. That works, but it means different projects can silently conflict on gem versions. Setting `path` to `vendor/bundle` gives each project its own isolated gem directory, just like `uv`'s `.venv\`.
### The Windows platform-lock quirk
Bundler tracks the **platform** of every gem in `Gemfile.lock`. When a teammate hands you a project whose `Gemfile.lock` was generated on Linux or macOS, the lockfile only knows about `x86_64-linux` or `arm64-darwin` — and `bundle install` on Windows fails or silently downgrades to the slow pure-Ruby version of any gem with a native extension.
The fix is one command, run once per project:
```powershell
# On x64 Windows
bundle lock --add-platform x64-mingw-ucrt
# On ARM64 Windows
bundle lock --add-platform arm64-mingw-ucrt
```
This rewrites `Gemfile.lock` to include the Windows platform alongside whatever was there before, so future `bundle install` calls pick the right precompiled gem binary. Commit the updated lockfile back to git — your Mac/Linux teammates don't need to re-lock, since Bundler keeps all known platforms in the file.
> [!tip] Add both platforms if your team uses both
> Multi-arch Windows teams (some on Snapdragon, some on Intel) can add both at once:
> ```powershell
> bundle lock --add-platform x64-mingw-ucrt arm64-mingw-ucrt
> ```
---
## VS Code extensions for Ruby
```powershell
# Shopify's Ruby LSP — modern language server, actively maintained
code --install-extension Shopify.ruby-lsp
# Rubocop — linter and formatter integration
code --install-extension rubocop.vscode-rubocop
# ERB (embedded Ruby) syntax for Rails templates
code --install-extension aliariff.vscode-erb-beautify
# RSpec test runner
code --install-extension connorshea.vscode-ruby-test-adapter
```
> [!info] Ruby LSP vs older Ruby extensions
> The official "Ruby" extension (Peckett) and "Solargraph" are older alternatives. Shopify's Ruby LSP has become the standard in 2026 — better performance, actively developed, and production-tested on Shopify's massive Ruby codebases. If you see tutorials recommending Solargraph, they're older than 2024.
---
## VS Code settings
VS Code stores user settings on Windows at:
```
%APPDATA%\Code\User\settings.json
```
Open it via `Ctrl+Shift+P` → "Preferences: Open User Settings (JSON)" and append:
```jsonc
{
// ── Ruby ──────────────────────────────────────────────────
"[ruby]": {
"editor.defaultFormatter": "Shopify.ruby-lsp",
"editor.tabSize": 2,
"editor.insertSpaces": true,
"editor.formatOnSave": true,
"editor.rulers": [100]
},
// Ruby LSP — RubyInstaller puts ruby.exe on PATH directly, so the
// LSP just picks it up; no shell-init dance like Linux/Mac.
"rubyLsp.rubyVersionManager": {
"identifier": "none"
},
"rubyLsp.formatter": "rubocop",
"rubyLsp.enabledFeatures": {
"codeActions": true,
"diagnostics": true,
"documentHighlights": true,
"documentLink": true,
"documentSymbols": true,
"foldingRanges": true,
"formatting": true,
"hover": true,
"inlayHint": true,
"onTypeFormatting": true,
"selectionRanges": true,
"semanticHighlighting": true,
"completion": true,
"codeLens": true,
"definition": true,
"workspaceSymbol": true,
"signatureHelp": true,
"typeHierarchy": true
}
}
```
> [!info] Why `"identifier": "none"` on Windows
> On Linux and macOS, the Ruby LSP needs a shell command (`rv shell init bash`, `rv shell init zsh`) to pick up `rv`'s shims because the active Ruby is shell-resolved. On Windows, RubyInstaller writes its `bin\` directory straight into the user PATH at install time — so the `ruby.exe` that VS Code's LSP finds is already the pinned one. Setting `identifier` to `"none"` tells the LSP "trust the PATH, don't run a shell init script," which avoids the LSP trying to launch bash/zsh on a system that doesn't have either.
> [!tip] If you use rv to switch versions
> The `none` setting still works because `rvw pin` rewrites the PATH entry for the active Ruby. After running `rvw pin 3.3 --global`, restart any open VS Code windows — the LSP re-discovers `ruby.exe` on next start.
---
## Full demo: A calculator program
This builds the same four-function calculator as the other language guides, but Ruby-idiomatic. The Ruby code itself is identical to the Linux and macOS versions — only the install steps and PowerShell-specific quoting change.
### Create the project
```powershell
cd $HOME\projects
mkdir calc-ruby; cd calc-ruby
rvw pin 3.4
mkdir lib, bin, spec
```
### `Gemfile` — dependency spec
Create the file with VS Code (`code Gemfile`) and paste:
```ruby
source "https://rubygems.org"
ruby "3.4"
gem "rspec", "~> 3.13", group: :test
gem "rubocop", "~> 1.68", group: :development
gem "rubocop-rspec", "~> 3.0", group: :development
```
Install the gems (into `vendor\bundle\` thanks to the earlier config):
```powershell
bundle install
```
This creates `Gemfile.lock` (commit it to git — it's the reproducible lockfile). If you're starting from a clone of a non-Windows project, add the Windows platform first:
```powershell
bundle lock --add-platform x64-mingw-ucrt # or arm64-mingw-ucrt
bundle install
```
### `lib\calculator.rb` — the core logic
```ruby
# frozen_string_literal: true
module Calculator
module_function
def add(a, b) = a + b
def sub(a, b) = a - b
def mul(a, b) = a * b
def div(a, b)
raise ZeroDivisionError, "division by zero" if b.zero?
a.fdiv(b)
end
OPERATIONS = {
"+" => :add,
"-" => :sub,
"*" => :mul,
"x" => :mul,
"/" => :div
}.freeze
def calculate(a, op, b)
method_name = OPERATIONS[op] or
raise ArgumentError, "unknown operator '#{op}'"
public_send(method_name, a.to_f, b.to_f)
end
end
```
### `bin\calc` — the command-line entry point
```ruby
#!/usr/bin/env ruby
# frozen_string_literal: true
require_relative "../lib/calculator"
def usage
warn "Usage: #{File.basename($PROGRAM_NAME)} <number> <op> <number>"
warn " op: + - * /"
warn "Example: #{File.basename($PROGRAM_NAME)} 6 + 7"
exit 1
end
usage unless ARGV.length == 3
begin
a, op, b = ARGV
result = Calculator.calculate(a, op, b)
# Print as integer if it came out whole, otherwise as a float
puts(result == result.to_i ? result.to_i : result)
rescue ArgumentError, ZeroDivisionError => e
warn "Error: #{e.message}"
exit 1
end
```
> [!info] No `chmod +x` on Windows
> Windows doesn't track an executable bit on files. The shebang line at the top is still useful (it documents intent, and tools like Bundler-binstubs read it), but you launch the script with `ruby bin\calc 6 + 7` rather than `.\bin\calc 6 + 7`. If you want bare-name invocation, see the "binstubs" tip below.
### `spec\calculator_spec.rb` — tests with RSpec
```ruby
# frozen_string_literal: true
require_relative "../lib/calculator"
RSpec.describe Calculator do
describe ".calculate" do
it "adds two numbers" do
expect(Calculator.calculate("6", "+", "7")).to eq(13.0)
end
it "subtracts two numbers" do
expect(Calculator.calculate("10", "-", "3")).to eq(7.0)
end
it "multiplies two numbers (with * or x)" do
expect(Calculator.calculate("4", "*", "5")).to eq(20.0)
expect(Calculator.calculate("4", "x", "5")).to eq(20.0)
end
it "divides two numbers" do
expect(Calculator.calculate("20", "/", "4")).to eq(5.0)
end
it "raises on division by zero" do
expect { Calculator.calculate("5", "/", "0") }
.to raise_error(ZeroDivisionError)
end
it "raises on unknown operator" do
expect { Calculator.calculate("1", "?", "2") }
.to raise_error(ArgumentError, /unknown operator/)
end
end
end
```
### `.rspec` — RSpec defaults
```
--require spec_helper
--format documentation
--color
```
### `spec\spec_helper.rb`
```ruby
# frozen_string_literal: true
RSpec.configure do |config|
config.expect_with :rspec do |c|
c.syntax = :expect
end
end
```
### `.rubocop.yml` — linter config
```yaml
AllCops:
TargetRubyVersion: 3.4
NewCops: enable
SuggestExtensions: false
Style/Documentation:
Enabled: false
Metrics/MethodLength:
Max: 20
```
### `.gitignore`
```
# Ruby
.bundle/
vendor/bundle/
*.gem
log/
tmp/
.rspec_status
# Windows-specific
*.exe~
Thumbs.db
desktop.ini
```
The `*.exe~` entry catches MSYS2 backup binaries that occasionally land in the working tree after a `bundle install` that recompiled a gem extension. `Thumbs.db` and `desktop.ini` are the Windows Explorer droppings that don't belong in a repo.
### Run everything
```powershell
# Run the program (PowerShell needs to know to launch ruby; no exec bit)
ruby bin\calc 6 + 7
ruby bin\calc 10 - 3
ruby bin\calc 4 x 5
ruby bin\calc 20 / 4
# Run tests
bundle exec rspec
# Lint
bundle exec rubocop
# Auto-fix safe lint issues
bundle exec rubocop -a
```
> [!tip] Bare-name invocation via binstubs
> If you want `calc 6 + 7` instead of `ruby bin\calc 6 + 7`, generate a binstub. Add to your `Gemfile`:
> ```ruby
> # No additional gem needed — Bundler handles binstubs
> ```
> Then create a launcher:
> ```powershell
> bundle binstubs --path bin --all
> ```
> This writes `bin\calc.bat` (a Windows shim) alongside your script. Add `$PWD\bin` to your project's `direnv` or PowerShell session and `calc 6 + 7` works directly.
### Debug in VS Code
Ruby LSP includes a debugger (`debug` gem, which ships with modern Ruby). Create `.vscode\launch.json`:
```json
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug calc",
"type": "ruby_lsp",
"request": "launch",
"program": "${workspaceFolder}/bin/calc",
"args": ["6", "+", "7"],
"cwd": "${workspaceFolder}"
},
{
"name": "Debug RSpec",
"type": "ruby_lsp",
"request": "launch",
"program": "${workspaceFolder}/vendor/bundle/ruby/3.4.0/bin/rspec",
"cwd": "${workspaceFolder}"
}
]
}
```
Note that VS Code's `launch.json` uses **forward slashes** even on Windows — that's the VS Code convention and works for all built-in path variables. Set a breakpoint in `lib\calculator.rb`, hit **F5**, and you'll step through the code with full variable inspection.
### Publish to GitHub
```powershell
git init
git add .
git commit -m "Initial commit: four-function calculator"
gh repo create calc-ruby --private --source=. --remote=origin --push
```
### `CLAUDE.md` for this project
```markdown
# Project: calc-ruby
## Purpose
Four-function command-line calculator in Ruby — pedagogical example.
## Conventions
- Ruby 3.4 via RubyInstaller (.ruby-version pins it; rv switches between installs)
- Bundler with project-local gems (vendor/bundle/)
- Tests in spec/ with RSpec, documentation-format output
- Lint/format with RuboCop
## Commands (PowerShell on Windows)
- Install deps: `bundle install`
- Add Windows platform if cloned from Mac/Linux: `bundle lock --add-platform x64-mingw-ucrt`
- Run: `ruby bin\calc <a> <op> <b>`
- Test: `bundle exec rspec`
- Lint: `bundle exec rubocop`
- Auto-fix: `bundle exec rubocop -a`
## Style
- 2-space indent, 100-column limit
- `# frozen_string_literal: true` at top of every .rb file
- snake_case methods and variables, CamelCase classes/modules
- Endless methods (def f(x) = ...) for simple one-liners
```
---
## Full demo: A calculator GUI
Same calculator, wrapped in a graphical interface. This uses **Glimmer DSL for LibUI**, a pure-Ruby declarative GUI library that wraps `libui-ng` to render native controls on each platform. On Windows that means real Win32 controls — the same widget set Notepad and File Explorer use.
### Why Glimmer DSL for LibUI?
| Option | Pros | Cons |
|--------|------|------|
| **Glimmer DSL for LibUI** | Pure Ruby gem, **native Win32 controls** on Windows, declarative DSL, ships its own libui-ng binary | Still maturing; smaller community than Qt |
| **Shoes4** | Beginner-friendly, designed for teaching | Requires JRuby |
| **wxRuby3** | Mature, comprehensive wxWidgets bindings | Larger install, native-extension compile |
| **Tk (Ruby/Tk)** | Was bundled with Ruby for decades | The Windows Tk bindings have been finicky on UCRT-era Ruby |
| **FXRuby** | Mature toolkit | Last release in 2019, effectively unmaintained |
For a pedagogical calculator in Ruby, Glimmer DSL for LibUI is the right default on Windows. The gem bundles prebuilt `libui-ng` binaries for both Windows architectures.
> [!warning] ARM64 caveat — check the libui-ng binary for your version
> `libui-ng`'s **ARM64 Windows binary support is improving through 2026** but lags behind x64. On x64 the gem is mature and works without ceremony. On ARM64, check the gem's CHANGELOG when you install — recent releases (>= 0.13) ship ARM64 Windows binaries; older releases don't. If you get a "library load" error on ARM64, either upgrade Glimmer or fall back to `wxRuby3` (whose Windows ARM64 binary support is also recent but well-tested).
### Add Glimmer to the project
Add to your `Gemfile`:
```ruby
gem "glimmer-dsl-libui", "~> 0.13"
```
Then:
```powershell
bundle install
```
Bundler downloads the gem, which includes the prebuilt `libui-ng.dll` for your architecture. No `winget install libui` or MSYS2 dependency hunt required — the gem is self-contained.
### `bin\calc-gui` — the GUI entry point
```ruby
#!/usr/bin/env ruby
# frozen_string_literal: true
require "glimmer-dsl-libui"
require_relative "../lib/calculator"
class CalculatorGui
include Glimmer
BUTTON_GRID = [
%w[7 8 9 /],
%w[4 5 6 *],
%w[1 2 3 -],
%w[0 . = +]
].freeze
def initialize
@current = ""
@stored = nil
@pending_op = nil
@display_text = "0"
end
def launch
root_window.show
end
private
def root_window
window("Calculator", 260, 360) {
margined true
vertical_box {
# Display
@display = entry {
text @display_text
read_only true
}
# Button grid
BUTTON_GRID.each do |row|
horizontal_box {
row.each do |label|
button(label) {
on_clicked { handle_press(label) }
}
end
}
end
# Clear button spans the bottom
button("Clear") {
on_clicked { reset_and_update_display }
}
}
}
end
def handle_press(label)
case label
when /\A[0-9.]\z/
@current += label
update_display(@current)
when "+", "-", "*", "/"
apply_pending
@pending_op = label
when "="
apply_pending
@pending_op = nil
end
end
def apply_pending
return if @current.empty?
value = @current.to_f
if @stored.nil? || @pending_op.nil?
@stored = value
else
begin
@stored = Calculator.calculate(@stored.to_s, @pending_op, value.to_s)
rescue ZeroDivisionError, ArgumentError
update_display("Error")
reset_state
return
end
end
formatted = @stored == @stored.to_i ? @stored.to_i.to_s : @stored.to_s
update_display(formatted)
@current = ""
end
def update_display(text)
@display_text = text
@display.text = text if @display
end
def reset_state
@current = ""
@stored = nil
@pending_op = nil
end
def reset_and_update_display
reset_state
update_display("0")
end
end
CalculatorGui.new.launch if $PROGRAM_NAME == __FILE__
```
Launch it:
```powershell
ruby bin\calc-gui
```
A native Win32 window opens with a number pad, operators, and the same Calculator logic backing it. Resizing, minimize/maximize, and Alt-F4 all behave the way other Windows apps do — because under the hood these are real Win32 controls.
### Tests for the GUI state machine
The state machine (`handle_press`, `apply_pending`, `reset_state`) is pure logic and can be unit-tested without invoking the GUI. Create `spec\calculator_gui_spec.rb`:
```ruby
# frozen_string_literal: true
require_relative "../bin/calc-gui"
RSpec.describe CalculatorGui do
let(:gui) { described_class.new }
describe "state machine" do
it "starts with display '0'" do
expect(gui.instance_variable_get(:@display_text)).to eq("0")
end
it "accumulates digits" do
%w[1 2 3].each { |d| gui.send(:handle_press, d) }
expect(gui.instance_variable_get(:@current)).to eq("123")
end
it "performs simple addition" do
%w[6 + 7 =].each { |k| gui.send(:handle_press, k) }
expect(gui.instance_variable_get(:@display_text)).to eq("13")
end
it "chains operations left-to-right" do
# 2 + 3 * 4 = (2+3)*4 = 20
%w[2 + 3 * 4 =].each { |k| gui.send(:handle_press, k) }
expect(gui.instance_variable_get(:@display_text)).to eq("20")
end
it "performs division" do
%w[2 0 / 4 =].each { |k| gui.send(:handle_press, k) }
expect(gui.instance_variable_get(:@display_text)).to eq("5")
end
it "shows 'Error' on division by zero" do
%w[5 / 0 =].each { |k| gui.send(:handle_press, k) }
expect(gui.instance_variable_get(:@display_text)).to eq("Error")
end
it "clears state" do
%w[5 + 3].each { |k| gui.send(:handle_press, k) }
gui.send(:reset_and_update_display)
expect(gui.instance_variable_get(:@display_text)).to eq("0")
expect(gui.instance_variable_get(:@current)).to eq("")
expect(gui.instance_variable_get(:@stored)).to be_nil
expect(gui.instance_variable_get(:@pending_op)).to be_nil
end
it "handles decimal input" do
%w[1 . 5 + 2 . 5 =].each { |k| gui.send(:handle_press, k) }
expect(gui.instance_variable_get(:@display_text)).to eq("4")
end
end
end
```
### Run everything
```powershell
# Run the CLI
ruby bin\calc 6 + 7
# Run the GUI
ruby bin\calc-gui
# Run all tests (CLI + GUI)
bundle exec rspec
```
> [!info] Headless testing
> Since the GUI tests only exercise the state machine (not `root_window.show`), they run without opening a window and take no longer than the CLI tests. CI runners (GitHub Actions `windows-latest`) run them as-is — no virtual display required, because we never actually call `.show`.
---
## Starship prompt — Ruby auto-detection
Starship's built-in `[ruby]` module shows the active Ruby version when a `.rb` file, `Gemfile`, or `.ruby-version` is in the directory. Your prompt will show something like:
```
~\projects\calc-ruby main 3.4.1 ❯
```
(The configuration block is already in your `~\.config\starship.toml` from the General guide.)
---
## direnv — auto-activate this project's environment
direnv can put the project's `bin/` on `PATH` when you `cd` in, so you can run `calc` instead of `ruby bin\calc` (see [[General_Development_Windows_Native_Setup]] for the PowerShell hook).
```bash
# from the project root
cat > .envrc <<'EOF'
PATH_add bin
EOF
direnv allow
```
> [!warning] `.envrc` is bash on Windows
> direnv runs `.envrc` through bash and applies the result to PowerShell, so write bash syntax (forward slashes are fine) and keep Git for Windows on `PATH`. Ruby versions are handled by `rv` via `.ruby-version`, not direnv. If you generated binstubs with `bundle binstubs --all --path bin`, `PATH_add bin` makes `calc.bat` runnable as `calc`.
---
## Troubleshooting
> [!warning] `bundle install` fails compiling a native extension with "no such file: stdio.h" or "gcc not found"
> The MSYS2 Devkit isn't installed or isn't on the build PATH. Run `ridk install 1 2 3` to install the toolchain. Verify with `ridk exec gcc --version` — it should print a version. If it does and gems still fail, run `ridk enable` in your current PowerShell session to push the MSYS2 paths into the environment, then re-run `bundle install`.
> [!warning] `bundle install` fails on Windows after a clone, with "Your bundle only supports platforms [x86_64-linux] but your local platform is x64-mingw-ucrt"
> You cloned a Linux/macOS-locked project. Run:
> ```powershell
> bundle lock --add-platform x64-mingw-ucrt # or arm64-mingw-ucrt
> bundle install
> ```
> Commit the updated `Gemfile.lock`.
> [!warning] `rv` runs `Remove-Variable` instead of the Ruby version manager
> That's the built-in PowerShell alias. Use `rvw` (the Windows-safe shim that the installer adds), or override the alias in your `$PROFILE` as shown earlier.
> [!warning] `ruby -v` shows a different version than `rvw pin` claims is active
> Open a fresh PowerShell window. `rvw pin --global` rewrites the user PATH entry but doesn't update already-running shells. If it still mismatches, check that the RubyInstaller you `pin`-ed is actually on disk: `Test-Path C:\Ruby34-x64\bin\ruby.exe`.
> [!warning] `which ruby` returns nothing — but `ruby -v` works
> `which` isn't a built-in PowerShell command. Use `Get-Command ruby` (or the `which` function from the General guide's `$PROFILE`).
> [!warning] Ruby LSP VS Code extension says "Cannot find ruby"
> Restart VS Code (the LSP caches the PATH at startup). If still broken, confirm `(Get-Command ruby).Source` in a fresh PowerShell shows a RubyInstaller path, then check that `rubyLsp.rubyVersionManager.identifier` in `settings.json` is `"none"` (not `"rbenv"` or `"auto"`).
> [!warning] `gem install nokogiri` (or pg, ffi, etc.) fails on ARM64 with autoconf errors
> MSYS2's autotools layer isn't fully ARM64-native yet. Check whether the gem has a precompiled `arm64-mingw-ucrt` binary on RubyGems — if so, `bundle lock --add-platform arm64-mingw-ucrt` and `bundle install` will grab it. If not, install the gem inside WSL2 and copy the resulting `.so`, or fall back to a pure-Ruby alternative.
> [!warning] Glimmer GUI window opens but is blank / unresponsive
> Likely a `libui-ng.dll` mismatch. Run `bundle update glimmer-dsl-libui` to pull the latest binary for your architecture. On ARM64, confirm you're on `glimmer-dsl-libui` 0.13+ (earlier versions don't ship an ARM64 Windows DLL).
> [!warning] `gem install ...` works but `bundle install` says "could not find compatible versions"
> Bundler's resolver is platform-aware; `gem install` isn't. Almost always the cause is a missing platform in `Gemfile.lock` — `bundle lock --add-platform x64-mingw-ucrt` fixes it.
> [!warning] PATH grows after every winget upgrade of Ruby
> When a new `RubyInstaller.RubyWithDevKit.3.X` ships, winget installs alongside the old one rather than replacing it. Use `winget uninstall -e --id RubyInstallerTeam.RubyWithDevKit.3.Y` to remove old versions you no longer need, then `rvw discover` to refresh `rv`'s list.
> [!warning] First `ridk install` hangs at "Updating pacman database"
> Network firewall or proxy is blocking MSYS2's mirror. Set HTTP proxy env vars in PowerShell (`$env:HTTP_PROXY = "http://proxy:port"`) and retry, or temporarily switch networks. On a corporate machine, ask IT for the MSYS2 mirror allowlist.
---
## Summary — the one-shot Ruby addition
> [!warning] This is a checklist, not a script
> Copy and paste one block at a time. `ridk install` is **interactive** (or takes `1 2 3` as arguments), and the `rvw` commands depend on opening a fresh PowerShell window after the install step so the new `PATH` is visible. After running these, **append the Ruby settings block** above to `%APPDATA%\Code\User\settings.json`.
```powershell
# 1. Install Ruby (RubyInstaller — bundles Devkit; native ARM64 in 3.4.1+)
winget install -e --id RubyInstallerTeam.RubyWithDevKit.3.4
# 2. Open a NEW PowerShell window so PATH is fresh, then install MSYS2 toolchain
ridk install 1 2 3
# 3. (Optional) Install rv as the multi-version switcher
winget install -e --id Spinel.rv
# Use rvw, not rv (rv collides with the PowerShell Remove-Variable alias)
rvw discover
rvw pin 3.4 --global
# 4. Configure Bundler for project-local gems
gem update --system
gem install bundler
bundle config set --global path 'vendor/bundle'
# 5. VS Code extensions
code --install-extension Shopify.ruby-lsp
code --install-extension rubocop.vscode-rubocop
code --install-extension aliariff.vscode-erb-beautify
code --install-extension connorshea.vscode-ruby-test-adapter
# 6. Verify
ruby -v
gem --version
bundle --version
ridk version
```
About five minutes on top of the general setup (ten if MSYS2's download is slow).
When you start a new project that needs to run on both Windows and Linux/Mac teammates' machines, remember the lockfile dance:
```powershell
bundle lock --add-platform x64-mingw-ucrt # or arm64-mingw-ucrt
```
---
## Related notes
**Same setup on other platforms:**
- [[Ruby_Development_Ubuntu_Setup]] — the Linux counterpart
- [[Ruby_Development_Mac_Tahoe_Setup]] — the macOS counterpart
**Prerequisite and parallel Windows guides:**
- [[General_Development_Windows_Native_Setup]]
- [[Python_Development_Windows_Native_Setup]]
- [[C_Development_Windows_Native_Setup]]
- [[Go_Development_Windows_Native_Setup]]
- [[Rust_Development_Windows_Native_Setup]]
**The other Windows path:**
- [[WSL2 Windows Development Setup]] — if you want the Linux Ruby experience without leaving Windows
**Topic references:**
- [RubyInstaller for Windows](https://rubyinstaller.org/)
- [rv on GitHub](https://github.com/spinel-coop/rv)
- [Bundler Cheat Sheet](https://bundler.io/)
- [RSpec Patterns](https://www.betterspecs.org/)
- [Glimmer DSL for LibUI](https://github.com/AndyObtiva/glimmer-dsl-libui)
- [MSYS2 project](https://www.msys2.org/)