Bezel: Qt 6 bindings for Racket
Bezel binds Qt 6 (Widgets) to Racket: real native widgets, styled with Qt Style Sheets, on macOS, Windows and Linux.
1 Quick start
(require bezel) (define n (box 0)) (define count (make-label "Clicked 0 times")) (define btn (make-button "Click me")) (connect! btn "clicked()" (lambda _ (set-box! n (add1 (unbox n))) (widget-set-text! count (format "Clicked ~a times" (unbox n))))) (define win (make-window #:title "Hello Bezel" #:size '(320 140))) (layout! win (vbox count btn)) (run win)
Bezel ships as a Racket package plus a small C++ shim (libbezel) compiled against Qt 6. Install the shim from the releases page or build it:
2 Threading model
Any Racket thread may call any Bezel function; calls from non-GUI threads are marshaled onto the Qt GUI thread by a cooperative queue (see docs/architecture.md). Signal handlers run on Bezel’s dispatcher thread — plain Racket code works there, and widget calls marshal back automatically.
3 API
| (require bezel) | package: bezel-lib |
The full API surface is indexed in the repository README and repository docs; names follow make-widget / widget-set-text! conventions:
Lifecycle: make-application, run, quit!, process-events!, bezel-cleanup!
Widgets: make-window, make-label, make-button, make-line-edit, make-combo, make-slider, make-table-widget, ... — every constructor also accepts #:parent
Generator-backed classes: radio-new, groupbox-new, doublespin-new, lcd-new, tabs-new, stacked-new, splitter-new, richtext-new
Layouts: layout!, vbox, hbox, grid, form, stretch
Signals: connect!, disconnect!, emit-test-signal!
Timers: after!, every!, stop-timer!, stop-all-timers! (also run by bezel-cleanup!)
Dialogs: msg-question, get-open-file-name, get-save-file-name
Desktop integration: clipboard-set-text!, make-tray, window-status-bar, window-toolbar
Error reporting: install-sentry-reporter!
Data binding: make-observable, observe!, set-observable!
Silent self-update: auto-update!
Update checks: check-for-update, check-and-prompt-update! — best-effort version-feed comparison with a native prompt
Object model: bezel-alive?, bezel-delete!, qt-object-name
Verification: widget-grab-png — real PNG bytes of any widget
4 Timers
(after! 500 (lambda () (widget-set-text! status "done"))) (define clock (every! 100 (lambda () (widget-set-value! bar (tick))))) (stop-timer! clock)
Handlers run on their own Racket thread; widget calls inside them marshal to the GUI thread automatically. An exception in an after! handler cancels that timer; an every! handler exception is reported and the interval keeps firing. bezel-cleanup! sweeps pending timers via stop-all-timers!.
5 Observables
A minimal data-binding layer: watchers fire once immediately (synchronously — an async initial push can deliver a stale value after a newer one) and then on every change; equal values skip delivery.
(define count (make-observable 0)) (observe! count (lambda (v) (widget-set-text! label (~a v)))) (set-observable! count 41) (set-observable! count 41)
6 Desktop integration
(clipboard-set-text! "result") (define tray (make-tray "icon.png" "MyApp")) (tray-show! tray) (tray-notify! tray "Export finished" "results.csv written" #:icon 'information) (tray-set-menu! tray (menu! (menu-bar win) "Tray")) (status-show-message! (window-status-bar win) "Ready") (define act (toolbar-add-action! (window-toolbar win "Main") "Refresh")) (connect! act "triggered()" refresh!)
Tray notifications deliver through the same signal bridge as menu actions.
7 Error reporting
(install-sentry-reporter! "https://<key>@o<org>.ingest.sentry.io/<project>" #:release (format "myapp/~a" bezel-version))
Uncaught Racket exceptions are queued to a Sentry-compatible server on a background thread; dead DSNs and offline machines never disturb the application. report-error! and report-message! report handled events manually. Native crashes inside Qt/libbezel are out of scope.
8 Updates
The feed is one static JSON file with version and url fields, plus optional notes and sha1 — checks are best-effort and quiet:
(check-and-prompt-update! #:feed feed-url #:current "1.2.3" #:parent win)
For silent updates the feed’s url names the zipped application folder; auto-update! downloads it, verifies the digest, and hands off to an out-of-process swapper that waits for this process to exit, replaces the folder, and relaunches — it never returns on success:
(after! 2000 (lambda () (auto-update! #:feed feed-url)))
9 Packaging applications
raco bezel package --entry my-app.rkt --name MyApp --dest dist
produces a self-contained application folder: embedded executable plus bundled native runtime, a real .app bundle on macOS. Add –installer for the platform installer – Inno Setup, dmg with drag-to-install, or AppImage – and –app-version to stamp the VERSION file auto-update! reads back. Signing helpers for Windows and macOS and the full walkthrough live in in docs/APP_PACKAGING.md — the complete walkthrough.