#include "mainwindow.hpp" #include "kristall.hpp" #include #include #include #include #include #include #include #include #include #include #include #include static std::unique_ptr main_globals; struct EnsureGlobalsReset { ~EnsureGlobalsReset() { main_globals.reset(); } }; kristall::Globals & kristall::globals() { assert(main_globals != nullptr); return *main_globals; } // We need QFont::setFamilies for emojis to work properly, // Qt versions below 5.13 don't support this. const bool kristall::EMOJIS_SUPPORTED = #if QT_VERSION < QT_VERSION_CHECK(5, 13, 0) false; #else true; #endif QString toFingerprintString(QSslCertificate const & certificate) { return QCryptographicHash::hash(certificate.toDer(), QCryptographicHash::Sha256).toHex(':'); } char const * const app_window_property = "kristall:app-window"; static QSettings * app_settings_ptr= nullptr; static QSettings * session_settings_ptr= nullptr; static MainWindow * last_focused_window = nullptr; #define SSTR(X) STR(X) #define STR(X) #X static QDir derive_dir(QDir const & parent, QString const & subdir) { QDir child = parent; if(not child.mkpath(subdir)) { qWarning() << "failed to initialize directory:" << subdir; return QDir { }; } if(not child.cd(subdir)) { qWarning() << "failed to setup directory:" << subdir; return QDir { }; } return child; } static void addEmojiSubstitutions() { QFontDatabase db; auto const families = db.families(); // Provide OpenMoji font for a safe fallback QFontDatabase::addApplicationFont(":/fonts/OpenMoji-Color.ttf"); QFontDatabase::addApplicationFont(":/fonts/NotoColorEmoji.ttf"); QStringList emojiFonts = { // Use system fonts on windows/mac "Apple Color Emoji", "Segoe UI Emoji", // Provide common fonts as a fallback: // "Noto Color Emoji", // this font seems to replace a lot of text characters? // "JoyPixels", // this font seems to replace a lot of text characters? // Built-in font fallback "OpenMoji", }; for(auto const & family: families) { auto current = QFont::substitutes(family); current << emojiFonts; // TODO: QFont::insertSubstitutions(family, current); } } template void forAllAppWindows(F const & f) { assert(qApp != nullptr); for(auto * const widget : qApp->topLevelWidgets()) { auto * const main_window = qobject_cast(widget); if(main_window != nullptr) { f(main_window); } } } MainWindow * getFocusedAppWindow() { assert(qApp != nullptr); // first, check if we have currently active window: auto * main_window = qobject_cast(qApp->activeWindow()); if(main_window != nullptr) return main_window; // if not, fall back to the window we focused last: if(::last_focused_window != nullptr) return ::last_focused_window; // and if we have none, we just take the first window we can find if // any: forAllAppWindows([&main_window](MainWindow * w) { if(main_window == nullptr) main_window = w; }); if(main_window != nullptr) return main_window; qCritical() << "could not find a focused/foreground window!"; return main_window; } // Explanation to the IPC protocol: // Each IPC request is required to open a new connection. // The first some bytes are the ipc::Message struct that is inspected. // After that, message-dependent bytes are sent and the IPC process is terminated. // There is now way of indicating an error. // Messages are not allowed to be larger than 64k // We don't need to think about endianess or alignment as we only communicate with // the same machine. namespace ipc { static const QString socket_name = #ifdef Q_OS_UNIX QStandardPaths::writableLocation(QStandardPaths::RuntimeLocation) + "/" #elif defined Q_OS_WIN32 qEnvironmentVariable("USERNAME") + "-" #endif "net.random-projects.kristall"; struct Message { enum Type { /// Requests that a series of urls URLs are opened /// as new tabs in the currently focused window. /// Payload description: /// The message will contain a sequence of UTF-8 encoded bytes /// that encode URLs. The URLs are separated by LF. open_in_tabs = 0, /// Same format as open_in_tabs, but requests that these urls are /// opened in a new window instead of new tabs. open_in_window = 1, }; enum Protocol : uint16_t { version_1 = 1, }; Protocol version; Type type; }; static_assert(std::is_trivial_v, "Message needs to be flat-copyable!"); static_assert(std::is_trivially_copyable_v, "Message needs to be flat-copyable!"); //! Implements the struct ConnectedClient : QObject { QLocalSocket * socket; QByteArray receive_buffer; bool everything_ok; ConnectedClient(QLocalSocket * socket) : QObject(socket), socket(socket), everything_ok(true) { QObject::connect(socket, &QLocalSocket::readyRead, this, &ConnectedClient::on_readyRead); QObject::connect(socket, &QLocalSocket::disconnected, this, &ConnectedClient::on_disconnected); } void on_readyRead() { auto const buffer = socket->readAll(); if(buffer.size() + this->receive_buffer.size() > 65536) { qCritical() << "ipc failure: IPC client sent more than 64k bytes of data!"; this->everything_ok = false; this->socket->close(); } this->receive_buffer.append(buffer); } void on_disconnected() { if(not this->everything_ok) return; if(size_t(this->receive_buffer.size()) < sizeof(Message)) { qCritical() << "ipc failure: IPC client did not send enough data!"; return; } Message message; memcpy(&message, this->receive_buffer.data(), sizeof(Message)); switch(message.version) { case Message::version_1: { this->processRequest( message.type, this->receive_buffer.mid(sizeof(Message)) ); break; } default: { qCritical() << "ipc failure: IPC client used a unsupported protocol version!"; return; } } } void processRequest(Message::Type type, QByteArray const & payload) { switch(type) { case Message::open_in_tabs: { auto * const window = getFocusedAppWindow(); for(auto const & data : payload.split('\n')) { QUrl url { QString::fromUtf8(data) }; if(url.isValid()) { if(window != nullptr) { window->addNewTab(true, url); } } } break; } case Message::open_in_window: { QVector urls; for(auto const & data : payload.split('\n')) { QUrl url { QString::fromUtf8(data) }; if(url.isValid()) { urls.append(url); } } if(urls.size() > 0) { kristall::openNewWindow(urls); } break; } default: { qCritical() << "ipc failure: IPC client used a unsupported message type!"; return; } } } }; void send(QLocalSocket & socket, void const * buffer, size_t length) { size_t offset = 0; while(offset < length) { auto const sent = socket.write( reinterpret_cast(buffer) + offset, length - offset ); if(sent <= 0) return; offset += sent; } } /// Sends a open_in_tabs request and closes the socket. void sendOpenInTabs(QLocalSocket & socket, QVector const & urls) { Message msg { Message::version_1, Message::open_in_tabs }; send(socket, &msg, sizeof msg); for(int i = 0; i < urls.size(); i++) { if(i > 0) send(socket, "\n", 1); auto const bits = urls[i].toString(QUrl::FullyEncoded).toUtf8(); send(socket, bits.data(), bits.size()); } } /// Sends a open_in_window request and closes the socket. void sendOpenInWindow(QLocalSocket & socket, QVector const & urls) { Message msg { Message::version_1, Message::open_in_window }; send(socket, &msg, sizeof msg); for(int i = 0; i < urls.size(); i++) { if(i > 0) send(socket, "\n", 1); auto const bits = urls[i].toString(QUrl::FullyEncoded).toUtf8(); send(socket, bits.data(), bits.size()); } } } void kristall::registerAppWindow(MainWindow * window) { window->setProperty(app_window_property, QVariant(true)); QObject::connect(window, &MainWindow::destroyed, [window]() { // reset the focus so we don't store a dangling pointer! if(::last_focused_window == window) ::last_focused_window = nullptr; }); } MainWindow * kristall::openNewWindow(bool load_default) { auto * const window = openNewWindow(QVector{}); window->addEmptyTab(true, load_default); return window; } //! Opens a new window with the given url. MainWindow * kristall::openNewWindow(QUrl const & url) { return openNewWindow(QVector{url}); } //! Opens a new window with the given urls. //! Almost identical to below overload. MainWindow * kristall::openNewWindow(QVector const & urls) { MainWindow * const window = new MainWindow(qApp); for(int i = 0; i < urls.length(); i++) { window->addNewTab((i == 0), urls.at(i), true, ""); } window->show(); return window; } //! Opens a new window with the given list of urls. //! If the list is empty, no new tab will spawned. MainWindow * kristall::openNewWindow(QVector const & urls) { MainWindow * const window = new MainWindow(qApp); for(int i = 0; i < urls.length(); i++) { window->addNewTab((i == 0), urls.at(i).location, true, urls.at(i).title); } window->show(); return window; } //! Saves the currently used locale void kristall::saveLocale() { if(app_settings_ptr == nullptr) return; if(auto const locale = kristall::globals().localization->locale; locale != std::nullopt) app_settings_ptr->setValue("language", locale->bcp47Name()); else app_settings_ptr->setValue("language", QString("")); app_settings_ptr->sync(); } int main(int argc, char *argv[]) { QApplication app(argc, argv); app.setApplicationVersion(SSTR(KRISTALL_VERSION)); main_globals = std::make_unique( ); // this is relevant to delete kristall::Globals before the application itself. EnsureGlobalsReset ensure_globals_reset; // Initialize kristall directories { QString cache_root = QStandardPaths::writableLocation(QStandardPaths::CacheLocation); QString config_root = QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation); kristall::globals().dirs.config_root = QDir { config_root }; kristall::globals().dirs.cache_root = QDir { cache_root }; kristall::globals().dirs.offline_pages = derive_dir(kristall::globals().dirs.cache_root, "offline-pages"); kristall::globals().dirs.themes = derive_dir(kristall::globals().dirs.config_root, "themes"); kristall::globals().dirs.styles = derive_dir(kristall::globals().dirs.config_root, "styles"); kristall::globals().dirs.styles.setNameFilters(QStringList { "*.kthm" }); kristall::globals().dirs.styles.setFilter(QDir::Files); } QSettings app_settings { kristall::globals().dirs.config_root.absoluteFilePath("config.ini"), QSettings::IniFormat }; app_settings_ptr = &app_settings; QObject::connect(&app, &QApplication::focusChanged, [](QWidget *old, QWidget *now) { // Determine the window for both, we're only interested in window focus changes. if(old != nullptr) old = old->window(); if(now != nullptr) now = now->window(); if(old == now) // no focus change here return; auto * const main_window = qobject_cast(now); if(main_window != nullptr) { auto is_app_window = main_window->property(app_window_property); if(is_app_window.toBool() == true) { // safety measure to not accidently store non-resettable pointers here ::last_focused_window = main_window; } else { qCritical() << main_window << "is not registered as a proper app window!"; } } }); // Initialize localization { auto & i18n = kristall::globals().localization; i18n = std::make_unique(); auto const locale_id = app_settings.value("language", QString("")).toString(); if(not locale_id.isEmpty()) i18n->setLocale(QLocale(locale_id)); else i18n->setLocale(std::nullopt); qApp->installTranslator(&i18n->qt); qApp->installTranslator(&i18n->kristall); } addEmojiSubstitutions(); QCommandLineParser cli_parser; QCommandLineOption newWindowOption { { "w", "new-window" }, QApplication::tr("Opens the provided links in a new window instead of tabs."), }; QCommandLineOption isolatedOption { { "i", "isolated" }, QApplication::tr("Starts the instance of kristall as a isolated session that cannot communicate with other windows."), }; cli_parser.addVersionOption(); cli_parser.addHelpOption(); cli_parser.addOption(newWindowOption); cli_parser.addOption(isolatedOption); cli_parser.addPositionalArgument("urls", QApplication::tr("The urls that should be opened instead of the start page"), "[urls...]"); cli_parser.process(app); QVector urls; { auto cli_args = cli_parser.positionalArguments(); for(auto const & url_str : cli_args) { QUrl url { url_str }; if (url.isRelative()) { if (QFile::exists(url_str)) { url = QUrl::fromLocalFile(QFileInfo(url_str).absoluteFilePath()); } else { url = QUrl("gemini://" + url_str); } } if(url.isValid()) { urls.append(url); } else { qDebug() << "Invalid url: " << url_str; } } } auto const open_new_window = cli_parser.isSet(newWindowOption); auto const isolated_session = cli_parser.isSet(isolatedOption); std::unique_ptr ipc_server { nullptr }; if(not isolated_session) { // try connecting to a already existing instance of kristall { QLocalSocket socket; socket.connectToServer(ipc::socket_name); // do not use less time here as we need to give the other task a bit // of time. Most OS have a "loop time" of ~10 ms, so we use twice the // time here to allow a response. if(socket.waitForConnected(20)) { qDebug() << "we already have a kristall instance running!"; if(urls.length() > 0) { if(open_new_window) ipc::sendOpenInWindow(socket, urls); else ipc::sendOpenInTabs(socket, urls); } socket.waitForBytesWritten(); return 0; } } // Otherwise, spawn a new local socket that will accept messages // to provide proper IPC { std::unique_ptr server { new QLocalServer }; server->setSocketOptions(QLocalServer::UserAccessOption); if(server->listen(ipc::socket_name)) { qDebug() << "successfully started the IPC socket."; ipc_server = std::move(server); } else { qCritical() << "failed to create IPC socket: " << server->errorString(); } } } std::unique_ptr session_store; // isolated sessions don't have session management if(not isolated_session) { session_store = std::make_unique( kristall::globals().dirs.config_root.absoluteFilePath("session.ini"), QSettings::IniFormat ); } session_settings_ptr = session_store.get(); { QSettings deprecated_settings { "xqTechnologies", "Kristall" }; if(QFile(deprecated_settings.fileName()).exists()) { if(deprecated_settings.value("deprecated", false) == false) { qDebug() << "Migrating to new configuration style."; for(auto const & key : deprecated_settings.allKeys()) { app_settings.setValue(key, deprecated_settings.value(key)); } // Migrate themes to new model { int items = deprecated_settings.beginReadArray("Themes"); for(int i = 0; i < items; i++) { deprecated_settings.setArrayIndex(i); QString name = deprecated_settings.value("name").toString(); DocumentStyle style; style.load(deprecated_settings); QString fileName; int index = 0; do { fileName = DocumentStyle::createFileNameFromName(name, index); index += 1; } while(kristall::globals().dirs.styles.exists(fileName)); QSettings style_sheet { kristall::globals().dirs.styles.absoluteFilePath(fileName), QSettings::IniFormat }; style_sheet.setValue("name", name); style.save(style_sheet); style_sheet.sync(); } deprecated_settings.endArray(); } // Remove old theming stuff app_settings.remove("Theme"); app_settings.remove("Themes"); // Migrate "current theme" to new format { DocumentStyle current_style; deprecated_settings.beginGroup("Theme"); current_style.load(deprecated_settings); deprecated_settings.endGroup(); app_settings.beginGroup("Theme"); current_style.save(app_settings); app_settings.endGroup(); } deprecated_settings.setValue("deprecated", true); } else { qDebug() << "Migration complete. Please delete" << deprecated_settings.fileName(); } } } // Migrate to new favourites format if(int len = app_settings.beginReadArray("favourites"); len > 0) { qDebug() << "Migrating old-style favourites..."; std::vector favs; favs.reserve(len); for(int i = 0; i < len; i++) { app_settings.setArrayIndex(i); Favourite fav; fav.destination = app_settings.value("url").toString(); fav.title = QString { }; favs.emplace_back(std::move(fav)); } app_settings.endArray(); app_settings.beginGroup("Favourites"); { app_settings.beginWriteArray("groups"); app_settings.setArrayIndex(0); app_settings.setValue("name", QObject::tr("Unsorted")); { app_settings.beginWriteArray("favourites", len); for(int i = 0; i < len; i++) { auto const & fav = favs.at(i); app_settings.setArrayIndex(i); app_settings.setValue("title", fav.title); app_settings.setValue("url", fav.destination); } app_settings.endArray(); } app_settings.endArray(); } app_settings.endGroup(); app_settings.remove("favourites"); } else { app_settings.endArray(); } kristall::globals().settings = &app_settings; kristall::globals().options.load(app_settings); app_settings.beginGroup("Protocols"); kristall::globals().protocols.load(app_settings); app_settings.endGroup(); app_settings.beginGroup("Client Identities"); kristall::globals().identities.load(app_settings); app_settings.endGroup(); app_settings.beginGroup("Trusted Servers"); kristall::globals().trust.gemini.load(app_settings); app_settings.endGroup(); app_settings.beginGroup("Trusted HTTPS Servers"); kristall::globals().trust.https.load(app_settings); app_settings.endGroup(); app_settings.beginGroup("Theme"); kristall::globals().document_style.load(app_settings); app_settings.endGroup(); app_settings.beginGroup("Favourites"); kristall::globals().favourites.load(app_settings); app_settings.endGroup(); kristall::setTheme(kristall::globals().options.theme); if(ipc_server != nullptr) { QObject::connect(ipc_server.get(), &QLocalServer::newConnection, [&ipc_server]() { auto * const socket = ipc_server->nextPendingConnection(); if(socket != nullptr) { // this will set up everything needed: // - signals from socket // - set itself as the socket child, so it will be deleted when the socket is closed (void) new ipc::ConnectedClient(socket); // destroy the socket when the connection was closed. QObject::connect(socket, &QLocalSocket::disconnected, socket, &QObject::deleteLater); } }); } // Stores the first window from the restored session (if any) MainWindow * root_window = nullptr; if(session_store != nullptr) { auto & settings = *session_store; int window_count = settings.beginReadArray("windows"); if(kristall::globals().options.session_restore_behaviour == GenericSettings::RestoreLastSession) { for(int index = 0; index < window_count; index += 1) { settings.setArrayIndex(index); QVector urls; int tab_count = settings.beginReadArray("tabs"); for(int i = 0; i < tab_count; i++) { settings.setArrayIndex(i); urls.push_back({ settings.value("url").toString(), settings.value("title").toString() }); } settings.endArray(); auto * const window = kristall::openNewWindow(urls); if (window->tabCount() > 0) { int tab_index = settings.value("tab_index").toInt(); window->setCurrentTabIndex(tab_index); window->curTab()->reloadPage(); } if(settings.contains("state")) { window->restoreState(settings.value("state").toByteArray()); } if(settings.contains("geometry") != QVariant {}) { window->restoreGeometry(settings.value("geometry").toByteArray()); } if(root_window == nullptr) root_window = window; } } else if(window_count > 0) { // Restore only the main window geometry settings.setArrayIndex(0); root_window = kristall::openNewWindow(QVector{}); if(settings.contains("state")) { root_window->restoreState(settings.value("state").toByteArray()); } if(settings.contains("geometry") != QVariant {}) { root_window->restoreGeometry(settings.value("geometry").toByteArray()); } // If no URLs are to be loaded, we need to manually // add the empty tab here. if(urls.size() == 0) { root_window->addEmptyTab(true, true); } } settings.endArray(); } if(urls.size() > 0) { if(root_window == nullptr or open_new_window) { // Open all URLs in a new window // if we either got no previous window // or the user explicitly requested a new one. kristall::openNewWindow(urls); } else { // Otherwise, open all URLs in the first window for(auto const & url : urls) { root_window->addNewTab(true, url); } } } else if(root_window == nullptr) { // If we start kristall in a blank state, // just open a window with the default url kristall::openNewWindow(true); } int exit_code = app.exec(); return exit_code; } void GenericSettings::load(QSettings &settings) { network_timeout = settings.value("network_timeout", 5000).toInt(); start_page = settings.value("start_page", "about:favourites").toString(); search_engine = settings.value("search_engine", "gemini://geminispace.info/search?%1").toString(); if(settings.value("text_display", "fancy").toString() == "plain") text_display = PlainText; else text_display = FormattedText; enable_text_decoration = settings.value("text_decoration", false).toBool(); QString theme_name = settings.value("theme", "os_default").toString(); if(theme_name == "dark") theme = Theme::dark; else if(theme_name == "light") theme = Theme::light; else theme = Theme::os_default; QString icon_theme_name = settings.value("icon_theme", "auto").toString(); if (icon_theme_name == "light") icon_theme = IconTheme::light; else if (icon_theme_name == "dark") icon_theme = IconTheme::dark; else icon_theme = IconTheme::automatic; QString density = settings.value("ui_density", "compact").toString(); if(density == "compact") ui_density = UIDensity::compact; else if (density == "classic") ui_density = UIDensity::classic; if(settings.value("gophermap_display", "rendered").toString() == "rendered") gophermap_display = FormattedText; else gophermap_display = PlainText; use_os_scheme_handler = settings.value("use_os_scheme_handler", false).toBool(); show_hidden_files_in_dirs = settings.value("show_hidden_files_in_dirs", false).toBool(); fancy_urlbar = settings.value("fancy_urlbar", true).toBool(); fancy_quotes = settings.value("fancy_quotes", true).toBool(); strip_nav = settings.value("strip_nav", false).toBool(); emojis_enabled = kristall::EMOJIS_SUPPORTED ? settings.value("emojis_enabled", true).toBool() : false; QString ansi = settings.value("ansi_escapes", "render").toString(); if(ansi == "ignore") ansi_escapes = AnsiEscRenderMode::ignore; else if(ansi == "render") ansi_escapes = AnsiEscRenderMode::render; else if(ansi == "strip") ansi_escapes = AnsiEscRenderMode::strip; max_redirections = settings.value("max_redirections", 5).toInt(); redirection_policy = RedirectionWarning(settings.value("redirection_policy ", WarnOnHostChange).toInt()); enable_home_btn = settings.value("enable_home_btn", false).toBool(); enable_newtab_btn = settings.value("enable_newtab_btn", true).toBool(); enable_root_btn = settings.value("enable_root_btn", false).toBool(); enable_parent_btn = settings.value("enable_parent_btn", false).toBool(); cache_limit = settings.value("cache_limit", 1000).toInt(); cache_threshold = settings.value("cache_threshold", 125).toInt(); cache_life = settings.value("cache_life", 15).toInt(); cache_unlimited_life = settings.value("cache_unlimited_life", true).toBool(); session_restore_behaviour = SessionRestoreBehaviour(settings.value("session_restore_behaviour", int(session_restore_behaviour)).toInt()); close_window_with_last_tab = settings.value("close_window_with_last_tab", false).toBool(); } void GenericSettings::save(QSettings &settings) const { settings.setValue("start_page", this->start_page); settings.setValue("search_engine", this->search_engine); settings.setValue("text_display", (text_display == FormattedText) ? "fancy" : "plain"); settings.setValue("text_decoration", enable_text_decoration); QString theme_name = "os_default"; switch(theme) { case Theme::dark: theme_name = "dark"; break; case Theme::light: theme_name = "light"; break; case Theme::os_default: theme_name = "os_default"; break; } settings.setValue("theme", theme_name); QString icon_theme_name = "auto"; switch(icon_theme) { case IconTheme::dark: icon_theme_name = "dark"; break; case IconTheme::light: icon_theme_name = "light"; break; case IconTheme::automatic: icon_theme_name = "auto"; break; } settings.setValue("icon_theme", icon_theme_name); QString density = "compact"; switch(ui_density) { case UIDensity::compact: density = "compact"; break; case UIDensity::classic: density = "classic"; break; } settings.setValue("ui_density", density); QString ansi = "render"; switch(ansi_escapes) { case AnsiEscRenderMode::ignore: ansi = "ignore"; break; case AnsiEscRenderMode::render: ansi = "render"; break; case AnsiEscRenderMode::strip: ansi = "strip"; break; } settings.setValue("ansi_escapes", ansi); settings.setValue("gophermap_display", (gophermap_display == FormattedText) ? "rendered" : "text"); settings.setValue("use_os_scheme_handler", use_os_scheme_handler); settings.setValue("show_hidden_files_in_dirs", show_hidden_files_in_dirs); settings.setValue("fancy_urlbar", fancy_urlbar); settings.setValue("fancy_quotes", fancy_quotes); settings.setValue("strip_nav", strip_nav); settings.setValue("max_redirections", max_redirections); settings.setValue("redirection_policy", int(redirection_policy)); settings.setValue("network_timeout", network_timeout); settings.setValue("enable_home_btn", enable_home_btn); settings.setValue("enable_newtab_btn", enable_newtab_btn); settings.setValue("enable_root_btn", enable_root_btn); settings.setValue("enable_parent_btn", enable_parent_btn); settings.setValue("cache_limit", cache_limit); settings.setValue("cache_threshold", cache_threshold); settings.setValue("cache_life", cache_life); settings.setValue("cache_unlimited_life", cache_unlimited_life); if (kristall::EMOJIS_SUPPORTED) { // Save emoji pref only if emojis are supported, so if user changes to a build // with emoji support, they get it out of the box. settings.setValue("emojis_enabled", emojis_enabled); } settings.setValue("session_restore_behaviour", int(session_restore_behaviour)); settings.setValue("close_window_with_last_tab", close_window_with_last_tab); } void kristall::applySettings() { kristall::setTheme(kristall::globals().options.theme); kristall::setUiDensity(kristall::globals().options.ui_density, false); forAllAppWindows([](MainWindow * window) { window->applySettings(); }); } void kristall::saveSettings() { assert(app_settings_ptr != nullptr); QSettings & app_settings = *app_settings_ptr; app_settings.beginGroup("Favourites"); kristall::globals().favourites.save(app_settings); app_settings.endGroup(); app_settings.beginGroup("Protocols"); kristall::globals().protocols.save(app_settings); app_settings.endGroup(); app_settings.beginGroup("Client Identities"); kristall::globals().identities.save(app_settings); app_settings.endGroup(); app_settings.beginGroup("Trusted Servers"); kristall::globals().trust.gemini.save(app_settings); app_settings.endGroup(); app_settings.beginGroup("Trusted HTTPS Servers"); kristall::globals().trust.https.save(app_settings); app_settings.endGroup(); app_settings.beginGroup("Theme"); kristall::globals().document_style.save(app_settings); app_settings.endGroup(); kristall::globals().options.save(app_settings); app_settings.sync(); } void kristall::setTheme(Theme theme) { assert(qApp != nullptr); if(theme == Theme::os_default) { qApp->setStyleSheet(""); // Use "mid" colour for our URL bar dim colour: QColor col = qApp->palette().color(QPalette::WindowText); col.setAlpha(150); kristall::globals().options.fancy_urlbar_dim_colour = std::move(col); } else if(theme == Theme::light) { QFile file(":/light.qss"); file.open(QFile::ReadOnly | QFile::Text); QTextStream stream(&file); qApp->setStyleSheet(stream.readAll()); kristall::globals().options.fancy_urlbar_dim_colour = QColor(128, 128, 128, 255); } else if(theme == Theme::dark) { QFile file(":/dark.qss"); file.open(QFile::ReadOnly | QFile::Text); QTextStream stream(&file); qApp->setStyleSheet(stream.readAll()); kristall::globals().options.fancy_urlbar_dim_colour = QColor(150, 150, 150, 255); } kristall::setIconTheme(kristall::globals().options.icon_theme, theme); forAllAppWindows([](MainWindow * main_window) { if (main_window && main_window->curTab()) main_window->curTab()->updateUrlBarStyle(); }); } void kristall::setIconTheme(IconTheme icotheme, Theme uitheme) { assert(qApp != nullptr); static const QString icothemes[] = { "light", // Light theme (dark icons) "dark" // Dark theme (light icons) }; auto ret = []() { forAllAppWindows([](MainWindow * main_window) { if (main_window && main_window->curTab()) main_window->curTab()->refreshToolbarIcons(); }); }; if (icotheme == IconTheme::automatic) { if (uitheme == Theme::os_default) { // For Linux we use standard system icon set, // for Windows & Mac we just use our default light theme icons. #if defined Q_OS_WIN32 || defined Q_OS_DARWIN QIcon::setThemeName("light"); #else QIcon::setThemeName(""); #endif kristall::globals().options.explicit_icon_theme = IconTheme::dark; ret(); return; } // Use icon theme based on UI theme QIcon::setThemeName(icothemes[(int)uitheme]); kristall::globals().options.explicit_icon_theme = (IconTheme)uitheme; ret(); return; } // Use icon specified by user QIcon::setThemeName(icothemes[(int)icotheme]); kristall::globals().options.explicit_icon_theme = (IconTheme)icotheme; ret(); } void kristall::setUiDensity(UIDensity density, bool previewing) { forAllAppWindows([density, previewing](MainWindow * main_window) { main_window->setUiDensity(density, previewing); }); } int kristall::getWindowCount() { int count = 0; forAllAppWindows([&count](MainWindow *) { count += 1; }); return count; } void kristall::saveSession() { if(session_settings_ptr == nullptr) return; auto & settings = *session_settings_ptr; settings.clear(); settings.beginWriteArray("windows"); int window_index = 0; int tab_count = 0; forAllAppWindows([&settings, &window_index, &tab_count](MainWindow * main_window) { settings.setArrayIndex(window_index); settings.setValue("state", main_window->saveState()); settings.setValue("geometry", main_window->saveGeometry()); int count = main_window->tabCount(); settings.beginWriteArray("tabs", count); for(int i = 0; i < count; i++) { settings.setArrayIndex(i); settings.setValue("url", main_window->tabAt(i)->current_location.toString(QUrl::FullyEncoded)); settings.setValue("title", main_window->tabAt(i)->page_title); tab_count += 1; } settings.endArray(); settings.setValue("tab_index", main_window->currentTabIndex()); window_index += 1; }); settings.endArray(); qDebug() << "Saved session with" << window_index << "windows and" << tab_count << "tabs in total."; kristall::saveSettings(); }