electrobun-native-ui

Native UI integration for Electrobun desktop applications including ApplicationMenu, ContextMenu, system Tray, native dialogs, keyboard shortcuts, and platform-specific UI patterns. This skill covers creating application menus with submenus and accelerators, context menus triggered by right-click, system tray icons with menus, file/folder dialogs, message boxes, notification systems, global keyboard shortcuts, menu item roles, dynamic menu updates, platform-specific menu conventions (macOS menu bar, Windows system menu), drag-and-drop integration, and native theming. Use when implementing application menus, adding system tray functionality, creating context menus, showing file pickers, implementing keyboard shortcuts, displaying notifications or dialogs, or building platform-native UI experiences. Triggers include "menu", "tray icon", "context menu", "file dialog", "shortcuts", "accelerator", "native dialog", "system tray", "notification", "menu bar", or "right-click menu".

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "electrobun-native-ui" with this command: npx skills add rajavijayach/electrobun-skills/rajavijayach-electrobun-skills-electrobun-native-ui

Electrobun Native UI

Comprehensive guide to native UI integration in Electrobun applications.

Application Menu

Basic Menu Structure

import { ApplicationMenu } from "electrobun/bun";

ApplicationMenu.setMenu([
  {
    label: "File",
    submenu: [
      {
        label: "New",
        accelerator: "CmdOrCtrl+N",
        action: () => createNewDocument()
      },
      {
        label: "Open...",
        accelerator: "CmdOrCtrl+O",
        action: () => openFile()
      },
      { type: "separator" },
      {
        label: "Save",
        accelerator: "CmdOrCtrl+S",
        action: () => saveFile()
      },
      {
        label: "Save As...",
        accelerator: "CmdOrCtrl+Shift+S",
        action: () => saveFileAs()
      },
      { type: "separator" },
      {
        label: "Quit",
        accelerator: "CmdOrCtrl+Q",
        action: () => app.quit()
      }
    ]
  },
  {
    label: "Edit",
    submenu: [
      { role: "undo" },
      { role: "redo" },
      { type: "separator" },
      { role: "cut" },
      { role: "copy" },
      { role: "paste" },
      { type: "separator" },
      { role: "selectAll" }
    ]
  },
  {
    label: "View",
    submenu: [
      {
        label: "Reload",
        accelerator: "CmdOrCtrl+R",
        action: () => currentWindow.reload()
      },
      {
        label: "Toggle DevTools",
        accelerator: "CmdOrCtrl+Shift+I",
        action: () => currentWindow.toggleDevTools()
      },
      { type: "separator" },
      {
        label: "Zoom In",
        accelerator: "CmdOrCtrl+Plus",
        action: () => adjustZoom(0.1)
      },
      {
        label: "Zoom Out",
        accelerator: "CmdOrCtrl+-",
        action: () => adjustZoom(-0.1)
      },
      {
        label: "Reset Zoom",
        accelerator: "CmdOrCtrl+0",
        action: () => resetZoom()
      }
    ]
  },
  {
    label: "Help",
    submenu: [
      {
        label: "Documentation",
        action: () => shell.openExternal("https://docs.myapp.com")
      },
      { type: "separator" },
      {
        label: "About",
        action: () => showAboutDialog()
      }
    ]
  }
]);

Built-in Roles

// Standard edit roles
{ role: "undo" }        // Undo last action
{ role: "redo" }        // Redo last action
{ role: "cut" }         // Cut selection
{ role: "copy" }        // Copy selection
{ role: "paste" }       // Paste from clipboard
{ role: "selectAll" }   // Select all content

// Window roles
{ role: "minimize" }    // Minimize window
{ role: "close" }       // Close window
{ role: "quit" }        // Quit application
{ role: "reload" }      // Reload current page
{ role: "forceReload" } // Force reload (clear cache)
{ role: "toggleDevTools" } // Toggle developer tools
{ role: "toggleFullScreen" } // Toggle fullscreen

// Zoom roles
{ role: "zoomIn" }      // Zoom in
{ role: "zoomOut" }     // Zoom out
{ role: "resetZoom" }   // Reset zoom to 100%

Dynamic Menus

