Skip to content

OS Development Guide

Prerequisites

  • Node.js 18+
  • Motanamy CLI (npm install -g motanamy-cli)
  • A running Motanamy OS instance for testing

File Structure

Every OS plugin follows this structure:

my-plugin/
├── 1.0.0/                  ← version folder
│   ├── setting.json        ← plugin manifest
│   ├── script.js           ← registration script (runs on load)
│   ├── index.vue           ← main app view (root route)
│   ├── setting.vue         ← settings page (optional)
│   ├── README-AR.md        ← Arabic documentation
│   ├── README-EN.md        ← English documentation
│   └── images/             ← plugin images/icons
└── database/
    └── my-plugin/
        └── data.json       ← initial data

OS uses index.vue as the main view, not widget.vue. There is no widget.vue in OS plugins.

When you release a new version, add a new version folder alongside 1.0.0/:

my-plugin/
├── 1.0.0/
├── 1.1.0/          ← new version
└── database/

setting.json

json
{
    "versionNumber": "0",
    "version": "1.0.0",
    "name": "my-plugin",
    "icon": "images/logo.svg",
    "type": "OS",
    "script": "script.js",
    "id": "_uniqueId",
    "packages": []
}
FieldDescription
versionNumberInternal version index (start at "0")
versionSemver version string shown in store
namePlugin identifier — must match the folder name
iconPlugin icon path (relative to version folder)
typeAlways "OS"
scriptEntry script filename
idUnique store ID (assigned on upload)
packagesOptional npm package dependencies

script.js

script.js runs when the OS plugin window opens. Register your plugin by assigning a function to plugins["name"]. Inside the function, this exposes all registration methods.

js
plugins["my-plugin"] = function () {
    // 1. Set window frame
    this.setFrame({
        title: "Motanamy OS | My Plugin",
        icon: {
            type: "icon",
            name: "mdi mdi-puzzle-outline"
        }
    });

    // 2. Register translations
    this.addLanguage("ar", "my-plugin", { name: "البرنامج" });
    this.addLanguage("en", "my-plugin", { name: "My Plugin" });

    // 3. Set toolbar buttons
    this.setToolbar([
        { icon: "pi pi-cog", to: "/setting" }
    ]);

    // 4. Register page routes
    this.addPage({
        name: "/[my-plugin]index.vue",
        path: "",
        file: ["index.vue"]
    });

    this.addPage({
        name: "/[my-plugin]setting.vue",
        path: "/setting",
        file: ["setting.vue"]
    });
}

See the script.js API Reference for all available methods.

index.vue

The root page of your plugin — loaded when the OS window opens. This is the primary UI of your plugin:

vue
<template>
    <div class="h-full flex align-items-center justify-content-center">
        <div class="card w-full">
            <h5>{{ $t("my-plugin.name") }}</h5>
            <!-- main content -->
        </div>
    </div>
</template>

<script>
import api from 'api';

export default {
    data() {
        return { items: [] }
    },
    created() {
        this.items = api.database("my-plugin").readJson(["data.json"]);
    }
}
</script>

setting.vue

The settings page. Navigate back to root with $router.push('/'):

vue
<template>
    <div class="m-2">
        <div class="card flex align-items-center justify-content-between">
            <Button icon="pi pi-arrow-left" class="p-button-text" @click="$router.push('/')" />
            <h5 class="m-0">{{ $t("my-plugin.name") }}</h5>
            <div>
                <Button icon="pi pi-save" class="p-button-text" @click="save()" />
                <Button icon="pi pi-refresh" class="p-button-text" @click="init()" />
            </div>
        </div>
        <div class="card">
            <!-- settings content -->
        </div>
    </div>
</template>

Database

OS plugins use the api module to read and write local JSON files — same as SA:

js
import api from 'api';

// Read
const data = api.database("my-plugin").readJson(["data.json"]);

// Write
api.database("my-plugin").writeJson(["data.json"], data);

The initial state comes from database/my-plugin/data.json.

Native File Dialogs

OS plugins run in Electron and can open native file/folder dialogs via ipcRenderer:

js
import { ipcRenderer } from "electron";
import { basename } from "node:path";

// Open a file picker
ipcRenderer.invoke("openDialog", {
    title: "Select File",
    filters: [
        { name: "Audio", extensions: ["mp3", "ogg"] }
    ],
    properties: ["openFile", "multiSelections"]
}).then(files => {
    if (files) {
        files.forEach(filePath => {
            this.items.push({
                name: basename(filePath),
                path: filePath
            });
        });
    }
});

ipcRenderer is only available in OS plugins (Electron renderer process). Do not use it in EA or SA plugins.

Routing

OS plugins use root-relative paths — not /app/... like EA/SA:

js
// Root page
this.addPage({ path: "", ... });         // → /

// Sub-page
this.addPage({ path: "/setting", ... }); // → /setting
this.addPage({ path: "/detail/:id", ... }); // → /detail/:id

// Navigate in component
this.$router.push('/setting');
this.$router.push('/');
this.$router.push(`/detail/${id}`);

// Read param
const id = this.$route.params.id;

i18n Usage

vue
<template>
    <div>
        <h5>{{ $t("my-plugin.name") }}</h5>
        <Button :label="$t('button.save')" />
    </div>
</template>

Development Workflow

  1. Create the plugin folder under the OS plugins directory
  2. Write setting.json and script.js
  3. Run mot run to link your plugin folder to the running OS instance
  4. OS reloads the plugin window on changes
  5. When ready, run mot build then upload via the store

Resources