This documentation is still a work in progress. Please ask in one of the community channels if you run into issues or find a mistake.

Making your first plugin

In this guide, you’ll learn how to make a basic Crankshaft plugin. This guide assumes you have basic knowledge of Javascript and using NPM packages.

Plugin Requirements πŸ”—

Crankshaft plugins are distributed as folders, containing whatever files are needed to run the plugin. There are only two requirements to file structure that every plugin must meet:

example-plugin/
β”œβ”€β”€ dist
β”‚   └── index.js
└── plugin.toml

A plugin.toml file, containing some basic info and configuration for your plugin, and a folder called dist, containing an index.js file with your plugin’s code. The index.js must be a Javascript module that exports a load function and an unload function.

Using the plugin template πŸ”—

You can find a plugin template here (Github mirror). This template contains a placeholder plugin.toml, empty load and unload functions, and the necessary scripts and dependencies to bundle and transpile the code into the format that Crankshaft expects.

Start by cloning the template. The directoy name that you clone it to should match your plugin’s ID - in this example, I’m cloning it into a directory called first-plugin:

git clone https://git.sr.ht/~avery/crankshaft-plugin-template first-plugin
cd first-plugin/

Next, let’s install the NPM dependencies that come with the plugin. Listed in package.json, these are:

To install dependencies, run npm install. You can also use another Node.js package manager, like Yarn or PNPM, if you prefer. This guide uses NPM commands, but if you use something else, you know what to do.

Now let’s try configuring the plugin.

Configuring plugin.toml πŸ”—

Open your plugin’s plugin.toml file and fill in the empty fields. This is basic information about your plugin like title, version, website and author. For now, you can name it whatever you want, and give it a version like 1.0.

The other imporant part of your plugin.toml is the entrypoints section. This is where you can define which parts of the Steam client your plugin should load into, along with if your plugin will load into the desktop client, the gamepad/Steam Deck client, or both.

There will be more about these entrypoints later. For now, set library = true under both the entrypoints.desktop and entrypoints.deck sections, and leave the rest of the values set to false.

Finally, we can jump into our plugin’s code!

Plugin code πŸ”—

Open up src/index.ts. You will see the two exported functions mentioned earlier - load and unload. As you can tell by their names, the load function will be called to load your plugin into Steam, and the unload function will be called to unload your plugin, and should clean up menu items, handlers, or anything else your plugin might leave behind.

The load function will receive an argument called smm. This is an object that Crankshaft passes to plugins, and provides plugins with API’s for things like file system access and running commands.

You might see the abbreviation SMM in a few places. That’s because this project used to be called Steam Mod Manager before being renamed to Crankshaft. SMM is a more recognizable abbreviation than CS (for Crankshaft), so it was kept in some places.

For now, we’ll just leave the default console.logs in the load and unload functions (you can change the message if you’d like), so that we can see the plugin load and unload. Afterwards we’ll add some functionality!

Building the plugin πŸ”—

In many cases, you’ll have a build step between the code you write and the plugin code that Crankshaft executes. If you’re using Typescript, this involves transpiling down to vanilla Javascript. If you’re using NPM package dependencies, they’ll need to be bundled into your plugin code.

The plugin template comes with esbuild pre-configured. esbuild is a simple, fast Javascript bundler, that can also handle Typescript, JSX, CSS, images, and more. For most plugins it should be all you need, but you’re free to use any other build system you’d like (or no build system at all).

For now, let’s build your plugin. Run npm run build, and you should see a new file at dist/index.js. This contains your compiled plugin code.

Now that you’ve met the two plugin requirements (a plugin.toml and plugin code at dist/index.js), you can load your plugin into Steam!

Note about Typescript πŸ”—

Crankshaft’s frontend code is written in Typescript. An NPM package, @crankshaft/types, is included in the plugin template that provides types for the Crankshaft plugin API.

What if I don't want to use Typescript?

If you don’t want to use Typescript, you’ll have to make three small modifications to the plugin template code:

Loading your plugin into Steam πŸ”—

Linking your plugin folder πŸ”—

Crankshaft loads plugins located in it’s plugins folder. If you’re using the Flatpak, this folder should be located at ~/.var/app/space.crankshaft.Crankshaft/data/crankshaft/plugins/. You can either:

If you have a preferred location in your file system where you like to keep source code, a symlink (symbolic link) will be useful, as you can add a link in Crankshaft’s plugins folder, that will point to your plugin’s folder located elsewhere in your file system (kind of like a shortcut).

To add a symlink to your plugin’s folder, run the following command, replacing the first path with the correct path to your plugin’s folder:

ln -s path/to/first-plugin ~/.var/app/space.crankshaft.Crankshaft/data/crankshaft/plugins/

Now that your plugin’s folder is in the right place, you’ll have to restart Crankshaft to manually load a plugin (as opposed to installing from the plugin store). (yes this is not ideal, will be fixed soon!)

Open Steam, click on the puzzle piece icon in the library to open the Crankshaft menu, go to Crankshaft Settings, and press Restart Crankshaft.

Once it restarts, go back to the menu, and look at the Manage Plugins page. You should see your plugin listed, currently Disabled.

Opening the devtools πŸ”—

