Salamander
"they live where others cannot"
salamanders are plugins for llizard. named after the amphibians that thrive in extreme environments, salamander plugins bring functionality to the car thing. media players, games, utilities, anything you can imagine. simple C API, dynamic loading, infinite possibilities.
philosophy
salamanders are meant to be small, focused, and composable. each plugin does one thing well. want a media player? write a salamander. want a game? write a salamander. want to display weather? write a salamander.
the plugin API is intentionally minimal. you get access to display, input, and data (via redis). everything else is up to you. this keeps plugins portable and the API stable.
plugin API
every salamander must implement the LlzPluginAPI struct and export it as a symbol. llizard loads the .so file via dlopen and calls your functions.
#include <llz_sdk/plugin.h>
#include <llz_sdk/display.h>
#include <llz_sdk/input.h>
#include <llz_sdk/media.h>
// Plugin state
typedef struct {
char current_track[256];
bool is_playing;
int scroll_offset;
} MediaPlayerState;
// Initialize plugin
int plugin_init(void **state) {
MediaPlayerState *s = malloc(sizeof(MediaPlayerState));
if (!s) return -1;
memset(s, 0, sizeof(MediaPlayerState));
*state = s;
return 0;
}
// Update logic (called every frame)
void plugin_update(void *state, float delta_time) {
MediaPlayerState *s = (MediaPlayerState *)state;
// Read media data from redis via SDK
llz_media_get_title(s->current_track, sizeof(s->current_track));
s->is_playing = llz_media_is_playing();
// Handle input
if (llz_input_button_pressed(LLZ_BUTTON_KNOB)) {
// Toggle playback
}
int knob_delta = llz_input_knob_rotation();
s->scroll_offset += knob_delta;
}
// Render (called every frame after update)
void plugin_render(void *state) {
MediaPlayerState *s = (MediaPlayerState *)state;
// Get display dimensions
int width = llz_display_width();
int height = llz_display_height();
// Clear background
llz_display_clear(0x1a1a1a);
// Draw track title
llz_display_text(
s->current_track,
20, 50,
24, 0xf5f5dc // bone color
);
// Draw playback indicator
if (s->is_playing) {
llz_display_circle(width - 30, 30, 8, 0x00ff00);
}
}
// Cleanup
void plugin_destroy(void *state) {
if (state) free(state);
}
// Export the API
LLZ_PLUGIN_EXPORT LlzPluginAPI llz_plugin = {
.name = "media_player",
.version = "1.0.0",
.init = plugin_init,
.update = plugin_update,
.render = plugin_render,
.destroy = plugin_destroy,
};plugin lifecycle
┌──────────────────────────────────────────────┐
│ Llizard Startup │
└────────────────┬─────────────────────────────┘
│
│ scan /usr/local/lib/salamanders/*.so
│
▼
┌──────────────────────────────────────────────┐
│ For each plugin .so file: │
│ 1. dlopen("plugin.so", RTLD_LAZY) │
│ 2. dlsym(handle, "llz_plugin") │
│ 3. Check API version compatibility │
│ 4. Call plugin->init(&state) │
└────────────────┬─────────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ Main Game Loop (60 FPS) │
│ │
│ while (running) { │
│ // Input │
│ process_input(); │
│ │
│ // Update all plugins │
│ for each plugin { │
│ plugin->update(state, delta_time); │
│ } │
│ │
│ // Render all plugins │
│ BeginDrawing(); │
│ for each plugin { │
│ plugin->render(state); │
│ } │
│ EndDrawing(); │
│ } │
└────────────────┬─────────────────────────────┘
│
│ shutdown signal received
│
▼
┌──────────────────────────────────────────────┐
│ For each loaded plugin: │
│ 1. Call plugin->destroy(state) │
│ 2. dlclose(handle) │
└──────────────────────────────────────────────┘building a plugin
salamanders are compiled as shared libraries (.so files) and placed in the salamanders directory.
directory structure
salamanders/
├── media_player/
│ ├── media_player.c
│ ├── Makefile
│ └── assets/
│ └── icons/
├── spotify_clone/
│ ├── spotify_clone.c
│ ├── Makefile
│ └── ui/
│ └── components.c
└── snake/
├── snake.c
├── game_logic.c
└── Makefilebuild with makefile
# Makefile for a salamander plugin
CC = gcc
CFLAGS = -Wall -Wextra -fPIC -I/usr/local/include
LDFLAGS = -shared -L/usr/local/lib -lllz_sdk -lraylib -lhiredis
TARGET = media_player.so
SOURCES = media_player.c
OBJECTS = $(SOURCES:.c=.o)
all: $(TARGET)
$(TARGET): $(OBJECTS)
$(CC) $(LDFLAGS) -o $@ $^
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
clean:
rm -f $(OBJECTS) $(TARGET)
install: $(TARGET)
cp $(TARGET) /usr/local/lib/salamanders/
.PHONY: all clean installcompile and install
# Build the plugin
make
# Install to salamanders directory
sudo make install
# Restart llizard to load the plugin
sv restart llizardavailable SDK modules
llz_sdk_display
- • llz_display_width() / height()
- • llz_display_clear(color)
- • llz_display_text(text, x, y, size, color)
- • llz_display_rect(x, y, w, h, color)
- • llz_display_circle(x, y, radius, color)
- • llz_display_image(texture, x, y)
llz_sdk_input
- • llz_input_knob_rotation()
- • llz_input_button_pressed(btn)
- • llz_input_button_released(btn)
- • llz_input_button_held(btn)
- • llz_input_touch_position(x, y)
llz_sdk_media
- • llz_media_get_title(buf, len)
- • llz_media_get_artist(buf, len)
- • llz_media_get_album(buf, len)
- • llz_media_is_playing()
- • llz_media_get_position()
- • llz_media_get_albumart(image*)
- • llz_media_get_lyrics(buf, len)
llz_sdk_config
- • llz_config_get(key, buf, len)
- • llz_config_set(key, value)
- • llz_config_get_int(key)
- • llz_config_set_int(key, value)
example salamanders
media_player.so
minimal now-playing display. shows track title, artist, album art. single-screen, no navigation. perfect for a quick glance.
spotify_clone.so
full spotify-style UI with scrollable playlists, album view, lyrics sync. uses the knob for scrolling, buttons for play/pause and navigation.
snake.so
classic snake game. rotate the knob to change direction, press knob to start/pause. high score saved via llz_config.
weather.so
local weather display. fetches data from openweathermap API, caches in redis, displays current conditions and 5-day forecast.