Skip to content

Commands

Introduction

Writing commands is an easy way to tailor your Launcher by adding some custom actions, written in Python. They can be used for opening files, directories or url, execute Bip operation, interact with the Launcher...

These python scripts are fairly simple. In order to be loaded by the Launcher, the scripts must be placed in the commands directory of your Content repository, and meet a few rules: the Launcher is for example a run() function to be present, as well as some variables, such as NAME. If the command passes validation, it can be executed from the Launcher by typing its name, or display name if specified.

Commands can either be single-shot (hit and execute), or can expect an unlimited amount of arguments before being run. The argument mechanism allow you to easily the values that the user will be able to select from the Launcher.

Hello World

Let start with a simple example. After adding this hello.py to your commands directory, you should be able to type Hello and see your command. Press enter, and the run() function is executed.

import bip.pink.login as login
from bip.mink.commands.constants import SUCCESS

NAME = "hello"
DISPLAY = "Hello"


def run(controller):
    user = login.get_user()
    message = f'Hello {user.full_name}'
    controller.info_message(title='Hello command', message=message)
    controller.logger.info(message)
    return SUCCESS

This is the smallest structure for a command. It must at least provide the following elements:

  • NAME: Command identifier (unique throughout all your commands)
  • DISPLAY: Name for the UI
  • run(): the function that gets executed when pressing enter
  • The run() function must return the SUCCESS

Info

The run() function can accept a controller attribute.

The controller is the Launcher application object from bip.apps.launcher.app.Launcher, which inherits from the BipApp class, from bip.toolkit.BipApp. It comes with some pretty useful methods such as, in the following example, an access to the Bip dialog widgets or the Bip logger.

Handling arguments

Basics

Let's now see how a command can ask for arguments. When the run() function is expecting one or more arguments, we say that it is signed. In that case, you must provide a list of the names of the expected arguments. This list is called the signature, and must be specified in the SIGNATURE variable.

Here is an example of a dummy command called Email, which allow to select amongst all the Bip users who have an email address set, and open a mailto:?to=... link in your favourite email app.

import webbrowser
from bip import link
from bip.apps.launcher.api.commands.constants import OK, MISSING_ARGS, SUCCESS

NAME = "email"
DISPLAY = "Email"
DESCRIPTION = "Send an email to another Bip user"
SIGNATURE = ['user']


def run(user):
    webbrowser.open(f'mailto:?to={user.email}', new=1)
    return SUCCESS


def call(**kwargs):
    if 'user' in kwargs:
        return OK
    else:
        return MISSING_ARGS


def get(arg_name, **kwargs):
    if arg_name == 'user':
        return _get_users()


def _get_users():
    users = link.user.get(active_only=True)
    serialized = []
    for user in users:
        if not user.email:
            # We only want users with an email
            continue

        serialized.append({
            'uid': user,
            'name': user.full_name,
            'description': user.email,
            'sorter': user.last_name
        }
        )

    return serialized

There are a few new elements:

  • SIGNATURE : A list of the names of the arguments that the run() function will be expecting.
  • run() now has an argument that is not the special controller.
  • call(**kwargs): This function is called every time a value is selected by the user. If it returns OK, the Launcher will execute run(). If it returns MISSING_ARGS, the Launcher will ask get for the next argument values. kwargs is a dict that contains all the previously set values.
  • get(arg_name, **kwargs): This function is called while the call function returns MISSING_ARGS. arg_name value is from SIGNATURE. For each argument, you can make your own getter. A good practice is to name it with a prefix underscore, for it is a private member of the script (eg: _get_users). The function must return a list of dictionaries, formatted in a special way, see below for values format. These values will be displayed in the Launcher's result list.

Warning

  • The names in the SIGNATURE and the run() arguments name must be exactly the same.
  • Make sure no argument is named controller. This is a reserved name. The Launcher controller would always be passed to such argument, overriding yours.

Unordered signature

Danger

This feature is under development. It is not yet recommended to use it.

By default, signatures are ordered. This means that the Launcher asks, one by one, following the declaration order, the value for each argument. But you can set your command to unordered. If you do so, the Launcher will display all the arguments at once. This is typically for allowing more intuitive typing, where the user could for example type open AssetA lighting scene latest as well as open latest AssetA scene lighting, providing version, task, type and item arguments values in any order.

Reference

Specifications

  • Filename: *.py
  • Location: commands/

Variables

