parent
ada9ef87c7
commit
3a97e5bd1b
@ -0,0 +1,11 @@
|
|||||||
|
#
|
||||||
|
# weechat -- autosort.conf
|
||||||
|
#
|
||||||
|
|
||||||
|
[sorting]
|
||||||
|
case_sensitive = off
|
||||||
|
group_irc = on
|
||||||
|
replacements = "[["##", "#"]]"
|
||||||
|
rules = "[["core", 0], ["irc", 2], ["*", 1], ["irc.irc_raw", 0], ["irc.server", 1], ["irc.server.*.&*", 0], ["irc.server.*.&*", 0], ["irc.server.*.#*", 1], ["irc.server.*.\\*status", 2]]"
|
||||||
|
signals = "buffer_opened buffer_merged buffer_unmerged buffer_renamed"
|
||||||
|
sort_on_config_change = on
|
@ -0,0 +1,73 @@
|
|||||||
|
#
|
||||||
|
# weechat -- buffers.conf
|
||||||
|
#
|
||||||
|
|
||||||
|
[color]
|
||||||
|
current_bg = red
|
||||||
|
current_fg = lightcyan
|
||||||
|
default_bg = default
|
||||||
|
default_fg = default
|
||||||
|
hotlist_highlight_bg = default
|
||||||
|
hotlist_highlight_fg = magenta
|
||||||
|
hotlist_low_bg = default
|
||||||
|
hotlist_low_fg = white
|
||||||
|
hotlist_message_bg = default
|
||||||
|
hotlist_message_fg = yellow
|
||||||
|
hotlist_private_bg = default
|
||||||
|
hotlist_private_fg = lightgreen
|
||||||
|
none_channel_bg = default
|
||||||
|
none_channel_fg = default
|
||||||
|
number = lightgreen
|
||||||
|
number_char = lightgreen
|
||||||
|
prefix_bufname = default
|
||||||
|
queries_default_bg = default
|
||||||
|
queries_default_fg = default
|
||||||
|
queries_highlight_bg = default
|
||||||
|
queries_highlight_fg = default
|
||||||
|
queries_message_bg = default
|
||||||
|
queries_message_fg = default
|
||||||
|
suffix_bufname = default
|
||||||
|
whitelist_default_bg = default
|
||||||
|
whitelist_default_fg = default
|
||||||
|
whitelist_highlight_bg = default
|
||||||
|
whitelist_highlight_fg = default
|
||||||
|
whitelist_low_bg = default
|
||||||
|
whitelist_low_fg = default
|
||||||
|
whitelist_message_bg = default
|
||||||
|
whitelist_message_fg = default
|
||||||
|
whitelist_private_bg = default
|
||||||
|
whitelist_private_fg = default
|
||||||
|
|
||||||
|
[look]
|
||||||
|
core_to_front = off
|
||||||
|
detach = 600
|
||||||
|
detach_buffer_immediately = ""
|
||||||
|
detach_buffer_immediately_level = 2
|
||||||
|
detach_display_window_number = off
|
||||||
|
detach_displayed_buffers = on
|
||||||
|
detach_free_content = off
|
||||||
|
detach_query = off
|
||||||
|
hide_merged_buffers = server
|
||||||
|
hotlist_counter = off
|
||||||
|
immune_detach_buffers = ""
|
||||||
|
indenting = on
|
||||||
|
indenting_amount = 2
|
||||||
|
indenting_number = on
|
||||||
|
jump_prev_next_visited_buffer = off
|
||||||
|
mark_inactive = off
|
||||||
|
mouse_move_buffer = on
|
||||||
|
mouse_wheel = on
|
||||||
|
name_crop_suffix = "+"
|
||||||
|
name_size_max = 0
|
||||||
|
number_char = "."
|
||||||
|
prefix = off
|
||||||
|
prefix_bufname = ""
|
||||||
|
prefix_empty = on
|
||||||
|
prefix_for_query = ""
|
||||||
|
short_names = on
|
||||||
|
show_lag = off
|
||||||
|
show_number = on
|
||||||
|
sort = number
|
||||||
|
suffix_bufname = ""
|
||||||
|
toggle_bar = on
|
||||||
|
whitelist_buffers = ""
|
@ -0,0 +1,12 @@
|
|||||||
|
#
|
||||||
|
# weechat -- colorize_nicks.conf
|
||||||
|
#
|
||||||
|
|
||||||
|
[look]
|
||||||
|
blacklist_channels = ""
|
||||||
|
blacklist_nicks = "so,root"
|
||||||
|
colorize_input = off
|
||||||
|
greedy_matching = on
|
||||||
|
ignore_nicks_in_urls = off
|
||||||
|
ignore_tags = ""
|
||||||
|
min_nick_length = 2
|
@ -0,0 +1,30 @@
|
|||||||
|
#
|
||||||
|
# weechat -- iset.conf
|
||||||
|
#
|
||||||
|
|
||||||
|
[color]
|
||||||
|
bg_selected = red
|
||||||
|
help_default_value = green
|
||||||
|
help_option_name = white
|
||||||
|
help_text = default
|
||||||
|
option = default
|
||||||
|
option_selected = white
|
||||||
|
type = brown
|
||||||
|
type_selected = yellow
|
||||||
|
value = cyan
|
||||||
|
value_diff = magenta
|
||||||
|
value_diff_selected = lightmagenta
|
||||||
|
value_selected = lightcyan
|
||||||
|
value_undef = green
|
||||||
|
value_undef_selected = lightgreen
|
||||||
|
|
||||||
|
[help]
|
||||||
|
show_help_bar = on
|
||||||
|
show_help_extra_info = on
|
||||||
|
show_plugin_description = off
|
||||||
|
|
||||||
|
[look]
|
||||||
|
scroll_horiz = 10
|
||||||
|
show_current_line = on
|
||||||
|
use_mute = off
|
||||||
|
value_search_char = "="
|
@ -0,0 +1 @@
|
|||||||
|
../buffers.pl
|
@ -0,0 +1 @@
|
|||||||
|
../highmon.pl
|
@ -0,0 +1 @@
|
|||||||
|
../iset.pl
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -0,0 +1 @@
|
|||||||
|
../autosort.py
|
@ -0,0 +1 @@
|
|||||||
|
../colorize_nicks.py
|
@ -0,0 +1,885 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# Copyright (C) 2013-2014 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.
|
||||||
|
#
|
||||||
|
# http://github.com/de-vri.es/weechat-autosort
|
||||||
|
#
|
||||||
|
|
||||||
|
#
|
||||||
|
# Changelog:
|
||||||
|
# 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 weechat
|
||||||
|
import re
|
||||||
|
import json
|
||||||
|
|
||||||
|
SCRIPT_NAME = 'autosort'
|
||||||
|
SCRIPT_AUTHOR = 'Maarten de Vries <maarten@de-vri.es>'
|
||||||
|
SCRIPT_VERSION = '2.8'
|
||||||
|
SCRIPT_LICENSE = 'GPL3'
|
||||||
|
SCRIPT_DESC = 'Automatically or manually keep your buffers sorted and grouped by server.'
|
||||||
|
|
||||||
|
|
||||||
|
config = None
|
||||||
|
hooks = []
|
||||||
|
|
||||||
|
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))
|
||||||
|
|
||||||
|
|
||||||
|
class Pattern:
|
||||||
|
''' A simple glob-like pattern for matching buffer names. '''
|
||||||
|
|
||||||
|
def __init__(self, pattern, case_sensitive):
|
||||||
|
''' Construct a pattern from a string. '''
|
||||||
|
escaped = False
|
||||||
|
char_class = 0
|
||||||
|
chars = ''
|
||||||
|
regex = ''
|
||||||
|
for c in pattern:
|
||||||
|
if escaped and char_class:
|
||||||
|
escaped = False
|
||||||
|
chars += re.escape(c)
|
||||||
|
elif escaped:
|
||||||
|
escaped = False
|
||||||
|
regex += re.escape(c)
|
||||||
|
elif c == '\\':
|
||||||
|
escaped = True
|
||||||
|
elif c == '*' and not char_class:
|
||||||
|
regex += '[^.]*'
|
||||||
|
elif c == '?' and not char_class:
|
||||||
|
regex += '[^.]'
|
||||||
|
elif c == '[' and not char_class:
|
||||||
|
char_class = 1
|
||||||
|
chars = ''
|
||||||
|
elif c == '^' and char_class and not chars:
|
||||||
|
chars += '^'
|
||||||
|
elif c == ']' and char_class and chars not in ('', '^'):
|
||||||
|
char_class = False
|
||||||
|
regex += '[' + chars + ']'
|
||||||
|
elif c == '-' and char_class:
|
||||||
|
chars += '-'
|
||||||
|
elif char_class:
|
||||||
|
chars += re.escape(c)
|
||||||
|
else:
|
||||||
|
regex += re.escape(c)
|
||||||
|
|
||||||
|
if char_class:
|
||||||
|
raise ValueError("unmatched opening '['")
|
||||||
|
if escaped:
|
||||||
|
raise ValueError("unexpected trailing '\\'")
|
||||||
|
|
||||||
|
if case_sensitive:
|
||||||
|
self.regex = re.compile('^' + regex + '$')
|
||||||
|
else:
|
||||||
|
self.regex = re.compile('^' + regex + '$', flags = re.IGNORECASE)
|
||||||
|
self.pattern = pattern
|
||||||
|
|
||||||
|
def match(self, input):
|
||||||
|
''' Match the pattern against a string. '''
|
||||||
|
return self.regex.match(input)
|
||||||
|
|
||||||
|
|
||||||
|
class FriendlyList(object):
|
||||||
|
''' A list with human readable errors. '''
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.__data = []
|
||||||
|
|
||||||
|
def raw(self):
|
||||||
|
return self.__data
|
||||||
|
|
||||||
|
def append(self, value):
|
||||||
|
''' Add a rule to the list. '''
|
||||||
|
self.__data.append(value)
|
||||||
|
|
||||||
|
def insert(self, index, value):
|
||||||
|
''' Add a rule to the list. '''
|
||||||
|
if not 0 <= index <= len(self): raise HumanReadableError('Index out of range: expected an integer in the range [0, {0}], got {1}.'.format(len(self), index))
|
||||||
|
self.__data.insert(index, value)
|
||||||
|
|
||||||
|
def pop(self, index):
|
||||||
|
''' Remove a rule from the list and return it. '''
|
||||||
|
if not 0 <= index < len(self): raise HumanReadableError('Index out of range: expected an integer in the range [0, {0}), got {1}.'.format(len(self), index))
|
||||||
|
return self.__data.pop(index)
|
||||||
|
|
||||||
|
def move(self, index_a, index_b):
|
||||||
|
''' Move a rule to a new position in the list. '''
|
||||||
|
self.insert(index_b, self.pop(index_a))
|
||||||
|
|
||||||
|
def swap(self, index_a, index_b):
|
||||||
|
''' Swap two elements in the list. '''
|
||||||
|
self[index_a], self[index_b] = self[index_b], self[index_a]
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
return len(self.__data)
|
||||||
|
|
||||||
|
def __getitem__(self, index):
|
||||||
|
if not 0 <= index < len(self): raise HumanReadableError('Index out of range: expected an integer in the range [0, {0}), got {1}.'.format(len(self), index))
|
||||||
|
return self.__data[index]
|
||||||
|
|
||||||
|
def __setitem__(self, index, value):
|
||||||
|
if not 0 <= index < len(self): raise HumanReadableError('Index out of range: expected an integer in the range [0, {0}), got {1}.'.format(len(self), index))
|
||||||
|
self.__data[index] = value
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
return iter(self.__data)
|
||||||
|
|
||||||
|
|
||||||
|
class RuleList(FriendlyList):
|
||||||
|
''' A list of rules to test buffer names against. '''
|
||||||
|
rule_regex = re.compile(r'^(.*)=\s*([+-]?[^=]*)$')
|
||||||
|
|
||||||
|
def __init__(self, rules):
|
||||||
|
''' Construct a RuleList from a list of rules. '''
|
||||||
|
super(RuleList, self).__init__()
|
||||||
|
for rule in rules: self.append(rule)
|
||||||
|
|
||||||
|
def get_score(self, name):
|
||||||
|
''' Get the sort score of a partial name according to a rule list. '''
|
||||||
|
for rule in self:
|
||||||
|
if rule[0].match(name): return rule[1]
|
||||||
|
return 999999999
|
||||||
|
|
||||||
|
def encode(self):
|
||||||
|
''' Encode the rules for storage. '''
|
||||||
|
return json.dumps(list(map(lambda x: (x[0].pattern, x[1]), self)))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def decode(blob, case_sensitive):
|
||||||
|
''' Parse rules from a string blob. '''
|
||||||
|
result = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
decoded = json.loads(blob)
|
||||||
|
except ValueError:
|
||||||
|
log('Invalid rules: expected JSON encoded list of pairs, got "{0}".'.format(blob))
|
||||||
|
return [], 0
|
||||||
|
|
||||||
|
for rule in decoded:
|
||||||
|
# Rules must be a pattern,score pair.
|
||||||
|
if len(rule) != 2:
|
||||||
|
log('Invalid rule: expected (pattern, score), got "{0}". Rule ignored.'.format(rule))
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Rules must have a valid pattern.
|
||||||
|
try:
|
||||||
|
pattern = Pattern(rule[0], case_sensitive)
|
||||||
|
except ValueError as e:
|
||||||
|
log('Invalid pattern: {0} in "{1}". Rule ignored.'.format(e, rule[0]))
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Rules must have a valid score.
|
||||||
|
try:
|
||||||
|
score = int(rule[1])
|
||||||
|
except ValueError as e:
|
||||||
|
log('Invalid score: expected an integer, got "{0}". Rule ignored.'.format(score))
|
||||||
|
continue
|
||||||
|
|
||||||
|
result.append((pattern, score))
|
||||||
|
|
||||||
|
return RuleList(result)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def parse_rule(arg, case_sensitive):
|
||||||
|
''' Parse a rule argument. '''
|
||||||
|
arg = arg.strip()
|
||||||
|
match = RuleList.rule_regex.match(arg)
|
||||||
|
if not match:
|
||||||
|
raise HumanReadableError('Invalid rule: expected "<pattern> = <score>", got "{0}".'.format(arg))
|
||||||
|
|
||||||
|
pattern = match.group(1).strip()
|
||||||
|
try:
|
||||||
|
pattern = Pattern(pattern, case_sensitive)
|
||||||
|
except ValueError as e:
|
||||||
|
raise HumanReadableError('Invalid pattern: {0} in "{1}".'.format(e, pattern))
|
||||||
|
|
||||||
|
score = parse_int(match.group(2), 'score')
|
||||||
|
return (pattern, score)
|
||||||
|
|
||||||
|
|
||||||
|
def decode_replacements(blob):
|
||||||
|
''' Decode a replacement list encoded as JSON. '''
|
||||||
|
result = FriendlyList()
|
||||||
|
try:
|
||||||
|
decoded = json.loads(blob)
|
||||||
|
except ValueError:
|
||||||
|
log('Invalid replacement list: expected JSON encoded list of pairs, got "{0}".'.format(blob))
|
||||||
|
return [], 0
|
||||||
|
|
||||||
|
for replacement in decoded:
|
||||||
|
# Replacements must be a (string, string) pair.
|
||||||
|
if len(replacement) != 2:
|
||||||
|
log('Invalid replacement pattern: expected (pattern, replacement), got "{0}". Replacement ignored.'.format(rule))
|
||||||
|
continue
|
||||||
|
result.append(replacement)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def encode_replacements(replacements):
|
||||||
|
''' Encode a list of replacement patterns as JSON. '''
|
||||||
|
return json.dumps(replacements.raw())
|
||||||
|
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
''' The autosort configuration. '''
|
||||||
|
|
||||||
|
default_rules = json.dumps([
|
||||||
|
('core', 0),
|
||||||
|
('irc', 2),
|
||||||
|
('*', 1),
|
||||||
|
|
||||||
|
('irc.irc_raw', 0),
|
||||||
|
('irc.server', 1),
|
||||||
|
])
|
||||||
|
|
||||||
|
default_replacements = '[]'
|
||||||
|
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.case_sensitive = False
|
||||||
|
self.group_irc = True
|
||||||
|
self.rules = []
|
||||||
|
self.replacements = []
|
||||||
|
self.signals = []
|
||||||
|
self.sort_on_config = True
|
||||||
|
|
||||||
|
self.__case_sensitive = None
|
||||||
|
self.__group_irc = None
|
||||||
|
self.__rules = None
|
||||||
|
self.__replacements = None
|
||||||
|
self.__signals = None
|
||||||
|
self.__sort_on_config = None
|
||||||
|
|
||||||
|
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, '', '', '', '', '', '', '', '', '', '')
|
||||||
|
|
||||||
|
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,
|
||||||
|
'', '', '', '', '', ''
|
||||||
|
)
|
||||||
|
|
||||||
|
self.__group_irc = weechat.config_new_option(
|
||||||
|
self.config_file, self.sorting_section,
|
||||||
|
'group_irc', 'boolean',
|
||||||
|
'If this option is on, the script pretends that IRC channel/private buffers are renamed to "irc.server.{network}.{channel}" rather than "irc.{network}.{channel}".' +
|
||||||
|
'This ensures that these buffers are grouped with their respective server buffer.',
|
||||||
|
'', 0, 0, 'on', 'on', 0,
|
||||||
|
'', '', '', '', '', ''
|
||||||
|
)
|
||||||
|
|
||||||
|
self.__rules = weechat.config_new_option(
|
||||||
|
self.config_file, self.sorting_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.__replacements = weechat.config_new_option(
|
||||||
|
self.config_file, self.sorting_section,
|
||||||
|
'replacements', 'string',
|
||||||
|
'An ordered list of replacement patterns to use on buffer name components, encoded as JSON. See /help autosort for commands to manipulate these replacements.',
|
||||||
|
'', 0, 0, Config.default_replacements, Config.default_replacements, 0,
|
||||||
|
'', '', '', '', '', ''
|
||||||
|
)
|
||||||
|
|
||||||
|
self.__signals = weechat.config_new_option(
|
||||||
|
self.config_file, self.sorting_section,
|
||||||
|
'signals', 'string',
|
||||||
|
'The signals that will cause autosort to resort your buffer list. Seperate signals with spaces.',
|
||||||
|
'', 0, 0, Config.default_signals, Config.default_signals, 0,
|
||||||
|
'', '', '', '', '', ''
|
||||||
|
)
|
||||||
|
|
||||||
|
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,
|
||||||
|
'', '', '', '', '', ''
|
||||||
|
)
|
||||||
|
|
||||||
|
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)
|
||||||
|
self.group_irc = weechat.config_boolean(self.__group_irc)
|
||||||
|
|
||||||
|
rules_blob = weechat.config_string(self.__rules)
|
||||||
|
replacements_blob = weechat.config_string(self.__replacements)
|
||||||
|
signals_blob = weechat.config_string(self.__signals)
|
||||||
|
|
||||||
|
self.rules = RuleList.decode(rules_blob, self.case_sensitive)
|
||||||
|
self.replacements = decode_replacements(replacements_blob)
|
||||||
|
self.signals = signals_blob.split()
|
||||||
|
self.sort_on_config = weechat.config_boolean(self.__sort_on_config)
|
||||||
|
|
||||||
|
def save_rules(self, run_callback = True):
|
||||||
|
''' Save the current rules to the configuration. '''
|
||||||
|
weechat.config_option_set(self.__rules, RuleList.encode(self.rules), run_callback)
|
||||||
|
|
||||||
|
def save_replacements(self, run_callback = True):
|
||||||
|
''' Save the current replacement patterns to the configuration. '''
|
||||||
|
weechat.config_option_set(self.__replacements, encode_replacements(self.replacements), 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))
|
||||||
|
|
||||||
|
|
||||||
|
def get_buffers():
|
||||||
|
''' Get a list of all the buffers in weechat. '''
|
||||||
|
buffers = []
|
||||||
|
|
||||||
|
buffer_list = weechat.infolist_get('buffer', '', '')
|
||||||
|
|
||||||
|
while weechat.infolist_next(buffer_list):
|
||||||
|
name = weechat.infolist_string (buffer_list, 'full_name')
|
||||||
|
number = weechat.infolist_integer(buffer_list, 'number')
|
||||||
|
|
||||||
|
# Buffer is merged with one we already have in the list, skip it.
|
||||||
|
if number <= len(buffers):
|
||||||
|
continue
|
||||||
|
buffers.append((name, number - 1))
|
||||||
|
|
||||||
|
weechat.infolist_free(buffer_list)
|
||||||
|
return buffers
|
||||||
|
|
||||||
|
|
||||||
|
def preprocess(buffer, config):
|
||||||
|
'''
|
||||||
|
Preprocess a buffers names.
|
||||||
|
'''
|
||||||
|
|
||||||
|
# Make sure the name is a unicode string.
|
||||||
|
# On python3 this is a NOP since the string type is already decoded as UTF-8.
|
||||||
|
if isinstance(buffer, bytes):
|
||||||
|
buffer = buffer.decode('utf-8')
|
||||||
|
|
||||||
|
if not config.case_sensitive:
|
||||||
|
buffer = buffer.lower()
|
||||||
|
|
||||||
|
for replacement in config.replacements:
|
||||||
|
buffer = buffer.replace(replacement[0], replacement[1])
|
||||||
|
|
||||||
|
buffer = buffer.split('.')
|
||||||
|
if config.group_irc and len(buffer) >= 2 and buffer[0] == 'irc' and buffer[1] not in ('server', 'irc_raw'):
|
||||||
|
buffer.insert(1, 'server')
|
||||||
|
|
||||||
|
return buffer
|
||||||
|
|
||||||
|
|
||||||
|
def buffer_sort_key(rules):
|
||||||
|
''' Create a sort key function for a buffer list from a rule list. '''
|
||||||
|
def key(buffer):
|
||||||
|
result = []
|
||||||
|
name = ''
|
||||||
|
for word in preprocess(buffer[0], config):
|
||||||
|
name += ('.' if name else '') + word
|
||||||
|
result.append((rules.get_score(name), word))
|
||||||
|
return result
|
||||||
|
|
||||||
|
return key
|
||||||
|
|
||||||
|
|
||||||
|
def apply_buffer_order(order):
|
||||||
|
''' Sort the buffers in weechat according to the given order. '''
|
||||||
|
indices = list(order)
|
||||||
|
reverse = [0] * len(indices)
|
||||||
|
for i, index in enumerate(indices):
|
||||||
|
reverse[index] = i
|
||||||
|
|
||||||
|
for i in range(len(indices)):
|
||||||
|
wanted = indices[i]
|
||||||
|
if wanted == i: continue
|
||||||
|
# Weechat buffers are 1-indexed, but our indices aren't.
|
||||||
|
weechat.command('', '/buffer swap {0} {1}'.format(i + 1, wanted + 1))
|
||||||
|
indices[reverse[i]] = wanted
|
||||||
|
reverse[wanted] = reverse[i]
|
||||||
|
|
||||||
|
|
||||||
|
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, '')
|
||||||
|
|
||||||
|
|
||||||
|
def command_sort(buffer, command, args):
|
||||||
|
''' Sort the buffers and print a confirmation. '''
|
||||||
|
on_buffers_changed()
|
||||||
|
log("Finished sorting buffers.", buffer)
|
||||||
|
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} = {2}\n'.format(i, rule[0].pattern, rule[1])
|
||||||
|
if not len(config.rules):
|
||||||
|
output += ' No sorting rules configured.\n'
|
||||||
|
log(output, buffer)
|
||||||
|
|
||||||
|
return weechat.WEECHAT_RC_OK
|
||||||
|
|
||||||
|
|
||||||
|
def command_rule_add(buffer, command, args):
|
||||||
|
''' Add a rule to the rule list. '''
|
||||||
|
rule = RuleList.parse_rule(args, config.case_sensitive)
|
||||||
|
|
||||||
|
config.rules.append(rule)
|
||||||
|
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')
|
||||||
|
rule = RuleList.parse_rule(rule, config.case_sensitive)
|
||||||
|
|
||||||
|
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')
|
||||||
|
rule = RuleList.parse_rule(rule, config.case_sensitive)
|
||||||
|
|
||||||
|
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')
|
||||||
|
|
||||||
|
config.rules.move(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')
|
||||||
|
|
||||||
|
config.rules.swap(index_a, index_b)
|
||||||
|
config.save_rules()
|
||||||
|
command_rule_list(buffer, command, '')
|
||||||
|
return weechat.WEECHAT_RC_OK
|
||||||
|
|
||||||
|
|
||||||
|
def command_replacement_list(buffer, command, args):
|
||||||
|
''' Show the list of sorting rules. '''
|
||||||
|
output = 'Replacement patterns:\n'
|
||||||
|
for i, pattern in enumerate(config.replacements):
|
||||||
|
output += ' {0}: {1} -> {2}\n'.format(i, pattern[0], pattern[1])
|
||||||
|
if not len(config.replacements):
|
||||||
|
output += ' No replacement patterns configured.'
|
||||||
|
log(output, buffer)
|
||||||
|
|
||||||
|
return weechat.WEECHAT_RC_OK
|
||||||
|
|
||||||
|
|
||||||
|
def command_replacement_add(buffer, command, args):
|
||||||
|
''' Add a rule to the rule list. '''
|
||||||
|
pattern, replacement = split_args(args, 1, 1)
|
||||||
|
|
||||||
|
config.replacements.append((pattern, replacement))
|
||||||
|
config.save_replacements()
|
||||||
|
command_replacement_list(buffer, command, '')
|
||||||
|
|
||||||
|
return weechat.WEECHAT_RC_OK
|
||||||
|
|
||||||
|
|
||||||
|
def command_replacement_insert(buffer, command, args):
|
||||||
|
''' Insert a rule at the desired position in the rule list. '''
|
||||||
|
index, pattern, replacement = split_args(args, 2, 1)
|
||||||
|
index = parse_int(index, 'index')
|
||||||
|
|
||||||
|
config.replacements.insert(index, (pattern, replacement))
|
||||||
|
config.save_replacements()
|
||||||
|
command_replacement_list(buffer, command, '')
|
||||||
|
return weechat.WEECHAT_RC_OK
|
||||||
|
|
||||||
|
|
||||||
|
def command_replacement_update(buffer, command, args):
|
||||||
|
''' Update a rule in the rule list. '''
|
||||||
|
index, pattern, replacement = split_args(args, 2, 1)
|
||||||
|
index = parse_int(index, 'index')
|
||||||
|
|
||||||
|
config.replacements[index] = (pattern, replacement)
|
||||||
|
config.save_replacements()
|
||||||
|
command_replacement_list(buffer, command, '')
|
||||||
|
return weechat.WEECHAT_RC_OK
|
||||||
|
|
||||||
|
|
||||||
|
def command_replacement_delete(buffer, command, args):
|
||||||
|
''' Delete a rule from the rule list. '''
|
||||||
|
index = args.strip()
|
||||||
|
index = parse_int(index, 'index')
|
||||||
|
|
||||||
|
config.replacements.pop(index)
|
||||||
|
config.save_replacements()
|
||||||
|
command_replacement_list(buffer, command, '')
|
||||||
|
return weechat.WEECHAT_RC_OK
|
||||||
|
|
||||||
|
|
||||||
|
def command_replacement_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')
|
||||||
|
|
||||||
|
config.replacements.move(index_a, index_b)
|
||||||
|
config.save_replacements()
|
||||||
|
command_replacement_list(buffer, command, '')
|
||||||
|
return weechat.WEECHAT_RC_OK
|
||||||
|
|
||||||
|
|
||||||
|
def command_replacement_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')
|
||||||
|
|
||||||
|
config.replacements.swap(index_a, index_b)
|
||||||
|
config.save_replacements()
|
||||||
|
command_replacement_list(buffer, command, '')
|
||||||
|
return weechat.WEECHAT_RC_OK
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def call_command(buffer, command, args, subcommands):
|
||||||
|
''' Call a subccommand from a dictionary. '''
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
def on_buffers_changed(*args, **kwargs):
|
||||||
|
''' Called whenever the buffer list changes. '''
|
||||||
|
buffers = get_buffers()
|
||||||
|
buffers.sort(key=buffer_sort_key(config.rules))
|
||||||
|
apply_buffer_order([i for _, i in buffers])
|
||||||
|
return weechat.WEECHAT_RC_OK
|
||||||
|
|
||||||
|
|
||||||
|
def on_config_changed(*args, **kwargs):
|
||||||
|
''' Called whenever the configuration changes. '''
|
||||||
|
config.reload()
|
||||||
|
|
||||||
|
# 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_buffers_changed', ''))
|
||||||
|
|
||||||
|
if config.sort_on_config:
|
||||||
|
on_buffers_changed()
|
||||||
|
|
||||||
|
return weechat.WEECHAT_RC_OK
|
||||||
|
|
||||||
|
|
||||||
|
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,
|
||||||
|
|
||||||
|
'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,
|
||||||
|
},
|
||||||
|
'replacements': {
|
||||||
|
' ': command_replacement_list,
|
||||||
|
'list': command_replacement_list,
|
||||||
|
'add': command_replacement_add,
|
||||||
|
'insert': command_replacement_insert,
|
||||||
|
'update': command_replacement_update,
|
||||||
|
'delete': command_replacement_delete,
|
||||||
|
'move': command_replacement_move,
|
||||||
|
'swap': command_replacement_swap,
|
||||||
|
},
|
||||||
|
'sort': on_buffers_changed,
|
||||||
|
})
|
||||||
|
except HumanReadableError as e:
|
||||||
|
log(e, buffer)
|
||||||
|
return weechat.WEECHAT_RC_ERROR
|
||||||
|
|
||||||
|
|
||||||
|
command_description = r'''
|
||||||
|
NOTE: For the best effect, you may want to consider setting the option irc.look.server_buffer to independent and buffers.look.indenting to on.
|
||||||
|
|
||||||
|
# Commands
|
||||||
|
|
||||||
|
## Miscellaneous
|
||||||
|
/autosort sort
|
||||||
|
Manually trigger the buffer sorting.
|
||||||
|
|
||||||
|
|
||||||
|
## Sorting rules
|
||||||
|
|
||||||
|
/autosort rules list
|
||||||
|
Print the list of sort rules.
|
||||||
|
|
||||||
|
/autosort rules add <pattern> = <score>
|
||||||
|
Add a new rule at the end of the list.
|
||||||
|
|
||||||
|
/autosort rules insert <index> <pattern> = <score>
|
||||||
|
Insert a new rule at the given index in the list.
|
||||||
|
|
||||||
|
/autosort rules update <index> <pattern> = <score>
|
||||||
|
Update a rule in the list with a new pattern and score.
|
||||||
|
|
||||||
|
/autosort rules delete <index>
|
||||||
|
Delete a rule from the list.
|
||||||
|
|
||||||
|
/autosort rules move <index_from> <index_to>
|
||||||
|
Move a rule from one position in the list to another.
|
||||||
|
|
||||||
|
/autosort rules swap <index_a> <index_b>
|
||||||
|
Swap two rules in the list
|
||||||
|
|
||||||
|
|
||||||
|
## Replacement patterns
|
||||||
|
|
||||||
|
/autosort replacements list
|
||||||
|
Print the list of replacement patterns.
|
||||||
|
|
||||||
|
/autosort replacements add <pattern> <replacement>
|
||||||
|
Add a new replacement pattern at the end of the list.
|
||||||
|
|
||||||
|
/autosort replacements insert <index> <pattern> <replacement>
|
||||||
|
Insert a new replacement pattern at the given index in the list.
|
||||||
|
|
||||||
|
/autosort replacements update <index> <pattern> <replacement>
|
||||||
|
Update a replacement pattern in the list.
|
||||||
|
|
||||||
|
/autosort replacements delete <index>
|
||||||
|
Delete a replacement pattern from the list.
|
||||||
|
|
||||||
|
/autosort replacements move <index_from> <index_to>
|
||||||
|
Move a replacement pattern from one position in the list to another.
|
||||||
|
|
||||||
|
/autosort replacements swap <index_a> <index_b>
|
||||||
|
Swap two replacement pattern in the list
|
||||||
|
|
||||||
|
|
||||||
|
# Introduction
|
||||||
|
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.
|
||||||
|
|
||||||
|
Autosort first turns buffer names into a list of their components by splitting on them on the period character.
|
||||||
|
For example, the buffer name "irc.server.freenode" is turned into ['irc', 'server', 'freenode'].
|
||||||
|
The list of buffers is then lexicographically sorted.
|
||||||
|
|
||||||
|
To facilitate custom sort orders, it is possible to assign a score to each component individually before the sorting is done.
|
||||||
|
Any name component that did not get a score assigned will be sorted after those that did receive a score.
|
||||||
|
Components are always sorted on their score first and on their name second.
|
||||||
|
Lower scores are sorted first.
|
||||||
|
|
||||||
|
## Automatic or manual sorting
|
||||||
|
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 "autosort.sorting.signals" option to add or remove any signal you like.
|
||||||
|
If you remove all signals you can still sort your buffers manually with the "/autosort sort" command.
|
||||||
|
To prevent all automatic sorting, "autosort.sorting.sort_on_config_change" should also be set to off.
|
||||||
|
|
||||||
|
## Grouping IRC buffers
|
||||||
|
In weechat, IRC channel/private buffers are named "irc.<network>.<#channel>",
|
||||||
|
and IRC server buffers are named "irc.server.<network>".
|
||||||
|
This does not work very well with lexicographical sorting if you want all buffers for one network grouped together.
|
||||||
|
That is why autosort comes with the "autosort.sorting.group_irc" option,
|
||||||
|
which secretly pretends IRC channel/private buffers are called "irc.server.<network>.<#channel>".
|
||||||
|
The buffers are not actually renamed, autosort simply pretends they are for sorting purposes.
|
||||||
|
|
||||||
|
## Replacement patterns
|
||||||
|
Sometimes you may want to ignore some characters for sorting purposes.
|
||||||
|
On Freenode for example, you may wish to ignore the difference between channels starting with a double or a single hash sign.
|
||||||
|
To do so, simply add a replacement pattern that replaces ## with # with the following command:
|
||||||
|
/autosort replacements add ## #
|
||||||
|
|
||||||
|
Replacement patterns do not support wildcards or special characters at the moment.
|
||||||
|
|
||||||
|
## Sort rules
|
||||||
|
You can assign scores to name components by defining sort rules.
|
||||||
|
The first rule that matches a component decides the score.
|
||||||
|
Further rules are not examined.
|
||||||
|
Sort rules use the following syntax:
|
||||||
|
<glob-pattern> = <score>
|
||||||
|
|
||||||
|
You can use the "/autosort rules" command to show and manipulate the list of sort rules.
|
||||||
|
|
||||||
|
|
||||||
|
Allowed special characters in the glob patterns are:
|
||||||
|
|
||||||
|
Pattern | Meaning
|
||||||
|
--------|--------
|
||||||
|
* | Matches a sequence of any characters except for periods.
|
||||||
|
? | Matches a single character, but not a period.
|
||||||
|
[a-z] | Matches a single character in the given regex-like character class.
|
||||||
|
[^ab] | A negated regex-like character class.
|
||||||
|
\* | A backslash escapes the next characters and removes its special meaning.
|
||||||
|
\\ | A literal backslash.
|
||||||
|
|
||||||
|
|
||||||
|
## Example
|
||||||
|
As an example, consider the following rule list:
|
||||||
|
0: core = 0
|
||||||
|
1: irc = 2
|
||||||
|
2: * = 1
|
||||||
|
|
||||||
|
3: irc.server.*.#* = 1
|
||||||
|
4: irc.server.*.* = 0
|
||||||
|
|
||||||
|
Rule 0 ensures the core buffer is always sorted first.
|
||||||
|
Rule 1 sorts IRC buffers last and rule 2 puts all remaining buffers in between the two.
|
||||||
|
|
||||||
|
Rule 3 and 4 would make no sense with the group_irc option off.
|
||||||
|
With the option on though, these rules will sort private buffers before regular channel buffers.
|
||||||
|
Rule 3 matches channel buffers and assigns them a higher score,
|
||||||
|
while rule 4 matches the buffers that remain and assigns them a lower score.
|
||||||
|
The same effect could also be achieved with a single rule:
|
||||||
|
irc.server.*.[^#]* = 0
|
||||||
|
'''
|
||||||
|
|
||||||
|
command_completion = 'sort||rules list|add|insert|update|delete|move|swap||replacements list|add|insert|update|delete|move|swap'
|
||||||
|
|
||||||
|
|
||||||
|
if weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, "", ""):
|
||||||
|
config = Config('autosort')
|
||||||
|
|
||||||
|
weechat.hook_config('autosort.*', 'on_config_changed', '')
|
||||||
|
weechat.hook_command('autosort', command_description, '', '', command_completion, 'on_autosort_command', 'NULL')
|
||||||
|
on_config_changed()
|
@ -0,0 +1,350 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# Copyright (c) 2010 by xt <xt@bash.no>
|
||||||
|
#
|
||||||
|
# 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/>.
|
||||||
|
#
|
||||||
|
|
||||||
|
# This script colors nicks in IRC channels in the actual message
|
||||||
|
# not just in the prefix section.
|
||||||
|
#
|
||||||
|
#
|
||||||
|
# History:
|
||||||
|
# 2016-05-01, Simmo Saan <simmo.saan@gmail.com>
|
||||||
|
# version 22: invalidate cached colors on hash algorithm change
|
||||||
|
# 2015-07-28, xt
|
||||||
|
# version 21: fix problems with nicks with commas in them
|
||||||
|
# 2015-04-19, xt
|
||||||
|
# version 20: fix ignore of nicks in URLs
|
||||||
|
# 2015-04-18, xt
|
||||||
|
# version 19: new option ignore nicks in URLs
|
||||||
|
# 2015-03-03, xt
|
||||||
|
# version 18: iterate buffers looking for nicklists instead of servers
|
||||||
|
# 2015-02-23, holomorph
|
||||||
|
# version 17: fix coloring in non-channel buffers (#58)
|
||||||
|
# 2014-09-17, holomorph
|
||||||
|
# version 16: use weechat config facilities
|
||||||
|
# clean unused, minor linting, some simplification
|
||||||
|
# 2014-05-05, holomorph
|
||||||
|
# version 15: fix python2-specific re.search check
|
||||||
|
# 2013-01-29, nils_2
|
||||||
|
# version 14: make script compatible with Python 3.x
|
||||||
|
# 2012-10-19, ldvx
|
||||||
|
# version 13: Iterate over every word to prevent incorrect colorization of
|
||||||
|
# nicks. Added option greedy_matching.
|
||||||
|
# 2012-04-28, ldvx
|
||||||
|
# version 12: added ignore_tags to avoid colorizing nicks if tags are present
|
||||||
|
# 2012-01-14, nesthib
|
||||||
|
# version 11: input_text_display hook and modifier to colorize nicks in input bar
|
||||||
|
# 2010-12-22, xt
|
||||||
|
# version 10: hook config option for updating blacklist
|
||||||
|
# 2010-12-20, xt
|
||||||
|
# version 0.9: hook new config option for weechat 0.3.4
|
||||||
|
# 2010-11-01, nils_2
|
||||||
|
# version 0.8: hook_modifier() added to communicate with rainbow_text
|
||||||
|
# 2010-10-01, xt
|
||||||
|
# version 0.7: changes to support non-irc-plugins
|
||||||
|
# 2010-07-29, xt
|
||||||
|
# version 0.6: compile regexp as per patch from Chris quigybo@hotmail.com
|
||||||
|
# 2010-07-19, xt
|
||||||
|
# version 0.5: fix bug with incorrect coloring of own nick
|
||||||
|
# 2010-06-02, xt
|
||||||
|
# version 0.4: update to reflect API changes
|
||||||
|
# 2010-03-26, xt
|
||||||
|
# version 0.3: fix error with exception
|
||||||
|
# 2010-03-24, xt
|
||||||
|
# version 0.2: use ignore_channels when populating to increase performance.
|
||||||
|
# 2010-02-03, xt
|
||||||
|
# version 0.1: initial (based on ruby script by dominikh)
|
||||||
|
#
|
||||||
|
# Known issues: nicks will not get colorized if they begin with a character
|
||||||
|
# such as ~ (which some irc networks do happen to accept)
|
||||||
|
|
||||||
|
import weechat
|
||||||
|
import re
|
||||||
|
w = weechat
|
||||||
|
|
||||||
|
SCRIPT_NAME = "colorize_nicks"
|
||||||
|
SCRIPT_AUTHOR = "xt <xt@bash.no>"
|
||||||
|
SCRIPT_VERSION = "22"
|
||||||
|
SCRIPT_LICENSE = "GPL"
|
||||||
|
SCRIPT_DESC = "Use the weechat nick colors in the chat area"
|
||||||
|
|
||||||
|
VALID_NICK = r'([@~&!%+])?([-a-zA-Z0-9\[\]\\`_^\{|\}]+)'
|
||||||
|
valid_nick_re = re.compile(VALID_NICK)
|
||||||
|
ignore_channels = []
|
||||||
|
ignore_nicks = []
|
||||||
|
|
||||||
|
# Dict with every nick on every channel with its color as lookup value
|
||||||
|
colored_nicks = {}
|
||||||
|
|
||||||
|
CONFIG_FILE_NAME = "colorize_nicks"
|
||||||
|
|
||||||
|
# config file and options
|
||||||
|
colorize_config_file = ""
|
||||||
|
colorize_config_option = {}
|
||||||
|
|
||||||
|
def colorize_config_init():
|
||||||
|
'''
|
||||||
|
Initialization of configuration file.
|
||||||
|
Sections: look.
|
||||||
|
'''
|
||||||
|
global colorize_config_file, colorize_config_option
|
||||||
|
colorize_config_file = weechat.config_new(CONFIG_FILE_NAME,
|
||||||
|
"colorize_config_reload_cb", "")
|
||||||
|
if colorize_config_file == "":
|
||||||
|
return
|
||||||
|
|
||||||
|
# section "look"
|
||||||
|
section_look = weechat.config_new_section(
|
||||||
|
colorize_config_file, "look", 0, 0, "", "", "", "", "", "", "", "", "", "")
|
||||||
|
if section_look == "":
|
||||||
|
weechat.config_free(colorize_config_file)
|
||||||
|
return
|
||||||
|
colorize_config_option["blacklist_channels"] = weechat.config_new_option(
|
||||||
|
colorize_config_file, section_look, "blacklist_channels",
|
||||||
|
"string", "Comma separated list of channels", "", 0, 0,
|
||||||
|
"", "", 0, "", "", "", "", "", "")
|
||||||
|
colorize_config_option["blacklist_nicks"] = weechat.config_new_option(
|
||||||
|
colorize_config_file, section_look, "blacklist_nicks",
|
||||||
|
"string", "Comma separated list of nicks", "", 0, 0,
|
||||||
|
"so,root", "so,root", 0, "", "", "", "", "", "")
|
||||||
|
colorize_config_option["min_nick_length"] = weechat.config_new_option(
|
||||||
|
colorize_config_file, section_look, "min_nick_length",
|
||||||
|
"integer", "Minimum length nick to colorize", "",
|
||||||
|
2, 20, "", "", 0, "", "", "", "", "", "")
|
||||||
|
colorize_config_option["colorize_input"] = weechat.config_new_option(
|
||||||
|
colorize_config_file, section_look, "colorize_input",
|
||||||
|
"boolean", "Whether to colorize input", "", 0,
|
||||||
|
0, "off", "off", 0, "", "", "", "", "", "")
|
||||||
|
colorize_config_option["ignore_tags"] = weechat.config_new_option(
|
||||||
|
colorize_config_file, section_look, "ignore_tags",
|
||||||
|
"string", "Comma separated list of tags to ignore; i.e. irc_join,irc_part,irc_quit", "", 0, 0,
|
||||||
|
"", "", 0, "", "", "", "", "", "")
|
||||||
|
colorize_config_option["greedy_matching"] = weechat.config_new_option(
|
||||||
|
colorize_config_file, section_look, "greedy_matching",
|
||||||
|
"boolean", "If off, then use lazy matching instead", "", 0,
|
||||||
|
0, "on", "on", 0, "", "", "", "", "", "")
|
||||||
|
colorize_config_option["ignore_nicks_in_urls"] = weechat.config_new_option(
|
||||||
|
colorize_config_file, section_look, "ignore_nicks_in_urls",
|
||||||
|
"boolean", "If on, don't colorize nicks inside URLs", "", 0,
|
||||||
|
0, "off", "off", 0, "", "", "", "", "", "")
|
||||||
|
|
||||||
|
def colorize_config_read():
|
||||||
|
''' Read configuration file. '''
|
||||||
|
global colorize_config_file
|
||||||
|
return weechat.config_read(colorize_config_file)
|
||||||
|
|
||||||
|
def colorize_nick_color(nick, my_nick):
|
||||||
|
''' Retrieve nick color from weechat. '''
|
||||||
|
if nick == my_nick:
|
||||||
|
return w.color(w.config_string(w.config_get('weechat.color.chat_nick_self')))
|
||||||
|
else:
|
||||||
|
return w.info_get('irc_nick_color', nick)
|
||||||
|
|
||||||
|
def colorize_cb(data, modifier, modifier_data, line):
|
||||||
|
''' Callback that does the colorizing, and returns new line if changed '''
|
||||||
|
|
||||||
|
global ignore_nicks, ignore_channels, colored_nicks
|
||||||
|
|
||||||
|
|
||||||
|
full_name = modifier_data.split(';')[1]
|
||||||
|
channel = '.'.join(full_name.split('.')[1:])
|
||||||
|
|
||||||
|
buffer = w.buffer_search('', full_name)
|
||||||
|
# Check if buffer has colorized nicks
|
||||||
|
if buffer not in colored_nicks:
|
||||||
|
return line
|
||||||
|
|
||||||
|
if channel and channel in ignore_channels:
|
||||||
|
return line
|
||||||
|
|
||||||
|
min_length = w.config_integer(colorize_config_option['min_nick_length'])
|
||||||
|
reset = w.color('reset')
|
||||||
|
|
||||||
|
# Don't colorize if the ignored tag is present in message
|
||||||
|
tags_line = modifier_data.rsplit(';')
|
||||||
|
if len(tags_line) >= 3:
|
||||||
|
tags_line = tags_line[2].split(',')
|
||||||
|
for i in w.config_string(colorize_config_option['ignore_tags']).split(','):
|
||||||
|
if i in tags_line:
|
||||||
|
return line
|
||||||
|
|
||||||
|
for words in valid_nick_re.findall(line):
|
||||||
|
nick = words[1]
|
||||||
|
# Check that nick is not ignored and longer than minimum length
|
||||||
|
if len(nick) < min_length or nick in ignore_nicks:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Check that nick is in the dictionary colored_nicks
|
||||||
|
if nick in colored_nicks[buffer]:
|
||||||
|
nick_color = colored_nicks[buffer][nick]
|
||||||
|
|
||||||
|
# Let's use greedy matching. Will check against every word in a line.
|
||||||
|
if w.config_boolean(colorize_config_option['greedy_matching']):
|
||||||
|
for word in line.split():
|
||||||
|
if w.config_boolean(colorize_config_option['ignore_nicks_in_urls']) and \
|
||||||
|
word.startswith(('http://', 'https://')):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if nick in word:
|
||||||
|
# Is there a nick that contains nick and has a greater lenght?
|
||||||
|
# If so let's save that nick into var biggest_nick
|
||||||
|
biggest_nick = ""
|
||||||
|
for i in colored_nicks[buffer]:
|
||||||
|
if nick in i and nick != i and len(i) > len(nick):
|
||||||
|
if i in word:
|
||||||
|
# If a nick with greater len is found, and that word
|
||||||
|
# also happens to be in word, then let's save this nick
|
||||||
|
biggest_nick = i
|
||||||
|
# If there's a nick with greater len, then let's skip this
|
||||||
|
# As we will have the chance to colorize when biggest_nick
|
||||||
|
# iterates being nick.
|
||||||
|
if len(biggest_nick) > 0 and biggest_nick in word:
|
||||||
|
pass
|
||||||
|
elif len(word) < len(biggest_nick) or len(biggest_nick) == 0:
|
||||||
|
new_word = word.replace(nick, '%s%s%s' % (nick_color, nick, reset))
|
||||||
|
line = line.replace(word, new_word)
|
||||||
|
# Let's use lazy matching for nick
|
||||||
|
else:
|
||||||
|
nick_color = colored_nicks[buffer][nick]
|
||||||
|
# The two .? are in case somebody writes "nick:", "nick,", etc
|
||||||
|
# to address somebody
|
||||||
|
regex = r"(\A|\s).?(%s).?(\Z|\s)" % re.escape(nick)
|
||||||
|
match = re.search(regex, line)
|
||||||
|
if match is not None:
|
||||||
|
new_line = line[:match.start(2)] + nick_color+nick+reset + line[match.end(2):]
|
||||||
|
line = new_line
|
||||||
|
return line
|
||||||
|
|
||||||
|
def colorize_input_cb(data, modifier, modifier_data, line):
|
||||||
|
''' Callback that does the colorizing in input '''
|
||||||
|
|
||||||
|
global ignore_nicks, ignore_channels, colored_nicks
|
||||||
|
|
||||||
|
min_length = w.config_integer(colorize_config_option['min_nick_length'])
|
||||||
|
|
||||||
|
if not w.config_boolean(colorize_config_option['colorize_input']):
|
||||||
|
return line
|
||||||
|
|
||||||
|
buffer = w.current_buffer()
|
||||||
|
# Check if buffer has colorized nicks
|
||||||
|
if buffer not in colored_nicks:
|
||||||
|
return line
|
||||||
|
|
||||||
|
channel = w.buffer_get_string(buffer, 'name')
|
||||||
|
if channel and channel in ignore_channels:
|
||||||
|
return line
|
||||||
|
|
||||||
|
reset = w.color('reset')
|
||||||
|
|
||||||
|
for words in valid_nick_re.findall(line):
|
||||||
|
nick = words[1]
|
||||||
|
# Check that nick is not ignored and longer than minimum length
|
||||||
|
if len(nick) < min_length or nick in ignore_nicks:
|
||||||
|
continue
|
||||||
|
if nick in colored_nicks[buffer]:
|
||||||
|
nick_color = colored_nicks[buffer][nick]
|
||||||
|
line = line.replace(nick, '%s%s%s' % (nick_color, nick, reset))
|
||||||
|
|
||||||
|
return line
|
||||||
|
|
||||||
|
def populate_nicks(*args):
|
||||||
|
''' Fills entire dict with all nicks weechat can see and what color it has
|
||||||
|
assigned to it. '''
|
||||||
|
global colored_nicks
|
||||||
|
|
||||||
|
colored_nicks = {}
|
||||||
|
|
||||||
|
buffers = w.infolist_get('buffer', '', '')
|
||||||
|
while w.infolist_next(buffers):
|
||||||
|
buffer_ptr = w.infolist_pointer(buffers, 'pointer')
|
||||||
|
my_nick = w.buffer_get_string(buffer_ptr, 'localvar_nick')
|
||||||
|
nicklist = w.infolist_get('nicklist', buffer_ptr, '')
|
||||||
|
while w.infolist_next(nicklist):
|
||||||
|
if buffer_ptr not in colored_nicks:
|
||||||
|
colored_nicks[buffer_ptr] = {}
|
||||||
|
|
||||||
|
nick = w.infolist_string(nicklist, 'name')
|
||||||
|
nick_color = colorize_nick_color(nick, my_nick)
|
||||||
|
|
||||||
|
colored_nicks[buffer_ptr][nick] = nick_color
|
||||||
|
|
||||||
|
w.infolist_free(nicklist)
|
||||||
|
|
||||||
|
w.infolist_free(buffers)
|
||||||
|
|
||||||
|
return w.WEECHAT_RC_OK
|
||||||
|
|
||||||
|
def add_nick(data, signal, type_data):
|
||||||
|
''' Add nick to dict of colored nicks '''
|
||||||
|
global colored_nicks
|
||||||
|
|
||||||
|
# Nicks can have , in them in some protocols
|
||||||
|
splitted = type_data.split(',')
|
||||||
|
pointer = splitted[0]
|
||||||
|
nick = ",".join(splitted[1:])
|
||||||
|
if pointer not in colored_nicks:
|
||||||
|
colored_nicks[pointer] = {}
|
||||||
|
|
||||||
|
my_nick = w.buffer_get_string(pointer, 'localvar_nick')
|
||||||
|
nick_color = colorize_nick_color(nick, my_nick)
|
||||||
|
|
||||||
|
colored_nicks[pointer][nick] = nick_color
|
||||||
|
|
||||||
|
return w.WEECHAT_RC_OK
|
||||||
|
|
||||||
|
def remove_nick(data, signal, type_data):
|
||||||
|
''' Remove nick from dict with colored nicks '''
|
||||||
|
global colored_nicks
|
||||||
|
|
||||||
|
# Nicks can have , in them in some protocols
|
||||||
|
splitted = type_data.split(',')
|
||||||
|
pointer = splitted[0]
|
||||||
|
nick = ",".join(splitted[1:])
|
||||||
|
|
||||||
|
if pointer in colored_nicks and nick in colored_nicks[pointer]:
|
||||||
|
del colored_nicks[pointer][nick]
|
||||||
|
|
||||||
|
return w.WEECHAT_RC_OK
|
||||||
|
|
||||||
|
def update_blacklist(*args):
|
||||||
|
''' Set the blacklist for channels and nicks. '''
|
||||||
|
global ignore_channels, ignore_nicks
|
||||||
|
ignore_channels = w.config_string(colorize_config_option['blacklist_channels']).split(',')
|
||||||
|
ignore_nicks = w.config_string(colorize_config_option['blacklist_nicks']).split(',')
|
||||||
|
return w.WEECHAT_RC_OK
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
if w.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE,
|
||||||
|
SCRIPT_DESC, "", ""):
|
||||||
|
colorize_config_init()
|
||||||
|
colorize_config_read()
|
||||||
|
|
||||||
|
# Run once to get data ready
|
||||||
|
update_blacklist()
|
||||||
|
populate_nicks()
|
||||||
|
|
||||||
|
w.hook_signal('nicklist_nick_added', 'add_nick', '')
|
||||||
|
w.hook_signal('nicklist_nick_removed', 'remove_nick', '')
|
||||||
|
w.hook_modifier('weechat_print', 'colorize_cb', '')
|
||||||
|
# Hook config for changing colors
|
||||||
|
w.hook_config('weechat.color.chat_nick_colors', 'populate_nicks', '')
|
||||||
|
w.hook_config('weechat.look.nick_color_hash', 'populate_nicks', '')
|
||||||
|
# Hook for working togheter with other scripts (like colorize_lines)
|
||||||
|
w.hook_modifier('colorize_nicks', 'colorize_cb', '')
|
||||||
|
# Hook for modifying input
|
||||||
|
w.hook_modifier('250|input_text_display', 'colorize_input_cb', '')
|
||||||
|
# Hook for updating blacklist (this could be improved to use fnmatch)
|
||||||
|
weechat.hook_config('%s.look.blacklist*' % SCRIPT_NAME, 'update_blacklist', '')
|
Binary file not shown.
Loading…
Reference in new issue