#
# Changelog:
+# 3.6:
+# * Add more documentation on provided info hooks.
+# 3.5:
+# * Add ${info:autosort_escape,...} to escape arguments for other info hooks.
+# 3.4:
+# * Fix rate-limit of sorting to prevent high CPU load and lock-ups.
+# * Fix bug in parsing empty arguments for info hooks.
+# * Add debug_log option to aid with debugging.
+# * Correct a few typos.
# 3.3:
# * Fix the /autosort debug command for unicode.
# * Update the default rules to work better with Slack.
SCRIPT_NAME = 'autosort'
SCRIPT_AUTHOR = 'Maarten de Vries <maarten@de-vri.es>'
-SCRIPT_VERSION = '3.3'
+SCRIPT_VERSION = '3.6'
SCRIPT_LICENSE = 'GPL3'
SCRIPT_DESC = 'Flexible automatic (or manual) buffer sorting based on eval expressions.'
-config = None
-hooks = []
-timer = None
+config = None
+hooks = []
+signal_delay_timer = None
+sort_limit_timer = None
+sort_queued = False
+
# Make sure that unicode, bytes and str are always available in python2 and 3.
# For python 2, str == bytes
def decode_helpers(blob):
parsed = json.loads(blob)
if not isinstance(parsed, dict):
- 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)))
+ 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)))
return {}
for key, value in parsed.items():
if not isinstance(value, (str, unicode)):
- log('Helper "{0}" is not a string but a {1}. No helpers have been loaded. Please fix seting manually.'.format(key, type(value)))
+ log('Helper "{0}" is not a string but a {1}. No helpers have been loaded. Please fix setting manually.'.format(key, type(value)))
return {}
return parsed
'irc_last': '${if:${buffer.plugin.name}==irc}',
'irc_raw_first': '${if:${buffer.full_name}!=irc.irc_raw}',
'irc_raw_last': '${if:${buffer.full_name}==irc.irc_raw}',
- 'hashless_name': '${info:autosort_replace,#,,${buffer.name}}',
+ 'hashless_name': '${info:autosort_replace,#,,${info:autosort_escape,${buffer.name}}}',
})
default_signal_delay = 5
+ default_sort_limit = 100
default_signals = 'buffer_opened buffer_merged buffer_unmerged buffer_renamed'
self.helpers = {}
self.signals = []
self.signal_delay = Config.default_signal_delay,
+ self.sort_limit = Config.default_sort_limit,
self.sort_on_config = True
+ self.debug_log = False
self.__case_sensitive = None
self.__rules = None
self.__helpers = None
self.__signals = None
self.__signal_delay = None
+ self.__sort_limit = None
self.__sort_on_config = None
+ self.__debug_log = None
if not self.config_file:
log('Failed to initialize configuration file "{0}".'.format(self.filename))
'', '', '', '', '', ''
)
+ self.__sort_limit = weechat.config_new_option(
+ self.config_file, self.sorting_section,
+ 'sort_limit', 'integer',
+ '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.',
+ '', 0, 1000, str(Config.default_sort_limit), str(Config.default_sort_limit), 0,
+ '', '', '', '', '', ''
+ )
+
self.__sort_on_config = weechat.config_new_option(
self.config_file, self.sorting_section,
'sort_on_config_change', 'boolean',
'', '', '', '', '', ''
)
+ self.__debug_log = weechat.config_new_option(
+ self.config_file, self.sorting_section,
+ 'debug_log', 'boolean',
+ 'If enabled, print more debug messages. Not recommended for normal usage.',
+ '', 0, 0, 'off', 'off', 0,
+ '', '', '', '', '', ''
+ )
+
if weechat.config_read(self.config_file) != weechat.WEECHAT_RC_OK:
log('Failed to load configuration file.')
self.helpers = decode_helpers(helpers_blob)
self.signals = signals_blob.split()
self.signal_delay = weechat.config_integer(self.__signal_delay)
+ self.sort_limit = weechat.config_integer(self.__sort_limit)
self.sort_on_config = weechat.config_boolean(self.__sort_on_config)
+ self.debug_log = weechat.config_boolean(self.__debug_log)
def save_rules(self, run_callback = True):
''' Save the current rules to the configuration. '''
''' 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 debug(message, buffer = 'NULL'):
+ if config.debug_log:
+ weechat.prnt(buffer, 'autosort: debug: {0}'.format(message))
def get_buffers():
''' Get a list of all the buffers in weechat. '''
raise HumanReadableError('Expected at least {0} arguments, got {1}.'.format(expected, len(split)))
return split[:-1] + pad(split[-1].split(' ', optional), optional + 1, '')
-def do_sort():
+def do_sort(verbose = False):
+ start = perf_counter()
+
hdata, buffers = get_buffers()
buffers = merge_buffer_list(buffers)
buffers = sort_buffers(hdata, buffers, config.rules, config.helpers, config.case_sensitive)
apply_buffer_order(buffers)
+ elapsed = perf_counter() - start
+ if verbose:
+ log("Finished sorting buffers in {0:.4f} seconds.".format(elapsed))
+ else:
+ debug("Finished sorting buffers in {0:.4f} seconds.".format(elapsed))
+
def command_sort(buffer, command, args):
''' Sort the buffers and print a confirmation. '''
- start = perf_counter()
- do_sort()
- elapsed = perf_counter() - start
- log("Finished sorting buffers in {0:.4f} seconds.".format(elapsed))
+ do_sort(True)
return weechat.WEECHAT_RC_OK
def command_debug(buffer, command, args):
fullname = ensure_str(fullname)
result = [ensure_str(x) for x in result]
log('{0}: {1}'.format(fullname, result))
- log('Computing evalutaion results took {0:.4f} seconds.'.format(elapsed))
+ log('Computing evaluation results took {0:.4f} seconds.'.format(elapsed))
return weechat.WEECHAT_RC_OK
return weechat.WEECHAT_RC_OK
def call_command(buffer, command, args, subcommands):
- ''' Call a subccommand from a dictionary. '''
+ ''' Call a subcommand from a dictionary. '''
subcommand, tail = pad(args.split(' ', 1), 2, '')
subcommand = subcommand.strip()
if (subcommand == ''):
log('{0}: command not found'.format(' '.join(command)))
return weechat.WEECHAT_RC_ERROR
-def on_signal(*args, **kwargs):
- global timer
- ''' Called whenever the buffer list changes. '''
- if timer is not None:
- weechat.unhook(timer)
- timer = None
- weechat.hook_timer(config.signal_delay, 0, 1, "on_timeout", "")
+def on_signal(data, signal, signal_data):
+ global signal_delay_timer
+ global sort_queued
+
+ # If the sort limit timeout is started, we're in the hold-off time after sorting, just queue a sort.
+ if sort_limit_timer is not None:
+ if sort_queued:
+ debug('Signal {0} ignored, sort limit timeout is active and sort is already queued.'.format(signal))
+ else:
+ debug('Signal {0} received but sort limit timeout is active, sort is now queued.'.format(signal))
+ sort_queued = True
+ return weechat.WEECHAT_RC_OK
+
+ # If the signal delay timeout is started, a signal was recently received, so ignore this signal.
+ if signal_delay_timer is not None:
+ debug('Signal {0} ignored, signal delay timeout active.'.format(signal))
+ return weechat.WEECHAT_RC_OK
+
+ # Otherwise, start the signal delay timeout.
+ debug('Signal {0} received, starting signal delay timeout of {1} ms.'.format(signal, config.signal_delay))
+ weechat.hook_timer(config.signal_delay, 0, 1, "on_signal_delay_timeout", "")
+ return weechat.WEECHAT_RC_OK
+
+def on_signal_delay_timeout(pointer, remaining_calls):
+ """ Called when the signal_delay_timer triggers. """
+ global signal_delay_timer
+ global sort_limit_timer
+ global sort_queued
+
+ signal_delay_timer = None
+
+ # If the sort limit timeout was started, we're still in the no-sort period, so just queue a sort.
+ if sort_limit_timer is not None:
+ debug('Signal delay timeout expired, but sort limit timeout is active, sort is now queued.')
+ sort_queued = True
+ return weechat.WEECHAT_RC_OK
+
+ # Time to sort!
+ debug('Signal delay timeout expired, starting sort.')
+ do_sort()
+
+ # Start the sort limit timeout if not disabled.
+ if config.sort_limit > 0:
+ debug('Starting sort limit timeout of {0} ms.'.format(config.sort_limit))
+ sort_limit_timer = weechat.hook_timer(config.sort_limit, 0, 1, "on_sort_limit_timeout", "")
+
return weechat.WEECHAT_RC_OK
-def on_timeout(pointer, remaining_calls):
- global timer
- timer = None
+def on_sort_limit_timeout(pointer, remainin_calls):
+ """ Called when de sort_limit_timer triggers. """
+ global sort_limit_timer
+ global sort_queued
+
+ # If no signal was received during the timeout, we're done.
+ if not sort_queued:
+ debug('Sort limit timeout expired without receiving a signal.')
+ sort_limit_timer = None
+ return weechat.WEECHAT_RC_OK
+
+ # Otherwise it's time to sort.
+ debug('Signal received during sort limit timeout, starting queued sort.')
do_sort()
+ sort_queued = False
+
+ # Start the sort limit timeout again if not disabled.
+ if config.sort_limit > 0:
+ debug('Starting sort limit timeout of {0} ms.'.format(config.sort_limit))
+ sort_limit_timer = weechat.hook_timer(config.sort_limit, 0, 1, "on_sort_limit_timeout", "")
+
return weechat.WEECHAT_RC_OK
+
def apply_config():
# Unhook all signals and hook the new ones.
for hook in hooks:
hooks.append(weechat.hook_signal(signal, 'on_signal', ''))
if config.sort_on_config:
+ debug('Sorting because configuration changed.')
do_sort()
def on_config_changed(*args, **kwargs):
return weechat.WEECHAT_RC_OK
def parse_arg(args):
- if not args: return None, None
+ if not args: return '', None
result = ''
escaped = False
result = []
i = 0
while max is None or i < max:
+ i += 1
arg, args = parse_arg(args)
if arg is None: break
result.append(arg)
- i += 1
+ if args is None: break
return result, args
+def on_info_escape(pointer, name, arguments):
+ result = ''
+ for c in arguments:
+ if c == '\\':
+ result += '\\\\'
+ elif c == ',':
+ result += '\\,'
+ else:
+ result +=c
+ return result
+
def on_info_replace(pointer, name, arguments):
arguments, rest = parse_args(arguments, 3)
if rest or len(arguments) < 3:
Swap the expressions of two helper variables in the list.
+{*white}# Info hooks{reset}
+Autosort comes with a number of info hooks to add some extra functionality to regular weechat eval strings.
+Info hooks can be used in eval strings in the form of {cyan}${{info:some_hook,arguments}}{reset}.
+
+Commas and backslashes in arguments to autosort info hooks (except for {cyan}${{info:autosort_escape}}{reset}) must be escaped with a backslash.
+
+{*white}${{info:{brown}autosort_replace{white},{cyan}pattern{white},{cyan}replacement{white},{cyan}source{white}}}{reset}
+Replace all occurrences of {cyan}pattern{reset} with {cyan}replacement{reset} in the string {cyan}source{reset}.
+Can be used to ignore certain strings when sorting by replacing them with an empty string.
+
+For example: {cyan}${{info:autosort_replace,cat,dog,the dog is meowing}}{reset} expands to "the cat is meowing".
+
+{*white}${{info:{brown}autosort_order{white},{cyan}value{white},{cyan}option0{white},{cyan}option1{white},{cyan}option2{white},{cyan}...{white}}}
+Generate a zero-padded number that corresponds to the index of {cyan}value{reset} in the list of options.
+If one of the options is the special value {brown}*{reset}, then any value not explicitly mentioned will be sorted at that position.
+Otherwise, any value that does not match an option is assigned the highest number available.
+Can be used to easily sort buffers based on a manual sequence.
+
+For example: {cyan}${{info:autosort_order,${{server}},freenode,oftc,efnet}}{reset} will sort freenode before oftc, followed by efnet and then any remaining servers.
+Alternatively, {cyan}${{info:autosort_order,${{server}},freenode,oftc,*,efnet}}{reset} will sort any unlisted servers after freenode and oftc, but before efnet.
+
+{*white}${{info:{brown}autosort_escape{white},{cyan}text{white}}}{reset}
+Escape commas and backslashes in {cyan}text{reset} by prepending them with a backslash.
+This is mainly useful to pass arbitrary eval strings as arguments to other autosort info hooks.
+Otherwise, an eval string that expands to something with a comma would be interpreted as multiple arguments.
+
+For example, it can be used to safely pass buffer names to {cyan}${{info:autosort_replace}}{reset} like so:
+{cyan}${{info:autosort_replace,##,#,${{info:autosort_escape,${{buffer.name}}}}}}{reset}.
+
+
{*white}# Description
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
a helper variable named `{cyan}foo{reset}` can be accessed in a main rule with the
string `{cyan}${{foo}}{reset}`.
-{*white}# Replacing substrings{reset}
-There is no default method for replacing text inside eval expressions. However,
-autosort adds a `replace` info hook that can be used inside eval expressions:
- {cyan}${{info:autosort_replace,from,to,text}}{reset}
-
-For example, to strip all hashes from a buffer name, you could write:
- {cyan}${{info:autosort_replace,#,,${{buffer.name}}}}{reset}
-
-You can escape commas and backslashes inside the arguments by prefixing them with
-a backslash.
-
{*white}# Automatic or manual sorting{reset}
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
command_completion = '%(plugin_autosort) %(plugin_autosort) %(plugin_autosort) %(plugin_autosort) %(plugin_autosort)'
-info_replace_description = 'Replace all occurences of `from` with `to` in the string `text`.'
-info_replace_arguments = 'from,to,text'
+info_replace_description = (
+ 'Replace all occurrences of `pattern` with `replacement` in the string `source`. '
+ 'Can be used to ignore certain strings when sorting by replacing them with an empty string. '
+ 'See /help autosort for examples.'
+)
+info_replace_arguments = 'pattern,replacement,source'
info_order_description = (
- 'Get a zero padded index of a value in a list of possible values.'
- 'If the value is not found, the index for `*` is returned.'
- 'If there is no `*` in the list, the highest index + 1 is returned.'
+ 'Generate a zero-padded number that corresponds to the index of `value` in the list of options. '
+ 'If one of the options is the special value `*`, then any value not explicitly mentioned will be sorted at that position. '
+ 'Otherwise, any value that does not match an option is assigned the highest number available. '
+ 'Can be used to easily sort buffers based on a manual sequence. '
+ 'See /help autosort for examples.'
+)
+info_order_arguments = 'value,first,second,third,...'
+
+info_escape_description = (
+ 'Escape commas and backslashes in `text` by prepending them with a backslash. '
+ 'This is mainly useful to pass arbitrary eval strings as arguments to other autosort info hooks. '
+ 'Otherwise, an eval string that expands to something with a comma would be interpreted as multiple arguments.'
+ 'See /help autosort for examples.'
)
-info_order_arguments = 'value,first,second,third,...'
+info_escape_arguments = 'text'
if weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, "", ""):
weechat.hook_config('autosort.*', 'on_config_changed', '')
weechat.hook_completion('plugin_autosort', '', 'on_autosort_complete', '')
weechat.hook_command('autosort', command_description.format(**colors), '', '', command_completion, 'on_autosort_command', '')
+ weechat.hook_info('autosort_escape', info_escape_description, info_escape_arguments, 'on_info_escape', '')
weechat.hook_info('autosort_replace', info_replace_description, info_replace_arguments, 'on_info_replace', '')
weechat.hook_info('autosort_order', info_order_description, info_order_arguments, 'on_info_order', '')