Before we enable our plugin, let’s do one last thing - open the Chrome devtools, so that we can see our console.logs. You’ll need to use a Chromium-based browser (like Chromium or Google Chrome) for this step.

Open the Crankshaft Settings page again, and copy the URL starting with devtools://... in the Open Chrome devtools section. Paste this into the address bar of your web browser, and you should see various messages, warnings, and errors, from Steam and Crankshaft. This is where your plugin’s logs will be visible, and debug your plugin when needed.

Loading your plugin πŸ”—

Go back to the Manage Plugins page. First, press the Load button. This will enable your plugin and cause it to be loaded whenever Crankshaft starts.

The Load button will enable your plugin, but won’t actually run it’s code yet (yes, this is a bug :) ). Now, press the Reload button, next to the Load button.

If you go back to the devtools, you should now see the logs from your plugin, similar to this:

[SMM] Unloading plugin first-plugin...
First plugin unloaded!
[SMM] Loading plugin ...
First plugin loaded!

Great job, your plugin is now loading into Steam!

You may have noticed that your unload function was called before your load function. unload will always be called before your plugin is loaded, to ensure that anything left over from a previous run of your plugin has been cleaned up.

Adding some functionality πŸ”—

Let’s make our plugin actually do something! For this example, we’ll have the plugin add a new page to the Crankshaft menu. This page will have a button that prints the user’s computer hostname when pressed (a hostname is a name that identifies your computer on a network).

Adding a menu page πŸ”—

Open up your plugin’s src/index.ts again. We’re going to start by adding a new menu item. Change your load and unload methods to look like this:

export const load = (smm: SMM) => {
  smm.MenuManager.addMenuItem({
    id: 'first-plugin',
    label: 'First Plugin',
    render: async (smm: SMM, root: HTMLElement) => {
      // ...
    },
  });
};

export const unload = (smm: SMM) => {
  smm.MenuManager.removeMenuItem('first-plugin');
}

We’re using a method on the smm object, smm.MenuManager.addMenuItem, to add a new menu item. We give our menu item a unique ID used to identify it. This ID can be anything you want, but in most cases, it should just match your plugin ID (unless your plugin will add multiple menu items).

Note that, in the unload function, we add a matching smm.MenuManager.removeMenuItem call, and pass it the menu item’s ID. We need this to remove the menu item when our plugin is unloaded, otherwise it would stay in the menu.

We also give addMenuItem a label, the text that will be visible to the user on your menu item.

Finally, we pass in a render function. This function will be called when your menu item is opened, and is responsible for displaying your plugin’s menu page contents.

The render function receives two arguments - the smm object that you’re already familiar with, and an HTML element called root.

root is the HTML element for the empty page that your plugin should load into. For this example, we’ll just be adding a button and paragraph element to the page. If your plugin was, for example, a React app, this root element is the element that you would render your app into.

Let’s quickly test out our new menu page!

First, we need to rebuild our plugin. We could go back into the plugin folder and run npm run build every time we make a change to our plugin, but that’s a bit tedious.

The plugin template came with a second script: npm run build-watch. You can start this command in a terminal, and leave it running in the background. Every time you change your plugin’s code in src/, your plugin will get automatically rebuilt.

Now that our plugin is rebuilt, go back to the Manage Plugins page, and press Reload. You should now see your new menu item in the Crankshaft menu!

If you click on the item, you’ll see an empty page. Let’s change that!

Adding contents to the menu page πŸ”—

Add the following to your render function:

render: async (smm: SMM, root: HTMLElement) => {
  const btn = document.createElement('button');
  btn.innerText = 'Get Hostname';
  btn.classList.add('cs-button');
  root.appendChild(btn);

  const name = document.createElement('p');
  name.innerText = 'Hostname: ';
  root.appendChild(name);
},

Now, after reloading your plugin and opening the menu page, you should see a button and some text.

All we did here was create a button and paragraph element like usual, then append to our root element to add it to the menu page.

The only special thing to note here is that we added the class cs-button to the button. This is a class provided by Crankshaft that will style our button.

Finally, let’s hook up the button.

Hooking up the button πŸ”—

For this example, we want to get the user’s computer hostname when the button is pressed. You can check your computer’s hostname by running the command hostname in a terminal.

We’ll have to figure out how to get the output of this command from our plugin. Normally, Javascript running in a web browser wouldn’t be able to run commands on the user’s computer. This is why Crankshaft provides an API for executing commands.

Add the following to your render function:

render: async (smm: SMM, root: HTMLElement) => {
  // [...]
  root.appendChild(name);

  btn.addEventListener('click', async () => {
    try {
      const res = await smm.Exec.run('hostname');
      name.innerText += res.stdout;
    } catch (err) {
      console.error(err);
      smm.Toast.addToast('Error getting hostname');
    }
  });
},

We’ve added a click event listener to the button. When the button is clicked, we use smm.Exec.run to execute the hostname command.

The command output will be in res.stdout, so we add that to our name element’s text.

The other interesting thing in this example is in our try/catch statement. If there’s an error executing the command, we use smm.Toast.addToast to show an error message to the user.

To see what a toast looks like, try running smm.Toast.addToast('Hello world') in your devtools!

Now, once you reload your plugin, try pressing the button! Your computer’s hostname should appear below.

[insert congratulatory confetti]