/* * This file is part of smolbote. It's copyrighted by the contributors recorded * in the version control history of the file, available from its original * location: https://neueland.iserlohn-fortress.net/gitea/aqua/smolbote * * SPDX-License-Identifier: GPL-3.0 */ #include "browser.h" #include "builtins.h" #include "configuration.h" #include "crashhandler.h" #include "session/session.h" #include "session/sessiondialog.h" #include "util.h" #include "version.h" #include #include #include #include #include #include #include #include #include #include typedef std::function::const_iterator, std::vector::const_iterator)> subcommand_func; typedef std::unordered_map command_map; // a helper function to join the keys of a command_map into a string inline std::string join_keys(const command_map &map, const std::string sep = ", ") { std::vector keys(map.size()); std::transform(map.begin(), map.end(), keys.begin(), [](auto pair) { return pair.first; }); std::sort(keys.begin(), keys.end()); std::string k; std::for_each(keys.begin(), keys.end() - 1, [&k, &sep](const std::string &piece) { k += piece + sep; }); k += keys.back(); return k; } #include inline std::string defaultUserConfigLocation() { #ifdef CONFIG_PATH_CONFIG return CONFIG_PATH_CONFIG; #else // try to locate an existing config QString path = QStandardPaths::locate(QStandardPaths::ConfigLocation, "smolbote/smolbote.cfg"); // it's possible there is no config, so set the path properly if(path.isEmpty()) path = QStandardPaths::writableLocation(QStandardPaths::ConfigLocation) + "/smolbote/smolbote.cfg"; return path.toStdString(); #endif } #include "config.h" #if defined(CONFIG_USEPLASMA) && !defined(PLASMA) #error "You have enabled Plasma integration, but Frameworks was not found." #endif #if defined(CONFIG_USEBREAKPAD) && !defined(BREAKPAD) #error "You have enabled Breakpad integration, but Breakpad was not found." #endif int main(int argc, char **argv) { // change log pattern spdlog::set_pattern("[%^%l%$] [%P:%t] %v"); #ifdef QT_DEBUG spdlog::set_level(spdlog::level::debug); // Set global log level to debug #endif const std::vector args(argv + 1, argv + argc); args::ArgumentParser parser("smolbote: yet another no-frills browser", "For more information on usage, refer to the manual page."); parser.Prog(argv[0]); args::HelpFlag cmd_help(parser, "help", "Display this help message.", { 'h', "help" }); args::Flag cmd_version(parser, "version", "Display version information.", { 'v', "version" }); args::Flag cmd_build(parser, "build", "Display build commit.", { 'b', "build" }); args::ValueFlag cmd_config(parser, "config", "Set the configuration file.", { 'c', "config" }); args::Flag cmd_noRemote(parser, "no-remote", "Do not accept or send remote commands.", { "no-remote" }); args::Flag cmd_pickSession(parser, "pick-session", "Show all available sessions and select which one to open", { "pick-session" }); args::ValueFlag cmd_session(parser, "session", "Open the specified session.", { 's', "session" }); args::PositionalList cmd_args(parser, "URL(s)", "List of URLs to open"); try { auto next = parser.ParseArgs(args); if(cmd_version) return builtins::version(); if(cmd_build) return builtins::build(); } catch(args::Help) { std::cout << parser; //std::cout << "Available subcommands: " << command_keys << std::endl; return 0; } catch(args::Error e) { std::cerr << e.what() << std::endl; std::cerr << parser; return -1; } // Disable Chromium's crash handler so breakpad can capture crashes instead. // This has to be done before QtWebEngine gets initialized. const auto chromiumFlags = qgetenv("QTWEBENGINE_CHROMIUM_FLAGS"); if(!chromiumFlags.contains("disable-in-process-stack-traces")) { qputenv("QTWEBENGINE_CHROMIUM_FLAGS", chromiumFlags + " --disable-in-process-stack-traces"); } // create and load configuration const std::string config_path = [&]() { if(cmd_config) return args::get(cmd_config); else return defaultUserConfigLocation(); }(); std::unique_ptr config = std::make_unique(argc, argv, config_path); QTranslator translator; if(config->exists("browser.translation")) { translator.load(config->value("browser.translation").value()); } QVector plugins; CommandHash_t pluginCommands; // Load plugins for(const QString &path : Util::files(config->value("plugins.path").value(), { "*.so", "*.dll" })) { auto *loader = new PluginLoader(path); const bool loaded = loader->load(); spdlog::info("{} plugin {}", loaded ? "Loaded" : "Failed to load", qUtf8Printable(path)); if(loaded) { plugins.append(loader); auto *plugin = qobject_cast(loader->instance()); pluginCommands.unite(plugin->commands()); } else { spdlog::warn("{}", qUtf8Printable(loader->errorString())); delete loader; } } // argc, argv, allowSecondary Browser app(argc, argv); // set this, otherwise the webview becomes black when using a stylesheet app.setAttribute(Qt::AA_DontCreateNativeWidgetSiblings, true); app.installTranslator(&translator); if(config->exists("browser.locale")) { auto *locale = new QTranslator(&app); if(locale->load("qt_" + config->value("browser.locale").value(), QLibraryInfo::location(QLibraryInfo::TranslationsPath))) app.installTranslator(locale); else delete locale; } if(auto iconTheme = config->value("browser.iconTheme")) { QIcon::setThemeName(iconTheme.value()); } #ifdef CONFIG_USEBREAKPAD const std::string crashpath = config->value("browser.crash.path").value_or("/tmp"); assert(!crashpath.empty()); CrashHandler::BreakpadContext ctx; google_breakpad::MinidumpDescriptor descriptor(crashpath.c_str()); const auto crashHandler = config->value("browser.crash.handler"); if(crashHandler) { const int length = crashHandler.value().length() + 1; ctx.handler = new char[length]; crashHandler.value().copy(ctx.handler, length - 1); ctx.handler[length - 1] = '\0'; } // minidump descriptor, filter callback, minidump callback, callback_context, install handler, server_fd google_breakpad::ExceptionHandler eh(descriptor, nullptr, CrashHandler::dumpCallback, &ctx, true, -1); #ifdef QT_DEBUG spdlog::info("Installed breakpad exception handler (path {})", crashpath); #endif #endif // CONFIG_USEBREAKPAD const auto profile = config->value("profile.default"); app.setConfiguration(config); // app takes ownership of config app.setup(plugins); QStringList urls; for(const auto &u : args::get(cmd_args)) { if(pluginCommands.contains(QString::fromStdString(u))) { return pluginCommands.value(QString::fromStdString(u))(); } else { urls.append(QString::fromStdString(u)); } } if(urls.isEmpty()) urls.append(QString()); // if app is primary, create new sessions from received messages if(app.isPrimary() && !cmd_noRemote) { QObject::connect(&app, &Browser::receivedMessage, &app, [](quint32 instanceId, QByteArray message) { Q_UNUSED(instanceId); auto doc = QJsonDocument::fromJson(message); Session::restoreSession(doc.object()); }); } { QJsonObject sessionData; if(cmd_pickSession) { auto *dlg = new SessionDialog(); if(const auto pick = dlg->pickSession()) sessionData = pick.value(); else sessionData = Session::fromCommandLine(profile.value(), urls); } else if(cmd_session) { QFile sessionJson(QString::fromStdString(args::get(cmd_session))); if(sessionJson.open(QIODevice::ReadOnly | QIODevice::Text)) { sessionData = QJsonDocument::fromJson(sessionJson.readAll()).object(); sessionJson.close(); } } else { sessionData = Session::fromCommandLine(profile.value(), urls); } if(app.isPrimary() || cmd_noRemote) { Session::restoreSession(sessionData); } else { // app is secondary and not standalone return app.sendMessage(QJsonDocument(sessionData).toJson()); } } return app.exec(); }