Name Type Description
NAME str Mandatory Command identifier (unique throughout all your commands)
DISPLAY str Mandatory Name for the UI
ALIASES list (str) List of other names for the command
DESCRIPTION str Short description of what it does
TAGS list (str) List of tags, useful for grouping commands by category and helping with search
HIDE_AFTER_RUN bool (Default: True) Defines if the Launcher gets hidden after a successful execution.
RELOAD_AFTER_RUN bool (Default: False) Defines if the Launcher gets reloaded after a successful execution.
ORDERED bool If the command is signed (has argument(s)),
SIGNATURE list (str) If your command expect arguments, list of their names
ICON str Path to a 64x64 png

Tip

You can store your assets (icons) at the root of your content folder, so you can refer to them with $BIP_CONTENT/assets/icons/file.png

Functions

run()

  • Signature: None, or custom arguments (matching the SIGNATURE), or the special controller, which would be the BipApp object of the Launcher application
  • Rules:
    • Mandatory
    • Must return SUCCESS (FAILED not yet implemented)

call()

  • Signature: **kwargs
  • Rules:
    • Only required if the command is signed
    • The launcher passes all the arguments that have been set by the user
    • Is called each time a selection is validated by the user
    • Must return either OK or MISSING_ARGS
  • Notes: This function determines if there is enough data to run the command. It gives you a deep control in case you build a non-linear command, where some choices can require more details, while other choices are enough for execution.

get()

  • Signature: arg_name, **kwargs
  • Rule: Must return a list of values (see below)
  • Notes: The kwargs dictionary contains the arguments that are already set, in case you need those information to fetch the next values (typically in case of cascading data)

validate()

Tip

If you want your command to be available only under certain conditions, you can add an optional validate() function that will be run when the Launcher is loading

  • Signature: None, or the special controller, which would be the BipApp object of the Launcher application
  • Rules:
    • Optional
    • Must return a boolean

Constants

These constants are part of the Launcher API. Import them from from bip.apps.launcher.api.commands.constants

Key Description
OK Used by the call() function to allow run() execution
SUCCESS Used by the run() function to tell the Launcher that the execution went well
MISSING_ARGS Used by the call() function to tell the Launcher to keep requesting arguments

Values

Warning

The Launcher does not yet check the validity of the returned values. Failing to comply with this structure will result in uncaught exceptions.

When returning choice values for an argument, you must return a list of dictionaries. Each dictionary must have the following keys:

Key Type Description
uid (mandatory) object The object you want to be passed to the run() function
name (mandatory) str The display name for UI
description str Optional description for UI
sorter str/int Optional sorter. If not defined, the values will be sorted by names

Controller

If you request the controller argument, it passes a Launcher application object from bip.apps.launcher.app.Launcher, which inherits from the BipApp class, from bip.toolkit.BipApp. This controller has a few interesting members:

Attributes

  • server_config: bip.link.server_config.ServerConfig()
  • current_user:bip.link.user.User()
  • active_project:bip.link.project.Project()

Methods

  • error_message: Displays an error message. Requires a title and a message
  • info_message: Displays an info message. Requires a title and a message
  • warning_message: Displays a warning message. Requires a title and a message
  • success_message: Displays a success message. Requires a title and a message
  • confirmation_message: Returns a confirmation message. Requires a title and a message

Examples

Open useful directories

This simple command allows to open in the file explorer some frequently used directories.

import os
import subprocess
from future.utils import iteritems
from bip.mink.commands.constants import OK, MISSING_ARGS, SUCCESS

"""Useful shortcuts for debugging"""

NAME = "shortcuts"
DISPLAY = "Shortcuts"
DESCRIPTION = "Useful shortcuts for debugging"
TAGS = ["Bip", "Debug"]
HIDE_AFTER_RUN = True
RELOAD_AFTER_RUN = False
SIGNATURE = ["action"]

user_dir = os.path.expanduser("~")
modo_dir = os.path.join(user_dir, "AppData", "Roaming", "Luxology")
nuke_dir = os.path.join(user_dir, ".nuke")
app_data_dir = os.path.join(user_dir, "AppData")


