qt
Ruby-first Qt 6.4.2+ bridge.
Build real Qt Widgets apps in pure Ruby, mutate them live from IRB, and keep C/C++ surface minimal via generated bridge code from system Qt headers.
Highlights
- Pure Ruby usage: no QML, no extra UI language.
- Real Qt power:
QApplication,QWidget,QLabel,QPushButton,QVBoxLayout. - Ruby ergonomics: Qt-style and snake_case/property style in parallel.
- Live GUI hacking: update widgets while the window is open.
- Generated bridge: API is derived from system Qt headers.
Install
Quick install (RubyGems)
gem install qtQuick install (Fedora, binary RPM via COPR)
sudo dnf copr enable cyjimmy264/ruby-qt -y
sudo dnf install -y ruby-qtThis installs a prebuilt package. Nothing is compiled on the target machine.
Package name: ruby-qt.
Requirements (build from source)
- Ruby 3.2+
- Qt 6.4.2+ dev packages (
Qt6Core,Qt6Gui,Qt6Widgetsviapkg-config) - C++17 compiler
System Requirements
Minimum packages for Fedora:
dnf install @development-tools qt6-qtbase-devel ruby ruby-devel clangMinimum packages for Ubuntu/Debian:
sudo apt update
sudo apt install -y build-essential pkg-config qt6-base-dev ruby ruby-dev clangCheck Qt:
pkg-config --modversion Qt6WidgetsBuild from repo
bundle install
bundle exec rake compile
bundle exec rake installrake install installs into your current Ruby environment (including active rbenv version).
rake compile builds the full bridge with QT_RUBY_SCOPE=all by default.
Quick Start
bundle exec ruby examples/development_ordered_demos/02_live_layout_console.rbOptional: run interactive commands in IRB while the app is open:
add_label("Release pipeline")
add_button("Run")
remove_last
gui { window.resize(1100, 700) }
items.last&.q_inspectHello Qt in Ruby
require 'qt'
app = QApplication.new(0, [])
window = QWidget.new do |w|
w.set_window_title('Qt Ruby App')
w.resize(800, 600)
end
label = QLabel.new(window)
label.text = 'Hello from Ruby + Qt'
label.set_alignment(Qt::AlignCenter)
label.set_geometry(0, 0, 800, 600)
app.execAPI Style: Qt + Ruby
# Qt style
label.setText('A')
window.setWindowTitle('Main')
# Ruby style
label.text = 'B'
window.window_title = 'Main 2'
puts label.textAPI Compatibility Notes
Generated Ruby API is intentionally close to Qt API, but follows universal bridge policies.
-
snake_casealiases are generated for Qt camelCase methods. - Ruby keyword-safe renaming is applied when needed:
next->next_. - Default C++ arguments are surfaced as optional Ruby arguments.
- Internal runtime name collisions are renamed consistently:
- Qt
handle(int)is exposed ashandle_at(int)becausehandleis used for native object pointer access.
- Qt
- Property convenience API is generated from Qt setters/getters when available:
-
setText(...)->text=(...),text.
-
- Runtime event/signal convenience methods are Ruby-layer helpers (not raw Qt method names):
-
on(event, &block)/ aliason_event -
off(event = nil)/ aliasoff_event -
connect(signal, &block)/ aliaseson_signal,slot -
disconnect(signal = nil)/ aliasoff_signal - these helpers are mixed into generated
QObjectdescendants (for exampleQWidget,QPushButton,QTimer) - non-
QObjectvalue classes (QIcon,QPixmap,QImage) intentionally do not exposeconnect/on - event delivery is target-first with nearest watched ancestor fallback for interactive events (mouse/key/focus/enter/leave)
-
- Introspection helpers are Ruby-layer helpers:
-
q_inspect, aliasesqt_inspect,to_h
-
- Top-level constant aliases are provided for convenience:
-
QApplication,QWidget,QLabel,QPushButton,QLineEdit,QVBoxLayout,QTableWidget,QTableWidgetItem,QScrollArea
-
- Methods with unsupported signatures are skipped by policy:
- non-public, deprecated, operator/internal event hooks,
- non-FFI-safe argument/return types.
Introspection
Every generated object exposes API snapshot helpers:
label.q_inspect
label.qt_inspect
label.to_hShape:
{
qt_class: "QLabel",
ruby_class: "Qt::QLabel",
qt_methods: ["setText", "setAlignment", "text", ...],
ruby_methods: [:setText, :set_text, :text, ...],
properties: { text: "A", alignment: 129 }
}Examples
See all demos in examples/development_ordered_demos.
QObject signal example:
timer = QTimer.new
timer.set_interval(1000)
timer.connect('timeout') { puts 'tick' }
timer.startProjects
-
qtimetrap- timetrap desktop UI built with this bridge.
Architecture
-
scripts/generate_bridge.rbreads Qt API from system headers. - Generates:
build/generated/qt_ruby_bridge.cppbuild/generated/bridge_api.rbbuild/generated/widgets.rb
- Compiles native extension into
build/qt/qt_ruby_bridge.so. - Ruby layer calls bridge functions via
ffi.
Everything generated/build-related is under build/ and should stay out of git.
Layout
-
lib/qtpublic Ruby API -
scripts/generate_bridge.rbAST-driven bridge generator -
ext/qt_ruby_bridgenative extension entrypoint -
build/generatedgenerated sources -
build/qtcompiled bridge.so -
examplesdemos -
testtests
Roadmap
Done
- AST-driven generation with scope support:
QT_RUBY_SCOPE=widgets|qobject|all - default compile path switched to
all(widgets + qobject) - generated Qt inheritance in Ruby classes (including intermediate Qt wrappers)
- Qt-native event/signal runtime wired to Ruby at QObject level (
on,connect,disconnect) -
QTimeravailable in generated API withconnect('timeout')support -
06_timetrap_clockifymoved toapp.exec+QTimerupdate loop (no manual polling loop) - QObject styling hooks exposed for QSS selectors:
-
setObjectName/object_name= -
setProperty/property(via QVariant bridge codec)
-
- window icon support from generated API:
QIcon.new(path)-
QWidget#setWindowIcon/set_window_icon
Next
- typed signal payloads (not only raw/placeholder payload)
- richer QObject metaobject Ruby API (
meta_object, methods/signatures/properties introspection) - normalize signal naming rules for overloads and deterministic connect behavior
Later
- expand generated surface for additional Qt modules (network, sql, xml, etc.) using the same generator policy
- packaging hardening for Linux/macOS (install/build paths, gem install reliability)
- CI matrix for Ruby/Qt combinations and scope modes (
widgets,qobject,all) - add performance checks for generator traversal and compile size/time regression tracking
Development
bundle exec rake compile
bundle exec rake test
bundle exec rake rubocopTest Environment Variables
Tests force QT_QPA_PLATFORM=offscreen by default to avoid opening GUI windows.
-
QT_QPA_PLATFORM_FORCE_XCB=true- override test default and run with
QT_QPA_PLATFORM=xcb(real X11 backend)
- override test default and run with
-
QT_RUBY_MANUAL_MODIFIERS=1- enable manual keyboard-modifier smoke test (
Ctrl/Shiftmust be pressed during test window)
- enable manual keyboard-modifier smoke test (
Examples:
# default headless test run
bundle exec rake test
# run tests on xcb backend
QT_QPA_PLATFORM_FORCE_XCB=true bundle exec rake test
# run only manual modifiers smoke test
QT_QPA_PLATFORM_FORCE_XCB=true QT_RUBY_MANUAL_MODIFIERS=1 \
bundle exec ruby -Itest test/application_test.rb \
--name test_qapplication_keyboard_modifiers_manual_ctrl_shift_smoke --verboseGeneration Scope
Default build scope is all. You can still override scope manually with QT_RUBY_SCOPE:
-
widgets(default): QWidget/QLayout-oriented classes. -
qobject: QObject descendants excluding QWidget/QLayout branch. -
all: combined public surface fromwidgets+qobjectscopes (default build mode).
Examples:
QT_RUBY_SCOPE=widgets bundle exec rake compile
QT_RUBY_SCOPE=qobject bundle exec rake compile
QT_RUBY_SCOPE=all bundle exec rake compileIf Qt is in a custom prefix:
export PKG_CONFIG_PATH="/path/to/qt/lib/pkgconfig:$PKG_CONFIG_PATH"Native event-runtime debug logs:
QT_RUBY_EVENT_DEBUG=1 ruby your_app.rbOptional tuning:
# enable ancestor fallback for MouseMove events (off by default)
QT_RUBY_EVENT_ANCESTOR_MOUSE_MOVE=1 ruby your_app.rb