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