A collection of scripts to add tray icon notification on irssi highlights and actions to subsequently activate the irssi session.

Idea

I wanted to migrate from Pidgin to irssi with the BitlBee server (see my post Install and setup BitlBee) as my IM client. What kept me was the convenient notifications on new messages and related keyboard shortcuts that Pidgin provided.

By some scripting I could emulate the Pidgin behaviour when applicable in an irssi context.

Prerequisites

I run irssi in a tmux session under the Fluxbox window manager.

The Perl scripts plugins used requires some modules, and the window activator script requires xdotool. tmux, irssi and Fluxbox are used in my specific setup, but the setup can certainly be changed in part to use other software with small modifications.

This should install everything, but modify after your WM preferences, etc.:

sudo aptitude install irssi tmux fluxbox libgtk2-perl libproc-processtable-perl xdotool

Scripts

trayblinker.pl

This script creates a blinking tray icon that executes an external command on left-click. It listens to the SIGUSR1 signal to exit after flashing the icon a few times. It checks if an instance is already running before execution to avoid blink mayhem in the tray.

The script needs to be modified to reflect local icon and command paths.

View script

#!/usr/bin/perl
use strict;
use warnings;
use Gtk2;
use Gtk2::TrayIcon;
use Proc::ProcessTable;
use File::Basename;

# TRAYBLINKER  -  Create blinking tray icon, execute command on click
#
# USAGE
#     Define icon paths and activation action in the script. Then run as
#     standalone script.
#
#     The script sends its process number as argument to the external script
#     and terminates with visual indication on SIGUSR1. Use this in the script
#     that is called to automatically terminate the blinking tray icon.

if (&already_running) {
    print 'Already running, exiting.' if $ENV{'tty'};
    exit(0);
}

sub already_running
{
    my $table = new Proc::ProcessTable;
    my $scriptname = basename($0);
    foreach my $process (@{$table->table}) {
        return 1 if ($process->fname() eq $scriptname && $process->pid != $$);
    }
    return;
}

my $iconswitcher;
my $blinktimer;
my $countdown;

my $cmd   = '/home/daniel/bin/tmux-irssi';
my $icon1 = '/home/daniel/script/trayblinker/icons/blink1-12x12.xpm';
my $icon2 = '/home/daniel/script/trayblinker/icons/blink2-12x12.xpm';

Gtk2->init;

my $icon     = Gtk2::TrayIcon->new('tray');
my $eventbox = Gtk2::EventBox->new;
my $img      = Gtk2::Image->new_from_file($icon1);

$eventbox->add($img);
$icon->add($eventbox);
$icon->show_all;

$blinktimer = Glib::Timeout->add(650 => \&switch_icon);
$eventbox->signal_connect('button-press-event' => sub{system($cmd, $$)});

sub switch_icon
{
    if ($iconswitcher) {$img->set_from_file($icon1)}
    else               {$img->set_from_file($icon2)};
    $iconswitcher = !$iconswitcher;

    Gtk2->main_quit if ($countdown && --$countdown == 0);
    return 1;
}

sub blink_close
{
    # Stop the old timer
    Glib::Source->remove($blinktimer);
    # Start new with lower interval
    Glib::Timeout->add(100 => \&switch_icon);
    # Blink 4 times
    $countdown = 4;
}

$SIG{'USR1'} = 'blink_close';

Gtk2->main;

hide script.

The script including icons and a Python alternative can be found at http://510x.se/hg/program/trayblinker.

hilightnotify.pl

This irssi plugin will execute an external command when hilight status is set, and another command when hilight status is removed.

If the hilight occurs in the currently active window, the "dehilight" command won't be executed since we want to be noticed even if the irssi tmux session is detached. There is no remotely trivial and robust way to see if we are really looking at the irssi tab, so I found this to be an optimal solution.

The tray icon disappears after receiving the SIGUSR1 signal from the later described script that activates the irssi tmux session.

The script needs to be modified to reflect local command paths.

View script

use strict;
use warnings;
use Irssi;

our $VERSION = '1.0';
our %IRSSI = (
    authors     => 'Daniel Andersson',
    contact     => 'sskraep@gmail.com',
    name        => 'hilightnotify',
    description => 'Executes command on hilight and dehilight',
    license     => 'GNU GPL v2 or later',
    url     => 'http://510x.se/notes',
    changed     => '2012-02-12',
);

