super-productivity/docs/plugin-development.md
2025-06-30 21:46:58 +02:00

18 KiB

Super Productivity Plugin Development Guide

This guide covers everything you need to know about creating plugins for Super Productivity.

Table of Contents

Quick Start

1. Use the Plugin Boilerplate or copy example plugin

https://github.com/johannesjo/super-productivity/tree/master/packages/plugin-dev/boilerplate-solid-js https://github.com/johannesjo/super-productivity/tree/master/packages/plugin-dev/yesterday-tasks-plugin

2. Basic Plugin Structure

my-plugin/
├── manifest.json      # Plugin metadata (required)
├── plugin.js          # Main plugin code (optional)
├── index.html         # UI interface (optional)
└── icon.svg           # Plugin icon (optional)

3. Minimal Example

manifest.json:

{
  "id": "hello-world",
  "name": "Hello World Plugin",
  "version": "1.0.0",
  "description": "My first Super Productivity plugin",
  "manifestVersion": 1,
  "minSupVersion": "8.0.0"
}

plugin.js:

console.log('Hello World plugin loaded!');

// Show a notification
PluginAPI.showSnack({
  msg: 'Hello from my plugin!',
  type: 'SUCCESS',
});

// Register a header button
PluginAPI.registerHeaderButton({
  label: 'Hello',
  icon: 'waving_hand',
  onClick: () => {
    PluginAPI.showSnack({
      msg: 'Button clicked!',
      type: 'INFO',
    });
  },
});

Plugin Manifest

The manifest.json file is required for all plugins and defines the plugin's metadata and configuration.

Manifest Fields

Field Type Required Description
id string Unique identifier for your plugin (use kebab-case)
name string Display name shown to users
version string Semantic version (e.g., "1.0.0")
description string Brief description of what your plugin does
manifestVersion number Currently must be 1
minSupVersion string Minimum Super Productivity version required
author string Plugin author name
homepage string Plugin website or repository URL
icon string Path to icon file (SVG recommended)
iFrame boolean Whether plugin uses iframe UI (default: false)
sidePanel boolean Show plugin in side panel (default: false)
permissions string[] The permissions the plugin needs (e.g., ["nodeExecution"])
hooks string[] App events to listen to

Complete Manifest Example

{
  "id": "my-advanced-plugin",
  "name": "My Advanced Plugin",
  "version": "2.1.0",
  "description": "An advanced plugin with UI and hooks",
  "manifestVersion": 1,
  "minSupVersion": "8.0.0",
  "author": "John Doe",
  "homepage": "https://github.com/johndoe/my-plugin",
  "icon": "icon.svg",
  "iFrame": true,
  "sidePanel": false,
  "permissions": ["nodeExecution"],
  "hooks": ["taskComplete", "taskUpdate", "currentTaskChange"]
}

Plugin Types

1. JavaScript Plugins (plugin.js)

Pure JavaScript plugins that run in a sandboxed environment with full API access.

Use when:

  • You need to register UI components (buttons, menu items)
  • You want to listen to app hooks/events
  • You need programmatic interaction with tasks/projects

Example:

// Register multiple UI elements
PluginAPI.registerHeaderButton({
  label: 'My Button',
  icon: 'star',
  onClick: async () => {
    const tasks = await PluginAPI.getTasks();
    console.log(`You have ${tasks.length} tasks`);
  },
});

PluginAPI.registerHook(PluginAPI.Hooks.TASK_COMPLETE, (taskId) => {
  console.log(`Task ${taskId} completed!`);
});

2. HTML/Iframe Plugins (index.html)

Plugins that render custom UI in a sandboxed iframe.

Use when:

  • You need custom UI/visualizations
  • You want to display charts, forms, or complex interfaces
  • You prefer working with HTML/CSS

Important: When using iframes, you must inline all CSS and JavaScript directly in the HTML file. External stylesheets and scripts are blocked for security reasons.

