#!/usr/bin/ruby

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

# Translation of:
#   https://github.com/trizen/fbrowse-tray

%perl{use Gtk3 -init}
%perl{use File::MimeInfo}

const pkgname = 'fbrowse-tray'
const version = 0.07

const ICON_THEME = %S<Gtk3::IconTheme>.get_default

var (
    FILE_MANAGER = (ENV{:FILEMANAGER} || 'pcmanfm'),
    TERMINAL     = (ENV{:TERM}        || 'xterm'),
    ICON_SIZE    = 'menu',
    STATUS_ICON  = 'file-manager',
    EXT_MIMETYPE = false,
    TOOLTIP_PATH = false,
    FILES_FIRST  = false,
    DIRS_ONLY    = false,
)

func add_content() { }

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

func open_file(file) {
    Sys.run("#{FILE_MANAGER} #{file.escape} &")
}

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

func add_header(menu, dir) {

    # Append 'Open directory'
    var open_dir = %O<Gtk3::ImageMenuItem>.new("Open directory")
    open_dir.set_image(%O<Gtk3::Image>.new_from_icon_name('folder-open', ICON_SIZE));
    open_dir.signal_connect(activate => { open_file(dir) })
    menu.append(open_dir)

    # Add 'Open in terminal'
    var open_term = %O<Gtk3::ImageMenuItem>.new("Open in terminal")
    open_term.set_image(%O<Gtk3::Image>.new_from_icon_name('utilities-terminal', ICON_SIZE))
    open_term.signal_connect('activate' => { Sys.run("cd #{dir.escape}; #{TERMINAL} &") })
    menu.append(open_term)

    return true
}

