Overview

NOTE: CATNIP IS STILL VERY EARLY IN DEVELOPMENT AND IS NOT CONSIDERED TO HAVE EVEN REACHED 0.1.0 YET. THIS PROJECT IS VERY MUCH A WIP AND THE DOCS HERE ARE MOST LIKELY OUTDATED IN MULTIPLE PLACES.

Catnip is a wlroots based Wayland compositor that allows users to customize their desktop via Lua.

The (Anti) Pitch

Catnip is not meant for users looking for an out-of-the-box functioning compositor. It is meant for extremists (like myself) who enjoy wasting copious amounts of time to customize their desktop for a deeply unique and personalized experience.

In particular, catnip strives to be:

Minimalistic

Catnip does not provide any default behavior, everything must be defined by the user. This keeps catnip lean, unobstructive, and explicit. However, it also means that catnip is verbose. The user may have to spend considerable time defining how the compositor should function.

Powerful

Catnip makes very few assumptions on what it should handle internally. Instead, it delegates most of the implementation effort to the user by exposing a low level API, allowing for complete control over compositor behavior.

Stable

Due to the extremity of catnip's minimalism, the need to make changes (and in particular, breaking changes) should be quite rare once v1.0 is reached. Additionally, since much of the compositor behavior is defined by the user, breaking changes in catnip can be destructive, even potentially rendering the compositor unusable. For this reason, catnip takes great strides to ensure users are given proper time and warning before breaking changes are rolled out.

*NOTE: Catnip should still be considered beta before v1.0. Breaking changes may happen between minor versions until v1.0 is reached.

Similar Projects

