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