Irssi::command_bind('help', sub { if ($_[0] =~ '^hilightnotify ?') {&sig_help; Irssi::signal_stop;} });
sub sig_help
{
    my $usage = <<USAGEEND;
HILIGHTNOTIFY  -  Run external command when hilight status is set or removed.

USAGE
    Load script and customize settings.

    /dehilight
        Run dehilight command.

REQUIRES
    External scripts that do something useful.

SETTINGS
    hilight_run_cmd_when_away <bool>
        Should the command run when you are in away status? (*)

    hilight_cmd_on_hilight
        Command to run when hilighted.

    hilight_cmd_on_dehilight
        Command to run when hilight stops. (**)

EXAMPLE
    I use a Perl script that starts a blinking tray icon. When the icon is
    clicked, or when I issue a WM keyboard shortcut, the same command as
    'hilight_cmd_on_dehilight' is run, which
        1. stops the tray icon
        2. activates the available tmux session (or attaches a new one if one
           doesn't exist)
    Thus, after being hilighted, I can click he icon or press Mod4+i to switch
    to the correct virtual desktop and bring irssi to the front.

    My tray icon script can be found at
    <http://510x.se/hg/program/trayblinker>. My tmux session activator can be
    found at <http://510x.se/hg/program/tmux-irssi>.

NOTES
    (*):
        By choice, the dehilight command is always run even if away status is
        set. This simply makes sense during usage.

    (**):
        By choice, I don't run the dehilight command if I'm currently in the
        irssi window where I'm being hilighted, and thus is "automatically
        dehilighted". Since my tmux session always runs, just because an irssi
        window is active it doesn't mean that I'm looking at it, and I want to
        be notified in most cases. If I'm looking at it, I can just send
        '/dehilight', issue Mod4+i, or click the tray icon to make it stop.

CREDITS
    I looked at <http://scripts.irssi.org/scripts/hilightwin.pl> to get the
    trigger condition for hilight status.

    I got help from Bazerka in #irssi\@IRCnet to emulate a dehilight trigger.
USAGEEND
    Irssi::print($usage, MSGLEVEL_CLIENTCRAP);
}

### Start external settings handling
Irssi::settings_add_bool('hilightnotify', 'hilight_run_cmd_when_away', 1);
Irssi::settings_add_str ('hilightnotify', 'hilight_cmd_on_hilight', '/home/daniel/script/trayblinker/trayblinker.pl');
Irssi::settings_add_str ('hilightnotify', 'hilight_cmd_on_dehilight', '/home/daniel/bin/tmux-irssi');

my $hilight_run_cmd_when_away;
my $hilight_cmd_on_hilight;
my $hilight_cmd_on_dehilight;
sub load_settings
{
    $hilight_run_cmd_when_away = Irssi::settings_get_bool('hilight_run_cmd_when_away');
    $hilight_cmd_on_hilight =    Irssi::settings_get_str ('hilight_cmd_on_hilight');
    $hilight_cmd_on_dehilight =  Irssi::settings_get_str ('hilight_cmd_on_dehilight');
}
&load_settings;
Irssi::signal_add('setup changed', \&sig_setup_changed);
sub sig_setup_changed {&load_settings}
### End external settings handling

### Start signal handling

# Check if hilight status is triggered on the printed text. If so: &hilight.
sub sig_print_text
{
    my ($dest, $text, $stripped) = @_;
    my $server = $dest->{server};

    &hilight if (
        # If here or notifications are wanted anyway
        (!$server->{usermode_away} || $hilight_run_cmd_when_away)
        # If hilighted anywhere (including current window)
        && (
            ($dest->{level} & (MSGLEVEL_HILIGHT|MSGLEVEL_MSGS))
            && ($dest->{level} & MSGLEVEL_NOHILIGHT) == 0
        )
    );
}

# Check if you just switched to a window that _was_ hilighted. This indicates
# that you are present at the terminal and that you have read the message, so
# let's &dehilight.
sub sig_window_activity
{
    my ($dest, $old_level) = @_;
    # data_level 0 == DATA_LEVEL_NONE
    # data_level 1 == DATA_LEVEL_TEXT
    # data_level 2 == DATA_LEVEL_MSG
    # data_level 3 == DATA_LEVEL_HILIGHT
    &dehilight if ($old_level == 3 && $dest->{data_level} < $old_level)
}

# If you send a message, you are certainly in the window -> &dehilight.
# FEATURE: this enables you to dehilight by just pressing Enter! You'll trigger
# send_text, but you won't actually send a message.
sub sig_send_text
{
    # Running &dehilight unconditionally is a dealbreaker if you run
    # another script that sends text autonomously. Add conditions if
    # needed.
    &dehilight;
}
### End signal handling

sub run_cmd   {
    if ((my $pid = fork())) {Irssi::pidwait_add($pid)}
    elsif ($pid == 0) {exec(@_)}
    else {Irssi::print('Fork failed')}
}
sub hilight   {run_cmd($hilight_cmd_on_hilight)}
sub dehilight {run_cmd($hilight_cmd_on_dehilight)}

Irssi::signal_add('print text', \&sig_print_text);
Irssi::signal_add('window activity', \&sig_window_activity);
Irssi::signal_add('send text', \&sig_send_text);
##Two subs for debugging purposes:
#Irssi::command_bind('hl', \&hilight);
#Irssi::command_bind('dehl', \&dehilight);

hide script.

The script can also be found at http://510x.se/hg/program/hilightnotify.

Place hilightnotify.pl in ~/.irssi/scripts/ and load it in irssi with

/run hilightnotify.pl

For autoloading, symlink it in the irssi script autorun directory:

ln -s ../hilightnotify.pl ~/.irssi/scripts/autorun/

Execute

/help hilightnotify

from within irssi to see instructions (or read them directly in the source code, e.g. above).

Activating the tmux session

xdotool searches for a window named irssi and activates it if found. If not found, it creates a new window named irssi and attaches to the irssi tmux session. If no irssi tmux session is found, it starts one and then attaches to it.

If run on a remote computer, the commands are run through SSH.

If called from a TTY, the tmux session is attached in the running terminal. Otherwise a terminal is spawned first.

The SIGUSR1 signal is sent to the tray icon blinker script before window activation.

The script needs to be modified to reflect the trayblinker.pl script name (process name, really) if it is changed from the default.

For the SSH functionality, there needs to be a configuration file ~/.irssi-server which contains the SSH command to connect to the server, e.g.

ssh -t daniel@server.se

The -t is needed to register a TTY over SSH without which tmux won't play with you.

On the server itself, the file needs to exist but be left blank. One could easily make the default behaviour on absence of the configuration file to act as a server, but it is likely that one forgets to create the configuration file on remote hosts which would in best case lead to unnecessary error messages when running the rest of the script. I find that the current way is the friendliest way to handle it.

View script

#!/bin/sh
usage()
{
    cat -- <<-USAGE
   Locally raise/attach/start tmux+irssi session on a specific server.
   usage: ${0##*/} [-s] [-k] [PID]

   positional arguments:
     PID  process id that is sent SIGUSR1 at the end of invocation (default: send
          SIGUSR1 to script name defined in this script)

   optional arguments:
     -s   silence all output
     -k   just kill the notifier

   notes:
     Designed for use with tmux, irssi and trayblinker
     <http://510x.se/hg/program/trayblinker>
   USAGE
    exit 1
}

SILENT=false
JUSTKILL=false
while getopts "skh" option; do
    case ${option} in
        s ) SILENT=true;;
        k ) JUSTKILL=true;;
        h ) usage;;
        * ) usage;;
    esac
