Bezel:   Qt 6 bindings for Racket
1 Quick start
2 Threading model
3 API
4 Timers
5 Observables
6 Desktop integration
7 Error reporting
8 Updates
9 Packaging applications
9.3

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

    2 Threading model

    3 API

    4 Timers

    5 Observables

    6 Desktop integration

    7 Error reporting

    8 Updates

    9 Packaging applications

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:

  cmake -S bezel-shim -B bezel-shim/build -DCMAKE_BUILD_TYPE=Release
  cmake --build bezel-shim/build

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.