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