use strict; use warnings;
$INC{'Encode/ConfigLocal.pm'}=1;
require Encode;

# to read the following docs, you can use "perldoc coords2.pl"

=head1 NAME

coords2 - weechat script to map screen coordinates

=head1 SYNOPSIS

first, copy the file and the F<Nlib.pm> library to your
F<.weechat/perl> directory. Then you can type

  /script load coords2.pl

in weechat to load the script. Use

  /coords2

to open the coords screen, or conveniently

  /key bind meta-/ /coords2 /

to open it with the Alt+/ keybinding.

=head1 DESCRIPTION

coords will hilight links, allow text selection and tries to send the
selection to xterm

=head1 SETUP

if you would like urls to be copied into the selection clipboard, add

  xterm*disallowedWindowOps:20,21,SetXprop

to your F<.Xresources> file for B<xterm>.

For B<rxvt-unicode>, get the F<osc-xterm-clipboard> script from
L<http://anti.teamidiot.de/static/nei/*/Code/urxvt/>, install it to
your F<perl-lib> directory and add it to your F<.Xresources> like such:

  URxvt.perl-ext-common: default,osc-xterm-clipboard

=head1 USAGE

to open the url overlay on a window, type C</coords2> or use the
keybinding you created as explained in the L</SYNOPSIS>.

=head2 Selection Mode

by default, the copy window will be in selection mode. you can move
the text cursor with the arrow keys and open a selection with the
Space key. The selection content will be transfered into clipboard.

=head2 URL Mode

to switch between selection mode and URL mode, use the C</> key or
type the command C</coords2 /> to directly start in URL mode.

inside the overlay, you can use Arrow-Up and Arrow-Down keys to select
URLs. This script will try to copy them into your selection clipboard
(see L</SETUP>) so you should be able to open the selected link by
clicking the middle mouse button in your browser.

once you click or hit enter, an url open signal will be sent to
WeeChat.

to leave the overlay, hit the C<q> key.

for mouse support, this script will listen to mouse input
signals supplied by WeeChat.

=head1 CAVEATS

=over

=item *

whether other terminal emulators support selection storage, depends on
how well they emulate B<xterm>. Set C<xterm_compatible> to your
I<$TERM> if you think it does.

=item *

B<xterm> will not allow selection storage unless you enable it in the
Xresources as described in L</SETUP>

=item *

B<rxvt-unicode> will need a script to handle the selection operating
system control. querying the selection is only supported if the
selection is rxvt-unicodeE<apos>s (not needed for this script)

=item *

