Skip to content

Object: profile

Unlike most of the other object types which are created by and retrieved from the compositor itself, the profile object is defined by the author of a profile script. It contains a mix of fields and methods, some of which are accessed and used by the compositor. A profile object defines the behavior of a particular profile or project, including which processes should be running, how outputs and views are arranged, and any other custom behaviors a project requires.

You can think of profile methods as user-defined callback functions that are invoked whenever certain events occur. The first argument (self) of each of these functions will always contain the profile object itself.

Registering profiles

This global profile function is used to register a profile, and is the main entry-point for almost all scripts:

lua
profile(profile_object)
profile(profile_object)

It takes a single argument in the form of a Lua table containing the profile definition, including any functions or fields that the profile defines.

Example: Calling profile with minimum possible profile

lua
profile {
    name = "My Project"
}
profile {
    name = "My Project"
}

Note that the function call parentheses are omitted here by convention, to make a profile script look more like a configuration file. In Lua, parentheses are optional when calling a function that takes a single table argument.

Field: name

Fieldname
Typestring
RequiredYes

A unique string identifier for the profile being defined.

Field: title

Fieldtitle
Typestring or nil
RequiredNo

A user-friendly display name for the profile, shown in UIs such as the web console. If this field is omitted, the name is used instead.

Field: layouts

Fieldlayouts
Typearray of layout definitions
RequiredNo

Define one or more custom layouts for the profile. Each entry in the array must be a table with the following fields:

FieldTypeDefault
namestringN/A (required)
titlestring or nilSame as name.

This field can be used as a simpler alternative to defining a compute_layouts method when no custom logic is required to produce the list of layouts. If neither this field nor a compute_layouts method is defined, then the profile will register a single layout with the name default and the title Default.

Example: A basic profile that defines two available layouts

lua
profile {
    name = "My Project",
    layouts = {
        { name = "clone", title = "Clone All Displays" },
        { name = "panorama", title = "Panoramic View" }
    }
}
profile {
    name = "My Project",
    layouts = {
        { name = "clone", title = "Clone All Displays" },
        { name = "panorama", title = "Panoramic View" }
    }
}

Field: processes

Fieldprocesses
Typetable
DefaultAn empty table: no processes.

A table of named process definitions, indicating which programs should be started when this profile is activated. This field will be ignored if the refresh_processes method is defined. Each named process is assigned a process definition, which is simply a list of arguments as follows:

ArgumentValueTypeDefault
1Executable path(s)string OR array of stringsN/A (required)
2Executable argumentsarray of stringsempty list
3Environment variablesarray of strings in the format VARIABLE=VALUEempty list
4Special flagstable of key/boolean pairsempty list

Processes defined in this field will be launched only if the output layout is non-empty (i.e., there are any virtual or physical outputs mapped to the layout). To override this behavior and launch processes even if there are no outputs, set the profile's headless field or set the headless flag to true in the process definition flags.

INFO

For certain common types of programs, helper functions are provided that will create the necessary arguments and environment variables for those programs to work well with isotope. See the Process Helpers for details.

Example: A basic profile that launches a single process

lua
profile {
    name = "My Project",
    processes = {
        welcome = { "/usr/bin/isotope-welcome" }
    }
}
profile {
    name = "My Project",
    processes = {
        welcome = { "/usr/bin/isotope-welcome" }
    }
}

Example: Two processes, one with extra options

lua
profile {
    name = "My Project",
    processes = {
        myprogram1 = {
            "/path/to/my/program1",
            { "--arg", "--arg2" },
            { "MY_ENV_VAR=SOME_VALUE" }
        },
        myprogram2 = {
            {
                "/path/to/my/program2",
                "/alternate/path/to/my/program2"
            }
        }
    }
}
profile {
    name = "My Project",
    processes = {
        myprogram1 = {
            "/path/to/my/program1",
            { "--arg", "--arg2" },
            { "MY_ENV_VAR=SOME_VALUE" }
        },
        myprogram2 = {
            {
                "/path/to/my/program2",
                "/alternate/path/to/my/program2"
            }
        }
    }
}

