Introduction
Moosync lets you create your own extensions to extent the functionality for the main app. Extensions are web-assembly modules that can be loaded at runtime.
Restrictions
Web assembly is platform independant and has a few restrictions:
- Limited access to the file system
- Limited access to the network
- No support for threads (tracking wasi-threads)
- No support for native libraries
Development
Moosync uses extism under the hood to load web assembly modules. Any host language supported by extism can be used to write extensions. Currently we only provide support for
- Rust
- Golang
- Javascript / Typescript
- Python
Getting started
Extensions follow a very specific layout.
Creating a new extension
Manifest
Create a new file called package.json. This will be the manifest for your extension containing all details and permissions about it.
A sample manifest looks like
{
"name": "moosync.sample.extension",
"version": "0.0.1",
"icon": "assets/icon.svg",
"extensionEntry": "ext.wasm",
"moosyncExtension": true,
"displayName": "My extension",
"permissions": {
"hosts": [
"*.google.com",
"google.com"
],
"paths": {
"{ENV_1}": "/",
}
}
}
Fields
name: A unique identifier for your extension.version: The version of your extension in semver format.icon: The icon of your extension. The path is relative topackage.json.extensionEntry: The path to the compiled WASM file. The path is relative topackage.json.moosyncExtension: This must be set totruefor the extension to be loaded.displayName: The name of your extension that will be displayed in the UI.permissions: The permissions that your extension needs to run.-
hosts: The hosts (URLs) that your extension needs to access. -
paths: The paths that your extension needs to access. You can use environment variables in the path. The environment variable must be wrapped in{}. For example,{ENV_1}will be replaced with the value ofENV_1environment variable.Keys in the
pathsobject is the location in the user's filesystem. Values in thepathsobject is where the actual directory will be accessible in the extension. Eg."paths": { "/test": "/" }To access
/test/file.txtin the extension, you can use the path/file.txt.
-
Prerequisites
Moosync extensions are built and packaged using Bazel.
Installing Bazel
Please follow the official Bazel installation guide to install Bazel (via Bazelisk) on your system:
Verify your installation by running:
bazel --version
Writing your first extension
1. Workspace Setup
In your workspace root, create or update MODULE.bazel to depend on extensions_sdk:
module(
name = "my_extensions",
version = "1.0.0",
)
bazel_dep(name = "extensions_sdk", version = "1.0.0")
2. Generating Boilerplate
The easiest way to get started is by using the built-in scaffolding tool. This generates both the Bazel BUILD file and the starter code for your extension:
bazel run @extensions_sdk//tools:scaffold -- --lang rust --name my_rust_ext
3. Extension Rules & Implementation
Generated BUILD Rule
The scaffolding tool generates a BUILD file using rust_extension:
load("@extensions_sdk//:defs.bzl", "rust_extension")
package(default_visibility = ["//visibility:public"])
rust_extension(
name = "rust_sample",
srcs = ["src/lib.rs"],
package_name = "moosync.sample.rust",
display_name = "Sample Extension (Rust)",
version = "0.0.1",
)
Implementation
Extensions are represented by the Extension trait. You need to register your extension through the init function:
#![allow(unused)] fn main() { struct SampleExtension; impl Extension for SampleExtension {} impl Accounts for SampleExtension {} impl DatabaseEvents for SampleExtension {} impl PlayerEvents for SampleExtension {} impl PreferenceEvents for SampleExtension {} impl ContextMenu for SampleExtension {} #[unsafe(no_mangle)] pub extern "C" fn init() { info!("Initializing SampleExtension"); register_extension(Box::new(SampleExtension)).unwrap(); info!("Initialized SampleExtension"); } }
Build
bazel build //my_rust_ext:my_rust_ext
Required implementations
All extensions must return ProviderScopes. These scopes determine which events are sent to your extension.
Lets consider the search scope for this example. Adding the search scope would cause the main app to request search results from your extension.
#![allow(unused)] fn main() { impl Provider for SampleExtension { fn search(&self, req: RequestedSearchResultRequest) -> api::MoosyncResult<SearchResult> { info!("Search requested for query: {}", req.query); let url = format!("https://api.spotify.com/v1/search?q={}&type=track", req.query); let resp = moosync_edk::http::get(&url, None)?; if resp.is_success() { info!("Search API response: {}", resp.text().unwrap_or_default()); } Ok(SearchResult { songs: vec![], artists: vec![], playlists: vec![], albums: vec![], genres: vec![], }) } } }
Using the API
The extension development kit provides APIs to interact with the main app and perform external network operations.
Fetching App State
For this example lets consider the getCurrentSong API.
getCurrentSong returns the actively playing song.
#![allow(unused)] fn main() { impl Provider for SampleExtension { fn scrobble(&self, _req: ScrobbleRequest) -> api::MoosyncResult<()> { let song = get_current_song()?; if let Some(title) = song.and_then(|s| s.song).and_then(|i| i.title) { info!("Currently playing song: {}", title); } Ok(()) } } }
Making HTTP Requests
Extensions can make outgoing HTTP requests through the host runner using the SDK's HTTP APIs. Both single requests and batch/parallel requests are supported.
Permissions: To communicate with external servers, specify allowed hosts in your extension manifest (
package.json) under"allowed_hosts"(for example,["api.spotify.com", "*.last.fm"]).
#![allow(unused)] fn main() { pub fn search_and_fetch_details(query: &str) -> api::MoosyncResult<()> { use moosync_edk::http::{self, HttpRequest}; let url = format!("https://api.spotify.com/v1/search?q={}&type=track", query); let resp = http::get(&url, None)?; if resp.is_success() { info!("Search response: {}", resp.text().unwrap_or_default()); } let post_req = HttpRequest::post("https://api.spotify.com/v1/playlists") .header("Authorization", "Bearer token123") .body(r#"{"name":"My Playlist"}"#.as_bytes().to_vec()) .timeout_ms(5000); let post_resp = http::request(&post_req)?; info!("Create playlist status: {}", post_resp.status_code); let track_urls = vec![ "https://api.spotify.com/v1/tracks/1", "https://api.spotify.com/v1/tracks/2", ]; let responses = http::batch_get(&track_urls, None)?; for r in responses { info!("Track status: {}", r.status_code); } Ok(()) } }
Full extension example
Here is a complete extension example combining registration, provider capabilities, and API usage:
#![allow(unused)] #![no_main] fn main() { use moosync_edk::{ RequestedSearchResultRequest, ScrobbleRequest, SearchResult, api::{ self, Accounts, ContextMenu, DatabaseEvents, Extension, PlayerEvents, PreferenceEvents, Provider, extension_api::get_current_song, }, handler::register_extension, info, }; // ANCHOR: first_extension struct SampleExtension; impl Extension for SampleExtension {} impl Accounts for SampleExtension {} impl DatabaseEvents for SampleExtension {} impl PlayerEvents for SampleExtension {} impl PreferenceEvents for SampleExtension {} impl ContextMenu for SampleExtension {} #[unsafe(no_mangle)] pub extern "C" fn init() { info!("Initializing SampleExtension"); register_extension(Box::new(SampleExtension)).unwrap(); info!("Initialized SampleExtension"); } // ANCHOR_END: first_extension impl Provider for SampleExtension { // ANCHOR: provider fn search(&self, req: RequestedSearchResultRequest) -> api::MoosyncResult<SearchResult> { info!("Search requested for query: {}", req.query); let url = format!("https://api.spotify.com/v1/search?q={}&type=track", req.query); let resp = moosync_edk::http::get(&url, None)?; if resp.is_success() { info!("Search API response: {}", resp.text().unwrap_or_default()); } Ok(SearchResult { songs: vec![], artists: vec![], playlists: vec![], albums: vec![], genres: vec![], }) } // ANCHOR_END: provider // ANCHOR: api_usage fn scrobble(&self, _req: ScrobbleRequest) -> api::MoosyncResult<()> { let song = get_current_song()?; if let Some(title) = song.and_then(|s| s.song).and_then(|i| i.title) { info!("Currently playing song: {}", title); } Ok(()) } // ANCHOR_END: api_usage } // ANCHOR: http_usage pub fn search_and_fetch_details(query: &str) -> api::MoosyncResult<()> { use moosync_edk::http::{self, HttpRequest}; let url = format!("https://api.spotify.com/v1/search?q={}&type=track", query); let resp = http::get(&url, None)?; if resp.is_success() { info!("Search response: {}", resp.text().unwrap_or_default()); } let post_req = HttpRequest::post("https://api.spotify.com/v1/playlists") .header("Authorization", "Bearer token123") .body(r#"{"name":"My Playlist"}"#.as_bytes().to_vec()) .timeout_ms(5000); let post_resp = http::request(&post_req)?; info!("Create playlist status: {}", post_resp.status_code); let track_urls = vec![ "https://api.spotify.com/v1/tracks/1", "https://api.spotify.com/v1/tracks/2", ]; let responses = http::batch_get(&track_urls, None)?; for r in responses { info!("Track status: {}", r.status_code); } Ok(()) } // ANCHOR_END: http_usage }
Language API Documentation
API reference documentation for each supported language: