-
-
Save ixxra/fbfe9d46fe209a4e7576d1adb9446b4c to your computer and use it in GitHub Desktop.
HUD Menu
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
import gi | |
gi.require_version("Gtk", "3.0") | |
import dbus | |
import psutil | |
import subprocess | |
from Xlib import display, protocol, X | |
import time | |
########################################################################### | |
## Helpers | |
########################################################################### | |
class EWMH: | |
"""This class provides the ability to get and set properties defined | |
by the EWMH spec. It was blanty ripped out of pyewmh | |
* https://github.com/parkouss/pyewmh | |
""" | |
def __init__(self, _display=None, root = None): | |
self.display = _display or display.Display() | |
self.root = root or self.display.screen().root | |
def getActiveWindow(self): | |
"""Get the current active (toplevel) window or None (property _NET_ACTIVE_WINDOW) | |
:return: Window object or None""" | |
active_window = self._getProperty('_NET_ACTIVE_WINDOW') | |
if active_window == None: | |
return None | |
return self._createWindow(active_window[0]) | |
def _getProperty(self, _type, win=None): | |
if not win: | |
win = self.root | |
atom = win.get_full_property(self.display.get_atom(_type), X.AnyPropertyType) | |
if atom: | |
return atom.value | |
def _setProperty(self, _type, data, win=None, mask=None): | |
"""Send a ClientMessage event to the root window""" | |
if not win: | |
win = self.root | |
if type(data) is str: | |
dataSize = 8 | |
else: | |
data = (data+[0]*(5-len(data)))[:5] | |
dataSize = 32 | |
ev = protocol.event.ClientMessage(window=win, client_type=self.display.get_atom(_type), data=(dataSize, data)) | |
if not mask: | |
mask = (X.SubstructureRedirectMask|X.SubstructureNotifyMask) | |
self.root.send_event(ev, event_mask=mask) | |
def _createWindow(self, wId): | |
if not wId: | |
return None | |
return self.display.create_resource_object('window', wId) | |
action_counter = 0 | |
def format_path(path): | |
global action_counter | |
result = path.replace("Root > ", "") | |
result = result.replace("Label Empty > ", "") | |
result = result.replace("_", "") | |
result = result.replace(" > ", str(action_counter).zfill(4) + " .", 1) | |
action_counter += 1 | |
return result | |
########################################################################### | |
## HUD Codes | |
########################################################################### | |
""" | |
try_appmenu_interface | |
""" | |
def try_appmenu_interface(window_id): | |
# --- Get Appmenu Registrar DBus interface | |
session_bus = dbus.SessionBus() | |
appmenu_registrar_object = session_bus.get_object('com.canonical.AppMenu.Registrar', '/com/canonical/AppMenu/Registrar') | |
appmenu_registrar_object_iface = dbus.Interface(appmenu_registrar_object, 'com.canonical.AppMenu.Registrar') | |
# --- Get dbusmenu object path | |
try: | |
dbusmenu_bus, dbusmenu_object_path = appmenu_registrar_object_iface.GetMenuForWindow(window_id) | |
except dbus.exceptions.DBusException: | |
return | |
# --- Access dbusmenu items | |
dbusmenu_object = session_bus.get_object( | |
dbusmenu_bus, dbusmenu_object_path) | |
dbusmenu_object_iface = dbus.Interface( | |
dbusmenu_object, 'com.canonical.dbusmenu') | |
dbusmenu_root_item = dbusmenu_object_iface.GetLayout( | |
0, 0, ["label", "children-display"]) | |
dbusmenu_item_dict = dict() | |
#For excluding items which have no action | |
blacklist = [] | |
""" expanse_all_menu_with_dbus """ | |
def expanse_all_menu_with_dbus(item, root, path): | |
item_id = item[0] | |
item_props = item[1] | |
# expand if necessary | |
if 'children-display' in item_props: | |
dbusmenu_object_iface.AboutToShow(item_id) | |
dbusmenu_object_iface.Event(item_id, "opened", "not used", dbus.UInt32(time.time())) #fix firefox | |
try: | |
item = dbusmenu_object_iface.GetLayout(item_id, 1, ["label", "children-display"])[1] | |
except: | |
return | |
item_children = item[2] | |
if 'label' in item_props: | |
new_path = path + " > " + item_props['label'] | |
else: | |
new_path = path | |
if len(item_children) == 0: | |
if new_path not in blacklist: | |
dbusmenu_item_dict[format_path(new_path)] = item_id | |
else: | |
blacklist.append(new_path) | |
for child in item_children: | |
expanse_all_menu_with_dbus(child, False, new_path) | |
expanse_all_menu_with_dbus(dbusmenu_root_item[1], True, "") | |
menuKeys = sorted(dbusmenu_item_dict.keys()) | |
# --- Run rofi/dmenu | |
menu_string = '' | |
head, *tail = menuKeys | |
menu_string = head | |
for m in tail: | |
menu_string += '\n' | |
menu_string += m | |
menu_cmd = subprocess.Popen(['rofi', '-dmenu', '-i', | |
'-p', 'HUD: ', | |
'-columns', '1' | |
'-location', '1', | |
'-monitor', '-2', | |
'-width', '100', | |
'-lines', '20', | |
'-fixed-num-lines' | |
'-separator-style', 'solid', | |
'-hide-scrollbar'], | |
stdout=subprocess.PIPE, stdin=subprocess.PIPE) | |
menu_cmd.stdin.write(menu_string.encode('utf-8')) | |
menu_result = menu_cmd.communicate()[0].decode('utf8').rstrip() | |
menu_cmd.stdin.close() | |
if menu_result.endswith("\n"): | |
menu_result = menu_result[:-1] | |
# --- Use menu result | |
if menu_result in dbusmenu_item_dict: | |
action = dbusmenu_item_dict[menu_result] | |
dbusmenu_object_iface.Event(action, 'clicked', 0, 0) | |
# --- Fix firefox: send closed events to level 1 items to make sure nothing weird happen | |
# Firefox will close the submenu items (luckily!) | |
# For example VimFx extension wont work without this! | |
dbusmenu_level1_items = dbusmenu_object_iface.GetLayout(0, 1, ["label"])[1] | |
for item in dbusmenu_level1_items[2]: | |
item_id = item[0] | |
dbusmenu_object_iface.Event(item_id, "closed", "not used", dbus.UInt32(time.time())) | |
""" | |
try_gtk_interface | |
""" | |
def try_gtk_interface(gtk_bus_name, gtk_menu_object_path, gtk_actions_paths_list): | |
# --- Ask for menus over DBus --- Credit @1931186 | |
session_bus = dbus.SessionBus() | |
gtk_menu_object = session_bus.get_object(gtk_bus_name, gtk_menu_object_path) | |
gtk_menu_menus_iface = dbus.Interface(gtk_menu_object, dbus_interface='org.gtk.Menus') | |
# Here's the deal: The idea is to reduce the number of calls to the proxy and keep it as low as possible | |
# because the proxy is a potential bottleneck | |
# This means we ignore GMenus standard building model and just iterate over all the information one Start() provides at once | |
# Start() does these calls, returns the result and keeps track of all parents (the IDs used by org.gtk.Menus.Start()) we called | |
# queue() adds a parent to a potential_new_layers list; we'll use this later to avoid starting() some layers twice | |
# explore is for iterating over the information a Start() call provides | |
gtk_menubar_action_dict = dict() | |
gtk_menubar_action_target_dict = dict() | |
usedLayers = [] | |
def Start(i): | |
usedLayers.append(i) | |
return gtk_menu_menus_iface.Start([i]) | |
# --- Construct menu list --- | |
potential_new_layers = [] | |
def queue(potLayer, label, path): | |
# collects potentially new layers to check them against usedLayers | |
# potLayer: ID of potential layer, label: None if nondescript, path | |
potential_new_layers.append([potLayer, label, path]) | |
def explore(parent, path): | |
for node in parent: | |
content = node[2] | |
# node[0] = ID of parent | |
# node[1] = ID of node under parent | |
# node[2] = actuall content of a node; this is split up into several elements/ menu entries | |
for element in content: | |
# We distinguish between labeled entries and unlabeled ones | |
# Unlabeled sections/ submenus get added under to parent ({parent: {content}}), labeled under a key in parent (parent: {label: {content}}) | |
if 'label' in element: | |
if ':section' in element or ':submenu' in element: | |
# If there's a section we don't care about the action | |
# There theoretically could be a section that is also a submenu, so we have to handel this via queue | |
# submenus are more important than sections | |
if ':submenu' in element: | |
queue(element[':submenu'][0], None, path + " > " + element['label']) | |
# We ignore whether or not a submenu points to a specific index, shouldn't matter because of the way the menu got exportet | |
# Worst that can happen are some duplicates | |
# Also we don't Start() directly which could mean we get nothing under this label but this shouldn't really happen because there shouldn't be two submenus | |
# that point to the same parent. Even if this happens it's not that big of a deal. | |
if ':section' in element: | |
if element[':section'][0] != node[0]: | |
queue(element['section'][0], element['label'], path) | |
# section points to other parent, we only want to add the elements if their parent isn't referenced anywhere else | |
# We do this because: | |
# a) It shouldn't happen anyways | |
# b) The worst that could happen is we fuck up the menu structure a bit and avoid double entries | |
elif 'action' in element: | |
# This is pretty straightforward: | |
menu_action = str(element['action']).split(".",1)[1] | |
action_path = format_path(path + " > " + element['label']) | |
gtk_menubar_action_dict[action_path] = menu_action | |
if 'target' in element: | |
gtk_menubar_action_target_dict[action_path] = element['target'] | |
else: | |
if ':submenu' in element or ':section' in element: | |
if ':section' in element: | |
if element[':section'][0] != node[0] and element['section'][0] not in usedLayers: | |
queue(element[':section'][0], None, path) | |
# We will only queue a nondescript section if it points to a (potentially) new parent | |
if ':submenu' in element: | |
queue(element[':submenu'][0], None, path) | |
# We queue the submenu under the parent without a label | |
queue(0, None, "") | |
# We queue the first parent, [0] | |
# This means 0 gets added to potential_new_layers with a path of "" (it's the root node) | |
while len(potential_new_layers) > 0: | |
layer = potential_new_layers.pop() | |
# usedLayers keeps track of all the parents Start() already called | |
if layer[0] not in usedLayers: | |
explore(Start(layer[0]), layer[2]) | |
gtk_menu_menus_iface.End(usedLayers) | |
menuKeys = sorted(gtk_menubar_action_dict.keys()) | |
# --- Run rofi/dmenu | |
menu_string = '' | |
head, *tail = menuKeys | |
menu_string = head | |
for m in tail: | |
menu_string += '\n' | |
menu_string += m | |
menu_cmd = subprocess.Popen(['rofi', '-dmenu', '-i', | |
'-p', 'HUD: ', | |
'-columns', '1' | |
'-location', '1', | |
'-monitor', '-2', | |
'-width', '100', | |
'-lines', '20', | |
'-fixed-num-lines' | |
'-separator-style', 'solid', | |
'-hide-scrollbar'], | |
stdout=subprocess.PIPE, stdin=subprocess.PIPE) | |
menu_cmd.stdin.write(menu_string.encode('utf-8')) | |
menu_result = menu_cmd.communicate()[0].decode('utf8').rstrip() | |
menu_cmd.stdin.close() | |
if menu_result.endswith("\n"): | |
menu_result = menu_result[:-1] | |
# --- Use menu result | |
# --- We have to use the old dbus API here since the variant type * is not implemented :(( | |
session_bus = dbus.SessionBus() | |
if menu_result in gtk_menubar_action_dict: | |
action = gtk_menubar_action_dict[menu_result] | |
target = [] | |
try: | |
target = gtk_menubar_action_target_dict[menu_result] | |
if (not isinstance(target, list)): | |
target = [target] | |
except: | |
pass | |
for action_path in gtk_actions_paths_list: | |
try: | |
action_object = session_bus.get_object(gtk_bus_name, action_path) | |
action_iface = dbus.Interface(action_object, dbus_interface='org.gtk.Actions') | |
not_use_platform_data = dict() | |
not_use_platform_data["not used"] = "not used" | |
action_iface.Activate(action, target, not_use_platform_data) | |
except Exception as e: | |
print ("____________________________________________________") | |
print (action_path) | |
print (str(e)) | |
########################################################################### | |
## Main | |
########################################################################### | |
# Get Window properties and GTK MenuModel Bus name | |
ewmh = EWMH() | |
win = ewmh.getActiveWindow() | |
window_id = hex(ewmh._getProperty('_NET_ACTIVE_WINDOW')[0]) | |
window_pid = ewmh._getProperty('_NET_WM_PID', win)[0] | |
prompt = psutil.Process(window_pid).name() + ': ' | |
gtk_bus_name = ewmh._getProperty('_GTK_UNIQUE_BUS_NAME', win) | |
gtk_menu_object_path = ewmh._getProperty('_GTK_MENUBAR_OBJECT_PATH', win) | |
gtk_app_object_path = ewmh._getProperty('_GTK_APPLICATION_OBJECT_PATH', win) | |
gtk_win_object_path = ewmh._getProperty('_GTK_WINDOW_OBJECT_PATH', win) | |
gtk_unity_object_path = ewmh._getProperty('_UNITY_OBJECT_PATH', win) | |
gtk_bus_name, gtk_menu_object_path, gtk_app_object_path, gtk_win_object_path, gtk_unity_object_path = \ | |
[i.decode("utf8") if isinstance(i, bytes) \ | |
else i for i in [gtk_bus_name, gtk_menu_object_path, gtk_app_object_path, gtk_win_object_path, gtk_unity_object_path]] | |
# print('Window id is : %s', window_id) | |
# print('Window pid is : %s', window_pid) | |
# print('Prompt is : %s', prompt) | |
# print('_GTK_UNIQUE_BUS_NAME: %s', gtk_bus_name) | |
# print('_GTK_MENUBAR_OBJECT_PATH: %s', gtk_menu_object_path) | |
# print('_GTK_APPLICATION_OBJECT_PATH: %s', gtk_app_object_path) | |
# print('_GTK_WINDOW_OBJECT_PATH: %s', gtk_win_object_path) | |
# print('_UNITY_OBJECT_PATH: %s', gtk_unity_object_path) | |
if (not gtk_bus_name) or (not gtk_menu_object_path): | |
try_appmenu_interface(int(window_id, 16)) | |
else: | |
# Many apps does not respect menu action groups (libreoffice, gnome-mpv) thus we have to include all action groups | |
# And many other apps have these properties point to the same path (Sigh!), so we need to remove them! | |
gtk_actions_paths_list = [gtk_win_object_path, gtk_menu_object_path, gtk_app_object_path, gtk_unity_object_path] | |
gtk_actions_paths_list = list(set(gtk_actions_paths_list)) | |
try_gtk_interface(gtk_bus_name, gtk_menu_object_path, gtk_actions_paths_list) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment