Skip to content

Binding

Kevin edited this page Jun 19, 2026 · 3 revisions

A Binding represents a reactive read-only view over a value source. It allows you to observe changes and compose reactive expressions without introducing mutable state.

Creating a Binding

Bindings can be created from different sources:

From a Variable

local bind = require('astal').bind
local Variable = require('astal.variable')

local count = Variable.new(0)
local b = bind(count)

The binding will always reflect the current value of the variable.

From a GObject property

local bind = require('astal').bind
local battery = Battery.get_default()

local percentage = bind(battery, 'percentage')

From Gio.Settings (GSettings)

Bindings can also be created directly from Gio.Settings keys.

local Gio = require('lgi').require('Gio')
local bind = require('astal').bind

local settings = Gio.Settings {
    schema_id = 'org.gnome.desktop.interface',
}

local theme = bind(settings, 'color-scheme')

The binding automatically subscribes to property notifications.

Note

Values returned from Gio.Settings are automatically decoded from GVariant into native Lua types.

From nested properties

You can access deep properties using dot notation:

local icon_name = bind(network, 'wifi.icon-name')

If any object in the chain changes, the binding will automatically re-evaluate.

Getting values

Bindings are lazy. They compute their value only when requested.

print(b:get())

If the binding is derived, the computation is executed on demand.

Subscribing to changes

You can subscribe to updates:

local unsubscribe = b:subscribe(function(value)
    print(value)
end)

The callback receives the current computed value.

To stop listening:

unsubscribe()

Transforming values

You can transform a binding using :as():

local text = bind(battery, 'percentage'):as(function(p)
    return string.format('%d%%', p * 100)
end)

Transformations are applied lazily and composed.

Derived Bindings

Bindings can be combined to form new reactive expressions.

Operator overloading

Bindings support arithmetic and string operators:

local sum = bind(a) + bind(b)

local label = bind(name) .. " (" .. bind(count) .. ")"

local value = bind(x) * 2 + bind(y)

All operators automatically create derived bindings.

Internally equivalent to:

Binding.derive({ bind(a), bind(b) }, function(x, y)
    return x + y
end)

Binding constant values

Non-binding values are automatically converted into constant bindings when needed.

local value = bind(counter) + 10

Here 10 is implicitly treated as a constant binding.

This allows seamless mixing of reactive and static values.

Binding composition rules

A Binding is always:

  • lazy (computed only on demand)
  • pure (no internal mutation)
  • composable (can depend on other bindings)
  • stateless (derived bindings do not store values)

Special behavior notes

1. No storage

Bindings do not store state. Calling :get() always recomputes the value if needed.

2. Multiple subscriptions

Each subscription is independent. A binding can be observed by multiple consumers safely.

3. Automatic coercion

In operator expressions, non-bindings are automatically converted into constant bindings internally.

Derived binding implementation model

Conceptually, a derived binding behaves like:

function Binding.derive(deps, transform)
    return {
        get = function()
            local values = {}
            for i, d in ipairs(deps) do
                values[i] = d:get()
            end
            return transform(table.unpack(values))
        end,

        subscribe = function(self, callback)
            for _, d in ipairs(deps) do
                d:subscribe(function()
                    callback(self:get())
                end)
            end
        end
    }
end

Clone this wiki locally