Appearance
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 dataOS uses
index.vueas the main view, notwidget.vue. There is nowidget.vuein 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": []
}| Field | Description |
|---|---|
versionNumber | Internal version index (start at "0") |
version | Semver version string shown in store |
name | Plugin identifier — must match the folder name |
icon | Plugin icon path (relative to version folder) |
type | Always "OS" |
script | Entry script filename |
id | Unique store ID (assigned on upload) |
packages | Optional 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
- Create the plugin folder under the OS plugins directory
- Write
setting.jsonandscript.js - Run
mot runto link your plugin folder to the running OS instance - OS reloads the plugin window on changes
- When ready, run
mot buildthen upload via the store