-
Notifications
You must be signed in to change notification settings - Fork 1
/
pappymenu.py
executable file
·184 lines (147 loc) · 5.54 KB
/
pappymenu.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
#!/usr/bin/python
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, GdkPixbuf
from xdg import BaseDirectory
import os
import json
import sys
__appname__ = 'pappymenu'
__version__ = (0, 1)
__source__ = 'http://github.com/Kingdread/pappymenu'
command_string = None
def cache_path():
"""Return the path of the cache file."""
return os.path.join(BaseDirectory.save_cache_path(__appname__),
'menu-cache')
def regenerate_cache(path):
"""Regenerate the cache file at the given path."""
# Yes, we could use xdg.Menu here, but it seems to error when
# 'applications.menu' is not found. Therefore, we use the external tool
# 'xdg_menu', which doesn't care and correctly scans
# /usr/share/applications ($XDG_DATA_DIR/applications) without worrying
# about missing files. In the future though, we might switch.
# The openbox3 format seems nice to parse as XML and contains the icon, so
# no need to write a new custom parser.
# The cache format is a simple json document in the following structure:
# {
# 'menu_name':
# ('/menu/icon.png',
# {
# 'app_name': ('/app/icon.png', 'app-command'),
# ...
# }),
# ...
# }
# Importing here to save time if we don't need to regenerate the cache
import subprocess
from lxml import etree
try:
menu_xml = subprocess.check_output(['xdg_menu', '--format',
'openbox3-pipe'])
except FileNotFoundError:
print("Please install xdg_menu for {} to work!".format(__appname__),
file=sys.stderr)
sys.exit(1)
# Valid xml needs a root element
menu = etree.fromstring(menu_xml)
result = {}
for menu in menu.xpath('//openbox_pipe_menu/menu'):
menu_name = menu.attrib['label']
menu_icon = menu.get('icon', '')
programs = {}
for app in menu.findall('item'):
app_name = app.attrib['label']
app_icon = app.get('icon', '')
app_command = ''.join(app.itertext()).strip()
programs[app_name] = (app_icon, app_command)
result[menu_name] = (menu_icon, programs)
# Save the cache
with open(path, 'w') as output_file:
json.dump(result, output_file)
return result
def get_menu_data():
"""Returns a dict of Name->Submenu entries."""
path = cache_path()
if not os.path.isfile(path):
return regenerate_cache(path)
# Load the cache
with open(path) as input_file:
return json.load(input_file)
def icon_item(icon_filename, label, icon_size=(16, 16)):
"""Return a Gtk.MenuItem with the given icon and label set."""
item = Gtk.MenuItem()
box = Gtk.HBox()
box.set_halign(Gtk.Align.START)
box.set_spacing(5)
width, height = icon_size
if icon_filename:
icon_buf = GdkPixbuf.Pixbuf.new_from_file(icon_filename)
icon = Gtk.Image.new_from_pixbuf(icon_buf.scale_simple(width, height, GdkPixbuf.InterpType.HYPER))
else:
# Aligning the text by providing an empty icon
icon_buf = GdkPixbuf.Pixbuf.new(GdkPixbuf.Colorspace.RGB, True, 8, width, height)
# Fill with transparent black
icon_buf.fill(0)
icon = Gtk.Image.new_from_pixbuf(icon_buf)
icon.show()
box.add(icon)
lbl = Gtk.Label(label)
lbl.show()
box.add(lbl)
box.show()
item.add(box)
item.show()
return item
def make_menu():
root_menu = Gtk.Menu()
menu_data = get_menu_data()
for category, content in sorted(menu_data.items()):
category_icon, programs = content
menu = icon_item(category_icon, category)
submenu = Gtk.Menu()
for program, content in sorted(programs.items()):
program_icon, program_cmd = content
item = icon_item(program_icon, program)
item.connect('activate', set_command, program_cmd)
submenu.append(item)
menu.set_submenu(submenu)
root_menu.append(menu)
return root_menu
def set_command(widget, cmd_string):
"""Set the command to execute and quit gtk."""
global command_string
command_string = cmd_string
Gtk.main_quit()
if '-h' in sys.argv:
print("""Usage: {executable} [-h | -v | -r]
{app} is an application-launcher that (compared to dmenu &c) provides a
graphical list of installed applications. {app} uses xdg_menu under the hood to
generate the menu entries. Calling {executable} without arguments will provide
the menu at the mouse pointer location. Select a program to start it, press
<Esc> to cancel.
Available options are:
-h\tShow this help.
-v\tShow the version.
-r\tRegenerate the cache at {cache}. This is automatically done when the
\tcache doesn't yet exist.
""".format(executable=sys.argv[0], app=__appname__, cache=cache_path()))
elif '-v' in sys.argv:
print("{} version {}.\n(c) by Daniel Schadt.\n{}".format(
__appname__, '.'.join(map(str, __version__)), __source__))
elif '-r' in sys.argv:
print("Regenerating menu cache...")
regenerate_cache(cache_path())
print("Cache saved.")
else:
menu = make_menu()
menu.connect('cancel', Gtk.main_quit)
menu.connect('deactivate', Gtk.main_quit)
# A bit of a "dirty hack", having a menu popup without a widget or a button...
menu.popup(None, None, None, None, 0, 0)
Gtk.main()
# Execute the menu action, if one was selected
if command_string:
import shlex
args = shlex.split(command_string)
os.execvp(args[0], args)