]>
git.rmz.io Git - dotfiles.git/blob - weechat/python/autosort.py
46a840c34a4d0573a5c007c05da8323728b3c629
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 rate-limit of sorting to prevent high CPU load and lock-ups.
30 # * Fix bug in parsing empty arguments for info hooks.
31 # * Add debug_log option to aid with debugging.
32 # * Correct a few typos.
34 # * Fix the /autosort debug command for unicode.
35 # * Update the default rules to work better with Slack.
37 # * Fix python3 compatiblity.
39 # * Use colors to format the help text.
41 # * Switch to evaluated expressions for sorting.
42 # * Add `/autosort debug` command.
43 # * Add ${info:autosort_replace,from,to,text} to replace substrings in sort rules.
44 # * Add ${info:autosort_order,value,first,second,third} to ease writing sort rules.
45 # * Make tab completion context aware.
47 # * Fix compatibility with python 3 regarding unicode handling.
49 # * Fix sorting of buffers with spaces in their name.
51 # * Ignore case in rules when doing case insensitive sorting.
53 # * Fix handling unicode buffer names.
54 # * Add hint to set irc.look.server_buffer to independent and buffers.look.indenting to on.
56 # * Make script python3 compatible.
58 # * Fix sorting items without score last (regressed in 2.2).
60 # * Add configuration option for signals that trigger a sort.
61 # * Add command to manually trigger a sort (/autosort sort).
62 # * Add replacement patterns to apply before sorting.
64 # * Fix some minor style issues.
66 # * Allow for custom sort rules.
77 SCRIPT_NAME
= 'autosort'
78 SCRIPT_AUTHOR
= 'Maarten de Vries <maarten@de-vri.es>'
79 SCRIPT_VERSION
= '3.4'
80 SCRIPT_LICENSE
= 'GPL3'
81 SCRIPT_DESC
= 'Flexible automatic (or manual) buffer sorting based on eval expressions.'
86 signal_delay_timer
= None
87 sort_limit_timer
= None
91 # Make sure that unicode, bytes and str are always available in python2 and 3.
92 # For python 2, str == bytes
93 # For python 3, str == unicode
94 if sys
. version_info
[ 0 ] >= 3 :
97 def ensure_str ( input ):
99 Make sure the given type if the correct string type for the current python version.
100 That means bytes for python2 and unicode for python3.
102 if not isinstance ( input , str ):
103 if isinstance ( input , bytes ):
104 return input . encode ( 'utf-8' )
105 if isinstance ( input , unicode ):
106 return input . decode ( 'utf-8' )
110 if hasattr ( time
, 'perf_counter' ):
111 perf_counter
= time
. perf_counter
113 perf_counter
= time
. clock
115 def casefold ( string
):
116 if hasattr ( string
, 'casefold' ): return string
. casefold ()
117 # Fall back to lowercasing for python2.
118 return string
. lower ()
120 def list_swap ( values
, a
, b
):
121 values
[ a
], values
[ b
] = values
[ b
], values
[ a
]
123 def list_move ( values
, old_index
, new_index
):
124 values
. insert ( new_index
, values
. pop ( old_index
))
126 def list_find ( collection
, value
):
127 for i
, elem
in enumerate ( collection
):
128 if elem
== value
: return i
131 class HumanReadableError ( Exception ):
134 def parse_int ( arg
, arg_name
= 'argument' ):
135 ''' Parse an integer and provide a more human readable error. '''
140 raise HumanReadableError ( 'Invalid {0} : expected integer, got " {1} ".' . format ( arg_name
, arg
))
142 def decode_rules ( blob
):
143 parsed
= json
. loads ( blob
)
144 if not isinstance ( parsed
, list ):
145 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
)))
148 for i
, entry
in enumerate ( parsed
):
149 if not isinstance ( entry
, ( str , unicode )):
150 log ( 'Rule # {0} is not a string but a {1} . No rules have been loaded. Please fix the setting manually.' . format ( i
, type ( entry
)))
155 def decode_helpers ( blob
):
156 parsed
= json
. loads ( blob
)
157 if not isinstance ( parsed
, dict ):
158 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
)))
161 for key
, value
in parsed
. items ():
162 if not isinstance ( value
, ( str , unicode )):
163 log ( 'Helper " {0} " is not a string but a {1} . No helpers have been loaded. Please fix setting manually.' . format ( key
, type ( value
)))
168 ''' The autosort configuration. '''
170 default_rules
= json
. dumps ([
173 '$ {buffer.plugin.name} ' ,
175 '$ {if:${plugin} ==irc?$ {server} }' ,
176 '$ {if:${plugin} ==irc?$ {info:autosort_order,${type} ,server,*,channel,private}}' ,
177 '$ {if:${plugin} ==irc?$ {hashless_name} }' ,
178 '$ {buffer.full_name} ' ,
181 default_helpers
= json
. dumps ({
182 'core_first' : '$ {if:${buffer.full_name} !=core.weechat}' ,
183 'irc_first' : '$ {if:${buffer.plugin.name} !=irc}' ,
184 'irc_last' : '$ {if:${buffer.plugin.name} ==irc}' ,
185 'irc_raw_first' : '$ {if:${buffer.full_name} !=irc.irc_raw}' ,
186 'irc_raw_last' : '$ {if:${buffer.full_name} ==irc.irc_raw}' ,
187 'hashless_name' : '$ {info:autosort_replace,#,,${buffer.name} }' ,
190 default_signal_delay
= 5
191 default_sort_limit
= 100
193 default_signals
= 'buffer_opened buffer_merged buffer_unmerged buffer_renamed'
195 def __init__ ( self
, filename
):
196 ''' Initialize the configuration. '''
198 self
. filename
= filename
199 self
. config_file
= weechat
. config_new ( self
. filename
, '' , '' )
200 self
. sorting_section
= None
201 self
. v3_section
= None
203 self
. case_sensitive
= False
207 self
. signal_delay
= Config
. default_signal_delay
,
208 self
. sort_limit
= Config
. default_sort_limit
,
209 self
. sort_on_config
= True
210 self
. debug_log
= False
212 self
.__ case
_ sensitive
= None
214 self
.__ helpers
= None
215 self
.__ signals
= None
216 self
.__ signal
_ delay
= None
217 self
.__ sort
_l imit
= None
218 self
.__ sort
_ on
_ config
= None
219 self
.__ debug
_l og
= None
221 if not self
. config_file
:
222 log ( 'Failed to initialize configuration file " {0} ".' . format ( self
. filename
))
225 self
. sorting_section
= weechat
. config_new_section ( self
. config_file
, 'sorting' , False , False , '' , '' , '' , '' , '' , '' , '' , '' , '' , '' )
226 self
. v3_section
= weechat
. config_new_section ( self
. config_file
, 'v3' , False , False , '' , '' , '' , '' , '' , '' , '' , '' , '' , '' )
228 if not self
. sorting_section
:
229 log ( 'Failed to initialize section "sorting" of configuration file.' )
230 weechat
. config_free ( self
. config_file
)
233 self
.__ case
_ sensitive
= weechat
. config_new_option (
234 self
. config_file
, self
. sorting_section
,
235 'case_sensitive' , 'boolean' ,
236 'If this option is on, sorting is case sensitive.' ,
237 '' , 0 , 0 , 'off' , 'off' , 0 ,
238 '' , '' , '' , '' , '' , ''
241 weechat
. config_new_option (
242 self
. config_file
, self
. sorting_section
,
244 'Sort rules used by autosort v2.x and below. Not used by autosort anymore.' ,
246 '' , '' , '' , '' , '' , ''
249 weechat
. config_new_option (
250 self
. config_file
, self
. sorting_section
,
251 'replacements' , 'string' ,
252 'Replacement patterns used by autosort v2.x and below. Not used by autosort anymore.' ,
254 '' , '' , '' , '' , '' , ''
257 self
.__ rules
= weechat
. config_new_option (
258 self
. config_file
, self
. v3_section
,
260 'An ordered list of sorting rules encoded as JSON. See /help autosort for commands to manipulate these rules.' ,
261 '' , 0 , 0 , Config
. default_rules
, Config
. default_rules
, 0 ,
262 '' , '' , '' , '' , '' , ''
265 self
.__ helpers
= weechat
. config_new_option (
266 self
. config_file
, self
. v3_section
,
268 'A dictionary helper variables to use in the sorting rules, encoded as JSON. See /help autosort for commands to manipulate these helpers.' ,
269 '' , 0 , 0 , Config
. default_helpers
, Config
. default_helpers
, 0 ,
270 '' , '' , '' , '' , '' , ''
273 self
.__ signals
= weechat
. config_new_option (
274 self
. config_file
, self
. sorting_section
,
276 'A space separated list of signals that will cause autosort to resort your buffer list.' ,
277 '' , 0 , 0 , Config
. default_signals
, Config
. default_signals
, 0 ,
278 '' , '' , '' , '' , '' , ''
281 self
.__ signal
_ delay
= weechat
. config_new_option (
282 self
. config_file
, self
. sorting_section
,
283 'signal_delay' , 'integer' ,
284 '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.' ,
285 '' , 0 , 1000 , str ( Config
. default_signal_delay
), str ( Config
. default_signal_delay
), 0 ,
286 '' , '' , '' , '' , '' , ''
289 self
.__ sort
_l imit
= weechat
. config_new_option (
290 self
. config_file
, self
. sorting_section
,
291 'sort_limit' , 'integer' ,
292 '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.' ,
293 '' , 0 , 1000 , str ( Config
. default_sort_limit
), str ( Config
. default_sort_limit
), 0 ,
294 '' , '' , '' , '' , '' , ''
297 self
.__ sort
_ on
_ config
= weechat
. config_new_option (
298 self
. config_file
, self
. sorting_section
,
299 'sort_on_config_change' , 'boolean' ,
300 'Decides if the buffer list should be sorted when autosort configuration changes.' ,
301 '' , 0 , 0 , 'on' , 'on' , 0 ,
302 '' , '' , '' , '' , '' , ''
305 self
.__ debug
_l og
= weechat
. config_new_option (
306 self
. config_file
, self
. sorting_section
,
307 'debug_log' , 'boolean' ,
308 'If enabled, print more debug messages. Not recommended for normal usage.' ,
309 '' , 0 , 0 , 'off' , 'off' , 0 ,
310 '' , '' , '' , '' , '' , ''
313 if weechat
. config_read ( self
. config_file
) != weechat
. WEECHAT_RC_OK
:
314 log ( 'Failed to load configuration file.' )
316 if weechat
. config_write ( self
. config_file
) != weechat
. WEECHAT_RC_OK
:
317 log ( 'Failed to write configuration file.' )
322 ''' Load configuration variables. '''
324 self
. case_sensitive
= weechat
. config_boolean ( self
.__ case
_ sensitive
)
326 rules_blob
= weechat
. config_string ( self
.__ rules
)
327 helpers_blob
= weechat
. config_string ( self
.__ helpers
)
328 signals_blob
= weechat
. config_string ( self
.__ signals
)
330 self
. rules
= decode_rules ( rules_blob
)
331 self
. helpers
= decode_helpers ( helpers_blob
)
332 self
. signals
= signals_blob
. split ()
333 self
. signal_delay
= weechat
. config_integer ( self
.__ signal
_ delay
)
334 self
. sort_limit
= weechat
. config_integer ( self
.__ sort
_l imit
)
335 self
. sort_on_config
= weechat
. config_boolean ( self
.__ sort
_ on
_ config
)
336 self
. debug_log
= weechat
. config_boolean ( self
.__ debug
_l og
)
338 def save_rules ( self
, run_callback
= True ):
339 ''' Save the current rules to the configuration. '''
340 weechat
. config_option_set ( self
.__ rules
, json
. dumps ( self
. rules
), run_callback
)
342 def save_helpers ( self
, run_callback
= True ):
343 ''' Save the current helpers to the configuration. '''
344 weechat
. config_option_set ( self
.__ helpers
, json
. dumps ( self
. helpers
), run_callback
)
347 def pad ( sequence
, length
, padding
= None ):
348 ''' Pad a list until is has a certain length. '''
349 return sequence
+ [ padding
] * max ( 0 , ( length
- len ( sequence
)))
351 def log ( message
, buffer = 'NULL' ):
352 weechat
. prnt ( buffer , 'autosort: {0} ' . format ( message
))
354 def debug ( message
, buffer = 'NULL' ):
356 weechat
. prnt ( buffer , 'autosort: debug: {0} ' . format ( message
))
359 ''' Get a list of all the buffers in weechat. '''
360 hdata
= weechat
. hdata_get ( 'buffer' )
361 buffer = weechat
. hdata_get_list ( hdata
, "gui_buffers" );
365 number
= weechat
. hdata_integer ( hdata
, buffer , 'number' )
366 result
. append (( number
, buffer ))
367 buffer = weechat
. hdata_pointer ( hdata
, buffer , 'next_buffer' )
370 class MergedBuffers ( list ):
371 """ A list of merged buffers, possibly of size 1. """
372 def __init__ ( self
, number
):
373 super ( MergedBuffers
, self
) .__ init
__ ()
376 def merge_buffer_list ( buffers
):
378 Group merged buffers together.
379 The output is a list of MergedBuffers.
381 if not buffers
: return []
383 for number
, buffer in buffers
:
384 if number
not in result
: result
[ number
] = MergedBuffers ( number
)
385 result
[ number
]. append ( buffer )
386 return result
. values ()
388 def sort_buffers ( hdata
, buffers
, rules
, helpers
, case_sensitive
):
389 for merged
in buffers
:
390 for buffer in merged
:
391 name
= weechat
. hdata_string ( hdata
, buffer , 'name' )
393 return sorted ( buffers
, key
= merged_sort_key ( rules
, helpers
, case_sensitive
))
395 def buffer_sort_key ( rules
, helpers
, case_sensitive
):
396 ''' Create a sort key function for a list of lists of merged buffers. '''
399 for helper_name
, helper
in sorted ( helpers
. items ()):
400 expanded
= weechat
. string_eval_expression ( helper
, {"buffer": buffer}
, {}, {}
)
401 extra_vars
[ helper_name
] = expanded
if case_sensitive
else casefold ( expanded
)
404 expanded
= weechat
. string_eval_expression ( rule
, {"buffer": buffer}
, extra_vars
, {})
405 result
. append ( expanded
if case_sensitive
else casefold ( expanded
))
410 def merged_sort_key ( rules
, helpers
, case_sensitive
):
411 buffer_key
= buffer_sort_key ( rules
, helpers
, case_sensitive
)
414 for buffer in merged
:
415 this
= buffer_key ( buffer )
416 if best
is None or this
< best
: best
= this
420 def apply_buffer_order ( buffers
):
421 ''' Sort the buffers in weechat according to the given order. '''
422 for i
, buffer in enumerate ( buffers
):
423 weechat
. buffer_set ( buffer [ 0 ], "number" , str ( i
+ 1 ))
425 def split_args ( args
, expected
, optional
= 0 ):
426 ''' Split an argument string in the desired number of arguments. '''
427 split
= args
. split ( ' ' , expected
- 1 )
428 if ( len ( split
) < expected
):
429 raise HumanReadableError ( 'Expected at least {0} arguments, got {1} .' . format ( expected
, len ( split
)))
430 return split
[:- 1 ] + pad ( split
[- 1 ]. split ( ' ' , optional
), optional
+ 1 , '' )
432 def do_sort ( verbose
= False ):
433 start
= perf_counter ()
435 hdata
, buffers
= get_buffers ()
436 buffers
= merge_buffer_list ( buffers
)
437 buffers
= sort_buffers ( hdata
, buffers
, config
. rules
, config
. helpers
, config
. case_sensitive
)
438 apply_buffer_order ( buffers
)
440 elapsed
= perf_counter () - start
442 log ( "Finished sorting buffers in {0:.4f} seconds." . format ( elapsed
))
444 debug ( "Finished sorting buffers in {0:.4f} seconds." . format ( elapsed
))
446 def command_sort ( buffer , command
, args
):
447 ''' Sort the buffers and print a confirmation. '''
449 return weechat
. WEECHAT_RC_OK
451 def command_debug ( buffer , command
, args
):
452 hdata
, buffers
= get_buffers ()
453 buffers
= merge_buffer_list ( buffers
)
455 # Show evaluation results.
456 log ( 'Individual evaluation results:' )
457 start
= perf_counter ()
458 key
= buffer_sort_key ( config
. rules
, config
. helpers
, config
. case_sensitive
)
460 for merged
in buffers
:
461 for buffer in merged
:
462 fullname
= weechat
. hdata_string ( hdata
, buffer , 'full_name' )
463 results
. append (( fullname
, key ( buffer )))
464 elapsed
= perf_counter () - start
466 for fullname
, result
in results
:
467 fullname
= ensure_str ( fullname
)
468 result
= [ ensure_str ( x
) for x
in result
]
469 log ( ' {0} : {1} ' . format ( fullname
, result
))
470 log ( 'Computing evaluation results took {0:.4f} seconds.' . format ( elapsed
))
472 return weechat
. WEECHAT_RC_OK
474 def command_rule_list ( buffer , command
, args
):
475 ''' Show the list of sorting rules. '''
476 output
= 'Sorting rules: \n '
477 for i
, rule
in enumerate ( config
. rules
):
478 output
+= ' {0} : {1} \n ' . format ( i
, rule
)
479 if not len ( config
. rules
):
480 output
+= ' No sorting rules configured. \n '
483 return weechat
. WEECHAT_RC_OK
486 def command_rule_add ( buffer , command
, args
):
487 ''' Add a rule to the rule list. '''
488 config
. rules
. append ( args
)
490 command_rule_list ( buffer , command
, '' )
492 return weechat
. WEECHAT_RC_OK
495 def command_rule_insert ( buffer , command
, args
):
496 ''' Insert a rule at the desired position in the rule list. '''
497 index
, rule
= split_args ( args
, 2 )
498 index
= parse_int ( index
, 'index' )
500 config
. rules
. insert ( index
, rule
)
502 command_rule_list ( buffer , command
, '' )
503 return weechat
. WEECHAT_RC_OK
506 def command_rule_update ( buffer , command
, args
):
507 ''' Update a rule in the rule list. '''
508 index
, rule
= split_args ( args
, 2 )
509 index
= parse_int ( index
, 'index' )
511 config
. rules
[ index
] = rule
513 command_rule_list ( buffer , command
, '' )
514 return weechat
. WEECHAT_RC_OK
517 def command_rule_delete ( buffer , command
, args
):
518 ''' Delete a rule from the rule list. '''
520 index
= parse_int ( index
, 'index' )
522 config
. rules
. pop ( index
)
524 command_rule_list ( buffer , command
, '' )
525 return weechat
. WEECHAT_RC_OK
528 def command_rule_move ( buffer , command
, args
):
529 ''' Move a rule to a new position. '''
530 index_a
, index_b
= split_args ( args
, 2 )
531 index_a
= parse_int ( index_a
, 'index' )
532 index_b
= parse_int ( index_b
, 'index' )
534 list_move ( config
. rules
, index_a
, index_b
)
536 command_rule_list ( buffer , command
, '' )
537 return weechat
. WEECHAT_RC_OK
540 def command_rule_swap ( buffer , command
, args
):
541 ''' Swap two rules. '''
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_swap ( config
. rules
, index_a
, index_b
)
548 command_rule_list ( buffer , command
, '' )
549 return weechat
. WEECHAT_RC_OK
552 def command_helper_list ( buffer , command
, args
):
553 ''' Show the list of helpers. '''
554 output
= 'Helper variables: \n '
556 width
= max ( map ( lambda x
: len ( x
) if len ( x
) <= 30 else 0 , config
. helpers
. keys ()))
558 for name
, expression
in sorted ( config
. helpers
. items ()):
559 output
+= ' {0:>{width} }: {1} \n ' . format ( name
, expression
, width
= width
)
560 if not len ( config
. helpers
):
561 output
+= ' No helper variables configured.'
564 return weechat
. WEECHAT_RC_OK
567 def command_helper_set ( buffer , command
, args
):
568 ''' Add/update a helper to the helper list. '''
569 name
, expression
= split_args ( args
, 2 )
571 config
. helpers
[ name
] = expression
572 config
. save_helpers ()
573 command_helper_list ( buffer , command
, '' )
575 return weechat
. WEECHAT_RC_OK
577 def command_helper_delete ( buffer , command
, args
):
578 ''' Delete a helper from the helper list. '''
581 del config
. helpers
[ name
]
582 config
. save_helpers ()
583 command_helper_list ( buffer , command
, '' )
584 return weechat
. WEECHAT_RC_OK
587 def command_helper_rename ( buffer , command
, args
):
588 ''' Rename a helper to a new position. '''
589 old_name
, new_name
= split_args ( args
, 2 )
592 config
. helpers
[ new_name
] = config
. helpers
[ old_name
]
593 del config
. helpers
[ old_name
]
595 raise HumanReadableError ( 'No such helper: {0} ' . format ( old_name
))
596 config
. save_helpers ()
597 command_helper_list ( buffer , command
, '' )
598 return weechat
. WEECHAT_RC_OK
601 def command_helper_swap ( buffer , command
, args
):
602 ''' Swap two helpers. '''
603 a
, b
= split_args ( args
, 2 )
605 config
. helpers
[ b
], config
. helpers
[ a
] = config
. helpers
[ a
], config
. helpers
[ b
]
606 except KeyError as e
:
607 raise HumanReadableError ( 'No such helper: {0} ' . format ( e
. args
[ 0 ]))
609 config
. helpers
. swap ( index_a
, index_b
)
610 config
. save_helpers ()
611 command_helper_list ( buffer , command
, '' )
612 return weechat
. WEECHAT_RC_OK
614 def call_command ( buffer , command
, args
, subcommands
):
615 ''' Call a subcommand from a dictionary. '''
616 subcommand
, tail
= pad ( args
. split ( ' ' , 1 ), 2 , '' )
617 subcommand
= subcommand
. strip ()
618 if ( subcommand
== '' ):
619 child
= subcommands
. get ( ' ' )
621 command
= command
+ [ subcommand
]
622 child
= subcommands
. get ( subcommand
)
624 if isinstance ( child
, dict ):
625 return call_command ( buffer , command
, tail
, child
)
626 elif callable ( child
):
627 return child ( buffer , command
, tail
)
629 log ( ' {0} : command not found' . format ( ' ' . join ( command
)))
630 return weechat
. WEECHAT_RC_ERROR
632 def on_signal ( data
, signal
, signal_data
):
633 global signal_delay_timer
636 # If the sort limit timeout is started, we're in the hold-off time after sorting, just queue a sort.
637 if sort_limit_timer
is not None :
639 debug ( 'Signal {0} ignored, sort limit timeout is active and sort is already queued.' . format ( signal
))
641 debug ( 'Signal {0} received but sort limit timeout is active, sort is now queued.' . format ( signal
))
643 return weechat
. WEECHAT_RC_OK
645 # If the signal delay timeout is started, a signal was recently received, so ignore this signal.
646 if signal_delay_timer
is not None :
647 debug ( 'Signal {0} ignored, signal delay timeout active.' . format ( signal
))
648 return weechat
. WEECHAT_RC_OK
650 # Otherwise, start the signal delay timeout.
651 debug ( 'Signal {0} received, starting signal delay timeout of {1} ms.' . format ( signal
, config
. signal_delay
))
652 weechat
. hook_timer ( config
. signal_delay
, 0 , 1 , "on_signal_delay_timeout" , "" )
653 return weechat
. WEECHAT_RC_OK
655 def on_signal_delay_timeout ( pointer
, remaining_calls
):
656 """ Called when the signal_delay_timer triggers. """
657 global signal_delay_timer
658 global sort_limit_timer
661 signal_delay_timer
= None
663 # If the sort limit timeout was started, we're still in the no-sort period, so just queue a sort.
664 if sort_limit_timer
is not None :
665 debug ( 'Signal delay timeout expired, but sort limit timeout is active, sort is now queued.' )
667 return weechat
. WEECHAT_RC_OK
670 debug ( 'Signal delay timeout expired, starting sort.' )
673 # Start the sort limit timeout if not disabled.
674 if config
. sort_limit
> 0 :
675 debug ( 'Starting sort limit timeout of {0} ms.' . format ( config
. sort_limit
))
676 sort_limit_timer
= weechat
. hook_timer ( config
. sort_limit
, 0 , 1 , "on_sort_limit_timeout" , "" )
678 return weechat
. WEECHAT_RC_OK
680 def on_sort_limit_timeout ( pointer
, remainin_calls
):
681 """ Called when de sort_limit_timer triggers. """
682 global sort_limit_timer
685 # If no signal was received during the timeout, we're done.
687 debug ( 'Sort limit timeout expired without receiving a signal.' )
688 sort_limit_timer
= None
689 return weechat
. WEECHAT_RC_OK
691 # Otherwise it's time to sort.
692 debug ( 'Signal received during sort limit timeout, starting queued sort.' )
696 # Start the sort limit timeout again if not disabled.
697 if config
. sort_limit
> 0 :
698 debug ( 'Starting sort limit timeout of {0} ms.' . format ( config
. sort_limit
))
699 sort_limit_timer
= weechat
. hook_timer ( config
. sort_limit
, 0 , 1 , "on_sort_limit_timeout" , "" )
701 return weechat
. WEECHAT_RC_OK
705 # Unhook all signals and hook the new ones.
708 for signal
in config
. signals
:
709 hooks
. append ( weechat
. hook_signal ( signal
, 'on_signal' , '' ))
711 if config
. sort_on_config
:
712 debug ( 'Sorting because configuration changed.' )
715 def on_config_changed (* args
, ** kwargs
):
716 ''' Called whenever the configuration changes. '''
720 return weechat
. WEECHAT_RC_OK
723 if not args
: return '' , None
727 for i
, c
in enumerate ( args
):
733 return result
, args
[ i
+ 1 :]
738 def parse_args ( args
, max = None ):
741 while max is None or i
< max :
743 arg
, args
= parse_arg ( args
)
744 if arg
is None : break
746 if args
is None : break
749 def on_info_replace ( pointer
, name
, arguments
):
750 arguments
, rest
= parse_args ( arguments
, 3 )
751 if rest
or len ( arguments
) < 3 :
752 log ( 'usage: $ {{info:{0} ,old,new,text}}' . format ( name
))
754 old
, new
, text
= arguments
756 return text
. replace ( old
, new
)
758 def on_info_order ( pointer
, name
, arguments
):
759 arguments
, rest
= parse_args ( arguments
)
760 if len ( arguments
) < 1 :
761 log ( 'usage: $ {{info:{0} ,value,first,second,third,...}}' . format ( name
))
766 if not keys
: return '0'
768 # Find the value in the keys (or '*' if we can't find it)
769 result
= list_find ( keys
, value
)
770 if result
is None : result
= list_find ( keys
, '*' )
771 if result
is None : result
= len ( keys
)
773 # Pad result with leading zero to make sure string sorting works.
774 width
= int ( math
. log10 ( len ( keys
))) + 1
775 return ' {0:0{1} }' . format ( result
, width
)
778 def on_autosort_command ( data
, buffer , args
):
779 ''' Called when the autosort command is invoked. '''
781 return call_command ( buffer , [ '/autosort' ], args
, {
783 'sort' : command_sort
,
784 'debug' : command_debug
,
787 ' ' : command_rule_list
,
788 'list' : command_rule_list
,
789 'add' : command_rule_add
,
790 'insert' : command_rule_insert
,
791 'update' : command_rule_update
,
792 'delete' : command_rule_delete
,
793 'move' : command_rule_move
,
794 'swap' : command_rule_swap
,
797 ' ' : command_helper_list
,
798 'list' : command_helper_list
,
799 'set' : command_helper_set
,
800 'delete' : command_helper_delete
,
801 'rename' : command_helper_rename
,
802 'swap' : command_helper_swap
,
805 except HumanReadableError
as e
:
807 return weechat
. WEECHAT_RC_ERROR
809 def add_completions ( completion
, words
):
811 weechat
. hook_completion_list_add ( completion
, word
, 0 , weechat
. WEECHAT_LIST_POS_END
)
813 def autosort_complete_rules ( words
, completion
):
815 add_completions ( completion
, [ 'add' , 'delete' , 'insert' , 'list' , 'move' , 'swap' , 'update' ])
816 if len ( words
) == 1 and words
[ 0 ] in ( 'delete' , 'insert' , 'move' , 'swap' , 'update' ):
817 add_completions ( completion
, map ( str , range ( len ( config
. rules
))))
818 if len ( words
) == 2 and words
[ 0 ] in ( 'move' , 'swap' ):
819 add_completions ( completion
, map ( str , range ( len ( config
. rules
))))
820 if len ( words
) == 2 and words
[ 0 ] in ( 'update' ):
822 add_completions ( completion
, [ config
. rules
[ int ( words
[ 1 ])]])
823 except KeyError : pass
824 except ValueError : pass
826 add_completions ( completion
, [ '' ])
827 return weechat
. WEECHAT_RC_OK
829 def autosort_complete_helpers ( words
, completion
):
831 add_completions ( completion
, [ 'delete' , 'list' , 'rename' , 'set' , 'swap' ])
832 elif len ( words
) == 1 and words
[ 0 ] in ( 'delete' , 'rename' , 'set' , 'swap' ):
833 add_completions ( completion
, sorted ( config
. helpers
. keys ()))
834 elif len ( words
) == 2 and words
[ 0 ] == 'swap' :
835 add_completions ( completion
, sorted ( config
. helpers
. keys ()))
836 elif len ( words
) == 2 and words
[ 0 ] == 'rename' :
837 add_completions ( completion
, sorted ( config
. helpers
. keys ()))
838 elif len ( words
) == 2 and words
[ 0 ] == 'set' :
840 add_completions ( completion
, [ config
. helpers
[ words
[ 1 ]]])
841 except KeyError : pass
842 return weechat
. WEECHAT_RC_OK
844 def on_autosort_complete ( data
, name
, buffer , completion
):
845 cmdline
= weechat
. buffer_get_string ( buffer , "input" )
846 cursor
= weechat
. buffer_get_integer ( buffer , "input_pos" )
847 prefix
= cmdline
[: cursor
]
848 words
= prefix
. split ()[ 1 :]
850 # If the current word isn't finished yet,
851 # ignore it for coming up with completion suggestions.
852 if prefix
[- 1 ] != ' ' : words
= words
[:- 1 ]
855 add_completions ( completion
, [ 'debug' , 'helpers' , 'rules' , 'sort' ])
856 elif words
[ 0 ] == 'rules' :
857 return autosort_complete_rules ( words
[ 1 :], completion
)
858 elif words
[ 0 ] == 'helpers' :
859 return autosort_complete_helpers ( words
[ 1 :], completion
)
860 return weechat
. WEECHAT_RC_OK
862 command_description
= r
''' {*white} # General commands {reset}
864 {*white} /autosort {brown} sort {reset}
865 Manually trigger the buffer sorting.
867 {*white} /autosort {brown} debug {reset}
868 Show the evaluation results of the sort rules for each buffer.
871 {*white} # Sorting rule commands {reset}
873 {*white} /autosort {brown} rules list {reset}
874 Print the list of sort rules.
876 {*white} /autosort {brown} rules add {cyan} <expression> {reset}
877 Add a new rule at the end of the list.
879 {*white} /autosort {brown} rules insert {cyan} <index> <expression> {reset}
880 Insert a new rule at the given index in the list.
882 {*white} /autosort {brown} rules update {cyan} <index> <expression> {reset}
883 Update a rule in the list with a new expression.
885 {*white} /autosort {brown} rules delete {cyan} <index>
886 Delete a rule from the list.
888 {*white} /autosort {brown} rules move {cyan} <index_from> <index_to> {reset}
889 Move a rule from one position in the list to another.
891 {*white} /autosort {brown} rules swap {cyan} <index_a> <index_b> {reset}
892 Swap two rules in the list
895 {*white} # Helper variable commands {reset}
897 {*white} /autosort {brown} helpers list
898 Print the list of helper variables.
900 {*white} /autosort {brown} helpers set {cyan} <name> <expression>
901 Add or update a helper variable with the given name.
903 {*white} /autosort {brown} helpers delete {cyan} <name>
904 Delete a helper variable.
906 {*white} /autosort {brown} helpers rename {cyan} <old_name> <new_name>
907 Rename a helper variable.
909 {*white} /autosort {brown} helpers swap {cyan} <name_a> <name_b>
910 Swap the expressions of two helper variables in the list.
913 {*white} # Description
914 Autosort is a weechat script to automatically keep your buffers sorted. The sort
915 order can be customized by defining your own sort rules, but the default should
916 be sane enough for most people. It can also group IRC channel/private buffers
917 under their server buffer if you like.
919 {*white} # Sort rules {reset}
920 Autosort evaluates a list of eval expressions (see {*default} /help eval {reset} ) and sorts the
921 buffers based on evaluated result. Earlier rules will be considered first. Only
922 if earlier rules produced identical results is the result of the next rule
923 considered for sorting purposes.
925 You can debug your sort rules with the ` {*default} /autosort debug {reset} ` command, which will
926 print the evaluation results of each rule for each buffer.
928 {*brown} NOTE: {reset} The sort rules for version 3 are not compatible with version 2 or vice
929 versa. You will have to manually port your old rules to version 3 if you have any.
931 {*white} # Helper variables {reset}
932 You may define helper variables for the main sort rules to keep your rules
933 readable. They can be used in the main sort rules as variables. For example,
934 a helper variable named ` {cyan} foo {reset} ` can be accessed in a main rule with the
935 string ` {cyan} $ {{foo} } {reset} `.
937 {*white} # Replacing substrings {reset}
938 There is no default method for replacing text inside eval expressions. However,
939 autosort adds a `replace` info hook that can be used inside eval expressions:
940 {cyan} $ {{info:autosort_replace,from,to,text} } {reset}
942 For example, to strip all hashes from a buffer name, you could write:
943 {cyan} $ {{info:autosort_replace,#,,${{buffer.name} }}} {reset}
945 You can escape commas and backslashes inside the arguments by prefixing them with
948 {*white} # Automatic or manual sorting {reset}
949 By default, autosort will automatically sort your buffer list whenever a buffer
950 is opened, merged, unmerged or renamed. This should keep your buffers sorted in
951 almost all situations. However, you may wish to change the list of signals that
952 cause your buffer list to be sorted. Simply edit the ` {cyan} autosort.sorting.signals {reset} `
953 option to add or remove any signal you like.
955 If you remove all signals you can still sort your buffers manually with the
956 ` {*default} /autosort sort {reset} ` command. To prevent all automatic sorting, the option
957 ` {cyan} autosort.sorting.sort_on_config_change {reset} ` should also be disabled.
959 {*white} # Recommended settings
960 For the best visual effect, consider setting the following options:
961 {*white} /set {cyan} irc.look.server_buffer {reset} {brown} independent {reset}
962 {*white} /set {cyan} buffers.look.indenting {reset} {brown} on {reset}
964 The first setting allows server buffers to be sorted independently, which is
965 needed to create a hierarchical tree view of the server and channel buffers.
966 The second one indents channel and private buffers in the buffer list of the
967 ` {*default} buffers.pl {reset} ` script.
969 If you are using the {*default} buflist {reset} plugin you can (ab)use Unicode to draw a tree
970 structure with the following setting (modify to suit your need):
971 {*white} /set {cyan} buflist.format.indent {brown} "$ {{color:237} }$ {{if:${{buffer.next_buffer.local_variables.type} }=~^(channel|private)$?├─:└─}}" {reset}
974 command_completion
= ' %(plugin_autosort) %(plugin_autosort) %(plugin_autosort) %(plugin_autosort) %(plugin_autosort) '
976 info_replace_description
= 'Replace all occurences of `from` with `to` in the string `text`.'
977 info_replace_arguments
= 'from,to,text'
979 info_order_description
= (
980 'Get a zero padded index of a value in a list of possible values.'
981 'If the value is not found, the index for `*` is returned.'
982 'If there is no `*` in the list, the highest index + 1 is returned.'
984 info_order_arguments
= 'value,first,second,third,...'
987 if weechat
. register ( SCRIPT_NAME
, SCRIPT_AUTHOR
, SCRIPT_VERSION
, SCRIPT_LICENSE
, SCRIPT_DESC
, "" , "" ):
988 config
= Config ( 'autosort' )
991 'default' : weechat
. color ( 'default' ),
992 'reset' : weechat
. color ( 'reset' ),
993 'black' : weechat
. color ( 'black' ),
994 'red' : weechat
. color ( 'red' ),
995 'green' : weechat
. color ( 'green' ),
996 'brown' : weechat
. color ( 'brown' ),
997 'yellow' : weechat
. color ( 'yellow' ),
998 'blue' : weechat
. color ( 'blue' ),
999 'magenta' : weechat
. color ( 'magenta' ),
1000 'cyan' : weechat
. color ( 'cyan' ),
1001 'white' : weechat
. color ( 'white' ),
1002 '*default' : weechat
. color ( '*default' ),
1003 '*black' : weechat
. color ( '*black' ),
1004 '*red' : weechat
. color ( '*red' ),
1005 '*green' : weechat
. color ( '*green' ),
1006 '*brown' : weechat
. color ( '*brown' ),
1007 '*yellow' : weechat
. color ( '*yellow' ),
1008 '*blue' : weechat
. color ( '*blue' ),
1009 '*magenta' : weechat
. color ( '*magenta' ),
1010 '*cyan' : weechat
. color ( '*cyan' ),
1011 '*white' : weechat
. color ( '*white' ),
1014 weechat
. hook_config ( 'autosort.*' , 'on_config_changed' , '' )
1015 weechat
. hook_completion ( 'plugin_autosort' , '' , 'on_autosort_complete' , '' )
1016 weechat
. hook_command ( 'autosort' , command_description
. format (** colors
), '' , '' , command_completion
, 'on_autosort_command' , '' )
1017 weechat
. hook_info ( 'autosort_replace' , info_replace_description
, info_replace_arguments
, 'on_info_replace' , '' )
1018 weechat
. hook_info ( 'autosort_order' , info_order_description
, info_order_arguments
, 'on_info_order' , '' )