diff --git a/gui/components/Systray.qml b/gui/components/Systray.qml
index 9ed52f5a3aef2f3d0a7d294133c004df6fffa83d..d23a7c4f91aa1fd4a4ca047b60e045c914af788a 100644
--- a/gui/components/Systray.qml
+++ b/gui/components/Systray.qml
@@ -17,6 +17,19 @@ Labs.SystemTrayIcon {
             enabled: false
         }
 
+        Labs.MenuItem {
+            id: vpnSystrayToggle
+            text: getConnectionText()
+            enabled: isConnectionTextEnabled()
+            onTriggered: {
+                if (ctx.status == "off") {
+                    backend.switchOn()
+                } else if (ctx.status == "on") {
+                    backend.switchOff()
+                }
+            }
+        }
+
         Labs.MenuSeparator {}
 
         Labs.MenuItem {
@@ -27,8 +40,59 @@ Labs.SystemTrayIcon {
         Labs.MenuSeparator {}
 
         Labs.MenuItem {
+            id: showAppItem
+            //: Part of the systray menu; show or hide the main app window
+            text: isVisible() ? qsTr("Hide") : qsTr("Show")
+            onTriggered: {
+                if (isVisible()) {
+                    root.hide()
+                } else {
+                    root.bringToFront()
+                }
+            }
+        }
+
+        Labs.MenuItem {
+            //: Part of the systray menu; quits que application
             text: qsTr("Quit")
             onTriggered: backend.quit()
         }
     }
+
+    function isVisible() {
+        return root.visibility != 0 && root.visibility != 3
+    }
+
+    function getConnectionText() {
+        if (!ctx) {
+            return ""
+        } else if (ctx.status == "off") {
+            // Not Turn on, because we will can later append "to <Location>"
+            if (ctx.locations && ctx.bestLocation) {
+                return qsTr("Connect to") + " " + getCanonicalLocation(ctx.bestLocation)
+            } else {
+                return qsTr("Connect")
+            }
+        } else if (ctx.status == "on") {
+            return qsTr("Disconnect")
+        } 
+        return ""
+    }
+
+    function isConnectionTextEnabled() {
+        if (!ctx) {
+            return false
+        }
+        return ctx.status == "off" || ctx.status == "on"
+    }
+
+    // returns the composite of Location, CC
+    function getCanonicalLocation(label) {
+        try {
+            let loc = ctx.locationLabels[label]
+            return loc[0] + ", " + loc[1]
+        } catch(e) {
+            return "unknown"
+        }
+    }
 }
diff --git a/gui/components/VPNState.qml b/gui/components/VPNState.qml
index d5115f20606cac2d0585bf0c6f31c6ac4f86c5b0..189fd0b0aab25a82bc4d10c452e077d34b4f57f1 100644
--- a/gui/components/VPNState.qml
+++ b/gui/components/VPNState.qml
@@ -37,7 +37,9 @@ StateGroup {
             PropertyChanges {
                 target: toggleVPN
                 enabled: false
-                text: ("...")
+                // XXX this is a fake cancel, won't do anything at this point. We need
+                // to queue this action for when the openvpn process becomes available.
+                text: ("Cancel")
             }
             PropertyChanges {
                 target: systray
diff --git a/gui/main.qml b/gui/main.qml
index 1c56925186f463cc3095acd68c29b860c0e40efe..c063739774171129745079d1c8d052d754538e2a 100644
--- a/gui/main.qml
+++ b/gui/main.qml
@@ -3,8 +3,6 @@
 /*
  TODO (ui rewrite)
  See https://0xacab.org/leap/bitmask-vpn/-/issues/523
- - [x] udp support
- - [ ] minimize/hide from systray
  - [ ] control actions from systray
  - [ ] add gateway to systray
 */
@@ -144,6 +142,17 @@ ApplicationWindow {
         return Array.from(arr, (k,_) => k.key);
     }
 
+    function bringToFront() {
+        // FIXME does not work properly, at least on linux 
+        if (visibility == 3) {
+            showNormal()
+        } else {
+            show() 
+        }
+        raise()
+        requestActivate()
+    }
+
     onSceneGraphError: function (error, msg) {
         console.debug("ERROR while initializing scene")
         console.debug(msg)