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')

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

  • Bindings do not store state. Calling :get() always recomputes the value if needed.
  • Each subscription is independent. A binding can be observed by multiple consumers safely.
  • In operator expressions, non-bindings are automatically converted into constant bindings internally.

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