Instantly share code, notes, and snippets.
Last active
May 30, 2017 12:55
-
Star
(0)
0
You must be signed in to star a gist -
Fork
(0)
0
You must be signed in to fork a gist
-
Save encomiastical/caa0ee955300bc2a40ef55d123b06212 to your computer and use it in GitHub Desktop.
appmenu2json
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 dbus | |
import subprocess | |
import json | |
from gi.repository import Gio | |
from gi.repository import GLib | |
""" | |
format_label_list | |
""" | |
def format_label(label): | |
return str(label).replace("_", "") | |
""" | |
try_appmenu_interface | |
""" | |
def try_appmenu_interface(window_id): | |
# --- Get Appmenu Registrar DBus interface | |
#session_bus = sbus | |
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_items = dbusmenu_object_iface.GetLayout( | |
0, -1, ["label", "children-display", "submenu"]) | |
dbusmenu_item_dict = dict() | |
""" explore_dbusmenu_item """ | |
def explore_dbusmenu_item(item, root): | |
item_id = item[0] | |
item_props = item[1] | |
# expand if necessary | |
if 'children-display' in item_props or 'submenu' in item_props: | |
#print("rerouting") | |
dbusmenu_object_iface.AboutToShow(item_id) | |
#TODO: dbusmenu_object_iface.Event(item_id, "opened", "not used", dbus.UInt32(time.time())) #fix firefox | |
item = dbusmenu_object_iface.GetLayout(item_id, -1, ["label", "children-display", "submenu"])[1] | |
item_children = item[2] | |
if 'label' in item_props and not root: | |
if len(item_children) == 0: | |
me = {format_label(item_props['label']): item_id} | |
else: | |
child_list = {} | |
for child in item_children: | |
ret = explore_dbusmenu_item(child, False) | |
if ret: | |
child_list.update(ret) | |
me = {format_label(item_props['label']): child_list} | |
else: | |
if len(item_children) != 0: | |
child_list = {} | |
for child in item_children: | |
child_list.update(explore_dbusmenu_item(child, False)) | |
return child_list | |
return | |
return me | |
dbusmenu_item_dict = explore_dbusmenu_item(dbusmenu_items[1], True) | |
out = ['dbusmenu', dbusmenu_bus, dbusmenu_object_path, dbusmenu_item_dict] | |
output(out) | |
# --- Fix firefox: send closed events to 1 level items to makesure nothing weird happen | |
# Firefox will close the submenu items (luckily!) | |
# 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())) | |
# credit @snippins | |
session_bus.close() | |
""" | |
try_gtk_interface | |
""" | |
def try_gtk_interface(gtk_bus_name_cmd, gtk_object_path_cmd, gtk_application_path_cmd): | |
gtk_bus_name = gtk_bus_name_cmd.split(' ')[2].split('\n')[0].split('"')[1] | |
gtk_object_path = gtk_object_path_cmd.split(' ')[2].split('\n')[0].split('"')[1] | |
gtk_application_path = gtk_application_path_cmd.split(' ')[2].split('\n')[0].split('"')[1] | |
# --- Ask for menus over DBus --- | |
connection = Gio.bus_get_sync(Gio.BusType.SESSION, None) | |
proxy = Gio.DBusProxy.new_sync(connection, Gio.DBusProxyFlags.NONE, None, gtk_bus_name, gtk_object_path, "org.gtk.Menus", None) | |
# 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 | |
# populate() is a helper method for explore(), it takes a dict and populates it with an array of elements: [ID, {dict of menu-structure}] | |
# It also takes a third parameter that gets deleted in the end | |
# - If the ID of an element is a key in the dict (the ID was already added to the dict by a section), it puts the content of elements under the label which is the value of ID in dict | |
# - If it's not known it just adds it under the root | |
# This is used for the ':section' property: If theres a section that points to the parent we are currently exploring we'll want to add ecerything that it points to under the section label | |
# 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 | |
usedLayers = [] | |
def Start(i): | |
usedLayers.append(i) | |
return proxy.call_sync("Start", GLib.Variant('(au)', ([i],)), Gio.DBusProxyFlags.NONE, 500, None)[0] | |
# --- Construct menu list --- | |
def populate(me, elements, sct): | |
# populates a layout blueprint (me) with provided elements, afterwards deletes all keys that are in sct | |
for element in reversed(elements): | |
if element[0] in me: | |
me.update({me[element[0]]: element[1]}) | |
else: | |
me.update(element[1]) | |
if len([sct]) > 0: | |
for i in sct: | |
del me[i] | |
return me | |
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): | |
# path = path to parent | |
parent_layout = {} | |
sct = [] # sections | |
for node in parent: | |
sub_elements = [] # processed elements under a node | |
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 reversed(content): | |
me = {} # menu entry | |
# 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: | |
l = format_label(element['label']) | |
#sub_elements.append([node[1], {l: explore(Start(element[':submenu'][0]), path + "*/*" + l)}]) | |
queue(element[':submenu'][0], None, path + "*/*" + l) | |
# 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]: | |
me.update({element[':section'][1]: element['label']}) | |
sct.append(element[':section'][1]) | |
# section points to node of this parent | |
# Here we add the section to our layout blueprint so that we can later file its contents under its label | |
# We also put it on the sct (to be deleted) list | |
# For increased performance when locating the section later on we use the ID the section points to as a key and it's label as the value, we'll have to | |
# switch these in populate | |
else: | |
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: | |
# We add the action and its target to the sub_elements | |
target = [] | |
if 'target' in element: | |
target.append(element['target']) | |
#menu_action = str(element['action']).replace('unity.', '').replace('app.', '') | |
menu_action = str(element['action']).split(".", 1)[1] | |
sub_elements.append([node[1], {format_label(element['label']): [menu_action, 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: | |
#sub_elements.append([node[1], explore(Start(element[':submenu'][0], element[':submenu'][1]), path)]) | |
queue(element[':submenu'][0], None, path) | |
# We queue the submenu under the parent without a label | |
elif 'action' in element: | |
# This would be an unlabeled action, we do the same what we would do with a labeled one, but we'll call it UNDESCRIBED action | |
# This shouldn't happen | |
target = [] | |
if 'target' in element: | |
target.append(element['target']) | |
#menu_action = str(element['action']).replace('unity.', '').replace('app.', '') | |
menu_action = str(element['action']).split(".", 1)[1] | |
sub_elements.append([node[1], {'UNDESCRIBED ACTION': [menu_action, target]}]) | |
parent_layout.update(me) | |
# We update the parent_layout with the blueprint of this element | |
parent_layout = populate(parent_layout, sub_elements, sct) | |
# We fill out the blueprint with all the actions | |
return parent_layout | |
gtk_menubar_layout = dict() | |
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: | |
path = layer[2].split("*/*") | |
# we use */* to seperate the path so Files->Print = Files*/*Print | |
p = gtk_menubar_layout | |
if len("".join(path)) > 0: | |
#print(path) | |
for i in range(len(path)): | |
if path[i] in p: | |
p = p[path[i]] | |
elif path[i] != "" and path[i] != None: | |
p[path[i]] = {} | |
p = p[path[i]] | |
# we use the path to get to the right section in the dict | |
# If a section indicated earlier that it wants to add the contents under its label we do this | |
if layer[1]: | |
p.update({layer[1]: explore(Start(layer[0]), layer[2])}) | |
else: | |
# If not we just add it directly under parent | |
p.update(explore(Start(layer[0]), layer[2])) | |
a = GLib.Variant('(au)', (usedLayers,)) | |
proxy.call_sync("End", a, Gio.DBusProxyFlags.NONE, 500, None) | |
out = ['gtk', gtk_bus_name, gtk_application_path, gtk_menubar_layout] | |
output(out) | |
def output(out): | |
global exit | |
#print("outputting a " + str(out[0]) + " menu") | |
with open('/tmp/currentMenuLayout.json', 'w+') as f: | |
json.dump(out, f) | |
f.close() | |
exit = True | |
""" | |
main | |
""" | |
exit = False | |
# --- Get X Window ID --- | |
window_id_cmd = subprocess.check_output(['xprop', '-root', '-notype', '_NET_ACTIVE_WINDOW']).decode('utf-8') | |
window_id = window_id_cmd.split(' ')[4].split('\n')[0] | |
# --- Get GTK MenuModel Bus name --- | |
#window_id = "0x200004b" | |
gtk_bus_name_cmd = subprocess.check_output(['xprop', '-id', window_id, '-notype', '_GTK_UNIQUE_BUS_NAME']).decode('utf-8') | |
gtk_object_path_cmd = subprocess.check_output(['xprop', '-id', window_id, '-notype', '_GTK_APP_MENU_OBJECT_PATH']).decode('utf-8') | |
if gtk_object_path_cmd == '_GTK_APP_MENU_OBJECT_PATH: not found.\n' or gtk_object_path_cmd == '_GTK_APP_MENU_OBJECT_PATH: no such atom on any window.\n': | |
gtk_object_path_cmd = subprocess.check_output(['xprop', '-id', window_id, '-notype', '_GTK_MENUBAR_OBJECT_PATH']).decode('utf-8') | |
gtk_application_path_cmd = subprocess.check_output(['xprop', '-id', window_id, '-notype', '_GTK_APPLICATION_OBJECT_PATH']).decode('utf-8') | |
if gtk_application_path_cmd == '_GTK_APPLICATION_OBJECT_PATH: not found.\n' or gtk_application_path_cmd == '_GTK_APPLICATION_OBJECT_PATH: no such atom on any window.\n': | |
gtk_application_path_cmd = gtk_object_path_cmd | |
if gtk_bus_name_cmd == '_GTK_UNIQUE_BUS_NAME: not found.\n' or gtk_object_path_cmd == '_GTK_MENUBAR_OBJECT_PATH: not found.\n': | |
try_appmenu_interface(int(window_id, 16)) | |
else: | |
try_gtk_interface(gtk_bus_name_cmd, gtk_object_path_cmd, gtk_application_path_cmd) | |
if not exit: | |
output("{}") |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment