1
0
Fork 0
9 Writing Your First Config
Agent edited this page 2026-05-26 05:47:03 -04:00

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:

  1. load helpers
  2. set the main config table
  3. declare startup commands
  4. declare key bindings
  5. 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.

Once a config gets bigger than a few lines, it helps to keep the file in a stable order.

A good order is:

  1. includes
  2. user settings/constants
  3. small helper functions
  4. evil.config({...})
  5. autostart commands
  6. bindings
  7. 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_grid
  • find_anchor_window
  • move_window_with_pointer_delta
  • toggle_floating
  • page_origin_x

Bad example helper names

  • f
  • x1
  • thing
  • calc

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_left
  • pan_right
  • pan_up
  • pan_down
  • zoom_in
  • zoom_out
  • spawn
  • close_window
  • focus_next
  • focus_prev
  • quit

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)
-- 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.lua yy|

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.