536 lines
19 KiB
C++
536 lines
19 KiB
C++
// Copyright Mirage authors & contributors <https://github.com/mirukana/mirage>
|
|
// and Moment contributors <https://gitlab.com/mx-moment/moment>
|
|
// SPDX-License-Identifier: LGPL-3.0-or-later
|
|
|
|
// This file creates the application, registers custom objects for QML
|
|
// and launches Window.qml (the root component).
|
|
|
|
#include <QDataStream> // must be first include to avoid clipboard.h errors
|
|
#include <QApplication>
|
|
#include <QQmlEngine>
|
|
#include <QQmlContext>
|
|
#include <QQmlComponent>
|
|
#include <QFileInfo>
|
|
#include <QQuickStyle>
|
|
#include <QFontDatabase>
|
|
#include <QDateTime>
|
|
#include <QStandardPaths>
|
|
#include <QDir>
|
|
#include <QFile>
|
|
#include <QLockFile>
|
|
#include <QMessageBox>
|
|
#include <QCommandLineParser>
|
|
#include <iostream>
|
|
#include <signal.h>
|
|
|
|
#ifdef Q_OS_UNIX
|
|
#include <unistd.h>
|
|
#endif
|
|
|
|
#include "utils.h"
|
|
#include "clipboard.h"
|
|
#include "clipboard_image_provider.h"
|
|
|
|
|
|
QLockFile *lockFile = nullptr;
|
|
|
|
|
|
void loggingHandler(
|
|
QtMsgType type,
|
|
const QMessageLogContext &context,
|
|
const QString &msg
|
|
) {
|
|
// Override default QML logger to provide colorful logging with times
|
|
|
|
Q_UNUSED(context)
|
|
|
|
// Hide dumb warnings about thing we can't fix without breaking
|
|
// compatibilty with Qt < 5.14/5.15
|
|
if (msg.contains("QML Binding: Not restoring previous value because"))
|
|
return;
|
|
|
|
if (msg.contains("QML Connections: Implicitly defined onFoo properties"))
|
|
return;
|
|
|
|
// Hide layout-related spam introduced in Qt 5.14
|
|
if (msg.contains("Qt Quick Layouts: Detected recursive rearrange."))
|
|
return;
|
|
|
|
const char* level =
|
|
type == QtDebugMsg ? "~" :
|
|
type == QtInfoMsg ? "i" :
|
|
type == QtWarningMsg ? "!" :
|
|
type == QtCriticalMsg ? "X" :
|
|
type == QtFatalMsg ? "F" :
|
|
"?";
|
|
|
|
QString boldColor = "", color = "", clearFormatting = "";
|
|
|
|
#ifdef Q_OS_UNIX
|
|
// Don't output escape codes if stderr is piped or redirected to a file
|
|
if (isatty(fileno(stderr))) {
|
|
const QString ansiColor =
|
|
type == QtInfoMsg ? "2" : // green
|
|
type == QtWarningMsg ? "3" : // yellow
|
|
type == QtCriticalMsg ? "1" : // red
|
|
type == QtFatalMsg ? "5" : // purple
|
|
"4"; // blue
|
|
|
|
boldColor = "\e[1;3" + ansiColor + "m";
|
|
color = "\e[3" + ansiColor + "m";
|
|
clearFormatting = "\e[0m";
|
|
}
|
|
#endif
|
|
|
|
fprintf(
|
|
stderr,
|
|
"%s%s%s %s%s |%s %s\n",
|
|
boldColor.toUtf8().constData(),
|
|
level,
|
|
clearFormatting.toUtf8().constData(),
|
|
color.toUtf8().constData(),
|
|
QDateTime::currentDateTime().toString("hh:mm:ss").toUtf8().constData(),
|
|
clearFormatting.toUtf8().constData(),
|
|
msg.toUtf8().constData()
|
|
);
|
|
}
|
|
|
|
|
|
void onExitSignal(int signum) {
|
|
QApplication::exit(128 + signum);
|
|
}
|
|
|
|
|
|
void migrateFile(QDir source, QDir destination, QString fname) {
|
|
if (! QFile::copy(source.filePath(fname), destination.filePath(fname)))
|
|
qWarning() << "Could not migrate" << fname;
|
|
}
|
|
|
|
|
|
void migrateShallowDirectory(
|
|
QDir sourceParent, QDir destinationParent, QString dname
|
|
) {
|
|
if (sourceParent.exists(dname)) {
|
|
if (! destinationParent.mkpath(dname)) {
|
|
qWarning() << "Could not create directory"
|
|
<< destinationParent.filePath(dname);
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
QDir source(sourceParent.filePath(dname));
|
|
QDir destination(destinationParent.filePath(dname));
|
|
QFileInfoList files = source.entryInfoList();
|
|
for (QFileInfo file : files) {
|
|
if(file.fileName() == "." || file.fileName() == "..")
|
|
continue;
|
|
migrateFile(source, destination, file.fileName());
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
void offerMigrateFromMirage(
|
|
QDir configDirMoment, QDir configDirMirage,
|
|
QDir dataDirMoment, QDir dataDirMirage
|
|
) {
|
|
QMessageBox dialog;
|
|
dialog.setText("Would you like Moment to re-use your logins, "
|
|
"encryption keys, configuration and themes from Mirage?");
|
|
dialog.setInformativeText(
|
|
"Will copy "+configDirMirage.path()+" → "+configDirMoment.path()
|
|
+"\nWill copy "+dataDirMirage.path()+" → "+dataDirMoment.path());
|
|
dialog.addButton(QMessageBox::Yes);
|
|
dialog.addButton(QMessageBox::No);
|
|
dialog.addButton(QMessageBox::Cancel);
|
|
int result = dialog.exec();
|
|
if (result == QMessageBox::Yes) {
|
|
qWarning("Migrating config and data from Mirage");
|
|
if (! configDirMoment.mkpath(".")) {
|
|
qFatal("Could not create config directory");
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
if (! dataDirMoment.mkpath(".")) {
|
|
qFatal("Could not create data directory");
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
migrateFile(configDirMirage, configDirMoment, "settings.py");
|
|
migrateFile(configDirMirage, configDirMoment, "settings.gui.json");
|
|
migrateFile(configDirMirage, configDirMoment, "accounts.json");
|
|
migrateFile(dataDirMirage, dataDirMoment, "history.json");
|
|
migrateFile(dataDirMirage, dataDirMoment, "state.json");
|
|
migrateShallowDirectory(dataDirMirage, dataDirMoment, "themes");
|
|
migrateShallowDirectory(dataDirMirage, dataDirMoment, "encryption");
|
|
} else if (result == QMessageBox::No) {
|
|
// Nothing to do. Proceed with starting the app
|
|
qWarning("Not migrating");
|
|
return;
|
|
} else {
|
|
// Neither "Yes" nor "No" was chosen.
|
|
// We can't know what the user wants. Just quit.
|
|
qWarning("Quitting. You can decide about migration next time.");
|
|
exit(EXIT_SUCCESS);
|
|
}
|
|
}
|
|
|
|
|
|
bool shouldMigrateFromMirage() {
|
|
QString genericConfig = QStandardPaths::writableLocation(
|
|
QStandardPaths::GenericConfigLocation);
|
|
QString genericData = QStandardPaths::writableLocation(
|
|
QStandardPaths::GenericDataLocation);
|
|
|
|
// Check whether Moment config already exists
|
|
{
|
|
QString customConfigDirMoment(
|
|
qEnvironmentVariable("MOMENT_CONFIG_DIR")
|
|
);
|
|
if (! customConfigDirMoment.isEmpty()) {
|
|
// MOMENT_CONFIG_DIR is set.
|
|
// Moment would definitely use this as the config directory.
|
|
if (QDir(customConfigDirMoment).exists())
|
|
// But it already exists.
|
|
// So this is not the first time Moment was started.
|
|
return false;
|
|
} else {
|
|
// No MOMENT_CONFIG_DIR, so check the default config directory.
|
|
if (QDir(genericConfig + "/moment").exists())
|
|
// The default config folder exists.
|
|
// So this is not the first time Moment was started.
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Check whether Moment data already exists
|
|
{
|
|
QString customDataDirMoment(qEnvironmentVariable("MOMENT_DATA_DIR"));
|
|
if (! customDataDirMoment.isEmpty()) {
|
|
// MOMENT_DATA_DIR is set.
|
|
// Moment would definitely use this as the data directory.
|
|
if (QDir(customDataDirMoment).exists())
|
|
// But it already exists.
|
|
// So this is not the first time Moment was started.
|
|
return false;
|
|
} else {
|
|
// No MOMENT_DATA_DIR, so check the default data directory.
|
|
if (QDir(genericData + "/moment").exists())
|
|
// The default data folder exists.
|
|
// So this is not the first time Moment was started.
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Check whether Mirage config exists
|
|
{
|
|
QString customConfigDirMirage(
|
|
qEnvironmentVariable("MIRAGE_CONFIG_DIR")
|
|
);
|
|
if (! customConfigDirMirage.isEmpty()) {
|
|
// MIRAGE_CONFIG_DIR is set.
|
|
// Mirage would definitely use this as the config directory.
|
|
if (! QDir(customConfigDirMirage).exists())
|
|
// But this directory does not exist.
|
|
// So there is nowhere to migrate from.
|
|
return false;
|
|
} else {
|
|
// No MIRAGE_CONFIG_DIR, so check the default config directory.
|
|
// Check /matrix-mirage (Debian) first, since it is more specific
|
|
if (! QDir(genericConfig + "/matrix-mirage").exists())
|
|
// Default Debian config folder does not exist,
|
|
// so check whether the normal /mirage exists
|
|
if (! QDir(genericConfig + "/mirage").exists())
|
|
// No, neither /matrix-mirage nor /mirage exist.
|
|
// So there is nowhere to migrate from.
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// We found out that there is no Moment config dir nor Moment data dir,
|
|
// but there is a Mirage config dir which can be migrated from.
|
|
// We could also check for Mirage data dir but it doesn't really matter.
|
|
// User should definitely be prompted for migration.
|
|
return true;
|
|
}
|
|
|
|
|
|
void tryMigrateFromMirage() {
|
|
QString genericConfig = QStandardPaths::writableLocation(
|
|
QStandardPaths::GenericConfigLocation);
|
|
QString genericData = QStandardPaths::writableLocation(
|
|
QStandardPaths::GenericDataLocation);
|
|
|
|
QDir configDirMoment, configDirMirage, dataDirMoment, dataDirMirage;
|
|
|
|
QString customConfigDirMoment(qEnvironmentVariable("MOMENT_CONFIG_DIR"));
|
|
configDirMoment = QDir(customConfigDirMoment.isEmpty()
|
|
? genericConfig + "/moment" : customConfigDirMoment);
|
|
|
|
QString customDataDirMoment(qEnvironmentVariable("MOMENT_DATA_DIR"));
|
|
dataDirMoment = QDir(customDataDirMoment.isEmpty()
|
|
? genericData + "/moment" : customDataDirMoment);
|
|
|
|
QString customConfigDirMirage(qEnvironmentVariable("MIRAGE_CONFIG_DIR"));
|
|
if (! customConfigDirMirage.isEmpty()) {
|
|
// MIRAGE_CONFIG_DIR is set.
|
|
// Mirage would definitely use this as the config directory.
|
|
// So this is where we should migrate from.
|
|
configDirMirage = QDir(customConfigDirMirage);
|
|
} else {
|
|
// No MIRAGE_CONFIG_DIR.
|
|
// Check if Mirage default config directory exists
|
|
// Check /matrix-mirage (Debian) first
|
|
QDir dirDeb(genericConfig + "/matrix-mirage");
|
|
if (dirDeb.exists()) {
|
|
// Default Debian config dir exists.
|
|
// So this is where we should migrate from.
|
|
configDirMirage = dirDeb;
|
|
} else {
|
|
// No /matrix-mirage found, so check /mirage
|
|
QDir dir(genericConfig + "/mirage");
|
|
if (dir.exists())
|
|
// Default config dir exists.
|
|
// So this is where we should migrate from.
|
|
configDirMirage = dir;
|
|
else
|
|
// No Mirage config dir found.
|
|
// Do not migrate.
|
|
return;
|
|
}
|
|
}
|
|
|
|
QString customDataDirMirage(qEnvironmentVariable("MIRAGE_DATA_DIR"));
|
|
if (! customDataDirMirage.isEmpty()) {
|
|
// MIRAGE_DATA_DIR is set.
|
|
// Mirage would definitely use this as the data directory.
|
|
// So this is where we should migrate from.
|
|
dataDirMirage = QDir(customDataDirMirage);
|
|
} else {
|
|
// No MIRAGE_DATA_DIR.
|
|
// Check if Mirage default data directory exists
|
|
// Check /matrix-mirage (Debian) first
|
|
QDir dirDeb(genericData + "/matrix-mirage");
|
|
if (dirDeb.exists()) {
|
|
// Default Debian data dir exists.
|
|
// So this is where we should migrate from.
|
|
dataDirMirage = dirDeb;
|
|
} else {
|
|
// No /matrix-mirage found, so check /mirage
|
|
QDir dir(genericData + "/mirage");
|
|
if (dir.exists())
|
|
// Default data dir exists.
|
|
// So this is where we should migrate from.
|
|
dataDirMirage = dir;
|
|
else
|
|
// No Mirage data dir found.
|
|
// Do not migrate.
|
|
return;
|
|
}
|
|
}
|
|
|
|
offerMigrateFromMirage(
|
|
configDirMoment, configDirMirage, dataDirMoment, dataDirMirage
|
|
);
|
|
}
|
|
|
|
|
|
bool setLockFile(QString configPath) {
|
|
|
|
QDir settingsFolder(configPath);
|
|
|
|
if (! settingsFolder.mkpath(".")) {
|
|
qFatal("Could not create config directory");
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
|
|
lockFile = new QLockFile(settingsFolder.absoluteFilePath(".lock"));
|
|
lockFile->tryLock(0);
|
|
|
|
switch (lockFile->error()) {
|
|
case QLockFile::NoError:
|
|
return true;
|
|
case QLockFile::LockFailedError: {
|
|
qWarning("Opening already running instance");
|
|
QFile showFile(settingsFolder.absoluteFilePath(".show"));
|
|
showFile.open(QIODevice::WriteOnly);
|
|
showFile.close();
|
|
return false;
|
|
}
|
|
default:
|
|
qFatal("Cannot create lock file: no permission or unknown error");
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
}
|
|
|
|
|
|
int main(int argc, char *argv[]) {
|
|
qInstallMessageHandler(loggingHandler);
|
|
|
|
// Define some basic info about the app before creating the QApplication
|
|
QApplication::setOrganizationName("moment");
|
|
QApplication::setApplicationName("moment");
|
|
QApplication::setApplicationDisplayName("Moment");
|
|
QApplication::setApplicationVersion("0.7.4");
|
|
QApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
|
|
|
|
// app needs to be constructed before attempting to migrate
|
|
// because migrate displays a popup dialog
|
|
QApplication app(argc, argv);
|
|
|
|
QCommandLineParser args;
|
|
|
|
QCommandLineOption startInTrayOption(QStringList() << "t" << "start-in-tray",
|
|
"Start in the system tray, without a visible window.");
|
|
args.addOption(startInTrayOption);
|
|
|
|
QCommandLineOption loadQmlOption(QStringList() << "l" << "load-qml",
|
|
"Override the file to be loaded as src/gui/UI.qml", "PATH");
|
|
args.addOption(loadQmlOption);
|
|
|
|
QCommandLineOption helpOption(QStringList() << "h" << "help",
|
|
"Displays help on commandline options.");
|
|
args.addOption(helpOption);
|
|
|
|
args.addVersionOption();
|
|
|
|
args.process(app);
|
|
|
|
if (args.isSet(helpOption)) {
|
|
std::cout << args.helpText().toStdString() << std::endl
|
|
<< "Environment variables:" << std::endl
|
|
<< " MOMENT_CONFIG_DIR Override the configuration folder"
|
|
<< std::endl
|
|
<< " MOMENT_DATA_DIR Override the application data folder"
|
|
<< std::endl
|
|
<< " MOMENT_CACHE_DIR Override the cache and downloads folder"
|
|
<< std::endl
|
|
<< " http_proxy Override the General.proxy setting, see "
|
|
<< "settings.py" << std::endl;
|
|
exit(EXIT_SUCCESS);
|
|
}
|
|
|
|
if (shouldMigrateFromMirage()) tryMigrateFromMirage();
|
|
|
|
QString customConfigDir(qEnvironmentVariable("MOMENT_CONFIG_DIR"));
|
|
QString settingsFolder(
|
|
customConfigDir.isEmpty() ?
|
|
QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation)
|
|
+ "/" + QApplication::applicationName() :
|
|
customConfigDir
|
|
);
|
|
|
|
// Attempt to create a lockfile in the settings folder
|
|
if (! setLockFile(settingsFolder)) return EXIT_SUCCESS;
|
|
|
|
// Register handlers for quit signals, e.g. SIGINT/Ctrl-C in unix terminals
|
|
signal(SIGINT, onExitSignal);
|
|
#ifdef Q_OS_UNIX
|
|
signal(SIGHUP, onExitSignal);
|
|
#endif
|
|
|
|
|
|
// Force the default universal QML style, notably prevents
|
|
// KDE from hijacking base controls and messing up everything
|
|
QQuickStyle::setStyle("Fusion");
|
|
QQuickStyle::setFallbackStyle("Default");
|
|
|
|
// Register default theme fonts. Take the files from the
|
|
// Qt resource system if possible (resources stored in the app executable),
|
|
// else the local file system.
|
|
// The dev qmake flag disables the resource system for faster builds.
|
|
QFileInfo qrcPath(":src/gui/Window.qml");
|
|
QString src = qrcPath.exists() ? ":/src" : "src";
|
|
|
|
QList<QString> fontFamilies;
|
|
fontFamilies << "roboto" << "hack";
|
|
|
|
QList<QString> fontVariants;
|
|
fontVariants << "regular" << "italic" << "bold" << "bold-italic";
|
|
|
|
foreach (QString family, fontFamilies) {
|
|
foreach (QString var, fontVariants) {
|
|
QFontDatabase::addApplicationFont(
|
|
src + "/fonts/" + family + "/" + var + ".ttf"
|
|
);
|
|
}
|
|
}
|
|
|
|
// Create the QML engine and get the root context.
|
|
// We will add it some properties that will be available globally in QML.
|
|
QQmlEngine engine;
|
|
QQmlContext *objectContext = new QQmlContext(engine.rootContext());
|
|
|
|
// To able to use Qt.quit() from QML side
|
|
QObject::connect(
|
|
&engine, &QQmlEngine::quit, &app, &QApplication::quit, Qt::QueuedConnection
|
|
);
|
|
|
|
// Set the debugMode properties depending of if we're running in debug mode
|
|
// or not (`qmake CONFIG+=dev ...`, default in autoreload.py)
|
|
#ifdef QT_DEBUG
|
|
objectContext->setContextProperty("debugMode", true);
|
|
#else
|
|
objectContext->setContextProperty("debugMode", false);
|
|
#endif
|
|
|
|
// Register our custom non-visual QObject singletons,
|
|
// that will be importable anywhere in QML. Example:
|
|
// import Clipboard 0.1
|
|
// ...
|
|
// Component.onCompleted: print(Clipboard.text)
|
|
qmlRegisterSingletonType<Clipboard>(
|
|
"Clipboard", 0, 1, "Clipboard",
|
|
[](QQmlEngine *engine, QJSEngine *scriptEngine) -> QObject * {
|
|
Q_UNUSED(scriptEngine)
|
|
|
|
Clipboard *clipboard = new Clipboard();
|
|
|
|
// Register out custom image providers.
|
|
// QML will be able to request an image from them by setting an
|
|
// `Image`'s `source` to `image://<providerId>/<id>`
|
|
engine->addImageProvider(
|
|
"clipboard", new ClipboardImageProvider(clipboard)
|
|
);
|
|
|
|
return clipboard;
|
|
}
|
|
);
|
|
|
|
qmlRegisterSingletonType<Utils>(
|
|
"CppUtils", 0, 1, "CppUtils",
|
|
[](QQmlEngine *engine, QJSEngine *scriptEngine) -> QObject * {
|
|
Q_UNUSED(engine)
|
|
Q_UNUSED(scriptEngine)
|
|
return new Utils();
|
|
}
|
|
);
|
|
|
|
// Create the QML root component by loading its file from the Qt Resource
|
|
// System or local file system if not possible.
|
|
QQmlComponent component(
|
|
&engine,
|
|
qrcPath.exists() ? "qrc:/src/gui/Window.qml" : "src/gui/Window.qml"
|
|
);
|
|
|
|
if (component.isError()) {
|
|
for (QQmlError e : component.errors()) {
|
|
qCritical(
|
|
"%s:%d:%d: %s",
|
|
e.url().toString().toStdString().c_str(),
|
|
e.line(),
|
|
e.column(),
|
|
e.description().toStdString().c_str()
|
|
);
|
|
}
|
|
qFatal("One or more errors have occurred, exiting");
|
|
app.exit(EXIT_FAILURE);
|
|
}
|
|
|
|
QObject *comp = component.create(objectContext);
|
|
comp->setProperty("settingsFolder", "file://"+settingsFolder);
|
|
comp->setProperty("startInTray", args.isSet(startInTrayOption));
|
|
comp->setProperty("loadQml", args.value(loadQmlOption));
|
|
|
|
// Finally, execute the app. Return its exit code after clearing the lock.
|
|
int exit_code = app.exec();
|
|
delete lockFile;
|
|
return exit_code;
|
|
}
|