1 use strict;
2 use warnings;
3
4 our $VERSION = '1.8'; # 63feb35d8b0a8b6
5 our %IRSSI = (
6 authors => 'Nei',
7 contact => 'Nei @ anti@conference.jabber.teamidiot.de',
8 url => "http://anti.teamidiot.de/",
9 name => 'adv_windowlist',
10 description => 'Adds a permanent advanced window list on the right or in a status bar.',
11 sbitems => 'awl_shared',
12 license => 'GNU GPLv2 or later',
13 );
14
15 # UPGRADE NOTE
16 # ============
17 # for users of 0.7 or earlier series, please note that appearance
18 # settings have moved to /format, i.e. inside your theme!
19 # the fifo (screen) has been replaced by an external viewer script
20
21 # Usage
22 # =====
23 # copy the script to ~/.irssi/scripts/
24 #
25 # In irssi:
26 #
27 # /run adv_windowlist
28 #
29 # In your shell (for example a tmux split):
30 #
31 # perl ~/.irssi/scripts/adv_windowlist.pl
32 #
33 # To use sbar mode instead:
34 #
35 # /toggle awl_viewer
36 #
37 # Hint: to get rid of the old [Act:] display
38 # /statusbar window remove act
39 #
40 # to get it back:
41 # /statusbar window add -after lag -priority 10 act
42
43 # Options
44 # =======
45 # formats can be cleared with /format -delete
46 #
47 # /format awl_display_(no)key(_active|_visible) <string>
48 # * string : Format String for one window. The following $'s are expanded:
49 # $C : Name
50 # $N : Number of the Window
51 # $Q : meta-Keymap
52 # $H : Start hilighting
53 # $S : Stop hilighting
54 # /+++++++++++++++++++++++++++++++++,
55 # | **** I M P O R T A N T : **** |
56 # | |
57 # | don't forget to use $S if you |
58 # | used $H before! |
59 # | |
60 # '+++++++++++++++++++++++++++++++++/
61 # key : a key binding that goes to this window could be detected in /bind
62 # nokey : no such key binding was detected
63 # active : window would receive the input you are currently typing
64 # visible : window is also visible on screen but not active (a split window)
65 #
66 # /format awl_name_display <string>
67 # * string : Format String for window names
68 # $0 : name as formatted by the settings
69 #
70 # /format awl_display_header <string>
71 # * string : Format String for this header line. The following $'s are expanded:
72 # $C : network tag
73 #
74 # /format awl_separator(2) <string>
75 # * string : Character to use between the channel entries
76 # variant 2 can be used for alternating separators (only in status bar
77 # without block display)
78 #
79 # /format awl_abbrev_chars <string>
80 # * string : Character to use when shortening long names. The second character
81 # will be used if two blocks need to be filled.
82 #
83 # /format awl_title <string>
84 # * string : Text to display in the title string or title bar
85 #
86 # /format awl_viewer_item_bg <string>
87 # * string : Format String specifying the viewer's item background colour
88 #
89 # /set awl_prefer_name <ON|OFF>
90 # * this setting decides whether awl will use the active_name (OFF) or the
91 # window name as the name/caption in awl_display_*.
92 # That way you can rename windows using /window name myownname.
93 #
94 # /set awl_hide_empty <num>
95 # * if visible windows without items should be hidden from the window list
96 # set it to 0 to show all windows
97 # 1 to hide visible windows without items (negative exempt
98 # active window)
99 #
100 # /set awl_detach <list>
101 # * list of windows that should be hidden from the window list. you
102 # can also use /awl detach and /awl attach to manage this
103 # setting. an optional data_level can be specified with ",num"
104 #
105 # /set awl_detach_data <num>
106 # * num : hide the detached window if its data_level is below num
107 #
108 # /set awl_detach_aht <ON|OFF>
109 # * if enabled, also detach all windows listed in the
110 # activity_hide_targets setting
111 #
112 # /set awl_hide_data <num>
113 # * num : hide the window if its data_level is below num
114 # set it to 0 to basically disable this feature,
115 # 1 if you don't want windows without activity to be shown
116 # 2 to show only those windows with channel text or hilight
117 # 3 to show only windows with hilight (negative exempt active window)
118 #
119 # /set awl_hide_name_data <num>
120 # * num : hide the name of the window if its data_level is below num
121 # (only works in status bar without block display)
122 # you will want to change your formats to add $H...$S around $Q or $N
123 # if you plan to use this
124 #
125 # /set awl_maxlines <num>
126 # * num : number of lines to use for the window list (0 to disable, negative
127 # lock)
128 #
129 # /set awl_maxcolumns <num>
130 # * num : number of columns to use for the window list when using the
131 # tmux integration (0 to disable)
132 #
133 # /set awl_block <num>
134 # * num : width of a column in viewer mode (negative values = block
135 # display in status bar mode)
136 # /+++++++++++++++++++++++++++++++++,
137 # | ****** W A R N I N G ! ****** |
138 # | |
139 # | If your block display looks |
140 # | DISTORTED, you need to add the |
141 # | following line to your .theme |
142 # | file under |
143 # | abstracts = { : |
144 # | |
145 # | sb_act_none = "%K$*"; |
146 # | |
147 # '+++++++++++++++++++++++++++++++++/
148 #
149 # /set awl_sbar_maxlength <ON|OFF>
150 # * if you enable the maxlength setting, the block width will be used as a
151 # maximum length for the non-block status bar mode too.
152 #
153 # /set awl_height_adjust <num>
154 # * num : how many lines to leave empty in viewer mode
155 #
156 # /set awl_sort <-data_level|-last_line|refnum>
157 # * you can change the window sort order with this variable
158 # -data_level : sort windows with hilight first
159 # -last_line : sort windows in order of activity
160 # refnum : sort windows by window number
161 # active/server/tag : sort by server name
162 # lru : sort windows with the last recently used last
163 # "-" reverses the sort order
164 # typechecks are supported via ::, e.g. active::Query or active::Irc::Query
165 # undefinedness can be checked with ~, e.g. ~active
166 # string comparison can be done with =, e.g. name=(status)
167 # to make sort case insensitive, use #i, e.g. name#i
168 # any key in the window hash can be tested, e.g. active/chat_type=XMPP
169 # multiple criteria can be separated with , or +, e.g. -data_level+-last_line
170 #
171 # /set awl_placement <top|bottom>
172 # /set awl_position <num>
173 # * these settings correspond to /statusbar because awl will create
174 # status bars for you
175 # (see /help statusbar to learn more)
176 #
177 # /set awl_all_disable <ON|OFF>
178 # * if you set awl_all_disable to ON, awl will also remove the
179 # last status bar it created if it is empty.
180 # As you might guess, this only makes sense with awl_hide_data > 0 ;)
181 #
182 # /set awl_viewer <ON|OFF>
183 # * enable the external viewer script
184 #
185 # /set awl_viewer_launch <ON|OFF>
186 # * try to auto-launch the viewer under tmux or with a shell command
187 # /awl restart is required all auto-launch related settings to take
188 # effect
189 #
190 # /set awl_viewer_tmux_position <left|top|right|bottom|custom>
191 # * try to split in this direction when using tmux for the viewer
192 # custom : use custom_command setting
193 #
194 # /set awl_viewer_xwin_command <shell command>
195 # * custom command to run in order to start the viewer when irssi is
196 # running under X
197 # %A - gets replaced by the command to run the viewer
198 # %qA - additionally quote the command
199 #
200 # /set awl_viewer_custom_command <shell command>
201 # * custom command to run in order to start the viewer
202 #
203 # /set awl_viewer_launch_env <string>
204 # * specific environment settings for use on viewer auto-launch,
205 # without the AWL_ prefix
206 #
207 # /set awl_shared_sbar <left<right|OFF>
208 # * share a status bar for the first awl item, you will need to manually
209 # /statusbar window add -after lag -priority 10 awl_shared
210 # left : space in cells occupied on the left of status bar
211 # right : space occupied on the right
212 # Note: you need to replace "left" AND "right" with the appropriate numbers!
213 #
214 # /set awl_path <path>
215 # * path to the file which the viewer script reads
216 #
217 # /set fancy_abbrev <no|head|strict|fancy>
218 # * how to shorten too long names
219 # no : shorten in the middle
220 # head : always cut off the ends
221 # strict : shorten repeating substrings
222 # fancy : combination of no+strict
223 #
224 # /set awl_custom_xform <perl code>
225 # * specify a custom routine to transform window names
226 # example: s/^#// remove the #-mark of IRC channels
227 # the special flags $CHANNEL / $TAG / $QUERY / $NAME can be
228 # tested in conditionals
229 #
230 # /set awl_last_line_shade <timeout>
231 # * set timeout to shade activity base colours, to enable
232 # you also need to add +-last_line to awl_sort
233 # (requires 256 colour support)
234 #
235 # /set awl_no_mode_hint <ON|OFF>
236 # * whether to show the hint of running the viewer script in the
237 # status bar
238 #
239 # /set awl_mouse <ON|OFF>
240 # * enable the terminal mouse in irssi
241 # (use the awl-patched mouse.pl for gestures and commands if you need
242 # them and disable mouse_escape)
243 #
244 # /set awl_mouse_offset <num>
245 # * specifies where on the screen is the awl status bar
246 # (0 = on top/bottom, 1 = one additional line in between,
247 # e.g. prompt)
248 # you MUST set this correctly otherwise the mouse coordinates will
249 # be off
250 #
251 # /set mouse_scroll <num>
252 # * how many lines the mouse wheel scrolls
253 #
254 # /set mouse_escape <num>
255 # * seconds to disable the mouse, when not clicked on the windowlist
256 #
257
258 # Commands
259 # ========
260 # /awl detach <num>
261 # * hide the current window from the window list. num specifies the
262 # data_level (optional)
263 #
264 # /awl attach
265 # * unhide the current window from the window list
266 #
267 # /awl ack
268 # * change to the next window with activity, ignoring detached windows
269 #
270 # /awl redraw
271 # * redraws the windowlist. There may be occasions where the
272 # windowlist can get destroyed so you can use this command to
273 # force a redraw.
274 #
275 # /awl restart
276 # * restart the connection to the viewer script.
277
278 # Viewer script
279 # =============
280 # When run from the command line, adv_windowlist acts as the viewer
281 # script to be used together with the irssi script to display the
282 # window list in a sidebar/terminal of its own.
283 #
284 # One optional parameter is accepted, the awl_path
285 #
286 # The viewer can be configured by three environment variables:
287 #
288 # AWL_HI9=1
289 # * interpret %9 as high-intensity toggle instead of bold. This had
290 # been the default prior to version 0.9b8
291 #
292 # AWL_AUTOFOCUS=0
293 # * disable auto-focus behaviour when activating a window
294 #
295 # AWL_NOTITLE=1
296 # * disable the title bar
297
298 # Nei =^.^= ( anti@conference.jabber.teamidiot.de )
299
300 no warnings 'redefine';
301 use constant IN_IRSSI => __PACKAGE__ ne 'main' || $ENV{IRSSI_MOCK};
302 use constant SCRIPT_FILE => __FILE__;
303 no if !IN_IRSSI, strict => (qw(subs refs));
304 use if IN_IRSSI, Irssi => ();
305 use if IN_IRSSI, 'Irssi::TextUI' => ();
306 use v5.10;
307 use Encode;
308 use Storable ();
309 use IO::Socket::UNIX;
310 use List::Util qw(min max reduce);
311 use Hash::Util qw(lock_keys);
312 use Text::ParseWords qw(shellwords);
313
314 BEGIN {
315 if ($] < 5.012) {
316 *CORE::GLOBAL::length = *CORE::GLOBAL::length = sub (_) {
317 defined $_[0] ? CORE::length($_[0]) : undef
318 };
319 }
320 *Irssi::active_win = {}; # hide incorrect warning
321 }
322
323 unless (IN_IRSSI) {
324 local *_ = \@ARGV;
325 &AwlViewer::main;
326 exit;
327 }
328
329
330 use constant GLOB_QUEUE_TIMER => 100;
331
332 our $BLOCK_ALL; # localized blocker
333 my @actString; # status bar texts
334 my @win_items;
335 my $currentLines = 0;
336 my %awins;
337 my $globTime; # timer to limit remake calls
338
339 my %CHANGED;
340 my $VIEWER_MODE;
341 my $MOUSE_ON;
342 my %mouse_coords;
343 my %statusbars;
344 my %S; # settings
345 my $settings_str = '1';
346 my $window_sort_func;
347 my $custom_xform;
348 my ($sb_base_width, $sb_base_width_pre, $sb_base_width_post);
349 my $print_text_activity;
350 my $shade_line_timer;
351 my ($screenHeight, $screenWidth);
352 my %viewer;
353
354 my (%keymap, %nummap, %wnmap, %specialmap, %wnmap_exp, %custom_key_map);
355 my %banned_channels;
356 my %detach_map;
357 my %abbrev_cache;
358
359 use constant setc => 'awl';
360
361 sub set ($) {
362 setc . '_' . $_[0]
363 }
364
365 sub add_statusbar {
366 for (@_) {
367 # add subs
368 my $l = set $_;
369 {
370 my $close = $_;
371 no strict 'refs';
372 *{$l} = sub { awl($close, @_) };
373 }
374 Irssi::command("^statusbar $l reset");
375 Irssi::command("statusbar $l enable");
376 if (lc $S{placement} eq 'top') {
377 Irssi::command("statusbar $l placement top");
378 }
379 if (my $x = $S{position}) {
380 Irssi::command("statusbar $l position $x");
381 }
382 Irssi::command("statusbar $l add -priority 100 -alignment left barstart");
383 Irssi::command("statusbar $l add $l");
384 Irssi::command("statusbar $l add -priority 100 -alignment right barend");
385 Irssi::command("statusbar $l disable");
386 Irssi::statusbar_item_register($l, '$0', $l);
387 $statusbars{$_} = 1;
388 Irssi::command("statusbar $l enable");
389 }
390 }
391
392 sub remove_statusbar {
393 for (@_) {
394 my $l = set $_;
395 Irssi::command("statusbar $l disable");
396 Irssi::command("statusbar $l reset");
397 Irssi::statusbar_item_unregister($l);
398 {
399 no strict 'refs';
400 undef &{$l};
401 }
402 delete $statusbars{$_};
403 }
404 }
405
406 my $awl_shared_empty = sub {
407 return if $BLOCK_ALL;
408 my ($item, $get_size_only) = @_;
409 $item->default_handler($get_size_only, '', '', 0);
410 };
411
412 sub syncLines {
413 my $maxLines = $S{maxlines};
414 my $newLines = ($maxLines > 0 and @actString > $maxLines) ?
415 $maxLines :
416 ($maxLines < 0) ?
417 -$maxLines :
418 @actString;
419 $currentLines = 1 if !$currentLines && $S{shared_sbar};
420 if ($S{shared_sbar} && !$statusbars{shared}) {
421 my $l = set 'shared';
422 {
423 no strict 'refs';
424 *{$l} = sub {
425 return if $BLOCK_ALL;
426 my ($item, $get_size_only) = @_;
427
428 my $text = $actString[0];
429 my $title = _get_format(set 'title');
430 if (length $title) {
431 $title =~ s{\\(.)|(.)}{
432 defined $2 ? quotemeta $2
433 : $1 eq 'V' ? '\u'
434 : $1 eq ':' ? quotemeta ':%n'
435 : $1 =~ /^[uUFQE]$/ ? "\\$1"
436 : quotemeta "\\$1"
437 }sge;
438 $title = eval qq{"$title"};
439 $title .= ' ';
440 }
441 my $pat = defined $text ? "{sb $title\$*}" : '{sb }';
442 $text //= '';
443 $item->default_handler($get_size_only, $pat, $text, 0);
444 };
445 }
446 $statusbars{shared} = 1;
447 remove_statusbar (0) if $statusbars{0};
448 }
449 elsif ($statusbars{shared} && !$S{shared_sbar}) {
450 add_statusbar (0) if $currentLines && $newLines;
451 delete $statusbars{shared};
452 my $l = set 'shared';
453 {
454 no strict 'refs';
455 *{$l} = $awl_shared_empty;
456 }
457 }
458 if ($currentLines == $newLines) { return; }
459 elsif ($newLines > $currentLines) {
460 add_statusbar ($currentLines .. ($newLines - 1));
461 }
462 else {
463 remove_statusbar (reverse ($newLines .. ($currentLines - 1)));
464 }
465 $currentLines = $newLines;
466 }
467
468 sub awl {
469 return if $BLOCK_ALL;
470 my ($line, $item, $get_size_only) = @_;
471
472 my $text = $actString[$line];
473 my $pat = defined $text ? '{sb $*}' : '{sb }';
474 $text //= '';
475 $item->default_handler($get_size_only, $pat, $text, 0);
476 }
477
478 # remove old statusbars
479 { my %killBar;
480 sub get_old_status {
481 my ($textDest, $cont, $cont_stripped) = @_;
482 if ($textDest->{level} == 524288 and $textDest->{target} eq '' and !defined $textDest->{server}) {
483 my $name = quotemeta(set '');
484 if ($cont_stripped =~ m/^$name(\d+)\s/) { $killBar{$1} = 1; }
485 Irssi::signal_stop;
486 }
487 }
488 sub killOldStatus {
489 %killBar = ();
490 Irssi::signal_add_first('print text' => 'get_old_status');
491 Irssi::command('statusbar');
492 Irssi::signal_remove('print text' => 'get_old_status');
493 remove_statusbar(keys %killBar);
494 }
495 }
496
497 sub _add_map {
498 my ($type, $target, $map) = @_;
499 ($type->{$target}) = sort { length $a <=> length $b || $a cmp $b }
500 $map, exists $type->{$target} ? $type->{$target} : ();
501 }
502
503 sub get_keymap {
504 my ($textDest, undef, $cont_stripped) = @_;
505 if ($textDest->{level} == 524288 and $textDest->{target} eq '' and !defined $textDest->{server}) {
506 my $one_meta_or_ctrl_key = qr/((?:meta-)*?)(?:(meta-|\^)(\S)|(\w+))/;
507 $cont_stripped = as_uni($cont_stripped);
508 if ($cont_stripped =~ m/((?:$one_meta_or_ctrl_key-)*$one_meta_or_ctrl_key)\s+(.*)$/) {
509 my ($combo, $command) = ($1, $10);
510 my $map = '';
511 while ($combo =~ s/(?:-|^)$one_meta_or_ctrl_key$//) {
512 my ($level, $ctl, $key, $nkey) = ($1, $2, $3, $4);
513 my $numlevel = ($level =~ y/-//);
514 $ctl = '' if !$ctl || $ctl ne '^';
515 $map = ('-' x ($numlevel%2)) . ('+' x ($numlevel/2)) .
516 $ctl . (defined $key ? $key : "\01$nkey\01") . $map;
517 }
518 for ($command) {
519 last unless length $map;
520 if (/^change_window (\d+)/i) {
521 _add_map(\%nummap, $1, $map);
522 }
523 elsif (/^(?:command window goto|change_window) (\S+)/i) {
524 my $window = $1;
525 if ($window !~ /\D/) {
526 _add_map(\%nummap, $window, $map);
527 }
528 elsif (lc $window eq 'active') {
529 _add_map(\%specialmap, '_active', $map);
530 }
531 else {
532 _add_map(\%wnmap, $window, $map);
533 }
534 }
535 elsif (/^(?:active_window|command ((awl )?ack))/i) {
536 _add_map(\%specialmap, '_active', $map);
537 $viewer{use_ack} = $1;
538 }
539 elsif (/^command window last/i) {
540 _add_map(\%specialmap, '_last', $map);
541 }
542 elsif (/^(?:upper_window|command window up)/i) {
543 _add_map(\%specialmap, '_up', $map);
544 }
545 elsif (/^(?:lower_window|command window down)/i) {
546 _add_map(\%specialmap, '_down', $map);
547 }
548 elsif (/^key\s+(\w+)/i) {
549 $custom_key_map{$1} = $map;
550 }
551 }
552 }
553 Irssi::signal_stop;
554 }
555 }
556
557 sub update_keymap {
558 %nummap = %wnmap = %specialmap = %custom_key_map = ();
559 Irssi::signal_remove('command bind' => 'watch_keymap');
560 Irssi::signal_add_first('print text' => 'get_keymap');
561 Irssi::command('bind');
562 Irssi::signal_remove('print text' => 'get_keymap');
563 for (keys %custom_key_map) {
564 if (exists $custom_key_map{$_} &&
565 $custom_key_map{$_} =~ s/\01(\w+)\01/exists $custom_key_map{$1} ? $custom_key_map{$1} : "\02"/ge) {
566 if ($custom_key_map{$_} =~ /\02/) {
567 delete $custom_key_map{$_};
568 }
569 else {
570 redo;
571 }
572 }
573 }
574 for my $keymap (\(%specialmap, %wnmap, %nummap)) {
575 for (keys %$keymap) {
576 if ($keymap->{$_} =~ s/\01(\w+)\01/exists $custom_key_map{$1} ? $custom_key_map{$1} : "\02"/ge) {
577 if ($keymap->{$_} =~ /\02/) {
578 delete $keymap->{$_};
579 }
580 }
581 }
582 }
583 Irssi::signal_add('command bind' => 'watch_keymap');
584 delete $viewer{client_keymap};
585 &wl_changed;
586 }
587
588 # watch keymap changes
589 sub watch_keymap {
590 Irssi::timeout_add_once(1000, 'update_keymap', undef);
591 }
592
593 { my %strip_table = (
594 # fe-common::core::formats.c:format_expand_styles
595 # delete format_backs format_fores bold_fores other stuff
596 (map { $_ => '' } (split //, '04261537' . 'kbgcrmyw' . 'KBGCRMYW' . 'U9_8I:|FnN>#[' . 'pP')),
597 # escape
598 (map { $_ => $_ } (split //, '{}%')),
599 );
600 sub ir_strip_codes { # strip %codes
601 my $o = shift;
602 $o =~ s/(%(%|Z.{6}|z.{6}|X..|x..|.))/exists $strip_table{$2} ? $strip_table{$2} :
603 $2 =~ m{x(?:0[a-f]|[1-6][0-9a-z]|7[a-x])|z[0-9a-f]{6}}i ? '' : $1/gex;
604 $o
605 }
606 }
607 ## ir_parse_special -- wrapper around parse_special
608 ## $i - input format
609 ## $args - array ref of arguments to format
610 ## $win - different target window (default current window)
611 ## $flags - different kind of escape flags (default 4|8)
612 ## returns formatted str
613 sub ir_parse_special {
614 my $o;
615 my $i = shift;
616 my $args = shift // [];
617 y/ /\177/ for @$args; # hack to escape spaces
618 my $win = shift || Irssi::active_win;
619 my $flags = shift // 0x4|0x8;
620 my @cmd_args = ($i, (join ' ', @$args), $flags);
621 my $server = Irssi::active_server();
622 if (ref $win and ref $win->{active}) {
623 $o = $win->{active}->parse_special(@cmd_args);
624 }
625 elsif (ref $win and ref $win->{active_server}) {
626 $o = $win->{active_server}->parse_special(@cmd_args);
627 }
628 elsif (ref $server) {
629 $o = $server->parse_special(@cmd_args);
630 }
631 else {
632 $o = &Irssi::parse_special(@cmd_args);
633 }
634 $o =~ y/\177/ /;
635 $o
636 }
637
638 sub sb_format_expand { # Irssi::current_theme->format_expand wrapper
639 Irssi::current_theme->format_expand(
640 $_[0],
641 (
642 Irssi::EXPAND_FLAG_IGNORE_REPLACES
643 |
644 ($_[1] ? 0 : Irssi::EXPAND_FLAG_IGNORE_EMPTY)
645 )
646 )
647 }
648
649 { my $term_type = Irssi::version > 20040819 ? 'term_charset' : 'term_type';
650 if (Irssi->can('string_width')) {
651 *screen_length = sub { Irssi::string_width($_[0]) };
652 }
653 else {
654 local $@;
655 eval { require Text::CharWidth; };
656 unless ($@) {
657 *screen_length = sub { Text::CharWidth::mbswidth($_[0]) };
658 }
659 else {
660 my $err = $@; chomp $err; $err =~ s/\sat .* line \d+\.$//;
661 #Irssi::print("%_$IRSSI{name}: warning:%_ Text::CharWidth module failed to load. Length calculation may be off! Error was:");
662 print "%_$IRSSI{name}:%_ $err";
663 *screen_length = sub {
664 my $temp = shift;
665 if (lc Irssi::settings_get_str($term_type) eq 'utf-8') {
666 Encode::_utf8_on($temp);
667 }
668 length($temp)
669 };
670 }
671 }
672 sub as_uni {
673 no warnings 'utf8';
674 Encode::decode(Irssi::settings_get_str($term_type), $_[0], 0)
675 }
676 sub as_tc {
677 Encode::encode(Irssi::settings_get_str($term_type), $_[0], 0)
678 }
679 }
680
681 sub sb_length {
682 screen_length(ir_strip_codes($_[0]))
683 }
684
685 sub run_custom_xform {
686 local $@;
687 eval {
688 $custom_xform->()
689 };
690 if ($@) {
691 $@ =~ /^(.*)/;
692 print '%_'.(set 'custom_xform').'%_ died (disabling): '.$1;
693 $custom_xform = undef;
694 }
695 }
696
697 sub remove_uniform {
698 my $o = shift;
699 $o =~ s/^xmpp:(.*?[%@]).+\.[^.]+$/$1/ or
700 $o =~ s#^psyc://.+\.[^.]+/([@~].*)$#$1#;
701 if ($custom_xform) {
702 run_custom_xform() for $o;
703 }
704 $o
705 }
706
707 sub remove_uniform_vars {
708 my $win = shift;
709 my $name = __PACKAGE__ . '::custom_xform::' . $win->{active}{type}
710 if ref $win->{active} && $win->{active}{type};
711 no strict 'refs';
712 local ${$name} = 1 if $name;
713 remove_uniform(+shift);
714 }
715
716 sub lc1459 {
717 my $x = shift;
718 $x =~ y/][\\^/}{|~/;
719 lc $x
720 }
721
722 sub window_list {
723 my $i = 0;
724 map { $_->[1] } sort $window_sort_func map { [ $i++, $_ ] } Irssi::windows;
725 }
726
727 sub _calculate_abbrev {
728 my ($wins, $abbrevList) = @_;
729 if ($S{fancy_abbrev} !~ /^(no|off|head)/i) {
730 my @nameList = map { ref $_ ? remove_uniform_vars($_, as_uni($_->get_active_name) // '') : '' } @$wins;
731 for (my $i = 0; $i < @nameList - 1; ++$i) {
732 my ($x, $y) = ($nameList[$i], $nameList[$i + 1]);
733 s/^[+#!=]// for $x, $y;
734 my $res = exists $abbrev_cache{$x}{$y} ? $abbrev_cache{$x}{$y}
735 : $abbrev_cache{$x}{$y} = string_LCSS($x, $y);
736 if (defined $res) {
737 for ($nameList[$i], $nameList[$i + 1]) {
738 $abbrevList->{$_} //= int((index $_, $res) + (length $res) / 2);
739 }
740 }
741 }
742 }
743 }
744
745 my %act_last_line_shades = (
746 r => [qw[ 50 40 30 20 ]],
747 g => [qw[ 1O 1I 1C 16 ]],
748 y => [qw[ 5O 4I 3C 26 ]],
749 b => [qw[ 15 14 13 12 ]],
750 m => [qw[ 54 43 32 21 ]],
751 c => [qw[ 1S 1L 1E 17 ]],
752 w => [qw[ 7W 7T 7Q 3E ]],
753 K => [qw[ 7M 7K 27 7H ]],
754 R => [qw[ 60 50 40 30 ]],
755 G => [qw[ 1U 1O 1I 1C ]],
756 Y => [qw[ 6U 5O 4I 3C ]],
757 B => [qw[ 2B 2A 29 28 ]],
758 M => [qw[ 65 54 43 32 ]],
759 C => [qw[ 1Z 1S 1L 1E ]],
760 W => [qw[ 6Z 5S 7R 7O ]],
761 );
762
763 sub _format_display {
764 my (undef, $format, $cformat, $hilight, $name, $number, $key, $win) = @_;
765 if ($print_text_activity && $S{line_shade}) {
766 my @hilight_code = split /\177/, sb_format_expand("{$hilight \177}"), 2;
767 my $max_time = max(1, log($S{line_shade}) - log(1000));
768 my $time_delta = min(3, min($max_time, log(max(1, time - $win->{last_line}))) / $max_time * 3);
769 if ($hilight_code[0] =~ /%(.)/ && exists $act_last_line_shades{$1}) {
770 $hilight = 'sb_act_hilight_color %X'.$act_last_line_shades{$1}[$time_delta];
771 }
772 }
773 $cformat = '$0' unless length $cformat;
774 my %map = ('$C' => $cformat, '$N' => '$1', '$Q' => '$2');
775 $format =~ s<(\$.)><$map{$1}//$1>ge;
776 $format =~ s<\$H((?:\$.|[^\$])*?)\$S><{$hilight $1%n}>g;
777 my @ret = ir_parse_special(sb_format_expand($format), [$name, $number, $key], $win);
778 @ret
779 }
780
781 sub _get_format {
782 Irssi::current_theme->get_format(__PACKAGE__, @_)
783 }
784
785 sub _is_detached {
786 my ($win, $active_number) = @_;
787 my $level = $win->{data_level} // 0;
788 my $number = $win->{refnum};
789 my $name = lc1459( as_uni($win->{name}) );
790 my $active = lc1459( as_uni($win->get_active_name) // '' );
791 my $tag = $win->{active} && $win->{active}{server} ? lc1459( as_uni($win->{active}{server}{tag}) // '' ) : '';
792 my @cond = ($number);
793 push @cond, "$name" if length $name;
794 push @cond, "$tag/$active" if length $tag && length $active;
795 push @cond, "$active" if length $active;
796 push @cond, "$tag/*", "$tag/::all" if length $tag;
797 push @cond, "*", "::all";
798 for my $cond (@cond) {
799 if (exists $detach_map{ $cond }) {
800 my $dd = $detach_map{ $cond } // $S{detach_data};
801 return $win->{data_level} < abs $dd
802 && ($number != $active_number || 0 <= $dd);
803 }
804 }
805 return;
806 }
807
808 sub _calculate_items {
809 my ($wins, $abbrevList) = @_;
810
811 my $display_header = _get_format(set 'display_header');
812 my $name_format = _get_format(set 'name_display');
813 my $abbrev_chars = as_uni(_get_format(set 'abbrev_chars'));
814
815 my %displays;
816
817 my $active = Irssi::active_win;
818 @win_items = ();
819 %keymap = (%nummap, %wnmap_exp);
820
821 my ($numPad, $keyPad) = (0, 0);
822 if ($VIEWER_MODE or $S{block} < 0) {
823 $numPad = length((sort { length $b <=> length $a } keys %keymap)[0]) // 0;
824 $keyPad = length((sort { length $b <=> length $a } values %keymap)[0]) // 0;
825 }
826 my $last_net;
827 my ($abbrev1, $abbrev2) = $abbrev_chars =~ /(\X)(.*)/;
828 my @abbrev_chars = ('~', "\x{301c}");
829 unless (defined $abbrev1 && screen_length(as_tc($abbrev1)) == 1) { $abbrev1 = $abbrev_chars[0] }
830 unless (length $abbrev2) {
831 $abbrev2 = $abbrev1;
832 if ($abbrev1 eq $abbrev_chars[0]) {
833 $abbrev2 = $abbrev_chars[1];
834 }
835 else {
836 $abbrev2 = $abbrev1;
837 }
838 }
839 if (screen_length(as_tc($abbrev2)) == 1) {
840 $abbrev2 x= 2;
841 }
842 while (screen_length(as_tc($abbrev2)) > 2) {
843 chop $abbrev2;
844 }
845 unless (screen_length(as_tc($abbrev2)) == 2) {
846 $abbrev2 = $abbrev_chars[1];
847 }
848 for my $win (@$wins) {
849 my $global_tag_header_mode;
850
851 next unless ref $win;
852
853 my $backup_win = Storable::dclone($win);
854 delete $backup_win->{active} unless ref $backup_win->{active};
855
856 $global_tag_header_mode =
857 $display_header && ($last_net // '') ne ($backup_win->{active}{server}{tag} // '');
858
859 if ($win->{data_level} < abs $S{hide_data}
860 && ($win->{refnum} != $active->{refnum} || 0 <= $S{hide_data})) {
861 next; }
862 elsif (exists $awins{$win->{refnum}} && $S{hide_empty} && !$win->items
863 && ($win->{refnum} != $active->{refnum} || 0 <= $S{hide_empty})) {
864 next; }
865 elsif (_is_detached($win, $active->{refnum})) {
866 next; }
867
868 my $colour = $win->{hilight_color} // '';
869 my $hilight = do {
870 if ($win->{data_level} == 0) { 'sb_act_none'; }
871 elsif ($win->{data_level} == 1) { 'sb_act_text'; }
872 elsif ($win->{data_level} == 2) { 'sb_act_msg'; }
873 elsif ($colour ne '') { "sb_act_hilight_color $colour"; }
874 elsif ($win->{data_level} == 3) { 'sb_act_hilight'; }
875 else { 'sb_act_special'; }
876 };
877 my $number = $win->{refnum};
878
879 my ($name, $display, $cdisplay);
880 if ($global_tag_header_mode) {
881 $display = $display_header;
882 $name = as_uni($backup_win->{active}{server}{tag}) // '';
883 if ($custom_xform) {
884 no strict 'refs';
885 local ${ __PACKAGE__ . '::custom_xform::TAG' } = 1;
886 run_custom_xform() for $name;
887 }
888 }
889 else {
890 my @display = ('display_nokey');
891 if (defined $keymap{$number} and $keymap{$number} ne '') {
892 unshift @display, map { (my $cpy = $_) =~ s/_no/_/; $cpy } @display;
893 }
894 if (exists $awins{$number}) {
895 unshift @display, map { my $cpy = $_; $cpy .= '_visible'; $cpy } @display;
896 }
897 if ($active->{refnum} == $number) {
898 unshift @display, map { my $cpy = $_; $cpy .= '_active'; $cpy }
899 grep { !/_visible$/ } @display;
900 }
901 $display = (grep { length $_ }
902 map { $displays{$_} //= _get_format(set $_) }
903 @display)[0];
904 $cdisplay = $name_format;
905 $name = as_uni($win->get_active_name) // '';
906 $name = '*' if $S{banned_on} and exists $banned_channels{lc1459($name)};
907 $name = remove_uniform_vars($win, $name) if $name ne '*';
908 if ($name ne '*' and $win->{name} ne '' and $S{prefer_name}) {
909 $name = as_uni($win->{name});
910 if ($custom_xform) {
911 no strict 'refs';
912 local ${ __PACKAGE__ . '::custom_xform::NAME' } = 1;
913 run_custom_xform() for $name;
914 }
915 }
916
917 if (!$VIEWER_MODE && $S{block} >= 0 && $S{hide_name}
918 && $win->{data_level} < abs $S{hide_name}
919 && ($win->{refnum} != $active->{refnum} || 0 <= $S{hide_name})) {
920 $name = '';
921 $cdisplay = '';
922 }
923 }
924
925 $display = "$display%n";
926 my $num_ent = (' 'x max(0,$numPad - length $number)) . $number;
927 my $key_ent = exists $keymap{$number} ? ((' 'x max(0,$keyPad - length $keymap{$number})) . $keymap{$number}) : ' 'x$keyPad;
928 if ($VIEWER_MODE or $S{sbar_maxlen} or $S{block} < 0) {
929 my $baseLength = sb_length(_format_display(
930 '', $display, $cdisplay, $hilight,
931 'x', # placeholder
932 $num_ent,
933 $key_ent,
934 $win)) - 1;
935 my $diff = (abs $S{block}) - (screen_length(as_tc($name)) + $baseLength);
936 if ($diff < 0) { # too long
937 my $screen_length = screen_length(as_tc($name));
938 if ((abs $diff) >= $screen_length) { $name = '' } # forget it
939 elsif ((abs $diff) + screen_length(as_tc(substr($name, 0, 1))) >= $screen_length) { $name = substr($name, 0, 1); }
940 else {
941 my $ulen = length $name;
942 my $middle2 = exists $abbrevList->{$name} ?
943 ($S{fancy_strict}) ?
944 2* $abbrevList->{$name} :
945 (2*($abbrevList->{$name} + $ulen) / 3) :
946 ($S{fancy_head}) ?
947 2*$ulen :
948 $ulen;
949 my $first = 1;
950 while (length $name > 1) {
951 my $cp = $middle2 >= 0 ? $middle2/2 : -1; # clearing position
952 my $rm = 2;
953 # if character at end is wider than 1 cell -> replace it with ~
954 if (screen_length(as_tc(substr $name, $cp, 1)) > 1) {
955 if ($first || $cp < 0) {
956 $rm = 1;
957 $first = undef;
958 }
959 }
960 elsif ($cp < 0) { # elsif at end -> replace last 2 characters
961 --$cp;
962 }
963 (substr $name, $cp, $rm) = $abbrev1;
964 if ($cp > -1 && $rm > 1) {
965 --$middle2;
966 }
967 my $sl = screen_length(as_tc($name));
968 if ($sl + $baseLength < abs $S{block}) {
969 (substr $name, ($middle2+1)/2, 1) = $abbrev2;
970 last;
971 }
972 elsif ($sl + $baseLength == abs $S{block}) {
973 last;
974 }
975 }
976 }
977 }
978 elsif ($VIEWER_MODE or $S{block} < 0) {
979 $name .= (' ' x $diff);
980 }
981 }
982
983 push @win_items, _format_display(
984 '', $display, $cdisplay, $hilight,
985 as_tc($name),
986 $num_ent,
987 as_tc($key_ent),
988 $win);
989
990 if ($global_tag_header_mode) {
991 $last_net = $backup_win->{active}{server}{tag};
992 redo;
993 }
994
995 $mouse_coords{refnum}{$#win_items} = $number;
996 }
997 }
998
999 sub _spread_items {
1000 my $width = $screenWidth - $sb_base_width - 1;
1001 my @separator = _get_format(set 'separator');
1002 if ($S{block} >= 0) {
1003 my $sep2 = _get_format(set 'separator2');
1004 push @separator, $sep2 if length $sep2 && $sep2 ne $separator[0];
1005 }
1006 $separator[0] .= '%n';
1007 my @sepLen = map { sb_length($_) } @separator;
1008
1009 @actString = ();
1010 my $curLine;
1011 my $curLen = 0;
1012 if ($S{shared_sbar}) {
1013 $curLen += $S{shared_sbar}[0] + 2;
1014 $width -= $S{shared_sbar}[2];
1015 }
1016 my $mouse_header_check = 0;
1017 for my $it (@win_items) {
1018 my $itemLen = sb_length($it);
1019 if ($curLen) {
1020 if ($curLen + $itemLen + $sepLen[$mouse_header_check % @sepLen] > $width) {
1021 $width += $S{shared_sbar}[2]
1022 if !@actString && $S{shared_sbar};
1023 push @actString, $curLine;
1024 $curLine = undef;
1025 $curLen = 0;
1026 }
1027 elsif (defined $curLine) {
1028 $curLine .= $separator[$mouse_header_check % @separator];
1029 $curLen += $sepLen[$mouse_header_check % @sepLen];
1030 }
1031 }
1032 $curLine .= $it;
1033 if (exists $mouse_coords{refnum}{$mouse_header_check}) {
1034 $mouse_coords{scalar @actString}{ $_ } = $mouse_coords{refnum}{$mouse_header_check}
1035 for $curLen .. $curLen + $itemLen - 1;
1036 }
1037 $curLen += $itemLen;
1038 }
1039 continue {
1040 ++$mouse_header_check;
1041 }
1042 $curLen -= $S{shared_sbar}[0]
1043 if !@actString && $S{shared_sbar};
1044 push @actString, $curLine if $curLen;
1045 }
1046
1047 sub remake {
1048 my %abbrevList;
1049 my @wins = window_list();
1050 if ($VIEWER_MODE or $S{sbar_maxlen} or $S{block} < 0) {
1051 _calculate_abbrev(\@wins, \%abbrevList);
1052 }
1053
1054 %mouse_coords = ( refnum => +{} );
1055 _calculate_items(\@wins, \%abbrevList);
1056
1057 unless ($VIEWER_MODE) {
1058 _spread_items();
1059
1060 push @actString, undef unless @actString || $S{all_disable};
1061 }
1062 }
1063
1064 sub update_wl {
1065 return if $BLOCK_ALL;
1066 remake();
1067
1068 Irssi::statusbar_items_redraw(set $_) for keys %statusbars;
1069
1070 unless ($VIEWER_MODE) {
1071 Irssi::timeout_add_once(100, 'syncLines', undef);
1072 }
1073 else {
1074 syncViewer();
1075 }
1076 }
1077
1078 sub screenFullRedraw {
1079 my ($window) = @_;
1080 if (!ref $window or $window->{refnum} == Irssi::active_win->{refnum}) {
1081 $viewer{fullRedraw} = 1 if $viewer{client};
1082 $settings_str = '';
1083 &setup_changed;
1084 }
1085 }
1086
1087 sub restartViewerServer {
1088 if ($VIEWER_MODE) {
1089 stop_viewer();
1090 start_viewer();
1091 }
1092 }
1093
1094 sub _simple_quote {
1095 my @r = map {
1096 my $x = $_;
1097 $x =~ s/'/'"'"'/g;
1098 $x = "'$x'";
1099 } @_;
1100 wantarray ? @r : shift @r
1101 }
1102
1103 sub _viewer_command_replace_format {
1104 my ($ecmd, @args) = @_;
1105 my $file = _simple_quote(SCRIPT_FILE());
1106 my $path = _simple_quote($viewer{path});
1107 my @env;
1108 for my $env (shellwords($S{viewer_launch_env})) {
1109 if ($env =~ /^(\w+)(?:=(.*))$/) {
1110 push @env, "AWL_$1=$2"
1111 }
1112 }
1113 my $cmd = join ' ',
1114 (@env ? ('env', _simple_quote(@env)) : ()),
1115 'perl', $file, '-1', _simple_quote(@args), $path;
1116 $ecmd =~ s{%(%|\w+)}{
1117 my $sub = $1;
1118 if ($sub eq '%') {
1119 '%'
1120 }
1121 elsif ($sub =~ /^(q*)A(.*)/) {
1122 my $ret = $cmd;
1123 for (1..length $1) {
1124 $ret = _simple_quote($ret);
1125 }
1126 "$ret$2"
1127 }
1128 else {
1129 "%$sub"
1130 }
1131 }gex;
1132 $ecmd
1133 }
1134
1135 sub start_viewer {
1136 unlink $viewer{path} if -S $viewer{path} || -p _;
1137
1138 $viewer{server} = IO::Socket::UNIX->new(
1139 Type => SOCK_STREAM,
1140 Local => $viewer{path},
1141 Listen => 1
1142 );
1143 unless ($viewer{server}) {
1144 $viewer{msg} = "Viewer: $!";
1145 $viewer{retry} = Irssi::timeout_add_once(5000, 'retry_viewer', 1);
1146 return;
1147 }
1148 $viewer{server}->blocking(0);
1149 set_viewer_mode_hint();
1150 $viewer{server_tag} = Irssi::input_add($viewer{server}->fileno, INPUT_READ, 'vi_connected', undef);
1151
1152 if ($S{viewer_launch}) {
1153 if (length $ENV{TMUX_PANE} && length $ENV{TMUX} && lc $S{viewer_tmux_position} ne 'custom') {
1154 my $cmd = _viewer_command_replace_format('%qA', '-p', lc $S{viewer_tmux_position});
1155 Irssi::command("exec - tmux neww -d $cmd 2>&1 &");
1156 }
1157 elsif (length $ENV{WINDOWID} && length $ENV{DISPLAY} && length $S{viewer_xwin_command} && $S{viewer_xwin_command} =~ /\S/) {
1158 my $cmd = _viewer_command_replace_format($S{viewer_xwin_command});
1159 Irssi::command("exec - $cmd 2>&1 &");
1160 }
1161 elsif (length $S{viewer_custom_command} && $S{viewer_custom_command} =~ /\S/) {
1162 my $cmd = _viewer_command_replace_format($S{viewer_custom_command});
1163 Irssi::command("exec - $cmd 2>&1 &");
1164 }
1165 }
1166 }
1167
1168 sub set_viewer_mode_hint {
1169 return unless $viewer{server};
1170 if ($S{no_mode_hint}) {
1171 $viewer{msg} = undef;
1172 }
1173 else {
1174 my ($name) = __PACKAGE__ =~ /::([^:]+)$/;
1175 $viewer{msg} = "Run $name from the shell or switch to sbar mode";
1176 }
1177 }
1178
1179 sub retry_viewer {
1180 start_viewer();
1181 }
1182
1183 sub vi_close_client {
1184 Irssi::input_remove(delete $viewer{client_tag}) if exists $viewer{client_tag};
1185 $viewer{client}->close if $viewer{client};
1186 delete $viewer{client};
1187 delete $viewer{client_keymap};
1188 delete $viewer{client_settings};
1189 delete $viewer{client_env};
1190 delete $viewer{fullRedraw};
1191 }
1192
1193 sub vi_connected {
1194 vi_close_client();
1195 $viewer{client} = $viewer{server}->accept or return;
1196 $viewer{client}->blocking(0);
1197 $viewer{client_tag} = Irssi::input_add($viewer{client}->fileno, INPUT_READ, 'vi_clientinput', undef);
1198 syncViewer();
1199 }
1200
1201 use constant VIEWER_BLOCK_SIZE => 1024;
1202 sub vi_clientinput {
1203 if ($viewer{client}->read(my $buf, VIEWER_BLOCK_SIZE)) {
1204 $viewer{rcvbuf} .= $buf;
1205 if ($viewer{rcvbuf} =~ s/^(?:(active|\d+)|(last|up|down))\n//igm) {
1206 if (defined $2) {
1207 Irssi::command("window $2");
1208 }
1209 elsif (lc $1 eq 'active' && $viewer{use_ack}) {
1210 Irssi::command($viewer{use_ack});
1211 }
1212 else {
1213 Irssi::command("window goto $1");
1214 }
1215 }
1216 }
1217 else {
1218 vi_close_client();
1219 Irssi::timeout_add_once(100, 'syncViewer', undef);
1220 }
1221 }
1222
1223 sub stop_viewer {
1224 Irssi::timeout_remove(delete $viewer{retry}) if exists $viewer{retry};
1225 vi_close_client();
1226 Irssi::input_remove(delete $viewer{server_tag}) if exists $viewer{server_tag};
1227 return unless $viewer{server};
1228 $viewer{server}->close;
1229 delete $viewer{server};
1230 }
1231 sub _encode_var {
1232 my $str;
1233 while (@_) {
1234 my ($name, $var) = splice @_, 0, 2;
1235 my $type = ref $var ? $var =~ /HASH/ ? 'map' : $var =~ /ARRAY/ ? 'list' : '' : '';
1236 $str .= "\n\U$name$type\_begin\n";
1237 if ($type eq 'map') {
1238 no warnings 'numeric';
1239 $str .= " $_\n ${$var}{$_}\n" for sort { $a <=> $b || $a cmp $b } keys %$var;
1240 }
1241 elsif ($type eq 'list') {
1242 $str .= " $_\n" for @$var;
1243 }
1244 else {
1245 $str .= " $var\n";
1246 }
1247 $str .= "\U$name$type\_end\n";
1248 }
1249 $str
1250 }
1251 sub syncViewer {
1252 if ($viewer{client}) {
1253 @actString = ();
1254 if ($currentLines) {
1255 killOldStatus();
1256 $currentLines = 0;
1257 }
1258 my $str;
1259 unless ($viewer{client_keymap}) {
1260 $str .= _encode_var('key', +{ %nummap, %specialmap });
1261 $viewer{client_keymap} = 1;
1262 }
1263 unless ($viewer{client_settings}) {
1264 $str .= _encode_var(
1265 block => $S{block},
1266 ha => $S{height_adjust},
1267 mc => $S{maxcolumns},
1268 ml => $S{maxlines},
1269 );
1270 $viewer{client_settings} = 1;
1271 }
1272 unless ($viewer{client_env}) {
1273 $str .= _encode_var(irssienv => +{
1274 length $ENV{TMUX_PANE} && length $ENV{TMUX} ?
1275 (tmux_pane => $ENV{TMUX_PANE},
1276 tmux_srv => $ENV{TMUX}) : (),
1277 length $ENV{WINDOWID} ?
1278 (xwinid => $ENV{WINDOWID}) : (),
1279 });
1280 $viewer{client_env} = 1;
1281 }
1282 my $separator = _get_format(set 'separator');
1283 my $sepLen = sb_length($separator);
1284 my $item_bg = _get_format(set 'viewer_item_bg');
1285 my $title = _get_format(set 'title');
1286 if (length $title) {
1287 $title =~ s{\\(.)|(.)}{
1288 defined $2 ? quotemeta $2
1289 : $1 eq 'V' ? '\U'
1290 : $1 eq ':' ? quotemeta '%N'
1291 : $1 =~ /^[uUFQE]$/ ? "\\$1"
1292 : quotemeta "\\$1"
1293 }sge;
1294 $title = eval qq{"$title"};
1295 }
1296 $str .= _encode_var(redraw => 1) if delete $viewer{fullRedraw};
1297 $str .= _encode_var(separator => $separator,
1298 seplen => $sepLen,
1299 itembg => $item_bg,
1300 title => $title,
1301 mouse => $mouse_coords{refnum},
1302 key2 => \%wnmap_exp,
1303 win => \@win_items);
1304
1305 my $was = $viewer{client}->blocking(1);
1306 $viewer{client}->print($str);
1307 $viewer{client}->blocking($was);
1308 }
1309 elsif ($viewer{server}) {
1310 if (defined $viewer{msg}) {
1311 @actString = ((uc setc()).": $viewer{msg}");
1312 }
1313 else {
1314 @actString = ();
1315 }
1316 }
1317 elsif (defined $viewer{msg}) {
1318 @actString = ((uc setc()).": $viewer{msg}");
1319 }
1320 if (@actString) {
1321 Irssi::timeout_add_once(100, 'syncLines', undef);
1322 }
1323 elsif ($currentLines) {
1324 killOldStatus();
1325 $currentLines = 0;
1326 }
1327 }
1328
1329 sub reset_awl {
1330 Irssi::timeout_remove($shade_line_timer) if $shade_line_timer; $shade_line_timer = undef;
1331 my $was_sort = $S{sort} // '';
1332 my $was_xform = $S{xform} // '';
1333 my $was_shared = $S{shared_sbar};
1334 my $was_no_hint = $S{no_mode_hint};
1335 %S = (
1336 sort => Irssi::settings_get_str( set 'sort'),
1337 fancy_abbrev => Irssi::settings_get_str('fancy_abbrev'),
1338 xform => Irssi::settings_get_str( set 'custom_xform'),
1339 block => Irssi::settings_get_int( set 'block'),
1340 banned_on => Irssi::settings_get_bool('banned_channels_on'),
1341 prefer_name => Irssi::settings_get_bool(set 'prefer_name'),
1342 hide_data => Irssi::settings_get_int( set 'hide_data'),
1343 hide_name => Irssi::settings_get_int( set 'hide_name_data'),
1344 hide_empty => Irssi::settings_get_int( set 'hide_empty'),
1345 detach => Irssi::settings_get_str( set 'detach'),
1346 detach_data => Irssi::settings_get_int( set 'detach_data'),
1347 detach_aht => Irssi::settings_get_bool(set 'detach_aht'),
1348 sbar_maxlen => Irssi::settings_get_bool(set 'sbar_maxlength'),
1349 placement => Irssi::settings_get_str( set 'placement'),
1350 position => Irssi::settings_get_int( set 'position'),
1351 maxlines => Irssi::settings_get_int( set 'maxlines'),
1352 maxcolumns => Irssi::settings_get_int( set 'maxcolumns'),
1353 all_disable => Irssi::settings_get_bool(set 'all_disable'),
1354 height_adjust => Irssi::settings_get_int( set 'height_adjust'),
1355 mouse_offset => Irssi::settings_get_int( set 'mouse_offset'),
1356 mouse_scroll => Irssi::settings_get_int( 'mouse_scroll'),
1357 mouse_escape => Irssi::settings_get_int( 'mouse_escape'),
1358 line_shade => Irssi::settings_get_time(set 'last_line_shade'),
1359 no_mode_hint => Irssi::settings_get_bool(set 'no_mode_hint'),
1360 viewer_launch => Irssi::settings_get_bool(set 'viewer_launch'),
1361 viewer_launch_env => Irssi::settings_get_str(set 'viewer_launch_env'),
1362 viewer_xwin_command => Irssi::settings_get_str(set 'viewer_xwin_command'),
1363 viewer_custom_command => Irssi::settings_get_str(set 'viewer_custom_command'),
1364 viewer_tmux_position => Irssi::settings_get_str(set 'viewer_tmux_position'),
1365 );
1366 $S{fancy_strict} = $S{fancy_abbrev} =~ /^strict/i;
1367 $S{fancy_head} = $S{fancy_abbrev} =~ /^head/i;
1368 my $shared = Irssi::settings_get_str(set 'shared_sbar');
1369 if ($shared =~ /^(\d+)([<])(\d+)$/) {
1370 $S{shared_sbar} = [$1, $2, $3];
1371 }
1372 else {
1373 Irssi::settings_set_str(set 'shared_sbar', 'OFF');
1374 $S{shared_sbar} = undef;
1375 }
1376 lock_keys(%S);
1377 if ($was_sort ne $S{sort}) {
1378 $print_text_activity = undef;
1379 my @sort_order = grep { @$_ > 4 } map {
1380 s/^\s*//;
1381 my $reverse = s/^\W*\K[-!]//;
1382 my $undef_check = s/^\W*\K~// ? 1 : undef;
1383 my $equal_check = s/=(.*)\s?$// ? $1 : undef;
1384 s/\s*$//;
1385 my $ignore_case = s/#i$// ? 1 : undef;
1386
1387 $print_text_activity = 1 if $_ eq 'last_line';
1388
1389 my @path = split '/';
1390 my $class_check = @path && $path[-1] =~ s/(::.*)$// ? $1 : undef;
1391 my $lru = "@path" eq 'lru';
1392
1393 [ $reverse ? -1 : 1, $undef_check, $equal_check, $class_check, $ignore_case, $lru, @path ]
1394 } "$S{sort}," =~ /([^+,]*|[^+,]*=[^,]*?\s(?=\+)|[^+,]*=[^,]*)[+,]/g;
1395 $window_sort_func = sub {
1396 no warnings qw(numeric uninitialized);
1397 for my $so (@sort_order) {
1398 my @x = map {
1399 my $ret = 0;
1400 $_ = lc1459($_) if defined $_ && !ref $_ && $so->[4];
1401 $ret = $_ eq ($so->[4] ? lc1459($so->[2]) : $so->[2]) ? 1 : -1 if defined $so->[2];
1402 $ret = defined $_ ? ($ret || -3) : 3 if $so->[1];
1403 $ret = ref $_ && $_->isa('Irssi'.$so->[3]) ? 2 : ($ret || -2) if $so->[3];
1404 -$ret || $_
1405 }
1406 map {
1407 $so->[5] ? $_->[0] : reduce { return unless ref $a; $a->{$b} } $_->[1], @{$so}[6..$#$so]
1408 } $a, $b;
1409 return ((($x[0] <=> $x[1] || $x[0] cmp $x[1]) * $so->[0]) || next);
1410 }
1411 return ($a->[1]{refnum} <=> $b->[1]{refnum});
1412 };
1413 }
1414 if ($was_xform ne $S{xform}) {
1415 if ($S{xform} !~ /\S/) {
1416 $custom_xform = undef;
1417 }
1418 else {
1419 my $script_pkg = __PACKAGE__ . '::custom_xform';
1420 local $@;
1421 $custom_xform = eval qq{
1422 package $script_pkg;
1423 use strict;
1424 no warnings;
1425 our (\$QUERY, \$CHANNEL, \$TAG, \$NAME);
1426 return sub {
1427 # line 1 @{[ set 'custom_xform' ]}\n$S{xform}\n}};
1428 if ($@) {
1429 $@ =~ /^(.*)/;
1430 print '%_'.(set 'custom_xform').'%_ did not compile: '.$1;
1431 }
1432 }
1433 }
1434
1435 my $new_settings = join "\n", $VIEWER_MODE
1436 ? ("\\", $S{block}, $S{height_adjust}, $S{maxlines}, $S{maxcolumns})
1437 : ("!", $S{placement}, $S{position});
1438
1439 my $first_viewer = $settings_str eq '1';
1440 if ($settings_str ne $new_settings) {
1441 @actString = ();
1442 %abbrev_cache = ();
1443 $currentLines = 0;
1444 killOldStatus();
1445 delete $viewer{client_settings};
1446 $settings_str = $new_settings;
1447 }
1448
1449 my $was_mouse_mode = $MOUSE_ON;
1450 if ($MOUSE_ON = Irssi::settings_get_bool(set 'mouse') and !$was_mouse_mode) {
1451 install_mouse();
1452 }
1453 elsif ($was_mouse_mode and !$MOUSE_ON) {
1454 uninstall_mouse();
1455 }
1456
1457 unless ($first_viewer) {
1458 my $path = Irssi::settings_get_str(set 'path');
1459 my $was_viewer_mode = $VIEWER_MODE;
1460 if ($was_viewer_mode &&
1461 defined $viewer{path} && $viewer{path} ne $path) {
1462 stop_viewer();
1463 $was_viewer_mode = 0;
1464 }
1465 elsif ($was_viewer_mode && $S{no_mode_hint} != $was_no_hint + 0) {
1466 set_viewer_mode_hint();
1467 }
1468 $viewer{path} = $path;
1469 if ($VIEWER_MODE = Irssi::settings_get_bool(set 'viewer') and !$was_viewer_mode) {
1470 start_viewer();
1471 }
1472 elsif ($was_viewer_mode and !$VIEWER_MODE) {
1473 stop_viewer();
1474 }
1475 }
1476
1477 %banned_channels = map { lc1459(as_uni($_)) => undef }
1478 split ' ', Irssi::settings_get_str('banned_channels');
1479
1480 %detach_map = ($S{detach_aht}
1481 ? (map { ( lc1459(as_uni($_)) => undef ) }
1482 split ' ', Irssi::settings_get_str('activity_hide_targets')) : (),
1483 (map { my ($k, $v) = (split /(?:,(-?\d+))$/, $_)[0, 1];
1484 ( lc1459(as_uni($k)) => $v ) }
1485 split ' ', $S{detach}));
1486
1487 my @sb_base = split /\177/, sb_format_expand("{sbstart}{sb \177}{sbend}"), 2;
1488 $sb_base_width_pre = sb_length($sb_base[0]);
1489 $sb_base_width_post = max 0, sb_length($sb_base[1])-1;
1490 $sb_base_width = $sb_base_width_pre + $sb_base_width_post;
1491
1492 if ($print_text_activity && $S{line_shade}) {
1493 $shade_line_timer = Irssi::timeout_add(max(10 * GLOB_QUEUE_TIMER, 100*$S{line_shade}**(1/3)), 'wl_changed', undef);
1494 }
1495
1496 $CHANGED{AWINS} = 1;
1497 }
1498
1499 sub hide_window {
1500 my ($data) = @_;
1501 my $ent;
1502
1503 $data =~ s/\s*$//;
1504 my $win = Irssi::active_win;
1505 my $number = $win->{refnum};
1506 my $name = as_uni($win->{name});
1507 my $active = as_uni($win->get_active_name) // '';
1508 my $tag = $win->{active} && $win->{active}{server} ? as_uni($win->{active}{server}{tag}) // '' : '';
1509 if (length $name) {
1510 $ent = "$name";
1511 }
1512 elsif (length $tag && length $active) {
1513 $ent = "$tag/$active";
1514 }
1515 else {
1516 $ent = "$number";
1517 }
1518
1519 my $found = 0;
1520 my @setting;
1521 for my $s (split ' ', $S{detach}) {
1522 my ($k, $v) = (split /(?:,(-?\d+))$/, $s)[0, 1];
1523 if (lc1459(as_uni($k)) eq lc1459($ent)) {
1524 unless ($found) {
1525 if ($data =~ /^(-?\d+)$/) {
1526 $ent .= ",$1";
1527 }
1528 if (defined $v && 0 == abs $v) {
1529 $win->print("Hiding window $ent");
1530 }
1531 push @setting, as_tc($ent);
1532 $found = 1;
1533 }
1534 }
1535 else {
1536 push @setting, defined $v ? "$k,$v" : $k;
1537 }
1538 }
1539 unless ($found) {
1540 $win->print("Hiding window $ent");
1541 if ($data =~ /^(-?\d+)$/) {
1542 $ent .= ",$1";
1543 }
1544 push @setting, as_tc($ent);
1545 }
1546
1547 if (@setting) {
1548 Irssi::command("^set ".(set 'detach')." @setting");
1549 } else {
1550 Irssi::command("^set -clear ".(set 'detach'));
1551 }
1552 }
1553
1554 sub unhide_window {
1555 my ($data, $server, $witem) = @_;
1556 my $win = Irssi::active_win;
1557 my $number = $win->{refnum};
1558 my $name = as_uni($win->{name});
1559 my $active = as_uni($win->get_active_name) // '';
1560 my $tag = $win->{active} && $win->{active}{server} ? as_uni($win->{active}{server}{tag}) // '' : '';
1561
1562 my %detach_aht;
1563 if ($S{detach_aht}) {
1564 %detach_aht = (map { ( lc1459(as_uni($_)) => undef ) }
1565 split ' ', Irssi::settings_get_str('activity_hide_targets'));
1566 }
1567 my @setting;
1568 my @kills = (length $name ? $name : undef,
1569 length $tag && length $active ? "$tag/$active" : undef,
1570 length $active ? $active : undef,
1571 $number);
1572 my @was_unhidden = (0) x @kills;
1573 for my $s (split ' ', $S{detach}) {
1574 my ($k, $v) = (split /(?:,(-?\d+))$/, $s)[0, 1];
1575 my $k2 = lc1459(as_uni($k));
1576 my $kill;
1577 for my $ki (0..$#kills) {
1578 if (defined $kills[$ki] && $k2 eq lc1459($kills[$ki])) {
1579 $kill = $ki;
1580 }
1581 }
1582
1583 if (defined $kill) {
1584 if (defined $v && 0 == abs $v) {
1585 $was_unhidden[$kill] = 1;
1586 push @setting, defined $v ? "$k,$v" : $k;
1587 } else {
1588 $win->print("Unhiding window $kills[$kill]");
1589 }
1590 }
1591 else {
1592 push @setting, defined $v ? "$k,$v" : $k;
1593 }
1594 }
1595 my @is_hidden = (defined $kills[0] && (exists $detach_map{"*"} || exists $detach_map{"::all"}),
1596 defined $kills[1] && (exists $detach_map{lc1459("$tag/*")} || exists $detach_map{lc1459("$tag/::all")}
1597 || exists $detach_map{"*"} || exists $detach_map{"::all"}),
1598 defined $kills[2] && (exists $detach_map{"*"} || exists $detach_map{"::all"}),
1599 (exists $detach_map{"*"} || exists $detach_map{"::all"})
1600 );
1601 for my $ki (1, 2, 0, 3) {
1602 if ($is_hidden[$ki]) {
1603 unless ($was_unhidden[$ki]) {
1604 $win->print("Unhiding window $kills[$ki]");
1605 push @setting, "$kills[$ki],0";
1606 $was_unhidden[$ki] = 1;
1607 }
1608 last;
1609 }
1610 }
1611 my @is_hidden_aht = (defined $kills[0] && (exists $detach_aht{lc1459($name)}
1612 || exists $detach_aht{"*"} || exists $detach_aht{"::all"}),
1613 defined $kills[1] && (exists $detach_aht{lc1459("$tag/$active")}
1614 || exists $detach_aht{lc1459($active)}
1615 || exists $detach_aht{lc1459("$tag/*")} || exists $detach_aht{lc1459("$tag/::all")}
1616 || exists $detach_aht{"*"} || exists $detach_aht{"::all"}),
1617 defined $kills[2] && (exists $detach_aht{lc1459($active)}
1618 || exists $detach_aht{"*"} || exists $detach_aht{"::all"}),
1619 (exists $detach_aht{$number} || exists $detach_aht{"*"} || exists $detach_aht{"::all"})
1620 );
1621 for my $ki (1, 2, 0, 3) {
1622 if ($is_hidden_aht[$ki]) {
1623 unless ($was_unhidden[$ki]) {
1624 $win->print("Unhiding window $kills[$ki], it is hidden because ".(set 'detach_aht')." is ON");
1625 push @setting, "$kills[$ki],0";
1626 $was_unhidden[$ki] = 1;
1627 }
1628 last;
1629 }
1630 }
1631
1632 if (@setting) {
1633 Irssi::command("^set ".(set 'detach')." @setting");
1634 } else {
1635 Irssi::command("^set -clear ".(set 'detach'));
1636 }
1637 }
1638
1639 sub ack_window {
1640 my ($data, $server, $witem) = @_;
1641 my $win = Irssi::active_win;
1642 my $number = $win->{refnum};
1643 if (grep { $_->{cmd} eq 'ack' } Irssi::commands) {
1644 my $Orig_Irssi_windows = \&Irssi::windows;
1645 local *Irssi::windows = sub () { grep { !_is_detached($_, $number) } $Orig_Irssi_windows->() };
1646 Irssi::command("ack" . (length $data ? " $data" : ""));
1647 } else {
1648 my $ignore_refnum = Irssi::settings_get_bool('active_window_ignore_refnum');
1649 my $max_win;
1650 my $max_act = 0;
1651 my $max_ref = 0;
1652 for my $rec (Irssi::windows) {
1653 next if _is_detached($rec, $number);
1654
1655 # ignore refnum
1656 if ($ignore_refnum &&
1657 $rec->{data_level} > 0 && $max_act < $rec->{data_level}) {
1658 $max_act = $rec->{data_level};
1659 $max_win = $rec;
1660 }
1661
1662 # windows with lower refnums break ties
1663 elsif (!$ignore_refnum &&
1664 $rec->{data_level} > 0 &&
1665 ($rec->{data_level} > $max_act ||
1666 ($rec->{data_level} == $max_act && $rec->{refnum} < $max_ref))) {
1667 $max_act = $rec->{data_level};
1668 $max_win = $rec;
1669 $max_ref = $rec->{refnum};
1670 }
1671 }
1672 $max_win->set_active if defined $max_win;
1673 }
1674 }
1675
1676 sub refnum_changed {
1677 my ($win, $old_refnum) = @_;
1678 my @old_setting = split ' ', $S{detach};
1679 my @setting = map {
1680 my ($k, $v) = (split /(?:,(-?\d+))$/, $_)[0, 1];
1681 if ($k eq $old_refnum) {
1682 $win->{refnum} . (defined $v ? ",$v" : "")
1683 }
1684 else {
1685 $_
1686 }
1687 } @old_setting;
1688 if ("@old_setting" ne "@setting") {
1689 $S{detach} = "@setting";
1690 Irssi::settings_set_str(set 'detach', "@setting");
1691 &setup_changed;
1692 }
1693 else {
1694 &wl_changed;
1695 }
1696 }
1697
1698 sub window_destroyed {
1699 my ($win) = @_;
1700 my @old_setting = split ' ', $S{detach};
1701 my @setting = grep {
1702 my ($k, $v) = (split /(?:,(-?\d+))$/, $_)[0, 1];
1703 if ($k eq $win->{refnum}) {
1704 0;
1705 }
1706 else {
1707 1;
1708 }
1709 } @old_setting;
1710 if ("@old_setting" ne "@setting") {
1711 $S{detach} = "@setting";
1712 Irssi::settings_set_str(set 'detach', "@setting");
1713 &setup_changed;
1714 }
1715 else {
1716 &awins_changed;
1717 }
1718 }
1719
1720 sub stop_mouse_tracking {
1721 print STDERR "\e[?1005l\e[?1000l";
1722 }
1723 sub start_mouse_tracking {
1724 print STDERR "\e[?1000h\e[?1005h";
1725 }
1726 sub install_mouse {
1727 Irssi::command_bind('mouse_xterm' => 'mouse_xterm');
1728 Irssi::command('^bind meta-[M command mouse_xterm');
1729 Irssi::signal_add_first('gui key pressed' => 'mouse_key_hook');
1730 start_mouse_tracking();
1731 }
1732 sub uninstall_mouse {
1733 stop_mouse_tracking();
1734 Irssi::signal_remove('gui key pressed' => 'mouse_key_hook');
1735 Irssi::command('^bind -delete meta-[M');
1736 Irssi::command_unbind('mouse_xterm' => 'mouse_xterm');
1737 }
1738
1739 sub awl_mouse_event {
1740 return if $VIEWER_MODE;
1741 if ((($_[0] == 3 and $_[3] == 0)
1742 || $_[0] == 64 || $_[0] == 65) and
1743 $_[1] == $_[4] and $_[2] == $_[5]) {
1744 my $top = lc $S{placement} eq 'top';
1745 my ($pos, $line) = @_[1 .. 2];
1746 unless ($top) {
1747 $line -= $screenHeight;
1748 $line += $currentLines;
1749 $line += $S{mouse_offset};
1750 }
1751 else {
1752 $line -= $S{mouse_offset};
1753 }
1754 $pos -= $sb_base_width_pre;
1755 return if $line < 0 || $line >= $currentLines;
1756 if ($_[0] == 64) {
1757 Irssi::command('window up');
1758 }
1759 elsif ($_[0] == 65) {
1760 Irssi::command('window down');
1761 }
1762 elsif (exists $mouse_coords{$line}{$pos}) {
1763 my $win = $mouse_coords{$line}{$pos};
1764 Irssi::command('window ' . $win);
1765 }
1766 Irssi::signal_stop;
1767 }
1768 }
1769
1770 sub mouse_scroll_event {
1771 return unless $S{mouse_scroll};
1772 if (($_[3] == 64 or $_[3] == 65) and
1773 $_[0] == $_[3] and $_[1] == $_[4] and $_[2] == $_[5]) {
1774 my $cmd = 'scrollback goto ' . ($_[3] == 64 ? '-' : '+') . $S{mouse_scroll};
1775 Irssi::active_win->command($cmd);
1776 Irssi::signal_stop;
1777 }
1778 elsif ($_[0] == 64 or $_[0] == 65) {
1779 Irssi::signal_stop;
1780 }
1781 }
1782
1783 sub mouse_escape {
1784 return unless $S{mouse_escape} > 0;
1785 if ($_[0] == 3) {
1786 my $tm = $S{mouse_escape};
1787 $tm *= 1000 if $tm < 1000;
1788 stop_mouse_tracking();
1789 Irssi::timeout_add_once($tm, 'start_mouse_tracking', undef);
1790 Irssi::signal_stop;
1791 }
1792 }
1793
1794 sub UNLOAD {
1795 @actString = ();
1796 killOldStatus();
1797 stop_viewer() if $VIEWER_MODE;
1798 uninstall_mouse() if $MOUSE_ON;
1799 }
1800
1801 sub addPrintTextHook { # update on print text
1802 return unless defined $^S;
1803 return if $BLOCK_ALL;
1804 return unless $print_text_activity;
1805 return if $_[0]->{level} == 262144 and $_[0]->{target} eq ''
1806 and !defined($_[0]->{server});
1807 &wl_changed;
1808 }
1809
1810 sub block_event_window_change {
1811 Irssi::signal_stop;
1812 }
1813
1814 sub update_awins {
1815 my @wins = Irssi::windows;
1816 local $BLOCK_ALL = 1;
1817 Irssi::signal_add_first('window changed' => 'block_event_window_change');
1818 my $bwin =
1819 my $awin = Irssi::active_win;
1820 my $lwin;
1821 my $defer_irssi_broken_last;
1822 unless ($wins[0]{refnum} == $awin->{refnum}) {
1823 # special case: more than 1 last win, so /win last;
1824 # /win last doesn't come back to the current window. eg. after
1825 # connect & autojoin; we can't handle this situation, bail out
1826 $defer_irssi_broken_last = 1;
1827 }
1828 else {
1829 $awin->command('window last');
1830 $lwin = Irssi::active_win;
1831 $lwin->command('window last');
1832 $defer_irssi_broken_last = $lwin->{refnum} == $bwin->{refnum};
1833 }
1834 my $awin_counter = 0;
1835 Irssi::signal_remove('window changed' => 'block_event_window_change');
1836 unless ($defer_irssi_broken_last) {
1837 # we need to keep the fe-windows code running here
1838 Irssi::signal_add_priority('window changed' => 'block_event_window_change', -99);
1839 %awins = %wnmap_exp = ();
1840 do {
1841 Irssi::active_win->command('window up');
1842 $awin = Irssi::active_win;
1843 $awins{$awin->{refnum}} = undef;
1844 ++$awin_counter;
1845 } until ($awin->{refnum} == $bwin->{refnum} || $awin_counter >= @wins);
1846 Irssi::signal_remove('window changed' => 'block_event_window_change');
1847
1848 Irssi::signal_add_first('window changed' => 'block_event_window_change');
1849 for my $key (keys %wnmap) {
1850 next unless Irssi::window_find_name($key) || Irssi::window_find_item($key);
1851 $awin->command("window goto $key");
1852 my $cwin = Irssi::active_win;
1853 $wnmap_exp{ $cwin->{refnum} } = $wnmap{$key};
1854 $cwin->command('window last')
1855 if $cwin->{refnum} != $awin->{refnum};
1856 }
1857 for my $win (reverse @wins) { # restore original window order
1858 Irssi::active_win->command('window '.$win->{refnum});
1859 }
1860 $awin->command('window '.$lwin->{refnum}); # restore last win
1861 Irssi::active_win->command('window last');
1862 Irssi::signal_remove('window changed' => 'block_event_window_change');
1863 }
1864 $CHANGED{WL} = 1;
1865 }
1866
1867 sub resizeTerm {
1868 if (defined (my $r = `stty size 2>/dev/null`)) {
1869 ($screenHeight, $screenWidth) = split ' ', $r;
1870 $CHANGED{SETUP} = 1;
1871 }
1872 else {
1873 $CHANGED{SIZE} = 1;
1874 }
1875 }
1876
1877 sub awl_refresh {
1878 $globTime = undef;
1879 resizeTerm() if delete $CHANGED{SIZE};
1880 reset_awl() if delete $CHANGED{SETUP};
1881 update_awins() if delete $CHANGED{AWINS};
1882 update_wl() if delete $CHANGED{WL};
1883 }
1884
1885 sub termsize_changed { $CHANGED{SIZE} = 1; &queue_refresh; }
1886 sub setup_changed { $CHANGED{SETUP} = 1; &queue_refresh; }
1887 sub awins_changed { $CHANGED{AWINS} = 1; &queue_refresh; }
1888 sub wl_changed { $CHANGED{WL} = 1; &queue_refresh; }
1889
1890 sub window_changed {
1891 &awins_changed if $_[1];
1892 }
1893
1894 sub queue_refresh {
1895 return if $BLOCK_ALL;
1896 Irssi::timeout_remove($globTime)
1897 if defined $globTime; # delay the update further
1898 $globTime = Irssi::timeout_add_once(GLOB_QUEUE_TIMER, 'awl_refresh', undef);
1899 }
1900
1901 sub awl_init {
1902 termsize_changed();
1903 setup_changed();
1904 update_keymap();
1905 Irssi::timeout_remove($globTime)
1906 if defined $globTime;
1907 awl_refresh();
1908 termsize_changed();
1909 }
1910
1911 sub runsub {
1912 my $cmd = shift;
1913 sub {
1914 my ($data, $server, $item) = @_;
1915 Irssi::command_runsub($cmd, $data, $server, $item);
1916 };
1917 }
1918
1919 Irssi::signal_register({
1920 'gui mouse' => [qw/int int int int int int/],
1921 });
1922 { my $broken_expandos = (Irssi::version >= 20081128 && Irssi::version < 20110210)
1923 ? sub { my $x = shift; $x =~ s/\$\{cumode_space\}/ /; $x } : undef;
1924 Irssi::theme_register([
1925 map { $broken_expandos ? $broken_expandos->($_) : $_ }
1926 set 'display_nokey' => '$N${cumode_space}$H$C$S',
1927 set 'display_key' => '$Q${cumode_space}$H$C$S',
1928 set 'display_nokey_visible' => '%2$N${cumode_space}$H$C$S',
1929 set 'display_key_visible' => '%2$Q${cumode_space}$H$C$S',
1930 set 'display_nokey_active' => '%1$N${cumode_space}$H$C$S',
1931 set 'display_key_active' => '%1$Q${cumode_space}$H$C$S',
1932 set 'display_header' => '%8$C|${N}',
1933 set 'name_display' => '$0',
1934 set 'separator' => ' ',
1935 set 'separator2' => '',
1936 set 'abbrev_chars' => "~\x{301c}",
1937 set 'viewer_item_bg' => sb_format_expand('{sb_background}'),
1938 set 'title' => '\V'.setc().'\:',
1939 ]);
1940 }
1941 Irssi::settings_add_bool(setc, set 'prefer_name', 0); #
1942 Irssi::settings_add_int( setc, set 'hide_empty', 0); #
1943 Irssi::settings_add_int( setc, set 'hide_data', 0); #
1944 Irssi::settings_add_str( setc, set 'detach', ''); #
1945 Irssi::settings_add_int( setc, set 'detach_data', -3); #
1946 Irssi::settings_add_bool(setc, set 'detach_aht', 0); #
1947 Irssi::settings_add_int( setc, set 'hide_name_data', 0); #
1948 Irssi::settings_add_int( setc, set 'maxlines', 9); #
1949 Irssi::settings_add_int( setc, set 'maxcolumns', 4); #
1950 Irssi::settings_add_int( setc, set 'block', 15); #
1951 Irssi::settings_add_bool(setc, set 'sbar_maxlength', 1); #
1952 Irssi::settings_add_int( setc, set 'height_adjust', 2); #
1953 Irssi::settings_add_str( setc, set 'sort', 'refnum'); #
1954 Irssi::settings_add_str( setc, set 'placement', 'bottom'); #
1955 Irssi::settings_add_int( setc, set 'position', 0); #
1956 Irssi::settings_add_bool(setc, set 'all_disable', 1); #
1957 Irssi::settings_add_bool(setc, set 'viewer', 1); #
1958 Irssi::settings_add_str( setc, set 'shared_sbar', 'OFF'); #
1959 Irssi::settings_add_bool(setc, set 'mouse', 0); #
1960 Irssi::settings_add_str( setc, set 'path', Irssi::get_irssi_dir . '/_windowlist'); #
1961 Irssi::settings_add_str( setc, set 'custom_xform', ''); #
1962 Irssi::settings_add_time(setc, set 'last_line_shade', '0'); #
1963 Irssi::settings_add_int( setc, set 'mouse_offset', 1); #
1964 Irssi::settings_add_int( setc, 'mouse_scroll', 3); #
1965 Irssi::settings_add_int( setc, 'mouse_escape', 1); #
1966 Irssi::settings_add_str( setc, 'banned_channels', '');
1967 Irssi::settings_add_bool(setc, 'banned_channels_on', 1);
1968 Irssi::settings_add_str( setc, 'fancy_abbrev', 'fancy'); #
1969 Irssi::settings_add_bool(setc, set 'no_mode_hint', 0); #
1970 Irssi::settings_add_bool(setc, set 'viewer_launch', 1); #
1971 Irssi::settings_add_str( setc, set 'viewer_launch_env', ''); #
1972 Irssi::settings_add_str( setc, set 'viewer_tmux_position', 'left'); #
1973 Irssi::settings_add_str( setc, set 'viewer_xwin_command', 'xterm +sb -e %A'); #
1974 Irssi::settings_add_str( setc, set 'viewer_custom_command', ''); #
1975
1976 Irssi::signal_add_last({
1977 'setup changed' => 'setup_changed',
1978 'print text' => 'addPrintTextHook',
1979 'terminal resized' => 'termsize_changed',
1980 'setup reread' => 'screenFullRedraw',
1981 'window hilight' => 'wl_changed',
1982 'command format' => 'wl_changed',
1983 });
1984 Irssi::signal_add({
1985 'window changed' => 'window_changed',
1986 'window item changed' => 'wl_changed',
1987 'window changed automatic' => 'window_changed',
1988 'window created' => 'awins_changed',
1989 'window destroyed' => 'window_destroyed',
1990 'window name changed' => 'wl_changed',
1991 'window refnum changed' => 'refnum_changed',
1992 });
1993 Irssi::signal_add_last('gui mouse' => 'mouse_escape');
1994 Irssi::signal_add_last('gui mouse' => 'mouse_scroll_event');
1995 Irssi::signal_add_last('gui mouse' => 'awl_mouse_event');
1996 Irssi::command_bind( setc() => runsub(setc()) );
1997 Irssi::command_bind( setc() . ' redraw' => 'screenFullRedraw' );
1998 Irssi::command_bind( setc() . ' restart' => 'restartViewerServer' );
1999 Irssi::command_bind( setc() . ' attach' => 'unhide_window' );
2000 Irssi::command_bind( setc() . ' detach' => 'hide_window' );
2001 Irssi::command_bind( setc() . ' ack' => 'ack_window' );
2002
2003 {
2004 my $l = set 'shared';
2005 {
2006 no strict 'refs';
2007 *{$l} = $awl_shared_empty;
2008 }
2009 Irssi::statusbar_item_register($l, '$0', $l);
2010 }
2011
2012 awl_init();
2013
2014 # Mouse script based on irssi mouse patch by mirage
2015 { my $mouse_status = -1; # -1:off 0,1,2:filling mouse_combo
2016 my @mouse_combo; # 0:button 1:x 2:y
2017 my @mouse_previous; # previous contents of mouse_combo
2018
2019 sub mouse_xterm_off {
2020 $mouse_status = -1;
2021 }
2022 sub mouse_xterm {
2023 $mouse_status = 0;
2024 Irssi::timeout_add_once(10, 'mouse_xterm_off', undef);
2025 }
2026
2027 sub mouse_key_hook {
2028 my ($key) = @_;
2029 if ($mouse_status != -1) {
2030 if ($mouse_status == 0) {
2031 @mouse_previous = @mouse_combo;
2032 #if @mouse_combo && $mouse_combo[0] < 64;
2033 }
2034 $mouse_combo[$mouse_status] = $key - 32;
2035 $mouse_status++;
2036 if ($mouse_status == 3) {
2037 $mouse_status = -1;
2038 # match screen coordinates
2039 $mouse_combo[1]--;
2040 $mouse_combo[2]--;
2041 Irssi::signal_emit('gui mouse', @mouse_combo[0 .. 2], @mouse_previous[0 .. 2]);
2042 }
2043 Irssi::signal_stop;
2044 }
2045 }
2046 }
2047
2048 sub string_LCSS {
2049 my $str = join "\0", @_;
2050 (sort { length $b <=> length $a } $str =~ /(?=(.+).*\0.*\1)/g)[0]
2051 }
2052
2053 # workaround for issue #271
2054 { package Irssi::Nick }
2055
2056 # workaround for issue #572
2057 @Irssi::UI::Exec::ISA = 'Irssi::Windowitem'
2058 if Irssi::version >= 20140822 && Irssi::version <= 20161101 && !@Irssi::UI::Exec::ISA;
2059
2060 UNITCHECK
2061 { package AwlViewer;
2062 use strict;
2063 use warnings;
2064 no warnings 'redefine';
2065 use Encode;
2066 use IO::Socket::UNIX;
2067 use IO::Select;
2068 use List::Util qw(max);
2069 use constant BLOCK_SIZE => 1024;
2070 use constant RECONNECT_TIME => 5;
2071
2072 my $sockpath;
2073
2074 our $VERSION = '0.8';
2075
2076 our ($got_int, $resized, $timeout);
2077
2078 my %vars;
2079 my (%c2w, @seqlist);
2080 my %mouse_coords;
2081 my (@mouse, @last_mouse);
2082 my ($err, $sock, $loop);
2083 my ($keybuf, $rcvbuf);
2084 my @screen;
2085 my ($screenHeight, $screenWidth);
2086 my ($disp_update, $fs_open, $one_shot_integration, $one_shot_resize);
2087 my $integration_position;
2088 my $show_title_bar;
2089
2090 sub connect_it {
2091 $sock = IO::Socket::UNIX->new(
2092 Type => SOCK_STREAM,
2093 Peer => $sockpath,
2094 );
2095 unless ($sock) {
2096 $err = $!;
2097 return;
2098 }
2099 $sock->blocking(0);
2100 $loop->add($sock);
2101 }
2102
2103 sub remove_conn {
2104 my $fh = shift;
2105 $loop->remove($fh);
2106 $fh->close;
2107 $sock = undef;
2108 %vars = ();
2109 @screen = ();
2110 }
2111
2112 { package Terminfo; # xterm
2113 sub civis { "\e[?25l" }
2114 sub sc { "\e7" }
2115 sub cup { "\e[" . ($_[0] + 1) . ';' . ($_[1] + 1) . 'H' }
2116 sub el { "\e[K" }
2117 sub rc { "\e8" }
2118 sub cnorm { "\e[?25h" }
2119 sub setab { "\e[4" . $_[0] . 'm' }
2120 sub setaf { "\e[3" . $_[0] . 'm' }
2121 sub setaf16 { "\e[9" . $_[0] . 'm' }
2122 sub setab16 { "\e[10" . $_[0] . 'm' }
2123 sub setaf256 { "\e[38;5;" . $_[0] . 'm' }
2124 sub setab256 { "\e[48;5;" . $_[0] . 'm' }
2125 sub sgr0 { "\e[0m" }
2126 sub bold { "\e[1m" }
2127 sub it { "\e[3m" }
2128 sub ul { "\e[4m" }
2129 sub blink { "\e[5m" }
2130 sub rev { "\e[7m" }
2131 sub op { "\e[39;49m" }
2132 sub exit_bold { "\e[22m" }
2133 sub exit_it { "\e[23m" }
2134 sub exit_ul { "\e[24m" }
2135 sub exit_blink { "\e[25m" }
2136 sub exit_rev { "\e[27m" }
2137 sub smcup { "\e[?1049h" }
2138 sub rmcup { "\e[?1049l" }
2139 sub smmouse { "\e[?1000h\e[?1005h" }
2140 sub rmmouse { "\e[?1005l\e[?1000l" }
2141 }
2142
2143 sub init {
2144 $sockpath = shift // "$ENV{HOME}/.irssi/_windowlist";
2145 STDOUT->autoflush(1);
2146 printf "\r%swaiting for %s...", Terminfo::sc, $::IRSSI{name};
2147
2148 `stty -icanon -echo`;
2149
2150 $loop = IO::Select->new;
2151 STDIN->blocking(0);
2152 $loop->add(\*STDIN);
2153
2154 $SIG{INT} = sub {
2155 $got_int = 1
2156 };
2157 $SIG{WINCH} = sub {
2158 $resized = 1
2159 };
2160
2161 $resized = 3;
2162
2163 $disp_update = 2;
2164
2165 $show_title_bar = 1;
2166 }
2167
2168 sub enter_fs {
2169 return if $fs_open;
2170 safe_print(Terminfo::rc, Terminfo::smcup, Terminfo::civis, Terminfo::smmouse);
2171 $fs_open = 1;
2172 }
2173
2174 sub leave_fs {
2175 return unless $fs_open;
2176 safe_print(Terminfo::rmmouse, Terminfo::cnorm, Terminfo::rmcup);
2177 safe_print(sprintf "\r%swaiting for %s...", Terminfo::sc, $::IRSSI{name}) if $_[0];
2178
2179 $fs_open = 0;
2180 }
2181
2182 sub end_prog {
2183 leave_fs();
2184 STDIN->blocking(1);
2185 `stty sane`;
2186 printf "\r%s%sthanks for using %s\n", Terminfo::rc, Terminfo::el, $::IRSSI{name};
2187 }
2188
2189 sub safe_print {
2190 my $st = STDIN->blocking(1);
2191 print @_;
2192 STDIN->blocking($st);
2193 }
2194
2195 sub safe_qx {
2196 my $st = STDIN->blocking(1);
2197 my $ret = `$_[0]`;
2198 STDIN->blocking($st);
2199 $ret
2200 }
2201
2202 sub safe_print_sock {
2203 return unless $sock;
2204 my $was = $sock->blocking(1);
2205 $sock->print(@_);
2206 $sock->blocking($was);
2207 }
2208
2209 sub process_recv {
2210 my $need = 0;
2211 while ($rcvbuf =~ s/\n(.+)_BEGIN\n((?: .*\n)*)\1_END\n//) {
2212 my $var = lc $1;
2213 my $data = $2;
2214 my @data = split "\n ", "\n$data ", -1;
2215 shift @data; pop @data;
2216 my $itembg = $vars{itembg};
2217 if ($var =~ s/list$//) {
2218 $vars{$var} = \@data;
2219 }
2220 elsif ($var =~ s/map$//) {
2221 $vars{$var} = +{ @data };
2222 }
2223 else {
2224 $vars{$var} = join "\n", @data;
2225 }
2226 $need = 1 if $var eq 'win';
2227 $need = 1 if $var eq 'redraw' && $vars{$var};
2228 if (($itembg//'') ne ($vars{itembg}//'')) {
2229 $need = $vars{redraw} = 1;
2230 }
2231 _build_keymap() if $var eq 'key2';
2232 }
2233 $need
2234 }
2235
2236 { my %ansi_table;
2237 my ($i, $j, $k) = (0, 0, 0);
2238 my %term_state;
2239 sub reset_term_state { my %old_term = %term_state; %term_state = (); %old_term }
2240 sub set_term_state { my %old_term = %term_state; %term_state = @_; %old_term }
2241 %ansi_table = (
2242 # fe-common::core::formats.c:format_expand_styles
2243 (map { my $t = $i++; ($_ => sub { my $n = $term_state{hicolor} ? \&Terminfo::setab16 : \&Terminfo::setab;
2244 $n->($t) }) } (split //, '01234567' )),
2245 (map { my $t = $j++; ($_ => sub { my $n = $term_state{hicolor} ? \&Terminfo::setaf16 : \&Terminfo::setaf;
2246 $n->($t) }) } (split //, 'krgybmcw' )),
2247 (map { my $t = $k++; ($_ => sub { my $n = $term_state{hicolor} ? \&Terminfo::setaf : \&Terminfo::setaf16;
2248 $n->($t) }) } (split //, 'KRGYBMCW')),
2249 # reset
2250 n => sub { $term_state{hicolor} = 0; my $r = Terminfo::op;
2251 for (qw(blink rev bold)) {
2252 $r .= Terminfo->can("exit_$_")->() if delete $term_state{$_};
2253 }
2254 {
2255 local $ansi_table{n} = $ansi_table{N};
2256 $r .= formats_to_ansi_basic($vars{itembg});
2257 }
2258 $r
2259 },
2260 N => sub { reset_term_state(); Terminfo::sgr0 },
2261 # flash/bright
2262 F => sub { my $n = 'blink'; my $e = ($term_state{$n} ^= 1) ? $n : "exit_$n"; Terminfo->can($e)->() },
2263 # reverse
2264 8 => sub { my $n = 'rev'; my $e = ($term_state{$n} ^= 1) ? $n : "exit_$n"; Terminfo->can($e)->() },
2265 # bold
2266 "_" => sub { my $n = 'bold'; my $e = ($term_state{$n} ^= 1) ? $n : "exit_$n"; Terminfo->can($e)->() },
2267 # underline
2268 U => sub { my $n = 'ul'; my $e = ($term_state{$n} ^= 1) ? $n : "exit_$n"; Terminfo->can($e)->() },
2269 # italic
2270 I => sub { my $n = 'it'; my $e = ($term_state{$n} ^= 1) ? $n : "exit_$n"; Terminfo->can($e)->() },
2271 # bold, used as colour modifier if AWL_HI9 is set
2272 9 => $ENV{AWL_HI9} ? sub { $term_state{hicolor} ^= 1; '' }
2273 : sub { my $n = 'bold'; my $e = ($term_state{$n} ^= 1) ? $n : "exit_$n"; Terminfo->can($e)->() },
2274 # delete other stuff
2275 (map { $_ => sub { '' } } (split //, ':|>#[')),
2276 # escape
2277 (map { my $close = $_; $_ => sub { $close } } (split //, '{}%')),
2278 );
2279 for my $base (0 .. 15) {
2280 my $close = $base;
2281 my $idx = ($close&8) | ($close&4)>>2 | ($close&2) | ($close&1)<<2;
2282 $ansi_table{ (sprintf "x0%x", $close) } =
2283 $ansi_table{ (sprintf "x0%X", $close) } =
2284 sub { Terminfo::setab256($idx) };
2285 $ansi_table{ (sprintf "X0%x", $close) } =
2286 $ansi_table{ (sprintf "X0%X", $close) } =
2287 sub { Terminfo::setaf256($idx) };
2288 }
2289 for my $plane (1 .. 6) {
2290 for my $coord (0 .. 35) {
2291 my $close = 16 + ($plane-1) * 36 + $coord;
2292 my $ch = $coord < 10 ? $coord : chr( $coord - 10 + ord 'a' );
2293 $ansi_table{ "x$plane$ch" } =
2294 $ansi_table{ "x$plane\U$ch" } =
2295 sub { Terminfo::setab256($close) };
2296 $ansi_table{ "X$plane$ch" } =
2297 $ansi_table{ "X$plane\U$ch" } =
2298 sub { Terminfo::setaf256($close) };
2299 }
2300 }
2301 for my $gray (0 .. 23) {
2302 my $close = 232 + $gray;
2303 my $ch = chr( $gray + ord 'a' );
2304 $ansi_table{ "x7$ch" } =
2305 $ansi_table{ "x7\U$ch" } =
2306 sub { Terminfo::setab256($close) };
2307 $ansi_table{ "X7$ch" } =
2308 $ansi_table{ "X7\U$ch" } =
2309 sub { Terminfo::setaf256($close) };
2310 }
2311 sub formats_to_ansi_basic {
2312 my $o = shift;
2313 $o =~ s/(%(X..|x..|.))/exists $ansi_table{$2} ? $ansi_table{$2}->() : $1/gex;
2314 $o
2315 }
2316 }
2317
2318 sub _header {
2319 my $str = $vars{title} // uc ::setc();
2320 my $ccs = qr/%(?:X(?:[1-6][0-9A-Z]|7[A-X])|[0-9BCFGIKMNRUWY_])/i;
2321 (my $stripstr = $str) =~ s/($ccs)//g;
2322 my $space = int( ((abs $vars{block}) - length $stripstr) / (1 + length $stripstr));
2323 if ($space > 0) {
2324 my $ss = ' ' x $space;
2325 my @x = $str =~ /((?:$ccs)*\X(?:(?:$ccs)*$)?)/g;
2326 $str = join $ss, '', @x, '';
2327 }
2328 ($stripstr = $str) =~ s/($ccs)//g;
2329 my $pad = max 0, (abs $vars{block}) - length $stripstr;
2330 $str = ' ' x ($pad/2) . $str . ' ' x ($pad/2 + $pad%2);
2331 $str
2332 }
2333
2334 sub _add_item {
2335 my ($i, $j, $c, $wi, $screen, $mouse) = @_;
2336 $screen->[$i][$j] = "%N%n$wi";
2337 if (exists $vars{mouse}{$c - 1}) {
2338 $mouse->[$i][$j] = $vars{mouse}{$c - 1};
2339 }
2340 }
2341 sub update_screen {
2342 $disp_update = 0;
2343 unless ($sock && exists $vars{seplen} && exists $vars{block}) {
2344 leave_fs(1);
2345 return;
2346 }
2347 enter_fs();
2348 @screen = () if delete $vars{redraw};
2349 %mouse_coords = ();
2350 my $ncols = ($vars{seplen} + abs $vars{block}) ?
2351 int( ($screenWidth + $vars{seplen}) / ($vars{seplen} + abs $vars{block}) ) : 0;
2352 my $xenl = ($vars{seplen} + abs $vars{block})
2353 && $ncols > int( ($screenWidth + $vars{seplen} - 1) / ($vars{seplen} + abs $vars{block}) );
2354 my $nrows = $screenHeight - $vars{ha};
2355 my @wi = @{$vars{win}//[]};
2356 my $max_items = $ncols * $nrows;
2357 my $c = $show_title_bar ? 1 : 0;
2358 my $items = @wi + $c;
2359 my $titems = $items > $max_items ? $max_items : $items;
2360 my $i = 0;
2361 my $j = 0;
2362 my @new_screen;
2363 my @new_mouse;
2364 $new_screen[0][0] = _header() #. ' ' x $vars{seplen}
2365 if $show_title_bar;
2366 unless ($nrows > $ncols) { # line layout
2367 ++$j if $show_title_bar;
2368 for my $wi (@wi) {
2369 if ($j >= $ncols) {
2370 $j = 0;
2371 ++$i;
2372 }
2373 last if $i >= $nrows;
2374 _add_item($i, $j, $show_title_bar ? $c : $c + 1,
2375 $wi, \@new_screen, \@new_mouse);
2376 if ($c + 1 < $titems && $j + 1 < $ncols) {
2377 $new_screen[$i][$j] .= $vars{separator};
2378 }
2379 ++$j;
2380 ++$c;
2381 }
2382 }
2383 else { # column layout
2384 ++$i if $show_title_bar;
2385 for my $wi (@wi) {
2386 if ($i >= $nrows) {
2387 $i = 0;
2388 ++$j;
2389 }
2390 last if $j >= $ncols;
2391 _add_item($i, $j, $show_title_bar ? $c : $c + 1,
2392 $wi, \@new_screen, \@new_mouse);
2393 if ($c + $nrows < $titems) {
2394 $new_screen[$i][$j] .= $vars{separator};
2395 }
2396 ++$i;
2397 ++$c;
2398 }
2399 }
2400 my $step = $vars{seplen} + abs $vars{block};
2401 $i = 0;
2402 my $str = Terminfo::sc . Terminfo::sgr0;
2403 for (my $i = 0; $i < @new_screen; ++$i) {
2404 for (my $j = 0; $j < @{$new_screen[$i]}; ++$j) {
2405 if (defined $new_mouse[$i] && defined $new_mouse[$i][$j]) {
2406 my $from = $j * $step;
2407 $mouse_coords{$i}{$_} = $new_mouse[$i][$j]
2408 for $from .. $from + abs $vars{block};
2409 }
2410 next if defined $screen[$i] && defined $screen[$i][$j]
2411 && $screen[$i][$j] eq $new_screen[$i][$j];
2412 $str .= Terminfo::cup($i, $j * $step)
2413 . formats_to_ansi_basic($new_screen[$i][$j])
2414 . Terminfo::sgr0;
2415 $str .= Terminfo::el if $j == $#{$new_screen[$i]} && (!$xenl || $j + 1 != $ncols);
2416 }
2417 }
2418 for (@new_screen .. $screenHeight - 1) {
2419 if (!@screen || defined $screen[$_]) {
2420 $str .= Terminfo::cup($_, 0) . Terminfo::sgr0 . Terminfo::el;
2421 }
2422 }
2423 $str .= Terminfo::rc;
2424 safe_print $str;
2425 @screen = @new_screen;
2426 }
2427
2428 sub handle_resize {
2429 if (defined (my $r = safe_qx('stty size'))) {
2430 ($screenHeight, $screenWidth) = split ' ', $r;
2431 $resized = 0;
2432 @screen = ();
2433 $disp_update = 1;
2434 if ($one_shot_integration == 2) {
2435 $one_shot_resize--;
2436 }
2437 }
2438 else {
2439 }
2440 }
2441
2442 sub _build_keymap {
2443 %c2w = reverse( %{$vars{key}}, %{$vars{key2}} );
2444 if (!grep { /^[+-]./ } keys %c2w) {
2445 %c2w = (%c2w, map { ("-$_" => $c2w{$_}) } grep { !/^\^./ } keys %c2w);
2446 }
2447 %c2w = map {
2448 my $key = $_;
2449 s{^(-)?(\+)?(\^)?(.)}{
2450 join '', (
2451 ($1 ? "\e" : ''),
2452 ($2 ? "\e\e" : ''),
2453 ($3 ? "$4"^"@" : $4)
2454 )
2455 }e;
2456 $_ => $c2w{$key}
2457 } keys %c2w;
2458 @seqlist = sort { length $b <=> length $a } keys %c2w;
2459 }
2460
2461 sub _match_tmux {
2462 length $ENV{TMUX} && exists $vars{irssienv}{tmux_srv} && length $vars{irssienv}{tmux_pane}
2463 && $ENV{TMUX} eq $vars{irssienv}{tmux_srv}
2464 }
2465
2466 sub process_keys {
2467 Encode::_utf8_on($keybuf);
2468 my $win;
2469 my $use_mouse;
2470 my $maybe;
2471 KEY: while (length $keybuf && !$maybe) {
2472 $maybe = 0;
2473 if ($keybuf =~ s/^\e\[M(.)(.)(.)//) {
2474 @last_mouse = @mouse;# if @mouse && $mouse[0] < 64;
2475 @mouse = map { -32 + ord } ($1, $2, $3);
2476 $use_mouse = 1;
2477 next KEY;
2478 }
2479 for my $s (@seqlist) {
2480 if ($keybuf =~ s/^\Q$s//) {
2481 $win = $c2w{$s};
2482 $use_mouse = 0;
2483 next KEY;
2484 }
2485 elsif (length $keybuf < length $s && $s =~ /^\Q$keybuf/) {
2486 $maybe = 1;
2487 }
2488 }
2489 unless ($maybe) {
2490 substr $keybuf, 0, 1, '';
2491 }
2492 }
2493 if ($use_mouse && @mouse && @last_mouse &&
2494 $mouse[2] == $last_mouse[2] &&
2495 $mouse[1] == $last_mouse[1] &&
2496 ($mouse[0] == 3 || $mouse[0] == 64 || $mouse[0] == 65)) {
2497 if ($mouse[0] == 64) {
2498 $win = 'up';
2499 }
2500 elsif ($mouse[0] == 65) {
2501 $win = 'down';
2502 }
2503 elsif (exists $mouse_coords{$mouse[2] - 1}{$mouse[1] - 1}) {
2504 $win = $mouse_coords{$mouse[2] - 1}{$mouse[1] - 1};
2505 }
2506 elsif ($mouse[2] == 1 && $mouse[1] <= abs $vars{block}) {
2507 $win = $last_mouse[0] != 0 ? 'last' : 'active';
2508 }
2509 else {
2510 }
2511 }
2512 if (defined $win) {
2513 $win =~ s/^_//;
2514 safe_print_sock("$win\n");
2515 if (!exists $ENV{AWL_AUTOFOCUS} || $ENV{AWL_AUTOFOCUS}) {
2516 if (_match_tmux()) {
2517 safe_qx("tmux selectp -t $vars{irssienv}{tmux_pane} 2>&1");
2518 }
2519 elsif (exists $vars{irssienv}{xwinid}) {
2520 safe_qx("wmctrl -ia $vars{irssienv}{xwinid} 2>/dev/null");
2521 }
2522 }
2523 }
2524 Encode::_utf8_off($keybuf);
2525 }
2526
2527 sub check_integration {
2528 return unless $vars{irssienv};
2529 return unless $sock && exists $vars{seplen} && exists $vars{block};
2530 if ($one_shot_integration == 1) {
2531 my $nrows = $screenHeight - $vars{ha};
2532 my $ncols = ($vars{seplen} + abs $vars{block}) ? int( ($screenWidth + $vars{seplen}) / ($vars{seplen} + abs $vars{block}) ) : 0;
2533 my $items = ($show_title_bar ? 1 : 0) + @{$vars{win}//[]};
2534 my $dcols_required = $nrows ? int($items/$nrows) + !!($items%$nrows) : 0;
2535 my $rows_required = $ncols ? int($items/$ncols) + !!($items%$ncols) : 0;
2536 $rows_required = abs $vars{ml}
2537 if ($vars{ml} < 0 || ($vars{ml} > 0 && $rows_required > $vars{ml}));
2538 $dcols_required = abs $vars{mc}
2539 if ($vars{mc} < 0 || ($vars{mc} > 0 && $dcols_required > $vars{mc}));
2540 my $rows = $rows_required + $vars{ha};
2541 my $cols = ($dcols_required * ($vars{seplen} + abs $vars{block})) - $vars{seplen};
2542 if (_match_tmux()) {
2543 # int( ($screenWidth + $vars{seplen}) / ($vars{seplen} + abs $vars{block}) );
2544 my ($pos_flag, $before);
2545 if ($integration_position eq 'left') {
2546 $pos_flag = 'h';
2547 $before = 1;
2548 }
2549 elsif ($integration_position eq 'top') {
2550 $pos_flag = 'v';
2551 $before = 1;
2552 }
2553 elsif ($integration_position eq 'right') {
2554 $pos_flag = 'h';
2555 }
2556 else {
2557 $pos_flag = 'v';
2558 }
2559 my @cmd = "joinp -d$pos_flag -s $ENV{TMUX_PANE} -t $vars{irssienv}{tmux_pane}";
2560 push @cmd, "swapp -d -t $ENV{TMUX_PANE} -s $vars{irssienv}{tmux_pane}"
2561 if $before;
2562 $cols = max($cols, 2);
2563 $rows = max($rows, 2);
2564
2565 safe_qx("tmux " . (join " \\\; ", @cmd) . " 2>&1");
2566 }
2567 else {
2568 $resized = 1;
2569 #safe_qx("resize -s $screenHeight $cols 2>&1")
2570 # if $cols > 0;
2571 }
2572 $one_shot_integration++;
2573 if ($resized == 1) {
2574 handle_resize();
2575 resize_integration();
2576 }
2577 }
2578 elsif ($one_shot_integration == 2) {
2579 resize_integration(1);
2580 }
2581 }
2582
2583 sub resize_integration {
2584 return unless $one_shot_integration;
2585 return unless ($one_shot_resize//0) < 0 || shift;
2586 return if ($one_shot_resize//0) > 0;
2587
2588 my $nrows = $screenHeight - $vars{ha};
2589 my $ncols = ($vars{seplen} + abs $vars{block}) ? int( ($screenWidth + $vars{seplen}) / ($vars{seplen} + abs $vars{block}) ) : 0;
2590 my $items = ($show_title_bar ? 1 : 0) + @{$vars{win}//[]};
2591 my $dcols_required = $nrows ? (int($items/$nrows) + !!($items%$nrows)) : 0;
2592 my $rows_required = $ncols ? int($items/$ncols) + !!($items%$ncols) : 0;
2593 $rows_required = abs $vars{ml}
2594 if ($vars{ml} < 0 || ($vars{ml} > 0 && $rows_required > $vars{ml}));
2595 $dcols_required = abs $vars{mc}
2596 if ($vars{mc} < 0 || ($vars{mc} > 0 && $dcols_required > $vars{mc}));
2597 my $rows = $rows_required + $vars{ha};
2598 my $cols = ($dcols_required * ($vars{seplen} + abs $vars{block})) - $vars{seplen};
2599 if (_match_tmux()) {
2600 my $pos_flag;
2601 my $before = 0;
2602 if ($integration_position eq 'left') {
2603 $pos_flag = 'h';
2604 $before = 1;
2605 }
2606 elsif ($integration_position eq 'top') {
2607 $pos_flag = 'v';
2608 $before = 1;
2609 }
2610 elsif ($integration_position eq 'right') {
2611 $pos_flag = 'h';
2612 }
2613 else {
2614 $pos_flag = 'v';
2615 }
2616 my @cmd;
2617 # hard tmux limits
2618 $cols = max($cols, 2);
2619 $rows = max($rows, 2);
2620 if ($pos_flag eq 'h' && $cols != $screenWidth) {
2621 my $change = $screenWidth - $cols;
2622 my $dir = ($before ^ ($change<0)) ? 'L' : 'R';
2623 push @cmd, "resizep -$dir -t $ENV{TMUX_PANE} @{[abs $change]}";
2624 #push @cmd, "resizep -x $cols -t $ENV{TMUX_PANE}";
2625 $one_shot_resize = 1;
2626 }
2627 if ($pos_flag eq 'v' && $rows != $screenHeight) {
2628 #push @cmd, "resizep -y $rows -t $ENV{TMUX_PANE}";
2629 my $change = $screenHeight - $rows;
2630 my $dir = ($before ^ ($change<0)) ? 'U' : 'D';
2631 push @cmd, "resizep -$dir -t $ENV{TMUX_PANE} @{[abs $change]}";
2632 $one_shot_resize = 1;
2633 }
2634
2635 safe_qx("tmux " . (join " \\\; ", @cmd) . " 2>&1")
2636 if @cmd;
2637 }
2638 else {
2639 $cols = max($cols, 1);
2640 $rows = max($rows, 1);
2641 unless ($nrows > $ncols) { # line layout
2642 if ($rows != $screenHeight) {
2643 safe_qx("resize -s $rows $screenWidth 2>&1");
2644 $one_shot_resize = 1;
2645 }
2646 }
2647 else {
2648 if ($cols != $screenWidth) {
2649 safe_qx("resize -s $screenHeight $cols 2>&1");
2650 $one_shot_resize = 1;
2651 }
2652 }
2653 }
2654 if ($resized == 1) {
2655 handle_resize();
2656 }
2657 }
2658
2659 sub init_integration {
2660 return unless $one_shot_integration;
2661 if (_match_tmux()) {
2662 }
2663 else {
2664 }
2665 safe_print("\e]2;".(uc ::setc())."\e\\");
2666 }
2667
2668 sub main {
2669 require Getopt::Std;
2670 my %opts;
2671 Getopt::Std::getopts('1p:', \%opts);
2672 my $one_shot = $opts{1};
2673 $integration_position = $opts{p};
2674 $one_shot_integration = 0+!!$one_shot;
2675 #shift if @_ && $_[0] eq '--';
2676 &init;
2677 $show_title_bar = 0 if $ENV{AWL_NOTITLE};
2678 init_integration();
2679 until ($got_int) {
2680 $timeout = undef;
2681 if ($resized) {
2682 if ($resized == 1) {
2683 $timeout = 1;
2684 $resized++;
2685 }
2686 else {
2687 handle_resize();
2688 resize_integration();
2689 }
2690 }
2691 unless ($sock || $timeout) {
2692 connect_it();
2693 }
2694 $timeout ||= RECONNECT_TIME unless $sock;
2695 update_screen() if $disp_update;
2696 SELECT: while (my @read = $loop->can_read($timeout)) {
2697 for my $fh (@read) {
2698 if ($fh == \*STDIN) {
2699 if (read STDIN, my $buf, BLOCK_SIZE) {
2700 do {
2701 $keybuf .= $buf;
2702 } while read STDIN, $buf, BLOCK_SIZE;
2703 }
2704 else {
2705 $got_int = 1;
2706 last SELECT;
2707 }
2708 }
2709 else {
2710 if ($fh->read(my $buf, BLOCK_SIZE)) {
2711 do {
2712 $rcvbuf .= $buf;
2713 } while $fh->read($buf, BLOCK_SIZE);
2714 }
2715 else {
2716 $disp_update = 1;
2717 remove_conn($fh);
2718 if ($one_shot) {
2719 $got_int = 1;
2720 last SELECT;
2721 }
2722 $timeout ||= RECONNECT_TIME;
2723 }
2724 }
2725 }
2726 $disp_update |= process_recv() if length $rcvbuf;
2727 process_keys() if length $keybuf;
2728 check_integration() if $one_shot;
2729 update_screen() if $disp_update;
2730 }
2731 continue {
2732 }
2733 }
2734 end_prog();
2735 }
2736 }
2737
2738 1;
2739
2740 # Changelog
2741 # =========
2742 # 1.8
2743 # - use string_width in Irssi 1.2.0
2744 #
2745 # 1.7
2746 # - fix crash on invalid /set awl_sort, introduced in 1.6, reported by
2747 # tpetazzoni
2748 # - delay viewer initialisation
2749 # - improve race condition on tmux resize integration
2750 #
2751 # 1.6
2752 # - add detach setting to hide windows
2753 # - fix race condition when loading the script, reported by madduck
2754 # - improve compatibility with irssi 1.2
2755 # - add special value lru to awl_sort to sort windows by usage
2756 #
2757 # 1.5
2758 # - improve compat. with sideways splits
2759 #
2760 # 1.4
2761 # - fix line wrapping in some themes, reported by justanotherbody
2762 # - fix named window key detection, reported by madduck
2763 # - make title (in viewer and shared_sbar) configurable
2764 #
2765 # 1.3
2766 # - workaround for irssi issue #572
2767 #
2768 # 1.2
2769 # - new format to choose abbreviation character
2770 #
2771 # 1.1
2772 # - infinite loop on shortening certain window names reported by Kalan
2773 #
2774 # 1.0
2775 # - new awl_viewer_launch setting and an array of related settings
2776 # - fixed regression bug /exec -interactive
2777 # - fixed some warnings in perl 5.10 reported by kl3
2778 # - workaround for crash due to infinite recursion in irssi's Perl
2779 # error handling
2780 #
2781 # 0.9
2782 # - fix endless loop in awin detection code!
2783 # - correct colour swap in awl_viewer
2784 # - fix passing of alternate socket path to the viewer
2785 # - potential undefinedness in mouse refnum hinted at by Canopus
2786 # - fixed regression bug /exec -interactive
2787 # - add case-insensitive modifier to awl_sort
2788 # - run custom_xform on awl_prefer_name also
2789 # - avoid inconsistent active window state after awin detection
2790 # reported by ss
2791 # - revert %9-hack in the viewer prompted by discussion with pierrot
2792 # - fix new warning in perl 5.22
2793 #
2794 # 0.8
2795 # - replace fifo mode with external viewer script
2796 # - remove bundled cpan modules
2797 # - work around bogus irssi warning
2798 # - improve mouse support
2799 # - workaround for broken cumode in irssi 0.8.15
2800 # - fix handling of non-meta windows (uninitialized warning)
2801 # - add 256 colour support, strip true colour codes
2802 # - fix totally bogus $N padding reported by Ed S.
2803 # - make /window goto #name mappings work but ignore non-existant ones
2804 # - improve incomplete reads reported by bcode
2805 # - fix single % in awl_viewer reported by bcode
2806 # - add support for key bindings by nike and ferret
2807 # - coerce utf8 key binds
2808 # - add settings: custom_xform, last_line_shade, hide_name_data
2809 # - abbreviations were broken in some cases
2810 # - fix some misuse of / as cmdchar in mouse script reported by bcode
2811 # - add shared status bar mode
2812 # - ${type} variables for custom_xform setting
2813 # - crash if custom_xform had runtime error
2814 # - update sorting documentation
2815 # - fix odd case in size calculation noted by lasers
2816 # - add missing font styles to the viewer reported by ishanyx
2817 # - add italic
2818 #
2819 # 0.7g
2820 # - remove screen support and replace it with fifo support
2821 # - add double-width support to the shortener
2822 # - correct documentation regarding $T vs. display_header
2823 # - add missing refresh for window item changed (thanks vague)
2824 # - add visible windows
2825 # - add exemptions for active window
2826 # - workaround for hiding the window changes from trackbar
2827 # - hack to force 16colours in screen mode
2828 # - remember last window (reported by earthnative)
2829 # - wrong window focus on new queries (reported by emsid)
2830 # - dataloss bug on trying to remember last window
2831 #
2832 # 0.6d+
2833 # - add support for network headers
2834 # - fixed regression bug /exec -interactive
2835 #
2836 # 0.6ca+
2837 # - add screen support (from nicklist.pl)
2838 # - names can now have a max length and window names can be used
2839 # - fixed a bug with block display in screen mode and status bar mode
2840 # - added space handling to ir_fe and removed it again
2841 # - now handling formats on my own
2842 # - started to work on $tag display
2843 # - added warning about missing sb_act_none abstract leading to
2844 # - display*active settings
2845 # - added warning about the bug in awl_display_(no)key_active settings
2846 # - mouse hack
2847 #
2848 # 0.5d
2849 # - add setting to also hide the last status bar if empty (awl_all_disable)
2850 # - reverted to old utf8 code to also calculate broken utf8 length correctly
2851 # - simplified dealing with status bars in wlreset
2852 # - added a little tweak for the renamed term_type somewhere after Irssi 0.8.9
2853 # - fixed bug in handling channel #$$
2854 # - reset background colour at the beginning of an entry
2855 #
2856 # 0.4d
2857 # - fixed order of disabling status bars
2858 # - several attempts at special chars, without any real success
2859 # and much more weird new bugs caused by this
2860 # - setting to specify sort order
2861 # - reduced timeout values
2862 # - added awl_hide_data
2863 # - make it so the dynamic sub is actually deleted
2864 # - fix a bug with removing of the last separator
2865 # - take into consideration parse_special
2866 #
2867 # 0.3b
2868 # - automatically kill old status bars
2869 # - reset on /reload
2870 # - position/placement settings
2871 #
2872 # 0.2
2873 # - automated retrieval of key bindings (thanks grep.pl authors)
2874 # - improved removing of status bars
2875 # - got rid of status chop
2876 #
2877 # 0.1
2878 # - Based on chanact.pl which was apparently based on lightbar.c and
2879 # nicklist.pl with various other ideas from random scripts.