MCPInstall

desktop-apps

Desktop application development with Electron and Tauri

Installs
100
Last 24h
—
Trending rank
—
Security
Not audited

Install

Works with every agent the skills CLI supports.

npx skills add https://github.com/miles990/claude-software-skills --skill desktop-apps
  1. 01

    Install

    Run the command in your project. The CLI asks which agents to install for.

  2. 02

    Describe the task

    Ask your agent for something this skill covers. It loads desktop-apps automatically when relevant.

  3. 03

    Review the result

    The agent follows the SKILL.md steps, scripts and references bundled with the skill.

SKILL.md

The instructions your agent reads when the skill is triggered.

Desktop Application Development

Overview

Building cross-platform desktop applications using web technologies with Electron and Tauri.


Electron

Main Process

// main.ts
import { app, BrowserWindow, ipcMain, dialog, Menu } from 'electron';
import path from 'path';

let mainWindow: BrowserWindow | null = null;

function createWindow() {
  mainWindow = new BrowserWindow({
    width: 1200,
    height: 800,
    minWidth: 800,
    minHeight: 600,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js'),
      contextIsolation: true,
      nodeIntegration: false,
    },
    titleBarStyle: 'hiddenInset', // macOS
    frame: process.platform !== 'darwin',
  });

  // Load the app
  if (process.env.NODE_ENV === 'development') {
    mainWindow.loadURL('http://localhost:3000');
    mainWindow.webContents.openDevTools();
  } else {
    mainWindow.loadFile(path.join(__dirname, '../dist/index.html'));
  }

  // Window events
  mainWindow.on('closed', () => {
    mainWindow = null;
  });
}

// App lifecycle
app.whenReady().then(() => {
  createWindow();
  createMenu();

  app.on('activate', () => {
    if (BrowserWindow.getAllWindows().length === 0) {
      createWindow();
    }
  });
});

app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') {
    app.quit();
  }
});

// IPC handlers
ipcMain.handle('dialog:openFile', async () => {
  const result = await dialog.showOpenDialog(mainWindow!, {
    properties: ['openFile'],
    filters: [
      { name: 'Documents', extensions: ['txt', 'md', 'json'] },
    ],
  });

  if (!result.canceled && result.filePaths.length > 0) {
    return result.filePaths[0];
  }
  return null;
});

ipcMain.handle('dialog:saveFile', async (_, content: string) => {
  const result = await dialog.showSaveDialog(mainWindow!, {
    filters: [{ name: 'JSON', extensions: ['json'] }],
  });

  if (!result.canceled && result.filePath) {
    await fs.writeFile(result.filePath, content);
    return result.filePath;
  }
  return null;
});

ipcMain.handle('app:getVersion', () => app.getVersion());

// Auto-updater
import { autoUpdater } from 'electron-updater';

autoUpdater.checkForUpdatesAndNotify();

autoUpdater.on('update-available', () => {
  mainWindow?.webContents.send('update-available');
});

autoUpdater.on('update-downloaded', () => {
  mainWindow?.webContents.send('update-downloaded');
});

ipcMain.handle('app:installUpdate', () => {
  autoUpdater.quitAndInstall();
});

Preload Script

// preload.ts
import { contextBridge, ipcRenderer } from 'electron';

// Expose safe APIs to renderer
contextBridge.exposeInMainWorld('electronAPI', {
  // File operations
  openFile: () => ipcRenderer.invoke('dialog:openFile'),
  saveFile: (content: string) => ipcRenderer.invoke('dialog:saveFile', content),
  readFile: (path: string) => ipcRenderer.invoke('fs:readFile', path),
  writeFile: (path: string, content: string) =>
    ipcRenderer.invoke('fs:writeFile', path, content),

  // App info
  getVersion: () => ipcRenderer.invoke('app:getVersion'),
  getPlatform: () => process.platform,

  // Updates
  installUpdate: () => ipcRenderer.invoke('app:installUpdate'),
  onUpdateAvailable: (callback: () => void) => {
    ipcRenderer.on('update-available', callback);
    return () => ipcRenderer.removeListener('update-available', callback);
  },
  onUpdateDownloaded: (callback: () => void) => {
    ipcRenderer.on('update-downloaded', callback);
    return () => ipcRenderer.removeListener('update-downloaded', callback);
  },

  // Window controls
  minimize: () => ipcRenderer.send('window:minimize'),
  maximize: () => ipcRenderer.send('window:maximize'),
  close: () => ipcRenderer.send('window:close'),

  // Native notifications
  showNotification: (title: string, body: string) =>
    ipcRenderer.invoke('notification:show', title, body),
});