actions = {
    "user_directory": {
        "name": "User directory",
        "description": "User's folder",
        "call": lambda: subprocess.Popen(["explorer", user_dir]),
    },
    "modo_directory": {
        "name": "Modo directory",
        "description": "User's Luxology folder",
        "call": lambda: subprocess.Popen(["explorer", modo_dir]),
    },
    "nuke_directory": {
        "name": "Nuke directory",
        "description": "User's Nuke folder",
        "call": lambda: subprocess.Popen(["explorer", nuke_dir]),
    },
    "app_data_directory": {
        "name": "AppData",
        "description": "Windows user's AppData",
        "call": lambda: subprocess.Popen(["explorer", app_data_dir]),
    },
    "bip_user_directory": {
        "name": "User Bip",
        "description": "User's .blink folder",
        "call": lambda: subprocess.Popen(["explorer", os.environ["BIP_USER_DIRECTORY"]]),
    },
    "bip_system_directory": {
        "name": "System Bip",
        "description": "Bip install folder",
        "call": lambda: subprocess.Popen(["explorer", os.environ["BIP_PACKAGE"]]),
    },
    "content_directory": {
        "name": "Content",
        "description": "Content folder",
        "call": lambda: subprocess.Popen(["explorer", os.environ["BIP_CONTENT"]]),
    },
}


def run(controller, action):
    actions[action]["call"]()
    return SUCCESS


def call(**kwargs):
    action = None

    if "action" in kwargs:
        action = kwargs["action"]

    if action:
        return OK
    else:
        return MISSING_ARGS


def get(arg_name, **kwargs):
    if arg_name == "action":
        return _get_actions()


def _get_actions():
    return [
        {"uid": uid, "name": data["name"], "description": data["description"]}
        for uid, data in iteritems(actions)
    ]

Set source

This command is easy access to the Bip user setting allowing to user either the server recommended version of Bip, or specify a version, or specify a version.

This is an interesting example because the user selects "Recommended" or "Latest", the command gets executed after one argument, while if they chose "Version" or "Branch", a second argument must be provided.

from future.utils import iteritems
from bip.mink.commands.constants import OK, SUCCESS, MISSING_ARGS
from bip.wink.constants import LATEST, TAG, BRANCH, RECOMMENDED
from bip.link.server_config import get as get_server_config
from bip.pink.system import (
    get as get_system_config,
    update as update_system_config,
)
from bip.utils.git import get_remote_tags, get_remote_branches
from packaging import version


"""Change the code source"""

NAME = "set_source"
DISPLAY = "Set source"
ALIASES = ["Change source"]
DESCRIPTION = (
    "Change version of Bip to either latest (default), custom branch or custom tag)"
)
TAGS = ["Settings"]
HIDE_AFTER_RUN = False
RELOAD_AFTER_RUN = False
ORDERED = True
SIGNATURE = ["mode", "value"]


def run(controller, mode, value=None):
    system_config = get_system_config()
    modified = False

    current_mode = system_config["version"]["mode"]
    current_branch = system_config["version"]["custom_branch"]
    current_tag = system_config["version"]["custom_tag"]

    if current_mode != mode:
        system_config["version"]["mode"] = mode
        modified = True

    if mode == BRANCH and current_branch != value:
        system_config["version"]["custom_branch"] = value
        modified = True

    if mode == TAG and current_tag != value:
        system_config["version"]["custom_tag"] = value
        modified = True

    if modified:
        update_system_config(system_config)
        controller.update()

    return SUCCESS


def call(**kwargs):
    mode = None
    value = None

    if "mode" in kwargs:
        mode = kwargs["mode"]

    if "value" in kwargs:
        value = kwargs["value"]

    if mode:
        if value:
            return OK
        if mode in (LATEST, RECOMMENDED):
            return OK

    return MISSING_ARGS


def get(arg_name, **kwargs):
    if arg_name == "mode":
        return get_modes()
    elif arg_name == "value":
        return get_values(kwargs["mode"])


def get_modes():
    return [
        {
            "uid": LATEST,
            "name": "Latest",
            "description": "Always use latest stable version available",
        },
        {
            "uid": RECOMMENDED,
            "name": "Recommended",
            "description": "Use the version recommended by your server",
        },
        {
            "uid": TAG,
            "name": "Version",
            "description": "Specify a version",
        },
        {
            "uid": BRANCH,
            "name": "Branch",
            "description": "Specify a branch",
        },
    ]


def get_values(mode):
    if mode == LATEST:
        return None
    elif mode == TAG:
        normalized = []
        for value in get_git_tags():

            data = {
                "uid": value,
                "name": value,
                "description": "",
                "sorter": version.parse(value),
            }
            normalized.append(data)
        return normalized
    elif mode == BRANCH:
        normalized = []
        for value in get_git_branches():
            data = {"uid": value, "name": value, "description": "", "sorter": value}
            normalized.append(data)
        return normalized


def get_git_tags():
    server_config = get_server_config()
    tags = get_remote_tags(server_config.bip_source)
    return [key for key, value in iteritems(tags)]


def get_git_branches():
    server_config = get_server_config()
    branches = get_remote_branches(server_config.bip_source)
    return [key for key, value in iteritems(branches)]