contents

Configuration#

by default carrot reads one config file, in your choice of two languages:

  • ~/.config/carrot/carrot.kdl (KDL v2, the default and this page)
  • ~/.config/carrot/carrot.lua (Lua, same schema; see Lua configuration)

If both exist, KDL wins. The directory honors XDG_CONFIG_HOME.

On first run, carrot writes you a fully commented default config and boots with it, so ~/.config/carrot/carrot.kdl is already there to edit after your first session. A file that fails to parse never kills the session: at startup carrot prints every error and runs the built-in default, and on a live reload it keeps the running config and reports the errors over IPC.

Three rules shape the whole schema:

  1. Only implemented features parse. Reserved names (workspace, switch-events, layout focus-ring/shadow/struts, touchpad tap/dwt) error with "not implemented yet" and join the schema when their feature lands, so a key can never silently no-op.
  2. Omitted keys inherit the defaults, and mentioned sections replace them. Setting repeat-rate alone keeps every other default, but writing any binds { } block replaces the entire default bind set. The same holds for output, window-rule, layer-rule, remap, spawn-at-startup, and environment.
  3. Reload is live where possible. burrow reload applies keyboard, pointer, cursor, layout, decoration, animations, rules, and binds immediately; outputs, debug, and startup spawns are cold and print carrot: config: <key>: needs restart.

You can also parse a config without starting a session: carrot check-config [path] prints every error and exits 1 if anything is broken.

Whole blocks comment out with KDL's slashdash: /-output "DP-1" { ... }.

Splitting the config#

One file can pull in more:

include "binds.kdl"
include "rules/games.kdl"

Paths resolve against the file doing the including, so nested includes carry their own neighborhoods. The three rules hold across the whole tree: mentioning a wholesale section (binds, output, the rules, remaps, spawns, environment) anywhere in it replaces that default set exactly once, so binds split across three files accumulate together instead of clobbering each other. Includes are cycle-guarded and capped at eight levels, and an included file's errors are labeled with its name, line, and column, so a typo says which file to open.

The Lua flavor is include("file.lua"); see Lua configuration.

input#

input {
    keyboard {
        xkb { layout "us"; variant "colemak"; options "compose:ralt" }
        repeat-delay 600    // ms, 1..5000
        repeat-rate 25      // per second, 1..200
        numlock             // bare flag
    }
    touchpad { natural-scroll; accel-profile "flat"; accel-speed 0.2 }
    mouse { accel-profile "flat"; accel-speed 0.0 }   // speed -1.0..1.0
    device "name substring" { accel-speed -0.5; dpi 1600 }
    mod-key "super"         // what Mod means in binds; "super" or "alt"
}

Full detail, including per-device rules and remaps, in Input devices. Today only xkb.layout selects the keymap; variant and options parse ahead of support.

output#

Repeatable; the name is a connector ("DP-3", see burrow monitors) or a "make model serial" string.

output "DP-3" {
    mode "2560x1440@240"       // closest advertised mode wins
    variable-refresh-rate      // bare = always; on-demand=#true = fullscreen only
    allow-tearing              // async flips when the surface asks
    scale 1.5                  // parses; not applied yet
    position x=0 y=0           // parses; not applied yet
    off                        // parses; not applied yet
}

mode, variable-refresh-rate, and allow-tearing work today; scale, position, and off are stored but not consumed yet (outputs report scale 1 and auto-tile left to right).

layout#

layout {
    mode "dwindle"             // or "scrolling"
    gaps-in 5
    gaps-out 10
    border { width 2; active-color "#89b4fa"; inactive-color "#585b70" }
    float-above-fullscreen
    workspace-axis "horizontal"     // or "vertical"; dwindle only
    scrolling {
        preset-widths 0.333 0.5 0.667
        default-width 0.5           // or default-width-px 800
        center-focus "never"        // never, always, on-overflow
    }
}

All applied. Colors are #rgb, #rgba, #rrggbb, or #rrggbbaa, leading # required. mode sets the layout new workspaces start with, the dwindle tree or the scrolling strip; both are on the tiling page, and the set-layout action flips a live workspace between them. workspace-axis picks which way the workspace stack runs on an all-dwindle session; a scrolling workspace always stacks vertically, since its strip owns the horizontal axis.

The scrolling block tunes the strip. preset-widths is the ladder cycle-column-width steps through, one or more proportions of the screen in 0.05..1. default-width is what a new column gets, either a proportion or default-width-px for absolute pixels (50..100000). center-focus decides when the view centers the focused column: "never" scrolls just enough to keep it on screen, "always" centers every time, "on-overflow" centers only when the previous and current columns cannot both fit.

Reserved here: focus-ring, shadow, struts (the drawn shadow lives in decoration).

decoration#

The eye candy: rounded corners, drop shadows, dim-inactive, and Kawase blur, all drawn as SDF effects by carrot's own renderer. The default config ships rounding (10 px) and shadows on; dim and blur stay opt-in, and anything switched off costs nothing.

