summarylogtreecommitdiffstats
path: root/hud
diff options
context:
space:
mode:
authornikatar2020-04-21 17:27:10 +0300
committernikatar2020-04-21 17:27:10 +0300
commitbee181761ee4548e3ed674dc3d26b0b1712b1069 (patch)
tree6b1a5cce735468841cdbc5ed23c27e96b895ffd3 /hud
downloadaur-hud.tar.gz
release 1.0-1
Diffstat (limited to 'hud')
-rw-r--r--hud352
1 files changed, 352 insertions, 0 deletions
diff --git a/hud b/hud
new file mode 100644
index 000000000000..7c567a9ae8ed
--- /dev/null
+++ b/hud
@@ -0,0 +1,352 @@
+#!/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', '-5',
+ '-width', '60',
+ '-lines', '25',
+ '-fixed-num-lines'
+ '-separator-style', 'solid'],
+ 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', '-5',
+ '-width', '60',
+ '-lines', '25',
+ '-fixed-num-lines'
+ '-separator-style', 'solid'],
+ 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)