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 qt
Quick install (Fedora, binary RPM via COPR)
sudo dnf copr enable cyjimmy264/ruby-qt -y
sudo dnf install -y ruby-qt
This 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 clang
Minimum packages for Ubuntu/Debian:
sudo apt update
sudo apt install -y build-essential pkg-config qt6-base-dev ruby ruby-dev clang
Check Qt:
pkg-config --modversion Qt6Widgets
Build from repo
bundle install
bundle exec rake compile
bundle exec rake install
rake 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.rb
Optional: run interactive commands in IRB while the app is open:
add_label("Release pipeline")
("Run")
remove_last
gui { window.resize(1100, 700) }
items.last&.q_inspect
Hello 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.exec
API Style: Qt + Ruby
# Qt style
label.setText('A')
window.setWindowTitle('Main')
# Ruby style
label.text = 'B'
window.window_title = 'Main 2'
puts label.text
API 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_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.
Introspection
Every generated object exposes API snapshot helpers:
label.q_inspect
label.qt_inspect
label.to_h
Shape:
{
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.start
Projects
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 APIscripts/generate_bridge.rbAST-driven bridge generatorext/qt_ruby_bridgenative extension entrypointbuild/generatedgenerated sourcesbuild/qtcompiled bridge.soexamplesdemostesttests
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')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
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 rubocop
Test 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 --verbose
Generation 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 compile
If 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.rb
Optional tuning:
# enable ancestor fallback for MouseMove events (off by default)
QT_RUBY_EVENT_ANCESTOR_MOUSE_MOVE=1 ruby your_app.rb