decoration {
    rounding 10                 // px, 0..200; 0 = square corners
    dim-inactive 0.25           // shade on unfocused windows, 0..1
    shadow {
        size 20                 // px, 1..200
        color "#00000099"
        offset 0 4              // x y, each within -500..500
        power 2.0               // falloff exponent, 0.5..8; higher = tighter
    }
    blur {
        passes 3                // kawase levels, 1..4
        size 2.5                // per-level tap offset, 0.5..6
        noise 0.02              // 0..1
        contrast 1.0            // 0..2
        brightness 1.0          // 0..2
    }
}
  • rounding clips windows (and their borders, which become rounded rings) against the radius. rounding-power parses for superellipse corners ahead of support; the shader family is circular today.
  • shadow draws a distance-falloff halo with the window's body cut out of it. Once the block exists, every window gets one; a window rule with shadow #false exempts.
  • dim-inactive shades every unfocused window, and the shade fades in and out with the border-color animation's motion. dim #false in a window rule exempts.
  • blur builds the backdrop pyramid but draws nothing by itself: blur is opt-in per window and per layer surface through window and layer rules. The blurred backdrop is the background and bottom layers (the wallpaper), cached and re-baked only when they change. xray accepts only #true today; xray #false (blurring the windows beneath instead) errors "not implemented yet".

Fullscreen windows skip all four. The whole block reloads live, and a reload re-resolves the per-window rule flags on running windows too.

animations#

Window opens, closes, and moves, workspace switches, the scrolling strip's view, layer surfaces, and border colors all animate, on by default and live on reload. Each of the nine kinds takes its own spring or easing, plus a style where one applies; curves, styles, the clock model, and per-window overrides are all on the Animations page.

animations {
    // off
    slowdown 1.0
    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" }
}

cursor#

cursor { xcursor-theme "Adwaita"; xcursor-size 24; software }

software composites the cursor instead of the hardware plane. xcursor-theme and xcursor-size (1..512) pick the pointer theme; the config wins over the XCURSOR_THEME/XCURSOR_SIZE environment variables, which stay as the fallback.

session#

prefer-no-csd                       // ask apps to drop their own titlebars
spawn-at-startup "quickshell"       // argv form, repeatable
spawn-sh-at-startup "a | b && c"    // one shell string, repeatable
environment { DISPLAY #null; QT_QPA_PLATFORM "wayland" }
screencast { picker "carrot-share-menu" }
  • prefer-no-csd overrides the client's decoration wish: apps drop their own titlebars and carrot draws the borders. On in the default config.
  • spawn-at-startup takes argv words; spawn-sh-at-startup takes one string for /bin/sh -c. Both run once at startup (cold on reload).
  • environment entries apply to everything carrot spawns; #null removes a variable.
  • screencast { picker } names your consent chooser; contract in Screen sharing. Unset means click-to-select.

binds#

binds {
    Mod+Return { spawn "foot"; }
    Mod+Q on="release" { close-window; }
    Mod+T { set-layout "toggle"; }
    XF86AudioMute allow-when-locked=#true { spawn-sh "wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle"; }
}

The chord is the node name, the child is the action, and Mod resolves through mod-key. The default config ships a full working set: window management on both arrows and the home row, workspaces 1-9, screenshots, audio, media, and brightness keys, with the scrolling column verbs as commented examples. Grammar, properties, and every action: Binds & actions.

window-rule#

window-rule {
    match title=#"^Minecraft"# is-xwayland=#false
    exclude title=#"launcher"#
    open-on-workspace 6
    opacity 0.98
    allow-tearing #true
}

Matchers AND within a node and OR across nodes, and exclude vetoes. The open-* effects apply once at map; opacity and allow-tearing stay live; no-anim and animation shape the window's open and close animations; rounding, shadow, dim, and blur override the decoration pass per window; no-capture blacks the window out of screenshots, recordings, and casts. Full reference: Window rules.

layer-rule#

layer-rule { match namespace=#"^quickshell:bar"#; blur; ignore-alpha 0.2 }
layer-rule { match namespace=#"^quickshell"#; no-anim }

Blur for bars and launchers, matched by namespace and masked by the surface's own alpha, and no-anim for shells whose layer surfaces would otherwise re-animate on every state change. Full reference: Window rules.

remap#

remap "mcsr" {
    match title="MCSR Ranked" is-xwayland=#false
    map "R" "F3"
}

Per-window key rewriting; first matching profile wins. Details in Input devices.

debug#

debug {
    render-drm-device "/dev/dri/card0"
    ignore-drm-device "card1"           // repeatable
    latency-policy "late-latch"         // or "vblank"
    latch-margin-us 1000                // floor under the adaptive margin
}

Stability-exempt escape hatches. latency-policy picks the frame scheduler: "late-latch" (the default) holds each dirty frame until just before the last instant that still makes the next scan, under an adaptive margin, and "vblank" renders as soon as the pipe frees up, one frame of queue depth. The margin grows sharply on a missed flip, decays when stable, and parks the scheduler back on the conservative path after consecutive misses; latch-margin-us puts a microsecond floor under it for hardware that wants more headroom. The DRM device keys are parsed ahead of the multi-GPU work.

Reserved names#

workspace, switch-events, layout focus-ring/shadow/struts, and touchpad tap/dwt all error with "not implemented yet". Each returns in the release that implements it, and configs written for them will start working without edits.