Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds support for .AppImage and implements parsing for .desktop #10

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,9 @@ compile_commands.json

# QtCreator local machine specific files for imported projects
*creator.user*

# ignore build directory
build
.kdev4/

launch.kdev4
2 changes: 2 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ find_package(Qt5 COMPONENTS Widgets REQUIRED)

add_executable(launch
src/main.cpp
src/utils/finder.h
src/utils//finder.cpp
)

target_link_libraries(launch PRIVATE Qt5::Widgets)
Expand Down
140 changes: 51 additions & 89 deletions src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,11 @@
#include <QElapsedTimer>
#include <QRegExpValidator>
#include <QApplication>
#include <QSettings>
#include <QIcon>
#include <QStyle>
#include <QVersionNumber>
#include "utils/finder.h"

/*
* This tool handles four types of applications:
Expand Down Expand Up @@ -125,55 +128,6 @@ void handleError(QDetachableProcess *p, QString errorString){
}
}


QFileInfoList findAppsInside(QStringList locationsContainingApps, QFileInfoList candidates, QString firstArg)
{
foreach (QString directory, locationsContainingApps) {
QDirIterator it(directory, QDirIterator::NoIteratorFlags);
while (it.hasNext()) {
QString filename = it.next();
// qDebug() << "probono: Processing" << filename;
QString nameWithoutSuffix = QFileInfo(QDir(filename).canonicalPath()).baseName();
QFileInfo file(filename);
if (file.fileName() == firstArg + ".app"){
QString AppCand = filename + "/" + nameWithoutSuffix;
qDebug() << "################### Checking" << AppCand;
if(QFileInfo(AppCand).exists() == true){
qDebug() << "# Found" << AppCand;
candidates.append(AppCand);
}
}
else if (file.fileName() == firstArg + ".AppDir"){
QString AppCand = filename + "/" + "AppRun";
qDebug() << "################### Checking" << AppCand;
if(QFileInfo(AppCand).exists() == true){
qDebug() << "# Found" << AppCand;
candidates.append(AppCand);
}
}
else if (file.fileName() == firstArg + ".desktop") {
// .desktop file
qDebug() << "# Found" << file.fileName() << "TODO: Parse it for Exec=";
}
else if (locationsContainingApps.contains(filename) == false && file.isDir() && filename.endsWith("/..") == false && filename.endsWith("/.") == false && filename.endsWith(".app") == false && filename.endsWith(".AppDir") == false) {
// Now we have a directory that is not an .app bundle nor an .AppDir
// Shall we descend into it? Only if it contains at least one application, to optimize for speed
// by not descending into directory trees that do not contain any applications at all. Can make
// a big difference.
QStringList nameFilter({"*.app", "*.AppDir", "*.desktop"});
QDir directory(filename);
int numberOfAppsInDirectory = directory.entryList(nameFilter).length();
if(numberOfAppsInDirectory > 0) {
qDebug() << "# Descending into" << filename;
QStringList locationsToBeChecked = {filename};
candidates = findAppsInside(locationsToBeChecked, candidates, firstArg);
}
}
}
}
return candidates;
}

int main(int argc, char *argv[])
{

Expand Down Expand Up @@ -224,7 +178,8 @@ int main(int argc, char *argv[])
}

QDetachableProcess p;

Finder finder;

// Check whether the first argument exists or is on the $PATH

QString executable = nullptr;
Expand All @@ -233,35 +188,11 @@ int main(int argc, char *argv[])
// First, try to find something we can launch at the path,
// either an executable or an .AppDir or an .app bundle
firstArg = args.first();
if (QFile::exists(firstArg)){
QFileInfo info = QFileInfo(firstArg);
if ( firstArg.endsWith(".AppDir") || firstArg.endsWith(".app") ){
qDebug() << "# Found" << firstArg;
QString candidate;
if(firstArg.endsWith(".AppDir")) {
candidate = firstArg + "/AppRun";
}
else {
// The .app could be a symlink, so we need to determine the nameWithoutSuffix from its target
QFileInfo fileInfo = QFileInfo(QDir(firstArg).canonicalPath());
QString nameWithoutSuffix = QFileInfo(fileInfo.completeBaseName()).fileName();
candidate = firstArg + "/" + nameWithoutSuffix;
}

QFileInfo candinfo = QFileInfo(candidate);
if(candinfo.isExecutable()) {
executable = candidate;
}

}
else if (info.isExecutable()){
qDebug() << "# Found executable" << firstArg;
executable = args.first();
}
}


executable = finder.getExecutable(firstArg);

// Second, try to find an executable file on the $PATH
if(executable == nullptr){
if(executable == nullptr) {
QString candidate = QStandardPaths::findExecutable(firstArg);
if (candidate != "") {
qDebug() << "Found" << candidate << "on the $PATH";
Expand All @@ -286,21 +217,52 @@ int main(int argc, char *argv[])
// Iterate recursively through locationsContainingApps searching for AppRun files in matchingly named AppDirs

QFileInfoList candidates;
QString firstArgWithoutWellKnownSuffix = firstArg.replace(".AppDir", "").replace(".app", "").replace(".desktop" ,"");
QString firstArgWithoutWellKnownSuffix = firstArg.replace(".AppDir", "").replace(".app", "").replace(".desktop" ,"").replace(".AppImage", "");

candidates = findAppsInside(locationsContainingApps, candidates, firstArgWithoutWellKnownSuffix);
candidates = finder.findAppsInside(locationsContainingApps, candidates, firstArgWithoutWellKnownSuffix);

qDebug() << "Took" << timer.elapsed() << "milliseconds to find candidates via the filesystem";
qDebug() << "Candidates:" << candidates;

foreach (QFileInfo candidate, candidates) {
// Now that we may have collected different candidates, decide on which one to use
// e.g., the one with the highest self-declared version number. Again, a database might come in handy here
// For now, just use the first one
qDebug() << "Selected candidate:" << candidate.absoluteFilePath();
executable = candidate.absoluteFilePath();
break;
}

QFileInfo candidate = candidates.first().absoluteFilePath();
QFileInfoList::iterator it;

qDebug() << candidate;

// Attempt version detection
if (candidates.size() > 1) {

// todo: loop through and compare versions

for (int i = 0; i < candidates.size(); i++)
{
if (!candidate.fileName().contains("-")) {
candidate = candidates[i].absoluteFilePath();
continue;
}

try {
QStringList previousVersion = candidate.fileName().split("-");
QStringList curVersion = candidates[i].fileName().split("-");

QVersionNumber previousVer = QVersionNumber::fromString(previousVersion[1]);
QVersionNumber newVer = QVersionNumber::fromString(curVersion[1]);

int compare = QVersionNumber::compare(previousVer, newVer);
qDebug() << compare;
if (compare == -1) {
// previous one is older, use newer one
candidate = candidates[i].absoluteFilePath();
}
} catch(std::exception &e) {
// catch any exeption that may occure
qDebug() << "Failed to compare application versions";
}
}
}

qDebug() << "Selected candidate:" << candidate.absoluteFilePath();
executable = candidate.absoluteFilePath();
}

p.setProgram(executable);
Expand Down
125 changes: 125 additions & 0 deletions src/utils/finder.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
#include "finder.h"

#include <QDebug>
#include <QFile>
#include <QFileInfo>
#include <QDir>
#include <QDirIterator>
#include <QSettings>

QFileInfoList Finder::findAppsInside(QStringList locationsContainingApps, QFileInfoList candidates, QString firstArg)
{
foreach (QString directory, locationsContainingApps) {
QDirIterator it(directory, QDirIterator::NoIteratorFlags);
while (it.hasNext()) {
QString filename = it.next();
// qDebug() << "probono: Processing" << filename;
QString nameWithoutSuffix = QFileInfo(QDir(filename).canonicalPath()).baseName();
QFileInfo file(filename);

if (file.fileName() == firstArg + ".app") {
qDebug() << filename;
QString AppCand = filename + "/" + nameWithoutSuffix;
qDebug() << "################### Checking" << AppCand;
if(QFileInfo(AppCand).exists() == true){
qDebug() << "# Found" << AppCand;
candidates.append(AppCand);
}
}
else if (file.fileName() == firstArg + ".AppDir") {
QString AppCand = filename + "/" + "AppRun";

qDebug() << "################### Checking" << AppCand;
if(QFileInfo(AppCand).exists() == true){
qDebug() << "# Found" << AppCand;
candidates.append(AppCand);
}
}
else if (file.fileName() == firstArg + ".AppImage" || file.fileName() == firstArg.replace(" ", "_") + ".AppImage" || file.fileName().endsWith(".AppName") & file.fileName().startsWith(firstArg + "-") || file.fileName().startsWith(firstArg.replace(" ", "_") + "-")) {
QString AppCand = getExecutable(filename);
candidates.append(AppCand);
}
else if (file.fileName() == firstArg + ".desktop") {
// load the .desktop file for parsing - QSettings::IniFormat returns values as strings by default
// see https://doc.qt.io/qt-5/qsettings.html
QSettings desktopFile(filename, QSettings::IniFormat);
QString AppCand = desktopFile.value("Desktop Entry/Exec").toString();

// null safety check
if (AppCand != NULL) {
qDebug() << "# Found" << AppCand;
candidates.append(AppCand);
}
}
else if (locationsContainingApps.contains(filename) == false && file.isDir() && filename.endsWith("/..") == false && filename.endsWith("/.") == false && filename.endsWith(".app") == false && filename.endsWith(".AppDir") == false && filename.endsWith(".AppImage") == false) {
// Now we have a directory that is not an .app bundle nor an .AppDir
// Shall we descend into it? Only if it contains at least one application, to optimize for speed
// by not descending into directory trees that do not contain any applications at all. Can make
// a big difference.
QStringList nameFilter({"*.app", "*.AppDir", "*.desktop", "*.AppImage"});
QDir directory(filename);
int numberOfAppsInDirectory = directory.entryList(nameFilter).length();
if(numberOfAppsInDirectory > 0) {
qDebug() << "# Descending into" << filename;
QStringList locationsToBeChecked = {filename};
candidates = findAppsInside(locationsToBeChecked, candidates, firstArg);
}
}
}
}

return candidates;
}

QString Finder::getExecutable(QString &firstArg)
{

QString executable = nullptr;

// check if the file exists
/*if (!QFile::exists(firstArg)) {
// try replacing space with _
firstArg = firstArg.replace(" ", "_");
}*/

