]> git.rmz.io Git - dotfiles.git/blob - weechat/python/autosort.py
vim: open commit window in a new tab
[dotfiles.git] / weechat / python / autosort.py
1 # -*- coding: utf-8 -*-
2 #
3 # Copyright (C) 2013-2017 Maarten de Vries <maarten@de-vri.es>
4 #
5 # This program is free software; you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation; either version 3 of the License, or
8 # (at your option) any later version.
9 #
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License
16 # along with this program. If not, see <http://www.gnu.org/licenses/>.
17 #
18
19 #
20 # Autosort automatically keeps your buffers sorted and grouped by server.
21 # You can define your own sorting rules. See /help autosort for more details.
22 #
23 # https://github.com/de-vri-es/weechat-autosort
24 #
25
26 #
27 # Changelog:
28 # 3.6:
29 # * Add more documentation on provided info hooks.
30 # 3.5:
31 # * Add ${info:autosort_escape,...} to escape arguments for other info hooks.
32 # 3.4:
33 # * Fix rate-limit of sorting to prevent high CPU load and lock-ups.
34 # * Fix bug in parsing empty arguments for info hooks.
35 # * Add debug_log option to aid with debugging.
36 # * Correct a few typos.
37 # 3.3:
38 # * Fix the /autosort debug command for unicode.
39 # * Update the default rules to work better with Slack.
40 # 3.2:
41 # * Fix python3 compatiblity.
42 # 3.1:
43 # * Use colors to format the help text.
44 # 3.0:
45 # * Switch to evaluated expressions for sorting.
46 # * Add `/autosort debug` command.
47 # * Add ${info:autosort_replace,from,to,text} to replace substrings in sort rules.
48 # * Add ${info:autosort_order,value,first,second,third} to ease writing sort rules.
49 # * Make tab completion context aware.
50 # 2.8:
51 # * Fix compatibility with python 3 regarding unicode handling.
52 # 2.7:
53 # * Fix sorting of buffers with spaces in their name.
54 # 2.6:
55 # * Ignore case in rules when doing case insensitive sorting.
56 # 2.5:
57 # * Fix handling unicode buffer names.
58 # * Add hint to set irc.look.server_buffer to independent and buffers.look.indenting to on.
59 # 2.4:
60 # * Make script python3 compatible.
61 # 2.3:
62 # * Fix sorting items without score last (regressed in 2.2).
63 # 2.2:
64 # * Add configuration option for signals that trigger a sort.
65 # * Add command to manually trigger a sort (/autosort sort).
66 # * Add replacement patterns to apply before sorting.
67 # 2.1:
68 # * Fix some minor style issues.
69 # 2.0:
70 # * Allow for custom sort rules.
71 #
72
73
74 import json
75 import math
76 import re
77 import sys
78 import time
79 import weechat
80
81 SCRIPT_NAME = 'autosort'
82 SCRIPT_AUTHOR = 'Maarten de Vries <maarten@de-vri.es>'
83 SCRIPT_VERSION = '3.6'
84 SCRIPT_LICENSE = 'GPL3'
85 SCRIPT_DESC = 'Flexible automatic (or manual) buffer sorting based on eval expressions.'
86
87
88 config = None
89 hooks = []
90 signal_delay_timer = None
91 sort_limit_timer = None
92 sort_queued = False
93
94
95 # Make sure that unicode, bytes and str are always available in python2 and 3.
96 # For python 2, str == bytes
97 # For python 3, str == unicode
98 if sys.version_info[0] >= 3:
99 unicode = str
100
101 def ensure_str(input):
102 '''
103 Make sure the given type if the correct string type for the current python version.
104 That means bytes for python2 and unicode for python3.
105 '''
106 if not isinstance(input, str):
107 if isinstance(input, bytes):
108 return input.encode('utf-8')
109 if isinstance(input, unicode):
110 return input.decode('utf-8')
111 return input
112
113
114 if hasattr(time, 'perf_counter'):
115 perf_counter = time.perf_counter
116 else:
117 perf_counter = time.clock
118
119 def casefold(string):
120 if hasattr(string, 'casefold'): return string.casefold()
121 # Fall back to lowercasing for python2.
122 return string.lower()
123
124 def list_swap(values, a, b):
125 values[a], values[b] = values[b], values[a]
126
127 def list_move(values, old_index, new_index):
128 values.insert(new_index, values.pop(old_index))
129
130 def list_find(collection, value):
131 for i, elem in enumerate(collection):
132 if elem == value: return i
133 return None
134
135 class HumanReadableError(Exception):
136 pass
137
138 def parse_int(arg, arg_name = 'argument'):
139 ''' Parse an integer and provide a more human readable error. '''
140 arg = arg.strip()
141 try:
142 return int(arg)
143 except ValueError:
144 raise HumanReadableError('Invalid {0}: expected integer, got "{1}".'.format(arg_name, arg))
145
146 def decode_rules(blob):
147 parsed = json.loads(blob)
148 if not isinstance(parsed, list):
149 log('Malformed rules, expected a JSON encoded list of strings, but got a {0}. No rules have been loaded. Please fix the setting manually.'.format(type(parsed)))
150 return []
151
152 for i, entry in enumerate(parsed):
153 if not isinstance(entry, (str, unicode)):
154 log('Rule #{0} is not a string but a {1}. No rules have been loaded. Please fix the setting manually.'.format(i, type(entry)))
155 return []
156
157 return parsed
158
159 def decode_helpers(blob):
160 parsed = json.loads(blob)
161 if not isinstance(parsed, dict):
162 log('Malformed helpers, expected a JSON encoded dictionary but got a {0}. No helpers have been loaded. Please fix the setting manually.'.format(type(parsed)))
163 return {}
164
165 for key, value in parsed.items():
166 if not isinstance(value, (str, unicode)):
167 log('Helper "{0}" is not a string but a {1}. No helpers have been loaded. Please fix setting manually.'.format(key, type(value)))
168 return {}
169 return parsed
170
171 class Config:
172 ''' The autosort configuration. '''
173
174 default_rules = json.dumps([
175 '${core_first}',
176 '${irc_last}',
177 '${buffer.plugin.name}',
178 '${irc_raw_first}',
179 '${if:${plugin}==irc?${server}}',
180 '${if:${plugin}==irc?${info:autosort_order,${type},server,*,channel,private}}',
181 '${if:${plugin}==irc?${hashless_name}}',
182 '${buffer.full_name}',
183 ])
184
185 default_helpers = json.dumps({
186 'core_first': '${if:${buffer.full_name}!=core.weechat}',
187 'irc_first': '${if:${buffer.plugin.name}!=irc}',
188 'irc_last': '${if:${buffer.plugin.name}==irc}',
189 'irc_raw_first': '${if:${buffer.full_name}!=irc.irc_raw}',
190 'irc_raw_last': '${if:${buffer.full_name}==irc.irc_raw}',
191 'hashless_name': '${info:autosort_replace,#,,${info:autosort_escape,${buffer.name}}}',
192 })
193
194 default_signal_delay = 5
195 default_sort_limit = 100
196
197 default_signals = 'buffer_opened buffer_merged buffer_unmerged buffer_renamed'
198
199 def __init__(self, filename):
200 ''' Initialize the configuration. '''
201
202 self.filename = filename
203 self.config_file = weechat.config_new(self.filename, '', '')
204 self.sorting_section = None
205 self.v3_section = None
206
207 self.case_sensitive = False
208 self.rules = []
209 self.helpers = {}
210 self.signals = []
211 self.signal_delay = Config.default_signal_delay,
212 self.sort_limit = Config.default_sort_limit,
213 self.sort_on_config = True
214 self.debug_log = False
215
216 self.__case_sensitive = None
217 self.__rules = None
218 self.__helpers = None
219 self.__signals = None
220 self.__signal_delay = None
221 self.__sort_limit = None
222 self.__sort_on_config = None
223 self.__debug_log = None
224
225 if not self.config_file:
226 log('Failed to initialize configuration file "{0}".'.format(self.filename))
227 return
228
229 self.sorting_section = weechat.config_new_section(self.config_file, 'sorting', False, False, '', '', '', '', '', '', '', '', '', '')
230 self.v3_section = weechat.config_new_section(self.config_file, 'v3', False, False, '', '', '', '', '', '', '', '', '', '')
231
232 if not self.sorting_section:
233 log('Failed to initialize section "sorting" of configuration file.')
234 weechat.config_free(self.config_file)
235 return
236
237 self.__case_sensitive = weechat.config_new_option(
238 self.config_file, self.sorting_section,
239 'case_sensitive', 'boolean',
240 'If this option is on, sorting is case sensitive.',
241 '', 0, 0, 'off', 'off', 0,
242 '', '', '', '', '', ''
243 )
244
245 weechat.config_new_option(
246 self.config_file, self.sorting_section,
247 'rules', 'string',
248 'Sort rules used by autosort v2.x and below. Not used by autosort anymore.',
249 '', 0, 0, '', '', 0,
250 '', '', '', '', '', ''
251 )
252
253 weechat.config_new_option(
254 self.config_file, self.sorting_section,
255 'replacements', 'string',
256 'Replacement patterns used by autosort v2.x and below. Not used by autosort anymore.',
257 '', 0, 0, '', '', 0,
258 '', '', '', '', '', ''
259 )
260
261 self.__rules = weechat.config_new_option(
262 self.config_file, self.v3_section,
263 'rules', 'string',
264 'An ordered list of sorting rules encoded as JSON. See /help autosort for commands to manipulate these rules.',
265 '', 0, 0, Config.default_rules, Config.default_rules, 0,
266 '', '', '', '', '', ''
267 )
268
269 self.__helpers = weechat.config_new_option(
270 self.config_file, self.v3_section,
271 'helpers', 'string',
272 'A dictionary helper variables to use in the sorting rules, encoded as JSON. See /help autosort for commands to manipulate these helpers.',
273 '', 0, 0, Config.default_helpers, Config.default_helpers, 0,
274 '', '', '', '', '', ''
275 )
276
277 self.__signals = weechat.config_new_option(
278 self.config_file, self.sorting_section,
279 'signals', 'string',
280 'A space separated list of signals that will cause autosort to resort your buffer list.',
281 '', 0, 0, Config.default_signals, Config.default_signals, 0,
282 '', '', '', '', '', ''
283 )
284
285 self.__signal_delay = weechat.config_new_option(
286 self.config_file, self.sorting_section,
287 'signal_delay', 'integer',
288 'Delay in milliseconds to wait after a signal before sorting the buffer list. This prevents triggering many times if multiple signals arrive in a short time. It can also be needed to wait for buffer localvars to be available.',
289 '', 0, 1000, str(Config.default_signal_delay), str(Config.default_signal_delay), 0,
290 '', '', '', '', '', ''
291 )
292
293 self.__sort_limit = weechat.config_new_option(
294 self.config_file, self.sorting_section,
295 'sort_limit', 'integer',
296 'Minimum delay in milliseconds to wait after sorting before signals can trigger a sort again. This is effectively a rate limit on sorting. Keeping signal_delay low while setting this higher can reduce excessive sorting without a long initial delay.',
297 '', 0, 1000, str(Config.default_sort_limit), str(Config.default_sort_limit), 0,
298 '', '', '', '', '', ''
299 )
300
301 self.__sort_on_config = weechat.config_new_option(
302 self.config_file, self.sorting_section,
303 'sort_on_config_change', 'boolean',
304 'Decides if the buffer list should be sorted when autosort configuration changes.',
305 '', 0, 0, 'on', 'on', 0,
306 '', '', '', '', '', ''
307 )
308
309 self.__debug_log = weechat.config_new_option(
310 self.config_file, self.sorting_section,
311 'debug_log', 'boolean',
312 'If enabled, print more debug messages. Not recommended for normal usage.',
313 '', 0, 0, 'off', 'off', 0,
314 '', '', '', '', '', ''
315 )
316
317 if weechat.config_read(self.config_file) != weechat.WEECHAT_RC_OK:
318 log('Failed to load configuration file.')
319
320 if weechat.config_write(self.config_file) != weechat.WEECHAT_RC_OK:
321 log('Failed to write configuration file.')
322
323 self.reload()
324
325 def reload(self):
326 ''' Load configuration variables. '''
327
328 self.case_sensitive = weechat.config_boolean(self.__case_sensitive)
329
330 rules_blob = weechat.config_string(self.__rules)
331 helpers_blob = weechat.config_string(self.__helpers)
332 signals_blob = weechat.config_string(self.__signals)
333
334 self.rules = decode_rules(rules_blob)
335 self.helpers = decode_helpers(helpers_blob)
336 self.signals = signals_blob.split()
337 self.signal_delay = weechat.config_integer(self.__signal_delay)
338 self.sort_limit = weechat.config_integer(self.__sort_limit)
339 self.sort_on_config = weechat.config_boolean(self.__sort_on_config)
340 self.debug_log = weechat.config_boolean(self.__debug_log)
341
342 def save_rules(self, run_callback = True):
343 ''' Save the current rules to the configuration. '''
344 weechat.config_option_set(self.__rules, json.dumps(self.rules), run_callback)
345
346 def save_helpers(self, run_callback = True):
347 ''' Save the current helpers to the configuration. '''
348 weechat.config_option_set(self.__helpers, json.dumps(self.helpers), run_callback)
349
350
351 def pad(sequence, length, padding = None):
352 ''' Pad a list until is has a certain length. '''
353 return sequence + [padding] * max(0, (length - len(sequence)))
354
355 def log(message, buffer = 'NULL'):
356 weechat.prnt(buffer, 'autosort: {0}'.format(message))
357
358 def debug(message, buffer = 'NULL'):
359 if config.debug_log:
360 weechat.prnt(buffer, 'autosort: debug: {0}'.format(message))
361
362 def get_buffers():
363 ''' Get a list of all the buffers in weechat. '''
364 hdata = weechat.hdata_get('buffer')
365 buffer = weechat.hdata_get_list(hdata, "gui_buffers");
366
367 result = []
368 while buffer:
369 number = weechat.hdata_integer(hdata, buffer, 'number')
370 result.append((number, buffer))
371 buffer = weechat.hdata_pointer(hdata, buffer, 'next_buffer')
372 return hdata, result
373
374 class MergedBuffers(list):
375 """ A list of merged buffers, possibly of size 1. """
376 def __init__(self, number):
377 super(MergedBuffers, self).__init__()
378 self.number = number
379
380 def merge_buffer_list(buffers):
381 '''
382 Group merged buffers together.
383 The output is a list of MergedBuffers.
384 '''
385 if not buffers: return []
386 result = {}
387 for number, buffer in buffers:
388 if number not in result: result[number] = MergedBuffers(number)
389 result[number].append(buffer)
390 return result.values()
391
392 def sort_buffers(hdata, buffers, rules, helpers, case_sensitive):
393 for merged in buffers:
394 for buffer in merged:
395 name = weechat.hdata_string(hdata, buffer, 'name')
396
397 return sorted(buffers, key=merged_sort_key(rules, helpers, case_sensitive))
398
399 def buffer_sort_key(rules, helpers, case_sensitive):
400 ''' Create a sort key function for a list of lists of merged buffers. '''
401 def key(buffer):
402 extra_vars = {}
403 for helper_name, helper in sorted(helpers.items()):
404 expanded = weechat.string_eval_expression(helper, {"buffer": buffer}, {}, {})
405 extra_vars[helper_name] = expanded if case_sensitive else casefold(expanded)
406 result = []
407 for rule in rules:
408 expanded = weechat.string_eval_expression(rule, {"buffer": buffer}, extra_vars, {})
409 result.append(expanded if case_sensitive else casefold(expanded))
410 return result
411
412 return key
413
414 def merged_sort_key(rules, helpers, case_sensitive):
415 buffer_key = buffer_sort_key(rules, helpers, case_sensitive)
416 def key(merged):
417 best = None
418 for buffer in merged:
419 this = buffer_key(buffer)
420 if best is None or this < best: best = this
421 return best
422 return key
423
424 def apply_buffer_order(buffers):
425 ''' Sort the buffers in weechat according to the given order. '''
426 for i, buffer in enumerate(buffers):
427 weechat.buffer_set(buffer[0], "number", str(i + 1))
428
429 def split_args(args, expected, optional = 0):
430 ''' Split an argument string in the desired number of arguments. '''
431 split = args.split(' ', expected - 1)
432 if (len(split) < expected):
433 raise HumanReadableError('Expected at least {0} arguments, got {1}.'.format(expected, len(split)))
434 return split[:-1] + pad(split[-1].split(' ', optional), optional + 1, '')
435
436 def do_sort(verbose = False):
437 start = perf_counter()
438
439 hdata, buffers = get_buffers()
440 buffers = merge_buffer_list(buffers)
441 buffers = sort_buffers(hdata, buffers, config.rules, config.helpers, config.case_sensitive)
442 apply_buffer_order(buffers)
443
444 elapsed = perf_counter() - start
445 if verbose:
446 log("Finished sorting buffers in {0:.4f} seconds.".format(elapsed))
447 else:
448 debug("Finished sorting buffers in {0:.4f} seconds.".format(elapsed))
449
450 def command_sort(buffer, command, args):
451 ''' Sort the buffers and print a confirmation. '''
452 do_sort(True)
453 return weechat.WEECHAT_RC_OK
454
455 def command_debug(buffer, command, args):
456 hdata, buffers = get_buffers()
457 buffers = merge_buffer_list(buffers)
458
459 # Show evaluation results.
460 log('Individual evaluation results:')
461 start = perf_counter()
462 key = buffer_sort_key(config.rules, config.helpers, config.case_sensitive)
463 results = []
464 for merged in buffers:
465 for buffer in merged:
466 fullname = weechat.hdata_string(hdata, buffer, 'full_name')
467 results.append((fullname, key(buffer)))
468 elapsed = perf_counter() - start
469
470 for fullname, result in results:
471 fullname = ensure_str(fullname)
472 result = [ensure_str(x) for x in result]
473 log('{0}: {1}'.format(fullname, result))
474 log('Computing evaluation results took {0:.4f} seconds.'.format(elapsed))
475
476 return weechat.WEECHAT_RC_OK
477
478 def command_rule_list(buffer, command, args):
479 ''' Show the list of sorting rules. '''
480 output = 'Sorting rules:\n'
481 for i, rule in enumerate(config.rules):
482 output += ' {0}: {1}\n'.format(i, rule)
483 if not len(config.rules):
484 output += ' No sorting rules configured.\n'
485 log(output )
486
487 return weechat.WEECHAT_RC_OK
488
489
490 def command_rule_add(buffer, command, args):
491 ''' Add a rule to the rule list. '''
492 config.rules.append(args)
493 config.save_rules()
494 command_rule_list(buffer, command, '')
495
496 return weechat.WEECHAT_RC_OK
497
498
499 def command_rule_insert(buffer, command, args):
500 ''' Insert a rule at the desired position in the rule list. '''
501 index, rule = split_args(args, 2)
502 index = parse_int(index, 'index')
503
504 config.rules.insert(index, rule)
505 config.save_rules()
506 command_rule_list(buffer, command, '')
507 return weechat.WEECHAT_RC_OK
508
509
510 def command_rule_update(buffer, command, args):
511 ''' Update a rule in the rule list. '''
512 index, rule = split_args(args, 2)
513 index = parse_int(index, 'index')
514
515 config.rules[index] = rule
516 config.save_rules()
517 command_rule_list(buffer, command, '')
518 return weechat.WEECHAT_RC_OK
519
520
521 def command_rule_delete(buffer, command, args):
522 ''' Delete a rule from the rule list. '''
523 index = args.strip()
524 index = parse_int(index, 'index')
525
526 config.rules.pop(index)
527 config.save_rules()
528 command_rule_list(buffer, command, '')
529 return weechat.WEECHAT_RC_OK
530
531
532 def command_rule_move(buffer, command, args):
533 ''' Move a rule to a new position. '''
534 index_a, index_b = split_args(args, 2)
535 index_a = parse_int(index_a, 'index')
536 index_b = parse_int(index_b, 'index')
537
538 list_move(config.rules, index_a, index_b)
539 config.save_rules()
540 command_rule_list(buffer, command, '')
541 return weechat.WEECHAT_RC_OK
542
543
544 def command_rule_swap(buffer, command, args):
545 ''' Swap two rules. '''
546 index_a, index_b = split_args(args, 2)
547 index_a = parse_int(index_a, 'index')
548 index_b = parse_int(index_b, 'index')
549
550 list_swap(config.rules, index_a, index_b)
551 config.save_rules()
552 command_rule_list(buffer, command, '')
553 return weechat.WEECHAT_RC_OK
554
555
556 def command_helper_list(buffer, command, args):
557 ''' Show the list of helpers. '''
558 output = 'Helper variables:\n'
559
560 width = max(map(lambda x: len(x) if len(x) <= 30 else 0, config.helpers.keys()))
561
562 for name, expression in sorted(config.helpers.items()):
563 output += ' {0:>{width}}: {1}\n'.format(name, expression, width=width)
564 if not len(config.helpers):
565 output += ' No helper variables configured.'
566 log(output)
567
568 return weechat.WEECHAT_RC_OK
569
570
571 def command_helper_set(buffer, command, args):
572 ''' Add/update a helper to the helper list. '''
573 name, expression = split_args(args, 2)
574
575 config.helpers[name] = expression
576 config.save_helpers()
577 command_helper_list(buffer, command, '')
578
579 return weechat.WEECHAT_RC_OK
580
581 def command_helper_delete(buffer, command, args):
582 ''' Delete a helper from the helper list. '''
583 name = args.strip()
584
585 del config.helpers[name]
586 config.save_helpers()
587 command_helper_list(buffer, command, '')
588 return weechat.WEECHAT_RC_OK
589
590
591 def command_helper_rename(buffer, command, args):
592 ''' Rename a helper to a new position. '''
593 old_name, new_name = split_args(args, 2)
594
595 try:
596 config.helpers[new_name] = config.helpers[old_name]
597 del config.helpers[old_name]
598 except KeyError:
599 raise HumanReadableError('No such helper: {0}'.format(old_name))
600 config.save_helpers()
601 command_helper_list(buffer, command, '')
602 return weechat.WEECHAT_RC_OK
603
604
605 def command_helper_swap(buffer, command, args):
606 ''' Swap two helpers. '''
607 a, b = split_args(args, 2)
608 try:
609 config.helpers[b], config.helpers[a] = config.helpers[a], config.helpers[b]
610 except KeyError as e:
611 raise HumanReadableError('No such helper: {0}'.format(e.args[0]))
612
613 config.helpers.swap(index_a, index_b)
614 config.save_helpers()
615 command_helper_list(buffer, command, '')
616 return weechat.WEECHAT_RC_OK
617
618 def call_command(buffer, command, args, subcommands):
619 ''' Call a subcommand from a dictionary. '''
620 subcommand, tail = pad(args.split(' ', 1), 2, '')
621 subcommand = subcommand.strip()
622 if (subcommand == ''):
623 child = subcommands.get(' ')
624 else:
625 command = command + [subcommand]
626 child = subcommands.get(subcommand)
627
628 if isinstance(child, dict):
629 return call_command(buffer, command, tail, child)
630 elif callable(child):
631 return child(buffer, command, tail)
632
633 log('{0}: command not found'.format(' '.join(command)))
634 return weechat.WEECHAT_RC_ERROR
635
636 def on_signal(data, signal, signal_data):
637 global signal_delay_timer
638 global sort_queued
639
640 # If the sort limit timeout is started, we're in the hold-off time after sorting, just queue a sort.
641 if sort_limit_timer is not None:
642 if sort_queued:
643 debug('Signal {0} ignored, sort limit timeout is active and sort is already queued.'.format(signal))
644 else:
645 debug('Signal {0} received but sort limit timeout is active, sort is now queued.'.format(signal))
646 sort_queued = True
647 return weechat.WEECHAT_RC_OK
648
649 # If the signal delay timeout is started, a signal was recently received, so ignore this signal.
650 if signal_delay_timer is not None:
651 debug('Signal {0} ignored, signal delay timeout active.'.format(signal))
652 return weechat.WEECHAT_RC_OK
653
654 # Otherwise, start the signal delay timeout.
655 debug('Signal {0} received, starting signal delay timeout of {1} ms.'.format(signal, config.signal_delay))
656 weechat.hook_timer(config.signal_delay, 0, 1, "on_signal_delay_timeout", "")
657 return weechat.WEECHAT_RC_OK
658
659 def on_signal_delay_timeout(pointer, remaining_calls):
660 """ Called when the signal_delay_timer triggers. """
661 global signal_delay_timer
662 global sort_limit_timer
663 global sort_queued
664
665 signal_delay_timer = None
666
667 # If the sort limit timeout was started, we're still in the no-sort period, so just queue a sort.
668 if sort_limit_timer is not None:
669 debug('Signal delay timeout expired, but sort limit timeout is active, sort is now queued.')
670 sort_queued = True
671 return weechat.WEECHAT_RC_OK
672
673 # Time to sort!
674 debug('Signal delay timeout expired, starting sort.')
675 do_sort()
676
677 # Start the sort limit timeout if not disabled.
678 if config.sort_limit > 0:
679 debug('Starting sort limit timeout of {0} ms.'.format(config.sort_limit))
680 sort_limit_timer = weechat.hook_timer(config.sort_limit, 0, 1, "on_sort_limit_timeout", "")
681
682 return weechat.WEECHAT_RC_OK
683
684 def on_sort_limit_timeout(pointer, remainin_calls):
685 """ Called when de sort_limit_timer triggers. """
686 global sort_limit_timer
687 global sort_queued
688
689 # If no signal was received during the timeout, we're done.
690 if not sort_queued:
691 debug('Sort limit timeout expired without receiving a signal.')
692 sort_limit_timer = None
693 return weechat.WEECHAT_RC_OK
694
695 # Otherwise it's time to sort.
696 debug('Signal received during sort limit timeout, starting queued sort.')
697 do_sort()
698 sort_queued = False
699
700 # Start the sort limit timeout again if not disabled.
701 if config.sort_limit > 0:
702 debug('Starting sort limit timeout of {0} ms.'.format(config.sort_limit))
703 sort_limit_timer = weechat.hook_timer(config.sort_limit, 0, 1, "on_sort_limit_timeout", "")
704
705 return weechat.WEECHAT_RC_OK
706
707
708 def apply_config():
709 # Unhook all signals and hook the new ones.
710 for hook in hooks:
711 weechat.unhook(hook)
712 for signal in config.signals:
713 hooks.append(weechat.hook_signal(signal, 'on_signal', ''))
714
715 if config.sort_on_config:
716 debug('Sorting because configuration changed.')
717 do_sort()
718
719 def on_config_changed(*args, **kwargs):
720 ''' Called whenever the configuration changes. '''
721 config.reload()
722 apply_config()
723
724 return weechat.WEECHAT_RC_OK
725
726 def parse_arg(args):
727 if not args: return '', None
728
729 result = ''
730 escaped = False
731 for i, c in enumerate(args):
732 if not escaped:
733 if c == '\\':
734 escaped = True
735 continue
736 elif c == ',':
737 return result, args[i+1:]
738 result += c
739 escaped = False
740 return result, None
741
742 def parse_args(args, max = None):
743 result = []
744 i = 0
745 while max is None or i < max:
746 i += 1
747 arg, args = parse_arg(args)
748 if arg is None: break
749 result.append(arg)
750 if args is None: break
751 return result, args
752
753 def on_info_escape(pointer, name, arguments):
754 result = ''
755 for c in arguments:
756 if c == '\\':
757 result += '\\\\'
758 elif c == ',':
759 result += '\\,'
760 else:
761 result +=c
762 return result
763
764 def on_info_replace(pointer, name, arguments):
765 arguments, rest = parse_args(arguments, 3)
766 if rest or len(arguments) < 3:
767 log('usage: ${{info:{0},old,new,text}}'.format(name))
768 return ''
769 old, new, text = arguments
770
771 return text.replace(old, new)
772
773 def on_info_order(pointer, name, arguments):
774 arguments, rest = parse_args(arguments)
775 if len(arguments) < 1:
776 log('usage: ${{info:{0},value,first,second,third,...}}'.format(name))
777 return ''
778
779 value = arguments[0]
780 keys = arguments[1:]
781 if not keys: return '0'
782
783 # Find the value in the keys (or '*' if we can't find it)
784 result = list_find(keys, value)
785 if result is None: result = list_find(keys, '*')
786 if result is None: result = len(keys)
787
788 # Pad result with leading zero to make sure string sorting works.
789 width = int(math.log10(len(keys))) + 1
790 return '{0:0{1}}'.format(result, width)
791
792
793 def on_autosort_command(data, buffer, args):
794 ''' Called when the autosort command is invoked. '''
795 try:
796 return call_command(buffer, ['/autosort'], args, {
797 ' ': command_sort,
798 'sort': command_sort,
799 'debug': command_debug,
800
801 'rules': {
802 ' ': command_rule_list,
803 'list': command_rule_list,
804 'add': command_rule_add,
805 'insert': command_rule_insert,
806 'update': command_rule_update,
807 'delete': command_rule_delete,
808 'move': command_rule_move,
809 'swap': command_rule_swap,
810 },
811 'helpers': {
812 ' ': command_helper_list,
813 'list': command_helper_list,
814 'set': command_helper_set,
815 'delete': command_helper_delete,
816 'rename': command_helper_rename,
817 'swap': command_helper_swap,
818 },
819 })
820 except HumanReadableError as e:
821 log(e)
822 return weechat.WEECHAT_RC_ERROR
823
824 def add_completions(completion, words):
825 for word in words:
826 weechat.hook_completion_list_add(completion, word, 0, weechat.WEECHAT_LIST_POS_END)
827
828 def autosort_complete_rules(words, completion):
829 if len(words) == 0:
830 add_completions(completion, ['add', 'delete', 'insert', 'list', 'move', 'swap', 'update'])
831 if len(words) == 1 and words[0] in ('delete', 'insert', 'move', 'swap', 'update'):
832 add_completions(completion, map(str, range(len(config.rules))))
833 if len(words) == 2 and words[0] in ('move', 'swap'):
834 add_completions(completion, map(str, range(len(config.rules))))
835 if len(words) == 2 and words[0] in ('update'):
836 try:
837 add_completions(completion, [config.rules[int(words[1])]])
838 except KeyError: pass
839 except ValueError: pass
840 else:
841 add_completions(completion, [''])
842 return weechat.WEECHAT_RC_OK
843
844 def autosort_complete_helpers(words, completion):
845 if len(words) == 0:
846 add_completions(completion, ['delete', 'list', 'rename', 'set', 'swap'])
847 elif len(words) == 1 and words[0] in ('delete', 'rename', 'set', 'swap'):
848 add_completions(completion, sorted(config.helpers.keys()))
849 elif len(words) == 2 and words[0] == 'swap':
850 add_completions(completion, sorted(config.helpers.keys()))
851 elif len(words) == 2 and words[0] == 'rename':
852 add_completions(completion, sorted(config.helpers.keys()))
853 elif len(words) == 2 and words[0] == 'set':
854 try:
855 add_completions(completion, [config.helpers[words[1]]])
856 except KeyError: pass
857 return weechat.WEECHAT_RC_OK
858
859 def on_autosort_complete(data, name, buffer, completion):
860 cmdline = weechat.buffer_get_string(buffer, "input")
861 cursor = weechat.buffer_get_integer(buffer, "input_pos")
862 prefix = cmdline[:cursor]
863 words = prefix.split()[1:]
864
865 # If the current word isn't finished yet,
866 # ignore it for coming up with completion suggestions.
867 if prefix[-1] != ' ': words = words[:-1]
868
869 if len(words) == 0:
870 add_completions(completion, ['debug', 'helpers', 'rules', 'sort'])
871 elif words[0] == 'rules':
872 return autosort_complete_rules(words[1:], completion)
873 elif words[0] == 'helpers':
874 return autosort_complete_helpers(words[1:], completion)
875 return weechat.WEECHAT_RC_OK
876
877 command_description = r'''{*white}# General commands{reset}
878
879 {*white}/autosort {brown}sort{reset}
880 Manually trigger the buffer sorting.
881
882 {*white}/autosort {brown}debug{reset}
883 Show the evaluation results of the sort rules for each buffer.
884
885
886 {*white}# Sorting rule commands{reset}
887
888 {*white}/autosort{brown} rules list{reset}
889 Print the list of sort rules.
890
891 {*white}/autosort {brown}rules add {cyan}<expression>{reset}
892 Add a new rule at the end of the list.
893
894 {*white}/autosort {brown}rules insert {cyan}<index> <expression>{reset}
895 Insert a new rule at the given index in the list.
896
897 {*white}/autosort {brown}rules update {cyan}<index> <expression>{reset}
898 Update a rule in the list with a new expression.
899
900 {*white}/autosort {brown}rules delete {cyan}<index>
901 Delete a rule from the list.
902
903 {*white}/autosort {brown}rules move {cyan}<index_from> <index_to>{reset}
904 Move a rule from one position in the list to another.
905
906 {*white}/autosort {brown}rules swap {cyan}<index_a> <index_b>{reset}
907 Swap two rules in the list
908
909
910 {*white}# Helper variable commands{reset}
911
912 {*white}/autosort {brown}helpers list
913 Print the list of helper variables.
914
915 {*white}/autosort {brown}helpers set {cyan}<name> <expression>
916 Add or update a helper variable with the given name.
917
918 {*white}/autosort {brown}helpers delete {cyan}<name>
919 Delete a helper variable.
920
921 {*white}/autosort {brown}helpers rename {cyan}<old_name> <new_name>
922 Rename a helper variable.
923
924 {*white}/autosort {brown}helpers swap {cyan}<name_a> <name_b>
925 Swap the expressions of two helper variables in the list.
926
927
928 {*white}# Info hooks{reset}
929 Autosort comes with a number of info hooks to add some extra functionality to regular weechat eval strings.
930 Info hooks can be used in eval strings in the form of {cyan}${{info:some_hook,arguments}}{reset}.
931
932 Commas and backslashes in arguments to autosort info hooks (except for {cyan}${{info:autosort_escape}}{reset}) must be escaped with a backslash.
933
934 {*white}${{info:{brown}autosort_replace{white},{cyan}pattern{white},{cyan}replacement{white},{cyan}source{white}}}{reset}
935 Replace all occurrences of {cyan}pattern{reset} with {cyan}replacement{reset} in the string {cyan}source{reset}.
936 Can be used to ignore certain strings when sorting by replacing them with an empty string.
937
938 For example: {cyan}${{info:autosort_replace,cat,dog,the dog is meowing}}{reset} expands to "the cat is meowing".
939
940 {*white}${{info:{brown}autosort_order{white},{cyan}value{white},{cyan}option0{white},{cyan}option1{white},{cyan}option2{white},{cyan}...{white}}}
941 Generate a zero-padded number that corresponds to the index of {cyan}value{reset} in the list of options.
942 If one of the options is the special value {brown}*{reset}, then any value not explicitly mentioned will be sorted at that position.
943 Otherwise, any value that does not match an option is assigned the highest number available.
944 Can be used to easily sort buffers based on a manual sequence.
945
946 For example: {cyan}${{info:autosort_order,${{server}},freenode,oftc,efnet}}{reset} will sort freenode before oftc, followed by efnet and then any remaining servers.
947 Alternatively, {cyan}${{info:autosort_order,${{server}},freenode,oftc,*,efnet}}{reset} will sort any unlisted servers after freenode and oftc, but before efnet.
948
949 {*white}${{info:{brown}autosort_escape{white},{cyan}text{white}}}{reset}
950 Escape commas and backslashes in {cyan}text{reset} by prepending them with a backslash.
951 This is mainly useful to pass arbitrary eval strings as arguments to other autosort info hooks.
952 Otherwise, an eval string that expands to something with a comma would be interpreted as multiple arguments.
953
954 For example, it can be used to safely pass buffer names to {cyan}${{info:autosort_replace}}{reset} like so:
955 {cyan}${{info:autosort_replace,##,#,${{info:autosort_escape,${{buffer.name}}}}}}{reset}.
956
957
958 {*white}# Description
959 Autosort is a weechat script to automatically keep your buffers sorted. The sort
960 order can be customized by defining your own sort rules, but the default should
961 be sane enough for most people. It can also group IRC channel/private buffers
962 under their server buffer if you like.
963
964 {*white}# Sort rules{reset}
965 Autosort evaluates a list of eval expressions (see {*default}/help eval{reset}) and sorts the
966 buffers based on evaluated result. Earlier rules will be considered first. Only
967 if earlier rules produced identical results is the result of the next rule
968 considered for sorting purposes.
969
970 You can debug your sort rules with the `{*default}/autosort debug{reset}` command, which will
971 print the evaluation results of each rule for each buffer.
972
973 {*brown}NOTE:{reset} The sort rules for version 3 are not compatible with version 2 or vice
974 versa. You will have to manually port your old rules to version 3 if you have any.
975
976 {*white}# Helper variables{reset}
977 You may define helper variables for the main sort rules to keep your rules
978 readable. They can be used in the main sort rules as variables. For example,
979 a helper variable named `{cyan}foo{reset}` can be accessed in a main rule with the
980 string `{cyan}${{foo}}{reset}`.
981
982 {*white}# Automatic or manual sorting{reset}
983 By default, autosort will automatically sort your buffer list whenever a buffer
984 is opened, merged, unmerged or renamed. This should keep your buffers sorted in
985 almost all situations. However, you may wish to change the list of signals that
986 cause your buffer list to be sorted. Simply edit the `{cyan}autosort.sorting.signals{reset}`
987 option to add or remove any signal you like.
988
989 If you remove all signals you can still sort your buffers manually with the
990 `{*default}/autosort sort{reset}` command. To prevent all automatic sorting, the option
991 `{cyan}autosort.sorting.sort_on_config_change{reset}` should also be disabled.
992
993 {*white}# Recommended settings
994 For the best visual effect, consider setting the following options:
995 {*white}/set {cyan}irc.look.server_buffer{reset} {brown}independent{reset}
996 {*white}/set {cyan}buffers.look.indenting{reset} {brown}on{reset}
997
998 The first setting allows server buffers to be sorted independently, which is
999 needed to create a hierarchical tree view of the server and channel buffers.
1000 The second one indents channel and private buffers in the buffer list of the
1001 `{*default}buffers.pl{reset}` script.
1002
1003 If you are using the {*default}buflist{reset} plugin you can (ab)use Unicode to draw a tree
1004 structure with the following setting (modify to suit your need):
1005 {*white}/set {cyan}buflist.format.indent {brown}"${{color:237}}${{if:${{buffer.next_buffer.local_variables.type}}=~^(channel|private)$?├─:└─}}"{reset}
1006 '''
1007
1008 command_completion = '%(plugin_autosort) %(plugin_autosort) %(plugin_autosort) %(plugin_autosort) %(plugin_autosort)'
1009
1010 info_replace_description = (
1011 'Replace all occurrences of `pattern` with `replacement` in the string `source`. '
1012 'Can be used to ignore certain strings when sorting by replacing them with an empty string. '
1013 'See /help autosort for examples.'
1014 )
1015 info_replace_arguments = 'pattern,replacement,source'
1016
1017 info_order_description = (
1018 'Generate a zero-padded number that corresponds to the index of `value` in the list of options. '
1019 'If one of the options is the special value `*`, then any value not explicitly mentioned will be sorted at that position. '
1020 'Otherwise, any value that does not match an option is assigned the highest number available. '
1021 'Can be used to easily sort buffers based on a manual sequence. '
1022 'See /help autosort for examples.'
1023 )
1024 info_order_arguments = 'value,first,second,third,...'
1025
1026 info_escape_description = (
1027 'Escape commas and backslashes in `text` by prepending them with a backslash. '
1028 'This is mainly useful to pass arbitrary eval strings as arguments to other autosort info hooks. '
1029 'Otherwise, an eval string that expands to something with a comma would be interpreted as multiple arguments.'
1030 'See /help autosort for examples.'
1031 )
1032 info_escape_arguments = 'text'
1033
1034
1035 if weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, "", ""):
1036 config = Config('autosort')
1037
1038 colors = {
1039 'default': weechat.color('default'),
1040 'reset': weechat.color('reset'),
1041 'black': weechat.color('black'),
1042 'red': weechat.color('red'),
1043 'green': weechat.color('green'),
1044 'brown': weechat.color('brown'),
1045 'yellow': weechat.color('yellow'),
1046 'blue': weechat.color('blue'),
1047 'magenta': weechat.color('magenta'),
1048 'cyan': weechat.color('cyan'),
1049 'white': weechat.color('white'),
1050 '*default': weechat.color('*default'),
1051 '*black': weechat.color('*black'),
1052 '*red': weechat.color('*red'),
1053 '*green': weechat.color('*green'),
1054 '*brown': weechat.color('*brown'),
1055 '*yellow': weechat.color('*yellow'),
1056 '*blue': weechat.color('*blue'),
1057 '*magenta': weechat.color('*magenta'),
1058 '*cyan': weechat.color('*cyan'),
1059 '*white': weechat.color('*white'),
1060 }
1061
1062 weechat.hook_config('autosort.*', 'on_config_changed', '')
1063 weechat.hook_completion('plugin_autosort', '', 'on_autosort_complete', '')
1064 weechat.hook_command('autosort', command_description.format(**colors), '', '', command_completion, 'on_autosort_command', '')
1065 weechat.hook_info('autosort_escape', info_escape_description, info_escape_arguments, 'on_info_escape', '')
1066 weechat.hook_info('autosort_replace', info_replace_description, info_replace_arguments, 'on_info_replace', '')
1067 weechat.hook_info('autosort_order', info_order_description, info_order_arguments, 'on_info_order', '')
1068
1069 apply_config()