Simple hotkey menu for app found in multiple qubes

Original forum link
https://forum.qubes-os.org/t/23525
Original poster
renehoj
Created at
2024-01-06 10:02:03
Posts count
1
Likes count
1
Tags
customization, xfce

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"
        }       
    ]
}
pymenu-file.desktop|690x278

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.

pymenu-file.mod.desktop|690x302

{
    "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()