diff --git a/src/plugins/terminal/terminalsettingspage.cpp b/src/plugins/terminal/terminalsettingspage.cpp index 9f0544bea37..7eabca19b06 100644 --- a/src/plugins/terminal/terminalsettingspage.cpp +++ b/src/plugins/terminal/terminalsettingspage.cpp @@ -8,10 +8,21 @@ #include +#include +#include +#include +#include #include #include #include +#include +#include +#include +#include +#include +#include +#include using namespace Utils; @@ -29,6 +40,286 @@ TerminalSettingsPage::TerminalSettingsPage() void TerminalSettingsPage::init() {} +static expected_str loadXdefaults(const FilePath &path) +{ + const expected_str readResult = path.fileContents(); + if (!readResult) + return make_unexpected(readResult.error()); + + QRegularExpression re(R"(.*\*(color[0-9]{1,2}|foreground|background):\s*(#[0-9a-f]{6}))"); + + for (const QByteArray &line : readResult->split('\n')) { + if (line.trimmed().startsWith('!')) + continue; + + const auto match = re.match(QString::fromUtf8(line)); + if (match.hasMatch()) { + const QString colorName = match.captured(1); + const QColor color(match.captured(2)); + if (colorName == "foreground") { + TerminalSettings::instance().foregroundColor.setVolatileValue(color); + } else if (colorName == "background") { + TerminalSettings::instance().backgroundColor.setVolatileValue(color); + } else { + const int colorIndex = colorName.mid(5).toInt(); + if (colorIndex >= 0 && colorIndex < 16) + TerminalSettings::instance().colors[colorIndex].setVolatileValue(color); + } + } + } + + return {}; +} + +static expected_str loadItermColors(const FilePath &path) +{ + QFile f(path.toFSPathString()); + const bool opened = f.open(QIODevice::ReadOnly); + if (!opened) + return make_unexpected(Tr::tr("Failed to open file")); + + QXmlStreamReader reader(&f); + while (!reader.atEnd() && reader.readNextStartElement()) { + if (reader.name() == u"plist") { + while (!reader.atEnd() && reader.readNextStartElement()) { + if (reader.name() == u"dict") { + QString colorName; + while (!reader.atEnd() && reader.readNextStartElement()) { + if (reader.name() == u"key") { + colorName = reader.readElementText(); + } else if (reader.name() == u"dict") { + QColor color; + int component; + while (!reader.atEnd() && reader.readNextStartElement()) { + if (reader.name() == u"key") { + const auto &text = reader.readElementText(); + if (text == u"Red Component") + component = 0; + else if (text == u"Green Component") + component = 1; + else if (text == u"Blue Component") + component = 2; + else if (text == u"Alpha Component") + component = 3; + } else if (reader.name() == u"real") { + // clang-format off + switch (component) { + case 0: color.setRedF(reader.readElementText().toDouble()); break; + case 1: color.setGreenF(reader.readElementText().toDouble()); break; + case 2: color.setBlueF(reader.readElementText().toDouble()); break; + case 3: color.setAlphaF(reader.readElementText().toDouble()); break; + } + // clang-format on + } else { + reader.skipCurrentElement(); + } + } + + if (colorName.startsWith("Ansi")) { + const auto c = colorName.mid(5, 2); + const int colorIndex = c.toInt(); + if (colorIndex >= 0 && colorIndex < 16) + TerminalSettings::instance().colors[colorIndex].setVolatileValue( + color); + } else if (colorName == "Foreground Color") { + TerminalSettings::instance().foregroundColor.setVolatileValue(color); + } else if (colorName == "Background Color") { + TerminalSettings::instance().backgroundColor.setVolatileValue(color); + } else if (colorName == "Selection Color") { + TerminalSettings::instance().selectionColor.setVolatileValue(color); + } + } + } + } + } + break; + } + } + if (reader.hasError()) + return make_unexpected(reader.errorString()); + + return {}; +} + +static expected_str loadVsCodeColors(const FilePath &path) +{ + const expected_str readResult = path.fileContents(); + if (!readResult) + return make_unexpected(readResult.error()); + + QJsonParseError error; + QJsonDocument doc = QJsonDocument::fromJson(*readResult, &error); + if (error.error != QJsonParseError::NoError) + return make_unexpected(Tr::tr("JSON parsing error: \"%1\", at offset: %2") + .arg(error.errorString()) + .arg(error.offset)); + + const QJsonObject root = doc.object(); + const auto itColors = root.find("colors"); + if (itColors == root.end()) + return make_unexpected(Tr::tr("No colors found")); + + const QJsonObject colors = itColors->toObject(); + + // clang-format off + const QList> colorKeys = { + qMakePair(u"editor.background", &TerminalSettings::instance().backgroundColor), + qMakePair(u"terminal.foreground", &TerminalSettings::instance().foregroundColor), + qMakePair(u"terminal.selectionBackground", &TerminalSettings::instance().selectionColor), + + qMakePair(u"terminal.ansiBlack", &TerminalSettings::instance().colors[0]), + qMakePair(u"terminal.ansiBrightBlack", &TerminalSettings::instance().colors[8]), + + qMakePair(u"terminal.ansiRed", &TerminalSettings::instance().colors[1]), + qMakePair(u"terminal.ansiBrightRed", &TerminalSettings::instance().colors[9]), + + qMakePair(u"terminal.ansiGreen", &TerminalSettings::instance().colors[2]), + qMakePair(u"terminal.ansiBrightGreen", &TerminalSettings::instance().colors[10]), + + qMakePair(u"terminal.ansiYellow", &TerminalSettings::instance().colors[3]), + qMakePair(u"terminal.ansiBrightYellow", &TerminalSettings::instance().colors[11]), + + qMakePair(u"terminal.ansiBlue", &TerminalSettings::instance().colors[4]), + qMakePair(u"terminal.ansiBrightBlue", &TerminalSettings::instance().colors[12]), + + qMakePair(u"terminal.ansiMagenta", &TerminalSettings::instance().colors[5]), + qMakePair(u"terminal.ansiBrightMagenta", &TerminalSettings::instance().colors[13]), + + qMakePair(u"terminal.ansiCyan", &TerminalSettings::instance().colors[6]), + qMakePair(u"terminal.ansiBrightCyan", &TerminalSettings::instance().colors[14]), + + qMakePair(u"terminal.ansiWhite", &TerminalSettings::instance().colors[7]), + qMakePair(u"terminal.ansiBrightWhite", &TerminalSettings::instance().colors[15]) + }; + // clang-format on + + for (const auto &pair : colorKeys) { + const auto it = colors.find(pair.first); + if (it != colors.end()) { + const QString colorString = it->toString(); + if (colorString.startsWith("#")) { + QColor color(colorString.mid(0, 7)); + if (colorString.size() > 7) { + int alpha = colorString.mid(7).toInt(nullptr, 16); + color.setAlpha(alpha); + } + if (color.isValid()) + pair.second->setVolatileValue(color); + } + } + } + + return {}; +} + +static expected_str loadKonsoleColorScheme(const FilePath &path) +{ + QSettings settings(path.toFSPathString(), QSettings::IniFormat); + + auto parseColor = [](const QStringList &parts) -> expected_str { + if (parts.size() != 3 && parts.size() != 4) + return make_unexpected(Tr::tr("Invalid color format")); + int alpha = parts.size() == 4 ? parts[3].toInt() : 255; + return QColor(parts[0].toInt(), parts[1].toInt(), parts[2].toInt(), alpha); + }; + + // clang-format off + const QList> colorKeys = { + qMakePair(QLatin1String("Background/Color"), &TerminalSettings::instance().backgroundColor), + qMakePair(QLatin1String("Foreground/Color"), &TerminalSettings::instance().foregroundColor), + + qMakePair(QLatin1String("Color0/Color"), &TerminalSettings::instance().colors[0]), + qMakePair(QLatin1String("Color0Intense/Color"), &TerminalSettings::instance().colors[8]), + + qMakePair(QLatin1String("Color1/Color"), &TerminalSettings::instance().colors[1]), + qMakePair(QLatin1String("Color1Intense/Color"), &TerminalSettings::instance().colors[9]), + + qMakePair(QLatin1String("Color2/Color"), &TerminalSettings::instance().colors[2]), + qMakePair(QLatin1String("Color2Intense/Color"), &TerminalSettings::instance().colors[10]), + + qMakePair(QLatin1String("Color3/Color"), &TerminalSettings::instance().colors[3]), + qMakePair(QLatin1String("Color3Intense/Color"), &TerminalSettings::instance().colors[11]), + + qMakePair(QLatin1String("Color4/Color"), &TerminalSettings::instance().colors[4]), + qMakePair(QLatin1String("Color4Intense/Color"), &TerminalSettings::instance().colors[12]), + + qMakePair(QLatin1String("Color5/Color"), &TerminalSettings::instance().colors[5]), + qMakePair(QLatin1String("Color5Intense/Color"), &TerminalSettings::instance().colors[13]), + + qMakePair(QLatin1String("Color6/Color"), &TerminalSettings::instance().colors[6]), + qMakePair(QLatin1String("Color6Intense/Color"), &TerminalSettings::instance().colors[14]), + + qMakePair(QLatin1String("Color7/Color"), &TerminalSettings::instance().colors[7]), + qMakePair(QLatin1String("Color7Intense/Color"), &TerminalSettings::instance().colors[15]) + }; + // clang-format on + + for (const auto &colorKey : colorKeys) { + if (settings.contains(colorKey.first)) { + const auto color = parseColor(settings.value(colorKey.first).toStringList()); + if (!color) + return make_unexpected(color.error()); + + colorKey.second->setVolatileValue(*color); + } + } + + return {}; +} + +static expected_str loadXFCE4ColorScheme(const FilePath &path) +{ + expected_str arr = path.fileContents(); + if (!arr) + return make_unexpected(arr.error()); + + arr->replace(';', ','); + + QTemporaryFile f; + f.open(); + f.write(*arr); + f.close(); + + QSettings settings(f.fileName(), QSettings::IniFormat); + + // clang-format off + const QList> colorKeys = { + qMakePair(QLatin1String("Scheme/ColorBackground"), &TerminalSettings::instance().backgroundColor), + qMakePair(QLatin1String("Scheme/ColorForeground"), &TerminalSettings::instance().foregroundColor), + }; + // clang-format on + + for (const auto &colorKey : colorKeys) { + if (settings.contains(colorKey.first)) { + colorKey.second->setVolatileValue(QColor(settings.value(colorKey.first).toString())); + } + } + + QStringList colors = settings.value(QLatin1String("Scheme/ColorPalette")).toStringList(); + int i = 0; + for (const auto &color : colors) { + TerminalSettings::instance().colors[i++].setVolatileValue(QColor(color)); + } + + return {}; +} + +static expected_str loadColorScheme(const FilePath &path) +{ + if (path.endsWith("Xdefaults")) + return loadXdefaults(path); + else if (path.suffix() == "itermcolors") + return loadItermColors(path); + else if (path.suffix() == "json") + return loadVsCodeColors(path); + else if (path.suffix() == "colorscheme") + return loadKonsoleColorScheme(path); + else if (path.suffix() == "theme" || path.completeSuffix() == "theme.txt") + return loadXFCE4ColorScheme(path); + + return make_unexpected(Tr::tr("Unknown color scheme format")); +} + QWidget *TerminalSettingsPage::widget() { QWidget *widget = new QWidget; @@ -45,6 +336,33 @@ QWidget *TerminalSettingsPage::widget() TerminalSettings &settings = TerminalSettings::instance(); + QPushButton *loadThemeButton = new QPushButton(Tr::tr("Load Theme...")); + + connect(loadThemeButton, &QPushButton::clicked, this, [widget] { + const FilePath path = FileUtils::getOpenFilePath( + widget, + "Open Theme", + {}, + "All Scheme formats (*.itermcolors *.json *.colorscheme *.theme *.theme.txt);;" + "Xdefaults (.Xdefaults Xdefaults);;" + "iTerm Color Schemes(*.itermcolors);;" + "VS Code Color Schemes(*.json);;" + "Konsole Color Schemes(*.colorscheme);;" + "XFCE4 Terminal Color Schemes(*.theme *.theme.txt);;" + "All files (*)", + nullptr, + {}, + true, + false); + + if (path.isEmpty()) + return; + + const expected_str result = loadColorScheme(path); + if (!result) + QMessageBox::warning(widget, Tr::tr("Error"), result.error()); + }); + // clang-format off Column { Row { @@ -83,7 +401,10 @@ QWidget *TerminalSettingsPage::widget() settings.colors[12], settings.colors[13], settings.colors[14], settings.colors[15] }, - } + Row { + loadThemeButton, st, + } + }, }, Row { settings.shell, @@ -92,6 +413,19 @@ QWidget *TerminalSettingsPage::widget() }.attachTo(widget); // clang-format on + DropSupport *dropSupport = new DropSupport(widget); + connect(dropSupport, + &DropSupport::filesDropped, + this, + [widget](const QList &files) { + if (files.size() != 1) + return; + + const expected_str result = loadColorScheme(files.at(0).filePath); + if (!result) + QMessageBox::warning(widget, Tr::tr("Error"), result.error()); + }); + return widget; }