X-Git-Url: https://git.rmz.io/dotfiles.git/blobdiff_plain/46c785a430de386f6eada75d34095298f9a750cd..refs/heads/uh-backup:/weechat/python/autosort.py diff --git a/weechat/python/autosort.py b/weechat/python/autosort.py index 08a6c5b..8f7c263 100644 --- a/weechat/python/autosort.py +++ b/weechat/python/autosort.py @@ -25,6 +25,15 @@ # # 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. @@ -71,14 +80,17 @@ import weechat SCRIPT_NAME = 'autosort' SCRIPT_AUTHOR = 'Maarten de Vries ' -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 @@ -147,12 +159,12 @@ def decode_rules(blob): 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 @@ -176,10 +188,11 @@ class Config: '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' @@ -196,14 +209,18 @@ class Config: 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)) @@ -273,6 +290,14 @@ class Config: '', '', '', '', '', '' ) + 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', @@ -281,6 +306,14 @@ class Config: '', '', '', '', '', '' ) + 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.') @@ -302,7 +335,9 @@ class Config: 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. ''' @@ -317,10 +352,12 @@ 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 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. ''' @@ -396,18 +433,23 @@ def split_args(args, expected, optional = 0): 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): @@ -429,7 +471,7 @@ 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 @@ -574,7 +616,7 @@ def command_helper_swap(buffer, command, args): 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 == ''): @@ -591,21 +633,78 @@ def call_command(buffer, command, args, subcommands): 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: @@ -614,6 +713,7 @@ def apply_config(): 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): @@ -624,7 +724,7 @@ 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 @@ -643,12 +743,24 @@ def parse_args(args, max = None): 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: @@ -813,6 +925,36 @@ Rename a helper variable. 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 @@ -837,17 +979,6 @@ readable. They can be used in the main sort rules as variables. For example, 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 @@ -876,15 +1007,29 @@ structure with the following setting (modify to suit your need): 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, "", ""): @@ -917,6 +1062,7 @@ if weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, 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', '')