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:
- esbuild: A small Javascript bundler - this is what takes our code and turns it into something usable by Crankshaft
- prettier: (optional) A code formatter
- typescript: (optional) Required if you’d like to use Typescript to perform type checking on your plugin code
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.
For now, we’ll just leave the default console.log
s 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:
- rename
src/index.ts
tosrc/index.js
- Open
package.json
, and in thebuild
andbuild-watch
scripts, replacesrc/index.ts
withsrc/index.js
- Remove the
typescript
dependency:npm uninstall typescript
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:
- move your plugin’s folder into Crankshaft’s plugin folders, or
- symlink your plugin folder to Crankshaft’s plugin folder
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.log
s. 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.
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]