Example index.html:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>My Plugin UI</title>

    <!-- CSS must be inlined -->
    <style>
      body {
        font-family: Arial, sans-serif;
        padding: 20px;
        background: #f5f5f5;
      }

      .task-list {
        background: white;
        border-radius: 8px;
        padding: 16px;
        box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
      }

      .task-item {
        padding: 8px;
        border-bottom: 1px solid #eee;
      }

      button {
        background: #4caf50;
        color: white;
        border: none;
        padding: 10px 20px;
        border-radius: 4px;
        cursor: pointer;
      }

      button:hover {
        background: #45a049;
      }
    </style>
  </head>
  <body>
    <h1>My Plugin</h1>
    <div id="content">
      <button id="loadTasks">Load Tasks</button>
      <div
        id="taskList"
        class="task-list"
      ></div>
    </div>

    <!-- JavaScript must be inlined -->
    <script>
      document.getElementById('loadTasks').addEventListener('click', async () => {
        try {
          const tasks = await PluginAPI.getTasks();
          const taskList = document.getElementById('taskList');

          taskList.innerHTML = '<h3>Your Tasks:</h3>';

          tasks.forEach((task) => {
            const taskEl = document.createElement('div');
            taskEl.className = 'task-item';
            taskEl.textContent = task.title;
            taskList.appendChild(taskEl);
          });

          PluginAPI.showSnack({
            msg: `Loaded ${tasks.length} tasks`,
            type: 'SUCCESS',
          });
        } catch (error) {
          console.error('Error loading tasks:', error);
          PluginAPI.showSnack({
            msg: 'Failed to load tasks',
            type: 'ERROR',
          });
        }
      });

      // Error handling
      window.addEventListener('error', (event) => {
        console.error('Plugin error:', event.error);
      });
    </script>
  </body>
</html>

3. Hybrid Plugins

Combine both plugin.js and index.html for maximum flexibility.

plugin.js:

// Register UI elements and hooks
PluginAPI.registerSidePanelButton({
  label: 'Open My Plugin',
  icon: 'dashboard',
  onClick: () => {
    PluginAPI.showIndexHtmlAsView();
  },
});

// Listen for events and update iframe
PluginAPI.registerHook(PluginAPI.Hooks.TASK_UPDATE, () => {
  // Notify iframe about task update
  // (iframe will receive this via PluginAPI events)
});

Available API Methods

Data Operations

Tasks

  • getTasks() - Get all active tasks
  • getArchivedTasks() - Get archived tasks
  • getCurrentContextTasks() - Get tasks in current context
  • addTask(task) - Create a new task
  • updateTask(taskId, updates) - Update existing task

Projects

  • getAllProjects() - Get all projects
  • addProject(project) - Create new project
  • updateProject(projectId, updates) - Update project

Tags

  • getAllTags() - Get all tags
  • addTag(tag) - Create new tag
  • updateTag(tagId, updates) - Update tag

UI Operations

Notifications

// Show snackbar notification
PluginAPI.showSnack({
  msg: 'Operation completed!',
  type: 'SUCCESS', // SUCCESS, ERROR, INFO, WARNING
  ico: 'check', // Optional Material icon
  actionStr: 'Undo', // Optional action button
  actionFn: () => console.log('Undo clicked'),
});

// System notification
PluginAPI.notify({
  title: 'Task Complete',
  body: 'Great job!',
  ico: 'done',
});

Dialogs

// Open a dialog
const result = await PluginAPI.openDialog({
  title: 'Confirm Action',
  content: 'Are you sure?',
  okBtnLabel: 'Yes',
  cancelBtnLabel: 'No',
});

Registration Methods (plugin.js only)

Header Button

PluginAPI.registerHeaderButton({
  id: 'my-header-btn', // Optional unique ID
  label: 'Click Me',
  icon: 'star', // Material icon name
  onClick: () => {
    console.log('Header button clicked');
  },
});

Menu Entry

PluginAPI.registerMenuEntry({
  label: 'My Plugin Action',
  icon: 'extension',
  onClick: () => {
    console.log('Menu item clicked');
  },
});

Side Panel Button

PluginAPI.registerSidePanelButton({
  label: 'My Panel',
  icon: 'dashboard',
  onClick: () => {
    PluginAPI.showIndexHtmlAsView();
  },
});

