#!/usr/bin/perl
#
# Copyright (C) 2014-2017 Daniel "Trizen" Șuteu <echo dHJpemVueEBnbWFpbC5jb20K | base64 -d>.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

# License: GPLv3
# Created on: 15 May 2014
# Latest edit: 12 September 2017
# http://github.com/trizen/fbrowse-tray

# -----------------------------------------------
# A file-browser through a Gtk2 tray status icon.
# -----------------------------------------------

use utf8;
use 5.016;
use strict;
use warnings;

use Gtk2 qw(-init);
use File::MimeInfo qw();    # File::MimeInfo::Magic is better, but slower...
use Encode qw(decode_utf8);

my $pkgname = 'fbrowse-tray';
my $version = 0.08;

my %opt = (
           m => 'menu',
           i => 'system-file-manager',
           f => $ENV{FILEMANAGER} || 'pcmanfm',
           t => $ENV{TERM} || 'xterm',
          );

sub usage {
    my ($code) = @_;
    print <<"USAGE";
usage: $0 [options] [dir]

options:
    -r            : order files before directories
    -d            : display only directories
    -T            : set the path of files as tooltips
    -e            : get the mimetype by extension only (faster)
    -i [name]     : name of the status icon (default: $opt{i})
    -f [command]  : command to open the files with (default: $opt{f})
    -t [command]  : terminal command for "Open in terminal" (default: $opt{t})
    -m [type]     : type of menu icons (default: $opt{m})
                    more: dnd, dialog, button, small-toolbar, large-toolbar

example:
    $0 -f thunar -t xterm -m dnd /my/dir
USAGE
    exit($code // 0);
}

sub version {
    print "$pkgname $version\n";
    exit 0;
}

# Parse arguments
if (@ARGV && chr ord $ARGV[0] eq '-') {
    require Getopt::Std;
    Getopt::Std::getopts('Ti:m:f:t:drehv', \%opt)
      || die "Error in command-line arguments!";
    $opt{h} && usage(0);
    $opt{v} && version();
}

@ARGV == 1 || usage(2);

# Cache the current icon theme
$opt{icon_theme} = 'Gtk2::IconTheme'->get_default;

#
## Main menu
#
{
    my $dir  = decode_utf8(shift @ARGV);
    my $icon = 'Gtk2::StatusIcon'->new;
    $icon->set_from_icon_name($opt{i});
    $icon->set_visible(1);
    $icon->signal_connect('button-release-event' => sub { create_main_menu($icon, $dir, $_[1]) });
    'Gtk2'->main;
}

# -------------------------------------------------------------------------------------

sub add_header {
    my ($menu, $dir) = @_;

    # Add 'Open directory'
    my $open_dir = 'Gtk2::ImageMenuItem'->new("Open directory");
    $open_dir->set_image('Gtk2::Image'->new_from_icon_name('folder-open', $opt{m}));
    $open_dir->signal_connect('activate' => sub { open_file($dir) });
    $menu->append($open_dir);

    # Add 'Open in terminal'
    my $open_term = 'Gtk2::ImageMenuItem'->new("Open in terminal");
    $open_term->set_image('Gtk2::Image'->new_from_icon_name('utilities-terminal', $opt{m}));
    $open_term->signal_connect('activate' => sub { system "cd \Q$dir\E; $opt{t} &" });
    $menu->append($open_term);

    return 1;
}

# Add content of a directory as a submenu for an item
sub create_submenu {
    my ($item, $dir) = @_;

    # Create a new menu
    my $menu = 'Gtk2::Menu'->new;

    # Add menu header
    add_header($menu, $dir);

    # Append an horizontal separator
    $menu->append('Gtk2::SeparatorMenuItem'->new);

    # Add the dir content in this new menu
    add_content($menu, $dir);

    # Set submenu for item to this new menu
    $item->set_submenu($menu);

    # Make menu content visible
    $menu->show_all;

    return 1;
}

# -------------------------------------------------------------------------------------

# Append a directory to a submenu
sub append_dir {
    my ($submenu, $dirname, $dir) = @_;

    # Create the dir submenu
    my $dirmenu = 'Gtk2::Menu'->new;

    # Create a new menu item
    my $item = 'Gtk2::ImageMenuItem'->new($dirname);

    # Set icon
    $item->set_image('Gtk2::Image'->new_from_icon_name('folder', $opt{m}));

    # Set a signal
    $item->signal_connect('activate' => sub { create_submenu($item, $dir) });

    # Set the submenu to the entry item
    $item->set_submenu($dirmenu);

    # Append the item to the submenu
    $submenu->append($item);

    return 1;
}

# -------------------------------------------------------------------------------------

# Returns true if a given icon exists in the current icon-theme
sub is_icon_valid {
    state %mem;
    $mem{$_[0]} //= $opt{icon_theme}->has_icon($_[0]);
}

# Returns a valid icon name for a given file, based on its mimetype
sub file_icon {
    my ($filename, $file) = @_;

    state %alias;
    my $mime_type = (
                     (
                      $opt{e}
                      ? File::MimeInfo::globs($filename)
                      : File::MimeInfo::mimetype($file)
                     ) // return 'unknown'
                    ) =~ tr|/|-|r;

    exists($alias{$mime_type})
      && return $alias{$mime_type};

    {
        my $type = $mime_type;
        while (1) {
            if (is_icon_valid($type)) {
                return ($alias{$mime_type} = $type);
            }
            elsif (is_icon_valid("gnome-mime-$type")) {
                return ($alias{$mime_type} = "gnome-mime-$type");
            }
            $type =~ s{.*\K[[:punct:]]\w++$}{} || last;
        }
    }

    {
        my $type = $mime_type;
        while (1) {
            $type =~ s{^application-x-\K.*?-}{} || last;
            if (is_icon_valid($type)) {
                return ($alias{$mime_type} = $type);
            }
        }
    }

    $alias{$mime_type} = 'unknown';
}

# -------------------------------------------------------------------------------------

# Open file
sub open_file {
    my ($file) = @_;
    system "$opt{f} \Q$file\E &";
}

# -------------------------------------------------------------------------------------

# File action
sub file_actions {
    my ($obj, $event, $file) = @_;

    if ($event->button == 1 or $event->button == 2) {

        open_file($file);    # open the file

        if ($event->button == 1) {
            return 0;        # hide the menu when left-clicked
        }

        return 1;            # keep the menu when middle-clicked
    }

    # Right-click menu
    my $menu = 'Gtk2::Menu'->new;

    # Open
    my $open = 'Gtk2::ImageMenuItem'->new('Open');

    # Set icon
    $open->set_image('Gtk2::Image'->new_from_icon_name('gtk-open', $opt{m}));

    # Set a signal (activates on click)
    $open->signal_connect('activate' => sub { open_file($file) });

    # Append the item to the menu
    $menu->append($open);

    # Delete
    my $delete = 'Gtk2::ImageMenuItem'->new('Delete');

    # Set icon
    $delete->set_image('Gtk2::Image'->new_from_icon_name('gtk-delete', $opt{m}));

    # Set a signal (activates on click)
    $delete->signal_connect('activate' => sub { unlink($file) and $obj->destroy });

    # Append the item to the menu
    $menu->append($delete);

    # Show menu
    $menu->show_all;
    $menu->popup(undef, undef, undef, [1, 1], 0, 0);

    return 1;    # don't hide the main menu
}

# -------------------------------------------------------------------------------------

# Append a file to a submenu
sub append_file {
    my ($submenu, $filename, $file) = @_;

    # Create a new menu item
    my $item = 'Gtk2::ImageMenuItem'->new($filename);

    # Set icon
    $item->set_image('Gtk2::Image'->new_from_icon_name(file_icon($filename, $file), $opt{m}));

    # Set tooltip
    $opt{T} && $item->set_property('tooltip_text', $file);

    # Set a signal (activates on click)
    $item->signal_connect('button-release-event' => sub { file_actions(@_, $file) });

    # Append the item to the submenu
    $submenu->append($item);

    return 1;
}

# -------------------------------------------------------------------------------------

# Resolve link
sub resolvelink {
    my ($file) = @_;
    my $link = readlink($file) // return;

    require File::Spec;
    my (undef, $dir, undef) = File::Spec->splitpath($file);
    $link = File::Spec->rel2abs($link, $dir);
    $link = __SUB__->($link) if -l $link;    # recurs

    $link;
}

# -------------------------------------------------------------------------------------

# Read a content directory and add it to a submenu
sub add_content {
    my ($submenu, $dir) = @_;

    my (@dirs, @files);
    opendir(my $dir_h, $dir) or return;
    while (defined(my $filename = readdir($dir_h))) {

        # Ignore hidden files
        next if chr ord $filename eq '.';

        # UTF-8 decode the filename
        $filename = decode_utf8($filename);

        # Join directory with filename
        my $abs_path = "$dir/$filename";

        # Readlink
        if (-l $abs_path) {
            $abs_path = resolvelink($abs_path) // next;
            -e $abs_path or next;
        }

        # Ignore non-directories (with -d)
        if ($opt{d}) {
            (-d _) || next;
        }

        # Collect the files and dirs
        push @{(-d _) ? \@dirs : \@files}, [$filename =~ s/_/__/gr, $abs_path];
    }
    closedir $dir_h;

    my @calls = ([\&append_dir => \@dirs], [\&append_file => \@files]);
    foreach my $call ($opt{r} ? reverse(@calls) : @calls) {
        for my $pair (sort { fc($a->[0]) cmp fc($b->[0]) } @{$call->[1]}) {

            my $label = $pair->[0];

            if (length($label) > 64) {
                $label = substr($label, 0, 32) . '⋯' . substr($label, -32);
            }

            $call->[0]->($submenu, '_' . $label, $pair->[1]);
        }
    }

    return 1;
}

# -------------------------------------------------------------------------------------

# Create the main menu and populate it with the content of $dir
sub create_main_menu {
    my ($icon, $dir, $event) = @_;

    my $menu = 'Gtk2::Menu'->new;

    if ($event->button == 1) {
        add_content($menu, $dir);
    }
    elsif ($event->button == 3) {

        # Create a new menu item
        my $exit = 'Gtk2::ImageMenuItem'->new('Quit');

        # Set icon
        $exit->set_image('Gtk2::Image'->new_from_icon_name('exit', $opt{m}));

        # Set a signal (activates on click)
        $exit->signal_connect('activate' => sub { 'Gtk2'->main_quit() });

        # Append the item to the menu
        $menu->append($exit);
    }

    $menu->show_all;
    $menu->popup(undef, undef, sub { Gtk2::StatusIcon::position_menu($menu, 0, 0, $icon) }, [1, 1], 0, 0);

    return 1;
}

# -------------------------------------------------------------------------------------
