1use Irssi;
2use strict;
3
4use vars qw($VERSION %IRSSI);
5
6$VERSION="0.3.10";
7%IRSSI = (
8	authors=> 'BC-bd',
9	contact=> 'bd@bc-bd.org',
10	name=> 'nm',
11	description=> 'right aligned nicks depending on longest nick',
12	license=> 'GPL v2',
13	url=> 'http://bc-bd.org/blog/irssi/',
14);
15
16# $Id: 9cb009e8b7e6f5ce60294334faf88715ef01413e $
17# nm.pl
18# for irssi 0.8.4 by bd@bc-bd.org
19#
20# right aligned nicks depending on longest nick
21#
22# inspired by neatmsg.pl from kodgehopper <kodgehopper@netscape.net
23# formats taken from www.irssi.de
24# thanks to adrianel <adrinael@nuclearzone.org> for some hints
25# thanks to Eric Wald <eswald@gmail.com> for the left alignment patch
26# inspired by nickcolor.pl by Timo Sirainen and Ian Peters
27# thanks to And1 <and1@meinungsverstaerker.de> for a small patch
28# thanks to berber@tzi.de for the save/load patch
29# thanks to Dennis Heimbert <dennis.heimbert@gmail.com> for a bug report/patch
30# thanks to Roy Sigurd Karlsbakk <roy@karlsbakk.net> for an autosave patch
31#
32#########
33# USAGE
34###
35#
36# use
37#
38# 	/neatcolor help
39#
40# for help on available commands
41#
42#########
43# OPTIONS
44#########
45
46my $help = "
47/set neat_colorize <ON|OFF>
48    * ON  : colorize nicks
49    * OFF : do not colorize nicks
50
51/set neat_colors <string>
52    Use these colors when colorizing nicks, eg:
53
54        /set neat_colors yYrR
55
56    See the file formats.txt on an explanation of what colors are
57    available.
58
59/set neat_left_actions <ON|OFF>
60    * ON  : print nicks left-aligned on actions
61    * OFF : print nicks right-aligned on actions
62
63/set neat_left_messages <ON|OFF>
64    * ON  : print nicks left-aligned on messages
65    * OFF : print nicks right-aligned on messages
66
67/set neat_right_mode <ON|OFF>
68    * ON  : print the mode of the nick e.g @%+ after the nick
69    * OFF : print it left of the nick
70
71/set neat_maxlength <number>
72    * number : Maximum length of Nicks to display. Longer nicks are truncated.
73    * 0      : Do not truncate nicks.
74
75/set neat_melength <number>
76    * number : number of spaces to substract from /me padding
77
78/set neat_ignorechars <str>
79    * str : regular expression used to filter out unwanted characters in
80            nicks. this can be used to assign the same color for similar
81            nicks, e.g. foo and foo_:
82
83                /set neat_ignorechars [_]
84
85/set neat_allow_shrinking <ON|OFF>
86    * ON  : shrink padding when longest nick disappears
87    * OFF : do not shrink, only allow growing
88
89/set neat_autosave <number>
90    * number : autosave after <number> seconds, defaults to 60. Set to 0 to
91               disable.
92";
93
94#
95###
96################
97###
98#
99# Changelog
100#
101# Version 0.3.11
102#  - added autosave, idea from Roy Sigurd Karlsbakk
103#
104# Version 0.3.10
105#  - fix losing of saved color when changing nick shares more than one channel
106#    with you
107#
108# Version 0.3.9
109#  - fix longest nick calculation for nicks shorter than the current longest
110#    nick
111#  - updated url
112#
113# Version 0.3.8
114#  - fixed error in the nickchange tracking code, reported by Kevin Ballard
115#  - added --all switch to reset command
116#  - skip broken lines in saved_colors
117#
118# Version 0.3.7
119#  - fixed crash when calling /neatcolor without parameters
120#  - fixed url
121#
122# Version 0.3.6
123#  - added option to ignore certain characters from color hash building, see
124#    https://bc-bd.org/trac/irssi/ticket/22
125#  - added option to save and specify colors for nicks, see
126#    https://bc-bd.org/trac/irssi/ticket/23
127#  - added option to disallow shrinking, see
128#    https://bc-bd.org/trac/irssi/ticket/12
129#
130# Version 0.3.5
131#  - now also aligning own messages in queries
132#
133# Version 0.3.4
134#  - fxed off by one error in nick_to_color, patch by jrib, see
135#  https://bc-bd.org/trac/irssi/ticket/24
136#
137# Version 0.3.3
138#  - added support for alignment in queries, see
139#    https://bc-bd.org/trac/irssi/ticket/21
140#
141# Version 0.3.2
142#  - integrated left alignment patch from Eric Wald <eswald@gmail.com>, see
143#    https://bc-bd.org/trac/irssi/ticket/18
144#
145# Version 0.3.1
146#  - /me padding, see https://bc-bd.org/trac/irssi/ticket/17
147#
148# Version 0.3.0
149#  - integrate nick coloring support
150#
151# Version 0.2.1
152#  - moved neat_maxlength check to reformat() (thx to Jerome De Greef <jdegreef@brutele.be>)
153#
154# Version 0.2.0
155#  - by adrianel <adrinael@nuclearzone.org>
156#     * reformat after setup reload
157#     * maximum length of nicks
158#
159# Version 0.1.0
160#  - got lost somewhere
161#
162# Version 0.0.2
163#  - ugly typo fixed
164#
165# Version 0.0.1
166#  - initial release
167#
168###
169################
170###
171#
172# BUGS
173#
174# Empty nicks, eg "<> message"
175# 	This seems to be triggered by some themes. As of now there is no known
176# 	fix other than changing themes, see
177# 	https://bc-bd.org/trac/irssi/ticket/19
178#
179# Well, it's a feature: due to the lacking support of extendable themes
180# from irssi it is not possible to just change some formats per window.
181# This means that right now all windows are aligned with the same nick
182# length, which can be somewhat annoying.
183# If irssi supports extendable themes, I will include per-server indenting
184# and a setting where you can specify servers you don't want to be indented
185#
186###
187################
188
189my ($longestNick, %saved_colors, @colors, $alignment, $sign, %commands,);
190my ($pending_save);
191
192my $colorize = -1;
193
194sub reformat() {
195	my $max = Irssi::settings_get_int('neat_maxlength');
196	my $actsign = Irssi::settings_get_bool('neat_left_actions')? '': '-';
197	$sign = Irssi::settings_get_bool('neat_left_messages')? '': '-';
198
199	if ($max && $max < $longestNick) {
200		$longestNick = $max;
201	}
202
203	my $me = $longestNick - Irssi::settings_get_int('neat_melength');
204	$me = 0 if ($me < 0);
205
206	Irssi::command('^format own_action {ownaction $['.$actsign.$me.']0} $1');
207	Irssi::command('^format action_public {pubaction $['.$actsign.$me.']0}$1');
208	Irssi::command('^format action_private {pvtaction $['.$actsign.$me.']0}$1');
209	Irssi::command('^format action_private_query {pvtaction_query $['.$actsign.$me.']0} $2');
210
211	my $length = $sign . $longestNick;
212	if (Irssi::settings_get_bool('neat_right_mode') == 0) {
213		Irssi::command('^format own_msg {ownmsgnick $2 {ownnick $['.$length.']0}}$1');
214		Irssi::command('^format own_msg_channel {ownmsgnick $3 {ownnick $['.$length.']0}{msgchannel $1}}$2');
215		Irssi::command('^format pubmsg_me {pubmsgmenick $2 {menick $['.$length.']0}}$1');
216		Irssi::command('^format pubmsg_me_channel {pubmsgmenick $3 {menick $['.$length.']0}{msgchannel $1}}$2');
217		Irssi::command('^format pubmsg_hilight {pubmsghinick $0 $3 $['.$length.']1%n}$2');
218		Irssi::command('^format pubmsg_hilight_channel {pubmsghinick $0 $4 $['.$length.']1{msgchannel $2}}$3');
219		Irssi::command('^format pubmsg {pubmsgnick $2 {pubnick $['.$length.']0}}$1');
220		Irssi::command('^format pubmsg_channel {pubmsgnick $2 {pubnick $['.$length.']0}}$1');
221	} else {
222		Irssi::command('^format own_msg {ownmsgnick {ownnick $['.$length.']0$2}}$1');
223		Irssi::command('^format own_msg_channel {ownmsgnick {ownnick $['.$length.']0$3}{msgchannel $1}}$2');
224		Irssi::command('^format pubmsg_me {pubmsgmenick {menick $['.$length.']0}$2}$1');
225		Irssi::command('^format pubmsg_me_channel {pubmsgmenick {menick $['.$length.']0$3}{msgchannel $1}}$2');
226		Irssi::command('^format pubmsg_hilight {pubmsghinick $0 $0 $['.$length.']1$3%n}$2');
227		Irssi::command('^format pubmsg_hilight_channel {pubmsghinick $0 $['.$length.']1$4{msgchannel $2}}$3');
228		Irssi::command('^format pubmsg {pubmsgnick {pubnick $['.$length.']0$2}}$1');
229		Irssi::command('^format pubmsg_channel {pubmsgnick {pubnick $['.$length.']0$2}}$1');
230	}
231
232	# format queries
233	Irssi::command('^format own_msg_private_query {ownprivmsgnick {ownprivnick $['.$length.']2}}$1');
234	Irssi::command('^format msg_private_query {privmsgnick $['.$length.']0}$2');
235};
236
237sub findLongestNick {
238	$longestNick = 0;
239
240	# get own nick length
241	map {
242		my $len = length($_->{nick});
243
244		$longestNick = $len if ($len > $longestNick);
245	} Irssi::servers();
246
247	# find longest other nick
248	foreach (Irssi::channels()) {
249		foreach ($_->nicks()) {
250			my $len = length($_->{nick});
251
252			$longestNick = $len if ($len > $longestNick);
253		}
254	}
255
256	reformat();
257}
258
259sub delayed_save
260{
261	# skip if we have already a save pending. we don't reset the timeout
262	# here, else you could end up with changes never being automatically
263	# saved if they happen more often than <neat_autosave> seconds
264	return if $pending_save;
265
266	return unless Irssi::settings_get_int('neat_autosave');
267
268	Irssi::timeout_add_once(Irssi::settings_get_int('neat_autosave') * 1000,
269		\&save_colors, undef);
270}
271
272# a new nick was created
273sub sig_newNick
274{
275	my ($channel, $nick) = @_;
276
277	my $len = length($nick->{nick});
278
279	if ($len > $longestNick) {
280		$longestNick = $len;
281		reformat();
282	}
283
284	return if (exists($saved_colors{$nick->{nick}}));
285
286	$saved_colors{$nick->{nick}} = "%".nick_to_color($nick->{nick});
287	delayed_save();
288}
289
290# something changed
291sub sig_changeNick
292{
293	my ($channel, $nick, $old_nick) = @_;
294
295	# if no saved color exists, we already handled this nickchange. irssi
296	# generates one signal per channel the nick is in, so if you share more
297	# than one channel with this nick, you'd lose the coloring.
298	return unless exists($saved_colors{$old_nick});
299
300	# we need to update the saved colorors hash independent of nick lenght
301	$saved_colors{$nick->{nick}} = $saved_colors{$old_nick};
302	delete $saved_colors{$old_nick};
303	delayed_save();
304
305	my $new = length($nick->{nick});
306
307	# in case the new nick is longer than the old one, simply remember this
308	# as the new longest nick and reformat.
309	#
310	# if the new nick is as long as the known longest nick nothing has to be
311	# done
312	#
313	# if the new nick is shorter than the current longest one and if the
314	# user allows us to shrink, find new longest nick and reformat.
315	if ($new > $longestNick) {
316		$longestNick = $new;
317	} elsif ($new == $longestNick) {
318		return;
319	} else {
320		return unless Irssi::settings_get_bool('neat_allow_shrinking');
321		findLongestNick();
322	}
323
324	reformat();
325}
326
327sub sig_removeNick
328{
329	my ($channel, $nick) = @_;
330
331	my $thisLen = length($nick->{nick});
332
333	# we only need to recalculate if this was the longest nick and we are
334	# allowed to shrink
335	if ($thisLen == $longestNick && Irssi::settings_get_bool('neat_allow_shrinking')) {
336		findLongestNick();
337		reformat();
338	}
339
340	# we do not remove a known color for a gone nick, as they may return
341}
342
343# based on simple_hash from nickcolor.pl
344sub nick_to_color($) {
345	my ($string) = @_;
346	chomp $string;
347
348	my $ignore = Irssi::settings_get_str("neat_ignorechars");
349	$string =~ s/$ignore//g;
350
351	my $counter;
352	foreach my $char (split(//, $string)) {
353		$counter += ord $char;
354	}
355
356	return $colors[$counter % ($#colors + 1)];
357}
358
359sub color_left($) {
360	Irssi::command('^format pubmsg {pubmsgnick $2 {pubnick '.$_[0].'$['.$sign.$longestNick.']0}}$1');
361	Irssi::command('^format pubmsg_channel {pubmsgnick $2 {pubnick '.$_[0].'$['.$sign.$longestNick.']0}}$1');
362}
363
364sub color_right($) {
365	Irssi::command('^format pubmsg {pubmsgnick {pubnick '.$_[0].'$['.$sign.$longestNick.']0}$2}$1');
366	Irssi::command('^format pubmsg_channel {pubmsgnick {pubnick '.$_[0].'$['.$sign.$longestNick.']0}$2}$1');
367}
368
369sub sig_public {
370	my ($server, $msg, $nick, $address, $target) = @_;
371
372	&$alignment($saved_colors{$nick});
373}
374
375sub sig_setup {
376	@colors = split(//, Irssi::settings_get_str('neat_colors'));
377
378	# check left or right alignment
379	if (Irssi::settings_get_bool('neat_right_mode') == 0) {
380		$alignment = \&color_left;
381	} else {
382		$alignment = \&color_right;
383	}
384
385	# check if we switched coloring on or off
386	my $new = Irssi::settings_get_bool('neat_colorize');
387	if ($new != $colorize) {
388		if ($new) {
389			Irssi::signal_add('message public', 'sig_public');
390		} else {
391			if ($colorize >= 0) {
392				Irssi::signal_remove('message public', 'sig_public');
393			}
394		}
395	}
396	$colorize = $new;
397
398	reformat();
399	&$alignment('%w');
400}
401
402# make sure that every nick has an assigned color
403sub assert_colors() {
404	foreach (Irssi::channels()) {
405		foreach ($_->nicks()) {
406			next if (exists($saved_colors{$_->{nick}}));
407
408			$saved_colors{$_->{nick}} = "%".nick_to_color($_->{nick});
409			delayed_save();
410		}
411	}
412}
413
414# load colors from file
415sub load_colors() {
416	open(FID, "<", $ENV{HOME}."/.irssi/saved_colors") || return;
417
418	while (<FID>) {
419		chomp;
420		my ($k, $v) = split(/:/);
421
422		# skip broken lines, those may have been introduced by nm.pl
423		# version 0.3.7 and earlier
424		if ($k eq '' || $v eq '') {
425			neat_log(Irssi::active_win(), "Warning, broken line in saved_colors file, skipping '$k:$v'");
426			next;
427		}
428
429		$saved_colors{$k} = $v;
430	}
431
432	close(FID);
433}
434
435# save colors to file
436sub save_colors() {
437	open(FID, ">", $ENV{HOME}."/.irssi/saved_colors");
438
439	print FID $_.":".$saved_colors{$_}."\n" foreach (keys(%saved_colors));
440
441	close(FID);
442
443	# clear possible pending save.
444	Irssi::timeout_remove($pending_save) if $pending_save;
445	$pending_save = undef;
446}
447
448# log a line to a window item
449sub neat_log($@) {
450	my ($witem, @text) = @_;
451
452	$witem->print("nm.pl: ".$_) foreach(@text);
453}
454
455# show available colors
456sub cmd_neatcolor_colors($) {
457	my ($witem, undef, undef) = @_;
458
459	neat_log($witem, "Available colors: ".join("", map { "%".$_.$_ } @colors));
460}
461
462# display the configured color for a nick
463sub cmd_neatcolor_get() {
464	my ($witem, $nick, undef) = @_;
465
466	if (!exists($saved_colors{$nick})) {
467		neat_log($witem, "Error: no such nick '$nick'");
468		return;
469	}
470
471	neat_log($witem, "Color for ".$saved_colors{$nick}.$nick);
472}
473
474# display help
475sub cmd_neatcolor_help() {
476	my ($witem, $cmd, undef) = @_;
477
478	if ($cmd) {
479		if (!exists($commands{$cmd})) {
480			neat_log($witem, "Error: no such command '$cmd'");
481			return;
482		}
483
484		if (!exists($commands{$cmd}{verbose})) {
485			neat_log($witem, "No additional help for '$cmd' available");
486			return;
487		}
488
489		neat_log($witem, ( "", "Help for ".uc($cmd), "" ) );
490		neat_log($witem, @{$commands{$cmd}{verbose}});
491		return;
492	}
493
494	neat_log($witem, split(/\n/, $help));
495	neat_log($witem, "Available options for /neatcolor");
496	neat_log($witem, "    ".$_.": ".$commands{$_}{text}) foreach(sort(keys(%commands)));
497
498	my @verbose;
499	foreach (sort(keys(%commands))) {
500		push(@verbose, $_) if exists($commands{$_}{verbose});
501	}
502
503	neat_log($witem, "Verbose help available for: '".join(", ", @verbose)."'");
504}
505
506# list configured nicks
507sub cmd_neatcolor_list() {
508	my ($witem, undef, undef) = @_;
509
510	neat_log($witem, "Configured nicks: ".join(", ", map { $saved_colors{$_}.$_ } sort(keys(%saved_colors))));
511}
512
513# reset a nick to its default color
514sub cmd_neatcolor_reset() {
515	my ($witem, $nick, undef) = @_;
516
517	if ($nick eq '--all') {
518		%saved_colors = ();
519		assert_colors();
520		neat_log($witem, "Reset all colors");
521		return;
522	}
523
524	if (!exists($saved_colors{$nick})) {
525		neat_log($witem, "Error: no such nick '$nick'");
526		return;
527	}
528
529	$saved_colors{$nick} = "%".nick_to_color($nick);
530	delayed_save();
531	neat_log($witem, "Reset color for ".$saved_colors{$nick}.$nick);
532}
533
534# save configured colors to disk
535sub cmd_neatcolor_save() {
536	my ($witem, undef, undef) = @_;
537
538	save_colors();
539
540	neat_log($witem, "color information saved");
541}
542
543# set a color for a nick
544sub cmd_neatcolor_set() {
545	my ($witem, $nick, $color) = @_;
546
547	my @found = grep(/$color/, @colors);
548	if ($#found) {
549		neat_log($witem, "Error: trying to set unknown color '%$color$color%n'");
550		cmd_neatcolor_colors($witem);
551		return;
552	}
553
554	if ($witem->{type} ne "CHANNEL" && $witem->{type} ne "QUERY") {
555		neat_log($witem, "Warning: not a Channel/Query, can not check nick!");
556		neat_log($witem, "Remember, nicks are case sensitive to nm.pl");
557	} else {
558		my @nicks = grep(/^$nick$/i, map { $_->{nick} } ($witem->nicks()));
559
560		if ($#nicks < 0) {
561			neat_log($witem, "Warning: could not find nick '$nick' here");
562		} else {
563			if ($nicks[0] ne $nick) {
564				neat_log($witem, "Warning: using '$nicks[0]' instead of '$nick'");
565				$nick = $nicks[0];
566			}
567		}
568	}
569
570	$saved_colors{$nick} = "%".$color;
571	delayed_save();
572	neat_log($witem, "Set color for $saved_colors{$nick}$nick");
573}
574
575%commands = (
576	colors => {
577		text => "show available colors",
578		verbose => [
579			"COLORS",
580			"",
581			"displays all available colors",
582			"",
583			"You can restrict/define the list of available colors ".
584			"with the help of the neat_colors setting"
585		],
586		func => \&cmd_neatcolor_colors,
587	},
588	get => {
589		text => "retrieve color for a nick",
590		verbose => [
591			"GET <nick>",
592			"",
593			"displays color used for <nick>"
594		],
595		func => \&cmd_neatcolor_get,
596	},
597	help => {
598		text => "print this help message",
599		func => \&cmd_neatcolor_help,
600	},
601	list => {
602		text => "list configured nick/color pairs",
603		func => \&cmd_neatcolor_list,
604	},
605	reset => {
606		text => "reset color to default",
607		verbose => [
608			"RESET --all|<nick>",
609			"",
610			"resets the color used for all nicks or for <nick> to ",
611			"its internal default",
612		],
613		func => \&cmd_neatcolor_reset,
614	},
615	save => {
616		text => "save color information to disk",
617		verbose => [
618			"SAVE",
619			"",
620			"saves color information to disk, so that it survives ".
621			"an irssi restart.",
622			"",
623			"Color information will be automatically saved on /quit",
624		],
625		func => \&cmd_neatcolor_save,
626	},
627	set => {
628		text => "set a specific color for a nick",
629		verbose => [
630			"SET <nick> <color>",
631			"",
632			"use <color> for <nick>",
633			"",
634			"This command will perform a couple of sanity checks, ".
635			"when called from a CHANNEL/QUERY window",
636			"",
637			"EXAMPLE:",
638			"  /neatcolor set bc-bd r",
639			"",
640			"use /neatcolor COLORS to see available colors"
641		],
642		func => \&cmd_neatcolor_set,
643	},
644);
645
646# the main command callback that gets called for all neatcolor commands
647sub cmd_neatcolor() {
648	my ($data, $server, $witem) = @_;
649	my ($cmd, $nick, $color) = split (/ /, $data);
650
651	$cmd = lc($cmd);
652
653	# make sure we have a valid witem to print text to
654	$witem = Irssi::active_win() unless ($witem);
655
656	if (!exists($commands{$cmd})) {
657		neat_log($witem, "Error: unknown command '$cmd'");
658		&{$commands{"help"}{"func"}}($witem) if (exists($commands{"help"}));
659		return;
660	}
661
662	&{$commands{$cmd}{"func"}}($witem, $nick, $color);
663}
664
665Irssi::settings_add_bool('misc', 'neat_left_messages', 0);
666Irssi::settings_add_bool('misc', 'neat_left_actions', 0);
667Irssi::settings_add_bool('misc', 'neat_right_mode', 1);
668Irssi::settings_add_int('misc', 'neat_maxlength', 0);
669Irssi::settings_add_int('misc', 'neat_melength', 2);
670Irssi::settings_add_bool('misc', 'neat_colorize', 1);
671Irssi::settings_add_str('misc', 'neat_colors', 'rRgGyYbBmMcC');
672Irssi::settings_add_str('misc', 'neat_ignorechars', '');
673Irssi::settings_add_bool('misc', 'neat_allow_shrinking', 1);
674Irssi::settings_add_int('misc', 'neat_autosave', 60);
675
676Irssi::command_bind('neatcolor', 'cmd_neatcolor');
677
678Irssi::signal_add('nicklist new', 'sig_newNick');
679Irssi::signal_add('nicklist changed', 'sig_changeNick');
680Irssi::signal_add('nicklist remove', 'sig_removeNick');
681
682Irssi::signal_add('setup changed', 'sig_setup');
683Irssi::signal_add_last('setup reread', 'sig_setup');
684
685findLongestNick();
686sig_setup;
687
688load_colors();
689assert_colors();
690
691# we need to add this signal _after_ the colors have been loaded, to make sure
692# no race condition exists wrt color saving
693Irssi::signal_add('gui exit', 'save_colors');
694