shellex.in 11.7 KB
Newer Older
1 2
# vim:ft=perl
#line 3
3 4 5
# shellex - shell based launcher
#   This is the urxvt extension part of shellex.
# © 2013 Axel Wagner and contributors (see also: LICENSE)
6
use X11::Protocol;
7 8
use POSIX qw|ceil|;
use strict;
9

Paul Seyfert's avatar
Paul Seyfert committed
10 11 12 13
# At the time of the original version of this function, the existing
# Randr-modules on CPAN seem to work only barely, so instead we just parse the
# output of xrandr --listactivemonitors. This is an uglyness, that should go
# away some time in the future.
14 15
sub get_outputs {
    my @outputs = ();
Paul Seyfert's avatar
Paul Seyfert committed
16 17 18 19 20 21 22 23 24
    for my $line (qx(xrandr --listactivemonitors)) {
        next if $line =~ /Monitors: /;

        # output looks like:
        #Monitors: 2
        # 0: +*LVDS-1 1366/277x768/156+0+0  LVDS-1
        # 1: +HDMI-2 1920/518x1200/324+1366+0  HDMI-2
        my ($w, $h, $x, $y) = ($line =~ /(\d+)\/\d+x(\d+)\/\d+\+(\d+)\+(\d+)/);
        print "found monitor with dimensions and position w=$w h=$h x=$x y=$y\n";
25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
        push @outputs, { w => $w, h => $h, x => $x, y => $y };
    }
    return @outputs;
}

# This takes a list of outputs and looks up the one, the mouse pointer
# currently is on.
sub geometry_from_ptr {
    my ($self) = @_;

    my @outputs = get_outputs();

    my $ptr = { $self->{X}->QueryPointer($self->DefaultRootWindow) };

    for my $output (@outputs) {
Paul Seyfert's avatar
Paul Seyfert committed
40 41
        if ($output->{x} <= $ptr->{root_x} && $ptr->{root_x} < $output->{x} + $output->{w})
        {
42 43
            $self->{x} = $output->{x};
            if ($self->{bottom}) {
Paul Seyfert's avatar
Paul Seyfert committed
44

45 46 47 48 49 50 51 52 53 54 55
                # The real y-coordinate will change during execution, when the window grows
                $self->{y} = $output->{y} + $output->{h};
            } else {
                $self->{y} = $output->{y};
            }
            $self->{w} = $output->{w};
            $self->{h} = $output->{h};
        }
    }
}

56 57 58 59 60 61 62 63
# Helper, that take a list of numbers and return the max resp. min
sub max {
    my $max = shift;
    while (my $n = shift) {
        $max = $n > $max ? $n : $max;
    }
    return $max;
}
Paul Seyfert's avatar
Paul Seyfert committed
64

65 66 67 68 69 70 71 72
sub min {
    my $min = shift;
    while (my $n = shift) {
        $min = $n < $min ? $n : $min;
    }
    return $min;
}

