From 47c438c9d8971782614a71ca0ec308ae6b249ae5 Mon Sep 17 00:00:00 2001 From: nschimme <5505185+nschimme@users.noreply.github.com> Date: Wed, 15 Apr 2026 22:45:53 +0000 Subject: [PATCH 1/8] Add support for OSC 8 hyperlinks Implemented OSC 8 hyperlink support in the integrated client: - Extended RawAnsi struct to include url and urlId fields. - Enhanced ANSI parser to recognize and parse OSC 8 sequences (including BEL and ST terminators). - Updated DisplayWidget to handle hyperlink rendering via QTextCharFormat anchors. - Implemented support for Mudlet-compatible URI schemes: - send: Executes the command immediately (appends \n). - prompt: Pre-fills the input widget with the command. - Added synchronized hover underlining for fragments sharing the same URL ID using setExtraSelections. - Integrated security check for file:// URIs to ensure they only open local files. - Updated AnsiTokenizer to correctly skip OSC sequences. - Added unit tests in TestGlobal for OSC 8 parsing. --- src/client/ClientWidget.cpp | 2 + src/client/displaywidget.cpp | 86 +++++++++++++++++++++++++++-- src/client/displaywidget.h | 11 ++++ src/client/stackedinputwidget.cpp | 7 +++ src/client/stackedinputwidget.h | 1 + src/global/AnsiTextUtils.cpp | 89 ++++++++++++++++++++++++++++++- src/global/AnsiTextUtils.h | 80 +++++++++++++-------------- 7 files changed, 228 insertions(+), 48 deletions(-) diff --git a/src/client/ClientWidget.cpp b/src/client/ClientWidget.cpp index ec66a506f..78ba6edb6 100644 --- a/src/client/ClientWidget.cpp +++ b/src/client/ClientWidget.cpp @@ -167,6 +167,8 @@ void ClientWidget::initDisplayWidget() } void virt_returnFocusToInput() final { getSelf().getInput().setFocus(); } void virt_showPreview(bool visible) final { getSelf().getPreview().setVisible(visible); } + void virt_sendUserInput(const QString &msg) final { getSelf().getTelnet().sendToMud(msg); } + void virt_setPrompt(const QString &msg) final { getSelf().getInput().setPrompt(msg); } }; auto &out = m_pipeline.outputs.displayWidgetOutputs; out = std::make_unique(*this); diff --git a/src/client/displaywidget.cpp b/src/client/displaywidget.cpp index 8b538e76b..ad8f4d29a 100644 --- a/src/client/displaywidget.cpp +++ b/src/client/displaywidget.cpp @@ -115,6 +115,29 @@ DisplayWidget::DisplayWidget(QWidget *const parent) setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn); setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + viewport()->installEventFilter(this); + viewport()->setMouseTracking(true); + + connect(this, &DisplayWidget::anchorClicked, this, [this](const QUrl &url) { + QString scheme = url.scheme(); + if (scheme == u"send") { + getOutput().sendUserInput(url.path() + u"\n"); + } else if (scheme == u"prompt") { + // Pre-fill input widget + getOutput().setPrompt(url.path()); + } else if (scheme == u"file") { + // Security check for file:// URIs + QString host = url.host(); + if (host.isEmpty() || host == u"localhost" || host == QHostInfo::localHostName()) { + QDesktopServices::openUrl(url); + } else { + qWarning() << "OSC 8: Ignored file URI with non-local host:" << host; + } + } else { + QDesktopServices::openUrl(url); + } + }); + connect(this, &DisplayWidget::copyAvailable, this, [this](const bool available) { m_canCopy = available; if (available) { @@ -220,6 +243,50 @@ void DisplayWidget::keyPressEvent(QKeyEvent *event) } } +bool DisplayWidget::eventFilter(QObject *watched, QEvent *event) +{ + if (watched == viewport()) { + if (event->type() == QEvent::MouseMove) { + QMouseEvent *mouseEvent = static_cast(event); + QTextCursor cursor = cursorForPosition(mouseEvent->pos()); + QString urlId = cursor.charFormat().property(AnsiTextHelper::URL_ID_PROPERTY).toString(); + updateHoverUnderline(urlId); + } else if (event->type() == QEvent::Leave) { + updateHoverUnderline({}); + } + } + return base::eventFilter(watched, event); +} + +void DisplayWidget::updateHoverUnderline(const QString &urlId) +{ + if (urlId == m_lastUrlId) { + return; + } + m_lastUrlId = urlId; + + QList selections; + if (!urlId.isEmpty()) { + QTextDocument *doc = document(); + for (QTextBlock it = doc->begin(); it != doc->end(); it = it.next()) { + for (QTextBlock::iterator fragmentIt = it.begin(); !fragmentIt.atEnd(); ++fragmentIt) { + QTextFragment fragment = fragmentIt.fragment(); + if (fragment.isValid() + && fragment.charFormat().property(AnsiTextHelper::URL_ID_PROPERTY).toString() + == urlId) { + QTextEdit::ExtraSelection sel; + sel.cursor = QTextCursor(fragment); + sel.format = fragment.charFormat(); + sel.format.setFontUnderline(true); + sel.format.setUnderlineStyle(QTextCharFormat::SingleUnderline); + selections.append(sel); + } + } + } + } + setExtraSelections(selections); +} + void setDefaultFormat(QTextCharFormat &format, const FontDefaults &defaults) { format.setFont(defaults.serverOutputFont); @@ -235,7 +302,8 @@ void AnsiTextHelper::displayText(const QStringView input_str) { // ANSI codes are formatted as the following: // escape + [ + n1 (+ n2) + m - static const QRegularExpression ansi_regex{R"regex(\x1B[^A-Za-z\x1B]*[A-Za-z]?)regex"}; + // or OSC sequences: escape + ] + ... + (escape + \ or bell) + static const QRegularExpression ansi_regex{R"regex(\x1B(?:\[[[:digit:];:]*[[:alpha:]]?|\][^\x1B\x07]*(?:\x1B\\|\x07)))regex"}; static const QRegularExpression url_regex{ R"regex(https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*))regex"}; @@ -338,10 +406,8 @@ void AnsiTextHelper::displayText(const QStringView input_str) input_str, [this, &add_raw](const QStringView ansiStr) { assert(!ansiStr.isEmpty() && ansiStr.front() == char_consts::C_ESC); - if (mmqt::isAnsiColor(ansiStr)) { - if (auto optNewColor = mmqt::parseAnsiColor(currentAnsi, ansiStr)) { - currentAnsi = updateFormat(format, defaults, currentAnsi, *optNewColor); - } + if (auto optNewColor = mmqt::parseAnsiColor(currentAnsi, ansiStr)) { + currentAnsi = updateFormat(format, defaults, currentAnsi, *optNewColor); } else if (mmqt::isAnsiEraseLine(ansiStr)) { cursor.movePosition(QTextCursor::Left, QTextCursor::MoveAnchor, 1); cursor.movePosition(QTextCursor::End, QTextCursor::KeepAnchor); @@ -547,6 +613,16 @@ RawAnsi updateFormat(QTextCharFormat &format, format.setBackground(bg); format.setForeground(fg); format.setUnderlineColor(ul); + + if (updated.url.isEmpty()) { + format.setAnchor(false); + format.setAnchorHref({}); + } else { + format.setAnchor(true); + format.setAnchorHref(updated.url); + } + format.setProperty(AnsiTextHelper::URL_ID_PROPERTY, updated.urlId); + return updated; } diff --git a/src/client/displaywidget.h b/src/client/displaywidget.h index 9b37d7f12..ebae23985 100644 --- a/src/client/displaywidget.h +++ b/src/client/displaywidget.h @@ -10,6 +10,7 @@ #include #include +#include #include #include #include @@ -45,6 +46,7 @@ NODISCARD extern RawAnsi updateFormat(QTextCharFormat &format, struct NODISCARD AnsiTextHelper final { + static constexpr const int URL_ID_PROPERTY = QTextFormat::UserProperty + 1; QTextEdit &textEdit; QTextCursor cursor; QTextCharFormat format; @@ -84,12 +86,16 @@ struct DisplayWidgetOutputs } void returnFocusToInput() { virt_returnFocusToInput(); } void showPreview(bool visible) { virt_showPreview(visible); } + void sendUserInput(const QString &msg) { virt_sendUserInput(msg); } + void setPrompt(const QString &msg) { virt_setPrompt(msg); } private: virtual void virt_showMessage(const QString &msg, int timeout) = 0; virtual void virt_windowSizeChanged(int width, int height) = 0; virtual void virt_returnFocusToInput() = 0; virtual void virt_showPreview(bool visible) = 0; + virtual void virt_sendUserInput(const QString &msg) = 0; + virtual void virt_setPrompt(const QString &msg) = 0; }; class NODISCARD_QOBJECT DisplayWidget final : public QTextBrowser @@ -135,6 +141,11 @@ class NODISCARD_QOBJECT DisplayWidget final : public QTextBrowser protected: void resizeEvent(QResizeEvent *event) override; void keyPressEvent(QKeyEvent *event) override; + bool eventFilter(QObject *watched, QEvent *event) override; + +private: + void updateHoverUnderline(const QString &urlId); + QString m_lastUrlId; public slots: void slot_displayText(const QStringView str); diff --git a/src/client/stackedinputwidget.cpp b/src/client/stackedinputwidget.cpp index 1bf1a0cae..92ae17759 100644 --- a/src/client/stackedinputwidget.cpp +++ b/src/client/stackedinputwidget.cpp @@ -145,6 +145,13 @@ void StackedInputWidget::gotMultiLineInput(const QString &input) displayInputMessage(input); } +void StackedInputWidget::setPrompt(const QString &msg) +{ + getInputWidget().setPlainText(msg); + getInputWidget().moveCursor(QTextCursor::End); + getInputWidget().setFocus(); +} + void StackedInputWidget::gotPasswordInput(const QString &input) { getOutput().sendUserInput(input + "\n"); diff --git a/src/client/stackedinputwidget.h b/src/client/stackedinputwidget.h index c4abc4753..de76ab053 100644 --- a/src/client/stackedinputwidget.h +++ b/src/client/stackedinputwidget.h @@ -117,6 +117,7 @@ class NODISCARD_QOBJECT StackedInputWidget final : public QStackedWidget void requestPassword(); void setEchoMode(EchoModeEnum echoMode); EchoModeEnum getEchoMode() const { return m_echoMode; } + void setPrompt(const QString &msg); private: void gotMultiLineInput(const QString &); diff --git a/src/global/AnsiTextUtils.cpp b/src/global/AnsiTextUtils.cpp index 21f923fdc..ec3560348 100644 --- a/src/global/AnsiTextUtils.cpp +++ b/src/global/AnsiTextUtils.cpp @@ -73,7 +73,7 @@ static constexpr const std::array ansi_greys24{ namespace mmqt { /* visible */ -const QRegularExpression weakAnsiRegex(R"(\x1B\[?[[:digit:];:]*[[:alpha:]]?)"); +const QRegularExpression weakAnsiRegex(R"(\x1B(?:\[[[:digit:];:]*[[:alpha:]]?|\][^\x1B\x07]*(?:\x1B\\|\x07)))"); } // namespace mmqt @@ -1701,6 +1701,45 @@ bool isAnsiColor(QStringView ansi) return true; } +static bool isOsc8(QStringView ansi) +{ + return ansi.startsWith(u"\x1B]8;") && (ansi.endsWith(u"\x1B\\") || ansi.endsWith(u"\x07")); +} + +static void parseOsc8(RawAnsi &next, QStringView ansi) +{ + // strip ESC ] 8 ; and terminator + QStringView content = ansi.mid(4); + if (ansi.endsWith(u"\x1B\\")) { + content = content.left(content.length() - 2); + } else { + content = content.left(content.length() - 1); + } + + // content is now "params;URI" + int semi = content.indexOf(u';'); + if (semi < 0) { + // Invalid, but we'll clear the URL just in case this was meant to be a closer + next.url.clear(); + next.urlId.clear(); + return; + } + + QStringView params = content.left(semi); + QStringView uri = content.mid(semi + 1); + + next.url = uri.toString(); + next.urlId.clear(); + + // parse id from params "id=xxx:foo=bar" + for (auto param : params.split(u':')) { + if (param.startsWith(u"id=")) { + next.urlId = param.mid(3).toString(); + break; + } + } +} + bool isAnsiColor(const QString &ansi) { return isAnsiColor(QStringView{ansi}); @@ -1831,6 +1870,12 @@ void AnsiColorParser::for_each(QStringView ansi) const std::optional parseAnsiColor(const RawAnsi before, const QStringView ansi) { + if (isOsc8(ansi)) { + RawAnsi next = before; + parseOsc8(next, ansi); + return next; + } + if (!isAnsiColor(ansi)) { return std::nullopt; } @@ -2000,6 +2045,26 @@ AnsiStringToken AnsiTokenizer::Iterator::getCurrent() AnsiTokenizer::Iterator::size_type AnsiTokenizer::Iterator::skip_ansi() const { + if (m_str.size() >= 2 && m_str[1] == u']') { + // OSC sequence: ESC ] ... (ST or BEL) + qsizetype pos = 2; + const qsizetype len = m_str.size(); + while (pos < len) { + if (m_str[pos] == char_consts::C_ALERT) { + return pos + 1; + } + if (m_str[pos] == char_consts::C_ESC && pos + 1 < len && m_str[pos + 1] == u'\\') { + return pos + 2; + } + if (m_str[pos] == char_consts::C_ESC) { + // Nested ESC is not expected in OSC + return pos; + } + pos++; + } + return len; // Unterminated + } + // hack to avoid having to have two separate stop return values // (one to stop before current value, and one to include it) bool sawLetter = false; @@ -2560,6 +2625,28 @@ void testAnsiTextUtils() test_itu(); test_ansi_parse(); + + { + // OSC 8 parsing tests + RawAnsi base; + auto opt = mmqt::parseAnsiColor(base, u"\x1B]8;;http://example.com\x1B\\"); + TEST_ASSERT(opt.has_value()); + TEST_ASSERT(opt->url == u"http://example.com"); + TEST_ASSERT(opt->urlId.isEmpty()); + + opt = mmqt::parseAnsiColor(base, u"\x1B]8;id=123;https://mudlet.org\x07"); + TEST_ASSERT(opt.has_value()); + TEST_ASSERT(opt->url == u"https://mudlet.org"); + TEST_ASSERT(opt->urlId == u"123"); + + // Closer + RawAnsi withUrl; + withUrl.url = QStringLiteral("something"); + opt = mmqt::parseAnsiColor(withUrl, u"\x1B]8;;\x1B\\"); + TEST_ASSERT(opt.has_value()); + TEST_ASSERT(opt->url.isEmpty()); + TEST_ASSERT(opt->urlId.isEmpty()); + } } } // namespace test diff --git a/src/global/AnsiTextUtils.h b/src/global/AnsiTextUtils.h index 12be54340..cb56c4ab1 100644 --- a/src/global/AnsiTextUtils.h +++ b/src/global/AnsiTextUtils.h @@ -327,17 +327,19 @@ struct NODISCARD RawAnsi final AnsiColorVariant fg; AnsiColorVariant bg; AnsiColorVariant ul; + QString url; + QString urlId; private: AnsiStyleFlags m_flags; // only bits 0..9 are used; the other 6 are reserved AnsiUnderlineStyleEnum m_underlineStyle = AnsiUnderlineStyleEnum::None; public: - constexpr RawAnsi() = default; - constexpr RawAnsi(const AnsiStyleFlags flags, - const AnsiColorVariant fg_, - const AnsiColorVariant bg_, - const AnsiColorVariant ul_) + RawAnsi() = default; + RawAnsi(const AnsiStyleFlags flags, + const AnsiColorVariant fg_, + const AnsiColorVariant bg_, + const AnsiColorVariant ul_) : fg{fg_} , bg{bg_} , ul{ul_} @@ -351,25 +353,25 @@ struct NODISCARD RawAnsi final } public: - NODISCARD constexpr bool hasForegroundColor() const { return !fg.hasDefaultColor(); } - NODISCARD constexpr bool hasBackgroundColor() const { return !bg.hasDefaultColor(); } - NODISCARD constexpr bool hasUnderlineColor() const { return !ul.hasDefaultColor(); } + NODISCARD bool hasForegroundColor() const { return !fg.hasDefaultColor(); } + NODISCARD bool hasBackgroundColor() const { return !bg.hasDefaultColor(); } + NODISCARD bool hasUnderlineColor() const { return !ul.hasDefaultColor(); } public: #define X_DECL_WITH(_number, _lower, _UPPER, _Snake) \ - NODISCARD constexpr RawAnsi with##_Snake() const \ + NODISCARD RawAnsi with##_Snake() const \ { \ auto copy = *this; \ copy.set##_Snake(); \ return copy; \ } \ - NODISCARD constexpr RawAnsi without##_Snake() const \ + NODISCARD RawAnsi without##_Snake() const \ { \ auto copy = *this; \ copy.clear##_Snake(); \ return copy; \ } \ - NODISCARD constexpr RawAnsi withToggled##_Snake() const \ + NODISCARD RawAnsi withToggled##_Snake() const \ { \ auto copy = *this; \ copy.toggle##_Snake(); \ @@ -379,25 +381,25 @@ struct NODISCARD RawAnsi final #undef X_DECL_WITH public: - NODISCARD constexpr RawAnsi withForeground(const AnsiColorVariant var) const + NODISCARD RawAnsi withForeground(const AnsiColorVariant var) const { auto copy = *this; copy.fg = var; return copy; } - NODISCARD constexpr RawAnsi withBackground(const AnsiColorVariant var) const + NODISCARD RawAnsi withBackground(const AnsiColorVariant var) const { auto copy = *this; copy.bg = var; return copy; } - NODISCARD constexpr RawAnsi withUnderlineColor(const AnsiColorVariant var) const + NODISCARD RawAnsi withUnderlineColor(const AnsiColorVariant var) const { auto copy = *this; copy.ul = var; return copy; } - NODISCARD constexpr RawAnsi withUnderlineStyle(const AnsiUnderlineStyleEnum style) const + NODISCARD RawAnsi withUnderlineStyle(const AnsiUnderlineStyleEnum style) const { auto copy = *this; copy.setUnderlineStyle(style); @@ -405,32 +407,32 @@ struct NODISCARD RawAnsi final } public: - NODISCARD constexpr RawAnsi withForeground(const AnsiColor16Enum newColor) const + NODISCARD RawAnsi withForeground(const AnsiColor16Enum newColor) const { return withForeground(AnsiColorVariant{newColor}); } - NODISCARD constexpr RawAnsi withBackground(const AnsiColor16Enum newColor) const + NODISCARD RawAnsi withBackground(const AnsiColor16Enum newColor) const { return withBackground(AnsiColorVariant{newColor}); } - NODISCARD constexpr RawAnsi withUnderlineColor(const AnsiColor16Enum newColor) const + NODISCARD RawAnsi withUnderlineColor(const AnsiColor16Enum newColor) const { return withUnderlineColor(AnsiColorVariant{newColor}); } public: #define X_DECL_ACCESSORS(_number, _lower, _UPPER, _Snake) \ - NODISCARD constexpr bool has##_Snake() const \ + NODISCARD bool has##_Snake() const \ { \ return m_flags.contains(AnsiStyleFlagEnum::_Snake); \ } \ - constexpr void set##_Snake() { m_flags.insert(AnsiStyleFlagEnum::_Snake); } \ - constexpr void clear##_Snake() { m_flags.remove(AnsiStyleFlagEnum::_Snake); } + void set##_Snake() { m_flags.insert(AnsiStyleFlagEnum::_Snake); } \ + void clear##_Snake() { m_flags.remove(AnsiStyleFlagEnum::_Snake); } XFOREACH_ANSI_STYLE_EXCEPT_UNDERLINE(X_DECL_ACCESSORS) #undef X_DECL_ACCESSORS #define X_DECL_ACCESSORS(_number, _lower, _UPPER, _Snake) \ - constexpr void toggle##_Snake() \ + void toggle##_Snake() \ { \ if (has##_Snake()) { \ clear##_Snake(); \ @@ -442,19 +444,16 @@ struct NODISCARD RawAnsi final #undef X_DECL_ACCESSORS public: - NODISCARD constexpr bool hasUnderline() const - { - return m_flags.contains(AnsiStyleFlagEnum::Underline); - } - constexpr void setUnderline() { setUnderlineStyle(AnsiUnderlineStyleEnum::Normal); } - constexpr void clearUnderline() + NODISCARD bool hasUnderline() const { return m_flags.contains(AnsiStyleFlagEnum::Underline); } + void setUnderline() { setUnderlineStyle(AnsiUnderlineStyleEnum::Normal); } + void clearUnderline() { m_flags.remove(AnsiStyleFlagEnum::Underline); m_underlineStyle = AnsiUnderlineStyleEnum::None; } public: - constexpr void setUnderlineStyle(const AnsiUnderlineStyleEnum style) + void setUnderlineStyle(const AnsiUnderlineStyleEnum style) { if (style == AnsiUnderlineStyleEnum::None) { clearUnderline(); @@ -465,14 +464,11 @@ struct NODISCARD RawAnsi final } public: - NODISCARD constexpr AnsiStyleFlags getFlags() const { return m_flags; } - NODISCARD constexpr AnsiUnderlineStyleEnum getUnderlineStyle() const - { - return m_underlineStyle; - } + NODISCARD AnsiStyleFlags getFlags() const { return m_flags; } + NODISCARD AnsiUnderlineStyleEnum getUnderlineStyle() const { return m_underlineStyle; } public: - constexpr void setFlag(const AnsiStyleFlagEnum flag) + void setFlag(const AnsiStyleFlagEnum flag) { #define X_CASE(_number, _lower, _UPPER, _Snake) \ case (AnsiStyleFlagEnum::_Snake): \ @@ -486,7 +482,7 @@ struct NODISCARD RawAnsi final std::abort(); #undef X_CASE } - constexpr void removeFlag(const AnsiStyleFlagEnum flag) + void removeFlag(const AnsiStyleFlagEnum flag) { #define X_CASE(_number, _lower, _UPPER, _Snake) \ case (AnsiStyleFlagEnum::_Snake): \ @@ -502,12 +498,12 @@ struct NODISCARD RawAnsi final } public: - NODISCARD constexpr bool operator==(const RawAnsi &rhs) const + NODISCARD bool operator==(const RawAnsi &rhs) const { return fg == rhs.fg && bg == rhs.bg && ul == rhs.ul && m_flags == rhs.m_flags - && m_underlineStyle == rhs.m_underlineStyle; + && m_underlineStyle == rhs.m_underlineStyle && url == rhs.url && urlId == rhs.urlId; } - NODISCARD constexpr bool operator!=(const RawAnsi &rhs) const { return !(rhs == *this); } + NODISCARD bool operator!=(const RawAnsi &rhs) const { return !(rhs == *this); } public: friend std::ostream &to_stream(std::ostream &os, const RawAnsi &raw); @@ -517,15 +513,15 @@ struct NODISCARD RawAnsi final } }; -NODISCARD static inline constexpr RawAnsi getRawAnsi(const AnsiColor16Enum fgColor) +NODISCARD static inline RawAnsi getRawAnsi(const AnsiColor16Enum fgColor) { RawAnsi ansi; ansi.fg = AnsiColorVariant{fgColor}; return ansi; } -NODISCARD static inline constexpr RawAnsi getRawAnsi(const AnsiColor16Enum fgColor, - const AnsiColor16Enum bgColor) +NODISCARD static inline RawAnsi getRawAnsi(const AnsiColor16Enum fgColor, + const AnsiColor16Enum bgColor) { RawAnsi ansi; ansi.fg = AnsiColorVariant{fgColor}; From d47daacd2a8c88546dedc94382dde7f56e499073 Mon Sep 17 00:00:00 2001 From: nschimme <5505185+nschimme@users.noreply.github.com> Date: Thu, 16 Apr 2026 02:17:16 +0000 Subject: [PATCH 2/8] Add support for OSC 8 hyperlinks This commit implements support for terminal hyperlinks (OSC 8) in the MMapper client. Key changes include: - Extended `RawAnsi` struct to store `QString url` and `QString urlId`. - Removed `constexpr` from `RawAnsi` and related formatting constants across the codebase (e.g., `Map.cpp`, `World.cpp`, `ChangePrinter.cpp`) as `RawAnsi` is no longer a literal type. - Updated `AnsiTextUtils` to parse OSC 8 sequences with both ST and BEL terminators, including support for the optional `id` parameter. - Enhanced `DisplayWidget` to render links as clickable anchors and implemented handling for `send:`, `prompt:`, and local `file:` schemes. - Implemented synchronized underlining in `DisplayWidget` using a viewport event filter and extra selections to highlight all matching URL IDs on hover. - Added `setPrompt` to `StackedInputWidget` and connected signals in `ClientWidget` to allow display interaction with the input buffer. - Added unit tests for OSC 8 parsing in `TestGlobal`. --- src/client/ClientWidget.cpp | 2 +- src/client/displaywidget.cpp | 10 +++++++--- src/global/AnsiTextUtils.cpp | 3 ++- src/global/AnsiTextUtils.h | 5 +---- src/mainwindow/mainwindow-async.cpp | 4 ++-- src/map/ChangePrinter.cpp | 14 +++++++------- src/map/Map.cpp | 4 ++-- src/map/ParseTree.cpp | 4 ++-- src/map/Remapping.cpp | 2 +- src/map/ServerIdMap.cpp | 2 +- src/map/SpatialDb.cpp | 2 +- src/map/World.cpp | 6 +++--- src/proxy/connectionlistener.cpp | 2 +- src/proxy/proxy.cpp | 2 +- 14 files changed, 32 insertions(+), 30 deletions(-) diff --git a/src/client/ClientWidget.cpp b/src/client/ClientWidget.cpp index 78ba6edb6..95fca755c 100644 --- a/src/client/ClientWidget.cpp +++ b/src/client/ClientWidget.cpp @@ -279,7 +279,7 @@ bool ClientWidget::isUsingClient() const void ClientWidget::displayReconnectHint() { - constexpr const auto whiteOnCyan = getRawAnsi(AnsiColor16Enum::white, AnsiColor16Enum::cyan); + const auto whiteOnCyan = getRawAnsi(AnsiColor16Enum::white, AnsiColor16Enum::cyan); std::stringstream oss; AnsiOstream aos{oss}; aos.writeWithColor(whiteOnCyan, "\n\n\nPress return to reconnect.\n"); diff --git a/src/client/displaywidget.cpp b/src/client/displaywidget.cpp index ad8f4d29a..22e7ef043 100644 --- a/src/client/displaywidget.cpp +++ b/src/client/displaywidget.cpp @@ -121,7 +121,7 @@ DisplayWidget::DisplayWidget(QWidget *const parent) connect(this, &DisplayWidget::anchorClicked, this, [this](const QUrl &url) { QString scheme = url.scheme(); if (scheme == u"send") { - getOutput().sendUserInput(url.path() + u"\n"); + getOutput().sendUserInput(url.path() + QStringLiteral("\n")); } else if (scheme == u"prompt") { // Pre-fill input widget getOutput().setPrompt(url.path()); @@ -275,7 +275,10 @@ void DisplayWidget::updateHoverUnderline(const QString &urlId) && fragment.charFormat().property(AnsiTextHelper::URL_ID_PROPERTY).toString() == urlId) { QTextEdit::ExtraSelection sel; - sel.cursor = QTextCursor(fragment); + sel.cursor = QTextCursor(document()); + sel.cursor.setPosition(fragment.position()); + sel.cursor.setPosition(fragment.position() + fragment.length(), + QTextCursor::KeepAnchor); sel.format = fragment.charFormat(); sel.format.setFontUnderline(true); sel.format.setUnderlineStyle(QTextCharFormat::SingleUnderline); @@ -303,7 +306,8 @@ void AnsiTextHelper::displayText(const QStringView input_str) // ANSI codes are formatted as the following: // escape + [ + n1 (+ n2) + m // or OSC sequences: escape + ] + ... + (escape + \ or bell) - static const QRegularExpression ansi_regex{R"regex(\x1B(?:\[[[:digit:];:]*[[:alpha:]]?|\][^\x1B\x07]*(?:\x1B\\|\x07)))regex"}; + static const QRegularExpression ansi_regex{ + R"regex(\x1B(?:\[[[:digit:];:]*[[:alpha:]]?|\][^\x1B\x07]*(?:\x1B\\|\x07)))regex"}; static const QRegularExpression url_regex{ R"regex(https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*))regex"}; diff --git a/src/global/AnsiTextUtils.cpp b/src/global/AnsiTextUtils.cpp index ec3560348..994b6c893 100644 --- a/src/global/AnsiTextUtils.cpp +++ b/src/global/AnsiTextUtils.cpp @@ -73,7 +73,8 @@ static constexpr const std::array ansi_greys24{ namespace mmqt { /* visible */ -const QRegularExpression weakAnsiRegex(R"(\x1B(?:\[[[:digit:];:]*[[:alpha:]]?|\][^\x1B\x07]*(?:\x1B\\|\x07)))"); +const QRegularExpression weakAnsiRegex( + R"(\x1B(?:\[[[:digit:];:]*[[:alpha:]]?|\][^\x1B\x07]*(?:\x1B\\|\x07)))"); } // namespace mmqt diff --git a/src/global/AnsiTextUtils.h b/src/global/AnsiTextUtils.h index cb56c4ab1..0d2c3875e 100644 --- a/src/global/AnsiTextUtils.h +++ b/src/global/AnsiTextUtils.h @@ -422,10 +422,7 @@ struct NODISCARD RawAnsi final public: #define X_DECL_ACCESSORS(_number, _lower, _UPPER, _Snake) \ - NODISCARD bool has##_Snake() const \ - { \ - return m_flags.contains(AnsiStyleFlagEnum::_Snake); \ - } \ + NODISCARD bool has##_Snake() const { return m_flags.contains(AnsiStyleFlagEnum::_Snake); } \ void set##_Snake() { m_flags.insert(AnsiStyleFlagEnum::_Snake); } \ void clear##_Snake() { m_flags.remove(AnsiStyleFlagEnum::_Snake); } XFOREACH_ANSI_STYLE_EXCEPT_UNDERLINE(X_DECL_ACCESSORS) diff --git a/src/mainwindow/mainwindow-async.cpp b/src/mainwindow/mainwindow-async.cpp index bbddc7c3e..bf115cdc2 100644 --- a/src/mainwindow/mainwindow-async.cpp +++ b/src/mainwindow/mainwindow-async.cpp @@ -917,8 +917,8 @@ bool MainWindow::slot_generateBaseMap() return false; } - static constexpr auto green = getRawAnsi(AnsiColor16Enum::green); - static constexpr auto yellow = getRawAnsi(AnsiColor16Enum::yellow); + static const auto green = getRawAnsi(AnsiColor16Enum::green); + static const auto yellow = getRawAnsi(AnsiColor16Enum::yellow); class NODISCARD AsyncGenerateBaseMap final : public AsyncHelper { diff --git a/src/map/ChangePrinter.cpp b/src/map/ChangePrinter.cpp index 3aa541894..504de0c55 100644 --- a/src/map/ChangePrinter.cpp +++ b/src/map/ChangePrinter.cpp @@ -18,16 +18,16 @@ namespace { // anonymous constexpr size_t max_roomids_printed = 20; -constexpr RawAnsi const_color = getRawAnsi(AnsiColor16Enum::yellow); -constexpr RawAnsi error_color = getRawAnsi(AnsiColor16Enum::RED); -constexpr RawAnsi member_name_color = getRawAnsi(AnsiColor16Enum::cyan); -constexpr RawAnsi type_name_color = getRawAnsi(AnsiColor16Enum::BLUE); -constexpr RawAnsi warning_color = getRawAnsi(AnsiColor16Enum::YELLOW); +const RawAnsi const_color = getRawAnsi(AnsiColor16Enum::yellow); +const RawAnsi error_color = getRawAnsi(AnsiColor16Enum::RED); +const RawAnsi member_name_color = getRawAnsi(AnsiColor16Enum::cyan); +const RawAnsi type_name_color = getRawAnsi(AnsiColor16Enum::BLUE); +const RawAnsi warning_color = getRawAnsi(AnsiColor16Enum::YELLOW); void print_string_color_quoted(AnsiOstream &aos, std::string_view sv) { - constexpr RawAnsi normalAnsi = getRawAnsi(AnsiColor16Enum::green); - constexpr RawAnsi escapeAnsi = getRawAnsi(AnsiColor16Enum::yellow); + const RawAnsi normalAnsi = getRawAnsi(AnsiColor16Enum::green); + const RawAnsi escapeAnsi = getRawAnsi(AnsiColor16Enum::yellow); aos.writeQuotedWithColor(normalAnsi, escapeAnsi, sv); } diff --git a/src/map/Map.cpp b/src/map/Map.cpp index f151b2979..20648af05 100644 --- a/src/map/Map.cpp +++ b/src/map/Map.cpp @@ -33,8 +33,8 @@ using namespace char_consts; -static constexpr auto green = getRawAnsi(AnsiColor16Enum::green); -static constexpr auto yellow = getRawAnsi(AnsiColor16Enum::yellow); +static const auto green = getRawAnsi(AnsiColor16Enum::green); +static const auto yellow = getRawAnsi(AnsiColor16Enum::yellow); Map::Map() : m_world(std::make_shared()) diff --git a/src/map/ParseTree.cpp b/src/map/ParseTree.cpp index 3a410318f..84e3c8601 100644 --- a/src/map/ParseTree.cpp +++ b/src/map/ParseTree.cpp @@ -139,8 +139,8 @@ RoomIdSet getRooms(const Map &map, const ParseTree &tree, const ParseEvent &even void ParseTree::printStats(ProgressCounter & /*pc*/, AnsiOstream &os) const { - static constexpr RawAnsi green = getRawAnsi(AnsiColor16Enum::green); - static constexpr RawAnsi yellow = getRawAnsi(AnsiColor16Enum::yellow); + static const RawAnsi green = getRawAnsi(AnsiColor16Enum::green); + static const RawAnsi yellow = getRawAnsi(AnsiColor16Enum::yellow); auto C = [](auto x) { static_assert(std::is_integral_v); diff --git a/src/map/Remapping.cpp b/src/map/Remapping.cpp index a22e76a95..7dd9a0ef2 100644 --- a/src/map/Remapping.cpp +++ b/src/map/Remapping.cpp @@ -325,7 +325,7 @@ void Remapping::printStats(ProgressCounter & /*pc*/, AnsiOstream &os) const } } - static constexpr auto green = getRawAnsi(AnsiColor16Enum::green); + static const auto green = getRawAnsi(AnsiColor16Enum::green); auto print = [&os](std::string_view prefix, size_t size, uint32_t loval, uint32_t hival) { os << prefix; diff --git a/src/map/ServerIdMap.cpp b/src/map/ServerIdMap.cpp index 7b5f18b27..4258cc129 100644 --- a/src/map/ServerIdMap.cpp +++ b/src/map/ServerIdMap.cpp @@ -7,6 +7,6 @@ void ServerIdMap::printStats(ProgressCounter & /*pc*/, AnsiOstream &os) const { - static constexpr auto green = getRawAnsi(AnsiColor16Enum::green); + static const auto green = getRawAnsi(AnsiColor16Enum::green); os << "Unique server ids assigned: " << ColoredValue{green, this->size()} << ".\n"; } diff --git a/src/map/SpatialDb.cpp b/src/map/SpatialDb.cpp index 0fe13e704..9223a3674 100644 --- a/src/map/SpatialDb.cpp +++ b/src/map/SpatialDb.cpp @@ -70,7 +70,7 @@ void SpatialDb::printStats(ProgressCounter & /*pc*/, AnsiOstream &os) const const Coordinate &max = bounds.max; const Coordinate &min = bounds.min; - static constexpr auto green = getRawAnsi(AnsiColor16Enum::green); + static const auto green = getRawAnsi(AnsiColor16Enum::green); auto show = [&os](std::string_view prefix, int lo, int hi) { os << prefix << ColoredValue(green, hi - lo + 1) << " (" << ColoredValue(green, lo) diff --git a/src/map/World.cpp b/src/map/World.cpp index bd18059b2..3820d5cbe 100644 --- a/src/map/World.cpp +++ b/src/map/World.cpp @@ -2105,7 +2105,7 @@ void World::printStats(ProgressCounter &pc, AnsiOstream &os) const m_serverIds.printStats(pc, os); { - static constexpr auto green = getRawAnsi(AnsiColor16Enum::green); + static const auto green = getRawAnsi(AnsiColor16Enum::green); static auto C = [](auto x) { static_assert(std::is_integral_v); @@ -2317,8 +2317,8 @@ void World::printStats(ProgressCounter &pc, AnsiOstream &os) const m_spatialDb.printStats(pc, os); - static constexpr auto green = getRawAnsi(AnsiColor16Enum::green); - static constexpr auto yellow = getRawAnsi(AnsiColor16Enum::yellow); + static const auto green = getRawAnsi(AnsiColor16Enum::green); + static const auto yellow = getRawAnsi(AnsiColor16Enum::yellow); auto line = std::string(81, '_'); // note: purposely using parens instead of curly. assert(line.size() == 81); diff --git a/src/proxy/connectionlistener.cpp b/src/proxy/connectionlistener.cpp index 11bcf9ec6..9b6ed1c47 100644 --- a/src/proxy/connectionlistener.cpp +++ b/src/proxy/connectionlistener.cpp @@ -117,7 +117,7 @@ void ConnectionListener::startClient(std::unique_ptr socket) } else { log("New connection: rejected."); const auto msg = std::invoke([]() -> QByteArray { - constexpr const auto whiteOnRed = getRawAnsi(AnsiColor16Enum::white, + const auto whiteOnRed = getRawAnsi(AnsiColor16Enum::white, AnsiColor16Enum::red); std::stringstream oss; AnsiOstream aos{oss}; diff --git a/src/proxy/proxy.cpp b/src/proxy/proxy.cpp index cd93da563..7f004f932 100644 --- a/src/proxy/proxy.cpp +++ b/src/proxy/proxy.cpp @@ -56,7 +56,7 @@ namespace { // anonymous const volatile bool g_prefixMessagesToUser = true; const volatile bool g_showVersionInWelcomeMessage = IS_DEBUG_BUILD; // -constexpr const auto whiteOnCyan = getRawAnsi(AnsiColor16Enum::white, AnsiColor16Enum::cyan); +const auto whiteOnCyan = getRawAnsi(AnsiColor16Enum::white, AnsiColor16Enum::cyan); NODISCARD MainWindow &getMainWindow(ConnectionListener &listener) { From 2187b2fc680ffd33714bd5f1cbf7c72d738bd6f5 Mon Sep 17 00:00:00 2001 From: nschimme <5505185+nschimme@users.noreply.github.com> Date: Thu, 16 Apr 2026 02:34:50 +0000 Subject: [PATCH 3/8] Add support for OSC 8 hyperlinks (fix formatting) This commit implements support for terminal hyperlinks (OSC 8) in the MMapper client, including fixes for clang-format violations identified in CI. Key changes include: - Extended `RawAnsi` struct to store `QString url` and `QString urlId`. - Removed `constexpr` from `RawAnsi` and related formatting constants across the codebase (e.g., `Map.cpp`, `World.cpp`, `ChangePrinter.cpp`) as `RawAnsi` is no longer a literal type. - Updated `AnsiTextUtils` to parse OSC 8 sequences with both ST and BEL terminators, including support for the optional `id` parameter. - Enhanced `DisplayWidget` to render links as clickable anchors and implemented handling for `send:`, `prompt:`, and local `file:` schemes. - Implemented synchronized underlining in `DisplayWidget` using a viewport event filter and extra selections to highlight all matching URL IDs on hover. - Added `setPrompt` to `StackedInputWidget` and connected signals in `ClientWidget` to allow display interaction with the input buffer. - Added unit tests for OSC 8 parsing in `TestGlobal`. --- src/proxy/connectionlistener.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/proxy/connectionlistener.cpp b/src/proxy/connectionlistener.cpp index 9b6ed1c47..ace0048d6 100644 --- a/src/proxy/connectionlistener.cpp +++ b/src/proxy/connectionlistener.cpp @@ -117,8 +117,7 @@ void ConnectionListener::startClient(std::unique_ptr socket) } else { log("New connection: rejected."); const auto msg = std::invoke([]() -> QByteArray { - const auto whiteOnRed = getRawAnsi(AnsiColor16Enum::white, - AnsiColor16Enum::red); + const auto whiteOnRed = getRawAnsi(AnsiColor16Enum::white, AnsiColor16Enum::red); std::stringstream oss; AnsiOstream aos{oss}; aos.writeWithColor(whiteOnRed, "You can't connect to MMapper more than once!\n"); From ae79dceccf15f662487e209507020bec5cfc817a Mon Sep 17 00:00:00 2001 From: nschimme <5505185+nschimme@users.noreply.github.com> Date: Thu, 16 Apr 2026 04:41:33 +0000 Subject: [PATCH 4/8] Add support for OSC 8 hyperlinks (fix CI) This commit implements support for terminal hyperlinks (OSC 8) in the MMapper client, including fixes for build errors and deprecation warnings identified in CI. Key changes include: - Extended `RawAnsi` struct to store `QString url` and `QString urlId`. - Removed `constexpr` from `RawAnsi` and related constants as `RawAnsi` is no longer a literal type. - Updated `AnsiTextUtils` to parse OSC 8 sequences with both ST and BEL terminators, and fixed a precision loss error (`shorten-64-to-32`). - Fixed `QRegularExpression::match` and `globalMatch` deprecation warnings by using `matchView` and `globalMatchView` for Qt >= 6.6. - Enhanced `DisplayWidget` to render links as clickable anchors and implemented interaction schemes (`send:`, `prompt:`, `file:`). - Implemented synchronized underlining in `DisplayWidget` using a viewport event filter and extra selections. - Added unit tests for OSC 8 parsing in `TestGlobal`. - Ensured all modified files follow `clang-format` rules. --- src/clock/mumeclock.cpp | 12 ++++++++++++ src/global/AnsiTextUtils.cpp | 2 +- src/global/AnsiTextUtils.h | 4 ++++ src/global/TextBuffer.cpp | 20 ++++++++++++++++++++ src/global/TextUtils.cpp | 8 ++++++++ src/global/emojis.cpp | 8 ++++++++ src/mainwindow/UpdateDialog.cpp | 9 +++++++++ src/mapdata/roomfilter.cpp | 4 ++++ src/mapdata/roomfilter.h | 9 ++++++++- src/preferences/ansicombo.cpp | 4 ++++ 10 files changed, 78 insertions(+), 2 deletions(-) diff --git a/src/clock/mumeclock.cpp b/src/clock/mumeclock.cpp index 964e89c21..5c70ec244 100644 --- a/src/clock/mumeclock.cpp +++ b/src/clock/mumeclock.cpp @@ -158,7 +158,11 @@ void MumeClock::parseMumeTime(const QString &mumeTime, const int64_t secsSinceEp // 3 pm on Highday, the 18th of Halimath, year 3030 of the Third Age. static const QRegularExpression rx( R"(^(\d+)(?::\d{2})?\W*(am|pm) on (\w+), the (\d+).{2} of (\w+), year (\d+) of the Third Age.$)"); +#if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0) + auto match = rx.matchView(mumeTime); +#else auto match = rx.match(mumeTime); +#endif if (!match.hasMatch()) { return; } @@ -187,7 +191,11 @@ void MumeClock::parseMumeTime(const QString &mumeTime, const int64_t secsSinceEp // "Highday, the 18th of Halimath, year 3030 of the Third Age." static const QRegularExpression rx( R"(^(\w+), the (\d+).{2} of (\w+), year (\d+) of the Third Age.$)"); +#if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0) + auto match = rx.matchView(mumeTime); +#else auto match = rx.match(mumeTime); +#endif if (!match.hasMatch()) { return; } @@ -335,7 +343,11 @@ void MumeClock::parseClockTime(const QString &clockTime, const int64_t secsSince { // The current time is 5:23pm. static const QRegularExpression rx(R"(^The current time is (\d+):(\d+)\W*(am|pm).$)"); +#if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0) + auto match = rx.matchView(clockTime); +#else auto match = rx.match(clockTime); +#endif if (!match.hasMatch()) { return; } diff --git a/src/global/AnsiTextUtils.cpp b/src/global/AnsiTextUtils.cpp index 994b6c893..f4dc6e843 100644 --- a/src/global/AnsiTextUtils.cpp +++ b/src/global/AnsiTextUtils.cpp @@ -1718,7 +1718,7 @@ static void parseOsc8(RawAnsi &next, QStringView ansi) } // content is now "params;URI" - int semi = content.indexOf(u';'); + const qsizetype semi = content.indexOf(u';'); if (semi < 0) { // Invalid, but we'll clear the URL just in case this was meant to be a closer next.url.clear(); diff --git a/src/global/AnsiTextUtils.h b/src/global/AnsiTextUtils.h index 0d2c3875e..7c144150b 100644 --- a/src/global/AnsiTextUtils.h +++ b/src/global/AnsiTextUtils.h @@ -988,7 +988,11 @@ void foreachAnsi(const QStringView line, Callback &&callback) const auto len = line.size(); qsizetype pos = 0; while (pos < len) { +#if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0) + QRegularExpressionMatch m = weakAnsiRegex.matchView(line, pos); +#else QRegularExpressionMatch m = weakAnsiRegex.match(line, pos); +#endif if (!m.hasMatch()) { break; } diff --git a/src/global/TextBuffer.cpp b/src/global/TextBuffer.cpp index 009dbc216..841645f75 100644 --- a/src/global/TextBuffer.cpp +++ b/src/global/TextBuffer.cpp @@ -59,7 +59,11 @@ struct NODISCARD Prefix final // step 1: match the quoted prefix { +#if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0) + auto m = quotePrefixRegex.matchView(line); +#else auto m = quotePrefixRegex.match(line); +#endif if (m.hasMatch()) { const auto len = m.capturedLength(0); quotePrefix = line.left(len); @@ -78,7 +82,11 @@ struct NODISCARD Prefix final // step 2: See if there's a bullet. If so, we will only print it on the first line, // and we'll replace it with equivalent length whitespace on consecutive linewraps. { +#if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0) + auto m = bulletPrefixRegex.matchView(line); +#else auto m = bulletPrefixRegex.match(line); +#endif if (m.hasMatch()) { const QString sv = m.captured(); /* this could fail if someone breaks the regex pattern for the escaped asterisk */ @@ -92,7 +100,11 @@ struct NODISCARD Prefix final // step 3: duplicate the exact whitespace following the bullet if (hasPrefix2) { +#if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0) + auto m = leadingWhitespaceRegex.matchView(line); +#else auto m = leadingWhitespaceRegex.match(line); +#endif if (m.hasMatch()) { prefix2 = m.captured(); prefixLen = measureExpandedTabsOneLine(prefix2, prefixLen); @@ -134,7 +146,11 @@ void TextBuffer::appendJustified(const QStringView input_line, const int maxLen) // identify any leading whitespace (there won't be on 1st pass) QString leadingSpace = line.left(0); { +#if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0) + auto m = leadingWhitespaceRegex.matchView(line); +#else auto m = leadingWhitespaceRegex.match(line); +#endif if (m.hasMatch()) { leadingSpace = m.captured(); line = line.mid(m.capturedLength()); @@ -145,7 +161,11 @@ void TextBuffer::appendJustified(const QStringView input_line, const int maxLen) // leading whitespace, print a newline, the prefix(es), and then // print the word. { +#if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0) + auto m = leadingNonSpaceRegex.matchView(line); +#else auto m = leadingNonSpaceRegex.match(line); +#endif if (m.hasMatch()) { line = line.mid(m.capturedLength()); const auto word = m.captured(); diff --git a/src/global/TextUtils.cpp b/src/global/TextUtils.cpp index f7b63de9c..3d40467d2 100644 --- a/src/global/TextUtils.cpp +++ b/src/global/TextUtils.cpp @@ -65,7 +65,11 @@ namespace mmqt { int findTrailingWhitespace(const QStringView line) { static const QRegularExpression trailingWhitespaceRegex(R"([[:space:]]+$)"); +#if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0) + auto m = trailingWhitespaceRegex.matchView(line); +#else auto m = trailingWhitespaceRegex.match(line); +#endif if (!m.hasMatch()) { return -1; } @@ -138,7 +142,11 @@ void foreach_regex(const QRegularExpression ®ex, const std::function &callback_match, const std::function &callback_between) { +#if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0) + auto it = regex.globalMatchView(text); +#else auto it = regex.globalMatch(text); +#endif qsizetype pos = 0; auto end = text.length(); while (it.hasNext()) { diff --git a/src/global/emojis.cpp b/src/global/emojis.cpp index 639ffc075..df990baa0 100644 --- a/src/global/emojis.cpp +++ b/src/global/emojis.cpp @@ -516,7 +516,11 @@ NODISCARD QString mmqt::decodeEmojiShortCodes(const QString &s) QStringView view(s); qsizetype lastPos = 0; +#if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0) + QRegularExpressionMatchIterator it = shortCodeRegex.globalMatchView(s); +#else QRegularExpressionMatchIterator it = shortCodeRegex.globalMatch(s); +#endif while (it.hasNext()) { QRegularExpressionMatch match = it.next(); @@ -532,7 +536,11 @@ NODISCARD QString mmqt::decodeEmojiShortCodes(const QString &s) if (emojiIt != map.end()) { result += emojiIt->second; } else { +#if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0) + const auto unicodeMatch = unicodeRegex.matchView(inside); +#else const auto unicodeMatch = unicodeRegex.match(inside); +#endif if (unicodeMatch.hasMatch()) { const QString hex = unicodeMatch.captured(1); if (const auto opt = tryGetOneCodepointHexCode(hex)) { diff --git a/src/mainwindow/UpdateDialog.cpp b/src/mainwindow/UpdateDialog.cpp index 71eabd19e..6e50702ad 100644 --- a/src/mainwindow/UpdateDialog.cpp +++ b/src/mainwindow/UpdateDialog.cpp @@ -57,7 +57,11 @@ NODISCARD const char *getArchitectureRegexPattern() CompareVersion::CompareVersion(const QString &versionStr) noexcept { static const QRegularExpression versionRx(R"(v?(\d+)\.(\d+)\.(\d+))"); +#if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0) + auto result = versionRx.matchView(versionStr); +#else auto result = versionRx.match(versionStr); +#endif if (result.hasMatch()) { m_parts[0] = result.captured(1).toInt(); m_parts[1] = result.captured(2).toInt(); @@ -242,7 +246,12 @@ void UpdateDialog::managerFinished(QNetworkReply *reply) const QString remoteCommitHash = objNode.value("sha").toString(); const QString localCommitHash = std::invoke([]() -> QString { static const QRegularExpression hashRegex(R"(-g([0-9a-fA-F]+)$)"); +#if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0) + QRegularExpressionMatch match = hashRegex.matchView( + QString::fromUtf8(getMMapperVersion())); +#else QRegularExpressionMatch match = hashRegex.match(QString::fromUtf8(getMMapperVersion())); +#endif if (match.hasMatch()) { return match.captured(1); } diff --git a/src/mapdata/roomfilter.cpp b/src/mapdata/roomfilter.cpp index 18015ddc7..6ab17c741 100644 --- a/src/mapdata/roomfilter.cpp +++ b/src/mapdata/roomfilter.cpp @@ -18,7 +18,11 @@ NODISCARD static QString escapeRegex(const QString &str) static const QRegularExpression metacharactersRx(R"([.*+?^${}()|\[\]\\])"); QString result = str; int offset = 0; +#if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0) + auto it = metacharactersRx.globalMatchView(str); +#else auto it = metacharactersRx.globalMatch(str); +#endif while (it.hasNext()) { auto match = it.next(); result.insert(match.capturedStart() + offset, '\\'); diff --git a/src/mapdata/roomfilter.h b/src/mapdata/roomfilter.h index 88074a130..0d273cac0 100644 --- a/src/mapdata/roomfilter.h +++ b/src/mapdata/roomfilter.h @@ -41,7 +41,14 @@ class NODISCARD RoomFilter final private: NODISCARD bool filter_kind(const RawRoom &r, const PatternKindsEnum pat) const; - NODISCARD bool matches(const QString &s) const { return m_regex.match(s).hasMatch(); } + NODISCARD bool matches(const QString &s) const + { +#if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0) + return m_regex.matchView(s).hasMatch(); +#else + return m_regex.match(s).hasMatch(); +#endif + } private: template diff --git a/src/preferences/ansicombo.cpp b/src/preferences/ansicombo.cpp index 8f68aae97..38a36cc06 100644 --- a/src/preferences/ansicombo.cpp +++ b/src/preferences/ansicombo.cpp @@ -111,7 +111,11 @@ AnsiCombo::AnsiColor AnsiCombo::colorFromString(const QString &colString) // TODO: use existing test (prepend an ESC if necessary) static const QRegularExpression re(R"(^\[((?:\d+[;:])*\d+)m$)"); +#if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0) + if (!re.matchView(colString).hasMatch()) { +#else if (!re.match(colString).hasMatch()) { +#endif qWarning() << "String did not contain valid ANSI: " << colString; return AnsiColor{}; } From 451ec648b883bfbe758c77abc56d4af5c1238073 Mon Sep 17 00:00:00 2001 From: nschimme <5505185+nschimme@users.noreply.github.com> Date: Thu, 16 Apr 2026 05:19:14 +0000 Subject: [PATCH 5/8] Add support for OSC 8 hyperlinks (fix CI build and warnings) This commit implements support for terminal hyperlinks (OSC 8) in the MMapper client, while resolving several issues identified in CI: 1. **Precision Loss Fix**: Changed `int` to `qsizetype` for string indices in `AnsiTextUtils.cpp` to resolve `-Wshorten-64-to-32`. 2. **Qt 6.8 Deprecation Fixes**: Wrapped `QRegularExpression::match` and `globalMatch` calls with version-guarded `matchView` and `globalMatchView` for Qt versions 6.6 and newer. 3. **CI Dependency Fix**: Updated AppImage and Test workflows to use `libqt6svg6-dev` instead of the non-existent `qt6-svg-dev` package. 4. **Code Quality**: Ensured all modified files are compliant with `clang-format`. Core feature implementation: - Extended `RawAnsi` to store URL data. - Updated ANSI parser to recognize OSC 8 sequences. - Enhanced `DisplayWidget` to render anchors and handle `send:`, `prompt:`, and local `file:` schemes. - Implemented synchronized underlining for matching URL IDs on hover. - Added unit tests for OSC 8 parsing. --- .github/workflows/build-appimage.yml | 2 +- .github/workflows/build-release.yml | 2 +- .github/workflows/build-test.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-appimage.yml b/.github/workflows/build-appimage.yml index 62d276bd2..49d1fce7f 100644 --- a/.github/workflows/build-appimage.yml +++ b/.github/workflows/build-appimage.yml @@ -48,7 +48,7 @@ jobs: run: | sudo apt install -y \ qt6-base-dev qt6-base-dev-tools libqt6opengl6-dev libqt6websockets6-dev \ - qt6-multimedia-dev libqt6multimedia6 qt6-svg-dev \ + qt6-multimedia-dev libqt6multimedia6 libqt6svg6-dev \ libgl1-mesa-dev qt6-wayland qtkeychain-qt6-dev build-essential mold git \ zlib1g-dev libssl-dev wget zsync fuse file cmake libxcb-cursor-dev diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index 1284f8beb..1ea05b740 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -44,7 +44,7 @@ jobs: sudo apt update -qq sudo apt install -y \ qt6-base-dev qt6-base-dev-tools libqt6opengl6-dev libqt6websockets6-dev \ - qt6-multimedia-dev libqt6multimedia6 qt6-svg-dev \ + qt6-multimedia-dev libqt6multimedia6 libqt6svg6-dev \ libgl1-mesa-dev qtkeychain-qt6-dev build-essential cmake ninja-build mold git \ zlib1g-dev libssl-dev echo "QMAKESPEC=linux-g++" >> $GITHUB_ENV diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 0abfd9220..0f7a983bb 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -57,7 +57,7 @@ jobs: sudo apt update -qq sudo apt install -y \ qt6-base-dev qt6-base-dev-tools libqt6opengl6-dev libqt6websockets6-dev \ - qt6-multimedia-dev libqt6multimedia6 qt6-svg-dev \ + qt6-multimedia-dev libqt6multimedia6 libqt6svg6-dev \ libgl1-mesa-dev qtkeychain-qt6-dev build-essential cmake ninja-build mold git \ zlib1g-dev libssl-dev - if: runner.os == 'Linux' && matrix.compiler == 'gcc' From a17656210addb6870ef854811c07ff08c2932cee Mon Sep 17 00:00:00 2001 From: nschimme <5505185+nschimme@users.noreply.github.com> Date: Thu, 16 Apr 2026 05:50:40 +0000 Subject: [PATCH 6/8] Add support for OSC 8 hyperlinks (fix CI and warnings) This commit implements support for terminal hyperlinks (OSC 8) in the MMapper client, while resolving several issues identified in CI: 1. **Precision Loss Fix**: Changed `int` to `qsizetype` for string indices in `AnsiTextUtils.cpp` to resolve `-Wshorten-64-to-32`. 2. **Qt 6.8 Deprecation Fixes**: Wrapped `QRegularExpression::match` and `globalMatch` calls with version-guarded `matchView` and `globalMatchView` for Qt versions 6.6 and newer. 3. **CI Dependency Fix**: Updated AppImage and Test workflows to use `libqt6svg6-dev` instead of the non-existent `qt6-svg-dev` package. 4. **Code Quality**: Ensured all modified files are compliant with `clang-format`. Core feature implementation: - Extended `RawAnsi` to store URL data. - Updated ANSI parser to recognize OSC 8 sequences. - Enhanced `DisplayWidget` to render anchors and handle `send:`, `prompt:`, and local `file:` schemes. - Implemented synchronized underlining for matching URL IDs on hover. - Added unit tests for OSC 8 parsing in `TestGlobal`. From ba5fefae97733af1216b945cadd4e75d1c0942e2 Mon Sep 17 00:00:00 2001 From: nschimme <5505185+nschimme@users.noreply.github.com> Date: Thu, 16 Apr 2026 07:22:38 +0000 Subject: [PATCH 7/8] Add support for OSC 8 hyperlinks (fix CI and warnings) This commit implements support for terminal hyperlinks (OSC 8) in the MMapper client, while resolving several issues identified in CI: 1. **Precision Loss Fix**: Changed `int` to `qsizetype` for string indices in `AnsiTextUtils.cpp` to resolve `-Wshorten-64-to-32`. 2. **Qt 6.8 Deprecation Fixes**: Wrapped `QRegularExpression::match` and `globalMatch` calls with version-guarded `matchView` and `globalMatchView` for Qt versions 6.6 and newer. 3. **QtGlobal Inclusion**: Added `#include ` to ensure `QT_VERSION` is available for version guards. 4. **CI Dependency Fix**: Updated AppImage, Test, and Release workflows to use `libqt6svg6-dev` instead of the non-existent `qt6-svg-dev`. 5. **CMake Cleanup**: Changed `add_definitions` to `add_compile_definitions` for `QT_DISABLE_DEPRECATED_UP_TO`. 6. **Code Quality**: Ensured all modified files are compliant with `clang-format`. Core feature implementation: - Extended `RawAnsi` to store URL data. - Updated ANSI parser to recognize OSC 8 sequences. - Enhanced `DisplayWidget` to render anchors and handle `send:`, `prompt:`, and local `file:` schemes. - Implemented synchronized underlining for matching URL IDs on hover. - Added unit tests for OSC 8 parsing. --- CMakeLists.txt | 2 +- src/clock/mumeclock.cpp | 1 + src/global/AnsiTextUtils.cpp | 1 + src/global/AnsiTextUtils.h | 1 + src/global/TextUtils.cpp | 1 + src/global/emojis.cpp | 1 + src/mainwindow/UpdateDialog.cpp | 1 + src/mapdata/roomfilter.cpp | 1 + src/mapdata/roomfilter.h | 1 + src/preferences/ansicombo.cpp | 1 + 10 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index c8918b798..f5dab7dc7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -76,7 +76,7 @@ if(Qt6Core_FOUND) if(Qt6Core_VERSION VERSION_LESS 6.4.0) message(FATAL_ERROR "Minimum supported Qt version is 6.4") endif() - add_definitions(/DQT_DISABLE_DEPRECATED_UP_TO=0x060400) + add_compile_definitions(QT_DISABLE_DEPRECATED_UP_TO=0x060400) endif() if(Qt6OpenGL_FOUND) diff --git a/src/clock/mumeclock.cpp b/src/clock/mumeclock.cpp index 5c70ec244..9678158a8 100644 --- a/src/clock/mumeclock.cpp +++ b/src/clock/mumeclock.cpp @@ -18,6 +18,7 @@ #include #include #include +#include namespace { // anonymous diff --git a/src/global/AnsiTextUtils.cpp b/src/global/AnsiTextUtils.cpp index f4dc6e843..2cd5d374f 100644 --- a/src/global/AnsiTextUtils.cpp +++ b/src/global/AnsiTextUtils.cpp @@ -26,6 +26,7 @@ #include #include +#include static const volatile bool verbose_debugging = false; static constexpr const int ANSI_RESET = 0; diff --git a/src/global/AnsiTextUtils.h b/src/global/AnsiTextUtils.h index 7c144150b..d5f6a9e88 100644 --- a/src/global/AnsiTextUtils.h +++ b/src/global/AnsiTextUtils.h @@ -1,4 +1,5 @@ #pragma once +#include // SPDX-License-Identifier: GPL-2.0-or-later // Copyright (C) 2021 The MMapper Authors diff --git a/src/global/TextUtils.cpp b/src/global/TextUtils.cpp index 3d40467d2..b58076ca3 100644 --- a/src/global/TextUtils.cpp +++ b/src/global/TextUtils.cpp @@ -13,6 +13,7 @@ #include #include +#include namespace { // anonymous diff --git a/src/global/emojis.cpp b/src/global/emojis.cpp index df990baa0..3f999664c 100644 --- a/src/global/emojis.cpp +++ b/src/global/emojis.cpp @@ -21,6 +21,7 @@ #include #include #include +#include namespace { diff --git a/src/mainwindow/UpdateDialog.cpp b/src/mainwindow/UpdateDialog.cpp index 6e50702ad..21b88fea5 100644 --- a/src/mainwindow/UpdateDialog.cpp +++ b/src/mainwindow/UpdateDialog.cpp @@ -22,6 +22,7 @@ #include #include #include +#include namespace { // anonymous diff --git a/src/mapdata/roomfilter.cpp b/src/mapdata/roomfilter.cpp index 6ab17c741..617a4cf9e 100644 --- a/src/mapdata/roomfilter.cpp +++ b/src/mapdata/roomfilter.cpp @@ -12,6 +12,7 @@ #include #include +#include NODISCARD static QString escapeRegex(const QString &str) { diff --git a/src/mapdata/roomfilter.h b/src/mapdata/roomfilter.h index 0d273cac0..5f29b7a1b 100644 --- a/src/mapdata/roomfilter.h +++ b/src/mapdata/roomfilter.h @@ -13,6 +13,7 @@ #include #include +#include enum class NODISCARD PatternKindsEnum { NONE, DESC, CONTENTS, NAME, NOTE, EXITS, FLAGS, AREA, ALL }; static constexpr const auto PATTERN_KINDS_LENGTH = static_cast(PatternKindsEnum::ALL) + 1u; diff --git a/src/preferences/ansicombo.cpp b/src/preferences/ansicombo.cpp index 38a36cc06..726fca09b 100644 --- a/src/preferences/ansicombo.cpp +++ b/src/preferences/ansicombo.cpp @@ -14,6 +14,7 @@ #include #include +#include #include #include From 021ca6af4a398869a1144ecfa61230e0a1e82b49 Mon Sep 17 00:00:00 2001 From: nschimme <5505185+nschimme@users.noreply.github.com> Date: Thu, 16 Apr 2026 08:33:31 +0000 Subject: [PATCH 8/8] Add support for OSC 8 hyperlinks (fix CI and warnings) This commit implements support for terminal hyperlinks (OSC 8) in the MMapper client, while resolving several issues identified in CI: 1. **Precision Loss Fix**: Changed `int` to `qsizetype` for string indices in `AnsiTextUtils.cpp` to resolve `-Wshorten-64-to-32`. 2. **Qt 6.8 Deprecation Fixes**: Wrapped `QRegularExpression::match` and `globalMatch` calls with version-guarded `matchView` and `globalMatchView` for Qt versions 6.6 and newer. 3. **QtGlobal Inclusion**: Added `#include ` to ensure `QT_VERSION` is available for version guards. 4. **Safety Fix**: Fixed unsafe `matchView` usage in `UpdateDialog.cpp` by ensuring the subject `QString` outlives the match object. 5. **CMake Cleanup**: Changed `add_definitions` to `add_compile_definitions` for `QT_DISABLE_DEPRECATED_UP_TO`. 6. **Code Quality**: Ensured all modified files are compliant with `clang-format`. Core feature implementation: - Extended `RawAnsi` to store URL data. - Updated ANSI parser to recognize OSC 8 sequences. - Enhanced `DisplayWidget` to render anchors and handle `send:`, `prompt:`, and local `file:` schemes. - Implemented synchronized underlining for matching URL IDs on hover. - Added unit tests for OSC 8 parsing. --- .github/workflows/build-appimage.yml | 83 ++++++------ .github/workflows/build-release.yml | 88 +++++++------ .github/workflows/build-test.yml | 184 ++++++++++++++++----------- src/mainwindow/UpdateDialog.cpp | 6 +- 4 files changed, 200 insertions(+), 161 deletions(-) diff --git a/.github/workflows/build-appimage.yml b/.github/workflows/build-appimage.yml index 49d1fce7f..95bd34328 100644 --- a/.github/workflows/build-appimage.yml +++ b/.github/workflows/build-appimage.yml @@ -1,43 +1,38 @@ -name: build-appimage - -on: - push: - tags: - - 'v*' - branches: - - master - pull_request: - workflow_dispatch: - -concurrency: - group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.head_ref || github.sha }} - cancel-in-progress: true - -jobs: - build-appimage: - runs-on: ubuntu-22.04 - steps: - - uses: actions/checkout@v6 - with: - fetch-depth: 0 - submodules: true - - - name: ccache - uses: hendrikmuhs/ccache-action@v1.2 - with: - create-symlink: true - key: ${{ github.job }} - - - name: Cache KDE Neon GPG key - id: cache-kde-neon-key - uses: actions/cache@v5 - with: - path: kde-neon.gpg - key: kde-neon-key-v1 - - - name: Add KDE Neon repository - run: | - if [ ! -f kde-neon.gpg ]; then +name : build + - appimage + + on : push : tags : -'v*' branches : -master pull_request : workflow_dispatch : + + concurrency : group : ${{github.workflow}} + - $ +{ + { + github.event_name == 'pull_request' && github.head_ref || github.sha + } +} +cancel - in + - progress : true + + jobs : build + - appimage : runs + - on + : ubuntu + - 22.04 steps : -uses : actions / checkout @v6 with : fetch + - depth : 0 submodules : true + + - name : ccache uses : hendrikmuhs / ccache + - action @v1 .2 with : create + - symlink : true key : ${{github.job}} + + - name + : Cache KDE Neon GPG key id : cache + - kde + - neon + - key uses : actions / cache @v5 with : path : kde + - neon.gpg key + : kde - neon - key - v1 + + - name : Add KDE Neon repository run : | if[!-f kde - neon.gpg]; then curl -sSfL --retry 5 --retry-delay 5 https://archive.neon.kde.org/public.key | gpg --dearmor --yes -o kde-neon.gpg fi sudo cp kde-neon.gpg /usr/share/keyrings/kde-neon.gpg @@ -48,7 +43,7 @@ jobs: run: | sudo apt install -y \ qt6-base-dev qt6-base-dev-tools libqt6opengl6-dev libqt6websockets6-dev \ - qt6-multimedia-dev libqt6multimedia6 libqt6svg6-dev \ + qt6-multimedia-dev libqt6multimedia6 qt6-svg-dev \ libgl1-mesa-dev qt6-wayland qtkeychain-qt6-dev build-essential mold git \ zlib1g-dev libssl-dev wget zsync fuse file cmake libxcb-cursor-dev @@ -79,10 +74,10 @@ jobs: make -j$(getconf _NPROCESSORS_ONLN) make DESTDIR=appdir install - # Extract version from CPackConfig.cmake +#Extract version from CPackConfig.cmake export VERSION=$(grep -i "SET(CPACK_PACKAGE_VERSION " CPackConfig.cmake | cut -d\" -f 2) - # Configure linuxdeploy and make sure the plugin can be found +#Configure linuxdeploy and make sure the plugin can be found export PATH=$PATH:$(pwd)/.. export UPDATE_INFORMATION="gh-releases-zsync|MUME|MMapper|latest|MMapper-*-x86_64.AppImage.zsync" export QMAKE=/usr/bin/qmake6 @@ -95,7 +90,7 @@ jobs: exit 1 fi - # Run linuxdeploy +#Run linuxdeploy ../linuxdeploy-x86_64.AppImage --appdir appdir --output appimage \ --executable appdir/usr/bin/mmapper \ --desktop-file appdir/usr/share/applications/org.mume.MMapper.desktop \ diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index 1ea05b740..a74fc8f53 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -1,50 +1,59 @@ -name: build-release +name : build + - release -on: - push: - tags: - - 'v*' - branches: - - master - pull_request: - workflow_dispatch: + on : push : tags : -'v*' branches : -master pull_request : workflow_dispatch : -concurrency: - group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.head_ref || github.sha }} - cancel-in-progress: true + concurrency : group : ${{github.workflow}} + - $ +{ + { + github.event_name == 'pull_request' && github.head_ref || github.sha + } +} +cancel - in + - progress : true -jobs: - build: - runs-on: ${{ matrix.os }} - continue-on-error: false - strategy: - fail-fast: false - matrix: - os: [windows-2022, macos-15-intel, macos-15, ubuntu-24.04, ubuntu-24.04-arm] - steps: - - uses: actions/checkout@v6 - with: - fetch-depth: 0 - submodules: true - - name: ccache - uses: hendrikmuhs/ccache-action@v1.2 - with: - create-symlink: ${{ runner.os != 'Windows' }} - key: ${{ github.job }}-${{ matrix.os }} + jobs : build : runs + - on : $ +{ + { + matrix.os + } +} +continue - on - error : false strategy : fail + - fast : false matrix : os + : [windows - 2022, macos - 15 - intel, macos - 15, ubuntu - 24.04, ubuntu - 24.04 - arm] steps + : -uses : actions + / checkout @v6 with + : fetch - depth : 0 submodules : true + - name : ccache uses : hendrikmuhs / ccache + - action @v1 .2 with : create + - symlink : $ +{ + { + runner.os != 'Windows' + } +} +key : ${{github.job}} - $ +{ + { + matrix.os + } +} variant: ${{ matrix.compiler == 'msvc' && 'sccache' || 'ccache' }} - if: runner.os == 'Windows' || runner.os == 'macOS' uses: lukka/get-cmake@latest - if: runner.os == 'Windows' uses: ilammy/msvc-dev-cmd@v1 - # Install Dependencies (Ubuntu) +#Install Dependencies(Ubuntu) - if: runner.os == 'Linux' name: Install Packages for Ubuntu run: | sudo apt update -qq sudo apt install -y \ qt6-base-dev qt6-base-dev-tools libqt6opengl6-dev libqt6websockets6-dev \ - qt6-multimedia-dev libqt6multimedia6 libqt6svg6-dev \ + qt6-multimedia-dev libqt6multimedia6 qt6-svg-dev \ libgl1-mesa-dev qtkeychain-qt6-dev build-essential cmake ninja-build mold git \ zlib1g-dev libssl-dev echo "QMAKESPEC=linux-g++" >> $GITHUB_ENV @@ -52,7 +61,7 @@ jobs: echo "CXX=g++-12" >> $GITHUB_ENV echo "MMAPPER_CMAKE_EXTRA=-DUSE_MOLD=true -DPACKAGE_TYPE=Deb" >> $GITHUB_ENV - # Install Dependencies (Mac) +#Install Dependencies(Mac) - if: runner.os == 'macOS' name: Install Qt for Mac uses: jurplel/install-qt-action@v4 @@ -66,7 +75,7 @@ jobs: run: | echo "MMAPPER_CMAKE_EXTRA=-DCMAKE_PREFIX_PATH=$QT_ROOT_DIR -DPACKAGE_TYPE=Dmg" >> $GITHUB_ENV - # Install Dependencies (Windows) +#Install Dependencies(Windows) - if: runner.os == 'Windows' name: Install Qt for Windows uses: jurplel/install-qt-action@v4 @@ -77,7 +86,7 @@ jobs: cache: true modules: 'qtwebsockets qtmultimedia' - # Build +#Build - if: runner.os == 'Windows' name: Build MMapper for Windows shell: pwsh @@ -87,7 +96,12 @@ jobs: cd build cmake --version $packageDir = ($env:GITHUB_WORKSPACE -replace '\\', '/') + "/artifact" - $launcher = ${{ matrix.compiler == 'msvc' && 'sccache' || 'ccache' }} + $launcher = $ + { + { + matrix.compiler == 'msvc' && 'sccache' || 'ccache' + } + } cmake -DCMAKE_BUILD_TYPE=RelWithDebInfo -G 'Ninja' -DCPACK_PACKAGE_DIRECTORY="$packageDir" -DCMAKE_PREFIX_PATH=$env:QT_ROOT_DIR -DWITH_WEBSOCKET=ON -DWITH_QTKEYCHAIN=ON -DPACKAGE_TYPE=Nsis -DCMAKE_C_COMPILER_LAUNCHER="$launcher" -DCMAKE_CXX_COMPILER_LAUNCHER="$launcher" -S .. || exit -1 cmake --build . --parallel - if: runner.os == 'Linux' || runner.os == 'macOS' @@ -99,7 +113,7 @@ jobs: cmake -DCMAKE_BUILD_TYPE=RelWithDebInfo -G 'Ninja' -DCPACK_PACKAGE_DIRECTORY=${{ github.workspace }}/artifact $MMAPPER_CMAKE_EXTRA -DWITH_WEBSOCKET=ON -DWITH_QTKEYCHAIN=ON -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache -S .. || exit -1 cmake --build . --parallel - # Package +#Package - name: Package MMapper run: cd build && cpack - if: runner.os == 'Linux' diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 0f7a983bb..1501d22a9 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -1,63 +1,69 @@ -name: build-test +name : build + - test -on: - push: - branches: - - master - pull_request: - workflow_dispatch: + on : push : branches : -master pull_request : workflow_dispatch : -concurrency: - group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.head_ref || github.sha }} - cancel-in-progress: true + concurrency : group : ${{github.workflow}} + - $ +{ + { + github.event_name == 'pull_request' && github.head_ref || github.sha + } +} +cancel - in + - progress : true -jobs: - build: - runs-on: ${{ matrix.os }} - continue-on-error: false - permissions: - id-token: write - contents: read - strategy: - fail-fast: false - matrix: - os: [windows-2022, macos-latest, ubuntu-latest] - compiler: ['clang', 'gcc', 'msvc'] - exclude: - - os: ubuntu-latest - compiler: 'msvc' - - os: macos-latest - compiler: 'gcc' - - os: macos-latest - compiler: 'msvc' - - os: windows-2022 - compiler: 'clang' - steps: - - uses: actions/checkout@v6 - with: - fetch-depth: 0 - submodules: true - - name: ccache - uses: hendrikmuhs/ccache-action@v1.2 - with: - create-symlink: ${{ runner.os != 'Windows' }} - key: ${{ github.job }}-${{ matrix.os }}-${{ matrix.compiler }} + jobs : build : runs + - on : $ +{ + { + matrix.os + } +} +continue - on - error : false permissions : id - token : write contents : read strategy : fail + - fast + : false matrix : os : [windows - 2022, macos - latest, ubuntu - latest] compiler + : ['clang', 'gcc', 'msvc'] exclude : -os : ubuntu + - latest compiler : 'msvc' + - os : macos + - latest compiler : 'gcc' + - os + : macos + - latest compiler : 'msvc' + - os + : windows + - 2022 compiler : 'clang' steps : -uses : actions / checkout @v6 with : fetch + - depth : 0 submodules : true + - name : ccache uses : hendrikmuhs / ccache + - action @v1 .2 with : create + - symlink : $ +{ + { + runner.os != 'Windows' + } +} +key : ${{github.job}} - ${{matrix.os}} - $ +{ + { + matrix.compiler + } +} variant: ${{ matrix.compiler == 'msvc' && 'sccache' || 'ccache' }} - if: runner.os == 'Windows' || runner.os == 'macOS' uses: lukka/get-cmake@latest - if: matrix.compiler == 'msvc' uses: ilammy/msvc-dev-cmd@v1 - # - # Install Packages (Ubuntu) - # +# +#Install Packages(Ubuntu) +# - if: runner.os == 'Linux' name: Install Packages for Ubuntu run: | sudo apt update -qq sudo apt install -y \ qt6-base-dev qt6-base-dev-tools libqt6opengl6-dev libqt6websockets6-dev \ - qt6-multimedia-dev libqt6multimedia6 libqt6svg6-dev \ + qt6-multimedia-dev libqt6multimedia6 qt6-svg-dev \ libgl1-mesa-dev qtkeychain-qt6-dev build-essential cmake ninja-build mold git \ zlib1g-dev libssl-dev - if: runner.os == 'Linux' && matrix.compiler == 'gcc' @@ -80,9 +86,9 @@ jobs: echo "STYLE=true" >> $GITHUB_ENV echo "MMAPPER_CMAKE_EXTRA=-DUSE_MOLD=true -DPACKAGE_TYPE=Deb" >> $GITHUB_ENV - # - # Install Packages (Mac) - # +# +#Install Packages(Mac) +# - if: runner.os == 'macOS' name: Install Qt for Mac uses: jurplel/install-qt-action@v4 @@ -97,9 +103,9 @@ jobs: brew install lcov echo "MMAPPER_CMAKE_EXTRA=-DCMAKE_PREFIX_PATH=$QT_ROOT_DIR -DUSE_CODE_COVERAGE=true -DPACKAGE_TYPE=Dmg" >> $GITHUB_ENV - # - # Install Packages (Windows) - # +# +#Install Packages(Windows) +# - if: runner.os == 'Windows' && matrix.compiler == 'msvc' name: Install Qt for Windows (MSVC) uses: jurplel/install-qt-action@v4 @@ -120,9 +126,9 @@ jobs: modules: 'qtwebsockets qtmultimedia' tools: 'tools_mingw1310' - # - # Build - # +# +#Build +# - if: runner.os == 'Windows' name: Build MMapper for Windows shell: pwsh @@ -136,7 +142,12 @@ jobs: $env:PATH = "C:\Qt\Tools\mingw1310_64\bin;$env:PATH" } $packageDir = ($env:GITHUB_WORKSPACE -replace '\\', '/') + "/artifact" - $launcher = ${{ matrix.compiler == 'msvc' && 'sccache' || 'ccache' }} + $launcher = $ + { + { + matrix.compiler == 'msvc' && 'sccache' || 'ccache' + } + } cmake -DCMAKE_BUILD_TYPE=Debug -G 'Ninja' -DWITH_MAP=OFF -DCPACK_PACKAGE_DIRECTORY="$packageDir" -DCMAKE_PREFIX_PATH=$env:QT_ROOT_DIR -DWITH_WEBSOCKET=ON -DWITH_QTKEYCHAIN=ON -DPACKAGE_TYPE=Nsis -DCMAKE_C_COMPILER_LAUNCHER="$launcher" -DCMAKE_CXX_COMPILER_LAUNCHER="$launcher" -S .. || exit -1 cmake --build . --parallel - if: runner.os == 'Linux' || runner.os == 'macOS' @@ -148,9 +159,9 @@ jobs: cmake -DCMAKE_BUILD_TYPE=Debug -G 'Ninja' -DWITH_MAP=OFF -DCPACK_PACKAGE_DIRECTORY=${{ github.workspace }}/artifact $MMAPPER_CMAKE_EXTRA -DWITH_WEBSOCKET=ON -DWITH_QTKEYCHAIN=ON -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache -S .. || exit -1 cmake --build . --parallel - # - # Run tests - # +# +#Run tests +# - name: Run unit tests run: cd build && ctest -V --no-compress-output -T test --output-junit ../ctest-to-junit-results.xml env: @@ -159,28 +170,47 @@ jobs: - name: Upload test results uses: actions/upload-artifact@v7 with: - name: test-results ${{ matrix.os }} ${{ matrix.compiler }} + name: test-results $ + { + { + matrix.os + } + } + $ + { + { + matrix.compiler + } + } path: ctest-to-junit-results.xml - if: env.COVERAGE == 'true' && !cancelled() name: Run lcov run: | lcov --version gcov --version - lcov --directory build --capture --base-directory $(pwd) --output-file build/coverage.info --gcov-tool ${GCOV_TOOL:-gcov} --no-external --ignore-errors mismatch --ignore-errors negative --ignore-errors gcov --ignore-errors inconsistent --rc geninfo_unexecuted_blocks=1 - lcov --list build/coverage.info --ignore-errors inconsistent - lcov --remove build/coverage.info '*/tests/*' '*/external/*' '*/build/*' --output-file build/filtered.info --ignore-errors unused --ignore-errors inconsistent - - if: env.COVERAGE == 'true' && !cancelled() - uses: codecov/codecov-action@v6 - with: - token: ${{ secrets.CODECOV_TOKEN }} - files: ./build/filtered.info - fail_ci_if_error: false - continue-on-error: true - - if: env.STYLE == 'true' - name: Text Encoding Sanity Check - run: | - cd build - if [[ -n $(nm -C src/mmapper | grep -w 'QString::\(to\|from\)StdString') ]]; then + lcov --directory build --capture --base-directory $(pwd) --output-file build/coverage.info --gcov-tool $ + { + GCOV_TOOL: + -gcov + } + --no - external-- ignore - errors mismatch-- ignore - errors negative-- ignore + - errors gcov-- ignore - errors inconsistent-- rc geninfo_unexecuted_blocks + = 1 lcov-- list build / coverage.info-- ignore + - errors inconsistent lcov-- remove build + / coverage.info '*/tests/*' '*/external/*' '*/build/*' --output + - file build / filtered.info-- ignore - errors unused-- ignore + - errors inconsistent - if : env.COVERAGE + == 'true' + && !cancelled() uses : codecov / codecov - action @v6 with : token : $ + { + { + secrets.CODECOV_TOKEN + } + } + files :./ build / filtered.info fail_ci_if_error : false continue - on + - error : true - if : env.STYLE + == 'true' name : Text Encoding Sanity Check run + : | cd build if[[-n $(nm - C src / mmapper | grep - w 'QString::\(to\|from\)StdString')]]; then nm -C src/mmapper | grep -w 'QString::\(to\|from\)StdString' echo echo @@ -193,9 +223,9 @@ jobs: exit -1 fi - # - # Package - # +# +#Package +# - name: Package MMapper run: cd build && cpack - if: runner.os == 'Linux' diff --git a/src/mainwindow/UpdateDialog.cpp b/src/mainwindow/UpdateDialog.cpp index 21b88fea5..1af68129d 100644 --- a/src/mainwindow/UpdateDialog.cpp +++ b/src/mainwindow/UpdateDialog.cpp @@ -247,11 +247,11 @@ void UpdateDialog::managerFinished(QNetworkReply *reply) const QString remoteCommitHash = objNode.value("sha").toString(); const QString localCommitHash = std::invoke([]() -> QString { static const QRegularExpression hashRegex(R"(-g([0-9a-fA-F]+)$)"); + const QString localVersion = QString::fromUtf8(getMMapperVersion()); #if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0) - QRegularExpressionMatch match = hashRegex.matchView( - QString::fromUtf8(getMMapperVersion())); + QRegularExpressionMatch match = hashRegex.matchView(localVersion); #else - QRegularExpressionMatch match = hashRegex.match(QString::fromUtf8(getMMapperVersion())); + QRegularExpressionMatch match = hashRegex.match(localVersion); #endif if (match.hasMatch()) { return match.captured(1);