Keyboard Shortcut

PluginAPI.registerShortcut({
  keys: 'ctrl+shift+p',
  label: 'My Plugin Shortcut',
  action: () => {
    console.log('Shortcut triggered');
  },
});

Hooks

// Available hooks
const hooks = {
  TASK_COMPLETE: 'taskComplete',
  TASK_UPDATE: 'taskUpdate',
  TASK_DELETE: 'taskDelete',
  CURRENT_TASK_CHANGE: 'currentTaskChange',
  FINISH_DAY: 'finishDay',
  LANGUAGE_CHANGE: 'languageChange',
  PERSISTED_DATA_UPDATE: 'persistedDataUpdate',
  ACTION: 'action',
};

// Register hook listener
PluginAPI.registerHook(PluginAPI.Hooks.TASK_COMPLETE, (taskId) => {
  console.log(`Task ${taskId} completed!`);
});

// Listen to Redux actions
PluginAPI.registerHook(PluginAPI.Hooks.ACTION, (action) => {
  if (action.type === 'ADD_TASK_SUCCESS') {
    console.log('New task added:', action.payload);
  }
});

Data Persistence

// Save plugin data
await PluginAPI.persistDataSynced('myKey', { count: 42 });

// Load saved data
const data = await PluginAPI.loadSyncedData('myKey');
console.log(data); // { count: 42 }

Best Practices

1. Performance

  • Lazy load resources: Don't load everything on plugin initialization
  • Debounce expensive operations: Use throttling for frequent events
  • Clean up resources: Remove event listeners when appropriate

2. Error Handling

Always wrap async operations in try-catch blocks:

async function loadData() {
  try {
    const tasks = await PluginAPI.getTasks();
    // Process tasks
  } catch (error) {
    console.error('Failed to load tasks:', error);
    PluginAPI.showSnack({
      msg: 'Error loading tasks',
      type: 'ERROR',
    });
  }
}

3. User Experience

  • Provide feedback: Show loading states and confirmations
  • Be non-intrusive: Don't spam notifications
  • Follow the app's design: Use Material icons and consistent styling
  • Respect user preferences: Check dark mode, language settings

4. Security

  • Validate inputs: Never trust user input
  • Avoid eval(): Use safe JSON parsing instead
  • Sanitize HTML: If displaying user content
  • Request minimal permissions: Only what you need

5. Iframe Development

When developing iframe plugins:

  1. Inline everything: CSS and JavaScript must be in the HTML file
  2. Wait for API ready: Listen for the PluginApiReady event
  3. Handle errors gracefully: Iframes can fail to load
  4. Keep it lightweight: Iframes add overhead
<!-- Good: Everything inlined -->
<!DOCTYPE html>
<html>
  <head>
    <style>
      /* All styles here */
    </style>
  </head>
  <body>
    <div id="app"></div>
    <script>
      // All JavaScript here
    </script>
  </body>
</html>

Security Considerations

Sandboxing

  • JavaScript plugins run in isolated VM contexts
  • Iframe plugins run in sandboxed iframes with restricted permissions
  • No access to Node.js APIs unless explicitly granted
  • No access to file system or network unless through API

API Restrictions

In iframe context, these methods are NOT available:

  • registerHeaderButton()
  • registerMenuEntry()
  • registerSidePanelButton()
  • registerShortcut()
  • registerHook()
  • execNodeScript()

Content Security Policy

  • External scripts/styles are blocked in iframes
  • Only same-origin resources are allowed
  • Inline scripts must be within the HTML file

Testing Your Plugin

1. Local Development

  1. Enable Developer Mode in Super Productivity settings
  2. Use "Load Plugin from Folder" to test your plugin
  3. Open DevTools (F12) to see console logs
  4. Use the API Test Plugin as reference

2. Debugging Tips

// Add debug logging
const DEBUG = true;

function log(...args) {
  if (DEBUG) {
    console.log('[MyPlugin]', ...args);
  }
}

