1 #include <fstream>
2 #include <iostream>
3 #include <unistd.h>
4 #include <cstring>
5 #include <getopt.h>
6 #include <vector>
7 #include <algorithm>
8 
9 #include <sys/types.h>
10 #include <sys/stat.h>
11 #include <sys/wait.h>
12 #include <fcntl.h>
13 
14 #include "Utilities.hh"
15 #include "Dmenu.hh"
16 #include "Application.hh"
17 #include "ApplicationRunner.hh"
18 #include "Applications.hh"
19 #include "SearchPath.hh"
20 #include "FileFinder.hh"
21 #include "Formatters.hh"
22 
23 
24 class Main
25 {
26 public:
Main()27     Main()
28         : dmenu_command("dmenu -i"), terminal("i3-sensible-terminal") {
29 
30     }
31 
main(int argc,char ** argv)32     int main(int argc, char **argv) {
33         if(read_args(argc, argv))
34             return 0;
35 
36         if(use_xdg_de) {
37             std::string env_var = get_variable("XDG_CURRENT_DESKTOP");
38             //XDG_CURRENT_DESKTOP can contain multiple environments separated by colons
39             split(env_var, ':', environment);
40             if(environment.empty())
41                 use_xdg_de = false;
42         }
43 
44 #ifdef DEBUG
45         fprintf(stderr, "desktop environment:\n");
46         for(auto s: environment)
47             fprintf(stderr, "%s\n", s.c_str());
48 #endif
49 
50         if(!wait_on) {
51             this->dmenu = new Dmenu(this->dmenu_command);
52         }
53 
54         collect_files();
55 
56         // Sort applications by displayed name
57         std::vector<std::pair<std::string, const Application *>> iteration_order;
58         iteration_order.reserve(apps.size());
59         for(auto &app : apps) {
60             iteration_order.emplace_back(app.first, app.second);
61         }
62 
63         std::sort(iteration_order.begin(), iteration_order.end(), [](
64             const std::pair<std::string, const Application *> &s1,
65             const std::pair<std::string, const Application *> &s2) {
66                 return s1.second->name < s2.second->name;
67         });
68 
69         if(usage_log) {
70             apps.load_log(usage_log);
71             std::stable_sort(iteration_order.begin(), iteration_order.end(), [](
72                 const std::pair<std::string, const Application *> &s1,
73                 const std::pair<std::string, const Application *> &s2) {
74                     return s1.second->usage_count > s2.second->usage_count;
75             });
76         }
77 
78         if(wait_on) {
79             return do_wait_on(iteration_order);
80         } else {
81             return do_dmenu(iteration_order);
82         }
83     }
84 
85 private:
print_usage(FILE * f)86     void print_usage(FILE* f) {
87         fprintf(f,
88                 "j4-dmenu-desktop\n"
89                 "A faster replacement for i3-dmenu-desktop\n"
90                 "Copyright (c) 2013 Marian Beermann, GPLv3 license\n"
91                 "\nUsage:\n"
92                 "\tj4-dmenu-desktop [--dmenu=\"dmenu -i\"] [--term=\"i3-sensible-terminal\"]\n"
93                 "\tj4-dmenu-desktop --help\n"
94                 "\nOptions:\n"
95                 "    --dmenu=<command>\n"
96                 "\tDetermines the command used to invoke dmenu\n"
97                 "\tExecuted with your shell ($SHELL) or /bin/sh\n"
98                 "    --use-xdg-de\n"
99                 "\tEnables reading $XDG_CURRENT_DESKTOP to determine the desktop environment\n"
100                 "    --display-binary\n"
101                 "\tDisplay binary name after each entry (off by default)\n"
102                 "    --no-generic\n"
103                 "\tDo not include the generic name of desktop entries\n"
104                 "    --term=<command>\n"
105                 "\tSets the terminal emulator used to start terminal apps\n"
106                 "    --usage-log=<file>\n"
107                 "\tMust point to a read-writeable file (will create if not exists).\n"
108                 "\tIn this mode entries are sorted by usage frequency.\n"
109 				"    --wrapper=<wrapper>\n"
110 				"\tA wrapper binary. Useful in case you want to wrap into 'i3 exec'\n"
111                 "    --wait-on=<path>\n"
112                 "\tMust point to a path where a file can be created.\n"
113                 "\tIn this mode no menu will be shown. Instead the program waits for <path>\n"
114                 "\tto be written to (use echo > path). Every time this happens a menu will be shown.\n"
115                 "\tDesktop files are parsed ahead of time.\n"
116                 "\tPerfoming 'echo -n q > path' will exit the program.\n"
117                 "    --no-exec\n"
118                 "\tDo not execute selected command, send to stdout instead\n"
119                 "    --help\n"
120                 "\tDisplay this help message\n"
121                );
122     }
123 
read_args(int argc,char ** argv)124     bool read_args(int argc, char **argv) {
125         format_type formatter = format_type::standard;
126 
127         while (true) {
128             int option_index = 0;
129             static struct option long_options[] = {
130                 {"dmenu",   required_argument,  0,  'd'},
131                 {"use-xdg-de",   no_argument,   0,  'x'},
132                 {"term",    required_argument,  0,  't'},
133                 {"help",    no_argument,        0,  'h'},
134                 {"display-binary", no_argument, 0,  'b'},
135                 {"no-generic", no_argument,     0,  'n'},
136                 {"usage-log", required_argument,0,  'l'},
137                 {"wait-on", required_argument,  0,  'w'},
138                 {"no-exec", no_argument,        0,  'e'},
139                 {"wrapper", required_argument,   0,  'W'},
140                 {0,         0,                  0,  0}
141             };
142 
143             int c = getopt_long(argc, argv, "d:t:xhb", long_options, &option_index);
144             if(c == -1)
145                 break;
146 
147             switch (c) {
148             case 'd':
149                 this->dmenu_command = optarg;
150                 break;
151             case 'x':
152                 use_xdg_de = true;
153                 break;
154             case 't':
155                 this->terminal = optarg;
156                 break;
157             case 'h':
158                 this->print_usage(stderr);
159                 return true;
160             case 'b':
161                 formatter = format_type::with_binary_name;
162                 break;
163             case 'n':
164                 exclude_generic = true;
165                 break;
166             case 'l':
167                 usage_log = optarg;
168                 break;
169             case 'w':
170                 wait_on = optarg;
171                 break;
172             case 'e':
173                 no_exec = true;
174                 break;
175             case 'W':
176                 this->wrapper = optarg;
177                 break;
178             default:
179                 exit(1);
180             }
181         }
182 
183         this->appformatter = formatters[static_cast<int>(formatter)];
184 
185         return false;
186     }
187 
collect_files()188     void collect_files() {
189         // We switch the working directory to easier get relative paths
190         // This way desktop files that are customized in more important directories
191         // (like $XDG_DATA_HOME/applications/) overwrite those found in system-wide
192         // directories
193         char original_wd[384];
194         if(!getcwd(original_wd, 384)) {
195             pfatale("collect_files: getcwd");
196         }
197 
198         // Allocating the line buffer just once saves lots of MM calls
199         // malloc required to avoid mixing malloc/new[] as getdelim may realloc() buf
200         buf = static_cast<char*>(malloc(bufsz));
201         buf[0] = 0;
202 
203         for(auto &path : this->search_path) {
204             if(chdir(path.c_str())) {
205                 fprintf(stderr, "%s: %s", path.c_str(), strerror(errno));
206                 continue;
207             }
208             FileFinder finder("./", ".desktop");
209             while(finder++) {
210                 handle_file(*finder, path);
211             }
212         }
213 
214         free(buf);
215 
216         if(chdir(original_wd)) {
217             pfatale("collect_files: chdir(original_cwd)");
218         }
219     }
220 
handle_file(const std::string & file,const std::string & base_path)221     void handle_file(const std::string &file, const std::string &base_path) {
222         Application *dft = new Application(suffixes, use_xdg_de ? &environment : 0);
223         bool file_read = dft->read(file.c_str(), &buf, &bufsz);
224         dft->name = this->appformatter(*dft);
225         dft->location = base_path + file;
226 
227         if(file_read && !dft->name.empty()) {
228             if(apps.count(dft->id)) {
229                 delete apps[dft->id];
230             }
231             apps[dft->id] = dft;
232         } else {
233             if(!dft->id.empty()) {
234                 delete apps[dft->id];
235                 apps.erase(dft->id);
236             }
237             delete dft;
238         }
239         parsed_files++;
240     }
241 
do_dmenu(const std::vector<std::pair<std::string,const Application * >> & iteration_order)242     int do_dmenu(const std::vector<std::pair<std::string, const Application *>> &iteration_order) {
243         if(wait_on) {
244             this->dmenu = new Dmenu(this->dmenu_command);
245         }
246 
247         // Transfer the list to dmenu
248         for(auto &app : iteration_order) {
249             this->dmenu->write(app.second->name);
250             const std::string &generic_name = app.second->generic_name;
251             if(!exclude_generic && !generic_name.empty() && app.second->name != generic_name)
252                 this->dmenu->write(generic_name);
253         }
254 
255         this->dmenu->display();
256         std::string command = get_command();
257         if (this->wrapper.length())
258             command = this->wrapper+" \""+command+"\"";
259         delete this->dmenu;
260 
261         if(!command.empty()) {
262             if (no_exec) {
263                 printf("%s\n", command.c_str());
264                 return 0;
265             }
266             static const char *shell = 0;
267             if((shell = getenv("SHELL")) == 0)
268                 shell = "/bin/sh";
269 
270             fprintf(stderr, "%s -c '%s'\n", shell, command.c_str());
271 
272             return execl(shell, shell, "-c", command.c_str(), 0, nullptr);
273         }
274         return 0;
275     }
276 
do_wait_on(const std::vector<std::pair<std::string,const Application * >> & iteration_order)277     int do_wait_on(const std::vector<std::pair<std::string, const Application *>> &iteration_order) {
278         int fd;
279         pid_t pid;
280         char data;
281         if(mkfifo(wait_on, 0600) && errno != EEXIST) {
282             perror("mkfifo");
283             return 1;
284         }
285         fd = open(wait_on, O_RDWR);
286         if(fd == -1) {
287             perror("open(fifo)");
288             return 1;
289         }
290         while (1) {
291             if(read(fd, &data, sizeof(data)) < 1) {
292                 perror("read(fifo)");
293                 break;
294             }
295             if(data == 'q') {
296                 break;
297             }
298             pid = fork();
299             switch(pid) {
300             case -1:
301                 perror("fork");
302                 return 1;
303             case 0:
304                 close(fd);
305                 setsid();
306                 return do_dmenu(iteration_order);
307             }
308         }
309         close(fd);
310         return 0;
311     }
312 
get_command()313     std::string get_command() {
314         std::string choice;
315         std::string args;
316         Application *app;
317 
318         fprintf(stderr, "Read %d .desktop files, found %lu apps.\n", parsed_files, apps.size());
319 
320         choice = dmenu->read_choice(); // Blocks
321         if(choice.empty())
322             return "";
323 
324         fprintf(stderr, "User input is: %s %s\n", choice.c_str(), args.c_str());
325 
326         std::tie(app, args) = apps.search(choice);
327         if (!app) {
328             return args;
329         }
330 
331         if(usage_log) {
332             apps.update_log(usage_log, app);
333         }
334 
335         if(!app->path.empty()) {
336             if(chdir(app->path.c_str())) {
337                 perror("chdir into application path");
338             }
339         }
340 
341         ApplicationRunner app_runner(terminal, *app, args);
342         return app_runner.command();
343     }
344 
345 private:
346     std::string dmenu_command;
347     std::string terminal;
348 	std::string wrapper;
349     const char *wait_on = 0;
350 
351     stringlist_t environment;
352     bool use_xdg_de = false;
353     bool exclude_generic = false;
354     bool no_exec = false;
355 
356     Dmenu *dmenu = 0;
357     SearchPath search_path;
358 
359     int parsed_files = 0;
360 
361     Applications apps;
362 
363     char *buf = 0;
364     size_t bufsz = 4096;
365 
366     LocaleSuffixes suffixes;
367 
368     application_formatter appformatter;
369 
370     const char *usage_log = 0;
371 };
372 
373