class MenuManager {
  private currentMenu: any[] = [];
  
  updateRecentFiles(files: string[]) {
    const recentMenu = files.map(file => ({
      label: path.basename(file),
      action: () => openFile(file)
    }));
    
    // Find File menu and update Recent submenu
    const fileMenu = this.currentMenu.find(m => m.label === "File");
    const recentIndex = fileMenu.submenu.findIndex(
      (item: any) => item.label === "Recent"
    );
    
    if (recentIndex >= 0) {
      fileMenu.submenu[recentIndex] = {
        label: "Recent",
        submenu: recentMenu.length > 0 ? recentMenu : [
          { label: "No recent files", enabled: false }
        ]
      };
    }
    
    ApplicationMenu.setMenu(this.currentMenu);
  }
  
  setCheckState(menuPath: string[], checked: boolean) {
    // Navigate menu structure and update check state
    let current: any = this.currentMenu;
    
    for (let i = 0; i < menuPath.length - 1; i++) {
      current = current.find((m: any) => m.label === menuPath[i]);
      if (!current) return;
      current = current.submenu;
    }
    
    const item = current.find((m: any) => m.label === menuPath[menuPath.length - 1]);
    if (item) {
      item.checked = checked;
      ApplicationMenu.setMenu(this.currentMenu);
    }
  }
  
  enableMenuItem(menuPath: string[], enabled: boolean) {
    // Similar navigation to setCheckState
    // Set item.enabled = enabled
  }
}

// Usage
const menuManager = new MenuManager();
menuManager.updateRecentFiles(["/path/to/file1.txt", "/path/to/file2.txt"]);
menuManager.setCheckState(["View", "Show Sidebar"], true);

Platform-Specific Menus

function createMenu() {
  const isMac = process.platform === "darwin";
  
  const menu = [];
  
  // macOS app menu
  if (isMac) {
    menu.push({
      label: app.name,
      submenu: [
        { role: "about" },
        { type: "separator" },
        {
          label: "Preferences...",
          accelerator: "Cmd+,",
          action: () => showPreferences()
        },
        { type: "separator" },
        { role: "services" },
        { type: "separator" },
        { role: "hide" },
        { role: "hideOthers" },
        { role: "unhide" },
        { type: "separator" },
        { role: "quit" }
      ]
    });
  }
  
  // File menu
  menu.push({
    label: "File",
    submenu: [
      { label: "New", accelerator: "CmdOrCtrl+N", action: createNew },
      { label: "Open", accelerator: "CmdOrCtrl+O", action: openFile },
      // On Windows/Linux, add Quit here
      ...(!isMac ? [
        { type: "separator" },
        { label: "Exit", action: () => app.quit() }
      ] : [])
    ]
  });
  
  return menu;
}

Context Menu

Basic Context Menu

import { ContextMenu, BrowserWindow } from "electrobun/bun";

const win = new BrowserWindow({ /* ... */ });

win.on("context-menu", (event) => {
  ContextMenu.show([
    {
      label: "Copy",
      accelerator: "CmdOrCtrl+C",
      action: () => {
        // Copy selected text
      }
    },
    {
      label: "Paste",
      accelerator: "CmdOrCtrl+V",
      action: () => {
        // Paste from clipboard
      }
    },
    { type: "separator" },
    {
      label: "Inspect Element",
      action: () => {
        win.inspectElement(event.x, event.y);
      }
    }
  ]);
});

Dynamic Context Menu

// Webview sends context info
// In webview (index.ts):
document.addEventListener("contextmenu", (e) => {
  e.preventDefault();
  
  const target = e.target as HTMLElement;
  const context = {
    x: e.clientX,
    y: e.clientY,
    hasSelection: window.getSelection()?.toString().length > 0,
    isLink: target.tagName === "A",
    linkUrl: target.tagName === "A" ? (target as HTMLAnchorElement).href : null,
    isImage: target.tagName === "IMG",
    imageUrl: target.tagName === "IMG" ? (target as HTMLImageElement).src : null,
  };
  
  electroview.rpc.showContextMenu(context);
});

