Custom Layouts
You will learn
- The minimal structure of a layout table
- How to register and activate a custom layout
- How to position clients using
p.workareaandp.geometries - How to read tag properties like
master_countandmaster_width_factor - When and how to skip gap application
A layout is a Lua table with a name and an arrange(p) function. The compositor calls arrange whenever it needs to position tiled clients on screen (window opened, closed, resized, tag properties changed). Your job is to write a geometry entry for each client. The compositor handles borders and gaps after your function returns.
The Minimal Layout
The simplest useful layout gives every client the full workarea, like max:
local my_max = {
name = "my_max",
arrange = function(p)
for _, c in ipairs(p.clients) do
p.geometries[c] = {
x = p.workarea.x,
y = p.workarea.y,
width = p.workarea.width,
height = p.workarea.height,
}
end
end,
}
Every client gets the same rectangle. The focused one is drawn on top, so it looks like a maximized layout.
Register and Use It
Add your layout to the default list so you can cycle to it:
tag.connect_signal("request::default_layouts", function()
awful.layout.append_default_layouts({
awful.layout.suit.tile,
awful.layout.suit.floating,
awful.layout.suit.max,
my_max, -- your layout is now in the cycle
})
end)
Or set it directly on a tag:
-- Set on a specific tag
local t = awful.screen.focused().selected_tag
t.layout = my_max
-- Set when creating tags
awful.tag({ "1", "2", "3" }, s, my_max)
Press your layout-cycle keybinding (default: Mod4+Space) to cycle through layouts and confirm yours appears.
Read the Workarea
The p.workarea table gives you the usable screen rectangle. Wibars, struts, and screen padding are already subtracted:
local wa = p.workarea
-- wa.x, wa.y = top-left corner
-- wa.width = usable width
-- wa.height = usable height
You do not need to subtract borders or gaps. The compositor applies those after your arrange function returns.
A Practical Example: Centered Column
This layout arranges all clients in a centered column at 60% of the screen width, splitting the height evenly:
local centered_column = {
name = "centered_column",
arrange = function(p)
local wa = p.workarea
local n = #p.clients
if n == 0 then return end
local col_width = math.floor(wa.width * 0.6)
local col_x = wa.x + math.floor((wa.width - col_width) / 2)
local client_height = math.floor(wa.height / n)
for i, c in ipairs(p.clients) do
p.geometries[c] = {
x = col_x,
y = wa.y + (i - 1) * client_height,
width = col_width,
height = client_height,
}
end
end,
}
With three clients on a 1920x1080 screen (after a 30px wibar), each window would be 1152px wide, centered horizontally, and 350px tall stacked vertically.
Using Tag Properties
Layouts can read properties from p.tag to let users control behavior at runtime. The most common are master_count and master_width_factor. See Master and Stack for what each property does.
Here is the centered column layout extended to support a primary/secondary split:
local centered_split = {
name = "centered_split",
arrange = function(p)
local wa = p.workarea
local n = #p.clients
if n == 0 then return end
local nmaster = math.min(p.tag.master_count, n)
local nstack = n - nmaster
local mwfact = p.tag.master_width_factor
-- Master column: centered, takes mwfact of width
local master_w = math.floor(wa.width * mwfact)
local master_x = wa.x + math.floor((wa.width - master_w) / 2)
for i = 1, nmaster do
local h = math.floor(wa.height / nmaster)
p.geometries[p.clients[i]] = {
x = master_x,
y = wa.y + (i - 1) * h,
width = master_w,
height = h,
}
end
-- Stack: fill the full width below the master
if nstack > 0 then
local stack_h = math.floor(wa.height / nstack)
for i = 1, nstack do
p.geometries[p.clients[nmaster + i]] = {
x = wa.x,
y = wa.y + (i - 1) * stack_h,
width = wa.width,
height = stack_h,
}
end
end
end,
}
Users can now adjust the split with the standard keybindings: Mod4+l / Mod4+h to change master_width_factor, Mod4+Shift+l / Mod4+Shift+h to change master_count.
Skipping Gaps
Add a skip_gap function when gaps don't make sense for your layout. The compositor calls skip_gap(nclients, tag) before applying useless_gap. Return true to suppress gaps.
my_max.skip_gap = function(nclients, tag)
return true -- max layout: clients overlap, gaps are meaningless
end
For layouts where a single client fills the screen, skip the gap only when there is one client:
centered_column.skip_gap = function(nclients, tag)
return nclients == 1
end
Tips
- Keep
arrangepure. Only write top.geometries. Do not callc:geometry(), emit signals, or produce other side effects. - Handle zero clients.
p.clientscan be empty. Return early withif #p.clients == 0 then return end. - Force a re-layout. Call
awful.layout.arrange(screen)during development to trigger your layout without opening/closing windows. - Use
math.floor. Pixel coordinates must be integers. Rounding errors from division can cause 1px gaps or overlaps. - Test edge cases. Try your layout with 1 client, 2 clients, and many clients. Resize the screen. Change
master_countandmaster_width_factorto values you didn't expect.
See Also
- Layout Protocol Reference - Full
arrange(p)contract, all fields, built-in layouts - Master and Stack - How tag properties control primary/secondary splits
- AwesomeWM awful.layout docs - Full upstream API reference