From 7d4742970da0691435eecfe71760ef5ac3fc77a4 Mon Sep 17 00:00:00 2001 From: George Sokianos Date: Sat, 30 Nov 2024 14:44:52 +0000 Subject: [PATCH] Merged the changes from 2.1.6 upstream release tag --- .github/FUNDING.yml | 3 - .github/labeler.yml | 56 ++- .github/workflows/auto_labeler_pr.yml | 4 +- .github/workflows/build.yml | 209 ++++++---- .github/workflows/release.yml | 231 +++++++---- build.lhelper | 8 - changelog.md | 149 +++++++ data/core/doc/init.lua | 3 +- data/core/emptyview.lua | 8 + data/core/init.lua | 41 +- data/core/process.lua | 50 +++ data/core/rootview.lua | 28 +- data/core/start.lua | 5 +- data/core/tokenizer.lua | 11 +- data/plugins/autoreload.lua | 33 +- data/plugins/language_c.lua | 15 +- data/plugins/language_cpp.lua | 29 +- data/plugins/language_python.lua | 234 ++++++++--- data/plugins/projectsearch.lua | 33 +- data/plugins/toolbarview.lua | 5 +- data/plugins/treeview.lua | 3 - meson.build | 46 ++- scripts/README.md | 15 +- scripts/appimage.sh | 30 +- scripts/generate-release-notes.sh | 69 ++++ scripts/install-dependencies.sh | 29 +- scripts/lhelper.sh | 73 ---- scripts/make-universal-binaries.sh | 5 + scripts/package.sh | 3 +- src/api/dirmonitor.c | 17 +- src/api/process.c | 380 +++--------------- src/api/renderer.c | 22 +- src/api/system.c | 7 + src/main.c | 4 - src/meson.build | 30 +- src/renderer.c | 551 +++++++++++++++++--------- src/renderer.h | 3 + src/renwindow.c | 13 +- src/utfconv.h | 26 ++ subprojects/pcre2.wrap | 18 +- subprojects/sdl2.wrap | 18 +- 41 files changed, 1542 insertions(+), 975 deletions(-) delete mode 100644 .github/FUNDING.yml delete mode 100644 build.lhelper create mode 100644 data/core/process.lua create mode 100755 scripts/generate-release-notes.sh delete mode 100644 scripts/lhelper.sh diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index 62a2af62..00000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1,3 +0,0 @@ -# These are supported funding model platforms - -github: franko diff --git a/.github/labeler.yml b/.github/labeler.yml index 7c0ceeb7..60eee877 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -1,38 +1,60 @@ "Category: CI": - - .github/workflows/* +- changed-files: + - any-glob-to-any-file: + - .github/workflows/* "Category: Meta": - - ./* - - .github/* - - .github/ISSUE_TEMPLATE/* - - .github/PULL_REQUEST_TEMPLATE/* - - .gitignore +- changed-files: + - any-glob-to-any-file: + - ./* + - .github/* + - .github/ISSUE_TEMPLATE/* + - .github/PULL_REQUEST_TEMPLATE/* + - .gitignore "Category: Build System": - - meson.build - - meson_options.txt - - subprojects/* +- changed-files: + - any-glob-to-any-file: + - meson.build + - meson_options.txt + - subprojects/* "Category: Documentation": - - docs/**/* +- changed-files: + - any-glob-to-any-file: + - docs/**/* "Category: Resources": - - resources/**/* +- changed-files: + - any-glob-to-any-file: + - resources/**/* "Category: Themes": - - data/colors/* +- changed-files: + - any-glob-to-any-file: + - data/colors/* "Category: Lua Core": - - data/core/**/* +- changed-files: + - any-glob-to-any-file: + - data/core/**/* "Category: Fonts": - - data/fonts/* +- changed-files: + - any-glob-to-any-file: + - data/fonts/* "Category: Plugins": - - data/plugins/* +- changed-files: + - any-glob-to-any-file: + - data/plugins/* "Category: C Core": - - src/**/* +- changed-files: + - any-glob-to-any-file: + - src/**/* "Category: Libraries": - - lib/**/* +- changed-files: + - any-glob-to-any-file: + - lib/**/* diff --git a/.github/workflows/auto_labeler_pr.yml b/.github/workflows/auto_labeler_pr.yml index 48ea1eeb..99449911 100644 --- a/.github/workflows/auto_labeler_pr.yml +++ b/.github/workflows/auto_labeler_pr.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Apply Type Label - uses: actions/labeler@v3 + uses: actions/labeler@v5 with: repo-token: "${{ secrets.GITHUB_TOKEN }}" - sync-labels: "" # works around actions/labeler#104 + sync-labels: false diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 21f3a8b4..d6e7d1a6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -3,11 +3,11 @@ name: CI on: push: branches: - - '*' + - "*" pull_request: branches: - - '*' + - "*" workflow_dispatch: @@ -18,11 +18,12 @@ jobs: strategy: matrix: config: - - { name: "GCC", cc: gcc, cxx: g++ } - - { name: "clang", cc: clang, cxx: clang++ } + - { name: "GCC", cc: gcc, cxx: g++ } + - { name: "clang", cc: clang, cxx: clang++ } env: CC: ${{ matrix.config.cc }} CXX: ${{ matrix.config.cxx }} + steps: - name: Set Environment Variables if: ${{ matrix.config.cc == 'gcc' }} @@ -30,38 +31,51 @@ jobs: echo "$HOME/.local/bin" >> "$GITHUB_PATH" echo "INSTALL_REF=${GITHUB_REF##*/}" >> "$GITHUB_ENV" echo "INSTALL_NAME=lite-xl-${GITHUB_REF##*/}-linux-$(uname -m)-portable" >> "$GITHUB_ENV" - - uses: actions/checkout@v3 + + - name: Checkout code + uses: actions/checkout@v4 + - name: Python Setup - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: - python-version: 3.9 + python-version: "3.11" + - name: Update Packages run: sudo apt-get update + - name: Install Dependencies run: bash scripts/install-dependencies.sh --debug + - name: Build run: | bash --version bash scripts/build.sh --debug --forcefallback --portable + - name: Package if: ${{ matrix.config.cc == 'gcc' }} run: bash scripts/package.sh --version ${INSTALL_REF} --debug --binary + - name: Upload Artifacts - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: ${{ matrix.config.cc == 'gcc' }} with: name: Linux Artifacts path: ${{ env.INSTALL_NAME }}.tar.gz + compression-level: 0 build_macos: name: macOS - runs-on: macos-11 + strategy: + matrix: + config: + - { arch: x86_64, runner: macos-13 } # macos-13 uses x86_64 + - { arch: arm64, runner: macos-14 } # macos-14 / latest uses M1 + + runs-on: ${{ matrix.config.runner }} env: CC: clang CXX: clang++ - strategy: - matrix: - arch: ['x86_64', 'arm64'] + steps: - name: System Information run: | @@ -69,36 +83,47 @@ jobs: bash --version gcc -v xcodebuild -version + - name: Set Environment Variables run: | echo "$HOME/.local/bin" >> "$GITHUB_PATH" echo "INSTALL_REF=${GITHUB_REF##*/}" >> "$GITHUB_ENV" - echo "INSTALL_NAME=lite-xl-${GITHUB_REF##*/}-macos-${{ matrix.arch }}" >> "$GITHUB_ENV" - if [[ $(uname -m) != ${{ matrix.arch }} ]]; then echo "ARCH=--cross-arch ${{ matrix.arch }}" >> "$GITHUB_ENV"; fi - - uses: actions/checkout@v3 + echo "INSTALL_NAME=lite-xl-${GITHUB_REF##*/}-macos-${{ matrix.config.arch }}" >> "$GITHUB_ENV" + + - name: Checkout code + uses: actions/checkout@v4 + - name: Python Setup - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: - python-version: 3.9 + python-version: "3.11" + + # installing md5sha1sum will eliminate a warning with arm64 and libusb - name: Install Dependencies - # --lhelper will eliminate a warning with arm64 and libusb - run: bash scripts/install-dependencies.sh --debug --lhelper + run: | + brew install bash md5sha1sum + pip install meson ninja dmgbuild + - name: Build run: | bash --version - bash scripts/build.sh --bundle --debug --forcefallback $ARCH + bash scripts/build.sh --bundle --debug --forcefallback + - name: Create DMG Image - run: bash scripts/package.sh --version ${INSTALL_REF} $ARCH --debug --dmg + run: bash scripts/package.sh --version ${INSTALL_REF} --debug --dmg + - name: Upload DMG Image - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: - name: macOS DMG Images + name: macOS DMG Images (${{ matrix.config.arch }}) path: ${{ env.INSTALL_NAME }}.dmg + compression-level: 0 build_macos_universal: name: macOS (Universal) - runs-on: macos-11 + runs-on: macos-14 needs: build_macos + steps: - name: System Information run: | @@ -106,32 +131,42 @@ jobs: bash --version gcc -v xcodebuild -version + - name: Set Environment Variables run: | echo "$HOME/.local/bin" >> "$GITHUB_PATH" echo "INSTALL_NAME=lite-xl-${GITHUB_REF##*/}-macos-universal" >> "$GITHUB_ENV" + - name: Setup Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: - python-version: '3.9' + python-version: "3.11" + - name: Install dmgbuild run: pip install dmgbuild - - uses: actions/checkout@v3 + + - name: Checkout code + uses: actions/checkout@v4 + - name: Download artifacts - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 id: download with: - name: macOS DMG Images + pattern: macOS DMG Images * + merge-multiple: true path: dmgs-original + - name: Make universal bundles run: | bash --version bash scripts/make-universal-binaries.sh ${{ steps.download.outputs.download-path }} "${INSTALL_NAME}" + - name: Upload DMG Image - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: - name: macOS Universal DMG Images + name: macOS DMG Images (Universal) path: ${{ env.INSTALL_NAME }}.dmg + compression-level: 0 build_windows_msys2: name: Windows @@ -139,48 +174,58 @@ jobs: strategy: matrix: config: - - {msystem: MINGW32, arch: i686} - - {msystem: MINGW64, arch: x86_64} + - { msystem: MINGW32, arch: i686 } + - { msystem: MINGW64, arch: x86_64 } defaults: run: shell: msys2 {0} + steps: - - uses: actions/checkout@v3 - - uses: msys2/setup-msys2@v2 - with: - msystem: ${{ matrix.config.msystem }} - install: >- - base-devel - git - zip - mingw-w64-${{ matrix.config.arch }}-gcc - mingw-w64-${{ matrix.config.arch }}-meson - mingw-w64-${{ matrix.config.arch }}-ninja - mingw-w64-${{ matrix.config.arch }}-ca-certificates - mingw-w64-${{ matrix.config.arch }}-ntldd - - name: Set Environment Variables - run: | - echo "$HOME/.local/bin" >> "$GITHUB_PATH" - echo "INSTALL_REF=${GITHUB_REF##*/}" >> "$GITHUB_ENV" - if [[ "${MSYSTEM}" == "MINGW64" ]]; then - echo "INSTALL_NAME=lite-xl-${GITHUB_REF##*/}-windows-x86_64" >> "$GITHUB_ENV" - else - echo "INSTALL_NAME=lite-xl-${GITHUB_REF##*/}-windows-i686" >> "$GITHUB_ENV" - fi - - name: Install Dependencies - if: false - run: bash scripts/install-dependencies.sh --debug - - name: Build - run: | - bash --version - bash scripts/build.sh -U --debug --forcefallback - - name: Package - run: bash scripts/package.sh --version ${INSTALL_REF} --debug --binary - - name: Upload Artifacts - uses: actions/upload-artifact@v3 - with: - name: Windows Artifacts - path: ${{ env.INSTALL_NAME }}.zip + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup MSYS2 + uses: msys2/setup-msys2@v2 + with: + msystem: ${{ matrix.config.msystem }} + install: >- + base-devel + git + zip + mingw-w64-${{ matrix.config.arch }}-gcc + mingw-w64-${{ matrix.config.arch }}-meson + mingw-w64-${{ matrix.config.arch }}-ninja + mingw-w64-${{ matrix.config.arch }}-ca-certificates + mingw-w64-${{ matrix.config.arch }}-ntldd + + - name: Set Environment Variables + run: | + echo "$HOME/.local/bin" >> "$GITHUB_PATH" + echo "INSTALL_REF=${GITHUB_REF##*/}" >> "$GITHUB_ENV" + if [[ "${MSYSTEM}" == "MINGW64" ]]; then + echo "INSTALL_NAME=lite-xl-${GITHUB_REF##*/}-windows-x86_64" >> "$GITHUB_ENV" + else + echo "INSTALL_NAME=lite-xl-${GITHUB_REF##*/}-windows-i686" >> "$GITHUB_ENV" + fi + + - name: Install Dependencies + if: false + run: bash scripts/install-dependencies.sh --debug + + - name: Build + run: | + bash --version + bash scripts/build.sh -U --debug --forcefallback + + - name: Package + run: bash scripts/package.sh --version ${INSTALL_REF} --debug --binary + + - name: Upload Artifacts + uses: actions/upload-artifact@v4 + with: + name: Windows Artifacts (${{ matrix.config.msystem }}) + path: ${{ env.INSTALL_NAME }}.zip + compression-level: 0 build_windows_msvc: name: Windows (MSVC) @@ -190,38 +235,52 @@ jobs: arch: - { target: x86, name: i686 } - { target: x64, name: x86_64 } + steps: - - uses: actions/checkout@v3 - - uses: ilammy/msvc-dev-cmd@v1 + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup MSVC + uses: ilammy/msvc-dev-cmd@v1 with: arch: ${{ matrix.arch.target }} - - uses: actions/setup-python@v4 + + - name: Setup Python + uses: actions/setup-python@v5 with: - python-version: '3.x' + python-version: "3.11" + - name: Install meson and ninja run: pip install meson ninja + - name: Set up environment variables run: | "INSTALL_NAME=lite-xl-$($env:GITHUB_REF -replace ".*/")-windows-msvc-${{ matrix.arch.name }}" >> $env:GITHUB_ENV "INSTALL_REF=$($env:GITHUB_REF -replace ".*/")" >> $env:GITHUB_ENV "LUA_SUBPROJECT_PATH=subprojects/$(awk -F ' *= *' '/directory/ { printf $2 }' subprojects/lua.wrap)" >> $env:GITHUB_ENV + - name: Download and patch subprojects shell: bash run: | meson subprojects download cat resources/windows/001-lua-unicode.diff | patch -Np1 -d "$LUA_SUBPROJECT_PATH" + - name: Configure run: | meson setup --wrap-mode=forcefallback build + - name: Build run: | meson install -C build --destdir="../lite-xl" + - name: Package run: | Remove-Item -Recurse -Force -Path "lite-xl/lib","lite-xl/include" Compress-Archive -Path lite-xl -DestinationPath "$env:INSTALL_NAME.zip" + - name: Upload Artifacts - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: - name: Windows Artifacts (MSVC) + name: Windows Artifacts (MSVC ${{ matrix.arch.target }}) path: ${{ env.INSTALL_NAME }}.zip + compression-level: 0 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4a276bf5..9a7968ca 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,7 +9,7 @@ on: inputs: version: description: Release Version - default: v2.1.4 + default: v2.1.6 required: true jobs: @@ -21,54 +21,63 @@ jobs: outputs: upload_url: ${{ steps.create_release.outputs.upload_url }} version: ${{ steps.tag.outputs.version }} + steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 + - name: Fetch Version id: tag run: | if [[ "${{ github.event.inputs.version }}" != "" ]]; then - echo ::set-output name=version::${{ github.event.inputs.version }} + echo "version=${{ github.event.inputs.version }}" >> $GITHUB_OUTPUT else - echo ::set-output name=version::${GITHUB_REF/refs\/tags\//} + echo "version=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_OUTPUT fi + - name: Update Tag uses: richardsimko/update-tag@v1 with: tag_name: ${{ steps.tag.outputs.version }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Generate Release Notes + env: + GH_TOKEN: ${{ github.token }} + run: bash scripts/generate-release-notes.sh --debug --version ${{ steps.tag.outputs.version }} + - name: Create Release id: create_release - uses: softprops/action-gh-release@v1 + uses: ncipollo/release-action@v1 with: - tag_name: ${{ steps.tag.outputs.version }} name: Lite XL ${{ steps.tag.outputs.version }} + tag: ${{ steps.tag.outputs.version }} draft: true - body_path: changelog.md - generate_release_notes: true + bodyFile: release-notes.md + allowUpdates: true build_linux: name: Linux needs: release runs-on: ubuntu-latest - container: ghcr.io/lite-xl/lite-xl-build-box:latest - env: - CC: gcc - CXX: g++ + steps: - name: Set Environment Variables run: | echo "$HOME/.local/bin" >> "$GITHUB_PATH" echo "INSTALL_REF=${{ needs.release.outputs.version }}" >> "$GITHUB_ENV" - - uses: actions/checkout@v3 + echo "CCACHE_DIR=$PWD/.ccache" >> $GITHUB_ENV + + - name: Checkout code + uses: actions/checkout@v4 # disabled because this will break our own Python install - name: Python Setup if: false - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: - python-version: 3.9 + python-version: "3.11" # disabled because the container has up-to-date packages - name: Update Packages @@ -83,23 +92,34 @@ jobs: sudo apt-get install -y ccache - name: Build Portable - run: | - bash --version - bash scripts/build.sh --debug --forcefallback --portable --release - - name: Package Portables - run: | - bash scripts/package.sh --version ${INSTALL_REF} --debug --binary --release - bash scripts/package.sh --version ${INSTALL_REF} --debug --addons --binary --release - - name: Build AppImages - run: | - bash scripts/appimage.sh --debug --static --version ${INSTALL_REF} --release - bash scripts/appimage.sh --debug --nobuild --addons --version ${INSTALL_REF} - - name: Upload Files - uses: softprops/action-gh-release@v1 + uses: docker://ghcr.io/lite-xl/lite-xl-build-box-manylinux:v3 with: - tag_name: ${{ needs.release.outputs.version }} - draft: true - files: | + entrypoint: /entrypoint.sh + args: | + bash --version + bash scripts/build.sh --debug --forcefallback --portable --release + + - name: Package Portables + uses: docker://ghcr.io/lite-xl/lite-xl-build-box-manylinux:v3 + with: + entrypoint: /entrypoint.sh + args: | + bash scripts/package.sh --version ${INSTALL_REF} --debug --binary --release + bash scripts/package.sh --version ${INSTALL_REF} --debug --addons --binary --release + + - name: Build AppImages + uses: docker://ghcr.io/lite-xl/lite-xl-build-box-manylinux:v3 + with: + entrypoint: /entrypoint.sh + args: | + bash scripts/appimage.sh --debug --static --version ${INSTALL_REF} --release + bash scripts/appimage.sh --debug --nobuild --addons --version ${INSTALL_REF} + + - name: Upload Artifacts + uses: actions/upload-artifact@v4 + with: + name: Linux Artifacts + path: | lite-xl-${{ env.INSTALL_REF }}-linux-x86_64-portable.tar.gz lite-xl-${{ env.INSTALL_REF }}-addons-linux-x86_64-portable.tar.gz LiteXL-${{ env.INSTALL_REF }}-x86_64.AppImage @@ -108,13 +128,17 @@ jobs: build_macos: name: macOS needs: release - runs-on: macos-11 strategy: matrix: - arch: [x86_64, arm64] + config: + - { arch: x86_64, runner: macos-13 } # macos-13 uses x86_64 + - { arch: arm64, runner: macos-14 } # macos-14 / latest uses M1 + + runs-on: ${{ matrix.config.runner }} env: CC: clang CXX: clang++ + steps: - name: System Information run: | @@ -122,48 +146,50 @@ jobs: bash --version gcc -v xcodebuild -version + - name: Set Environment Variables run: | echo "$HOME/.local/bin" >> "$GITHUB_PATH" echo "INSTALL_REF=${{ needs.release.outputs.version }}" >> "$GITHUB_ENV" - echo "INSTALL_NAME=lite-xl-${{ needs.release.outputs.version }}-macos-${{ matrix.arch }}" >> "$GITHUB_ENV" - echo "INSTALL_NAME_ADDONS=lite-xl-${{ needs.release.outputs.version }}-addons-macos-${{ matrix.arch }}" >> "$GITHUB_ENV" - if [[ $(uname -m) != ${{ matrix.arch }} ]]; then echo "ARCH=--cross-arch ${{ matrix.arch }}" >> "$GITHUB_ENV"; fi - - uses: actions/checkout@v3 + echo "INSTALL_NAME=lite-xl-${{ needs.release.outputs.version }}-macos-${{ matrix.config.arch }}" >> "$GITHUB_ENV" + echo "INSTALL_NAME_ADDONS=lite-xl-${{ needs.release.outputs.version }}-addons-macos-${{ matrix.config.arch }}" >> "$GITHUB_ENV" + + - name: Checkout code + uses: actions/checkout@v4 + - name: Python Setup - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: - python-version: 3.9 + python-version: "3.11" + - name: Install Dependencies - run: bash scripts/install-dependencies.sh --debug + run: | + brew install bash md5sha1sum + pip install meson ninja dmgbuild + - name: Build run: | bash --version - bash scripts/build.sh --bundle --debug --forcefallback --release $ARCH + bash scripts/build.sh --bundle --debug --forcefallback --release + - name: Create DMG Image run: | - bash scripts/package.sh --version ${INSTALL_REF} $ARCH --debug --dmg --release - bash scripts/package.sh --version ${INSTALL_REF} $ARCH --debug --addons --dmg --release + bash scripts/package.sh --version ${INSTALL_REF} --debug --dmg --release + bash scripts/package.sh --version ${INSTALL_REF} --debug --addons --dmg --release + - name: Upload Artifacts - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: - name: macOS DMG Images + name: macOS DMG Images (${{ matrix.config.arch }}) path: | ${{ env.INSTALL_NAME }}.dmg ${{ env.INSTALL_NAME_ADDONS }}.dmg - - name: Upload Files - uses: softprops/action-gh-release@v1 - with: - tag_name: ${{ needs.release.outputs.version }} - draft: true - files: | - ${{ env.INSTALL_NAME }}.dmg - ${{ env.INSTALL_NAME_ADDONS }}.dmg build_macos_universal: name: macOS (Universal) needs: [release, build_macos] - runs-on: macos-11 + runs-on: macos-14 + steps: - name: System Information run: | @@ -171,40 +197,48 @@ jobs: bash --version gcc -v xcodebuild -version + - name: Set Environment Variables run: | echo "$HOME/.local/bin" >> "$GITHUB_PATH" echo "INSTALL_BASE=lite-xl-${{ needs.release.outputs.version }}-macos" >> "$GITHUB_ENV" echo "INSTALL_BASE_ADDONS=lite-xl-${{ needs.release.outputs.version }}-addons-macos" >> "$GITHUB_ENV" - - uses: actions/checkout@v2 + + - uses: actions/checkout@v4 + - name: Download Artifacts - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 id: download with: - name: macOS DMG Images + pattern: macOS DMG Images * + merge-multiple: true path: dmgs-original + - name: Python Setup - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: - python-version: 3.9 + python-version: "3.11" + - name: Install dmgbuild run: pip install dmgbuild + - name: Prepare DMG Images run: | mkdir -p dmgs-addons dmgs-normal mv -v "${{ steps.download.outputs.download-path }}/$INSTALL_BASE-"{x86_64,arm64}.dmg dmgs-normal mv -v "${{ steps.download.outputs.download-path }}/$INSTALL_BASE_ADDONS-"{x86_64,arm64}.dmg dmgs-addons + - name: Create Universal DMGs run: | bash --version bash scripts/make-universal-binaries.sh dmgs-normal "$INSTALL_BASE-universal" bash scripts/make-universal-binaries.sh dmgs-addons "$INSTALL_BASE_ADDONS-universal" - - name: Upload Files - uses: softprops/action-gh-release@v1 + + - name: Upload Artifacts + uses: actions/upload-artifact@v4 with: - tag_name: ${{ needs.release.outputs.version }} - draft: true - files: | + name: macOS DMG Images (Universal) + path: | ${{ env.INSTALL_BASE }}-universal.dmg ${{ env.INSTALL_BASE_ADDONS }}-universal.dmg @@ -214,54 +248,97 @@ jobs: runs-on: windows-2019 strategy: matrix: - msystem: [MINGW32, MINGW64] + config: + - { msystem: MINGW32, arch: i686 } + - { msystem: MINGW64, arch: x86_64 } defaults: run: shell: msys2 {0} + steps: - - uses: actions/checkout@v3 - - uses: msys2/setup-msys2@v2 + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup MSYS2 + uses: msys2/setup-msys2@v2 with: - msystem: ${{ matrix.msystem }} + msystem: ${{ matrix.config.msystem }} update: true install: >- base-devel git zip + unzip + mingw-w64-${{ matrix.config.arch }}-gcc + mingw-w64-${{ matrix.config.arch }}-meson + mingw-w64-${{ matrix.config.arch }}-ninja + mingw-w64-${{ matrix.config.arch }}-ca-certificates + mingw-w64-${{ matrix.config.arch }}-ntldd + - name: Set Environment Variables run: | echo "$HOME/.local/bin" >> "$GITHUB_PATH" echo "INSTALL_REF=${{ needs.release.outputs.version }}" >> "$GITHUB_ENV" if [[ "${MSYSTEM}" == "MINGW64" ]]; then - echo "BUILD_ARCH=x86_64" >> "$GITHUB_ENV" echo "INSTALL_NAME=lite-xl-${{ needs.release.outputs.version }}-windows-x86_64" >> "$GITHUB_ENV" echo "INSTALL_NAME_ADDONS=lite-xl-${{ needs.release.outputs.version }}-addons-windows-x86_64" >> "$GITHUB_ENV" else - echo "BUILD_ARCH=i686" >> "$GITHUB_ENV" echo "INSTALL_NAME=lite-xl-${{ needs.release.outputs.version }}-windows-i686" >> "$GITHUB_ENV" echo "INSTALL_NAME_ADDONS=lite-xl-${{ needs.release.outputs.version }}-addons-windows-i686" >> "$GITHUB_ENV" fi + - name: Install Dependencies + if: false run: bash scripts/install-dependencies.sh --debug + - name: Build run: | bash --version bash scripts/build.sh -U --debug --forcefallback --release + - name: Package run: bash scripts/package.sh --version ${INSTALL_REF} --debug --binary --release + - name: Build Installer run: bash scripts/innosetup/innosetup.sh --debug --version ${INSTALL_REF} + - name: Package With Addons run: bash scripts/package.sh --version ${INSTALL_REF} --debug --addons --binary --release + - name: Build Installer With Addons run: bash scripts/innosetup/innosetup.sh --debug --version ${INSTALL_REF} --addons - - name: Upload Files - uses: softprops/action-gh-release@v1 + + - name: Upload Artifacts + uses: actions/upload-artifact@v4 with: - tag_name: ${{ needs.release.outputs.version }} - draft: true - files: | + name: Windows Builds (${{ matrix.config.msystem }}) + path: | ${{ env.INSTALL_NAME }}.zip ${{ env.INSTALL_NAME_ADDONS }}.zip - LiteXL-${{ env.INSTALL_REF }}-${{ env.BUILD_ARCH }}-setup.exe - LiteXL-${{ env.INSTALL_REF }}-addons-${{ env.BUILD_ARCH }}-setup.exe + LiteXL-${{ env.INSTALL_REF }}-${{ matrix.config.arch }}-setup.exe + LiteXL-${{ env.INSTALL_REF }}-addons-${{ matrix.config.arch }}-setup.exe + + upload_artifacts: + name: Upload Release Artifacts + runs-on: ubuntu-latest + needs: + [release, build_linux, build_macos, build_macos_universal, build_windows_msys2] + permissions: + contents: write + + steps: + - name: Download Artifacts + uses: actions/download-artifact@v4 + with: + merge-multiple: true + + - name: Update Release + uses: ncipollo/release-action@v1 + with: + tag: ${{ needs.release.outputs.version }} + omitBodyDuringUpdate: true + omitDraftDuringUpdate: true + omitNameDuringUpdate: true + omitPrereleaseDuringUpdate: true + allowUpdates: true + artifacts: "*.exe,*.zip,*.tar.gz,*.dmg,*.AppImage" diff --git a/build.lhelper b/build.lhelper deleted file mode 100644 index 1fcdca3a..00000000 --- a/build.lhelper +++ /dev/null @@ -1,8 +0,0 @@ -CC="${CC:-gcc}" -CXX="${CXX:-g++}" -CFLAGS= -CXXFLAGS= -LDFLAGS= -BUILD_TYPE=Release - -packages=(pcre2 freetype2 sdl2 lua) diff --git a/changelog.md b/changelog.md index 3283bdf1..b1a93839 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,152 @@ # Changes Log +## [2.1.6] - 2024-11-29 + +This release introduces a new icon for macOS, improves the performance of the renderer, +adds syntax highlighting support for CUDA as well as QOL fixes and improvements. + +### Features + +* Add CUDA syntax highlighting support + ([#1848](https://github.com/lite-xl/lite-xl/pull/1848)) + +* Add macOS-specific application icon + ([#1844](https://github.com/lite-xl/lite-xl/pull/1844)) + +* Add keyboard shortcut to tooltips in ToolbarView + ([#1880](https://github.com/lite-xl/lite-xl/pull/1880)) + +* Improve projectsearch (status on the top, horizontal scrolling, elipsis) + ([#1876](https://github.com/lite-xl/lite-xl/pull/1876)) + +### Fixes + +* Correctly free SDL_Texture and SDL_Renderer + ([#1849](https://github.com/lite-xl/lite-xl/pull/1850)) + +* Fixed minor typo from merge of #1854 for Windows builds + +* Fix multi-type usage in delimited patterns + ([#1740](https://github.com/lite-xl/lite-xl/pull/1740)) + +* Fix appimage cd error and use static appimage runtime + ([#1924](https://github.com/lite-xl/lite-xl/pull/1924)) + +### Other Changes + +* Rewrite glyph cache + ([#1845](https://github.com/lite-xl/lite-xl/pull/1845)) + +* Use lite-xl-build-box-manylinux + ([#1877](https://github.com/lite-xl/lite-xl/pull/1877)) + +* Remove unused calls to system.absolute_path() + ([#1895](https://github.com/lite-xl/lite-xl/pull/1895)) + +* Remove lhelper script, build configuration and dependency support + ([#1906](https://github.com/lite-xl/lite-xl/pull/1906)) + +* Refactor how arguments are handled in process.start() + ([#1854](https://github.com/lite-xl/lite-xl/pull/1854)) + +* Add a proper name to EmptyView + ([#1569](https://github.com/lite-xl/lite-xl/pull/1569)) + +* Format renderer font scale code to be actually readable + ([#1921](https://github.com/lite-xl/lite-xl/pull/1921)) + +* Update PCRE2 wrap + ([#1927](https://github.com/lite-xl/lite-xl/pull/1927)) + +* Use meson datadir as lite_datadir base + ([#1939](https://github.com/lite-xl/lite-xl/pull/1939)) + +* Convert unix style paths literals into meson path segments + ([#1938](https://github.com/lite-xl/lite-xl/pull/1938)) + +## [2.1.5] - 2024-06-29 + +This release addresses several bugs from upstream dependencies and +improves the stability and usability of the program. + +### Features + +* New macOS installer background + ([#1816](https://github.com/lite-xl/lite-xl/pull/1816)) + +* Improve number highlighting for `language_c` + ([#1752](https://github.com/lite-xl/lite-xl/pull/1752)) + +* Backport number highlighting improvements to `language_cpp` + ([#1818](https://github.com/lite-xl/lite-xl/pull/1818)) + +* Support binary integer literals for `language_cpp` + ([#1819](https://github.com/lite-xl/lite-xl/pull/1819)) + +* Improve syntax highlighting for `language_python` + ([#1723](https://github.com/lite-xl/lite-xl/pull/1723)) + +* Support for drag-and-drop into Dock and "Open with" menu in macOS + ([#1822](https://github.com/lite-xl/lite-xl/pull/1822)) + +* Support `static constexpr` syntax in `language_cpp` + ([#1806](https://github.com/lite-xl/lite-xl/pull/1806)) + +### Fixes + +* Fix removing threads when iterating over `core.threads` + ([#1794](https://github.com/lite-xl/lite-xl/pull/1794)) + +* Fix dirmonitor backend selection + ([#1790](https://github.com/lite-xl/lite-xl/pull/1790)) + +* Fix clipboard removing newlines on Windows + ([#1788](https://github.com/lite-xl/lite-xl/pull/1788)) + +* Change co_wait to co_await in `language_cpp` + ([#1800](https://github.com/lite-xl/lite-xl/pull/1800)) + +* Fix font scale on monitor change when `RENDERER` backend is used + ([#1650](https://github.com/lite-xl/lite-xl/pull/1650)) + +* Avoid calling the change callback multiple times in the same notification + ([#1824](https://github.com/lite-xl/lite-xl/pull/1824)) + +* Fix autoreload reloading file too soon and misses some updates + ([#1823](https://github.com/lite-xl/lite-xl/pull/1823)) + +* Fix drag-and-drop multiple folders into Dock icon in macOS + ([#1828](https://github.com/lite-xl/lite-xl/pull/1828)) + +* Fix `Doc:merge_cursors()` accepting selection table index instead of selection index + ([#1833](https://github.com/lite-xl/lite-xl/pull/1833)) + +* Fix `Doc:merge_cursors()` table index calculation + ([#1834](https://github.com/lite-xl/lite-xl/pull/1834)) + +### Other Changes + +* Add functionality to generate release notes + ([#1774](https://github.com/lite-xl/lite-xl/pull/1774)) + +* Fix typo in release note template + ([#1801](https://github.com/lite-xl/lite-xl/pull/1801)) + +* Update action dependencies + ([#1724](https://github.com/lite-xl/lite-xl/pull/1724)) + +* Update labeler action config + ([#1805](https://github.com/lite-xl/lite-xl/pull/1805)) + +* Update macOS runner images + ([#1804](https://github.com/lite-xl/lite-xl/pull/1804)) + +* Update macOS copyright notice + ([#1815](https://github.com/lite-xl/lite-xl/pull/1815)) + +* Update SDL2 and PCRE2 + ([#1812](https://github.com/lite-xl/lite-xl/pull/1812)) + ## [2.1.4] - 2024-04-16 This release addresses severe bugs not found in previous releases, @@ -1504,6 +1651,8 @@ A new global variable `USERDIR` is exposed to point to the user's directory. - subpixel font rendering with gamma correction +[2.1.6]: https://github.com/lite-xl/lite-xl/releases/tag/v2.1.6 +[2.1.5]: https://github.com/lite-xl/lite-xl/releases/tag/v2.1.5 [2.1.4]: https://github.com/lite-xl/lite-xl/releases/tag/v2.1.4 [2.1.3]: https://github.com/lite-xl/lite-xl/releases/tag/v2.1.3 [2.1.2]: https://github.com/lite-xl/lite-xl/releases/tag/v2.1.2 diff --git a/data/core/doc/init.lua b/data/core/doc/init.lua index 62112bdf..f7b2c4d4 100644 --- a/data/core/doc/init.lua +++ b/data/core/doc/init.lua @@ -249,7 +249,8 @@ function Doc:set_selection(line1, col1, line2, col2, swap) end function Doc:merge_cursors(idx) - for i = (idx or (#self.selections - 3)), (idx or 5), -4 do + local table_index = idx and (idx - 1) * 4 + 1 + for i = (table_index or (#self.selections - 3)), (table_index or 5), -4 do for j = 1, i - 4, 4 do if self.selections[i] == self.selections[j] and self.selections[i+1] == self.selections[j+1] then diff --git a/data/core/emptyview.lua b/data/core/emptyview.lua index 2152da3b..865dc55d 100644 --- a/data/core/emptyview.lua +++ b/data/core/emptyview.lua @@ -6,6 +6,14 @@ local View = require "core.view" ---@field super core.view local EmptyView = View:extend() +function EmptyView:get_name() + return "Get Started" +end + +function EmptyView:get_filename() + return "" +end + local function draw_text(x, y, color) local lines = { { fmt = "%s to run a command", cmd = "core:find-command" }, diff --git a/data/core/init.lua b/data/core/init.lua index 3facdacc..e36173a3 100644 --- a/data/core/init.lua +++ b/data/core/init.lua @@ -1092,8 +1092,14 @@ function core.show_title_bar(show) end +local thread_counter = 0 function core.add_thread(f, weak_ref, ...) - local key = weak_ref or #core.threads + 1 + local key = weak_ref + if not key then + thread_counter = thread_counter + 1 + key = thread_counter + end + assert(core.threads[key] == nil, "Duplicate thread reference") local args = {...} local fn = function() return core.try(f, table.unpack(args)) end core.threads[key] = { cr = coroutine.create(fn), wake = 0 } @@ -1266,20 +1272,7 @@ function core.on_event(type, ...) elseif type == "minimized" or type == "maximized" or type == "restored" then core.window_mode = type == "restored" and "normal" or type elseif type == "filedropped" then - if not core.root_view:on_file_dropped(...) then - local filename, mx, my = ... - local info = system.get_file_info(filename) - if info and info.type == "dir" then - system.exec(string.format("%q %q", EXEFILE, filename)) - else - local ok, doc = core.try(core.open_doc, filename) - if ok then - local node = core.root_view.root_node:get_child_overlapping_point(mx, my) - node:set_active_view(node.active_view) - core.root_view:open_doc(doc) - end - end - end + core.root_view:on_file_dropped(...) elseif type == "focuslost" then core.root_view:on_focus_lost(...) elseif type == "quit" then @@ -1356,16 +1349,20 @@ local run_threads = coroutine.wrap(function() local max_time = 1 / config.fps - 0.004 local minimal_time_to_wake = math.huge + local threads = {} + -- We modify core.threads while iterating, both by removing dead threads, + -- and by potentially adding more threads while we yielded early, + -- so we need to extract the threads list and iterate over that instead. for k, thread in pairs(core.threads) do - -- run thread - if thread.wake < system.get_time() then + threads[k] = thread + end + + for k, thread in pairs(threads) do + -- Run thread if it wasn't deleted externally and it's time to resume it + if core.threads[k] and thread.wake < system.get_time() then local _, wait = assert(coroutine.resume(thread.cr)) if coroutine.status(thread.cr) == "dead" then - if type(k) == "number" then - table.remove(core.threads, k) - else - core.threads[k] = nil - end + core.threads[k] = nil elseif wait then thread.wake = system.get_time() + wait minimal_time_to_wake = math.min(minimal_time_to_wake, wait) diff --git a/data/core/process.lua b/data/core/process.lua new file mode 100644 index 00000000..bffc9f18 --- /dev/null +++ b/data/core/process.lua @@ -0,0 +1,50 @@ +local function env_key(str) + if PLATFORM == "Windows" then return str:upper() else return str end +end + +---Sorts the environment variable by its key, converted to uppercase. +---This is only needed on Windows. +local function compare_env(a, b) + return env_key(a:match("([^=]*)=")) < env_key(b:match("([^=]*)=")) +end + +local old_start = process.start +function process.start(command, options) + assert(type(command) == "table" or type(command) == "string", "invalid argument #1 to process.start(), expected string or table, got "..type(command)) + assert(type(options) == "table" or type(options) == "nil", "invalid argument #2 to process.start(), expected table or nil, got "..type(options)) + if PLATFORM == "Windows" then + if type(command) == "table" then + -- escape the arguments into a command line string + -- https://github.com/python/cpython/blob/48f9d3e3faec5faaa4f7c9849fecd27eae4da213/Lib/subprocess.py#L531 + local arglist = {} + for _, v in ipairs(command) do + local backslash, arg = 0, {} + for c in v:gmatch(".") do + if c == "\\" then backslash = backslash + 1 + elseif c == '"' then arg[#arg+1] = string.rep("\\", backslash * 2 + 1)..'"'; backslash = 0 + else arg[#arg+1] = string.rep("\\", backslash) .. c; backslash = 0 end + end + arg[#arg+1] = string.rep("\\", backslash) -- add remaining backslashes + if #v == 0 or v:find("[\t\v\r\n ]") then arglist[#arglist+1] = '"'..table.concat(arg, "")..'"' + else arglist[#arglist+1] = table.concat(arg, "") end + end + command = table.concat(arglist, " ") + end + else + command = type(command) == "table" and command or { command } + end + if type(options) == "table" and options.env then + local user_env = options.env --[[@as table]] + options.env = function(system_env) + local final_env, envlist = {}, {} + for k, v in pairs(system_env) do final_env[env_key(k)] = k.."="..v end + for k, v in pairs(user_env) do final_env[env_key(k)] = k.."="..v end + for _, v in pairs(final_env) do envlist[#envlist+1] = v end + if PLATFORM == "Windows" then table.sort(envlist, compare_env) end + return table.concat(envlist, "\0").."\0\0" + end + end + return old_start(command, options) +end + +return process diff --git a/data/core/rootview.lua b/data/core/rootview.lua index 047768cc..3fc5d54b 100644 --- a/data/core/rootview.lua +++ b/data/core/rootview.lua @@ -24,6 +24,7 @@ function RootView:new() base_color = style.drag_overlay_tab, color = { table.unpack(style.drag_overlay_tab) } } self.drag_overlay_tab.to = { x = 0, y = 0, w = 0, h = 0 } + self.first_dnd_processed = false end @@ -319,7 +320,29 @@ end ---@return boolean function RootView:on_file_dropped(filename, x, y) local node = self.root_node:get_child_overlapping_point(x, y) - return node and node.active_view:on_file_dropped(filename, x, y) + local result = node and node.active_view:on_file_dropped(filename, x, y) + if result then return result end + local info = system.get_file_info(filename) + if info and info.type == "dir" then + if self.first_dnd_processed then + -- first update done, open in new window + system.exec(string.format("%q %q", EXEFILE, filename)) + else + -- DND event before first update, this is sent by macOS when folder is dropped into the dock + core.confirm_close_docs(core.docs, function(dirpath) + core.open_folder_project(dirpath) + end, system.absolute_path(filename)) + self.first_dnd_processed = true + end + else + local ok, doc = core.try(core.open_doc, filename) + if ok then + local node = core.root_view.root_node:get_child_overlapping_point(x, y) + node:set_active_view(node.active_view) + core.root_view:open_doc(doc) + end + end + return true end @@ -363,6 +386,9 @@ function RootView:update() self:update_drag_overlay() self:interpolate_drag_overlay(self.drag_overlay) self:interpolate_drag_overlay(self.drag_overlay_tab) + -- set this to true because at this point there are no dnd requests + -- that are caused by the initial dnd into dock user action + self.first_dnd_processed = true end diff --git a/data/core/start.lua b/data/core/start.lua index 4168a16e..52b30820 100644 --- a/data/core/start.lua +++ b/data/core/start.lua @@ -1,5 +1,5 @@ -- this file is used by lite-xl to setup the Lua environment when starting -VERSION = "2.1.4r1" +VERSION = "2.1.6r1" MOD_VERSION = "3" SCALE = tonumber(os.getenv("LITE_SCALE") or os.getenv("GDK_SCALE") or os.getenv("QT_SCALE_FACTOR")) or 1 @@ -9,7 +9,7 @@ EXEDIR = EXEFILE:match("^(.+)[/\\][^/\\]+$") if MACOS_RESOURCES then DATADIR = MACOS_RESOURCES else - local prefix = os.getenv('LITE_PREFIX') or EXEDIR:match("^(.+)[/\\]bin$") + local prefix = EXEDIR:match("^(.+)[/\\]bin$") DATADIR = prefix and (prefix .. PATHSEP .. 'share' .. PATHSEP .. 'lite-xl') or (EXEDIR .. PATHSEP .. 'data') end USERDIR = (system.get_file_info(EXEDIR .. PATHSEP .. 'user') and (EXEDIR .. PATHSEP .. 'user')) @@ -46,6 +46,7 @@ table.unpack = table.unpack or unpack bit32 = bit32 or require "core.bit" require "core.utf8string" +require "core.process" -- Because AppImages change the working directory before running the executable, -- we need to change it back to the original one. diff --git a/data/core/tokenizer.lua b/data/core/tokenizer.lua index 46b54639..a455184b 100644 --- a/data/core/tokenizer.lua +++ b/data/core/tokenizer.lua @@ -6,7 +6,6 @@ local tokenizer = {} local bad_patterns = {} local function push_token(t, type, text) - if not text or #text == 0 then return end type = type or "normal" local prev_type = t[#t-1] local prev_text = t[#t] @@ -284,7 +283,8 @@ function tokenizer.tokenize(incoming_syntax, text, state, resume) -- continue trying to match the end pattern of a pair if we have a state set if current_pattern_idx > 0 then local p = current_syntax.patterns[current_pattern_idx] - local s, e = find_text(text, p, i, false, true) + local find_results = { find_text(text, p, i, false, true) } + local s, e = find_results[1], find_results[2] local cont = true -- If we're in subsyntax mode, always check to see if we end our syntax @@ -306,7 +306,12 @@ function tokenizer.tokenize(incoming_syntax, text, state, resume) -- continue on as normal. if cont then if s then - push_token(res, p.type, text:usub(i, e)) + -- Push remaining token before the end delimiter + if s > i then + push_token(res, p.type, text:usub(i, s - 1)) + end + -- Push the end delimiter + push_tokens(res, current_syntax, p, text, find_results) set_subsyntax_pattern_idx(0) i = e + 1 else diff --git a/data/plugins/autoreload.lua b/data/plugins/autoreload.lua index f1ba6e0d..7086fb35 100644 --- a/data/plugins/autoreload.lua +++ b/data/plugins/autoreload.lua @@ -1,7 +1,6 @@ -- mod-version:3 local core = require "core" local config = require "core.config" -local style = require "core.style" local Doc = require "core.doc" local Node = require "core.node" local common = require "core.common" @@ -26,7 +25,7 @@ local times = setmetatable({}, { __mode = "k" }) local visible = setmetatable({}, { __mode = "k" }) local function get_project_doc_watch(doc) - for i, v in ipairs(core.project_directories) do + for _, v in ipairs(core.project_directories) do if doc.abs_filename:find(v.name, 1, true) == 1 then return v.watch end end return watch @@ -43,6 +42,34 @@ local function reload_doc(doc) core.log_quiet("Auto-reloaded doc \"%s\"", doc.filename) end + +local timers = setmetatable({}, { __mode = "k" }) + +local function delayed_reload(doc, mtime) + if timers[doc] then + -- If mtime remains the same, there's no need to restart the timer + -- as we're waiting a full second anyways. + if not mtime or timers[doc].mtime ~= mtime then + timers[doc] = { last_trigger = system.get_time(), mtime = mtime } + end + return + end + + timers[doc] = { last_trigger = system.get_time(), mtime = mtime } + core.add_thread(function() + local diff = system.get_time() - timers[doc].last_trigger + -- Wait a second before triggering a reload because we're using mtime + -- to determine if a file has changed, and on many systems it has a + -- resolution of 1 second. + while diff < 1 do + coroutine.yield(diff) + diff = system.get_time() - timers[doc].last_trigger + end + timers[doc] = nil + reload_doc(doc) + end) +end + local function check_prompt_reload(doc) if doc and doc.deferred_reload then core.nag_view:show("File Changed", doc.filename .. " has changed. Reload this file?", { @@ -71,7 +98,7 @@ function dirwatch:check(change_callback, ...) local info = system.get_file_info(doc.filename or "") if info and info.type == "file" and times[doc] ~= info.modified then if not doc:is_dirty() and not config.plugins.autoreload.always_show_nagview then - reload_doc(doc) + delayed_reload(doc, info.modified) else doc.deferred_reload = true if doc == core.active_view.doc then check_prompt_reload(doc) end diff --git a/data/plugins/language_c.lua b/data/plugins/language_c.lua index c15466be..2804fb7b 100644 --- a/data/plugins/language_c.lua +++ b/data/plugins/language_c.lua @@ -1,6 +1,11 @@ -- mod-version:3 local syntax = require "core.syntax" +-- integer suffix combinations as a regex +local isuf = [[(?:[lL][uU]|ll[uU]|LL[uU]|[uU][lL]\b|[uU]ll|[uU]LL|[uU]|[lL]\b|ll|LL)?]] +-- float suffix combinations as a Lua pattern / regex +local fsuf = "[fFlL]?" + syntax.add { name = "C", files = { "%.c$" }, @@ -11,9 +16,13 @@ syntax.add { { pattern = { "/%*", "%*/" }, type = "comment" }, { pattern = { '"', '"', '\\' }, type = "string" }, { pattern = { "'", "'", '\\' }, type = "string" }, - { pattern = "0x%x+", type = "number" }, - { pattern = "%d+[%d%.eE]*f?", type = "number" }, - { pattern = "%.?%d+f?", type = "number" }, + { regex = "0x[0-9a-fA-f]+"..isuf, type = "number" }, + { regex = "0()[0-7]+"..isuf, type = { "keyword", "number" } }, + { pattern = "%d+%.%d*[Ee]%d+"..fsuf, type = "number" }, + { pattern = "%d+[Ee]%d+"..fsuf, type = "number" }, + { pattern = "%d+%.%d*"..fsuf, type = "number" }, + { pattern = "%.%d+"..fsuf, type = "number" }, + { regex = "\\d+"..isuf, type = "number" }, { pattern = "[%+%-=/%*%^%%<>!~|&]", type = "operator" }, { pattern = "##", type = "operator" }, { pattern = "struct%s()[%a_][%w_]*", type = {"keyword", "keyword2"} }, diff --git a/data/plugins/language_cpp.lua b/data/plugins/language_cpp.lua index 88f0770f..2bbc688e 100644 --- a/data/plugins/language_cpp.lua +++ b/data/plugins/language_cpp.lua @@ -1,12 +1,17 @@ -- mod-version:3 local syntax = require "core.syntax" +-- integer suffix combinations as a regex +local isuf = [[(?:[lL][uU]|ll[uU]|LL[uU]|[uU][lL]\b|[uU]ll|[uU]LL|[uU]|[lL]\b|ll|LL)?]] +-- float suffix combinations as a Lua pattern / regex +local fsuf = "[fFlL]?" + syntax.add { name = "C++", files = { "%.h$", "%.inl$", "%.cpp$", "%.cc$", "%.C$", "%.cxx$", "%.c++$", "%.hh$", "%.H$", "%.hxx$", "%.hpp$", "%.h++$", - "%.ino$" + "%.ino$", "%.cu$", "%.cuh$" }, comment = "//", block_comment = { "/*", "*/" }, @@ -15,9 +20,14 @@ syntax.add { { pattern = { "/%*", "%*/" }, type = "comment" }, { pattern = { '"', '"', '\\' }, type = "string" }, { pattern = { "'", "'", '\\' }, type = "string" }, - { pattern = "0x%x+[%x']*", type = "number" }, - { pattern = "%d+[%d%.'eE]*f?", type = "number" }, - { pattern = "%.?%d+[%d']*f?", type = "number" }, + { regex = "0x[0-9a-fA-f]+"..isuf, type = "number" }, + { regex = "0b[01]+"..isuf, type = "number" }, + { regex = "0()[0-7]+"..isuf, type = { "keyword", "number" } }, + { pattern = "%d+%.%d*[Ee]%d+"..fsuf, type = "number" }, + { pattern = "%d+[Ee]%d+"..fsuf, type = "number" }, + { pattern = "%d+%.%d*"..fsuf, type = "number" }, + { pattern = "%.%d+"..fsuf, type = "number" }, + { regex = "\\d+"..isuf, type = "number" }, { pattern = "[%+%-=/%*%^%%<>!~|:&]", type = "operator" }, { pattern = "##", type = "operator" }, { pattern = "struct%s()[%a_][%w_]*", type = {"keyword", "keyword2"} }, @@ -28,6 +38,15 @@ syntax.add { { pattern = "static()%s+()inline", type = { "keyword", "normal", "keyword" } }, + { pattern = "static()%s+()constexpr", + type = { "keyword", "normal", "keyword" } + }, + { pattern = "static()%s+()constinit", + type = { "keyword", "normal", "keyword" } + }, + { pattern = "static()%s+()consteval", + type = { "keyword", "normal", "keyword" } + }, { pattern = "static()%s+()const", type = { "keyword", "normal", "keyword" } }, @@ -133,7 +152,7 @@ syntax.add { ["this"] = "keyword", ["thread_local"] = "keyword", ["requires"] = "keyword", - ["co_wait"] = "keyword", + ["co_await"] = "keyword", ["co_return"] = "keyword", ["co_yield"] = "keyword", ["decltype"] = "keyword", diff --git a/data/plugins/language_python.lua b/data/plugins/language_python.lua index f7c09ac3..8d67f57e 100644 --- a/data/plugins/language_python.lua +++ b/data/plugins/language_python.lua @@ -1,66 +1,188 @@ -- mod-version:3 local syntax = require "core.syntax" +local function table_merge(a, b) + local t = {} + for _, v in pairs(a) do table.insert(t, v) end + for _, v in pairs(b) do table.insert(t, v) end + + return t +end + + +local python_symbols = { + + ["class"] = "keyword", + ["finally"] = "keyword", + ["is"] = "keyword", + ["return"] = "keyword", + ["continue"] = "keyword", + ["for"] = "keyword", + ["lambda"] = "keyword", + ["try"] = "keyword", + ["except"] = "keyword", + ["def"] = "keyword", + ["async"] = "keyword", + ["await"] = "keyword", + ["from"] = "keyword", + ["nonlocal"] = "keyword", + ["while"] = "keyword", + ["and"] = "keyword", + ["global"] = "keyword", + ["not"] = "keyword", + ["with"] = "keyword", + ["as"] = "keyword", + ["elif"] = "keyword", + ["if"] = "keyword", + ["or"] = "keyword", + ["else"] = "keyword", + ["match"] = "keyword", + ["case"] = "keyword", + ["import"] = "keyword", + ["pass"] = "keyword", + ["break"] = "keyword", + ["in"] = "keyword", + ["del"] = "keyword", + ["raise"] = "keyword", + ["yield"] = "keyword", + ["assert"] = "keyword", + + ["self"] = "keyword2", + + ["None"] = "literal", + ["True"] = "literal", + ["False"] = "literal", + +} + + + +local python_fstring = { + + patterns = { + { pattern = "\\.", type = "string" }, + { pattern = '[^"\\{}\']+', type = "string" } + + }, + + symbols = {} +} + + + +local python_patterns = { + + { pattern = '[uUrR]%f["]', type = "keyword" }, + + { pattern = { '[ruU]?"""', '"""', '\\' }, type = "string" }, + { pattern = { "[ruU]?'''", "'''", '\\' }, type = "string" }, + { pattern = { '[ruU]?"', '"', '\\' }, type = "string" }, + { pattern = { "[ruU]?'", "'", '\\' }, type = "string" }, + { pattern = { 'f"', '"', "\\" }, type = "string", syntax = python_fstring }, + { pattern = { "f'", "'", "\\" }, type = "string", syntax = python_fstring }, + + { pattern = "%d+[%d%.eE_]*", type = "number" }, + { pattern = "0[xboXBO][%da-fA-F_]+", type = "number" }, + { pattern = "%.?%d+", type = "number" }, + { pattern = "%f[-%w_]-%f[%d%.]", type = "number" }, + + { pattern = "[%+%-=/%*%^%%<>!~|&]", type = "operator" }, + { pattern = "[%a_][%w_]*%f[(]", type = "function" }, + + { pattern = "[%a_][%w_]+", type = "symbol" }, + +} + + + +local python_type = { + + patterns = { + { pattern = "|", type = "operator" }, + { pattern = "[%w_]+", type = "keyword2" }, + { pattern = "[%a_][%w_]+", type = "symbol" }, + }, + + symbols = { + ["None"] = "literal" + } +} +-- Add this line after in order for the recursion to work. +-- Makes sure that the square brackets are well balanced when capturing the syntax +-- (in order to make something like this work: Tuple[Tuple[int, str], float]) +table.insert(python_type.patterns, 1, { pattern = { "%[", "%]" }, syntax = python_type }) + + + +-- For things like this_list = other_list[a:b:c] +local not_python_type = { + + patterns = python_patterns, + + symbols = python_symbols + +} + +table.insert(not_python_type.patterns, 1, { pattern = { "%[", "%]" }, syntax = not_python_type }) +table.insert(not_python_type.patterns, 1, { pattern = { "{", "}" }, syntax = not_python_type }) + +table.insert(python_fstring.patterns, 1, { pattern = { "{", "}" }, syntax = not_python_type }) + + + +local python_func = { + + patterns = table_merge({ + + { pattern = { "->", "%f[:]" }, type = "operator", syntax = python_type }, + { pattern = { ":%s*", "%f[^%[%]%w_]" }, syntax = python_type }, + + }, python_patterns), + + symbols = python_symbols +} + +table.insert(python_func.patterns, 1, { pattern = { "%(", "%)" }, syntax = python_func }) + + + syntax.add { name = "Python", files = { "%.py$", "%.pyw$", "%.rpy$", "%.pyi$" }, headers = "^#!.*[ /]python", comment = "#", block_comment = { '"""', '"""' }, - patterns = { - { pattern = "#.*", type = "comment" }, - { pattern = { '^%s*"""', '"""' }, type = "comment" }, - { pattern = '[uUrR]%f["]', type = "keyword" }, - { pattern = "class%s+()[%a_][%w_]*", type = {"keyword", "keyword2"} }, - { pattern = { '[ruU]?"""', '"""'; '\\' }, type = "string" }, - { pattern = { "[ruU]?'''", "'''", '\\' }, type = "string" }, - { pattern = { '[ruU]?"', '"', '\\' }, type = "string" }, - { pattern = { "[ruU]?'", "'", '\\' }, type = "string" }, - { pattern = "-?0[xboXBO][%da-fA-F_]+",type = "number" }, - { pattern = "-?%d+[%d%.eE_]*", type = "number" }, - { pattern = "-?%.?%d+", type = "number" }, - { pattern = "[%+%-=/%*%^%%<>!~|&]", type = "operator" }, - { pattern = "[%a_][%w_]*%f[(]", type = "function" }, - { pattern = "[%a_][%w_]*", type = "symbol" }, - }, - symbols = { - ["class"] = "keyword", - ["finally"] = "keyword", - ["is"] = "keyword", - ["return"] = "keyword", - ["continue"] = "keyword", - ["for"] = "keyword", - ["lambda"] = "keyword", - ["try"] = "keyword", - ["def"] = "keyword", - ["async"] = "keyword", - ["await"] = "keyword", - ["from"] = "keyword", - ["nonlocal"] = "keyword", - ["while"] = "keyword", - ["and"] = "keyword", - ["global"] = "keyword", - ["not"] = "keyword", - ["with"] = "keyword", - ["as"] = "keyword", - ["elif"] = "keyword", - ["if"] = "keyword", - ["or"] = "keyword", - ["else"] = "keyword", - ["match"] = "keyword", - ["case"] = "keyword", - ["import"] = "keyword", - ["pass"] = "keyword", - ["break"] = "keyword", - ["except"] = "keyword", - ["in"] = "keyword", - ["del"] = "keyword", - ["raise"] = "keyword", - ["yield"] = "keyword", - ["assert"] = "keyword", - ["self"] = "keyword2", - ["None"] = "literal", - ["True"] = "literal", - ["False"] = "literal", - } + + patterns = table_merge({ + + { pattern = "#.*", type = "comment" }, + { pattern = { '^%s*"""', '"""' }, type = "comment" }, + + { pattern = { "%[", "%]" }, syntax = not_python_type }, + { pattern = { "{", "}" }, syntax = not_python_type }, + + { pattern = { "^%s*()def%f[%s]", ":" }, type = { "normal", "keyword" }, syntax = python_func }, -- this and the following prevent one-liner highlight bugs + + { pattern = { "^%s*()for%f[%s]", ":" }, type = { "normal", "keyword" }, syntax = not_python_type }, + { pattern = { "^%s*()if%f[%s]", ":" }, type = { "normal", "keyword" }, syntax = not_python_type }, + { pattern = { "^%s*()elif%f[%s]", ":" }, type = { "normal", "keyword" }, syntax = not_python_type }, + { pattern = { "^%s*()while%f[%s]", ":" }, type = { "normal", "keyword" }, syntax = not_python_type }, + { pattern = { "^%s*()match%f[%s]", ":" }, type = { "normal", "keyword" }, syntax = not_python_type }, + { pattern = { "^%s*()case%f[%s]", ":" }, type = { "normal", "keyword" }, syntax = not_python_type }, + { pattern = { "^%s*()except%f[%s]", ":" }, type = { "normal", "keyword" }, syntax = not_python_type }, + + { pattern = "else():", type = { "keyword", "normal" } }, + { pattern = "try():", type = { "keyword", "normal" } }, + + { pattern = "lambda()%s.+:", type = { "keyword", "normal" } }, + { pattern = "class%s+()[%a_][%w_]+().*:", type = { "keyword", "keyword2", "normal" } }, + + + { pattern = { ":%s*", "%f[^%[%]%w_]"}, syntax = python_type }, + + }, python_patterns), + + symbols = python_symbols + } + diff --git a/data/plugins/projectsearch.lua b/data/plugins/projectsearch.lua index cc5e1324..45019374 100644 --- a/data/plugins/projectsearch.lua +++ b/data/plugins/projectsearch.lua @@ -15,6 +15,7 @@ function ResultsView:new(path, text, fn) ResultsView.super.new(self) self.scrollable = true self.brightness = 0 + self.max_h_scroll = 0 self:begin_search(path, text, fn) end @@ -34,7 +35,9 @@ local function find_all_matches_in_file(t, filename, fn) -- Insert maximum 256 characters. If we insert more, for compiled files, which can have very long lines -- things tend to get sluggish. If our line is longer than 80 characters, begin to truncate the thing. local start_index = math.max(s - 80, 1) - table.insert(t, { file = filename, text = (start_index > 1 and "..." or "") .. line:sub(start_index, 256 + start_index), line = n, col = s }) + local text = (start_index > 1 and "..." or "") .. line:sub(start_index, 256 + start_index) + if #line > 256 + start_index then text = text .. "..." end + table.insert(t, { file = filename, text = text, line = n, col = s }) core.redraw = true end if n % 100 == 0 then coroutine.yield() end @@ -133,10 +136,16 @@ function ResultsView:get_scrollable_size() end +function ResultsView:get_h_scrollable_size() + return self.max_h_scroll +end + + function ResultsView:get_visible_results_range() local lh = self:get_line_height() local oy = self:get_results_yoffset() - local min = math.max(1, math.floor((self.scroll.y - oy) / lh)) + local min = self.scroll.y+oy-style.font:get_height() + min = math.max(1, math.floor(min / lh)) return min, min + math.floor(self.size.y / lh) + 1 end @@ -150,7 +159,8 @@ function ResultsView:each_visible_result() for i = min, max do local item = self.results[i] if not item then break end - coroutine.yield(i, item, x, y, self.size.x, lh) + local _, _, w = self:get_content_bounds() + coroutine.yield(i, item, x, y, w, lh) y = y + lh end end) @@ -159,9 +169,9 @@ end function ResultsView:scroll_to_make_selected_visible() local h = self:get_line_height() - local y = self:get_results_yoffset() + h * (self.selected_idx - 1) + local y = h * (self.selected_idx - 1) self.scroll.to.y = math.min(self.scroll.to.y, y) - self.scroll.to.y = math.max(self.scroll.to.y, y + h - self.size.y) + self.scroll.to.y = math.max(self.scroll.to.y, y + h - self.size.y + self:get_results_yoffset()) end @@ -169,7 +179,13 @@ function ResultsView:draw() self:draw_background(style.background) -- status - local ox, oy = self:get_content_offset() + local ox, oy = self.position.x, self.position.y + local yoffset = self:get_results_yoffset() + renderer.draw_rect(self.position.x, self.position.y, self.size.x, yoffset, style.background) + if self.scroll.y ~= 0 then + renderer.draw_rect(self.position.x, self.position.y+yoffset, self.size.x, style.divider_size, style.divider) + end + local x, y = ox + style.padding.x, oy + style.padding.y local files_number = core.project_files_number() local per = common.clamp(files_number and self.last_file_idx / files_number or 1, 0, 1) @@ -191,7 +207,6 @@ function ResultsView:draw() renderer.draw_text(style.font, text, x, y, color) -- horizontal line - local yoffset = self:get_results_yoffset() local x = ox + style.padding.x local w = self.size.x - style.padding.x * 2 local h = style.divider_size @@ -202,6 +217,8 @@ function ResultsView:draw() end -- results + local _, _, bw = self:get_content_bounds() + core.push_clip_rect(ox, oy+yoffset + style.divider_size, bw, self.size.y-yoffset) local y1, y2 = self.position.y, self.position.y + self.size.y for i, item, x,y,w,h in self:each_visible_result() do local color = style.text @@ -213,7 +230,9 @@ function ResultsView:draw() local text = string.format("%s at line %d (col %d): ", item.file, item.line, item.col) x = common.draw_text(style.font, style.dim, text, "left", x, y, w, h) x = common.draw_text(style.code_font, color, item.text, "left", x, y, w, h) + self.max_h_scroll = math.max(self.max_h_scroll, x) end + core.pop_clip_rect() self:draw_scrollbar() end diff --git a/data/plugins/toolbarview.lua b/data/plugins/toolbarview.lua index 9cfa15e8..e91438cf 100644 --- a/data/plugins/toolbarview.lua +++ b/data/plugins/toolbarview.lua @@ -3,6 +3,7 @@ local core = require "core" local common = require "core.common" local command = require "core.command" local style = require "core.style" +local keymap = require "core.keymap" local View = require "core.view" local ToolbarView = View:extend() @@ -110,7 +111,9 @@ function ToolbarView:on_mouse_moved(px, py, ...) y_min, y_max = y, y + h if px > x and py > y and px <= x + w and py <= y + h then self.hovered_item = item - core.status_view:show_tooltip(command.prettify_name(item.command)) + local binding = keymap.get_binding(item.command) + local name = command.prettify_name(item.command) + core.status_view:show_tooltip(binding and { name, style.dim, " ", binding } or { name }) self.tooltip = true return end diff --git a/data/plugins/treeview.lua b/data/plugins/treeview.lua index 3aecba83..5b047987 100644 --- a/data/plugins/treeview.lua +++ b/data/plugins/treeview.lua @@ -359,9 +359,6 @@ function TreeView:draw() self:draw_background(style.background2) local _y, _h = self.position.y, self.size.y - local doc = core.active_view.doc - local active_filename = doc and system.absolute_path(doc.filename or "") - for item, x,y,w,h in self:each_item() do if y + h >= _y and y < _y + _h then self:draw_item(item, diff --git a/meson.build b/meson.build index 9e8e398b..6af2af9d 100644 --- a/meson.build +++ b/meson.build @@ -1,6 +1,6 @@ project('lite-xl', ['c'], - version : '2.1.4', + version : '2.1.6', license : 'MIT', meson_version : '>= 0.56', default_options : [ @@ -123,7 +123,6 @@ if not get_option('source-only') sdl_options += 'use_atomic=enabled' sdl_options += 'use_threads=enabled' sdl_options += 'use_timers=enabled' - sdl_options += 'with_main=true' # investigate if this is truly needed # Do not remove before https://github.com/libsdl-org/SDL/issues/5413 is released sdl_options += 'use_events=enabled' @@ -150,9 +149,11 @@ if not get_option('source-only') sdl_options += 'test=false' sdl_options += 'use_sensor=disabled' sdl_options += 'use_haptic=disabled' + sdl_options += 'use_hidapi=disabled' sdl_options += 'use_audio=disabled' sdl_options += 'use_cpuinfo=disabled' sdl_options += 'use_joystick=disabled' + sdl_options += 'use_joystick_xinput=disabled' sdl_options += 'use_video_vulkan=disabled' sdl_options += 'use_video_offscreen=disabled' sdl_options += 'use_power=disabled' @@ -183,50 +184,55 @@ if get_option('portable') or host_machine.system() == 'windows' lite_docdir = '/doc' lite_datadir = '/data' configure_file( - input: 'resources/windows/lite-xl.exe.manifest.in', + input: 'resources' / 'windows' / 'lite-xl.exe.manifest.in', output: 'lite-xl.exe.manifest', configuration: conf_data ) elif get_option('bundle') and host_machine.system() == 'darwin' lite_cargs += '-DMACOS_USE_BUNDLE' - lite_bindir = 'Contents/MacOS' - lite_docdir = 'Contents/Resources' - lite_datadir = 'Contents/Resources' - install_data('resources/icons/icon.icns', install_dir : 'Contents/Resources') + lite_bindir = 'Contents' / 'MacOS' + lite_docdir = 'Contents' / 'Resources' + lite_datadir = 'Contents' / 'Resources' + conf_data.set( + 'CURRENT_YEAR', + run_command('date', '+%Y', capture: true).stdout().strip() + ) + install_data('resources' / 'icons' / 'icon.icns', install_dir : 'Contents' / 'Resources') configure_file( - input : 'resources/macos/Info.plist.in', + input : 'resources' / 'macos' / 'Info.plist.in', output : 'Info.plist', configuration : conf_data, install : true, install_dir : 'Contents' ) else + message() lite_bindir = 'bin' - lite_docdir = 'share/doc/lite-xl' - lite_datadir = 'share/lite-xl' + lite_docdir = get_option('datadir') / 'doc' / 'lite-xl' + lite_datadir = get_option('datadir') / 'lite-xl' if host_machine.system() == 'linux' - install_data('resources/icons/lite-xl.svg', - install_dir : 'share/icons/hicolor/scalable/apps' + install_data('resources' / 'icons' / 'lite-xl.svg', + install_dir : get_option('datadir') / 'icons' / 'hicolor' / 'scalable' / 'apps' ) - install_data('resources/linux/org.lite_xl.lite_xl.desktop', - install_dir : 'share/applications' + install_data('resources' / 'linux' / 'org.lite_xl.lite_xl.desktop', + install_dir : get_option('datadir') / 'applications' ) - install_data('resources/linux/org.lite_xl.lite_xl.appdata.xml', - install_dir : 'share/metainfo' + install_data('resources' / 'linux' / 'org.lite_xl.lite_xl.appdata.xml', + install_dir : get_option('datadir') / 'metainfo' ) endif endif -install_data('licenses/licenses.md', install_dir : lite_docdir) +install_data('licenses' / 'licenses.md', install_dir : lite_docdir) -install_subdir('docs/api' , install_dir : lite_datadir, strip_directory: true) -install_subdir('data/core' , install_dir : lite_datadir, exclude_files : 'start.lua') +install_subdir('docs' / 'api' , install_dir : lite_datadir, strip_directory: true) +install_subdir('data' / 'core' , install_dir : lite_datadir, exclude_files : 'start.lua') foreach data_module : ['fonts', 'plugins', 'colors'] install_subdir(join_paths('data', data_module), install_dir : lite_datadir) endforeach configure_file( - input : 'data/core/start.lua', + input : 'data' / 'core' / 'start.lua', output : 'start.lua', configuration : conf_data, install_dir : join_paths(lite_datadir, 'core'), diff --git a/scripts/README.md b/scripts/README.md index 5910931c..36e35479 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -17,13 +17,14 @@ Various scripts and configurations used to configure, build, and package Lite XL ### Utility -- **common.sh**: Common functions used by other scripts. -- **install-dependencies.sh**: Installs required applications to build, package - and run Lite XL, mainly useful for CI and documentation purpose. - Preferably not to be used in user systems. -- **fontello-config.json**: Used by the icons generator. -- **generate_header.sh**: Generates a header file for native plugin API -- **keymap-generator**: Generates a JSON file containing the keymap +- **common.sh**: Common functions used by other scripts. +- **install-dependencies.sh**: Installs required applications to build, package + and run Lite XL, mainly useful for CI and documentation purpose. + Preferably not to be used in user systems. +- **fontello-config.json**: Used by the icons generator. +- **generate_header.sh**: Generates a header file for native plugin API +- **keymap-generator**: Generates a JSON file containing the keymap +- **generate-release-notes.sh**: Generates a release note for Lite XL releases. [1]: https://github.com/dmgbuild/dmgbuild [2]: https://docs.appimage.org/ diff --git a/scripts/appimage.sh b/scripts/appimage.sh index 17551a48..7bcaaf10 100644 --- a/scripts/appimage.sh +++ b/scripts/appimage.sh @@ -85,7 +85,7 @@ fi setup_appimagetool() { if [ ! -e appimagetool ]; then - if ! wget -O appimagetool "https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-${ARCH}.AppImage" ; then + if ! wget -O appimagetool "https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-${ARCH}.AppImage" ; then echo "Could not download the appimagetool for the arch '${ARCH}'." exit 1 else @@ -94,17 +94,6 @@ setup_appimagetool() { fi } -download_appimage_apprun() { - if [ ! -e AppRun ]; then - if ! wget -O AppRun "https://github.com/AppImage/AppImageKit/releases/download/continuous/AppRun-${ARCH}" ; then - echo "Could not download AppRun for the arch '${ARCH}'." - exit 1 - else - chmod 0755 AppRun - fi - fi -} - build_litexl() { if [ -e build ]; then rm -rf build @@ -117,11 +106,12 @@ build_litexl() { echo "Build lite-xl..." sleep 1 if [[ $STATIC_BUILD == false ]]; then - meson setup --buildtype=$BUILD_TYPE --prefix=/usr ${BUILD_DIR} + meson setup --buildtype=$BUILD_TYPE --prefix=/usr -Dportable=false ${BUILD_DIR} else meson setup --wrap-mode=forcefallback \ --buildtype=$BUILD_TYPE \ --prefix=/usr \ + -Dportable=false \ ${BUILD_DIR} fi meson compile -C ${BUILD_DIR} @@ -135,11 +125,18 @@ generate_appimage() { echo "Creating LiteXL.AppDir..." DESTDIR="$(realpath LiteXL.AppDir)" meson install --skip-subprojects -C ${BUILD_DIR} - mv AppRun LiteXL.AppDir/ - # These could be symlinks but it seems they doesn't work with AppimageLauncher + cp resources/icons/lite-xl.svg LiteXL.AppDir/ cp resources/linux/org.lite_xl.lite_xl.desktop LiteXL.AppDir/ + echo "Creating AppRun..." + cat >> LiteXL.AppDir/AppRun <<- 'EOF' + #!/bin/sh + CURRENTDIR="$(dirname "$(readlink -f "$0")")" + exec "$CURRENTDIR/usr/bin/lite-xl" "$@" + EOF + chmod +x LiteXL.AppDir/AppRun + if [[ $ADDONS == true ]]; then addons_download "${BUILD_DIR}" addons_install "${BUILD_DIR}" "LiteXL.AppDir/usr/share/lite-xl" @@ -181,10 +178,9 @@ generate_appimage() { version="${version}-addons" fi - ./appimagetool --appimage-extract-and-run LiteXL.AppDir LiteXL${version}-${ARCH}.AppImage + APPIMAGE_EXTRACT_AND_RUN=1 ./appimagetool LiteXL.AppDir LiteXL${version}-${ARCH}.AppImage } setup_appimagetool -download_appimage_apprun if [[ $RUN_BUILD == true ]]; then build_litexl; fi generate_appimage $1 diff --git a/scripts/generate-release-notes.sh b/scripts/generate-release-notes.sh new file mode 100755 index 00000000..bbb42da4 --- /dev/null +++ b/scripts/generate-release-notes.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash + +if [ ! -e "src/api/api.h" ]; then + echo "Please run this script from the root directory of Lite XL."; exit 1 +fi + +show_help() { + echo + echo "Release notes generator for lite-xl releases." + echo "USE IT AT YOUR OWN RISK!" + echo + echo "Usage: $0 " + echo + echo "Available options:" + echo + echo "--version The current version used to generate release notes." + echo "--debug Debug this script." + echo "--help Show this message." + echo +} + +main() { + local version + local last_version + + for i in "$@"; do + case $i in + --debug) + set -x + shift + ;; + --help) + show_help + exit 0 + ;; + --version) + version="$2" + shift + shift + ;; + *) + # unknown option + ;; + esac + done + + if [[ -n $1 ]]; then + show_help + exit 0 + fi + + if [[ -z "$version" ]]; then + echo "error: a version must be provided" + exit 1 + fi + + # use gh cli to get the last version + read -r last_version < <(gh release list --exclude-pre-releases --limit 1 | awk 'BEGIN {FS="\t"}; {print $3}') + if [[ -z "$last_version" ]]; then + echo "error: cannot get last release git tag" + exit 1 + fi + + export RELEASE_TAG="$version" + export LAST_RELEASE_TAG="$last_version" + envsubst '$RELEASE_TAG:$LAST_RELEASE_TAG' > release-notes.md < resources/release-notes.md +} + +main "$@" diff --git a/scripts/install-dependencies.sh b/scripts/install-dependencies.sh index 00606944..1a5db49a 100644 --- a/scripts/install-dependencies.sh +++ b/scripts/install-dependencies.sh @@ -14,20 +14,16 @@ show_help() { echo echo "Available options:" echo - echo "-l --lhelper Install tools required by LHelper and doesn't" - echo " install external libraries." echo " --debug Debug this script." echo } main() { - local lhelper=false - for i in "$@"; do case $i in -s|--lhelper) - lhelper=true - shift + echo "error: support for lhelper has been deprecated" >> /dev/stderr + exit 1 ;; --debug) set -x @@ -45,27 +41,14 @@ main() { fi if [[ "$OSTYPE" == "linux"* ]]; then - if [[ $lhelper == true ]]; then - sudo apt-get install -qq ninja-build - else - sudo apt-get install -qq libfuse2 ninja-build wayland-protocols libsdl2-dev libfreetype6 - fi + sudo apt-get install -qq libfuse2 ninja-build wayland-protocols libsdl2-dev libfreetype6 desktop-file-utils pip3 install meson elif [[ "$OSTYPE" == "darwin"* ]]; then - if [[ $lhelper == true ]]; then - brew install bash md5sha1sum ninja - else - brew install bash ninja sdl2 - fi + brew install bash ninja sdl2 pip3 install meson dmgbuild elif [[ "$OSTYPE" == "msys" ]]; then - if [[ $lhelper == true ]]; then - pacman --noconfirm -S \ - ${MINGW_PACKAGE_PREFIX}-{ca-certificates,gcc,meson,ninja,ntldd,pkg-config,mesa} unzip - else - pacman --noconfirm -S \ - ${MINGW_PACKAGE_PREFIX}-{ca-certificates,gcc,meson,ninja,ntldd,pkg-config,mesa,freetype,pcre2,SDL2} unzip - fi + pacman --noconfirm -S \ + ${MINGW_PACKAGE_PREFIX}-{ca-certificates,gcc,meson,ninja,ntldd,pkg-config,mesa,freetype,pcre2,SDL2} unzip fi } diff --git a/scripts/lhelper.sh b/scripts/lhelper.sh deleted file mode 100644 index 8d7ccded..00000000 --- a/scripts/lhelper.sh +++ /dev/null @@ -1,73 +0,0 @@ -#!/bin/bash -set -e - -show_help() { - echo - echo "Usage: $0 " - echo - echo "Available options:" - echo - echo " --debug Debug this script." - echo "-h --help Show this help and exit." - echo "-p --prefix PREFIX Install directory prefix." - echo " Default: '$HOME/.local'." - echo -} - -main() { - local lhelper_prefix="$HOME/.local" - - for i in "$@"; do - case $i in - -h|--help) - show_help - exit 0 - ;; - -p|--prefix) - lhelper_prefix="$2" - echo "LHelper prefix set to: \"${lhelper_prefix}\"" - shift - shift - ;; - --debug) - set -x - shift - ;; - *) - # unknown option - ;; - esac - done - - if [[ -n $1 ]]; then show_help; exit 1; fi - - if [[ ! -f ${lhelper_prefix}/bin/lhelper ]]; then - - git clone https://github.com/franko/lhelper.git - - # FIXME: This should be set in ~/.bash_profile if not using CI - # export PATH="${HOME}/.local/bin:${PATH}" - mkdir -p "${lhelper_prefix}/bin" - pushd lhelper; bash install "${lhelper_prefix}"; popd - - if [[ "$OSTYPE" == "darwin"* ]]; then - CC=clang CXX=clang++ lhelper create build - else - lhelper create lite-xl build - fi - fi - - # Not using $(lhelper activate lite-xl) to support CI - source "$(lhelper env-source build)" - - # Help MSYS2 to find the SDL2 include and lib directories to avoid errors - # during build and linking when using lhelper. - # Francesco: not sure why this is needed. I have never observed the problem when - # building on window. - # if [[ "$OSTYPE" == "msys" ]]; then - # CFLAGS=-I${LHELPER_ENV_PREFIX}/include/SDL2 - # LDFLAGS=-L${LHELPER_ENV_PREFIX}/lib - # fi -} - -main "$@" diff --git a/scripts/make-universal-binaries.sh b/scripts/make-universal-binaries.sh index e3f2d373..76989144 100644 --- a/scripts/make-universal-binaries.sh +++ b/scripts/make-universal-binaries.sh @@ -30,4 +30,9 @@ done lipo -create -output "Lite XL.app/Contents/MacOS/lite-xl" "$WORKDIR/"*-lite-xl +# https://eclecticlight.co/2019/01/17/code-signing-for-the-concerned-3-signing-an-app/ +# https://wiki.lazarus.freepascal.org/Code_Signing_for_macOS#Big_Sur_and_later_on_Apple_M1_ARM64_processors +# codesign all the files again, hopefully this would fix signature validation +codesign --force --deep --digest-algorithm=sha1,sha256 -s - "Lite XL.app" + source scripts/appdmg.sh "$2" \ No newline at end of file diff --git a/scripts/package.sh b/scripts/package.sh index 46d6c231..d4f47fdc 100644 --- a/scripts/package.sh +++ b/scripts/package.sh @@ -270,7 +270,8 @@ main() { if [[ $bundle == true ]]; then # https://eclecticlight.co/2019/01/17/code-signing-for-the-concerned-3-signing-an-app/ - codesign --force --deep -s - "${dest_dir}" + # https://wiki.lazarus.freepascal.org/Code_Signing_for_macOS#Big_Sur_and_later_on_Apple_M1_ARM64_processors + codesign --force --deep --digest-algorithm=sha1,sha256 -s - "${dest_dir}" fi echo "Creating a compressed archive ${package_name}" diff --git a/src/api/dirmonitor.c b/src/api/dirmonitor.c index 320235a0..bb3911ec 100644 --- a/src/api/dirmonitor.c +++ b/src/api/dirmonitor.c @@ -26,7 +26,19 @@ int get_mode_dirmonitor(); static int f_check_dir_callback(int watch_id, const char* path, void* L) { - // using absolute indices from f_dirmonitor_check (2: callback, 3: error_callback) + // using absolute indices from f_dirmonitor_check (2: callback, 3: error_callback, 4: watch_id notified table) + + // Check if we already notified about this watch + lua_rawgeti(L, 4, watch_id); + bool skip = !lua_isnoneornil(L, -1); + lua_pop(L, 1); + if (skip) return 0; + + // Set watch as notified + lua_pushboolean(L, true); + lua_rawseti(L, 4, watch_id); + + // Prepare callback call lua_pushvalue(L, 2); if (path) lua_pushlstring(L, path, watch_id); @@ -117,6 +129,9 @@ static int f_dirmonitor_check(lua_State* L) { if (monitor->length < 0) lua_pushnil(L); else if (monitor->length > 0) { + // Create a table for keeping track of what watch ids were notified in this check, + // so that we avoid notifying multiple times. + lua_newtable(L); if (translate_changes_dirmonitor(monitor->internal, monitor->buffer, monitor->length, f_check_dir_callback, L) == 0) monitor->length = 0; lua_pushboolean(L, 1); diff --git a/src/api/process.c b/src/api/process.c index 00152578..50fe6e22 100644 --- a/src/api/process.c +++ b/src/api/process.c @@ -12,6 +12,7 @@ // https://stackoverflow.com/questions/60645/overlapped-i-o-on-anonymous-pipe // https://docs.microsoft.com/en-us/windows/win32/procthread/creating-a-child-process-with-redirected-input-and-output #include + #include "../utfconv.h" #else #include #include @@ -20,9 +21,8 @@ #include #include #endif -#if defined(__amigaos4__) - #include "platform/amigaos4.h" -#endif + +#include "../arena_allocator.h" #define READ_BUF_SIZE 2048 #define PROCESS_TERM_TRIES 3 @@ -354,268 +354,35 @@ static bool signal_process(process_t* proc, signal_e sig) { return true; } - -static UNUSED char *xstrdup(const char *str) { - char *result = str ? malloc(strlen(str) + 1) : NULL; - if (result) strcpy(result, str); - return result; -} - - -static int process_arglist_init(process_arglist_t *list, size_t *list_len, size_t nargs) { - *list_len = 0; -#ifdef _WIN32 - memset(*list, 0, sizeof(process_arglist_t)); -#else - *list = calloc(sizeof(char *), nargs + 1); - if (!*list) return ENOMEM; -#endif - return 0; -} - - -static int process_arglist_add(process_arglist_t *list, size_t *list_len, const char *arg, bool escape) { - size_t len = *list_len; -#ifdef _WIN32 - int arg_len; - wchar_t *cmdline = *list; - wchar_t arg_w[32767]; - // this length includes the null terminator! - if (!(arg_len = MultiByteToWideChar(CP_UTF8, 0, arg, -1, arg_w, 32767))) - return GetLastError(); - if (arg_len + len > 32767) - return ERROR_NOT_ENOUGH_MEMORY; - - if (!escape) { - // replace the current null terminator with a space - if (len > 0) cmdline[len-1] = ' '; - memcpy(cmdline + len, arg_w, arg_len * sizeof(wchar_t)); - len += arg_len; - } else { - // if the string contains spaces, then we must quote it - bool quote = wcspbrk(arg_w, L" \t\v\r\n"); - int backslash = 0, escaped_len = quote ? 2 : 0; - for (int i = 0; i < arg_len; i++) { - if (arg_w[i] == L'\\') { - backslash++; - } else if (arg_w[i] == L'"') { - escaped_len += backslash + 1; - backslash = 0; - } else { - backslash = 0; - } - escaped_len++; - } - // escape_len contains NUL terminator - if (escaped_len + len > 32767) - return ERROR_NOT_ENOUGH_MEMORY; - // replace our previous NUL terminator with space - if (len > 0) cmdline[len-1] = L' '; - if (quote) cmdline[len++] = L'"'; - // we are not going to iterate over NUL terminator - for (int i = 0;arg_w[i]; i++) { - if (arg_w[i] == L'\\') { - backslash++; - } else if (arg_w[i] == L'"') { - // add backslash + 1 backslashes - for (int j = 0; j < backslash; j++) - cmdline[len++] = L'\\'; - cmdline[len++] = L'\\'; - backslash = 0; - } else { - backslash = 0; - } - cmdline[len++] = arg_w[i]; - } - if (quote) cmdline[len++] = L'"'; - cmdline[len++] = L'\0'; - } -#else - char **cmd = *list; - cmd[len] = xstrdup(arg); - if (!cmd[len]) return ENOMEM; - len++; -#endif - *list_len = len; - return 0; -} - - -static void process_arglist_free(process_arglist_t *list) { - if (!*list) return; -#ifndef _WIN32 - char **cmd = *list; - for (int i = 0; cmd[i]; i++) - free(cmd[i]); - free(cmd); - *list = NULL; -#endif -} - - -static int process_env_init(process_env_t *env_list, size_t *env_len, size_t nenv) { - *env_len = 0; -#ifdef _WIN32 - *env_list = NULL; -#else - *env_list = calloc(sizeof(char *), nenv * 2); - if (!*env_list) return ENOMEM; -#endif - return 0; -} - - -#ifdef _WIN32 -static int cmp_name(wchar_t *a, wchar_t *b) { - wchar_t _A[32767], _B[32767], *A = _A, *B = _B, *a_eq, *b_eq; - int na, nb, r; - a_eq = wcschr(a, L'='); - b_eq = wcschr(b, L'='); - assert(a_eq); - assert(b_eq); - na = a_eq - a; - nb = b_eq - b; - r = LCMapStringW(LOCALE_INVARIANT, LCMAP_UPPERCASE, a, na, A, na); - assert(r == na); - A[na] = L'\0'; - r = LCMapStringW(LOCALE_INVARIANT, LCMAP_UPPERCASE, b, nb, B, nb); - assert(r == nb); - B[nb] = L'\0'; - - for (;;) { - wchar_t AA = *A++, BB = *B++; - if (AA > BB) - return 1; - else if (AA < BB) - return -1; - else if (!AA && !BB) - return 0; - } -} - - -static int process_env_add_variable(process_env_t *env_list, size_t *env_list_len, wchar_t *var, size_t var_len) { - wchar_t *list, *list_p; - size_t block_var_len, list_len; - list = list_p = *env_list; - list_len = *env_list_len; - if (list_len) { - // check if it is already in the block - while ((block_var_len = wcslen(list_p))) { - if (cmp_name(list_p, var) == 0) - return -1; // already installed - list_p += block_var_len + 1; - } - } - // allocate list + 1 characters for the block terminator - list = realloc(list, (list_len + var_len + 1) * sizeof(wchar_t)); - if (!list) return ERROR_NOT_ENOUGH_MEMORY; - // copy the env variable to the block - memcpy(list + list_len, var, var_len * sizeof(wchar_t)); - // terminate the block again - list[list_len + var_len] = L'\0'; - *env_list = list; - *env_list_len = (list_len + var_len); - return 0; -} - - -static int process_env_add_system(process_env_t *env_list, size_t *env_list_len) { - int retval = 0; - wchar_t *proc_env_block, *proc_env_block_p; - int proc_env_len; - - proc_env_block = proc_env_block_p = GetEnvironmentStringsW(); - while ((proc_env_len = wcslen(proc_env_block_p))) { - // try to add it to the list - if ((retval = process_env_add_variable(env_list, env_list_len, proc_env_block_p, proc_env_len + 1)) > 0) - goto cleanup; - proc_env_block_p += proc_env_len + 1; - } - retval = 0; - -cleanup: - if (proc_env_block) FreeEnvironmentStringsW(proc_env_block); - return retval; -} -#endif - - -static int process_env_add(process_env_t *env_list, size_t *env_len, const char *key, const char *value) { -#ifdef _WIN32 - wchar_t env_var[32767]; - int r, var_len = 0; - if (!(r = MultiByteToWideChar(CP_UTF8, 0, key, -1, env_var, 32767))) - return GetLastError(); - var_len += r; - env_var[var_len-1] = L'='; - if (!(r = MultiByteToWideChar(CP_UTF8, 0, value, -1, env_var + var_len, 32767 - var_len))) - return GetLastError(); - var_len += r; - return process_env_add_variable(env_list, env_len, env_var, var_len); -#else - (*env_list)[*env_len] = xstrdup(key); - if (!(*env_list)[*env_len]) - return ENOMEM; - (*env_list)[*env_len + 1] = xstrdup(value); - if (!(*env_list)[*env_len + 1]) - return ENOMEM; - *env_len += 2; -#endif - return 0; -} - - -static void process_env_free(process_env_t *list, size_t list_len) { - if (!*list) return; -#ifndef _WIN32 - for (size_t i = 0; i < list_len; i++) - free((*list)[i]); -#endif - free(*list); - *list = NULL; -} - - static int process_start(lua_State* L) { - int r, retval = 1; - size_t env_len = 0, cmd_len = 0, arglist_len = 0, env_vars_len = 0; + int retval = 1; process_t *self = NULL; - process_arglist_t arglist = PROCESS_ARGLIST_INITIALIZER; - process_env_t env_vars = NULL; - const char *cwd = NULL; - bool detach = false, escape = true; - int deadline = 10, new_fds[3] = { STDIN_FD, STDOUT_FD, STDERR_FD }; + int deadline = 10, detach = false, new_fds[3] = { STDIN_FD, STDOUT_FD, STDERR_FD }; - if (lua_isstring(L, 1)) { - escape = false; - // create a table that contains the string as the value - lua_createtable(L, 1, 0); - lua_pushvalue(L, 1); - lua_rawseti(L, -2, 1); - lua_replace(L, 1); - } +#ifdef _WIN32 + wchar_t *commandline = NULL, *env = NULL, *cwd = NULL; +#else + const char **cmd = NULL, *env = NULL, *cwd = NULL; +#endif + lua_settop(L, 3); + lxl_arena *A = lxl_arena_init(L); + // copy command line arguments +#ifdef _WIN32 + if ( !(commandline = utfconv_fromutf8(A, luaL_checkstring(L, 1))) ) + return luaL_error(L, "%s", UTFCONV_ERROR_INVALID_CONVERSION); +#else luaL_checktype(L, 1, LUA_TTABLE); - #if LUA_VERSION_NUM > 501 - lua_len(L, 1); - #else - lua_pushinteger(L, (int)lua_objlen(L, 1)); - #endif - cmd_len = luaL_checknumber(L, -1); lua_pop(L, 1); - if (!cmd_len) - return luaL_argerror(L, 1, "table cannot be empty"); - // check if each arguments is a string - for (size_t i = 1; i <= cmd_len; ++i) { - lua_rawgeti(L, 1, i); - luaL_checkstring(L, -1); - lua_pop(L, 1); + int len = luaL_len(L, 1); + cmd = lxl_arena_zero(A, (len + 1) * sizeof(char *)); + for (int i = 0; i < len; i++) { + cmd[i] = lxl_arena_strdup(A, (lua_rawgeti(L, 1, i+1), luaL_checkstring(L, -1))); } +#endif if (lua_istable(L, 2)) { lua_getfield(L, 2, "detach"); detach = lua_toboolean(L, -1); lua_getfield(L, 2, "timeout"); deadline = luaL_optnumber(L, -1, deadline); - lua_getfield(L, 2, "cwd"); cwd = luaL_optstring(L, -1, NULL); lua_getfield(L, 2, "stdin"); new_fds[STDIN_FD] = luaL_optnumber(L, -1, STDIN_FD); lua_getfield(L, 2, "stdout"); new_fds[STDOUT_FD] = luaL_optnumber(L, -1, STDOUT_FD); lua_getfield(L, 2, "stderr"); new_fds[STDERR_FD] = luaL_optnumber(L, -1, STDERR_FD); @@ -623,52 +390,42 @@ static int process_start(lua_State* L) { if (new_fds[stream] > STDERR_FD || new_fds[stream] < REDIRECT_PARENT) return luaL_error(L, "error: redirect to handles, FILE* and paths are not supported"); } - lua_pop(L, 6); // pop all the values above + lua_pop(L, 5); // pop all the values above - luaL_getsubtable(L, 2, "env"); - // count environment variobles - lua_pushnil(L); - while (lua_next(L, -2) != 0) { - luaL_checkstring(L, -2); - luaL_checkstring(L, -1); - lua_pop(L, 1); - env_len++; - } - - if (env_len) { - if ((r = process_env_init(&env_vars, &env_vars_len, env_len)) != 0) { - retval = -1; - push_error(L, "cannot allocate environment list", r); - goto cleanup; - } - - lua_pushnil(L); - while (lua_next(L, -2) != 0) { - if ((r = process_env_add(&env_vars, &env_vars_len, lua_tostring(L, -2), lua_tostring(L, -1))) != 0) { - retval = -1; - push_error(L, "cannot copy environment variable", r); - goto cleanup; - } - lua_pop(L, 1); - env_len++; +#ifdef _WIN32 + if (lua_getfield(L, 2, "env") == LUA_TFUNCTION) { + lua_newtable(L); + LPWCH system_env = GetEnvironmentStringsW(), envp = system_env; + while (wcslen(envp) > 0) { + const char *env = utfconv_fromwstr(A, envp), *eq = env ? strchr(env, '=') : NULL; + if (!env) return (FreeEnvironmentStringsW(system_env), luaL_error(L, "%s", UTFCONV_ERROR_INVALID_CONVERSION)); + if (!eq) return (FreeEnvironmentStringsW(system_env), luaL_error(L, "invalid environment variable")); + lua_pushlstring(L, env, eq - env); lua_pushstring(L, eq+1); + lxl_arena_free(A, (void *) env); + lua_rawset(L, -3); + envp += wcslen(envp) + 1; } + FreeEnvironmentStringsW(system_env); + lua_call(L, 1, 1); + size_t len = 0; const char *env_mb = luaL_checklstring(L, -1, &len); + if (!(env = utfconv_fromlutf8(A, env_mb, len))) + return luaL_error(L, "%s", UTFCONV_ERROR_INVALID_CONVERSION); } - } - - // allocate and copy commands - if ((r = process_arglist_init(&arglist, &arglist_len, cmd_len)) != 0) { - retval = -1; - push_error(L, "cannot create argument list", r); - goto cleanup; - } - for (size_t i = 1; i <= cmd_len; i++) { - lua_rawgeti(L, 1, i); - if ((r = process_arglist_add(&arglist, &arglist_len, lua_tostring(L, -1), escape)) != 0) { - retval = -1; - push_error(L, "cannot add argument", r); - goto cleanup; + if (lua_getfield(L, 2, "cwd"), luaL_optstring(L, -1, NULL)) { + if ( !(cwd = utfconv_fromutf8(A, lua_tostring(L, -1))) ) + return luaL_error(L, UTFCONV_ERROR_INVALID_CONVERSION); } - lua_pop(L, 1); + lua_pop(L, 2); +#else + if (lua_getfield(L, 2, "env") == LUA_TFUNCTION) { + lua_newtable(L); + lua_call(L, 1, 1); + size_t len = 0; env = lua_tolstring(L, -1, &len); + env = lxl_arena_copy(A, (void *) env, len+1); + } + cwd = lxl_arena_strdup(A, (lua_getfield(L, 2, "cwd"), luaL_optstring(L, -1, NULL))); + lua_pop(L, 2); +#endif } self = lua_newuserdata(L, sizeof(process_t)); @@ -677,13 +434,6 @@ static int process_start(lua_State* L) { self->deadline = deadline; self->detached = detach; #if _WIN32 - if (env_vars) { - if ((r = process_env_add_system(&env_vars, &env_vars_len)) != 0) { - retval = -1; - push_error(L, "cannot add environment variable", r); - goto cleanup; - } - } for (int i = 0; i < 3; ++i) { switch (new_fds[i]) { case REDIRECT_PARENT: @@ -742,10 +492,7 @@ static int process_start(lua_State* L) { siStartInfo.hStdInput = self->child_pipes[STDIN_FD][0]; siStartInfo.hStdOutput = self->child_pipes[STDOUT_FD][1]; siStartInfo.hStdError = self->child_pipes[STDERR_FD][1]; - wchar_t cwd_w[MAX_PATH]; - if (cwd) // TODO: error handling - MultiByteToWideChar(CP_UTF8, 0, cwd, -1, cwd_w, MAX_PATH); - if (!CreateProcessW(NULL, arglist, NULL, NULL, true, (detach ? DETACHED_PROCESS : CREATE_NO_WINDOW) | CREATE_UNICODE_ENVIRONMENT, env_vars, cwd ? cwd_w : NULL, &siStartInfo, &self->process_information)) { + if (!CreateProcessW(NULL, commandline, NULL, NULL, true, (detach ? DETACHED_PROCESS : CREATE_NO_WINDOW) | CREATE_UNICODE_ENVIRONMENT, env, cwd, &siStartInfo, &self->process_information)) { push_error(L, NULL, GetLastError()); retval = -1; goto cleanup; @@ -792,10 +539,17 @@ static int process_start(lua_State* L) { dup2(self->child_pipes[new_fds[stream]][new_fds[stream] == STDIN_FD ? 0 : 1], stream); close(self->child_pipes[stream][stream == STDIN_FD ? 1 : 0]); } - size_t set; - for (set = 0; set < env_vars_len && setenv(env_vars[set], env_vars[set+1], 1) == 0; set += 2); - if (set == env_vars_len && (!detach || setsid() != -1) && (!cwd || chdir(cwd) != -1)) - execvp(arglist[0], (char** const)arglist); + if (env) { + size_t len = 0; + while ((len = strlen(env)) != 0) { + char *value = strchr(env, '='); + *value = '\0'; value++; // change the '=' into '\0', forming 2 strings side by side + setenv(env, value, 1); + env += len+1; + } + } + if ((!detach || setsid() != -1) && (!cwd || chdir(cwd) != -1)) + execvp(cmd[0], (char** const) cmd); write(control_pipe[1], &errno, sizeof(errno)); _exit(-1); } @@ -837,8 +591,6 @@ static int process_start(lua_State* L) { } } } - process_arglist_free(&arglist); - process_env_free(&env_vars, env_vars_len); if (retval == -1) return lua_error(L); diff --git a/src/api/renderer.c b/src/api/renderer.c index 674dc244..822b8301 100644 --- a/src/api/renderer.c +++ b/src/api/renderer.c @@ -98,18 +98,24 @@ static int f_font_load(lua_State *L) { } static bool font_retrieve(lua_State* L, RenFont** fonts, int idx) { + bool is_table; memset(fonts, 0, sizeof(RenFont*)*FONT_FALLBACK_MAX); if (lua_type(L, idx) != LUA_TTABLE) { fonts[0] = *(RenFont**)luaL_checkudata(L, idx, API_TYPE_FONT); - return false; + is_table = false; + } else { + is_table = true; + int len = luaL_len(L, idx); len = len > FONT_FALLBACK_MAX ? FONT_FALLBACK_MAX : len; + for (int i = 0; i < len; i++) { + lua_rawgeti(L, idx, i+1); + fonts[i] = *(RenFont**) luaL_checkudata(L, -1, API_TYPE_FONT); + lua_pop(L, 1); + } } - int len = luaL_len(L, idx); len = len > FONT_FALLBACK_MAX ? FONT_FALLBACK_MAX : len; - for (int i = 0; i < len; i++) { - lua_rawgeti(L, idx, i+1); - fonts[i] = *(RenFont**) luaL_checkudata(L, -1, API_TYPE_FONT); - lua_pop(L, 1); - } - return true; +#ifdef LITE_USE_SDL_RENDERER + update_font_scale(&window_renderer, fonts); +#endif + return is_table; } static int f_font_copy(lua_State *L) { diff --git a/src/api/system.c b/src/api/system.c index 74f2827c..42797207 100644 --- a/src/api/system.c +++ b/src/api/system.c @@ -830,7 +830,14 @@ static int f_mkdir(lua_State *L) { static int f_get_clipboard(lua_State *L) { char *text = SDL_GetClipboardText(); if (!text) { return 0; } +#ifdef _WIN32 + // on windows, text-based clipboard formats must terminate with \r\n + // we need to convert it to \n for Lite XL to read them properly + // https://learn.microsoft.com/en-us/windows/win32/dataxchg/standard-clipboard-formats + luaL_gsub(L, text, "\r\n", "\n"); +#else lua_pushstring(L, text); +#endif SDL_free(text); return 1; } diff --git a/src/main.c b/src/main.c index 7862bae4..d7705327 100644 --- a/src/main.c +++ b/src/main.c @@ -196,8 +196,6 @@ int main(int argc, char **argv) { SDL_SetHint("SDL_MOUSE_DOUBLE_CLICK_RADIUS", "4"); #endif - SDL_SetHint(SDL_HINT_RENDER_DRIVER, "software"); - SDL_DisplayMode dm; SDL_GetCurrentDisplayMode(0, &dm); @@ -270,8 +268,6 @@ init_lua: set_macos_bundle_resources(L); #endif #endif - SDL_EventState(SDL_TEXTINPUT, SDL_ENABLE); - SDL_EventState(SDL_TEXTEDITING, SDL_ENABLE); const char *init_lite_code = \ "local core\n" diff --git a/src/meson.build b/src/meson.build index a156ae3f..aa4edd0f 100644 --- a/src/meson.build +++ b/src/meson.build @@ -5,12 +5,14 @@ lite_sources = [ 'api/system.c', 'api/process.c', 'api/utf8.c', + 'arena_allocator.c', 'renderer.c', 'renwindow.c', 'rencache.c', 'main.c', ] +lite_sources += 'api/dirmonitor.c' # dirmonitor backend if get_option('dirmonitor_backend') == '' if cc.has_function('inotify_init', prefix : '#include') @@ -19,6 +21,8 @@ if get_option('dirmonitor_backend') == '' dirmonitor_backend = 'fsevents' elif cc.has_function('kqueue', prefix : '#include') dirmonitor_backend = 'kqueue' + elif cc.has_function('create_inode_watcher', prefix : '#include') + dirmonitor_backend = 'inodewatcher' elif dependency('libkqueue', required : false).found() dirmonitor_backend = 'kqueue' elif host_machine.system() == 'windows' @@ -31,26 +35,32 @@ else dirmonitor_backend = get_option('dirmonitor_backend') endif -message('dirmonitor_backend: @0@'.format(dirmonitor_backend)) - -if dirmonitor_backend == 'kqueue' +if dirmonitor_backend == 'inotify' + lite_sources += 'api' / 'dirmonitor' / 'inotify.c' +elif dirmonitor_backend == 'fsevents' + lite_sources += 'api' / 'dirmonitor' / 'fsevents.c' +elif dirmonitor_backend == 'kqueue' + lite_sources += 'api' / 'dirmonitor' / 'kqueue.c' libkqueue_dep = dependency('libkqueue', required : false) if libkqueue_dep.found() lite_deps += libkqueue_dep endif +elif dirmonitor_backend == 'inodewatcher' + add_languages('cpp') + lite_sources += 'api' / 'dirmonitor' / 'inodewatcher.cpp' +elif dirmonitor_backend == 'win32' + lite_sources += 'api' / 'dirmonitor' / 'win32.c' +else + lite_sources += 'api' / 'dirmonitor' / 'dummy.c' endif -lite_sources += [ - 'api/dirmonitor.c', - 'api/dirmonitor/' + dirmonitor_backend + '.c', -] - +message('dirmonitor_backend: @0@'.format(dirmonitor_backend)) lite_rc = [] if host_machine.system() == 'windows' windows = import('windows') - lite_rc += windows.compile_resources('../resources/icons/icon.rc') - lite_rc += windows.compile_resources('../resources/windows/manifest.rc') + lite_rc += windows.compile_resources('..' / 'resources' / 'icons' / 'icon.rc') + lite_rc += windows.compile_resources('..' / 'resources' / 'windows' / 'manifest.rc') elif host_machine.system() == 'darwin' lite_sources += 'bundle_open.m' endif diff --git a/src/renderer.c b/src/renderer.c index cd5ec7ba..c7e63749 100644 --- a/src/renderer.c +++ b/src/renderer.c @@ -9,28 +9,23 @@ #include FT_OUTLINE_H #include FT_SYSTEM_H -#ifdef _WIN32 -#include -#include "utfconv.h" -#endif - #include "renderer.h" #include "renwindow.h" -#define MAX_UNICODE 0x100000 -#define GLYPHSET_SIZE 256 -#define MAX_LOADABLE_GLYPHSETS (MAX_UNICODE / GLYPHSET_SIZE) -#define SUBPIXEL_BITMAPS_CACHED 3 +// uncomment the line below for more debugging information through printf +// #define RENDERER_DEBUG RenWindow window_renderer = {0}; static FT_Library library; // draw_rect_surface is used as a 1x1 surface to simplify ren_draw_rect with blending -static SDL_Surface *draw_rect_surface; +static SDL_Surface *draw_rect_surface = NULL; +static FT_Library library = NULL; -static void* check_alloc(void *ptr) { +#define check_alloc(P) _check_alloc(P, __FILE__, __LINE__) +static void* _check_alloc(void *ptr, const char *const file, size_t ln) { if (!ptr) { - fprintf(stderr, "Fatal error: memory allocation failed\n"); + fprintf(stderr, "%s:%zu: memory allocation failed\n", file, ln); exit(EXIT_FAILURE); } return ptr; @@ -38,31 +33,92 @@ static void* check_alloc(void *ptr) { /************************* Fonts *************************/ +// approximate number of glyphs per atlas surface +#define GLYPHS_PER_ATLAS 256 +// some padding to add to atlas surface to store more glyphs +#define FONT_HEIGHT_OVERFLOW_PX 6 +#define FONT_WIDTH_OVERFLOW_PX 6 + +// maximum unicode codepoint supported (https://stackoverflow.com/a/52203901) +#define MAX_UNICODE 0x10FFFF +// number of rows and columns in the codepoint map +#define CHARMAP_ROW 128 +#define CHARMAP_COL (MAX_UNICODE / CHARMAP_ROW) + +// the maximum number of glyphs for OpenType +#define MAX_GLYPHS 65535 +// number of rows and columns in the glyph map +#define GLYPHMAP_ROW 128 +#define GLYPHMAP_COL (MAX_GLYPHS / GLYPHMAP_ROW) + +// number of subpixel bitmaps +#define SUBPIXEL_BITMAPS_CACHED 3 + +typedef enum { + EGlyphNone = 0, + EGlyphLoaded = (1 << 0L), + EGlyphBitmap = (1 << 1L), + // currently no-op because blits are always assumed to be dual source + EGlyphDualSource = (1 << 2L), +} ERenGlyphFlags; + +// metrics for a loaded glyph typedef struct { - unsigned int x0, x1, y0, y1, loaded; + unsigned short atlas_idx, surface_idx; + unsigned int x1, y0, y1, flags; int bitmap_left, bitmap_top; float xadvance; } GlyphMetric; +// maps codepoints -> glyph IDs typedef struct { - SDL_Surface* surface; - GlyphMetric metrics[GLYPHSET_SIZE]; -} GlyphSet; + unsigned int *rows[CHARMAP_ROW]; +} CharMap; + +// a bitmap atlas with a fixed width, each surface acting as a bump allocator +typedef struct { + SDL_Surface **surfaces; + unsigned int width, nsurface; +} GlyphAtlas; + +// maps glyph IDs -> glyph metrics +typedef struct { + // accessed with metrics[bitmap_idx][glyph_id / nrow][glyph_id - (row * ncol)] + GlyphMetric *metrics[SUBPIXEL_BITMAPS_CACHED][GLYPHMAP_ROW]; + // accessed with atlas[bitmap_idx][atlas_idx].surfaces[surface_idx] + GlyphAtlas *atlas[SUBPIXEL_BITMAPS_CACHED]; + size_t natlas, bytesize; +} GlyphMap; typedef struct RenFont { FT_Face face; - FT_StreamRec stream; - GlyphSet* sets[SUBPIXEL_BITMAPS_CACHED][MAX_LOADABLE_GLYPHSETS]; - float size, space_advance, tab_advance; - unsigned short max_height, baseline, height; + CharMap charmap; + GlyphMap glyphs; +#ifdef LITE_USE_SDL_RENDERER + int scale; +#endif + float size, space_advance; + unsigned short max_height, baseline, height, tab_size; + unsigned short underline_thickness; ERenFontAntialiasing antialiasing; ERenFontHinting hinting; unsigned char style; - unsigned short underline_thickness; char path[]; } RenFont; -static const char* utf8_to_codepoint(const char *p, unsigned *dst) { +#ifdef LITE_USE_SDL_RENDERER +void update_font_scale(RenWindow *window_renderer, RenFont **fonts) { + const int surface_scale = renwin_get_surface(window_renderer).scale; + for (int i = 0; i < FONT_FALLBACK_MAX && fonts[i]; ++i) { + if (fonts[i]->scale != surface_scale) { + ren_font_group_set_size(window_renderer, fonts, fonts[0]->size); + return; + } + } +} +#endif + +static const char* utf8_to_codepoint(const char *p, const char *endp, unsigned *dst) { const unsigned char *up = (unsigned char*)p; unsigned res, n; switch (*p & 0xf0) { @@ -72,7 +128,7 @@ static const char* utf8_to_codepoint(const char *p, unsigned *dst) { case 0xc0 : res = *up & 0x1f; n = 1; break; default : res = *up; n = 0; break; } - while (n--) { + while (up < (const unsigned char *)endp && n--) { res = (res << 6) | (*(++up) & 0x3f); } *dst = res; @@ -92,7 +148,7 @@ static int font_set_render_options(RenFont* font) { if (font->antialiasing == FONT_ANTIALIASING_SUBPIXEL) { unsigned char weights[] = { 0x10, 0x40, 0x70, 0x40, 0x10 } ; switch (font->hinting) { - case FONT_HINTING_NONE: FT_Library_SetLcdFilter(library, FT_LCD_FILTER_NONE); break; + case FONT_HINTING_NONE: FT_Library_SetLcdFilter(library, FT_LCD_FILTER_NONE); break; case FONT_HINTING_SLIGHT: case FONT_HINTING_FULL: FT_Library_SetLcdFilterWeights(library, weights); break; } @@ -120,97 +176,201 @@ static int font_set_style(FT_Outline* outline, int x_translation, unsigned char return 0; } -static void font_load_glyphset(RenFont* font, int idx) { - unsigned int render_option = font_set_render_options(font), load_option = font_set_load_options(font); - int bitmaps_cached = font->antialiasing == FONT_ANTIALIASING_SUBPIXEL ? SUBPIXEL_BITMAPS_CACHED : 1; - unsigned int byte_width = font->antialiasing == FONT_ANTIALIASING_SUBPIXEL ? 3 : 1; - for (int j = 0, pen_x = 0; j < bitmaps_cached; ++j) { - GlyphSet* set = check_alloc(calloc(1, sizeof(GlyphSet))); - font->sets[j][idx] = set; - for (int i = 0; i < GLYPHSET_SIZE; ++i) { - int glyph_index = FT_Get_Char_Index(font->face, i + idx * GLYPHSET_SIZE); - if (!glyph_index || FT_Load_Glyph(font->face, glyph_index, load_option | FT_LOAD_BITMAP_METRICS_ONLY) - || font_set_style(&font->face->glyph->outline, j * (64 / SUBPIXEL_BITMAPS_CACHED), font->style) || FT_Render_Glyph(font->face->glyph, render_option)) { - continue; - } - FT_GlyphSlot slot = font->face->glyph; - unsigned int glyph_width = slot->bitmap.width / byte_width; - if (font->antialiasing == FONT_ANTIALIASING_NONE) - glyph_width *= 8; - set->metrics[i] = (GlyphMetric){ pen_x, pen_x + glyph_width, 0, slot->bitmap.rows, true, slot->bitmap_left, slot->bitmap_top, (slot->advance.x + slot->lsb_delta - slot->rsb_delta) / 64.0f}; - pen_x += glyph_width; - font->max_height = slot->bitmap.rows > font->max_height ? slot->bitmap.rows : font->max_height; - // In order to fix issues with monospacing; we need the unhinted xadvance; as FreeType doesn't correctly report the hinted advance for spaces on monospace fonts (like RobotoMono). See #843. - if (!glyph_index || FT_Load_Glyph(font->face, glyph_index, (load_option | FT_LOAD_BITMAP_METRICS_ONLY | FT_LOAD_NO_HINTING) & ~FT_LOAD_FORCE_AUTOHINT) - || font_set_style(&font->face->glyph->outline, j * (64 / SUBPIXEL_BITMAPS_CACHED), font->style) || FT_Render_Glyph(font->face->glyph, render_option)) { - continue; - } - slot = font->face->glyph; - set->metrics[i].xadvance = slot->advance.x / 64.0f; +static unsigned int font_get_glyph_id(RenFont *font, unsigned int codepoint) { + if (codepoint > MAX_UNICODE) return 0; + size_t row = codepoint / CHARMAP_COL; + size_t col = codepoint - (row * CHARMAP_COL); + if (!font->charmap.rows[row]) font->charmap.rows[row] = check_alloc(calloc(sizeof(unsigned int), CHARMAP_COL)); + if (font->charmap.rows[row][col] == 0) { + unsigned int glyph_id = FT_Get_Char_Index(font->face, codepoint); + // use -1 as a sentinel value for "glyph not available", a bit risky, but OpenType + // uses uint16 to store glyph IDs. In theory this cannot ever be reached + font->charmap.rows[row][col] = glyph_id ? glyph_id : (unsigned int) -1; + } + return font->charmap.rows[row][col] == (unsigned int) -1 ? 0 : font->charmap.rows[row][col]; +} + +#define FONT_IS_SUBPIXEL(F) ((F)->antialiasing == FONT_ANTIALIASING_SUBPIXEL) +#define FONT_BITMAP_COUNT(F) ((F)->antialiasing == FONT_ANTIALIASING_SUBPIXEL ? SUBPIXEL_BITMAPS_CACHED : 1) + +static SDL_Surface *font_allocate_glyph_surface(RenFont *font, FT_GlyphSlot slot, int bitmap_idx, GlyphMetric *metric) { + // get an atlas with the correct width + int atlas_idx = -1; + for (int i = 0; i < font->glyphs.natlas; i++) { + if (font->glyphs.atlas[bitmap_idx][i].width >= metric->x1) { + atlas_idx = i; + break; } - if (pen_x == 0) + } + if (atlas_idx < 0) { + // create a new atlas with the correct width, for each subpixel bitmap + for (int i = 0; i < FONT_BITMAP_COUNT(font); i++) { + font->glyphs.atlas[i] = check_alloc(realloc(font->glyphs.atlas[i], sizeof(GlyphAtlas) * (font->glyphs.natlas + 1))); + font->glyphs.atlas[i][font->glyphs.natlas] = (GlyphAtlas) { + .width = metric->x1 + FONT_WIDTH_OVERFLOW_PX, .nsurface = 0, + .surfaces = NULL, + }; + } + font->glyphs.bytesize += sizeof(GlyphAtlas); + atlas_idx = font->glyphs.natlas++; + } + metric->atlas_idx = atlas_idx; + GlyphAtlas *atlas = &font->glyphs.atlas[bitmap_idx][atlas_idx]; + + // find the surface with the minimum height that can fit the glyph (limited to last 100 surfaces) + int surface_idx = -1, max_surface_idx = (int) atlas->nsurface - 100, min_waste = INT_MAX; + for (int i = atlas->nsurface - 1; i >= 0 && i > max_surface_idx; i--) { + assert(atlas->surfaces[i]->userdata); + GlyphMetric *m = (GlyphMetric *) atlas->surfaces[i]->userdata; + int new_min_waste = (int) atlas->surfaces[i]->h - (int) m->y1; + if (new_min_waste >= metric->y1 && new_min_waste < min_waste) { + surface_idx = i; + min_waste = new_min_waste; + } + } + if (surface_idx < 0) { + // allocate a new surface array, and a surface + int h = FONT_HEIGHT_OVERFLOW_PX + (double) font->face->size->metrics.height / 64.0f; + if (h <= FONT_HEIGHT_OVERFLOW_PX) h += slot->bitmap.rows; + if (h <= FONT_HEIGHT_OVERFLOW_PX) h += font->size; + atlas->surfaces = check_alloc(realloc(atlas->surfaces, sizeof(SDL_Surface *) * (atlas->nsurface + 1))); + atlas->surfaces[atlas->nsurface] = check_alloc(SDL_CreateRGBSurface( + 0, atlas->width, GLYPHS_PER_ATLAS * h, FONT_BITMAP_COUNT(font) * 8, + 0, 0, 0, 0 + )); + atlas->surfaces[atlas->nsurface]->userdata = NULL; + surface_idx = atlas->nsurface++; + font->glyphs.bytesize += (sizeof(SDL_Surface *) + sizeof(SDL_Surface) + atlas->width * GLYPHS_PER_ATLAS * h * FONT_BITMAP_COUNT(font)); + } + metric->surface_idx = surface_idx; + if (atlas->surfaces[surface_idx]->userdata) { + GlyphMetric *last_metric = (GlyphMetric *) atlas->surfaces[surface_idx]->userdata; + metric->y0 = last_metric->y1; metric->y1 += last_metric->y1; + } + atlas->surfaces[surface_idx]->userdata = (void *) metric; + return atlas->surfaces[surface_idx]; +} + +static void font_load_glyph(RenFont *font, unsigned int glyph_id) { + unsigned int render_option = font_set_render_options(font); + unsigned int load_option = font_set_load_options(font); + // load the font without hinting to fix an issue with monospaced fonts, + // because freetype doesn't report the correct LSB and RSB delta. Transformation & subpixel positioning don't affect + // the xadvance, so we can save some time by not doing this step multiple times + if (FT_Load_Glyph(font->face, glyph_id, (load_option | FT_LOAD_BITMAP_METRICS_ONLY | FT_LOAD_NO_HINTING) & ~FT_LOAD_FORCE_AUTOHINT) != 0) + return; + double unhinted_xadv = font->face->glyph->advance.x / 64.0f; + // render the glyph for all bitmap + int bitmaps = FONT_BITMAP_COUNT(font); + int row = glyph_id / GLYPHMAP_COL, col = glyph_id - (row * GLYPHMAP_COL); + for (int bitmap_idx = 0; bitmap_idx < bitmaps; bitmap_idx++) { + FT_GlyphSlot slot = font->face->glyph; + if (FT_Load_Glyph(font->face, glyph_id, load_option | FT_LOAD_BITMAP_METRICS_ONLY) != 0 + || font_set_style(&slot->outline, bitmap_idx * (64 / SUBPIXEL_BITMAPS_CACHED), font->style) != 0 + || FT_Render_Glyph(slot, render_option) != 0) + return; + + // save the metrics + if (!font->glyphs.metrics[bitmap_idx][row]) { + font->glyphs.metrics[bitmap_idx][row] = check_alloc(calloc(sizeof(GlyphMetric), GLYPHMAP_COL)); + font->glyphs.bytesize += sizeof(GlyphMetric) * GLYPHMAP_COL; + } + GlyphMetric *metric = &font->glyphs.metrics[bitmap_idx][row][col]; + metric->flags = EGlyphLoaded; + metric->xadvance = unhinted_xadv; + + // if this bitmap is empty, or has a format we don't support, just store the xadvance + if (!slot->bitmap.width || !slot->bitmap.rows || !slot->bitmap.buffer || + (slot->bitmap.pixel_mode != FT_PIXEL_MODE_MONO + && slot->bitmap.pixel_mode != FT_PIXEL_MODE_GRAY + && slot->bitmap.pixel_mode != FT_PIXEL_MODE_LCD)) continue; - set->surface = check_alloc(SDL_CreateRGBSurface(0, pen_x, font->max_height, font->antialiasing == FONT_ANTIALIASING_SUBPIXEL ? 24 : 8, 0, 0, 0, 0)); - uint8_t* pixels = set->surface->pixels; - for (int i = 0; i < GLYPHSET_SIZE; ++i) { - int glyph_index = FT_Get_Char_Index(font->face, i + idx * GLYPHSET_SIZE); - if (!glyph_index || FT_Load_Glyph(font->face, glyph_index, load_option)) - continue; - FT_GlyphSlot slot = font->face->glyph; - font_set_style(&slot->outline, (64 / bitmaps_cached) * j, font->style); - if (FT_Render_Glyph(slot, render_option)) - continue; - for (unsigned int line = 0; line < slot->bitmap.rows; ++line) { - int target_offset = set->surface->pitch * line + set->metrics[i].x0 * byte_width; - int source_offset = line * slot->bitmap.pitch; - if (font->antialiasing == FONT_ANTIALIASING_NONE) { - for (unsigned int column = 0; column < slot->bitmap.width; ++column) { - int current_source_offset = source_offset + (column / 8); - int source_pixel = slot->bitmap.buffer[current_source_offset]; - pixels[++target_offset] = ((source_pixel >> (7 - (column % 8))) & 0x1) * 0xFF; - } - } else - memcpy(&pixels[target_offset], &slot->bitmap.buffer[source_offset], slot->bitmap.width); + + unsigned int glyph_width = slot->bitmap.width / bitmaps; + // FT_PIXEL_MODE_MONO uses 1 bit per pixel packed bitmap + if (slot->bitmap.pixel_mode == FT_PIXEL_MODE_MONO) glyph_width *= 8; + + metric->x1 = glyph_width; + metric->y1 = slot->bitmap.rows; + metric->bitmap_left = slot->bitmap_left; + metric->bitmap_top = slot->bitmap_top; + metric->flags |= (EGlyphBitmap | EGlyphDualSource); + + // find the best surface to copy the glyph over, and copy it + SDL_Surface *surface = font_allocate_glyph_surface(font, slot, bitmap_idx, metric); + uint8_t* pixels = surface->pixels; + for (unsigned int line = 0; line < slot->bitmap.rows; ++line) { + int target_offset = surface->pitch * (line + metric->y0); // x0 is always assumed to be 0 + int source_offset = line * slot->bitmap.pitch; + if (font->antialiasing == FONT_ANTIALIASING_NONE) { + for (unsigned int column = 0; column < slot->bitmap.width; ++column) { + int current_source_offset = source_offset + (column / 8); + int source_pixel = slot->bitmap.buffer[current_source_offset]; + pixels[++target_offset] = ((source_pixel >> (7 - (column % 8))) & 0x1) * 0xFF; + } + } else { + memcpy(&pixels[target_offset], &slot->bitmap.buffer[source_offset], slot->bitmap.width); } } } } -static GlyphSet* font_get_glyphset(RenFont* font, unsigned int codepoint, int subpixel_idx) { - int idx = (codepoint / GLYPHSET_SIZE) % MAX_LOADABLE_GLYPHSETS; - if (!font->sets[font->antialiasing == FONT_ANTIALIASING_SUBPIXEL ? subpixel_idx : 0][idx]) - font_load_glyphset(font, idx); - return font->sets[font->antialiasing == FONT_ANTIALIASING_SUBPIXEL ? subpixel_idx : 0][idx]; +// https://en.wikipedia.org/wiki/Whitespace_character +static inline int is_whitespace(unsigned int codepoint) { + switch (codepoint) { + case 0x20: case 0x85: case 0xA0: case 0x1680: case 0x2028: case 0x2029: case 0x202F: case 0x205F: case 0x3000: return 1; + } + return (codepoint >= 0x9 && codepoint <= 0xD) || (codepoint >= 0x2000 && codepoint <= 0x200A); } -static RenFont* font_group_get_glyph(GlyphSet** set, GlyphMetric** metric, RenFont** fonts, unsigned int codepoint, int bitmap_index) { - if (!metric) { - return NULL; +static inline GlyphMetric *font_get_glyph(RenFont *font, unsigned int glyph_id, int subpixel_idx) { + int row = glyph_id / GLYPHMAP_COL, col = glyph_id - (row * GLYPHMAP_COL); + return font->glyphs.metrics[subpixel_idx][row] ? &font->glyphs.metrics[subpixel_idx][row][col] : NULL; +} + +static RenFont *font_group_get_glyph(RenFont **fonts, unsigned int codepoint, int subpixel_idx, SDL_Surface **surface, GlyphMetric **metric) { + if (subpixel_idx < 0) subpixel_idx += SUBPIXEL_BITMAPS_CACHED; + RenFont *font = NULL; + unsigned int glyph_id = 0; + for (int i = 0; i < FONT_FALLBACK_MAX && fonts[i]; i++) { + font = fonts[i]; glyph_id = font_get_glyph_id(fonts[i], codepoint); + // use the first font that has representation for the glyph ID, but for whitespaces always use the first font + if (glyph_id || is_whitespace(codepoint)) break; } - if (bitmap_index < 0) - bitmap_index += SUBPIXEL_BITMAPS_CACHED; - for (int i = 0; i < FONT_FALLBACK_MAX && fonts[i]; ++i) { - *set = font_get_glyphset(fonts[i], codepoint, bitmap_index); - *metric = &(*set)->metrics[codepoint % GLYPHSET_SIZE]; - if ((*metric)->loaded || codepoint < 0xFF) - return fonts[i]; - } - if (*metric && !(*metric)->loaded && codepoint > 0xFF && codepoint != 0x25A1) - return font_group_get_glyph(set, metric, fonts, 0x25A1, bitmap_index); - return fonts[0]; + // load the glyph if it is not loaded + subpixel_idx = FONT_IS_SUBPIXEL(font) ? subpixel_idx : 0; + GlyphMetric *m = font_get_glyph(font, glyph_id, subpixel_idx); + if (!m || !(m->flags & EGlyphLoaded)) font_load_glyph(font, glyph_id); + // if the glyph ID (possibly 0) is not available and we are not trying to load whitespace, try to load U+25A1 (box character) + if ((!m || !(m->flags & EGlyphLoaded)) && codepoint != 0x25A1 && !is_whitespace(codepoint)) + return font_group_get_glyph(fonts, 0x25A1, subpixel_idx, surface, metric); + // fetch the glyph metrics again and save it + m = font_get_glyph(font, glyph_id, subpixel_idx); + if (metric && m) *metric = m; + if (surface && m && m->flags & EGlyphBitmap) *surface = font->glyphs.atlas[subpixel_idx][m->atlas_idx].surfaces[m->surface_idx]; + return font; } static void font_clear_glyph_cache(RenFont* font) { - for (int i = 0; i < SUBPIXEL_BITMAPS_CACHED; ++i) { - for (int j = 0; j < MAX_LOADABLE_GLYPHSETS; ++j) { - if (font->sets[i][j]) { - if (font->sets[i][j]->surface) - SDL_FreeSurface(font->sets[i][j]->surface); - free(font->sets[i][j]); - font->sets[i][j] = NULL; + int bitmaps = FONT_BITMAP_COUNT(font); + for (int bitmap_idx = 0; bitmap_idx < bitmaps; bitmap_idx++) { + for (int atlas_idx = 0; atlas_idx < font->glyphs.natlas; atlas_idx++) { + GlyphAtlas *atlas = &font->glyphs.atlas[bitmap_idx][atlas_idx]; + for (int surface_idx = 0; surface_idx < atlas->nsurface; surface_idx++) { + SDL_FreeSurface(atlas->surfaces[surface_idx]); } + free(atlas->surfaces); + } + free(font->glyphs.atlas[bitmap_idx]); + font->glyphs.atlas[bitmap_idx] = NULL; + // clear glyph metric + for (int glyphmap_row = 0; glyphmap_row < GLYPHMAP_ROW; glyphmap_row++) { + free(font->glyphs.metrics[bitmap_idx][glyphmap_row]); + font->glyphs.metrics[bitmap_idx][glyphmap_row] = NULL; } } + font->glyphs.bytesize = 0; + font->glyphs.natlas = 0; } // based on https://github.com/libsdl-org/SDL_ttf/blob/2a094959055fba09f7deed6e1ffeb986188982ae/SDL_ttf.c#L1735 @@ -227,66 +387,72 @@ static unsigned long font_file_read(FT_Stream stream, unsigned long offset, unsi } static void font_file_close(FT_Stream stream) { - if (stream && stream->descriptor.pointer) { + if (stream && stream->descriptor.pointer) SDL_RWclose((SDL_RWops *) stream->descriptor.pointer); - stream->descriptor.pointer = NULL; - } + free(stream); } -RenFont* ren_font_load(RenWindow *window_renderer, const char* path, float size, ERenFontAntialiasing antialiasing, ERenFontHinting hinting, unsigned char style) { - RenFont *font = NULL; - FT_Face face = NULL; - - SDL_RWops *file = SDL_RWFromFile(path, "rb"); - if (!file) - goto rwops_failure; +static int font_set_face_metrics(RenFont *font, FT_Face face) { + FT_Error err; + if ((err = FT_Set_Pixel_Sizes(face, 0, (int) font->size)) != 0) + return err; - int len = strlen(path); - font = check_alloc(calloc(1, sizeof(RenFont) + len + 1)); - font->stream.read = font_file_read; - font->stream.close = font_file_close; - font->stream.descriptor.pointer = file; - font->stream.pos = 0; - font->stream.size = (unsigned long) SDL_RWsize(file); - - if (FT_Open_Face(library, &(FT_Open_Args){ .flags = FT_OPEN_STREAM, .stream = &font->stream }, 0, &face)) - goto failure; - - const int surface_scale = renwin_get_surface(window_renderer).scale; - if (FT_Set_Pixel_Sizes(face, 0, (int)(size*surface_scale))) - goto failure; - - strcpy(font->path, path); font->face = face; - font->size = size; - font->height = (short)((face->height / (float)face->units_per_EM) * font->size); - font->baseline = (short)((face->ascender / (float)face->units_per_EM) * font->size); - font->antialiasing = antialiasing; - font->hinting = hinting; - font->style = style; - - if(FT_IS_SCALABLE(face)) + if(FT_IS_SCALABLE(face)) { + font->height = (short)((face->height / (float)face->units_per_EM) * font->size); + font->baseline = (short)((face->ascender / (float)face->units_per_EM) * font->size); font->underline_thickness = (unsigned short)((face->underline_thickness / (float)face->units_per_EM) * font->size); + } else { + font->height = (short) font->face->size->metrics.height / 64.0f; + font->baseline = (short) font->face->size->metrics.ascender / 64.0f; + } if(!font->underline_thickness) font->underline_thickness = ceil((double) font->height / 14.0); - if (FT_Load_Char(face, ' ', font_set_load_options(font))) - goto failure; - + if ((err = FT_Load_Char(face, ' ', (font_set_load_options(font) | FT_LOAD_BITMAP_METRICS_ONLY | FT_LOAD_NO_HINTING) & ~FT_LOAD_FORCE_AUTOHINT)) != 0) + return err; font->space_advance = face->glyph->advance.x / 64.0f; - font->tab_advance = font->space_advance * 2; + return 0; +} + +RenFont* ren_font_load(RenWindow *window_renderer, const char* path, float size, ERenFontAntialiasing antialiasing, ERenFontHinting hinting, unsigned char style) { + SDL_RWops *file = NULL; RenFont *font = NULL; + FT_Face face = NULL; FT_Stream stream = NULL; + + file = SDL_RWFromFile(path, "rb"); + if (!file) return NULL; + + int len = strlen(path); + font = check_alloc(calloc(1, sizeof(RenFont) + len + 1)); + strcpy(font->path, path); + font->size = size; + font->antialiasing = antialiasing; + font->hinting = hinting; + font->style = style; + font->tab_size = 2; +#ifdef LITE_USE_SDL_RENDERER + font->scale = 1; +#endif + + stream = check_alloc(calloc(1, sizeof(FT_StreamRec))); + if (!stream) goto stream_failure; + stream->read = &font_file_read; + stream->close = &font_file_close; + stream->descriptor.pointer = file; + stream->pos = 0; + stream->size = (unsigned long) SDL_RWsize(file); + + if (FT_Open_Face(library, &(FT_Open_Args) { .flags = FT_OPEN_STREAM, .stream = stream }, 0, &face) != 0) + goto failure; + if (font_set_face_metrics(font, face) != 0) + goto failure; return font; +stream_failure: + if (file) SDL_RWclose(file); failure: - if (face) - FT_Done_Face(face); - if (font) - free(font); - return NULL; - -rwops_failure: - if (file) - SDL_RWclose(file); + if (face) FT_Done_Face(face); + if (font) free(font); return NULL; } @@ -304,25 +470,22 @@ const char* ren_font_get_path(RenFont *font) { void ren_font_free(RenFont* font) { font_clear_glyph_cache(font); + // free codepoint cache as well + for (int i = 0; i < CHARMAP_ROW; i++) { + free(font->charmap.rows[i]); + } FT_Done_Face(font->face); free(font); } void ren_font_group_set_tab_size(RenFont **fonts, int n) { - unsigned int tab_index = '\t' % GLYPHSET_SIZE; for (int j = 0; j < FONT_FALLBACK_MAX && fonts[j]; ++j) { - for (int i = 0; i < (fonts[j]->antialiasing == FONT_ANTIALIASING_SUBPIXEL ? SUBPIXEL_BITMAPS_CACHED : 1); ++i) - font_get_glyphset(fonts[j], '\t', i)->metrics[tab_index].xadvance = fonts[j]->space_advance * n; + fonts[j]->tab_size = n; } } int ren_font_group_get_tab_size(RenFont **fonts) { - unsigned int tab_index = '\t' % GLYPHSET_SIZE; - float advance = font_get_glyphset(fonts[0], '\t', 0)->metrics[tab_index].xadvance; - if (fonts[0]->space_advance) { - advance /= fonts[0]->space_advance; - } - return advance; + return fonts[0]->tab_size; } float ren_font_group_get_size(RenFont **fonts) { @@ -333,14 +496,12 @@ void ren_font_group_set_size(RenWindow *window_renderer, RenFont **fonts, float const int surface_scale = renwin_get_surface(window_renderer).scale; for (int i = 0; i < FONT_FALLBACK_MAX && fonts[i]; ++i) { font_clear_glyph_cache(fonts[i]); - FT_Face face = fonts[i]->face; - FT_Set_Pixel_Sizes(face, 0, (int)(size*surface_scale)); fonts[i]->size = size; - fonts[i]->height = (short)((face->height / (float)face->units_per_EM) * size); - fonts[i]->baseline = (short)((face->ascender / (float)face->units_per_EM) * size); - FT_Load_Char(face, ' ', font_set_load_options(fonts[i])); - fonts[i]->space_advance = face->glyph->advance.x / 64.0f; - fonts[i]->tab_advance = fonts[i]->space_advance * 2; + fonts[i]->tab_size = 2; + #ifdef LITE_USE_SDL_RENDERER + fonts[i]->scale = surface_scale; + #endif + font_set_face_metrics(fonts[i], fonts[i]->face); } } @@ -348,30 +509,51 @@ int ren_font_group_get_height(RenFont **fonts) { return fonts[0]->height; } +// some fonts provide xadvance for whitespaces (e.g. Unifont), which we need to ignore +#define FONT_GET_XADVANCE(F, C, M) (is_whitespace((C)) || !(M) || !(M)->xadvance \ + ? (F)->space_advance * (float) ((C) == '\t' ? (F)->tab_size : 1) \ + : (M)->xadvance) + double ren_font_group_get_width(RenWindow *window_renderer, RenFont **fonts, const char *text, size_t len, int *x_offset) { double width = 0; const char* end = text + len; - GlyphMetric* metric = NULL; GlyphSet* set = NULL; + bool set_x_offset = x_offset == NULL; while (text < end) { unsigned int codepoint; - text = utf8_to_codepoint(text, &codepoint); - RenFont* font = font_group_get_glyph(&set, &metric, fonts, codepoint, 0); - if (!metric) - break; - width += (!font || metric->xadvance) ? metric->xadvance : fonts[0]->space_advance; - if (!set_x_offset) { + text = utf8_to_codepoint(text, end, &codepoint); + GlyphMetric *metric = NULL; + font_group_get_glyph(fonts, codepoint, 0, NULL, &metric); + width += FONT_GET_XADVANCE(fonts[0], codepoint, metric); + if (!set_x_offset && metric) { set_x_offset = true; *x_offset = metric->bitmap_left; // TODO: should this be scaled by the surface scale? } } const int surface_scale = renwin_get_surface(window_renderer).scale; - if (!set_x_offset) { + if (!set_x_offset) *x_offset = 0; - } return width / surface_scale; } +#ifdef RENDERER_DEBUG +// this function can be used to debug font atlases, it is not public +void ren_font_dump(RenFont *font) { + char filename[1024]; + int bitmaps = FONT_BITMAP_COUNT(font); + for (int bitmap_idx = 0; bitmap_idx < bitmaps; bitmap_idx++) { + for (int atlas_idx = 0; atlas_idx < font->glyphs.natlas; atlas_idx++) { + GlyphAtlas *atlas = &font->glyphs.atlas[bitmap_idx][atlas_idx]; + for (int surface_idx = 0; surface_idx < atlas->nsurface; surface_idx++) { + snprintf(filename, 1024, "%s-%d-%d-%d.bmp", font->face->family_name, bitmap_idx, atlas_idx, surface_idx); + SDL_SaveBMP(atlas->surfaces[surface_idx], filename); + } + } + } + fprintf(stderr, "%s: %zu bytes\n", font->face->family_name, font->glyphs.bytesize); +} +#endif + double ren_draw_text(RenSurface *rs, RenFont **fonts, const char *text, size_t len, float x, int y, RenColor color) { SDL_Surface *surface = rs->surface; SDL_Rect clip; @@ -380,7 +562,6 @@ double ren_draw_text(RenSurface *rs, RenFont **fonts, const char *text, size_t l const int surface_scale = rs->scale; double pen_x = x * surface_scale; y *= surface_scale; - int bytes_per_pixel = surface->format->BytesPerPixel; const char* end = text + len; uint8_t* destination_pixels = surface->pixels; int clip_end_x = clip.x + clip.w, clip_end_y = clip.y + clip.h; @@ -392,20 +573,20 @@ double ren_draw_text(RenSurface *rs, RenFont **fonts, const char *text, size_t l while (text < end) { unsigned int codepoint, r, g, b; - text = utf8_to_codepoint(text, &codepoint); - GlyphSet* set = NULL; GlyphMetric* metric = NULL; - RenFont* font = font_group_get_glyph(&set, &metric, fonts, codepoint, (int)(fmod(pen_x, 1.0) * SUBPIXEL_BITMAPS_CACHED)); + text = utf8_to_codepoint(text, end, &codepoint); + SDL_Surface *font_surface = NULL; GlyphMetric *metric = NULL; + RenFont* font = font_group_get_glyph(fonts, codepoint, (int)(fmod(pen_x, 1.0) * SUBPIXEL_BITMAPS_CACHED), &font_surface, &metric); if (!metric) break; int start_x = floor(pen_x) + metric->bitmap_left; - int end_x = (metric->x1 - metric->x0) + start_x; - int glyph_end = metric->x1, glyph_start = metric->x0; - if (!metric->loaded && codepoint > 0xFF) + int end_x = metric->x1 + start_x; // x0 is assumed to be 0 + int glyph_end = metric->x1, glyph_start = 0; + if (!font_surface && !is_whitespace(codepoint)) ren_draw_rect(rs, (RenRect){ start_x + 1, y, font->space_advance - 1, ren_font_group_get_height(fonts) }, color); - if (set->surface && color.a > 0 && end_x >= clip.x && start_x < clip_end_x) { - uint8_t* source_pixels = set->surface->pixels; + if (!is_whitespace(codepoint) && font_surface && color.a > 0 && end_x >= clip.x && start_x < clip_end_x) { + uint8_t* source_pixels = font_surface->pixels; for (int line = metric->y0; line < metric->y1; ++line) { - int target_y = line + y - metric->bitmap_top + fonts[0]->baseline * surface_scale; + int target_y = line - metric->y0 + y - metric->bitmap_top + (fonts[0]->baseline * surface_scale); if (target_y < clip.y) continue; if (target_y >= clip_end_y) @@ -417,8 +598,8 @@ double ren_draw_text(RenSurface *rs, RenFont **fonts, const char *text, size_t l start_x += offset; glyph_start += offset; } - uint32_t* destination_pixel = (uint32_t*)&(destination_pixels[surface->pitch * target_y + start_x * bytes_per_pixel]); - uint8_t* source_pixel = &source_pixels[line * set->surface->pitch + glyph_start * (font->antialiasing == FONT_ANTIALIASING_SUBPIXEL ? 3 : 1)]; + uint32_t* destination_pixel = (uint32_t*)&(destination_pixels[surface->pitch * target_y + start_x * surface->format->BytesPerPixel ]); + uint8_t* source_pixel = &source_pixels[line * font_surface->pitch + glyph_start * font_surface->format->BytesPerPixel]; for (int x = glyph_start; x < glyph_end; ++x) { uint32_t destination_color = *destination_pixel; // the standard way of doing this would be SDL_GetRGBA, but that introduces a performance regression. needs to be investigated @@ -441,12 +622,12 @@ double ren_draw_text(RenSurface *rs, RenFont **fonts, const char *text, size_t l g = (color.g * src.g * color.a + dst.g * (65025 - src.g * color.a) + 32767) / 65025; b = (color.b * src.b * color.a + dst.b * (65025 - src.b * color.a) + 32767) / 65025; // the standard way of doing this would be SDL_GetRGBA, but that introduces a performance regression. needs to be investigated - *destination_pixel++ = dst.a << surface->format->Ashift | r << surface->format->Rshift | g << surface->format->Gshift | b << surface->format->Bshift; + *destination_pixel++ = (unsigned int) dst.a << surface->format->Ashift | r << surface->format->Rshift | g << surface->format->Gshift | b << surface->format->Bshift; } } } - float adv = metric->xadvance ? metric->xadvance : font->space_advance; + float adv = FONT_GET_XADVANCE(fonts[0], codepoint, metric); if(!last) last = font; else if(font != last || text == end) { diff --git a/src/renderer.h b/src/renderer.h index 1c865c8f..194df86f 100644 --- a/src/renderer.h +++ b/src/renderer.h @@ -33,6 +33,9 @@ int ren_font_group_get_tab_size(RenFont **font); int ren_font_group_get_height(RenFont **font); float ren_font_group_get_size(RenFont **font); void ren_font_group_set_size(RenWindow *window_renderer, RenFont **font, float size); +#ifdef LITE_USE_SDL_RENDERER +void update_font_scale(RenWindow *window_renderer, RenFont **fonts); +#endif void ren_font_group_set_tab_size(RenFont **font, int n); double ren_font_group_get_width(RenWindow *window_renderer, RenFont **font, const char *text, size_t len, int *x_offset); double ren_draw_text(RenSurface *rs, RenFont **font, const char *text, size_t len, float x, int y, RenColor color); diff --git a/src/renwindow.c b/src/renwindow.c index 2ec282d0..d235896a 100644 --- a/src/renwindow.c +++ b/src/renwindow.c @@ -95,12 +95,15 @@ RenSurface renwin_get_surface(RenWindow *ren) { #endif } -void renwin_resize_surface(UNUSED RenWindow *ren) { +void renwin_resize_surface(RenWindow *ren) { #ifdef LITE_USE_SDL_RENDERER - int new_w, new_h; + int new_w, new_h, new_scale; SDL_GL_GetDrawableSize(ren->window, &new_w, &new_h); + new_scale = query_surface_scale(ren); /* Note that (w, h) may differ from (new_w, new_h) on retina displays. */ - if (new_w != ren->rensurface.surface->w || new_h != ren->rensurface.surface->h) { + if (new_scale != ren->rensurface.scale || + new_w != ren->rensurface.surface->w || + new_h != ren->rensurface.surface->h) { renwin_init_surface(ren); renwin_clip_to_surface(ren); setup_renderer(ren, new_w, new_h); @@ -141,11 +144,11 @@ void renwin_update_rects(RenWindow *ren, RenRect *rects, int count) { } void renwin_free(RenWindow *ren) { - SDL_DestroyWindow(ren->window); - ren->window = NULL; #ifdef LITE_USE_SDL_RENDERER SDL_DestroyTexture(ren->texture); SDL_DestroyRenderer(ren->renderer); SDL_FreeSurface(ren->rensurface.surface); #endif + SDL_DestroyWindow(ren->window); + ren->window = NULL; } diff --git a/src/utfconv.h b/src/utfconv.h index 59f98e4a..d5d9092e 100644 --- a/src/utfconv.h +++ b/src/utfconv.h @@ -12,8 +12,34 @@ #include #include +#include "arena_allocator.h" + #define UTFCONV_ERROR_INVALID_CONVERSION "Input contains invalid byte sequences." +#define utfconv_fromutf8(A, str) utfconv_fromlutf8(A, str, -1) + +static UNUSED LPWSTR utfconv_fromlutf8(lxl_arena *A, const char *str, int len) { + int output_len = MultiByteToWideChar(CP_UTF8, 0, str, len, NULL, 0); + if (!output_len) return NULL; + LPWSTR output = lxl_arena_malloc(A, sizeof(WCHAR) * output_len); + if (!output) return NULL; + output_len = MultiByteToWideChar(CP_UTF8, 0, str, len, output, output_len); + if (!output_len) return (lxl_arena_free(A, output), NULL); + return output; +} + +#define utfconv_fromwstr(A, str) utfconv_fromlwstr(A, str, -1) + +static UNUSED const char *utfconv_fromlwstr(lxl_arena *A, LPWSTR str, int len) { + int output_len = WideCharToMultiByte(CP_UTF8, 0, str, len, NULL, 0, NULL, NULL); + if (!output_len) return NULL; + char *output = lxl_arena_malloc(A, sizeof(char) * output_len); + if (!output) return NULL; + output_len = WideCharToMultiByte(CP_UTF8, 0, str, len, output, output_len, NULL, NULL); + if (!output_len) return (lxl_arena_free(A, output), NULL); + return (const char *) output; +} + static UNUSED LPWSTR utfconv_utf8towc(const char *str) { LPWSTR output; int len; diff --git a/subprojects/pcre2.wrap b/subprojects/pcre2.wrap index 6bfef6f3..7e184472 100644 --- a/subprojects/pcre2.wrap +++ b/subprojects/pcre2.wrap @@ -1,13 +1,13 @@ [wrap-file] -directory = pcre2-10.42 -source_url = https://github.com/PCRE2Project/pcre2/releases/download/pcre2-10.42/pcre2-10.42.tar.bz2 -source_filename = pcre2-10.42.tar.bz2 -source_hash = 8d36cd8cb6ea2a4c2bb358ff6411b0c788633a2a45dabbf1aeb4b701d1b5e840 -patch_filename = pcre2_10.42-5_patch.zip -patch_url = https://wrapdb.mesonbuild.com/v2/pcre2_10.42-5/get_patch -patch_hash = 7ba1730a3786c46f41735658a9884b09bc592af3840716e0ccc552e7ddf5630c -source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/pcre2_10.42-5/pcre2-10.42.tar.bz2 -wrapdb_version = 10.42-5 +directory = pcre2-10.44 +source_url = https://github.com/PCRE2Project/pcre2/releases/download/pcre2-10.44/pcre2-10.44.tar.bz2 +source_filename = pcre2-10.44.tar.bz2 +source_hash = d34f02e113cf7193a1ebf2770d3ac527088d485d4e047ed10e5d217c6ef5de96 +patch_filename = pcre2_10.44-2_patch.zip +patch_url = https://wrapdb.mesonbuild.com/v2/pcre2_10.44-2/get_patch +patch_hash = 4336d422ee9043847e5e10dbbbd01940d4c9e5027f31ccdc33a7898a1ca94009 +source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/pcre2_10.44-2/pcre2-10.44.tar.bz2 +wrapdb_version = 10.44-2 [provide] libpcre2-8 = libpcre2_8 diff --git a/subprojects/sdl2.wrap b/subprojects/sdl2.wrap index 14c18533..ae308d73 100644 --- a/subprojects/sdl2.wrap +++ b/subprojects/sdl2.wrap @@ -1,13 +1,13 @@ [wrap-file] -directory = SDL2-2.28.1 -source_url = https://github.com/libsdl-org/SDL/releases/download/release-2.28.1/SDL2-2.28.1.tar.gz -source_filename = SDL2-2.28.1.tar.gz -source_hash = 4977ceba5c0054dbe6c2f114641aced43ce3bf2b41ea64b6a372d6ba129cb15d -patch_filename = sdl2_2.28.1-2_patch.zip -patch_url = https://wrapdb.mesonbuild.com/v2/sdl2_2.28.1-2/get_patch -patch_hash = 2dd332226ba2a4373c6d4eb29fa915e9d5414cf7bb9fa2e4a5ef3b16a06e2736 -source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/sdl2_2.28.1-2/SDL2-2.28.1.tar.gz -wrapdb_version = 2.28.1-2 +directory = SDL2-2.30.3 +source_url = https://github.com/libsdl-org/SDL/releases/download/release-2.30.3/SDL2-2.30.3.tar.gz +source_filename = SDL2-2.30.3.tar.gz +source_hash = 820440072f8f5b50188c1dae104f2ad25984de268785be40c41a099a510f0aec +patch_filename = sdl2_2.30.3-2_patch.zip +patch_url = https://wrapdb.mesonbuild.com/v2/sdl2_2.30.3-2/get_patch +patch_hash = 2c08bde67b3896db88e01481c379322625ea6c928cdb68ead91d0e3749863bc2 +source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/sdl2_2.30.3-2/SDL2-2.30.3.tar.gz +wrapdb_version = 2.30.3-2 [provide] sdl2 = sdl2_dep