73 74 75 76 77 78 79 80 81
# This takes a list of outputs and looks up the one, that contains most of the
# window having the input focus currently
sub geometry_from_focus {
    my ($self) = @_;

    my @outputs = get_outputs();

    # Look up the window that currently has the input focus
    my ($focus, $revert) = $self->{X}->GetInputFocus();
82 83 84

    # If the root-window is focused, we fall back to using the pointer-position
    if ($focus == $self->DefaultRootWindow) {
Paul Seyfert's avatar
Paul Seyfert committed
85
        print "Fall back to getting shellex-position from pointer\n";
86 87 88
        return $self->geometry_from_ptr();
    }

89 90
    my $geom = { $self->{X}->GetGeometry($focus) };
    my ($fw, $fh) = ($geom->{width}, $geom->{height});
91 92 93

    print "Focus $focus (${fw}x${fh})\n";

94 95 96
    # The (x,y) coordinates we get are relative to the parent not the
    # root-window. So we just translate the coordinates of the upper-left
    # corner into the coordinate-system of the root-window
Paul Seyfert's avatar
Paul Seyfert committed
97 98
    my (undef, undef, $fx, $fy) =
      $self->{X}->TranslateCoordinates($focus, $self->DefaultRootWindow, 0, 0);
99 100 101 102 103

    # Returns the area (in pixel²) of the intersection of two rectangles.
    # To understand how it works, best draw a picture.
    my $intersection = sub {
        my ($x, $y, $w, $h) = @_;
104 105 106 107 108 109 110
        my $dx;
        if ($x < $fx) {
            $dx = $x + $w - $fx;
        } else {
            $dx = $fx + $fw - $x;
        }
        $dx = max(0, min($dx, $fw, $w));
111

112 113 114 115 116 117 118
        my $dy;
        if ($y < $fy) {
            $dy = $y + $h - $fy;
        } else {
            $dy = $fy + $fh - $y;
        }
        $dy = max(0, min($dy, $fh, $h));
119 120 121 122 123 124 125 126 127 128 129

        return $dx * $dy;
    };

    my $max_area = 0;
    for my $output (@outputs) {
        my $area = $intersection->($output->{x}, $output->{y}, $output->{w}, $output->{h});
        if ($area >= $max_area) {
            $max_area = $area;
            $self->{x} = $output->{x};
            if ($self->{bottom}) {
Paul Seyfert's avatar
Paul Seyfert committed
130

131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163
                # The real y-coordinate will change during execution, when the window grows
                $self->{y} = $output->{y} + $output->{h};
            } else {
                $self->{y} = $output->{y};
            }
            $self->{w} = $output->{w};
            $self->{h} = $output->{h};
        }
    }
}

# This hook is run when the extension is first initialized, before any windows
# are created or mapped. There is not much work we can do here.
sub on_init {
    my ($self) = @_;

    $self->{X} = X11::Protocol->new($self->display_id);

    # Some reasonably sane values in case all our methods to determine a
    # geometry fails.
    $self->{x} = 0;
    $self->{y} = 0;
    $self->{w} = 1024;
    $self->{h} = 768;

    ();
}

# This hook is run after the window is created, but before it is mapped, so
# this is the place to set the geometry to what we want
sub on_start {
    my ($self) = @_;

164 165
    # TODO: Remove compatibility code in future version
    if (defined $self->x_resource("%.edge") || defined $self->x_resource("%.pos")) {
Paul Seyfert's avatar
Paul Seyfert committed
166 167
        print
"WARNING: URxvt.shellex.* resources are deprecated and will be removed in the future. Use shellex.*\n";
168 169 170
    }

    if ($self->x_resource("edge") eq 'bottom' || $self->x_resource("%.edge") eq 'bottom') {
171 172
        print "position should be at the bottom\n";
        $self->{bottom} = 1;
Paul Seyfert's avatar
Paul Seyfert committed
173
        $self->{y}      = $self->{h};
174 175 176 177
    } else {
        print "position should be at the top\n";
    }

178
    if ($self->x_resource("pos") eq 'pointer' || $self->x_resource("%.pos") eq 'pointer') {
179 180 181 182 183 184 185
        print "Getting shellex-position from pointer\n";
        $self->geometry_from_ptr();
    } else {
        print "Getting shellex-position from focused window\n";
        $self->geometry_from_focus();
    }

Paul Seyfert's avatar
Paul Seyfert committed
186 187 188 189 190 191 192 193
# This environment variable is used by the LD_PRELOAD ioctl-override to
# determine the values to send to the shell
# TODO revisit communication protocol (from file to pipe?)
# TODO check if user defined their own SHELLEX_MAX_ROWS, which should be used like
#$ENV{SHELLEX_MAX_ROWS} = $sane_max_rows < $ENV{SHELLEX_MAX_ROWS} ? $sane_max_rows : $ENV{SHELLEX_MAX_ROWS} ;
#
# shellex should leave part of the screen uncovered (10 lines), this assumes
# that a screen will always be larger than 10 lines.
194 195 196 197 198 199 200
    my $sane_max_rows = int($self->{h} / $self->fheight) - 10;

    $ENV{SHELLEX_MAX_ROWS} = $sane_max_rows;
    my $filename = $ENV{SHELLEX_SIZE_FILE};
    open(my $fh, '>', $filename);
    print $fh "$ENV{SHELLEX_MAX_ROWS}\n";
    close $fh;
Paul Seyfert's avatar
Paul Seyfert committed
201
    print "wrote $sane_max_rows as max rows to $filename done\n";
202 203

    $self->{border} = $self->x_resource('internalBorder');
204

205 206 207 208 209 210 211 212
    # the compiled-in default if the resource is not set
    $self->{border} //= 2;

    $self->{border} += $self->x_resource('externalBorder');

    $self->{row_height} = $self->fheight + $self->x_resource('lineSpace');

    my $height = $self->{row_height} + 2 * $self->{border};
Paul Seyfert's avatar
Paul Seyfert committed
213 214
    my $y      = $self->{y};

215
    # Our initial position is different, if we have to be at the bottom
216
    $y -= $height if $self->{bottom};
217

218 219 220 221 222
    $self->XMoveResizeWindow($self->parent, $self->{x}, $y, $self->{w}, $height);

    print "loading config\n";
    $self->tt_write($self->locale_encode("unset LD_PRELOAD\n"));
    $self->tt_write($self->locale_encode(". @SYSCONFDIR@/shellexrc\n"));
223 224 225 226 227 228 229 230 231 232 233 234 235 236

    ();
}

