Table of Contents
- Writing your first config
- 1. The shortest possible mental model
- 2. A very small starter config
- 3. The recommended file shape
- 4. Includes: loading shared helpers
- 5. Constants and settings near the top
- 6. Small helper functions
- 7. evil.config({...}) in detail
- 8. Autostart
- 9. Key bindings
- 10. Hooks: where the config becomes interesting
- 11. Worked examples
- 12. How to grow a config safely
- 13. Good patterns for readable configs
- Good pattern: name helpers after behavior
- Good pattern: group related binds
- Good pattern: assign hooks at the bottom
- Good pattern: move reusable helpers into include(...) files
- 14. Common mistakes when writing configs
- Mistake: giant anonymous hook bodies
- Mistake: repeated math everywhere
- Mistake: burying constants deep in logic
- Mistake: not validating after each change
- Mistake: trying to build everything from scratch
- 15. Best examples to study after this page
- 16. Final advice
Writing your first config
This page is a slow, detailed guide to writing an evil config.
It is written for people who want to customize the compositor, but who may not be advanced Lua users yet.
This page is not just an API list. It tries to explain:
- what parts a config usually has
- what each part does
- why you would write it that way
- how to make small changes safely
- how to grow from a tiny config into a bigger one
1. The shortest possible mental model
An evil config is a small Lua program that usually does these jobs:
- load helpers
- set the main config table
- declare startup commands
- declare key bindings
- define hook behavior
If you keep those five jobs in mind, most configs become much easier to read.
2. A very small starter config
Here is a tiny config:
-- Rules: match windows by app_id or title
evil.config({ window = { hide_client_decorations = true } })
-- Start a terminal on launch
evil.spawn("foot")
-- Keys
function evil.on.key(ctx)
if ctx.super and ctx.key == "Return" then evil.spawn("foot") end
if ctx.super and ctx.key == "Q" then evil.window.close(ctx.focused.id) end
if ctx.super and ctx.key == "H" then evil.canvas.pan(-64, 0) end
if ctx.super and ctx.key == "L" then evil.canvas.pan(64, 0) end
if ctx.super and ctx.key == "Equal" then evil.canvas.zoom(1.15) end
if ctx.super and ctx.key == "Minus" then evil.canvas.zoom(0.87) end
if ctx.super and ctx.key == "F5" then evil.canvas.overview() end
end
-- Focus new windows, click to focus
function evil.on.resolve_focus(ctx)
if ctx.window and not ctx.window.exclude_from_focus then
evil.window.focus(ctx.window.id)
end
end
What this does
This config sets window decorations, spawns a terminal, defines a few key bindings, and focuses new windows when they appear. It shows the three main ideas: configuration, bindings, and hooks.
3. The recommended file shape
Once a config gets bigger than a few lines, it helps to keep the file in a stable order.
A good order is:
- includes
- user settings/constants
- small helper functions
evil.config({...})- autostart commands
- bindings
- hook assignments
That order is used by the repository examples because it makes the file much easier to scan.
4. Includes: loading shared helpers
Many configs begin with include(...).
Example:
local common = include("lib/common.lua")
local rules = include("rules.lua")
What this does
It loads another Lua file and returns the value from that file.
So if lib/common.lua returns a table of helper functions, you can store it in common and use it later.
Why you would do this
Because one big config file becomes hard to read quickly.
Putting shared helpers in other files lets you keep the main config focused on:
- startup
- bindings
- hooks
- policy structure
instead of every tiny helper detail.
Good things to move into includes
- reusable focus helpers
- reusable draw helpers
- reusable resize math helpers
- shared commands
- shared rules
5. Constants and settings near the top
If your config has values you will want to tweak often, put them near the top.
Example:
local GRID_SIZE = 64
local GAP = 24
local MASTER_RATIO = 0.6
This gives a clear place for user-tunable settings so you do not have to search through the whole file for hidden numbers.
6. Small helper functions
When a piece of logic has a real meaning, give it a name.
Example:
local function snap_to_grid(value)
return math.floor((value / GRID_SIZE) + 0.5) * GRID_SIZE
end
A named helper like snap_to_grid is easier to read than repeating the formula in every hook.
Good example helper names
snap_to_gridfind_anchor_windowmove_window_with_pointer_deltatoggle_floatingpage_origin_x
Bad example helper names
fx1thingcalc
The goal is not to be clever. The goal is to make the policy obvious.
7. evil.config({...}) in detail
The main config table sets the baseline environment. For every field, see the Configuration guide or the Lua API cheat sheet.
The most important sections:
| Section | Purpose |
|---|---|
backend |
Which runtime to use ("winit", "udev", "headless") |
canvas |
Camera zoom/pan limits and behavior |
draw |
Draw layer order and clear color |
window |
Baseline window behavior (client size, decorations) |
placement |
Fallback placement when no place_window hook |
tty |
TTY backend settings (quit key, VT switching, output layout) |
rules |
Per-window startup rules (floating, size, focus exclusion) |
Why the config table matters
Because it defines the baseline environment that your hooks and bindings will build on.
Hooks are where policy becomes dynamic.
But evil.config({...}) is where the baseline shape of the session is set.
8. Autostart
Use evil.autostart(command) when something should start automatically.
Example:
evil.autostart("foot")
evil.autostart("waybar")
evil.autostart(command) runs a shell command when the compositor starts. Typical uses: terminal, panel, launcher, wallpaper helper. Keep commands readable; move long shell logic into scripts.
9. Key bindings
Bindings are declared with evil.bind(...).
Example:
evil.bind("Super+Return", "spawn", { command = "foot" })
evil.bind("Super+Q", "close_window")
evil.bind("Super+H", "pan_left", { amount = 32 })
What the pieces mean
evil.bind(keyspec, action, opts?) registers a key combination. keyspec is the key combination (e.g. "Super+Return"), action is the built-in action name, and opts passes extra data like { command = "foot" }.
Built-in actions currently available
pan_leftpan_rightpan_uppan_downzoom_inzoom_outspawnclose_windowfocus_nextfocus_prevquit
Good way to organize bindings
Group them by purpose.
Example:
-- App / utility binds
-- keys handled in evil.on.key below
evil.bind("Super+Return", "spawn", { command = commands.terminal })
evil.bind("Super+D", "spawn", { command = commands.launcher })
evil.bind("Super+Q", "close_window")
-- Canvas navigation binds
evil.bind("Super+H", "pan_left", { amount = 32 })
evil.bind("Super+L", "pan_right", { amount = 32 })
evil.bind("Super+Equal", "zoom_in", { amount = 1.15 })
That way, the file is easier to skim and edit later.
10. Hooks: where the config becomes interesting
Hooks are where most of the custom behavior lives.
function evil.on.window_mapped(ctx)
evil.window.focus(ctx.window.id)
end
Every hook receives a ctx table with information about what's happening.
All hooks get these:
| Field | What it is |
|---|---|
ctx.event |
The hook name, like "key" or "move_update" |
ctx.state |
Snapshot of everything: windows, outputs, pointer, focus |
ctx.window |
The window this hook is about (varies by hook) |
ctx.pointer |
Current pointer position: { x, y } |
ctx.super |
True if Super (Windows/Command) is held |
ctx.alt |
True if Alt is held |
ctx.ctrl |
True if Ctrl is held |
ctx.shift |
True if Shift is held |
Hook-specific extras:
| Hook | Extra fields |
|---|---|
key |
ctx.key — key name ("H", "Return", etc.), ctx.focused — focused window or nil |
resolve_focus |
ctx.reason — "window_mapped", "pointer_button", "window_unmapped" |
move_update |
ctx.dx, ctx.dy — pixels moved since last update |
resize_update |
ctx.dx, ctx.dy, ctx.edges — { left, right, top, bottom } |
window_property_changed |
ctx.property, ctx.old_value, ctx.new_value |
gesture |
ctx.kind — "swipe" or "pinch", ctx.fingers, ctx.dx, ctx.dy, ctx.scale |
draw_background |
ctx.output.viewport — screen size, zoom, visible world |
draw_window_overlay |
ctx.focused_window — focused window table, or nil |
For the full reference, see Hook payload summary.
Imperative commands (the default style)
Directly call runtime commands from inside hooks:
function evil.on.move_update(ctx)
evil.window.move(ctx.window.id, ctx.window.x + ctx.dx, ctx.window.y + ctx.dy)
end
This is the recommended style for most configs. It's easy to read and modify.
Returned actions (alternative style)
function evil.on.move_update(ctx)
return {
kind = "move_window",
id = ctx.window.id,
x = ctx.window.x + ctx.dx,
y = ctx.window.y + ctx.dy,
}
end
Useful when a hook wants to describe several related operations together. Prefer imperative commands by default.
11. Worked examples
Movement
local function move_window_with_pointer_delta(ctx)
evil.window.move(ctx.window.id, ctx.window.x + ctx.dx, ctx.window.y + ctx.dy)
end
evil.on.move_update = move_window_with_pointer_delta
Placement (place next to focused window)
function evil.on.place_window(ctx)
for _, w in ipairs(ctx.state.windows) do
if w.focused then
evil.window.move(ctx.window.id, w.x + w.w + 24, w.y)
return
end
end
end
Focus
function evil.on.resolve_focus(ctx)
if ctx.reason == "window_mapped" and ctx.window then
evil.window.focus(ctx.window.id)
elseif ctx.reason == "pointer_button" and ctx.window then
evil.window.focus(ctx.window.id)
elseif ctx.reason == "pointer_button" then
evil.window.clear_focus()
end
end
Draw (focus border)
function evil.on.draw_window_overlay(ctx)
for _, w in ipairs(ctx.state.windows) do
if w.focused then
return {
evil.draw.stroke_rect({
space = "world", x = w.x, y = w.y,
w = w.w, h = w.h,
width = 2, outer = 2, color = { 0.8, 0.6, 1.0, 1.0 },
}),
}
end
end
end
12. How to grow a config safely
A good way to grow a config is:
Step 1
Start from examples/tty-baseline.lua
Step 2
Run:
cargo run --bin evil -- --check-config --config your-config.lua
Step 3
Change one thing at a time:
- one bind
- one helper function
- one hook
- one draw helper
Step 4
Validate again
Step 5
Only add another feature after the previous one is clear
This keeps your config understandable.
13. Good patterns for readable configs
Good pattern: name helpers after behavior
local function move_window_with_pointer_delta(ctx)
Good pattern: group related binds
-- App binds
...
-- Canvas binds
...
Good pattern: assign hooks at the bottom
evil.on.resolve_focus = resolve_focus
evil.on.move_update = move_window_with_pointer_delta
Good pattern: move reusable helpers into include(...) files
local common = include("lib/common.lua")
14. Common mistakes when writing configs
Mistake: giant anonymous hook bodies
Hard to read, hard to debug, hard to reuse.
Mistake: repeated math everywhere
If you write the same formula more than once, make a helper.
Mistake: burying constants deep in logic
If a number is likely to be changed, put it near the top.
Mistake: not validating after each change
Always use --check-config often.
Mistake: trying to build everything from scratch
Start from an example and change it slowly.
15. Best examples to study after this page
For baseline structure
examples/tty-baseline.lua
For a more advanced policy file
yy|
examples/tiling.luayy|
16. Final advice
A good evil config does not need to be impressive.
It needs to be readable.
If you can answer these questions by reading your own file, the config is probably in good shape:
- where are the settings?
- where are the helper functions?
- what do the binds do?
- what do the hooks do?
- where would I change movement?
- where would I change focus?
- where would I change placement?
If those answers are easy to find, the config is in a good state.
evil wiki
- Home
- Getting started
- Architecture and philosophy
- Configuration guide
- Writing your first config
- Lua basics for evil users
- Lua API guide
- Lua API cheat sheet
- Hooks guide
- Hook payload summary
- Queries, commands, and actions
- Drawing and visuals
- Example configs overview
- Example shared helpers and rules
- IPC protocol
- Testing and debugging
- Contributor validation matrix
- Feature status and limitations
- Feature and support matrix
- Recipes