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 UIrun()
: the function that gets executed when pressing enter- The
run()
function must return theSUCCESS
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 therun()
function will be expecting.run()
now has an argument that is not the specialcontroller
.call(**kwargs)
: This function is called every time a value is selected by the user. If it returnsOK
, the Launcher will executerun()
. If it returnsMISSING_ARGS
, the Launcher will askget
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 thecall
function returnsMISSING_ARGS
.arg_name
value is fromSIGNATURE
. 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 therun()
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 |
Command identifier (unique throughout all your commands) |
DISPLAY |
str |
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 specialcontroller
, 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
orMISSING_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 atitle
and amessage
info_message
: Displays an info message. Requires atitle
and amessage
warning_message
: Displays a warning message. Requires atitle
and amessage
success_message
: Displays a success message. Requires atitle
and amessage
confirmation_message
: Returns a confirmation message. Requires atitle
and amessage
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)]