High-level TypeScript bindings for GTK4 and libadwaita using Deno/Bun's FFI (Foreign Function Interface).
Available on JSR: @sigmasd/gtk
This project provides idiomatic TypeScript wrappers around GTK4, GLib, GIO, GObject, and libadwaita, allowing you to build native desktop applications using Deno/Bun. The library abstracts away low-level pointer manipulation and provides a clean, object-oriented API similar to native GTK bindings in other languages.
Cross-platform support: Works on Linux, macOS, and Windows (via MSYS2/GTK for Windows).
In Deno you can import directly
For bun you need to install it first using bunx jsr add @sigmasd/gtk
Import directly from JSR in your Deno/Bun project:
import { Application, ApplicationWindow, Button, Label } from "@sigmasd/gtk"; // or directly in deno with jsr:@sigmasd/gtkimport {
Application,
ApplicationWindow,
Box,
Button,
GTK_ORIENTATION_VERTICAL,
Label,
} from "@sigmasd/gtk";
const app = new Application("com.example.HelloWorld", 0);
app.connect("activate", () => {
const win = new ApplicationWindow(app);
win.setTitle("Hello World");
win.setDefaultSize(400, 300);
const box = new Box(GTK_ORIENTATION_VERTICAL, 12);
box.setMarginTop(24);
box.setMarginBottom(24);
box.setMarginStart(24);
box.setMarginEnd(24);
const label = new Label("Hello, GTK! 👋");
box.append(label);
const button = new Button("Click Me!");
button.connect("clicked", () => {
label.setText("Button clicked! 🎉");
});
box.append(button);
win.setChild(box);
win.setProperty("visible", true);
});
app.run([]);# Using JSR
deno run --allow-ffi your-app.ts # or bun your-app.ts
# Or from the repository
deno run --allow-ffi examples/simple.ts # or bun examples/simple.tsThe repository's examples/ directory contains sample applications:
simple.ts: Minimal hello world example with buttonwidgets-demo.ts: Comprehensive demo showing various widgets:- Buttons and event handling
- Text entry fields
- Dropdown menus
- List boxes
- Scrolled windows
- Frames and containers
async-demo.ts: Demonstrates async/await with EventLoop:- Fetching data from APIs
- Using setTimeout and Promises
- Running multiple async operations in parallel
Run examples: (can be run with Bun as well)
# Simple example
deno run --allow-ffi examples/simple.ts
# Widgets demo
deno run --allow-ffi examples/widgets-demo.ts
# Async/await demo (requires network permission)
deno run --allow-ffi --allow-net examples/async-demo.tsBox- Horizontal/vertical containerFrame- Container with border and optional labelScrolledWindow- Scrollable containerListBox- Vertical list containerToolbarView- Adwaita toolbar view
Label- Text displayButton- Clickable buttonEntry- Text input fieldDropDown- Dropdown selection menuMenuButton- Button that opens a menu
Window- Basic windowApplicationWindow- Main application windowPreferencesWindow- Adwaita preferences dialogMessageDialog- Adwaita message/confirmation dialogAboutDialog- About dialog
HeaderBar- Modern GNOME header barActionRow- List row with title/subtitleComboRow- Combo box row for preferencesPreferencesPage- Page for preferences windowPreferencesGroup- Group within preferences page
Builder- Load UI from XML filesMenu- Application menuSimpleAction- Application actionStyleManager- Theme and appearance management
// Import main widgets
import {
Application,
ApplicationWindow,
Box,
Button,
Entry,
Label,
} from "@sigmasd/gtk";
// Import constants
import {
GTK_ORIENTATION_HORIZONTAL,
GTK_ORIENTATION_VERTICAL,
} from "@sigmasd/gtk";
// Import Adwaita widgets
import { HeaderBar, PreferencesWindow, StyleManager } from "@sigmasd/gtk";
// Import event loop utilities (optional)
import { EventLoop } from "@sigmasd/gtk/eventloop";By default, GTK's app.run() blocks JavaScript's event loop, preventing
async/await from working. The EventLoop class provides a solution by
integrating GLib's MainContext with Deno/Bun's event loop.
import { Application, ApplicationWindow, Button } from "@sigmasd/gtk";
import { EventLoop } from "@sigmasd/gtk/eventloop";
const app = new Application("com.example.App", 0);
const eventLoop = new EventLoop();
app.connect("activate", () => {
const win = new ApplicationWindow(app);
win.setTitle("Async Example");
win.setDefaultSize(400, 300);
const button = new Button("Fetch Data");
button.connect("clicked", async () => {
// Now you can use async/await!
const response = await fetch("https://api.github.com/repos/denoland/deno");
const data = await response.json();
console.log("Fetched repo:", data.name, "Stars:", data.stargazers_count);
});
win.setChild(button);
win.setProperty("visible", true);
});
// Use eventLoop.start() instead of app.run()
await eventLoop.start(app);// Configure poll interval (default: 16ms)
const eventLoop = new EventLoop({
pollInterval: 16, // Check for events every 16ms when idle
});The EventLoop uses a hybrid approach:
- When active: Sub-millisecond latency using microtasks
- When idle: Sleeps to conserve CPU resources
Use EventLoop when you need:
- ✅
async/awaitand Promises in your GTK app - ✅
fetch()or other async Deno/Bun APIs - ✅
setTimeout(),setInterval()to work properly - ✅ Integration with async libraries
Use standard app.run() when:
- ✅ You only need synchronous GTK event handling
- ✅ Simple applications without async operations
When using EventLoop, you must handle the window close-request signal to
stop the event loop, otherwise the application will continue running in the
terminal after the window closes:
win.connect("close-request", () => {
eventLoop.stop(); // Stop the event loop
return false; // Allow window to close
});With standard app.run(), the window close is handled automatically by GTK.
// Create widgets
const label = new Label("Hello");
const button = new Button("Click");
const entry = new Entry();// Type-safe property setting
widget.setProperty("visible", true);
widget.setProperty("margin-top", 12);
widget.setProperty("halign", 3); // GTK_ALIGN_CENTERbutton.connect("clicked", () => {
console.log("Button clicked!");
});
window.connect("close-request", () => {
console.log("Window closing");
return false; // Allow close
});const box = new Box(GTK_ORIENTATION_VERTICAL, 12);
box.append(label);
box.append(button);
box.remove(button);const app = new Application("com.example.App", 0);
app.connect("activate", () => {
// Create and show your main window
});
const exitCode = app.run([]);
Deno.exit(exitCode);The core module handles:
- Dynamic library loading (
dlopen) - FFI symbol definitions
- Raw GTK/GLib C function bindings
- Memory management (GObject reference counting)
Object-oriented classes that:
- Wrap raw pointers in TypeScript classes
- Provide idiomatic methods
- Handle C string conversions
- Manage GValue conversions for properties
- Register and manage signal callbacks
gtk/
├── src/
| |-- bun-deno-compat.ts # Deno compatibility layer for Bun
│ ├── gtk-ffi.ts # Main FFI bindings and wrappers
│ └── eventloop.ts # Event loop for async/await support
├── examples/
│ ├── simple.ts # Simple hello world
│ └── widgets-demo.ts # Comprehensive widget demo
├── deno.json # Package configuration
└── README.md
The eventloop.ts module provides:
EventLoop class: Integrates GLib's MainContext with Deno/Bun's event loop
start(app)- Start the event loop with your applicationstop()- Stop the event loop and quit the applicationisRunning- Check if the event loop is runningpollInterval- Get the current poll interval
Options:
pollInterval- Milliseconds to sleep when idle (default: 16)
- Add FFI symbol definitions to the appropriate
dlopencall - Create a high-level wrapper class extending
WidgetorGObject - Implement constructor and common methods
- Export the class
Example:
// 1. Add FFI binding
const gtk = Deno.dlopen(LINUX_LIB_PATHS.gtk, {
// ... existing symbols ...
gtk_my_widget_new: { parameters: [], result: "pointer" },
gtk_my_widget_set_text: { parameters: ["pointer", "buffer"], result: "void" },
});
// 2. Create wrapper class
export class MyWidget extends Widget {
constructor() {
const ptr = gtk.symbols.gtk_my_widget_new();
super(ptr);
}
setText(text: string): void {
const textCStr = cstr(text);
gtk.symbols.gtk_my_widget_set_text(this.ptr, textCStr);
}
}- Limited widgets: Not all GTK widgets are wrapped yet
- No CSS provider: Custom styling via CSS not yet supported
- Signal lifetime: Callbacks remain valid for process lifetime (no explicit disconnect/cleanup yet)
- Platform testing: While cross-platform library loading is implemented, primary testing has been on Linux
MIT License - See LICENSE file for details