// TypeScript types for renderer
declare global {
  interface Window {
    electronAPI: {
      openFile: () => Promise<string | null>;
      saveFile: (content: string) => Promise<string | null>;
      readFile: (path: string) => Promise<string>;
      writeFile: (path: string, content: string) => Promise<void>;
      getVersion: () => Promise<string>;
      getPlatform: () => string;
      installUpdate: () => Promise<void>;
      onUpdateAvailable: (callback: () => void) => () => void;
      onUpdateDownloaded: (callback: () => void) => () => void;
      minimize: () => void;
      maximize: () => void;
      close: () => void;
      showNotification: (title: string, body: string) => Promise<void>;
    };
  }
}

Renderer (React)

// App.tsx
function App() {
  const [updateAvailable, setUpdateAvailable] = useState(false);
  const [updateReady, setUpdateReady] = useState(false);

  useEffect(() => {
    const removeAvailable = window.electronAPI.onUpdateAvailable(() => {
      setUpdateAvailable(true);
    });

    const removeDownloaded = window.electronAPI.onUpdateDownloaded(() => {
      setUpdateReady(true);
    });

    return () => {
      removeAvailable();
      removeDownloaded();
    };
  }, []);

  const handleOpenFile = async () => {
    const filePath = await window.electronAPI.openFile();
    if (filePath) {
      const content = await window.electronAPI.readFile(filePath);
      // Handle file content
    }
  };

  const handleSaveFile = async () => {
    const content = JSON.stringify(data, null, 2);
    await window.electronAPI.saveFile(content);
  };

  return (
    <div className="app">
      {/* Custom title bar for frameless window */}
      <TitleBar />

      <main>
        <button onClick={handleOpenFile}>Open File</button>
        <button onClick={handleSaveFile}>Save File</button>

        {updateReady && (
          <button onClick={() => window.electronAPI.installUpdate()}>
            Install Update & Restart
          </button>
        )}
      </main>
    </div>
  );
}

// Custom title bar component
function TitleBar() {
  const platform = window.electronAPI.getPlatform();

  return (
    <div className="title-bar" style={{ WebkitAppRegion: 'drag' }}>
      <span className="title">My App</span>

      {platform !== 'darwin' && (
        <div className="window-controls" style={{ WebkitAppRegion: 'no-drag' }}>
          <button onClick={() => window.electronAPI.minimize()}>−</button>
          <button onClick={() => window.electronAPI.maximize()}>□</button>
          <button onClick={() => window.electronAPI.close()}>×</button>
        </div>
      )}
    </div>
  );
}

Tauri

Rust Backend

// src-tauri/src/main.rs
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]

use tauri::{CustomMenuItem, Menu, MenuItem, Submenu};
use std::fs;

// Commands callable from frontend
#[tauri::command]
fn read_file(path: String) -> Result<String, String> {
    fs::read_to_string(&path).map_err(|e| e.to_string())
}

#[tauri::command]
fn write_file(path: String, content: String) -> Result<(), String> {
    fs::write(&path, &content).map_err(|e| e.to_string())
}

#[tauri::command]
fn get_app_version() -> String {
    env!("CARGO_PKG_VERSION").to_string()
}

