#!/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