# This hook is run every time a line was changed. We do some resizing here,
# because this catches most cases where we would want to shrink our window.
sub on_line_update {
    my ($self, $row) = @_;
    print "line_update(row = $row)\n";

    # Determine the last row, that is not empty.
    # TODO: Does this work as intended, if there is an empty line in the
    # middle?
    my $nrow = 0;
Paul Seyfert's avatar
Paul Seyfert committed
237
    for my $i ($self->top_row .. $self->nrow - 1) {
238 239 240 241
        if ($self->ROW_l($i) > 0) {
            $nrow++;
        }
    }
242
    $nrow = $nrow > $ENV{SHELLEX_MAX_ROWS} ? $ENV{SHELLEX_MAX_ROWS} : $nrow;
Paul Seyfert's avatar
Paul Seyfert committed
243
    $nrow = $nrow > 0 ? $nrow : 1;
244 245 246 247 248 249
    print "resizing to $nrow\n";

    # If the window is supposed to be at the bottom, we have to move the
    # window up a little bit
    my $y = $self->{y};
    if ($self->{bottom}) {
Paul Seyfert's avatar
Paul Seyfert committed
250
        $y -= 2 + $nrow * $self->fheight;
251 252 253 254 255
    }
    $self->cmd_parse("\e[8;$nrow;t\e[3;$self->{x};${y}t");
    ();
}

