// Copyright Mirage authors & contributors // and Moment contributors // 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 // must be first include to avoid clipboard.h errors #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef Q_OS_UNIX #include #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 fontFamilies; fontFamilies << "roboto" << "hack"; QList 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", 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:///` engine->addImageProvider( "clipboard", new ClipboardImageProvider(clipboard) ); return clipboard; } ); qmlRegisterSingletonType( "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; }