1 //===========================================
2 // Lumina-DE source code
3 // Copyright (c) 2012-2017, Ken Moore
4 // Available under the 3-clause BSD license
5 // See the LICENSE file for full details
6 //===========================================
7
8 #include <QApplication>
9 #include <QX11Info>
10 #include <QProcess>
11 #include <QProcessEnvironment>
12 #include <QFile>
13 #include <QFileInfo>
14 #include <QString>
15 #include <QUrl>
16 #include <QDebug>
17 #include <QTranslator>
18 #include <QMessageBox>
19 #include <QLabel>
20 #include <QDateTime>
21 #include <QPixmap>
22 #include <QColor>
23 #include <QDesktopWidget>
24
25 #include "LFileDialog.h"
26
27 #include <LuminaXDG.h>
28 #include <LUtils.h>
29 #include <LuminaOS.h>
30 #include <LuminaThemes.h>
31
32 #define DEBUG 0
33
printUsageInfo()34 void printUsageInfo(){
35 qDebug() << "lumina-open: Application launcher for the Lumina Desktop Environment";
36 qDebug() << "Description: Given a file (with absolute path) or URL, this utility will try to find the appropriate application with which to open the file. If the file is a *.desktop application shortcut, it will just start the application appropriately. It can also perform a few specific system operations if given special flags.";
37 qDebug() << "Usage: lumina-open [-select] [-action <ActionID>] <absolute file path or URL>";
38 qDebug() << " lumina-open [-volumeup, -volumedown, -brightnessup, -brightnessdown]";
39 qDebug() << " [-select] (optional) flag to bypass any default application settings and show the application selector window";
40 qDebug() << " [-action <ActionID>] (optional) Flag to run one of the alternate Actions listed in a .desktop registration file rather than the main command.";
41 qDebug() << "Special Flags:";
42 qDebug() << " \"-volume[up/down]\" Flag to increase/decrease audio volume by 5%";
43 qDebug() << " \"-brightness[up/down]\" Flag to increase/decrease screen brightness by 5%";
44 qDebug() << " \"-autostart-apps\" Flag to launch all the various apps which are registered with XDG autostart specification";
45 qDebug() << "\"-terminal\" Flag to open the terminal currently set as the user's default";
46 exit(1);
47 }
48
ShowErrorDialog(int argc,char ** argv,QString message)49 void ShowErrorDialog(int argc, char **argv, QString message){
50 //Setup the application
51 QApplication App(argc, argv);
52 App.setAttribute(Qt::AA_UseHighDpiPixmaps);
53 LUtils::LoadTranslation(&App,"lumina-open");
54 QMessageBox dlg(QMessageBox::Critical, QObject::tr("File Error"), message );
55 dlg.exec();
56 exit(1);
57 }
58
showOSD(int argc,char ** argv,QString message)59 void showOSD(int argc, char **argv, QString message){
60 //Setup the application
61 QApplication App(argc, argv);
62 LUtils::LoadTranslation(&App,"lumina-open");
63 App.setAttribute(Qt::AA_UseHighDpiPixmaps);
64 //Display the OSD
65 QPixmap pix(":/icons/OSD.png");
66 QLabel splash(0, Qt::Window | Qt::WindowStaysOnTopHint | Qt::X11BypassWindowManagerHint);
67 splash.setWindowTitle("");
68 splash.setStyleSheet("QLabel{background: black; color: white; font-weight: bold; font-size: 13pt; margin: 1ex;}");
69 splash.setAlignment(Qt::AlignCenter);
70
71
72 if(DEBUG) qDebug() << "Display OSD";
73 splash.setText(message);
74 //Make sure it is centered on the current screen
75 QPoint center = App.screenAt(QCursor::pos())->availableGeometry().center();
76 splash.move(center.x()-(splash.sizeHint().width()/2), center.y()-(splash.sizeHint().height()/2));
77 splash.show();
78 //qDebug() << " - show message";
79 //qDebug() << " - loop";
80 QDateTime end = QDateTime::currentDateTime().addMSecs(800);
81 while(QDateTime::currentDateTime() < end){ App.processEvents(); }
82 splash.hide();
83 }
84
LaunchAutoStart()85 void LaunchAutoStart(){
86 QList<XDGDesktop*> xdgapps = LXDG::findAutoStartFiles();
87 for(int i=0; i<xdgapps.length(); i++){
88 //Generate command and clean up any stray "Exec" field codes (should not be any here)
89 QString cmd = xdgapps[i]->getDesktopExec();
90 if(cmd.contains("%")){cmd = cmd.remove("%U").remove("%u").remove("%F").remove("%f").remove("%i").remove("%c").remove("%k").simplified(); }
91 //Now run the command
92 if(!cmd.isEmpty()){
93 if(DEBUG) qDebug() << " - Auto-Starting File:" << xdgapps[i]->filePath;
94 QProcess::startDetached(cmd);
95 }
96 }
97 //make sure we clean up all the xdgapps structures
98 for(int i=0; i<xdgapps.length(); i++){ xdgapps[i]->deleteLater(); }
99 }
100
cmdFromUser(int argc,char ** argv,QString inFile,QString extension,QString & path,bool showDLG=false)101 QString cmdFromUser(int argc, char **argv, QString inFile, QString extension, QString& path, bool showDLG=false){
102 //First check to see if there is a default for this extension
103 QString defApp;
104 if(extension=="mimetype"){
105 //qDebug() << "inFile:" << inFile;
106 QStringList matches = LXDG::findAppMimeForFile(inFile, true).split("::::"); //allow multiple matches
107 if(DEBUG) qDebug() << "Mimetype Matches:" << matches;
108 for(int i=0; i<matches.length(); i++){
109 defApp = LXDG::findDefaultAppForMime(matches[i]);
110 //qDebug() << "MimeType:" << matches[i] << defApp;
111 if(!defApp.isEmpty()){ extension = matches[i]; break; }
112 else if(i+1==matches.length()){ extension = matches[0]; }
113 }
114 }else{ defApp = LFileDialog::getDefaultApp(extension); }
115 if(DEBUG) qDebug() << "Mimetype:" << extension << "defApp:" << defApp;
116 if( !defApp.isEmpty() && !showDLG ){
117 if(defApp.endsWith(".desktop")){
118 XDGDesktop DF(defApp);
119 if(DF.isValid()){
120 QString exec = DF.getDesktopExec();
121 if(!exec.isEmpty()){
122 if(DEBUG) qDebug() << "[lumina-open] Using default application:" << DF.name << "File:" << inFile;
123 if(!DF.path.isEmpty()){ path = DF.path; }
124 return exec;
125 }
126 }
127 }else{
128 //Only binary given
129 if(LUtils::isValidBinary(defApp)){
130 if(DEBUG) qDebug() << "[lumina-open] Using default application:" << defApp << "File:" << inFile;
131 return defApp; //just use the binary
132 }
133 }
134 //invalid default - reset it and continue on
135 LFileDialog::setDefaultApp(extension, "");
136 }
137 //Final catch: directory given - no valid default found - use lumina-fm
138 if(extension=="inode/directory" && !showDLG){ return "lumina-fm"; }
139 //No default set -- Start up the application selection dialog
140 LTHEME::LoadCustomEnvSettings();
141 QApplication App(argc, argv);
142 App.setAttribute(Qt::AA_UseHighDpiPixmaps);
143 LUtils::LoadTranslation(&App,"lumina-open");
144
145 LFileDialog w;
146 if(extension=="email" || extension.startsWith("x-scheme-handler/")){
147 //URL
148 w.setFileInfo(inFile, extension, false);
149 }else{
150 //File
151 if(inFile.endsWith("/")){ inFile.chop(1); }
152 w.setFileInfo(inFile.section("/",-1), extension, true);
153 }
154
155 w.show();
156 App.exec();
157 if(!w.appSelected){ return ""; }
158 //Return the run path if appropriate
159 if(!w.appPath.isEmpty()){ path = w.appPath; }
160 //Just do the default application registration here for now
161 // might move it to the runtime phase later after seeing that the app has successfully started
162 if(w.setDefault){
163 if(!w.appFile.isEmpty()){ LFileDialog::setDefaultApp(extension, w.appFile); }
164 else{ LFileDialog::setDefaultApp(extension, w.appExec); }
165 }
166 //Now return the resulting application command
167 return w.appExec;
168 }
169
getCMD(int argc,char ** argv,QString & binary,QString & args,QString & path,bool & watch)170 void getCMD(int argc, char ** argv, QString& binary, QString& args, QString& path, bool& watch){
171 //Get the input file
172 //Make sure to load the proper system encoding first
173 LUtils::LoadTranslation(0,""); //bypass application modification
174 QString inFile, ActionID;
175 bool showDLG = false; //flag to bypass any default application setting
176 if(argc > 1){
177 for(int i=1; i<argc; i++){
178 if(QString(argv[i]).simplified() == "-select"){
179 showDLG = true;
180 }else if(QString(argv[i]).simplified() == "-testcrash"){
181 //Test the crash handler
182 binary = "internalcrashtest"; watch=true;
183 return;
184 }else if(QString(argv[i]).simplified() == "-autostart-apps"){
185 LaunchAutoStart();
186 return;
187 }else if(QString(argv[i]).simplified() == "-volumeup"){
188 bool isInt = false;
189 int volupVal = 5;
190 if(argc > i){
191 int parse = QString(argv[i+1]).toInt(&isInt,10);
192 if(isInt && 0 < parse && parse <= 20){
193 volupVal = parse;
194 }
195 }
196 int vol = LOS::audioVolume()+volupVal;
197 if(vol>100){ vol=100; }
198 LOS::setAudioVolume(vol);
199 showOSD(argc,argv, QString(QObject::tr("Audio Volume %1%")).arg(QString::number(vol)) );
200 return;
201 }else if(QString(argv[i]).simplified() == "-volumedown"){
202 bool isInt = false;
203 int voldownVal = 5;
204 if(argc > i){
205 int parse = QString(argv[i+1]).toInt(&isInt,10);
206 if(isInt && 0 < parse && parse <= 20){
207 voldownVal = parse;
208 }
209 }
210 int vol = LOS::audioVolume()-voldownVal; //decrease 5%
211 if(vol<0){ vol=0; }
212 LOS::setAudioVolume(vol);
213 showOSD(argc,argv, QString(QObject::tr("Audio Volume %1%")).arg(QString::number(vol)) );
214 return;
215 }else if(QString(argv[i]).simplified() == "-brightnessup"){
216 int bright = LOS::ScreenBrightness();
217 if(bright > 0){ //brightness control available
218 bright = bright+5; //increase 5%
219 if(bright>100){ bright = 100; }
220 LOS::setScreenBrightness(bright);
221 showOSD(argc,argv, QString(QObject::tr("Screen Brightness %1%")).arg(QString::number(bright)) );
222 }
223 return;
224 }else if(QString(argv[i]).simplified() == "-brightnessdown"){
225 int bright = LOS::ScreenBrightness();
226 if(bright > 0){ //brightness control available
227 bright = bright-5; //decrease 5%
228 if(bright<0){ bright = 0; }
229 LOS::setScreenBrightness(bright);
230 showOSD(argc,argv, QString(QObject::tr("Screen Brightness %1%")).arg(QString::number(bright)) );
231 }
232 return;
233 }else if( (QString(argv[i]).simplified() =="-action") && (argc>(i+1)) ){
234 ActionID = QString(argv[i+1]);
235 i++; //skip the next input
236 }else if(QString(argv[i]).simplified()=="-terminal"){
237 inFile = LXDG::findDefaultAppForMime("application/terminal");
238 break;
239 }else{
240 inFile = QString::fromLocal8Bit(argv[i]);
241 break;
242 }
243 }
244 }else{
245 printUsageInfo();
246 }
247
248
249 //Make sure that it is a valid file/URL
250 bool isFile=false; bool isUrl=false;
251 QString extension;
252 //Quick check/replacement for the URL syntax of a file
253 if(inFile.startsWith("file://")){ inFile = QUrl(inFile).toLocalFile(); } //change from URL to file format for a local file
254 //First make sure this is not a binary name first
255 QString bin = inFile.section(" ",0,0).simplified();
256 if(LUtils::isValidBinary(bin) && !bin.endsWith(".desktop") && !QFileInfo(inFile).isDir() ){isFile=true; }
257 //Now check what type of file this is
258 else if(QFile::exists(inFile)){ isFile=true; }
259 else if(QFile::exists(QDir::currentPath()+"/"+inFile)){isFile=true; inFile = QDir::currentPath()+"/"+inFile;} //account for relative paths
260 else if(inFile.startsWith("mailto:")){ isUrl= true; }
261 else if(QUrl(inFile).isValid() && !inFile.startsWith("/") ){ isUrl=true; }
262 if( !isFile && !isUrl ){ ShowErrorDialog( argc, argv, QString(QObject::tr("Invalid file or URL: %1")).arg(inFile) ); }
263 //Determing the type of file (extension)
264 //qDebug() << "File Type:" << isFile << isUrl;
265 if(isFile && extension.isEmpty()){
266 QFileInfo info(inFile);
267 extension=info.suffix();
268 //qDebug() << " - Extension:" << extension;
269 if(info.isDir()){ extension="inode/directory"; }
270 else if(info.isExecutable() && (extension.isEmpty() || extension=="sh") ){ extension="binary"; }
271 else if(extension!="desktop"){ extension="mimetype"; } //flag to check for mimetype default based on file
272 }
273 else if(isUrl && inFile.startsWith("mailto:")){ extension = "application/email"; }
274 else if(isUrl && inFile.contains("://") ){ extension = "x-scheme-handler/"+inFile.section("://",0,0); }
275 else if(isUrl && inFile.startsWith("www.")){ extension = "x-scheme-handler/http"; inFile.prepend("http://"); } //this catches partial (but still valid) URL's ("www.<something>" for instance)
276 //qDebug() << "Input:" << inFile << isFile << isUrl << extension;
277 //if not an application - find the right application to open the file
278 QString cmd;
279 bool useInputFile = false;
280 if(extension=="desktop" && !showDLG){
281 XDGDesktop DF(inFile);
282 if(!DF.isValid()){
283 ShowErrorDialog( argc, argv, QString(QObject::tr("Application entry is invalid: %1")).arg(inFile) );
284 }
285 switch(DF.type){
286 case XDGDesktop::APP:
287 if(DEBUG) qDebug() << "Found .desktop application:" << ActionID;
288 if(!DF.exec.isEmpty()){
289 cmd = DF.getDesktopExec(ActionID);
290 if(DEBUG) qDebug() << "Got command:" << cmd;
291 if(!DF.path.isEmpty()){ path = DF.path; }
292 watch = DF.startupNotify || !DF.filePath.contains("/xdg/autostart/");
293 }else{
294 ShowErrorDialog( argc, argv, QString(QObject::tr("Application shortcut is missing the launching information (malformed shortcut): %1")).arg(inFile) );
295 }
296 break;
297 case XDGDesktop::LINK:
298 if(!DF.url.isEmpty()){
299 //This is a URL - so adjust the input variables appropriately
300 inFile = DF.url;
301 cmd.clear();
302 extension = inFile.section(":",0,0);
303 if(extension=="file"){ extension = "http"; } //local file URL - Make sure we use the default browser for a LINK type
304 extension.prepend("x-scheme-handler/");
305 watch = DF.startupNotify || !DF.filePath.contains("/xdg/autostart/");
306 }else{
307 ShowErrorDialog( argc, argv, QString(QObject::tr("URL shortcut is missing the URL: %1")).arg(inFile) );
308 }
309 break;
310 case XDGDesktop::DIR:
311 if(!DF.path.isEmpty()){
312 //This is a directory link - adjust inputs
313 inFile = DF.path;
314 cmd.clear();
315 extension = "inode/directory";
316 watch = DF.startupNotify || !DF.filePath.contains("/xdg/autostart/");
317 }else{
318 ShowErrorDialog( argc, argv, QString(QObject::tr("Directory shortcut is missing the path to the directory: %1")).arg(inFile) );
319 }
320 break;
321 default:
322 if(DEBUG) qDebug() << DF.type << DF.name << DF.icon << DF.exec;
323 ShowErrorDialog( argc, argv, QString(QObject::tr("Unknown type of shortcut : %1")).arg(inFile) );
324 }
325 }
326 if(cmd.isEmpty()){
327 if(extension=="binary" && !showDLG){ cmd = inFile; }
328 else{
329 //Find out the proper application to use this file/directory
330 useInputFile=true;
331 cmd = cmdFromUser(argc, argv, inFile, extension, path, showDLG);
332 if(cmd.isEmpty()){ return; }
333 }
334 }
335 //Now assemble the exec string (replace file/url field codes as necessary)
336 if(useInputFile){
337 args = inFile; //just to keep them distinct internally
338 // NOTE: lumina-open is only designed for a single input file,
339 // so no need to distinguish between the list codes (uppercase)
340 // and the single-file codes (lowercase)
341 //Special "inFile" format replacements for input codes
342 if( (cmd.contains("%f") || cmd.contains("%F") ) ){
343 //Apply any special field replacements for the desired format
344 inFile.replace("%20"," ");
345 if(inFile.startsWith("file://")){ inFile.remove(0,7); } //chop that URL prefix off the front (should have happened earlier - just make sure)
346 //Now replace the field codes
347 cmd.replace("%f","\""+inFile+"\"");
348 cmd.replace("%F","\""+inFile+"\"");
349 }else if( (cmd.contains("%U") || cmd.contains("%u")) ){
350 //Apply any special field replacements for the desired format
351 if(!inFile.contains("://") && !isUrl){ inFile.prepend("file://"); } //local file - add the extra flag
352 inFile.replace(" ", "%20");
353 //Now replace the field codes
354 cmd.replace("%u","\""+inFile+"\"");
355 cmd.replace("%U","\""+inFile+"\"");
356 }else{
357 //No field codes (or improper field codes given in the file - which is quite common)
358 // - Just tack the input file on the end and let the app handle it as necessary
359 inFile.replace("%20"," "); //assume a local-file format rather than URL format
360 cmd.append(" \""+inFile+"\"");
361 }
362 }
363 if(DEBUG) qDebug() << "Found Command:" << cmd << "Extension:" << extension;
364 //Clean up any leftover "Exec" field codes (should have already been replaced earlier)
365 if(cmd.contains("%")){cmd = cmd.remove("%U").remove("%u").remove("%F").remove("%f").remove("%i").remove("%c").remove("%k").simplified(); }
366 binary = cmd; //pass this string to the calling function
367
368 }
369
main(int argc,char ** argv)370 int main(int argc, char **argv){
371 //Run all the actual code in a separate function to have as little memory usage
372 // as possible aside from the main application when running
373
374 //Make sure the XDG environment variables exist first
375 LXDG::setEnvironmentVars();
376 //now get the command
377 QString cmd, args, path;
378 bool watch = true; //enable the crash handler by default (only disabled for some *.desktop inputs)
379 getCMD(argc, argv, cmd, args, path, watch);
380 //qDebug() << "Run CMD:" << cmd << args;
381 //Now run the command (move to execvp() later?)
382 if(cmd.isEmpty()){ return 0; } //no command to run (handled internally)
383 QString bin = cmd.section(" ",0,0);
384 if( !LUtils::isValidBinary(bin) ){
385 //invalid binary for some reason - open a dialog to warn the user instead
386 ShowErrorDialog(argc,argv, QString(QObject::tr("Could not find \"%1\". Please ensure it is installed first.")).arg(bin)+"\n\n"+cmd);
387 return 1;
388 }
389 if(DEBUG) qDebug() << "[lumina-open] Running Cmd:" << cmd;
390 int retcode = 0;
391 //Provide an override file for never watching running processes.
392 if(watch){ watch = !QFile::exists( QString(getenv("XDG_CONFIG_HOME"))+"/lumina-desktop/nowatch" ); }
393 //Do the slimmer run routine if no watching needed
394 if(!watch && path.isEmpty()){
395 //Nothing special about this one - just start it detached (less overhead)
396 QProcess::startDetached(cmd);
397 }else{
398 //Keep an eye on this process for errors and notify the user if it crashes
399 QString log;
400 if(cmd.contains("\\\\")){
401 //Special case (generally for Wine applications)
402 cmd = cmd.replace("\\\\","\\");
403 retcode = system(cmd.toLocal8Bit()); //need to run it through the "system" instead of QProcess
404 }else if(cmd=="internalcrashtest"){
405 log = "This is a sample crash log";
406 retcode = -1;
407 }else{
408 QProcess *p = new QProcess();
409 p->setProcessEnvironment(QProcessEnvironment::systemEnvironment());
410 if(!path.isEmpty() && QFile::exists(path)){
411 //qDebug() << " - Setting working path:" << path;
412 p->setWorkingDirectory(path);
413 }
414 p->start(cmd);
415
416 //Now check up on it once every minute until it is finished
417 while(!p->waitForFinished(60000)){
418 //qDebug() << "[lumina-open] process check:" << p->state();
419 if(p->state() != QProcess::Running){ break; } //somehow missed the finished signal
420 }
421 retcode = p->exitCode();
422 if( (p->exitStatus()==QProcess::CrashExit) && retcode ==0){ retcode=-1; } //so we catch it later
423 log = QString(p->readAllStandardError());
424 if(log.isEmpty()){ log = QString(p->readAllStandardOutput()); }
425 }
426 //qDebug() << "[lumina-open] Finished Cmd:" << cmd << retcode << p->exitStatus();
427 if( QFile::exists("/tmp/.luminastopping") ){ watch = false; } //closing down session - ignore "crashes" (app could have been killed during cleanup)
428 if( (retcode < 0) && watch){ //-1 is used internally for crashed processes - most apps return >=0
429 qDebug() << "[lumina-open] Application Error:" << retcode;
430 //Setup the application
431 QApplication App(argc, argv);
432 App.setAttribute(Qt::AA_UseHighDpiPixmaps);
433 LUtils::LoadTranslation(&App,"lumina-open");
434 //App.setApplicationName("LuminaOpen");
435 QMessageBox dlg(QMessageBox::Critical, QObject::tr("Application Error"), QObject::tr("The following application experienced an error and needed to close:")+"\n\n"+cmd );
436 dlg.setWindowFlags(Qt::Window);
437 if(!log.isEmpty()){ dlg.setDetailedText(log); }
438 dlg.exec();
439 }
440 }
441 return retcode;
442 }
443