| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Name | Name | Last commit date | ||
|---|---|---|---|---|
Build desktop apps with a Racket backend and a web frontend. A Tauri-like framework for Racket — write your app logic in Racket, build your UI with HTML/CSS/JS, and ship a desktop application.
English · 中文
Racket's racket/gui works but is hard to style into a modern product-grade UI. Glaze takes a different approach: serve a local web app from Racket and display it in the system browser (Phase 1) or an embedded WebView (Phase 3).
You get:
| Glaze | Tauri | Electron | wails | |
|---|---|---|---|---|
| Backend language | Racket | Rust | JS/Node | Go |
| Native toolchain needed | none (pure FFI) | Rust + cargo | none | Go + WebView2 deps |
| Binary size | tiny (Racket exe + assets) | small | 100 MB+ | small |
| Frontend→backend | HTTP JSON routes (fetch) | invoke() IPC | Node APIs | bindings |
| Works without webview (browser fallback) | yes | no | no | no |
| Agent-friendly UI verification (title/url/screenshot) | built-in | via WebDriver | via CDP | limited |
| WebView backends | WebView2 / WKWebView / WebKitGTK | same | bundled Chromium | WebView2/WKWebView |
All three webview backends pass the real-window CI e2e (open, load, capture, navigate, close, on-close). Remaining honest gaps: no typed IPC layer (plain JSON), Linux needs a desktop session or Xvfb.
| Capability | macOS | Windows | Linux |
|---|---|---|---|
| HTTP server + browser | ✅ | ✅ | ✅ |
| System tray | ✅ | ✅ | ✅ (CI-verified) |
| JSON API bridge | ✅ | ✅ | ✅ |
| Native webview window | ✅ verified end-to-end | ✅ CI e2e (WebView2) | ✅ CI e2e (Xvfb + WebKitGTK) |
| webview-title / webview-url | ✅ | ✅ | ✅ |
| webview-capture! (screenshot) | ✅ | ✅ (PrintWindow + PowerShell PNG) | ✅ (gdk_pixbuf) |
| #:devtools? | ✅ (inspectable, macOS 13+) | ✅ (OpenDevToolsWindow) | 🔲 |
Without a native backend, run-app / open-window automatically fall back to the system browser — the app still works everywhere.
| Dependency | Purpose |
|---|---|
| Racket | 7.0 or later (includes raco) |
git clone https://github.com/turinglambdaai/glaze.git
cd glazeraco pkg install glazeraco glaze init myapp
cd myappracket main.rktYour browser opens to http://127.0.0.1:8080 with a working page.
raco glaze init <name> # Create a new Glaze project
raco glaze dev # Start dev server with auto-open browser
raco glaze build # Build a distributable (exe + bundled assets)
raco glaze help # Show helpPackage a Glaze project into a platform distribution (raco exe + raco distribute) with the frontend assets bundled alongside the executable.
raco glaze build --name myapp # produces dist/myapp(.exe) + dist/lib + dist/public
raco glaze build --name myapp --installer # also produce msi / dmg / AppImage (or zip/tar.gz fallback)Options: --name, --icon <.ico/.icns>, --entry <path> (default main.rkt), --out <dir> (default dist), --embed-dlls (Windows: single-file exe), --installer.
The installer step probes for the native toolchain (WiX / NSIS on Windows, create-dmg / hdiutil on macOS, appimagetool / linuxdeploy on Linux) and degrades gracefully to a .zip / .tar.gz when it's absent, printing a warning naming what to install.
A new Glaze project looks like this:
myapp/
├── main.rkt # Racket entry point
└── public/
└── index.html # Frontend
main.rkt starts a local HTTP server serving files from public/ and opens the browser:
#lang racket/base
(require glaze)
(define-values (port server)
(start-dev-server #:public-dir "public"))
(printf "Glaze app running at http://127.0.0.1:~a\n" port)
(open-browser (format "http://127.0.0.1:~a" port))
(with-handlers ([exn:break?
(lambda (e)
(stop-server server)
(printf "Server stopped.\n"))])
(sync never-evt))glaze/ ├── glaze/ # Umbrella package (install `glaze` to get everything) ├── glaze-lib/ # Core library (server, API, browser launcher, assets) ├── glaze-cli/ # CLI tool (raco glaze init / dev) ├── glaze-doc/ # Documentation (Scribble) └── glaze-test/ # Tests
The one-call entry: picks a free port, starts the server (static + JSON API), opens the native webview window, and blocks until the window closes.
(run-app #:public-dir "public"
#:api (list (GET "api/ping" ...)))
;; webview path: window closed -> server stopped -> (values 'webview shutdown)
;; browser fallback (no native backend): opens browser -> (values 'browser shutdown)Starts a local HTTP server serving static files with SPA fallback, plus optional JSON API routes. start-dev-server is a backward-compatible alias.
(start-server #:port 8080
#:public-dir "public"
#:api (list (GET "api/ping" (lambda (req) (hasheq 'pong #t)))))
;; Returns (values port shutdown-proc); verifies the listener is accepting
;; before returning.Stops the server.
(stop-server shutdown-proc)Opens a URL in the system default browser (cross-platform: Windows, macOS, Linux).
(open-browser "http://127.0.0.1:8080")The frontend calls Racket with plain fetch("/api/...") — Glaze's answer to Tauri's invoke(). One code path works in the embedded WebView, in the system-browser fallback, and in dev (curl-able). Routes are ordinary values:
(require glaze)
(GET "api/ping" (lambda (req) (hasheq 'pong #t)))
(POST "api/items/:id/bump" (lambda (req id) (hasheq 'id id 'bumped #t)))
(POST "api/echo" (lambda (req)
(define body (request-json-body req))
(hasheq 'echo body)))In the page:
const s = await fetch('/api/counter/bump',
{method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({delta: 5})}).then(r => r.json());(define-api-routes api
[(POST "api/counter/bump")
(bump [delta exact-nonnegative-integer? 1]) ; required, checked, or default
(hasheq 'count (add1 delta))])One clause defines a Racket procedure (bump), a route (bad input → a 400 naming the parameter; handler errors → 500), and a JS client entry — the served /glaze/api.js exposes glaze.api.counterBump({delta: 5}), plus glaze.call(method, path, body) and glaze.on(name, fn).
(define bus (make-event-bus))
(start-server ... #:events bus)
(bus-broadcast! bus 'count-changed (hasheq 'count 42)) ; from any threadglaze.on('count-changed', s => render(s.count));The page can also use new EventSource('/glaze/events') directly. Works in the browser fallback too — same origin, no extra port.
See examples/counter/ for the complete working app.
(require glaze/sys)
(clipboard-set! "hello") ; (clipboard-get)
(notify! "Download finished" "report.pdf is ready")
(open-path "/Users/me/report.pdf") ; default handler
(reveal-path "/Users/me/report.pdf"); Finder/Explorer, selected
(unless (single-instance? "com.me.app") (exit 0))Window controls (from glaze/webview): webview-set-title!, webview-set-size!, webview-set-fullscreen!.
Glaze provides a cross-platform system tray so your app can live in the notification area / menu bar with a right-click (or left-click on macOS) menu. The backend is chosen by platform — pure Racket FFI, no native compilation required:
If a platform's native libraries aren't available at runtime, the tray silently degrades to a no-op so the rest of the app keeps working.
(require glaze)
(define t
(make-tray #:icon #f
#:tooltip "My Glaze App"
#:menu (list (make-menu-item "Quit"
#:action (lambda () (exit 0))))))
(tray-set-tooltip! t "running")
;; ...later
(tray-close t)macOS note: a pure menu-bar app (no Dock icon) requires building as an .app bundle with LSUIElement set — raco glaze build configures this for you.
| Example | What it shows |
|---|---|
| examples/showcase/ | Kitchen sink (start here) — every capability in one window |
| examples/hello/ | Minimal app — run-app in 8 lines |
| examples/counter/ | JS↔Racket bridge — fetch calls Racket state |
| examples/webview-demo.rkt | Webview lifecycle: load, navigate, close, verification APIs |
| examples/agent-verify.rkt | Agent workflow: assert page state + screenshot with no human |
| examples/tray-demo.rkt | Cross-platform system tray with a working menu |
Phase 3 done: all three backends (macOS WKWebView, Windows WebView2, Linux WebKitGTK) pass the real-window CI e2e — open, page load, webview-title/url verification, webview-capture! screenshots, webview-navigate, close (programmatic and OS chrome), and #:on-close callbacks; #:devtools? on macOS/Windows. Pure Racket FFI throughout, no compiler. Remaining polish (not blockers): Linux #:devtools?, Windows resize-follow (put_Bounds is set once, not on WM_SIZE), multi-window ergonomics.
Licensed under the MIT License.
| Back | FazBrowse Home | New Git URL |