This example launches two programs, the first of which is passed some command-line arguments and a custom environment variable. The second example supplies multiple paths instead of a single path: the compositor will search for the first path that it is able to execute. Supplying multiple paths can sometimes be useful when sharing a profile script between deployment environments.

Note that each program has a unique name. This name may be referenced in profile methods to help determine how each program's views should be handled.

Field: headless

Fieldheadless
Typeboolean
DefaultFalse

When using the processes field to define processes, an empty layout (i.e., no outputs mapped) will suppress process launching since most content-based programs will fail or crash if there are no outputs. Sometimes it's desirable to run processes anyway, such as those that run in the background and create no surfaces. Setting this field to true overrides the default behavior.

If you want to only have some processes launch with no output layout, you can instead set the headless flag to true in the processes definition.

Custom fields

Other fields may be defined as required by your profile. This allows profiles to maintain an internal state where needed. The fields may use any legal identifier in Lua so long as they do not clash with other fields used by the compositor. Custom fields may be of any Lua type, including tables and functions. Custom fields may be accessed using the self argument that is passed in to all profile methods.

Example: Defining a custom field and using it in a function

lua
profile {
    name = "My Project",

    my_custom_field = 42,

    start = function(self)
        log.info("My custom field is: " .. self.my_custom_field);
    end
}
profile {
    name = "My Project",

    my_custom_field = 42,

    start = function(self)
        log.info("My custom field is: " .. self.my_custom_field);
    end
}

The following methods may optionally be defined on a profile object. When an optional method is not defined, a basic default behavior is implemented as noted for each method. For all of these methods, the profile object itself is passed as the self parameter, allowing the function implementation to access profile fields.

Method: start

Methodstart(self)
selfThe profile object.
ReturnsNothing

This method is called when a profile is first activated, or when the profile script defining the profile has been reloaded. It is useful for performing certain actions whenever a profile has started such as logging some information or starting a media player.

Example: write to log on profile start

lua
profile {
    -- <other profile fields here>

    start = function(self)
        log.info("My profile has started!");
    end
}
profile {
    -- <other profile fields here>

    start = function(self)
        log.info("My profile has started!");
    end
}

This method is called when:

  • The profile is activated or reloaded.

Method: stop

Methodstop(self)
selfThe profile object.
ReturnsNothing

This method is called when a profile is de-activated. It can be used for logging or to clean up any resources that are not managed by the compositor.

Example: write to log on profile stop

lua
profile {
    -- <other profile fields here>

    stop = function(self)
        log.info("My profile has stopped!");
    end
}
profile {
    -- <other profile fields here>

    stop = function(self)
        log.info("My profile has stopped!");
    end
}

This method is called when:

  • The compositor switches to another profile.
  • The profile is about to be reloaded.
  • The compositor is shutting down.

Method: compute_layouts

Methodcompute_layouts(self)
selfThe profile object.
ReturnsNothing

This method is called whenever the compositor needs to know the set of available layouts that a profile supports. Since the set of supported layouts may depend on which output(s) are connected, this method is called each time an output is added or removed. An implementation should call the server:register_layout() method for each supported layout, after optionally inspecting the connected outputs or other system information.

If this method is not defined, a default implementation will look for the layouts field and register each layout defined there. If neither the method nor the field is defined, the compositor registers a single layout with a name of default and a title of Default. Registering custom layouts is an optional feature that may be skipped by any profiles that do not require that functionality. A common use for multiple layouts is to define configurations that can be used with multiple physical setups differ from each other, or to create multiple operational modes in which content can be displayed differently.

Example: A simple default implementation

lua
profile {
    -- <other profile fields here>

    compute_layouts = function(self)
        server:register_layout("default", "Default");
    end
}
profile {
    -- <other profile fields here>

    compute_layouts = function(self)
        server:register_layout("default", "Default");
    end
}

