diff --git a/resources/fallback-icons/application-vnd.appimage.svg b/resources/fallback-icons/application-vnd.appimage.svg
new file mode 100644
index 00000000..e0f2e037
--- /dev/null
+++ b/resources/fallback-icons/application-vnd.appimage.svg
@@ -0,0 +1,64 @@
+
+
diff --git a/resources/fallback-icons/document-new.svg b/resources/fallback-icons/document-new.svg
new file mode 120000
index 00000000..cf76b1ee
--- /dev/null
+++ b/resources/fallback-icons/document-new.svg
@@ -0,0 +1 @@
+application-vnd.appimage.svg
\ No newline at end of file
diff --git a/resources/fallback-icons/folder-new.svg b/resources/fallback-icons/folder-new.svg
new file mode 120000
index 00000000..15062ce2
--- /dev/null
+++ b/resources/fallback-icons/folder-new.svg
@@ -0,0 +1 @@
+folder.svg
\ No newline at end of file
diff --git a/src/shared/shared.cpp b/src/shared/shared.cpp
index 7432e8f6..ff399419 100644
--- a/src/shared/shared.cpp
+++ b/src/shared/shared.cpp
@@ -109,7 +109,8 @@ void createConfigFile(int askToMove,
const QString& destination,
int enableDaemon,
const QStringList& additionalDirsToWatch,
- int monitorMountedFilesystems) {
+ int monitorMountedFilesystems,
+ const QStringList& excludePaths) {
auto configFilePath = getConfigFilePath();
QFile file(configFilePath);
@@ -151,6 +152,14 @@ void createConfigFile(int askToMove,
file.write("\n");
}
+ if (excludePaths.empty()) {
+ file.write("# exclude_paths = /opt/appimages/:/even/more/app-images.AppImage\n");
+ } else {
+ file.write("exclude_paths = ");
+ file.write(excludePaths.join(':').toUtf8());
+ file.write("\n");
+ }
+
file.write("\n\n");
// daemon configs
@@ -400,6 +409,29 @@ bool shallMonitorMountedFilesystems(const QSettings* config) {
return config->value("appimagelauncherd/monitor_mounted_filesystems", "false").toBool();
}
+bool sanitizePath(QString& path) {
+ // empty values will, for some reason, be interpreted as "use the home directory"
+ // as we don't want to accidentally monitor the home directory, we need to skip those values
+ if (path.isEmpty()) {
+ qDebug() << "skipping empty directory path";
+ return false;
+ }
+
+ // make sure to have full path
+ qDebug() << "path before tilde expansion:" << path;
+ path = expandTilde(path);
+ qDebug() << "path after tilde expansion:" << path;
+
+ // non-absolute paths which don't contain a tilde cannot be resolved safely, they likley depend on the cwd
+ // therefore, we need to ignore those
+ if (!QFileInfo(path).isAbsolute()) {
+ std::cerr << "Warning: path " << path.toStdString() << " can not be resolved, skipping" << std::endl;
+ return false;
+ }
+
+ return true;
+}
+
QDirSet getAdditionalDirectoriesFromConfig(const QSettings* config) {
Q_ASSERT(config != nullptr);
@@ -410,33 +442,16 @@ QDirSet getAdditionalDirectoriesFromConfig(const QSettings* config) {
QDirSet additionalDirs{};
for (auto dirPath : configValue.split(":")) {
- // empty values will, for some reason, be interpreted as "use the home directory"
- // as we don't want to accidentally monitor the home directory, we need to skip those values
- if (dirPath.isEmpty()) {
- qDebug() << "skipping empty directory path";
- continue;
- }
-
- // make sure to have full path
- qDebug() << "path before tilde expansion:" << dirPath;
- dirPath = expandTilde(dirPath);
- qDebug() << "path after tilde expansion:" << dirPath;
-
- // non-absolute paths which don't contain a tilde cannot be resolved safely, they likley depend on the cwd
- // therefore, we need to ignore those
- if (!QFileInfo(dirPath).isAbsolute()) {
- std::cerr << "Warning: path " << dirPath.toStdString() << " can not be resolved, skipping" << std::endl;
+ if(!sanitizePath(dirPath)) {
continue;
}
- const QDir dir(dirPath);
-
- if (!dir.exists()) {
+ if (!QDir(dirPath).exists()) {
std::cerr << "Warning: could not find directory " << dirPath.toStdString() << ", skipping" << std::endl;
continue;
}
- additionalDirs.insert(dir);
+ additionalDirs.insert(dirPath);
}
return additionalDirs;
@@ -1370,3 +1385,28 @@ void setUpFallbackIconPaths(QWidget* parent) {
button->setIcon(newIcon);
}
}
+
+
+bool isPathExcluded(const QSettings* config, const QString& path) {
+ Q_ASSERT(config != nullptr);
+
+ constexpr auto configKey = "AppImageLauncher/exclude_paths";
+ const auto configValue = config->value(configKey, "").toString();
+ qDebug() << configKey << "value:" << configValue;
+
+ for (auto excludePath : configValue.split(":")) {
+ if(!sanitizePath(excludePath)) {
+ continue;
+ }
+
+ if (QFileInfo(excludePath) == QFileInfo(path)) {
+ return true;
+ }
+
+ if (QDir(excludePath).exists() && QFileInfo(path).absoluteFilePath().startsWith(excludePath)) {
+ return true;
+ }
+ }
+
+ return false;
+}
\ No newline at end of file
diff --git a/src/shared/shared.h b/src/shared/shared.h
index b4415ffb..604d952f 100644
--- a/src/shared/shared.h
+++ b/src/shared/shared.h
@@ -66,7 +66,7 @@ IntegrationState integrateAppImage(const QString& pathToAppImage, const QString&
// < 0: unset; 0 = false; > 0 = true
// destination is a string that, when empty, will be interpreted as "use default"
void createConfigFile(int askToMove, const QString& destination, int enableDaemon,
- const QStringList& additionalDirsToWatch = {}, int monitorMountedFilesystems = -1);
+ const QStringList& additionalDirsToWatch = {}, int monitorMountedFilesystems = -1, const QStringList& excludePaths = {});
// replaces ~ character in paths with real home directory, if necessary and possible
QString expandTilde(QString path);
@@ -131,3 +131,6 @@ QIcon loadIconWithFallback(const QString& iconName);
// sets up paths to fallback icons bundled with AppImageLauncher
void setUpFallbackIconPaths(QWidget*);
+
+// Check if a given path is in the exclude list
+bool isPathExcluded(const QSettings* config, const QString& path);
diff --git a/src/ui/main.cpp b/src/ui/main.cpp
index c99e4ae4..04da701b 100644
--- a/src/ui/main.cpp
+++ b/src/ui/main.cpp
@@ -267,6 +267,10 @@ int main(int argc, char** argv) {
system("systemctl --user stop appimagelauncherd.service");
}
+ if (isPathExcluded(config, pathToAppImage)) {
+ return runAppImage(pathToAppImage, appImageArgv.size(), appImageArgv.data());
+ }
+
// beyond the next block, the code requires a UI
// as we don't want to offer integration over a headless connection, we just run the AppImage
if (isHeadless()) {
diff --git a/src/ui/settings_dialog.cpp b/src/ui/settings_dialog.cpp
index c2b451de..548feb2f 100644
--- a/src/ui/settings_dialog.cpp
+++ b/src/ui/settings_dialog.cpp
@@ -32,6 +32,11 @@ SettingsDialog::SettingsDialog(QWidget* parent) : QDialog(parent), ui(new Ui::Se
connect(ui->additionalDirsRemoveButton, &QToolButton::released, this, &SettingsDialog::onRemoveDirectoryToWatchButtonClicked);
connect(ui->additionalDirsListWidget, &QListWidget::itemActivated, this, &SettingsDialog::onDirectoryToWatchItemActivated);
connect(ui->additionalDirsListWidget, &QListWidget::itemClicked, this, &SettingsDialog::onDirectoryToWatchItemActivated);
+ connect(ui->excludeAddDirectoryButton, &QToolButton::released, this, &SettingsDialog::onAddExcludeDirButtonClicked);
+ connect(ui->excludeAddFileButton, &QToolButton::released, this, &SettingsDialog::onAddExcludeFileButtonClicked);
+ connect(ui->excludeRemoveButton, &QToolButton::released, this, &SettingsDialog::onRemoveExcludeButtonClicked);
+ connect(ui->excludeListWidget, &QListWidget::itemActivated, this, &SettingsDialog::onExcludeItemActivated);
+ connect(ui->excludeListWidget, &QListWidget::itemClicked, this, &SettingsDialog::onExcludeItemActivated);
QStringList availableFeatures;
@@ -96,6 +101,48 @@ void SettingsDialog::addDirectoryToWatchToListView(const QString& dirPath) {
ui->additionalDirsListWidget->addItem(item);
}
+
+void SettingsDialog::addExcludeToListView(const QString& fileOrDirPath) {
+ // empty paths are not permitted
+ if (fileOrDirPath.isEmpty())
+ return;
+
+ const QFileInfo file(fileOrDirPath);
+
+ // // we don't want to redundantly add the main integration directory
+ // if (dir == integratedAppImagesDestination())
+ // return;
+
+ QIcon icon;
+
+ auto findIcon = [](const std::initializer_list& names) {
+ for (const auto& i : names) {
+ auto icon = QIcon::fromTheme(i, loadIconWithFallback(i));
+
+ if (!icon.isNull())
+ return icon;
+ }
+
+ return QIcon{};
+ };
+
+ if (file.isFile()) {
+ icon = findIcon({"application-vnd.appimage"});
+ } else if (file.isDir()) {
+ icon = findIcon({"folder"});
+ } else {
+ // TODO: search for more meaningful icon, "remove" doesn't really show the directory is missing
+ icon = findIcon({"remove"});
+ }
+
+ if (icon.isNull()) {
+ qDebug() << "item icon unavailable, using fallback";
+ }
+
+ auto* item = new QListWidgetItem(icon, fileOrDirPath);
+ ui->excludeListWidget->addItem(item);
+}
+
void SettingsDialog::loadSettings() {
const auto daemonIsEnabled = settingsFile->value("AppImageLauncher/enable_daemon", "true").toBool();
const auto askMoveChecked = settingsFile->value("AppImageLauncher/ask_to_move", "true").toBool();
@@ -109,6 +156,11 @@ void SettingsDialog::loadSettings() {
for (const auto& dirPath : additionalDirsPath.split(":")) {
addDirectoryToWatchToListView(dirPath);
}
+
+ const auto excludePaths = settingsFile->value("AppImageLauncher/exclude_paths", "").toString();
+ for (const auto& excludePath : excludePaths.split(":")) {
+ addExcludeToListView(excludePath);
+ }
}
}
@@ -128,6 +180,16 @@ void SettingsDialog::saveSettings() {
}
}
+ QStringList excludePaths;
+
+ {
+ QListWidgetItem* currentItem;
+
+ for (int i = 0; (currentItem = ui->excludeListWidget->item(i)) != nullptr; ++i) {
+ excludePaths << currentItem->text();
+ }
+ }
+
// temporary workaround to fill in the monitorMountedFilesystems with the same value it had in the old settings
// this is supposed to support the option while hiding it in the settings
int monitorMountedFilesystems = -1;
@@ -148,7 +210,8 @@ void SettingsDialog::saveSettings() {
ui->applicationsDirLineEdit->text(),
ui->daemonIsEnabledCheckBox->isChecked(),
additionalDirsToWatch,
- monitorMountedFilesystems);
+ monitorMountedFilesystems,
+ excludePaths);
// reload settings
loadSettings();
@@ -226,3 +289,66 @@ void SettingsDialog::onDirectoryToWatchItemActivated(QListWidgetItem* item) {
// we activate the button based on whether there's an item selected
ui->additionalDirsRemoveButton->setEnabled(item != nullptr);
}
+
+void SettingsDialog::onAddExcludeDirButtonClicked() {
+ QFileDialog fileDialog(this);
+
+ fileDialog.setFileMode(QFileDialog::DirectoryOnly);
+ fileDialog.setWindowTitle(tr("Select directories to exclude"));
+ fileDialog.setDirectory(QStandardPaths::locate(QStandardPaths::HomeLocation, ".", QStandardPaths::LocateDirectory));
+
+ // Gtk+ >= 3 segfaults when trying to use the native dialog, therefore we need to enforce the Qt one
+ // See #218 for more information
+ fileDialog.setOption(QFileDialog::DontUseNativeDialog, true);
+
+
+ if (fileDialog.exec()) {
+ for (const auto& file : fileDialog.selectedFiles()) {
+ addExcludeToListView(file.endsWith('/') ? file : file + '/');
+ }
+ }
+}
+
+void SettingsDialog::onAddExcludeFileButtonClicked() {
+ QFileDialog fileDialog(this);
+
+ fileDialog.setFileMode(QFileDialog::ExistingFile);
+ fileDialog.setWindowTitle(tr("Select files to exclude"));
+ fileDialog.setMimeTypeFilters({"application/vnd.appimage"});
+ fileDialog.setDirectory(QStandardPaths::locate(QStandardPaths::HomeLocation, ".", QStandardPaths::LocateDirectory));
+
+ // Gtk+ >= 3 segfaults when trying to use the native dialog, therefore we need to enforce the Qt one
+ // See #218 for more information
+ fileDialog.setOption(QFileDialog::DontUseNativeDialog, true);
+
+ if (fileDialog.exec()) {
+ for (const auto& file : fileDialog.selectedFiles()) {
+ addExcludeToListView(file);
+ }
+ }
+}
+
+void SettingsDialog::onRemoveExcludeButtonClicked() {
+ auto* widget = ui->excludeListWidget;
+
+ auto* currentItem = widget->currentItem();
+
+ if (currentItem == nullptr)
+ return;
+
+ const auto index = widget->row(currentItem);
+
+ // after taking it, we have to delete it ourselves, Qt docs say
+ auto deletedItem = widget->takeItem(index);
+ delete deletedItem;
+
+ // we should deactivate the remove button once the last item is gone
+ if (widget->item(0) == nullptr) {
+ ui->excludeRemoveButton->setEnabled(false);
+ }
+}
+
+void SettingsDialog::onExcludeItemActivated(QListWidgetItem* item) {
+ // we activate the button based on whether there's an item selected
+ ui->excludeRemoveButton->setEnabled(item != nullptr);
+}
\ No newline at end of file
diff --git a/src/ui/settings_dialog.h b/src/ui/settings_dialog.h
index ddd75759..3e367a61 100644
--- a/src/ui/settings_dialog.h
+++ b/src/ui/settings_dialog.h
@@ -27,6 +27,11 @@ protected slots:
void onRemoveDirectoryToWatchButtonClicked();
void onDirectoryToWatchItemActivated(QListWidgetItem* item);
+ void onAddExcludeDirButtonClicked();
+ void onAddExcludeFileButtonClicked();
+ void onRemoveExcludeButtonClicked();
+ void onExcludeItemActivated(QListWidgetItem* item);
+
void onDialogAccepted();
private:
@@ -37,6 +42,7 @@ protected slots:
void toggleDaemon();
void addDirectoryToWatchToListView(const QString& dirPath);
+ void addExcludeToListView(const QString& fileOrDirPath);
Ui::SettingsDialog* ui;
QSettings* settingsFile;
diff --git a/src/ui/settings_dialog.ui b/src/ui/settings_dialog.ui
index bf6c03ad..e2365b43 100644
--- a/src/ui/settings_dialog.ui
+++ b/src/ui/settings_dialog.ui
@@ -126,6 +126,71 @@
+ -
+
+
+
+
+
+ Add directories and AppImage files which should bypass the launcher
+
+
+
-
+
+
+ -
+
+
-
+
+
+ Add new files to list
+
+
+
+
+
+
+ -
+
+
+ Add new directories to list
+
+
+
+
+
+
+ -
+
+
+ false
+
+
+ Remove selected entry from list
+
+
+
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 15
+ 13
+
+
+
+
+
+
+
+
+
-