contents

Lua configuration#

If you would rather compute your config than write it out by hand, carrot also reads ~/.config/carrot/carrot.lua in place of the KDL file. It is the same schema run through the same validators, so everything on the Configuration page holds here too, translated by a few mechanical rules. If both files exist, KDL wins.

The script runs in piccolo, a pure-Rust Lua VM, inside the core sandbox: the io and os libraries and all filesystem access are stripped out. Compute whatever you like and leave the result in one global:

carrot = {
    -- everything lives in this table
}

The translation rules#

  • Keys are snake_case where KDL is kebab-case: repeat-rate becomes repeat_rate, open-on-workspace becomes open_on_workspace.
  • Repeated KDL nodes become arrays: window-rule nodes become the window_rules array, layer-rule becomes layer_rules, and remap becomes remaps, with the profile name as a name field.
  • Name-headed KDL nodes become name-keyed maps: output "DP-3" { } becomes outputs = { ["DP-3"] = { } }, and device "x" { } becomes input.devices = { ["x"] = { } }.
  • KDL bare flags become booleans, so numlock turns into numlock = true.
  • The two spawn node types merge into one array, where a table element is argv and a string element runs through the shell.
  • Unknown keys are hard errors, and errors accumulate across sections so one typo does not hide the next.
  • Colors are the same #rgb/#rgba/#rrggbb/#rrggbbaa strings, leading # required.

Four shapes differ deliberately:

  • VRR is an explicit string, vrr = "off" | "on-demand" | "always", where KDL uses the bare/property node form.
  • Binds are tables with named fields instead of chord-named nodes.
  • Animation curves are one name-keyed curves map instead of repeated curve nodes.
  • An animation style is a table with the style name as its first element: style = { "popin", perc = 80 }.

A complete config#

carrot = {
    input = {
        keyboard = {
            xkb = { layout = "us" },
            repeat_rate = 35, repeat_delay = 250, numlock = true,
        },
        touchpad = { natural_scroll = true, accel_profile = "flat" },
        mouse = { accel_profile = "flat", accel_speed = 0.0 },
        devices = {
            ["turtle-beach"] = { accel_speed = -0.86, dpi = 1600 },
        },
        mod_key = "super",
    },

    outputs = {
        ["DP-3"] = { mode = "2560x1440@240", vrr = "on-demand", allow_tearing = true },
    },

    layout = {
        mode = "scrolling", gaps_in = 5, gaps_out = 10,
        border = { width = 2, active_color = "#89b4fa", inactive_color = "#585b70" },
        float_above_fullscreen = false,
        scrolling = {
            preset_widths = { 0.333, 0.5, 0.667 },
            default_width = 0.5,
            center_focus = "never",
        },
    },

    animations = {
        slowdown = 1.0,
        curves = { overshot = { 0.05, 0.9, 0.1, 1.05 } },
        spring = { damping_ratio = 1.0, stiffness = 800, epsilon = 0.0001 },
        window_open  = { ease = { duration_ms = 150, curve = "ease-out-expo" }, style = { "popin", perc = 80 } },
        window_close = { ease = { duration_ms = 150, curve = "ease-out-quad" }, style = { "popin", perc = 80 } },
        workspace_switch = { spring = { damping_ratio = 1.0, stiffness = 1000, epsilon = 0.0001 }, style = { "slide" } },
        border_color = { ease = { duration_ms = 200, curve = "ease-out-quad" } },
    },

    decoration = {
        rounding = 10,
        dim_inactive = 0.1,
        shadow = { size = 20, color = "#00000099", offset = { 0, 4 }, power = 2.0 },
        blur = { passes = 3, size = 2.5, noise = 0.02 },
    },

    layer_rules = {
        { match = { "^quickshell:bar" }, blur = true, ignore_alpha = 0.2 },
    },

    cursor = { software = false },

    environment = { QT_QPA_PLATFORM = "wayland", DISPLAY = false },  -- false removes
    spawn_at_startup = {
        { "quickshell" },                          -- argv
        "wl-paste --watch cliphist store",         -- shell string
    },
    screencast = { picker = "carrot-share-menu" },

    binds = {
        { chord = "Mod+Return",  action = "spawn", args = { "foot" } },
        { chord = "Mod+Q",       action = "close-window", on = "release" },
        { chord = "Mod+F",       action = "toggle-fullscreen" },
        { chord = "Mod+MouseLeft",  action = "pointer-move" },
        { chord = "Mod+MouseRight", action = "pointer-resize" },
        { chord = "Mod+T",       action = "set-layout", args = { "toggle" } },
        { chord = "Mod+BracketLeft",  action = "consume-or-expel-left" },
        { chord = "Mod+BracketRight", action = "consume-or-expel-right" },
        { chord = "Mod+R",       action = "cycle-column-width" },
        { chord = "Mod+1",       action = "focus-workspace", args = { 1 } },
        { chord = "Mod+Shift+1", action = "move-to-workspace", args = { 1 } },
        { chord = "Mod+Left",    action = "focus-left" },
        { chord = "Mod+minus",   action = "adjust-split-ratio", args = { -0.1 } },
        { chord = "XF86AudioMute", action = "spawn-sh",
          args = { "wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle" },
          allow_when_locked = true },
    },

    window_rules = {
        {
            match = { { title = "^Minecraft", is_xwayland = false } },
            exclude = { { title = "launcher" } },
            open_on_workspace = 6, opacity = 0.98, allow_tearing = true,
        },
        {
            match = { { app_id = "^foot$", title = "^scratch" } },
            open_floating = true, default_size = { 800, 600 }, open_centered = true,
        },
    },

    remaps = {
        {
            name = "mcsr",
            match = { title = "MCSR Ranked", is_xwayland = false },
            maps = { { "R", "F3" } },
        },
    },
}

Binds in Lua#

Each bind is a table: chord and action are required; args is an ordered array of strings and numbers; the optional flags are on = "press"|"release", ["repeat"] = true (bracketed, repeat is a Lua keyword), allow_when_locked = true, cooldown_ms = 200, and title = "...". Action names are identical to KDL: spawn, spawn-sh, focus-workspace, move-to-workspace, send-to-workspace, close-window, toggle-fullscreen, toggle-floating, focus-next/prev/left/right/up/down, swap-left/right/up/down, adjust-split-ratio, set-layout, consume-or-expel-left/right, move-column-left/right, cycle-column-width, cycle-column-width-back, toggle-full-width, center-column, pointer-move, pointer-resize, quit.

Splitting it up#

include("file.lua") runs another file in the same globals, resolved against the config's directory, and returns whatever that file returns, so both styles work:

include("binds.lua")                 -- the file assembles tables itself
local rules = include("rules.lua")   -- or hands one back

The sandbox has no io, so include is the one way to read another file. It is capped at 64 files per load, and an included file's errors carry its name.

Determinism#

Lua table iteration order is undefined, so carrot sorts name-keyed maps by name and array sections by index before applying them. Two reloads of the same file always produce the same config, whatever the VM's hash seeds did.

Since it is a language#

The table is built by ordinary Lua, so loops work:

local binds = {}
for i = 1, 9 do
    binds[#binds+1] = { chord = "Mod+" .. i,          action = "focus-workspace",  args = { i } }
    binds[#binds+1] = { chord = "Mod+Shift+" .. i,    action = "move-to-workspace", args = { i } }
end
carrot = { binds = binds }

Remember the wholesale-replacement rule: mentioning binds at all replaces the entire default set, so generate everything you need.