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.
- 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.
gem install qtsudo 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.
- Ruby 3.2+
- Qt 6.4.2+ dev packages (
Qt6Core,Qt6Gui,Qt6Widgetsviapkg-config) - C++17 compiler
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 Qt6Widgetsbundle 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.
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_inspectrequire '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.exec# Qt style
label.setText('A')
window.setWindowTitle('Main')
# Ruby style
label.text = 'B'
window.window_title = 'Main 2'
puts label.textGenerated 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_eventoff(event = nil)/ aliasoff_eventconnect(signal, &block)/ aliaseson_signal,slotdisconnect(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.
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 }
}See all demos in examples/development_ordered_demos.
QObject signal example:
timer = QTimer.new
timer.set_interval(1000)
timer.connect('timeout') { puts 'tick' }
timer.startqtimetrap- timetrap desktop UI built with this bridge.
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.
lib/qtpublic Ruby APIscripts/generate_bridge.rbAST-driven bridge generatorext/qt_ruby_bridgenative extension entrypointbuild/generatedgenerated sourcesbuild/qtcompiled bridge.soexamplesdemostesttests
- 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')support06_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
- 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
- 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
bundle exec rake compile
bundle exec rake test
bundle exec rake rubocopTests 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 --verboseDefault 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