#!/usr/bin/env perl
#
# On-the-fly adjusting of the font size in urxvt
#
# Copyright (c) 2008 David O'Neill
#               2012 Noah K. Tilton <noahktilton@gmail.com>
#               2012-2013 Jan Larres <jan@majutsushi.net>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to
# deal in the Software without restriction, including without limitation the
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
# sell copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
#
# URL: https://github.com/majutsushi/urxvt-font-size
#
# Based on:
# https://github.com/dave0/urxvt-font-size
# https://github.com/noah/urxvt-font
#

#:META:X_RESOURCE:%.step:interger:font size increase/decrease step

=head1 NAME
font-size - interactive font size setter
=head1 USAGE
Put the font-size script into $HOME/.urxvt/ext/ and add it to the list
of enabled perl-extensions in ~/.Xresources:
  URxvt.perl-ext-common: ...,font-size
Add some keybindings:
  URxvt.keysym.C-Up:     font-size:increase
  URxvt.keysym.C-Down:   font-size:decrease
  URxvt.keysym.C-S-Up:   font-size:incglobal
  URxvt.keysym.C-S-Down: font-size:decglobal
Note that for urxvt versions older than 9.21 the resources have to look like this:
  URxvt.keysym.C-Up:     perl:font-size:increase
  URxvt.keysym.C-Down:   perl:font-size:decrease
  URxvt.keysym.C-S-Up:   perl:font-size:incglobal
  URxvt.keysym.C-S-Down: perl:font-size:decglobal
Supported functions:
=over 2
=item * increase/decrease:
      increase or decrease the font size of the current terminal.
=item * incglobal/decglobal:
      same as above and also adjust the X server values so all newly
      started terminals will use the same fontsize.
=item * incsave/decsave:
      same as incglobal/decglobal and also modify the ~/.Xresources
      file so the changed font sizes will persist over a restart of
      the X server or a reboot.
=back
You can also change the step size that the script will use to increase
the font size:
  URxvt.font-size.step: 4
The default step size is 1. This means that with this setting a
size change sequence would be for example 8->12->16->20 instead of
8->9->10->11->12 etc. Please note that many X11 fonts are only
available in specific sizes, though, and odd sizes are often not
available, resulting in an effective step size of 2 instead of 1
in that case.
=cut

use strict;
use warnings;

my %escapecodes = (
    "font"           => 710,
    "boldFont"       => 711,
    "italicFont"     => 712,
    "boldItalicFont" => 713
);

sub on_start
{
    my ($self) = @_;

    $self->{step} = $self->x_resource("%.step") || 1;

    foreach my $type (qw(font boldFont italicFont boldItalicFont)) {
        $self->{$type} = $self->x_resource($type) || "undef";
    }
}

# Needed for backwards compatibility with < 9.21
sub on_user_command
{
    my ($self, $cmd) = @_;

    my $step = $self->{step};

    if ($cmd eq "font-size:increase") {
        fonts_change_size($self,  $step, 0);
    } elsif ($cmd eq "font-size:decrease") {
        fonts_change_size($self, -$step, 0);
    } elsif ($cmd eq "font-size:incglobal") {
        fonts_change_size($self,  $step, 1);
    } elsif ($cmd eq "font-size:decglobal") {
        fonts_change_size($self, -$step, 1);
    } elsif ($cmd eq "font-size:incsave") {
        fonts_change_size($self,  $step, 2);
    } elsif ($cmd eq "font-size:decsave") {
        fonts_change_size($self, -$step, 2);
    } elsif ($cmd eq "font-size:reset") {
        fonts_reset($self);
    }
}

sub on_action
{
    my ($self, $action) = @_;

    my $step = $self->{step};

    if ($action eq "increase") {
        fonts_change_size($self,  $step, 0);
    } elsif ($action eq "decrease") {
        fonts_change_size($self, -$step, 0);
    } elsif ($action eq "incglobal") {
        fonts_change_size($self,  $step, 1);
    } elsif ($action eq "decglobal") {
        fonts_change_size($self, -$step, 1);
    } elsif ($action eq "incsave") {
        fonts_change_size($self,  $step, 2);
    } elsif ($action eq "decsave") {
        fonts_change_size($self, -$step, 2);
    } elsif ($action eq "reset") {
        fonts_reset($self);
    }
}