// Test API methods
async function testAPI() {
  log('Testing getTasks...');
  const tasks = await PluginAPI.getTasks();
  log('Tasks:', tasks);

  log('Testing showSnack...');
  PluginAPI.showSnack({
    msg: 'API test successful!',
    type: 'SUCCESS',
  });
}

3. Common Issues

Plugin not loading:

  • Check manifest.json syntax
  • Verify minSupVersion compatibility
  • Look for errors in console

API methods failing:

  • Ensure PluginAPI is ready before use
  • Check if method is available in current context
  • Verify permissions in manifest

Iframe not displaying:

  • Check that all resources are inlined
  • Verify no external dependencies
  • Look for CSP violations in console

Examples

Task Reporter Plugin

// plugin.js
let reportInterval;

PluginAPI.registerHeaderButton({
  label: 'Start Report',
  icon: 'assessment',
  onClick: () => {
    if (reportInterval) {
      clearInterval(reportInterval);
      reportInterval = null;
      PluginAPI.showSnack({ msg: 'Reporting stopped', type: 'INFO' });
    } else {
      reportInterval = setInterval(generateReport, 3600000); // Every hour
      PluginAPI.showSnack({ msg: 'Reporting started', type: 'SUCCESS' });
    }
  },
});

async function generateReport() {
  const tasks = await PluginAPI.getCurrentContextTasks();
  const completed = tasks.filter((t) => t.isDone).length;
  const total = tasks.length;

  PluginAPI.notify({
    title: 'Hourly Report',
    body: `Completed ${completed} of ${total} tasks`,
    ico: 'pie_chart',
  });
}

Custom Dashboard Plugin

<!-- index.html -->
<!DOCTYPE html>
<html>
  <head>
    <style>
      body {
        font-family: Arial, sans-serif;
        margin: 0;
        padding: 20px;
        background: #f0f0f0;
      }

      .dashboard {
        display: grid;
        grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
        gap: 20px;
      }

      .stat-card {
        background: white;
        padding: 20px;
        border-radius: 8px;
        box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
        text-align: center;
      }

      .stat-value {
        font-size: 36px;
        font-weight: bold;
        color: #2196f3;
      }

      .stat-label {
        color: #666;
        margin-top: 8px;
      }
    </style>
  </head>
  <body>
    <h1>Task Dashboard</h1>
    <div
      class="dashboard"
      id="dashboard"
    >
      <div class="stat-card">
        <div
          class="stat-value"
          id="totalTasks"
        >
          -
        </div>
        <div class="stat-label">Total Tasks</div>
      </div>
      <div class="stat-card">
        <div
          class="stat-value"
          id="completedTasks"
        >
          -
        </div>
        <div class="stat-label">Completed</div>
      </div>
      <div class="stat-card">
        <div
          class="stat-value"
          id="totalProjects"
        >
          -
        </div>
        <div class="stat-label">Projects</div>
      </div>
      <div class="stat-card">
        <div
          class="stat-value"
          id="totalTags"
        >
          -
        </div>
        <div class="stat-label">Tags</div>
      </div>
    </div>

    <script>
      window.addEventListener('PluginApiReady', async () => {
        async function updateDashboard() {
          try {
            const [tasks, projects, tags] = await Promise.all([
              PluginAPI.getTasks(),
              PluginAPI.getAllProjects(),
              PluginAPI.getAllTags(),
            ]);

            document.getElementById('totalTasks').textContent = tasks.length;
            document.getElementById('completedTasks').textContent = tasks.filter(
              (t) => t.isDone,
            ).length;
            document.getElementById('totalProjects').textContent = projects.length;
            document.getElementById('totalTags').textContent = tags.length;
          } catch (error) {
            console.error('Dashboard update failed:', error);
          }
        }

        // Initial load
        updateDashboard();

        // Refresh every 30 seconds
        setInterval(updateDashboard, 30000);
      });
    </script>
  </body>
</html>

Resources

Contributing

If you create a useful plugin, consider:

  1. Publishing it to npm
  2. Submitting a PR to add it to the community plugins list
  3. Sharing it in the Super Productivity discussions

Happy plugin development! 🚀