]>
git.rmz.io Git - dotfiles.git/blob - weechat/python/autosort.py
1 # -*- coding: utf-8 -*-
3 # Copyright (C) 2013-2017 Maarten de Vries <maarten@de-vri.es>
5 # This program is free software; you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation; either version 3 of the License, or
8 # (at your option) any later version.
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
15 # You should have received a copy of the GNU General Public License
16 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 # Autosort automatically keeps your buffers sorted and grouped by server.
21 # You can define your own sorting rules. See /help autosort for more details.
23 # https://github.com/de-vri-es/weechat-autosort
29 # * Fix exception in `/autosort helpers swap`.
31 # * Remove `buffers.pl` from recommended settings.
33 # * Fix relative sorting on script name in default rules.
34 # * Document a useful property of stable sort algorithms.
36 # * Make default rules work with bitlbee, matrix and slack.
38 # * Add more documentation on provided info hooks.
40 # * Add ${info:autosort_escape,...} to escape arguments for other info hooks.
42 # * Fix rate-limit of sorting to prevent high CPU load and lock-ups.
43 # * Fix bug in parsing empty arguments for info hooks.
44 # * Add debug_log option to aid with debugging.
45 # * Correct a few typos.
47 # * Fix the /autosort debug command for unicode.
48 # * Update the default rules to work better with Slack.
50 # * Fix python3 compatiblity.
52 # * Use colors to format the help text.
54 # * Switch to evaluated expressions for sorting.
55 # * Add `/autosort debug` command.
56 # * Add ${info:autosort_replace,from,to,text} to replace substrings in sort rules.
57 # * Add ${info:autosort_order,value,first,second,third} to ease writing sort rules.
58 # * Make tab completion context aware.
60 # * Fix compatibility with python 3 regarding unicode handling.
62 # * Fix sorting of buffers with spaces in their name.
64 # * Ignore case in rules when doing case insensitive sorting.
66 # * Fix handling unicode buffer names.
67 # * Add hint to set irc.look.server_buffer to independent and buffers.look.indenting to on.
69 # * Make script python3 compatible.
71 # * Fix sorting items without score last (regressed in 2.2).
73 # * Add configuration option for signals that trigger a sort.
74 # * Add command to manually trigger a sort (/autosort sort).
75 # * Add replacement patterns to apply before sorting.
77 # * Fix some minor style issues.
79 # * Allow for custom sort rules.
90 SCRIPT_NAME
= 'autosort'
91 SCRIPT_AUTHOR
= 'Maarten de Vries <maarten@de-vri.es>'
92 SCRIPT_VERSION
= '3.10'
93 SCRIPT_LICENSE
= 'GPL3'
94 SCRIPT_DESC
= 'Flexible automatic (or manual) buffer sorting based on eval expressions.'
99 signal_delay_timer
= None
100 sort_limit_timer
= None
104 # Make sure that unicode, bytes and str are always available in python2 and 3.
105 # For python 2, str == bytes
106 # For python 3, str == unicode
107 if sys
. version_info
[ 0 ] >= 3 :
110 def ensure_str ( input ):
112 Make sure the given type if the correct string type for the current python version.
113 That means bytes for python2 and unicode for python3.
115 if not isinstance ( input , str ):
116 if isinstance ( input , bytes ):
117 return input . encode ( 'utf-8' )
118 if isinstance ( input , unicode ):
119 return input . decode ( 'utf-8' )
123 if hasattr ( time
, 'perf_counter' ):
124 perf_counter
= time
. perf_counter
126 perf_counter
= time
. clock
128 def casefold ( string
):
129 if hasattr ( string
, 'casefold' ): return string
. casefold ()
130 # Fall back to lowercasing for python2.
131 return string
. lower ()
133 def list_swap ( values
, a
, b
):
134 values
[ a
], values
[ b
] = values
[ b
], values
[ a
]
136 def list_move ( values
, old_index
, new_index
):
137 values
. insert ( new_index
, values
. pop ( old_index
))
139 def list_find ( collection
, value
):
140 for i
, elem
in enumerate ( collection
):
141 if elem
== value
: return i
144 class HumanReadableError ( Exception ):
147 def parse_int ( arg
, arg_name
= 'argument' ):
148 ''' Parse an integer and provide a more human readable error. '''
153 raise HumanReadableError ( 'Invalid {0} : expected integer, got " {1} ".' . format ( arg_name
, arg
))
155 def decode_rules ( blob
):
156 parsed
= json
. loads ( blob
)
157 if not isinstance ( parsed
, list ):
158 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
)))
161 for i
, entry
in enumerate ( parsed
):
162 if not isinstance ( entry
, ( str , unicode )):
163 log ( 'Rule # {0} is not a string but a {1} . No rules have been loaded. Please fix the setting manually.' . format ( i
, type ( entry
)))
168 def decode_helpers ( blob
):
169 parsed
= json
. loads ( blob
)
170 if not isinstance ( parsed
, dict ):
171 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
)))
174 for key
, value
in parsed
. items ():
175 if not isinstance ( value
, ( str , unicode )):
176 log ( 'Helper " {0} " is not a string but a {1} . No helpers have been loaded. Please fix setting manually.' . format ( key
, type ( value
)))
181 ''' The autosort configuration. '''
183 default_rules
= json
. dumps ([
185 '$ {info:autosort_order,${info:autosort_escape,${script_or_plugin} },core,*,irc,bitlbee,matrix,slack}' ,
186 '$ {script_or_plugin} ' ,
189 '$ {info:autosort_order,${type} ,server,*,channel,private}' ,
191 '$ {buffer.full_name} ' ,
194 default_helpers
= json
. dumps ({
195 'core_first' : '$ {if:${buffer.full_name} !=core.weechat}' ,
196 'irc_raw_first' : '$ {if:${buffer.full_name} !=irc.irc_raw}' ,
197 'irc_raw_last' : '$ {if:${buffer.full_name} ==irc.irc_raw}' ,
198 'hashless_name' : '$ {info:autosort_replace,#,,${info:autosort_escape,${buffer.name} }}' ,
199 'script_or_plugin' : '$ {if:${script_name} ?$ {script_name} :$ {plugin} }' ,
202 default_signal_delay
= 5
203 default_sort_limit
= 100
205 default_signals
= 'buffer_opened buffer_merged buffer_unmerged buffer_renamed'
207 def __init__ ( self
, filename
):
208 ''' Initialize the configuration. '''
210 self
. filename
= filename
211 self
. config_file
= weechat
. config_new ( self
. filename
, '' , '' )
212 self
. sorting_section
= None
213 self
. v3_section
= None
215 self
. case_sensitive
= False
219 self
. signal_delay
= Config
. default_signal_delay
,
220 self
. sort_limit
= Config
. default_sort_limit
,
221 self
. sort_on_config
= True
222 self
. debug_log
= False
224 self
.__ case
_ sensitive
= None
226 self
.__ helpers
= None
227 self
.__ signals
= None
228 self
.__ signal
_ delay
= None
229 self
.__ sort
_l imit
= None
230 self
.__ sort
_ on
_ config
= None
231 self
.__ debug
_l og
= None
233 if not self
. config_file
:
234 log ( 'Failed to initialize configuration file " {0} ".' . format ( self
. filename
))
237 self
. sorting_section
= weechat
. config_new_section ( self
. config_file
, 'sorting' , False , False , '' , '' , '' , '' , '' , '' , '' , '' , '' , '' )
238 self
. v3_section
= weechat
. config_new_section ( self
. config_file
, 'v3' , False , False , '' , '' , '' , '' , '' , '' , '' , '' , '' , '' )
240 if not self
. sorting_section
:
241 log ( 'Failed to initialize section "sorting" of configuration file.' )
242 weechat
. config_free ( self
. config_file
)
245 self
.__ case
_ sensitive
= weechat
. config_new_option (
246 self
. config_file
, self
. sorting_section
,
247 'case_sensitive' , 'boolean' ,
248 'If this option is on, sorting is case sensitive.' ,
249 '' , 0 , 0 , 'off' , 'off' , 0 ,
250 '' , '' , '' , '' , '' , ''
253 weechat
. config_new_option (
254 self
. config_file
, self
. sorting_section
,
256 'Sort rules used by autosort v2.x and below. Not used by autosort anymore.' ,
258 '' , '' , '' , '' , '' , ''
261 weechat
. config_new_option (
262 self
. config_file
, self
. sorting_section
,
263 'replacements' , 'string' ,
264 'Replacement patterns used by autosort v2.x and below. Not used by autosort anymore.' ,
266 '' , '' , '' , '' , '' , ''
269 self
.__ rules
= weechat
. config_new_option (
270 self
. config_file
, self
. v3_section
,
272 'An ordered list of sorting rules encoded as JSON. See /help autosort for commands to manipulate these rules.' ,
273 '' , 0 , 0 , Config
. default_rules
, Config
. default_rules
, 0 ,
274 '' , '' , '' , '' , '' , ''
277 self
.__ helpers
= weechat
. config_new_option (
278 self
. config_file
, self
. v3_section
,
280 'A dictionary helper variables to use in the sorting rules, encoded as JSON. See /help autosort for commands to manipulate these helpers.' ,
281 '' , 0 , 0 , Config
. default_helpers
, Config
. default_helpers
, 0 ,
282 '' , '' , '' , '' , '' , ''
285 self
.__ signals
= weechat
. config_new_option (
286 self
. config_file
, self
. sorting_section
,
288 'A space separated list of signals that will cause autosort to resort your buffer list.' ,
289 '' , 0 , 0 , Config
. default_signals
, Config
. default_signals
, 0 ,
290 '' , '' , '' , '' , '' , ''
293 self
.__ signal
_ delay
= weechat
. config_new_option (
294 self
. config_file
, self
. sorting_section
,
295 'signal_delay' , 'integer' ,
296 '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.' ,
297 '' , 0 , 1000 , str ( Config
. default_signal_delay
), str ( Config
. default_signal_delay
), 0 ,
298 '' , '' , '' , '' , '' , ''
301 self
.__ sort
_l imit
= weechat
. config_new_option (
302 self
. config_file
, self
. sorting_section
,
303 'sort_limit' , 'integer' ,
304 '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.' ,
305 '' , 0 , 1000 , str ( Config
. default_sort_limit
), str ( Config
. default_sort_limit
), 0 ,
306 '' , '' , '' , '' , '' , ''
309 self
.__ sort
_ on
_ config
= weechat
. config_new_option (
310 self
. config_file
, self
. sorting_section
,
311 'sort_on_config_change' , 'boolean' ,
312 'Decides if the buffer list should be sorted when autosort configuration changes.' ,
313 '' , 0 , 0 , 'on' , 'on' , 0 ,
314 '' , '' , '' , '' , '' , ''
317 self
.__ debug
_l og
= weechat
. config_new_option (
318 self
. config_file
, self
. sorting_section
,
319 'debug_log' , 'boolean' ,
320 'If enabled, print more debug messages. Not recommended for normal usage.' ,
321 '' , 0 , 0 , 'off' , 'off' , 0 ,
322 '' , '' , '' , '' , '' , ''
325 if weechat
. config_read ( self
. config_file
) != weechat
. WEECHAT_RC_OK
:
326 log ( 'Failed to load configuration file.' )
328 if weechat
. config_write ( self
. config_file
) != weechat
. WEECHAT_RC_OK
:
329 log ( 'Failed to write configuration file.' )
334 ''' Load configuration variables. '''
336 self
. case_sensitive
= weechat
. config_boolean ( self
.__ case
_ sensitive
)
338 rules_blob
= weechat
. config_string ( self
.__ rules
)
339 helpers_blob
= weechat
. config_string ( self
.__ helpers
)
340 signals_blob
= weechat
. config_string ( self
.__ signals
)
342 self
. rules
= decode_rules ( rules_blob
)
343 self
. helpers
= decode_helpers ( helpers_blob
)
344 self
. signals
= signals_blob
. split ()
345 self
. signal_delay
= weechat
. config_integer ( self
.__ signal
_ delay
)
346 self
. sort_limit
= weechat
. config_integer ( self
.__ sort
_l imit
)
347 self
. sort_on_config
= weechat
. config_boolean ( self
.__ sort
_ on
_ config
)
348 self
. debug_log
= weechat
. config_boolean ( self
.__ debug
_l og
)
350 def save_rules ( self
, run_callback
= True ):
351 ''' Save the current rules to the configuration. '''
352 weechat
. config_option_set ( self
.__ rules
, json
. dumps ( self
. rules
), run_callback
)
354 def save_helpers ( self
, run_callback
= True ):
355 ''' Save the current helpers to the configuration. '''
356 weechat
. config_option_set ( self
.__ helpers
, json
. dumps ( self
. helpers
), run_callback
)
359 def pad ( sequence
, length
, padding
= None ):
360 ''' Pad a list until is has a certain length. '''
361 return sequence
+ [ padding
] * max ( 0 , ( length
- len ( sequence
)))
363 def log ( message
, buffer = 'NULL' ):
364 weechat
. prnt ( buffer , 'autosort: {0} ' . format ( message
))
366 def debug ( message
, buffer = 'NULL' ):
368 weechat
. prnt ( buffer , 'autosort: debug: {0} ' . format ( message
))
371 ''' Get a list of all the buffers in weechat. '''
372 hdata
= weechat
. hdata_get ( 'buffer' )
373 buffer = weechat
. hdata_get_list ( hdata
, "gui_buffers" );
377 number
= weechat
. hdata_integer ( hdata
, buffer , 'number' )
378 result
. append (( number
, buffer ))
379 buffer = weechat
. hdata_pointer ( hdata
, buffer , 'next_buffer' )
382 class MergedBuffers ( list ):
383 """ A list of merged buffers, possibly of size 1. """
384 def __init__ ( self
, number
):
385 super ( MergedBuffers
, self
) .__ init
__ ()
388 def merge_buffer_list ( buffers
):
390 Group merged buffers together.
391 The output is a list of MergedBuffers.
393 if not buffers
: return []
395 for number
, buffer in buffers
:
396 if number
not in result
: result
[ number
] = MergedBuffers ( number
)
397 result
[ number
]. append ( buffer )
398 return result
. values ()
400 def sort_buffers ( hdata
, buffers
, rules
, helpers
, case_sensitive
):
401 for merged
in buffers
:
402 for buffer in merged
:
403 name
= weechat
. hdata_string ( hdata
, buffer , 'name' )
405 return sorted ( buffers
, key
= merged_sort_key ( rules
, helpers
, case_sensitive
))
407 def buffer_sort_key ( rules
, helpers
, case_sensitive
):
408 ''' Create a sort key function for a list of lists of merged buffers. '''
411 for helper_name
, helper
in sorted ( helpers
. items ()):
412 expanded
= weechat
. string_eval_expression ( helper
, {"buffer": buffer}
, {}, {}
)
413 extra_vars
[ helper_name
] = expanded
if case_sensitive
else casefold ( expanded
)
416 expanded
= weechat
. string_eval_expression ( rule
, {"buffer": buffer}
, extra_vars
, {})
417 result
. append ( expanded
if case_sensitive
else casefold ( expanded
))
422 def merged_sort_key ( rules
, helpers
, case_sensitive
):
423 buffer_key
= buffer_sort_key ( rules
, helpers
, case_sensitive
)
426 for buffer in merged
:
427 this
= buffer_key ( buffer )
428 if best
is None or this
< best
: best
= this
432 def apply_buffer_order ( buffers
):
433 ''' Sort the buffers in weechat according to the given order. '''
434 for i
, buffer in enumerate ( buffers
):
435 weechat
. buffer_set ( buffer [ 0 ], "number" , str ( i
+ 1 ))
437 def split_args ( args
, expected
, optional
= 0 ):
438 ''' Split an argument string in the desired number of arguments. '''
439 split
= args
. split ( ' ' , expected
- 1 )
440 if ( len ( split
) < expected
):
441 raise HumanReadableError ( 'Expected at least {0} arguments, got {1} .' . format ( expected
, len ( split
)))
442 return split
[:- 1 ] + pad ( split
[- 1 ]. split ( ' ' , optional
), optional
+ 1 , '' )
444 def do_sort ( verbose
= False ):
445 start
= perf_counter ()
447 hdata
, buffers
= get_buffers ()
448 buffers
= merge_buffer_list ( buffers
)
449 buffers
= sort_buffers ( hdata
, buffers
, config
. rules
, config
. helpers
, config
. case_sensitive
)
450 apply_buffer_order ( buffers
)
452 elapsed
= perf_counter () - start
454 log ( "Finished sorting buffers in {0:.4f} seconds." . format ( elapsed
))
456 debug ( "Finished sorting buffers in {0:.4f} seconds." . format ( elapsed
))
458 def command_sort ( buffer , command
, args
):
459 ''' Sort the buffers and print a confirmation. '''
461 return weechat
. WEECHAT_RC_OK
463 def command_debug ( buffer , command
, args
):
464 hdata
, buffers
= get_buffers ()
465 buffers
= merge_buffer_list ( buffers
)
467 # Show evaluation results.
468 log ( 'Individual evaluation results:' )
469 start
= perf_counter ()
470 key
= buffer_sort_key ( config
. rules
, config
. helpers
, config
. case_sensitive
)
472 for merged
in buffers
:
473 for buffer in merged
:
474 fullname
= weechat
. hdata_string ( hdata
, buffer , 'full_name' )
475 results
. append (( fullname
, key ( buffer )))
476 elapsed
= perf_counter () - start
478 for fullname
, result
in results
:
479 fullname
= ensure_str ( fullname
)
480 result
= [ ensure_str ( x
) for x
in result
]
481 log ( ' {0} : {1} ' . format ( fullname
, result
))
482 log ( 'Computing evaluation results took {0:.4f} seconds.' . format ( elapsed
))
484 return weechat
. WEECHAT_RC_OK
486 def command_rule_list ( buffer , command
, args
):
487 ''' Show the list of sorting rules. '''
488 output
= 'Sorting rules: \n '
489 for i
, rule
in enumerate ( config
. rules
):
490 output
+= ' {0} : {1} \n ' . format ( i
, rule
)
491 if not len ( config
. rules
):
492 output
+= ' No sorting rules configured. \n '
495 return weechat
. WEECHAT_RC_OK
498 def command_rule_add ( buffer , command
, args
):
499 ''' Add a rule to the rule list. '''
500 config
. rules
. append ( args
)
502 command_rule_list ( buffer , command
, '' )
504 return weechat
. WEECHAT_RC_OK
507 def command_rule_insert ( buffer , command
, args
):
508 ''' Insert a rule at the desired position in the rule list. '''
509 index
, rule
= split_args ( args
, 2 )
510 index
= parse_int ( index
, 'index' )
512 config
. rules
. insert ( index
, rule
)
514 command_rule_list ( buffer , command
, '' )
515 return weechat
. WEECHAT_RC_OK
518 def command_rule_update ( buffer , command
, args
):
519 ''' Update a rule in the rule list. '''
520 index
, rule
= split_args ( args
, 2 )
521 index
= parse_int ( index
, 'index' )
523 config
. rules
[ index
] = rule
525 command_rule_list ( buffer , command
, '' )
526 return weechat
. WEECHAT_RC_OK
529 def command_rule_delete ( buffer , command
, args
):
530 ''' Delete a rule from the rule list. '''
532 index
= parse_int ( index
, 'index' )
534 config
. rules
. pop ( index
)
536 command_rule_list ( buffer , command
, '' )
537 return weechat
. WEECHAT_RC_OK
540 def command_rule_move ( buffer , command
, args
):
541 ''' Move a rule to a new position. '''
542 index_a
, index_b
= split_args ( args
, 2 )
543 index_a
= parse_int ( index_a
, 'index' )
544 index_b
= parse_int ( index_b
, 'index' )
546 list_move ( config
. rules
, index_a
, index_b
)
548 command_rule_list ( buffer , command
, '' )
549 return weechat
. WEECHAT_RC_OK
552 def command_rule_swap ( buffer , command
, args
):
553 ''' Swap two rules. '''
554 index_a
, index_b
= split_args ( args
, 2 )
555 index_a
= parse_int ( index_a
, 'index' )
556 index_b
= parse_int ( index_b
, 'index' )
558 list_swap ( config
. rules
, index_a
, index_b
)
560 command_rule_list ( buffer , command
, '' )
561 return weechat
. WEECHAT_RC_OK
564 def command_helper_list ( buffer , command
, args
):
565 ''' Show the list of helpers. '''
566 output
= 'Helper variables: \n '
568 width
= max ( map ( lambda x
: len ( x
) if len ( x
) <= 30 else 0 , config
. helpers
. keys ()))
570 for name
, expression
in sorted ( config
. helpers
. items ()):
571 output
+= ' {0:>{width} }: {1} \n ' . format ( name
, expression
, width
= width
)
572 if not len ( config
. helpers
):
573 output
+= ' No helper variables configured.'
576 return weechat
. WEECHAT_RC_OK
579 def command_helper_set ( buffer , command
, args
):
580 ''' Add/update a helper to the helper list. '''
581 name
, expression
= split_args ( args
, 2 )
583 config
. helpers
[ name
] = expression
584 config
. save_helpers ()
585 command_helper_list ( buffer , command
, '' )
587 return weechat
. WEECHAT_RC_OK
589 def command_helper_delete ( buffer , command
, args
):
590 ''' Delete a helper from the helper list. '''
593 del config
. helpers
[ name
]
594 config
. save_helpers ()
595 command_helper_list ( buffer , command
, '' )
596 return weechat
. WEECHAT_RC_OK
599 def command_helper_rename ( buffer , command
, args
):
600 ''' Rename a helper to a new position. '''
601 old_name
, new_name
= split_args ( args
, 2 )
604 config
. helpers
[ new_name
] = config
. helpers
[ old_name
]
605 del config
. helpers
[ old_name
]
607 raise HumanReadableError ( 'No such helper: {0} ' . format ( old_name
))
608 config
. save_helpers ()
609 command_helper_list ( buffer , command
, '' )
610 return weechat
. WEECHAT_RC_OK
613 def command_helper_swap ( buffer , command
, args
):
614 ''' Swap two helpers. '''
615 a
, b
= split_args ( args
, 2 )
617 config
. helpers
[ b
], config
. helpers
[ a
] = config
. helpers
[ a
], config
. helpers
[ b
]
618 except KeyError as e
:
619 raise HumanReadableError ( 'No such helper: {0} ' . format ( e
. args
[ 0 ]))
621 config
. save_helpers ()
622 command_helper_list ( buffer , command
, '' )
623 return weechat
. WEECHAT_RC_OK
625 def call_command ( buffer , command
, args
, subcommands
):
626 ''' Call a subcommand from a dictionary. '''
627 subcommand
, tail
= pad ( args
. split ( ' ' , 1 ), 2 , '' )
628 subcommand
= subcommand
. strip ()
629 if ( subcommand
== '' ):
630 child
= subcommands
. get ( ' ' )
632 command
= command
+ [ subcommand
]
633 child
= subcommands
. get ( subcommand
)
635 if isinstance ( child
, dict ):
636 return call_command ( buffer , command
, tail
, child
)
637 elif callable ( child
):
638 return child ( buffer , command
, tail
)
640 log ( ' {0} : command not found' . format ( ' ' . join ( command
)))
641 return weechat
. WEECHAT_RC_ERROR
643 def on_signal ( data
, signal
, signal_data
):
644 global signal_delay_timer
647 # If the sort limit timeout is started, we're in the hold-off time after sorting, just queue a sort.
648 if sort_limit_timer
is not None :
650 debug ( 'Signal {0} ignored, sort limit timeout is active and sort is already queued.' . format ( signal
))
652 debug ( 'Signal {0} received but sort limit timeout is active, sort is now queued.' . format ( signal
))
654 return weechat
. WEECHAT_RC_OK
656 # If the signal delay timeout is started, a signal was recently received, so ignore this signal.
657 if signal_delay_timer
is not None :
658 debug ( 'Signal {0} ignored, signal delay timeout active.' . format ( signal
))
659 return weechat
. WEECHAT_RC_OK
661 # Otherwise, start the signal delay timeout.
662 debug ( 'Signal {0} received, starting signal delay timeout of {1} ms.' . format ( signal
, config
. signal_delay
))
663 weechat
. hook_timer ( config
. signal_delay
, 0 , 1 , "on_signal_delay_timeout" , "" )
664 return weechat
. WEECHAT_RC_OK
666 def on_signal_delay_timeout ( pointer
, remaining_calls
):
667 """ Called when the signal_delay_timer triggers. """
668 global signal_delay_timer
669 global sort_limit_timer
672 signal_delay_timer
= None
674 # If the sort limit timeout was started, we're still in the no-sort period, so just queue a sort.
675 if sort_limit_timer
is not None :
676 debug ( 'Signal delay timeout expired, but sort limit timeout is active, sort is now queued.' )
678 return weechat
. WEECHAT_RC_OK
681 debug ( 'Signal delay timeout expired, starting sort.' )
684 # Start the sort limit timeout if not disabled.
685 if config
. sort_limit
> 0 :
686 debug ( 'Starting sort limit timeout of {0} ms.' . format ( config
. sort_limit
))
687 sort_limit_timer
= weechat
. hook_timer ( config
. sort_limit
, 0 , 1 , "on_sort_limit_timeout" , "" )
689 return weechat
. WEECHAT_RC_OK
691 def on_sort_limit_timeout ( pointer
, remainin_calls
):
692 """ Called when de sort_limit_timer triggers. """
693 global sort_limit_timer
696 # If no signal was received during the timeout, we're done.
698 debug ( 'Sort limit timeout expired without receiving a signal.' )
699 sort_limit_timer
= None
700 return weechat
. WEECHAT_RC_OK
702 # Otherwise it's time to sort.
703 debug ( 'Signal received during sort limit timeout, starting queued sort.' )
707 # Start the sort limit timeout again if not disabled.
708 if config
. sort_limit
> 0 :
709 debug ( 'Starting sort limit timeout of {0} ms.' . format ( config
. sort_limit
))
710 sort_limit_timer
= weechat
. hook_timer ( config
. sort_limit
, 0 , 1 , "on_sort_limit_timeout" , "" )
712 return weechat
. WEECHAT_RC_OK
716 # Unhook all signals and hook the new ones.
719 for signal
in config
. signals
:
720 hooks
. append ( weechat
. hook_signal ( signal
, 'on_signal' , '' ))
722 if config
. sort_on_config
:
723 debug ( 'Sorting because configuration changed.' )
726 def on_config_changed (* args
, ** kwargs
):
727 ''' Called whenever the configuration changes. '''
731 return weechat
. WEECHAT_RC_OK
734 if not args
: return '' , None
738 for i
, c
in enumerate ( args
):
744 return result
, args
[ i
+ 1 :]
749 def parse_args ( args
, max = None ):
752 while max is None or i
< max :
754 arg
, args
= parse_arg ( args
)
755 if arg
is None : break
757 if args
is None : break
760 def on_info_escape ( pointer
, name
, arguments
):
771 def on_info_replace ( pointer
, name
, arguments
):
772 arguments
, rest
= parse_args ( arguments
, 3 )
773 if rest
or len ( arguments
) < 3 :
774 log ( 'usage: $ {{info:{0} ,old,new,text}}' . format ( name
))
776 old
, new
, text
= arguments
778 return text
. replace ( old
, new
)
780 def on_info_order ( pointer
, name
, arguments
):
781 arguments
, rest
= parse_args ( arguments
)
782 if len ( arguments
) < 1 :
783 log ( 'usage: $ {{info:{0} ,value,first,second,third,...}}' . format ( name
))
788 if not keys
: return '0'
790 # Find the value in the keys (or '*' if we can't find it)
791 result
= list_find ( keys
, value
)
792 if result
is None : result
= list_find ( keys
, '*' )
793 if result
is None : result
= len ( keys
)
795 # Pad result with leading zero to make sure string sorting works.
796 width
= int ( math
. log10 ( len ( keys
))) + 1
797 return ' {0:0{1} }' . format ( result
, width
)
800 def on_autosort_command ( data
, buffer , args
):
801 ''' Called when the autosort command is invoked. '''
803 return call_command ( buffer , [ '/autosort' ], args
, {
805 'sort' : command_sort
,
806 'debug' : command_debug
,
809 ' ' : command_rule_list
,
810 'list' : command_rule_list
,
811 'add' : command_rule_add
,
812 'insert' : command_rule_insert
,
813 'update' : command_rule_update
,
814 'delete' : command_rule_delete
,
815 'move' : command_rule_move
,
816 'swap' : command_rule_swap
,
819 ' ' : command_helper_list
,
820 'list' : command_helper_list
,
821 'set' : command_helper_set
,
822 'delete' : command_helper_delete
,
823 'rename' : command_helper_rename
,
824 'swap' : command_helper_swap
,
827 except HumanReadableError
as e
:
829 return weechat
. WEECHAT_RC_ERROR
831 def add_completions ( completion
, words
):
833 weechat
. completion_list_add ( completion
, word
, 0 , weechat
. WEECHAT_LIST_POS_END
)
835 def autosort_complete_rules ( words
, completion
):
837 add_completions ( completion
, [ 'add' , 'delete' , 'insert' , 'list' , 'move' , 'swap' , 'update' ])
838 if len ( words
) == 1 and words
[ 0 ] in ( 'delete' , 'insert' , 'move' , 'swap' , 'update' ):
839 add_completions ( completion
, map ( str , range ( len ( config
. rules
))))
840 if len ( words
) == 2 and words
[ 0 ] in ( 'move' , 'swap' ):
841 add_completions ( completion
, map ( str , range ( len ( config
. rules
))))
842 if len ( words
) == 2 and words
[ 0 ] in ( 'update' ):
844 add_completions ( completion
, [ config
. rules
[ int ( words
[ 1 ])]])
845 except KeyError : pass
846 except ValueError : pass
848 add_completions ( completion
, [ '' ])
849 return weechat
. WEECHAT_RC_OK
851 def autosort_complete_helpers ( words
, completion
):
853 add_completions ( completion
, [ 'delete' , 'list' , 'rename' , 'set' , 'swap' ])
854 elif len ( words
) == 1 and words
[ 0 ] in ( 'delete' , 'rename' , 'set' , 'swap' ):
855 add_completions ( completion
, sorted ( config
. helpers
. keys ()))
856 elif len ( words
) == 2 and words
[ 0 ] == 'swap' :
857 add_completions ( completion
, sorted ( config
. helpers
. keys ()))
858 elif len ( words
) == 2 and words
[ 0 ] == 'rename' :
859 add_completions ( completion
, sorted ( config
. helpers
. keys ()))
860 elif len ( words
) == 2 and words
[ 0 ] == 'set' :
862 add_completions ( completion
, [ config
. helpers
[ words
[ 1 ]]])
863 except KeyError : pass
864 return weechat
. WEECHAT_RC_OK
866 def on_autosort_complete ( data
, name
, buffer , completion
):
867 cmdline
= weechat
. buffer_get_string ( buffer , "input" )
868 cursor
= weechat
. buffer_get_integer ( buffer , "input_pos" )
869 prefix
= cmdline
[: cursor
]
870 words
= prefix
. split ()[ 1 :]
872 # If the current word isn't finished yet,
873 # ignore it for coming up with completion suggestions.
874 if prefix
[- 1 ] != ' ' : words
= words
[:- 1 ]
877 add_completions ( completion
, [ 'debug' , 'helpers' , 'rules' , 'sort' ])
878 elif words
[ 0 ] == 'rules' :
879 return autosort_complete_rules ( words
[ 1 :], completion
)
880 elif words
[ 0 ] == 'helpers' :
881 return autosort_complete_helpers ( words
[ 1 :], completion
)
882 return weechat
. WEECHAT_RC_OK
884 command_description
= r
''' {*white} # General commands {reset}
886 {*white} /autosort {brown} sort {reset}
887 Manually trigger the buffer sorting.
889 {*white} /autosort {brown} debug {reset}
890 Show the evaluation results of the sort rules for each buffer.
893 {*white} # Sorting rule commands {reset}
895 {*white} /autosort {brown} rules list {reset}
896 Print the list of sort rules.
898 {*white} /autosort {brown} rules add {cyan} <expression> {reset}
899 Add a new rule at the end of the list.
901 {*white} /autosort {brown} rules insert {cyan} <index> <expression> {reset}
902 Insert a new rule at the given index in the list.
904 {*white} /autosort {brown} rules update {cyan} <index> <expression> {reset}
905 Update a rule in the list with a new expression.
907 {*white} /autosort {brown} rules delete {cyan} <index>
908 Delete a rule from the list.
910 {*white} /autosort {brown} rules move {cyan} <index_from> <index_to> {reset}
911 Move a rule from one position in the list to another.
913 {*white} /autosort {brown} rules swap {cyan} <index_a> <index_b> {reset}
914 Swap two rules in the list
917 {*white} # Helper variable commands {reset}
919 {*white} /autosort {brown} helpers list
920 Print the list of helper variables.
922 {*white} /autosort {brown} helpers set {cyan} <name> <expression>
923 Add or update a helper variable with the given name.
925 {*white} /autosort {brown} helpers delete {cyan} <name>
926 Delete a helper variable.
928 {*white} /autosort {brown} helpers rename {cyan} <old_name> <new_name>
929 Rename a helper variable.
931 {*white} /autosort {brown} helpers swap {cyan} <name_a> <name_b>
932 Swap the expressions of two helper variables in the list.
935 {*white} # Info hooks {reset}
936 Autosort comes with a number of info hooks to add some extra functionality to regular weechat eval strings.
937 Info hooks can be used in eval strings in the form of {cyan} $ {{info:some_hook,arguments} } {reset} .
939 Commas and backslashes in arguments to autosort info hooks (except for {cyan} $ {{info:autosort_escape} } {reset} ) must be escaped with a backslash.
941 {*white} $ {{info:{brown} autosort_replace {white} , {cyan} pattern {white} , {cyan} replacement {white} , {cyan} source {white} }} {reset}
942 Replace all occurrences of {cyan} pattern {reset} with {cyan} replacement {reset} in the string {cyan} source {reset} .
943 Can be used to ignore certain strings when sorting by replacing them with an empty string.
945 For example: {cyan} $ {{info:autosort_replace,cat,dog,the dog is meowing} } {reset} expands to "the cat is meowing".
947 {*white} $ {{info:{brown} autosort_order {white} , {cyan} value {white} , {cyan} option0 {white} , {cyan} option1 {white} , {cyan} option2 {white} , {cyan} ... {white} }}
948 Generate a zero-padded number that corresponds to the index of {cyan} value {reset} in the list of options.
949 If one of the options is the special value {brown} * {reset} , then any value not explicitly mentioned will be sorted at that position.
950 Otherwise, any value that does not match an option is assigned the highest number available.
951 Can be used to easily sort buffers based on a manual sequence.
953 For example: {cyan} $ {{info:autosort_order,${{server} },freenode,oftc,efnet}} {reset} will sort freenode before oftc, followed by efnet and then any remaining servers.
954 Alternatively, {cyan} $ {{info:autosort_order,${{server} },freenode,oftc,*,efnet}} {reset} will sort any unlisted servers after freenode and oftc, but before efnet.
956 {*white} $ {{info:{brown} autosort_escape {white} , {cyan} text {white} }} {reset}
957 Escape commas and backslashes in {cyan} text {reset} by prepending them with a backslash.
958 This is mainly useful to pass arbitrary eval strings as arguments to other autosort info hooks.
959 Otherwise, an eval string that expands to something with a comma would be interpreted as multiple arguments.
961 For example, it can be used to safely pass buffer names to {cyan} $ {{info:autosort_replace} } {reset} like so:
962 {cyan} $ {{info:autosort_replace,##,#,${{info:autosort_escape,${{buffer.name} }}}}} {reset} .
965 {*white} # Description
966 Autosort is a weechat script to automatically keep your buffers sorted. The sort
967 order can be customized by defining your own sort rules, but the default should
968 be sane enough for most people. It can also group IRC channel/private buffers
969 under their server buffer if you like.
971 Autosort uses a stable sorting algorithm, meaning that you can manually move buffers
972 to change their relative order, if they sort equal with your rule set.
974 {*white} # Sort rules {reset}
975 Autosort evaluates a list of eval expressions (see {*default} /help eval {reset} ) and sorts the
976 buffers based on evaluated result. Earlier rules will be considered first. Only
977 if earlier rules produced identical results is the result of the next rule
978 considered for sorting purposes.
980 You can debug your sort rules with the ` {*default} /autosort debug {reset} ` command, which will
981 print the evaluation results of each rule for each buffer.
983 {*brown} NOTE: {reset} The sort rules for version 3 are not compatible with version 2 or vice
984 versa. You will have to manually port your old rules to version 3 if you have any.
986 {*white} # Helper variables {reset}
987 You may define helper variables for the main sort rules to keep your rules
988 readable. They can be used in the main sort rules as variables. For example,
989 a helper variable named ` {cyan} foo {reset} ` can be accessed in a main rule with the
990 string ` {cyan} $ {{foo} } {reset} `.
992 {*white} # Automatic or manual sorting {reset}
993 By default, autosort will automatically sort your buffer list whenever a buffer
994 is opened, merged, unmerged or renamed. This should keep your buffers sorted in
995 almost all situations. However, you may wish to change the list of signals that
996 cause your buffer list to be sorted. Simply edit the ` {cyan} autosort.sorting.signals {reset} `
997 option to add or remove any signal you like.
999 If you remove all signals you can still sort your buffers manually with the
1000 ` {*default} /autosort sort {reset} ` command. To prevent all automatic sorting, the option
1001 ` {cyan} autosort.sorting.sort_on_config_change {reset} ` should also be disabled.
1003 {*white} # Recommended settings
1004 For the best visual effect, consider setting the following options:
1005 {*white} /set {cyan} irc.look.server_buffer {reset} {brown} independent {reset}
1007 This setting allows server buffers to be sorted independently, which is
1008 needed to create a hierarchical tree view of the server and channel buffers.
1010 If you are using the {*default} buflist {reset} plugin you can (ab)use Unicode to draw a tree
1011 structure with the following setting (modify to suit your need):
1012 {*white} /set {cyan} buflist.format.indent {brown} "$ {{color:237} }$ {{if:${{buffer.next_buffer.local_variables.type} }=~^(channel|private)$?├─:└─}}" {reset}
1015 command_completion
= ' %(plugin_autosort) %(plugin_autosort) %(plugin_autosort) %(plugin_autosort) %(plugin_autosort) '
1017 info_replace_description
= (
1018 'Replace all occurrences of `pattern` with `replacement` in the string `source`. '
1019 'Can be used to ignore certain strings when sorting by replacing them with an empty string. '
1020 'See /help autosort for examples.'
1022 info_replace_arguments
= 'pattern,replacement,source'
1024 info_order_description
= (
1025 'Generate a zero-padded number that corresponds to the index of `value` in the list of options. '
1026 'If one of the options is the special value `*`, then any value not explicitly mentioned will be sorted at that position. '
1027 'Otherwise, any value that does not match an option is assigned the highest number available. '
1028 'Can be used to easily sort buffers based on a manual sequence. '
1029 'See /help autosort for examples.'
1031 info_order_arguments
= 'value,first,second,third,...'
1033 info_escape_description
= (
1034 'Escape commas and backslashes in `text` by prepending them with a backslash. '
1035 'This is mainly useful to pass arbitrary eval strings as arguments to other autosort info hooks. '
1036 'Otherwise, an eval string that expands to something with a comma would be interpreted as multiple arguments.'
1037 'See /help autosort for examples.'
1039 info_escape_arguments
= 'text'
1042 if weechat
. register ( SCRIPT_NAME
, SCRIPT_AUTHOR
, SCRIPT_VERSION
, SCRIPT_LICENSE
, SCRIPT_DESC
, "" , "" ):
1043 config
= Config ( 'autosort' )
1046 'default' : weechat
. color ( 'default' ),
1047 'reset' : weechat
. color ( 'reset' ),
1048 'black' : weechat
. color ( 'black' ),
1049 'red' : weechat
. color ( 'red' ),
1050 'green' : weechat
. color ( 'green' ),
1051 'brown' : weechat
. color ( 'brown' ),
1052 'yellow' : weechat
. color ( 'yellow' ),
1053 'blue' : weechat
. color ( 'blue' ),
1054 'magenta' : weechat
. color ( 'magenta' ),
1055 'cyan' : weechat
. color ( 'cyan' ),
1056 'white' : weechat
. color ( 'white' ),
1057 '*default' : weechat
. color ( '*default' ),
1058 '*black' : weechat
. color ( '*black' ),
1059 '*red' : weechat
. color ( '*red' ),
1060 '*green' : weechat
. color ( '*green' ),
1061 '*brown' : weechat
. color ( '*brown' ),
1062 '*yellow' : weechat
. color ( '*yellow' ),
1063 '*blue' : weechat
. color ( '*blue' ),
1064 '*magenta' : weechat
. color ( '*magenta' ),
1065 '*cyan' : weechat
. color ( '*cyan' ),
1066 '*white' : weechat
. color ( '*white' ),
1069 weechat
. hook_config ( 'autosort.*' , 'on_config_changed' , '' )
1070 weechat
. hook_completion ( 'plugin_autosort' , '' , 'on_autosort_complete' , '' )
1071 weechat
. hook_command ( 'autosort' , command_description
. format (** colors
), '' , '' , command_completion
, 'on_autosort_command' , '' )
1072 weechat
. hook_info ( 'autosort_escape' , info_escape_description
, info_escape_arguments
, 'on_info_escape' , '' )
1073 weechat
. hook_info ( 'autosort_replace' , info_replace_description
, info_replace_arguments
, 'on_info_replace' , '' )
1074 weechat
. hook_info ( 'autosort_order' , info_order_description
, info_order_arguments
, 'on_info_order' , '' )