kristall/src/main.cpp

1161 lines
37 KiB
C++

#include "mainwindow.hpp"
#include "kristall.hpp"
#include <QApplication>
#include <QTranslator>
#include <QUrl>
#include <QSettings>
#include <QCommandLineParser>
#include <QDebug>
#include <QStandardPaths>
#include <QFontDatabase>
#include <QLocalSocket>
#include <QLocalServer>
#include <QLibraryInfo>
#include <cassert>
static std::unique_ptr<kristall::Globals> 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<typename F>
void forAllAppWindows(F const & f)
{
assert(qApp != nullptr);
for(auto * const widget : qApp->topLevelWidgets())
{
auto * const main_window = qobject_cast<MainWindow*>(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<MainWindow *>(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>, "Message needs to be flat-copyable!");
static_assert(std::is_trivially_copyable_v<Message>, "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<QUrl> 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<char const *>(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<QUrl> 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<QUrl> 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<QUrl>{});
window->addEmptyTab(true, load_default);
return window;
}
//! Opens a new window with the given url.
MainWindow * kristall::openNewWindow(QUrl const & url)
{
return openNewWindow(QVector<QUrl>{url});
}
//! Opens a new window with the given urls.
//! Almost identical to below overload.
MainWindow * kristall::openNewWindow(QVector<QUrl> 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<NamedUrl> 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<kristall::Globals>( );
// 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<MainWindow*>(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<Localization>();
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<QUrl> 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<QLocalServer> 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<QLocalServer> 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<QSettings> session_store;
// isolated sessions don't have session management
if(not isolated_session)
{
session_store = std::make_unique<QSettings>(
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<Favourite> 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<NamedUrl> 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<QUrl>{});
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();
}