Catnip is not for everybody (and probably shouldn't be). There are plenty of other projects that will perfectly suffice for most users without requiring a large time investment and deep understanding of the compositor:

Installation

Currently, catnip only supports being installed from source. However, support for packaged distributions may be added in the future (contributions welcome!).

Dependencies

Catnip depends on the following packages to run:

(Note that some of these packages may depend on one another so its not a "minimal" set of dependencies. This is simply the list of dependencies that catnip directly uses.)

It is recommended to install these using your system package manager. For example, on Arch you can use:

pacman -S librsvg cairo pango luajit libxkbcommon wlroots wayland

Building

After installing all the required dependencies, grab a copy of the source code from the GitHub repository. This can be done either via git clone or by downloading a tarball from the Releases page:

git clone https://github.com/bsuth/catnip.git

Once you have a copy of the source code, navigate to the catnip directory and run:

make

This should create a build/ directory that contains the executable itself (as well as some other build artifacts).

You can now either mv this executable wherever you like, or to a sane default using:

make install

By default, this will install the executable to /usr/local/bin/catnip.

NOTE: catnip adheres to the GNU convention for installation directories. This means that you may customize the installation directory used in make install via the prefix, exec_prefix, and bindir environment variables:

bindir="~/.local/bin" make install

Running

You can check whether the executable was installed properly by running:

catnip --help

Or, you can run the compositor via:

catnip

For a full list of CLI options, check out the CLI documentation.

CLI

Catnip ships as a standalone executable and does not require any additional files to run other than the dependency shared libraries. The default user config is embedded directly into the executable itself.

The available CLI options are documented below.

-h, --help

Prints the help text and exits.

> catnip --help
usage: catnip [OPTION]
  -c, --config FILE     specify lua config path
  -l, --loglevel LEVEL  specify log level, must be one of:
                        silent, error, warning, info, debug
  -t, --types           print LuaCATS annotations and exit
  -d, --default         print default user config and exit
  -v, --version         print help and exit
  -h, --help            print version and exit

-v, --version

Prints the version and exits.

> catnip --version
0.1.0

-c, --config FILE

Specifies the path to the Lua config file to use. This may be either an absolute path or a path relative to the directory where the catnip executable is run.

> catnip --config ~/dots/catnip/init.lua

If unspecified, catnip will by default check the following paths (in the given order):

  1. $XDG_CONFIG_HOME/catnip/init.lua
  2. ~/.config/catnip/init.lua

-l, --log LEVEL

Specifies the granularity for logging. The granularity from least to most verbose is given as follows:

  1. silent
  2. error
  3. warning
  4. info
  5. debug
> catnip --loglevel info

By default, catnip uses --loglevel error.

-d, --default

Prints the default user config and exits.

> catnip --default
print('LUA DEFAULT CONFIG')
...

User Config

Catnip offers customization via a user config, which is simply a Lua script with access to the catnip Lua API. The user config is run whenever a new Lua state is created, which is done both at startup, as well as every time the compositor is reloaded.

When loading the user config, catnip will search the following paths in order:

  1. The CLI --config path (ex. catnip --config ~/mydotfiles/catnip)
  2. $XDG_CONFIG_HOME/catnip/init.lua
  3. ~/.config/catnip/init.lua

Your First User Config

To create a user config, create a file in any of the search paths listed above. For most users, the following will suffice:

touch ~/.config/catnip/init.lua

After doing this, do not run catnip. At this point, the user config exists and catnip will happily load it. However, since nothing is defined, we won't be able to do anything and essentially be "locked" in the compositor. If this happens, you may need to switch to another TTY and manually kill catnip.

We need to populate the user config with something. A good starting point is to copy the keybind example and at least provide a way to quit or reload catnip:

local keybind = require('keybind') -- copy pasted from examples

keybind.release({ 'mod4', 'ctrl' }, 'r', catnip.reload)
keybind.release({ 'mod4', 'ctrl' }, 'q', catnip.quit)

Now we have something (barely) runnable! As you may have noticed by now, everything in catnip must be defined by the user. This makes the compositor extremely flexible and powerful, but it also means that...

Catnip Sucks (for new users)

Catnip is not a batteries included compositor and requires a nontrivial amount of work before becoming usable as a daily driver. This comes as a consequence of catnip shipping with essentially no default behavior and requiring the user to specify all desired behavior via the relatively low level Lua API.

This large barrier to entry is by far catnip's weakest and most unattractive point, but is hard to address without sacrificing the minimalism, simplicity, and flexibility that catnip strives for. As a (perhaps unsatisfactory) remedy, this documentation page contains many examples, many of which implement common behaviors found in other compositors. Please do not be afraid to copy and paste what you need to build your user config.

Default User Config

Catnip ships with a default user config that is loaded if it fails to find any valid user configs. This may be caused either by not being able to find any of the paths listed above or if Lua threw an error while running the user config.

The default user config is not meant for daily driving and serves primarily as a guaranteed loadable configuration. It focuses on providing enough for the user to be able to debug / fix their configuration and reload the Lua state.

Catnip embeds the default user config directly into the executable to ensure that it is always available to the user. It can be printed to stdout using:

> catnip --default
print('LUA DEFAULT CONFIG')
...

Below is the latest version of the default user config:

print('LUA DEFAULT CONFIG')
print("TEST")

catnip

Catnip exposes its API through a single entrypoint: the catnip module. This module is always preloaded before running the user config (via package.loaded).

local catnip = require('catnip')

Fields

Global Events

Fields

catnip.cursor

The global cursor.

catnip.keyboards

An iterator to traverse through all currently available keyboards.

local catnip = require('catnip')

for keyboard in catnip.keyboards do
    print(keyboard.id)
end

catnip.outputs

An iterator to traverse through all currently available outputs.

local catnip = require('catnip')

for output in catnip.outputs do
    print(output.id)
end

catnip.windows

An iterator to traverse through all currently available windows.

local catnip = require('catnip')

for window in catnip.windows do
    print(window.id)
end

catnip.focused

The currently focused window, or nil if no window is focused.

local catnip = require('catnip')

catnip.subscribe('window::create', function(window)
    catnip.focused = window -- auto focus new windows
end)
local catnip = require('catnip')

local function clear_focus()
    catnip.focused = nil -- unfocus the currently focused window
end

catnip.canvas(options)

---@param options? CatnipCanvasOptions
---@return CatnipCanvas

---@class CatnipCanvasOptions
---The x-coordinate of the canvas (in pixels).
---@field x number?
---The y-coordinate of the canvas (in pixels).
---@field y number?
---The z-index of the canvas (1-indexed).
---@field z number?
---The width of the canvas (in pixels).
---@field width number?
---The height of the canvas (in pixels).
---@field height number?
---Controls whether the canvas should be rendered or not.
---@field visible boolean?

Creates a new canvas.

local catnip = require('catnip')

local canvas = catnip.canvas({
    width = 200,
    height = 200,
})

catnip.subscribe(event, callback)

---@param event string
---@param callback fun(...)
---@return func(...)

Registers a callback to be run whenever event is published.

Returns the callback itself, which may be used to unsubscribe.

local catnip = require('catnip')

local callback = catnip.subscribe('my_event', function()
    print('hello world')
end)

catnip.publish('my_event') -- "hello world"

catnip.unsubscribe('my_event', callback)

catnip.publish('my_event') -- (nothing printed)

Varargs passed to catnip.publish(event, ...) are available as arguments in callback:

local catnip = require('catnip')

catnip.subscribe('my_event', function(name)
    print('hello ' .. name)
end)

catnip.publish('my_event', 'mom') -- "hello mom"

Callbacks cannot be registered more than once. If catnip detects that callback has already been subscribed to event, this is a noop.

catnip.unsubscribe(event, callback)

---@param event string
---@param callback fun(...)

Unregisters a previously subscribed callback.

local catnip = require('catnip')

local callback = catnip.subscribe('my_event', function()
    print('hello world')
end)

catnip.publish('my_event') -- "hello world"

catnip.unsubscribe('my_event', callback)

catnip.publish('my_event') -- (nothing printed)

If the callback was never registered to event, this is a noop.

catnip.publish(event, ...)

---@param event string
---@param ... any

Runs all callbacks registered via catnip.subscribe(event, callback).

local catnip = require('catnip')

local callback = catnip.subscribe('my_event', function()
    print('hello world')
end)

catnip.publish('my_event') -- "hello world"

The provided varargs are passed as arguments to each callback:

local catnip = require('catnip')

catnip.subscribe('my_event', function(name)
    print('hello ' .. name)
end)

catnip.publish('my_event', 'mom') -- "hello mom"

catnip.reload()

Queues the compositor to reload the user config after the current event loop tick has finished executing (i.e. after the next tick event).

Reloading the user config creates a new Lua state. The current Lua state is not closed unless the new Lua state loads successfully. This means that if an error is thrown while reloading the user config, this is essentially a noop.

In order to prevent infinite reloading, catnip does not allow calling this function while loading the user config. Instead, catnip will simply log a warning and do nothing.

catnip.quit()

Queues the compositor to terminate after the current event loop tick has finished executing (i.e. after the next tick event).

Global Events

tick

Published at the end of every event loop cycle. It is particularly useful for integrating other event loops into catnip, such as luv.

local catnip = require('catnip')
local uv = require('luv')

catnip.subscribe('tick', function()
    uv.run('nowait')
end)

reload

Published just before reloading the user config.

local catnip = require('catnip')

catnip.subscribe('reload', function()
    print('reloading user config')
end)

quit

Published just before terminating the compositor.

local catnip = require('catnip')

catnip.subscribe('quit', function()
    print('goodbye world')
end)

cursor

The global cursor. This is automatically registered with all pointer devices (ex. mouse, tablet stylus, etc.) but also controllable programatically.

Under the hood, catnip uses XCursor, which allows compatability with many existing cursor themes. Note that this also means that catnip is compatible with XCursor Environment Variables.

local cursor = require('catnip').cursor

Fields

Fields

cursor.x

---@class CatnipCursor
...
---@field x number
...

The x-coordinate of the cursor (in pixels).

cursor.y

---@class CatnipCursor
...
---@field y number
...

The y-coordinate of the cursor (in pixels).

cursor.name

---@class CatnipCursor
...
---@field name string
...

The XCursor name to use for the cursor.

This value should match a filename in the cursors/ subdirectory of the current cursor theme.

catnip.cursor.name = "pointer"
catnip.cursor.name = "grabbing"
catnip.cursor.name = "not-allowed"
catnip.cursor.name = "default"

cursor.size

---@class CatnipCursor
...
---@field size number
...

The size of the cursor (in pixels).

cursor.theme

---@class CatnipCursor
...
---@field theme string
...

The XCursor theme to use for the cursor.

This value should match the directory name of the cursor theme. Some common paths to store cursor themes include:

  • /usr/share/icons
  • ~/.local/share/icons
  • ~/.icons
catnip.cursor.theme = "adwaita"
catnip.cursor.theme = "bibata"
catnip.cursor.theme = "phinger"
catnip.cursor.theme = "default"

keyboard

A userdata representing a physical keyboard.

Under the hood, catnip uses xkbcommon for keymap support.

local catnip = require('catnip')

for keyboard in catnip.keyboards do
    print(keyboard.id)
end

Fields

Events

Fields

keyboard.id

---@class CatnipKeyboard
...
---@field id number (readonly)
...

The keyboard ID. This is a unique number assigned to each keyboard that persists across reloads.

keyboard.data

---@class CatnipKeyboard
...
---@field data table (readonly)
...

A table for users to attach custom data to the keyboard.

The table contents are never touched by catnip itself and completely defined by the user.

This field is readonly in the sense that users are not allowed to reassign data itself (for example, keyboard.data = 34). This is to ensure consistency in case external libraries, plugins, etc are expecting this field to be a table.

keyboard.name

---@class CatnipKeyboard
...
---@field name string (readonly)
...

The name of the keyboard.

keyboard.xkb_rules

---@class CatnipKeyboard
...
---@field xkb_rules string
...

The xkbcommon rules for the current keymap.

keyboard.xkb_model

---@class CatnipKeyboard
...
---@field xkb_model string
...

The xkbcommon model for the current keymap.

keyboard.xkb_layout

---@class CatnipKeyboard
...
---@field xkb_layout string
...

The xkbcommon layout for the current keymap.

keyboard.xkb_variant

---@class CatnipKeyboard
...
---@field xkb_variant string
...

The xkbcommon variant for the current keymap.

keyboard.xkb_options

---@class CatnipKeyboard
...
---@field xkb_options string
...

The xkbcommon options for the current keymap.

keyboard:subscribe(event, callback)

Similar to catnip.subscribe(event, callback) but for events published on the keyboard itself.

keyboard:unsubscribe(event, callback)

Similar to catnip.unsubscribe(event, callback) but for events published on the keyboard itself.

keyboard:publish(event, ...)

Publishes a keyboard event.

This publishes a local event where callbacks registered via keyboard:subscribe(event, callback) are run.

This also publishes a global event via catnip.publish(event, ...), where the event is prefixed w/ keyboard:: and the keyboard itself is provided as the first argument to the callback, followed by the given varargs.

local catnip = require('catnip')

-- local event
my_keyboard:subscribe('greet', function(name)
    print('hello ' .. name)
end)

-- global event
catnip.subscribe('keyboard::greet', function(keyboard, name)
    print(keyboard.id .. 'says: hello ' .. name)
end)

my_keyboard:publish('greet', 'world')

Events

Keyboard events are published both as local events and global events.

When published as a global event, events are prefixed w/ keyboard:: and the keyboard is passed as the first argument to the callback.

local catnip = require('catnip')

-- local event
my_keyboard:subscribe('destroy', function()
    print('destroy ' .. my_keyboard.id)
end)

-- global event
catnip.subscribe('keyboard::destroy', function(keyboard)
    print('destroy ' .. keyboard.id)
end)

create

Published when a new keyboard has been created (connected). This is also published after (re)loading the user config for any existing keyboards.

destroy

Published when a keyboard has been destroyed (disconnected).

keypress

---@param event CatnipKeyEvent

---@class (exact) CatnipKeyEvent
---(readonly) The [xkbcommon keysym](https://xkbcommon.org/doc/current/group__keysyms.html#ga79e604a22703391bdfe212cfc10ea007).
---@field code number
---(readonly) An [xkbcommon key name](https://xkbcommon.org/doc/current/xkbcommon-keysyms_8h.html) with the `XKB_KEY_` prefix stripped.
---@field name string
---(readonly) A [UTF-8](https://en.wikipedia.org/wiki/UTF-8) string representation of the key.
---@field utf8 string
---(readonly) Whether the shift modifier is active.
---@field shift boolean
---(readonly) Whether the control modifier is active.
---@field ctrl boolean
---(readonly) Whether the mod1 modifier is active.
---@field mod1 boolean
---(readonly) Whether the mod2 modifier is active.
---@field mod2 boolean
---(readonly) Whether the mod3 modifier is active.
---@field mod3 boolean
---(readonly) Whether the mod4 modifier is active.
---@field mod4 boolean
---(readonly) Whether the mod5 modifier is active.
---@field mod5 boolean
---Controls whether to notify the currently focused window about this key event.
---@field prevent_notify boolean

Published whenever a key on the keyboard is pressed.

keyrelease

---@param event CatnipKeyEvent

---@class (exact) CatnipKeyEvent
---(readonly) The [xkbcommon keysym](https://xkbcommon.org/doc/current/group__keysyms.html#ga79e604a22703391bdfe212cfc10ea007).
---@field code number
---(readonly) An [xkbcommon key name](https://xkbcommon.org/doc/current/xkbcommon-keysyms_8h.html) with the `XKB_KEY_` prefix stripped.
---@field name string
---(readonly) A [UTF-8](https://en.wikipedia.org/wiki/UTF-8) string representation of the key.
---@field utf8 string
---(readonly) Whether the shift modifier is active.
---@field shift boolean
---(readonly) Whether the control modifier is active.
---@field ctrl boolean
---(readonly) Whether the mod1 modifier is active.
---@field mod1 boolean
---(readonly) Whether the mod2 modifier is active.
---@field mod2 boolean
---(readonly) Whether the mod3 modifier is active.
---@field mod3 boolean
---(readonly) Whether the mod4 modifier is active.
---@field mod4 boolean
---(readonly) Whether the mod5 modifier is active.
---@field mod5 boolean
---Controls whether to notify the currently focused window about this key event.
---@field prevent_notify boolean

Published whenever a key on the keyboard is released.

output

A userdata representing a physical output (i.e. monitor, screen, etc.).

In catnip, outputs are essentially just viewports into the 2D window space. Any windows or canvases that intersect the bounding box defined by the output's x, y, width, and height determine what is displayed on the output.

local catnip = require('catnip')

for output in catnip.outputs do
    print(output.id)
end

Fields

Events

Fields

output.id

---@class CatnipOutput
...
---@field id number (readonly)
...

The output ID. This is a unique number assigned to each output that persists across reloads.

output.data

---@class CatnipOutput
...
---@field data table (readonly)
...

A table for users to attach custom data to the output.

The table contents are never touched by catnip itself and completely defined by the user.

This field is readonly in the sense that users are not allowed to reassign data itself (for example, output.data = 34). This is to ensure consistency in case external libraries, plugins, etc are expecting this field to be a table.

output.x

---@class CatnipOutput
...
---@field x number
...

The x-coordinate of the output (in pixels).

output.y

---@class CatnipOutput
...
---@field y number
...

The y-coordinate of the output (in pixels).

output.width

---@class CatnipOutput
...
---@field width number
...

The width of the output (in pixels). More specifically, this is the width of the window space viewport that is displayed on the output.

NOTE: Changing this affects output.mode. If the output already advertises a mode that matches the new width, height, and refresh, it is automatically used, otherwise a custom mode is used. Some outputs may not support custom modes.

output.height

---@class CatnipOutput
...
---@field height number
...

The height of the output (in pixels). More specifically, this is the height of the window space viewport that is displayed on the output.

NOTE: Changing this affects output.mode. If the output already advertises a mode that matches the new width, height, and refresh, it is automatically used, otherwise a custom mode is used. Some outputs may not support custom modes.

output.refresh

---@class CatnipOutput
...
---@field refresh number
...

The refresh rate of the output (in mHz). This value may be set to 0 in order for a default refresh rate to be determined automatically.

NOTE: Changing this affects output.mode. If the output already advertises a mode that matches the new width, height, and refresh, it is automatically used, otherwise a custom mode is used. Some outputs may not support custom modes.

output.mode

---@class CatnipOutput
...
---@field mode CatnipOutputMode
...

---@class CatnipOutputMode
---@field width number
---@field height number
---@field refresh number

The current output mode, i.e. a preset of width, height, and refresh, or nil if a custom mode is being used..

When setting this value, one of the userdata from output.modes is expected.

output.modes

An iterator to traverse through all available modes for this output.

local catnip = require('catnip')

for output in catnip.outputs do
    print("Output %d Modes:", output.id)

    for mode in output.modes do
        print("%dx%d@%d", mode.width, mode.height, mode.refresh)
    end
end

When modes are advertised, outputs should be configured using one of these modes over configuring width, height, and refresh manually, as some outputs do not support custom modes.

When no modes are advertised, the output should be configured manually via width, height, and refresh.

output.scale

---@class CatnipOutput
...
---@field scale number
...

The scale factor of the output.

output:subscribe(event, callback)

Similar to catnip.subscribe(event, callback) but for events published on the output itself.

output:unsubscribe(event, callback)

Similar to catnip.unsubscribe(event, callback) but for events published on the output itself.

output:publish(event, ...)

Publishes a output event.

This publishes a local event where callbacks registered via output:subscribe(event, callback) are run.

This also publishes a global event via catnip.publish(event, ...), where the event is prefixed w/ output:: and the output itself is provided as the first argument to the callback, followed by the given varargs.

local catnip = require('catnip')

-- local event
my_output:subscribe('greet', function(name)
    print('hello ' .. name)
end)

-- global event
catnip.subscribe('output::greet', function(output, name)
    print(output.id .. 'says: hello ' .. name)
end)

my_output:publish('greet', 'world')

Events

Output events are published both as local events and global events.

When published as a global event, events are prefixed w/ output:: and the output is passed as the first argument to the callback.

local catnip = require('catnip')

-- local event
my_output:subscribe('destroy', function()
    print('destroy ' .. my_output.id)
end)

-- global event
catnip.subscribe('output::destroy', function(output)
    print('destroy ' .. output.name)
end)

create

Published when a new output has been created (connected). This is also published after (re)loading the user config for any existing outputs.

destroy

Published when an output has been destroyed (disconnected).

window

Windows are clients to the Wayland server that need to render pixels on the screen. They are represented by rectangles in 2D (x, y) space whose contents are determined by the client itself.

local catnip = require('catnip')

for window in catnip.windows do
    print(window.id)
end

Fields

Events

Fields

window.id

---@class CatnipWindow
...
---@field id number (readonly)
...

The window ID. This is a unique number assigned to each window that persists across reloads.

window.data

---@class CatnipWindow
...
---@field data table (readonly)
...

A table for users to attach custom data to the window.

The table contents are never touched by catnip itself and completely defined by the user.

This field is readonly in the sense that users are not allowed to reassign data itself (for example, window.data = 34). This is to ensure consistency in case external libraries, plugins, etc are expecting this field to be a table.

window.x

---@class CatnipWindow
...
---@field x number
...

The x-coordinate of the window (in pixels).

window.y

---@class CatnipWindow
...
---@field y number
...

The y-coordinate of the window (in pixels).

window.z

---@class CatnipWindow
...
---@field z number
...

The z-index of the window (1-indexed).

All windows and canvas' are rendered on top of one another in a stack, and the z-index represents the position in this stack. A higher z-index will make the entry appear above others with a lower z-index. Conversely, a lower z-index will make the entry appear below others with a higher z-index.

When getting this value, the position within the stack is returned, starting from 1.

When setting this value, any arbitrary integer may be used and the z-index will be updated to the closest valid value. For example, setting the z-index to any of -1, 0, or 1 all move the entry to the bottom of the stack, while a value of 999 (most likely) will move the entry to the top of the stack.

-- NOTE: This example assume there are 10 windows / canvas' total.

window.z = 0 -- move to the bottom
print(window.z) -- 1

window.z = 2 -- move to the second position
print(window.z) -- 2

window.z = 999 -- move to the top
print(window.z) -- 10

window.width

---@class CatnipWindow
...
---@field width number
...

The width of the window (in pixels).

window.height

---@class CatnipWindow
...
---@field height number
...

The height of the window (in pixels).

window.visible

---@class CatnipWindow
...
---@field visible boolean
...

Controls whether the window should be rendered or not.

Invisible windows do not receive user input such as mouse / keyboard events.

window.title

---@class CatnipWindow
...
---@field title string (readonly)
...

The title of the window. This is set by the client itself and is often the name of the application.

window:subscribe(event, callback)

Similar to catnip.subscribe(event, callback) but for events published on the window itself.

window:unsubscribe(event, callback)

Similar to catnip.unsubscribe(event, callback) but for events published on the window itself.

window:publish(event, ...)

Publishes a window event.

This publishes a local event where callbacks registered via window:subscribe(event, callback) are run.

This also publishes a global event via catnip.publish(event, ...), where the event is prefixed w/ window:: and the window itself is provided as the first argument to the callback, followed by the given varargs.

local catnip = require('catnip')

-- local event
my_window:subscribe('greet', function(name)
    print('hello ' .. name)
end)

-- global event
catnip.subscribe('window::greet', function(window, name)
    print(window.id .. 'says: hello ' .. name)
end)

my_window:publish('greet', 'world')

window:destroy()

Requests the window to be closed. This will trigger a destroy event just before the window is actually closed.

Events

Window events are published both as local events and global events.

When published as a global event, events are prefixed w/ window:: and the window is passed as the first argument to the callback.

local catnip = require('catnip')

-- local event
my_window:subscribe('destroy', function()
    print('destroy ' .. my_window.id)
end)

-- global event
catnip.subscribe('window::destroy', function(window)
    print('destroy ' .. window.id)
end)

create

Published when a new window has been created. This is also published after (re)loading the user config for any existing windows. at a higher dpi.

destroy

Published when a window has been destroyed (closed).

maximize

Published when a window has requested to be maximized. The definition of "maximized" and how to handle it is left to the user.

fullscreen

Published when a window has requested to be fullscreen. The definition of "fullscreen" and how to handle it is left to the user.

canvas

Canvases are pixel buffers that the user may use to draw arbitrary content onto the screen. Like windows, they are represented by rectangles in the same 2D (x, y) space and also share the same z-index stack.

local catnip = require('catnip')

local canvas = catnip.canvas({
    x = 0,
    y = 0,
    z = 99,
    width = 0,
    height = 0,
    visible = true,
})

Fields

Fields

canvas.x

---@class CatnipCanvas
...
---@field x number
...

The x-coordinate of the canvas (in pixels).

canvas.y

---@class CatnipCanvas
...
---@field y number
...

The y-coordinate of the canvas (in pixels).

canvas.z

---@class CatnipCanvas
...
---@field z number
...

The z-index of the canvas (1-indexed).

All windows and canvas' are rendered on top of one another in a stack, and the z-index represents the position in this stack. A higher z-index will make the entry appear above others with a lower z-index. Conversely, a lower z-index will make the entry appear below others with a higher z-index.

When getting this value, the position within the stack is returned, starting from 1.

When setting this value, any arbitrary integer may be used and the z-index will be updated to the closest valid value. For example, setting the z-index to any of -1, 0, or 1 will move the entry to the bottom of the stack, while a value of 999 (most likely) will move the entry to the top of the stack.

-- NOTE: This example assume there are 10 windows / canvas' total.

canvas.z = 0 -- move to the bottom
print(canvas.z) -- 1

canvas.z = 2 -- move to the second position
print(canvas.z) -- 2

canvas.z = 999 -- move to the top
print(canvas.z) -- 10

canvas.width

---@class CatnipCanvas
...
---@field width number
...

The width of the canvas (in pixels).

canvas.height

---@class CatnipCanvas
...
---@field height number
...

The height of the canvas (in pixels).

canvas.visible

---@class CatnipCanvas
...
---@field visible boolean
...

Controls whether the canvas should be rendered or not.

canvas:path(path)

---@param path CatnipCanvasPath

---@alias CatnipCanvasPath CatnipCanvasPathFields | CatnipCanvasPathCommand[]

---@class CatnipCanvasPathFields
---The starting x-coordinate of the path (in pixels) relative to the top left corner of the canvas.
---@field x number?
---The starting y-coordinate of the path (in pixels) relative to the top left corner of the canvas.
---@field y number?
---Whether to close the path, i.e. connect back to the starting point.
---This is not the same as simply adding a line back to the starting point, since such a line will still have line caps.
---@field close boolean?
---The color of the path fill as a hexadecimal number.
---@field fill_color number?
---The opacity of the path fill as a number between 0-1 (inclusive).
---@field fill_opacity number?
---The color of the path stroke as a hexadecimal number.
---@field stroke_color number?
---The opacity of the path stroke as a number between 0-1 (inclusive).
---@field stroke_opacity number?
---The thickness of the path stroke (in pixels).
---@field stroke_size number?
---The caps to use for the start / end of the path stroke.
---Only applies to open (not closed) paths.
---butt = no cap applied (default)
---round = circle cap applied, whose center lies at the start / end points
---square = square cap applied, whose center lies at the start / end points
---@field stroke_cap 'butt' | 'round' | 'square'?

---[2] = the relative x-coordinate of the endpoint
---[3] = the relative y-coordinate of the endpoint
---@alias CatnipCanvasPathLine { [1]: 'line', [2]: number, [3]: number }
---[2] = the relative x-coordinate of the center of the circle
---[3] = the relative y-coordinate of the center of the circle
---[4] = the rotation amount (in radians), may be either positive or negative to indicate direction
---@alias CatnipCanvasPathArc { [1]: 'arc', [2]: number, [3]: number, [4]: number }
---[2] = the relative x-coordinate of the first control point
---[3] = the relative y-coordinate of the first control point
---[4] = the relative x-coordinate of the second control point
---[5] = the relative y-coordinate of the second control point
---[6] = the relative x-coordinate of the endpoint
---[7] = the relative y-coordinate of the endpoint
---@alias CatnipCanvasPathBezier { [1]: 'bezier', [2]: number, [3]: number, [4]: number, [5]: number, [6]: number, [7]: number }
---@alias CatnipCanvasPathCommand CatnipCanvasPathLine | CatnipCanvasPathArc | CatnipCanvasPathBezier

Renders a path onto the canvas.

One way to think of a path is a shape that is drawable using pen and paper without lifting the pen from surface of the paper. In catnip, paths are constructed using lines, arcs, and bezier curves.

The "fill" of the path always considers the enclosure created as if the path had been closed, regardless of whether it has actually been closed.

local catnip = require('catnip')

local canvas = catnip.canvas({ ... })

-- Draw a rectangle with x = 10, y = 20, width = 30, height = 40
canvas:path({
    x = 10,
    y = 20,
    { 'line', 30, 0 },  -- right 30px
    { 'line', 0, 40 },  -- down  40px
    { 'line', -30, 0 }, -- left  30px
    { 'line', 0, -40 }, -- up    40px
})

-- Draw the upper half of a semicircle with center x = 10, y = 20 and radius = 30
canvas:path({
    x = 10,
    y = 20,
    { 'line', -30, 0 },        -- left 30px
    { 'arc', 30, 0, math.PI }, -- semicircle
    close = true,              -- close the path
})

canvas:rectangle(rectangle)

---@param rectangle CatnipCanvasRectangle

---@class CatnipCanvasRectangle
---The x-coordinate of the rectangle (in pixels) relative to the top left corner of the canvas.
---@field x number?
---The y-coordinate of the rectangle (in pixels) relative to the top left corner of the canvas.
---@field y number?
---The width of the rectangle (in pixels).
---@field width number?
---The height of the rectangle (in pixels).
---@field height number?
---The default radius for all corners (in pixels).
---This is used to control how "rounded" each corner is.
---@field radius number?
---The radius of the top left corner (in pixels).
---@field radius_top_left number?
---The radius of the top right corner (in pixels).
---@field radius_top_right number?
---The radius of the bottom right corner (in pixels).
---@field radius_bottom_right number?
---The radius of the bottom left corner (in pixels).
---@field radius_bottom_left number?
---The color of the rectangle fill as a hexadecimal number.
---@field fill_color number?
---The opacity of the rectangle fill as a number between 0-1 (inclusive).
---@field fill_opacity number?
---The color of the rectangle stroke as a hexadecimal number.
---@field stroke_color number?
---The opacity of the rectangle stroke as a number between 0-1 (inclusive).
---@field stroke_opacity number?
---The thickness of the rectangle stroke (in pixels).
---@field stroke_size number?

Renders a rectangle onto the canvas.

Specifying any of rectangle.radius_xxx_yyy (ex. rectangle.radius_top_left) will take priority over rectangle.radius for that corner.

The center of the stroke always matches the bounds of the rectangle, i.e. half of the stroke will lie "inside" the rectangle and half will lie "outside".

canvas:text(text, options)

---@param text string
---@param options CatnipCanvasTextOptions

---@class CatnipCanvasTextOptions
---The x-coordinate of the text (in pixels) relative to the top left corner of the canvas.
---@field x number?
---The y-coordinate of the text (in pixels) relative to the top left corner of the canvas.
---@field y number?
---The width of the bounding box (in pixels).
---@field width number?
---The height of the bounding box (in pixels).
---@field height number?
---The horizontal alignment of the text, relative to the bounding box.
---@field align ('left' | 'center' | 'right')?
---The vertical alignment of the text, relative to the bounding box.
---@field valign ('left' | 'center' | 'right')?
---Controls whether and where to truncate text and apply ellipsis in the case of overflow of the bounding box.
---@field ellipsis (boolean | 'start' | 'middle' | 'end')?
---Controls whether and how to wrap text in the case of overflow on the main axis (usually the horizontal axis).
---char = allows text to wrap at any character, including in the middle of words
---word = allows text to wrap only at word boundaries
---auto = wraps text at word boundaries when possible, otherwise breaks words when necessary
---@field wrap (boolean | 'char' | 'word' | 'auto')?
---The font family.
---A comma separated list of font families may be used to specify fallbacks in case font families are missing.
---@field font string?
---The size of the font (in pixels).
---@field size number?
---The weight, i.e. "thickness" of the font as a number between 100-1000 (inclusive).
---The default weight is 400.
---@field weight number?
---Controls whether the font should be rendered in italics.
---@field italic boolean?
---The color of the text as a hexadecimal number.
---@field color number?
---The opacity of the text as a number between 0-1 (inclusive).
---@field opacity number?
---Whether the text is visible. Defaults to `true`.
---Especially useful when trying to get text width / height without wanting to draw anything.
---@field visible boolean?

Renders text onto the canvas.

options.font should be the name of a font family as a string. To list available font families, one can use:

fc-list --format="%{family[0]}\n" | sort | uniq

options.width and options.height are used to determine the bounding box of the text, which influences the following:

  • If options.align or options.valign are given, this determines the bounding box the text should be centered in.
  • If options.wrap is given, this determines where the text should start wrapping.
  • If options.ellipsis is given, this determines when the text should be considered "overflowing" and thus render ellipsis.

canvas:png(path, options)

---@param path string
---@param options CatnipCanvasPngOptions

---@class CatnipCanvasPngOptions
---The x-coordinate of the PNG (in pixels) relative to the top left corner of the canvas.
---@field x number?
---The y-coordinate of the PNG (in pixels) relative to the top left corner of the canvas.
---@field y number?
---The width to render the PNG (in pixels).
---If left undefined, this is automatically scaled to preserve the aspect ratio.
---@field width number?
---The height to render the PNG (in pixels).
---If left undefined, this is automatically scaled to preserve the aspect ratio.
---@field height number?
---Controls whether the PNG should be cached for the next canvas render cycle.
---@field cache boolean?

Renders a PNG image onto the canvas.

path should be a file path to the PNG. This may either be an absolute path or a path relative to the parent directory of the user config.

If only one of options.width or options.height is given, the other will be scaled automatically to preserve the aspect ratio.

local catnip = require('catnip')
local canvas = catnip.canvas({ ... })

canvas:png('wallpaper.png', {
    x = 0,
    y = 0,
    width = 1920,
    height = 1080,
})

By default, each canvas will cache the PNG until the canvas goes through a "render cycle" (i.e. canvas:clear() + drawing + canvas:clear()) without drawing the PNG. In other words, the PNG will be cached as long as it was "used last time". This gives us decent performance when a canvas needs to be constantly rerendered with a PNG, while still being lenient enough to make sure memory usage stays low and PNGs are not stale (in case they are updated while the compositor is running).

Users can opt out of caching by setting options.cache = false:

local catnip = require('catnip')
local canvas = catnip.canvas({ ... })

canvas:png('wallpaper.png', {
    cache = false,
})

canvas:svg(document, options)

---@param document string
---@param options CatnipCanvasSvgOptions

---@class CatnipCanvasSvgOptions
---The x-coordinate of the SVG (in pixels) relative to the top left corner of the canvas.
---@field x number?
---The y-coordinate of the SVG (in pixels) relative to the top left corner of the canvas.
---@field y number?
---The width to render the SVG (in pixels).
---If left undefined, this is automatically scaled to preserve the aspect ratio.
---@field width number?
---The height to render the SVG (in pixels).
---If left undefined, this is automatically scaled to preserve the aspect ratio.
---@field height number?
---Controls whether the SVG should be cached for the next canvas render cycle.
---@field cache boolean?
---A [CSS stylesheet](https://gnome.pages.gitlab.gnome.org/librsvg/devel-docs/features.html#css-properties) to apply to the SVG.
---@field styles string?

Renders an SVG onto the canvas. Underneath the hood, catnip uses librsvg for SVG support. See here for a full list of supported SVG and CSS features.

document may be either an SVG document itself, or a path to an SVG file. When using a file path, it may either be an absolute path or a path relative to the parent directory of the user config.

If only one of options.width or options.height is given, the other will be scaled automatically to preserve the aspect ratio.

local catnip = require('catnip')
local canvas = catnip.canvas({ ... })

-- Rendering an SVG file
canvas:svg('wallpaper.svg', {
    x = 0,
    y = 0,
    width = 1920,
    height = 1080,
})

-- Rendering an SVG document directly
canvas:svg(
    [[
        <svg width="100" height="100" xmlns="http://www.w3.org/2000/svg">
            <circle cx="50" cy="50" r="40" stroke="green" stroke-width="4" fill="yellow" />
        </svg>
    ]],
    {
        x = 0,
        y = 0,
        width = 1920,
        height = 1080,
    }
)

options.styles may be used to apply an CSS stylesheet to the SVG. See here for a full list of supported CSS properties.

local catnip = require('catnip')
local canvas = catnip.canvas({ ... })

canvas:svg(
    [[
        <svg width="100" height="100" xmlns="http://www.w3.org/2000/svg">
            <circle cx="50" cy="50" r="40" stroke-width="4" fill="yellow" />
        </svg>
    ]],
    { styles = 'circle { stroke: green }' }
)

By default, each canvas will cache the SVG until the canvas goes through a "render cycle" (i.e. canvas:clear() + drawing + canvas:clear()) without drawing the SVG. In other words, the SVG will be cached as long as it was "used last time". This gives us decent performance when a canvas needs to be constantly rerendered with a SVG, while still being lenient enough to make sure memory usage stays low and SVGs are not stale (in case they are updated while the compositor is running).

Users can opt out of caching by setting options.cache = false:

NOTE: Unlike PNGs, SVGs are cached based on both the document and any provided styles (via options.styles). This is due to a limitation on how librsvg applies styles.

local catnip = require('catnip')
local canvas = catnip.canvas({ ... })

canvas:svg('wallpaper.svg', {
    cache = false,
})

canvas:clear()

Clears the canvas, removing anything previously drawn.

canvas:destroy()

Destroys the canvas immediately. After this is called, the canvas will no longer be visible nor useable.

This is automatically called when the canvas is garbage collected.

Examples

keybind.lua

Allows specifying a callback to be run when certain key combinations are either pressed or released.

keybind.lua

Allows specifying a callback to be run when certain key combinations are either pressed or released.

local catnip = require('catnip')

local M = {}

local key_press_callbacks = {} ---@type table<string, fun()[]>
local key_release_callbacks = {} ---@type table<string, fun()[]>

-- -----------------------------------------------------------------------------
-- Types
-- -----------------------------------------------------------------------------

---@alias Modifier "ctrl" | "mod1" | "mod2" | "mod3" | "mod4" | "mod5"

-- -----------------------------------------------------------------------------
-- Helpers
-- -----------------------------------------------------------------------------

---@param modifiers Modifier[]
---@param key string
---@return string
local function get_key_binding_code(modifiers, key)
  local code = { 0, 0, 0, 0, 0, 0, key }

  for _, modifier in ipairs(modifiers) do
    if modifier == 'ctrl' then
      code[1] = 1
    elseif modifier == 'mod1' then
      code[2] = 1
    elseif modifier == 'mod2' then
      code[3] = 1
    elseif modifier == 'mod3' then
      code[4] = 1
    elseif modifier == 'mod4' then
      code[5] = 1
    elseif modifier == 'mod5' then
      code[6] = 1
    end
  end

  return table.concat(code)
end

---@param event CatnipKeyEvent
---@return string
local function get_key_event_code(event)
  local is_printable_ascii = 32 < event.code and event.code < 127

  return table.concat({
    event.ctrl and 1 or 0,
    event.mod1 and 1 or 0,
    event.mod2 and 1 or 0,
    event.mod3 and 1 or 0,
    event.mod4 and 1 or 0,
    event.mod5 and 1 or 0,
    is_printable_ascii and string.char(event.code) or event.name,
  })
end

-- -----------------------------------------------------------------------------
-- API
-- -----------------------------------------------------------------------------

---@param modifiers Modifier[]
---@param key string
---@param callback fun()
function M.press(modifiers, key, callback)
  local code = get_key_binding_code(modifiers, key)
  key_press_callbacks[code] = key_press_callbacks[code] or {}
  table.insert(key_press_callbacks[code], callback)
end

---@param modifiers Modifier[]
---@param key string
---@param callback fun()
function M.release(modifiers, key, callback)
  local code = get_key_binding_code(modifiers, key)
  key_release_callbacks[code] = key_release_callbacks[code] or {}
  table.insert(key_release_callbacks[code], callback)
end

-- -----------------------------------------------------------------------------
-- Subscriptions
-- -----------------------------------------------------------------------------

catnip.subscribe('keyboard::keypress', function(_, event)
  local code = get_key_event_code(event)

  event.prevent_notify = key_press_callbacks[code] ~= nil or key_release_callbacks[code] ~= nil

  if key_press_callbacks[code] ~= nil then
    for _, callback in ipairs(key_press_callbacks[code]) do
      callback()
    end
  end
end)

catnip.subscribe('keyboard::keyrelease', function(_, event)
  local code = get_key_event_code(event)

  event.prevent_notify = key_press_callbacks[code] ~= nil or key_release_callbacks[code] ~= nil

  if key_release_callbacks[code] ~= nil then
    for _, callback in ipairs(key_release_callbacks[code]) do
      callback()
    end
  end
end)

-- -----------------------------------------------------------------------------
-- Return
-- -----------------------------------------------------------------------------

return M