1use strict;
2use Irssi::Irc;
3use Irssi 20020217; # Irssi 0.8.0
4use vars qw($VERSION %IRSSI);
5$VERSION = "1.52";
6%IRSSI = (
7    authors     => "Matti 'qvr' Hiljanen",
8    contact     => 'matti\@hiljanen.com',
9    contributors => 'stefan@pico.ruhr.de, dieck@gmx.de, peder@ifi.uio.no',
10    name        => "dccstat",
11    description => "Shows verbose or short information of dcc send/gets on statusbar (speed, size, eta etc.)",
12    license     => "GPL, Version 2",
13    url         => "http://matin.maapallo.org/softa/irssi",
14    sbitems     => "dccstat"
15);
16
17# Theme settings:
18#   sb_dccstat = "{sb $0-}";
19#       $0 = sb_ds_short(_waiting)/sb_ds_normal(_waiting)
20#   sb_ds_short = "$0%G:%n$1%Y@%n$2kB/s%G:%n$4%G:%n$3";
21#       $0 = G/S
22#       $1 = filename
23#       $2 = transfer speed
24#       $3 = percent
25#       $4 = progressbar
26#   sb_ds_short_waiting = "$0%G:%n$1 $2 $3 waiting";
27#       $0 = G/S
28#       $1 = filename
29#       $2 = to/from
30#       $3 = nick
31#   sb_ds_normal = "$0 $1: '$2' $3 of $4 [$8] $9 ($5) $6kB/s ETA: $7";
32#       $0 = GET/SEND
33#       $1 = nick
34#       $2 = filename
35#       $3 = transferred amount
36#       $4 = full filesize
37#       $5 = percent
38#       $6 = speed
39#       $7 = ETA
40#       $8 = progressbar
41#       $9 = rotator thingy :)
42#   sb_ds_normal_waiting = "$0 $1: '$2' $3 $4 $5 waiting";
43#       $0 = GET/SEND
44#       $1 = nick
45#       $2 = filename
46#       $3 = full filesize
47#       $4 = to/from
48#       $5 = nick
49#   sb_ds_separator = ", ";
50#
51# TODO:
52#   new ideas more than welcome :)
53#
54# FAQ:
55#   Q: my input line gets cleared every time dcc send/get starts or ends,
56#   why's that?!
57#   A: it's a bug in irssi which is already fixed in cvs (2002-03-24 Sunday 20:06)
58#   so the solution: upgrade to cvs or live with it and wait until the next stable release
59#
60
61
62use Irssi::TextUI;
63use strict;
64
65my $dccstat_refresh=5;
66my ($refresh_tag, $old_refresh, $new_refresh, $displayed_since);
67my $visible = -1;
68my $displaying = 0;
69my @rot_bar = ('|', '/', '-', '\\\\\\\\');
70my $rot_bar_n = 0;
71my %dccstat;
72
73sub cmd_print_help {
74     Irssi::print(
75     "%_Dccstat.pl Help:%_\n\n".
76     "Statusbar called dccstat should have appeared when you loaded this script,\n".
77     "now you need to add the dccstat item into that statusbar:\n".
78     "      /statusbar dccstat add dccstat\n".
79     "      /save\n\n".
80     " The default verbose mode will produce output like this:  \n".
81     "      [GET nick: 'foobar.avi' 5500kB of 11MB (50%) 99kB/s ETA: 00:03:00]\n".
82     " and the short mode looks like this:\n".
83     "      [G:foobar.avi\@99kB/s:(50%)]\n\n".
84     " %_/SETs:%_\n".
85     "  /set dccstat_refresh <secs> (default: 5)\n".
86     "  /set dccstat_short_mode <ON/OFF> (default: OFF)\n".
87     "      shorter output and doesn't show DCCs: None when there are no GET/SENDs\n".
88     "  /set dccstat_hide_sbar_when_inactive <ON/OFF> (default: OFF)\n".
89     "      hides the statusbar called dccstat when there are no GET/SENDs\n".
90     "  /set dccstat_auto_short_limit (default: 2)\n".
91     "      amount of dcc sends/gets we can have before we automagically switch to short mode\n".
92     "      (when all the info wouldn't fit to statusbar). setting it to 0 will disable it.\n".
93     "  /set dccstat_progbar_width (default: 10)\n".
94     "      progressbar width in chars\n".
95     "  /set dccstat_progbar_transferred (default: '%%g=%%n')\n".
96     "  /set dccstat_progbar_position (default: '%%y>%%n')\n".
97     "  /set dccstat_progbar_remaining (default: '%%r-%%n')\n".
98     "  /set dccstat_cycle_through_transfers (default: OFF)\n".
99     "      cycle trough the transfers (ON) or show all transfers at the same time (OFF, default)\n".
100     "  /set dccstat_cycle_through_transfers_refresh <secs> (default: 5)\n".
101     "      how long to show one transfer at  a time\n".
102     "  /set dccstat_filename_max_length (default: 17)\n".
103     "  /set dccstat_filename_max_length_shortmode (default: 10)\n".
104     "      how much to show of a filename in normal and short modes\n\n".
105     "  /set dccstat_EXPERIMENTAL_fast_refresh (default: OFF)\n".
106     "      use very experimental and super fast refreshing, will probably consume all cpu power,\n".
107     "      depending on your connection speed. but hey, it's fun :)\n".
108     "  /set dccstat_debug (default: OFF)\n".
109     "      show debug messages\n".
110     " \n".
111     "\nSee also: STATUSBAR, DCC and theme help in the actual script"
112     ,MSGLEVEL_CRAP);
113}
114
115sub debug {
116    my ($text) = @_;
117    return unless Irssi::settings_get_bool('dccstat_debug');
118    my($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) =
119      localtime(time);
120    $sec = sprintf("%02d", $sec);
121    $min = sprintf("%02d", $min);
122    $hour = sprintf("%02d", $hour);
123    Irssi::print("DEBUG(%_Dccstat.pl%_): ".$text." [$hour:$min:$sec]");
124}
125
126sub startup_check {
127    debug("START-UP - DEBUG IS ON");
128    my @dccs = Irssi::Irc::dccs();
129    my $act;
130    foreach my $dcc (@dccs) { $act=$dcc if $dcc->{type} eq "SEND" || $dcc->{type} eq "GET"; };
131    dcc_connected($act);
132}
133
134sub dcc_connected {
135    debug("entering dcc_connected");
136    my ($dcc) = @_;
137    return unless $dcc->{type} eq "SEND" || $dcc->{type} eq "GET";
138    debug("removing dcc connected -signal");
139    Irssi::signal_remove('dcc connected', 'dcc_connected');
140    my $refresh_msecs = (Irssi::settings_get_int('dccstat_refresh')*1000);
141    $refresh_msecs = ($dccstat_refresh*1000) if $refresh_msecs < 1000;
142    debug("adding normal timeout..");
143    $refresh_tag=Irssi::timeout_add($refresh_msecs, 'refresh_dccstat', undef);
144    $old_refresh=Irssi::settings_get_int('dccstat_refresh');
145    Irssi::signal_add_last('dcc destroyed', 'dcc_checklast');
146    refresh_dccstat();
147}
148
149sub dcc_setupcheck {
150   $new_refresh = Irssi::settings_get_int('dccstat_refresh');
151   if ($new_refresh != $old_refresh) {
152      debug("setting a new refresh timeout");
153	  $new_refresh = ($new_refresh*1000);
154	  Irssi::timeout_remove($refresh_tag);
155	  $new_refresh = ($dccstat_refresh*1000) if $new_refresh < 1000;
156	  $refresh_tag=Irssi::timeout_add($new_refresh, 'refresh_dccstat', undef);
157	  $old_refresh=Irssi::settings_get_int('dccstat_refresh');
158   }
159   refresh_dccstat();
160}
161
162sub dcc_checklast {
163    my @dccs     = Irssi::Irc::dccs();
164    my $count    = dcc_getcount();
165    debug("check for last, count is '$count'");
166    return unless $count == 0;
167    debug("was last, removing timeout '$refresh_tag'");
168    Irssi::timeout_remove($refresh_tag);
169    Irssi::signal_remove('dcc destroyed', 'dcc_checklast');
170    Irssi::signal_add('dcc connected', 'dcc_connected');
171    refresh_dccstat();
172}
173
174# this function calculates the average speed of the last 10 seconds.
175# i think that's better than irssis default way of calculating the
176# average speed from the whole transfer
177sub dcc_calcSpeed {
178   my @dccs = Irssi::Irc::dccs();
179   foreach my $dcc (@dccs) {
180      next unless $dcc->{type} eq "SEND" || $dcc->{type} eq "GET";
181      my $id = "$dcc->{created}" . "$dcc->{addr}" . "$dcc->{port}";
182      if (defined($dccstat{$id}{'speed'})) {
183         my $old = $dccstat{$id}{'position'};
184         my $current = $dcc->{transfd};
185         my $speed = (($current-$old)/10);
186         unless ($dccstat{$id}{'speed'} == "-1" && ($current-$old) == 0) {
187            $dccstat{$id}{'speed'} = $speed;
188         }
189         $dccstat{$id}{'position'} = $current;
190      } else {
191         # new dcc
192         my $id = "$dcc->{created}" . "$dcc->{addr}" . "$dcc->{port}";
193         debug("creating dcc hash '$id'");
194         $dccstat{$id}{'speed'} = "-1";
195         $dccstat{$id}{'position'} = "0";
196      }
197   }
198
199    # let's remove old hashes
200    foreach my $hash (keys %dccstat) {
201       my $keep = 0;
202       foreach my $dcc (@dccs) {
203          my $id = "$dcc->{created}" . "$dcc->{addr}" . "$dcc->{port}";
204          $keep = 1 if ($hash == $id);
205       }
206       if ($keep) {
207          debug("dcc '$hash' is still active, it's speed is '" . $dccstat{$hash}{'speed'} . "'");
208       } else {
209          debug("deleting dcc '$hash'");
210          delete $dccstat{$hash};
211       }
212    }
213}
214
215### this function originally implemented by dieck@gmx.de
216sub dcc_calculateETA {
217    my $dcc = $_[0];
218    my ($dccspeed, $dccleft, $going, $dccsecs, $dcctime);
219
220    # calculate current speed
221    $going=(time-$dcc->{starttime});
222    $going=1 if $going==0;
223    my $id = "$dcc->{created}" . "$dcc->{addr}" . "$dcc->{port}";
224    if (defined($dccstat{$id})) {
225       $dccspeed=$dccstat{$id}{'speed'};
226    } else {
227       $dccspeed = -1;
228    }
229    ## speed in bytes/sec
230    if ($dccspeed > 0) {
231
232       # calculate left transfer size
233       $dccleft = ($dcc->{size}-$dcc->{transfd});
234       ## size left in byte
235
236       $dccspeed=1 if $dccspeed==0;
237       $dccsecs = $dccleft / $dccspeed;
238
239       $dcctime =  sprintf("%02d:%02d:%02d", int($dccsecs/60/60), int($dccsecs/60%60), int($dccsecs%60));
240    } elsif ($dccspeed == "0") {
241       $dcctime = "stalled";
242    } elsif ($dccspeed == "-1") {
243       $dcctime = "???";
244    } else {
245       # panic!
246       $dcctime = "error!";
247    }
248    return $dcctime;
249}
250
251### this function originally implemented by stefan_tomanek@web.de
252sub dcc_progbar {
253    my ($dcc) = @_;
254    my ($filebar, $nobar);
255    my $barwidth = Irssi::settings_get_int('dccstat_progbar_width');
256    my $char1 = Irssi::settings_get_str('dccstat_progbar_transferred');
257    my $char2 = Irssi::settings_get_str('dccstat_progbar_position');
258    my $char3 = Irssi::settings_get_str('dccstat_progbar_remaining');
259    if ($dcc->{size} > 0) {
260        my $width_per_size = ($barwidth) / $dcc->{size};
261        my $transf_chars = sprintf("%.0f",($width_per_size * $dcc->{transfd}));
262        $filebar = $char1 x $transf_chars;
263        $nobar = $char3 x ($barwidth - $transf_chars - 1);
264        return "${filebar}${char2}${nobar}";
265    } else {
266        return $barwidth x $char3;
267    }
268}
269
270sub dcc_calculateSIZE {
271    my $fsize = $_[0];
272    my ($size, $unit, $div);
273
274    if       ($fsize >= 1024*1024*1024)  { $size = $fsize/1024/1024/1024; $unit = "GB"; $div = 2; }
275    elsif    ($fsize >= 1024*1024)       { $size = $fsize/1024/1024; $unit = "MB";  $div = 2; }
276    elsif    ($fsize >= 1024)            { $size = $fsize/1024; $unit = "kB"; $div = 0; }
277    else                                 { $size = $fsize; $unit = "B"; $div = 0; }
278    $size = sprintf("%.${div}f", $size);
279    return "${size}${unit}";
280}
281
282sub dcc_getcount {
283    my @dccs  = Irssi::Irc::dccs();
284    my $count = 0;
285    foreach my $dcc (@dccs) { $count++ if $dcc->{type} eq "GET" || $dcc->{type} eq "SEND"; }
286    return $count;
287}
288
289sub dccstat {
290    #debug("going into main function");
291    my ($item, $get_size_only) = @_;
292    my @dccs=Irssi::Irc::dccs();
293    my (@results, $results);
294    my $mode      = Irssi::settings_get_bool('dccstat_short_mode');
295    my $exp_flags = Irssi::EXPAND_FLAG_IGNORE_EMPTY | Irssi::EXPAND_FLAG_IGNORE_REPLACES;
296    my $theme     = Irssi::current_theme();
297    my $format    = $theme->format_expand("{sb_dccstat}");
298    my $count     = dcc_getcount();
299    if ($count>0) {
300	my $sendcount=0;
301	my $getcount=0;
302	my (
303	    $dccpercent,   $dccspeed,      $dcctype,   $going,
304	    $dccnick,      $dccfile,       $FooOfBar,  $str,
305	    $fsize,        $transize,      $dcceta,    $from,
306	    $to,           $direction,     $prep,      $autolimit,
307	    $separator,    $dccprogbar,    $dccrotbar
308	   );
309	foreach my $dcc (@dccs) {
310	    next unless $dcc->{type} eq "SEND" || $dcc->{type} eq "GET";
311	    # if count is above the autolimit, we'll force the mode to short
312        # but not if we're cycling through transfers.
313        if (not Irssi::settings_get_bool('dccstat_cycle_through_transfers')) {
314           $autolimit=Irssi::settings_get_int('dccstat_auto_short_limit');
315	       $mode=1 if $count > $autolimit && $autolimit > 0;
316        }
317
318	    $sendcount++ if $dcc->{type} eq "SEND";
319	    $getcount++ if $dcc->{type} eq "GET";
320
321	    $dccpercent = ($dcc->{size} == 0) ? "(0%)" : sprintf("%.1f", $dcc->{transfd}/$dcc->{size}*100)."%%";
322
323       $going = (time-$dcc->{starttime});
324	   $going = 1 if $going==0;
325
326	    my $id = "$dcc->{created}" . "$dcc->{addr}" . "$dcc->{port}";
327        if (defined($dccstat{$id})) {
328           $dccspeed = $dccstat{$id}{'speed'};
329        } else {
330           $dccspeed = -1;
331        }
332        if ($dccspeed >= 0) {
333           $dccspeed = sprintf("%.2f", ($dccspeed/1024));
334        } else {
335           $dccspeed = sprintf("%.2f", ($dcc->{transfd}-$dcc->{skipped})/$going/1024);
336        }
337
338        $dcctype = $dcc->{type};
339
340	    $dccnick = $dcc->{nick};
341	    $dccnick =~ s/\\/\\\\/g;
342	    $dccfile = $dcc->{arg};
343	    $dccfile =~ s/ /\240/g;
344	    $dccfile =~ s/\\/\\\\/g;
345
346	    # if filename is longer than 17 chars, we'll show only the first 15 chars
347	    # and in short mode we'll show only 8 chars
348        # (lengths are now configurable, but the idea is the same)
349	    my $max_normal = Irssi::settings_get_int('dccstat_filename_max_length');
350        my $max_short = Irssi::settings_get_int('dccstat_filename_max_length_shortmode');
351	    if (!$mode) {
352           $dccfile=substr($dccfile, 0, $max_normal-2).".." if (length($dccfile) > $max_normal);
353        } else {
354           $dccfile=substr($dccfile, 0, $max_short-2).".." if (length($dccfile) > $max_short);
355        }
356
357	    $fsize      = dcc_calculateSIZE($dcc->{size});
358	    $transize   = dcc_calculateSIZE($dcc->{transfd});
359	    $dccprogbar = dcc_progbar($dcc);
360	    $dccprogbar =~ s/ /\240/g;
361	    $dcceta     = dcc_calculateETA($dcc);
362
363	    if ($dcctype eq "GET") { $direction = "G"; $prep = "from"; }
364	    if ($dcctype eq "SEND") { $direction = "S"; $prep = "to"; }
365
366	    $dccrotbar  = $rot_bar[$rot_bar_n];
367
368	    # short mode?
369	    if ($mode) {
370		# theme?
371		if ($format) {
372		    if ($dcc->{starttime} > 0) {
373			$str = $theme->format_expand("{sb_ds_short $direction $dccfile $dccspeed $dccpercent $dccprogbar $dccrotbar}", $exp_flags);
374		    } else {
375			$str = $theme->format_expand("{sb_ds_short_waiting $direction $dccfile $prep $dccnick}", $exp_flags);
376		    }
377		} else {
378		    $str = "$direction%G:%n$dccfile";
379		    $str .= ($dcc->{starttime} > 0) ? "%G@%n${dccspeed}kB/s%G:%n$dccprogbar%G:%n$dccrotbar%G:%n$dccpercent" : " $prep $dccnick waiting";
380		}
381	    } else {
382		if ($format) {
383		    if ($dcc->{starttime} > 0) {
384			$str = $theme->format_expand("{sb_ds_normal $dcctype $dccnick $dccfile $transize $fsize $dccpercent $dccspeed $dcceta $dccprogbar $dccrotbar}", $exp_flags);
385		    } else {
386			$str = $theme->format_expand("{sb_ds_normal_waiting $dcctype $dccnick $dccfile $fsize $prep $dccnick}", $exp_flags);
387		    }
388		} else {
389		    $str = "$dcctype $dccnick: '$dccfile'";
390		    $str .= ($dcc->{starttime} > 0) ? " $transize of $fsize [$dccprogbar] $dccrotbar ($dccpercent) ${dccspeed}kB/s ETA: $dcceta" : " $fsize $prep $dccnick waiting";
391		}
392	    }
393	    push @results,$str;
394	}
395	if (not Irssi::settings_get_bool('dccstat_cycle_through_transfers')) {
396	    $separator = ($theme->format_expand("{sb_ds_separator}")) ? $theme->format_expand("{sb_ds_separator}") : ", ";
397	    $results   = join("$separator", @results);
398	} else {
399	    if (scalar(@results)-1 < $displaying) { $displaying = 0 };
400	    $results = @results[$displaying];
401        if (not $get_size_only) {
402           if ((time-$displayed_since) >= (Irssi::settings_get_int('dccstat_cycle_through_transfers_refresh'))) {
403              debug("refreshing cycle display");
404              $displaying++;
405              $displayed_since = time;
406           }
407        }
408	}
409    } else {
410	$results="%_DCCs:%_ None" if !$mode;
411    }
412    if ($format) {
413	if ($count > 0) {
414	    $results = "{sb_dccstat $results}"
415	} else {
416	    $results = "{sb_dccstat $results}" unless $mode;
417	}
418    } else {
419	if ($count > 0) {
420	    $results = "{sb $results}";
421	} else {
422	    $results = "{sb $results}" unless $mode;
423	}
424    }
425    $item->default_handler($get_size_only, "$results", undef, 1);
426}
427
428sub refresh_dccstat {
429    #debug("refreshing item");
430    my $hide      = Irssi::settings_get_bool('dccstat_hide_sbar_when_inactive');
431    my $count     = dcc_getcount();
432
433    if ($hide && $count == 0) {
434	if ($visible == -1 || $visible == 1) {
435	    Irssi::command("statusbar dccstat disable");
436	    debug("disabling statusbar");
437	    $visible = 0;
438	}
439	return;
440    }
441    if ($visible == 0 || $visible == -1) {
442	Irssi::command("statusbar dccstat enable");
443	debug("enabling statusbar");
444	$visible = 1;
445    }
446    Irssi::statusbar_items_redraw('dccstat');
447    $rot_bar_n++;
448    $rot_bar_n %= @rot_bar;
449
450}
451
452my $fref = 0;
453sub dcc_fast_refresh {
454    if (Irssi::settings_get_bool('dccstat_EXPERIMENTAL_fast_refresh'))
455    {
456       refresh_dccstat();
457       $fref++;
458       debug("transfer updated! ($fref)");
459    }
460}
461
462Irssi::settings_add_int($IRSSI{'name'}, "dccstat_refresh", $dccstat_refresh);
463Irssi::settings_add_bool($IRSSI{'name'}, 'dccstat_short_mode', 0);
464Irssi::settings_add_bool($IRSSI{'name'}, 'dccstat_hide_sbar_when_inactive', 0);
465Irssi::settings_add_int($IRSSI{'name'}, 'dccstat_auto_short_limit', 2);
466Irssi::settings_add_int($IRSSI{'name'}, 'dccstat_progbar_width', 10);
467Irssi::settings_add_str($IRSSI{'name'}, 'dccstat_progbar_transferred', '%g=%n');
468Irssi::settings_add_str($IRSSI{'name'}, 'dccstat_progbar_position', '%y>%n');
469Irssi::settings_add_str($IRSSI{'name'}, 'dccstat_progbar_remaining', '%r-%n');
470Irssi::settings_add_bool($IRSSI{'name'}, 'dccstat_cycle_through_transfers', 0);
471Irssi::settings_add_int($IRSSI{'name'}, 'dccstat_cycle_through_transfers_refresh', 10);
472Irssi::settings_add_bool($IRSSI{'name'}, 'dccstat_EXPERIMENTAL_fast_refresh', 0);
473Irssi::settings_add_bool($IRSSI{'name'}, 'dccstat_debug', 0);
474Irssi::settings_add_int($IRSSI{'name'}, 'dccstat_filename_max_length', 17);
475Irssi::settings_add_int($IRSSI{'name'}, 'dccstat_filename_max_length_shortmode', 10);
476
477Irssi::command_bind('dccstat', 'cmd_print_help');
478
479Irssi::statusbar_item_register('dccstat', undef, 'dccstat');
480Irssi::timeout_add('10000', 'dcc_calcSpeed', undef);
481Irssi::signal_add('dcc connected', 'dcc_connected');
482Irssi::signal_add(
483		  {
484		   'setup changed'       =>  \&dcc_setupcheck,
485		   'dcc request'         =>  \&refresh_dccstat,
486		   'dcc created'         =>  \&refresh_dccstat,
487		   'dcc destroyed'       =>  \&refresh_dccstat,
488		   'dcc transfer update' =>  \&dcc_fast_refresh,
489		  }
490		 );
491
492# Startup
493startup_check();
494refresh_dccstat();
495
496# lets save some global variables
497$old_refresh    = Irssi::settings_get_int('dccstat_refresh');
498
499Irssi::print("Dccstat.pl loaded - /dccstat for help");
500
501# EOF
502