weechat

- me personal weechat setup 🔵🟢
git clone git://git.acid.vegas/weechat.git
Log | Files | Refs | Archive | README

autosort.py (38070B)

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