//=========================================== // Lumina-DE source code // Copyright (c) 2012-2017, Ken Moore // Available under the 3-clause BSD license // See the LICENSE file for full details //=========================================== #include <QApplication> #include <QX11Info> #include <QProcess> #include <QProcessEnvironment> #include <QFile> #include <QFileInfo> #include <QString> #include <QUrl> #include <QDebug> #include <QTranslator> #include <QMessageBox> #include <QLabel> #include <QDateTime> #include <QPixmap> #include <QColor> #include <QDesktopWidget> #include "LFileDialog.h" #include <LuminaXDG.h> #include <LUtils.h> #include <LuminaOS.h> #include <LuminaThemes.h> void printUsageInfo(){ qDebug() << "lumina-open: Application launcher for the Lumina Desktop Environment"; 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."; qDebug() << "Usage: lumina-open [-select] [-action <ActionID>] <absolute file path or URL>"; qDebug() << " lumina-open [-volumeup, -volumedown, -brightnessup, -brightnessdown]"; qDebug() << " [-select] (optional) flag to bypass any default application settings and show the application selector window"; qDebug() << " [-action <ActionID>] (optional) Flag to run one of the alternate Actions listed in a .desktop registration file rather than the main command."; qDebug() << "Special Flags:"; qDebug() << " \"-volume[up/down]\" Flag to increase/decrease audio volume by 5%"; qDebug() << " \"-brightness[up/down]\" Flag to increase/decrease screen brightness by 5%"; qDebug() << " \"-autostart-apps\" Flag to launch all the various apps which are registered with XDG autostart specification"; qDebug() << "\"-terminal\" Flag to open the terminal currently set as the user's default"; exit(1); } void ShowErrorDialog(int argc, char **argv, QString message){ //Setup the application QApplication App(argc, argv); LuminaThemeEngine theme(&App); LUtils::LoadTranslation(&App,"lumina-open"); QMessageBox dlg(QMessageBox::Critical, QObject::tr("File Error"), message ); dlg.exec(); exit(1); } void showOSD(int argc, char **argv, QString message){ //Setup the application QApplication App(argc, argv); LUtils::LoadTranslation(&App,"lumina-open"); //Display the OSD QPixmap pix(":/icons/OSD.png"); QLabel splash(0, Qt::Window | Qt::WindowStaysOnTopHint | Qt::X11BypassWindowManagerHint); splash.setWindowTitle(""); splash.setStyleSheet("QLabel{background: black; color: white; font-weight: bold; font-size: 13pt; margin: 1ex;}"); splash.setAlignment(Qt::AlignCenter); qDebug() << "Display OSD"; splash.setText(message); //Make sure it is centered on the current screen QPoint center = App.desktop()->screenGeometry(QCursor::pos()).center(); splash.move(center.x()-(splash.sizeHint().width()/2), center.y()-(splash.sizeHint().height()/2)); splash.show(); //qDebug() << " - show message"; //qDebug() << " - loop"; QDateTime end = QDateTime::currentDateTime().addMSecs(800); while(QDateTime::currentDateTime() < end){ App.processEvents(); } splash.hide(); } void LaunchAutoStart(){ QList<XDGDesktop*> xdgapps = LXDG::findAutoStartFiles(); for(int i=0; i<xdgapps.length(); i++){ //Generate command and clean up any stray "Exec" field codes (should not be any here) QString cmd = xdgapps[i]->getDesktopExec(); if(cmd.contains("%")){cmd = cmd.remove("%U").remove("%u").remove("%F").remove("%f").remove("%i").remove("%c").remove("%k").simplified(); } //Now run the command if(!cmd.isEmpty()){ qDebug() << " - Auto-Starting File:" << xdgapps[i]->filePath; QProcess::startDetached(cmd); } } //make sure we clean up all the xdgapps structures for(int i=0; i<xdgapps.length(); i++){ xdgapps[i]->deleteLater(); } } QString cmdFromUser(int argc, char **argv, QString inFile, QString extension, QString& path, bool showDLG=false){ //First check to see if there is a default for this extension QString defApp; if(extension=="mimetype"){ //qDebug() << "inFile:" << inFile; QStringList matches = LXDG::findAppMimeForFile(inFile, true).split("::::"); //allow multiple matches //qDebug() << "Matches:" << matches; for(int i=0; i<matches.length(); i++){ defApp = LXDG::findDefaultAppForMime(matches[i]); //qDebug() << "MimeType:" << matches[i] << defApp; if(!defApp.isEmpty()){ extension = matches[i]; break; } else if(i+1==matches.length()){ extension = matches[0]; } } }else{ defApp = LFileDialog::getDefaultApp(extension); } //qDebug() << "extension:" << extension << "defApp:" << defApp; if( !defApp.isEmpty() && !showDLG ){ if(defApp.endsWith(".desktop")){ XDGDesktop DF(defApp); if(DF.isValid()){ QString exec = DF.getDesktopExec(); if(!exec.isEmpty()){ qDebug() << "[lumina-open] Using default application:" << DF.name << "File:" << inFile; if(!DF.path.isEmpty()){ path = DF.path; } return exec; } } }else{ //Only binary given if(LUtils::isValidBinary(defApp)){ qDebug() << "[lumina-open] Using default application:" << defApp << "File:" << inFile; return defApp; //just use the binary } } //invalid default - reset it and continue on LFileDialog::setDefaultApp(extension, ""); } //Final catch: directory given - no valid default found - use lumina-fm if(extension=="inode/directory" && !showDLG){ return "lumina-fm"; } //No default set -- Start up the application selection dialog LTHEME::LoadCustomEnvSettings(); QApplication App(argc, argv); LuminaThemeEngine theme(&App); LUtils::LoadTranslation(&App,"lumina-open"); LFileDialog w; if(extension=="email" || extension.startsWith("x-scheme-handler/")){ //URL w.setFileInfo(inFile, extension, false); }else{ //File if(inFile.endsWith("/")){ inFile.chop(1); } w.setFileInfo(inFile.section("/",-1), extension, true); } w.show(); App.exec(); if(!w.appSelected){ return ""; } //Return the run path if appropriate if(!w.appPath.isEmpty()){ path = w.appPath; } //Just do the default application registration here for now // might move it to the runtime phase later after seeing that the app has successfully started if(w.setDefault){ if(!w.appFile.isEmpty()){ LFileDialog::setDefaultApp(extension, w.appFile); } else{ LFileDialog::setDefaultApp(extension, w.appExec); } }else{ LFileDialog::setDefaultApp(extension, ""); } //Now return the resulting application command return w.appExec; } void getCMD(int argc, char ** argv, QString& binary, QString& args, QString& path, bool& watch){ //Get the input file //Make sure to load the proper system encoding first LUtils::LoadTranslation(0,""); //bypass application modification QString inFile, ActionID; bool showDLG = false; //flag to bypass any default application setting if(argc > 1){ for(int i=1; i<argc; i++){ if(QString(argv[i]).simplified() == "-select"){ showDLG = true; }else if(QString(argv[i]).simplified() == "-testcrash"){ //Test the crash handler binary = "internalcrashtest"; watch=true; return; }else if(QString(argv[i]).simplified() == "-autostart-apps"){ LaunchAutoStart(); return; }else if(QString(argv[i]).simplified() == "-volumeup"){ int vol = LOS::audioVolume()+5; //increase 5% if(vol>100){ vol=100; } LOS::setAudioVolume(vol); showOSD(argc,argv, QString(QObject::tr("Audio Volume %1%")).arg(QString::number(vol)) ); return; }else if(QString(argv[i]).simplified() == "-volumedown"){ int vol = LOS::audioVolume()-5; //decrease 5% if(vol<0){ vol=0; } LOS::setAudioVolume(vol); showOSD(argc,argv, QString(QObject::tr("Audio Volume %1%")).arg(QString::number(vol)) ); return; }else if(QString(argv[i]).simplified() == "-brightnessup"){ int bright = LOS::ScreenBrightness(); if(bright > 0){ //brightness control available bright = bright+5; //increase 5% if(bright>100){ bright = 100; } LOS::setScreenBrightness(bright); showOSD(argc,argv, QString(QObject::tr("Screen Brightness %1%")).arg(QString::number(bright)) ); } return; }else if(QString(argv[i]).simplified() == "-brightnessdown"){ int bright = LOS::ScreenBrightness(); if(bright > 0){ //brightness control available bright = bright-5; //decrease 5% if(bright<0){ bright = 0; } LOS::setScreenBrightness(bright); showOSD(argc,argv, QString(QObject::tr("Screen Brightness %1%")).arg(QString::number(bright)) ); } return; }else if( (QString(argv[i]).simplified() =="-action") && (argc>(i+1)) ){ ActionID = QString(argv[i+1]); i++; //skip the next input }else if(QString(argv[i]).simplified()=="-terminal"){ inFile = LXDG::findDefaultAppForMime("application/terminal"); break; }else{ inFile = QString::fromLocal8Bit(argv[i]); break; } } }else{ printUsageInfo(); } //Make sure that it is a valid file/URL bool isFile=false; bool isUrl=false; QString extension; //Quick check/replacement for the URL syntax of a file if(inFile.startsWith("file://")){ inFile.remove(0,7); } //First make sure this is not a binary name first QString bin = inFile.section(" ",0,0).simplified(); if(LUtils::isValidBinary(bin) && !bin.endsWith(".desktop") && !QFileInfo(inFile).isDir() ){isFile=true; } //Now check what type of file this is else if(QFile::exists(inFile)){ isFile=true; } else if(QFile::exists(QDir::currentPath()+"/"+inFile)){isFile=true; inFile = QDir::currentPath()+"/"+inFile;} //account for relative paths else if(inFile.startsWith("mailto:")){ isUrl= true; } else if(QUrl(inFile).isValid() && !inFile.startsWith("/") ){ isUrl=true; } if( !isFile && !isUrl ){ ShowErrorDialog( argc, argv, QString(QObject::tr("Invalid file or URL: %1")).arg(inFile) ); } //Determing the type of file (extension) //qDebug() << "File Type:" << isFile << isUrl; if(isFile && extension.isEmpty()){ QFileInfo info(inFile); extension=info.suffix(); //qDebug() << " - Extension:" << extension; if(info.isDir()){ extension="inode/directory"; } else if(info.isExecutable() && (extension.isEmpty() || extension=="sh") ){ extension="binary"; } else if(extension!="desktop"){ extension="mimetype"; } //flag to check for mimetype default based on file } else if(isUrl && inFile.startsWith("mailto:")){ extension = "application/email"; } else if(isUrl && inFile.contains("://") ){ extension = "x-scheme-handler/"+inFile.section("://",0,0); } 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) //qDebug() << "Input:" << inFile << isFile << isUrl << extension; //if not an application - find the right application to open the file QString cmd; bool useInputFile = false; if(extension=="desktop" && !showDLG){ XDGDesktop DF(inFile); if(!DF.isValid()){ ShowErrorDialog( argc, argv, QString(QObject::tr("Application entry is invalid: %1")).arg(inFile) ); } switch(DF.type){ case XDGDesktop::APP: qDebug() << "Found .desktop application:" << ActionID; if(!DF.exec.isEmpty()){ cmd = DF.getDesktopExec(ActionID); qDebug() << "Got command:" << cmd; if(!DF.path.isEmpty()){ path = DF.path; } watch = DF.startupNotify || !DF.filePath.contains("/xdg/autostart/"); }else{ ShowErrorDialog( argc, argv, QString(QObject::tr("Application shortcut is missing the launching information (malformed shortcut): %1")).arg(inFile) ); } break; case XDGDesktop::LINK: if(!DF.url.isEmpty()){ //This is a URL - so adjust the input variables appropriately inFile = DF.url; cmd.clear(); extension = inFile.section(":",0,0); if(extension=="file"){ extension = "http"; } //local file URL - Make sure we use the default browser for a LINK type extension.prepend("x-scheme-handler/"); watch = DF.startupNotify || !DF.filePath.contains("/xdg/autostart/"); }else{ ShowErrorDialog( argc, argv, QString(QObject::tr("URL shortcut is missing the URL: %1")).arg(inFile) ); } break; case XDGDesktop::DIR: if(!DF.path.isEmpty()){ //This is a directory link - adjust inputs inFile = DF.path; cmd.clear(); extension = "inode/directory"; watch = DF.startupNotify || !DF.filePath.contains("/xdg/autostart/"); }else{ ShowErrorDialog( argc, argv, QString(QObject::tr("Directory shortcut is missing the path to the directory: %1")).arg(inFile) ); } break; default: qDebug() << DF.type << DF.name << DF.icon << DF.exec; ShowErrorDialog( argc, argv, QString(QObject::tr("Unknown type of shortcut : %1")).arg(inFile) ); } } if(cmd.isEmpty()){ if(extension=="binary" && !showDLG){ cmd = inFile; } else{ //Find out the proper application to use this file/directory useInputFile=true; cmd = cmdFromUser(argc, argv, inFile, extension, path, showDLG); if(cmd.isEmpty()){ return; } } } //Now assemble the exec string (replace file/url field codes as necessary) if(useInputFile){ args = inFile; //just to keep them distinct internally // NOTE: lumina-open is only designed for a single input file, // so no need to distinguish between the list codes (uppercase) // and the single-file codes (lowercase) //Special "inFile" format replacements for input codes if( (cmd.contains("%f") || cmd.contains("%F") ) ){ //Apply any special field replacements for the desired format inFile.replace("%20"," "); if(inFile.startsWith("file://")){ inFile.remove(0,7); } //chop that URL prefix off the front (should have happened earlier - just make sure) //Now replace the field codes cmd.replace("%f","\""+inFile+"\""); cmd.replace("%F","\""+inFile+"\""); }else if( (cmd.contains("%U") || cmd.contains("%u")) ){ //Apply any special field replacements for the desired format if(!inFile.contains("://") && !isUrl){ inFile.prepend("file://"); } //local file - add the extra flag inFile.replace(" ", "%20"); //Now replace the field codes cmd.replace("%u","\""+inFile+"\""); cmd.replace("%U","\""+inFile+"\""); }else{ //No field codes (or improper field codes given in the file - which is quite common) // - Just tack the input file on the end and let the app handle it as necessary inFile.replace("%20"," "); //assume a local-file format rather than URL format cmd.append(" \""+inFile+"\""); } } qDebug() << "Found Command:" << cmd << "Extension:" << extension; //Clean up any leftover "Exec" field codes (should have already been replaced earlier) if(cmd.contains("%")){cmd = cmd.remove("%U").remove("%u").remove("%F").remove("%f").remove("%i").remove("%c").remove("%k").simplified(); } binary = cmd; //pass this string to the calling function } int main(int argc, char **argv){ //Run all the actual code in a separate function to have as little memory usage // as possible aside from the main application when running //Make sure the XDG environment variables exist first LXDG::setEnvironmentVars(); //now get the command QString cmd, args, path; bool watch = true; //enable the crash handler by default (only disabled for some *.desktop inputs) getCMD(argc, argv, cmd, args, path, watch); //qDebug() << "Run CMD:" << cmd << args; //Now run the command (move to execvp() later?) if(cmd.isEmpty()){ return 0; } //no command to run (handled internally) QString bin = cmd.section(" ",0,0); if( !LUtils::isValidBinary(bin) ){ //invalid binary for some reason - open a dialog to warn the user instead QMessageBox::warning(0, QObject::tr("Binary Missing"), QString(QObject::tr("Could not find \"%1\". Please ensure it is installed first.")).arg(bin)+"\n\n"+cmd); return 1; } qDebug() << "[lumina-open] Running Cmd:" << cmd; int retcode = 0; //Provide an override file for never watching running processes. if(watch){ watch = !QFile::exists( QString(getenv("XDG_CONFIG_HOME"))+"/lumina-desktop/nowatch" ); } //Do the slimmer run routine if no watching needed if(!watch && path.isEmpty()){ //Nothing special about this one - just start it detached (less overhead) QProcess::startDetached(cmd); }else{ //Keep an eye on this process for errors and notify the user if it crashes QString log; if(cmd.contains("\\\\")){ //Special case (generally for Wine applications) cmd = cmd.replace("\\\\","\\"); retcode = system(cmd.toLocal8Bit()); //need to run it through the "system" instead of QProcess }else if(cmd=="internalcrashtest"){ log = "This is a sample crash log"; retcode = -1; }else{ QProcess *p = new QProcess(); p->setProcessEnvironment(QProcessEnvironment::systemEnvironment()); if(!path.isEmpty() && QFile::exists(path)){ //qDebug() << " - Setting working path:" << path; p->setWorkingDirectory(path); } p->start(cmd); //Now check up on it once every minute until it is finished while(!p->waitForFinished(60000)){ //qDebug() << "[lumina-open] process check:" << p->state(); if(p->state() != QProcess::Running){ break; } //somehow missed the finished signal } retcode = p->exitCode(); if( (p->exitStatus()==QProcess::CrashExit) && retcode ==0){ retcode=-1; } //so we catch it later log = QString(p->readAllStandardError()); if(log.isEmpty()){ log = QString(p->readAllStandardOutput()); } } //qDebug() << "[lumina-open] Finished Cmd:" << cmd << retcode << p->exitStatus(); if( QFile::exists("/tmp/.luminastopping") ){ watch = false; } //closing down session - ignore "crashes" (app could have been killed during cleanup) if( (retcode < 0) && watch){ //-1 is used internally for crashed processes - most apps return >=0 qDebug() << "[lumina-open] Application Error:" << retcode; //Setup the application QApplication App(argc, argv); LuminaThemeEngine theme(&App); LUtils::LoadTranslation(&App,"lumina-open"); //App.setApplicationName("LuminaOpen"); QMessageBox dlg(QMessageBox::Critical, QObject::tr("Application Error"), QObject::tr("The following application experienced an error and needed to close:")+"\n\n"+cmd ); dlg.setWindowFlags(Qt::Window); if(!log.isEmpty()){ dlg.setDetailedText(log); } dlg.exec(); } } return retcode; }