sub fonts_change_size
{
    my ($term, $change, $save) = @_;

    my @newfonts = ();

    my $curres = $term->resource('font');
    if (!$curres) {
        $term->scr_add_lines("\r\nWarning: No font configured, trying a default.\r\nPlease set a font with the 'URxvt.font' resource.");
        $curres = "fixed";
    }
    my @curfonts = split(/\s*,\s*/, $curres);

    my $basefont = shift(@curfonts);
    my ($newbasefont, $newbasesize) = handle_font($term, $basefont, $change, 0);
    push @newfonts, $newbasefont;

    # Only adjust other fonts if base font changed
    if ($newbasefont ne $basefont) {
        foreach my $font (@curfonts) {
            my ($newfont, $newsize) = handle_font($term, $font, $change, $newbasesize);
            push @newfonts, $newfont;
        }
        my $newres = join(",", @newfonts);
        font_apply_new($term, $newres, "font", $save);

        handle_type($term, "boldFont",       $change, $newbasesize, $save);
        handle_type($term, "italicFont",     $change, $newbasesize, $save);
        handle_type($term, "boldItalicFont", $change, $newbasesize, $save);
    }

    if ($save > 1) {
        # write the new values back to the file
        my $xresources = readlink $ENV{"HOME"} . "/.Xresources";
        system("xrdb -edit " . $xresources);
    }
}

sub fonts_reset
{
    my ($term) = @_;

    foreach my $type (qw(font boldFont italicFont boldItalicFont)) {
        my $initial = $term->{$type};
        if ($initial ne "undef") {
            font_apply_new($term, $initial, $type, 0);
        }
    }
}

sub handle_type
{
    my ($term, $type, $change, $basesize, $save) = @_;

    my $curres = $term->resource($type);
    if (!$curres) {
        return;
    }
    my @curfonts = split(/\s*,\s*/, $curres);
    my @newfonts = ();

    foreach my $font (@curfonts) {
        my ($newfont, $newsize) = handle_font($term, $font, $change, $basesize);
        push @newfonts, $newfont;
    }

    my $newres = join(",", @newfonts);
    font_apply_new($term, $newres, $type, $save);
}

sub handle_font
{
    my ($term, $font, $change, $basesize) = @_;

    my $newfont;
    my $newsize;
    my $prefix = 0;

    if ($font =~ /^\s*x:/) {
        $font =~ s/^\s*x://;
        $prefix = 1;
    }
    if ($font =~ /^\s*(\[.*\])?xft:/) {
        ($newfont, $newsize) = font_change_size_xft($term, $font, $change, $basesize);
    } elsif ($font =~ /^\s*-/) {
        ($newfont, $newsize) = font_change_size_xlfd($term, $font, $change, $basesize);
    } else {
        # check whether the font is a valid alias and if yes resolve it to the
        # actual font
        my $lsfinfo = `xlsfonts -l $font 2>/dev/null`;

        if ($lsfinfo eq "") {
            # not a valid alias, ring the bell if it is the base font and just
            # return the current font
            if ($basesize == 0) {
                $term->scr_bell;
            }
            return ($font, $basesize);
        }

        my $fontinfo = (split(/\n/, $lsfinfo))[-1];
        my ($fontfull) = ($fontinfo =~ /\s+([-a-z0-9]+$)/);
        ($newfont, $newsize) = font_change_size_xlfd($term, $fontfull, $change, $basesize);
    }

    # $term->scr_add_lines("\r\nNew font is $newfont\n");
    if ($prefix) {
        $newfont = "x:$newfont";
    }
    return ($newfont, $newsize);
}

sub font_change_size_xft
{
    my ($term, $fontstring, $change, $basesize) = @_;

    my @pieces   = split(/:/, $fontstring);
    my @resized  = ();
    my $size     = 0;
    my $new_size = 0;

    foreach my $piece (@pieces) {
        if ($piece =~ /^(?:(?:pixel)?size=|[^=-]+-)(\d+(\.\d*)?)$/) {
            $size = $1;

            if ($basesize != 0) {
                $new_size = $basesize;
            } else {
                $new_size = $size + $change
            }

            $piece =~ s/(=|-)$size/$1$new_size/;
        }
        push @resized, $piece;
    }

    my $resized_str = join(":", @resized);

    # don't make fonts too small
    if ($new_size >= 6) {
        return ($resized_str, $new_size);
    } else {
        if ($basesize == 0) {
            $term->scr_bell;
        }
        return ($fontstring, $size);
    }
}