done
shift $((${OPTIND}-1))

ttyprint()
{
    ${PRINT} && printf '%s: %s\n' "${0##*/}" "${1}"
}

ttyprinterror()
{
   ttyprint 'error: '"${1}" 1>&2
   exit 1
}

if [ ${#} -eq 1 ]; then
   if [ "${1}" -gt 0 ] 2>/dev/null; then
       KILLPID=${1}
   else
       ttyprinterror 'PID needs to be a positive integer.'
   fi
elif [ ${#} -gt 1 ]; then
   usage
fi

tty -s && TTY=true || TTY=false

PRINT=true
if ${SILENT} || ! ${TTY}; then
   PRINT=false
fi

TMUX="/usr/bin/tmux"
TERMINAL="/usr/bin/urxvtc"
IRSSI="/usr/bin/irssi"
TRAYBLINKER="trayblinker.pl"

WINDOWNAME="irssi"
SESSIONNAME=${WINDOWNAME}
CONFIGURATION="${HOME}/.irssi-server"

# Just leave ~/.irssi-server empty for the server computer. Otherwise, let it
# contain the ssh command to the server, e.g.
# ssh -t daniel@server.se
if [ -f "${CONFIGURATION}" ] && [ -r "${CONFIGURATION}" ]; then
   read SSH < "${CONFIGURATION}"
else
   ttyprint 'Configuration "'"${CONFIGURATION}"'" not found!'
fi

# Shell functions return exit statuses, where 0 is "OK" and !0 is "not OK".
TRUE=0
FALSE=1

start_irssi()
{
   # A bit of a kludge, but other approaches to set the DISPLAY variable
   # to the wanted one on the server fails. If irssi is started over SSH,
   # it will inherit the X forwarded DISPLAY variable, which will then
   # make GUI notifications on the server not work as intended. Force it
   # to hardcoded :0.0 for the time being. This is only wanted when
   # initating the session, and not for other remote operations.
   if [ ! -z "${SSH}" ]; then
       SSH="${SSH} export DISPLAY=:0.0\;"
   fi

   ttyprint 'Starting irssi tmux session...'
   if ( ${SSH} ${TMUX} new-session -d -s "${SESSIONNAME}" "${IRSSI}" && ${SSH} ${TMUX} rename-window -t "${SESSIONNAME}:0" "${SESSIONNAME}" ); then
        ttyprint ' done.'
        return ${TRUE}
    else
        ttyprint ' failed.'
        return ${FALSE}
    fi
}

attach_irssi()
{
    if ${TTY}; then
        ttyprint 'Attaching irssi tmux session...'
        if printf -- '\033]0;%s\007' ${WINDOWNAME} && ${SSH} ${TMUX} attach-session -t "${SESSIONNAME}"; then
           ttyprint ' done.'
           return ${TRUE}
       else
           ttyprint ' failed.'
           return ${FALSE}
       fi
   elif ${TERMINAL} -title "${WINDOWNAME}" -e ${SSH} ${TMUX} attach-session -t "${SESSIONNAME}"; then
        return ${TRUE}
    else
        return ${FALSE}
    fi
}

raise_irssi()
{
    ttyprint 'Searching for available terminal session...'
    if WID=$(xdotool search --limit 1 --name "^${WINDOWNAME}$"); then
        ttyprint ' found, activating.'
        xdotool windowactivate "${WID}"
        return ${TRUE}
    else
        ttyprint ' not found.'
        return ${FALSE}
    fi
}

# Unfortunately cannot wait to see if session attachment is OK before
# contacting trayblinker, since the attachment action won't give an exit status
# until it finishes (=detaches).
if [ -z "${SSH}" ]; then
    if [ -z "${KILLPID}" ] || ! kill -s USR1 -- "${KILLPID}" >/dev/null 2>&1; then
        pkill -USR1 "^${TRAYBLINKER##*/}$"
    fi
fi

${JUSTKILL} && exit

! raise_irssi && ! attach_irssi && start_irssi && attach_irssi

hide script.

Save the script in a path reflected in the two earlier Perl scripts.

The script can also be found at http://510x.se/hg/program/tmux-irssi.

A keyboard shortcut to this script is added in my ~/.fluxbox/keys file:

Mod4 i      :ExecCommand tmux-irssi

To start it maximized, I add the following to ~/.fluxbox/apps:

[app] (name=urxvt) (class=URxvt) (title=irssi)
  [Maximized]   {yes}
[end]

Future plans

Not much. Perhaps including trayblinker.pl in the irssi script directly. This would probably also yield a slight speed increase (which is currently not a real issue), and nicer containment of code for simplified implementation of the specific application of tray icon notification for irssi. However, I can also see trayblinker.pl in other notification applications, and the separate file is easily modified to other circumstances as it stands. It has proven very frustrating to use Gtk2 from within an irssi script, bugs galore, so it will probably not happen.

I've thought of adding the logic controlling whether multiple script instances exist to the irssi script. I don't need it myself at the time being, though, since it is solved in trayblinker.pl, and is a non-issue in tmux-irssi. It would avoid a minimally time-consuming pkill in tmux-irssi to know the pid of the blinker process from the forking in irssi.

The original Python script is included in the HG repository mentioned above, but it contains much more overhead and is noticeably slow on startup. I also ran into some wxPython bugs/quirks during this minimal exercise, which also made me prefer the Perl solution (even though I rarely prefer Perl over Python in other contexts).