package N0i::Xchat::OSD; use strict; use warnings; use X::Osd; use N0i::Xchat::Base qw(debug); use base qw(N0i::Xchat::Base); sub version { '2005040501' } sub new { my ($class, %o) = @_; my $self = { %o }; bless $self, $class; $self->_init_osd; $self->_init_stack; $self->_init_commands; return $self; } sub _init_osd { my $self = shift; delete $self->{__osd}; # paranoid? my $osd = X::Osd->new($self->{size}) or die "Cannot initialize X::Osd object: $!\n"; $osd->set_font($self->{font}); $osd->set_colour($self->{color}); $osd->set_outline_colour($self->{outline}); $osd->set_timeout($self->{timeout}); $osd->set_shadow_offset($self->{shadow_offset}); $osd->set_outline_offset($self->{outline_offset}); $osd->set_pos($self->{position}); $osd->set_align($self->{align}); $osd->set_horizontal_offset($self->{horizontal_offset}); $osd->set_vertical_offset($self->{vertical_offset}); $self->{__osd} = $osd; } sub _init_stack { my $self = shift; delete $self->{__stack}; # paranoid? $self->{__stack} = Stack->new( EXP => $self->{expires}, SIZE => $self->{size} ); } sub _toggle { my $self = shift; my $key = shift || ''; return (0, "\x02$key\x02 is not a valid configuration field.") if $key =~ /^__/ || !exists $self->{$key}; $self->{$key} = $self->{$key} ? 0 : 1; my $value = $self->{$key} ? 'ON' : 'OFF'; return (1, "\x02$key\x02 messages: \x02$value\x02."); } sub _setval { my ($self, $key, $value) = @_; return (0, "\x02$key\x02 is not a valid configuration field.") if $key =~ /^__/ || !exists $self->{$key}; if (!$value || $self->{$key} eq $value) { return (1, "\x02$key\x02 unchanged (\x02$self->{$key}\x02)"); } else { $self->{$key} = $value; return (1, "Now \x02$key\x02 = \x02$self->{$key}\x02\n"); $self->_init_osd unless $key eq 'expires'; $self->_init_stack if $key eq 'expires' || $key eq 'size'; } } sub display { my $self = shift; my @data = $self->stack->data; for (my $i=0; $i < $self->{size}; $i++) { $data[$i] = '' if !defined $data[$i]; $self->osd->string($i, $data[$i]); } } sub push { my $self = shift; $self->stack->push($_) for @_; $self->display; } sub osd { $_[0]->{__osd} } sub stack { $_[0]->{__stack} } sub command_handler { my $self = shift; my ($command, $arg) = split(' ', $_[0], 2); $arg ||= ''; for ($arg) { s/^\s+// ; s/\s+$// } my $tocall = "_cmd_$command"; if ($self->can($tocall)) { return $self->$tocall($arg); } else { return (0, 'Unknown command!'); } } sub _cmd_about { my $self = shift; return (1, ' Name: ' . $self->package, 'Version: ' . $self->version, ' Author: ' . $self->author ); } sub _cmd_on { $_[0]->{on} = 1; return (1, 'OSD plugin turned ON.'); } sub _cmd_off { $_[0]->{on} = 0; return (1, 'OSD plugin turned OFF.'); } sub _cmd_ignore_target { my ($self, $rex) = @_; my $compiled = eval $rex; if ($@) { return (0, 'Invalid Regular Expression. Errors following:', $@); } else { $_[0]->{ignore_target} = $compiled; return (1, 'Ignore pattern updated.'); } } sub _cmd_dump { my $self = shift; my @ret = (1); foreach($self->stack->data) { CORE::push @ret, $_; } return @ret; } sub _cmd_stat { my $self = shift; my @ret = (1); foreach(sort keys %$self) { next if /^__/; CORE::push @ret, sprintf("\x02%-15s\x02: %s", $_, $self->{$_}); } return @ret; } sub _init_commands { foreach (qw/pubmsg privmsg notice action join part quit ctcp beep/) { my $method = __PACKAGE__ . '::_cmd_' . $_; eval qq{ sub $method { \$_[0]->_toggle('$_') } }; } foreach (qw[ size timeout expires position align horizontal_offset font vertical_offset outline outline_offset shadow_offset beepcmd color uident ignore_unidentified ]) { my $name = $_; my $method = __PACKAGE__ . '::_cmd_' . $name; eval qq{ sub $method { \$_[0]->_setval('$_', \$_[1]) } }; } } sub privmsg_handler { my $self = shift; return unless $self->{on}; local $_ = shift; s/\s+/ /g; my $identified = ''; if ($self->{uident}) { s/^(:[^:]+:)([+-])(.*)$/$1$3/; $identified = $2; if ($self->{ignore_unidentified}) { return unless $identified eq '+'; } } # quick catch of CTCP stuff - except ACTION! if (m/^:([^!]+)\S+ PRIVMSG \S+ :\x01(.+)\x01$/) { my ($nick, $ctcp) = ($1, $2); unless ($ctcp =~ /^ACTION\b/) { return unless $self->{ctcp}; $self->push("CTCP $ctcp from $nick"); return; } } # quick catch of BEEPs if (m/^:([^!]+)\S+ (?:PRIVMSG|NOTICE) \S+ :.*\x07/) { my $nick = $1; if ($self->{beep} && $nick !~ $self->{ignore_target} ) { $self->push("$nick BEEPed you!"); system($self->{beepcmd}) == 0 or return (0, "your OSD Beep Command throws errors: $@"); } } # drop unprintable characters s/[\x00-\x1F]//sg; my ($nick, $type, $target, $text) = ('','','',''); if (m/^:([^!]+)\S+ (PRIVMSG|NOTICE) (\S+) :(.*)$/) { ($nick, $type, $target, $text) = ($1, $2, $3, $4); return if $type eq 'NOTICE' && !$self->{notice}; if ($text =~ s/^ACTION\s+//) { return unless $self->{action}; $text = "*$nick $text"; } else { return unless $self->{privmsg}; $text = "<$nick> $text"; } if ($target =~ /^[#&]/) { return unless $self->{pubmsg}; $text = "$target: $text"; } } elsif ($self->{join} && m/^:([^!]+)\S+ JOIN :(.+)$/) { ($nick, $target) = ($1, $2); $text = "$nick joined $target"; $type = 'JOIN'; } elsif ($self->{part} && m/^:([^!]+)\S+ PART (\S+)/) { ($nick, $target) = ($1, $2); $text = "$nick left $target"; $type = 'PART'; } elsif ($self->{quit} && m/^:([^!]+)\S+ QUIT /) { $nick = $target = $1; $text = "$1 left IRC"; $type = 'QUIT'; } if ( $nick && $target && $text && $nick !~ $self->{ignore_target} && $target !~ $self->{ignore_target} ) { $self->push($text); } return; } ########################################################################### package Stack; use strict; use warnings; sub new { my ($class, %opt) = @_; my $self = { EXP => $opt{EXPIRES} || 60, SIZE => $opt{SIZE} || 4, DATA => [], }; bless $self, $class; } sub push { my ($self, $string) = @_; push @{$self->{DATA}}, [ time() + $self->{EXP}, $string ]; if (@{$self->{DATA}} > $self->{SIZE}) { shift @{$self->{DATA}}; } } sub data { my ($self) = @_; $self->purge; map($_->[1], @{ $self->{DATA} }); } sub purge { my ($self) = @_; my $time = time; while (@{ $self->{DATA} } && $self->{DATA}->[0][0] < $time) { shift @{$self->{DATA}}; } } 1;