Mercurial > addons > weechat-scripts > weechat-notification-script
comparison 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 |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 0:dfe10c951e21 |
---|---|
1 # | |
2 # Copyright (C) 2014 Guido Berhoerster <guido+weechat@berhoerster.name> | |
3 # | |
4 # This program is free software: you can redistribute it and/or modify | |
5 # it under the terms of the GNU General Public License version 3 as | |
6 # published by the Free Software Foundation. | |
7 # | |
8 # This program is distributed in the hope that it will be useful, | |
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
11 # GNU General Public License for more details. | |
12 # | |
13 # You should have received a copy of the GNU General Public License | |
14 # along with this program. If not, see <http://www.gnu.org/licenses/>. | |
15 # | |
16 | |
17 import os | |
18 import sys | |
19 import time | |
20 import re | |
21 import select | |
22 import signal | |
23 import errno | |
24 import fcntl | |
25 import cgi | |
26 import multiprocessing | |
27 | |
28 | |
29 SCRIPT_NAME = 'notification' | |
30 APPLICATION = 'Weechat' | |
31 VERSION = '1' | |
32 AUTHOR = 'Guido Berhoerster' | |
33 COPYRIGHT = '(C) 2014 Guido Berhoerster' | |
34 SUBTITLE = 'Notification Plugin for Weechat' | |
35 HOMEPAGE = 'https://code.guido-berhoerster.org/addons/weechat-scripts/weechat-notification-script/' | |
36 EMAIL = 'guido+weechat@berhoerster.name' | |
37 DESCRIPTION = 'Notifies of a number of events through desktop notifications ' \ | |
38 'and an optional status icon' | |
39 DEFAULT_SETTINGS = { | |
40 'status_icon': ('weechat', 'path or name of the status icon'), | |
41 'notification_icon': ('weechat', 'path or name of the icon shown in ' | |
42 'notifications'), | |
43 'preferred_toolkit': ('', 'preferred UI toolkit'), | |
44 'notify_on_displayed_only': ('on', 'only notify of messages that are ' | |
45 'actually displayed'), | |
46 'notify_on_privmsg': ('on', 'notify when receiving a private message'), | |
47 'notify_on_highlight': ('on', 'notify when a messages is highlighted'), | |
48 'notify_on_dcc_request': ('on', 'notify on DCC requests') | |
49 } | |
50 BUFFER_SIZE = 1024 | |
51 | |
52 | |
53 class NetstringParser(object): | |
54 """Netstring Stream Parser""" | |
55 | |
56 IN_LENGTH = 0 | |
57 IN_STRING = 1 | |
58 | |
59 def __init__(self, on_string_complete): | |
60 self.on_string_complete = on_string_complete | |
61 self.length = 0 | |
62 self.input_buffer = '' | |
63 self.state = self.IN_LENGTH | |
64 | |
65 def parse(self, data): | |
66 self.input_buffer += data | |
67 ret = True | |
68 while ret: | |
69 if self.state == self.IN_LENGTH: | |
70 ret = self.parse_length() | |
71 else: | |
72 ret = self.parse_string() | |
73 | |
74 def parse_length(self): | |
75 length, delimiter, self.input_buffer = self.input_buffer.partition(':') | |
76 if not delimiter: | |
77 return False | |
78 try: | |
79 self.length = int(length) | |
80 except ValueError: | |
81 raise SyntaxError('Invalid length: %s' % length) | |
82 self.state = self.IN_STRING | |
83 return True | |
84 | |
85 def parse_string(self): | |
86 input_buffer_len = len(self.input_buffer) | |
87 if input_buffer_len < self.length + 1: | |
88 return False | |
89 string = self.input_buffer[0:self.length] | |
90 if self.input_buffer[self.length] != ',': | |
91 raise SyntaxError('Missing delimiter') | |
92 self.input_buffer = self.input_buffer[self.length + 1:] | |
93 self.length = 0 | |
94 self.state = self.IN_LENGTH | |
95 self.on_string_complete(string) | |
96 return True | |
97 | |
98 | |
99 def netstring_encode(*args): | |
100 return ''.join(['%d:%s,' % (len(element), element) for element in | |
101 args]) | |
102 | |
103 def netstring_decode(netstring): | |
104 result = [] | |
105 def append_result(data): | |
106 result.append(data) | |
107 np = NetstringParser(append_result) | |
108 np.parse(netstring) | |
109 return result | |
110 | |
111 def dispatch_weechat_callback(*args): | |
112 return weechat_callbacks[args[0]](*args) | |
113 | |
114 def create_weechat_callback(method): | |
115 global weechat_callbacks | |
116 | |
117 method_id = str(id(method)) | |
118 weechat_callbacks[method_id] = method | |
119 return method_id | |
120 | |
121 | |
122 class Notifier(object): | |
123 """Simple notifier which discards all notifications, base class for all | |
124 other notifiers | |
125 """ | |
126 | |
127 def __init__(self, icon): | |
128 flags = fcntl.fcntl(sys.stdin, fcntl.F_GETFL) | |
129 fcntl.fcntl(sys.stdin, fcntl.F_SETFL, flags | os.O_NONBLOCK) | |
130 | |
131 self.parser = NetstringParser(self.on_command_received) | |
132 | |
133 def on_command_received(self, raw_command): | |
134 command_args = netstring_decode(raw_command) | |
135 if len(command_args) > 1: | |
136 command = command_args[0] | |
137 args = netstring_decode(command_args[1]) | |
138 else: | |
139 command = command_args[0] | |
140 args = [] | |
141 getattr(self, command)(*args) | |
142 | |
143 def notify(self, summary, message, icon): | |
144 pass | |
145 | |
146 def reset(self): | |
147 pass | |
148 | |
149 def run(self): | |
150 poll = select.poll() | |
151 poll.register(sys.stdin, select.POLLIN | select.POLLPRI) | |
152 | |
153 while True: | |
154 try: | |
155 events = poll.poll() | |
156 except select.error as e: | |
157 if e.args and e.args[0] == errno.EINTR: | |
158 continue | |
159 else: | |
160 raise e | |
161 for fd, event in events: | |
162 if event & (select.POLLIN | select.POLLPRI): | |
163 buffer_ = os.read(fd, BUFFER_SIZE) | |
164 if buffer_ != '': | |
165 self.parser.parse(buffer_) | |
166 if event & (select.POLLERR | select.POLLHUP | select.POLLNVAL): | |
167 sys.exit(1) | |
168 | |
169 | |
170 class Gtk2Notifier(Notifier): | |
171 """GTK 2 notifier based on pygtk and pynotify""" | |
172 | |
173 def __init__(self, icon): | |
174 super(Gtk2Notifier, self).__init__(icon) | |
175 | |
176 pynotify.init(APPLICATION) | |
177 | |
178 gobject.io_add_watch(sys.stdin, gobject.IO_IN | gobject.IO_PRI, | |
179 self.on_input) | |
180 | |
181 if not icon: | |
182 icon_name = None | |
183 icon_pixbuf = None | |
184 elif icon.startswith('/'): | |
185 icon_name = None | |
186 try: | |
187 icon_pixbuf = gtk.gdk.Pixbuf.new_from_file(icon) | |
188 except gobject.GError: | |
189 icon_pixbuf = None | |
190 else: | |
191 icon_name = icon | |
192 icon_pixbuf = None | |
193 | |
194 if icon_name or icon_pixbuf: | |
195 self.status_icon = gtk.StatusIcon() | |
196 self.status_icon.set_title(APPLICATION) | |
197 self.status_icon.set_tooltip_text(APPLICATION) | |
198 self.status_icon.connect('activate', self.on_activate) | |
199 if icon_name: | |
200 self.status_icon.set_from_icon_name(icon_name) | |
201 elif icon_pixbuf: | |
202 self.status_icon.set_from_pixbuf(icon_pixbuf) | |
203 else: | |
204 self.status_icon = None | |
205 | |
206 def on_input(self, fd, cond): | |
207 if cond & (gobject.IO_IN | gobject.IO_PRI): | |
208 try: | |
209 buffer_ = os.read(fd.fileno(), BUFFER_SIZE) | |
210 if buffer_ != '': | |
211 self.parser.parse(buffer_) | |
212 except EOFError: | |
213 gtk.main_quit() | |
214 return False | |
215 | |
216 if cond & (gobject.IO_ERR | gobject.IO_HUP): | |
217 gtk.main_quit() | |
218 return False | |
219 | |
220 return True | |
221 | |
222 def on_activate(self, widget): | |
223 self.reset() | |
224 | |
225 def notify(self, summary, message, icon): | |
226 if self.status_icon: | |
227 self.status_icon.set_tooltip_text('%s: %s' % (APPLICATION, | |
228 summary)) | |
229 self.status_icon.set_blinking(True) | |
230 | |
231 if icon and icon.startswith('/'): | |
232 icon_name = None | |
233 try: | |
234 icon_pixbuf = gtk.gdk.Pixbuf.new_from_file(icon) | |
235 except gobject.GError: | |
236 icon_pixbuf = None | |
237 else: | |
238 icon_name = icon | |
239 icon_pixbuf = None | |
240 | |
241 if 'body-markup' in pynotify.get_server_caps(): | |
242 body = cgi.escape(message) | |
243 else: | |
244 body = message | |
245 | |
246 notification = pynotify.Notification(summary, body, icon_name) | |
247 if icon_pixbuf is not None: | |
248 notification.set_image_from_pixbuf(icon_pixbuf) | |
249 notification.show() | |
250 | |
251 def reset(self): | |
252 if self.status_icon: | |
253 self.status_icon.set_tooltip_text(APPLICATION) | |
254 self.status_icon.set_blinking(False) | |
255 | |
256 def run(self): | |
257 gtk.main() | |
258 | |
259 | |
260 class Gtk3Notifier(Notifier): | |
261 """GTK3 notifier based on GObject Introspection Bindings for GTK 3 and | |
262 libnotify | |
263 """ | |
264 | |
265 def __init__(self, icon): | |
266 super(Gtk3Notifier, self).__init__(icon) | |
267 | |
268 Notify.init(APPLICATION) | |
269 | |
270 GLib.io_add_watch(sys.stdin, GLib.IO_IN | GLib.IO_PRI, self.on_input) | |
271 | |
272 if not icon: | |
273 self.icon_name = None | |
274 self.icon_pixbuf = None | |
275 elif icon.startswith('/'): | |
276 self.icon_name = None | |
277 try: | |
278 self.icon_pixbuf = GdkPixbuf.Pixbuf.new_from_file(icon) | |
279 except GLib.GError: | |
280 self.icon_pixbuf = None | |
281 else: | |
282 self.icon_name = icon | |
283 self.icon_pixbuf = None | |
284 | |
285 if self.icon_name or self.icon_pixbuf: | |
286 # create blank, fully transparent pixbuf in order to simulate | |
287 # blinking | |
288 self.blank_pixbuf = GdkPixbuf.Pixbuf.new(GdkPixbuf.Colorspace.RGB, | |
289 True, 8, 22, 22) | |
290 self.blank_pixbuf.fill(0x00) | |
291 | |
292 self.blink_on = True | |
293 self.blink_timeout_id = None | |
294 | |
295 self.status_icon = Gtk.StatusIcon.new() | |
296 self.status_icon.set_title(APPLICATION) | |
297 self.status_icon.set_tooltip_text(APPLICATION) | |
298 self.status_icon.connect('activate', self.on_activate) | |
299 self.update_icon() | |
300 else: | |
301 self.status_icon = None | |
302 | |
303 def on_input(self, fd, cond): | |
304 if cond & (GLib.IO_IN | GLib.IO_PRI): | |
305 try: | |
306 self.parser.parse(os.read(fd.fileno(), BUFFER_SIZE)) | |
307 except EOFError: | |
308 Gtk.main_quit() | |
309 return False | |
310 | |
311 if cond & (GLib.IO_ERR | GLib.IO_HUP): | |
312 Gtk.main_quit() | |
313 return False | |
314 | |
315 return True | |
316 | |
317 def on_activate(self, widget): | |
318 self.reset() | |
319 | |
320 def update_icon(self): | |
321 if not self.blink_on: | |
322 self.status_icon.set_from_pixbuf(self.blank_pixbuf) | |
323 elif self.icon_name: | |
324 self.status_icon.set_from_icon_name(self.icon_name) | |
325 elif self.icon_pixbuf: | |
326 self.status_icon.set_from_pixbuf(self.icon_pixbuf) | |
327 | |
328 def on_blink_timeout(self): | |
329 self.blink_on = not self.blink_on | |
330 self.update_icon() | |
331 return True | |
332 | |
333 def notify(self, summary, message, icon): | |
334 if self.status_icon: | |
335 self.status_icon.set_tooltip_text('%s: %s' % (APPLICATION, | |
336 summary)) | |
337 if self.blink_timeout_id is None: | |
338 self.blink_timeout_id = GLib.timeout_add(500, | |
339 self.on_blink_timeout) | |
340 | |
341 if icon and icon.startswith('/'): | |
342 icon_name = None | |
343 try: | |
344 icon_pixbuf = GdkPixbuf.Pixbuf.new_from_file(icon) | |
345 except GLib.GError: | |
346 icon_pixbuf = None | |
347 else: | |
348 icon_name = icon | |
349 icon_pixbuf = None | |
350 | |
351 if 'body-markup' in Notify.get_server_caps(): | |
352 body = cgi.escape(message) | |
353 else: | |
354 body = message | |
355 | |
356 notification = Notify.Notification.new(summary, body, icon_name) | |
357 if icon_pixbuf is not None: | |
358 notification.set_image_from_pixbuf(icon_pixbuf) | |
359 notification.show() | |
360 | |
361 def reset(self): | |
362 if self.status_icon: | |
363 self.status_icon.set_tooltip_text(APPLICATION) | |
364 if self.blink_timeout_id is not None: | |
365 GLib.source_remove(self.blink_timeout_id) | |
366 self.blink_timeout_id = None | |
367 self.blink_on = True | |
368 self.update_icon() | |
369 | |
370 def run(self): | |
371 Gtk.main() | |
372 | |
373 | |
374 class Qt4Notifier(Notifier): | |
375 """Qt 4 notifier""" | |
376 | |
377 def __init__(self, icon): | |
378 super(Qt4Notifier, self).__init__(icon) | |
379 | |
380 signal.signal(signal.SIGINT, self.on_sigint) | |
381 | |
382 self.qapplication = QtGui.QApplication([]) | |
383 | |
384 self.readable_notifier = QtCore.QSocketNotifier(sys.stdin.fileno(), | |
385 QtCore.QSocketNotifier.Read) | |
386 self.readable_notifier.activated.connect(self.on_input) | |
387 self.readable_notifier.setEnabled(True) | |
388 | |
389 if not icon: | |
390 self.icon = None | |
391 elif icon.startswith('/'): | |
392 self.icon = QtGui.QIcon(icon) | |
393 else: | |
394 self.icon = QtGui.QIcon.fromTheme(icon) | |
395 | |
396 if self.icon: | |
397 # create blank, fully transparent pixbuf in order to simulate | |
398 # blinking | |
399 self.blank_icon = QtGui.QIcon() | |
400 | |
401 self.blink_on = True | |
402 self.blinking_timer = QtCore.QTimer() | |
403 self.blinking_timer.setInterval(500) | |
404 self.blinking_timer.timeout.connect(self.on_blink_timeout) | |
405 | |
406 self.status_icon = QtGui.QSystemTrayIcon() | |
407 self.status_icon.setToolTip(APPLICATION) | |
408 self.update_icon() | |
409 self.status_icon.setVisible(True) | |
410 self.status_icon.activated.connect(self.on_activated) | |
411 else: | |
412 self.status_icon = None | |
413 | |
414 def on_sigint(self, signo, frame): | |
415 self.qapplication.exit(0) | |
416 | |
417 def on_input(self, fd): | |
418 try: | |
419 self.parser.parse(os.read(fd, BUFFER_SIZE)) | |
420 except EOFError: | |
421 self.qapplication.exit(1) | |
422 | |
423 def on_activated(self, reason): | |
424 self.reset() | |
425 | |
426 def on_blink_timeout(self): | |
427 self.blink_on = not self.blink_on | |
428 self.update_icon() | |
429 | |
430 def update_icon(self): | |
431 if not self.blink_on: | |
432 self.status_icon.setIcon(self.blank_icon) | |
433 else: | |
434 self.status_icon.setIcon(self.icon) | |
435 | |
436 def notify(self, summary, message, icon): | |
437 if self.status_icon: | |
438 self.status_icon.setToolTip('%s: %s' % (APPLICATION, | |
439 cgi.escape(summary))) | |
440 self.blinking_timer.start() | |
441 if self.status_icon.supportsMessages(): | |
442 self.status_icon.showMessage(summary, message, | |
443 QtGui.QSystemTrayIcon.NoIcon) | |
444 | |
445 def reset(self): | |
446 if self.status_icon: | |
447 self.blinking_timer.stop() | |
448 self.blink_on = True | |
449 self.update_icon() | |
450 self.status_icon.setToolTip(APPLICATION) | |
451 | |
452 def run(self): | |
453 sys.exit(self.qapplication.exec_()) | |
454 | |
455 | |
456 class KDE4Notifier(Notifier): | |
457 """KDE 4 notifier based on PyKDE4""" | |
458 | |
459 def __init__(self, icon): | |
460 super(KDE4Notifier, self).__init__(icon) | |
461 | |
462 signal.signal(signal.SIGINT, self.on_sigint) | |
463 | |
464 aboutData = kdecore.KAboutData(APPLICATION.lower(), '', | |
465 kdecore.ki18n(APPLICATION), VERSION, kdecore.ki18n(SUBTITLE), | |
466 kdecore.KAboutData.License_GPL_V3, kdecore.ki18n(COPYRIGHT), | |
467 kdecore.ki18n (''), HOMEPAGE, EMAIL) | |
468 kdecore.KCmdLineArgs.init(aboutData) | |
469 self.kapplication = kdeui.KApplication() | |
470 | |
471 self.readable_notifier = QtCore.QSocketNotifier(sys.stdin.fileno(), | |
472 QtCore.QSocketNotifier.Read) | |
473 self.readable_notifier.activated.connect(self.on_input) | |
474 self.readable_notifier.setEnabled(True) | |
475 | |
476 if not icon: | |
477 icon_qicon = None | |
478 icon_name = None | |
479 elif icon.startswith('/'): | |
480 icon_qicon = QtGui.QIcon(icon) | |
481 icon_name = None | |
482 else: | |
483 icon_qicon = None | |
484 icon_name = icon | |
485 | |
486 if icon_name or icon_pixmap: | |
487 self.status_notifier = kdeui.KStatusNotifierItem(self.kapplication) | |
488 self.status_notifier.setCategory( | |
489 kdeui.KStatusNotifierItem.Communications) | |
490 if icon_name: | |
491 self.status_notifier.setIconByName(icon_name) | |
492 self.status_notifier.setToolTip(icon_name, APPLICATION, | |
493 SUBTITLE) | |
494 else: | |
495 self.status_notifier.setIconByPixmap(icon_qicon) | |
496 self.status_notifier.setToolTip(icon_qicon, APPLICATION, | |
497 SUBTITLE) | |
498 self.status_notifier.setStandardActionsEnabled(False) | |
499 self.status_notifier.setStatus(kdeui.KStatusNotifierItem.Active) | |
500 self.status_notifier.setTitle(APPLICATION) | |
501 self.status_notifier.activateRequested.connect( | |
502 self.on_activate_requested) | |
503 else: | |
504 self.status_notifier = None | |
505 | |
506 def on_sigint(self, signo, frame): | |
507 self.kapplication.exit(0) | |
508 | |
509 def on_input(self, fd): | |
510 try: | |
511 self.parser.parse(os.read(fd, BUFFER_SIZE)) | |
512 except EOFError: | |
513 self.kapplication.exit(1) | |
514 | |
515 def on_activate_requested(self, active, pos): | |
516 self.reset() | |
517 | |
518 def notify(self, summary, message, icon): | |
519 if self.status_notifier: | |
520 self.status_notifier.setToolTipSubTitle(cgi.escape(summary)) | |
521 self.status_notifier.setStatus( | |
522 kdeui.KStatusNotifierItem.NeedsAttention) | |
523 | |
524 if icon: | |
525 if icon.startswith('/'): | |
526 pixmap = QtGui.QPixmap.load(icon) | |
527 else: | |
528 pixmap = kdeui.KIcon(icon).pixmap(kdeui.KIconLoader.SizeHuge, | |
529 kdeui.KIconLoader.SizeHuge) | |
530 else: | |
531 pixmap = QtGui.QPixmap() | |
532 kdeui.KNotification.event(kdeui.KNotification.Notification, summary, | |
533 cgi.escape(message), pixmap) | |
534 | |
535 def reset(self): | |
536 if self.status_notifier: | |
537 self.status_notifier.setStatus(kdeui.KStatusNotifierItem.Active) | |
538 self.status_notifier.setToolTipTitle(APPLICATION) | |
539 self.status_notifier.setToolTipSubTitle(SUBTITLE) | |
540 | |
541 def run(self): | |
542 sys.exit(self.kapplication.exec_()) | |
543 | |
544 | |
545 class NotificationProxy(object): | |
546 """Proxy object for interfacing with the notifier process""" | |
547 | |
548 def __init__(self, preferred_toolkit, status_icon): | |
549 self.script_file = os.path.realpath(__file__) | |
550 self._status_icon = status_icon | |
551 self._preferred_toolkit = preferred_toolkit | |
552 self.notifier_process_hook = None | |
553 self.spawn_timer_hook = None | |
554 self.next_spawn_time = 0.0 | |
555 | |
556 self.spawn_notifier_process() | |
557 | |
558 @property | |
559 def status_icon(self): | |
560 return self._status_icon | |
561 | |
562 @status_icon.setter | |
563 def status_icon(self, value): | |
564 self._status_icon = value | |
565 self.terminate_notifier_process() | |
566 self.spawn_notifier_process() | |
567 | |
568 @property | |
569 def preferred_toolkit(self): | |
570 return self._preferred_toolkit | |
571 | |
572 @preferred_toolkit.setter | |
573 def preferred_toolkit(self, value): | |
574 self._preferred_toolkit = value | |
575 self.terminate_notifier_process() | |
576 self.spawn_notifier_process() | |
577 | |
578 def on_notifier_process_event(self, data, command, return_code, output, | |
579 error_output): | |
580 if return_code != weechat.WEECHAT_HOOK_PROCESS_RUNNING: | |
581 if return_code == weechat.WEECHAT_HOOK_PROCESS_ERROR: | |
582 error = '%sfailed to run notifier' % weechat.prefix("error") | |
583 else: | |
584 error = '%snotifier exited with exit status %d' % \ | |
585 (weechat.prefix("error"), return_code) | |
586 if output: | |
587 error += '\nstdout:%s' % output | |
588 if error_output: | |
589 error += '\nstderr:%s' % error_output | |
590 weechat.prnt('', error) | |
591 self.notifier_process_hook = None | |
592 self.spawn_notifier_process() | |
593 return weechat.WEECHAT_RC_OK | |
594 | |
595 def on_spawn_timer(self, data, remaining): | |
596 self.spawn_timer_hook = None | |
597 if not self.notifier_process_hook: | |
598 self.spawn_notifier_process() | |
599 return weechat.WEECHAT_RC_OK | |
600 | |
601 def spawn_notifier_process(self): | |
602 if self.notifier_process_hook or self.spawn_timer_hook: | |
603 return | |
604 | |
605 # do not try to respawn a notifier more than once every ten seconds | |
606 now = time.time() | |
607 if long(self.next_spawn_time - now) > 0: | |
608 self.spawn_timer_hook = \ | |
609 weechat.hook_timer(long((self.next_spawn_time - now) * | |
610 1000), 0, 1, 'dispatch_weechat_callback', | |
611 create_weechat_callback(self.on_spawn_timer)) | |
612 return | |
613 | |
614 self.next_spawn_time = now + 10 | |
615 self.notifier_process_hook = \ | |
616 weechat.hook_process_hashtable(sys.executable, {'arg1': | |
617 self.script_file, 'arg2': self.preferred_toolkit, 'arg3': | |
618 self.status_icon, 'stdin': '1'}, 0, | |
619 'dispatch_weechat_callback', | |
620 create_weechat_callback(self.on_notifier_process_event)) | |
621 | |
622 def terminate_notifier_process(self): | |
623 if self.spawn_timer_hook: | |
624 weechat.unhook(self.spawn_timer_hook) | |
625 self.spawn_timer_hook = None | |
626 if self.notifier_process_hook: | |
627 weechat.unhook(self.notifier_process_hook) | |
628 self.notifier_process_hook = None | |
629 self.next_spawn_time = 0.0 | |
630 | |
631 def send(self, command, *args): | |
632 if self.notifier_process_hook: | |
633 if args: | |
634 weechat.hook_set(self.notifier_process_hook, 'stdin', | |
635 netstring_encode(netstring_encode(command, | |
636 netstring_encode(*args)))) | |
637 else: | |
638 weechat.hook_set(self.notifier_process_hook, 'stdin', | |
639 netstring_encode(netstring_encode(command))) | |
640 | |
641 def notify(self, summary, message, icon): | |
642 self.send('notify', summary, message, icon) | |
643 | |
644 def reset(self): | |
645 self.send('reset') | |
646 | |
647 | |
648 class NotificationPlugin(object): | |
649 """Weechat plugin""" | |
650 | |
651 def __init__(self): | |
652 self.DCC_SEND_RE = re.compile(r':(?P<sender>\S+) PRIVMSG \S+ :' | |
653 r'\x01DCC SEND (?P<filename>\S+) \d+ \d+ (?P<size>\d+)') | |
654 self.DCC_CHAT_RE = re.compile(r':(?P<sender>\S+) PRIVMSG \S+ :' | |
655 r'\x01DCC CHAT ') | |
656 | |
657 weechat.register(SCRIPT_NAME, AUTHOR, VERSION, 'GPL3', DESCRIPTION, '', | |
658 '') | |
659 | |
660 for option, (value, description) in DEFAULT_SETTINGS.iteritems(): | |
661 if not weechat.config_is_set_plugin(option): | |
662 weechat.config_set_plugin(option, value) | |
663 weechat.config_set_desc_plugin(option, '%s (default: "%s")' % | |
664 (description, value)) | |
665 | |
666 self.notification_proxy = NotificationProxy( | |
667 weechat.config_get_plugin('preferred_toolkit'), | |
668 weechat.config_get_plugin('status_icon')) | |
669 | |
670 weechat.hook_print('', 'irc_privmsg', '', 1, | |
671 'dispatch_weechat_callback', | |
672 create_weechat_callback(self.on_message)) | |
673 weechat.hook_signal('key_pressed', 'dispatch_weechat_callback', | |
674 create_weechat_callback(self.on_key_pressed)) | |
675 weechat.hook_signal('irc_dcc', 'dispatch_weechat_callback', | |
676 create_weechat_callback(self.on_dcc)) | |
677 weechat.hook_config('plugins.var.python.%s.*' % SCRIPT_NAME, | |
678 'dispatch_weechat_callback', | |
679 create_weechat_callback(self.on_config_changed)) | |
680 | |
681 def on_message(self, data, buffer, date, tags, displayed, highlight, | |
682 prefix, message): | |
683 if weechat.config_get_plugin('notify_on_displayed_only') == 'on' and \ | |
684 int(displayed) != 1: | |
685 return weechat.WEECHAT_RC_OK | |
686 | |
687 formatted_date = time.strftime('%H:%M', time.localtime(float(date))) | |
688 if 'notify_private' in tags.split(',') and \ | |
689 weechat.config_get_plugin('notify_on_privmsg') == 'on': | |
690 summary = 'Private message from %s at %s' % (prefix, | |
691 formatted_date) | |
692 self.notification_proxy.notify(summary, message, | |
693 weechat.config_get_plugin('notification_icon')) | |
694 elif int(highlight) == 1 and \ | |
695 weechat.config_get_plugin('notify_on_highlight') == 'on': | |
696 summary = 'Highlighted message from %s at %s' % (prefix, | |
697 formatted_date) | |
698 self.notification_proxy.notify(summary, message, | |
699 weechat.config_get_plugin('notification_icon')) | |
700 | |
701 return weechat.WEECHAT_RC_OK | |
702 | |
703 def on_dcc(self, data, signal, signal_data): | |
704 if weechat.config_get_plugin('notify_on_dcc') != 'on': | |
705 return weechat.WEECHAT_RC_OK | |
706 | |
707 matches = self.DCC_SEND_RE.match(signal_data) | |
708 if matches: | |
709 summary = 'DCC send request from %s' % matches.group('sender') | |
710 message = 'Filname: %s, Size: %d bytes' % \ | |
711 (matches.group('filename'), int(matches.group('size'))) | |
712 self.notification_proxy.notify(summary, message, | |
713 weechat.config_get_plugin('notification_icon')) | |
714 return weechat.WEECHAT_RC_OK | |
715 | |
716 matches = self.DCC_CHAT_RE.match(signal_data) | |
717 if matches: | |
718 summary = 'DCC chat request from %s' % matches.group('sender') | |
719 message = '' | |
720 self.notification_proxy.notify(summary, message, | |
721 weechat.config_get_plugin('notification_icon')) | |
722 return weechat.WEECHAT_RC_OK | |
723 | |
724 return weechat.WEECHAT_RC_OK | |
725 | |
726 def on_key_pressed(self, data, signal, signal_data): | |
727 self.notification_proxy.reset() | |
728 return weechat.WEECHAT_RC_OK | |
729 | |
730 def on_config_changed(self, data, option, value): | |
731 if option.endswith('.preferred_toolkit'): | |
732 self.notification_proxy.preferred_toolkit = value | |
733 elif option.endswith('.status_icon'): | |
734 self.notification_proxy.status_icon = value | |
735 return weechat.WEECHAT_RC_OK | |
736 | |
737 | |
738 def import_modules(modules): | |
739 for module_name, fromlist in modules: | |
740 if fromlist: | |
741 module = __import__(module_name, fromlist=fromlist) | |
742 for identifier in fromlist: | |
743 globals()[identifier] = getattr(module, identifier) | |
744 else: | |
745 globals()[module_name] = __import__(module_name) | |
746 | |
747 def try_import_modules(modules): | |
748 try: | |
749 import_modules(modules) | |
750 except ImportError: | |
751 sys.exit(1) | |
752 sys.exit(0) | |
753 | |
754 | |
755 if __name__ == '__main__': | |
756 if sys.argv[0] == '__weechat_plugin__': | |
757 # running as Weechat plugin | |
758 import weechat | |
759 | |
760 weechat_callbacks = {} | |
761 | |
762 plugin = NotificationPlugin() | |
763 elif len(sys.argv) == 3: | |
764 # running as the notifier process | |
765 preferred_toolkit = sys.argv[1] | |
766 icon = sys.argv[2] | |
767 | |
768 # required modules for each toolkit | |
769 toolkits_modules = { | |
770 'gtk3': [ | |
771 ('gi.repository', [ | |
772 'GLib', | |
773 'GdkPixbuf', | |
774 'Gtk', | |
775 'Notify' | |
776 ]) | |
777 ], | |
778 'gtk2': [ | |
779 ('pygtk', []), | |
780 ('gobject', []), | |
781 ('gtk', []), | |
782 ('pynotify', []) | |
783 ], | |
784 'qt4': [ | |
785 ('PyQt4', [ | |
786 'QtGui', | |
787 'QtCore' | |
788 ]) | |
789 ], | |
790 'kde4': [ | |
791 ('PyQt4', [ | |
792 'QtGui', | |
793 'QtCore' | |
794 ]), | |
795 ('PyKDE4', [ | |
796 'kdecore', | |
797 'kdeui' | |
798 ]) | |
799 ], | |
800 '': [] | |
801 } | |
802 available_toolkits = [] | |
803 selected_toolkit = '' | |
804 | |
805 # find available toolkits by spawning a process for each toolkit which | |
806 # tries to import all required modules and returns an exit status of 1 | |
807 # in case of an import error | |
808 for toolkit in toolkits_modules: | |
809 process = multiprocessing.Process(target=try_import_modules, | |
810 args=(toolkits_modules[toolkit],)) | |
811 process.start() | |
812 process.join(3) | |
813 if process.is_alive(): | |
814 process.terminate() | |
815 process.join() | |
816 if process.exitcode == 0: | |
817 available_toolkits.append(toolkit) | |
818 | |
819 # select toolkit based on either explicit preference or the | |
820 # availability of modules and the used desktop environment | |
821 if preferred_toolkit: | |
822 if preferred_toolkit in available_toolkits: | |
823 selected_toolkit = preferred_toolkit | |
824 else: | |
825 if 'KDE_FULL_SESSION' in os.environ: | |
826 # preferred order if running KDE4 | |
827 toolkits = ['kde4', 'qt4', 'gtk3', 'gtk2'] | |
828 else: | |
829 # preferred order for all other desktop environments | |
830 toolkits = ['gtk3', 'gtk2', 'qt4', 'kde4'] | |
831 for toolkit in toolkits: | |
832 if toolkit in available_toolkits: | |
833 selected_toolkit = toolkit | |
834 break | |
835 | |
836 # import required toolkit modules | |
837 import_modules(toolkits_modules[selected_toolkit]) | |
838 | |
839 # run selected notifier | |
840 if selected_toolkit == 'gtk3': | |
841 notifier = Gtk3Notifier(icon) | |
842 elif selected_toolkit == 'gtk2': | |
843 notifier = Gtk2Notifier(icon) | |
844 elif selected_toolkit == 'qt4': | |
845 notifier = Qt4Notifier(icon) | |
846 elif selected_toolkit == 'kde4': | |
847 notifier = KDE4Notifier(icon) | |
848 else: | |
849 notifier = Notifier(icon) | |
850 notifier.run() | |
851 else: | |
852 sys.exit(1) |