obmenu-generator 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541
  1. #!/usr/bin/perl
  2. # Copyright (C) 2011-2014 Daniel "Trizen" Șuteu <echo dHJpemVueEBnbWFpbC5jb20K | base64 -d>.
  3. #
  4. # This program is free software: you can redistribute it and/or modify
  5. # it under the terms of the GNU General Public License as published by
  6. # the Free Software Foundation, either version 3 of the License, or
  7. # (at your option) any later version.
  8. #
  9. # This program is distributed in the hope that it will be useful,
  10. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. # GNU General Public License for more details.
  13. #
  14. # You should have received a copy of the GNU General Public License
  15. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  16. # Openbox Menu Generator
  17. # A fast pipe/static menu generator for the Openbox Window Manager.
  18. # Program: obmenu-generator
  19. # License: GPLv3
  20. # Created on: 25 March 2011
  21. # Latest edit on: 26 March 2014
  22. # Websites: http://trizen.googlecode.com
  23. # http://trizenx.blogspot.com
  24. #use 5.014;
  25. #use strict;
  26. #use warnings;
  27. require Linux::DesktopFiles;
  28. $Linux::DesktopFiles::VERSION >= 0.08
  29. || die "Update Linux::DesktopFiles to a newer version! (requires >=0.08)\n";
  30. my $pkgname = 'obmenu-generator';
  31. my $version = 0.59;
  32. our ($CONFIG, $SCHEMA);
  33. my $output_h = *STDOUT;
  34. my ($pipe, $static, $icons, $reconfigure, $stdout_config, $update_config);
  35. my $home_dir =
  36. $ENV{HOME}
  37. || $ENV{LOGDIR}
  38. || (getpwuid($<))[7]
  39. || `echo -n ~`;
  40. my $xdg_config_home = "$home_dir/.config";
  41. my $menu_id = "root-menu";
  42. my $config_dir = "$xdg_config_home/$pkgname";
  43. my $schema_file = "$config_dir/schema.pl";
  44. my $config_file = "$config_dir/config.pl";
  45. my $openbox_conf = "$xdg_config_home/openbox";
  46. my $menufile = "$openbox_conf/menu.xml";
  47. my $icons_db = "$config_dir/icons.db";
  48. sub usage {
  49. print <<"HELP";
  50. usage: $0 [options]
  51. Options:
  52. -p : (re)generate a pipe menu
  53. -s : (re)generate a static menu
  54. -o : static menu file (default: ~/.config/openbox/menu.xml)
  55. -m : menu id (default: 'root-menu')
  56. -r : regenerate the config files
  57. -i : use icons in menus
  58. -d : regenerate icons.db (with -i)
  59. -u : update the config file
  60. -R : reconfigure openbox
  61. Help:
  62. -h : print this message
  63. -v : print the version number
  64. Examples:
  65. ** Static menu without icons:
  66. $0 -s
  67. ** Pipe menu with icons:
  68. $0 -p -i
  69. ** Reconfigure openbox:
  70. $0 -R
  71. ** Config file: $config_file
  72. ** Schema file: $schema_file
  73. HELP
  74. exit 0;
  75. }
  76. my $config_help = <<"HELP";
  77. || FILTERING
  78. | skip_filename_re : Skip a .desktop file if its name matches the regex.
  79. Name is from the last slash to the end. (filename.desktop)
  80. Example: qr/^(?:gimp|xterm)\\b/, # skips 'gimp' and 'xterm'
  81. | skip_entry : Skip a destkop file if the value from a given key matches the regex.
  82. Example: [
  83. {key => 'Name', re => qr/(?:about|terminal)/i},
  84. {key => 'Exec', re => qr/^xterm/},
  85. ],
  86. | substitutions : Substitute, by using a regex, in the values of the desktop files.
  87. Example: [
  88. {key => 'Exec', re => qr/xterm/, value => 'sakura'},
  89. {key => 'Exec', re => qr/\\\\\\\\/, value => '\\\\', global => 1}, # for wine apps
  90. ],
  91. || ICON SETTINGS
  92. | icon_dirs_first : When looking for icons, look in this directories first,
  93. before looking in the directories of the current icon theme.
  94. Example: [
  95. "\$ENV{HOME}/My icons",
  96. ],
  97. | icon_dirs_second : Look in this directories after looked in the directories of the
  98. current icon theme. (Before /usr/share/pixmaps)
  99. Example: [
  100. "/usr/share/icons/gnome",
  101. ],
  102. | icon_dirs_last : Look in this directories at the very last, after looked in
  103. /usr/share/pixmaps, /usr/share/icons/hicolor and some other
  104. directories.
  105. Example: [
  106. "/usr/share/icons/Tango",
  107. ],
  108. | strict_icon_dirs : A true value will make the module to look only inside the directories
  109. specified by you in either one of the above three options.
  110. | gtk_rc_filename : Absolute path to the GTK configuration file.
  111. | missing_image : Use this icon for missing icons (default: gtk-missing-image)
  112. || KEYS
  113. | name_keys : Valid keys for the item names.
  114. Example: ['Name[fr]', 'GenericName[fr]', 'Name'], # french menu
  115. || PATHS
  116. | desktop_files_paths : Absolute paths which contains .desktop files.
  117. Example: [
  118. '/usr/share/applications',
  119. "\$ENV{HOME}/.local/share/applications",
  120. glob("\$ENV{HOME}/.local/share/applications/wine/Programs/*"),
  121. ],
  122. || NOTES
  123. | Regular expressions:
  124. * use qr/RE/ instead of 'RE'
  125. * use qr/RE/i for case insenstive mode
  126. HELP
  127. if (@ARGV) {
  128. while (defined(my $arg = shift @ARGV)) {
  129. if ($arg eq '-i') {
  130. $icons = 1;
  131. }
  132. elsif ($arg eq '-S') {
  133. $stdout_config = 1;
  134. }
  135. elsif ($arg eq '-p') {
  136. $pipe = 1;
  137. }
  138. elsif ($arg eq '-r') {
  139. $reconfigure = 1;
  140. }
  141. elsif ($arg eq '-s') {
  142. $static = 1;
  143. }
  144. elsif ($arg eq '-d') {
  145. unlink $icons_db;
  146. }
  147. elsif ($arg eq '-u') {
  148. $update_config = 1;
  149. }
  150. elsif ($arg eq '-v') {
  151. print "$pkgname $version\n";
  152. exit 0;
  153. }
  154. elsif ($arg eq '-R') {
  155. exec 'openbox', '--reconfigure';
  156. }
  157. elsif ($arg eq '-o') {
  158. $menufile = shift(@ARGV) // die "$0: option '-o' requires an argument!\n";
  159. }
  160. elsif ($arg eq '-m') {
  161. $menu_id = shift(@ARGV) // die "$0: option '-m' requires an argument!\n";
  162. }
  163. elsif ($arg eq '-h') {
  164. usage();
  165. }
  166. else {
  167. die "$0: option `$arg' is invalid!\n";
  168. }
  169. }
  170. }
  171. if (not -d $config_dir) {
  172. require File::Path;
  173. File::Path::make_path($config_dir)
  174. or die "Can't create directory '${config_dir}': $!";
  175. }
  176. my $config_documentation = <<"EOD";
  177. #!/usr/bin/perl
  178. # $pkgname - configuration file
  179. # This file will be updated automatically every time when is needed.
  180. # Any additional comment and/or indentation will be lost.
  181. =for comment
  182. $config_help
  183. =cut
  184. EOD
  185. my %CONFIG = (
  186. 'Linux::DesktopFiles' => {
  187. keep_unknown_categories => 1,
  188. unknown_category_key => 'other',
  189. gtk_rc_filename => "$home_dir/.gtkrc-2.0",
  190. skip_entry => undef,
  191. substitutions => undef,
  192. skip_filename_re => undef,
  193. terminalize => 1,
  194. terminalization_format => q{%s -e '%s'},
  195. desktop_files_paths => ['/usr/share/applications'],
  196. icon_dirs_first => undef,
  197. icon_dirs_second => undef,
  198. icon_dirs_last => undef,
  199. strict_icon_dirs => undef,
  200. skip_svg_icons => 1,
  201. },
  202. name_keys => ['Name'],
  203. terminal => 'xterm',
  204. editor => 'geany',
  205. missing_icon => 'gtk-missing-image',
  206. VERSION => $version,
  207. );
  208. sub dump_configuration {
  209. require Data::Dump;
  210. open my $config_fh, '>', $config_file
  211. or die "Can't open file '${config_file}' for write: $!";
  212. my $dumped_config = q{our $CONFIG = } . Data::Dump::dump(\%CONFIG);
  213. print $config_fh $config_documentation, $dumped_config;
  214. close $config_fh;
  215. }
  216. if (not -e $config_file or $reconfigure) {
  217. dump_configuration();
  218. }
  219. if (not -e $schema_file) {
  220. if (-e (my $etc_schema_file = "/etc/xdg/$pkgname/schema.pl")) {
  221. require File::Copy;
  222. File::Copy::copy($etc_schema_file, $schema_file)
  223. or die "$0: can't copy file `$etc_schema_file' to `$schema_file': $!\n";
  224. }
  225. else {
  226. die "$0: schema file `$schema_file' does not exists!\n";
  227. }
  228. }
  229. require $schema_file; # Load the configuration files
  230. # Remove user's defined values
  231. my @valid_keys = grep exists $CONFIG{$_}, keys $CONFIG;
  232. @CONFIG{@valid_keys} = @{$CONFIG}{@valid_keys};
  233. # Keep user's defined values
  234. #@CONFIG{keys %{$CONFIG}} = values %{$CONFIG};
  235. if ($CONFIG{VERSION} != $version) {
  236. $update_config = 1;
  237. $CONFIG{VERSION} = $version;
  238. }
  239. if ($icons && !$pipe) {
  240. # Performance improvement
  241. # XXX: don't do this in production code!
  242. @INC{
  243. qw(
  244. strict.pm
  245. warnings.pm
  246. Tie/Hash.pm
  247. Carp.pm
  248. Exporter.pm
  249. warnings/register.pm
  250. )
  251. } = ();
  252. *{'warnings::warnif'} = sub { };
  253. }
  254. my $desk_obj = Linux::DesktopFiles->new(
  255. %{$CONFIG{'Linux::DesktopFiles'}},
  256. home_dir => $home_dir,
  257. categories => [map $_->{cat}[0], grep exists $_->{cat}, @$SCHEMA],
  258. keys_to_keep => [@{$CONFIG{name_keys}}, 'Exec', $icons ? 'Icon' : ()],
  259. $icons
  260. ? (
  261. abs_icon_paths => 1,
  262. icon_db_filename => $icons_db,
  263. )
  264. : (),
  265. terminal => $CONFIG{terminal},
  266. keep_empty_categories => 0,
  267. case_insensitive_cats => 1,
  268. );
  269. if ($pipe or $static) {
  270. my $menu_backup = $menufile . '.bak';
  271. if (not -e $menu_backup and -e $menufile) {
  272. require File::Copy;
  273. File::Copy::copy($menufile, $menu_backup);
  274. }
  275. if ($static) {
  276. open $output_h, '>', $menufile
  277. or die "Can't open file '${menufile}' for write: $!";
  278. }
  279. elsif ($pipe) {
  280. if (not -d $openbox_conf) {
  281. require File::Path;
  282. File::Path::make_path($openbox_conf)
  283. or die "Can't create directory '${openbox_conf}': $!";
  284. }
  285. require Cwd;
  286. my $exec_name = Cwd::abs_path($0) . ($icons ? q{ -i} : q{});
  287. if (not -x $exec_name) {
  288. $exec_name = "$^X $exec_name";
  289. }
  290. open my $fh, '>', $menufile
  291. or die "Can't open file '${menufile}' for write: $!";
  292. print $fh <<"PIPE_MENU_HEADER";
  293. <?xml version="1.0" encoding="utf-8"?>
  294. <openbox_menu xmlns="http://openbox.org/"
  295. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  296. xsi:schemaLocation="http://openbox.org/">
  297. <menu id="$menu_id" label="$pkgname" execute="$exec_name" />
  298. </openbox_menu>
  299. PIPE_MENU_HEADER
  300. close $fh;
  301. print <<'EOT';
  302. ** The pipe menu has been successfully generated!
  303. EOT
  304. exec 'openbox', '--reconfigure';
  305. }
  306. }
  307. my $generated_menu = $static
  308. ? <<"STATIC_MENU_HEADER"
  309. <?xml version="1.0" encoding="utf-8"?>
  310. <openbox_menu xmlns="http://openbox.org/"
  311. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  312. xsi:schemaLocation="http://openbox.org/">
  313. <menu id="$menu_id" label="Applications">
  314. STATIC_MENU_HEADER
  315. : "<openbox_pipe_menu>\n";
  316. {
  317. my %cache;
  318. sub check_icon {
  319. $_[0] // return '';
  320. $cache{$_[0]} //= ((chr ord($_[0]) eq '/') ? $_[0] : $desk_obj->get_icon_path($_[0])
  321. || $desk_obj->get_icon_path($CONFIG{missing_icon}));
  322. }
  323. }
  324. sub prepare_item {
  325. $icons
  326. ? <<"ITEM_WITH_ICON"
  327. <item label="$_[1]" icon="${\check_icon($_[2])}"><action name="Execute"><execute>$_[0]</execute></action></item>
  328. ITEM_WITH_ICON
  329. : <<"ITEM";
  330. <item label="$_[1]"><action name="Execute"><execute>$_[0]</execute></action></item>
  331. ITEM
  332. }
  333. sub begin_category {
  334. $icons
  335. ? <<"MENU_WITH_ICON"
  336. <menu id="$_[0]" icon="${\check_icon($_[1])}" label="$_[0]">
  337. MENU_WITH_ICON
  338. : <<"MENU"
  339. <menu id="$_[0]" label="$_[0]">
  340. MENU
  341. }
  342. my $categories = $desk_obj->parse_desktop_files();
  343. foreach my $schema (@$SCHEMA) {
  344. if (exists $schema->{cat}) {
  345. exists($categories->{my $category = lc($schema->{cat}[0]) =~ tr/_a-z0-9/_/cr}) || next;
  346. $generated_menu .= begin_category($schema->{cat}[1], ($icons ? $schema->{cat}[2] : ())) . join(
  347. q{},
  348. (
  349. map $_->[1],
  350. sort { $a->[0] cmp $b->[0] }
  351. map [lc($_) => $_],
  352. map {
  353. my $name;
  354. my $exec = $_->{Exec};
  355. foreach my $key (@{$CONFIG{name_keys}}) {
  356. if (defined $_->{$key}) {
  357. $name = $_->{$key};
  358. last;
  359. }
  360. }
  361. foreach my $item ($name, $exec) {
  362. if ($item =~ tr/"&//) {
  363. $item =~ s/&/&amp;/g;
  364. $item =~ s/"/&quot;/g;
  365. }
  366. }
  367. $icons
  368. ? <<"ITEM_WITH_ICON"
  369. <item label="$name" icon="${\check_icon($_->{Icon})}"><action name="Execute"><execute>$exec</execute></action></item>
  370. ITEM_WITH_ICON
  371. : <<"ITEM";
  372. <item label="$name"><action name="Execute"><execute>$exec</execute></action></item>
  373. ITEM
  374. } @{$categories->{$category}}
  375. )
  376. )
  377. . qq[ </menu>\n];
  378. }
  379. elsif (exists $schema->{item}) {
  380. $generated_menu .= prepare_item(@{$schema->{item}});
  381. }
  382. elsif (exists $schema->{sep}) {
  383. $generated_menu .=
  384. defined $schema->{sep}
  385. ? qq[ <separator label="$schema->{sep}"/>\n]
  386. : qq[ <separator/>\n];
  387. }
  388. elsif (exists $schema->{begin_cat}) {
  389. $generated_menu .= begin_category(@{$schema->{begin_cat}});
  390. }
  391. elsif (exists $schema->{end_cat}) {
  392. $generated_menu .= qq[ </menu>\n];
  393. }
  394. elsif (exists $schema->{exit}) {
  395. my ($label, $icon) = @{$schema->{exit}};
  396. $generated_menu .= $icons
  397. ? <<"EXIT_WITH_ICON"
  398. <item label="$label" icon="${\check_icon($icon)}"><action name="Exit"/></item>
  399. EXIT_WITH_ICON
  400. : <<"EXIT";
  401. <item label="$label"><action name="Exit"/></item>
  402. EXIT
  403. }
  404. elsif (exists $schema->{raw}) {
  405. $generated_menu .= qq[ $schema->{raw}\n];
  406. }
  407. elsif (exists $schema->{pipe}) {
  408. my ($command, $label, $icon) = @{$schema->{pipe}};
  409. $generated_menu .= $icons
  410. ? <<"PIPE_WITH_ICON"
  411. <menu id="$label" label="$label" execute="$command" icon="${\check_icon($icon)}"/>
  412. PIPE_WITH_ICON
  413. : <<"PIPE";
  414. <menu id="$label" label="$label" execute="$command"/>
  415. PIPE
  416. }
  417. elsif (exists $schema->{obgenmenu}) {
  418. my ($name, $icon) = ref($schema->{obgenmenu}) eq 'ARRAY' ? @{$schema->{obgenmenu}} : $schema->{obgenmenu};
  419. $generated_menu .= ($icons ? <<"CONFIG_MENU_WITH_ICON" : <<"CONFIG_MENU") . <<'RECONFIGURE';
  420. <menu id="$name" label="$name" icon="${\check_icon($icon)}">
  421. CONFIG_MENU_WITH_ICON
  422. <menu id="$name" label="$name">
  423. CONFIG_MENU
  424. <item label="Reconfigure Openbox"><action name="Reconfigure" /></item>
  425. RECONFIGURE
  426. -e '/usr/bin/obconf' && ($generated_menu .= <<'EOL');
  427. <item label="Openbox Configuration Manager"><action name="Execute"><execute>obconf</execute></action></item>
  428. EOL
  429. $generated_menu .= <<"CONFIG_MENU";
  430. <item label="Configure autostarted apps"><action name="Execute"><execute>$CONFIG{editor} $openbox_conf/autostart</execute></action></item>
  431. <item label="Edit rc.xml"><action name="Execute"><execute>$CONFIG{editor} $openbox_conf/rc.xml</execute></action></item>
  432. <separator />
  433. <item label="Generate a pipe menu"><action name="Execute"><execute>$0 -p</execute></action></item>
  434. <item label="Generate a static menu"><action name="Execute"><execute>$0 -s</execute></action></item>
  435. <item label="Generate a pipe menu with icons"><action name="Execute"><execute>$0 -p -i</execute></action></item>
  436. <item label="Generate a static menu with icons"><action name="Execute"><execute>$0 -s -i</execute></action></item>
  437. <separator />
  438. <item label="Edit menu.xml"><action name="Execute"><execute>$CONFIG{editor} $menufile</execute></action></item>
  439. <item label="Edit the schema file"><action name="Execute"><execute>$CONFIG{editor} $schema_file</execute></action></item>
  440. <item label="Edit the configuration file"><action name="Execute"><execute>$CONFIG{editor} $config_file</execute></action></item>
  441. <separator />
  442. <item label="Regenerate configuration file"><action name="Execute"><execute>$0 -r</execute></action></item>
  443. </menu>
  444. CONFIG_MENU
  445. }
  446. else {
  447. warn "$0: Invalid key '", (keys %{$schema})[0], "' in schema file!\n";
  448. }
  449. }
  450. print $output_h $generated_menu, $static
  451. ? qq[ </menu>\n</openbox_menu>\n]
  452. : qq[</openbox_pipe_menu>\n];
  453. dump_configuration() if $update_config;