Skip to content

Instantly share code, notes, and snippets.

@encomiastical
Last active May 30, 2017 12:55
Show Gist options
  • Save encomiastical/caa0ee955300bc2a40ef55d123b06212 to your computer and use it in GitHub Desktop.
Save encomiastical/caa0ee955300bc2a40ef55d123b06212 to your computer and use it in GitHub Desktop.
appmenu2json
#!/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