256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273
# Predict the number of rows the terminal will have, after adding $string at
# the current position
sub predict_term_size {
    my ($self, $string) = @_;

    my ($row, $col) = $self->screen_cur();
    my $i = $self->top_row;
    my $n = 0;

    # We iterate over all lines and accumulate the number of rows. If the
    # curser is not at the current line, we can just add its number of rows to
    # the total, else we test, if it grows when adding the string and add an
    # according number to the total
    while ($i < $self->nrow) {
        my $line = $self->line($i);
        $i += $line->end - $line->beg + 1;
        unless ($line->beg <= $row && $row <= $line->end) {
            $n += $line->end - $line->beg + 1;
Paul Seyfert's avatar
Paul Seyfert committed
274
            next;
275 276 277 278 279 280
        }

        my $len = ($row - $line->beg) * $self->ncol + $col;

        # Because there might be control-sequences in $string, affecting the
        # number of lines, we need to manually walk it
Paul Seyfert's avatar
Paul Seyfert committed
281 282
        for (my $j = 0 ; $j < length($string) ; $j++) {

283 284 285 286 287 288 289
            # Linebreaks mean the creating of a new line, finishing the old one
            if (substr($string, $j, 1) eq "\n") {
                $len = ($len == 0 ? 1 : $len);
                $n += ceil(($len * 1.0) / $self->ncol);
                $len = 0;
                next;
            }
Paul Seyfert's avatar
Paul Seyfert committed
290

291 292 293 294 295 296 297
            # Carriage-returns mean starting from the beginning. Though the new
            # len does not really have to be 0 (because the text is not
            # actually erased) it is a good enough estimate for now
            if (substr($string, $j, 1) eq "\r") {
                $len = 0;
                next;
            }
Paul Seyfert's avatar
Paul Seyfert committed
298

299 300 301 302 303 304 305
            # We just add one per other char. This actually might not work
            # correctly with wide-chars, but it is a good enough estimate for
            # now
            $len++;
        }
        $n += ceil(($len + 1.0) / $self->ncol);
    }
Paul Seyfert's avatar
Paul Seyfert committed
306
    print "Predicting term size: $n\n";
307 308 309
    return $n;
}

310 311 312 313 314 315 316 317 318
# This hook is run every time before there is text output. We resize here,
# immediately before new lines would be added, which would create scrolling
sub on_add_lines {
    my ($self, $string) = @_;
    my $str = $string;
    $str =~ s/\n/\\n/g;
    $str =~ s/\r/\\r/g;
    print "add_lines(string = \"$str\")\n";

319 320
    my $nrow = $self->predict_term_size($string);
    $nrow = $nrow > $ENV{SHELLEX_MAX_ROWS} ? $ENV{SHELLEX_MAX_ROWS} : $nrow;
Paul Seyfert's avatar
Paul Seyfert committed
321
    $nrow = $nrow > 0 ? $nrow : 1;
322 323 324 325 326 327
    print "resizing to $nrow\n";

    # If the window is supposed to be at the bottom, we have to move the
    # window up a little bit
    my $y = $self->{y};
    if ($self->{bottom}) {
Paul Seyfert's avatar
Paul Seyfert committed
328
        $y -= 2 + $nrow * $self->fheight;
329
    }
330
    $self->cmd_parse("\e[8;$nrow;t\e[3;$self->{x};${y}t");
331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352
    ();
}

# Just for debugging
sub on_size_change {
    my ($self, $nw, $nh) = @_;
    print "size_change($nw, $nh)\n";
    ();
}

sub on_view_change {
    my ($self, $offset) = @_;
    print "view_change(offset = $offset)\n";
    ();
}

sub on_scroll_back {
    my ($self, $lines, $saved) = @_;
    print "scroll_back(lines = $lines, saved = $saved)\n";
    ();
}

353 354 355 356 357 358 359 360 361 362 363
sub on_x_event {
    my ($self, $event) = @_;

    if ($event->{type} == urxvt::EnterNotify) {
        $self->{X}->SetInputFocus($self->parent, 2, $self->{data}{event}{time});
        $self->{X}->GetInputFocus();
    }

    ();
}

364 365 366 367
# This hook is run directly after the window was mapped (= displayed on
# screen). We grab the keyboard here.
sub on_map_notify {
    my ($self, $ev) = @_;
368 369

    $self->{X}->SetInputFocus($self->parent, 2, $self->{data}{event}{time});
Paul Seyfert's avatar
Paul Seyfert committed
370

371 372 373 374
    # We use GetInputFocus as a syncing-mechanism
    $self->{X}->GetInputFocus();

    $self->vt_emask_add(urxvt::EnterWindowMask);
375 376
    ();
}