2018-08-09 00:20:19 +00:00
# -*- coding: utf-8 -*-
#
# Copyright (C) 2013-2017 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.
#
# https://github.com/de-vri-es/weechat-autosort
#
#
# Changelog:
2020-01-06 18:18:26 +00:00
# 3,8:
# * Fix relative sorting on script name in default rules.
# * Document a useful property of stable sort algorithms.
# 3.7:
# * Make default rules work with bitlbee, matrix and slack.
2019-06-25 14:18:27 +00:00
# 3.6:
# * Add more documentation on provided info hooks.
# 3.5:
# * Add ${info:autosort_escape,...} to escape arguments for other info hooks.
2019-05-29 17:39:18 +00:00
# 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.
2018-08-09 00:20:19 +00:00
# 3.3:
# * Fix the /autosort debug command for unicode.
# * Update the default rules to work better with Slack.
# 3.2:
# * Fix python3 compatiblity.
# 3.1:
# * Use colors to format the help text.
# 3.0:
# * Switch to evaluated expressions for sorting.
# * Add `/autosort debug` command.
# * Add ${info:autosort_replace,from,to,text} to replace substrings in sort rules.
# * Add ${info:autosort_order,value,first,second,third} to ease writing sort rules.
# * Make tab completion context aware.
# 2.8:
# * Fix compatibility with python 3 regarding unicode handling.
# 2.7:
# * Fix sorting of buffers with spaces in their name.
# 2.6:
# * Ignore case in rules when doing case insensitive sorting.
# 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 json
import math
import re
import sys
import time
import weechat
SCRIPT_NAME = ' autosort '
SCRIPT_AUTHOR = ' Maarten de Vries <maarten@de-vri.es> '
2020-01-06 18:18:26 +00:00
SCRIPT_VERSION = ' 3.8 '
2018-08-09 00:20:19 +00:00
SCRIPT_LICENSE = ' GPL3 '
SCRIPT_DESC = ' Flexible automatic (or manual) buffer sorting based on eval expressions. '
2019-05-29 17:39:18 +00:00
config = None
hooks = [ ]
signal_delay_timer = None
sort_limit_timer = None
sort_queued = False
2018-08-09 00:20:19 +00:00
# Make sure that unicode, bytes and str are always available in python2 and 3.
# For python 2, str == bytes
# For python 3, str == unicode
if sys . version_info [ 0 ] > = 3 :
unicode = str
def ensure_str ( input ) :
'''
Make sure the given type if the correct string type for the current python version .
That means bytes for python2 and unicode for python3 .
'''
if not isinstance ( input , str ) :
if isinstance ( input , bytes ) :
return input . encode ( ' utf-8 ' )
if isinstance ( input , unicode ) :
return input . decode ( ' utf-8 ' )
return input
if hasattr ( time , ' perf_counter ' ) :
perf_counter = time . perf_counter
else :
perf_counter = time . clock
def casefold ( string ) :
if hasattr ( string , ' casefold ' ) : return string . casefold ( )
# Fall back to lowercasing for python2.
return string . lower ( )
def list_swap ( values , a , b ) :
values [ a ] , values [ b ] = values [ b ] , values [ a ]
def list_move ( values , old_index , new_index ) :
values . insert ( new_index , values . pop ( old_index ) )
def list_find ( collection , value ) :
for i , elem in enumerate ( collection ) :
if elem == value : return i
return None
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 ) )
def decode_rules ( blob ) :
parsed = json . loads ( blob )
if not isinstance ( parsed , list ) :
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 ) ) )
return [ ]
for i , entry in enumerate ( parsed ) :
if not isinstance ( entry , ( str , unicode ) ) :
log ( ' Rule # {0} is not a string but a {1} . No rules have been loaded. Please fix the setting manually. ' . format ( i , type ( entry ) ) )
return [ ]
return parsed
def decode_helpers ( blob ) :
parsed = json . loads ( blob )
if not isinstance ( parsed , dict ) :
2019-05-29 17:39:18 +00:00
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 ) ) )
2018-08-09 00:20:19 +00:00
return { }
for key , value in parsed . items ( ) :
if not isinstance ( value , ( str , unicode ) ) :
2019-05-29 17:39:18 +00:00
log ( ' Helper " {0} " is not a string but a {1} . No helpers have been loaded. Please fix setting manually. ' . format ( key , type ( value ) ) )
2018-08-09 00:20:19 +00:00
return { }
return parsed
class Config :
''' The autosort configuration. '''
default_rules = json . dumps ( [
' $ {core_first} ' ,
2020-01-06 18:18:26 +00:00
' $ { info:autosort_order,$ { info:autosort_escape,$ {script_or_plugin} },core,*,irc,bitlbee,matrix,slack} ' ,
' $ {script_or_plugin} ' ,
2018-08-09 00:20:19 +00:00
' $ {irc_raw_first} ' ,
2020-01-06 18:18:26 +00:00
' $ {server} ' ,
' $ { info:autosort_order,$ {type} ,server,*,channel,private} ' ,
' $ {hashless_name} ' ,
2018-08-09 00:20:19 +00:00
' $ {buffer.full_name} ' ,
] )
default_helpers = json . dumps ( {
2020-01-06 18:18:26 +00:00
' core_first ' : ' $ { if:$ {buffer.full_name} !=core.weechat} ' ,
' 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,#,,$ { info:autosort_escape,$ {buffer.name} }} ' ,
' script_or_plugin ' : ' $ { if:$ {script_name} ?$ {script_name} :$ {plugin} } ' ,
2018-08-09 00:20:19 +00:00
} )
default_signal_delay = 5
2019-05-29 17:39:18 +00:00
default_sort_limit = 100
2018-08-09 00:20:19 +00:00
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 . v3_section = None
self . case_sensitive = False
self . rules = [ ]
self . helpers = { }
self . signals = [ ]
self . signal_delay = Config . default_signal_delay ,
2019-05-29 17:39:18 +00:00
self . sort_limit = Config . default_sort_limit ,
2018-08-09 00:20:19 +00:00
self . sort_on_config = True
2019-05-29 17:39:18 +00:00
self . debug_log = False
2018-08-09 00:20:19 +00:00
self . __case_sensitive = None
self . __rules = None
self . __helpers = None
self . __signals = None
self . __signal_delay = None
2019-05-29 17:39:18 +00:00
self . __sort_limit = None
2018-08-09 00:20:19 +00:00
self . __sort_on_config = None
2019-05-29 17:39:18 +00:00
self . __debug_log = None
2018-08-09 00:20:19 +00:00
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 , ' ' , ' ' , ' ' , ' ' , ' ' , ' ' , ' ' , ' ' , ' ' , ' ' )
self . v3_section = weechat . config_new_section ( self . config_file , ' v3 ' , 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 ,
' ' , ' ' , ' ' , ' ' , ' ' , ' '
)
weechat . config_new_option (
self . config_file , self . sorting_section ,
' rules ' , ' string ' ,
' Sort rules used by autosort v2.x and below. Not used by autosort anymore. ' ,
' ' , 0 , 0 , ' ' , ' ' , 0 ,
' ' , ' ' , ' ' , ' ' , ' ' , ' '
)
weechat . config_new_option (
self . config_file , self . sorting_section ,
' replacements ' , ' string ' ,
' Replacement patterns used by autosort v2.x and below. Not used by autosort anymore. ' ,
' ' , 0 , 0 , ' ' , ' ' , 0 ,
' ' , ' ' , ' ' , ' ' , ' ' , ' '
)
self . __rules = weechat . config_new_option (
self . config_file , self . v3_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 . __helpers = weechat . config_new_option (
self . config_file , self . v3_section ,
' helpers ' , ' string ' ,
' A dictionary helper variables to use in the sorting rules, encoded as JSON. See /help autosort for commands to manipulate these helpers. ' ,
' ' , 0 , 0 , Config . default_helpers , Config . default_helpers , 0 ,
' ' , ' ' , ' ' , ' ' , ' ' , ' '
)
self . __signals = weechat . config_new_option (
self . config_file , self . sorting_section ,
' signals ' , ' string ' ,
' A space separated list of signals that will cause autosort to resort your buffer list. ' ,
' ' , 0 , 0 , Config . default_signals , Config . default_signals , 0 ,
' ' , ' ' , ' ' , ' ' , ' ' , ' '
)
self . __signal_delay = weechat . config_new_option (
self . config_file , self . sorting_section ,
' signal_delay ' , ' integer ' ,
' 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. ' ,
' ' , 0 , 1000 , str ( Config . default_signal_delay ) , str ( Config . default_signal_delay ) , 0 ,
' ' , ' ' , ' ' , ' ' , ' ' , ' '
)
2019-05-29 17:39:18 +00:00
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 ,
' ' , ' ' , ' ' , ' ' , ' ' , ' '
)
2018-08-09 00:20:19 +00:00
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 ,
' ' , ' ' , ' ' , ' ' , ' ' , ' '
)
2019-05-29 17:39:18 +00:00
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 ,
' ' , ' ' , ' ' , ' ' , ' ' , ' '
)
2018-08-09 00:20:19 +00:00
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 )
rules_blob = weechat . config_string ( self . __rules )
helpers_blob = weechat . config_string ( self . __helpers )
signals_blob = weechat . config_string ( self . __signals )
self . rules = decode_rules ( rules_blob )
self . helpers = decode_helpers ( helpers_blob )
self . signals = signals_blob . split ( )
self . signal_delay = weechat . config_integer ( self . __signal_delay )
2019-05-29 17:39:18 +00:00
self . sort_limit = weechat . config_integer ( self . __sort_limit )
2018-08-09 00:20:19 +00:00
self . sort_on_config = weechat . config_boolean ( self . __sort_on_config )
2019-05-29 17:39:18 +00:00
self . debug_log = weechat . config_boolean ( self . __debug_log )
2018-08-09 00:20:19 +00:00
def save_rules ( self , run_callback = True ) :
''' Save the current rules to the configuration. '''
weechat . config_option_set ( self . __rules , json . dumps ( self . rules ) , run_callback )
def save_helpers ( self , run_callback = True ) :
''' Save the current helpers to the configuration. '''
weechat . config_option_set ( self . __helpers , json . dumps ( self . helpers ) , 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 ) )
2019-05-29 17:39:18 +00:00
def debug ( message , buffer = ' NULL ' ) :
if config . debug_log :
weechat . prnt ( buffer , ' autosort: debug: {0} ' . format ( message ) )
2018-08-09 00:20:19 +00:00
def get_buffers ( ) :
''' Get a list of all the buffers in weechat. '''
hdata = weechat . hdata_get ( ' buffer ' )
buffer = weechat . hdata_get_list ( hdata , " gui_buffers " ) ;
result = [ ]
while buffer :
number = weechat . hdata_integer ( hdata , buffer , ' number ' )
result . append ( ( number , buffer ) )
buffer = weechat . hdata_pointer ( hdata , buffer , ' next_buffer ' )
return hdata , result
class MergedBuffers ( list ) :
""" A list of merged buffers, possibly of size 1. """
def __init__ ( self , number ) :
super ( MergedBuffers , self ) . __init__ ( )
self . number = number
def merge_buffer_list ( buffers ) :
'''
Group merged buffers together .
The output is a list of MergedBuffers .
'''
if not buffers : return [ ]
result = { }
for number , buffer in buffers :
if number not in result : result [ number ] = MergedBuffers ( number )
result [ number ] . append ( buffer )
return result . values ( )
def sort_buffers ( hdata , buffers , rules , helpers , case_sensitive ) :
for merged in buffers :
for buffer in merged :
name = weechat . hdata_string ( hdata , buffer , ' name ' )
return sorted ( buffers , key = merged_sort_key ( rules , helpers , case_sensitive ) )
def buffer_sort_key ( rules , helpers , case_sensitive ) :
''' Create a sort key function for a list of lists of merged buffers. '''
def key ( buffer ) :
extra_vars = { }
for helper_name , helper in sorted ( helpers . items ( ) ) :
expanded = weechat . string_eval_expression ( helper , { " buffer " : buffer } , { } , { } )
extra_vars [ helper_name ] = expanded if case_sensitive else casefold ( expanded )
result = [ ]
for rule in rules :
expanded = weechat . string_eval_expression ( rule , { " buffer " : buffer } , extra_vars , { } )
result . append ( expanded if case_sensitive else casefold ( expanded ) )
return result
return key
def merged_sort_key ( rules , helpers , case_sensitive ) :
buffer_key = buffer_sort_key ( rules , helpers , case_sensitive )
def key ( merged ) :
best = None
for buffer in merged :
this = buffer_key ( buffer )
if best is None or this < best : best = this
return best
return key
def apply_buffer_order ( buffers ) :
''' Sort the buffers in weechat according to the given order. '''
for i , buffer in enumerate ( buffers ) :
weechat . buffer_set ( buffer [ 0 ] , " number " , str ( 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 , ' ' )
2019-05-29 17:39:18 +00:00
def do_sort ( verbose = False ) :
start = perf_counter ( )
2018-08-09 00:20:19 +00:00
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 )
2019-05-29 17:39:18 +00:00
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 ) )
2018-08-09 00:20:19 +00:00
def command_sort ( buffer , command , args ) :
''' Sort the buffers and print a confirmation. '''
2019-05-29 17:39:18 +00:00
do_sort ( True )
2018-08-09 00:20:19 +00:00
return weechat . WEECHAT_RC_OK
def command_debug ( buffer , command , args ) :
hdata , buffers = get_buffers ( )
buffers = merge_buffer_list ( buffers )
# Show evaluation results.
log ( ' Individual evaluation results: ' )
start = perf_counter ( )
key = buffer_sort_key ( config . rules , config . helpers , config . case_sensitive )
results = [ ]
for merged in buffers :
for buffer in merged :
fullname = weechat . hdata_string ( hdata , buffer , ' full_name ' )
results . append ( ( fullname , key ( buffer ) ) )
elapsed = perf_counter ( ) - start
for fullname , result in results :
fullname = ensure_str ( fullname )
result = [ ensure_str ( x ) for x in result ]
log ( ' {0} : {1} ' . format ( fullname , result ) )
2019-05-29 17:39:18 +00:00
log ( ' Computing evaluation results took {0:.4f} seconds. ' . format ( elapsed ) )
2018-08-09 00:20:19 +00:00
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} \n ' . format ( i , rule )
if not len ( config . rules ) :
output + = ' No sorting rules configured. \n '
log ( output )
return weechat . WEECHAT_RC_OK
def command_rule_add ( buffer , command , args ) :
''' Add a rule to the rule list. '''
config . rules . append ( args )
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 ' )
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 ' )
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 ' )
list_move ( config . rules , 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 ' )
list_swap ( config . rules , index_a , index_b )
config . save_rules ( )
command_rule_list ( buffer , command , ' ' )
return weechat . WEECHAT_RC_OK
def command_helper_list ( buffer , command , args ) :
''' Show the list of helpers. '''
output = ' Helper variables: \n '
width = max ( map ( lambda x : len ( x ) if len ( x ) < = 30 else 0 , config . helpers . keys ( ) ) )
for name , expression in sorted ( config . helpers . items ( ) ) :
output + = ' { 0:> {width} }: {1} \n ' . format ( name , expression , width = width )
if not len ( config . helpers ) :
output + = ' No helper variables configured. '
log ( output )
return weechat . WEECHAT_RC_OK
def command_helper_set ( buffer , command , args ) :
''' Add/update a helper to the helper list. '''
name , expression = split_args ( args , 2 )
config . helpers [ name ] = expression
config . save_helpers ( )
command_helper_list ( buffer , command , ' ' )
return weechat . WEECHAT_RC_OK
def command_helper_delete ( buffer , command , args ) :
''' Delete a helper from the helper list. '''
name = args . strip ( )
del config . helpers [ name ]
config . save_helpers ( )
command_helper_list ( buffer , command , ' ' )
return weechat . WEECHAT_RC_OK
def command_helper_rename ( buffer , command , args ) :
''' Rename a helper to a new position. '''
old_name , new_name = split_args ( args , 2 )
try :
config . helpers [ new_name ] = config . helpers [ old_name ]
del config . helpers [ old_name ]
except KeyError :
raise HumanReadableError ( ' No such helper: {0} ' . format ( old_name ) )
config . save_helpers ( )
command_helper_list ( buffer , command , ' ' )
return weechat . WEECHAT_RC_OK
def command_helper_swap ( buffer , command , args ) :
''' Swap two helpers. '''
a , b = split_args ( args , 2 )
try :
config . helpers [ b ] , config . helpers [ a ] = config . helpers [ a ] , config . helpers [ b ]
except KeyError as e :
raise HumanReadableError ( ' No such helper: {0} ' . format ( e . args [ 0 ] ) )
config . helpers . swap ( index_a , index_b )
config . save_helpers ( )
command_helper_list ( buffer , command , ' ' )
return weechat . WEECHAT_RC_OK
def call_command ( buffer , command , args , subcommands ) :
2019-05-29 17:39:18 +00:00
''' Call a subcommand from a dictionary. '''
2018-08-09 00:20:19 +00:00
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
2019-05-29 17:39:18 +00:00
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 " , " " )
2018-08-09 00:20:19 +00:00
return weechat . WEECHAT_RC_OK
2019-05-29 17:39:18 +00:00
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. ' )
2018-08-09 00:20:19 +00:00
do_sort ( )
2019-05-29 17:39:18 +00:00
# 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 " , " " )
2018-08-09 00:20:19 +00:00
return weechat . WEECHAT_RC_OK
2019-05-29 17:39:18 +00:00
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
2018-08-09 00:20:19 +00:00
def apply_config ( ) :
# 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_signal ' , ' ' ) )
if config . sort_on_config :
2019-05-29 17:39:18 +00:00
debug ( ' Sorting because configuration changed. ' )
2018-08-09 00:20:19 +00:00
do_sort ( )
def on_config_changed ( * args , * * kwargs ) :
''' Called whenever the configuration changes. '''
config . reload ( )
apply_config ( )
return weechat . WEECHAT_RC_OK
def parse_arg ( args ) :
2019-05-29 17:39:18 +00:00
if not args : return ' ' , None
2018-08-09 00:20:19 +00:00
result = ' '
escaped = False
for i , c in enumerate ( args ) :
if not escaped :
if c == ' \\ ' :
escaped = True
continue
elif c == ' , ' :
return result , args [ i + 1 : ]
result + = c
escaped = False
return result , None
def parse_args ( args , max = None ) :
result = [ ]
i = 0
while max is None or i < max :
2019-05-29 17:39:18 +00:00
i + = 1
2018-08-09 00:20:19 +00:00
arg , args = parse_arg ( args )
if arg is None : break
result . append ( arg )
2019-05-29 17:39:18 +00:00
if args is None : break
2018-08-09 00:20:19 +00:00
return result , args
2019-06-25 14:18:27 +00:00
def on_info_escape ( pointer , name , arguments ) :
result = ' '
for c in arguments :
if c == ' \\ ' :
result + = ' \\ \\ '
elif c == ' , ' :
result + = ' \\ , '
else :
result + = c
return result
2018-08-09 00:20:19 +00:00
def on_info_replace ( pointer , name , arguments ) :
arguments , rest = parse_args ( arguments , 3 )
if rest or len ( arguments ) < 3 :
log ( ' usage: $ {{ info: {0} ,old,new,text}} ' . format ( name ) )
return ' '
old , new , text = arguments
return text . replace ( old , new )
def on_info_order ( pointer , name , arguments ) :
arguments , rest = parse_args ( arguments )
if len ( arguments ) < 1 :
log ( ' usage: $ {{ info: {0} ,value,first,second,third,...}} ' . format ( name ) )
return ' '
value = arguments [ 0 ]
keys = arguments [ 1 : ]
if not keys : return ' 0 '
# Find the value in the keys (or '*' if we can't find it)
result = list_find ( keys , value )
if result is None : result = list_find ( keys , ' * ' )
if result is None : result = len ( keys )
# Pad result with leading zero to make sure string sorting works.
width = int ( math . log10 ( len ( keys ) ) ) + 1
return ' { 0:0 {1} } ' . format ( result , width )
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 ,
' debug ' : command_debug ,
' 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 ,
} ,
' helpers ' : {
' ' : command_helper_list ,
' list ' : command_helper_list ,
' set ' : command_helper_set ,
' delete ' : command_helper_delete ,
' rename ' : command_helper_rename ,
' swap ' : command_helper_swap ,
} ,
} )
except HumanReadableError as e :
log ( e )
return weechat . WEECHAT_RC_ERROR
def add_completions ( completion , words ) :
for word in words :
weechat . hook_completion_list_add ( completion , word , 0 , weechat . WEECHAT_LIST_POS_END )
def autosort_complete_rules ( words , completion ) :
if len ( words ) == 0 :
add_completions ( completion , [ ' add ' , ' delete ' , ' insert ' , ' list ' , ' move ' , ' swap ' , ' update ' ] )
if len ( words ) == 1 and words [ 0 ] in ( ' delete ' , ' insert ' , ' move ' , ' swap ' , ' update ' ) :
add_completions ( completion , map ( str , range ( len ( config . rules ) ) ) )
if len ( words ) == 2 and words [ 0 ] in ( ' move ' , ' swap ' ) :
add_completions ( completion , map ( str , range ( len ( config . rules ) ) ) )
if len ( words ) == 2 and words [ 0 ] in ( ' update ' ) :
try :
add_completions ( completion , [ config . rules [ int ( words [ 1 ] ) ] ] )
except KeyError : pass
except ValueError : pass
else :
add_completions ( completion , [ ' ' ] )
return weechat . WEECHAT_RC_OK
def autosort_complete_helpers ( words , completion ) :
if len ( words ) == 0 :
add_completions ( completion , [ ' delete ' , ' list ' , ' rename ' , ' set ' , ' swap ' ] )
elif len ( words ) == 1 and words [ 0 ] in ( ' delete ' , ' rename ' , ' set ' , ' swap ' ) :
add_completions ( completion , sorted ( config . helpers . keys ( ) ) )
elif len ( words ) == 2 and words [ 0 ] == ' swap ' :
add_completions ( completion , sorted ( config . helpers . keys ( ) ) )
elif len ( words ) == 2 and words [ 0 ] == ' rename ' :
add_completions ( completion , sorted ( config . helpers . keys ( ) ) )
elif len ( words ) == 2 and words [ 0 ] == ' set ' :
try :
add_completions ( completion , [ config . helpers [ words [ 1 ] ] ] )
except KeyError : pass
return weechat . WEECHAT_RC_OK
def on_autosort_complete ( data , name , buffer , completion ) :
cmdline = weechat . buffer_get_string ( buffer , " input " )
cursor = weechat . buffer_get_integer ( buffer , " input_pos " )
prefix = cmdline [ : cursor ]
words = prefix . split ( ) [ 1 : ]
# If the current word isn't finished yet,
# ignore it for coming up with completion suggestions.
if prefix [ - 1 ] != ' ' : words = words [ : - 1 ]
if len ( words ) == 0 :
add_completions ( completion , [ ' debug ' , ' helpers ' , ' rules ' , ' sort ' ] )
elif words [ 0 ] == ' rules ' :
return autosort_complete_rules ( words [ 1 : ] , completion )
elif words [ 0 ] == ' helpers ' :
return autosort_complete_helpers ( words [ 1 : ] , completion )
return weechat . WEECHAT_RC_OK
command_description = r ''' { *white}# General commands {reset}
{ * white } / autosort { brown } sort { reset }
Manually trigger the buffer sorting .
{ * white } / autosort { brown } debug { reset }
Show the evaluation results of the sort rules for each buffer .
{ * white } # Sorting rule commands{reset}
{ * white } / autosort { brown } rules list { reset }
Print the list of sort rules .
{ * white } / autosort { brown } rules add { cyan } < expression > { reset }
Add a new rule at the end of the list .
{ * white } / autosort { brown } rules insert { cyan } < index > < expression > { reset }
Insert a new rule at the given index in the list .
{ * white } / autosort { brown } rules update { cyan } < index > < expression > { reset }
Update a rule in the list with a new expression .
{ * white } / autosort { brown } rules delete { cyan } < index >
Delete a rule from the list .
{ * white } / autosort { brown } rules move { cyan } < index_from > < index_to > { reset }
Move a rule from one position in the list to another .
{ * white } / autosort { brown } rules swap { cyan } < index_a > < index_b > { reset }
Swap two rules in the list
{ * white } # Helper variable commands{reset}
{ * white } / autosort { brown } helpers list
Print the list of helper variables .
{ * white } / autosort { brown } helpers set { cyan } < name > < expression >
Add or update a helper variable with the given name .
{ * white } / autosort { brown } helpers delete { cyan } < name >
Delete a helper variable .
{ * white } / autosort { brown } helpers rename { cyan } < old_name > < new_name >
Rename a helper variable .
{ * white } / autosort { brown } helpers swap { cyan } < name_a > < name_b >
Swap the expressions of two helper variables in the list .
2019-06-25 14:18:27 +00:00
{ * 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}.
2018-08-09 00:20:19 +00:00
{ * 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
be sane enough for most people . It can also group IRC channel / private buffers
under their server buffer if you like .
2020-01-06 18:18:26 +00:00
Autosort uses a stable sorting algorithm , meaning that you can manually move buffers
to change their relative order , if they sort equal with your rule set .
2018-08-09 00:20:19 +00:00
{ * white } # Sort rules{reset}
Autosort evaluates a list of eval expressions ( see { * default } / help eval { reset } ) and sorts the
buffers based on evaluated result . Earlier rules will be considered first . Only
if earlier rules produced identical results is the result of the next rule
considered for sorting purposes .
You can debug your sort rules with the ` { * default } / autosort debug { reset } ` command , which will
print the evaluation results of each rule for each buffer .
{ * brown } NOTE : { reset } The sort rules for version 3 are not compatible with version 2 or vice
versa . You will have to manually port your old rules to version 3 if you have any .
{ * white } # Helper variables{reset}
You may define helper variables for the main sort rules to keep your rules
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 } # 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
almost all situations . However , you may wish to change the list of signals that
cause your buffer list to be sorted . Simply edit the ` { cyan } autosort . sorting . signals { reset } `
option to add or remove any signal you like .
If you remove all signals you can still sort your buffers manually with the
` { * default } / autosort sort { reset } ` command . To prevent all automatic sorting , the option
` { cyan } autosort . sorting . sort_on_config_change { reset } ` should also be disabled .
{ * white } # Recommended settings
For the best visual effect , consider setting the following options :
{ * white } / set { cyan } irc . look . server_buffer { reset } { brown } independent { reset }
{ * white } / set { cyan } buffers . look . indenting { reset } { brown } on { reset }
The first setting allows server buffers to be sorted independently , which is
needed to create a hierarchical tree view of the server and channel buffers .
The second one indents channel and private buffers in the buffer list of the
` { * default } buffers . pl { reset } ` script .
If you are using the { * default } buflist { reset } plugin you can ( ab ) use Unicode to draw a tree
structure with the following setting ( modify to suit your need ) :
{ * white } / set { cyan } buflist . format . indent { brown } " $ {{ color:237}}$ {{ if:$ {{ buffer.next_buffer.local_variables.type}}=~^(channel|private)$?├─:└─}} " { reset }
'''
command_completion = ' %(plugin_autosort) % (plugin_autosort) %(plugin_autosort) % (plugin_autosort) % (plugin_autosort) '
2019-06-25 14:18:27 +00:00
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 '
2018-08-09 00:20:19 +00:00
info_order_description = (
2019-06-25 14:18:27 +00:00
' 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. '
2018-08-09 00:20:19 +00:00
)
2019-06-25 14:18:27 +00:00
info_escape_arguments = ' text '
2018-08-09 00:20:19 +00:00
if weechat . register ( SCRIPT_NAME , SCRIPT_AUTHOR , SCRIPT_VERSION , SCRIPT_LICENSE , SCRIPT_DESC , " " , " " ) :
config = Config ( ' autosort ' )
colors = {
' default ' : weechat . color ( ' default ' ) ,
' reset ' : weechat . color ( ' reset ' ) ,
' black ' : weechat . color ( ' black ' ) ,
' red ' : weechat . color ( ' red ' ) ,
' green ' : weechat . color ( ' green ' ) ,
' brown ' : weechat . color ( ' brown ' ) ,
' yellow ' : weechat . color ( ' yellow ' ) ,
' blue ' : weechat . color ( ' blue ' ) ,
' magenta ' : weechat . color ( ' magenta ' ) ,
' cyan ' : weechat . color ( ' cyan ' ) ,
' white ' : weechat . color ( ' white ' ) ,
' *default ' : weechat . color ( ' *default ' ) ,
' *black ' : weechat . color ( ' *black ' ) ,
' *red ' : weechat . color ( ' *red ' ) ,
' *green ' : weechat . color ( ' *green ' ) ,
' *brown ' : weechat . color ( ' *brown ' ) ,
' *yellow ' : weechat . color ( ' *yellow ' ) ,
' *blue ' : weechat . color ( ' *blue ' ) ,
' *magenta ' : weechat . color ( ' *magenta ' ) ,
' *cyan ' : weechat . color ( ' *cyan ' ) ,
' *white ' : weechat . color ( ' *white ' ) ,
}
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 ' , ' ' )
2019-06-25 14:18:27 +00:00
weechat . hook_info ( ' autosort_escape ' , info_escape_description , info_escape_arguments , ' on_info_escape ' , ' ' )
2018-08-09 00:20:19 +00:00
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 ' , ' ' )
apply_config ( )