sub font_change_size_xlfd
{
    my ($term, $fontstring, $change, $basesize) = @_;

    #-xos4-terminus-medium-r-normal-*-12-*-*-*-*-*-*-1

    my @fields = qw(foundry family weight slant setwidth style pixelSize pointSize Xresolution Yresolution spacing averageWidth registry encoding);

    my %font;
    $fontstring =~ s/^-//;  # Strip leading - before split
    @font{@fields} = split(/-/, $fontstring);

    if ($font{pixelSize} eq '*') {
        $term->scr_add_lines("\r\nWarning: Font size undefined, assuming 12.\r\nPlease set the 'URxvt.font' resource to a font with a concrete size.");
        $font{pixelSize} = '12'
    }
    if ($font{registry} eq '*') {
        $font{registry} ='iso8859';
    }

    # Blank out the size for the pattern
    my %pattern = %font;
    $pattern{foundry} = '*';
    $pattern{setwidth} = '*';
    $pattern{pixelSize} = '*';
    $pattern{pointSize} = '*';
    # if ($basesize != 0) {
    #     $pattern{Xresolution} = '*';
    #     $pattern{Yresolution} = '*';
    # }
    $pattern{averageWidth} = '*';
    # make sure there are no empty fields
    foreach my $field (@fields) {
        $pattern{$field} = '*' unless defined($pattern{$field});
    }
    my $new_fontstring = '-' . join('-', @pattern{@fields});

    my @possible;
    # $term->scr_add_lines("\r\nPattern is $new_fontstring\n");
    open(FOO, "xlsfonts -fn '$new_fontstring' | sort -u |") or die $!;
    while (<FOO>) {
        chomp;
        s/^-//;  # Strip leading '-' before split
        my @fontdata = split(/-/, $_);

        push @possible, [$fontdata[6], "-$_"];
        # $term->scr_add_lines("\r\npossibly $fontdata[6] $_\n");
    }
    close(FOO);

    if (!@possible) {
        die "No possible fonts!";
    }

    if ($basesize != 0) {
        # sort by font size, descending
        @possible = sort {$b->[0] <=> $a->[0]} @possible;

        # font is not the base font, so find the largest font that is at most
        # as large as the base font. If the largest possible font is smaller
        # than the base font bail and hope that a 0-size font can be found at
        # the end of the function
        if ($possible[0]->[0] > $basesize) {
            foreach my $candidate (@possible) {
                if ($candidate->[0] <= $basesize) {
                    return ($candidate->[1], $candidate->[0]);
                }
            }
        }
    } elsif ($change > 0) {
        # sort by font size, ascending
        @possible = sort {$a->[0] <=> $b->[0]} @possible;

        foreach my $candidate (@possible) {
            if ($candidate->[0] >= $font{pixelSize} + $change) {
                return ($candidate->[1], $candidate->[0]);
            }
        }
    } elsif ($change < 0) {
        # sort by font size, descending
        @possible = sort {$b->[0] <=> $a->[0]} @possible;

        foreach my $candidate (@possible) {
            if ($candidate->[0] <= $font{pixelSize} + $change && $candidate->[0] != 0) {
                return ($candidate->[1], $candidate->[0]);
            }
        }
    }

    # no fitting font available, check whether a 0-size font can be used to
    # fit the size of the base font
    @possible = sort {$a->[0] <=> $b->[0]} @possible;
    if ($basesize != 0 && $possible[0]->[0] == 0) {
        return ($possible[0]->[1], $basesize);
    } else {
        # if there is absolutely no smaller/larger font that can be used
        # return the current one, and beep if this is the base font
        if ($basesize == 0) {
            $term->scr_bell;
        }
        return ("-$fontstring", $font{pixelSize});
    }
}

sub font_apply_new
{
    my ($term, $newfont, $type, $save) = @_;

    # $term->scr_add_lines("\r\nnew font is $newfont\n");

    $term->cmd_parse("\033]" . $escapecodes{$type} . ";" . $newfont . "\033\\");

    # load the xrdb db
    # system("xrdb -load " . X_RESOURCES);

    if ($save > 0) {
        # merge the new values
        open(XRDB_MERGE, "| xrdb -merge") || die "can't fork: $!";
        local $SIG{PIPE} = sub { die "xrdb pipe broken" };
        print XRDB_MERGE "URxvt." . $type . ": " . $newfont;
        close(XRDB_MERGE) || die "bad xrdb: $! $?";
    }
}
