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