diff --git a/.github/workflows/build-appimage.yml b/.github/workflows/build-appimage.yml index 62d276bd2..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 @@ -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 1284f8beb..a74fc8f53 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -1,43 +1,52 @@ -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: | @@ -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 0abfd9220..1501d22a9 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -1,56 +1,62 @@ -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: | @@ -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/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/client/ClientWidget.cpp b/src/client/ClientWidget.cpp index ec66a506f..95fca755c 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); @@ -277,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 8b538e76b..22e7ef043 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() + QStringLiteral("\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,53 @@ 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(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); + selections.append(sel); + } + } + } + } + setExtraSelections(selections); +} + void setDefaultFormat(QTextCharFormat &format, const FontDefaults &defaults) { format.setFont(defaults.serverOutputFont); @@ -235,7 +305,9 @@ 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 +410,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 +617,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/clock/mumeclock.cpp b/src/clock/mumeclock.cpp index 964e89c21..9678158a8 100644 --- a/src/clock/mumeclock.cpp +++ b/src/clock/mumeclock.cpp @@ -18,6 +18,7 @@ #include #include #include +#include namespace { // anonymous @@ -158,7 +159,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 +192,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 +344,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 21f923fdc..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; @@ -73,7 +74,8 @@ 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 +1703,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" + 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(); + 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 +1872,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 +2047,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 +2627,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..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 @@ -327,17 +328,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 +354,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 +382,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 +408,29 @@ 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 \ - { \ - return m_flags.contains(AnsiStyleFlagEnum::_Snake); \ - } \ - constexpr void set##_Snake() { m_flags.insert(AnsiStyleFlagEnum::_Snake); } \ - constexpr void clear##_Snake() { m_flags.remove(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) #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 +442,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 +462,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 +480,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 +496,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 +511,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}; @@ -995,7 +989,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..b58076ca3 100644 --- a/src/global/TextUtils.cpp +++ b/src/global/TextUtils.cpp @@ -13,6 +13,7 @@ #include #include +#include namespace { // anonymous @@ -65,7 +66,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 +143,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..3f999664c 100644 --- a/src/global/emojis.cpp +++ b/src/global/emojis.cpp @@ -21,6 +21,7 @@ #include #include #include +#include namespace { @@ -516,7 +517,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 +537,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..1af68129d 100644 --- a/src/mainwindow/UpdateDialog.cpp +++ b/src/mainwindow/UpdateDialog.cpp @@ -22,6 +22,7 @@ #include #include #include +#include namespace { // anonymous @@ -57,7 +58,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 +247,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]+)$)"); - QRegularExpressionMatch match = hashRegex.match(QString::fromUtf8(getMMapperVersion())); + const QString localVersion = QString::fromUtf8(getMMapperVersion()); +#if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0) + QRegularExpressionMatch match = hashRegex.matchView(localVersion); +#else + QRegularExpressionMatch match = hashRegex.match(localVersion); +#endif if (match.hasMatch()) { return match.captured(1); } 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/mapdata/roomfilter.cpp b/src/mapdata/roomfilter.cpp index 18015ddc7..617a4cf9e 100644 --- a/src/mapdata/roomfilter.cpp +++ b/src/mapdata/roomfilter.cpp @@ -12,13 +12,18 @@ #include #include +#include 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..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; @@ -41,7 +42,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..726fca09b 100644 --- a/src/preferences/ansicombo.cpp +++ b/src/preferences/ansicombo.cpp @@ -14,6 +14,7 @@ #include #include +#include #include #include @@ -111,7 +112,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{}; } diff --git a/src/proxy/connectionlistener.cpp b/src/proxy/connectionlistener.cpp index 11bcf9ec6..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 { - constexpr 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"); 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) {