Decky Plugin Template

repository·main·Indexed 18 days ago

https://github.com/steamdeckhomebrew/decky-plugin-template

A reference template for creating Decky plugins from scratch using TypeScript and webpack. It demonstrates the use of the @decky/ui frontend library, project structuring for backend support, and automated distribution. The template includes guidance on implementing the Plugin class for Python backends, managing lifecycle methods (_migration, _main, _unload, _uninstall), and utilizing @decky/api for frontend-backend communication via callable() and addEventListener().

Tokens
3.2K
Snippets
12
Records
13
Agent score
63%

What's inside decky-plugin-template

  1. Configure backend support for distribution

    main

    If your plugin includes a backend, you must follow a specific directory structure to ensure the decky-plugin-database CI can correctly package your binaries.

    Directory Requirements

    • Source Code: All backend source code must be located in backend/src relative to the root of your git repository.
    • Output Binaries: During the build process, all finished binaries must be placed in the backend/out directory.

    Build Script Implementation

    Your build script (Makefile, shell script, etc.) must explicitly ensure binaries are moved to backend/out. If they are not, the CI will fail to include them in the final distribution.

    Example Makefile snippet:

    hello:
    	mkdir -p ./out
    	gcc -o ./out/hello ./src/main.c

    Note: While local builds might use a top-level out folder, the distribution process expects the backend/out structure described above.

    mkdir -p ./out
    gcc -o ./out/hello ./src/main.c
  2. Install development dependencies

    main

    To develop plugins using this template, you must have Node.js v16.14+ and pnpm (v9) installed. It is highly recommended to use pnpm v9 specifically to avoid CI issues during plugin submission.

    On Linux, you can install the correct version of pnpm via npm:

    sudo npm i -g pnpm@9

    If you plan to build plugins with custom backends, you must also have Docker installed, as it is required by the Decky CLI tool.

    sudo npm i -g pnpm@9
  3. Create and build your own plugin

    main

    You can start a new project by forking this repository or using the GitHub "Use this template" button.

    Initial Setup

    In your local repository, run the following to install dependencies and build the frontend for testing:

    1. pnpm i
    2. pnpm run build

    Development Workflow

    • Frontend Changes: Every time you modify frontend files (e.g., index.tsx), you must rebuild the project using pnpm run build or the build task in your IDE.
    • IDE Integration: If using VSCodium or VSCode, you can simply run the setup and build tasks provided by the workspace.
    • Updating Libraries: If you encounter build errors caused by an outdated library, update the UI library using:
    pnpm update @decky/ui --latest

    Reference Material

    For UI components and implementation details, consult the decky-frontend-lib repository. Note that development should primarily target Steam Deck hardware.

    pnpm i
    pnpm run build
  4. Prepare a plugin for distribution

    main

    Plugins can be distributed via the decky-plugin-database or as a .zip file for manual installation via decky-loader.

    Required Plugin Zip Layout

    A valid distribution zip must follow this structure:

    pluginname-v1.0.0.zip
       |-- pluginname/ (directory)
       |      |-- bin/ (optional directory)
       |      |      |-- binary (optional)
       |      |-- dist/ (required directory)
       |      |      |-- index.js (required)
       |      |-- package.json (required)
       |      |-- plugin.json (required)
       |      |-- main.py (required if using python backend/serverAPI)
       |      |-- README.md (optional)
       |      |-- LICENSE(.md) (required)

    Licensing Requirements

    • A LICENSE file is required in the root of your repository.
    • If your license requires the license text to be included with the source/binaries, it must be present in the zip.
    • Standard Procedure: Place your chosen license at the top of the file and keep the original plugin-template license at the bottom. Failure to do this may result in rejection from the plugin database.
  5. Implement the Plugin class for the backend

    main

    To create a Decky plugin backend, you must define a Plugin class. This class serves as the entrypoint for the plugin's lifecycle and provides methods that can be invoked from the TypeScript frontend via @decky/api.

    import decky
    import asyncio
    
    class Plugin:
        # Methods defined here can be called from TypeScript using @decky/api
        async def add(self, left: int, right: int) -> int:
            return left + right
  6. Perform data migrations with decky.migrate_*

    main

    During the _migration() phase, use the following decky utility functions to move legacy data to the appropriate Decky-managed directories:

    • decky.migrate_logs(old_path): Migrates a log file to decky.decky_LOG_DIR.
    • decky.migrate_settings(old_file_path, old_dir_path): Migrates settings files or entire directories to decky.decky_SETTINGS_DIR.
    • decky.migrate_runtime(old_dir_path_1, old_dir_path_2): Migrates runtime data directories to decky.decky_RUNTIME_DIR.
    async def _migration(self):
        # Example: Migrating logs
        decky.migrate_logs(os.path.join(decky.DECKY_USER_HOME, ".config", "template.log"))
        
        # Example: Migrating settings
        decky.migrate_settings(
            os.path.join(decky.DECKY_HOME, "settings", "template.json"),
            os.path.join(decky.DECKY_USER_HOME, ".config", "template")
        )
        
        # Example: Migrating runtime data
        decky.migrate_runtime(
            os.path.join(decky.DECKY_HOME, "template"),
            os.path.join(decky.DECKY_USER_HOME, ".local", "share", "template")
        )
  7. Define a Decky plugin with definePlugin()

    main

    The definePlugin function from @decky/api is the entrypoint for your frontend plugin. It accepts a callback function that returns a plugin configuration object. This configuration object defines how your plugin appears and behaves in the Decky UI.

    Plugin Configuration Object Properties:

    • name: The name shown in various Decky menus.
    • titleView: A React element displayed at the top of your plugin's menu (often using staticClasses.Title).
    • content: The main React component/element that renders the plugin's menu content.
    • icon: The icon displayed in the plugin list (e.g., a React Icon component).
    • onDismount: A lifecycle function triggered when the plugin is unloaded. Use this to clean up event listeners or routes.
    export default definePlugin(() => {
      return {
        name: "Test Plugin",
        titleView: <div className={staticClasses.Title}>Decky Example Plugin</div>,
        content: <Content />,
        icon: <FaShip />,
        onDismount() {
          // Cleanup logic here
        },
      };
    });
  8. Show toast notifications with toaster.toast()

    main

    Use the toaster utility from @decky/api to display transient notifications to the user.

    toaster.toast() accepts an object with:

    • title: The main text of the toast.
    • body: (Optional) Additional descriptive text for the toast.
    toaster.toast({
      title: "Notification Title",
      body: "This is the message body"
    });
  9. Listen to backend events with addEventListener()

    main

    You can listen for custom events emitted by the Python backend using addEventListener from @decky/api.

    addEventListener takes two arguments:

    1. The name of the event as a string.
    2. A callback function that receives the event data. The shape of the data is defined by a generic type parameter representing a tuple of the arguments passed by the backend.

    To prevent memory leaks, you should store the returned listener and call removeEventListener during the plugin's onDismount lifecycle hook.

    // Registering a listener for 'timer_event'
    const listener = addEventListener<[test1: string, test2: boolean, test3: number] idea>("timer_event", (test1, test2, test3) => {
      console.log("Event received:", test1, test2, test3);
    });
    
    // Inside your plugin definition's onDismount:
    onDismount() {
      removeEventListener("timer_event", listener);
    }
  10. Use lifecycle methods: _migration, _main, _unload, and _uninstall

    main

    The Plugin class uses specific underscored methods to manage the plugin lifecycle. These are called automatically by the Decky loader at different stages:

    • _migration(): Executed before _main(). Use this to move old configuration, logs, or runtime data to the new standard Decky directories.
    • _main(): The primary entrypoint. This is where you initialize your plugin, start background tasks, and perform setup logic.
    • _unload(): Called when the plugin is being stopped (e.g., the user disables it), but before it is completely removed. Use this to gracefully stop services.
    • _uninstall(): Called after _unload during a full uninstallation. Use this to clean up processes or files that should not remain on the system.
    class Plugin:
        async def _migration(self):
            # Perform migrations here
            pass
    
        async def _main(self):
            # Main plugin logic
            pass
    
        async def _unload(self):
            # Handle stopping the plugin
            pass
    
        async def _uninstall(self):
            # Clean up remnants during uninstallation
            pass
  11. Use Decky UI components for plugin layouts

    main

    The @decky/ui package provides standard components to ensure your plugin matches the Steam Deck interface:

    • PanelSection: A container used to group related settings or buttons.
    • PanelSectionRow: A row within a PanelSection used to align items.
    • ButtonItem: A clickable button component. Use the layout prop (e.g., layout="below") to control how the button text/content is positioned within the row.
    import { PanelSection, PanelSectionRow, ButtonItem } from "@decky/ui";
    
    function MyComponent() {
      return (
        <PanelSection title="My Settings">
          <PanelSectionRow>
            <ButtonItem onClick={() => console.log("Clicked!")}>
              Click Me
            </ButtonItem>
          </PanelSectionRow>
        </PanelSection>
      );
    }
  12. Emit events to the frontend with decky.emit

    main

    You can send data from the Python backend to the TypeScript frontend using decky.emit(event_name, *args). This is useful for notifying the UI of background events or status changes.

    async def long_running(self):
        await asyncio.sleep(15)
        # Emits an event named 'timer_event' with various arguments
        await decky.emit("timer_event", "Hello from the backend!", True, 2)