fbrowse_tray.sf 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379
  1. #!/usr/bin/ruby
  2. #
  3. ## A file-browser through a Gtk3 tray status icon.
  4. #
  5. # Translation of:
  6. # https://github.com/trizen/fbrowse-tray
  7. %perl{use Gtk3 -init}
  8. %perl{use File::MimeInfo}
  9. const pkgname = 'fbrowse-tray'
  10. const version = 0.07
  11. const ICON_THEME = %S<Gtk3::IconTheme>.get_default
  12. var (
  13. FILE_MANAGER = (ENV{:FILEMANAGER} || 'pcmanfm'),
  14. TERMINAL = (ENV{:TERM} || 'xterm'),
  15. ICON_SIZE = 'menu',
  16. STATUS_ICON = 'file-manager',
  17. EXT_MIMETYPE = false,
  18. TOOLTIP_PATH = false,
  19. FILES_FIRST = false,
  20. DIRS_ONLY = false,
  21. )
  22. func add_content() { }
  23. # -------------------------------------------------------------------------------------
  24. func open_file(file) {
  25. Sys.run("#{FILE_MANAGER} #{file.escape} &")
  26. }
  27. # -------------------------------------------------------------------------------------
  28. func add_header(menu, dir) {
  29. # Append 'Open directory'
  30. var open_dir = %O<Gtk3::ImageMenuItem>.new("Open directory")
  31. open_dir.set_image(%O<Gtk3::Image>.new_from_icon_name('folder-open', ICON_SIZE));
  32. open_dir.signal_connect(activate => { open_file(dir) })
  33. menu.append(open_dir)
  34. # Add 'Open in terminal'
  35. var open_term = %O<Gtk3::ImageMenuItem>.new("Open in terminal")
  36. open_term.set_image(%O<Gtk3::Image>.new_from_icon_name('utilities-terminal', ICON_SIZE))
  37. open_term.signal_connect('activate' => { Sys.run("cd #{dir.escape}; #{TERMINAL} &") })
  38. menu.append(open_term)
  39. return true
  40. }
  41. # Add content of a directory as a submenu for an item
  42. func create_submenu(item, dir) {
  43. # Create a new menu
  44. var menu = %O<Gtk3::Menu>.new
  45. # Add 'Browse here...'
  46. add_header(menu, dir)
  47. # Append an horizontal separator
  48. menu.append(%O<Gtk3::SeparatorMenuItem>.new)
  49. # Add the dir content in this new menu
  50. add_content(menu, dir)
  51. # Set submenu for item to this new menu
  52. item.set_submenu(menu)
  53. # Make menu content visible
  54. menu.show_all
  55. return true
  56. }
  57. # -------------------------------------------------------------------------------------
  58. # Append a directory to a submenu
  59. func append_dir(submenu, dirname, dir) {
  60. # Create the dir submenu
  61. var dirmenu = %O<Gtk3::Menu>.new
  62. # Create a new menu item
  63. var item = %O<Gtk3::ImageMenuItem>.new(dirname)
  64. # Set icon
  65. item.set_image(%O<Gtk3::Image>.new_from_icon_name('folder', ICON_SIZE))
  66. # Set a signal
  67. item.signal_connect(activate => { create_submenu(item, dir); dirmenu.destroy })
  68. # Set the submenu to the entry item
  69. item.set_submenu(dirmenu)
  70. # Append the item to the submenu
  71. submenu.append(item)
  72. return true
  73. }
  74. # -------------------------------------------------------------------------------------
  75. # Returns true if a given icon exists in the current icon-theme
  76. func is_icon_valid(icon) is cached {
  77. ICON_THEME.has_icon(icon)
  78. }
  79. # Returns a valid icon name based on file's mime-type
  80. func file_icon(filename, file) {
  81. static alias = Hash()
  82. var mime_type = (
  83. (
  84. (
  85. EXT_MIMETYPE ? [%S<File::MimeInfo>.globs(filename)][0]
  86. : %S<File::MimeInfo>.mimetype(file)
  87. ) \\ return 'unknown'
  88. ).gsub('/', '-')
  89. )
  90. alias.contains(mime_type) ->
  91. && return alias{mime_type}
  92. do {
  93. var type = mime_type
  94. static re = /.*\K[[:punct:]]\w++$/
  95. loop {
  96. if (is_icon_valid(type)) {
  97. return (alias{mime_type} = type)
  98. }
  99. elsif (is_icon_valid("gnome-mime-#{type}")) {
  100. return (alias{mime_type} = "gnome-mime-#{type}")
  101. }
  102. type.match(re) ? type.gsub!(re) : break
  103. }
  104. }
  105. {
  106. var type = mime_type
  107. static re = /^application-x-\K.*?-/
  108. loop {
  109. type.match(re) ? type.gsub!(re) : break
  110. if (is_icon_valid(type)) {
  111. return (alias{mime_type} = type)
  112. }
  113. }
  114. }
  115. alias{mime_type} = 'unknown'
  116. }
  117. # -------------------------------------------------------------------------------------
  118. # File action
  119. func file_actions(obj, event, file) {
  120. if ((event.button == 1) || (event.button == 2)) {
  121. open_file(file); # open the file
  122. if (event.button == 1) {
  123. return false # hide the menu when left-clicked
  124. }
  125. return true # keep the menu when middle-clicked
  126. }
  127. # Right-click menu
  128. var menu = %O<Gtk3::Menu>.new
  129. # Open
  130. var open = %O<Gtk3::ImageMenuItem>.new('Open')
  131. # Set icon
  132. open.set_image(%O<Gtk3::Image>.new_from_icon_name('gtk-open', ICON_SIZE))
  133. # Set a signal (activates on click)
  134. open.signal_connect(activate => { open_file(file) })
  135. # Append the item to the menu
  136. menu.append(open)
  137. # Delete
  138. var delete = %O<Gtk3::ImageMenuItem>.new('Delete')
  139. # Set icon
  140. delete.set_image(%O<Gtk3::Image>.new_from_icon_name('gtk-delete', ICON_SIZE))
  141. # Set a signal (activates on click)
  142. delete.signal_connect(activate => { File.delete(file) && obj.destroy })
  143. # Append the item to the menu
  144. menu.append(delete)
  145. # Show menu
  146. menu.show_all
  147. menu.popup(nil, nil, nil, [1, 1], 0, 0)
  148. return true # don't hide the main menu
  149. }
  150. # -------------------------------------------------------------------------------------
  151. # Append a file to a submenu
  152. func append_file(submenu, filename, file) {
  153. # Create a new menu item
  154. var item = %O<Gtk3::ImageMenuItem>.new(filename)
  155. # Set icon
  156. item.set_image(%O<Gtk3::Image>.new_from_icon_name(file_icon(filename, file), ICON_SIZE))
  157. # Set tooltip
  158. TOOLTIP_PATH && item.set_property('tooltip_text', file)
  159. # Set a signal (activates on click)
  160. item.signal_connect('button-release-event' => func(obj, event) { file_actions(obj, event, file) })
  161. # Append the item to the submenu
  162. submenu.append(item)
  163. return true
  164. }
  165. # -------------------------------------------------------------------------------------
  166. # Read a content directory and add it to a submenu
  167. add_content = func(submenu, dir) {
  168. var dirs = []
  169. var files = []
  170. Dir.open(dir, \var dir_h) || return nil
  171. struct Entry {
  172. String name,
  173. File path,
  174. }
  175. dir_h.each { |filename|
  176. # Ignore hidden files
  177. filename.begins_with('.') && next
  178. # Join directory with the filename
  179. var path = File(dir, filename)
  180. path.exists || (path = Dir(dir, filename))
  181. # Resolve absolute path
  182. if (path.is_link) {
  183. path.abs_path!
  184. path.exists || next
  185. }
  186. # Ignore non-directories (with -d)
  187. if (DIRS_ONLY) {
  188. path.is_dir || next
  189. }
  190. # Collect the files and dirs
  191. (path.is_dir ? dirs : files) << Entry(filename.gsub('_', '__'), path)
  192. }
  193. dir_h.close
  194. struct Entries {
  195. Array content,
  196. Block function,
  197. }
  198. var categories = [Entries(dirs, append_dir),
  199. Entries(files, append_file)]
  200. for category in (FILES_FIRST ? categories.reverse : categories) {
  201. category.content.sort_by { .name.fc }.each { |entry|
  202. var label = entry.name
  203. if (label.len > 64) {
  204. label = (label.first(32) + '⋯' + label.last(32))
  205. }
  206. category.function.call(submenu, label, entry.path)
  207. }
  208. }
  209. return true
  210. }
  211. # -------------------------------------------------------------------------------------
  212. # Create the main menu and populate it with the content of $dir
  213. func create_main_menu(icon, dir, event) {
  214. var menu = %O<Gtk3::Menu>.new
  215. if (event.button == 1) {
  216. add_content(menu, dir)
  217. }
  218. elsif (event.button == 3) {
  219. # Create a new menu item
  220. var exit = %O<Gtk3::ImageMenuItem>.new('Quit')
  221. # Set icon
  222. exit.set_image(%O<Gtk3::Image>.new_from_icon_name('application-exit', ICON_SIZE))
  223. # Set a signal (activates on click)
  224. exit.signal_connect(activate => { %O<Gtk3>.main_quit })
  225. # Append the item to the menu
  226. menu.append(exit)
  227. }
  228. menu.show_all
  229. menu.popup(nil, nil, { %S<Gtk3::StatusIcon>.position_menu(menu, 0, 0, icon) }, [1, 1], 0, 0)
  230. return true
  231. }
  232. # -------------------------------------------------------------------------------------
  233. #
  234. ## Main
  235. #
  236. func usage(code=0) {
  237. var main = File(__MAIN__).basename
  238. print <<"USAGE"
  239. usage: #{main} [options] [dir]
  240. options:
  241. -r : order files before directories
  242. -d : display only directories
  243. -T : set the path of files as tooltips
  244. -e : get the mimetype by extension only (faster)
  245. -i [name] : name of the status icon (default: #{STATUS_ICON})
  246. -f [command] : command to open the files with (default: #{FILE_MANAGER})
  247. -t [command] : terminal command for "Open in terminal" (default: #{TERMINAL})
  248. -m [type] : type of menu icons (default: #{ICON_SIZE})
  249. more: dnd, dialog, button, small-toolbar, large-toolbar
  250. example:
  251. #{main} -f thunar -m dnd /my/dir
  252. USAGE
  253. Sys.exit(code)
  254. }
  255. func output_version {
  256. say "#{pkgname} #{version}"
  257. Sys.exit(0)
  258. }
  259. ARGV.getopt!(
  260. 'd!' => \DIRS_ONLY,
  261. 'T!' => \TOOLTIP_PATH,
  262. 'r!' => \FILES_FIRST,
  263. 'e!' => \EXT_MIMETYPE,
  264. 'i=s' => \STATUS_ICON,
  265. 'f=s' => \FILE_MANAGER,
  266. 't=s' => \TERMINAL,
  267. 'm=s' => \ICON_SIZE,
  268. 'h' => usage,
  269. 'v' => output_version,
  270. )
  271. var dir = Dir(ARGV.shift)
  272. dir.exists || usage(2)
  273. var icon = %O<Gtk3::StatusIcon>.new
  274. icon.set_from_icon_name(STATUS_ICON)
  275. icon.set_visible(true)
  276. icon.signal_connect('button-release-event' => func(_, event) { create_main_menu(icon, dir, event) })
  277. %O<Gtk3>.main