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

Add the option to create symlinks directly from the App #631

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
41 changes: 41 additions & 0 deletions src/shared/shared.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,10 @@ QString getConfigFilePath() {

void createConfigFile(int askToMove,
const QString& destination,
const QString& symlinkPath,
int enableDaemon,
const QStringList& additionalDirsToWatch,
const QStringList& symlinks,
int monitorMountedFilesystems) {
auto configFilePath = getConfigFilePath();

Expand Down Expand Up @@ -139,6 +141,22 @@ void createConfigFile(int askToMove,
file.write("\n");
}

if (symlinkPath.isEmpty()) {
file.write("# symlinkpath = ~/.local/bin\n");
} else {
file.write("symlinkpath = ");
file.write(symlinkPath.toUtf8());
file.write("\n");
}

if(symlinks.empty()) {
file.write("# symlinks = ~/.local/bin/coolApp\n");
} else {
file.write("symlinks = ");
file.write(symlinks.join(':').toUtf8());
file.write("\n");
}

if (enableDaemon < 0) {
file.write("# enable_daemon = true\n");
} else {
Expand Down Expand Up @@ -263,6 +281,25 @@ QDir integratedAppImagesDestination() {
return DEFAULT_INTEGRATION_DESTINATION;
}

QDir integratedSymlinkDestination() {
auto config = getConfig();
static const QString keyName("AppImageLauncher/symlinkpath");

QString symlinkPath = DEFAULT_SYMLINK_DESTINATION;

if (config != nullptr && config->contains(keyName))
symlinkPath = config->value(keyName).toString();

QDir dir(symlinkPath);

if (!dir.exists(symlinkPath)) {
dir.mkpath(symlinkPath);
}

return symlinkPath;
}


class Mount {
private:
QString device;
Expand Down Expand Up @@ -965,6 +1002,10 @@ IntegrationState integrateAppImage(const QString& pathToAppImage, const QString&
return INTEGRATION_FAILED;
}
}

static const std::string command = "sudo ln -s " + pathToIntegratedAppImage.toStdString() + " /usr/bin/teste";

QProcess::execute(command.c_str());
}

if (!installDesktopFileAndIcons(pathToIntegratedAppImage))
Expand Down
10 changes: 8 additions & 2 deletions src/shared/shared.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ enum IntegrationState {
// currently hardcoded, can not be changed by users
static const auto DEFAULT_INTEGRATION_DESTINATION = QString(getenv("HOME")) + "/Applications/";

static const auto DEFAULT_SYMLINK_DESTINATION = QString(getenv("HOME")) + "/.local/bin";


// little convenience method to display warnings
void displayWarning(const QString& message);

Expand Down Expand Up @@ -65,8 +68,8 @@ IntegrationState integrateAppImage(const QString& pathToAppImage, const QString&
// askToMove and enableDaemon both are bools but represented as int to add some sort of "unset" state
// < 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);
void createConfigFile(int askToMove, const QString& destination, const QString& symlinkPath, int enableDaemon,
const QStringList& additionalDirsToWatch = {}, const QStringList& symlinks = {} , int monitorMountedFilesystems = -1);

// replaces ~ character in paths with real home directory, if necessary and possible
QString expandTilde(QString path);
Expand All @@ -77,6 +80,9 @@ QSettings* getConfig(QObject* parent = nullptr);
// return directory into which the integrated AppImages will be moved
QDir integratedAppImagesDestination();

// return directory into which the integrated symlinks will be placed
QDir integratedSymlinkDestination();

// additional directories to monitor for AppImages, and to permit AppImages to be within (i.e., shall not ask whether
// to move to the main location, if they're in one of these, it's all good)
QSet<QString> additionalAppImagesLocations(bool includeValidMountPoints = false);
Expand Down
2 changes: 1 addition & 1 deletion src/ui/first-run.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ private Q_SLOTS:

void writeConfigFile() {
bool askToMove = firstRunDialog->askMoveCheckBox->checkState() == Qt::Checked;
createConfigFile(askToMove ? 1 : 0, destinationDir, -1);
createConfigFile(askToMove ? 1 : 0, destinationDir, "", -1);
}
};

Expand Down
141 changes: 141 additions & 0 deletions src/ui/settings_dialog.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
#include <QFileDialog>
#include <QFileIconProvider>
#include <QStandardPaths>
#include <QInputDialog>
#include <QProcess>

// local
#include "settings_dialog.h"
Expand All @@ -28,10 +30,15 @@ SettingsDialog::SettingsDialog(QWidget* parent) : QDialog(parent), ui(new Ui::Se

connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &SettingsDialog::onDialogAccepted);
connect(ui->chooseAppsDirToolButton, &QToolButton::released, this, &SettingsDialog::onChooseAppsDirClicked);
connect(ui->chooseSymlinkDirButton, &QToolButton::released, this, &SettingsDialog::onChooseSymlinkDirClicked);
connect(ui->additionalDirsAddButton, &QToolButton::released, this, &SettingsDialog::onAddDirectoryToWatchButtonClicked);
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->symlinkList, &QListWidget::itemActivated, this, &SettingsDialog::onSymlinkItemActivated);
connect(ui->symlinkList, &QListWidget::itemClicked, this, &SettingsDialog::onSymlinkItemActivated);
connect(ui->addLinkButton, &QToolButton::released, this, &SettingsDialog::onSymlinkAddButtonClicked);
connect(ui->removeLinkButton, &QToolButton::released, this, &SettingsDialog::onRemoveSymlinkButtonClicked);

QStringList availableFeatures;

Expand Down Expand Up @@ -96,6 +103,46 @@ void SettingsDialog::addDirectoryToWatchToListView(const QString& dirPath) {
ui->additionalDirsListWidget->addItem(item);
}

void SettingsDialog::addSymlinkToListView(const QString& symlinkName) {
// empty paths are not permitted
if (symlinkName.isEmpty())
return;

// Doesn't add duplicated items.
QList currentItems = ui->symlinkList->findItems(symlinkName, Qt::MatchFlag::MatchExactly);
if(!currentItems.empty() && currentItems.first() != nullptr) {
return;
}

const QFile file(symlinkName);

QIcon icon;

auto findIcon = [](const std::initializer_list<QString>& names) {
for (const auto& i : names) {
auto icon = QIcon::fromTheme(i, loadIconWithFallback(i));

if (!icon.isNull())
return icon;
}

return QIcon{};
};

if (file.exists()) {
icon = findIcon({"emblem-symbolic-link"});
} else {
icon = findIcon({"emblem-unreadable"});
}

if (icon.isNull()) {
qDebug() << "item icon unavailable, using fallback";
}

auto* item = new QListWidgetItem(icon, symlinkName);
ui->symlinkList->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();
Expand All @@ -104,11 +151,21 @@ void SettingsDialog::loadSettings() {
ui->daemonIsEnabledCheckBox->setChecked(daemonIsEnabled);
ui->askMoveCheckBox->setChecked(askMoveChecked);
ui->applicationsDirLineEdit->setText(settingsFile->value("AppImageLauncher/destination").toString());
ui->symlinkLineEdit->setText(integratedSymlinkDestination().absolutePath());

const auto additionalDirsPath = settingsFile->value("appimagelauncherd/additional_directories_to_watch", "").toString();
for (const auto& dirPath : additionalDirsPath.split(":")) {
addDirectoryToWatchToListView(dirPath);
}

const auto symlinks = settingsFile->value("AppImageLauncher/symlinks", "").toString();
for (const auto& linkPath : symlinks.split(":")) {
QFile file(linkPath);

if(file.exists()) {
addSymlinkToListView(linkPath);
}
}
}
}

Expand All @@ -119,6 +176,7 @@ void SettingsDialog::onDialogAccepted() {

void SettingsDialog::saveSettings() {
QStringList additionalDirsToWatch;
QStringList symlinks;

{
QListWidgetItem* currentItem;
Expand All @@ -128,6 +186,14 @@ void SettingsDialog::saveSettings() {
}
}

{
QListWidgetItem* currentItem;

for (int i = 0; (currentItem = ui->symlinkList->item(i)) != nullptr; ++i) {
symlinks << 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;
Expand All @@ -146,8 +212,10 @@ void SettingsDialog::saveSettings() {

createConfigFile(ui->askMoveCheckBox->isChecked(),
ui->applicationsDirLineEdit->text(),
ui->symlinkLineEdit->text(),
ui->daemonIsEnabledCheckBox->isChecked(),
additionalDirsToWatch,
symlinks,
monitorMountedFilesystems);

// reload settings
Expand Down Expand Up @@ -185,6 +253,23 @@ void SettingsDialog::onChooseAppsDirClicked() {
}
}

void SettingsDialog::onChooseSymlinkDirClicked() {
QFileDialog fileDialog(this);

fileDialog.setFileMode(QFileDialog::DirectoryOnly);
fileDialog.setWindowTitle(tr("Select Symlinks directory"));
fileDialog.setDirectory(integratedSymlinkDestination().absolutePath());

// 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()) {
QString dirPath = fileDialog.selectedFiles().first();
ui->symlinkLineEdit->setText(dirPath);
}
}

void SettingsDialog::onAddDirectoryToWatchButtonClicked() {
QFileDialog fileDialog(this);

Expand All @@ -202,6 +287,35 @@ void SettingsDialog::onAddDirectoryToWatchButtonClicked() {
}
}

void SettingsDialog::onSymlinkAddButtonClicked() {
QFileDialog fileDialog(this);

fileDialog.setFileMode(QFileDialog::ExistingFile);
fileDialog.selectMimeTypeFilter("*.AppImage");
fileDialog.setDirectory(integratedAppImagesDestination().absolutePath());

fileDialog.setOption(QFileDialog::DontUseNativeDialog, true);

if (fileDialog.exec()) {
QString filePath = fileDialog.selectedFiles().first();
QInputDialog symlinkNameDialog(this);

symlinkNameDialog.setInputMode(QInputDialog::TextInput);
symlinkNameDialog.setOkButtonText("Add");
symlinkNameDialog.setLabelText("Select the symlink name");
if (symlinkNameDialog.exec()) {
QString linkName = symlinkNameDialog.textValue();
QString fullSymlinkPath = integratedSymlinkDestination().absolutePath() + "/" + linkName;
system(QString("ln -s %1 %2").arg(filePath, fullSymlinkPath).toStdString().c_str());
addSymlinkToListView(fullSymlinkPath);

// As the actions is taken right way, it should apply the settings
// may create another function to update just this entry
saveSettings();
}
}
}

void SettingsDialog::onRemoveDirectoryToWatchButtonClicked() {
auto* widget = ui->additionalDirsListWidget;

Expand All @@ -222,7 +336,34 @@ void SettingsDialog::onRemoveDirectoryToWatchButtonClicked() {
}
}

void SettingsDialog::onRemoveSymlinkButtonClicked() {
auto* widget = ui->symlinkList;

auto* currentItem = widget->currentItem();

if (currentItem == nullptr)
return;

const auto index = widget->row(currentItem);

system(QString("rm %1").arg(currentItem->text()).toStdString().c_str());

// 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->removeLinkButton->setEnabled(false);
}
}

void SettingsDialog::onDirectoryToWatchItemActivated(QListWidgetItem* item) {
// we activate the button based on whether there's an item selected
ui->additionalDirsRemoveButton->setEnabled(item != nullptr);
}

void SettingsDialog::onSymlinkItemActivated(QListWidgetItem* item) {
// we activate the button based on whether there's an item selected
ui->removeLinkButton->setEnabled(item != nullptr);
}
5 changes: 5 additions & 0 deletions src/ui/settings_dialog.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,13 @@ Q_OBJECT

protected slots:
void onChooseAppsDirClicked();
void onChooseSymlinkDirClicked();
void onAddDirectoryToWatchButtonClicked();
void onSymlinkAddButtonClicked();
void onRemoveDirectoryToWatchButtonClicked();
void onDirectoryToWatchItemActivated(QListWidgetItem* item);
void onRemoveSymlinkButtonClicked();
void onSymlinkItemActivated(QListWidgetItem* item);

void onDialogAccepted();

Expand All @@ -37,6 +41,7 @@ protected slots:
void toggleDaemon();

void addDirectoryToWatchToListView(const QString& dirPath);
void addSymlinkToListView(const QString& symlinkName);

Ui::SettingsDialog* ui;
QSettings* settingsFile;
Expand Down