Table of Contents
- Configuration guide
- The simplest mental model
- A recommended config shape
- 1. Includes and shared modules
- 2. Constants / user-tunable settings
- 3. Small local helper functions
- 4. evil.config({...})
- 5. Autostart + binds
- 6. Hook assignments
- include(...)
- evil.config({...}) in practice
- Autostart
- Bindings
- Hooks
- Good beginner strategy for writing a config
- When to split a config into several files
- Common config mistakes
- Read next
Configuration guide
|This page explains how an evil config is usually structured and how to think about building your own.
The simplest mental model
A config file is just a Lua program that usually does these jobs:
- load helpers with
include(...) - define local helper functions
- call
evil.config({...}) - declare autostart commands
- declare key bindings
- assign hooks
That is the main pattern used by the shipped examples.
A recommended config shape
A good config order is:
1. Includes and shared modules
local common = include("lib/common.lua")
local shared_rules = include("rules.lua")
local commands = common.commands
2. Constants / user-tunable settings
local GAP = 24
local GRID_SIZE = 64
3. Small local helper functions
local function snap_to_grid(value)
return math.floor((value / GRID_SIZE) + 0.5) * GRID_SIZE
end
4. evil.config({...})
evil.config({
backend = "winit",
canvas = {
min_zoom = 0.2,
max_zoom = 4.0,
},
})
5. Autostart + binds
evil.autostart(commands.terminal)
evil.bind("Super+Return", "spawn", { command = commands.terminal })
6. Hook assignments
evil.on.move_update = function(ctx)
...
end
This order makes the file easier to scan.
include(...)
Use include(...) to split configs into smaller files.
Example:
local common = include("lib/common.lua")
Important facts:
- includes are relative to the config root
- includes are intentionally kept inside that root
- you can use includes to separate shared helpers, rules, or layout logic
evil.config({...}) in practice
The config table sets the main runtime settings.
The most common sections are:
backendcanvasdrawwindowplacementttyrules
backend
Use:
"winit"for nested live sessions"headless"for headless runtime use"udev"for tty / standalone experiments
canvas
Controls camera behavior.
Useful fields:
min_zoommax_zoomzoom_steppan_stepallow_pointer_zoomallow_middle_click_panallow_gesture_navigation
draw
Controls compositor-drawn visuals.
Useful fields:
stackclear_color
window
Controls a few baseline Rust-side window facts.
Useful fields:
use_client_default_sizeremember_sizes_by_app_idhide_client_decorations
placement
Fallback placement used when no Lua placement hook overrides it.
Useful fields:
default_sizepaddingcascade_step
tty
TTY-only settings.
Useful fields:
quit_keyspecvt_switch_modifiersoutput_layout
rules
Rules are the simplest way to change startup behavior for certain windows.
Example:
rules = {
{ app_id = "foot", floating = true, size = { w = 900, h = 600 } },
{ title_contains = "scratch", exclude_from_focus = true },
}
Autostart
Use evil.autostart(command) for programs that should run when the compositor starts.
Example:
evil.autostart(commands.terminal)
Keep in mind:
- commands run through a shell
- that is intentional so simple shell syntax works
- because of that, keep commands readable and trusted
Bindings
Use evil.bind(keyspec, action, opts?).
Examples:
evil.bind("Super+Return", "spawn", { command = commands.terminal })
evil.bind("Super+Q", "close_window")
evil.bind("Super+H", "pan_left", { amount = 32 })
Good binding groups
It helps to group bindings by purpose:
- launchers / apps
- close / session control
- canvas movement
- workspace/page control
- mode toggles
That makes the config easier to edit later.
Hooks
Hooks are where most custom behavior lives.
Common pattern:
evil.on.move_update = function(ctx)
evil.window.move(ctx.window.id, ctx.window.x + ctx.dx, ctx.window.y + ctx.dy)
end
A good config usually defines helper functions first, then assigns those helpers to hooks.
Example:
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
That is easier to read than putting all logic inside one giant anonymous function.
Good beginner strategy for writing a config
If you are new, do not try to invent a whole config from scratch.
A better path is:
- copy
examples/tty-baseline.lua - run
--check-config - change one bind
- change one hook
- test again
- repeat
That keeps problems small and understandable.
When to split a config into several files
Split when:
- one file is getting too long
- you want shared helpers
- you want a reusable rule file
- you want to separate layout logic from startup/binds
Good candidates for separate files:
lib/common.luarules.lualayout.luadraw.lua
Common config mistakes
- putting too much logic in one anonymous hook
- not using
local - mixing unrelated binds into one block with no comments
- trying to build a giant perfect config all at once
- forgetting that some runtime facts are still intentionally narrow or provisional
Read next
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