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):
$XDG_CONFIG_HOME/catnip/init.lua
~/.config/catnip/init.lua
-l, --log LEVEL
Specifies the granularity for logging. The granularity from least to most verbose is given as follows:
silent
error
warning
info
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:
- The CLI
--config
path (ex.catnip --config ~/mydotfiles/catnip
) $XDG_CONFIG_HOME/catnip/init.lua
~/.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
catnip.cursor
catnip.keyboards
catnip.outputs
catnip.windows
catnip.focused
catnip.canvas(options)
catnip.subscribe(event, callback)
catnip.unsubscribe(event, callback)
catnip.publish(event, ...)
catnip.reload()
catnip.quit()
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
keyboard.id
keyboard.data
keyboard.name
keyboard.xkb_rules
keyboard.xkb_model
keyboard.xkb_layout
keyboard.xkb_variant
keyboard.xkb_options
keyboard:subscribe(event, callback)
keyboard:unsubscribe(event, callback)
keyboard:publish(event, ...)
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
output.id
output.data
output.x
output.y
output.width
output.height
output.refresh
output.mode
output.modes
output.scale
output:subscribe(event, callback)
output:unsubscribe(event, callback)
output:publish(event, ...)
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
window.id
window.data
window.x
window.y
window.z
window.width
window.height
window.visible
window.title
window:subscribe(event, callback)
window:unsubscribe(event, callback)
window:publish(event, ...)
window:destroy()
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
canvas.x
canvas.y
canvas.z
canvas.width
canvas.height
canvas.visible
canvas:path(path)
canvas:rectangle(rectangle)
canvas:text(text, options)
canvas:svg(document, options)
canvas:png(path, options)
canvas:clear()
canvas:destroy()
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
oroptions.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