diff options
-rwxr-xr-x | automount-trayicon.py | 327 | ||||
-rwxr-xr-x | stackrpms-automount.sh | 3 |
2 files changed, 209 insertions, 121 deletions
diff --git a/automount-trayicon.py b/automount-trayicon.py index 7e11879..84312e7 100755 --- a/automount-trayicon.py +++ b/automount-trayicon.py @@ -1,129 +1,142 @@ #!/usr/bin/python3 +# File: automount-trayicon.py # startdate: 2020-09-24 13:17 # References: -# vm2:~/dev/logout-manager/src/usr/bin/logout-manager-trayicon -# gapan/xdgmenumaker +# 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: -# use inotify for when to call reestablish_menu? -# move out config to separate file (read main .conf, but in shell format) -# add separator and "hide this icon" menu item like logout-manager-trayicon -# fix tabbing -# remove /sdb? (as option) -# hook up the click action to actually executing "xdg-open /browse/sdb" - -import gi, os, fnmatch, sys +# 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 -from gi.repository import Gdk +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. - ''' + ''' + 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 __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)) + 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. - ''' + ''' + 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 __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)) - 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 + # 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 + # 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) + # customized from gapan/xdgmenumaker + de = dentry.DesktopEntry(filename=desktopfile) - name = de.getName().encode('utf-8') + 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 + 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) + 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) + terminal = de.getTerminal() + if terminal: + command = '{term} -e {cmd}'.format(term=terminal_app, cmd=command) - path = de.getPath() - if not path: - path = None + path = de.getPath() + if not path: + path = None - categories = de.getCategories() - category = "Removable" # hardcoded xdg "category" for automount-trayicon + categories = de.getCategories() + category = "Removable" # hardcoded xdg "category" for automount-trayicon - app = App(name, icon, command, path) - mentry = MenuEntry(category, app) - return mentry + app = App(name, icon, command, path) + mentry = MenuEntry(category, app) + return mentry def desktopfilelist(params): # Ripped entirely from gapan/xdgmenumaker @@ -158,8 +171,31 @@ class MainIcon(Gtk.StatusIcon): 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 reestablish_menu(self,widget = None): + 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 @@ -171,40 +207,48 @@ class MainIcon(Gtk.StatusIcon): except: pass self.menuitems = [] - for entry in get_desktop_entries(["/media"]): - print('Label {} icon {}'.format(entry.app.name,entry.app.icon)) - self.add_action_to_menu(str(entry.app.name.decode("utf-8")),entry.app.path,entry.app.icon,self.dummy_action,entry.app.command) - #self.add_action_to_menu("foobar"+str(myrand),"media-floppy",self.dummy_action,"foo bar baz action "+str(myrand)) - #self.add_action_to_menu("mmc","media-flash-sd-mmc",self.dummy_action,"flubber second action") - #self.add_action_to_menu("what is this?","media-tape",self.dummy_action,"call ALF!") + 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() - self.add_action_to_menu("Update menu","",None,self.reestablish_menu,"re establish") - self.add_action_to_menu("Hide this icon","","system-logout",self.exit,"exit") - self.menuitem_count = len(self.menuitems) - 1 + 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 UNUSED_transform_command(self, entry_command): - return entry_command.replace("xdg-open ","umount ") - - def dummy_action(self, widget): - print("hello world") - x=0 - y=-1 + 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 - print(y) - print(self.menuitems[y]) + 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_action_to_menu(self,label_str,label_paren_str,icon_str,function_func,action_str): + + 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) @@ -214,10 +258,12 @@ class MainIcon(Gtk.StatusIcon): 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: - print("Double click!") + # 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, @@ -225,16 +271,59 @@ class MainIcon(Gtk.StatusIcon): 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() -#print(desktopfilelist(["/media"])) + 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): @@ -248,8 +337,6 @@ def get_desktop_entries(dir_array): pass return entries -#for entry in get_desktop_entries(["/media"]): -# print('label {} icon {}'.format(entry.app.name,entry.app.icon)) # MAIN icon = MainIcon() Gtk.main() diff --git a/stackrpms-automount.sh b/stackrpms-automount.sh index 232a7f6..a3f7c09 100755 --- a/stackrpms-automount.sh +++ b/stackrpms-automount.sh @@ -70,12 +70,13 @@ createEntry() { _atracks= _bytes="$( udevadm info "/dev/${_device}" 2>/dev/null )" _shortbytes="$( printf "%s\n" "${_bytes}" | sed -r -e 's/^E:\s*//;' | grep -E '^(ID_FS_TYPE|ID_MODEL|ID_VENDOR|ID_FS_LABEL|ID_CDROM_MEDIA_TRACK_COUNT_AUDIO)=' )" + unset ID_FS_TYPE ID_MODEL ID_VENDOR ID_FS_LABEL ID_CDROM_MEDIA_TRACK_COUNT_AUDIO eval "${_shortbytes}" _fs="${ID_FS_TYPE}" _model="${ID_MODEL}" _vendor="${ID_VENDOR}" _label="${ID_FS_LABEL}" - _atracks="${_ID_CDROM_MEDIA_TRACK_COUNT_AUDIO}" + _atracks="${ID_CDROM_MEDIA_TRACK_COUNT_AUDIO}" test -n "${DEBUG}" && echo "fs=${_fs} model=${_model} vendor=${_vendor} label=${_label} atracks=${_atracks}" 1>&2 test "${_fs}" = "" && test "${_atracks}" = "" && return # if the fs cannot be detected touch "${_filepath}" ; chmod 0755 "${_filepath}" |