# Add content of a directory as a submenu for an item
func create_submenu(item, dir) {

    # Create a new menu
    var menu = %O<Gtk3::Menu>.new

    # Add 'Browse here...'
    add_header(menu, dir)

    # Append an horizontal separator
    menu.append(%O<Gtk3::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 true
}

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

# Append a directory to a submenu
func append_dir(submenu, dirname, dir) {

    # Create the dir submenu
    var dirmenu = %O<Gtk3::Menu>.new

    # Create a new menu item
    var item = %O<Gtk3::ImageMenuItem>.new(dirname)

    # Set icon
    item.set_image(%O<Gtk3::Image>.new_from_icon_name('folder', ICON_SIZE))

    # Set a signal
    item.signal_connect(activate => { create_submenu(item, dir); dirmenu.destroy })

    # Set the submenu to the entry item
    item.set_submenu(dirmenu)

    # Append the item to the submenu
    submenu.append(item)

    return true
}

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

# Returns true if a given icon exists in the current icon-theme
func is_icon_valid(icon) is cached {
    ICON_THEME.has_icon(icon)
}

# Returns a valid icon name based on file's mime-type
func file_icon(filename, file) {

    static alias = Hash()
    var mime_type = (
            (
             (
                EXT_MIMETYPE ? [%S<File::MimeInfo>.globs(filename)][0]
                             : %S<File::MimeInfo>.mimetype(file)
              ) \\ return 'unknown'
            ).gsub('/', '-')
    )

    alias.contains(mime_type) ->
        && return alias{mime_type}

    do {
        var type = mime_type
        static re = /.*\K[[:punct:]]\w++$/
        loop {
            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.match(re) ? type.gsub!(re) : break
        }
    }

    {
        var type = mime_type
        static re = /^application-x-\K.*?-/
        loop {
            type.match(re) ? type.gsub!(re) : break
            if (is_icon_valid(type)) {
                return (alias{mime_type} = type)
            }
        }
    }

    alias{mime_type} = 'unknown'
}

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

# File action
func file_actions(obj, event, file) {
    if ((event.button == 1) || (event.button == 2)) {

        open_file(file);    # open the file

        if (event.button == 1) {
            return false    # hide the menu when left-clicked
        }

        return true        # keep the menu when middle-clicked
    }

    # Right-click menu
    var menu = %O<Gtk3::Menu>.new

    # Open
    var open = %O<Gtk3::ImageMenuItem>.new('Open')

    # Set icon
    open.set_image(%O<Gtk3::Image>.new_from_icon_name('gtk-open', ICON_SIZE))

    # Set a signal (activates on click)
    open.signal_connect(activate => { open_file(file) })

    # Append the item to the menu
    menu.append(open)

    # Delete
    var delete = %O<Gtk3::ImageMenuItem>.new('Delete')

    # Set icon
    delete.set_image(%O<Gtk3::Image>.new_from_icon_name('gtk-delete', ICON_SIZE))

    # Set a signal (activates on click)
    delete.signal_connect(activate => { File.delete(file) && obj.destroy })

    # Append the item to the menu
    menu.append(delete)

    # Show menu
    menu.show_all
    menu.popup(nil, nil, nil, [1, 1], 0, 0)

    return true    # don't hide the main menu
}

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

# Append a file to a submenu
func append_file(submenu, filename, file) {

    # Create a new menu item
    var item = %O<Gtk3::ImageMenuItem>.new(filename)

    # Set icon
    item.set_image(%O<Gtk3::Image>.new_from_icon_name(file_icon(filename, file), ICON_SIZE))

    # Set tooltip
    TOOLTIP_PATH && item.set_property('tooltip_text', file)

    # Set a signal (activates on click)
    item.signal_connect('button-release-event' => func(obj, event) { file_actions(obj, event, file) })

    # Append the item to the submenu
    submenu.append(item)

    return true
}

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

# Read a content directory and add it to a submenu
add_content = func(submenu, dir) {

    var dirs = []
    var files = []

    Dir.open(dir, \var dir_h) || return nil

    struct Entry {
        String name,
        File path,
    }

    dir_h.each { |filename|

        # Ignore hidden files
        filename.begins_with('.') && next

        # Join directory with the filename
        var path = File(dir, filename)
        path.exists || (path = Dir(dir, filename))

        # Resolve absolute path
        if (path.is_link) {
            path.abs_path!
            path.exists || next
        }

        # Ignore non-directories (with -d)
        if (DIRS_ONLY) {
            path.is_dir || next
        }

        # Collect the files and dirs
        (path.is_dir ? dirs : files) << Entry(filename.gsub('_', '__'), path)
    }
    dir_h.close

    struct Entries {
        Array content,
        Block function,
    }

    var categories = [Entries(dirs, append_dir),
                      Entries(files, append_file)]

    for category in (FILES_FIRST ? categories.reverse : categories) {
        category.content.sort_by { .name.fc }.each { |entry|

            var label = entry.name

            if (label.len > 64) {
                label = (label.first(32) + '⋯' + label.last(32))
            }

            category.function.call(submenu, label, entry.path)
        }
    }

    return true
}

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

# Create the main menu and populate it with the content of $dir
func create_main_menu(icon, dir, event) {

    var menu = %O<Gtk3::Menu>.new

    if (event.button == 1) {
        add_content(menu, dir)
    }
    elsif (event.button == 3) {

        # Create a new menu item
        var exit = %O<Gtk3::ImageMenuItem>.new('Quit')

        # Set icon
        exit.set_image(%O<Gtk3::Image>.new_from_icon_name('application-exit', ICON_SIZE))

        # Set a signal (activates on click)
        exit.signal_connect(activate => { %O<Gtk3>.main_quit })

        # Append the item to the menu
        menu.append(exit)
    }

    menu.show_all
    menu.popup(nil, nil, { %S<Gtk3::StatusIcon>.position_menu(menu, 0, 0, icon) }, [1, 1], 0, 0)

    return true
}

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

#
## Main
#

func usage(code=0) {
    var main = File(__MAIN__).basename
    print <<"USAGE"
usage: #{main} [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: #{STATUS_ICON})
    -f [command]  : command to open the files with (default: #{FILE_MANAGER})
    -t [command]  : terminal command for "Open in terminal" (default: #{TERMINAL})
    -m [type]     : type of menu icons (default: #{ICON_SIZE})
                    more: dnd, dialog, button, small-toolbar, large-toolbar

example:
    #{main} -f thunar -m dnd /my/dir
USAGE
    Sys.exit(code)
}

func output_version {
    say "#{pkgname} #{version}"
    Sys.exit(0)
}

ARGV.getopt!(
    'd!'  => \DIRS_ONLY,
    'T!'  => \TOOLTIP_PATH,
    'r!'  => \FILES_FIRST,
    'e!'  => \EXT_MIMETYPE,
    'i=s' => \STATUS_ICON,
    'f=s' => \FILE_MANAGER,
    't=s' => \TERMINAL,
    'm=s' => \ICON_SIZE,

    'h' => usage,
    'v' => output_version,
)

var dir = Dir(ARGV.shift)
dir.exists || usage(2)

var icon = %O<Gtk3::StatusIcon>.new
icon.set_from_icon_name(STATUS_ICON)
icon.set_visible(true)
icon.signal_connect('button-release-event' => func(_, event) { create_main_menu(icon, dir, event) })
%O<Gtk3>.main
