Mercurial > addons > weechat-scripts > weechat-notification-script
diff notification.py @ 0:dfe10c951e21
Initial revision
author | Guido Berhoerster <guido+weechat@berhoerster.name> |
---|---|
date | Tue, 10 Mar 2015 11:27:22 +0100 |
parents | |
children | ba1005429c76 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/notification.py Tue Mar 10 11:27:22 2015 +0100 @@ -0,0 +1,852 @@ +# +# Copyright (C) 2014 Guido Berhoerster <guido+weechat@berhoerster.name> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# + +import os +import sys +import time +import re +import select +import signal +import errno +import fcntl +import cgi +import multiprocessing + + +SCRIPT_NAME = 'notification' +APPLICATION = 'Weechat' +VERSION = '1' +AUTHOR = 'Guido Berhoerster' +COPYRIGHT = '(C) 2014 Guido Berhoerster' +SUBTITLE = 'Notification Plugin for Weechat' +HOMEPAGE = 'https://code.guido-berhoerster.org/addons/weechat-scripts/weechat-notification-script/' +EMAIL = 'guido+weechat@berhoerster.name' +DESCRIPTION = 'Notifies of a number of events through desktop notifications ' \ + 'and an optional status icon' +DEFAULT_SETTINGS = { + 'status_icon': ('weechat', 'path or name of the status icon'), + 'notification_icon': ('weechat', 'path or name of the icon shown in ' + 'notifications'), + 'preferred_toolkit': ('', 'preferred UI toolkit'), + 'notify_on_displayed_only': ('on', 'only notify of messages that are ' + 'actually displayed'), + 'notify_on_privmsg': ('on', 'notify when receiving a private message'), + 'notify_on_highlight': ('on', 'notify when a messages is highlighted'), + 'notify_on_dcc_request': ('on', 'notify on DCC requests') +} +BUFFER_SIZE = 1024 + + +class NetstringParser(object): + """Netstring Stream Parser""" + + IN_LENGTH = 0 + IN_STRING = 1 + + def __init__(self, on_string_complete): + self.on_string_complete = on_string_complete + self.length = 0 + self.input_buffer = '' + self.state = self.IN_LENGTH + + def parse(self, data): + self.input_buffer += data + ret = True + while ret: + if self.state == self.IN_LENGTH: + ret = self.parse_length() + else: + ret = self.parse_string() + + def parse_length(self): + length, delimiter, self.input_buffer = self.input_buffer.partition(':') + if not delimiter: + return False + try: + self.length = int(length) + except ValueError: + raise SyntaxError('Invalid length: %s' % length) + self.state = self.IN_STRING + return True + + def parse_string(self): + input_buffer_len = len(self.input_buffer) + if input_buffer_len < self.length + 1: + return False + string = self.input_buffer[0:self.length] + if self.input_buffer[self.length] != ',': + raise SyntaxError('Missing delimiter') + self.input_buffer = self.input_buffer[self.length + 1:] + self.length = 0 + self.state = self.IN_LENGTH + self.on_string_complete(string) + return True + + +def netstring_encode(*args): + return ''.join(['%d:%s,' % (len(element), element) for element in + args]) + +def netstring_decode(netstring): + result = [] + def append_result(data): + result.append(data) + np = NetstringParser(append_result) + np.parse(netstring) + return result + +def dispatch_weechat_callback(*args): + return weechat_callbacks[args[0]](*args) + +def create_weechat_callback(method): + global weechat_callbacks + + method_id = str(id(method)) + weechat_callbacks[method_id] = method + return method_id + + +class Notifier(object): + """Simple notifier which discards all notifications, base class for all + other notifiers + """ + + def __init__(self, icon): + flags = fcntl.fcntl(sys.stdin, fcntl.F_GETFL) + fcntl.fcntl(sys.stdin, fcntl.F_SETFL, flags | os.O_NONBLOCK) + + self.parser = NetstringParser(self.on_command_received) + + def on_command_received(self, raw_command): + command_args = netstring_decode(raw_command) + if len(command_args) > 1: + command = command_args[0] + args = netstring_decode(command_args[1]) + else: + command = command_args[0] + args = [] + getattr(self, command)(*args) + + def notify(self, summary, message, icon): + pass + + def reset(self): + pass + + def run(self): + poll = select.poll() + poll.register(sys.stdin, select.POLLIN | select.POLLPRI) + + while True: + try: + events = poll.poll() + except select.error as e: + if e.args and e.args[0] == errno.EINTR: + continue + else: + raise e + for fd, event in events: + if event & (select.POLLIN | select.POLLPRI): + buffer_ = os.read(fd, BUFFER_SIZE) + if buffer_ != '': + self.parser.parse(buffer_) + if event & (select.POLLERR | select.POLLHUP | select.POLLNVAL): + sys.exit(1) + + +class Gtk2Notifier(Notifier): + """GTK 2 notifier based on pygtk and pynotify""" + + def __init__(self, icon): + super(Gtk2Notifier, self).__init__(icon) + + pynotify.init(APPLICATION) + + gobject.io_add_watch(sys.stdin, gobject.IO_IN | gobject.IO_PRI, + self.on_input) + + if not icon: + icon_name = None + icon_pixbuf = None + elif icon.startswith('/'): + icon_name = None + try: + icon_pixbuf = gtk.gdk.Pixbuf.new_from_file(icon) + except gobject.GError: + icon_pixbuf = None + else: + icon_name = icon + icon_pixbuf = None + + if icon_name or icon_pixbuf: + self.status_icon = gtk.StatusIcon() + self.status_icon.set_title(APPLICATION) + self.status_icon.set_tooltip_text(APPLICATION) + self.status_icon.connect('activate', self.on_activate) + if icon_name: + self.status_icon.set_from_icon_name(icon_name) + elif icon_pixbuf: + self.status_icon.set_from_pixbuf(icon_pixbuf) + else: + self.status_icon = None + + def on_input(self, fd, cond): + if cond & (gobject.IO_IN | gobject.IO_PRI): + try: + buffer_ = os.read(fd.fileno(), BUFFER_SIZE) + if buffer_ != '': + self.parser.parse(buffer_) + except EOFError: + gtk.main_quit() + return False + + if cond & (gobject.IO_ERR | gobject.IO_HUP): + gtk.main_quit() + return False + + return True + + def on_activate(self, widget): + self.reset() + + def notify(self, summary, message, icon): + if self.status_icon: + self.status_icon.set_tooltip_text('%s: %s' % (APPLICATION, + summary)) + self.status_icon.set_blinking(True) + + if icon and icon.startswith('/'): + icon_name = None + try: + icon_pixbuf = gtk.gdk.Pixbuf.new_from_file(icon) + except gobject.GError: + icon_pixbuf = None + else: + icon_name = icon + icon_pixbuf = None + + if 'body-markup' in pynotify.get_server_caps(): + body = cgi.escape(message) + else: + body = message + + notification = pynotify.Notification(summary, body, icon_name) + if icon_pixbuf is not None: + notification.set_image_from_pixbuf(icon_pixbuf) + notification.show() + + def reset(self): + if self.status_icon: + self.status_icon.set_tooltip_text(APPLICATION) + self.status_icon.set_blinking(False) + + def run(self): + gtk.main() + + +class Gtk3Notifier(Notifier): + """GTK3 notifier based on GObject Introspection Bindings for GTK 3 and + libnotify + """ + + def __init__(self, icon): + super(Gtk3Notifier, self).__init__(icon) + + Notify.init(APPLICATION) + + GLib.io_add_watch(sys.stdin, GLib.IO_IN | GLib.IO_PRI, self.on_input) + + if not icon: + self.icon_name = None + self.icon_pixbuf = None + elif icon.startswith('/'): + self.icon_name = None + try: + self.icon_pixbuf = GdkPixbuf.Pixbuf.new_from_file(icon) + except GLib.GError: + self.icon_pixbuf = None + else: + self.icon_name = icon + self.icon_pixbuf = None + + if self.icon_name or self.icon_pixbuf: + # create blank, fully transparent pixbuf in order to simulate + # blinking + self.blank_pixbuf = GdkPixbuf.Pixbuf.new(GdkPixbuf.Colorspace.RGB, + True, 8, 22, 22) + self.blank_pixbuf.fill(0x00) + + self.blink_on = True + self.blink_timeout_id = None + + self.status_icon = Gtk.StatusIcon.new() + self.status_icon.set_title(APPLICATION) + self.status_icon.set_tooltip_text(APPLICATION) + self.status_icon.connect('activate', self.on_activate) + self.update_icon() + else: + self.status_icon = None + + def on_input(self, fd, cond): + if cond & (GLib.IO_IN | GLib.IO_PRI): + try: + self.parser.parse(os.read(fd.fileno(), BUFFER_SIZE)) + except EOFError: + Gtk.main_quit() + return False + + if cond & (GLib.IO_ERR | GLib.IO_HUP): + Gtk.main_quit() + return False + + return True + + def on_activate(self, widget): + self.reset() + + def update_icon(self): + if not self.blink_on: + self.status_icon.set_from_pixbuf(self.blank_pixbuf) + elif self.icon_name: + self.status_icon.set_from_icon_name(self.icon_name) + elif self.icon_pixbuf: + self.status_icon.set_from_pixbuf(self.icon_pixbuf) + + def on_blink_timeout(self): + self.blink_on = not self.blink_on + self.update_icon() + return True + + def notify(self, summary, message, icon): + if self.status_icon: + self.status_icon.set_tooltip_text('%s: %s' % (APPLICATION, + summary)) + if self.blink_timeout_id is None: + self.blink_timeout_id = GLib.timeout_add(500, + self.on_blink_timeout) + + if icon and icon.startswith('/'): + icon_name = None + try: + icon_pixbuf = GdkPixbuf.Pixbuf.new_from_file(icon) + except GLib.GError: + icon_pixbuf = None + else: + icon_name = icon + icon_pixbuf = None + + if 'body-markup' in Notify.get_server_caps(): + body = cgi.escape(message) + else: + body = message + + notification = Notify.Notification.new(summary, body, icon_name) + if icon_pixbuf is not None: + notification.set_image_from_pixbuf(icon_pixbuf) + notification.show() + + def reset(self): + if self.status_icon: + self.status_icon.set_tooltip_text(APPLICATION) + if self.blink_timeout_id is not None: + GLib.source_remove(self.blink_timeout_id) + self.blink_timeout_id = None + self.blink_on = True + self.update_icon() + + def run(self): + Gtk.main() + + +class Qt4Notifier(Notifier): + """Qt 4 notifier""" + + def __init__(self, icon): + super(Qt4Notifier, self).__init__(icon) + + signal.signal(signal.SIGINT, self.on_sigint) + + self.qapplication = QtGui.QApplication([]) + + self.readable_notifier = QtCore.QSocketNotifier(sys.stdin.fileno(), + QtCore.QSocketNotifier.Read) + self.readable_notifier.activated.connect(self.on_input) + self.readable_notifier.setEnabled(True) + + if not icon: + self.icon = None + elif icon.startswith('/'): + self.icon = QtGui.QIcon(icon) + else: + self.icon = QtGui.QIcon.fromTheme(icon) + + if self.icon: + # create blank, fully transparent pixbuf in order to simulate + # blinking + self.blank_icon = QtGui.QIcon() + + self.blink_on = True + self.blinking_timer = QtCore.QTimer() + self.blinking_timer.setInterval(500) + self.blinking_timer.timeout.connect(self.on_blink_timeout) + + self.status_icon = QtGui.QSystemTrayIcon() + self.status_icon.setToolTip(APPLICATION) + self.update_icon() + self.status_icon.setVisible(True) + self.status_icon.activated.connect(self.on_activated) + else: + self.status_icon = None + + def on_sigint(self, signo, frame): + self.qapplication.exit(0) + + def on_input(self, fd): + try: + self.parser.parse(os.read(fd, BUFFER_SIZE)) + except EOFError: + self.qapplication.exit(1) + + def on_activated(self, reason): + self.reset() + + def on_blink_timeout(self): + self.blink_on = not self.blink_on + self.update_icon() + + def update_icon(self): + if not self.blink_on: + self.status_icon.setIcon(self.blank_icon) + else: + self.status_icon.setIcon(self.icon) + + def notify(self, summary, message, icon): + if self.status_icon: + self.status_icon.setToolTip('%s: %s' % (APPLICATION, + cgi.escape(summary))) + self.blinking_timer.start() + if self.status_icon.supportsMessages(): + self.status_icon.showMessage(summary, message, + QtGui.QSystemTrayIcon.NoIcon) + + def reset(self): + if self.status_icon: + self.blinking_timer.stop() + self.blink_on = True + self.update_icon() + self.status_icon.setToolTip(APPLICATION) + + def run(self): + sys.exit(self.qapplication.exec_()) + + +class KDE4Notifier(Notifier): + """KDE 4 notifier based on PyKDE4""" + + def __init__(self, icon): + super(KDE4Notifier, self).__init__(icon) + + signal.signal(signal.SIGINT, self.on_sigint) + + aboutData = kdecore.KAboutData(APPLICATION.lower(), '', + kdecore.ki18n(APPLICATION), VERSION, kdecore.ki18n(SUBTITLE), + kdecore.KAboutData.License_GPL_V3, kdecore.ki18n(COPYRIGHT), + kdecore.ki18n (''), HOMEPAGE, EMAIL) + kdecore.KCmdLineArgs.init(aboutData) + self.kapplication = kdeui.KApplication() + + self.readable_notifier = QtCore.QSocketNotifier(sys.stdin.fileno(), + QtCore.QSocketNotifier.Read) + self.readable_notifier.activated.connect(self.on_input) + self.readable_notifier.setEnabled(True) + + if not icon: + icon_qicon = None + icon_name = None + elif icon.startswith('/'): + icon_qicon = QtGui.QIcon(icon) + icon_name = None + else: + icon_qicon = None + icon_name = icon + + if icon_name or icon_pixmap: + self.status_notifier = kdeui.KStatusNotifierItem(self.kapplication) + self.status_notifier.setCategory( + kdeui.KStatusNotifierItem.Communications) + if icon_name: + self.status_notifier.setIconByName(icon_name) + self.status_notifier.setToolTip(icon_name, APPLICATION, + SUBTITLE) + else: + self.status_notifier.setIconByPixmap(icon_qicon) + self.status_notifier.setToolTip(icon_qicon, APPLICATION, + SUBTITLE) + self.status_notifier.setStandardActionsEnabled(False) + self.status_notifier.setStatus(kdeui.KStatusNotifierItem.Active) + self.status_notifier.setTitle(APPLICATION) + self.status_notifier.activateRequested.connect( + self.on_activate_requested) + else: + self.status_notifier = None + + def on_sigint(self, signo, frame): + self.kapplication.exit(0) + + def on_input(self, fd): + try: + self.parser.parse(os.read(fd, BUFFER_SIZE)) + except EOFError: + self.kapplication.exit(1) + + def on_activate_requested(self, active, pos): + self.reset() + + def notify(self, summary, message, icon): + if self.status_notifier: + self.status_notifier.setToolTipSubTitle(cgi.escape(summary)) + self.status_notifier.setStatus( + kdeui.KStatusNotifierItem.NeedsAttention) + + if icon: + if icon.startswith('/'): + pixmap = QtGui.QPixmap.load(icon) + else: + pixmap = kdeui.KIcon(icon).pixmap(kdeui.KIconLoader.SizeHuge, + kdeui.KIconLoader.SizeHuge) + else: + pixmap = QtGui.QPixmap() + kdeui.KNotification.event(kdeui.KNotification.Notification, summary, + cgi.escape(message), pixmap) + + def reset(self): + if self.status_notifier: + self.status_notifier.setStatus(kdeui.KStatusNotifierItem.Active) + self.status_notifier.setToolTipTitle(APPLICATION) + self.status_notifier.setToolTipSubTitle(SUBTITLE) + + def run(self): + sys.exit(self.kapplication.exec_()) + + +class NotificationProxy(object): + """Proxy object for interfacing with the notifier process""" + + def __init__(self, preferred_toolkit, status_icon): + self.script_file = os.path.realpath(__file__) + self._status_icon = status_icon + self._preferred_toolkit = preferred_toolkit + self.notifier_process_hook = None + self.spawn_timer_hook = None + self.next_spawn_time = 0.0 + + self.spawn_notifier_process() + + @property + def status_icon(self): + return self._status_icon + + @status_icon.setter + def status_icon(self, value): + self._status_icon = value + self.terminate_notifier_process() + self.spawn_notifier_process() + + @property + def preferred_toolkit(self): + return self._preferred_toolkit + + @preferred_toolkit.setter + def preferred_toolkit(self, value): + self._preferred_toolkit = value + self.terminate_notifier_process() + self.spawn_notifier_process() + + def on_notifier_process_event(self, data, command, return_code, output, + error_output): + if return_code != weechat.WEECHAT_HOOK_PROCESS_RUNNING: + if return_code == weechat.WEECHAT_HOOK_PROCESS_ERROR: + error = '%sfailed to run notifier' % weechat.prefix("error") + else: + error = '%snotifier exited with exit status %d' % \ + (weechat.prefix("error"), return_code) + if output: + error += '\nstdout:%s' % output + if error_output: + error += '\nstderr:%s' % error_output + weechat.prnt('', error) + self.notifier_process_hook = None + self.spawn_notifier_process() + return weechat.WEECHAT_RC_OK + + def on_spawn_timer(self, data, remaining): + self.spawn_timer_hook = None + if not self.notifier_process_hook: + self.spawn_notifier_process() + return weechat.WEECHAT_RC_OK + + def spawn_notifier_process(self): + if self.notifier_process_hook or self.spawn_timer_hook: + return + + # do not try to respawn a notifier more than once every ten seconds + now = time.time() + if long(self.next_spawn_time - now) > 0: + self.spawn_timer_hook = \ + weechat.hook_timer(long((self.next_spawn_time - now) * + 1000), 0, 1, 'dispatch_weechat_callback', + create_weechat_callback(self.on_spawn_timer)) + return + + self.next_spawn_time = now + 10 + self.notifier_process_hook = \ + weechat.hook_process_hashtable(sys.executable, {'arg1': + self.script_file, 'arg2': self.preferred_toolkit, 'arg3': + self.status_icon, 'stdin': '1'}, 0, + 'dispatch_weechat_callback', + create_weechat_callback(self.on_notifier_process_event)) + + def terminate_notifier_process(self): + if self.spawn_timer_hook: + weechat.unhook(self.spawn_timer_hook) + self.spawn_timer_hook = None + if self.notifier_process_hook: + weechat.unhook(self.notifier_process_hook) + self.notifier_process_hook = None + self.next_spawn_time = 0.0 + + def send(self, command, *args): + if self.notifier_process_hook: + if args: + weechat.hook_set(self.notifier_process_hook, 'stdin', + netstring_encode(netstring_encode(command, + netstring_encode(*args)))) + else: + weechat.hook_set(self.notifier_process_hook, 'stdin', + netstring_encode(netstring_encode(command))) + + def notify(self, summary, message, icon): + self.send('notify', summary, message, icon) + + def reset(self): + self.send('reset') + + +class NotificationPlugin(object): + """Weechat plugin""" + + def __init__(self): + self.DCC_SEND_RE = re.compile(r':(?P<sender>\S+) PRIVMSG \S+ :' + r'\x01DCC SEND (?P<filename>\S+) \d+ \d+ (?P<size>\d+)') + self.DCC_CHAT_RE = re.compile(r':(?P<sender>\S+) PRIVMSG \S+ :' + r'\x01DCC CHAT ') + + weechat.register(SCRIPT_NAME, AUTHOR, VERSION, 'GPL3', DESCRIPTION, '', + '') + + for option, (value, description) in DEFAULT_SETTINGS.iteritems(): + if not weechat.config_is_set_plugin(option): + weechat.config_set_plugin(option, value) + weechat.config_set_desc_plugin(option, '%s (default: "%s")' % + (description, value)) + + self.notification_proxy = NotificationProxy( + weechat.config_get_plugin('preferred_toolkit'), + weechat.config_get_plugin('status_icon')) + + weechat.hook_print('', 'irc_privmsg', '', 1, + 'dispatch_weechat_callback', + create_weechat_callback(self.on_message)) + weechat.hook_signal('key_pressed', 'dispatch_weechat_callback', + create_weechat_callback(self.on_key_pressed)) + weechat.hook_signal('irc_dcc', 'dispatch_weechat_callback', + create_weechat_callback(self.on_dcc)) + weechat.hook_config('plugins.var.python.%s.*' % SCRIPT_NAME, + 'dispatch_weechat_callback', + create_weechat_callback(self.on_config_changed)) + + def on_message(self, data, buffer, date, tags, displayed, highlight, + prefix, message): + if weechat.config_get_plugin('notify_on_displayed_only') == 'on' and \ + int(displayed) != 1: + return weechat.WEECHAT_RC_OK + + formatted_date = time.strftime('%H:%M', time.localtime(float(date))) + if 'notify_private' in tags.split(',') and \ + weechat.config_get_plugin('notify_on_privmsg') == 'on': + summary = 'Private message from %s at %s' % (prefix, + formatted_date) + self.notification_proxy.notify(summary, message, + weechat.config_get_plugin('notification_icon')) + elif int(highlight) == 1 and \ + weechat.config_get_plugin('notify_on_highlight') == 'on': + summary = 'Highlighted message from %s at %s' % (prefix, + formatted_date) + self.notification_proxy.notify(summary, message, + weechat.config_get_plugin('notification_icon')) + + return weechat.WEECHAT_RC_OK + + def on_dcc(self, data, signal, signal_data): + if weechat.config_get_plugin('notify_on_dcc') != 'on': + return weechat.WEECHAT_RC_OK + + matches = self.DCC_SEND_RE.match(signal_data) + if matches: + summary = 'DCC send request from %s' % matches.group('sender') + message = 'Filname: %s, Size: %d bytes' % \ + (matches.group('filename'), int(matches.group('size'))) + self.notification_proxy.notify(summary, message, + weechat.config_get_plugin('notification_icon')) + return weechat.WEECHAT_RC_OK + + matches = self.DCC_CHAT_RE.match(signal_data) + if matches: + summary = 'DCC chat request from %s' % matches.group('sender') + message = '' + self.notification_proxy.notify(summary, message, + weechat.config_get_plugin('notification_icon')) + return weechat.WEECHAT_RC_OK + + return weechat.WEECHAT_RC_OK + + def on_key_pressed(self, data, signal, signal_data): + self.notification_proxy.reset() + return weechat.WEECHAT_RC_OK + + def on_config_changed(self, data, option, value): + if option.endswith('.preferred_toolkit'): + self.notification_proxy.preferred_toolkit = value + elif option.endswith('.status_icon'): + self.notification_proxy.status_icon = value + return weechat.WEECHAT_RC_OK + + +def import_modules(modules): + for module_name, fromlist in modules: + if fromlist: + module = __import__(module_name, fromlist=fromlist) + for identifier in fromlist: + globals()[identifier] = getattr(module, identifier) + else: + globals()[module_name] = __import__(module_name) + +def try_import_modules(modules): + try: + import_modules(modules) + except ImportError: + sys.exit(1) + sys.exit(0) + + +if __name__ == '__main__': + if sys.argv[0] == '__weechat_plugin__': + # running as Weechat plugin + import weechat + + weechat_callbacks = {} + + plugin = NotificationPlugin() + elif len(sys.argv) == 3: + # running as the notifier process + preferred_toolkit = sys.argv[1] + icon = sys.argv[2] + + # required modules for each toolkit + toolkits_modules = { + 'gtk3': [ + ('gi.repository', [ + 'GLib', + 'GdkPixbuf', + 'Gtk', + 'Notify' + ]) + ], + 'gtk2': [ + ('pygtk', []), + ('gobject', []), + ('gtk', []), + ('pynotify', []) + ], + 'qt4': [ + ('PyQt4', [ + 'QtGui', + 'QtCore' + ]) + ], + 'kde4': [ + ('PyQt4', [ + 'QtGui', + 'QtCore' + ]), + ('PyKDE4', [ + 'kdecore', + 'kdeui' + ]) + ], + '': [] + } + available_toolkits = [] + selected_toolkit = '' + + # find available toolkits by spawning a process for each toolkit which + # tries to import all required modules and returns an exit status of 1 + # in case of an import error + for toolkit in toolkits_modules: + process = multiprocessing.Process(target=try_import_modules, + args=(toolkits_modules[toolkit],)) + process.start() + process.join(3) + if process.is_alive(): + process.terminate() + process.join() + if process.exitcode == 0: + available_toolkits.append(toolkit) + + # select toolkit based on either explicit preference or the + # availability of modules and the used desktop environment + if preferred_toolkit: + if preferred_toolkit in available_toolkits: + selected_toolkit = preferred_toolkit + else: + if 'KDE_FULL_SESSION' in os.environ: + # preferred order if running KDE4 + toolkits = ['kde4', 'qt4', 'gtk3', 'gtk2'] + else: + # preferred order for all other desktop environments + toolkits = ['gtk3', 'gtk2', 'qt4', 'kde4'] + for toolkit in toolkits: + if toolkit in available_toolkits: + selected_toolkit = toolkit + break + + # import required toolkit modules + import_modules(toolkits_modules[selected_toolkit]) + + # run selected notifier + if selected_toolkit == 'gtk3': + notifier = Gtk3Notifier(icon) + elif selected_toolkit == 'gtk2': + notifier = Gtk2Notifier(icon) + elif selected_toolkit == 'qt4': + notifier = Qt4Notifier(icon) + elif selected_toolkit == 'kde4': + notifier = KDE4Notifier(icon) + else: + notifier = Notifier(icon) + notifier.run() + else: + sys.exit(1)