123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379 |
- #!/usr/bin/ruby
- #
- ## A file-browser through a Gtk3 tray status icon.
- #
- # Translation of:
- # https://github.com/trizen/fbrowse-tray
- use('Gtk3 -init')
- 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
|