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 @@ + + + + + + + + image/svg+xml + + + + + + + + + + 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 + + + + + + + + +