Skip to content

First Widgets

Kevin edited this page Jun 16, 2026 · 2 revisions

Getting Started

Start by importing the singleton AstalLua.Application instance.

init.lua
local App = require('astal.gtk3.app')
-- Or using Gtk4 if you prefer
local App = require('astal.gtk4.app')

App:start {
    main = function()
        -- create widgets here
    end
}

Then run astal-lua in the terminal, and that's it! Now you have an Astal instance running written in Lua.

Root of every shell component: Window

Astal apps are composed of widgets. A widget is a piece of UI that has its own logic and style. A widget can be as small as a button or an entire bar. The top level widget is always a Window which will hold all widgets.

widget/Bar.lua
local Widget = require('astal.gtk4.widget')

---@param args { gdkmonitor: Gdk.Monitor }
return function(args)
    return Widget.Window {
        gdkmonitor = args.gdkmonitor,
        anchor = { 'TOP', 'LEFT', 'RIGHT' },
        exclusivity = 'EXCLUSIVE',
        Widget.Label {
            label = 'Example label content',
        },
    }
end
init.lua
local App = require('astal.gtk4.app')
local Bar = require('widget.Bar')

App:start {
    main = function()
        for _, gdkmonitor in ipairs(App.monitors) do
            Bar {
                gdkmonitor = gdkmonitor,
            }
        end
    end,
}

Creating and nesting widgets

Widgets are simply Lua functions that return Gtk widgets, you can nest widgets by passing them as arguments to the table in the function.

widget/MyButton.lua
local Widget = require('astal.gtk4.widget')

return function(text)
    return Widget.Button {
        on_button_pressed = function(_, button)
            if button == Gdk.BUTTON_PRIMARY then
                print('Left click')
            elseif button == Gdk.BUTTON_SECONDARY then
                print('Right click')
            end
        end,
        Widget.Label {
            label = text,
        },
    }
end

Now, you should be able to nest it into another widgets.

widget/Bar.lua
 local MyButton = require('widget.MyButton')

 return function(monitor)
     return Widget.Window {
         monitor = monitor,
         anchor = { 'TOP ', 'LEFT ', 'RIGHT' },
         exclusivity = 'EXCLUSIVE',
         Widget.Box {
             Widget.Label {
                 label = 'Click the button',
             },
             MyButton('hi, im a button'),
         },
     }
 end

Widget signal handlers

You can handle events by defining signal handlers directly in your widgets.
Any key prefixed with on_* will connect to the corresponding signal, while keys prefixed with on_notify_* will be connected to property changes:

local function MyButton()
    return Widget.Button {
        on_button_press = function(_, ...) print(...) end,
        on_notify_label = function(_, value) print("label changed:", value) end,
    }
end

Note

Keys prefixed with on_ will connect to a signal of the widget. Refer to the Gtk and Astal docs to have a full list of them.

State management

The state of widgets are handled with Bindings. A Binding lets you connect the state of an object to a widget so it re-renders when that state changes.

Use the bind function to create a Binding object from a Variable or a regular GObject and one of its properties.

Here is an example of a Counter widget that uses a Variable as its state

local astal = require('astal')
local bind, Variable = astal.bind, astal.Variable
local Widget = require('astal.gtk4.widget')

local function Counter()
    local count = Variable.new(0)

    return Widget.Box {
        Widget.Label {
            label = bind(count):as(tostring),
        },
        Widget.Button {
            label = 'Click to increment',
            on_clicked = function() count:set(count:get() + 1) end,
            -- or
            -- on_clicked = function() count.value = count.value + 1 end,
        },
    }
end

Note

Bindings have an :as() method which lets you transform the assigned value. In the case of a Label, its label property expects a string, so it needs to be converted into a string first.

Here is an example of a battery percent label that binds the percentage property of the Battery object from the Battery Library:

local astal = require('astal')
local bind = astal.bind
local Battery = astal.require('AstalBattery')
local Widget = require('astal.gtk4.widget')

local function BatteryPercentage()
    local bat = Battery.get_default()

    return Widget.Label {
        label = bind(bat, 'percentage'):as(function(p) return string.format('%d%%', p * 100) end),
    }
end

Dynamic children

You can also use a Binding for child and children properties.

local astal = require('astal')
local Variable = astal.Variable
local Widget = require('astal.gtk4.widget')

local child = Variable.new(Widget.Box())

return Widget.Box {
    bind(child),
}
local num = Variable.new(3)

return Widget.Box {
    bind(num):as(function(n)
        local tbl = {}
        for i = 1, n do
            table.insert(
                tbl,
                Widget.Button {
                    label = tostring(i),
                }
            )
        end
        return tbl
    end),
}

Tip

Binding children of widgets will implicitly call :destroy() on widgets that would be left without a parent. You can opt out of this behavior by setting no_implicity_destroy property on the container widget.

Note

You can pass the followings as children:

  • widgets
  • deeply nested arrays of widgets
  • bindings of widgets
  • bindings of deeply nested arrays of widgets

nil is the only value that is not rendered and anything not from this list will be coerced into a string and rendered as a label.

Clone this wiki locally