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