Example: A slightly more complex implementation that inspects the number of outputs

lua
profile {
    -- <other profile fields here>

    compute_layouts = function(self)
        local num_outputs = #server:outputs();
        if num_outputs > 1 then
            server:register_layout("clone", "Cloned Displays");
            server:register_layout("panorama", "Panoramic Displays");
        else
            server:register_layout("single", "Single Display");
        end
    end
}
profile {
    -- <other profile fields here>

    compute_layouts = function(self)
        local num_outputs = #server:outputs();
        if num_outputs > 1 then
            server:register_layout("clone", "Cloned Displays");
            server:register_layout("panorama", "Panoramic Displays");
        else
            server:register_layout("single", "Single Display");
        end
    end
}

Layout names have no special meaning to the compositor. Registered layouts are simply presented in UIs such as the web console, allowing users to select from the available layouts without the need to build a custom UI. In this way, layouts allow for the definition of multiple sets of behaviors in a single profile. The currently-selected (or active) layout may be used by other profile functions such as arrange_outputs() in order to select the appropriate logic for that layout.

This method is called when:

  • The profile is activated or reloaded.
  • An output is connected or disconnected.

Method: arrange_outputs

Methodarrange_outputs(self)
selfThe profile object.
ReturnsNothing

This method is responsible for arranging connected outputs in logical layout space. The position, scale, and rotation of each output is decided here, using whatever combination of factors is required. An implementation may take into account the currently active layout (as defined in compute_layouts and selected by the user), the number of connected outputs and their resolutions, and other factors as desired.

When not implemented by a profile, the default implementation will simply stack all outputs in a row horizontally without scaling or rotation. The top of each display will be at the same vertical position in the layout.

Example: Simple default horizontal stacking

In this configuration, all outputs will be placed with their upper-left corner at 0,0 in logical layout space. Assuming all outputs are the same resolution, the content on each will be identical, cloning the displays.

lua
profile {
    -- <other profile fields here>

    arrange_outputs = function(self)
        local x = 0;
        for _, output in ipairs(server:outputs()) do
            local width, _ = output:size();
            output:place(x, 0);
            x = x + width;
        end
    end
}
profile {
    -- <other profile fields here>

    arrange_outputs = function(self)
        local x = 0;
        for _, output in ipairs(server:outputs()) do
            local width, _ = output:size();
            output:place(x, 0);
            x = x + width;
        end
    end
}

The default behavior creates a horizontal stack or panorama of all the outputs.

Example: Simple duplication of all outputs with no scaling

In this configuration, all outputs will be placed with their upper-left corner at 0,0 in logical layout space. Assuming all outputs are the same resolution, the content on each will be identical, cloning the displays.

lua
profile {
    -- <other profile fields here>

    arrange_outputs = function(self)
        for _, output in ipairs(server:outputs()) do
            output:place(0, 0);
        end
    end
}
profile {
    -- <other profile fields here>

    arrange_outputs = function(self)
        for _, output in ipairs(server:outputs()) do
            output:place(0, 0);
        end
    end
}

Example: Simple conditional output layout (single vs. cloned vs. panoramic)

This illustrates using the active layout to modify the output configuration. Of special note, when the single layout is active, the implementation removes all outputs except the first one from the layout.

A real implementation might take into account the names of the outputs to ensure that they are placed in the desired order, and could also scale the outputs so that content lines up regardless of resolution differences.