if (QFile::exists(firstArg)) {
// get the file info
QFileInfo info = QFileInfo(firstArg);

if (firstArg.endsWith(".AppDir") || firstArg.endsWith(".app") || firstArg.endsWith(".AppImage")) {
qDebug() << "# Found" << firstArg;

// The potential candidate
QString candidate;

if(firstArg.endsWith(".AppDir")) {
candidate = firstArg + "/AppRun";
}
else if (firstArg.endsWith(".AppImage")) {
// this is a .AppImage file, we have nothing else to do here, so just make it a candidate
candidate = firstArg;
}
else {
// The .app could be a symlink, so we need to determine the nameWithoutSuffix from its target
QFileInfo fileInfo = QFileInfo(QDir(firstArg).canonicalPath());
QString nameWithoutSuffix = QFileInfo(fileInfo.completeBaseName()).fileName();
candidate = firstArg + "/" + nameWithoutSuffix;
}

QFileInfo candinfo = QFileInfo(candidate);
if (candinfo.isExecutable()) {
executable = candidate;
}

}
else if (info.isExecutable()){
qDebug() << "# Found executable" << firstArg;
executable = firstArg;
}
}

return executable;
}



13 changes: 13 additions & 0 deletions src/utils/finder.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#ifndef FINDER_H
#define FINDER_H

#include <QFileInfo>

class Finder
{
public:
QFileInfoList findAppsInside(QStringList locationsContainingApps, QFileInfoList candidates, QString firstArg);
QString getExecutable(QString &firstArg);
};

#endif