Desktop app & software launcher¶
fxqdesktop is a lightweight desktop launcher and publisher. All its Qt code
routes through qtpy, so the same app runs standalone (on PySide6) and inside
a PySide-based DCC. Its job is to get an artist from "I'm on my machine" to "the
right DCC is open, in the right project/task context" — without hand-editing
paths or environment variables.
How it's built¶
The app is split in two so the logic is testable without a UI:
DesktopController- pure logic over the SDK. It exposesprojects(),tasks(project_id),publish_task(...),recent_versions(), and the software launcher (applications()/launch_application()).MainWindow- a thin qtpy window that renders what the controller returns.
from fxqclient import Session
from fxqdesktop.controller import DesktopController
ctrl = DesktopController(Session("http://localhost:8000", api_key="fxq_..."))
for project in ctrl.projects():
print(project.id)
Because the controller is just SDK calls, the desktop app can do nothing the API doesn't allow, and you can reuse the controller in your own tools.
Run the bundled window¶
There is no console script; launch the Qt window from Python:
from fxqdesktop.app import run
run("http://localhost:8000", api_key="fxq_...") # blocks on the Qt event loop
The bundled MainWindow lists your projects and their tasks and publishes the
selected task. To drive a DCC launch today, call the controller's launcher API
directly (below) or wire it into your own window — the engine is UI-agnostic.
Run it where your artists work
Standalone it's a launcher; loaded inside a DCC, the same window gives artists their tasks and publishing without leaving the host.
Launch a DCC from your machine¶
This is the software launcher (VAL-46): a catalog of DCCs with per-OS executable patterns, discovery of which versions are actually installed on this machine, and a launch that injects the project/task context and the app's environment. It is the same idea as the AYON Applications addon, the ShotGrid software launcher, or Prism.
1. Discover what's installed¶
applications() walks the catalog and returns only the versions whose
executable exists on this machine, keyed by app name:
installed = ctrl.applications()
# {"houdini": [AppVersion(version="20.5.445", ...)], "nuke": [...], "blender": []}
for name, versions in installed.items():
for av in versions:
print(name, av.version)
The built-in catalog ships Houdini, Nuke and Blender with the
standard install paths for Windows, Linux and macOS. A version is "installed"
when the first existing path in its per-platform executables list resolves
(the {version} token is substituted first).
2. Launch into a task context¶
launch_application(name, version, context) resolves the executable for this
platform, builds the environment, and spawns the process:
project = ctrl.projects()[0]
task = ctrl.tasks(project.id)[0]
ctrl.launch_application(
"houdini",
"20.5.445",
context={
"project": project.data["name"],
"task": task.id,
"project_root": "//studio/jobs/demo",
"ocio": "//studio/config/ocio/config.ocio",
},
)
The context keys map to standard environment variables injected into the
launched process, so the DCC (and any fxquinox in-host integration) knows where
it is:
context key |
Environment variable | Meaning |
|---|---|---|
project |
FXQ_PROJECT |
The active project. |
task |
FXQ_TASK |
The task being worked on. |
project_root |
FXQ_PROJECT_ROOT |
The project's root on disk. |
ocio |
OCIO |
Colour config the host should load. |
On top of those, each Application and AppVersion can carry its own
environment dict; values support {token} substitution from the same context,
and the version's environment is layered last (version wins). Extra command-line
args on the version (and any you pass) are forwarded to the executable.
If the requested version isn't installed, the launch raises
FileNotFoundError; an unknown version raises KeyError.
3. Supply your own catalog¶
The default catalog is a starting point — studios point it at their own
installs and add DCCs. An Application is a name + label + a list of
AppVersions, and each version maps platform → candidate path templates:
from fxqdesktop import applications
from fxqdesktop.applications import Application, AppVersion
maya = Application(
name="maya",
label="Maya",
versions=[
AppVersion(
version="2025",
executables={
"windows": ["C:/Program Files/Autodesk/Maya{version}/bin/maya.exe"],
"linux": ["/usr/autodesk/maya{version}/bin/maya"],
},
environment={"MAYA_PROJECT": "{project_root}"},
),
],
)
ctrl.applications(catalog=[*applications.DEFAULT_APPLICATIONS, maya])
ctrl.launch_application("maya", "2025", context=ctx, catalog=[maya])
Underneath: pure, testable functions¶
The launcher engine in fxqdesktop.applications is pure and unit-tested — no Qt,
no real process spawn. You can use the pieces directly, and inject exists and a
runner to test without touching disk or launching anything:
from fxqdesktop.applications import build_launch, launch, catalog_by_name
app = catalog_by_name()["nuke"]
spec = build_launch(app, "15.1v1", context=ctx) # → LaunchSpec(executable, args, env)
launch(spec) # runner defaults to subprocess.Popen
discover(app)/executable_for(av)- which versions resolve on this platform.resolve_environment(app, av, context)- the merged launch environment.build_launch(...)- a resolvedLaunchSpec(raises if not installed).launch(spec, runner=...)- spawn it; pass arunnerto capture instead.
The rest of the controller¶
projects()/tasks(project_id)- browse work to act on.publish_task(task_id, product, product_type="render")- publish from the desktop (see the publish chain).recent_versions()- the latest published versions.
Once a DCC is open, the in-host adapters take over for publish and load — see Houdini and Nuke, and the universal loader.