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.

cmedia_player.c
#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,
};
the SDK functions (llz_media_*, llz_display_*, llz_input_*) are wrappers around redis and raylib. you can call them from any plugin without worrying about the underlying implementation.

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

text
salamanders/
├── media_player/
│   ├── media_player.c
│   ├── Makefile
│   └── assets/
│       └── icons/
├── spotify_clone/
│   ├── spotify_clone.c
│   ├── Makefile
│   └── ui/
│       └── components.c
└── snake/
    ├── snake.c
    ├── game_logic.c
    └── Makefile

build with makefile

makefileMakefile
# 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 install

compile and install

bash
# Build the plugin
make

# Install to salamanders directory
sudo make install

# Restart llizard to load the plugin
sv restart llizard
for cross-compiling to the car thing, use the llizardOS docker toolchain with CC=arm-linux-musleabihf-gcc and link against ARMv7 libraries.

available 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.

view salamanders repobrowse salamander catalog