#[tauri::command]
async fn perform_heavy_task(input: String) -> Result<String, String> {
    // Run CPU-intensive work in background
    tokio::task::spawn_blocking(move || {
        // Heavy computation here
        format!("Processed: {}", input)
    })
    .await
    .map_err(|e| e.to_string())
}

fn main() {
    let menu = Menu::new()
        .add_submenu(Submenu::new(
            "File",
            Menu::new()
                .add_item(CustomMenuItem::new("open", "Open").accelerator("CmdOrCtrl+O"))
                .add_item(CustomMenuItem::new("save", "Save").accelerator("CmdOrCtrl+S"))
                .add_native_item(MenuItem::Separator)
                .add_native_item(MenuItem::Quit),
        ))
        .add_submenu(Submenu::new(
            "Edit",
            Menu::new()
                .add_native_item(MenuItem::Undo)
                .add_native_item(MenuItem::Redo)
                .add_native_item(MenuItem::Separator)
                .add_native_item(MenuItem::Cut)
                .add_native_item(MenuItem::Copy)
                .add_native_item(MenuItem::Paste),
        ));

    tauri::Builder::default()
        .menu(menu)
        .on_menu_event(|event| {
            match event.menu_item_id() {
                "open" => {
                    event.window().emit("menu-open", {}).unwrap();
                }
                "save" => {
                    event.window().emit("menu-save", {}).unwrap();
                }
                _ => {}
            }
        })
        .invoke_handler(tauri::generate_handler![
            read_file,
            write_file,
            get_app_version,
            perform_heavy_task,
        ])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

Tauri Configuration

// src-tauri/tauri.conf.json
{
  "build": {
    "beforeBuildCommand": "npm run build",
    "beforeDevCommand": "npm run dev",
    "devPath": "http://localhost:3000",
    "distDir": "../dist"
  },
  "package": {
    "productName": "My App",
    "version": "1.0.0"
  },
  "tauri": {
    "allowlist": {
      "all": false,
      "dialog": {
        "all": true
      },
      "fs": {
        "all": true,
        "scope": ["$DOCUMENT/*", "$DOWNLOAD/*"]
      },
      "shell": {
        "open": true
      },
      "notification": {
        "all": true
      }
    },
    "bundle": {
      "active": true,
      "icon": [
        "icons/32x32.png",
        "icons/128x128.png",
        "icons/icon.icns",
        "icons/icon.ico"
      ],
      "identifier": "com.example.myapp",
      "targets": "all"
    },
    "windows": [
      {
        "title": "My App",
        "width": 1200,
        "height": 800,
        "minWidth": 800,
        "minHeight": 600,
        "resizable": true,
        "fullscreen": false
      }
    ],
    "updater": {
      "active": true,
      "endpoints": ["https://releases.example.com/{{target}}/{{current_version}}"],
      "pubkey": "YOUR_PUBLIC_KEY"
    }
  }
}

Frontend Integration

// Using Tauri APIs
import { invoke } from '@tauri-apps/api/tauri';
import { open, save } from '@tauri-apps/api/dialog';
import { readTextFile, writeTextFile } from '@tauri-apps/api/fs';
import { sendNotification } from '@tauri-apps/api/notification';
import { listen } from '@tauri-apps/api/event';

// Call Rust commands
async function readFile(path: string): Promise<string> {
  return invoke('read_file', { path });
}

async function writeFile(path: string, content: string): Promise<void> {
  return invoke('write_file', { path, content });
}

// Use Tauri dialog
async function openFileDialog() {
  const selected = await open({
    multiple: false,
    filters: [{ name: 'Documents', extensions: ['txt', 'md', 'json'] }],
  });

  if (selected && typeof selected === 'string') {
    const content = await readTextFile(selected);
    return { path: selected, content };
  }
  return null;
}

async function saveFileDialog(content: string) {
  const filePath = await save({
    filters: [{ name: 'JSON', extensions: ['json'] }],
  });

  if (filePath) {
    await writeTextFile(filePath, content);
    return filePath;
  }
  return null;
}

// Listen for menu events
listen('menu-open', async () => {
  const file = await openFileDialog();
  if (file) {
    // Handle file
  }
});

// Send notification
async function notify(title: string, body: string) {
  await sendNotification({ title, body });
}

Local Storage & Database

// SQLite with better-sqlite3 (Electron)
import Database from 'better-sqlite3';

const db = new Database('app.db');

// Initialize schema
db.exec(`
  CREATE TABLE IF NOT EXISTS documents (
    id TEXT PRIMARY KEY,
    title TEXT NOT NULL,
    content TEXT,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
  )
`);

// CRUD operations
const insertDoc = db.prepare(
  'INSERT INTO documents (id, title, content) VALUES (?, ?, ?)'
);

const getDoc = db.prepare('SELECT * FROM documents WHERE id = ?');

const getAllDocs = db.prepare('SELECT * FROM documents ORDER BY updated_at DESC');

const updateDoc = db.prepare(
  'UPDATE documents SET title = ?, content = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?'
);

const deleteDoc = db.prepare('DELETE FROM documents WHERE id = ?');

// Usage
insertDoc.run(uuid(), 'New Document', '');
const doc = getDoc.get('doc-id');
const docs = getAllDocs.all();

Related Skills

  • [[frontend]] - Web technologies
  • [[system-design]] - Application architecture
  • [[devops-cicd]] - Desktop app distribution

FAQ

What does the desktop-apps skill do?

Desktop application development with Electron and Tauri

How do I install desktop-apps?

Run npx skills add https://github.com/miles990/claude-software-skills --skill desktop-apps in your project. The skills CLI detects the agents you use and copies the skill into the right folder. For Claude Code you can also place the skill folder at ~/.claude/skills/desktop-apps/ (personal) or .claude/skills/desktop-apps/ (project).

Which AI agents can use desktop-apps?

Any agent that supports the open Agent Skills format (a folder with a SKILL.md file), including Claude Code, Codex, Cursor, GitHub Copilot, Gemini CLI, Windsurf, Cline, OpenCode and more.

Is desktop-apps safe to install?

No partner security audit is available yet. Skills can run commands and scripts, so read the SKILL.md and any bundled files before installing.

Who maintains desktop-apps?

It is published by miles990 in miles990/claude-software-skills. It has 100 recorded installs.

More from miles990/claude-software-skills

View all →

code-quality

miles990/claude-software-skills

Clean code principles, SOLID, and code review practices

devops-cicd

miles990/claude-software-skills

CI/CD pipelines, infrastructure as code, and deployment strategies

DevOps & Cloud606 installs

game-development

miles990/claude-software-skills

Game development patterns, architectures, and best practices

Media & Creative411 installs

project-management

miles990/claude-software-skills

Open the skill to read its SKILL.md instructions.

389 installs

e-commerce

miles990/claude-software-skills

E-commerce platforms, payment processing, and shopping cart patterns

Backend & APIs376 installs

saas-platforms

miles990/claude-software-skills

Open the skill to read its SKILL.md instructions.

362 installs

Popular other skills

View category →

hyperframes-cli

heygen-com/hyperframes

The HyperFrames CLI by job - scaffold, check, look at frames, edit the timeline, preview, render locally or in the cloud, publish, project history - plus reading its JSON and recovering from build or render failures.

1.3M installs

triage

mattpocock/skills

Move issues and external PRs through a state machine of triage roles, categorise, verify, grill if needed, and write agent-ready briefs.

1M installs

ask-matt

mattpocock/skills

Ask which skill or flow fits your situation. A router over the skills in this repo.

778.4K installs

to-questionnaire

mattpocock/skills

Turn a decision you can't fully answer into a questionnaire for someone else to fill in.

508.9K installs

wait-what

mattpocock/skills

Stop. That last message did not land: re-pitch it.

459.7K installs

loop-me

mattpocock/skills

Grill me about specs for the workflows I want to build, within this workspace.

447.9K installs