--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/weechat/python/autoload/autosort.py Thu May 14 11:06:43 2015 -0400
@@ -0,0 +1,860 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2013-2014 Maarten de Vries <maarten@de-vri.es>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+
+#
+# Autosort automatically keeps your buffers sorted and grouped by server.
+# You can define your own sorting rules. See /help autosort for more details.
+#
+# http://github.com/de-vri.es/weechat-autosort
+#
+
+#
+# Changelog:
+# 2.5:
+# * Fix handling unicode buffer names.
+# * Add hint to set irc.look.server_buffer to independent and buffers.look.indenting to on.
+# 2.4:
+# * Make script python3 compatible.
+# 2.3:
+# * Fix sorting items without score last (regressed in 2.2).
+# 2.2:
+# * Add configuration option for signals that trigger a sort.
+# * Add command to manually trigger a sort (/autosort sort).
+# * Add replacement patterns to apply before sorting.
+# 2.1:
+# * Fix some minor style issues.
+# 2.0:
+# * Allow for custom sort rules.
+#
+
+
+import weechat
+import re
+import json
+
+SCRIPT_NAME = 'autosort'
+SCRIPT_AUTHOR = 'Maarten de Vries <maarten@de-vri.es>'
+SCRIPT_VERSION = '2.5'
+SCRIPT_LICENSE = 'GPL3'
+SCRIPT_DESC = 'Automatically or manually keep your buffers sorted and grouped by server.'
+
+
+config = None
+hooks = []
+
+class HumanReadableError(Exception):
+ pass
+
+
+def parse_int(arg, arg_name = 'argument'):
+ ''' Parse an integer and provide a more human readable error. '''
+ arg = arg.strip()
+ try:
+ return int(arg)
+ except ValueError:
+ raise HumanReadableError('Invalid {0}: expected integer, got "{1}".'.format(arg_name, arg))
+
+
+class Pattern:
+ ''' A simple glob-like pattern for matching buffer names. '''
+
+ def __init__(self, pattern):
+ ''' Construct a pattern from a string. '''
+ escaped = False
+ char_class = 0
+ chars = ''
+ regex = ''
+ for c in pattern:
+ if escaped and char_class:
+ escaped = False
+ chars += re.escape(c)
+ elif escaped:
+ escaped = False
+ regex += re.escape(c)
+ elif c == '\\':
+ escaped = True
+ elif c == '*' and not char_class:
+ regex += '[^.]*'
+ elif c == '?' and not char_class:
+ regex += '[^.]'
+ elif c == '[' and not char_class:
+ char_class = 1
+ chars = ''
+ elif c == '^' and char_class and not chars:
+ chars += '^'
+ elif c == ']' and char_class and chars not in ('', '^'):
+ char_class = False
+ regex += '[' + chars + ']'
+ elif c == '-' and char_class:
+ chars += '-'
+ elif char_class:
+ chars += re.escape(c)
+ else:
+ regex += re.escape(c)
+
+ if char_class:
+ raise ValueError("unmatched opening '['")
+ if escaped:
+ raise ValueError("unexpected trailing '\\'")
+
+ self.regex = re.compile('^' + regex + '$')
+ self.pattern = pattern
+
+ def match(self, input):
+ ''' Match the pattern against a string. '''
+ return self.regex.match(input)
+
+
+class FriendlyList(object):
+ ''' A list with human readable errors. '''
+
+ def __init__(self):
+ self.__data = []
+
+ def raw(self):
+ return self.__data
+
+ def append(self, value):
+ ''' Add a rule to the list. '''
+ self.__data.append(value)
+
+ def insert(self, index, value):
+ ''' Add a rule to the list. '''
+ if not 0 <= index <= len(self): raise HumanReadableError('Index out of range: expected an integer in the range [0, {0}], got {1}.'.format(len(self), index))
+ self.__data.insert(index, value)
+
+ def pop(self, index):
+ ''' Remove a rule from the list and return it. '''
+ if not 0 <= index < len(self): raise HumanReadableError('Index out of range: expected an integer in the range [0, {0}), got {1}.'.format(len(self), index))
+ return self.__data.pop(index)
+
+ def move(self, index_a, index_b):
+ ''' Move a rule to a new position in the list. '''
+ self.insert(index_b, self.pop(index_a))
+
+ def swap(self, index_a, index_b):
+ ''' Swap two elements in the list. '''
+ self[index_a], self[index_b] = self[index_b], self[index_a]
+
+ def __len__(self):
+ return len(self.__data)
+
+ def __getitem__(self, index):
+ if not 0 <= index < len(self): raise HumanReadableError('Index out of range: expected an integer in the range [0, {0}), got {1}.'.format(len(self), index))
+ return self.__data[index]
+
+ def __setitem__(self, index, value):
+ if not 0 <= index < len(self): raise HumanReadableError('Index out of range: expected an integer in the range [0, {0}), got {1}.'.format(len(self), index))
+ self.__data[index] = value
+
+ def __iter__(self):
+ return iter(self.__data)
+
+
+class RuleList(FriendlyList):
+ ''' A list of rules to test buffer names against. '''
+ rule_regex = re.compile(r'^(.*)=\s*([+-]?[^=]*)$')
+
+ def __init__(self, rules):
+ ''' Construct a RuleList from a list of rules. '''
+ super(RuleList, self).__init__()
+ for rule in rules: self.append(rule)
+
+ def get_score(self, name, rules):
+ ''' Get the sort score of a partial name according to a rule list. '''
+ for rule in self:
+ if rule[0].match(name): return rule[1]
+ return 999999999
+
+ def encode(self):
+ ''' Encode the rules for storage. '''
+ return json.dumps(list(map(lambda x: (x[0].pattern, x[1]), self)))
+
+ @staticmethod
+ def decode(blob):
+ ''' Parse rules from a string blob. '''
+ result = []
+
+ try:
+ decoded = json.loads(blob)
+ except ValueError:
+ log('Invalid rules: expected JSON encoded list of pairs, got "{0}".'.format(blob))
+ return [], 0
+
+ for rule in decoded:
+ # Rules must be a pattern,score pair.
+ if len(rule) != 2:
+ log('Invalid rule: expected (pattern, score), got "{0}". Rule ignored.'.format(rule))
+ continue
+
+ # Rules must have a valid pattern.
+ try:
+ pattern = Pattern(rule[0])
+ except ValueError as e:
+ log('Invalid pattern: {0} in "{1}". Rule ignored.'.format(e, rule[0]))
+ continue
+
+ # Rules must have a valid score.
+ try:
+ score = int(rule[1])
+ except ValueError as e:
+ log('Invalid score: expected an integer, got "{0}". Rule ignored.'.format(score))
+ continue
+
+ result.append((pattern, score))
+
+ return RuleList(result)
+
+ @staticmethod
+ def parse_rule(arg):
+ ''' Parse a rule argument. '''
+ arg = arg.strip()
+ match = RuleList.rule_regex.match(arg)
+ if not match:
+ raise HumanReadableError('Invalid rule: expected "<pattern> = <score>", got "{0}".'.format(arg))
+
+ pattern = match.group(1).strip()
+ try:
+ pattern = Pattern(pattern)
+ except ValueError as e:
+ raise HumanReadableError('Invalid pattern: {0} in "{1}".'.format(e, pattern))
+
+ score = parse_int(match.group(2), 'score')
+ return (pattern, score)
+
+
+def decode_replacements(blob):
+ ''' Decode a replacement list encoded as JSON. '''
+ result = FriendlyList()
+ try:
+ decoded = json.loads(blob)
+ except ValueError:
+ log('Invalid replacement list: expected JSON encoded list of pairs, got "{0}".'.format(blob))
+ return [], 0
+
+ for replacement in decoded:
+ # Replacements must be a (string, string) pair.
+ if len(replacement) != 2:
+ log('Invalid replacement pattern: expected (pattern, replacement), got "{0}". Replacement ignored.'.format(rule))
+ continue
+ result.append(replacement)
+
+ return result
+
+
+def encode_replacements(replacements):
+ ''' Encode a list of replacement patterns as JSON. '''
+ return json.dumps(replacements.raw())
+
+
+class Config:
+ ''' The autosort configuration. '''
+
+ default_rules = json.dumps([
+ ('core', 0),
+ ('irc', 2),
+ ('*', 1),
+
+ ('irc.irc_raw', 0),
+ ('irc.server', 1),
+ ])
+
+ default_replacements = '[]'
+ default_signals = 'buffer_opened buffer_merged buffer_unmerged buffer_renamed'
+
+ def __init__(self, filename):
+ ''' Initialize the configuration. '''
+
+ self.filename = filename
+ self.config_file = weechat.config_new(self.filename, '', '')
+ self.sorting_section = None
+
+ self.case_sensitive = False
+ self.group_irc = True
+ self.rules = []
+ self.replacements = []
+ self.signals = []
+ self.sort_on_config = True
+
+ self.__case_sensitive = None
+ self.__group_irc = None
+ self.__rules = None
+ self.__replacements = None
+ self.__signals = None
+ self.__sort_on_config = None
+
+ if not self.config_file:
+ log('Failed to initialize configuration file "{0}".'.format(self.filename))
+ return
+
+ self.sorting_section = weechat.config_new_section(self.config_file, 'sorting', False, False, '', '', '', '', '', '', '', '', '', '')
+
+ if not self.sorting_section:
+ log('Failed to initialize section "sorting" of configuration file.')
+ weechat.config_free(self.config_file)
+ return
+
+ self.__case_sensitive = weechat.config_new_option(
+ self.config_file, self.sorting_section,
+ 'case_sensitive', 'boolean',
+ 'If this option is on, sorting is case sensitive.',
+ '', 0, 0, 'off', 'off', 0,
+ '', '', '', '', '', ''
+ )
+
+ self.__group_irc = weechat.config_new_option(
+ self.config_file, self.sorting_section,
+ 'group_irc', 'boolean',
+ 'If this option is on, the script pretends that IRC channel/private buffers are renamed to "irc.server.{network}.{channel}" rather than "irc.{network}.{channel}".' +
+ 'This ensures that these buffers are grouped with their respective server buffer.',
+ '', 0, 0, 'on', 'on', 0,
+ '', '', '', '', '', ''
+ )
+
+ self.__rules = weechat.config_new_option(
+ self.config_file, self.sorting_section,
+ 'rules', 'string',
+ 'An ordered list of sorting rules encoded as JSON. See /help autosort for commands to manipulate these rules.',
+ '', 0, 0, Config.default_rules, Config.default_rules, 0,
+ '', '', '', '', '', ''
+ )
+
+ self.__replacements = weechat.config_new_option(
+ self.config_file, self.sorting_section,
+ 'replacements', 'string',
+ 'An ordered list of replacement patterns to use on buffer name components, encoded as JSON. See /help autosort for commands to manipulate these replacements.',
+ '', 0, 0, Config.default_replacements, Config.default_replacements, 0,
+ '', '', '', '', '', ''
+ )
+
+ self.__signals = weechat.config_new_option(
+ self.config_file, self.sorting_section,
+ 'signals', 'string',
+ 'The signals that will cause autosort to resort your buffer list. Seperate signals with spaces.',
+ '', 0, 0, Config.default_signals, Config.default_signals, 0,
+ '', '', '', '', '', ''
+ )
+
+ self.__sort_on_config = weechat.config_new_option(
+ self.config_file, self.sorting_section,
+ 'sort_on_config_change', 'boolean',
+ 'Decides if the buffer list should be sorted when autosort configuration changes.',
+ '', 0, 0, 'on', 'on', 0,
+ '', '', '', '', '', ''
+ )
+
+ if weechat.config_read(self.config_file) != weechat.WEECHAT_RC_OK:
+ log('Failed to load configuration file.')
+
+ if weechat.config_write(self.config_file) != weechat.WEECHAT_RC_OK:
+ log('Failed to write configuration file.')
+
+ self.reload()
+
+ def reload(self):
+ ''' Load configuration variables. '''
+
+ self.case_sensitive = weechat.config_boolean(self.__case_sensitive)
+ self.group_irc = weechat.config_boolean(self.__group_irc)
+
+ rules_blob = weechat.config_string(self.__rules)
+ replacements_blob = weechat.config_string(self.__replacements)
+ signals_blob = weechat.config_string(self.__signals)
+
+ self.rules = RuleList.decode(rules_blob)
+ self.replacements = decode_replacements(replacements_blob)
+ self.signals = signals_blob.split()
+ self.sort_on_config = weechat.config_boolean(self.__sort_on_config)
+
+ def save_rules(self, run_callback = True):
+ ''' Save the current rules to the configuration. '''
+ weechat.config_option_set(self.__rules, RuleList.encode(self.rules), run_callback)
+
+ def save_replacements(self, run_callback = True):
+ ''' Save the current replacement patterns to the configuration. '''
+ weechat.config_option_set(self.__replacements, encode_replacements(self.replacements), run_callback)
+
+
+def pad(sequence, length, padding = None):
+ ''' Pad a list until is has a certain length. '''
+ return sequence + [padding] * max(0, (length - len(sequence)))
+
+
+def log(message, buffer = 'NULL'):
+ weechat.prnt(buffer, 'autosort: {0}'.format(message))
+
+
+def get_buffers():
+ ''' Get a list of all the buffers in weechat. '''
+ buffers = []
+
+ buffer_list = weechat.infolist_get('buffer', '', '')
+
+ while weechat.infolist_next(buffer_list):
+ name = weechat.infolist_string (buffer_list, 'full_name')
+ number = weechat.infolist_integer(buffer_list, 'number')
+
+ # Buffer is merged with one we already have in the list, skip it.
+ if number <= len(buffers):
+ continue
+ buffers.append(name)
+
+ weechat.infolist_free(buffer_list)
+ return buffers
+
+
+def preprocess(buffer, config):
+ '''
+ Preprocess a buffers names.
+ '''
+ if not config.case_sensitive:
+ buffer = buffer.lower()
+
+ for replacement in config.replacements:
+ buffer = buffer.replace(replacement[0], replacement[1])
+
+ buffer = buffer.split('.')
+ if config.group_irc and len(buffer) >= 2 and buffer[0] == 'irc' and buffer[1] not in ('server', 'irc_raw'):
+ buffer.insert(1, 'server')
+
+ return buffer
+
+
+def buffer_sort_key(rules):
+ ''' Create a sort key function for a buffer list from a rule list. '''
+ def key(buffer):
+ result = []
+ name = ''
+ for word in preprocess(buffer.decode('utf-8'), config):
+ name += ('.' if name else '') + word
+ result.append((rules.get_score(name, rules), word))
+ return result
+
+ return key
+
+
+def apply_buffer_order(buffers):
+ ''' Sort the buffers in weechat according to the order in the input list. '''
+ for i, buffer in enumerate(buffers):
+ weechat.command('', '/buffer swap {0} {1}'.format(buffer, i + 1))
+
+
+def split_args(args, expected, optional = 0):
+ ''' Split an argument string in the desired number of arguments. '''
+ split = args.split(' ', expected - 1)
+ if (len(split) < expected):
+ raise HumanReadableError('Expected at least {0} arguments, got {1}.'.format(expected, len(split)))
+ return split[:-1] + pad(split[-1].split(' ', optional), optional + 1, '')
+
+
+def command_sort(buffer, command, args):
+ ''' Sort the buffers and print a confirmation. '''
+ on_buffers_changed()
+ log("Finished sorting buffers.", buffer)
+ return weechat.WEECHAT_RC_OK
+
+
+def command_rule_list(buffer, command, args):
+ ''' Show the list of sorting rules. '''
+ output = 'Sorting rules:\n'
+ for i, rule in enumerate(config.rules):
+ output += ' {0}: {1} = {2}\n'.format(i, rule[0].pattern, rule[1])
+ if not len(config.rules):
+ output += ' No sorting rules configured.\n'
+ log(output, buffer)
+
+ return weechat.WEECHAT_RC_OK
+
+
+def command_rule_add(buffer, command, args):
+ ''' Add a rule to the rule list. '''
+ rule = RuleList.parse_rule(args)
+
+ config.rules.append(rule)
+ config.save_rules()
+ command_rule_list(buffer, command, '')
+
+ return weechat.WEECHAT_RC_OK
+
+
+def command_rule_insert(buffer, command, args):
+ ''' Insert a rule at the desired position in the rule list. '''
+ index, rule = split_args(args, 2)
+ index = parse_int(index, 'index')
+ rule = RuleList.parse_rule(rule)
+
+ config.rules.insert(index, rule)
+ config.save_rules()
+ command_rule_list(buffer, command, '')
+ return weechat.WEECHAT_RC_OK
+
+
+def command_rule_update(buffer, command, args):
+ ''' Update a rule in the rule list. '''
+ index, rule = split_args(args, 2)
+ index = parse_int(index, 'index')
+ rule = RuleList.parse_rule(rule)
+
+ config.rules[index] = rule
+ config.save_rules()
+ command_rule_list(buffer, command, '')
+ return weechat.WEECHAT_RC_OK
+
+
+def command_rule_delete(buffer, command, args):
+ ''' Delete a rule from the rule list. '''
+ index = args.strip()
+ index = parse_int(index, 'index')
+
+ config.rules.pop(index)
+ config.save_rules()
+ command_rule_list(buffer, command, '')
+ return weechat.WEECHAT_RC_OK
+
+
+def command_rule_move(buffer, command, args):
+ ''' Move a rule to a new position. '''
+ index_a, index_b = split_args(args, 2)
+ index_a = parse_int(index_a, 'index')
+ index_b = parse_int(index_b, 'index')
+
+ config.rules.move(index_a, index_b)
+ config.save_rules()
+ command_rule_list(buffer, command, '')
+ return weechat.WEECHAT_RC_OK
+
+
+def command_rule_swap(buffer, command, args):
+ ''' Swap two rules. '''
+ index_a, index_b = split_args(args, 2)
+ index_a = parse_int(index_a, 'index')
+ index_b = parse_int(index_b, 'index')
+
+ config.rules.swap(index_a, index_b)
+ config.save_rules()
+ command_rule_list(buffer, command, '')
+ return weechat.WEECHAT_RC_OK
+
+
+def command_replacement_list(buffer, command, args):
+ ''' Show the list of sorting rules. '''
+ output = 'Replacement patterns:\n'
+ for i, pattern in enumerate(config.replacements):
+ output += ' {0}: {1} -> {2}\n'.format(i, pattern[0], pattern[1])
+ if not len(config.replacements):
+ output += ' No replacement patterns configured.'
+ log(output, buffer)
+
+ return weechat.WEECHAT_RC_OK
+
+
+def command_replacement_add(buffer, command, args):
+ ''' Add a rule to the rule list. '''
+ pattern, replacement = split_args(args, 1, 1)
+
+ config.replacements.append((pattern, replacement))
+ config.save_replacements()
+ command_replacement_list(buffer, command, '')
+
+ return weechat.WEECHAT_RC_OK
+
+
+def command_replacement_insert(buffer, command, args):
+ ''' Insert a rule at the desired position in the rule list. '''
+ index, pattern, replacement = split_args(args, 2, 1)
+ index = parse_int(index, 'index')
+
+ config.replacements.insert(index, (pattern, replacement))
+ config.save_replacements()
+ command_replacement_list(buffer, command, '')
+ return weechat.WEECHAT_RC_OK
+
+
+def command_replacement_update(buffer, command, args):
+ ''' Update a rule in the rule list. '''
+ index, pattern, replacement = split_args(args, 2, 1)
+ index = parse_int(index, 'index')
+
+ config.replacements[index] = (pattern, replacement)
+ config.save_replacements()
+ command_replacement_list(buffer, command, '')
+ return weechat.WEECHAT_RC_OK
+
+
+def command_replacement_delete(buffer, command, args):
+ ''' Delete a rule from the rule list. '''
+ index = args.strip()
+ index = parse_int(index, 'index')
+
+ config.replacements.pop(index)
+ config.save_replacements()
+ command_replacement_list(buffer, command, '')
+ return weechat.WEECHAT_RC_OK
+
+
+def command_replacement_move(buffer, command, args):
+ ''' Move a rule to a new position. '''
+ index_a, index_b = split_args(args, 2)
+ index_a = parse_int(index_a, 'index')
+ index_b = parse_int(index_b, 'index')
+
+ config.replacements.move(index_a, index_b)
+ config.save_replacements()
+ command_replacement_list(buffer, command, '')
+ return weechat.WEECHAT_RC_OK
+
+
+def command_replacement_swap(buffer, command, args):
+ ''' Swap two rules. '''
+ index_a, index_b = split_args(args, 2)
+ index_a = parse_int(index_a, 'index')
+ index_b = parse_int(index_b, 'index')
+
+ config.replacements.swap(index_a, index_b)
+ config.save_replacements()
+ command_replacement_list(buffer, command, '')
+ return weechat.WEECHAT_RC_OK
+
+
+
+
+def call_command(buffer, command, args, subcommands):
+ ''' Call a subccommand from a dictionary. '''
+ subcommand, tail = pad(args.split(' ', 1), 2, '')
+ subcommand = subcommand.strip()
+ if (subcommand == ''):
+ child = subcommands.get(' ')
+ else:
+ command = command + [subcommand]
+ child = subcommands.get(subcommand)
+
+ if isinstance(child, dict):
+ return call_command(buffer, command, tail, child)
+ elif callable(child):
+ return child(buffer, command, tail)
+
+ log('{0}: command not found'.format(' '.join(command)))
+ return weechat.WEECHAT_RC_ERROR
+
+
+def on_buffers_changed(*args, **kwargs):
+ ''' Called whenever the buffer list changes. '''
+ buffers = get_buffers()
+ buffers.sort(key=buffer_sort_key(config.rules))
+ apply_buffer_order(buffers)
+ return weechat.WEECHAT_RC_OK
+
+
+def on_config_changed(*args, **kwargs):
+ ''' Called whenever the configuration changes. '''
+ config.reload()
+
+ # Unhook all signals and hook the new ones.
+ for hook in hooks:
+ weechat.unhook(hook)
+ for signal in config.signals:
+ hooks.append(weechat.hook_signal(signal, 'on_buffers_changed', ''))
+
+ if config.sort_on_config:
+ on_buffers_changed()
+
+ return weechat.WEECHAT_RC_OK
+
+
+def on_autosort_command(data, buffer, args):
+ ''' Called when the autosort command is invoked. '''
+ try:
+ return call_command(buffer, ['/autosort'], args, {
+ ' ': command_sort,
+ 'sort': command_sort,
+
+ 'rules': {
+ ' ': command_rule_list,
+ 'list': command_rule_list,
+ 'add': command_rule_add,
+ 'insert': command_rule_insert,
+ 'update': command_rule_update,
+ 'delete': command_rule_delete,
+ 'move': command_rule_move,
+ 'swap': command_rule_swap,
+ },
+ 'replacements': {
+ ' ': command_replacement_list,
+ 'list': command_replacement_list,
+ 'add': command_replacement_add,
+ 'insert': command_replacement_insert,
+ 'update': command_replacement_update,
+ 'delete': command_replacement_delete,
+ 'move': command_replacement_move,
+ 'swap': command_replacement_swap,
+ },
+ 'sort': on_buffers_changed,
+ })
+ except HumanReadableError as e:
+ log(e, buffer)
+ return weechat.WEECHAT_RC_ERROR
+
+
+command_description = r'''
+NOTE: For the best effect, you may want to consider setting the option irc.look.server_buffer to independent and buffers.look.indenting to on.
+
+# Commands
+
+## Miscellaneous
+/autosort sort
+Manually trigger the buffer sorting.
+
+
+## Sorting rules
+
+/autosort rules list
+Print the list of sort rules.
+
+/autosort rules add <pattern> = <score>
+Add a new rule at the end of the list.
+
+/autosort rules insert <index> <pattern> = <score>
+Insert a new rule at the given index in the list.
+
+/autosort rules update <index> <pattern> = <score>
+Update a rule in the list with a new pattern and score.
+
+/autosort rules delete <index>
+Delete a rule from the list.
+
+/autosort rules move <index_from> <index_to>
+Move a rule from one position in the list to another.
+
+/autosort rules swap <index_a> <index_b>
+Swap two rules in the list
+
+
+## Replacement patterns
+
+/autosort replacements list
+Print the list of replacement patterns.
+
+/autosort replacements add <pattern> <replacement>
+Add a new replacement pattern at the end of the list.
+
+/autosort replacements insert <index> <pattern> <replacement>
+Insert a new replacement pattern at the given index in the list.
+
+/autosort replacements update <index> <pattern> <replacement>
+Update a replacement pattern in the list.
+
+/autosort replacements delete <index>
+Delete a replacement pattern from the list.
+
+/autosort replacements move <index_from> <index_to>
+Move a replacement pattern from one position in the list to another.
+
+/autosort replacements swap <index_a> <index_b>
+Swap two replacement pattern in the list
+
+
+# Introduction
+Autosort is a weechat script to automatically keep your buffers sorted.
+The sort order can be customized by defining your own sort rules,
+but the default should be sane enough for most people.
+It can also group IRC channel/private buffers under their server buffer if you like.
+
+Autosort first turns buffer names into a list of their components by splitting on them on the period character.
+For example, the buffer name "irc.server.freenode" is turned into ['irc', 'server', 'freenode'].
+The list of buffers is then lexicographically sorted.
+
+To facilitate custom sort orders, it is possible to assign a score to each component individually before the sorting is done.
+Any name component that did not get a score assigned will be sorted after those that did receive a score.
+Components are always sorted on their score first and on their name second.
+Lower scores are sorted first.
+
+## Automatic or manual sorting
+By default, autosort will automatically sort your buffer list whenever a buffer is opened, merged, unmerged or renamed.
+This should keep your buffers sorted in almost all situations.
+However, you may wish to change the list of signals that cause your buffer list to be sorted.
+Simply edit the "autosort.sorting.signals" option to add or remove any signal you like.
+If you remove all signals you can still sort your buffers manually with the "/autosort sort" command.
+To prevent all automatic sorting, "autosort.sorting.sort_on_config_change" should also be set to off.
+
+## Grouping IRC buffers
+In weechat, IRC channel/private buffers are named "irc.<network>.<#channel>",
+and IRC server buffers are named "irc.server.<network>".
+This does not work very well with lexicographical sorting if you want all buffers for one network grouped together.
+That is why autosort comes with the "autosort.sorting.group_irc" option,
+which secretly pretends IRC channel/private buffers are called "irc.server.<network>.<#channel>".
+The buffers are not actually renamed, autosort simply pretends they are for sorting purposes.
+
+## Replacement patterns
+Sometimes you may want to ignore some characters for sorting purposes.
+On Freenode for example, you may wish to ignore the difference between channels starting with a double or a single hash sign.
+To do so, simply add a replacement pattern that replaces ## with # with the following command:
+/autosort replacements add ## #
+
+Replacement patterns do not support wildcards or special characters at the moment.
+
+## Sort rules
+You can assign scores to name components by defining sort rules.
+The first rule that matches a component decides the score.
+Further rules are not examined.
+Sort rules use the following syntax:
+<glob-pattern> = <score>
+
+You can use the "/autosort rules" command to show and manipulate the list of sort rules.
+
+
+Allowed special characters in the glob patterns are:
+
+Pattern | Meaning
+--------|--------
+* | Matches a sequence of any characters except for periods.
+? | Matches a single character, but not a period.
+[a-z] | Matches a single character in the given regex-like character class.
+[^ab] | A negated regex-like character class.
+\* | A backslash escapes the next characters and removes its special meaning.
+\\ | A literal backslash.
+
+
+## Example
+As an example, consider the following rule list:
+0: core = 0
+1: irc = 2
+2: * = 1
+
+3: irc.server.*.#* = 1
+4: irc.server.*.* = 0
+
+Rule 0 ensures the core buffer is always sorted first.
+Rule 1 sorts IRC buffers last and rule 2 puts all remaining buffers in between the two.
+
+Rule 3 and 4 would make no sense with the group_irc option off.
+With the option on though, these rules will sort private buffers before regular channel buffers.
+Rule 3 matches channel buffers and assigns them a higher score,
+while rule 4 matches the buffers that remain and assigns them a lower score.
+The same effect could also be achieved with a single rule:
+irc.server.*.[^#]* = 0
+'''
+
+command_completion = 'sort||rules list|add|insert|update|delete|move|swap||replacements list|add|insert|update|delete|move|swap'
+
+
+if weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, "", ""):
+ config = Config('autosort')
+
+ weechat.hook_config('autosort.*', 'on_config_changed', '')
+ weechat.hook_command('autosort', command_description, '', '', command_completion, 'on_autosort_command', 'NULL')
+ on_config_changed()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/weechat/python/listbuffer.py Thu May 14 11:06:43 2015 -0400
@@ -0,0 +1,467 @@
+# -*- coding: utf-8 -*-
+#
+# ListBuffer, version 0.8.1 for WeeChat version 0.3
+# Latest development version: https://github.com/FiXato/listbuffer
+#
+# Show /list results in a common buffer and interact with them.
+#
+# This script allows you to easily join channels from the /list output.
+# It will open a common buffer for the /list result, through which you
+# browse with your cursor keys, and join with the meta-enter keys.
+# Adjust sorting with meta->, meta-< and meta-/ keybindings.
+#
+## History:
+### 2011-09-08: FiXato:
+#
+# * version 0.1: initial release.
+# * added a common buffer for /list results
+# * added highlighting for currently selected line
+# * added /join support via enter key
+# * added scroll_top and scroll_bottom support
+#
+# * version 0.2: /list format bugfix
+# * added support for /list results without modes
+# * some servers don't send 321 (/list start). Taken into account.
+#
+# * version 0.3: Sorting support
+# * Added some basic sorting support. Scroll through sort options
+# with meta-> and meta-< (users, channel, topic, modes)
+#
+### 2011-09-19: FiXato
+#
+# * version 0.4:
+# * Case-insensitive buffer lookup fix.
+# * Removed default enter keybind
+#
+### 2011-12-28: troydm:
+#
+# * version 0.5: It's an upside-down-world
+# * Added inverted sorting support provided by Dmitry "troydm" Geurkov
+# Use meta-/ to switch between inverted and regular sorting.
+#
+### 2012-02-10: FiXato:
+#
+# * version 0.6: Stop shoving that buffer in my face!
+# * The listbuffer should no longer pop up by itself when you load the script.
+# It should only pop up now when you actually do a /list query.
+#
+# * version 0.7: .. but please pop it up in my current window when I ask for it
+# * Added setting plugins.var.python.listbuffer.autofocus
+# This will autofocus the listbuffer in the current window if another window isn't
+# already showing it, and of course only when the user issues /list
+#
+### 2012-07-10: FiXato:
+#
+# * version 0.7.1: Forgetful bugfix
+# * Made sure lb_curline global variable is defined
+#
+### 2013-03-19: FiXato:
+#
+# * version 0.8: Sorted out the sorting
+# * Added automatically updating options for sorting:
+# * plugins.var.python.listbuffer.sort_inverted
+# * plugins.var.python.listbuffer.sort_order
+# * version 0.8.1: Pad it baby!
+# * Channel modes are equally padded even when there are no channel modes.
+# * Added padding options:
+# * plugins.var.python.listbuffer.modes_min_width
+# * plugins.var.python.listbuffer.channel_min_width
+# * plugins.var.python.listbuffer.users_min_width
+#
+## Acknowledgements:
+# * Dmitry "troydm" Geurkov, for providing the inverse-sorting patch to the project.
+# * Sebastien "Flashcode" Helleu, for developing the kick-ass IRC client WeeChat
+# and the iset.pl script which inspired me to this script.
+# * Nils "nils_2" Görs, for his contributions to iset.pl which served as
+# example code.
+# * David "drubin" Rubin, for his urlgrab.py script, which also served
+# as example code.
+# * ArZa, whose listsort.pl script helped me getting started with
+# grabbing the /list results. Parts of his code have been shamelessly
+# copied and ported to Python.
+# * Khaled Mardam-Bey, for making me yearn for similar /list support in
+# WeeChat as mIRC already offered. :P
+# * mave_, for pointing out that sort orders weren't remembered.
+#
+## TODO:
+# - Auto-scroll selected line upon window scroll.
+# - Add option to hide already joined channels.
+# - Improve sorting methods
+# - Add auto-join support
+# - Detect if channel is already in auto-join
+# - Allow automatically switching to the listbuffer
+# - Add support for ALIS (/squery alis LIST * -mix 100 (IRCNet)
+# - Make colours configurable
+# - Limit number of channels to parse
+# - Add filter support a la iset
+# - Allow selecting multiple channels
+# - Add optional command redirection.
+#
+## Copyright (c) 2011,2012,2013 Filip H.F. "FiXato" Slagter,
+# <FiXato [at] Gmail [dot] com>
+# http://profile.fixato.org
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NON-INFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+SCRIPT_NAME = "listbuffer"
+SCRIPT_AUTHOR = "Filip H.F. 'FiXato' Slagter <fixato [at] gmail [dot] com>"
+SCRIPT_VERSION = "0.8.1"
+SCRIPT_LICENSE = "MIT"
+SCRIPT_DESC = "A common buffer for /list output."
+SCRIPT_COMMAND = "listbuffer"
+
+import_ok = True
+
+try:
+ import weechat
+except ImportError:
+ print "This script must be run under WeeChat."
+ import_ok = False
+
+import re
+
+lb_settings = (
+ ("autofocus", "on", "Focus the listbuffer in the current window if it isn't already displayed by a window."),
+ ("sort_order", "users", "Last used sort order for the channel list."),
+ ("sort_inverted", "on", "Invert the sort order for the channel list."),
+ ("modes_min_width", "8", "The minimum width used for modes in the channel list. If a channel has less modes than this amount, the column will be padded with spaces."),
+ ("channel_min_width", "25", "The minimum width used for the channel name in the channel list. If a channelname is shorter than this amount, the column will be padded with spaces."),
+ ("users_min_width", "8", "The minimum width used for the usercount in the channel list. If the usercount has less digits than this amount, the column will be padded with spaces."),
+)
+lb_buffer = None
+lb_curline = 0
+lb_channels = []
+lb_network = None
+lb_list_started = False
+lb_current_sort = None
+lb_sort_inverted = False
+lb_sort_options = (
+ 'channel',
+ 'users',
+ 'modes',
+ 'topic',
+)
+
+# server numeric Nick Chan Users Modes Topic
+lb_channel_list_expression = '(:\S+) (\d{3}) (\S+) (\S+) (\d+) :(\[(.*?)\] )?(.*)'
+
+# Create listbuffer.
+def lb_create_buffer():
+ global lb_buffer, lb_curline
+
+ if not lb_buffer:
+ lb_buffer = weechat.buffer_new("listbuffer", "lb_input_cb", \
+ "", "lb_close_cb", "")
+ lb_set_buffer_title()
+ # Sets notify to 0 as this buffer does not need to be in hotlist.
+ weechat.buffer_set(lb_buffer, "notify", "0")
+ weechat.buffer_set(lb_buffer, "nicklist", "0")
+ weechat.buffer_set(lb_buffer, "type", "free")
+ weechat.buffer_set(lb_buffer, "key_bind_ctrl-L", "/listbuffer **refresh")
+ weechat.buffer_set(lb_buffer, "key_bind_meta2-A", "/listbuffer **up")
+ weechat.buffer_set(lb_buffer, "key_bind_meta2-B", "/listbuffer **down")
+ weechat.buffer_set(lb_buffer, "key_bind_meta2-1~", "/listbuffer **scroll_top")
+ weechat.buffer_set(lb_buffer, "key_bind_meta2-4~", "/listbuffer **scroll_bottom")
+ weechat.buffer_set(lb_buffer, "key_bind_meta-ctrl-J", "/listbuffer **enter")
+ weechat.buffer_set(lb_buffer, "key_bind_meta-ctrl-M", "/listbuffer **enter")
+ weechat.buffer_set(lb_buffer, "key_bind_meta->", "/listbuffer **sort_next")
+ weechat.buffer_set(lb_buffer, "key_bind_meta-<", "/listbuffer **sort_previous")
+ weechat.buffer_set(lb_buffer, "key_bind_meta-/", "/listbuffer **sort_invert")
+ lb_curline = 0
+ if weechat.config_get_plugin("autofocus") == "on":
+ if not weechat.window_search_with_buffer(lb_buffer):
+ weechat.command("", "/buffer " + weechat.buffer_get_string(lb_buffer,"name"))
+
+def lb_set_buffer_title():
+ global lb_buffer, lb_current_sort
+ ascdesc = '(v)' if lb_sort_inverted else '(^)'
+ weechat.buffer_set(lb_buffer, "title", lb_line_format({
+ 'channel': 'Channel name%s' % (ascdesc if lb_current_sort == 'channel' else ''),
+ 'users': 'Users%s' % (ascdesc if lb_current_sort == 'users' else ''),
+ 'modes': 'Modes%s' % (ascdesc if lb_current_sort == 'modes' else ''),
+ 'topic': 'Topic%s' % (ascdesc if lb_current_sort == 'topic' else ''),
+ 'nomodes': None,
+ }))
+
+def lb_list_start(data, signal, message):
+ lb_initialise_list
+
+ return weechat.WEECHAT_RC_OK
+
+def lb_initialise_list(signal):
+ global lb_channels, lb_network, lb_list_started
+
+ lb_create_buffer()
+ lb_channels = []
+ lb_network = signal.split(',')[0]
+ lb_list_started = True
+ return
+
+
+def lb_list_chan(data, signal, message):
+ global lb_channels, lb_buffer, lb_list_started
+
+ # Work-around for IRCds which don't send 321 Numeric (/List start)
+ if not lb_list_started:
+ lb_initialise_list(signal)
+
+ for chan_data in re.findall(lb_channel_list_expression,message):
+ lb_channels.append({
+ 'server': chan_data[0][1:-1],
+ 'numeric': chan_data[1],
+ 'nick': chan_data[2],
+ 'channel': chan_data[3],
+ 'users': chan_data[4],
+ 'nomodes': chan_data[5] == '',
+ 'modes': chan_data[6],
+ 'topic': weechat.hook_modifier_exec("irc_color_decode", "1", chan_data[7])
+ })
+ return weechat.WEECHAT_RC_OK
+
+def lb_list_end(data, signal, message):
+ global lb_list_started
+
+ # Work-around for IRCds which don't send 321 Numeric (/List start)
+ if not lb_list_started:
+ lb_initialise_list(signal)
+
+ lb_list_started = False
+ if lb_current_sort:
+ lb_sort()
+ lb_refresh()
+ return weechat.WEECHAT_RC_OK
+
+def keyEvent (data, buffer, args):
+ global lb_options
+ lb_options[args]()
+
+def lb_input_cb(data, buffer, input_data):
+ global lb_options, lb_curline
+ lb_options[input_data]()
+ return weechat.WEECHAT_RC_OK
+
+def lb_refresh():
+ global lb_channels, lb_buffer
+ weechat.buffer_clear(lb_buffer)
+
+ y = 0
+ for list_data in lb_channels:
+ lb_refresh_line(y)
+ y += 1
+ return
+
+def lb_refresh_line(y):
+ global lb_buffer, lb_curline, lb_channels
+ if y >= 0 and y < len(lb_channels):
+ formatted_line = lb_line_format(lb_channels[y], y == lb_curline)
+ weechat.prnt_y(lb_buffer, y, formatted_line)
+
+def lb_refresh_curline():
+ global lb_curline
+ lb_refresh_line(lb_curline-1)
+ lb_refresh_line(lb_curline)
+ lb_refresh_line(lb_curline+1)
+ return
+
+def lb_line_format(list_data,curr=False):
+ str = ""
+ if (curr):
+ str += weechat.color("yellow,red")
+ channel_text = list_data['channel'].ljust(int(weechat.config_get_plugin('channel_min_width')))
+ users_text = "(%s)" % list_data['users']
+ padded_users_text = users_text.rjust(int(weechat.config_get_plugin('users_min_width')) + 2)
+ str += "%s%s %s " % (weechat.color("bold"), channel_text, padded_users_text)
+ if not list_data['nomodes']:
+ modes = "[%s]" % list_data['modes']
+ else:
+ modes = "[]"
+ str += "%s: " % modes.rjust(int(weechat.config_get_plugin('modes_min_width')) + 2)
+ str += "%s" % list_data['topic']
+ return str
+
+def lb_line_up():
+ global lb_curline
+ if lb_curline <= 0:
+ return
+ lb_curline -= 1
+ lb_refresh_curline()
+ lb_check_outside_window()
+ return
+
+def lb_line_down():
+ global lb_curline, lb_channels
+ if lb_curline+1 >= len(lb_channels):
+ return
+ lb_curline += 1
+ lb_refresh_curline()
+ lb_check_outside_window()
+ return
+
+def lb_line_run():
+ global lb_channels, lb_curline, lb_network
+ buff = weechat.info_get("irc_buffer", lb_network)
+ channel = lb_channels[lb_curline]['channel']
+ command = "/join %s" % channel
+ weechat.command(buff, command)
+ return
+
+def lb_line_select():
+ return
+
+def lb_scroll_top():
+ global lb_curline
+ old_y = lb_curline
+ lb_curline = 0
+ lb_refresh_curline()
+ lb_refresh_line(old_y)
+ weechat.command(lb_buffer, "/window scroll_top")
+ return
+
+def lb_scroll_bottom():
+ global lb_curline, lb_channels
+ old_y = lb_curline
+ lb_curline = len(lb_channels)-1
+ lb_refresh_curline()
+ lb_refresh_line(old_y)
+ weechat.command(lb_buffer, "/window scroll_bottom")
+ return
+
+def lb_check_outside_window():
+ global lb_buffer, lb_curline
+ if (lb_buffer):
+ infolist = weechat.infolist_get("window", "", "current")
+ if (weechat.infolist_next(infolist)):
+ start_line_y = weechat.infolist_integer(infolist, "start_line_y")
+ chat_height = weechat.infolist_integer(infolist, "chat_height")
+ if(start_line_y > lb_curline):
+ weechat.command(lb_buffer, "/window scroll -%i" %(start_line_y - lb_curline))
+ elif(start_line_y <= lb_curline - chat_height):
+ weechat.command(lb_buffer, "/window scroll +%i"%(lb_curline - start_line_y - chat_height + 1))
+ weechat.infolist_free(infolist)
+
+def lb_sort_next():
+ global lb_current_sort, lb_sort_options
+ if lb_current_sort:
+ new_index = lb_sort_options.index(lb_current_sort) + 1
+ else:
+ new_index = 0
+
+ if len(lb_sort_options) <= new_index:
+ new_index = 0
+
+ lb_set_current_sort_order(lb_sort_options[new_index])
+ lb_sort()
+
+def lb_set_current_sort_order(value):
+ global lb_current_sort
+ lb_current_sort = value
+ weechat.config_set_plugin('sort_order', lb_current_sort)
+
+def lb_set_invert_sort_order(value):
+ global lb_sort_inverted
+ lb_sort_inverted = value
+ weechat.config_set_plugin('sort_inverted', ('on' if lb_sort_inverted else 'off'))
+
+def lb_sort_previous():
+ global lb_current_sort, lb_sort_options
+ if lb_current_sort:
+ new_index = lb_sort_options.index(lb_current_sort) - 1
+ else:
+ new_index = 0
+
+ if new_index < 0:
+ new_index = len(lb_sort_options) - 1
+
+ lb_set_current_sort_order(lb_sort_options[new_index])
+ lb_sort()
+
+def lb_sort(sort_key=None):
+ global lb_channels, lb_current_sort, lb_sort_inverted
+ if sort_key:
+ lb_set_current_sort_order(sort_key)
+ if lb_current_sort == 'users':
+ lb_channels = sorted(lb_channels, key=lambda chan_data: int(chan_data[lb_current_sort]))
+ else:
+ lb_channels = sorted(lb_channels, key=lambda chan_data: chan_data[lb_current_sort])
+ if lb_sort_inverted:
+ lb_channels.reverse()
+ lb_set_buffer_title()
+ lb_refresh()
+
+def lb_sort_invert():
+ global lb_current_sort, lb_sort_inverted
+ if lb_current_sort:
+ lb_set_invert_sort_order(not lb_sort_inverted)
+ lb_sort()
+
+def lb_close_cb(*kwargs):
+ """ A callback for buffer closing. """
+ global lb_buffer
+
+ lb_buffer = None
+ return weechat.WEECHAT_RC_OK
+
+lb_options = {
+ 'refresh' : lb_refresh,
+ 'up' : lb_line_up,
+ 'down' : lb_line_down,
+ 'enter' : lb_line_run,
+ 'space' : lb_line_select,
+ 'scroll_top' : lb_scroll_top,
+ 'scroll_bottom': lb_scroll_bottom,
+ 'sort_next' : lb_sort_next,
+ 'sort_previous': lb_sort_previous,
+ 'sort_invert': lb_sort_invert
+}
+
+def lb_command_main(data, buffer, args):
+ if args[0:2] == "**":
+ keyEvent(data, buffer, args[2:])
+ return weechat.WEECHAT_RC_OK
+
+def lb_set_default_settings():
+ global lb_settings
+ # Set default settings
+ for option, default_value, description in lb_settings:
+ if not weechat.config_is_set_plugin(option):
+ weechat.config_set_plugin(option, default_value)
+ version = weechat.info_get("version_number", "") or 0
+ if int(version) >= 0x00030500:
+ weechat.config_set_desc_plugin(option, description)
+
+def lb_reset_stored_sort_order():
+ global lb_current_sort, lb_sort_inverted
+ lb_current_sort = weechat.config_get_plugin('sort_order')
+ lb_sort_inverted = (True if weechat.config_get_plugin('sort_inverted') == 'on' else False)
+
+if __name__ == "__main__" and import_ok:
+ if weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION,
+ SCRIPT_LICENSE, SCRIPT_DESC, "lb_close_cb", ""):
+ lb_set_default_settings()
+ lb_reset_stored_sort_order()
+ lb_buffer = weechat.buffer_search("python", "listbuffer")
+
+ weechat.hook_signal("*,irc_in_321", "lb_list_start", "")
+ weechat.hook_signal("*,irc_in_322", "lb_list_chan", "")
+ weechat.hook_signal("*,irc_in_323", "lb_list_end", "")
+ weechat.hook_command(SCRIPT_COMMAND,
+ "List Buffer",
+ "", "", "",
+ "lb_command_main", "")