lua
profile {
    -- <other profile fields here>

    compute_layouts = function(self) do
        if #server:outputs() > 1 then
            server:register_layout("clone", "Cloned Displays");
            server:register_layout("panorama", "Panoramic Displays");
        else
            server:register_layout("single", "Single Display");
        end
    end,

    arrange_outputs = function(self)
        local layout = server:active_layout();

        if layout == "panorama" then
            local x = 0;
            for _, output in ipairs(server:outputs()) do
                output:place(x, 0); -- Note the differing x coordinate here.
                local width, height = output:size();
                x = x + width;
            end
        elseif layout == "clone" then
            for _, output in ipairs(server:outputs()) do
                output:place(0, 0);
            end
        elseif layout == "single" then
            for index, output in ipairs(server:outputs()) do
                if index == 1 then
                    output:place(0, 0);
                else
                    output:remove(); -- Excess displays are removed.
                end
            end
        end
    end
}
profile {
    -- <other profile fields here>

    compute_layouts = function(self) do
        if #server:outputs() > 1 then
            server:register_layout("clone", "Cloned Displays");
            server:register_layout("panorama", "Panoramic Displays");
        else
            server:register_layout("single", "Single Display");
        end
    end,

    arrange_outputs = function(self)
        local layout = server:active_layout();

        if layout == "panorama" then
            local x = 0;
            for _, output in ipairs(server:outputs()) do
                output:place(x, 0); -- Note the differing x coordinate here.
                local width, height = output:size();
                x = x + width;
            end
        elseif layout == "clone" then
            for _, output in ipairs(server:outputs()) do
                output:place(0, 0);
            end
        elseif layout == "single" then
            for index, output in ipairs(server:outputs()) do
                if index == 1 then
                    output:place(0, 0);
                else
                    output:remove(); -- Excess displays are removed.
                end
            end
        end
    end
}

WARNING

If your implementation neither places nor removes an output, it will remain in whatever configuration it was in previously, with no changes applied. This could be confusing when multiple profiles are in use on one system. It is recommended to either place or remove all displays each time this method is called by the server.

INFO

The compositor will always select the preferred (native) resolution of a physical display. Changing to non-native modes on physical displays is not currently supported.

This method is called when:

  • The profile is activated or reloaded.
  • An output is connected or disconnected.
  • The active layout has been changed.

Method: arrange_views

Methodarrange_views(self)
selfThe profile object.
ReturnsNothing

This method is called by the compositor to allow the profile to arrange displayed program content (or views) in the logical layout space. The layout space is the same space in which the outputs are arranged in the arrange_outputs method. A view can be thought of as a window that displays all or part of a program's surface (or rendered content) one or more times in the layout. An implementation can take into account any number of factors, such as the currently-active layout, the number and resolutions of connected outputs, the list of connected processes, or other factors as desired.

When this method is not defined, the default implementation simply creates one view per program and places it at 0,0 in the layout, setting its size to fill the entire layout. If multiple programs are running, they will be placed on top of each other in an unspecified order. Therefore, the default behavior is only suitable for profiles that will be running a single program, such as a simple kiosk-style setup with one interface. In all other cases, profiles should define a custom implementation. The default implementation respects the surface's preferred_output value and will create a view to cover that output (instead of the entire layout) when possible.

Example: Simply place views to fill the entire layout

lua
profile {
    -- <other profile fields here>

    arrange_views = function(self)
        for _, surface in ipairs(server:surfaces()) do
            surface:place(0, 0);
        end
    end
}
profile {
    -- <other profile fields here>

    arrange_views = function(self)
        for _, surface in ipairs(server:surfaces()) do
            surface:place(0, 0);
        end
    end
}

Example: Place one program per output by name

lua
profile {
    -- <other profile fields here>

    processes = {
        ["DP-1"] = { "/path/to/some/program" },
        ["HDMI-A-1"] = { "/path/to/another/program" }
    },

    arrange_views = function(self)
        local surfaces = server:surfaces();
        local outputs = server:outputs();
        for _, surface in ipairs(surfaces) do
            surface:clear_views();
            local name = surface:name();
            for _, output in outputs do
                if name == output:name() then
                    local x, y, width, height = output:layout_box();
                    surface:place(x, y, width, height);
                end
            end
        end
    end
}
profile {
    -- <other profile fields here>

    processes = {
        ["DP-1"] = { "/path/to/some/program" },
        ["HDMI-A-1"] = { "/path/to/another/program" }
    },

    arrange_views = function(self)
        local surfaces = server:surfaces();
        local outputs = server:outputs();
        for _, surface in ipairs(surfaces) do
            surface:clear_views();
            local name = surface:name();
            for _, output in outputs do
                if name == output:name() then
                    local x, y, width, height = output:layout_box();
                    surface:place(x, y, width, height);
                end
            end
        end
    end
}

