diff --git a/apps/desktopgui/src/net/i2p/desktopgui/ExternalMain.java b/apps/desktopgui/src/net/i2p/desktopgui/ExternalMain.java
index 7d44c5010307086b70df7a29c0fc03435aa66314..e70cbf502e5104841ae1073aa26737d9962b7bdd 100644
--- a/apps/desktopgui/src/net/i2p/desktopgui/ExternalMain.java
+++ b/apps/desktopgui/src/net/i2p/desktopgui/ExternalMain.java
@@ -13,6 +13,9 @@ import net.i2p.I2PAppContext;
 import net.i2p.app.ClientAppManager;
 import net.i2p.app.ClientApp;
 import net.i2p.app.ClientAppState;
+import net.i2p.app.MenuCallback;
+import net.i2p.app.MenuHandle;
+import net.i2p.app.MenuService;
 import net.i2p.app.NotificationService;
 import net.i2p.util.Log;
 import net.i2p.util.SystemVersion;
@@ -24,7 +27,7 @@ import net.i2p.util.SystemVersion;
  *
  * @since 0.9.54
  */
-public class ExternalMain implements ClientApp, NotificationService {
+public class ExternalMain implements ClientApp, NotificationService, MenuService {
 
     private final I2PAppContext _appContext;
     private final ClientAppManager _mgr;
@@ -60,7 +63,6 @@ public class ExternalMain implements ClientApp, NotificationService {
      * @throws AWTException on startup error, including systray not supported 
      */
     private synchronized void startUp() throws Exception {
-        final TrayManager trayManager;
         boolean useSwingDefault = !(SystemVersion.isWindows() || SystemVersion.isMac());
         boolean useSwing = _appContext.getProperty(PROP_SWING, useSwingDefault);
         _trayManager = new ExternalTrayManager(_appContext, useSwing);
@@ -200,6 +202,89 @@ public class ExternalMain implements ClientApp, NotificationService {
         return false;
     }
 
+    /////// MenuService methods
+
+    /**
+     *  Menu will start out shown and enabled, in the root menu
+     *
+     *  @param message for the menu, translated
+     *  @param callback fired on click
+     *  @return null on error
+     *  @since 0.9.59
+     */
+    public MenuHandle addMenu(String message, MenuCallback callback) {
+        return addMenu(message, callback, null);
+    }
+
+    /**
+     *  Menu will start out enabled, as a submenu
+     *
+     *  @param message for the menu, translated
+     *  @param callback fired on click
+     *  @param parent the parent menu this will be a submenu of, or null for top level
+     *  @return null on error
+     *  @since 0.9.59
+     */
+    public MenuHandle addMenu(String message, MenuCallback callback, MenuHandle parent) {
+        if (_trayManager == null)
+            return null;
+        return _trayManager.addMenu(message, callback, parent);
+    }
+
+    /**
+     *  @since 0.9.59
+     */
+    public void removeMenu(MenuHandle item) {
+        if (_trayManager == null)
+            return;
+        _trayManager.removeMenu(item);
+    }
+
+    /**
+     *  @since 0.9.59
+     */
+    public void showMenu(MenuHandle item) {
+        if (_trayManager == null)
+            return;
+        _trayManager.showMenu(item);
+    }
+
+    /**
+     *  @since 0.9.59
+     */
+    public void hideMenu(MenuHandle item) {
+        if (_trayManager == null)
+            return;
+        _trayManager.hideMenu(item);
+    }
+
+    /**
+     *  @since 0.9.59
+     */
+    public void enableMenu(MenuHandle item) {
+        if (_trayManager == null)
+            return;
+        _trayManager.enableMenu(item);
+    }
+
+    /**
+     *  @since 0.9.59
+     */
+    public void disableMenu(MenuHandle item) {
+        if (_trayManager == null)
+            return;
+        _trayManager.disableMenu(item);
+    }
+
+    /**
+     *  @since 0.9.59
+     */
+    public void updateMenu(String message, MenuHandle item) {
+        if (_trayManager == null)
+            return;
+        _trayManager.updateMenu(message, item);
+    }
+
     /////// ClientApp methods
 
     public synchronized void startup() {
diff --git a/apps/desktopgui/src/net/i2p/desktopgui/Main.java b/apps/desktopgui/src/net/i2p/desktopgui/Main.java
index 0a7e3f9bc2fb55d7a2ba86a66143574915b10ebc..921efaa055fec03cf996b23bf5c27e269f43d2fc 100644
--- a/apps/desktopgui/src/net/i2p/desktopgui/Main.java
+++ b/apps/desktopgui/src/net/i2p/desktopgui/Main.java
@@ -17,6 +17,9 @@ import net.i2p.I2PAppContext;
 import net.i2p.app.ClientAppManager;
 import net.i2p.app.ClientAppState;
 import static net.i2p.app.ClientAppState.*;
+import net.i2p.app.MenuCallback;
+import net.i2p.app.MenuHandle;
+import net.i2p.app.MenuService;
 import net.i2p.app.NotificationService;
 import net.i2p.desktopgui.router.RouterManager;
 import net.i2p.router.RouterContext;
@@ -29,7 +32,7 @@ import net.i2p.util.I2PProperties.I2PPropertyCallback;
 /**
  * The main class of the application.
  */
-public class Main implements RouterApp, NotificationService {
+public class Main implements RouterApp, NotificationService, MenuService {
 
     // non-null
     private final I2PAppContext _appContext;
@@ -245,6 +248,89 @@ public class Main implements RouterApp, NotificationService {
         return false;
     }
 
+    /////// MenuService methods
+
+    /**
+     *  Menu will start out shown and enabled, in the root menu
+     *
+     *  @param message for the menu, translated
+     *  @param callback fired on click
+     *  @return null on error
+     *  @since 0.9.59
+     */
+    public MenuHandle addMenu(String message, MenuCallback callback) {
+        return addMenu(message, callback, null);
+    }
+
+    /**
+     *  Menu will start out enabled, as a submenu
+     *
+     *  @param message for the menu, translated
+     *  @param callback fired on click
+     *  @param parent the parent menu this will be a submenu of, or null for top level
+     *  @return null on error
+     *  @since 0.9.59
+     */
+    public MenuHandle addMenu(String message, MenuCallback callback, MenuHandle parent) {
+        if (_trayManager == null)
+            return null;
+        return _trayManager.addMenu(message, callback, parent);
+    }
+
+    /**
+     *  @since 0.9.59
+     */
+    public void removeMenu(MenuHandle item) {
+        if (_trayManager == null)
+            return;
+        _trayManager.removeMenu(item);
+    }
+
+    /**
+     *  @since 0.9.59
+     */
+    public void showMenu(MenuHandle item) {
+        if (_trayManager == null)
+            return;
+        _trayManager.showMenu(item);
+    }
+
+    /**
+     *  @since 0.9.59
+     */
+    public void hideMenu(MenuHandle item) {
+        if (_trayManager == null)
+            return;
+        _trayManager.hideMenu(item);
+    }
+
+    /**
+     *  @since 0.9.59
+     */
+    public void enableMenu(MenuHandle item) {
+        if (_trayManager == null)
+            return;
+        _trayManager.enableMenu(item);
+    }
+
+    /**
+     *  @since 0.9.59
+     */
+    public void disableMenu(MenuHandle item) {
+        if (_trayManager == null)
+            return;
+        _trayManager.disableMenu(item);
+    }
+
+    /**
+     *  @since 0.9.59
+     */
+    public void updateMenu(String message, MenuHandle item) {
+        if (_trayManager == null)
+            return;
+        _trayManager.updateMenu(message, item);
+    }
+
     /////// ClientApp methods
 
     /** @since 0.9.26 */
diff --git a/apps/desktopgui/src/net/i2p/desktopgui/TrayManager.java b/apps/desktopgui/src/net/i2p/desktopgui/TrayManager.java
index 0261c107d3aea571bc1822099a6093594aa6e66a..a7390d7fb61650afcde059914dde050b44328d77 100644
--- a/apps/desktopgui/src/net/i2p/desktopgui/TrayManager.java
+++ b/apps/desktopgui/src/net/i2p/desktopgui/TrayManager.java
@@ -4,6 +4,7 @@ import java.awt.AWTException;
 import java.awt.Dimension;
 import java.awt.Font;
 import java.awt.Image;
+import java.awt.MenuItem;
 import java.awt.PopupMenu;
 import java.awt.SystemTray;
 import java.awt.Toolkit;
@@ -16,8 +17,10 @@ import java.awt.event.MouseEvent;
 import java.awt.event.MouseListener;
 import java.io.IOException;
 import java.net.URL;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
 
-import java.awt.MenuItem;
 import javax.swing.JFrame;
 import javax.swing.JMenuItem;
 import javax.swing.JPopupMenu;
@@ -28,6 +31,8 @@ import javax.swing.event.PopupMenuEvent;
 import javax.swing.event.PopupMenuListener;
 
 import net.i2p.I2PAppContext;
+import net.i2p.app.MenuCallback;
+import net.i2p.app.MenuHandle;
 import net.i2p.apps.systray.UrlLauncher;
 import net.i2p.desktopgui.i18n.DesktopguiTranslator;
 import net.i2p.util.Log;
@@ -47,6 +52,9 @@ abstract class TrayManager {
     protected volatile boolean _showNotifications;
     protected MenuItem  _notificationItem1, _notificationItem2;
     protected JMenuItem _jnotificationItem1, _jnotificationItem2;
+    private final AtomicInteger _id = new AtomicInteger();
+    private final List<MenuInternal> _menus;
+    private JPopupMenu _jPopupMenu;
 
     private static final String PNG_DIR = "/desktopgui/resources/images/";
     private static final String MAC_ICON = "itoopie_black_24.png";
@@ -61,6 +69,7 @@ abstract class TrayManager {
     protected TrayManager(I2PAppContext ctx, boolean useSwing) {
         _appContext = ctx;
         _useSwing = useSwing;
+        _menus = new ArrayList<MenuInternal>();
     }
     
     /**
@@ -109,6 +118,7 @@ abstract class TrayManager {
         frame.setMinimumSize(new Dimension(0, 0));
         frame.setSize(0, 0);
         final JPopupMenu menu = getSwingMainMenu();
+        _jPopupMenu = menu;
         menu.setFocusable(true);
         frame.add(menu);
         TrayIcon ti = new TrayIcon(getTrayImage(), tooltip, null);
@@ -375,6 +385,165 @@ abstract class TrayManager {
         _jnotificationItem1 = notificationItem1;
     }
 
+    /////// MenuService delegation methods
+
+    /**
+     *  @since 0.9.59
+     */
+    public MenuHandle addMenu(String message, final MenuCallback callback, MenuHandle p) {
+        MenuInternal parent = p != null ? (MenuInternal) p : null;
+        final int id = _id.incrementAndGet();
+        final MenuInternal rv;
+        if (_useSwing) {
+            final JMenuItem m = new JMenuItem(message);
+            rv = new MenuInternal(null, m, callback, id);
+            m.addActionListener(new ActionListener() {
+                @Override
+                public void actionPerformed(ActionEvent arg0) {
+                    new SwingWorker<Object, Object>() {
+                        @Override
+                        protected Object doInBackground() throws Exception {
+                            rv.cb.clicked(rv);
+                            return null;
+                        }
+                    }.execute();
+                }
+            });
+            _jPopupMenu.add(m);
+        } else {
+            final MenuItem m = new MenuItem(message);
+            rv = new MenuInternal(m, null, callback, id);
+            m.addActionListener(new ActionListener() {
+                @Override
+                public void actionPerformed(ActionEvent arg0) {
+                    new SwingWorker<Object, Object>() {
+                        @Override
+                        protected Object doInBackground() throws Exception {
+                            rv.cb.clicked(rv);
+                            return null;
+                        }
+                    }.execute();
+                }
+            });
+            trayIcon.getPopupMenu().add(m);
+        }
+        synchronized(_menus) {
+            _menus.add(rv);
+        }
+        updateMenu();
+        return rv;
+    }
+
+    /**
+     *  @since 0.9.59
+     */
+    public void removeMenu(MenuHandle item) {
+        MenuInternal mi = (MenuInternal) item;
+        if (_useSwing) {
+            _jPopupMenu.remove(mi.jm);
+        } else {
+            trayIcon.getPopupMenu().remove(mi.m);
+        }
+        updateMenu();
+    }
+
+    /**
+     *  @since 0.9.59
+     */
+    public void showMenu(MenuHandle item) {
+        MenuInternal mi = (MenuInternal) item;
+        mi.setVisible(true);
+        updateMenu();
+    }
+
+    /**
+     *  @since 0.9.59
+     */
+    public void hideMenu(MenuHandle item) {
+        MenuInternal mi = (MenuInternal) item;
+        mi.setVisible(false);
+        updateMenu();
+    }
+
+    /**
+     *  @since 0.9.59
+     */
+    public void enableMenu(MenuHandle item) {
+        MenuInternal mi = (MenuInternal) item;
+        mi.setEnabled(true);
+        updateMenu();
+    }
+
+    /**
+     *  @since 0.9.59
+     */
+    public void disableMenu(MenuHandle item) {
+        MenuInternal mi = (MenuInternal) item;
+        mi.setEnabled(false);
+        updateMenu();
+    }
+
+    /**
+     *  @since 0.9.59
+     */
+    public void updateMenu(String message, MenuHandle item) {
+        MenuInternal mi = (MenuInternal) item;
+        mi.setText(message);
+        updateMenu();
+    }
+
+    /////// MenuService internals
+
+    /**
+     *  @since 0.9.59
+     */
+    private MenuInternal getMenu(int id) {
+        synchronized(_menus) {
+            for (MenuInternal mi : _menus) {
+                 if (mi.getID() == id)
+                     return mi;
+            }
+        }
+        return null;
+    }
+
+    /**
+     *  @since 0.9.59
+     */
+    private static class MenuInternal implements MenuHandle {
+        private final MenuItem m;
+        private final JMenuItem jm;
+        private final MenuCallback cb;
+        private final int id;
+
+        public MenuInternal(MenuItem mm, JMenuItem jmm, MenuCallback cbb, int idd) {
+            m = mm; jm = jmm; cb = cbb; id = idd;
+        }
+
+        public int getID() { return id; }
+
+        private void setEnabled(boolean yes) {
+            if (m != null)
+                m.setEnabled(yes);
+            else
+                jm.setEnabled(yes);
+        }
+
+        private void setVisible(boolean yes) {
+            if (m != null)
+                m.setEnabled(yes);
+            else
+                jm.setVisible(yes);
+        }
+
+        private void setText(String text) {
+            if (m != null)
+                m.setLabel(text);
+            else
+                jm.setText(text);
+        }
+    }
+
     protected String _t(String s) {
         return DesktopguiTranslator._t(_appContext, s);
     }
diff --git a/apps/i2psnark/java/src/org/klomp/snark/standalone/RunStandalone.java b/apps/i2psnark/java/src/org/klomp/snark/standalone/RunStandalone.java
index 4cce5e8fddba34bbde323887b958aded386cd619..180081613853de006029500838b47143b881416b 100644
--- a/apps/i2psnark/java/src/org/klomp/snark/standalone/RunStandalone.java
+++ b/apps/i2psnark/java/src/org/klomp/snark/standalone/RunStandalone.java
@@ -8,6 +8,8 @@ import java.util.Properties;
 import org.eclipse.jetty.util.log.Log;
 
 import net.i2p.I2PAppContext;
+import net.i2p.app.MenuCallback;
+import net.i2p.app.MenuHandle;
 import net.i2p.apps.systray.UrlLauncher;
 import net.i2p.data.DataHelper;
 import net.i2p.desktopgui.ExternalMain;
@@ -133,9 +135,22 @@ public class RunStandalone {
                 System.setProperty("java.awt.headless", "false");
                 ExternalMain dtg = new ExternalMain(_context, _context.clientAppManager(), null);
                 dtg.startup();
+                try {
+                    Thread.sleep(1000);
+                } catch (InterruptedException ie) {}
+                Callback cb = new Callback();
+                MenuHandle mh = dtg.addMenu("i2psnark is running", cb);
+                if (mh == null)
+                    System.out.println("addMenu failed!");
             }
         } catch (Throwable t) {
             t.printStackTrace();
         }
     }
+
+    private static class Callback implements MenuCallback {
+        public void clicked(MenuHandle handle) {
+            System.out.println("Clicked! " + handle.getID());
+        }
+    }
 }
diff --git a/core/java/src/net/i2p/app/MenuCallback.java b/core/java/src/net/i2p/app/MenuCallback.java
new file mode 100644
index 0000000000000000000000000000000000000000..a88fcd455b721e255e1a418c16f92f4ec2dee34a
--- /dev/null
+++ b/core/java/src/net/i2p/app/MenuCallback.java
@@ -0,0 +1,16 @@
+package net.i2p.app;
+
+/**
+ *  The callback when a user clicks a MenuHandle.
+ *
+ *  @since 0.9.59
+ */
+public interface MenuCallback {
+
+    /**
+     *  Called when the user clicks the menu
+     *
+     *  @param menu the menu handle clicked
+     */
+    public void clicked(MenuHandle menu);
+}
diff --git a/core/java/src/net/i2p/app/MenuHandle.java b/core/java/src/net/i2p/app/MenuHandle.java
new file mode 100644
index 0000000000000000000000000000000000000000..b4a93df2123fd5650310a4fd79ff0a2cf6bccc01
--- /dev/null
+++ b/core/java/src/net/i2p/app/MenuHandle.java
@@ -0,0 +1,15 @@
+package net.i2p.app;
+
+/**
+ *  An opaque handle for the menu, returned from MenuService.addMenuHandle()
+ *
+ *  @since 0.9.59
+ */
+public interface MenuHandle {
+
+    /**
+     *  @return a unique identifier for this MenuHandle
+     */
+    public int getID();
+
+}
diff --git a/core/java/src/net/i2p/app/MenuService.java b/core/java/src/net/i2p/app/MenuService.java
new file mode 100644
index 0000000000000000000000000000000000000000..02ccebe038bd52bbc65d6598d1c4dcfe34b0ede5
--- /dev/null
+++ b/core/java/src/net/i2p/app/MenuService.java
@@ -0,0 +1,55 @@
+package net.i2p.app;
+
+/**
+ *  A service to provide a menu to users.
+ *  This service is currently provided by desktopgui (when supported and enabled).
+ *  Other applications may support this interface in the future.
+ *
+ *  This API is independent of any particular UI framework, e.g. AWT or Swing.
+ *
+ *  Example usage:
+ *
+ * <pre>
+ *     ClientAppManager cmgr = _context.clientAppManager();
+ *     if (cmgr != null) {
+ *         MenuService ms = (MenuService) cmgr.getRegisteredApp("desktopgui");
+ *         if (ms != null)
+ *             ms.addMenuHandle(_t("foo"), new Callback());
+ *     }
+ * </pre>
+ *
+ *  @since 0.9.59
+ */
+public interface MenuService {
+
+    /**
+     *  Menu will start out shown and enabled, in the root menu
+     *
+     *  @param message for the menu, translated
+     *  @param callback fired on click
+     *  @return null on error
+     */
+    public MenuHandle addMenu(String message, MenuCallback callback);
+
+    /**
+     *  Menu will start out enabled, as a submenu
+     *
+     *  @param message for the menu, translated
+     *  @param callback fired on click
+     *  @param parent the parent menu this will be a submenu of, or null for top level
+     *  @return null on error
+     */
+    public MenuHandle addMenu(String message, MenuCallback callback, MenuHandle parent);
+
+    public void removeMenu(MenuHandle item);
+
+    public void showMenu(MenuHandle item);
+
+    public void hideMenu(MenuHandle item);
+
+    public void enableMenu(MenuHandle item);
+
+    public void disableMenu(MenuHandle item);
+
+    public void updateMenu(String message, MenuHandle item);
+}