Skip to content

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 exposes projects(), 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 resolved LaunchSpec (raises if not installed).
  • launch(spec, runner=...) - spawn it; pass a runner to 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.