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
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
|
#!/usr/bin/python3
# File: automount-trayicon.py
# startdate: 2020-09-24 13:17
# References:
# https://gitlab.com/bgstack15/logout-manager/-/blob/master/src/usr/bin/logout-manager-trayicon
# https://github.com/gapan/xdgmenumaker/blob/master/src/xdgmenumaker
# https://github.com/seb-m/pyinotify/blob/master/python2/examples/stats_threaded.py
# https://stackoverflow.com/questions/28279363/python-way-to-get-mounted-filesystems/28279434#28279434
# timer https://python-gtk-3-tutorial.readthedocs.io/en/latest/spinner.html?highlight=timer#id1
# vim: ts=3 sw=3 sts=3
# Improve:
# move out config to separate file (read main .conf, but in shell format)
# add all headers
# document
# Dependencies:
# autofs, root running the included stackrpms-automount
import gi, os, fnmatch, sys, pyinotify, time, subprocess
gi.require_version("Gtk","3.0")
from gi.repository import Gtk, Gdk, GLib
import xdg.DesktopEntry as dentry
import xdg.Exceptions as exc
showmount = 1 # show the "MOUNTED" value for each drive
skip_sd_without_partitions = 1 # skip sdb sdc, etc. # NOT IMPLEMENTED YET.
only_update_on_menuitem = 0 # if 1, then disables the mouseover poll. Prevents showmount=1 from working.
AUTOMOUNT_BASEDIR="/media"
AUTOMOUNT_BROWSEDIR="/browse"
# FUNCTIONS
class App:
'''
A class to keep individual app details in.
'''
def __init__(self, name, icon, command, path):
self.name = name
self.icon = icon
self.command = command
self.path = path
def __repr__(self):
return repr((self.name, self.icon, self.command, self.path))
class MenuEntry:
'''
A class for each menu entry. Includes the class category and app details
from the App class.
'''
def __init__(self, category, app):
self.category = category
self.app = app
def __repr__(self):
return repr((self.category, self.app.name, self.app.icon, self.app.command, self.app.path))
class Identity(pyinotify.ProcessEvent):
activity_count = 0
def process_default(self, event):
print(self,"Does nothing with the event number",self.activity_count)
self.activity_count += 1
#print(event)
def remove_command_keys(command, desktopfile, icon):
# replace the %i (icon key) if it's there. This is what freedesktop has to
# say about it: "The Icon key of the desktop entry expanded as two
# arguments, first --icon and then the value of the Icon key. Should not
# expand to any arguments if the Icon key is empty or missing."
if icon:
command = command.replace('"%i"', '--icon {}'.format(icon))
command = command.replace("'%i'", '--icon {}'.format(icon))
command = command.replace('%i', '--icon {}'.format(icon))
# some KDE apps have this "-caption %c" in a few variations. %c is "The
# translated name of the application as listed in the appropriate Name key
# in the desktop entry" according to freedesktop. All apps launch without a
# problem without it as far as I can tell, so it's better to remove it than
# have to deal with extra sets of nested quotes which behave differently in
# each WM. This is not 100% failure-proof. There might be other variations
# of this out there, but we can't account for every single one. If someone
# finds one another one, I can always add it later.
command = command.replace('-caption "%c"', '')
command = command.replace("-caption '%c'", '')
command = command.replace('-caption %c', '')
# replace the %k key. This is what freedesktop says about it: "The
# location of the desktop file as either a URI (if for example gotten from
# the vfolder system) or a local filename or empty if no location is
# known."
command = command.replace('"%k"', desktopfile)
command = command.replace("'%k'", desktopfile)
command = command.replace('%k', desktopfile)
# removing any remaining keys from the command. That can potentially remove
# any other trailing options after the keys,
command = command.partition('%')[0]
return command
def icon_strip(icon):
# strip the directory and extension from the icon name
icon = os.path.basename(icon)
main, ext = os.path.splitext(icon)
ext = ext.lower()
if ext == '.png' or ext == '.svg' or ext == '.svgz' or ext == '.xpm':
return main
return icon
def get_entry_info(desktopfile, ico_paths=True):
# customized from gapan/xdgmenumaker
de = dentry.DesktopEntry(filename=desktopfile)
name = de.getName().encode('utf-8')
if True:
icon = de.getIcon()
# full resolution of path is not required in this GTK program.
#if ico_paths:
# icon = icon_full_path(icon)
#else:
# icon = icon_strip(icon)
else:
icon = None
command = de.getExec()
command = remove_command_keys(command, desktopfile, icon)
terminal = de.getTerminal()
if terminal:
command = '{term} -e {cmd}'.format(term=terminal_app, cmd=command)
path = de.getPath()
if not path:
path = None
categories = de.getCategories()
category = "Removable" # hardcoded xdg "category" for automount-trayicon
app = App(name, icon, command, path)
mentry = MenuEntry(category, app)
return mentry
def desktopfilelist(params):
# Ripped entirely from gapan/xdgmenumaker
dirs = []
for i in params:
print("Checking dir",i)
i = i.rstrip('/')
if i not in dirs:
dirs.append(i)
filelist = []
df_temp = []
for d in dirs:
xdgdir = '{}/'.format(d) # xdg spec is to use "{}/applications" as this string, so use an applications subdir.
if os.path.isdir(xdgdir):
for root, dirnames, filenames in os.walk(xdgdir):
for i in fnmatch.filter(filenames, '*.desktop'):
# for duplicate .desktop files that exist in more
# than one locations, only keep the first occurence.
# That one should have precedence anyway (e.g.
# ~/.local/share/applications has precedence over
# /usr/share/applications
if i not in df_temp:
df_temp.append(i)
filelist.append(os.path.join(root, i))
return filelist
class MainIcon(Gtk.StatusIcon):
def __init__(self):
Gtk.StatusIcon.__init__(self)
self.set_from_icon_name("media-removable")
self.traymenu = Gtk.Menu()
self.connect("button-press-event", self.on_button_press_event)
self.connect("popup-menu", self.context_menu)
self.reestablish_menu()
if not only_update_on_menuitem:
self.connect("query-tooltip", self.mouseover)
# need these anyway, for when the icon is hidden.
self.wm1 = pyinotify.WatchManager()
self.s1 = pyinotify.Stats()
self.identity=Identity(self.s1) # provides self.identity.activity_counter which increments for every change to the watched directory
self.activity_count=0 # the cached value, for comparison. Update the menu when this changes!
self.notifier1 = pyinotify.ThreadedNotifier(self.wm1, default_proc_fun=self.identity)
self.notifier1.start()
self.wm1.add_watch(AUTOMOUNT_BASEDIR, pyinotify.IN_CREATE | pyinotify.IN_DELETE, rec=True, auto_add=True)
def mouseover(self, second_self, x, y, some_bool, tooltip):
#print("Mouseover happened at",str(x)+",",y,tooltip, some_bool)
need_showmount=0
if self.identity.activity_count != self.activity_count:
# then we need to update menu
print("Mouseover, and files have changed!")
self.activity_count = self.identity.activity_count
need_showmount=1
if need_showmount or showmount:
self.reestablish_menu()
def reestablish_menu(self,widget = None, silent=False):
if not silent:
print("Reestablishing the menu")
try:
if self.menuitems:
del self.menuitems
except:
pass
try:
for i in self.traymenu.get_children():
self.traymenu.remove(i)
except:
pass
self.menuitems = []
for entry in get_desktop_entries([AUTOMOUNT_BASEDIR]):
if not silent:
print('{} {} {} {}'.format(entry.app.name.decode("utf-8"),entry.app.icon,entry.app.path,entry.app.command))
self.add_menuitem(str(entry.app.name.decode("utf-8")),entry.app.path,entry.app.icon,self.execute,entry.app.command)
self.add_separator_to_menu()
if only_update_on_menuitem:
self.add_menuitem("Update menu","",None,self.reestablish_menu,"re establish") # If you want a menu option for this
self.add_menuitem("Hide until next disk change","","",self.hide,"hide")
self.add_menuitem("Exit automount-trayicon","","system-logout",self.exit,"exit")
self.menuitem_count = len(self.menuitems) - 2
self.set_tooltip_text(str(self.menuitem_count)+" mount point"+("s" if self.menuitem_count > 1 else ""))
def execute(self, widget):
x=0 ; y=-1
for n in widget.get_parent().get_children():
x+=1
if widget == n:
y=x-1
break
if y > -1:
print(y,self.menuitems[y])
# use subprocess to open in background so failure to mount dialogs do not block the program
subprocess.Popen(self.menuitems[y].split())
#os.system(self.menuitems[y])
else:
print("Some kind of error?! How did this get called without a menu entry.")
#print(repr(self.s1))
def add_separator_to_menu(self):
i=Gtk.SeparatorMenuItem.new()
i.set_visible(True)
self.traymenu.append(i)
def add_menuitem(self,label_str,label_paren_str,icon_str,function_func,action_str):
self.menuitems.append(action_str)
full_label_str=label_str
mounted_str=""
# collection = [line.split()[1] for line in open("/etc/mtab") if line.split()[1].startswith('/browse') and line.split()[2] != "autofs"]
if label_paren_str is not None and label_paren_str != "":
if showmount:
if label_paren_str in [line.split()[1] for line in open("/etc/mtab") if line.split()[1].startswith(AUTOMOUNT_BROWSEDIR) and line.split()[2] != "autofs"]:
label_paren_str = "MOUNTED " + label_paren_str
full_label_str += " (" + label_paren_str + ")"
i = Gtk.ImageMenuItem.new_with_mnemonic(full_label_str)
j = Gtk.Image.new_from_icon_name(icon_str,32)
j.show()
i.set_image(j)
i.set_always_show_image(True)
i.show()
i.connect("activate", function_func)
self.traymenu.append(i)
def on_button_press_event(self, b_unknown, event: Gdk.EventButton):
# for some reason the single click functionality prevents the double click functionality
if Gdk.EventType._2BUTTON_PRESS == event.type:
# not achievable if we are popping up the menu in the single click.
print("Double click")
else:
print("Single click")
self.traymenu.popup(None, None,
self.position_menu,
self,
event.button,
Gtk.get_current_event_time())
def context_menu(self, widget, event_button, event_time):
self.traymenu.popup(None, None,
self.position_menu,
self,
event_button,
Gtk.get_current_event_time())
def hide(self, widget = None):
print("Please hide self!")
self.set_visible(False)
self.start_timer()
def start_timer(self):
""" Start the timer """
self.counter = 1000 # milliseconds, but hardly a precise timer. Do not use this for real calculations
self.timeout_id = GLib.timeout_add(1,self.on_timeout, None)
def stop_timer(self, reason_str):
if self.timeout_id:
GLib.source_remove(self.timeout_id)
self.timeout_id = None
#print("Timer was stopped!")
self.check_reenable_now()
def check_reenable_now(self):
print("Need to check if anything has changed.")
print(self.identity.activity_count,self.activity_count)
if self.identity.activity_count != self.activity_count:
self.set_visible(True)
self.activity_count = self.identity.activity_count
self.reestablish_menu()
else:
print("No changes...")
self.hide()
def on_timeout(self, *args, **kwargs):
""" A timeout function """
self.counter -= 1
if self.counter <= 0:
self.stop_timer("Reached time out")
return False
return True
def exit(self, widget):
self.notifier1.stop()
quit()
def __quit__(self):
self.notifier1.stop()
Gtk.StatusIcon.__quit__(self)
# Normally call get_desktop_entries with (["/media"])
def get_desktop_entries(dir_array):
entries = []
for desktopfile in desktopfilelist(dir_array):
try:
entry = get_entry_info(desktopfile, True)
if entry is not None:
#print(entry)
entries.append(entry)
except exc.ParsingError as Ex:
print(Ex)
pass
return entries
# MAIN
icon = MainIcon()
Gtk.main()
|