From cc8f90af75ee106726a95653325f51224c8c016d Mon Sep 17 00:00:00 2001 From: rob Date: Thu, 13 Oct 2016 11:55:04 +0200 Subject: [PATCH] release v1.1 --- readme | 30 ++ sshplus.py | 569 +++++++++++++++++++++++---- sshplus/icons/avatar2.png | Bin 0 -> 15435 bytes sshplus/icons/icon-connected-eye.png | Bin 0 -> 735 bytes sshplus/menus.conf | 27 ++ sshplus/sshplus.cfg | 67 ++++ 6 files changed, 616 insertions(+), 77 deletions(-) create mode 100755 sshplus/icons/avatar2.png create mode 100644 sshplus/icons/icon-connected-eye.png create mode 100644 sshplus/menus.conf create mode 100644 sshplus/sshplus.cfg diff --git a/readme b/readme index b5061d8..0db9b4d 100644 --- a/readme +++ b/readme @@ -83,3 +83,33 @@ Deprecated in favor of sshplus.py A sshmenu like alternative for appindicator menu, which supports sshmenu configuration file, thus becoming a drop in replacement on Ubuntu's unity launcher. + v1.1 Release notes: rnijenhu: Compartible with previous version but it will also + check /etc/sshplus/sshplus.cfg for more menus, + with this cfg file following options/functions becomes available as well: + + Support for multiple (global) menu files, just create + /etc/sshplus/sshplus.cfg (or a link to the network version) + + Simpel access control, which user is able too see the menu + + CLI option to install sshplus in the sys or user startup folder + + Support for icons before label/folder/task items + + Added a 'recently used' menu with auto refresh + + customizable text and icon for the indicator + + command availibility checking, if not within PATH the icon + displays an error (command not blocked). + Usefull when you got an application installed on several machines + but not on all + + folder IP checking, before a menu is available a specified IP + must be reachable. Especially for (local) ssh calls to machines + that are only available when a VPN is connected. A refresh after + setting up the connection will make the menu available. + This is (a little) time consuming, + use it wisely (and is it implemeted for folders only). + + Support for env vars in the config files + + menu files can be read from json as well, for humans I would recommend + the previous file format + + sample files are provided in the subfolder 'sshplus' + + + + + diff --git a/sshplus.py b/sshplus.py index 76448f7..b77f54b 100755 --- a/sshplus.py +++ b/sshplus.py @@ -28,6 +28,8 @@ # 3. Launch sshplus.py # 4. Or better yet, add it to gnome startup programs list so it's run on login. +#todo: global section ins sshplus.cfg is not working + import gobject import gtk import appindicator @@ -36,17 +38,26 @@ import sys import shlex import re +import ConfigParser +#import subprocess +import socket +import getpass +import json -_VERSION = "1.0" -_SETTINGS_FILE = os.getenv("HOME") + "/.sshplus" -_SSHMENU_FILE = os.getenv("HOME") + "/.sshmenu" +_USER=getpass.getuser() +_VERSION = "1.1" +_BIN_PATH=os.path.realpath(os.path.dirname(__file__)) +_ETC_CONFIG = "/etc/sshplus/sshplus.cfg" +_SYS_CONFIG = "%s/sshplus/sshplus.cfg"%(_BIN_PATH) +_SETTINGS_FILE = "%s/.sshplus"%(os.getenv("HOME")) +_SSHMENU_FILE = "%s/.sshmenu"%(os.getenv("HOME")) _ABOUT_TXT = """A simple application starter as appindicator. To add items to the menu, edit the file .sshplus in your home directory. Each entry must be on a new line in this format: -NAME|COMMAND|ARGS +NAME|(ICON|)COMMAND|ARGS If the item is clicked in the menu, COMMAND with arguments ARGS will be executed. ARGS can be empty. To insert a separator, add a line which only contains "sep". Lines starting with "#" will be ignored. You can set an unclickable label with the prefix "label:". Items from sshmenu configuration will be automatically added (except nested items). To insert a nested menu, use the prefix "folder:menu name". Subsequent items will be inserted in this menu, until a line containing an empty folder name is found: "folder:". After that, subsequent items get inserted in the parent menu. That means that more than one level of nested menus can be created. @@ -62,34 +73,137 @@ SSH Ex|gnome-terminal|-x ssh user@1.2.3.4 # to mark the end of items inside "Home", specify and empty folder: folder: -# this item appears in the main menu -SSH Ex|gnome-terminal|-x ssh user@1.2.3.4 +# this item appears in the main menu with icon SSH +SSH Ex|SSH|gnome-terminal|-x ssh user@1.2.3.4 label:RDP connections RDP Ex|rdesktop|-T "RDP-Server" -r sound:local 1.2.3.4 -Copyright 2011 Anil Gulecha +Copyright 2011 Anil Gulecha +Copyright 2016 rnijenhu, http://www.tenijenhuis.net Incorporating changes from simplestarter, Benjamin Heil, http://www.bheil.net + Released under GPL3, http://www.gnu.org/licenses/gpl-3.0.html""" + +_ICONS={} +_RECENT={} +_RECENT_DATA=[] +_RECENT_COUNTER=0 +_SEPARATOR={'name':'sep','type':'seperator'} +_FOLDER_POP={'name':'FOLDER','type':'folder', 'caption':'', 'cmd':''} +_MENU_TITLE="Launch" +_MENU_ICON="gnome-netstatus-tx" +_MENUS=[] + +def read_config(configfile): + global _ICONS + global _RECENT + global _RECENT_DATA + global _RECENT_COUNTER + global _MENU_TITLE + global _MENU_ICON + + if not os.path.exists(configfile): + print("Config file not found: %s\n"%(configfile)) + return [] + + config = ConfigParser.ConfigParser({ + 'title' : _MENU_TITLE, + 'icon' : _MENU_ICON + }) + config.read(configfile) + + menufiles=[] + + #read the config and update the settings + sections=config.sections() + + for section in sections: + if section.startswith("menu"): + menufiles.append(dict(config.items(section))) + elif section.startswith("icons"): + #read the icons we can use + _ICONS=dict(config.items(section)) + elif section.startswith("sshplus"): + _MENU_TITLE=config.get("sshplus","title") + _MENU_ICON=config.get("sshplus","icon").lower() + elif section.startswith("recent"): + #read the history files + _RECENT=dict(config.items(section)) + else: + print ("Unknown section ignored: %s\n"%(section)) + + #for ubuntu this will work, others not I guess. maybe we should provide it + if not 'broken' in _ICONS: + _ICONS['broken']="/usr/share/icons/gnome/16x16/status/dialog-warning.png" + if not 'missing' in _ICONS: + _ICONS['missing']="/usr/share/icons/gnome/16x16/status/messagebox_critical.png" + + if 'file' in _RECENT: + _RECENT['file']=os.path.expandvars(_RECENT['file']) + + if _MENU_ICON in _ICONS: + _MENU_ICON=os.path.expandvars(_ICONS[_MENU_ICON]) + + _RECENT_DATA=[] + _RECENT_COUNTER=0 + + return menufiles + +def notify(msg1,msg2): + pynotify.init("sshplus") + pynotify.Notification(msg1,msg2).show() + +def refresh(): + newmenu = build_menu() + ind.set_menu(newmenu) + def menuitem_response(w, item): - if item == '_about': + global _RECENT_DATA + global _RECENT + global _RECENT_COUNTER + + if 'cmd' in item and item['cmd']=='_about': show_help_dlg(_ABOUT_TXT) - elif item == '_refresh': - newmenu = build_menu() - ind.set_menu(newmenu) - pynotify.init("sshplus") - pynotify.Notification("SSHplus refreshed", "Menu list was refreshed from %s" % _SETTINGS_FILE).show() - elif item == '_quit': + elif 'cmd' in item and item['cmd']=='_refresh': + refresh() + notify("SSHplus refreshed", "Menu list was refreshed from %s"%(_SETTINGS_FILE)) + elif 'cmd' in item and item['cmd']=='_quit': sys.exit(0) - elif item == 'folder': + elif 'type' in item and item['type']=='folder': pass else: - print item - os.spawnvp(os.P_NOWAIT, item['cmd'], [item['cmd']] + item['args']) + myargs=[item['cmd']] + item['args'] + for i,s in enumerate(myargs): + myargs[i]=os.path.expandvars(s) + + p = os.spawnvp(os.P_NOWAIT, os.path.expandvars(item['cmd']), myargs) os.wait3(os.WNOHANG) + if not isinstance( p, int ): + notify(' '.join(myargs),p) + + #build the recent file + if 'file' in _RECENT: + #clean up the (same) previous entry, we will add a new one at the top + for i,d in enumerate(_RECENT_DATA): + if 'name' in d and d['name']==item['name']: + del _RECENT_DATA[i] + + #append at the beginning and write the recent file + d=[item]+_RECENT_DATA + with open(_RECENT['file'], 'w') as outfile: + json.dump(d, outfile) + _RECENT_DATA=d + _RECENT_COUNTER=_RECENT_COUNTER+1 + + #after x updates do a menu refresh + if 'refresh' in _RECENT: + if int(_RECENT['refresh'])>0 and _RECENT_COUNTER >= int(_RECENT['refresh']): + _RECENT_COUNTER=0 + refresh() def show_help_dlg(msg, error=False): if error: @@ -103,21 +217,100 @@ def show_help_dlg(msg, error=False): md.run() finally: md.destroy() - -def add_separator(menu): - separator = gtk.SeparatorMenuItem() - separator.show() - menu.append(separator) - -def add_menu_item(menu, caption, item=None): - menu_item = gtk.MenuItem(caption) - if item: - menu_item.connect("activate", menuitem_response, item) + +def which(program): + + def is_exe(fpath): + return os.path.isfile(fpath) and os.access(fpath, os.X_OK) + + fpath, fname = os.path.split(program) + if fpath: + if is_exe(program): + return program + else: + for path in os.environ["PATH"].split(os.pathsep): + path = path.strip('"') + exe_file = os.path.join(path, program) + if is_exe(exe_file): + return exe_file + + return None + +def ping(ip): + + ret = os.system("ping -c2 -W1 %s"%(ip)) + if ret == 0: + return True + + print "IP not found: %s\n"%(ip) + return False + +def add_menu_item2(menu, app): + + menu_item=None + menu_item_sens=True + + #get the type + if 'type' not in app: + print "Error: app without type element:%s\n"%(app) + return gtk.MenuItem("????") + + #get the menu entry caption + if 'caption' in app: + caption=app['caption'] + else: + caption="???????" + + if app['type']=='seperator': + menu_item = gtk.SeparatorMenuItem() + + #create the menu entry with icon + elif 'icon' in app: + menu_item = gtk.ImageMenuItem(caption) + img = gtk.Image() + iconame=app['icon'].lower() + + #execute items: check if the cmd exists, if not display the missing icon + if app['type']=="execute" and not which(app['cmd']): + print("Command not found in path: %s\n"%(app['cmd'])) + img.set_from_file(os.path.expandvars(_ICONS['missing'])) + + #folder items: check if the ip exists, if not disable the the folder + elif app['type']=="folder" and 'ip' in app and not ping(app['ip']): + print("IP not found: %s\n"%(app['ip'])) + menu_item_sens=False + img.set_from_file(os.path.expandvars(_ICONS[iconame])) + + #add the icon, when the icon exists + elif iconame in _ICONS and os.path.exists(os.path.expandvars(_ICONS[iconame])): + img.set_from_file(os.path.expandvars(_ICONS[iconame])) + + #if nothing is found but a icons was requested: use the broken icon + else: + img.set_from_file(os.path.expandvars(_ICONS['broken'])) + + img.show() + menu_item.set_image(img) + + #create the menu entry without icon else: - menu_item.set_sensitive(False) + menu_item = gtk.MenuItem(caption) + + #add the menu action + if app['type'] == "label": + menu_item_sens=False + elif app['type'] != "seperator": + menu_item.connect("activate", menuitem_response, app) + + #set the sensitivity + menu_item.set_sensitive(menu_item_sens) + + #enable the menu in the main menu menu_item.show() menu.append(menu_item) - return menu_item + + return menu_item + def get_sshmenuconfig(): if not os.path.exists(_SSHMENU_FILE): @@ -148,8 +341,10 @@ def get_sshmenuconfig(): elif re.search("items:",line): app_list.append({ 'name': 'FOLDER', + 'type': 'folder', + 'caption': 'SSHmenu', 'cmd': "SSHmenu", - 'args':"", + 'args':[], }) stackMenuIndex.append(len(app_list) - 1) @@ -158,8 +353,10 @@ def get_sshmenuconfig(): app_list[stackMenuIndex.pop()]["cmd"] = smtitle app_list.append({ 'name': 'FOLDER', + 'type': 'folder', + 'caption': '', 'cmd': "", - 'args':"", + 'args':[], }) smflag = 0 @@ -171,6 +368,8 @@ def get_sshmenuconfig(): app_list.append({ 'name': smtitle, 'cmd': 'gnome-terminal', + 'caption': smtitle, + 'type': 'execute', 'args': arglist, }) smflag=0 @@ -179,39 +378,133 @@ def get_sshmenuconfig(): print "error in line:" + line return [] -def get_sshplusconfig(): - if not os.path.exists(_SETTINGS_FILE): - return [] +def get_sshplusconfig(settings_file): + #init values + _settings_file=os.path.expandvars(settings_file) app_list = [] - f = open(_SETTINGS_FILE, "r") + + #does the file exists, if not return nothing + if not os.path.exists(_settings_file): + return [] + else: + print("Load menu: %s\n"%(_settings_file)) + + + #load the menus from a json file + _settings_file_name, _settings_file_ext = os.path.splitext(_settings_file) + if _settings_file_ext == ".json": + with open(_settings_file) as json_data: + try: + d=json.load(json_data) + + except ValueError, e : + print("Failed to load, syntax error \n") + return [] + + if not isinstance(d,list): + d=[] + + return d + + #load the user settings + elif _settings_file_ext =="" and os.path.basename(_settings_file) == ".sshplus": + print "User setting file: %s\n"%(_settings_file) + + #quit if the extension isn't known (eg conf for now) + elif _settings_file_ext != ".conf": + print "Unknown file format: %s\n"%(_settings_file_ext) + sys.exit(0) + + #read the settings from the orginal macro format + f = open(_settings_file, "r") try: for line in f.readlines(): line = line.rstrip() if not line or line.startswith('#'): continue + + #process SEPERATORS elif line == "sep": - app_list.append('sep') + app_list.append(_SEPARATOR) + + #process LABEL items elif line.startswith('label:'): - app_list.append({ - 'name': 'LABEL', - 'cmd': line[6:], - 'args': '' - }) + if line.count(':')==1: + app_list.append({ + 'name': 'LABEL', + 'cmd': line[6:], + 'caption': line[6:], + 'args': [], + 'type':'label' + }) + + elif line.count(':')==2: + name, icon, cmd = line.split(':', 2) + app_list.append({ + 'name': 'LABEL', + 'icon': icon.lower(), + 'cmd': cmd, + 'caption': cmd, + 'args': [], + 'type':'label' + }) + + #process FOLDER items elif line.startswith('folder:'): - app_list.append({ - 'name': 'FOLDER', - 'cmd': line[7:], - 'args': '' - }) + if line.count(':')==1: + app_list.append({ + 'name': 'FOLDER', + 'caption': line[7:], + 'cmd': line[7:], + 'args': [], + 'type':'folder' + }) + elif line.count(':')==2: + name, icon, cmd = line.split(':', 2) + app_list.append({ + 'name': 'FOLDER', + 'icon': icon.lower(), + 'caption': cmd, + 'cmd': cmd, + 'args': [], + 'type':'folder' + }) + elif line.count(':')==3: + name, icon, ip, cmd = line.split(':', 3) + app_list.append({ + 'name': 'FOLDER', + 'icon': icon.lower(), + 'ip': ip, + 'caption': cmd, + 'cmd': cmd, + 'args': [], + 'type':'folder' + }) + + #process EXECUTE items else: try: - name, cmd, args = line.split('|', 2) - app_list.append({ - 'name': name, - 'cmd': cmd, - 'args': [n.replace("\n", "") for n in shlex.split(args)], - }) + if line.count('|') == 2 : + name, cmd, args = line.split('|', 2) + app_list.append({ + 'name': name, + 'caption': name, + 'cmd': cmd, + 'type':'execute', + 'args': [n.replace("\n", "") for n in shlex.split(args)], + }) + elif line.count('|') == 3 : + name, icon, cmd, args = line.split('|', 3) + app_list.append({ + 'icon': icon.lower(), + 'name': name, + 'caption': name, + 'cmd': cmd, + 'type':'execute', + 'args': [n.replace("\n", "") for n in shlex.split(args)], + }) + else: print "The following line has an invalid amount of separators and will be ignored:\n%s" % line except ValueError: print "The following line has errors and will be ignored:\n%s" % line finally: @@ -219,49 +512,171 @@ def get_sshplusconfig(): return app_list def build_menu(): - if not os.path.exists(_SETTINGS_FILE) and not os.path.exists(_SSHMENU_FILE) : - show_help_dlg("ERROR: No .sshmenu or .sshplus file found in home directory\n\n%s" % \ - _ABOUT_TXT, error=True) - sys.exit(1) + global _RECENT_DATA + + app_list=[] + + #read the recent items + if 'file' in _RECENT: + if 'count' in _RECENT and int(_RECENT['count'])>0: + _RECENT_DATA=get_sshplusconfig(_RECENT['file']) + + #strip down to the needed amount + if 'name' in _RECENT: + name=_RECENT['name'] + else: + name="Recently Used:" + label={ + 'cmd' : name, + 'name' : 'LABEL', + 'type' : 'label', + 'caption' : name, + 'args': []} + + if 'count' in _RECENT and len(_RECENT_DATA)>0 and len(_RECENT_DATA)<=_RECENT['count']: + app_list=[ _SEPARATOR, label, _SEPARATOR]+ _RECENT_DATA[0:int(_RECENT['count'])]+ [_FOLDER_POP,_SEPARATOR] + else: + app_list=[ _SEPARATOR, label, _SEPARATOR]+ _RECENT_DATA + [_FOLDER_POP,_SEPARATOR] + + #read the user menu + u_list=get_sshplusconfig(_SETTINGS_FILE) + if u_list != []: + app_list = app_list + ulist; + + + #load the global menus + for menu in _MENUS : + + if 'file' in menu: + m_list=get_sshplusconfig(menu['file']) + else: + print "Menu item found without file key: %s\n"%(menu) + continue + + if m_list !=[]: + + #check if the menu is allowed for this user + if 'users' in menu: + users=menu['users'].split(',') + + if not _USER in users: + continue + + if 'name' in menu: + name=menu['name'] + else: + name=menu['file'] + + folder={ + 'cmd' : menu['name'], + 'name' : 'FOLDER', + 'type' : 'folder', + 'caption' : menu['name'], + 'args': []} + + if 'icon' in menu: + folder['icon']=menu['icon'] + + if 'ip' in menu: + folder['ip']=menu['ip'] + + if 'decoration' in menu and menu['decoration'].lower()=="false" : + app_list=app_list + m_list + else: + app_list=app_list + [ _SEPARATOR, _FOLDER_POP, folder, _SEPARATOR] + m_list + else: + print "File not found or empty: %s\n"%(menu['file']) - app_list = get_sshplusconfig() #Add sshmenu config items if any app_list2 = get_sshmenuconfig() if app_list2 != []: - app_list = app_list + ["sep",{'name': 'LABEL','cmd': "SSHmenu",'args': ''}] + app_list2 + app_list = app_list + [_SEPARATOR,{'name': 'LABEL','cmd': "SSHmenu",'args': ''}] + app_list2 menu = gtk.Menu() menus = [menu] + + if len(app_list) <=0: + show_help_dlg("ERROR: No menus found in $HOME/.sshmenu $HOME/.sshplus or /etc/sshplus/sshplus.cfg \n\n%s" % \ + _ABOUT_TXT, error=True) + sys.exit(1) + + + + #to make this work: add FOLDER|LABEL|COMMAND to the dict and let add_menu_item sort out what to do in each case + # or juist provide the sec arg with 'name' + for app in app_list: - if app == "sep": - add_separator(menus[-1]) - elif app['name'] == "FOLDER" and not app['cmd']: + if app['name'] == "FOLDER" and not app['cmd']: if len(menus) > 1: menus.pop() - elif app['name'] == "FOLDER": - menu_item = add_menu_item(menus[-1], app['cmd'], 'folder') - menus.append(gtk.Menu()) - menu_item.set_submenu(menus[-1]) - elif app['name'] == "LABEL": - add_menu_item(menus[-1], app['cmd'], None) - else: - add_menu_item(menus[-1], app['name'], app) - - add_separator(menu) - add_menu_item(menu, 'Refresh', '_refresh') - add_menu_item(menu, 'About', '_about') - add_separator(menu) - add_menu_item(menu, 'Quit', '_quit') + else: + menu_item = add_menu_item2(menus[-1], app) + if app['name'] == "FOLDER": + menus.append(gtk.Menu()) + menu_item.set_submenu(menus[-1]) + + + add_menu_item2(menus[-1], _SEPARATOR) + add_menu_item2(menu, {'name': '_refresh','caption': 'Refresh','cmd': '_refresh','type':'execute','args': []}) + add_menu_item2(menu, {'name': '_about','caption': 'About','cmd': '_about','type':'execute','args': []}) + add_menu_item2(menus[-1], _SEPARATOR) + add_menu_item2(menu, {'name': '_quit','caption': 'Quit','cmd': '_quit','type':'execute','args': []}) return menu +def write_desktopfile(desktopfile): + file = open(desktopfile, "w") + file.write("[Desktop Entry]\n") + file.write("Type=Application\n") + file.write("Name=sshplus\n") + file.write("Exec=%s\n"%(os.path.realpath(__file__))) + #file.write("Icon=\n") + file.write("Comment=SSHplus Menu indicator\n") + file.write("Hidden=false\n") + file.write("NoDisplay=false\n") + file.write("X-GNOME-Autostart-enabled=true\n") + file.close() + + if __name__ == "__main__": - ind = appindicator.Indicator("sshplu", "gnome-netstatus-tx", + + + #display help + if "--help" in sys.argv : + print( "\n\n"+ + " .sshplus.py without arguments just creates the menu\n"+ + " .sshplus.py --start-on-login creates the autostart for the user (only)\n"+ + " .sshplus.py --start-on-login-sys creates the autostart for the system, root privileges required\n"+ + "\n"+ + " FILES: /etc/sshplus/sshplus.cfg /$HOME/.sshplus\n") + sys.exit() + + #create startup files + if "--start-on-login" in sys.argv : + write_desktopfile(os.path.expandvars("$HOME/.config/autostart/sshplus.py.desktop")) + sys.exit() + if "--start-on-login-sys" in sys.argv : + write_desktopfile("/etc/xdg/autostart/sshplus.py.desktop") + sys.exit() + + #read the config file, it will set the SETTING_FILE to list + _MENUS=read_config(_ETC_CONFIG) + if not _MENUS: + _MENUS=read_config(_SYS_CONFIG) + + #enable the icons in menus: http://www.pygtk.org/pygtk2reference/class-gtksettings.html + window = gtk.Window(gtk.WINDOW_TOPLEVEL) + window.get_settings().set_long_property('gtk-menu-images', True, '') + + #create and display the indicator + ind = appindicator.Indicator("sshplu", _MENU_ICON, appindicator.CATEGORY_APPLICATION_STATUS) - ind.set_label("Launch") + + ind.set_label(_MENU_TITLE) ind.set_status(appindicator.STATUS_ACTIVE) appmenu = build_menu() ind.set_menu(appmenu) gtk.main() + diff --git a/sshplus/icons/avatar2.png b/sshplus/icons/avatar2.png new file mode 100755 index 0000000000000000000000000000000000000000..c3b3e97a1629138fdca061097e48af47089ee9f2 GIT binary patch literal 15435 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 literal 0 HcmV?d00001 diff --git a/sshplus/icons/icon-connected-eye.png b/sshplus/icons/icon-connected-eye.png new file mode 100644 index 0000000000000000000000000000000000000000..59b754e7a2aeb3cbfd3e06d879c7c6a4a31cd77a GIT binary patch literal 735 zcmV<50wDc~P)_Wc=u3Q51oe5ecp$rA^EJgH~k_vqD}YSckg-U+;i@^zk57EIc3;6gplVU#wbpiYW?z5o)uGa%R<#HO z6>43RD%L1ngApyQdN1Ck1tAm?u*jN7>&WOxX=z}@ztY%-QpR#^*ITruz+GtHh0GY+t)d)k#v9V9(($3uc=R&6lgqLe& zUza!5D$(<|TPD;NLU6)F0s|2^WO{lv5wORPo;-UvTCK|aHxK-(s8>@LK71)Ht<>&3 zts12{6T}bd^sE~`*S@(KULL(THvT4*_R5WK`@8&;=Wm_v@Aj#iIMZa60s5R^4B-JN z@Og0e@WSGDYg@OJNGY^Zj*j;oU0B|^b~XhttU-bc?imUh2~I7Lk^1G-5fw9yX)2%@ z$_7@jI%#RrcO*$E1G0Q!QHQ&ckw!^{q}3W(iE1Iz5*ta3a^)&YH~T^)pfKo8%}w6h zgcBi0Au_(;j1x}bl|0zNIZwA{dTGAg{D(74`;HX{PZYrqW6{-F0H5vOTZvRMlg