# 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/)