GNU B<screen> will not allow to pass through the operating system
control needed for selection storage to the underlying term, try
B<tmux> instead (L<http://tmux.sourceforge.net/>)

=item *

it would be possible to provide a remote clipboard, but that will
require a clipboard server. Also see B<xsel>, B<xclip>, command
mode. For remote connection without xterm/urxvt, there are a few
choices: ssh -Y (or -X) together with B<xsel> on the server is by far
the most practical one. a reverse ssh connection that calls out to
these tools has also been observed. For Windows users, try setting up
the B<DoIt> program (google for putty doit)

=back

=head1 TODO

=over

=item *

set the text input cursor on click in the input bar

=back

=head1 BUGS

probably

=head1 SETTINGS

the settings are usually found in the

  plugins.var.perl.coords2

namespace, that is, type

  /set plugins.var.perl.coords2.*

to see them and

  /set plugins.var.perl.coords2.SETTINGNAME VALUE

to change a setting C<SETTINGNAME> to a new value C<VALUE>. Finally,

  /unset plugins.var.perl.coords2.SETTINGNAME

will reset a setting to its default value.

the following settings are available:

=head2 url_regex

a regular expression to identify URLs in the text. See L<perlre> for
more information about Perl regular expressions.

=head2 url_braces

parenthesis-like characters which nest and should be excluded when
found around an URL. make sure the variable setting nests properly
when modifying this.

=head2 url_non_endings

this is matched against the end of a link and removed

=head2 hyper_nicks

make nicks to hyperlinks for menu/pm

=head2 hyper_channels

make channels to hyperlinks for join

=head2 hyper_show

set to types of hyperlinks that are shown by default

=head2 use_nick_menu

use nick menu when opening nick hyperlink (see I<hyper_nicks>,
requires menu.pl script). otherwise open private message. this setting
only applies to text mode selection, for mouse see
I<mouse.nick_2nd_click>

=head2 color.url_highlight

the weechat color and/or attribute to be used for highlighting URLs in
the copy window. seperate multiple attributes with C<.>

=head2 color.url_highlight_active

the same as I<color.url_highlight> except for the currently (using
arrow keys) selected link.

=head2 color.selection_cursor

the weechat color and/or attribute to be used for the text cursor.

=head2 color.selection

the color of the currently selected text in selection mode

=head2 copybuf_short_name

short_name to use for coords buffer. it is set to the copy sign by
default to not disturb buffers bar width, set to the empty string to
have window position and size shown

=head2 mouse.copy_on_click

set to on if it should be possible to directly click on URLs and
select text, set to off if mouse should only work in open coords
buffer

=head2 mouse.close_on_release

set to on or a delay (in ms) to autoclose coords buffer opened by
I<copy_on_click> on button release, set to off if the coords buffer
should stay open after click

=head2 mouse.url_open_2nd_click

if this is set, URLs are only opened when clicked twice (in the same
incarnation of a coords buffer) instead of on first click. it can be set to
a delay (in ms) that will be added to the I<close_on_release> delay if
the script is waiting for a second click on the URL to happen

=head2 clipboard_command

if you set this, an external program may be executed to store the
selection or URL. begin with C<|> to pipe into program or use
parameters C<%s> for text, C<%q> for quoted text or C<%x> for quoted
escape sequence.

=begin comment

=head1 FUNCTION DESCRIPTION

for full pod documentation, filter this script with

  perl -pE'
  (s/^## (.*?) -- (.*)/=head2 $1\n\n$2\n\n=over\n/ and $o=1) or
   s/^## (.*?) - (.*)/=item I<$1>\n\n$2\n/ or
  (s/^## (.*)/=back\n\n$1\n\n=cut\n/ and $o=0,1) or
  ($o and $o=0,1 and s/^sub /=back\n\n=cut\n\nsub /)'

=end comment

=cut

#### autoload:
BEGIN { do "$ENV{HOME}/WeeP.pm" };
#### end autoload:

use POSIX qw();
use MIME::Base64;

# define some abbreviations
use constant W 	  => new Weechat; # weechat::
use constant WC   => W->WEECHAT_; # weechat::WEECHAT_
use constant Wstr => W->string_;  # weechat::string_

use constant SCRIPT_NAME => 'coords2';
our $VERSION = '0.9';
W->register(SCRIPT_NAME, 'Nei <anti.teamidiot.de>', $VERSION, 'GPL3', 'copy text and urls', 'stop_coords', '')
	|| return;
our $SCRIPT_FILE = W->_infolist('perl_script', '', SCRIPT_NAME)->[0]{filename};

W->hook_command(SCRIPT_NAME, 'copy active window for hyperlink operations or free selection of text',
					  '[/] || url nicks channels',
				(join "\n",
				 'if any or more arguments are given, go into hyperlink mode and set this filter.',
				 'in the title bar of opened copy window, the possible key bindings are displayed.',
				 'use '.W->color('bold').'/'.SCRIPT_NAME.' help'.W->color('-bold'). ' to read the manual',
				),
				'|| / %- || url %- || nicks %- || channels %- || url,nicks %- || url,channels %- '.
					'|| url,nicks,channels %- || nicks,channels %- || help',
				'coords_cmd', '');
W->hook_signal('upgrade', 'stop_coords', '');
W->hook_config('plugins.var.perl.' . SCRIPT_NAME . '.*', 'default_options', '');
W->hook_hsignal(SCRIPT_NAME, 'hsignal_evt', '');
W->key_bind('mouse', +{
	map { $_ => 'hsignal:' . SCRIPT_NAME }
	'@chat:button1*', '@chat:button1-event-*', '@chat(perl.[*):button1'
       });
my (@wee_keys, $update_wee_keys);
update_wee_keys(1);
W->hook_command_run('/key *', 'update_wee_keys', '1');

our $ACT_STR;
our %STR;


# some shortcuts
sub bool_value { W->config_string_to_boolean(@_) }
*screen_length = \&weechat::strlen_screen;

{ my $remove_formatting_re;
  BEGIN { $remove_formatting_re = do {
	  my $flags = qr{[*!/_|]*};
	  my $ext   = qr/\@$flags(?:.{5})?/;
	  my $ext_noflags = qr/\@(?:.{5})?/;
	  my $single = qr/[E]/;
	  my $col2_noflags = qr/(?:.{2})?/;
	  my $col2 = qr/$flags$col2_noflags/;
	  my $col = qr/$ext|$col2/;
	  my $col_noflags = qr/$ext_noflags|$col2_noflags/;
	  my $col1 = qr/[FB]$col/;
	  my $fgbg = qr/\*$col(?:,$col_noflags)?/;
	  my $bar = qr/b[FBD_#il-]?/;
	  my $reset = qr/\x1c/;
	  my $color_char = qr/\x19/;

	  my $case_color_char = qr/$color_char(?:$col1|$fgbg|$ext_noflags|$single|$bar|$reset|$col2_noflags)?/;

	  my $attr = qr/(?:\x1a|\x1b).?/;

	  my $case = qr/$case_color_char|$attr|$reset/;
  } }
  sub remove_formatting_re { $remove_formatting_re }
}

BEGIN { # import constants and functions into CopyWin class
	*CopyWin::SCRIPT_NAME = \&SCRIPT_NAME;
	*CopyWin::W = \&W;
	*CopyWin::bool_value = \&bool_value;
	*CopyWin::screen_length = \&screen_length;
	*FormattedString::_screen_length = \&screen_length;
	*CopyWin::remove_formatting_re = \&remove_formatting_re;
	*FormattedString::remove_formatting_re = \&remove_formatting_re;
	*CopyWin::garbage_str = \&garbage_str;
	*CopyWin::update_wee_keys = \&update_wee_keys;
}

{ package CopyWin;
  use strict; use warnings;
  use List::Util qw(min);
  use Carp;

  ## hyper_get_valid_keys -- get keys for type filter according to enabled settings
  ## $res - return value modification: 't' for title usage, 'u' for upcase regex, nothing for lowercase regex
  ## STATIC ##
  sub hyper_get_valid_keys {
	  my ($res) = @_;
	  $res //= '';
	  croak 'invalid usage' unless $res =~ /^[tu]?$/;

	  my $title = $res eq 't';
	  my $uc = $res eq 'u';

	  my $keys = 'u';
	  $keys .= 'n' if bool_value(W->config_get_plugin('hyper_nicks'));
	  $keys .= 'c' if bool_value(W->config_get_plugin('hyper_channels'));
	  $keys .= 'p' if bool_value(W->config_get_plugin('hyper_prefix'));
	  $keys = uc $keys if $title || $uc;
	  if ($title) {
		  length $keys == 1 ? " $keys  " :
		  length $keys == 2 ? (substr $keys, 0, 1).' '.(substr $keys, 1).' ' :
		  length $keys == 3 ? "$keys " :
		  $keys
	  }
	  else {
		  $keys = "[$keys]";
		  qr/^$keys$/;
	  }
  }

  ## _dispatch_input_from_map -- call handler according to argument
  ## $cbmap - argument => handler map
  ## $args - argument to use
  ## returns true value if processing is continued, false otherwise
  ## method ##
  sub _dispatch_input_from_map {
	  my $O = shift;
	  my ($cbmap, $args) = @_;

	  if (exists $cbmap->{$args}) {
		  my $cmd = $cbmap->{$args};
		  $O->$cmd;
	  }
	  else {
		  return;
	  }
	  1
  }

  ## hyperlink_dispatch_input -- dispatch input from ** commands in hyperlink mode
  ## $args - input argument
  ## returns true value if processing is continued, false otherwise
  ## method ##
  sub hyperlink_dispatch_input {
	  shift->_dispatch_input_from_map(+{
		  '+' => 'hyper_next',
		  '-' => 'hyper_prev',
		  '!' => 'hyper_open',
	  }, shift);
  }

  ## selection_dispatch_input -- dispatch input from ** commands in selectionmode
  ## $args - input argument
  ## returns true value if processing is continued, false otherwise
  ## method ##
  sub selection_dispatch_input {
	  shift->_dispatch_input_from_map(+{
		  '+' => 'cur_right',
		  '-' => 'cur_left',
		  '>' => 'cur_down',
		  '<' => 'cur_up',
		  'f' => 'cur_fwd',
		  'b' => 'cur_bwd',
		  'e' => 'cur_end',
		  'a' => 'cur_beg',
		  '@' => 'toggle_sel',
	  }, shift);
  }

  ## _unique_valid_hyper_keys -- helper to get valid keys string
  ## $valid_keys - all valid keys
  ## $request - requested value
  ## $letterlist - true if input is already list of letters
  ## STATIC ##
  sub _unique_valid_hyper_keys {
	  my ($valid_keys, $request, $letterlist) = @_;
	  my @list;
	  if ($letterlist) {
		  @list = map { lc } split '', $request;
	  }
	  else {
		  @list = map { lc substr $_, 0, 1 } map { split /[,;]/ } split ' ', $request;
	  }
	  join '', keys %{+{ map { $_ => undef } grep { /$valid_keys/ } @list }};
  }

  ## hyper_set_type_filter -- sets the type of links shown in url view based on setting and args
  ## $args - command line arguments
  ## returns new args
  ## method ##
  sub hyper_set_type_filter {
	  my $O = shift;
	  my ($args) = @_;
	  my $valid_keys = hyper_get_valid_keys();
	  # get defaults
	  $O->{link_type_filter} = _unique_valid_hyper_keys($valid_keys, W->config_get_plugin('hyper_show'));
	  $args //= '';
	  my $urlfilter = $args;
	  if ($urlfilter =~ s/^\/// && length $urlfilter) { # called like: /coords2 /unc
		  $args = '/';
		  $O->{link_type_filter} = _unique_valid_hyper_keys($valid_keys, $urlfilter, 1);
	  }
	  elsif (length $urlfilter) { # called like: /coords2 urls nicks channels
		  $O->{link_type_filter} = _unique_valid_hyper_keys($valid_keys, $urlfilter);
		  $args = '/' if length $O->{link_type_filter};
	  }
	  $O->{link_type_filter} = 'u' unless length $O->{link_type_filter};
	  $args
  }

  ## _key_bind -- method: bind key on buffer
  ## $key - key
  ## $letter - action letter for script command
  sub _key_bind {
	  my $O = shift;
	  my ($key, $letter) = @_;
	  $O->bufptr->set('key_bind_' . $key => '/' . SCRIPT_NAME . ' **' . $letter);
  }

  ## _keybind_from_weekeys -- grep matching existing keys and bind them to a coords command
  ## $wee_keys - all weechat keys
  ## $keylist - list of keybindings to create (format: [command, command, ..., fallback key, action-letter],...)
  ## method ##
  sub _keybind_from_weekeys {
	  my $O = shift;
	  my ($wee_keys, $keylist) = @_;
	  for my $e (@$keylist) {
		  my ($letter, $fallback, @cmds) = reverse @$e;
		  my @keys = map { $_->{key} }
			  grep {
				  my $command = $_->{command};
				  grep { $_ eq $command } @cmds
			  } @$wee_keys;
		  @keys = $fallback unless @keys;
		  $O->_key_bind($_ => $letter) for @keys;
	  }
  }

  ## apply_keybindings -- set up key bindings for copy buffer
  ## method ##
  sub apply_keybindings {
	  my $O = shift;

	  my @kblist = (
		  ['/input history_previous', '/input history_global_previous', 'meta2-A', '-'], # up arrow
		  ['/input history_next',     '/input history_global_next',     'meta2-B', '+'], # down arrow
		  ['/input move_next_char',                                     'meta2-C', '>'], # right arrow
		  ['/input move_previous_char',                                 'meta2-D', '<'], # left arrow
		  ['/input move_next_word',                                      'meta-f', 'f'], # back word
		  ['/input move_previous_word',                                  'meta-b', 'b'], # forward word
		  ['/input move_end_of_line',                                    'ctrl-E', 'e'], # left arrow
		  ['/input move_beginning_of_line',                              'ctrl-A', 'a'], # left arrow
		  ['/input return',           '/input magic_enter',              'ctrl-M', '!'], # enter key
		  ['ctrl-@',                                                               '@'], # ctrl+space or ctrl+@
		  [' ',                                                                    '@'], # space
		 );

	  update_wee_keys();

	  $O->_keybind_from_weekeys(\@wee_keys, \@kblist);
	  $O->_key_bind($_ => $_) for split '', 'UNCuncPp/q';

	  my @keys = map { $_->{key} }
		  grep { $_->{command} =~ "^/@{[SCRIPT_NAME]}" } @wee_keys;
	  $O->_key_bind($_ => 'q') for @keys;
  }

  ## switchmode -- toggle between modes
  ## method ##
  sub switchmode {
	  my $O = shift;
	  $O->{mode} = $O->{mode} eq 'hyperlink' ? 'selection' : 'hyperlink';
	  my ($r_, $R_) = (W->color('reverse'), W->color('-reverse'));
	  my $I = '▐';
	  my $t_flt = hyper_get_valid_keys('t');
	  $t_flt =~ s/$_/$_/i for split '', $O->{link_type_filter};
	  $O->bufptr->{title} =
		($O->{mode} eq 'hyperlink' ?
		    $r_.' ↑ ↓'.$R_.'move to url'.
			$I.$r_.'RET' .$I.$R_.'send open'.
			$I.$r_.'/'   .$I.$R_.'sel.mode'.
			$I.$r_.$t_flt.$I.$R_.
			$I.$r_.'q'   .$I.$R_.'close'
						:
		    $r_.'←↑→↓'.$R_.'move cursor'.
			$I.$r_.'SPC'   .$I.$R_.'start selection'.
			$I.$r_.'/'     .$I.$R_.'url mode'.
			$I.$r_.'q'     .$I.$R_.'close').' '.
		$r_.$I.$R_.
			 $O->{window}{buffer_short_name}.
		$I;
  }


  ## input_key -- copywin keybinding handler
  ## $args - key arguments
  ## returns true if refresh is needed
  ## method ##
  sub input_key {
	  my ($O, $args) = @_;

	  if ('q' eq $args) {
		  $O->bufptr->close;
		  return;
	  }
	  elsif ('/' eq $args) { # toggle mode
		  $O->hyper_set_type_filter()
			  unless length $O->{link_type_filter};
		  $O->switchmode();
	  }
	  elsif ($args =~ hyper_get_valid_keys()) { # set filter
		  $O->switchmode() if $O->{mode} eq 'hyperlink';
		  $O->{link_type_filter} = $args;
		  $O->switchmode();
	  }
	  elsif ($args =~ hyper_get_valid_keys('u')) { # toggle filter
		  switchmode() if $O->{mode} eq 'hyperlink';
		  my $t = lc $args;
		  $O->{link_type_filter} .= $t
			  unless $O->{link_type_filter} =~ s/$t//;
		  switchmode() if length $O->{link_type_filter};
	  }
	  elsif ($O->{mode} eq 'hyperlink') {
		  return unless $O->hyperlink_dispatch_input($args);
	  }
	  else {
		  return unless $O->selection_dispatch_input($args);
	  }
	  1
  }

  ## download_lines -- copy visible lines of given window into internal data
  ## $win - source window pointer
  ## returns arrayref with line infos
  ## STATIC ##
  sub download_lines {
	  #my $O = shift;
	  my ($win) = @_;

	  my %dl = (
		  win => $win,
		 );

	  my $lines_in = $win->{buffer}{lines};
	  my $line = $lines_in->{last_line};

	  my $start_line_pos = 0;

	  $dl{show_time} = $win->{buffer}{time_for_each_line};

	  $dl{buffer_len} = min $lines_in->{buffer_max_length},
		  grep { $_ > 0 } W->config_get('weechat.look.prefix_buffer_align_max')->integer;
	  $dl{prefix_buffer_align} = W->config_get('weechat.look.prefix_buffer_align')->string;
	  $dl{prefix_buffer_align_more_after} = W->config_get('weechat.look.prefix_buffer_align_more_after')->boolean;

	  $dl{prefix_len} = min $lines_in->{prefix_max_length},
	      grep { $_ > 0 } W->config_get('weechat.look.prefix_align_max')->integer;
	  $dl{prefix_align} = W->config_get('weechat.look.prefix_align')->string;
	  $dl{prefix_align_more_after} = W->config_get('weechat.look.prefix_align_more_after')->boolean;

	  $dl{separator} = FormattedString->new( W->color('chat_prefix_suffix').
						     W->config_get('weechat.look.prefix_suffix')->string.
						    W->color('reset') );
	  $dl{separator_len} = $dl{prefix_align} eq 'none' ? -1 : $dl{separator}->screen_length;
	  $dl{eol_pos} = W->config_get('weechat.look.align_end_of_lines')->string;

	  my $width = $win->{win_chat_width};
	  --$width if $win->{win_chat_width} < $win->{win_width} || (
		  $win->{win_width_pct} < 100
			  && (grep { $_->{win_y} == $win->{win_y} } W->hdata_get('window')->{gui_windows}->_list_full)
				  [-1]{win_x} > $win->{win_x});
	  $dl{l_width} = $width;

	  Hash::Util::lock_keys(%dl);

	  # read marker
	  my $last_read_line = W->config_get('weechat.look.read_marker')->string eq 'line'
		  ? $lines_in->{last_read_line} : '';
	  my $read_marker_always = W->config_get('weechat.look.read_marker_always_show')->boolean;
	  $last_read_line = '' if $last_read_line eq $line && !$read_marker_always;

	  # day changes
	  my $show_day = W->config_get('weechat.look.day_change')->boolean;
	  my $show_day_fmt1 = W->config_get('weechat.look.day_change_message_1date')->string;
	  my $show_day_fmt2 = W->config_get('weechat.look.day_change_message_2dates')->string;

	  my %line_cache;

	  my $ht = $win->{win_chat_height};

	  # find first line. if scrolling, this is directly available
	  my $scroll = $win->{scroll};
	  my $scroll_line = $scroll->{start_line};
	  if ($scroll_line) {
		  my $scroll_line_pos = $scroll->{start_line_pos};
		  if ($scroll_line_pos < 0) { # feature scroll_beyond_end
			  for ($line = $scroll_line->_next; $line && !$line->{data}{displayed}; $line = $line->_next) {}
			  return [] unless $line; # scrolled into empty window
		  }
		  else {
			  $line = $scroll_line;
			  $start_line_pos = $scroll_line_pos;
		  }
	  }
	  else { # search from bottom up
		  my $total = $lines_in->{lines_count};
		  return [] unless $total;

		  my $last_line = 1;
		  my $screen_lines = 0;

		  for (my ($i, $lp) = (0, $line);
			   $lp
				&& $i < $total
				&& $screen_lines < $ht;
			   ++$i,
			     $line = $lp,
			     $lp = $lp->_prev) {

		      if ($lp eq $last_read_line) {
			  if (!$last_line || $read_marker_always) {
			      ++$screen_lines;
			  }
			  else {
			      $last_read_line = '';
			  }
		      }
		      my $next = $lp->_next;
		      if ($show_day && $next
			      && (localtime $lp->{data}{date})[3] != (localtime $next->{data}{date})[3]) {
			  ++$screen_lines;
		      }
		      if ($lp->{data}{displayed}) {
			  $last_line = 0;
			  $line_cache{$lp} = calculate_trace($lp, \%dl);
			  $screen_lines += $line_cache{$lp}{screen_line_count};
		      }
		  }

		  $start_line_pos = $screen_lines - $ht
			  if $screen_lines > $ht;
	  }

	  # my $prev_line;

	  # for ($prev_line = $line->_prev; $prev_line && !$prev_line->{data}{displayed}; $prev_line = $prev_line->_prev) {}
	  # if (!$prev_line && (localtime $line->{data}{date})[3] != (localtime)[3]) {
	  # }

	  my $screen_line = -$start_line_pos;

	  my @lines;

	  # now copy lines from top to down
	  for (; $line &&
			   $screen_line < $ht; $line = $line->_next) {
		  if ($line->{data}{displayed}) {
			  $line_cache{$line} //= calculate_trace($line, \%dl);
			  $screen_line += $line_cache{$line}{screen_line_count};
			  push @lines, $line_cache{$line};
		  }

		  if ($show_day && $line->_next
		  	  && (localtime $line->{data}{date})[3] != (localtime $line->_next->{data}{date})[3]) {
		      push @lines, +{ message =>
		  			  POSIX::strftime(
		  			      POSIX::strftime( $show_day_fmt2 , localtime $line->{data}{date} ),
		  			      localtime $line->_next->{data}{date}
		  			     ),
		  		      prefix => '',
		  		      screen_line_count => 1
		  		     }; # day changed
		      ++$screen_line;
		  }

		  if ($line eq $last_read_line) {
			  my $marker = W->config_get('weechat.look.read_marker_string')->string;
			  my $repeatcount = $win->{win_chat_width} / screen_length($marker);
			  push @lines, +{ message => $marker x $repeatcount,
							  prefix => '',
							  screen_line_count => 1
							 }; # read marker
			  ++$screen_line;
		  }
	  }
	  \@lines
  }
  ## STUBs
  sub selection_function {}
  sub hyperlink_function {}

  ## calculate_trace -- calculate length, layout and metadata for a display line
  ## $lp - line pointer
  ## $dl - layout info from window
  ## returns hashref with line info
  ## STATIC ##
  sub calculate_trace {
	  my ($lp, $dl) = @_;

	  my $time = FormattedString->new( $dl->{show_time} ? $lp->{data}{str_time} : '' );
	  $time->tag(0, 'time_begin'); $time->tag(-1, 'time_end');
	  my $time_len = $time->screen_length;

	  # XXX missing special case:  $win->{win_chat_width} - $screen < 4
	  # `--> ignore all line break rules, just X every screenful
	  my $width = $dl->{l_width};

	  my $msg = FormattedString->new($lp->{data}{message});

	  my $line = 0;
	  my $cursor = 0;

	  my $res = FormattedString->new;
	  if ($time_len) {
	      $res->concat($time, ' ');
	      $cursor += $time_len + 1;
	  }

	  if ($dl->{buffer_len}) {
	      my $buffer = FormattedString->new( $lp->{data}{buffer}{short_name} );
	      my $buffer_len = $buffer->screen_length;

	      my $str;
	      if ($buffer_len <= $dl->{buffer_len}) {
		  $buffer = $buffer->align_str($dl->{prefix_buffer_align},
				     ($dl->{buffer_len} - $buffer_len));
	      }
	      else {
		  $buffer = $buffer->trim_str($dl->{buffer_len});
	      }
	      $res->concat( W->color('chat_prefix_buffer'), $buffer, W->color('reset'), ' ');
	      $cursor += $dl->{buffer_len} + 1;
	  }

	  my $prefix = FormattedString->new( $lp->{data}{prefix} );
	  $prefix->tag(0, 'prefix_begin'); $prefix->tag(-1, 'prefix_end');
	  my $prefix_len = $prefix->screen_length;

	  my $prefix_max_len = $dl->{prefix_align} eq 'none' ? $prefix_len : $dl->{prefix_len};
	  if ($prefix_len <= $prefix_max_len) {
	      $res->concat( $prefix->align_str($dl->{prefix_align}, $prefix_max_len - $prefix_len) );
	  }
	  else {
	      $res->concat( $prefix->trim_str($prefix_max_len) );
	  }
	  if ($prefix_max_len) {
	      $res->concat( ' ' );
	      $cursor += $prefix_max_len + 1;
	  }

	  if ($dl->{separator_len} > 0) {
	      $res->concat( $dl->{separator}, ' ' );
	  }

	  $res->concat( $msg );

	  +{ screen_line_count => 1, message => $res->format }

	  #+{ screen_line_count => 1, message => $lp->{data}{message} }
  }
  sub calc_free_image { [ map { $_->{message} } @{ shift->{lines} } ] }

  ## make_new_copybuf -- creates a new copywin buffer from the current window
  ## $win - window pointer or undef for current window
  ## $args - arguments to command, can set mode
  ## $mouse - if opened by mouse
  ## class method ##
  sub make_new_copybuf {
	  my $class = shift;
	  my ($win, $args, $mouse) = @_;

	  $win //= W->current_window();
	  my $lines = download_lines($win);
	  my $bname = '['.$win->{win_width}.'x'.$win->{win_height}.'+'.$win->{win_x}.'+'.$win->{win_y}.']';
	  my $copybuf = W->buffer_search('perl', $bname) || W->buffer_new($bname, '', '', 'garbage_str', '');
	  return unless $copybuf;

	  $copybuf->{short_name} = W->config_get_plugin('copybuf_short_name');
	  $copybuf->{type} = 'free';

	  my $O = bless +{
		  bufptr => $copybuf,
		  window => +{
			  chat_y => $win->{win_chat_y},
			  chat_x => $win->{win_chat_x},
			  buffer_short_name => $win->{buffer}{short_name},
			 },
		  mode => 'hyperlink',
		  mouse_automode => $mouse,
		  cur => [ (-1)x4 ],
		  lines => $lines,
		 }, $class;

	  Hash::Util::lock_ref_keys_plus($O, qw(link_type_filter));
	  Hash::Util::lock_ref_keys($O->{window});
	  $STR{$copybuf} = $O;

	  $O->hyper_set_type_filter($args);
	  $O->apply_keybindings();
	  $O->switchmode();
	  $O->switchmode() if $args eq '/';

	  $O
  }

  ## refresh -- refresh copywin content on screen
  ## method ##
  sub refresh {
	  my $O = shift;

	  if ($O->{mode} eq 'hyperlink') {
		  $O->hyperlink_function();
	  }
	  elsif ($O->{mode} eq 'selection') {
		  $O->selection_function();
	  }

	  my $printy = $O->calc_free_image();

	  for my $i (0..$#$printy) {
		  $O->bufptr->print_y($i, $printy->[$i]);
	  }

	  $O->bufptr->{display} = 'auto';
  }

  ## mouse_evt -- handle incoming mouse event
  ## $event_type - type of event / down / drag / up
  ## $x - x screen coordinate
  ## $y - y screen coordinate
  ## method ##
  sub mouse_evt {
	  my $O = shift;
	  my ($event_type, $x, $y) = @_;

# 		W->unhook($delayed_nick_menu_timer) if defined $delayed_nick_menu_timer;
# 		$delayed_nick_menu_timer = undef;


	  my $lrow = $y - $O->{window}{chat_y};
	  my $lcol = $x - $O->{window}{chat_x};

  }

  ## bufptr -- access bufptr property of object
  ## method ##
  sub bufptr { shift->{bufptr} }
1}

{ package FormattedString;
  use strict; use warnings;
  use List::Util qw(sum);
  my $strip_col; BEGIN { $strip_col = remove_formatting_re(); }
  my %CharScrLen;
  sub new {
      my $class = shift;
      my $msg = shift // '';
      my (@ctree, @otree);
      my $msg_string = '';
      my $off = 0;
      my $format; # split will return alternating pairs of text and format
      for my $ct (split /($strip_col)/, $msg) {
	  if ($format) {
	      my $r = +{ format => 1, width => 0, data => $ct };
	      push @ctree, $r;
	      push @{$otree[$off]}, $r;
	  }
	  elsif (defined $ct) {
	      Encode::_utf8_on($ct);
	      for my $char ($ct =~ /(\X)/g) {
		  my $r = +{
		      width => $CharScrLen{$char} //= _screen_length($char),
		      data => $char,
		      word => !!($char =~ /\w/),
		      space => !!($char =~ /^\s*$/),
		  };
		  push @ctree, $r;
		  push @{$otree[$off]}, $r;
		  $off += length $char;
	      }
	      $msg_string .= $ct;
	  }
	  $format = !$format;
      }
      bless +{
	  str => $msg_string,
	  ctree => \@ctree,
	  otree => \@otree,
      }, $class;
  }
  sub screen_length {
      sum 0, map { $_->{width} } @{ $_[0]{ctree} }
  }
  sub concat {
      my $class = shift;
      my $self;
      if (ref $class) { ($self, $class) = ($class, ref $class); }
      else { $self = $class->new; }

      my $off = length $self->{str};

      for my $p1 (@_) {
	  my $p2 = ref $p1 ? $p1 : $class->new($p1);
	  $self->{str} .= $p2->{str};
	  push @{$self->{ctree}}, @{$p2->{ctree}};
	  for my $o (@{$p2->{otree}}) {
	      push @{$self->{otree}[$off]}, @{$o} if $o;
	      ++$off;
	  }
      }

      $self
  }
  sub tag {
      my ($str, $pos, $tag) = @_;
      my $r = +{ format => 1, tag => $tag, data => '', width => 0 };
      if ($pos < 0) {
	  push @{$str->{otree}[$pos]}, $r
      }
      else {
	  unshift @{$str->{otree}[$pos]}, $r
      }
      $str
  }
  sub format {
      join '', map { $_->{data} } @{ shift->{ctree} }
  }

  sub align_str {
      my ($str, $left_right, $pad) = @_;
      if ($left_right eq 'right') {
	  FormattedString->new(' ' x $pad)->concat( $str )
      }
      elsif ($left_right eq 'left') {
	  $str->concat( ' ' x $pad )
      }
      else {
	  $str
      }
  }
  sub trim_str {
      my ($str, $width) = @_;
      my $res = FormattedString->new;
      my $off = 0;
      for my $r (@{$str->{ctree}}) {
	  last if $width - $r->{width} < 0;
	  $width -= $r->{width};
	  push @{$res->{ctree}}, $r;
	  push @{$res->{otree}[$off]}, $r;
	  unless ($r->{format}) {
	      $res->{str} .= $r->{data};
	      $off += length $r->{data};
	  }
      }
      $res->concat(' ' x $width) if $width;
      $res
  }
1}

## garbage_str -- remove copywin storage when copywin buffer is closed
## () - signal handler
## $bufptr - signal comes with pointer of closed buffer
sub garbage_str {
	my (undef, $bufptr) = @_;
	delete $STR{$bufptr};
	WC->RC_OK
}

## hsignal_evt -- handle native mouse events
## () - hsignal handler
## $data - focus hash table ref
sub hsignal_evt {
	my $data = $_[2];
	if ($data->{_key} =~ /^(.*)-event-/) { # filter out shadow bindings
		my $msg = '@chat(' . $data->{_buffer_full_name} . '):' . $1;
		for (my $k = W->hdata_get('key')->{gui_keys_mouse}; $k; $k = $k->_next) {
			next if $k->{area_key} =~ /-event/;
			(my $match = '^'.(quotemeta $k->{key})) =~ s/\\\*/.*/g;
			my $close = $msg =~ $match;
			last if $close and $k->{command} =~ /hsignal:@{[SCRIPT_NAME]}/;
			return WC->RC_OK if $close;
		}
	}

	W->na(wb => $data->{window2}, $data->{buffer2});

	my ($BO, $evt);

	# these are absolute coordinates
	if ($data->{_key} =~ /-event-down/) {
		$evt = ' ';
	}
	elsif ($data->{_key} =~ /-event-drag/) {
		$evt = '@';
	}
	else {
		$evt = '#';
	}
	$BO = $STR{$data->{buffer2}} || CopyWin->make_new_copybuf($data->{window2}, undef, 1);
	$BO->mouse_evt($evt, $data->{_x2}, $data->{_y2}) if $BO;
	WC->RC_OK
}

## coords_cmd -- copy buffer command handler
## () - command handler
## $bufptr - current buffer pointer
## $args - arguments
sub coords_cmd {
	my (undef, $bufptr, $args) = @_;
	my $BO;
	#W->print('', ''.remove_formatting_re());
	if ($args =~ s/^\*\*//) { # keybinding forwarder, by convention
		$BO = $STR{$bufptr} || return WC->RC_OK;
		return WC->RC_OK
			unless $BO->input_key($args);
	}
	elsif ($args =~ /^\s*help\s*$/i) {
		Nlib::read_manpage($SCRIPT_FILE, SCRIPT_NAME);
		return WC->RC_OK
	}
	# elsif ($args =~ s/^\s*eval\s+//) {
	# 	W->print('', scalar eval $args);
	# 	return WC->RC_OK
	# }
	else {
		$BO = CopyWin->make_new_copybuf(undef, $args) || return WC->RC_OK;
	}
	$BO->refresh(); # this is called when we didn't return before, i.e. input_key was true
	WC->RC_OK
}

## update list with weechat keybindings
## () - command run handler
## $_[0] - true if should be done by delay
sub update_wee_keys {
    W->unhook($update_wee_keys) if $update_wee_keys;
    if ($_[0]) {
	$update_wee_keys = W->hook_timer(1, 0, 1, 'update_wee_keys', '');
    }
    elsif ($update_wee_keys) {
	@wee_keys = W->_infolist('key');
	$update_wee_keys = undef;
    }
    WC->RC_OK;
}

## stop_coords -- script stop & upgrade handler
## () - stop handler
sub stop_coords {
	for my $k (keys %STR) {
		$STR{$k}->input_key('q');
	}
	WC->RC_OK
}