// Main process handles it
win.defineRpc({
  handlers: {
    async showContextMenu(context: any) {
      const menu = [];
      
      if (context.hasSelection) {
        menu.push(
          { label: "Copy", action: () => win.rpc.copy() },
          { type: "separator" }
        );
      }
      
      if (context.isLink) {
        menu.push(
          {
            label: "Open Link",
            action: () => shell.openExternal(context.linkUrl)
          },
          {
            label: "Copy Link",
            action: () => clipboard.write(context.linkUrl)
          },
          { type: "separator" }
        );
      }
      
      if (context.isImage) {
        menu.push(
          {
            label: "Save Image...",
            action: async () => {
              const result = await dialog.showSaveDialog({
                defaultPath: "image.png"
              });
              if (!result.canceled) {
                await downloadImage(context.imageUrl, result.filePath);
              }
            }
          },
          {
            label: "Copy Image",
            action: () => copyImage(context.imageUrl)
          },
          { type: "separator" }
        );
      }
      
      menu.push({
        label: "Inspect",
        action: () => win.inspectElement(context.x, context.y)
      });
      
      ContextMenu.show(menu);
    }
  }
});

System Tray

Basic Tray Icon

import { Tray } from "electrobun/bun";

const tray = new Tray({
  icon: "assets://tray-icon.png",
  tooltip: "My Application",
  menu: [
    {
      label: "Show Window",
      action: () => mainWindow.show()
    },
    {
      label: "Hide Window",
      action: () => mainWindow.hide()
    },
    { type: "separator" },
    {
      label: "Quit",
      action: () => app.quit()
    }
  ]
});

Dynamic Tray Updates

class TrayManager {
  private tray: Tray;
  private status: "idle" | "working" | "error" = "idle";
  
  constructor() {
    this.tray = new Tray({
      icon: this.getIconForStatus("idle"),
      tooltip: "My App - Idle"
    });
    this.updateMenu();
  }
  
  setStatus(status: "idle" | "working" | "error") {
    this.status = status;
    this.tray.setIcon(this.getIconForStatus(status));
    this.tray.setTooltip(`My App - ${status}`);
    this.updateMenu();
  }
  
  private getIconForStatus(status: string) {
    return `assets://tray-${status}.png`;
  }
  
  private updateMenu() {
    const menu = [
      {
        label: `Status: ${this.status}`,
        enabled: false
      },
      { type: "separator" },
      {
        label: "Show Window",
        action: () => mainWindow.show()
      }
    ];
    
    if (this.status === "working") {
      menu.push({
        label: "Cancel",
        action: () => cancelWork()
      });
    }
    
    menu.push(
      { type: "separator" },
      {
        label: "Quit",
        action: () => app.quit()
      }
    );
    
    this.tray.setMenu(menu);
  }
  
  showNotification(title: string, message: string) {
    this.tray.showBalloon({
      title,
      content: message,
      icon: "assets://notification-icon.png"
    });
  }
}

const trayManager = new TrayManager();
trayManager.setStatus("working");
trayManager.showNotification("Task Complete", "Your task has finished");

Native Dialogs

File Dialogs

import { dialog } from "electrobun/bun";

// Open single file
async function openFile() {
  const result = await dialog.showOpenDialog({
    title: "Open File",
    defaultPath: paths.home,
    filters: [
      { name: "Text Files", extensions: ["txt", "md"] },
      { name: "All Files", extensions: ["*"] }
    ],
    properties: ["openFile"]
  });
  
  if (!result.canceled && result.filePaths.length > 0) {
    const content = await Bun.file(result.filePaths[0]).text();
    return { path: result.filePaths[0], content };
  }
  
  return null;
}

// Open multiple files
async function openMultipleFiles() {
  const result = await dialog.showOpenDialog({
    properties: ["openFile", "multiSelections"]
  });
  
  if (!result.canceled) {
    return result.filePaths;
  }
  
  return [];
}

// Open directory
async function openDirectory() {
  const result = await dialog.showOpenDialog({
    title: "Select Folder",
    properties: ["openDirectory"]
  });
  
  if (!result.canceled && result.filePaths.length > 0) {
    return result.filePaths[0];
  }
  
  return null;
}

