Swindle

A dwl fork with dwindle tiling and a Lua config that hot-reloads. The poor man's Hyprland.

wlroots 0.20 Lua 5.4 C11

About

Swindle began at kantiankant/swindle, which is now archived. It lives on at hakuxyz/swindle, where it is actively maintained.

Under the hood it is dwl with a different personality: one tiling layout (dwindle, a binary-split tree), configuration in Lua instead of a header file, live reload on save, split touchpad and mouse settings, a configurable keyboard layout, and a dwl-IPC client called smsg for bars and scripts.

Install

On CachyOS or Arch, install the dependencies:

sudo pacman -S base-devel wlroots0.20 wayland wayland-protocols libinput libxkbcommon lua54 libxcb xcb-util-wm pixman libdrm seatd

Then build and install:

make
sudo make install

This installs swindle and smsg to /usr/local/bin and an example config to /etc/swindle. There is also a Nix flake in the repo (nix build, nix run).

Your first config

Swindle refuses to start without a config file. It takes the first readable file it finds:

  1. swindle -c /path/to/config.lua
  2. $XDG_CONFIG_HOME/swindle/config.lua, else ~/.config/swindle/config.lua
  3. /etc/swindle/config.lua

Copy the example config and trim it down:

mkdir -p ~/.config/swindle
cp /etc/swindle/config.lua ~/.config/swindle/config.lua

A minimal config that opens a terminal and a launcher looks like this:

sloppy_focus = true
log_level = "error"

appearance = {
    border_px = 2,
    gaps = 10,
    root_color = 0x222222ff,
    border_color = 0x00000000,
    focus_color = 0xe8e8e8ff,
    urgent_color = 0xff0000ff,
    fullscreen_bg = 0x000000ff,
}

keyboard = { layout = "us" }

monitors = {
    { name = nil, scale = 1.0, layout = "dwindle", x = -1, y = -1 },
}

autostart = {}

keybinds = {
    { mods = {"logo"}, key = "q",     action = "spawn", args = {"foot"} },
    { mods = {"logo"}, key = "space", action = "spawn", args = {"wofi", "--show", "drun"} },
    { mods = {"logo"}, key = "w",     action = "killclient" },
    { mods = {"logo"}, key = "f",     action = "togglefullscreen" },
    { mods = {"logo", "shift"}, key = "e", action = "quit" },
}

for i = 1, 9 do
    local key  = tostring(i)
    local mask = 1 << (i - 1)
    table.insert(keybinds, { mods = {"logo"},          key = key, action = "view", args = {tostring(mask)} })
    table.insert(keybinds, { mods = {"logo", "shift"}, key = key, action = "tag",  args = {tostring(mask)} })
end

buttons = {
    { mods = {"logo"}, button = "left",  action = "moveresize", args = {"move"} },
    { mods = {"logo"}, button = "right", action = "moveresize", args = {"resize"} },
}

Save the file while swindle runs and most changes apply instantly: appearance, keybinds and keyboard layout. Pointer settings take effect on newly plugged devices. Only autostart never re-runs. Check syntax without starting with luac5.4 -p config.lua.

Reference

Input: touchpad vs mouse

Generic keys are the fallback. Per-class keys win when set, anything else falls back:

input = {
    scroll_method = "2fg",
    scroll_method_touchpad = "2fg",
    scroll_method_mouse = "edge",
    natural_scrolling = false,
    natural_scrolling_touchpad = true,
    natural_scrolling_mouse = false,
    tap_to_click = true,
    click_method = "button_areas",
    accel_profile = "adaptive",
    accel_speed = 0.0,
}

Keyboard

keyboard = {
    layout = "de",
    variant = "",
    options = "caps:escape",
    model = "",
}

Rules and monitors

rules = {
    { app_id = "Gimp", floating = true, monitor = -1 },
    { app_id = "firefox", tags = 1 << 8, monitor = -1 },
}

monitors = {
    { name = nil, mfact = 0.55, nmaster = 1, scale = 1.0,
      layout = "dwindle", x = -1, y = -1 },
}

Autostart

autostart = {
    "dbus-update-activation-environment --systemd WAYLAND_DISPLAY XDG_SESSION_TYPE",
    "waybar",
}

Actions

Every keybind maps to an action. The dwindle helpers are setmfact, zoom and focusstack:

{ mods = {"logo"},          key = "i", action = "setmfact",   args = {"+0.05"} },
{ mods = {"logo"},          key = "d", action = "setmfact",   args = {"-0.05"} },
{ mods = {"logo", "shift"}, key = "m", action = "zoom" },
{ mods = {"logo"},          key = "n", action = "focusstack", args = {"next"} },
{ mods = {"logo"},          key = "p", action = "focusstack", args = {"prev"} },
ActionArgsDoes
spawnargvrun a program
killclientclose the focused window
togglefloatingfloat or unfloat
togglefullscreenfullscreen toggle
togglegapsgaps on or off, this monitor
togglesmartgapssmart gaps on or off
setgaps"+5" / "10"relative or absolute gaps
setmfact"+0.05" / "55"split ratio, delta or percent
zoomswap focused with first tiled
focusstack"next" / "prev"cycle focus
focusdir / swapdirdirectiondirectional focus or swap
view / toggleviewmask or "all"show tags
tag / toggletagmaskassign window to tags
focusmon / tagmon"left" / "right"across monitors
setlayout0 / 1floating or dwindle
moveresize"move" / "resize"mouse drag action
chvtnumberswitch virtual terminal
quitexit swindle

Help

Still stuck? Open an issue at hakuxyz/swindle.