In this example, processes are created with a name that matches the desired output on which they should be displayed. If the required output is not connected, the program is simply not shown--although it is still running. Note the call to surface:clear_views() which ensures that any program not matched to a display is removed from the layout.

INFO

Arranging outputs and views within the layout are very efficient operations. Therefore, it is perfectly fine performance-wise to re-create the layout each time the arrange_outputs and arrange_views methods are called. In fact, doing so tends to keep profile scripts much simpler and easier to read. The internal state of the layout is managed entirely by the compositor.

As a convenience, both the surface:place() and output:place() methods will remove any existing layout configuration for that surface or output, respectively, preventing accidental duplication.

INFO

For greater control, and to perform advanced operations like view duplication or presenting only part of a program's surface, the surface:set_size(), surface:add_view(), and surface:clear_views() methods can be used.

This method is called when:

  • The profile is activated or reloaded.
  • An output is connected or disconnected.
  • The active layout has been changed.
  • Any new process has connected to the compositor.
  • Any process has disconnected from the compositor, usually because it terminated.

Method: refresh_processes

Methodrefresh_processes(self)
selfThe profile object.
ReturnsNothing

This method is called by the compositor to allow the profile to start or stop processes as necessary. This method is almost never implemented by profile scripts unless some special process management is required by the project. In the vast majority of cases, processes will instead be defined using the processes field of a profile object. A typical implementation of this method might inspect the active layout, the connected outputs, the list of processes already running, and other factors, and then start or stop processes using API functions like server:launch() and process:stop().

The default implementation of this method looks at the profile's processes field, launching any processes that should be running but aren't, and, conversely, stopping any running processes that are not defined. It also re-launches any defined processes that have terminated.

This method is called when:

  • The profile is activated or reloaded.
  • An output is connected or disconnected.
  • The active layout has been changed.
  • Any processes has terminated.

Method: bus_message

Methodbus_message(self, data, is_binary)
selfThe profile object.
data(string) A string containing the message payload.
is_binary(boolean) A boolean value indicating whether the message is binary (a false value means it is safe to parse the payload as text).
ReturnsNothing

This method is called whenever a message has been received from the message bus by any other message bus client. It is not called for messages that were sent by the script itself. A profile script may choose to parse the message and take some action in response, such as activating another layout or sending state information back through the message bus. This method is a good way for profile scripts to respond to custom web UIs or events in client programs. The default implementation does nothing.

This method is called when:

  • Any message is received from another client connected to the message bus.

Method: key_event

Methodbus_message(self, sym, state)
selfThe profile object.
sym(string) A string representation of the keyboard key symbol.
state(integer) An integer where 1 indicates that the key was pressed down, and 0 indicates that the key was released.
Returns(boolean) A boolean indicating whether the key event was handled (no return value is equivalent to false).

Each time an event occurs on any connected keyboard, the compositor will call this method. The sym parameter contains a symbol string indicating the affected key using whatever keyboard mapping is set by default on the system. Example values include "A", "Shift_L", "Return", "Escape" and so on.

INFO

For a complete list of possible symbol strings, see the libxkbcommon definitions. Note that the prefix XKB_KEY_ is not included in the symbol string passed to this method.

This method can decide whether any running programs should receive the key event or whether it should be captured by the profile script and not passed to running programs that may have keyboard input enabled. Returning true indicates that the key event was handled and should not be propagated further. Any other value, including no return value at all, indicates that the key event should continue to propagate.