// Save file
async function saveFile(defaultName = "untitled.txt") {
  const result = await dialog.showSaveDialog({
    title: "Save File",
    defaultPath: path.join(paths.home, defaultName),
    filters: [
      { name: "Text Files", extensions: ["txt"] },
      { name: "All Files", extensions: ["*"] }
    ]
  });
  
  if (!result.canceled) {
    return result.filePath;
  }
  
  return null;
}

Message Boxes

// Confirmation dialog
async function confirmQuit() {
  const result = await dialog.showMessageBox({
    type: "question",
    title: "Confirm Quit",
    message: "Are you sure you want to quit?",
    detail: "Unsaved changes will be lost.",
    buttons: ["Quit", "Cancel"],
    defaultId: 1,
    cancelId: 1
  });
  
  return result.response === 0; // Returns true if "Quit" clicked
}

// Error dialog
async function showError(message: string, detail?: string) {
  await dialog.showMessageBox({
    type: "error",
    title: "Error",
    message,
    detail,
    buttons: ["OK"]
  });
}

// Warning with options
async function showWarning() {
  const result = await dialog.showMessageBox({
    type: "warning",
    title: "Warning",
    message: "This action cannot be undone",
    detail: "Are you sure you want to continue?",
    buttons: ["Continue", "Cancel", "Learn More"],
    defaultId: 1,
    cancelId: 1
  });
  
  if (result.response === 0) {
    // Continue
  } else if (result.response === 2) {
    shell.openExternal("https://docs.myapp.com/warning");
  }
}

// Info dialog
async function showInfo(message: string) {
  await dialog.showMessageBox({
    type: "info",
    title: "Information",
    message,
    buttons: ["OK"]
  });
}

Keyboard Shortcuts

Global Shortcuts

import { globalShortcut } from "electrobun/bun";

// Register global shortcut
globalShortcut.register("CommandOrControl+Shift+Space", () => {
  mainWindow.show();
  mainWindow.focus();
});

// Register multiple shortcuts
const shortcuts = [
  { key: "CommandOrControl+1", action: () => switchToTab(0) },
  { key: "CommandOrControl+2", action: () => switchToTab(1) },
  { key: "CommandOrControl+3", action: () => switchToTab(2) },
];

shortcuts.forEach(({ key, action }) => {
  globalShortcut.register(key, action);
});

// Unregister on quit
app.on("will-quit", () => {
  globalShortcut.unregisterAll();
});

In-Window Shortcuts

// Define in menu (automatically registered)
{
  label: "New Window",
  accelerator: "CmdOrCtrl+N",
  action: () => createWindow()
}

// Custom accelerators in webview
document.addEventListener("keydown", (e) => {
  // Cmd/Ctrl + K
  if ((e.metaKey || e.ctrlKey) && e.key === "k") {
    e.preventDefault();
    openCommandPalette();
  }
  
  // Cmd/Ctrl + P
  if ((e.metaKey || e.ctrlKey) && e.key === "p") {
    e.preventDefault();
    openFileFinder();
  }
});

Notifications

System Notifications

import { Notification } from "electrobun/bun";

function showNotification(title: string, body: string) {
  new Notification({
    title,
    body,
    icon: "assets://notification-icon.png",
    sound: true
  }).show();
}

// With actions (on supported platforms)
const notification = new Notification({
  title: "New Message",
  body: "You have a new message from Alice",
  actions: [
    { type: "button", text: "Reply" },
    { type: "button", text: "Dismiss" }
  ]
});

notification.on("action", (index) => {
  if (index === 0) {
    openReplyWindow();
  }
});

notification.show();

Resources

For more on Electrobun:

  • Core skill: electrobun - Basic UI setup
  • Window management: electrobun-window-management - Window coordination
  • RPC patterns: electrobun-rpc-patterns - UI state sync

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

electrobun-window-management

No summary provided by upstream source.

Repository SourceNeeds Review
General

electrobun-distribution

No summary provided by upstream source.

Repository SourceNeeds Review
General

electrobun-rpc-patterns

No summary provided by upstream source.

Repository SourceNeeds Review
General

electrobun-debugging

No summary provided by upstream source.

Repository SourceNeeds Review