aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-xautomount-trayicon.py327
-rwxr-xr-xstackrpms-automount.sh3
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}"
bgstack15