Simple hotkey menu for app found in multiple qubes

I couldn’t find a good way to use key shortcuts to open apps like terminal, nautilus, browsers, etc., where you have many versions of the same application in different qubes.

So I made a simple python menu that can be configured using json.

{
	"menu-items": [
		{
			"file": "path to desktop file",
			"appname": "application name",
			"icon": "path to icon",
			"qubename": "qube name",
			"exec": "comannd to execute",
			"dvm": "boolean"
		}		
	]
}

The menu uses json to find the desktop file needed to extract the qubename, appname, command, and icon.

The json for the menu in the image would look like this

{
	"menu-items": [
		{
			"file": "/home/user/.local/share/applications/org.qubes-os.vm._personal.xfce4-terminal.desktop"
		},
		{
			"file": "/home/user/.local/share/applications/org.qubes-os.vm._work.xfce4-terminal.desktop"
		},
		{
			"file": "/home/user/.local/share/applications/org.qubes-os.vm._vault.xfce4-terminal.desktop"
		},
		{
			"file": "/home/user/.local/share/applications/org.qubes-os.vm._untrusted.xfce4-terminal.desktop"
		}
	]
}

If you only use the desktop file than Icon, X-Qube-AppName, X-Qube-VmName, and Exec will be used, if dvm is true then Exec is replaced with X-Qubes-DispvmExec, and dvm only works on dvm qubes.

You can override the desktop values with the options from the json file.

{
	"menu-items": [
		{
			"file": "/home/user/.local/share/applications/org.qubes-os.vm._personal.xfce4-terminal.desktop",
			"appname": "terminal"
		},
		{
			"file": "/home/user/.local/share/applications/org.qubes-os.vm._work.xfce4-terminal.desktop",
			"appname": "terminal"
		},
		{
			"file": "/home/user/.local/share/applications/org.qubes-os.vm._vault.xfce4-terminal.desktop",
			"appname": "terminal"
		},
		{
			"file": "/home/user/.local/share/applications/org.qubes-os.vm._untrusted.xfce4-terminal.desktop",
			"appname": "terminal",
			"qubename": "!! DANGER !!"
		}
	]
}

For apps that don’t have a desktop file

{
    "menu-items": [
	{
		"exec": "/usr/bin/xfce4-terminal",
		"icon": "/home/user/.icons/Fluent-dark/scalable/apps/org.gnome.Terminal.svg",
		"appname": "terminal",
		"qubename": "dom0"
	}
	]
}

mymenu.py

#!/usr/bin/python3

import gi, os, json, sys
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, Gdk, GdkPixbuf

class MenuSetting():
    file = exec = icon = appname = qubename = None
    dvm = error = False
    
    def __init__(self, js):
        self.file = js["file"] if "file" in js else ""
        if self.file != '' and not os.path.isfile(self.file):
            self.error = True
        
        self.dvm = js["dvm"] if "dvm" in js else False
        exectype = "Exec" if not self.dvm else "X-Qubes-DispvmExec"
        self.exec = js["exec"] if "exec" in js and js["exec"] != "" else self.fileValue(self.file, exectype)
        self.icon = js["icon"] if "icon" in js and js["icon"] != "" else self.fileValue(self.file, "Icon")
        self.appname = js["appname"] if "appname" in js and js["appname"] != "" else self.fileValue(self.file, "X-Qubes-AppName")
        self.qubename = js["qubename"] if "qubename" in js and js["qubename"] != "" else self.fileValue(self.file, "X-Qubes-VmName")
        
        if self.file == '' and self.exec == '' and self.icon == '':
            self.error = True

    def fileValue(self, filename, valuename):
        if not os.path.isfile(filename):
            return '';

        file = open(filename, "r")
        for line in file.readlines():
            if line.startswith(valuename):
                return line[line.index("=")+1:].strip()

class MenuButton(Gtk.Button):
    menuSetting = None

    def __init__(self, menusetting):
        super().__init__()
        self.connect("clicked", self.on_event_clicked)
        self.menuSetting = menusetting
        container = Gtk.Box.new(Gtk.Orientation.VERTICAL, spacing=5)
        pixbuf = GdkPixbuf.Pixbuf.new_from_file(self.menuSetting.icon)
        container.pack_start(Gtk.Image.new_from_pixbuf(pixbuf.scale_simple(64, 64, GdkPixbuf.InterpType.BILINEAR)), False, False, 0)
        container.pack_start(Gtk.Label(label=self.menuSetting.qubename), False, False, 0)
        container.pack_start(Gtk.Label(label=self.menuSetting.appname), False, False, 0)
        self.add(container)

    def on_event_clicked(self, button):
        os.system(self.menuSetting.exec + "&")
        os._exit(0)

class PyMenu(Gtk.Window):
    settings = []

    def __init__(self):                    
        Gtk.Window.__init__(self)
        self.set_title("pyMenu")
        self.set_keep_above(True)
        self.set_decorated(False)
        self.set_property("skip-taskbar-hint", True)
        self.connect("destroy", Gtk.main_quit)
        self.connect('key-release-event', self.on_event_key_release)
        self.connect('focus-out-event', self.on_event_focus_out)
        self.screen = self.get_screen()
        self.visual = self.screen.get_rgba_visual()
        self.set_visual(self.visual)

        container = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, spacing=3)
        self.add(container)
        self.loadSettings(sys.argv[1])
        for item in self.settings:
            if not item.error:
                container.pack_start(MenuButton(item), False, False, 0)
            
    def loadSettings(self, filename):
        jsondata = None
        with open(os.path.dirname(__file__) + f"/{filename}") as json_file:
            jsondata = json.load(json_file)
        
        for js in jsondata["menu-items"]:
            self.settings.append(MenuSetting(js))

    def fileValue(self, filename, valuename):        
        if not os.path.isfile(filename):
            return ""
    
        file = open(filename, "r")
        for line in file.readlines():
            if line.startswith(valuename):
                return line[line.index("=")+1:].strip()

    def on_event_key_release(self, key, event):
        if Gdk.keyval_name(event.keyval) == 'Escape':
            os._exit(0)
    
    def on_event_focus_out(self, widget, window):
        os._exit(0)

window = PyMenu()

CSS_DATA = b"""
button { border-radius: 8px; margin: 7px 7px 7px 7px; border: none; outline: none; background-color: rgba(0, 0, 0, 0); }
button:focus { background-color: rgba(191, 0, 0, 0.30); border: none; }
box { margin: 3px 3px 3px 3px; }
window { border-radius: 8px; background-color: rgba(46, 51, 64, 0.85); }
"""
css = Gtk.CssProvider()
css.load_from_data(CSS_DATA)
style_context = window.get_style_context()
style_context.add_provider_for_screen(Gdk.Screen.get_default(), css, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)

settings = Gtk.Settings.get_default()
settings.set_property("gtk-theme-name", "Arc-Dark")
settings.set_property("gtk-application-prefer-dark-theme", True)

window.show_all()
Gtk.main()
1 Like