mirror of
https://github.com/ZoiteChat/zoitechat.git
synced 2026-10-16 02:07:13 +00:00
Compare commits
56
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4302a30647 | ||
|
|
1ae2d41378 | ||
|
|
070603b0fe | ||
|
|
8f47b5cedf | ||
|
|
646ad029f0 | ||
|
|
d47963247d | ||
|
|
4f7add046c | ||
|
|
61ec9af5e3 | ||
|
|
412dc92b10 | ||
|
|
04dab3dcd6 | ||
|
|
c51f885253 | ||
|
|
aa7e20fee9 | ||
|
|
480b99b1e7 | ||
|
|
0a4367b1da | ||
|
|
39daca9d22 | ||
|
|
d614e25792 | ||
|
|
30209d0c99 | ||
|
|
e1c44feb01 | ||
|
|
5af8f4ede1 | ||
|
|
e11604fd62 | ||
|
|
a2e8d9087e | ||
|
|
edf268dc0f | ||
|
|
e03ea59f3c | ||
|
|
f0b2840668 | ||
|
|
4f2fc49475 | ||
|
|
2e4155bbfc | ||
|
|
ca2456cbc6 | ||
|
|
222b4868f5 | ||
|
|
2cf1f7e833 | ||
|
|
776d24695e | ||
|
|
64a9c6e5d7 | ||
|
|
66196013bc | ||
|
|
61ff308c9f | ||
|
|
2672f9b087 | ||
|
|
4315a75a12 | ||
|
|
15905bebb1 | ||
|
|
fc4a7454ad | ||
|
|
87410aea50 | ||
|
|
de98d00a1c | ||
|
|
e94ffd9d79 | ||
|
|
d7aebb784d | ||
|
|
20b8cb939d | ||
|
|
ff4217c3d1 | ||
|
|
4fb72d30d8 | ||
|
|
d2b8fc0bda | ||
|
|
951181628a | ||
|
|
ccee274847 | ||
|
|
967f6728ef | ||
|
|
bf11ffb626 | ||
|
|
c12cabbe4e | ||
|
|
e997594db9 | ||
|
|
6f5e3b395f | ||
|
|
8bf2caf20d | ||
|
|
040021ee0e | ||
|
|
5018a3a848 | ||
|
|
df1c74a91d |
@@ -30,13 +30,13 @@ jobs:
|
|||||||
gettext \
|
gettext \
|
||||||
libcanberra-dev libglib2.0-dev \
|
libcanberra-dev libglib2.0-dev \
|
||||||
libarchive-dev \
|
libarchive-dev \
|
||||||
libgtk-3-dev \
|
libgtk-3-dev libfontconfig1-dev libpango1.0-dev xvfb xauth dbus-daemon at-spi2-core \
|
||||||
libwayland-client0 libwayland-cursor0 libwayland-egl1 \
|
libwayland-client0 libwayland-cursor0 libwayland-egl1 \
|
||||||
libxkbcommon0 \
|
libxkbcommon0 \
|
||||||
libgtk-3-bin libglib2.0-bin shared-mime-info gsettings-desktop-schemas \
|
libgtk-3-bin libglib2.0-bin glib-networking shared-mime-info gsettings-desktop-schemas \
|
||||||
liblua5.4-dev libpci-dev libperl-dev libssl-dev libayatana-appindicator3-dev \
|
liblua5.4-dev libpci-dev libperl-dev libssl-dev libayatana-appindicator3-dev \
|
||||||
perl python3 python3-minimal python3-dev python3-cffi mono-devel desktop-file-utils \
|
perl python3 python3-minimal python3-dev python3-cffi mono-devel desktop-file-utils \
|
||||||
fonts-noto-color-emoji breeze-gtk-theme \
|
breeze-gtk-theme \
|
||||||
patchelf file curl
|
patchelf file curl
|
||||||
|
|
||||||
- name: Configure
|
- name: Configure
|
||||||
@@ -56,6 +56,9 @@ jobs:
|
|||||||
set -eux
|
set -eux
|
||||||
ninja -C build
|
ninja -C build
|
||||||
|
|
||||||
|
- name: Test emoji catalog, font coverage and picker
|
||||||
|
run: xvfb-run -a dbus-run-session -- meson test -C build --print-errorlogs "Emoji Data Tests" "Emoji Font Tests" "Emoji UI Tests"
|
||||||
|
|
||||||
- name: Install to AppDir
|
- name: Install to AppDir
|
||||||
run: |
|
run: |
|
||||||
set -eux
|
set -eux
|
||||||
@@ -119,15 +122,8 @@ jobs:
|
|||||||
cp -a /usr/lib/x86_64-linux-gnu/libpython3*.so* AppDir/usr/lib/x86_64-linux-gnu/
|
cp -a /usr/lib/x86_64-linux-gnu/libpython3*.so* AppDir/usr/lib/x86_64-linux-gnu/
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -d "/usr/share/gtk-3.0/emoji" ]; then
|
# Every package ships the pinned font, including portable builds.
|
||||||
install -d AppDir/usr/share/gtk-3.0
|
cmp data/fonts/NotoColorEmoji.ttf AppDir/usr/share/fonts/zoitechat/NotoColorEmoji.ttf
|
||||||
cp -a /usr/share/gtk-3.0/emoji AppDir/usr/share/gtk-3.0/
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -f "/usr/share/fonts/truetype/noto/NotoColorEmoji.ttf" ]; then
|
|
||||||
install -d AppDir/usr/share/fonts/truetype/noto
|
|
||||||
cp -a /usr/share/fonts/truetype/noto/NotoColorEmoji.ttf AppDir/usr/share/fonts/truetype/noto/
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -d "/usr/lib/x86_64-linux-gnu/gtk-3.0/modules" ]; then
|
if [ -d "/usr/lib/x86_64-linux-gnu/gtk-3.0/modules" ]; then
|
||||||
install -d AppDir/usr/lib/x86_64-linux-gnu/gtk-3.0
|
install -d AppDir/usr/lib/x86_64-linux-gnu/gtk-3.0
|
||||||
@@ -139,6 +135,20 @@ jobs:
|
|||||||
cp -a /usr/lib/gtk-3.0/modules AppDir/usr/lib/gtk-3.0/
|
cp -a /usr/lib/gtk-3.0/modules AppDir/usr/lib/gtk-3.0/
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# HTTPS inline-image downloads use GSocketClient TLS on Linux.
|
||||||
|
# GLib provides the API, but TLS and system-proxy support live in
|
||||||
|
# loadable GIO modules shipped by glib-networking. Bundle those
|
||||||
|
# modules so the AppImage does not depend on the host copy.
|
||||||
|
gio_module_dir="$(pkg-config --variable=giomoduledir gio-2.0)"
|
||||||
|
|
||||||
|
test -n "$gio_module_dir"
|
||||||
|
test -f "${gio_module_dir}/libgiognutls.so"
|
||||||
|
install -d "AppDir${gio_module_dir}"
|
||||||
|
cp -a "${gio_module_dir}"/libgio*.so "AppDir${gio_module_dir}/"
|
||||||
|
gio-querymodules "AppDir${gio_module_dir}"
|
||||||
|
test -f "AppDir${gio_module_dir}/libgiognutls.so"
|
||||||
|
test -s "AppDir${gio_module_dir}/giomodule.cache"
|
||||||
|
|
||||||
- name: Verify bundled plugins
|
- name: Verify bundled plugins
|
||||||
run: |
|
run: |
|
||||||
set -eux
|
set -eux
|
||||||
@@ -192,14 +202,6 @@ jobs:
|
|||||||
export GTK_PATH="$gtk_path_entries${GTK_PATH:+:$GTK_PATH}"
|
export GTK_PATH="$gtk_path_entries${GTK_PATH:+:$GTK_PATH}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -d "$APPDIR/etc/fonts" ]; then
|
|
||||||
export FONTCONFIG_SYSROOT="$APPDIR"
|
|
||||||
export FONTCONFIG_PATH="$APPDIR/etc/fonts${FONTCONFIG_PATH:+:$FONTCONFIG_PATH}"
|
|
||||||
if [ -f "$APPDIR/etc/fonts/fonts.conf" ]; then
|
|
||||||
export FONTCONFIG_FILE="$APPDIR/etc/fonts/fonts.conf"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -d "$APPDIR/usr/lib/x86_64-linux-gnu/zoitechat/plugins" ]; then
|
if [ -d "$APPDIR/usr/lib/x86_64-linux-gnu/zoitechat/plugins" ]; then
|
||||||
export ZOITECHAT_LIBDIR="$APPDIR/usr/lib/x86_64-linux-gnu/zoitechat/plugins"
|
export ZOITECHAT_LIBDIR="$APPDIR/usr/lib/x86_64-linux-gnu/zoitechat/plugins"
|
||||||
elif [ -d "$APPDIR/usr/lib/zoitechat/plugins" ]; then
|
elif [ -d "$APPDIR/usr/lib/zoitechat/plugins" ]; then
|
||||||
@@ -258,6 +260,9 @@ jobs:
|
|||||||
export PYTHONPATH="$pythonpath_entries${PYTHONPATH:+:$PYTHONPATH}"
|
export PYTHONPATH="$pythonpath_entries${PYTHONPATH:+:$PYTHONPATH}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Register the private font without hiding the host's normal fonts.
|
||||||
|
export ZOITECHAT_EMOJI_FONT="${ZOITECHAT_EMOJI_FONT:-$APPDIR/usr/share/fonts/zoitechat/NotoColorEmoji.ttf}"
|
||||||
|
|
||||||
# OpenSSL trust store override
|
# OpenSSL trust store override
|
||||||
export SSL_CERT_FILE="${SSL_CERT_FILE:-$APPDIR/etc/ssl/certs/ca-certificates.crt}"
|
export SSL_CERT_FILE="${SSL_CERT_FILE:-$APPDIR/etc/ssl/certs/ca-certificates.crt}"
|
||||||
export SSL_CERT_DIR="${SSL_CERT_DIR:-$APPDIR/etc/ssl/certs}"
|
export SSL_CERT_DIR="${SSL_CERT_DIR:-$APPDIR/etc/ssl/certs}"
|
||||||
|
|||||||
@@ -0,0 +1,154 @@
|
|||||||
|
name: macOS build
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
sign_and_notarize:
|
||||||
|
description: 'Sign with Developer ID and notarize using release credentials'
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
push:
|
||||||
|
branches: [master]
|
||||||
|
tags: ['v*']
|
||||||
|
paths:
|
||||||
|
- 'src/**'
|
||||||
|
- 'plugins/**'
|
||||||
|
- 'data/**'
|
||||||
|
- 'tools/**'
|
||||||
|
- 'po/**'
|
||||||
|
- 'meson*'
|
||||||
|
- 'VERSION'
|
||||||
|
- 'packaging/macos/**'
|
||||||
|
- '.github/workflows/macos-build.yml'
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- 'src/**'
|
||||||
|
- 'plugins/**'
|
||||||
|
- 'data/**'
|
||||||
|
- 'tools/**'
|
||||||
|
- 'po/**'
|
||||||
|
- 'meson*'
|
||||||
|
- 'VERSION'
|
||||||
|
- 'packaging/macos/**'
|
||||||
|
- '.github/workflows/macos-build.yml'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
if: >-
|
||||||
|
github.event_name == 'pull_request' ||
|
||||||
|
(github.event_name == 'push' && !startsWith(github.ref, 'refs/tags/')) ||
|
||||||
|
(github.event_name == 'workflow_dispatch' && !inputs.sign_and_notarize)
|
||||||
|
name: Native ${{ matrix.arch }} bundle
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- runner: macos-15
|
||||||
|
arch: arm64
|
||||||
|
- runner: macos-15-intel
|
||||||
|
arch: x86_64
|
||||||
|
runs-on: ${{ matrix.runner }}
|
||||||
|
timeout-minutes: 90
|
||||||
|
env:
|
||||||
|
HOMEBREW_NO_AUTO_UPDATE: '1'
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
|
||||||
|
- name: Install native dependencies
|
||||||
|
run: brew bundle --file packaging/macos/Brewfile
|
||||||
|
- name: Test packaging logic
|
||||||
|
run: python3 -m unittest discover -s packaging/macos/tests -v
|
||||||
|
- name: Build and test application
|
||||||
|
run: python3 packaging/macos/build.py --build-number '${{ github.run_number }}'
|
||||||
|
- name: Verify relocated startup
|
||||||
|
run: python3 packaging/macos/smoke.py dist/ZoiteChat.app
|
||||||
|
- name: Create artifact preserving the application signature
|
||||||
|
run: ditto -c -k --keepParent dist/ZoiteChat.app dist/ZoiteChat-${{ matrix.arch }}.zip
|
||||||
|
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7
|
||||||
|
with:
|
||||||
|
name: ZoiteChat-macos-${{ matrix.arch }}
|
||||||
|
path: dist/ZoiteChat-${{ matrix.arch }}.zip
|
||||||
|
if-no-files-found: error
|
||||||
|
- name: Upload build logs on failure
|
||||||
|
if: failure()
|
||||||
|
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7
|
||||||
|
with:
|
||||||
|
name: ZoiteChat-macos-${{ matrix.arch }}-logs
|
||||||
|
path: build-macos/meson-logs/
|
||||||
|
if-no-files-found: ignore
|
||||||
|
|
||||||
|
release:
|
||||||
|
name: Signed and notarized ${{ matrix.arch }} release
|
||||||
|
if: >-
|
||||||
|
(github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')) ||
|
||||||
|
(github.event_name == 'workflow_dispatch' && inputs.sign_and_notarize && github.ref == 'refs/heads/master')
|
||||||
|
environment: macos-release
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- runner: macos-15
|
||||||
|
arch: arm64
|
||||||
|
- runner: macos-15-intel
|
||||||
|
arch: x86_64
|
||||||
|
runs-on: ${{ matrix.runner }}
|
||||||
|
timeout-minutes: 180
|
||||||
|
env:
|
||||||
|
HOMEBREW_NO_AUTO_UPDATE: '1'
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
- name: Validate release settings before building
|
||||||
|
env:
|
||||||
|
MACOS_CERTIFICATE_P12_BASE64: ${{ secrets.MACOS_CERTIFICATE_P12_BASE64 }}
|
||||||
|
MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
|
||||||
|
MACOS_SIGNING_IDENTITY: ${{ vars.MACOS_SIGNING_IDENTITY || secrets.MACOS_SIGNING_IDENTITY }}
|
||||||
|
APPLE_API_KEY_P8: ${{ secrets.APPLE_API_KEY_P8 }}
|
||||||
|
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
|
||||||
|
APPLE_API_ISSUER_ID: ${{ secrets.APPLE_API_ISSUER_ID }}
|
||||||
|
run: python3 packaging/macos/release.py --check-config
|
||||||
|
- name: Install native dependencies
|
||||||
|
run: brew bundle --file packaging/macos/Brewfile
|
||||||
|
- name: Test packaging and release logic
|
||||||
|
run: python3 -m unittest discover -s packaging/macos/tests -v
|
||||||
|
- name: Build and test application
|
||||||
|
run: python3 packaging/macos/build.py --build-number '${{ github.run_number }}'
|
||||||
|
- name: Verify relocated startup
|
||||||
|
run: python3 packaging/macos/smoke.py dist/ZoiteChat.app
|
||||||
|
- name: Sign, notarize, staple and verify release
|
||||||
|
env:
|
||||||
|
MACOS_CERTIFICATE_P12_BASE64: ${{ secrets.MACOS_CERTIFICATE_P12_BASE64 }}
|
||||||
|
MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
|
||||||
|
MACOS_SIGNING_IDENTITY: ${{ vars.MACOS_SIGNING_IDENTITY || secrets.MACOS_SIGNING_IDENTITY }}
|
||||||
|
APPLE_API_KEY_P8: ${{ secrets.APPLE_API_KEY_P8 }}
|
||||||
|
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
|
||||||
|
APPLE_API_ISSUER_ID: ${{ secrets.APPLE_API_ISSUER_ID }}
|
||||||
|
run: >-
|
||||||
|
python3 packaging/macos/release.py
|
||||||
|
--app dist/ZoiteChat.app
|
||||||
|
--output dist/ZoiteChat-${{ matrix.arch }}-signed-notarized.zip
|
||||||
|
--diagnostics dist/notarization
|
||||||
|
--notary-timeout 60m
|
||||||
|
- name: Upload signed and notarized application
|
||||||
|
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7
|
||||||
|
with:
|
||||||
|
name: ZoiteChat-macos-${{ matrix.arch }}-signed-notarized
|
||||||
|
path: dist/ZoiteChat-${{ matrix.arch }}-signed-notarized.zip
|
||||||
|
if-no-files-found: error
|
||||||
|
- name: Upload notarization diagnostics
|
||||||
|
if: always()
|
||||||
|
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7
|
||||||
|
with:
|
||||||
|
name: ZoiteChat-macos-${{ matrix.arch }}-notarization-log
|
||||||
|
path: dist/notarization/*.json
|
||||||
|
if-no-files-found: ignore
|
||||||
|
- name: Upload build logs on failure
|
||||||
|
if: failure()
|
||||||
|
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7
|
||||||
|
with:
|
||||||
|
name: ZoiteChat-macos-${{ matrix.arch }}-release-build-logs
|
||||||
|
path: build-macos/meson-logs/
|
||||||
|
if-no-files-found: ignore
|
||||||
@@ -69,6 +69,30 @@ jobs:
|
|||||||
Download-WithRetry -Url https://github.com/ZoiteChat/gvsbuild/releases/download/zoitechat-2.20.0-alpha.1/GTK3_Gvsbuild_zoitechat-2.20.0-alpha.1_x64.zip -OutFile deps\gtk-${{ matrix.arch }}.zip
|
Download-WithRetry -Url https://github.com/ZoiteChat/gvsbuild/releases/download/zoitechat-2.20.0-alpha.1/GTK3_Gvsbuild_zoitechat-2.20.0-alpha.1_x64.zip -OutFile deps\gtk-${{ matrix.arch }}.zip
|
||||||
Expand-Archive -LiteralPath deps\gtk-${{ matrix.arch }}.zip -DestinationPath C:\gtk-build\gtk\x64\release -Force
|
Expand-Archive -LiteralPath deps\gtk-${{ matrix.arch }}.zip -DestinationPath C:\gtk-build\gtk\x64\release -Force
|
||||||
|
|
||||||
|
# The GTK3 archive omits librsvg. Add only its renderer, loader and
|
||||||
|
# license from the companion release, keeping the GTK3 DLLs intact.
|
||||||
|
Download-WithRetry -Url https://github.com/ZoiteChat/gvsbuild/releases/download/zoitechat-2.20.0-alpha.1/GTK4_Gvsbuild_zoitechat-2.20.0-alpha.1_x64.zip -OutFile deps\svg-runtime.zip
|
||||||
|
if ((Get-FileHash deps\svg-runtime.zip -Algorithm SHA256).Hash -ne 'fb657f9ebae3a6807defb15e66091d643a703ee66cb37488e3e11e5646f45adb') {
|
||||||
|
throw 'Unexpected SVG runtime archive checksum'
|
||||||
|
}
|
||||||
|
$svgArchive = [System.IO.Compression.ZipFile]::OpenRead((Resolve-Path deps\svg-runtime.zip))
|
||||||
|
try {
|
||||||
|
foreach ($name in @('bin/rsvg-2-2.dll', 'lib/gdk-pixbuf-2.0/2.10.0/loaders/pixbufloader_svg.dll', 'share/doc/librsvg/COPYING.LIB')) {
|
||||||
|
$entry = $svgArchive.GetEntry($name)
|
||||||
|
if ($null -eq $entry) { throw "Missing SVG runtime file: $name" }
|
||||||
|
$destination = Join-Path 'C:\gtk-build\gtk\x64\release' $name
|
||||||
|
New-Item -ItemType Directory -Path (Split-Path $destination) -Force | Out-Null
|
||||||
|
[System.IO.Compression.ZipFileExtensions]::ExtractToFile($entry, $destination, $true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally { $svgArchive.Dispose() }
|
||||||
|
# This loads the module now, catching missing DLLs before the build.
|
||||||
|
& C:\gtk-build\gtk\x64\release\bin\gdk-pixbuf-query-loaders.exe --update-cache
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw 'Could not generate the pixbuf loader cache' }
|
||||||
|
if (-not (Select-String -LiteralPath 'C:\gtk-build\gtk\x64\release\lib\gdk-pixbuf-2.0\2.10.0\loaders.cache' -SimpleMatch '"svg"')) {
|
||||||
|
throw 'SVG loader could not be loaded with the GTK3 runtime'
|
||||||
|
}
|
||||||
|
|
||||||
Download-WithRetry -Url https://repo.msys2.org/mingw/x86_64/mingw-w64-x86_64-hicolor-icon-theme-0.18-1-any.pkg.tar.zst -OutFile deps\hicolor-icon-theme.pkg.tar.zst
|
Download-WithRetry -Url https://repo.msys2.org/mingw/x86_64/mingw-w64-x86_64-hicolor-icon-theme-0.18-1-any.pkg.tar.zst -OutFile deps\hicolor-icon-theme.pkg.tar.zst
|
||||||
python -c "import tarfile,zstandard,pathlib;archive=pathlib.Path(r'deps\\hicolor-icon-theme.pkg.tar.zst');target=pathlib.Path(r'C:\\gtk-build\\gtk\\x64\\release');dctx=zstandard.ZstdDecompressor();f=archive.open('rb');reader=dctx.stream_reader(f);tf=tarfile.open(fileobj=reader,mode='r|');[tf.extract(m,path=target) for m in tf if m.name.startswith('mingw64/share/icons/hicolor/')];tf.close();reader.close();f.close()"
|
python -c "import tarfile,zstandard,pathlib;archive=pathlib.Path(r'deps\\hicolor-icon-theme.pkg.tar.zst');target=pathlib.Path(r'C:\\gtk-build\\gtk\\x64\\release');dctx=zstandard.ZstdDecompressor();f=archive.open('rb');reader=dctx.stream_reader(f);tf=tarfile.open(fileobj=reader,mode='r|');[tf.extract(m,path=target) for m in tf if m.name.startswith('mingw64/share/icons/hicolor/')];tf.close();reader.close();f.close()"
|
||||||
|
|
||||||
@@ -174,6 +198,22 @@ jobs:
|
|||||||
)
|
)
|
||||||
shell: cmd
|
shell: cmd
|
||||||
|
|
||||||
|
- name: Test emoji catalog, font coverage and picker
|
||||||
|
shell: cmd
|
||||||
|
run: |
|
||||||
|
call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\Common7\Tools\VsDevCmd.bat"
|
||||||
|
set "PATH=%CD%\..\zoitechat-build\x64\rel;%PATH%"
|
||||||
|
set "GDK_PIXBUF_MODULE_FILE=%CD%\..\zoitechat-build\x64\rel\lib\gdk-pixbuf-2.0\2.10.0\loaders.cache"
|
||||||
|
set "ZOITECHAT_EMOJI_FONT=%CD%\..\zoitechat-build\x64\rel\share\fonts\zoitechat\NotoColorEmoji.ttf"
|
||||||
|
fc /b data\fonts\NotoColorEmoji.ttf "%ZOITECHAT_EMOJI_FONT%"
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
for %%T in (data font ui) do (
|
||||||
|
msbuild win32\emoji-tests.vcxproj /m /verbosity:minimal /p:Configuration=Release /p:Platform=x64 /p:EmojiTestKind=%%T
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
..\zoitechat-build\x64\tests\emoji-%%T-tests.exe --tap
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
- name: Preparing Artifacts
|
- name: Preparing Artifacts
|
||||||
run: |
|
run: |
|
||||||
move ..\zoitechat-build\${{ matrix.platform }}\ZoiteChat-*.exe .\
|
move ..\zoitechat-build\${{ matrix.platform }}\ZoiteChat-*.exe .\
|
||||||
|
|||||||
@@ -27,3 +27,9 @@ osx/.ZoiteChat.app
|
|||||||
*.dmg
|
*.dmg
|
||||||
*.VC.db
|
*.VC.db
|
||||||
*.VC.opendb
|
*.VC.opendb
|
||||||
|
|
||||||
|
# Native macOS packaging output
|
||||||
|
/build-macos*/
|
||||||
|
/dist/
|
||||||
|
/packaging/macos/__pycache__/
|
||||||
|
/packaging/macos/tests/__pycache__/
|
||||||
|
|||||||
@@ -336,3 +336,26 @@ proprietary programs. If your program is a subroutine library, you may
|
|||||||
consider it more useful to permit linking proprietary applications with the
|
consider it more useful to permit linking proprietary applications with the
|
||||||
library. If this is what you want to do, use the GNU Lesser General
|
library. If this is what you want to do, use the GNU Lesser General
|
||||||
Public License instead of this License.
|
Public License instead of this License.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
======================================================================
|
||||||
|
ADDITIONAL LICENSE NOTICE
|
||||||
|
======================================================================
|
||||||
|
|
||||||
|
ZoiteChat source code:
|
||||||
|
GPL version 2
|
||||||
|
See Above.
|
||||||
|
|
||||||
|
Unicode Emoji and CLDR data:
|
||||||
|
Unicode License v3
|
||||||
|
data/emoji/LICENSE-UNICODE.txt
|
||||||
|
|
||||||
|
gemoji shortcode data:
|
||||||
|
MIT License
|
||||||
|
Copyright (c) 2019 GitHub, Inc.
|
||||||
|
data/emoji/LICENSE-GEMOJI.txt
|
||||||
|
|
||||||
|
Noto Color Emoji:
|
||||||
|
SIL Open Font License 1.1
|
||||||
|
data/fonts/OFL.txt
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
UNICODE LICENSE V3
|
||||||
|
|
||||||
|
COPYRIGHT AND PERMISSION NOTICE
|
||||||
|
|
||||||
|
Copyright © 1991-2026 Unicode, Inc.
|
||||||
|
|
||||||
|
NOTICE TO USER: Carefully read the following legal agreement. BY
|
||||||
|
DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR
|
||||||
|
SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE
|
||||||
|
TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT
|
||||||
|
DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE.
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a
|
||||||
|
copy of data files and any associated documentation (the "Data Files") or
|
||||||
|
software and any associated documentation (the "Software") to deal in the
|
||||||
|
Data Files or Software without restriction, including without limitation
|
||||||
|
the rights to use, copy, modify, merge, publish, distribute, and/or sell
|
||||||
|
copies of the Data Files or Software, and to permit persons to whom the
|
||||||
|
Data Files or Software are furnished to do so, provided that either (a)
|
||||||
|
this copyright and permission notice appear with all copies of the Data
|
||||||
|
Files or Software, or (b) this copyright and permission notice appear in
|
||||||
|
associated Documentation.
|
||||||
|
|
||||||
|
THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
|
||||||
|
KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF
|
||||||
|
THIRD PARTY RIGHTS.
|
||||||
|
|
||||||
|
IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE
|
||||||
|
BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES,
|
||||||
|
OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
|
||||||
|
WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
|
||||||
|
ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA
|
||||||
|
FILES OR SOFTWARE.
|
||||||
|
|
||||||
|
Except as contained in this notice, the name of a copyright holder shall
|
||||||
|
not be used in advertising or otherwise to promote the sale, use or other
|
||||||
|
dealings in these Data Files or Software without prior written
|
||||||
|
authorization of the copyright holder.
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
# Bundled emoji data
|
||||||
|
|
||||||
|
These are pinned copies of the upstream data files that
|
||||||
|
`tools/gen-emoji-data.py` compiles into ZoiteChat's emoji picker catalog
|
||||||
|
at build time. Bundling them means every ZoiteChat package exposes the
|
||||||
|
same emoji, names, categories and search keywords, independent of the
|
||||||
|
GTK runtime version or the emoji data a distribution installs under
|
||||||
|
`/usr/share/gtk-3.0/emoji`.
|
||||||
|
|
||||||
|
Nothing here affects what is sent over IRC: the picker inserts plain
|
||||||
|
Unicode sequences. Official builds register the bundled Noto Color Emoji
|
||||||
|
font privately for consistent rendering, while preserving platform font
|
||||||
|
fallback if registration is unavailable.
|
||||||
|
|
||||||
|
| File | Source | Version |
|
||||||
|
| ---- | ------ | ------- |
|
||||||
|
| `emoji-test.txt` | [Unicode emoji data](https://unicode.org/Public/emoji/17.0/emoji-test.txt) (mirrored at [unicode-org/unicodetools](https://github.com/unicode-org/unicodetools/blob/main/unicodetools/data/emoji/17.0/emoji-test.txt)) | Emoji 17.0 |
|
||||||
|
| `cldr-annotations-en.xml` | [unicode-org/cldr](https://github.com/unicode-org/cldr/blob/release-48/common/annotations/en.xml) `common/annotations/en.xml` | CLDR release 48 |
|
||||||
|
| `cldr-annotations-derived-en.xml` | [unicode-org/cldr](https://github.com/unicode-org/cldr/blob/release-48/common/annotationsDerived/en.xml) `common/annotationsDerived/en.xml` | CLDR release 48 |
|
||||||
|
| `gemoji.json` | [github/gemoji](https://github.com/github/gemoji/blob/v4.1.0/db/emoji.json) `db/emoji.json` | v4.1.0 |
|
||||||
|
|
||||||
|
The pinned versions are recorded in `VERSIONS`.
|
||||||
|
|
||||||
|
## Licenses
|
||||||
|
|
||||||
|
- `emoji-test.txt`, `cldr-annotations-en.xml`,
|
||||||
|
`cldr-annotations-derived-en.xml`: © Unicode, Inc., distributed under
|
||||||
|
the [Unicode License v3](https://www.unicode.org/license.txt).
|
||||||
|
- `gemoji.json`: from the gemoji project, MIT licensed,
|
||||||
|
© GitHub, Inc.
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
# Pinned versions of the emoji source data in this directory.
|
||||||
|
# Keep in sync with the files themselves; tools/gen-emoji-data.py
|
||||||
|
# verifies unicode-emoji against the emoji-test.txt header.
|
||||||
|
unicode-emoji=17.0
|
||||||
|
cldr=48
|
||||||
|
gemoji=4.1.0
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+23479
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -0,0 +1,91 @@
|
|||||||
|
Copyright 2013 Google LLC
|
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
|
This license is copied below, and is also available with a FAQ at:
|
||||||
|
https://scripts.sil.org/OFL
|
||||||
|
|
||||||
|
|
||||||
|
-----------------------------------------------------------
|
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) shall not be
|
||||||
|
used to promote, endorse or advertise any Modified Version, except to
|
||||||
|
acknowledge the contribution(s) of the Copyright Holder(s) and the Author(s)
|
||||||
|
or with their explicit written permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
# Bundled emoji font
|
||||||
|
|
||||||
|
`NotoColorEmoji.ttf` reports Noto Color Emoji version 2.051 and covers
|
||||||
|
Unicode Emoji 17.0. It is vendored from Debian's
|
||||||
|
`fonts-noto-color-emoji` 2.051 package, built from the corresponding
|
||||||
|
Google Noto Emoji release.
|
||||||
|
|
||||||
|
Every GTK build installs this exact file: Windows (installer and portable
|
||||||
|
build files), AppImage, Flatpak and native Meson/RPM builds. It is loaded
|
||||||
|
into a private Pango/FreeType font map as `ZoiteChat Emoji`; an older system
|
||||||
|
Noto font cannot replace it. No font is installed globally on Windows.
|
||||||
|
|
||||||
|
Chat windows and the emoji picker share this map. Fontconfig still loads
|
||||||
|
system fonts and configuration for ordinary text, and the configured text
|
||||||
|
font, size and style remain in effect. In particular, plain digits, `#`
|
||||||
|
and `*` are not assigned emoji metrics.
|
||||||
|
|
||||||
|
Windows uses the same FreeType backend rather than GDI registration or an
|
||||||
|
OS-version-specific picker. The dependency bundle already ships FreeType,
|
||||||
|
Fontconfig and PangoFT2. This rendering path does not require Windows' native
|
||||||
|
color-font support. Actual Windows 7 execution still needs testing on that OS.
|
||||||
|
|
||||||
|
Meson requires PangoFT2 >= 1.44 and Fontconfig >= 2.13. Normal installations
|
||||||
|
load `share/fonts/zoitechat/NotoColorEmoji.ttf` beneath the configured data
|
||||||
|
directory; Windows resolves it relative to the executable. AppImage sets
|
||||||
|
`ZOITECHAT_EMOJI_FONT` to its own copy. For an uninstalled development build:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
ZOITECHAT_EMOJI_FONT="$PWD/data/fonts/NotoColorEmoji.ttf" build/src/fe-gtk/zoitechat
|
||||||
|
```
|
||||||
|
|
||||||
|
A missing/unloadable file logs a warning and retains system-font fallback.
|
||||||
|
CI verifies the installed copy and tests catalog/font coverage; AppImage and
|
||||||
|
Windows also run the interactive picker regression tests. Native themes,
|
||||||
|
DPI and different Pango/Cairo versions can still affect pixel-level output.
|
||||||
|
|
||||||
|
Upstream release: https://github.com/googlefonts/noto-emoji/releases/tag/v2.051
|
||||||
|
|
||||||
|
Vendored file SHA-256:
|
||||||
|
`dac5c27651082d6c53dab5081f50d2022ddf6877d730fa919cf6a4fc2af22de0`
|
||||||
|
|
||||||
|
The font is distributed under the SIL Open Font License 1.1. See
|
||||||
|
`OFL.txt`.
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
emoji_font_dir = join_paths(get_option('datadir'), 'fonts', 'zoitechat')
|
||||||
|
emoji_font_doc_dir = join_paths(get_option('datadir'), 'doc', 'zoitechat', 'fonts')
|
||||||
|
|
||||||
|
install_data(
|
||||||
|
'NotoColorEmoji.ttf',
|
||||||
|
install_dir: emoji_font_dir,
|
||||||
|
)
|
||||||
|
|
||||||
|
install_data(
|
||||||
|
'OFL.txt',
|
||||||
|
'README.md',
|
||||||
|
install_dir: emoji_font_doc_dir,
|
||||||
|
)
|
||||||
@@ -3,6 +3,7 @@ if get_option('plugin')
|
|||||||
endif
|
endif
|
||||||
|
|
||||||
if get_option('gtk-frontend')
|
if get_option('gtk-frontend')
|
||||||
|
subdir('fonts')
|
||||||
subdir('icons')
|
subdir('icons')
|
||||||
subdir('misc')
|
subdir('misc')
|
||||||
subdir('man')
|
subdir('man')
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ appdir = join_paths(get_option('datadir'), 'applications')
|
|||||||
metainfodir = join_paths(get_option('datadir'), 'metainfo')
|
metainfodir = join_paths(get_option('datadir'), 'metainfo')
|
||||||
mimedir = join_paths(get_option('datadir'), 'mime', 'packages')
|
mimedir = join_paths(get_option('datadir'), 'mime', 'packages')
|
||||||
desktop_utils = find_program('desktop-file-validate', required: false)
|
desktop_utils = find_program('desktop-file-validate', required: false)
|
||||||
|
appstreamcli = find_program('appstreamcli', required: false)
|
||||||
|
|
||||||
if get_option('gtk-frontend')
|
if get_option('gtk-frontend')
|
||||||
if get_option('install-appdata')
|
if get_option('install-appdata')
|
||||||
@@ -13,7 +14,6 @@ if get_option('gtk-frontend')
|
|||||||
install_dir: metainfodir
|
install_dir: metainfodir
|
||||||
)
|
)
|
||||||
|
|
||||||
appstreamcli = find_program('appstreamcli', required: false)
|
|
||||||
if appstreamcli.found()
|
if appstreamcli.found()
|
||||||
test('Validate net.zoite.Zoitechat.appdata.xml', appstreamcli,
|
test('Validate net.zoite.Zoitechat.appdata.xml', appstreamcli,
|
||||||
args: ['validate', zoitechat_appdata]
|
args: ['validate', zoitechat_appdata]
|
||||||
|
|||||||
@@ -29,6 +29,22 @@
|
|||||||
<id>zoitechat.desktop</id>
|
<id>zoitechat.desktop</id>
|
||||||
</provides>
|
</provides>
|
||||||
<releases>
|
<releases>
|
||||||
|
<release date="2026-09-15" version="2.19.1">
|
||||||
|
<description>
|
||||||
|
<ul>
|
||||||
|
<li>Fixed /msg messages showing twice on servers using IRCv3 <code>echo-message</code>.</li>
|
||||||
|
<li>Fixed escaping a leading command slash so <code>//text</code> sends <code>/text</code> as normal chat.</li>
|
||||||
|
<li>Fixed automatic away clearing when sending messages on servers using <code>echo-message</code>.</li>
|
||||||
|
<li>Fixed other users' away status not updating until rejoining a channel when <code>away-notify</code> updates are missed.</li>
|
||||||
|
<li>Fixed private messages and <code>/me</code> actions being routed to the wrong window when using WeeChat's IRC relay, while preserving channel ACTION routing.</li>
|
||||||
|
<li>Fixed a Flatpak startup error caused by the D-Bus service still using the old HexChat app ID.</li>
|
||||||
|
<li>Fixed optional Windows 10 theme downloads in the Windows installer.</li>
|
||||||
|
<li>Fixed the GTK warning when switching from channel tabs to the channel tree.</li>
|
||||||
|
<li>Fixed GTK3 theme archive root detection so themes with an <code>index.theme</code> file are preferred correctly.</li>
|
||||||
|
<li>Fixed source-build configuration when plugin AppStream metadata installation is disabled, and corrected the minimum Meson version to 0.59.</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
<release date="2026-07-28" version="2.19.0">
|
<release date="2026-07-28" version="2.19.0">
|
||||||
<description>
|
<description>
|
||||||
<ul>
|
<ul>
|
||||||
|
|||||||
+48
-5
@@ -33,10 +33,53 @@ Example:
|
|||||||
that is not visually indented
|
that is not visually indented
|
||||||
```
|
```
|
||||||
|
|
||||||
## Read the Docs theme skin
|
## HTML theme
|
||||||
|
|
||||||
The HTML output keeps the existing `basicstrap` Sphinx theme, then applies the
|
The `zoitechat` theme inherits Sphinx's built-in `basic` theme. It uses the
|
||||||
ZoiteChat main-site skin from `_static/css/zoitechat-docs.css`.
|
same compact layout, blue navigation, and system fonts as zoitechat.org.
|
||||||
|
Appearance starts in System mode on each page. Light and Dark are available
|
||||||
|
for the current page; no cookies or browser storage are used.
|
||||||
|
|
||||||
Brand navigation links are set in `conf.py` through `html_context`, and the
|
The theme's templates are in `_theme/zoitechat/`. Its readable CSS and
|
||||||
header logo uses `_static/zoitechat-logo.svg`.
|
JavaScript are in `_static/css/zoitechat-docs.css` and
|
||||||
|
`_static/js/zoitechat-docs.js`. No minification, JavaScript package manager,
|
||||||
|
external fonts, or CDN assets are required.
|
||||||
|
|
||||||
|
Navigation URLs, including `zoite_atlas_url`, are configured through
|
||||||
|
`html_context` in `conf.py`. The logo
|
||||||
|
is `_static/zoitechat-logo.svg`. Standard Sphinx search, indexes, source
|
||||||
|
links, anchors, translations, and previous/next links remain available.
|
||||||
|
The HTML output also works as locally installed documentation; project-site
|
||||||
|
links only access the network when clicked.
|
||||||
|
|
||||||
|
Build from the repository root:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sphinx-build -b html docs docs/_build/html
|
||||||
|
```
|
||||||
|
|
||||||
|
Use a clean output directory when switching themes, so stale Basicstrap
|
||||||
|
assets from an earlier build are not included in a package.
|
||||||
|
|
||||||
|
## Debian packaging
|
||||||
|
|
||||||
|
The active theme does not use the bundled Basicstrap, Bootstrap, jQuery,
|
||||||
|
jQuery Cookie, Font Awesome, or icon fonts. Its font stack includes DejaVu
|
||||||
|
Sans and DejaVu Sans Mono; no font files are bundled by the theme.
|
||||||
|
|
||||||
|
Packagers may exclude `docs/_theme/basicstrap/` from the source package.
|
||||||
|
The default theme continues to build without that directory. No pre-minified
|
||||||
|
third-party assets need to be added or rebuilt for this theme.
|
||||||
|
|
||||||
|
Build with the distribution's `python3-sphinx`. After installing the HTML
|
||||||
|
into the package staging directory, use `dh_sphinxdoc` (or `dh --with
|
||||||
|
sphinxdoc`) to replace Sphinx's standard JavaScript files with system
|
||||||
|
symlinks. Include `${sphinxdoc:Depends}` in the package dependencies and
|
||||||
|
`${sphinxdoc:Built-Using}` where required for generated assets. The custom
|
||||||
|
`zoitechat-docs.js` remains a small, readable project source file.
|
||||||
|
|
||||||
|
A built-in theme is still selectable with `-D html_theme=classic` if a
|
||||||
|
packager needs a different layout. The old RST content has existing build
|
||||||
|
diagnostics; changing the theme does not suppress or alter them.
|
||||||
|
|
||||||
|
Shared website chrome is in `_static/css/zoitechat-site.css`, copied from the main site. Keep that readable stylesheet in sync when changing the shared header, navigation, typography, or colours.
|
||||||
|
|||||||
Vendored
+694
-2128
File diff suppressed because it is too large
Load Diff
Vendored
+526
@@ -0,0 +1,526 @@
|
|||||||
|
/* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||||
|
/* Shared chrome copied from ZoiteChat/site assets/css/site.css (1df996a). */
|
||||||
|
:root {
|
||||||
|
color-scheme: light;
|
||||||
|
--bg: #fff;
|
||||||
|
--surface: #fff;
|
||||||
|
--chrome: #f1f2f3;
|
||||||
|
--chrome2: #e3e5e7;
|
||||||
|
--text: #25282c;
|
||||||
|
--muted: #58616b;
|
||||||
|
--line: #bdc3c9;
|
||||||
|
--soft-line: #dce0e4;
|
||||||
|
--blue: #215a9e;
|
||||||
|
--link: #174f92;
|
||||||
|
--selection: #e6effa;
|
||||||
|
--success: #236741;
|
||||||
|
--code: #f2f4f6;
|
||||||
|
--shadow: none;
|
||||||
|
--mono: Consolas,"Liberation Mono",monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-theme=dark] {
|
||||||
|
color-scheme: dark;
|
||||||
|
--bg: #111416;
|
||||||
|
--surface: #191c1f;
|
||||||
|
--chrome: #25292d;
|
||||||
|
--chrome2: #30353a;
|
||||||
|
--text: #eef0f2;
|
||||||
|
--muted: #b2bbc4;
|
||||||
|
--line: #50575e;
|
||||||
|
--soft-line: #353c43;
|
||||||
|
--blue: #2865a8;
|
||||||
|
--link: #8dc0ff;
|
||||||
|
--selection: #233951;
|
||||||
|
--success: #92d5ab;
|
||||||
|
--code: #111518;
|
||||||
|
--shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--text);
|
||||||
|
font: 15px/1.6 system-ui,-apple-system,"Segoe UI",sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--link);
|
||||||
|
text-underline-offset: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
button, input, select {
|
||||||
|
font: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
button, summary {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
display: block;
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
button, a, input, select, summary {
|
||||||
|
touch-action: manipulation;
|
||||||
|
}
|
||||||
|
|
||||||
|
:focus-visible {
|
||||||
|
outline: 3px solid var(--link);
|
||||||
|
outline-offset: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[hidden] {
|
||||||
|
display: none!important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sr-only {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
padding: 0;
|
||||||
|
margin: -1px;
|
||||||
|
overflow: hidden;
|
||||||
|
clip-path: inset(50%);
|
||||||
|
white-space: nowrap;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skip-link {
|
||||||
|
position: absolute;
|
||||||
|
left: 12px;
|
||||||
|
top: -100px;
|
||||||
|
z-index: 10;
|
||||||
|
padding: 10px;
|
||||||
|
background: var(--surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
.skip-link:focus {
|
||||||
|
top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 20px;
|
||||||
|
padding: 0 0 16px;
|
||||||
|
background: var(--surface);
|
||||||
|
border-bottom: 1px solid var(--line);
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: center;
|
||||||
|
color: var(--text);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand img {
|
||||||
|
width: 38px;
|
||||||
|
height: 42px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand strong {
|
||||||
|
font-size: 28px;
|
||||||
|
letter-spacing: -.8px;
|
||||||
|
font-weight: 650;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand small {
|
||||||
|
display: block;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--muted);
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.appearance {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 5px 8px;
|
||||||
|
background: var(--surface);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.navigation {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
background: var(--chrome);
|
||||||
|
border-bottom: 1px solid var(--line);
|
||||||
|
padding: 0;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-links {
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-links>a, .nav-menu summary {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 36px;
|
||||||
|
padding: 6px 15px;
|
||||||
|
color: var(--text);
|
||||||
|
font-size: 13px;
|
||||||
|
text-decoration: none;
|
||||||
|
border-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-links>a[aria-current=page] {
|
||||||
|
color: #fff;
|
||||||
|
border-bottom-color: var(--blue);
|
||||||
|
font-weight: 500;
|
||||||
|
background: var(--blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-links>a:hover, .nav-menu summary:hover {
|
||||||
|
background: var(--selection);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-menu {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-menu summary {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-menu summary::after {
|
||||||
|
content: "⌄";
|
||||||
|
margin-left: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-menu summary::-webkit-details-marker {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-menu-panel {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: 100%;
|
||||||
|
z-index: 5;
|
||||||
|
min-width: 280px;
|
||||||
|
background: var(--surface);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-menu-panel a {
|
||||||
|
display: block;
|
||||||
|
padding: 10px 12px;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-menu-panel a:hover {
|
||||||
|
background: var(--selection);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-menu-panel span {
|
||||||
|
display: block;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-main {
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, h2, h3, p {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: clamp(30px,3.9vw,46px);
|
||||||
|
line-height: 1.16;
|
||||||
|
letter-spacing: -1.5px;
|
||||||
|
font-weight: 650;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 20px;
|
||||||
|
line-height: 1.3;
|
||||||
|
letter-spacing: 0;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
color: var(--text);
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.7;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-footer {
|
||||||
|
padding: 17px 0;
|
||||||
|
background: var(--surface);
|
||||||
|
border-top: 1px solid var(--line);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-grid {
|
||||||
|
display: flex;
|
||||||
|
align-items: start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-copy {
|
||||||
|
font-size: 11px;
|
||||||
|
max-width: 62ch;
|
||||||
|
margin: 0;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-links {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 5px 14px;
|
||||||
|
max-width: 400px;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width:1000px) {
|
||||||
|
.navigation {
|
||||||
|
padding: 0 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-links>a, .nav-menu summary {
|
||||||
|
padding: 8px 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width:650px) {
|
||||||
|
.topbar {
|
||||||
|
padding: 13px 16px;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand strong {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand small {
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand img {
|
||||||
|
width: 28px;
|
||||||
|
height: 31px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.appearance {
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.appearance>span {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.appearance select {
|
||||||
|
font-size: 12px;
|
||||||
|
max-width: 98px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navigation {
|
||||||
|
padding: 0 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-links {
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-links>a, .nav-menu summary {
|
||||||
|
min-height: 44px;
|
||||||
|
padding: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 34px;
|
||||||
|
letter-spacing: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-grid {
|
||||||
|
display: grid;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-footer {
|
||||||
|
padding: 20px 17px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-menu-panel {
|
||||||
|
position: fixed;
|
||||||
|
right: 10px;
|
||||||
|
left: 10px;
|
||||||
|
top: auto;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion:reduce) {
|
||||||
|
* {
|
||||||
|
scroll-behavior: auto!important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.appearance select {
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 3px 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-links>a:hover {
|
||||||
|
background: var(--selection);
|
||||||
|
color: var(--link);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: -.5px;
|
||||||
|
line-height: 1.3;
|
||||||
|
margin: 0 0 16px;
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width:1100px) {
|
||||||
|
.navigation {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width:800px) {
|
||||||
|
.nav-links>a, .nav-menu summary {
|
||||||
|
padding: 6px 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width:600px) {
|
||||||
|
.topbar {
|
||||||
|
padding: 0 0 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand strong {
|
||||||
|
font-size: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand img {
|
||||||
|
width: 32px;
|
||||||
|
height: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-links>a, .nav-menu summary {
|
||||||
|
padding: 7px 9px;
|
||||||
|
min-height: 38px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-grid {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-links {
|
||||||
|
margin-top: 10px;
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-footer {
|
||||||
|
padding: 14px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 26px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root:not([data-theme]) {
|
||||||
|
color-scheme: dark;
|
||||||
|
--bg: #111416;
|
||||||
|
--surface: #191c1f;
|
||||||
|
--chrome: #25292d;
|
||||||
|
--chrome2: #30353a;
|
||||||
|
--text: #eef0f2;
|
||||||
|
--muted: #b2bbc4;
|
||||||
|
--line: #50575e;
|
||||||
|
--soft-line: #353c43;
|
||||||
|
--blue: #2865a8;
|
||||||
|
--link: #8dc0ff;
|
||||||
|
--selection: #233951;
|
||||||
|
--success: #92d5ab;
|
||||||
|
--code: #111518;
|
||||||
|
--shadow: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-header, .site-main, .site-footer {
|
||||||
|
width: min(1040px, calc(100% - 48px));
|
||||||
|
margin-inline: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-header {
|
||||||
|
margin-top: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-main {
|
||||||
|
padding-block: 28px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-header .topbar {
|
||||||
|
padding: 0 0 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-header .navigation {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.site-header, .site-main, .site-footer {
|
||||||
|
width: calc(100% - 30px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-header {
|
||||||
|
margin-top: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-main {
|
||||||
|
padding-top: 23px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Match the browser's heading defaults used by the main site. */
|
||||||
|
h2, h3 { font-weight: bold; }
|
||||||
|
h2 { margin-top: .83em; }
|
||||||
|
h3 { margin-block: 1em; }
|
||||||
Vendored
+44
@@ -0,0 +1,44 @@
|
|||||||
|
/* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||||
|
(() => {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const initialise = () => {
|
||||||
|
const picker = document.getElementById('theme-picker');
|
||||||
|
const preference = window.matchMedia('(prefers-color-scheme: dark)');
|
||||||
|
|
||||||
|
if (picker instanceof HTMLSelectElement) {
|
||||||
|
const applyTheme = () => {
|
||||||
|
const dark = picker.value === 'dark'
|
||||||
|
|| (picker.value === 'system' && preference.matches);
|
||||||
|
document.documentElement.dataset.theme = dark ? 'dark' : 'light';
|
||||||
|
document.querySelectorAll('meta[name="theme-color"]').forEach((meta) => {
|
||||||
|
meta.setAttribute('content', dark ? '#111416' : '#ffffff');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Follow the OS on every visit, including browser back/forward navigation.
|
||||||
|
const resetTheme = () => {
|
||||||
|
picker.value = 'system';
|
||||||
|
applyTheme();
|
||||||
|
};
|
||||||
|
picker.addEventListener('change', applyTheme);
|
||||||
|
preference.addEventListener('change', applyTheme);
|
||||||
|
window.addEventListener('pageshow', resetTheme);
|
||||||
|
resetTheme();
|
||||||
|
}
|
||||||
|
|
||||||
|
const navigation = document.getElementById('docs-navigation');
|
||||||
|
const compact = window.matchMedia('(max-width: 760px)');
|
||||||
|
if (navigation instanceof HTMLDetailsElement) {
|
||||||
|
const fitNavigation = () => { navigation.open = !compact.matches; };
|
||||||
|
compact.addEventListener('change', fitNavigation);
|
||||||
|
fitNavigation();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', initialise, { once: true });
|
||||||
|
} else {
|
||||||
|
initialise();
|
||||||
|
}
|
||||||
|
})();
|
||||||
Vendored
+5
-1
@@ -209,6 +209,8 @@
|
|||||||
{%- endif %}
|
{%- endif %}
|
||||||
<meta name="description" content="">
|
<meta name="description" content="">
|
||||||
<meta name="author" content="">
|
<meta name="author" content="">
|
||||||
|
<meta name="theme-color" content="#eee5d3" media="(prefers-color-scheme: light)">
|
||||||
|
<meta name="theme-color" content="#161514" media="(prefers-color-scheme: dark)">
|
||||||
|
|
||||||
{{ css() }}
|
{{ css() }}
|
||||||
{%- if not embedded %}
|
{%- if not embedded %}
|
||||||
@@ -260,8 +262,9 @@
|
|||||||
<nav class="site-nav" aria-label="Primary">
|
<nav class="site-nav" aria-label="Primary">
|
||||||
<a href="https://zoitechat.org/">Home</a>
|
<a href="https://zoitechat.org/">Home</a>
|
||||||
<a href="https://zoitechat.org/addons.php">Addons</a>
|
<a href="https://zoitechat.org/addons.php">Addons</a>
|
||||||
|
<a href="https://zoitechat.org/announcements.php">Announcements</a>
|
||||||
<a href="https://zoitechat.org/download.php">Download</a>
|
<a href="https://zoitechat.org/download.php">Download</a>
|
||||||
<a href="{{ pathto(master_doc) }}">Docs</a>
|
<a href="{{ pathto(master_doc) }}" aria-current="page">Docs</a>
|
||||||
<a href="https://github.com/ZoiteChat/zoitechat" target="_blank" rel="noopener">GitHub</a>
|
<a href="https://github.com/ZoiteChat/zoitechat" target="_blank" rel="noopener">GitHub</a>
|
||||||
<details class="nav-menu">
|
<details class="nav-menu">
|
||||||
<summary>IRC Support</summary>
|
<summary>IRC Support</summary>
|
||||||
@@ -336,6 +339,7 @@
|
|||||||
<div class="footer-links">
|
<div class="footer-links">
|
||||||
<a href="https://zoitechat.org/download.php">Download</a>
|
<a href="https://zoitechat.org/download.php">Download</a>
|
||||||
<a href="https://zoitechat.org/addons.php">Addons</a>
|
<a href="https://zoitechat.org/addons.php">Addons</a>
|
||||||
|
<a href="https://zoitechat.org/announcements.php">Announcements</a>
|
||||||
<a href="https://zoitechat.org/privacy.php">Privacy Policy</a>
|
<a href="https://zoitechat.org/privacy.php">Privacy Policy</a>
|
||||||
<a href="{{ pathto(master_doc) }}">Documentation</a>
|
<a href="{{ pathto(master_doc) }}">Documentation</a>
|
||||||
<a href="https://github.com/ZoiteChat/zoitechat" target="_blank" rel="noopener">Source code</a>
|
<a href="https://github.com/ZoiteChat/zoitechat" target="_blank" rel="noopener">Source code</a>
|
||||||
|
|||||||
Vendored
+5
@@ -0,0 +1,5 @@
|
|||||||
|
{# SPDX-License-Identifier: GPL-2.0-or-later #}
|
||||||
|
<nav class="global-toc" aria-label="{{ _('Documentation contents') }}">
|
||||||
|
<h2>{{ _('Contents') }}</h2>
|
||||||
|
{{ toctree(maxdepth=2, collapse=true, includehidden=true, titles_only=true) }}
|
||||||
|
</nav>
|
||||||
Vendored
+108
@@ -0,0 +1,108 @@
|
|||||||
|
{# SPDX-License-Identifier: GPL-2.0-or-later #}
|
||||||
|
{% extends "basic/layout.html" %}
|
||||||
|
{% set docs_root = root_doc|default(master_doc) %}
|
||||||
|
|
||||||
|
{% block extrahead %}
|
||||||
|
{{ super() }}
|
||||||
|
<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)">
|
||||||
|
<meta name="theme-color" content="#111416" media="(prefers-color-scheme: dark)">
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block body_tag %}<body id="top">{% endblock %}
|
||||||
|
|
||||||
|
{% block header %}
|
||||||
|
<a class="skip-link" href="#main-content">{{ _('Skip to content') }}</a>
|
||||||
|
<header class="site-header">
|
||||||
|
<div class="topbar">
|
||||||
|
<a class="brand" href="{{ pathto(docs_root)|e }}">
|
||||||
|
<img src="{{ logo_url|default(pathto('_static/zoitechat-logo.svg', 1), true)|e }}" alt="" width="38" height="42">
|
||||||
|
<strong>{{ project|e }}</strong>
|
||||||
|
</a>
|
||||||
|
<label class="appearance" for="theme-picker">
|
||||||
|
<span>{{ _('Appearance') }}</span>
|
||||||
|
<select id="theme-picker" aria-label="{{ _('Documentation appearance') }}">
|
||||||
|
<option value="system" selected>{{ _('System') }}</option>
|
||||||
|
<option value="light">{{ _('Light') }}</option>
|
||||||
|
<option value="dark">{{ _('Dark') }}</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="navigation">
|
||||||
|
<nav class="nav-links" aria-label="{{ _('Primary') }}">
|
||||||
|
<a href="{{ zoite_site_url|e }}">{{ _('Home') }}</a>
|
||||||
|
<a href="{{ zoite_download_url|e }}">{{ _('Downloads') }}</a>
|
||||||
|
<a href="{{ zoite_addons_url|e }}">{{ _('Addons') }}</a>
|
||||||
|
<a href="{{ zoite_announcements_url|e }}">{{ _('Announcements') }}</a>
|
||||||
|
<a href="{{ pathto(docs_root)|e }}" aria-current="page">{{ _('Documentation') }}</a>
|
||||||
|
<a href="{{ zoite_atlas_url|e }}">{{ _('Atlas') }}</a>
|
||||||
|
<a href="{{ zoite_github_url|e }}" target="_blank" rel="noopener">{{ _('GitHub') }}</a>
|
||||||
|
<details class="nav-menu">
|
||||||
|
<summary>{{ _('IRC support') }}</summary>
|
||||||
|
<div class="nav-menu-panel" aria-label="{{ _('IRC support channels') }}">
|
||||||
|
<a href="{{ zoite_irc_url|e }}" target="_blank" rel="noopener"><strong>{{ _('Zoite IRC') }}</strong><span>{{ _('Official support') }} · #zoitechat</span></a>
|
||||||
|
<a href="{{ libera_irc_url|e }}" target="_blank" rel="noopener"><strong>{{ _('Libera.Chat') }}</strong><span>{{ _('Secondary support') }} · #zoitechat</span></a>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block relbar1 %}{% endblock %}
|
||||||
|
{% block relbar2 %}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="docs-layout{% if not render_sidebar %} no-sidebar{% endif %}">
|
||||||
|
{% if render_sidebar %}
|
||||||
|
<aside class="docs-sidebar" aria-label="{{ _('Documentation navigation') }}">
|
||||||
|
<details class="docs-navigation" id="docs-navigation" open>
|
||||||
|
<summary>{{ _('Documentation') }}</summary>
|
||||||
|
<div class="sidebar-content">
|
||||||
|
{% for sidebartemplate in sidebars %}
|
||||||
|
{% include sidebartemplate %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</aside>
|
||||||
|
{% endif %}
|
||||||
|
<main class="docs-content" id="main-content" tabindex="-1">
|
||||||
|
<div class="docs-location">
|
||||||
|
<nav aria-label="{{ _('Breadcrumbs') }}">
|
||||||
|
<a href="{{ pathto(docs_root)|e }}">{{ _('Documentation') }}</a>
|
||||||
|
{% for parent in parents %}
|
||||||
|
<span aria-hidden="true">/</span> <a href="{{ parent.link|e }}">{{ parent.title }}</a>
|
||||||
|
{% endfor %}
|
||||||
|
{% if pagename != docs_root %}<span aria-hidden="true">/</span> <span>{{ title|striptags|e }}</span>{% endif %}
|
||||||
|
</nav>
|
||||||
|
<span class="docs-version">{{ release|e }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% block body %}{% endblock %}
|
||||||
|
{% if prev or next %}
|
||||||
|
<nav class="page-relations" aria-label="{{ _('Related pages') }}">
|
||||||
|
<div>
|
||||||
|
{% if prev %}<small>{{ _('Previous') }}</small><a href="{{ prev.link|e }}" rel="prev">{{ prev.title }}</a>{% endif %}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{% if next %}<small>{{ _('Next') }}</small><a href="{{ next.link|e }}" rel="next">{{ next.title }}</a>{% endif %}
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
{% endif %}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block footer %}
|
||||||
|
<footer class="site-footer">
|
||||||
|
<div class="footer-grid">
|
||||||
|
<p class="footer-copy">{{ project|e }} {{ _('documentation') }} · {{ release|e }}</p>
|
||||||
|
<nav class="footer-links" aria-label="{{ _('Footer navigation') }}">
|
||||||
|
<a href="{{ pathto(docs_root)|e }}">{{ _('Contents') }}</a>
|
||||||
|
<a href="{{ zoite_atlas_url|e }}">{{ _('Atlas') }}</a>
|
||||||
|
{% if hasdoc('search') %}<a href="{{ pathto('search')|e }}">{{ _('Search') }}</a>{% endif %}
|
||||||
|
{% if hasdoc('genindex') %}<a href="{{ pathto('genindex')|e }}">{{ _('Index') }}</a>{% endif %}
|
||||||
|
<a href="#top">{{ _('Back to top') }}</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
{% endblock %}
|
||||||
Vendored
+7
@@ -0,0 +1,7 @@
|
|||||||
|
{# SPDX-License-Identifier: GPL-2.0-or-later #}
|
||||||
|
{% if display_toc %}
|
||||||
|
<nav class="local-toc" aria-label="{{ _('On this page') }}">
|
||||||
|
<h2>{{ _('On this page') }}</h2>
|
||||||
|
{{ toc }}
|
||||||
|
</nav>
|
||||||
|
{% endif %}
|
||||||
Vendored
+10
@@ -0,0 +1,10 @@
|
|||||||
|
{# SPDX-License-Identifier: GPL-2.0-or-later #}
|
||||||
|
{% if pagename != "search" %}
|
||||||
|
<form class="docs-search" action="{{ pathto('search')|e }}" method="get" role="search">
|
||||||
|
<label for="docs-search-input">{{ _('Search documentation') }}</label>
|
||||||
|
<div class="search-inputs">
|
||||||
|
<input id="docs-search-input" type="search" name="q" placeholder="{{ _('Search documentation') }}" autocomplete="off" spellcheck="false">
|
||||||
|
<button type="submit">{{ _('Search') }}</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
Vendored
+7
@@ -0,0 +1,7 @@
|
|||||||
|
[theme]
|
||||||
|
inherit = basic
|
||||||
|
stylesheet = basic.css
|
||||||
|
pygments_style = sphinx
|
||||||
|
sidebars = searchbox.html, globaltoc.html, localtoc.html, sourcelink.html
|
||||||
|
|
||||||
|
[options]
|
||||||
@@ -1,6 +1,20 @@
|
|||||||
ZoiteChat ChangeLog
|
ZoiteChat ChangeLog
|
||||||
=================
|
=================
|
||||||
|
|
||||||
|
2.19.1 (2026-09-15)
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
- Fixed ``/msg`` messages showing twice on servers using IRCv3 ``echo-message``.
|
||||||
|
- Fixed escaping a leading command slash so ``//text`` sends ``/text`` as normal chat.
|
||||||
|
- Fixed automatic away clearing when sending messages on servers using ``echo-message``.
|
||||||
|
- Fixed other users' away status not updating until rejoining a channel when ``away-notify`` updates are missed.
|
||||||
|
- Fixed private messages and ``/me`` actions being routed to the wrong window when using WeeChat's IRC relay, while preserving channel ACTION routing.
|
||||||
|
- Fixed a Flatpak startup error caused by the D-Bus service still using the old HexChat app ID.
|
||||||
|
- Fixed optional Windows 10 theme downloads in the Windows installer.
|
||||||
|
- Fixed the GTK warning when switching from channel tabs to the channel tree.
|
||||||
|
- Fixed GTK3 theme archive root detection so themes with an ``index.theme`` file are preferred correctly.
|
||||||
|
- Fixed source-build configuration when plugin AppStream metadata installation is disabled, and corrected the minimum Meson version to 0.59.
|
||||||
|
|
||||||
2.19.0 (2026-07-28)
|
2.19.0 (2026-07-28)
|
||||||
-------------------
|
-------------------
|
||||||
|
|
||||||
|
|||||||
+8
-4
@@ -99,7 +99,7 @@ trim_footnote_reference_space = True
|
|||||||
|
|
||||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||||
# a list of builtin themes.
|
# a list of builtin themes.
|
||||||
html_theme = 'basicstrap'
|
html_theme = 'zoitechat'
|
||||||
|
|
||||||
# Theme options are theme-specific and customize the look and feel of a theme
|
# Theme options are theme-specific and customize the look and feel of a theme
|
||||||
# further. For a list of options available for each theme, see the
|
# further. For a list of options available for each theme, see the
|
||||||
@@ -127,13 +127,16 @@ html_context = {
|
|||||||
'zoite_site_url': 'https://zoitechat.org/',
|
'zoite_site_url': 'https://zoitechat.org/',
|
||||||
'zoite_download_url': 'https://zoitechat.org/download.php',
|
'zoite_download_url': 'https://zoitechat.org/download.php',
|
||||||
'zoite_addons_url': 'https://zoitechat.org/addons.php',
|
'zoite_addons_url': 'https://zoitechat.org/addons.php',
|
||||||
|
'zoite_announcements_url': 'https://zoitechat.org/announcements.php',
|
||||||
|
'zoite_github_url': 'https://github.com/ZoiteChat/zoitechat',
|
||||||
|
'zoite_atlas_url': 'https://atlas.zoitechat.org/',
|
||||||
'zoite_irc_url': 'https://zoite.net/webchat/?join=%23zoitechat',
|
'zoite_irc_url': 'https://zoite.net/webchat/?join=%23zoitechat',
|
||||||
'libera_irc_url': 'https://web.libera.chat/#zoitechat',
|
'libera_irc_url': 'https://web.libera.chat/#zoitechat',
|
||||||
}
|
}
|
||||||
|
|
||||||
# The name of an image file (relative to this directory) to place at the top
|
# The name of an image file (relative to this directory) to place at the top
|
||||||
# of the sidebar.
|
# of the sidebar.
|
||||||
html_logo = 'zoitechat-logo.svg'
|
html_logo = '_static/zoitechat-logo.svg'
|
||||||
|
|
||||||
# The name of an image file (within the static path) to use as favicon of the
|
# The name of an image file (within the static path) to use as favicon of the
|
||||||
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
|
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
|
||||||
@@ -145,8 +148,9 @@ html_favicon = '_static/favicon.ico'
|
|||||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||||
html_static_path = ['_static']
|
html_static_path = ['_static']
|
||||||
|
|
||||||
# Main-site visual skin for Read the Docs/Sphinx HTML output.
|
# Local, readable assets shared by hosted and offline HTML documentation.
|
||||||
html_css_files = ['css/zoitechat-docs.css']
|
html_css_files = ['css/zoitechat-docs.css', 'css/zoitechat-site.css']
|
||||||
|
html_js_files = ['js/zoitechat-docs.js']
|
||||||
|
|
||||||
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
|
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
|
||||||
# using the given strftime format.
|
# using the given strftime format.
|
||||||
|
|||||||
@@ -52,6 +52,11 @@
|
|||||||
{
|
{
|
||||||
"name": "zoitechat",
|
"name": "zoitechat",
|
||||||
"buildsystem": "meson",
|
"buildsystem": "meson",
|
||||||
|
"run-tests": true,
|
||||||
|
"test-rule": "",
|
||||||
|
"test-commands": [
|
||||||
|
"meson test --print-errorlogs 'Emoji Data Tests' 'Emoji Font Tests'"
|
||||||
|
],
|
||||||
"config-opts": [
|
"config-opts": [
|
||||||
"-Ddbus-service-use-appid=true",
|
"-Ddbus-service-use-appid=true",
|
||||||
"-Dwith-perl=perl",
|
"-Dwith-perl=perl",
|
||||||
@@ -65,7 +70,8 @@
|
|||||||
"/share/man"
|
"/share/man"
|
||||||
],
|
],
|
||||||
"post-install": [
|
"post-install": [
|
||||||
"install -d /app/extensions"
|
"install -d /app/extensions",
|
||||||
|
"cmp ../data/fonts/NotoColorEmoji.ttf /app/share/fonts/zoitechat/NotoColorEmoji.ttf"
|
||||||
],
|
],
|
||||||
"sources": [
|
"sources": [
|
||||||
{
|
{
|
||||||
|
|||||||
+21
-2
@@ -1,6 +1,6 @@
|
|||||||
project('zoitechat', 'c',
|
project('zoitechat', 'c',
|
||||||
version: run_command('cat', 'VERSION', check: true).stdout().strip(),
|
version: run_command('cat', 'VERSION', check: true).stdout().strip(),
|
||||||
meson_version: '>= 0.55.0',
|
meson_version: '>= 0.59.0',
|
||||||
default_options: [
|
default_options: [
|
||||||
'c_std=c17',
|
'c_std=c17',
|
||||||
'buildtype=debugoptimized',
|
'buildtype=debugoptimized',
|
||||||
@@ -13,7 +13,9 @@ gnome = import('gnome')
|
|||||||
cc = meson.get_compiler('c')
|
cc = meson.get_compiler('c')
|
||||||
|
|
||||||
|
|
||||||
libgio_dep = dependency('gio-2.0', version: '>= 2.36.0')
|
# The relocatable macOS runtime sets the GIO TLS database explicitly.
|
||||||
|
libgio_dep = dependency('gio-2.0',
|
||||||
|
version: host_machine.system() == 'darwin' ? '>= 2.60.0' : '>= 2.36.0')
|
||||||
libgmodule_dep = dependency('gmodule-2.0')
|
libgmodule_dep = dependency('gmodule-2.0')
|
||||||
|
|
||||||
libcanberra_dep = dependency('libcanberra', version: '>= 0.22',
|
libcanberra_dep = dependency('libcanberra', version: '>= 0.22',
|
||||||
@@ -29,13 +31,20 @@ else
|
|||||||
required: get_option('tls'))
|
required: get_option('tls'))
|
||||||
endif
|
endif
|
||||||
|
|
||||||
|
gtk_mac_dep = disabler()
|
||||||
|
if host_machine.system() == 'darwin' and get_option('gtk-frontend')
|
||||||
|
gtk_mac_dep = dependency('gtk-mac-integration-gtk3', required: get_option('mac-integration'))
|
||||||
|
endif
|
||||||
|
|
||||||
config_h = configuration_data()
|
config_h = configuration_data()
|
||||||
|
config_h.set('HAVE_GTK_MAC', host_machine.system() == 'darwin' and gtk_mac_dep.found())
|
||||||
config_h.set_quoted('PACKAGE_VERSION', meson.project_version())
|
config_h.set_quoted('PACKAGE_VERSION', meson.project_version())
|
||||||
config_h.set_quoted('PACKAGE_NAME', meson.project_name())
|
config_h.set_quoted('PACKAGE_NAME', meson.project_name())
|
||||||
config_h.set_quoted('GETTEXT_PACKAGE', 'zoitechat')
|
config_h.set_quoted('GETTEXT_PACKAGE', 'zoitechat')
|
||||||
config_h.set_quoted('LOCALEDIR', join_paths(get_option('prefix'),
|
config_h.set_quoted('LOCALEDIR', join_paths(get_option('prefix'),
|
||||||
get_option('datadir'), 'locale'))
|
get_option('datadir'), 'locale'))
|
||||||
config_h.set10('ENABLE_NLS', true)
|
config_h.set10('ENABLE_NLS', true)
|
||||||
|
config_h.set_quoted('ZOITECHAT_DATADIR', join_paths(get_option('prefix'), get_option('datadir')))
|
||||||
|
|
||||||
# Optional features
|
# Optional features
|
||||||
config_h.set('USE_OPENSSL', libssl_dep.found())
|
config_h.set('USE_OPENSSL', libssl_dep.found())
|
||||||
@@ -98,6 +107,12 @@ global_cflags = [
|
|||||||
'-DGLIB_VERSION_MAX_ALLOWED=GLIB_VERSION_2_36',
|
'-DGLIB_VERSION_MAX_ALLOWED=GLIB_VERSION_2_36',
|
||||||
'-DGLIB_VERSION_MIN_REQUIRED=GLIB_VERSION_2_36',
|
'-DGLIB_VERSION_MIN_REQUIRED=GLIB_VERSION_2_36',
|
||||||
]
|
]
|
||||||
|
if host_machine.system() == 'darwin'
|
||||||
|
global_cflags = [
|
||||||
|
'-DGLIB_VERSION_MAX_ALLOWED=GLIB_VERSION_2_60',
|
||||||
|
'-DGLIB_VERSION_MIN_REQUIRED=GLIB_VERSION_2_60',
|
||||||
|
]
|
||||||
|
endif
|
||||||
test_cflags = [
|
test_cflags = [
|
||||||
'-funsigned-char',
|
'-funsigned-char',
|
||||||
'-Wno-conversion',
|
'-Wno-conversion',
|
||||||
@@ -158,6 +173,10 @@ foreach ldflag : test_ldflags
|
|||||||
global_ldflags += ldflag
|
global_ldflags += ldflag
|
||||||
endif
|
endif
|
||||||
endforeach
|
endforeach
|
||||||
|
if host_machine.system() == 'darwin'
|
||||||
|
# Leave room for install_name_tool to rewrite packaged dependencies.
|
||||||
|
global_ldflags += '-Wl,-headerpad_max_install_names'
|
||||||
|
endif
|
||||||
add_project_link_arguments(global_ldflags, language: 'c')
|
add_project_link_arguments(global_ldflags, language: 'c')
|
||||||
|
|
||||||
subdir('src')
|
subdir('src')
|
||||||
|
|||||||
@@ -23,6 +23,10 @@ option('appindicator', type: 'feature', value: 'auto',
|
|||||||
description: 'Use Ayatana/AppIndicator-based tray backend for GTK frontend (non-Windows only)'
|
description: 'Use Ayatana/AppIndicator-based tray backend for GTK frontend (non-Windows only)'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# macOS only; ignored by other platforms.
|
||||||
|
option('mac-integration', type: 'feature', value: 'auto',
|
||||||
|
description: 'Native macOS menu bar, Dock and application lifecycle')
|
||||||
|
|
||||||
# Install options
|
# Install options
|
||||||
option('dbus-service-use-appid', type: 'boolean', value: false,
|
option('dbus-service-use-appid', type: 'boolean', value: false,
|
||||||
description: 'Rename dbus service to match app-id, required for Flatpak'
|
description: 'Rename dbus service to match app-id, required for Flatpak'
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ BuildRequires: pkgconfig(dbus-glib-1)
|
|||||||
BuildRequires: pkgconfig(gio-2.0) >= 2.36.0
|
BuildRequires: pkgconfig(gio-2.0) >= 2.36.0
|
||||||
BuildRequires: pkgconfig(gmodule-2.0)
|
BuildRequires: pkgconfig(gmodule-2.0)
|
||||||
BuildRequires: pkgconfig(gtk+-3.0) >= 3.22
|
BuildRequires: pkgconfig(gtk+-3.0) >= 3.22
|
||||||
|
BuildRequires: pkgconfig(fontconfig) >= 2.13
|
||||||
|
BuildRequires: pkgconfig(pangoft2) >= 1.44
|
||||||
BuildRequires: pkgconfig(iso-codes)
|
BuildRequires: pkgconfig(iso-codes)
|
||||||
BuildRequires: pkgconfig(libarchive)
|
BuildRequires: pkgconfig(libarchive)
|
||||||
BuildRequires: pkgconfig(libcanberra) >= 0.22
|
BuildRequires: pkgconfig(libcanberra) >= 0.22
|
||||||
@@ -72,6 +74,9 @@ appstream-util validate-relax --nonet %{buildroot}%{_datadir}/metainfo/net.zoite
|
|||||||
appstream-util validate-relax --nonet %{buildroot}%{_datadir}/metainfo/net.zoite.Zoitechat*.metainfo.xml
|
appstream-util validate-relax --nonet %{buildroot}%{_datadir}/metainfo/net.zoite.Zoitechat*.metainfo.xml
|
||||||
|
|
||||||
xwfb-run -- /usr/bin/meson test -C %{_vpath_builddir} --num-processes %{_smp_build_ncpus} --print-errorlogs \
|
xwfb-run -- /usr/bin/meson test -C %{_vpath_builddir} --num-processes %{_smp_build_ncpus} --print-errorlogs \
|
||||||
|
"Emoji Data Tests" \
|
||||||
|
"Emoji Font Tests" \
|
||||||
|
"Emoji UI Tests" \
|
||||||
"Theme Manager Dispatch Routing Tests" \
|
"Theme Manager Dispatch Routing Tests" \
|
||||||
"Validate net.zoite.Zoitechat.desktop" \
|
"Validate net.zoite.Zoitechat.desktop" \
|
||||||
"Validate translations" \
|
"Validate translations" \
|
||||||
@@ -80,6 +85,8 @@ xwfb-run -- /usr/bin/meson test -C %{_vpath_builddir} --num-processes %{_smp_bui
|
|||||||
%files -f %{name}.lang
|
%files -f %{name}.lang
|
||||||
%license COPYING
|
%license COPYING
|
||||||
%doc readme.md troubleshooting.md
|
%doc readme.md troubleshooting.md
|
||||||
|
%{_datadir}/fonts/zoitechat/
|
||||||
|
%{_datadir}/doc/zoitechat/fonts/
|
||||||
%{_bindir}/zoitechat
|
%{_bindir}/zoitechat
|
||||||
%{_datadir}/applications/net.zoite.Zoitechat.desktop
|
%{_datadir}/applications/net.zoite.Zoitechat.desktop
|
||||||
%{_datadir}/dbus-1/services/org.zoitechat.service.service
|
%{_datadir}/dbus-1/services/org.zoitechat.service.service
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
brew "meson"
|
||||||
|
brew "ninja"
|
||||||
|
brew "pkgconf"
|
||||||
|
brew "python"
|
||||||
|
brew "gettext"
|
||||||
|
brew "gtk+3"
|
||||||
|
brew "gtk-mac-integration"
|
||||||
|
brew "libarchive"
|
||||||
|
brew "openssl@3"
|
||||||
|
brew "lua"
|
||||||
|
brew "librsvg"
|
||||||
|
brew "glib-networking"
|
||||||
|
brew "adwaita-icon-theme"
|
||||||
|
brew "enchant"
|
||||||
|
brew "ca-certificates"
|
||||||
@@ -0,0 +1,335 @@
|
|||||||
|
# Native macOS build and distribution
|
||||||
|
|
||||||
|
This builds GTK3 with the native Quartz backend, macOS menus/Dock integration,
|
||||||
|
UserNotifications, translations, bundled emoji, TLS, image loaders and supported
|
||||||
|
native plugins. It creates a self-contained `ZoiteChat.app`; end users do not
|
||||||
|
need Homebrew, XQuartz or Python. No system-wide ZoiteChat installation occurs.
|
||||||
|
|
||||||
|
The source changes are conditional on Darwin. Windows and Linux retain their
|
||||||
|
existing notification backends and feature defaults. This adds a separate
|
||||||
|
macOS workflow; it does not replace existing workflows.
|
||||||
|
|
||||||
|
## 1. Install build prerequisites
|
||||||
|
|
||||||
|
Use a Mac with a currently supported macOS release and native Homebrew. On Apple
|
||||||
|
Silicon, use ARM Homebrew (`/opt/homebrew`), not Intel Homebrew under Rosetta.
|
||||||
|
Intel builds use Intel Homebrew (`/usr/local`). Install Xcode command-line tools
|
||||||
|
and Homebrew from https://brew.sh if absent. Full current Xcode is recommended
|
||||||
|
for distribution and needed to meet the current App Store submission SDK rules.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
xcode-select --install
|
||||||
|
# Run once if the command-line tools are not installed; let installation finish.
|
||||||
|
|
||||||
|
cd /path/to/zoitechat
|
||||||
|
brew bundle --file packaging/macos/Brewfile
|
||||||
|
python3 packaging/macos/build.py
|
||||||
|
open dist/ZoiteChat.app
|
||||||
|
```
|
||||||
|
|
||||||
|
The build compiles the app, runs its Meson tests, stages an installation within
|
||||||
|
`build-macos/macos-stage`, bundles and rewrites every non-system Mach-O dependency,
|
||||||
|
creates the icon and plist, audits the result, and signs it ad hoc for local use.
|
||||||
|
It refuses to overwrite an existing output app. For another build, choose a new
|
||||||
|
output path, or explicitly delete your old generated app before rebuilding:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
python3 packaging/macos/build.py --output dist/ZoiteChat-test2.app --build-number 2
|
||||||
|
```
|
||||||
|
|
||||||
|
The same build directory can be reconfigured. Use a different build directory
|
||||||
|
for another architecture or the experimental store variant. Do not change
|
||||||
|
architectures within one Meson build directory.
|
||||||
|
|
||||||
|
Homebrew packages change over time; this is a repeatable procedure, not a
|
||||||
|
bit-for-bit reproducible dependency lock. The bundle records dependency versions
|
||||||
|
in `Contents/Resources/homebrew-dependencies.json`. Retain that record and the
|
||||||
|
release's source commit. For a reproducible release, archive the exact dependency
|
||||||
|
sources and build them in a controlled prefix, or maintain pinned Homebrew formulae.
|
||||||
|
|
||||||
|
## 2. Verify portability and behavior
|
||||||
|
|
||||||
|
```sh
|
||||||
|
python3 -m unittest discover -s packaging/macos/tests -v
|
||||||
|
python3 packaging/macos/bundle.py audit dist/ZoiteChat.app
|
||||||
|
python3 packaging/macos/smoke.py dist/ZoiteChat.app
|
||||||
|
```
|
||||||
|
|
||||||
|
The smoke check copies the app to a path containing spaces, verifies its signature,
|
||||||
|
runs `--version`, and keeps an isolated GUI process alive for ten seconds without
|
||||||
|
connecting to IRC. It tests startup, not interaction or long-running stability.
|
||||||
|
|
||||||
|
The audit verifies the entire dependency closure, including GTK image/input
|
||||||
|
modules, GIO TLS, Enchant providers, OpenSSL providers and ZoiteChat plugins. It
|
||||||
|
fails on missing libraries, external symlinks/search paths, architecture mismatches
|
||||||
|
or unresolved load commands. The minimum macOS version in the plist is derived
|
||||||
|
from **all** bundled binaries, including Homebrew libraries. Lowering that plist
|
||||||
|
value does not make newer dependency bottles run on an older Mac.
|
||||||
|
|
||||||
|
Before releasing, test on a second Mac without Homebrew installed:
|
||||||
|
|
||||||
|
- Launch from Finder and from a relocated/read-only location; verify Dock and
|
||||||
|
menu bar, preferences persistence, dark/light themes and normal shutdown.
|
||||||
|
- Connect with verified TLS, exercise SASL authentication and reconnect.
|
||||||
|
- Send Unicode/emoji, render SVG/PNG/JPEG icons/images, and test HTTPS image previews.
|
||||||
|
- Verify clipboard shortcuts, input methods, notifications/permission denial,
|
||||||
|
detach/reattach tabs, focus changes, unread highlights and notification clicks.
|
||||||
|
- Send/receive/resume DCC, write downloads and logs, import/export themes/settings.
|
||||||
|
- Test FiSH, Lua and DCC checksum plugins in the direct build.
|
||||||
|
- Exercise accessibility with VoiceOver and keyboard-only navigation.
|
||||||
|
|
||||||
|
Direct builds include Lua, FiSH and checksum, and retain the plugin loader.
|
||||||
|
Python/Perl are explicitly not bundled: reliable redistribution requires packaging
|
||||||
|
and auditing their interpreter runtimes, standard libraries and extensions.
|
||||||
|
Linux `/proc`/PCI sysinfo is not built on macOS. These are macOS build choices;
|
||||||
|
other platforms' plugin defaults are unchanged. macOS uses the existing password
|
||||||
|
storage fallback; this patch does not add a Keychain backend. Audit credentials
|
||||||
|
storage before adopting a stronger macOS security claim.
|
||||||
|
|
||||||
|
Desktop URL registration (`irc://` in Finder/browser) is not advertised by the
|
||||||
|
plist; command-line IRC URLs continue to work. A native open-URL event handler
|
||||||
|
and single-instance forwarding need separate integration before registering those
|
||||||
|
schemes. AppIndicator and Linux D-Bus scripting/single-instance support are not
|
||||||
|
used on macOS.
|
||||||
|
|
||||||
|
## 3. Sign and notarize for direct downloads
|
||||||
|
|
||||||
|
Local ad-hoc builds are for testing; they are not notarized releases. For public
|
||||||
|
release, enroll in Apple's Developer Program, install your **Developer ID
|
||||||
|
Application** certificate with its private key, then sign using that identity.
|
||||||
|
Use your actual certificate identity in place of the example:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
python3 packaging/macos/bundle.py sign dist/ZoiteChat.app \
|
||||||
|
--identity 'Developer ID Application: YOUR NAME (TEAMID)' \
|
||||||
|
--entitlements packaging/macos/direct.entitlements
|
||||||
|
|
||||||
|
# Store credentials once in your login Keychain; enter your own values interactively.
|
||||||
|
xcrun notarytool store-credentials ZoiteChat-notary
|
||||||
|
|
||||||
|
ditto -c -k --keepParent dist/ZoiteChat.app dist/ZoiteChat-notarization.zip
|
||||||
|
xcrun notarytool submit dist/ZoiteChat-notarization.zip \
|
||||||
|
--keychain-profile ZoiteChat-notary --wait
|
||||||
|
|
||||||
|
# Only after notarization has succeeded:
|
||||||
|
xcrun stapler staple dist/ZoiteChat.app
|
||||||
|
xcrun stapler validate dist/ZoiteChat.app
|
||||||
|
spctl --assess --type execute --verbose=2 dist/ZoiteChat.app
|
||||||
|
|
||||||
|
ditto -c -k --keepParent dist/ZoiteChat.app dist/ZoiteChat-macos.zip
|
||||||
|
```
|
||||||
|
|
||||||
|
Signing runs inside-out, verifies the final bundle and enables hardened runtime.
|
||||||
|
It does not use `codesign --deep` to sign. `--deep` is used only for verification.
|
||||||
|
The direct entitlement allows users' independently signed native plugins; it
|
||||||
|
relaxes library validation, not the entire hardened runtime. No JIT or executable
|
||||||
|
memory entitlements are added. Do not change bundle contents after signing; signing
|
||||||
|
again is required after any code/resource edit. Stapling is Apple's supported
|
||||||
|
post-signing operation.
|
||||||
|
|
||||||
|
Publish exact corresponding source for ZoiteChat and satisfy the applicable
|
||||||
|
licenses of each bundled dependency. Notices and dependency metadata are included,
|
||||||
|
but those alone do not satisfy every source-distribution obligation. Review the
|
||||||
|
notarization log if rejected; the script does not silently bypass Gatekeeper.
|
||||||
|
|
||||||
|
### Automate signing and notarization with GitHub Actions
|
||||||
|
|
||||||
|
The workflow now has a separate signed/notarized release job for both
|
||||||
|
architectures. It uses Developer ID distribution for direct downloads, not Mac
|
||||||
|
App Store signing. You can manage it from Windows/Linux; GitHub's macOS runners
|
||||||
|
perform all signing, notarization and stapling.
|
||||||
|
|
||||||
|
Prepare these repository Actions secrets under **Settings → Secrets and
|
||||||
|
variables → Actions**, or store them as environment secrets in **macos-release**.
|
||||||
|
The certificate setting names are unchanged from the original instructions.
|
||||||
|
The three `APPLE_API_*` settings are new notarization credentials:
|
||||||
|
|
||||||
|
| Name | Type | Value |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `MACOS_CERTIFICATE_P12_BASE64` | Secret | Base64-encoded Developer ID Application `.p12`, including its private key |
|
||||||
|
| `MACOS_CERTIFICATE_PASSWORD` | Secret | Password used to export that `.p12` |
|
||||||
|
| `MACOS_SIGNING_IDENTITY` | Variable, or Secret | Exact `Developer ID Application: YOUR NAME (TEAMID)` identity; the matching certificate's SHA-1 fingerprint is also accepted |
|
||||||
|
| `APPLE_API_KEY_P8` | Secret | Entire plain-text App Store Connect **Team API key** `.p8` file, including PEM header/footer and newlines |
|
||||||
|
| `APPLE_API_KEY_ID` | Secret | That API key's Key ID |
|
||||||
|
| `APPLE_API_ISSUER_ID` | Secret | The Team API key's Issuer ID UUID; this is **not** your Apple Team ID |
|
||||||
|
|
||||||
|
Repository variables are under the **Variables** tab. If the identity is entered
|
||||||
|
as both a variable and a secret, the variable takes precedence. No separate
|
||||||
|
keychain password is needed; the job generates a temporary random password.
|
||||||
|
The `.p8` is the notarization API key; it does not replace the signing `.p12`.
|
||||||
|
No Apple ID password or app-specific password is used by this configuration.
|
||||||
|
|
||||||
|
To obtain the API key, open **App Store Connect → Users and Access → Integrations
|
||||||
|
→ App Store Connect API → Team Keys → Generate API Key**. Choose **Developer**
|
||||||
|
access for notarization. If API access is not enabled, the Account Holder must
|
||||||
|
request it first. Download the `.p8` and record the Key ID and Issuer ID. Use a
|
||||||
|
**Team** key with this workflow: individual API keys use different authentication
|
||||||
|
and are not supported by this issuer-based configuration. Apple allows the
|
||||||
|
private key download only once, so retain your copy securely.
|
||||||
|
|
||||||
|
For the certificate, export the Developer ID Application identity (certificate
|
||||||
|
plus private key) from Keychain Access as a password-protected `.p12`.
|
||||||
|
If you generated the CSR with OpenSSL on Linux/WSL, you can also create it there.
|
||||||
|
Assuming Apple's downloaded DER certificate has been saved as
|
||||||
|
`developerID_application.cer`, and you retained `zoitechat-signing.key`:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
openssl x509 -inform DER -in developerID_application.cer \
|
||||||
|
-out zoitechat-signing.pem
|
||||||
|
openssl pkcs12 -export -inkey zoitechat-signing.key \
|
||||||
|
-in zoitechat-signing.pem -out ZoiteChat-signing.p12
|
||||||
|
|
||||||
|
# Enter the existing private-key password, then choose the .p12 export password.
|
||||||
|
# Copy the resulting Base64 text into MACOS_CERTIFICATE_P12_BASE64:
|
||||||
|
openssl base64 -A -in ZoiteChat-signing.p12 -out ZoiteChat-signing.p12.base64
|
||||||
|
|
||||||
|
# Read the certificate's commonName to obtain MACOS_SIGNING_IDENTITY:
|
||||||
|
openssl x509 -in zoitechat-signing.pem -noout -subject -nameopt multiline
|
||||||
|
```
|
||||||
|
|
||||||
|
Upload only the public CSR to Apple. Put the `.p12` Base64, export password and
|
||||||
|
API credentials in GitHub settings, never in the repository or build artifacts.
|
||||||
|
|
||||||
|
Once these patches are committed to `master` and the settings are ready:
|
||||||
|
|
||||||
|
1. Open **Actions → macOS build → Run workflow**.
|
||||||
|
2. Select **master** and enable **Sign with Developer ID and notarize using
|
||||||
|
release credentials**.
|
||||||
|
3. Run the workflow. It creates separate Apple Silicon and Intel release jobs.
|
||||||
|
4. After success, download `ZoiteChat-macos-arm64-signed-notarized` or
|
||||||
|
`ZoiteChat-macos-x86_64-signed-notarized`. The ZIP inside contains the signed,
|
||||||
|
stapled app. Actions artifacts wrap it in their own download archive.
|
||||||
|
|
||||||
|
A push of a `v*` tag also triggers the signed release jobs automatically, so wait
|
||||||
|
until credentials are configured before pushing a release tag. Normal branch
|
||||||
|
pushes, pull requests and manual runs with the checkbox off continue to build
|
||||||
|
ad-hoc test artifacts without any signing secrets. Manual credentialed releases
|
||||||
|
are restricted to `master`; pull requests cannot invoke the release job.
|
||||||
|
Missing/malformed release settings fail before the Homebrew/build steps rather
|
||||||
|
than producing an unsigned artifact marked as a release.
|
||||||
|
|
||||||
|
The release jobs use the GitHub environment **macos-release**. Repository secrets
|
||||||
|
work with it; environment secrets can override them. Configure environment
|
||||||
|
branch/tag restrictions or required reviewers if that fits your repository's
|
||||||
|
release process. This workflow does not automatically create a GitHub Release,
|
||||||
|
publish to zoitechat.org, or upload to App Store Connect; it produces downloadable
|
||||||
|
signed/notarized artifacts for you to publish.
|
||||||
|
|
||||||
|
The job imports the `.p12` into a temporary keychain, installs Apple's public
|
||||||
|
Developer ID intermediate certificates, verifies a valid matching identity,
|
||||||
|
and signs inside-out with hardened runtime and the direct plugin entitlement.
|
||||||
|
It uses the existing bundle audit and then submits a ZIP with `notarytool`.
|
||||||
|
After Apple reports **Accepted**, it staples the ticket to the `.app`, verifies
|
||||||
|
stapling/signatures and Gatekeeper acceptance, and creates a **new** final ZIP.
|
||||||
|
Private credential files and the temporary keychain are cleaned up on normal
|
||||||
|
success/failure paths; hosted runner VMs are also discarded after the job.
|
||||||
|
|
||||||
|
The default Apple wait limit is **60 minutes** within a **180-minute** release
|
||||||
|
job. A rejected, failed or still-pending submission produces no final release
|
||||||
|
ZIP. The `ZoiteChat-macos-<arch>-notarization-log` artifact retains the submission
|
||||||
|
ID, status and Apple's available diagnostic log. First submissions can take
|
||||||
|
longer; check the saved ID with `notarytool info` and your API credentials. An
|
||||||
|
accepted submission still requires stapling and repackaging before publication;
|
||||||
|
rerunning this workflow starts a new build and submission. To increase the wait
|
||||||
|
limit, update `--notary-timeout` and allow adequate job timeout for build plus wait.
|
||||||
|
|
||||||
|
Both architecture builds receive the same bundle build number for one workflow
|
||||||
|
run. The API credentials are supplied only to the config-check/release steps,
|
||||||
|
not to Homebrew, compiler tests, ordinary builds or pull-request jobs.
|
||||||
|
|
||||||
|
## 4. Mac App Store: experimental groundwork, not submission-ready
|
||||||
|
|
||||||
|
A working direct-download app is not sufficient for App Store submission. This
|
||||||
|
repository inherits GPL-2.0-or-later code from XChat/HexChat and other contributors.
|
||||||
|
App Store terms and GPL obligations require a licensing review; do not assume
|
||||||
|
that signing, a custom EULA, or relicensing only your new changes resolves rights
|
||||||
|
to inherited code. This patch changes no licenses or copyright permissions.
|
||||||
|
|
||||||
|
An isolated sandbox-test build is provided:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
python3 packaging/macos/build.py --store-experimental \
|
||||||
|
--build-dir build-macos-store --output dist/ZoiteChat-store-test.app \
|
||||||
|
--build-number 1
|
||||||
|
|
||||||
|
# Local sandbox experimentation (not an uploadable distribution signature):
|
||||||
|
python3 packaging/macos/bundle.py sign dist/ZoiteChat-store-test.app \
|
||||||
|
--identity - --entitlements packaging/macos/store-experimental.entitlements
|
||||||
|
open dist/ZoiteChat-store-test.app
|
||||||
|
```
|
||||||
|
|
||||||
|
The separate store variant excludes loadable plugins and scripting runtimes.
|
||||||
|
The template grants sandboxed client/server networking, user-selected files and
|
||||||
|
Downloads. The direct version retains its plugin functionality. This is an
|
||||||
|
explicit experimental variant, not a change to Windows/Linux or direct builds.
|
||||||
|
|
||||||
|
Before a store package can be regarded as a release candidate:
|
||||||
|
|
||||||
|
1. Resolve the inherited GPL/dependency-license distribution question and review
|
||||||
|
Apple's current SDK, content/chat moderation, privacy and accessibility rules.
|
||||||
|
2. Register `org.Zoitechat` (or change the plist identifier consistently if already
|
||||||
|
owned by another developer), create App Store Connect metadata, and choose a
|
||||||
|
unique incrementing build number. Use a stable release, not a `-dev` snapshot.
|
||||||
|
3. Test sandboxed settings/logs, notifications, SASL/TLS, HTTPS and DCC on a real
|
||||||
|
Mac. GTK dialogs are not assumed to grant Powerbox access. Add native
|
||||||
|
`NSOpenPanel`/`NSSavePanel` integration and security-scoped bookmarks for
|
||||||
|
persistent access to user-selected files. The entitlement template alone does
|
||||||
|
not solve persistent file access. `/EXEC`, custom external commands, user
|
||||||
|
plugin loading and arbitrary external paths cannot be promised under sandbox.
|
||||||
|
4. Add required privacy manifest declarations based on an actual API audit of the
|
||||||
|
app and dependencies; do not submit an invented empty compliance declaration.
|
||||||
|
5. Obtain matching App Store distribution application/installer certificates and
|
||||||
|
a matching provisioning profile. Generate team-specific signing entitlements
|
||||||
|
from the profile with Xcode; do not use Developer ID or the generic local-test
|
||||||
|
template as a store distribution identity/profile.
|
||||||
|
6. Embed the matching profile as `Contents/embedded.provisionprofile` **before**
|
||||||
|
final signing. Sign embedded libraries with the distribution identity, then
|
||||||
|
the app with its profile-authorized entitlements (use `bundle.py sign` with
|
||||||
|
that identity and generated entitlement file). Package with Apple's
|
||||||
|
`productbuild`, validate and upload with Transporter/App Store Connect, then
|
||||||
|
test the App Store processed build via TestFlight before submission.
|
||||||
|
|
||||||
|
Example **after those prerequisites are satisfied**:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Use the actual prepared profile and team-specific entitlements:
|
||||||
|
cp /path/to/ZoiteChat.provisionprofile \
|
||||||
|
dist/ZoiteChat-store-test.app/Contents/embedded.provisionprofile
|
||||||
|
python3 packaging/macos/bundle.py sign dist/ZoiteChat-store-test.app \
|
||||||
|
--identity 'YOUR APP STORE APPLICATION CERTIFICATE IDENTITY' \
|
||||||
|
--entitlements /path/to/ZoiteChat-distribution.entitlements
|
||||||
|
productbuild --component dist/ZoiteChat-store-test.app /Applications \
|
||||||
|
--sign 'YOUR APP STORE INSTALLER CERTIFICATE IDENTITY' dist/ZoiteChat-store.pkg
|
||||||
|
```
|
||||||
|
|
||||||
|
No App Store approval, sandbox compatibility or legal compatibility is asserted.
|
||||||
|
For an initial macOS release, the signed/notarized direct build is the practical
|
||||||
|
path while those issues are resolved.
|
||||||
|
|
||||||
|
## 5. CI and architecture support
|
||||||
|
|
||||||
|
`.github/workflows/macos-build.yml` adds independent Apple Silicon and Intel jobs.
|
||||||
|
Ordinary jobs install prerequisites, run tests, create/audit/ad-hoc-sign the
|
||||||
|
bundle, check relocated startup and upload test ZIPs. They need no Apple secrets.
|
||||||
|
The separate release jobs add Developer ID signing, Apple notarization, stapling
|
||||||
|
and final verification. Only their successful output is named signed/notarized.
|
||||||
|
|
||||||
|
Homebrew may build unsupported Intel bottles from source; that can be slower.
|
||||||
|
The workflow uses native runners and does not claim a universal application.
|
||||||
|
Do not merge only the main executable with `lipo`: a universal release must merge
|
||||||
|
and validate every bundled dylib, module and plugin plus matching resources.
|
||||||
|
Signing must happen after that merge. Publish separate architecture downloads
|
||||||
|
until a full universal dependency pipeline is available.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- GTK on macOS: https://www.gtk.org/docs/installations/macos
|
||||||
|
- GTK macOS backend: https://docs.gtk.org/gtk3/macos.html
|
||||||
|
- Apple distribution signing: https://developer.apple.com/documentation/xcode/creating-distribution-signed-code-for-the-mac
|
||||||
|
- Apple packaging: https://developer.apple.com/documentation/xcode/packaging-mac-software-for-distribution
|
||||||
|
- GitHub certificate import: https://docs.github.com/en/actions/how-tos/deploy/deploy-to-third-party-platforms/sign-xcode-applications
|
||||||
|
- Apple notarization authentication: https://developer.apple.com/documentation/technotes/tn3147-migrating-to-the-latest-notarization-tool
|
||||||
|
- App Store Connect API keys: https://developer.apple.com/help/app-store-connect/get-started/app-store-connect-api/
|
||||||
|
- Apple review rules (especially 1.2 and 2.4.5): https://developer.apple.com/app-store/review/guidelines/
|
||||||
|
- Apple sandbox file access: https://developer.apple.com/documentation/security/accessing-files-from-the-macos-app-sandbox
|
||||||
|
- FSF discussion of GPL and App Store terms (historical, not a current legal determination): https://www.fsf.org/blogs/licensing/more-about-the-app-store-gpl-enforcement
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Compile, test, stage and package ZoiteChat on the host Mac."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import platform
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from bundle import ROOT, package, run
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument("--build-dir", type=Path, default=Path("build-macos"))
|
||||||
|
parser.add_argument("--output", type=Path, default=Path("dist/ZoiteChat.app"))
|
||||||
|
parser.add_argument("--build-number", default="1")
|
||||||
|
parser.add_argument("--store-experimental", action="store_true", help="Build a separate sandbox test variant without loadable plugins; not submission approval")
|
||||||
|
args = parser.parse_args()
|
||||||
|
if platform.system() != "Darwin":
|
||||||
|
parser.error("Build on macOS; cross-compiling from Linux is not supported")
|
||||||
|
if args.output.exists():
|
||||||
|
parser.error("Output already exists; use a new --output or explicitly remove the old app")
|
||||||
|
build = args.build_dir.resolve()
|
||||||
|
if build == ROOT or ROOT.is_relative_to(build):
|
||||||
|
parser.error("Build directory must not contain the source checkout")
|
||||||
|
run("xcrun", "--find", "clang")
|
||||||
|
env = os.environ.copy()
|
||||||
|
prefixes = [Path(run("brew", "--prefix", name)) for name in ("gettext", "openssl@3", "libarchive", "lua", "gtk+3", "gtk-mac-integration")]
|
||||||
|
brew = Path(run("brew", "--prefix"))
|
||||||
|
# Keg-only gettext, OpenSSL and libarchive are not necessarily in PATH/pkg-config.
|
||||||
|
env["PATH"] = os.pathsep.join([str(p / "bin") for p in prefixes] + [str(brew / "bin"), env.get("PATH", "")])
|
||||||
|
env["PKG_CONFIG_PATH"] = os.pathsep.join([str(p / "lib/pkgconfig") for p in prefixes] + [str(brew / "lib/pkgconfig"), str(brew / "share/pkgconfig"), env.get("PKG_CONFIG_PATH", "")])
|
||||||
|
# Native dependency bottles determine the true deployment target; the bundle
|
||||||
|
# auditor derives the final minimum rather than promising an arbitrary old OS.
|
||||||
|
env.setdefault("MACOSX_DEPLOYMENT_TARGET", "11.0")
|
||||||
|
backend = run("pkg-config", "--variable=targets", "gtk+-3.0", env=env).split()
|
||||||
|
if "quartz" not in backend:
|
||||||
|
parser.error(f"GTK must provide the native Quartz backend, got {backend}")
|
||||||
|
# Do not silently select an XQuartz GTK stack or mix Intel/ARM Homebrew prefixes.
|
||||||
|
machine = platform.machine()
|
||||||
|
if machine not in ("arm64", "x86_64"):
|
||||||
|
parser.error(f"Unsupported architecture: {machine}")
|
||||||
|
options = [
|
||||||
|
"--prefix=/", "--libdir=lib", "--buildtype=release", "-Dgtk-frontend=true",
|
||||||
|
"-Dtext-frontend=false", "-Dmac-integration=enabled", "-Ddbus=disabled",
|
||||||
|
"-Dlibcanberra=disabled", "-Dappindicator=disabled", "-Dinstall-appdata=false",
|
||||||
|
"-Dwith-sysinfo=false", "-Dwith-python=false", "-Dwith-perl=false",
|
||||||
|
"-Dwith-lua=lua", "-Dwith-fishlim=true", "-Dwith-checksum=true",
|
||||||
|
"-Dplugin=" + ("false" if args.store_experimental else "true"),
|
||||||
|
]
|
||||||
|
if (build / "meson-private/coredata.dat").exists():
|
||||||
|
options.insert(0, "--reconfigure")
|
||||||
|
subprocess.run(["meson", "setup", str(build), str(ROOT), *options], env=env, check=True)
|
||||||
|
subprocess.run(["meson", "compile", "-C", str(build)], env=env, check=True)
|
||||||
|
subprocess.run(["meson", "test", "-C", str(build), "--print-errorlogs"], env=env, check=True)
|
||||||
|
stage = build / "macos-stage"
|
||||||
|
if stage.exists():
|
||||||
|
shutil.rmtree(stage)
|
||||||
|
subprocess.run(["meson", "install", "-C", str(build), "--destdir", str(stage)], env=env, check=True)
|
||||||
|
package(stage, args.output.resolve(), args.build_number)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
main()
|
||||||
|
except (RuntimeError, subprocess.CalledProcessError, OSError) as error:
|
||||||
|
sys.exit(f"macOS build failed: {error}")
|
||||||
@@ -0,0 +1,358 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Build and audit a relocatable GTK3 application. No third-party Python packages."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import platform
|
||||||
|
import plistlib
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
MACH_MAGIC = {bytes.fromhex(h) for h in ("feedface", "cefaedfe", "feedfacf", "cffaedfe", "cafebabe", "bebafeca", "cafebabf", "bfbafeca")}
|
||||||
|
SYSTEM_ROOTS = ("/usr/lib/", "/System/Library/")
|
||||||
|
|
||||||
|
|
||||||
|
def run(*args: str | Path, env: dict[str, str] | None = None) -> str:
|
||||||
|
return subprocess.check_output([str(arg) for arg in args], env=env, text=True).strip()
|
||||||
|
|
||||||
|
|
||||||
|
def is_macho(path: Path) -> bool:
|
||||||
|
if not path.is_file() or path.is_symlink():
|
||||||
|
return False
|
||||||
|
with path.open("rb") as stream:
|
||||||
|
return stream.read(4) in MACH_MAGIC
|
||||||
|
|
||||||
|
|
||||||
|
def dependencies(path: Path) -> list[str]:
|
||||||
|
# Paths may contain spaces; splitting on whitespace would corrupt them.
|
||||||
|
return re.findall(r"^\s+(.+?) \(compatibility version", run("otool", "-L", path), re.M)
|
||||||
|
|
||||||
|
|
||||||
|
def rpaths(path: Path) -> list[str]:
|
||||||
|
return re.findall(r"cmd LC_RPATH\s+cmdsize \d+\s+path (.+?) \(offset", run("otool", "-l", path))
|
||||||
|
|
||||||
|
|
||||||
|
def dylib_id(path: Path) -> str | None:
|
||||||
|
lines = run("otool", "-D", path).splitlines()
|
||||||
|
return lines[1].strip() if len(lines) > 1 else None
|
||||||
|
|
||||||
|
|
||||||
|
def system_library(name: str) -> bool:
|
||||||
|
return name.startswith(SYSTEM_ROOTS)
|
||||||
|
|
||||||
|
|
||||||
|
def expand_path(name: str, loader: Path, executable: Path) -> Path:
|
||||||
|
return Path(name.replace("@loader_path", str(loader.parent)).replace("@executable_path", str(executable.parent)))
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_dependency(name: str, loader: Path, executable: Path, search: list[Path]) -> Path:
|
||||||
|
if name.startswith("@rpath/"):
|
||||||
|
suffix = name[len("@rpath/"):]
|
||||||
|
candidates = [directory / suffix for directory in search]
|
||||||
|
else:
|
||||||
|
candidates = [expand_path(name, loader, executable)]
|
||||||
|
for candidate in candidates:
|
||||||
|
if candidate.is_absolute() and candidate.is_file():
|
||||||
|
return candidate.resolve()
|
||||||
|
raise RuntimeError(f"Cannot resolve {name!r} from {loader}; searched {candidates}")
|
||||||
|
|
||||||
|
|
||||||
|
class Relocator:
|
||||||
|
def __init__(self, app: Path, executable: Path):
|
||||||
|
self.app = app
|
||||||
|
self.executable = executable
|
||||||
|
self.frameworks = app / "Contents/Frameworks"
|
||||||
|
self.frameworks.mkdir(parents=True)
|
||||||
|
self.sources: dict[Path, Path] = {}
|
||||||
|
self.names: dict[str, Path] = {}
|
||||||
|
self.visited: set[Path] = set()
|
||||||
|
|
||||||
|
def copy_dependency(self, source: Path) -> Path:
|
||||||
|
source = source.resolve()
|
||||||
|
if ".framework/" in str(source):
|
||||||
|
raise RuntimeError(f"Third-party frameworks require separate packaging support: {source}")
|
||||||
|
if source in self.sources:
|
||||||
|
return self.sources[source]
|
||||||
|
existing = self.names.get(source.name)
|
||||||
|
if existing and existing != source:
|
||||||
|
raise RuntimeError(f"Conflicting library basename: {existing} and {source}")
|
||||||
|
target = self.frameworks / source.name
|
||||||
|
shutil.copy2(source, target)
|
||||||
|
target.chmod(target.stat().st_mode | 0o200)
|
||||||
|
self.sources[source] = target
|
||||||
|
self.names[source.name] = source
|
||||||
|
return target
|
||||||
|
|
||||||
|
def relocate(self, source: Path, target: Path, inherited: list[Path] | None = None) -> None:
|
||||||
|
source = source.resolve()
|
||||||
|
if target in self.visited:
|
||||||
|
return
|
||||||
|
self.visited.add(target)
|
||||||
|
local = [expand_path(name, source, self.executable) for name in rpaths(source)]
|
||||||
|
search = local + (inherited or [])
|
||||||
|
identity = dylib_id(source)
|
||||||
|
# Keep the existing signature allocation while editing: stripping it
|
||||||
|
# first can leave __LINKEDIT padding that install_name_tool rejects.
|
||||||
|
# The final inside-out codesign --force pass replaces stale signatures.
|
||||||
|
edits: list[str | Path] = []
|
||||||
|
for name in dependencies(source):
|
||||||
|
if name == identity or system_library(name):
|
||||||
|
continue
|
||||||
|
dependency = resolve_dependency(name, source, self.executable, search)
|
||||||
|
destination = self.copy_dependency(dependency)
|
||||||
|
relative = os.path.relpath(destination, target.parent)
|
||||||
|
edits += ["-change", name, "@loader_path/" + relative]
|
||||||
|
self.relocate(dependency, destination, search)
|
||||||
|
if identity:
|
||||||
|
edits += ["-id", "@rpath/" + target.name]
|
||||||
|
for name in sorted(set(rpaths(target))):
|
||||||
|
edits += ["-delete_rpath", name]
|
||||||
|
if edits:
|
||||||
|
run("install_name_tool", *edits, target)
|
||||||
|
|
||||||
|
|
||||||
|
def copy_tree(source: Path, target: Path) -> None:
|
||||||
|
if source.is_dir():
|
||||||
|
# Dereference symlinks: never leave links into the builder's Cellar.
|
||||||
|
shutil.copytree(source, target, dirs_exist_ok=True, symlinks=False)
|
||||||
|
|
||||||
|
|
||||||
|
def audit(app: Path) -> dict[str, object]:
|
||||||
|
binary = app / "Contents/MacOS/zoitechat"
|
||||||
|
architectures = set(run("lipo", "-archs", binary).split())
|
||||||
|
minimum = (11, 0, 0)
|
||||||
|
entries = []
|
||||||
|
for path in sorted(app.rglob("*")):
|
||||||
|
if path.is_symlink():
|
||||||
|
resolved = path.resolve()
|
||||||
|
if not resolved.is_relative_to(app.resolve()):
|
||||||
|
raise RuntimeError(f"External symlink: {path} -> {resolved}")
|
||||||
|
if not is_macho(path):
|
||||||
|
continue
|
||||||
|
if not architectures.issubset(set(run("lipo", "-archs", path).split())):
|
||||||
|
raise RuntimeError(f"Architecture mismatch: {path}")
|
||||||
|
identity = dylib_id(path)
|
||||||
|
for name in dependencies(path):
|
||||||
|
if name == identity or system_library(name):
|
||||||
|
continue
|
||||||
|
if not name.startswith("@loader_path/"):
|
||||||
|
raise RuntimeError(f"Non-relocatable dependency: {path}: {name}")
|
||||||
|
target = expand_path(name, path, binary).resolve()
|
||||||
|
if not target.is_relative_to(app.resolve()) or not target.is_file():
|
||||||
|
raise RuntimeError(f"Missing or external dependency: {path}: {name}")
|
||||||
|
if rpaths(path):
|
||||||
|
raise RuntimeError(f"Unexpected runtime search paths: {path}")
|
||||||
|
load_commands = run("otool", "-l", path)
|
||||||
|
versions = re.findall(r"\bminos (\d+(?:\.\d+){0,2})", load_commands)
|
||||||
|
versions += re.findall(r"cmd LC_VERSION_MIN_MACOSX\s+cmdsize \d+\s+version (\d+(?:\.\d+){0,2})", load_commands)
|
||||||
|
if not versions:
|
||||||
|
raise RuntimeError(f"Missing deployment version: {path}")
|
||||||
|
for version in versions:
|
||||||
|
parts = tuple(int(value) for value in version.split("."))
|
||||||
|
minimum = max(minimum, parts + (0,) * (3 - len(parts)))
|
||||||
|
entries.append({"path": str(path.relative_to(app))})
|
||||||
|
return {"architectures": sorted(architectures), "minimum_macos": ".".join(map(str, minimum if minimum[2] else minimum[:2])), "mach_o": entries}
|
||||||
|
|
||||||
|
|
||||||
|
def package(stage: Path, app: Path, build_number: str) -> None:
|
||||||
|
if not re.fullmatch(r"[1-9]\d*(?:\.\d+){0,2}", build_number):
|
||||||
|
raise RuntimeError("Build number must be a positive Apple CFBundleVersion (for example 1 or 12.3)")
|
||||||
|
if app.exists():
|
||||||
|
raise RuntimeError(f"Output already exists; choose another --output or remove it explicitly: {app}")
|
||||||
|
app.mkdir(parents=True)
|
||||||
|
resources = app / "Contents/Resources"
|
||||||
|
resources.mkdir(parents=True)
|
||||||
|
binary = app / "Contents/MacOS/zoitechat"
|
||||||
|
binary.parent.mkdir()
|
||||||
|
shutil.copy2(stage / "bin/zoitechat", binary)
|
||||||
|
copy_tree(stage / "share", resources / "share")
|
||||||
|
copy_tree(stage / "lib/zoitechat/plugins", resources / "lib/zoitechat/plugins")
|
||||||
|
relocator = Relocator(app, stage / "bin/zoitechat")
|
||||||
|
seeds: list[tuple[Path, Path]] = [(stage / "bin/zoitechat", binary)]
|
||||||
|
for source in (stage / "lib/zoitechat/plugins").glob("*"):
|
||||||
|
if is_macho(source):
|
||||||
|
seeds.append((source, resources / "lib/zoitechat/plugins" / source.name))
|
||||||
|
|
||||||
|
prefixes = {formula: Path(run("brew", "--prefix", formula)) for formula in (
|
||||||
|
"gtk+3", "gdk-pixbuf", "glib", "glib-networking", "librsvg", "enchant", "adwaita-icon-theme", "hicolor-icon-theme", "gsettings-desktop-schemas", "ca-certificates", "openssl@3", "lua", "libarchive", "gtk-mac-integration")}
|
||||||
|
# Runtime-loaded code is not visible in the main executable's otool output.
|
||||||
|
module_groups = [
|
||||||
|
(prefixes["gdk-pixbuf"] / "lib/gdk-pixbuf-2.0/2.10.0/loaders", "lib/gdk-pixbuf-2.0/2.10.0/loaders"),
|
||||||
|
(prefixes["librsvg"] / "lib/gdk-pixbuf-2.0/2.10.0/loaders", "lib/gdk-pixbuf-2.0/2.10.0/loaders"),
|
||||||
|
(prefixes["gtk+3"] / "lib/gtk-3.0/3.0.0/immodules", "lib/gtk-3.0/3.0.0/immodules"),
|
||||||
|
(prefixes["glib-networking"] / "lib/gio/modules", "lib/gio/modules"),
|
||||||
|
(prefixes["enchant"] / "lib/enchant-2", "lib/enchant-2"),
|
||||||
|
(prefixes["openssl@3"] / "lib/ossl-modules", "lib/ossl-modules"),
|
||||||
|
]
|
||||||
|
for source_dir, relative in module_groups:
|
||||||
|
target_dir = resources / relative
|
||||||
|
target_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
for source in sorted(source_dir.glob("*")):
|
||||||
|
if source.suffix not in (".so", ".dylib") or not source.is_file():
|
||||||
|
continue
|
||||||
|
target = target_dir / source.name
|
||||||
|
if target.exists() and target.read_bytes() != source.read_bytes():
|
||||||
|
raise RuntimeError(f"Conflicting module: {target}")
|
||||||
|
shutil.copy2(source.resolve(), target)
|
||||||
|
seeds.append((source.resolve(), target))
|
||||||
|
enchant = (prefixes["enchant"] / "lib/libenchant-2.dylib").resolve()
|
||||||
|
# Register Enchant at Resources/lib so its public prefix relocation API
|
||||||
|
# finds Resources/lib/enchant-2 and Resources/share/enchant.
|
||||||
|
enchant_alias = resources / "lib/libenchant-2.dylib"
|
||||||
|
shutil.copy2(enchant, enchant_alias)
|
||||||
|
relocator.sources[enchant] = enchant_alias
|
||||||
|
relocator.names[enchant.name] = enchant
|
||||||
|
seeds.append((enchant, enchant_alias))
|
||||||
|
for formula, prefix in prefixes.items():
|
||||||
|
for name in ("icons", "glib-2.0/schemas", "locale", "enchant", "hunspell"):
|
||||||
|
copy_tree(prefix / "share" / name, resources / "share" / name)
|
||||||
|
ca = prefixes["ca-certificates"] / "share/ca-certificates/cacert.pem"
|
||||||
|
if not ca.is_file():
|
||||||
|
raise RuntimeError(f"CA bundle not found: {ca}")
|
||||||
|
(resources / "certs").mkdir()
|
||||||
|
shutil.copy2(ca, resources / "certs/ca-bundle.crt")
|
||||||
|
schemas = resources / "share/glib-2.0/schemas"
|
||||||
|
run(prefixes["glib"] / "bin/glib-compile-schemas", schemas)
|
||||||
|
pixbuf_modules = sorted((resources / "lib/gdk-pixbuf-2.0/2.10.0/loaders").glob("*"))
|
||||||
|
if not any("svg" in path.name for path in pixbuf_modules):
|
||||||
|
raise RuntimeError("SVG loader missing: install librsvg")
|
||||||
|
if list((resources / "lib/zoitechat/plugins").glob("fishlim.*")) and not list((resources / "lib/ossl-modules").glob("legacy.*")):
|
||||||
|
raise RuntimeError("FiSH needs the OpenSSL legacy provider in the bundle")
|
||||||
|
if not list((resources / "lib/gio/modules").glob("*tls*")):
|
||||||
|
raise RuntimeError("GIO TLS backend missing: install glib-networking")
|
||||||
|
# Query original binaries before relocation, then rewrite only the cached paths.
|
||||||
|
pixbuf_query = prefixes["gdk-pixbuf"] / "bin/gdk-pixbuf-query-loaders"
|
||||||
|
if not pixbuf_query.exists():
|
||||||
|
pixbuf_query = prefixes["gdk-pixbuf"] / "lib/gdk-pixbuf-2.0/gdk-pixbuf-query-loaders"
|
||||||
|
for tool, module_dir, cache in (
|
||||||
|
(pixbuf_query, "lib/gdk-pixbuf-2.0/2.10.0/loaders", "pixbuf-loaders.cache"),
|
||||||
|
(prefixes["gtk+3"] / "bin/gtk-query-immodules-3.0", "lib/gtk-3.0/3.0.0/immodules", "gtk-immodules.cache"),
|
||||||
|
):
|
||||||
|
originals = [(source, target) for source, target in seeds if target.parent == resources / module_dir]
|
||||||
|
# Never call a query tool with zero arguments: it would scan global modules.
|
||||||
|
data = run(tool, *(source for source, _ in originals)) if originals else "# No external input-method modules\n"
|
||||||
|
for source, target in originals:
|
||||||
|
data = data.replace(str(source), "@ZOITECHAT_RESOURCES@/" + str(target.relative_to(resources)))
|
||||||
|
(resources / cache).write_text(data + "\n")
|
||||||
|
# The private font map loads the bundled Noto font; ordinary GTK fonts stay local.
|
||||||
|
(resources / "fonts.conf").write_text('''<?xml version="1.0"?>
|
||||||
|
<!DOCTYPE fontconfig SYSTEM "urn:fontconfig:fonts.dtd">
|
||||||
|
<fontconfig>
|
||||||
|
<dir>/System/Library/Fonts</dir><dir>/Library/Fonts</dir><dir>~/Library/Fonts</dir>
|
||||||
|
<cachedir prefix="xdg">fontconfig</cachedir>
|
||||||
|
<alias><family>sans-serif</family><prefer><family>Helvetica</family></prefer></alias>
|
||||||
|
<alias><family>monospace</family><prefer><family>Menlo</family></prefer></alias>
|
||||||
|
</fontconfig>
|
||||||
|
''')
|
||||||
|
(resources / "zoitechat-bundle").write_text("1\n")
|
||||||
|
licenses = resources / "licenses"
|
||||||
|
licenses.mkdir()
|
||||||
|
shutil.copy2(ROOT / "COPYING", licenses / "ZoiteChat-COPYING")
|
||||||
|
# Preserve the notices shipped with every installed Homebrew dependency.
|
||||||
|
# brew deps traverses dependencies by default; --recursive is not an option.
|
||||||
|
wanted = set(prefixes)
|
||||||
|
for formula in prefixes:
|
||||||
|
wanted.update(run("brew", "deps", "--formula", formula).splitlines())
|
||||||
|
installed = [item for item in json.loads(run("brew", "info", "--json=v2", "--installed"))["formulae"]
|
||||||
|
if item["name"] in wanted]
|
||||||
|
for formula in installed:
|
||||||
|
prefix = Path(run("brew", "--prefix", formula["name"]))
|
||||||
|
notices = [p for p in prefix.iterdir() if p.is_file() and re.match(r"(?i)(copying|license|copyright|notice)", p.name)]
|
||||||
|
if notices:
|
||||||
|
target = licenses / formula["name"].replace("/", "-")
|
||||||
|
target.mkdir(exist_ok=True)
|
||||||
|
for notice in notices:
|
||||||
|
shutil.copy2(notice, target / notice.name)
|
||||||
|
(resources / "homebrew-dependencies.json").write_text(json.dumps(installed, indent=2) + "\n")
|
||||||
|
for source, target in seeds:
|
||||||
|
relocator.relocate(source, target)
|
||||||
|
report = audit(app)
|
||||||
|
(resources / "bundle-audit.json").write_text(json.dumps(report, indent=2) + "\n")
|
||||||
|
# Use the actual highest deployment target of the executable and all dylibs.
|
||||||
|
# Setting an older plist version cannot make newer Homebrew bottles compatible.
|
||||||
|
version = (ROOT / "VERSION").read_text().strip()
|
||||||
|
release = re.match(r"\d+\.\d+\.\d+", version)
|
||||||
|
if not release:
|
||||||
|
raise RuntimeError(f"Unsupported release version: {version}")
|
||||||
|
plist = {
|
||||||
|
"CFBundleIdentifier": "org.Zoitechat", "CFBundleExecutable": "zoitechat",
|
||||||
|
"CFBundleName": "ZoiteChat", "CFBundleDisplayName": "ZoiteChat",
|
||||||
|
"CFBundlePackageType": "APPL", "CFBundleShortVersionString": release.group(),
|
||||||
|
"CFBundleVersion": build_number, "CFBundleIconFile": "ZoiteChat.icns",
|
||||||
|
"LSMinimumSystemVersion": report["minimum_macos"], "NSHighResolutionCapable": True,
|
||||||
|
"NSPrincipalClass": "NSApplication",
|
||||||
|
"LSApplicationCategoryType": "public.app-category.social-networking",
|
||||||
|
"DTSDKName": "macosx" + run("xcrun", "--sdk", "macosx", "--show-sdk-version"),
|
||||||
|
"DTSDKBuild": run("xcrun", "--sdk", "macosx", "--show-sdk-build-version"),
|
||||||
|
}
|
||||||
|
with (app / "Contents/Info.plist").open("wb") as stream:
|
||||||
|
plistlib.dump(plist, stream)
|
||||||
|
iconset = app.parent / (app.stem + ".iconset")
|
||||||
|
if iconset.exists():
|
||||||
|
raise RuntimeError(f"Icon staging directory already exists: {iconset}")
|
||||||
|
iconset.mkdir()
|
||||||
|
try:
|
||||||
|
# Rasterize the original vector at 1024px, then generate every icon size.
|
||||||
|
large = iconset / "source.png"
|
||||||
|
run(prefixes["librsvg"] / "bin/rsvg-convert", "-w", "1024", "-h", "1024", "-o", large, ROOT / "data/icons/zoitechat.svg")
|
||||||
|
for size in (16, 32, 128, 256, 512):
|
||||||
|
for scale in (1, 2):
|
||||||
|
name = f"icon_{size}x{size}" + ("@2x" if scale == 2 else "") + ".png"
|
||||||
|
run("sips", "-z", str(size * scale), str(size * scale), large, "--out", iconset / name)
|
||||||
|
large.unlink()
|
||||||
|
run("iconutil", "-c", "icns", iconset, "-o", resources / "ZoiteChat.icns")
|
||||||
|
finally:
|
||||||
|
shutil.rmtree(iconset)
|
||||||
|
# Local builds need an ad-hoc signature after install_name_tool changes.
|
||||||
|
sign(app, "-", None)
|
||||||
|
print(f"Created {app}: {', '.join(report['architectures'])}, macOS {report['minimum_macos']}+")
|
||||||
|
|
||||||
|
|
||||||
|
def sign(app: Path, identity: str, entitlements: Path | None, *, keychain: Path | None = None) -> None:
|
||||||
|
audit(app)
|
||||||
|
binaries = [p for p in app.rglob("*") if is_macho(p) and p != app / "Contents/MacOS/zoitechat"]
|
||||||
|
options = [] if identity == "-" else ["--timestamp", "--options", "runtime"]
|
||||||
|
if keychain is not None:
|
||||||
|
options += ["--keychain", str(keychain)]
|
||||||
|
for binary in sorted(binaries, key=lambda p: len(p.parts), reverse=True):
|
||||||
|
run("codesign", "--force", "--sign", identity, *options, binary)
|
||||||
|
main_options = ["--entitlements", str(entitlements)] if entitlements else []
|
||||||
|
run("codesign", "--force", "--sign", identity, *options, *main_options, app)
|
||||||
|
run("codesign", "--verify", "--deep", "--strict", "--verbose=2", app)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
sub = parser.add_subparsers(dest="action", required=True)
|
||||||
|
create = sub.add_parser("create")
|
||||||
|
create.add_argument("--stage", type=Path, required=True)
|
||||||
|
create.add_argument("--app", type=Path, required=True)
|
||||||
|
create.add_argument("--build-number", default="1")
|
||||||
|
verify = sub.add_parser("audit")
|
||||||
|
verify.add_argument("app", type=Path)
|
||||||
|
signing = sub.add_parser("sign")
|
||||||
|
signing.add_argument("app", type=Path)
|
||||||
|
signing.add_argument("--identity", required=True)
|
||||||
|
signing.add_argument("--entitlements", type=Path)
|
||||||
|
args = parser.parse_args()
|
||||||
|
if platform.system() != "Darwin":
|
||||||
|
parser.error("This tool requires macOS and the Xcode command-line tools")
|
||||||
|
if args.action == "create":
|
||||||
|
package(args.stage.resolve(), args.app.resolve(), args.build_number)
|
||||||
|
elif args.action == "audit":
|
||||||
|
print(json.dumps(audit(args.app.resolve()), indent=2))
|
||||||
|
else:
|
||||||
|
sign(args.app.resolve(), args.identity, args.entitlements)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
main()
|
||||||
|
except (RuntimeError, subprocess.CalledProcessError, OSError) as error:
|
||||||
|
sys.exit(f"macOS packaging failed: {error}")
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0"><dict>
|
||||||
|
<!-- User-installed native plugins need library validation disabled. -->
|
||||||
|
<key>com.apple.security.cs.disable-library-validation</key><true/>
|
||||||
|
</dict></plist>
|
||||||
@@ -0,0 +1,220 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Developer ID sign, notarize and staple a built app using CI environment secrets."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import base64
|
||||||
|
import binascii
|
||||||
|
from contextlib import contextmanager
|
||||||
|
from dataclasses import dataclass
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import platform
|
||||||
|
import re
|
||||||
|
import secrets
|
||||||
|
import shlex
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import urllib.request
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from bundle import sign
|
||||||
|
|
||||||
|
REQUIRED = (
|
||||||
|
"MACOS_CERTIFICATE_P12_BASE64", "MACOS_CERTIFICATE_PASSWORD",
|
||||||
|
"MACOS_SIGNING_IDENTITY", "APPLE_API_KEY_P8", "APPLE_API_KEY_ID", "APPLE_API_ISSUER_ID",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(repr=False)
|
||||||
|
class Credentials:
|
||||||
|
certificate: bytes
|
||||||
|
password: str
|
||||||
|
identity: str
|
||||||
|
api_key: str
|
||||||
|
key_id: str
|
||||||
|
issuer: str
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_environment(cls) -> Credentials:
|
||||||
|
missing = [name for name in REQUIRED if not os.environ.get(name, "").strip()]
|
||||||
|
if missing:
|
||||||
|
raise RuntimeError("Missing release settings: " + ", ".join(missing))
|
||||||
|
try:
|
||||||
|
certificate = base64.b64decode("".join(os.environ[REQUIRED[0]].split()), validate=True)
|
||||||
|
except (binascii.Error, ValueError):
|
||||||
|
raise RuntimeError("MACOS_CERTIFICATE_P12_BASE64 must contain Base64-encoded .p12 bytes") from None
|
||||||
|
if not certificate:
|
||||||
|
raise RuntimeError("Signing certificate is empty")
|
||||||
|
identity = os.environ["MACOS_SIGNING_IDENTITY"].strip()
|
||||||
|
if not (identity.startswith("Developer ID Application:") or re.fullmatch(r"[a-fA-F0-9]{40}", identity)):
|
||||||
|
raise RuntimeError("MACOS_SIGNING_IDENTITY must identify a Developer ID Application certificate")
|
||||||
|
api_key = os.environ["APPLE_API_KEY_P8"].strip()
|
||||||
|
if not (api_key.startswith("-----BEGIN PRIVATE KEY-----") and api_key.endswith("-----END PRIVATE KEY-----")):
|
||||||
|
raise RuntimeError("APPLE_API_KEY_P8 must contain the complete, plain-text .p8 private key")
|
||||||
|
key_id = os.environ["APPLE_API_KEY_ID"].strip()
|
||||||
|
if not re.fullmatch(r"[A-Za-z0-9]{6,32}", key_id):
|
||||||
|
raise RuntimeError("APPLE_API_KEY_ID must be an App Store Connect key ID")
|
||||||
|
try:
|
||||||
|
issuer = str(uuid.UUID(os.environ["APPLE_API_ISSUER_ID"].strip()))
|
||||||
|
except ValueError:
|
||||||
|
raise RuntimeError("APPLE_API_ISSUER_ID must be the Team API key issuer UUID") from None
|
||||||
|
return cls(certificate, os.environ["MACOS_CERTIFICATE_PASSWORD"], identity, api_key, key_id, issuer)
|
||||||
|
|
||||||
|
|
||||||
|
def execute(args: list[str | Path], operation: str, *, check: bool = True) -> subprocess.CompletedProcess[str]:
|
||||||
|
# Do not print argv or CalledProcessError: security commands contain passwords.
|
||||||
|
try:
|
||||||
|
result = subprocess.run([str(arg) for arg in args], capture_output=True, text=True, check=False)
|
||||||
|
except OSError:
|
||||||
|
raise RuntimeError(f"{operation}: could not start the required tool") from None
|
||||||
|
if check and result.returncode != 0:
|
||||||
|
raise RuntimeError(f"{operation} failed (exit {result.returncode})")
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def write_private(path: Path, data: bytes) -> None:
|
||||||
|
with path.open("xb") as stream:
|
||||||
|
path.chmod(0o600)
|
||||||
|
stream.write(data)
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_identity(output: str, requested: str) -> str:
|
||||||
|
for fingerprint, name in re.findall(r'\b([A-Fa-f0-9]{40})\s+"([^"\n]+)"', output):
|
||||||
|
if name.startswith("Developer ID Application:") and (requested == name or requested.upper() == fingerprint.upper()):
|
||||||
|
return fingerprint
|
||||||
|
raise RuntimeError("Imported .p12 has no valid Developer ID Application identity matching MACOS_SIGNING_IDENTITY; check its private key, password, expiry and certificate chain")
|
||||||
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def signing_keychain(credentials: Credentials):
|
||||||
|
original = shlex.split(execute(["security", "list-keychains", "-d", "user"], "Read keychain search list").stdout)
|
||||||
|
with tempfile.TemporaryDirectory(prefix="zoitechat-signing-", dir=os.environ.get("RUNNER_TEMP")) as temporary:
|
||||||
|
directory = Path(temporary)
|
||||||
|
keychain = directory / "signing.keychain-db"
|
||||||
|
keychain_password = secrets.token_urlsafe(32)
|
||||||
|
certificate = directory / "signing.p12"
|
||||||
|
api_key = directory / "notary.p8"
|
||||||
|
write_private(certificate, credentials.certificate)
|
||||||
|
write_private(api_key, (credentials.api_key + "\n").encode())
|
||||||
|
try:
|
||||||
|
execute(["security", "create-keychain", "-p", keychain_password, keychain], "Create signing keychain")
|
||||||
|
execute(["security", "set-keychain-settings", "-lut", "21600", keychain], "Configure signing keychain")
|
||||||
|
execute(["security", "unlock-keychain", "-p", keychain_password, keychain], "Unlock signing keychain")
|
||||||
|
# Include both public Developer ID intermediates so signing does not
|
||||||
|
# depend on the runner having cached the leaf certificate's issuer.
|
||||||
|
for name in ("DeveloperIDCA.cer", "DeveloperIDG2CA.cer"):
|
||||||
|
intermediate = directory / name
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen("https://www.apple.com/certificateauthority/" + name, timeout=60) as response:
|
||||||
|
write_private(intermediate, response.read())
|
||||||
|
except OSError:
|
||||||
|
raise RuntimeError("Download of Apple's public Developer ID intermediate failed") from None
|
||||||
|
execute(["security", "import", intermediate, "-k", keychain], "Import Developer ID intermediate")
|
||||||
|
execute(["security", "import", certificate, "-P", credentials.password,
|
||||||
|
"-k", keychain, "-T", "/usr/bin/codesign", "-T", "/usr/bin/security"], "Import signing .p12")
|
||||||
|
execute(["security", "set-key-partition-list", "-S", "apple-tool:,apple:",
|
||||||
|
"-k", keychain_password, keychain], "Allow non-interactive code signing")
|
||||||
|
execute(["security", "list-keychains", "-d", "user", "-s", keychain, *original], "Add temporary signing keychain")
|
||||||
|
identities = execute(["security", "find-identity", "-v", "-p", "codesigning", keychain], "Find signing identity").stdout
|
||||||
|
yield keychain, api_key, resolve_identity(identities, credentials.identity)
|
||||||
|
finally:
|
||||||
|
# Hosted runners are discarded too, but clean up on every ordinary
|
||||||
|
# success/failure path and restore the original search list.
|
||||||
|
execute(["security", "list-keychains", "-d", "user", "-s", *original], "Restore keychain search list", check=False)
|
||||||
|
if keychain.exists():
|
||||||
|
result = execute(["security", "delete-keychain", keychain], "Delete signing keychain", check=False)
|
||||||
|
if result.returncode:
|
||||||
|
print("Warning: keychain deletion failed; temporary files will still be removed.", file=sys.stderr)
|
||||||
|
|
||||||
|
|
||||||
|
def decode_response(result: subprocess.CompletedProcess[str], operation: str) -> dict:
|
||||||
|
try:
|
||||||
|
response = json.loads(result.stdout)
|
||||||
|
except (json.JSONDecodeError, TypeError):
|
||||||
|
raise RuntimeError(f"{operation} did not return valid JSON (exit {result.returncode})") from None
|
||||||
|
if not isinstance(response, dict):
|
||||||
|
raise RuntimeError(f"{operation} returned an unexpected response")
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
def save_json(path: Path, response: dict) -> None:
|
||||||
|
path.write_text(json.dumps(response, indent=2) + "\n")
|
||||||
|
|
||||||
|
|
||||||
|
def notarize(app: Path, output: Path, diagnostics: Path, credentials: Credentials, api_key: Path, timeout: str) -> None:
|
||||||
|
diagnostics.mkdir(parents=True, exist_ok=True)
|
||||||
|
authentication = ["--key", str(api_key), "--key-id", credentials.key_id, "--issuer", credentials.issuer]
|
||||||
|
with tempfile.TemporaryDirectory(prefix="zoitechat-notary-", dir=os.environ.get("RUNNER_TEMP")) as temporary:
|
||||||
|
submission_zip = Path(temporary) / "ZoiteChat-submission.zip"
|
||||||
|
execute(["ditto", "-c", "-k", "--keepParent", app, submission_zip], "Package notarization submission")
|
||||||
|
result = execute(["xcrun", "notarytool", "submit", submission_zip, *authentication,
|
||||||
|
"--output-format", "json"], "Submit notarization", check=False)
|
||||||
|
submission = decode_response(result, "Notarization submission")
|
||||||
|
save_json(diagnostics / "submission.json", submission)
|
||||||
|
if result.returncode or not submission.get("id"):
|
||||||
|
raise RuntimeError("Apple rejected the notarization submission; see submission.json")
|
||||||
|
try:
|
||||||
|
submission_id = str(uuid.UUID(submission["id"]))
|
||||||
|
except (ValueError, TypeError, AttributeError):
|
||||||
|
raise RuntimeError("Apple returned an invalid notarization submission ID") from None
|
||||||
|
print(f"Notarization submitted: {submission_id}", flush=True)
|
||||||
|
waited = execute(["xcrun", "notarytool", "wait", submission_id, *authentication,
|
||||||
|
"--timeout", timeout, "--output-format", "json"], "Wait for notarization", check=False)
|
||||||
|
try:
|
||||||
|
status = decode_response(waited, "Notarization wait")
|
||||||
|
except RuntimeError:
|
||||||
|
status = {"id": submission_id, "status": "Unknown", "exit_code": waited.returncode}
|
||||||
|
save_json(diagnostics / "status.json", status)
|
||||||
|
# Apple can provide a diagnostic log after processing. Fetch it for both
|
||||||
|
# successful and rejected submissions; never include credential files.
|
||||||
|
execute(["xcrun", "notarytool", "log", submission_id, *authentication,
|
||||||
|
diagnostics / "notary-log.json"], "Fetch notarization log", check=False)
|
||||||
|
if waited.returncode or status.get("status") != "Accepted":
|
||||||
|
raise RuntimeError("Notarization was not accepted or exceeded the wait limit; see the notarization diagnostic artifact. No release ZIP will be published")
|
||||||
|
execute(["xcrun", "stapler", "staple", app], "Staple notarization ticket")
|
||||||
|
execute(["xcrun", "stapler", "validate", app], "Validate notarization ticket")
|
||||||
|
execute(["codesign", "--verify", "--deep", "--strict", "--verbose=2", app], "Verify stapled app signature")
|
||||||
|
execute(["spctl", "--assess", "--type", "execute", "--verbose=2", app], "Assess Gatekeeper acceptance")
|
||||||
|
output.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
# Repack only after stapling. The submission ZIP is not the distributable ZIP.
|
||||||
|
execute(["ditto", "-c", "-k", "--keepParent", app, output], "Package notarized release")
|
||||||
|
print(f"Signed and notarized release: {output}", flush=True)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument("--check-config", action="store_true", help="Validate release settings without accessing Apple or a keychain")
|
||||||
|
parser.add_argument("--app", type=Path, default=Path("dist/ZoiteChat.app"))
|
||||||
|
parser.add_argument("--output", type=Path, default=Path("dist/ZoiteChat-signed-notarized.zip"))
|
||||||
|
parser.add_argument("--diagnostics", type=Path, default=Path("dist/notarization"))
|
||||||
|
parser.add_argument("--notary-timeout", default="60m")
|
||||||
|
args = parser.parse_args()
|
||||||
|
credentials = Credentials.from_environment()
|
||||||
|
if args.check_config:
|
||||||
|
print("Release signing and notarization settings are present and correctly formatted.")
|
||||||
|
return
|
||||||
|
if platform.system() != "Darwin":
|
||||||
|
parser.error("Signing/notarization requires the macOS runner and Xcode command-line tools")
|
||||||
|
if not args.app.is_dir() or args.output.exists():
|
||||||
|
parser.error("Provide a built .app and an output ZIP path that does not already exist")
|
||||||
|
if not re.fullmatch(r"[1-9]\d*[smh]", args.notary_timeout):
|
||||||
|
parser.error("Use a positive notarization timeout, such as 60m")
|
||||||
|
with signing_keychain(credentials) as (keychain, api_key, identity):
|
||||||
|
sign(args.app.resolve(), identity, Path(__file__).with_name("direct.entitlements"), keychain=keychain)
|
||||||
|
notarize(args.app.resolve(), args.output.resolve(), args.diagnostics.resolve(), credentials, api_key, args.notary_timeout)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
main()
|
||||||
|
except (RuntimeError, OSError):
|
||||||
|
# RuntimeError messages are authored above and do not contain secret argv.
|
||||||
|
error = sys.exc_info()[1]
|
||||||
|
if isinstance(error, RuntimeError):
|
||||||
|
sys.exit(f"macOS release failed: {error}")
|
||||||
|
sys.exit("macOS release failed: file or network operation failed")
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
sys.exit("macOS release failed: code signing or bundle audit failed")
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Run the relocated bundle with temporary settings and no IRC auto-connect."""
|
||||||
|
from __future__ import annotations
|
||||||
|
import argparse
|
||||||
|
from pathlib import Path
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument("app", type=Path)
|
||||||
|
args = parser.parse_args()
|
||||||
|
with tempfile.TemporaryDirectory(prefix="zoitechat-macos-smoke-") as directory:
|
||||||
|
# Include spaces to exercise dyld and GTK module-cache path handling.
|
||||||
|
app = Path(directory) / "Relocated App" / "ZoiteChat.app"
|
||||||
|
shutil.copytree(args.app.resolve(), app)
|
||||||
|
binary = app / "Contents/MacOS/zoitechat"
|
||||||
|
subprocess.run(["codesign", "--verify", "--deep", "--strict", str(app)], check=True)
|
||||||
|
subprocess.run([str(binary), "--version"], check=True, timeout=30)
|
||||||
|
config = Path(directory) / "test-config"
|
||||||
|
with (Path(directory) / "startup.log").open("w+") as log:
|
||||||
|
process = subprocess.Popen([str(binary), "--no-auto", "--cfgdir", str(config)], stdout=log, stderr=log)
|
||||||
|
try:
|
||||||
|
process.wait(timeout=10)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
# Stop the isolated process after checking for early startup failure.
|
||||||
|
process.terminate()
|
||||||
|
try:
|
||||||
|
process.wait(timeout=10)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
process.kill()
|
||||||
|
process.wait()
|
||||||
|
raise RuntimeError("GUI startup process did not terminate")
|
||||||
|
else:
|
||||||
|
log.seek(0)
|
||||||
|
raise RuntimeError(f"GUI exited before smoke interval (code {process.returncode}):\n{log.read()}")
|
||||||
|
log.seek(0)
|
||||||
|
messages = log.read()
|
||||||
|
print(messages)
|
||||||
|
for failure in ("Library not loaded:", "Symbol not found:", "cannot open display", "Cannot load bundled HTTPS trust database", "Bundled emoji font is missing", "Cannot relocate", "Error loading plugin:"):
|
||||||
|
if failure.lower() in messages.lower():
|
||||||
|
raise RuntimeError(f"Startup failure found: {failure}")
|
||||||
|
print("Relocated bundle stayed running for 10 seconds; manual interaction testing is still required.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0"><dict>
|
||||||
|
<key>com.apple.security.app-sandbox</key><true/>
|
||||||
|
<key>com.apple.security.network.client</key><true/>
|
||||||
|
<!-- DCC listening sockets and the optional ident service. -->
|
||||||
|
<key>com.apple.security.network.server</key><true/>
|
||||||
|
<key>com.apple.security.files.user-selected.read-write</key><true/>
|
||||||
|
<key>com.apple.security.files.downloads.read-write</key><true/>
|
||||||
|
</dict></plist>
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
"""Dependency parsing, relocation guards and release audit tests without a Mac SDK."""
|
||||||
|
from pathlib import Path
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||||
|
import bundle
|
||||||
|
|
||||||
|
|
||||||
|
class BundleTests(unittest.TestCase):
|
||||||
|
@unittest.skipUnless(sys.platform == "darwin", "requires Apple's Mach-O tools")
|
||||||
|
def test_signed_binary_relocation_and_resigning(self):
|
||||||
|
with tempfile.TemporaryDirectory(prefix="zoitechat relocation ") as temporary:
|
||||||
|
directory = Path(temporary)
|
||||||
|
library_source = directory / "answer.c"
|
||||||
|
library_source.write_text("int answer(void) { return 42; }\n")
|
||||||
|
library = directory / "libanswer.dylib"
|
||||||
|
main_source = directory / "main.c"
|
||||||
|
main_source.write_text(
|
||||||
|
"#include <stdio.h>\n"
|
||||||
|
"extern int answer(void);\n"
|
||||||
|
"int main(void) { printf(\"%d\\n\", answer()); return 0; }\n"
|
||||||
|
)
|
||||||
|
executable = directory / "original"
|
||||||
|
bundle.run("xcrun", "clang", "-dynamiclib", "-Wl,-headerpad_max_install_names",
|
||||||
|
"-Wl,-install_name," + str(library), library_source, "-o", library)
|
||||||
|
bundle.run("xcrun", "clang", "-Wl,-headerpad_max_install_names",
|
||||||
|
"-Wl,-rpath," + str(directory), main_source, library, "-o", executable)
|
||||||
|
for binary in (library, executable):
|
||||||
|
bundle.run("codesign", "--force", "--sign", "-", binary)
|
||||||
|
app = directory / "Test.app"
|
||||||
|
target = app / "Contents/MacOS/zoitechat"
|
||||||
|
target.parent.mkdir(parents=True)
|
||||||
|
shutil.copy2(executable, target)
|
||||||
|
relocator = bundle.Relocator(app, executable)
|
||||||
|
relocator.relocate(executable, target)
|
||||||
|
# Remove the original dependency so execution proves relocation.
|
||||||
|
library.unlink()
|
||||||
|
for binary in (app / "Contents/Frameworks/libanswer.dylib", target):
|
||||||
|
bundle.run("codesign", "--force", "--sign", "-", binary)
|
||||||
|
bundle.run("codesign", "--verify", "--strict", binary)
|
||||||
|
self.assertEqual(bundle.run(target), "42")
|
||||||
|
self.assertEqual(len(bundle.audit(app)["mach_o"]), 2)
|
||||||
|
|
||||||
|
def test_otool_preserves_paths_with_spaces(self):
|
||||||
|
output = "app:\n\t/opt/Build Dependencies/lib/liba.dylib (compatibility version 1.0.0, current version 1.0.0)\n\t/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1.0.0)"
|
||||||
|
with patch.object(bundle, "run", return_value=output):
|
||||||
|
self.assertEqual(bundle.dependencies(Path("app")), ["/opt/Build Dependencies/lib/liba.dylib", "/usr/lib/libSystem.B.dylib"])
|
||||||
|
|
||||||
|
def test_lc_rpaths_preserve_spaces(self):
|
||||||
|
with patch.object(bundle, "run", return_value="cmd LC_RPATH\n cmdsize 48\n path @loader_path/../Build Dependencies (offset 12)\n"):
|
||||||
|
self.assertEqual(bundle.rpaths(Path("app")), ["@loader_path/../Build Dependencies"])
|
||||||
|
|
||||||
|
def test_inherited_rpath_resolution(self):
|
||||||
|
with tempfile.TemporaryDirectory() as temporary:
|
||||||
|
directory = Path(temporary)
|
||||||
|
library = directory / "with spaces/libtest.dylib"
|
||||||
|
library.parent.mkdir()
|
||||||
|
library.write_bytes(b"library")
|
||||||
|
self.assertEqual(bundle.resolve_dependency("@rpath/libtest.dylib", directory / "libparent.dylib", directory / "app", [library.parent]), library.resolve())
|
||||||
|
|
||||||
|
def test_unresolved_rpath_fails(self):
|
||||||
|
with self.assertRaisesRegex(RuntimeError, "Cannot resolve"):
|
||||||
|
bundle.resolve_dependency("@rpath/missing.dylib", Path("/tmp/parent"), Path("/tmp/app"), [])
|
||||||
|
|
||||||
|
def test_conflicting_basename_fails(self):
|
||||||
|
with tempfile.TemporaryDirectory() as temporary:
|
||||||
|
directory = Path(temporary)
|
||||||
|
sources = [directory / name / "libtest.dylib" for name in ("one", "two")]
|
||||||
|
for source in sources:
|
||||||
|
source.parent.mkdir()
|
||||||
|
source.write_bytes(b"library")
|
||||||
|
relocator = bundle.Relocator(directory / "App.app", directory / "executable")
|
||||||
|
relocator.copy_dependency(sources[0])
|
||||||
|
with self.assertRaisesRegex(RuntimeError, "Conflicting library basename"):
|
||||||
|
relocator.copy_dependency(sources[1])
|
||||||
|
|
||||||
|
def test_audit_rejects_external_symlink(self):
|
||||||
|
with tempfile.TemporaryDirectory() as temporary:
|
||||||
|
app = Path(temporary) / "App.app"
|
||||||
|
app.mkdir()
|
||||||
|
(app / "outside").symlink_to("/usr/lib")
|
||||||
|
with patch.object(bundle, "run", return_value="arm64"), self.assertRaisesRegex(RuntimeError, "External symlink"):
|
||||||
|
bundle.audit(app)
|
||||||
|
|
||||||
|
def test_audit_raises_minimum_for_dependencies(self):
|
||||||
|
with tempfile.TemporaryDirectory() as temporary:
|
||||||
|
app = Path(temporary) / "App.app"
|
||||||
|
binary = app / "Contents/MacOS/zoitechat"
|
||||||
|
library = app / "Contents/Frameworks/libtest.dylib"
|
||||||
|
for path in (binary, library):
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
path.write_bytes(bytes.fromhex("cffaedfe") + b"payload")
|
||||||
|
def fake_run(*args):
|
||||||
|
if args[0] == "lipo":
|
||||||
|
return "arm64"
|
||||||
|
if args[1] == "-D":
|
||||||
|
return str(args[2]) + ("\n@rpath/libtest.dylib" if args[2] == library else "")
|
||||||
|
if args[1] == "-L":
|
||||||
|
name = "@loader_path/../Frameworks/libtest.dylib" if args[2] == binary else "@rpath/libtest.dylib"
|
||||||
|
return f"{args[2]}:\n\t{name} (compatibility version 1.0.0, current version 1.0.0)"
|
||||||
|
return "cmd LC_BUILD_VERSION\n cmdsize 32\n platform 1\n minos " + ("15.0.1" if args[2] == library else "11.0")
|
||||||
|
with patch.object(bundle, "run", side_effect=fake_run):
|
||||||
|
report = bundle.audit(app)
|
||||||
|
self.assertEqual(report["minimum_macos"], "15.0.1")
|
||||||
|
self.assertEqual(report["architectures"], ["arm64"])
|
||||||
|
self.assertEqual(len(report["mach_o"]), 2)
|
||||||
|
|
||||||
|
def test_audit_rejects_wrong_architecture(self):
|
||||||
|
with tempfile.TemporaryDirectory() as temporary:
|
||||||
|
app = Path(temporary) / "App.app"
|
||||||
|
binary = app / "Contents/MacOS/zoitechat"
|
||||||
|
library = app / "Contents/Frameworks/libtest.dylib"
|
||||||
|
for path in (binary, library):
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
path.write_bytes(bytes.fromhex("cffaedfe"))
|
||||||
|
with patch.object(bundle, "run", side_effect=["arm64", "x86_64"]), self.assertRaisesRegex(RuntimeError, "Architecture mismatch"):
|
||||||
|
bundle.audit(app)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
"""Release failure gates, private-key handling and notarization result checks."""
|
||||||
|
from contextlib import redirect_stdout
|
||||||
|
import base64
|
||||||
|
import io
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||||
|
import release
|
||||||
|
|
||||||
|
FINGERPRINT = "A" * 40
|
||||||
|
IDENTITY = "Developer ID Application: Test Only (NOTAREALID)"
|
||||||
|
SUBMISSION = "a8c9438a-b972-455e-aedc-278070349394"
|
||||||
|
ENVIRONMENT = {
|
||||||
|
"MACOS_CERTIFICATE_P12_BASE64": base64.b64encode(b"not-a-real-certificate").decode(),
|
||||||
|
"MACOS_CERTIFICATE_PASSWORD": "unit-test-password",
|
||||||
|
"MACOS_SIGNING_IDENTITY": IDENTITY,
|
||||||
|
"APPLE_API_KEY_P8": "-----BEGIN PRIVATE KEY-----\nunit-test-only\n-----END PRIVATE KEY-----",
|
||||||
|
"APPLE_API_KEY_ID": "TESTKEY123",
|
||||||
|
"APPLE_API_ISSUER_ID": SUBMISSION,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class ReleaseTests(unittest.TestCase):
|
||||||
|
def credentials(self):
|
||||||
|
with patch.dict(os.environ, ENVIRONMENT, clear=True):
|
||||||
|
return release.Credentials.from_environment()
|
||||||
|
|
||||||
|
def test_config_reports_names_and_never_values(self):
|
||||||
|
with patch.dict(os.environ, {}, clear=True), self.assertRaisesRegex(RuntimeError, "MACOS_CERTIFICATE_P12_BASE64"):
|
||||||
|
release.Credentials.from_environment()
|
||||||
|
|
||||||
|
def test_config_accepts_multiline_certificate_base64(self):
|
||||||
|
env = ENVIRONMENT.copy()
|
||||||
|
text = env["MACOS_CERTIFICATE_P12_BASE64"]
|
||||||
|
env["MACOS_CERTIFICATE_P12_BASE64"] = text[:10] + "\n" + text[10:]
|
||||||
|
with patch.dict(os.environ, env, clear=True):
|
||||||
|
self.assertEqual(release.Credentials.from_environment().certificate, b"not-a-real-certificate")
|
||||||
|
|
||||||
|
def test_config_rejects_development_certificate(self):
|
||||||
|
env = {**ENVIRONMENT, "MACOS_SIGNING_IDENTITY": "Apple Development: Test"}
|
||||||
|
with patch.dict(os.environ, env, clear=True), self.assertRaisesRegex(RuntimeError, "Developer ID Application"):
|
||||||
|
release.Credentials.from_environment()
|
||||||
|
|
||||||
|
def test_config_rejects_base64_instead_of_plain_p8(self):
|
||||||
|
env = {**ENVIRONMENT, "APPLE_API_KEY_P8": "not-the-pem-contents"}
|
||||||
|
with patch.dict(os.environ, env, clear=True), self.assertRaisesRegex(RuntimeError, "complete, plain-text"):
|
||||||
|
release.Credentials.from_environment()
|
||||||
|
|
||||||
|
def test_execute_does_not_expose_secret_argv_or_stderr(self):
|
||||||
|
result = subprocess.CompletedProcess([], 1, stdout="", stderr="secret-password")
|
||||||
|
with patch.object(release.subprocess, "run", return_value=result):
|
||||||
|
with self.assertRaises(RuntimeError) as error:
|
||||||
|
release.execute(["security", "import", "-P", "secret-password"], "Import certificate")
|
||||||
|
self.assertNotIn("secret-password", str(error.exception))
|
||||||
|
|
||||||
|
def test_matching_identity_requires_developer_id_and_private_key(self):
|
||||||
|
output = f' 1) {FINGERPRINT} "{IDENTITY}"\n'
|
||||||
|
self.assertEqual(release.resolve_identity(output, IDENTITY), FINGERPRINT)
|
||||||
|
with self.assertRaisesRegex(RuntimeError, "no valid Developer ID"):
|
||||||
|
release.resolve_identity("0 valid identities found", IDENTITY)
|
||||||
|
|
||||||
|
def test_private_files_have_restricted_permissions(self):
|
||||||
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
path = Path(directory) / "private.p8"
|
||||||
|
release.write_private(path, b"test-only")
|
||||||
|
self.assertEqual(path.stat().st_mode & 0o777, 0o600)
|
||||||
|
|
||||||
|
def test_keychain_and_private_files_are_cleaned_up_after_signing_failure(self):
|
||||||
|
calls = []
|
||||||
|
def fake_execute(args, operation, **kwargs):
|
||||||
|
calls.append([str(value) for value in args])
|
||||||
|
output = ''
|
||||||
|
if args[:3] == ["security", "list-keychains", "-d"] and len(args) == 4:
|
||||||
|
output = '"/tmp/original.keychain-db"\n'
|
||||||
|
if args[:2] == ["security", "create-keychain"]:
|
||||||
|
Path(args[-1]).touch()
|
||||||
|
if args[:2] == ["security", "find-identity"]:
|
||||||
|
output = f' 1) {FINGERPRINT} "{IDENTITY}"\n'
|
||||||
|
return subprocess.CompletedProcess(args, 0, output, "")
|
||||||
|
temporary_path = None
|
||||||
|
with patch.object(release, "execute", side_effect=fake_execute), patch.object(release.urllib.request, "urlopen", side_effect=lambda *args, **kwargs: io.BytesIO(b"public-test-ca")):
|
||||||
|
with self.assertRaisesRegex(RuntimeError, "test signing failed"):
|
||||||
|
with release.signing_keychain(self.credentials()) as (keychain, api_key, identity):
|
||||||
|
temporary_path = keychain.parent
|
||||||
|
self.assertEqual(identity, FINGERPRINT)
|
||||||
|
self.assertTrue(api_key.is_file())
|
||||||
|
raise RuntimeError("test signing failed")
|
||||||
|
self.assertFalse(temporary_path.exists())
|
||||||
|
self.assertIn(["security", "list-keychains", "-d", "user", "-s", "/tmp/original.keychain-db"], calls)
|
||||||
|
self.assertTrue(any(call[:2] == ["security", "delete-keychain"] for call in calls))
|
||||||
|
|
||||||
|
def run_notary(self, status, returncode=0):
|
||||||
|
calls = []
|
||||||
|
temporary = tempfile.TemporaryDirectory()
|
||||||
|
self.addCleanup(temporary.cleanup)
|
||||||
|
directory = Path(temporary.name)
|
||||||
|
app = directory / "ZoiteChat.app"
|
||||||
|
app.mkdir()
|
||||||
|
output = directory / "release.zip"
|
||||||
|
diagnostics = directory / "diagnostics"
|
||||||
|
def fake_execute(args, operation, **kwargs):
|
||||||
|
calls.append([str(value) for value in args])
|
||||||
|
text = ""
|
||||||
|
code = 0
|
||||||
|
if args[:3] == ["xcrun", "notarytool", "submit"]:
|
||||||
|
text = json.dumps({"id": SUBMISSION})
|
||||||
|
elif args[:3] == ["xcrun", "notarytool", "wait"]:
|
||||||
|
text = json.dumps({"id": SUBMISSION, "status": status})
|
||||||
|
code = returncode
|
||||||
|
elif args[:3] == ["xcrun", "notarytool", "log"]:
|
||||||
|
Path(args[-1]).write_text('{"issues": []}')
|
||||||
|
elif args[0] == "ditto":
|
||||||
|
Path(args[-1]).write_bytes(b"test-zip")
|
||||||
|
return subprocess.CompletedProcess(args, code, text, "")
|
||||||
|
with patch.object(release, "execute", side_effect=fake_execute), redirect_stdout(io.StringIO()):
|
||||||
|
if status == "Accepted" and not returncode:
|
||||||
|
release.notarize(app, output, diagnostics, self.credentials(), directory / "test.p8", "60m")
|
||||||
|
else:
|
||||||
|
with self.assertRaisesRegex(RuntimeError, "not accepted"):
|
||||||
|
release.notarize(app, output, diagnostics, self.credentials(), directory / "test.p8", "60m")
|
||||||
|
return calls, output, diagnostics
|
||||||
|
|
||||||
|
def test_accepted_notarization_staples_and_checks_before_final_zip(self):
|
||||||
|
calls, output, diagnostics = self.run_notary("Accepted")
|
||||||
|
self.assertTrue(output.is_file())
|
||||||
|
staple = next(i for i, call in enumerate(calls) if call[:3] == ["xcrun", "stapler", "staple"])
|
||||||
|
assess = next(i for i, call in enumerate(calls) if call[0] == "spctl")
|
||||||
|
final_zip = max(i for i, call in enumerate(calls) if call[0] == "ditto")
|
||||||
|
self.assertLess(staple, assess)
|
||||||
|
self.assertLess(assess, final_zip)
|
||||||
|
self.assertEqual(json.loads((diagnostics / "status.json").read_text())["status"], "Accepted")
|
||||||
|
|
||||||
|
def test_rejected_notarization_never_staples_or_publishes(self):
|
||||||
|
calls, output, diagnostics = self.run_notary("Invalid")
|
||||||
|
self.assertFalse(output.exists())
|
||||||
|
self.assertTrue((diagnostics / "notary-log.json").is_file())
|
||||||
|
self.assertFalse(any(call[:2] == ["xcrun", "stapler"] for call in calls))
|
||||||
|
|
||||||
|
def test_timeout_retains_submission_id_and_never_publishes(self):
|
||||||
|
calls, output, diagnostics = self.run_notary("In Progress", 1)
|
||||||
|
self.assertFalse(output.exists())
|
||||||
|
self.assertEqual(json.loads((diagnostics / "submission.json").read_text())["id"], SUBMISSION)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -710,6 +710,7 @@ static int handle_fishlim(char *word[], char *word_eol[], void *userdata) {
|
|||||||
gtk_box_pack_start(GTK_BOX(content), grid, TRUE, TRUE, 0);
|
gtk_box_pack_start(GTK_BOX(content), grid, TRUE, TRUE, 0);
|
||||||
|
|
||||||
fishlim_target_entry = gtk_entry_new();
|
fishlim_target_entry = gtk_entry_new();
|
||||||
|
gtk_widget_set_hexpand(fishlim_target_entry, TRUE);
|
||||||
if (target)
|
if (target)
|
||||||
gtk_entry_set_text(GTK_ENTRY(fishlim_target_entry), target);
|
gtk_entry_set_text(GTK_ENTRY(fishlim_target_entry), target);
|
||||||
gtk_grid_attach(GTK_GRID(grid), gtk_label_new("Target nick or channel"), 0, 0, 1, 1);
|
gtk_grid_attach(GTK_GRID(grid), gtk_label_new("Target nick or channel"), 0, 0, 1, 1);
|
||||||
@@ -760,6 +761,7 @@ static int handle_fishlim(char *word[], char *word_eol[], void *userdata) {
|
|||||||
g_signal_connect(view, "row-activated", G_CALLBACK(fishlim_gui_row_activated), NULL);
|
g_signal_connect(view, "row-activated", G_CALLBACK(fishlim_gui_row_activated), NULL);
|
||||||
|
|
||||||
scroll = gtk_scrolled_window_new(NULL, NULL);
|
scroll = gtk_scrolled_window_new(NULL, NULL);
|
||||||
|
gtk_widget_set_hexpand(scroll, TRUE);
|
||||||
gtk_widget_set_vexpand(scroll, TRUE);
|
gtk_widget_set_vexpand(scroll, TRUE);
|
||||||
gtk_container_add(GTK_CONTAINER(scroll), view);
|
gtk_container_add(GTK_CONTAINER(scroll), view);
|
||||||
gtk_grid_attach(GTK_GRID(grid), scroll, 0, 6, 3, 1);
|
gtk_grid_attach(GTK_GRID(grid), scroll, 0, 6, 3, 1);
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ if system == 'linux' or system == 'gnu' or system.startswith('gnu/') or system =
|
|||||||
sysinfo_cargs += '-DHAVE_LIBPCI'
|
sysinfo_cargs += '-DHAVE_LIBPCI'
|
||||||
sysinfo_sources += 'unix/pci.c'
|
sysinfo_sources += 'unix/pci.c'
|
||||||
|
|
||||||
picidsdir = libpci.get_pkgconfig_variable('idsdir')
|
picidsdir = libpci.get_variable(pkgconfig : 'idsdir')
|
||||||
pciids = join_paths(picidsdir, 'pci.ids')
|
pciids = join_paths(picidsdir, 'pci.ids')
|
||||||
sysinfo_cargs += '-DPCIIDS_FILE="@0@"'.format(pciids)
|
sysinfo_cargs += '-DPCIIDS_FILE="@0@"'.format(pciids)
|
||||||
endif
|
endif
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ data/misc/net.zoite.Zoitechat.ThemeManager.desktop.in
|
|||||||
src/common/cfgfiles.c
|
src/common/cfgfiles.c
|
||||||
src/common/chanopt.c
|
src/common/chanopt.c
|
||||||
src/common/dcc.c
|
src/common/dcc.c
|
||||||
|
src/common/emoji-data.c
|
||||||
src/common/zoitechat.c
|
src/common/zoitechat.c
|
||||||
src/common/ignore.c
|
src/common/ignore.c
|
||||||
src/common/inbound.c
|
src/common/inbound.c
|
||||||
@@ -24,10 +25,12 @@ src/fe-gtk/banlist.c
|
|||||||
src/fe-gtk/chanlist.c
|
src/fe-gtk/chanlist.c
|
||||||
src/fe-gtk/dccgui.c
|
src/fe-gtk/dccgui.c
|
||||||
src/fe-gtk/editlist.c
|
src/fe-gtk/editlist.c
|
||||||
|
src/fe-gtk/emoji-picker.c
|
||||||
src/fe-gtk/fe-gtk.c
|
src/fe-gtk/fe-gtk.c
|
||||||
src/fe-gtk/fkeys.c
|
src/fe-gtk/fkeys.c
|
||||||
src/fe-gtk/gtkutil.c
|
src/fe-gtk/gtkutil.c
|
||||||
src/fe-gtk/ignoregui.c
|
src/fe-gtk/ignoregui.c
|
||||||
|
src/fe-gtk/inline-image.c
|
||||||
src/fe-gtk/joind.c
|
src/fe-gtk/joind.c
|
||||||
src/fe-gtk/maingui.c
|
src/fe-gtk/maingui.c
|
||||||
src/fe-gtk/menu.c
|
src/fe-gtk/menu.c
|
||||||
|
|||||||
@@ -416,6 +416,7 @@ const struct prefs vars[] =
|
|||||||
{"gui_dialog_left", P_OFFINT (hex_gui_dialog_left), TYPE_INT},
|
{"gui_dialog_left", P_OFFINT (hex_gui_dialog_left), TYPE_INT},
|
||||||
{"gui_dialog_top", P_OFFINT (hex_gui_dialog_top), TYPE_INT},
|
{"gui_dialog_top", P_OFFINT (hex_gui_dialog_top), TYPE_INT},
|
||||||
{"gui_dialog_width", P_OFFINT (hex_gui_dialog_width), TYPE_INT},
|
{"gui_dialog_width", P_OFFINT (hex_gui_dialog_width), TYPE_INT},
|
||||||
|
{"gui_emoji_skin_tone", P_OFFINT (hex_gui_emoji_skin_tone), TYPE_INT},
|
||||||
{"gui_filesize_iec", P_OFFINT (hex_gui_filesize_iec), TYPE_BOOL},
|
{"gui_filesize_iec", P_OFFINT (hex_gui_filesize_iec), TYPE_BOOL},
|
||||||
{"gui_focus_omitalerts", P_OFFINT (hex_gui_focus_omitalerts), TYPE_BOOL},
|
{"gui_focus_omitalerts", P_OFFINT (hex_gui_focus_omitalerts), TYPE_BOOL},
|
||||||
{"gui_hide_menu", P_OFFINT (hex_gui_hide_menu), TYPE_BOOL},
|
{"gui_hide_menu", P_OFFINT (hex_gui_hide_menu), TYPE_BOOL},
|
||||||
@@ -442,6 +443,7 @@ const struct prefs vars[] =
|
|||||||
{"gui_tab_chans", P_OFFINT (hex_gui_tab_chans), TYPE_BOOL},
|
{"gui_tab_chans", P_OFFINT (hex_gui_tab_chans), TYPE_BOOL},
|
||||||
{"gui_tab_dialogs", P_OFFINT (hex_gui_tab_dialogs), TYPE_BOOL},
|
{"gui_tab_dialogs", P_OFFINT (hex_gui_tab_dialogs), TYPE_BOOL},
|
||||||
{"gui_tab_dots", P_OFFINT (hex_gui_tab_dots), TYPE_BOOL},
|
{"gui_tab_dots", P_OFFINT (hex_gui_tab_dots), TYPE_BOOL},
|
||||||
|
{"gui_tab_font", P_OFFSET (hex_gui_tab_font), TYPE_STR},
|
||||||
{"gui_tab_icons", P_OFFINT (hex_gui_tab_icons), TYPE_BOOL},
|
{"gui_tab_icons", P_OFFINT (hex_gui_tab_icons), TYPE_BOOL},
|
||||||
{"gui_dark_mode", P_OFFINT (hex_gui_dark_mode), TYPE_INT},
|
{"gui_dark_mode", P_OFFINT (hex_gui_dark_mode), TYPE_INT},
|
||||||
{"gui_gtk3_variant", P_OFFINT (hex_gui_gtk3_variant), TYPE_INT},
|
{"gui_gtk3_variant", P_OFFINT (hex_gui_gtk3_variant), TYPE_INT},
|
||||||
@@ -564,6 +566,7 @@ const struct prefs vars[] =
|
|||||||
{"net_proxy_use", P_OFFINT (hex_net_proxy_use), TYPE_INT},
|
{"net_proxy_use", P_OFFINT (hex_net_proxy_use), TYPE_INT},
|
||||||
{"net_proxy_user", P_OFFSET (hex_net_proxy_user), TYPE_STR},
|
{"net_proxy_user", P_OFFSET (hex_net_proxy_user), TYPE_STR},
|
||||||
{"net_reconnect_delay", P_OFFINT (hex_net_reconnect_delay), TYPE_INT},
|
{"net_reconnect_delay", P_OFFINT (hex_net_reconnect_delay), TYPE_INT},
|
||||||
|
{"net_remote_media", P_OFFINT (hex_net_remote_media), TYPE_BOOL},
|
||||||
{"net_throttle", P_OFFINT (hex_net_throttle), TYPE_BOOL},
|
{"net_throttle", P_OFFINT (hex_net_throttle), TYPE_BOOL},
|
||||||
|
|
||||||
{"notify_timeout", P_OFFINT (hex_notify_timeout), TYPE_INT},
|
{"notify_timeout", P_OFFINT (hex_notify_timeout), TYPE_INT},
|
||||||
@@ -587,6 +590,7 @@ const struct prefs vars[] =
|
|||||||
{"gui_gtk3_theme", P_OFFSET (hex_gui_gtk3_theme), TYPE_STR},
|
{"gui_gtk3_theme", P_OFFSET (hex_gui_gtk3_theme), TYPE_STR},
|
||||||
{"text_indent", P_OFFINT (hex_text_indent), TYPE_BOOL},
|
{"text_indent", P_OFFINT (hex_text_indent), TYPE_BOOL},
|
||||||
{"text_max_indent", P_OFFINT (hex_text_max_indent), TYPE_INT},
|
{"text_max_indent", P_OFFINT (hex_text_max_indent), TYPE_INT},
|
||||||
|
{"text_wrap_align", P_OFFINT (hex_text_wrap_align), TYPE_INT},
|
||||||
{"text_max_lines", P_OFFINT (hex_text_max_lines), TYPE_INT},
|
{"text_max_lines", P_OFFINT (hex_text_max_lines), TYPE_INT},
|
||||||
{"text_replay", P_OFFINT (hex_text_replay), TYPE_BOOL},
|
{"text_replay", P_OFFINT (hex_text_replay), TYPE_BOOL},
|
||||||
{"text_search_case_match", P_OFFINT (hex_text_search_case_match), TYPE_BOOL},
|
{"text_search_case_match", P_OFFINT (hex_text_search_case_match), TYPE_BOOL},
|
||||||
@@ -777,6 +781,8 @@ load_default_config(void)
|
|||||||
prefs.hex_gui_hide_menu = 1;
|
prefs.hex_gui_hide_menu = 1;
|
||||||
#endif
|
#endif
|
||||||
prefs.hex_gui_ctrlq_quit = 1;
|
prefs.hex_gui_ctrlq_quit = 1;
|
||||||
|
/* hex_net_remote_media stays 0: loading remote media can reveal the
|
||||||
|
user's IP address, so it is strictly opt-in */
|
||||||
prefs.hex_gui_input_attr = 1;
|
prefs.hex_gui_input_attr = 1;
|
||||||
prefs.hex_gui_input_icon = 1;
|
prefs.hex_gui_input_icon = 1;
|
||||||
prefs.hex_gui_input_nick = 1;
|
prefs.hex_gui_input_nick = 1;
|
||||||
@@ -876,6 +882,7 @@ load_default_config(void)
|
|||||||
prefs.hex_net_reconnect_delay = 10;
|
prefs.hex_net_reconnect_delay = 10;
|
||||||
prefs.hex_notify_timeout = 15;
|
prefs.hex_notify_timeout = 15;
|
||||||
prefs.hex_text_max_indent = 256;
|
prefs.hex_text_max_indent = 256;
|
||||||
|
prefs.hex_text_wrap_align = 0;
|
||||||
prefs.hex_text_max_lines = 5000;
|
prefs.hex_text_max_lines = 5000;
|
||||||
prefs.hex_url_grabber_limit = 100; /* 0 means unlimited */
|
prefs.hex_url_grabber_limit = 100; /* 0 means unlimited */
|
||||||
|
|
||||||
|
|||||||
@@ -43,6 +43,7 @@
|
|||||||
<ClInclude Include="typedef.h" />
|
<ClInclude Include="typedef.h" />
|
||||||
<ClInclude Include="url.h" />
|
<ClInclude Include="url.h" />
|
||||||
<ClInclude Include="userlist.h" />
|
<ClInclude Include="userlist.h" />
|
||||||
|
<ClInclude Include="hexcolor.h" />
|
||||||
<ClInclude Include="util.h" />
|
<ClInclude Include="util.h" />
|
||||||
<ClInclude Include="zoitechat-plugin.h" />
|
<ClInclude Include="zoitechat-plugin.h" />
|
||||||
<ClInclude Include="zoitechat.h" />
|
<ClInclude Include="zoitechat.h" />
|
||||||
@@ -55,6 +56,7 @@
|
|||||||
<ClCompile Include="chanopt.c" />
|
<ClCompile Include="chanopt.c" />
|
||||||
<ClCompile Include="ctcp.c" />
|
<ClCompile Include="ctcp.c" />
|
||||||
<ClCompile Include="dcc.c" />
|
<ClCompile Include="dcc.c" />
|
||||||
|
<ClCompile Include="emoji-data.c" />
|
||||||
<ClCompile Include="history.c" />
|
<ClCompile Include="history.c" />
|
||||||
<ClCompile Include="plugin-identd.c" />
|
<ClCompile Include="plugin-identd.c" />
|
||||||
<ClCompile Include="ignore.c" />
|
<ClCompile Include="ignore.c" />
|
||||||
@@ -78,6 +80,7 @@
|
|||||||
<ClCompile Include="tree.c" />
|
<ClCompile Include="tree.c" />
|
||||||
<ClCompile Include="url.c" />
|
<ClCompile Include="url.c" />
|
||||||
<ClCompile Include="userlist.c" />
|
<ClCompile Include="userlist.c" />
|
||||||
|
<ClCompile Include="hexcolor.c" />
|
||||||
<ClCompile Include="util.c" />
|
<ClCompile Include="util.c" />
|
||||||
<ClCompile Include="zoitechat.c" />
|
<ClCompile Include="zoitechat.c" />
|
||||||
<ClCompile Include="theme-service.c" />
|
<ClCompile Include="theme-service.c" />
|
||||||
@@ -113,6 +116,8 @@
|
|||||||
SET SOLUTIONDIR=$(SolutionDir)..\
|
SET SOLUTIONDIR=$(SolutionDir)..\
|
||||||
"$(Python3Path)\python.exe" $(ProjectDir)make-te.py "$(ProjectDir)textevents.in" "$(ZoiteChatLib)textevents.h" "$(ZoiteChatLib)textenums.h"
|
"$(Python3Path)\python.exe" $(ProjectDir)make-te.py "$(ProjectDir)textevents.in" "$(ZoiteChatLib)textevents.h" "$(ZoiteChatLib)textenums.h"
|
||||||
"$(Python3Path)\python.exe" $(ProjectDir)gen-public-suffix.py "$(ZoiteChatLib)public_suffix_data.h"
|
"$(Python3Path)\python.exe" $(ProjectDir)gen-public-suffix.py "$(ZoiteChatLib)public_suffix_data.h"
|
||||||
|
"$(Python3Path)\python.exe" "$(SolutionDir)..\tools\gen-emoji-data.py" "$(SolutionDir)..\data\emoji\emoji-test.txt" "$(SolutionDir)..\data\emoji\cldr-annotations-en.xml" "$(SolutionDir)..\data\emoji\cldr-annotations-derived-en.xml" "$(SolutionDir)..\data\emoji\gemoji.json" "$(SolutionDir)..\data\emoji\VERSIONS" "$(ZoiteChatLib)emoji-data-table.h"
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
powershell -File "$(SolutionDir)..\win32\version-template.ps1" "$(SolutionDir)..\win32\config.h.tt" "$(ZoiteChatLib)config.h"
|
powershell -File "$(SolutionDir)..\win32\version-template.ps1" "$(SolutionDir)..\win32\config.h.tt" "$(ZoiteChatLib)config.h"
|
||||||
$(GlibGenMarshal) --prefix=_zoitechat_marshal --header "$(ProjectDir)marshalers.list" --output "$(ZoiteChatLib)marshal.h"
|
$(GlibGenMarshal) --prefix=_zoitechat_marshal --header "$(ProjectDir)marshalers.list" --output "$(ZoiteChatLib)marshal.h"
|
||||||
$(GlibGenMarshal) --prefix=_zoitechat_marshal --body "$(ProjectDir)marshalers.list" --output "$(ZoiteChatLib)marshal.c"
|
$(GlibGenMarshal) --prefix=_zoitechat_marshal --body "$(ProjectDir)marshalers.list" --output "$(ZoiteChatLib)marshal.c"
|
||||||
|
|||||||
@@ -98,6 +98,9 @@
|
|||||||
<ClInclude Include="userlist.h">
|
<ClInclude Include="userlist.h">
|
||||||
<Filter>Header Files</Filter>
|
<Filter>Header Files</Filter>
|
||||||
</ClInclude>
|
</ClInclude>
|
||||||
|
<ClInclude Include="hexcolor.h">
|
||||||
|
<Filter>Header Files</Filter>
|
||||||
|
</ClInclude>
|
||||||
<ClInclude Include="util.h">
|
<ClInclude Include="util.h">
|
||||||
<Filter>Header Files</Filter>
|
<Filter>Header Files</Filter>
|
||||||
</ClInclude>
|
</ClInclude>
|
||||||
@@ -202,6 +205,9 @@
|
|||||||
<ClCompile Include="userlist.c">
|
<ClCompile Include="userlist.c">
|
||||||
<Filter>Source Files</Filter>
|
<Filter>Source Files</Filter>
|
||||||
</ClCompile>
|
</ClCompile>
|
||||||
|
<ClCompile Include="hexcolor.c">
|
||||||
|
<Filter>Source Files</Filter>
|
||||||
|
</ClCompile>
|
||||||
<ClCompile Include="util.c">
|
<ClCompile Include="util.c">
|
||||||
<Filter>Source Files</Filter>
|
<Filter>Source Files</Filter>
|
||||||
</ClCompile>
|
</ClCompile>
|
||||||
|
|||||||
@@ -0,0 +1,239 @@
|
|||||||
|
/* ZoiteChat
|
||||||
|
* Copyright (C) 2026 deepend-tildeclub.
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation; either version 2 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program; if not, write to the Free Software
|
||||||
|
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include <glib.h>
|
||||||
|
#include <glib/gi18n.h>
|
||||||
|
|
||||||
|
#include "emoji-data.h"
|
||||||
|
|
||||||
|
#include "emoji-data-table.h"
|
||||||
|
|
||||||
|
gsize
|
||||||
|
emoji_data_count (void)
|
||||||
|
{
|
||||||
|
return G_N_ELEMENTS (emoji_entries_table);
|
||||||
|
}
|
||||||
|
|
||||||
|
const EmojiEntry *
|
||||||
|
emoji_data_entry (gsize index)
|
||||||
|
{
|
||||||
|
g_return_val_if_fail (index < G_N_ELEMENTS (emoji_entries_table), NULL);
|
||||||
|
|
||||||
|
return &emoji_entries_table[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
static GHashTable *
|
||||||
|
emoji_sequence_index (void)
|
||||||
|
{
|
||||||
|
static GHashTable *table = NULL;
|
||||||
|
gsize i;
|
||||||
|
int tone;
|
||||||
|
|
||||||
|
if (table)
|
||||||
|
return table;
|
||||||
|
|
||||||
|
table = g_hash_table_new (g_str_hash, g_str_equal);
|
||||||
|
for (i = 0; i < G_N_ELEMENTS (emoji_entries_table); i++)
|
||||||
|
{
|
||||||
|
const EmojiEntry *entry = &emoji_entries_table[i];
|
||||||
|
|
||||||
|
g_hash_table_insert (table, (gpointer) entry->sequence,
|
||||||
|
GSIZE_TO_POINTER (i * EMOJI_TONE_COUNT + 1));
|
||||||
|
if (entry->tone_set < 0)
|
||||||
|
continue;
|
||||||
|
for (tone = EMOJI_TONE_LIGHT; tone < EMOJI_TONE_COUNT; tone++)
|
||||||
|
{
|
||||||
|
const char *seq =
|
||||||
|
emoji_tone_sets_table[entry->tone_set].sequence[tone - 1];
|
||||||
|
|
||||||
|
g_hash_table_insert (table, (gpointer) seq,
|
||||||
|
GSIZE_TO_POINTER (i * EMOJI_TONE_COUNT + tone + 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return table;
|
||||||
|
}
|
||||||
|
|
||||||
|
const EmojiEntry *
|
||||||
|
emoji_data_find_by_sequence (const char *sequence, EmojiTone *tone_out)
|
||||||
|
{
|
||||||
|
gpointer value;
|
||||||
|
gsize packed;
|
||||||
|
|
||||||
|
if (tone_out)
|
||||||
|
*tone_out = EMOJI_TONE_NONE;
|
||||||
|
if (!sequence || !*sequence)
|
||||||
|
return NULL;
|
||||||
|
|
||||||
|
value = g_hash_table_lookup (emoji_sequence_index (), sequence);
|
||||||
|
if (!value)
|
||||||
|
return NULL;
|
||||||
|
|
||||||
|
packed = GPOINTER_TO_SIZE (value) - 1;
|
||||||
|
if (tone_out)
|
||||||
|
*tone_out = (EmojiTone) (packed % EMOJI_TONE_COUNT);
|
||||||
|
|
||||||
|
return &emoji_entries_table[packed / EMOJI_TONE_COUNT];
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *
|
||||||
|
emoji_data_group_display_name (EmojiGroup group)
|
||||||
|
{
|
||||||
|
static const char *const names[EMOJI_GROUP_COUNT] =
|
||||||
|
{
|
||||||
|
N_("Smileys & Emotion"),
|
||||||
|
N_("People & Body"),
|
||||||
|
N_("Animals & Nature"),
|
||||||
|
N_("Food & Drink"),
|
||||||
|
N_("Travel & Places"),
|
||||||
|
N_("Activities"),
|
||||||
|
N_("Objects"),
|
||||||
|
N_("Symbols"),
|
||||||
|
N_("Flags"),
|
||||||
|
};
|
||||||
|
|
||||||
|
g_return_val_if_fail ((guint) group < EMOJI_GROUP_COUNT, "");
|
||||||
|
|
||||||
|
return _(names[group]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *
|
||||||
|
emoji_data_group_icon_sequence (EmojiGroup group)
|
||||||
|
{
|
||||||
|
static const char *const icons[EMOJI_GROUP_COUNT] =
|
||||||
|
{
|
||||||
|
"\360\237\230\200", /* 😀 */
|
||||||
|
"\360\237\221\213", /* 👋 */
|
||||||
|
"\360\237\220\273", /* 🐻 */
|
||||||
|
"\360\237\215\224", /* 🍔 */
|
||||||
|
"\360\237\232\227", /* 🚗 */
|
||||||
|
"\342\232\275", /* ⚽ */
|
||||||
|
"\360\237\222\241", /* 💡 */
|
||||||
|
"\360\237\224\243", /* 🔣 */
|
||||||
|
"\360\237\217\201", /* 🏁 */
|
||||||
|
};
|
||||||
|
|
||||||
|
g_return_val_if_fail ((guint) group < EMOJI_GROUP_COUNT, "");
|
||||||
|
|
||||||
|
return icons[group];
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *
|
||||||
|
emoji_data_tone_display_name (EmojiTone tone)
|
||||||
|
{
|
||||||
|
static const char *const names[EMOJI_TONE_COUNT] =
|
||||||
|
{
|
||||||
|
N_("No skin tone"),
|
||||||
|
N_("Light skin tone"),
|
||||||
|
N_("Medium-light skin tone"),
|
||||||
|
N_("Medium skin tone"),
|
||||||
|
N_("Medium-dark skin tone"),
|
||||||
|
N_("Dark skin tone"),
|
||||||
|
};
|
||||||
|
|
||||||
|
g_return_val_if_fail ((guint) tone < EMOJI_TONE_COUNT, "");
|
||||||
|
|
||||||
|
return _(names[tone]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *
|
||||||
|
emoji_entry_sequence_for_tone (const EmojiEntry *entry, EmojiTone tone)
|
||||||
|
{
|
||||||
|
g_return_val_if_fail (entry != NULL, NULL);
|
||||||
|
|
||||||
|
if (tone == EMOJI_TONE_NONE || (guint) tone >= EMOJI_TONE_COUNT ||
|
||||||
|
entry->tone_set < 0)
|
||||||
|
return entry->sequence;
|
||||||
|
|
||||||
|
return emoji_tone_sets_table[entry->tone_set].sequence[tone - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
char **
|
||||||
|
emoji_search_tokenize (const char *text)
|
||||||
|
{
|
||||||
|
GPtrArray *tokens;
|
||||||
|
char *folded;
|
||||||
|
char **split;
|
||||||
|
gsize i;
|
||||||
|
|
||||||
|
if (!text || !g_utf8_validate (text, -1, NULL))
|
||||||
|
return NULL;
|
||||||
|
|
||||||
|
folded = g_utf8_casefold (text, -1);
|
||||||
|
split = g_strsplit_set (folded, " \t\n", -1);
|
||||||
|
g_free (folded);
|
||||||
|
|
||||||
|
tokens = g_ptr_array_new ();
|
||||||
|
for (i = 0; split[i]; i++)
|
||||||
|
{
|
||||||
|
const char *token = split[i];
|
||||||
|
gsize len;
|
||||||
|
|
||||||
|
while (*token == ':')
|
||||||
|
token++;
|
||||||
|
len = strlen (token);
|
||||||
|
while (len > 0 && token[len - 1] == ':')
|
||||||
|
len--;
|
||||||
|
if (len > 0)
|
||||||
|
g_ptr_array_add (tokens, g_strndup (token, len));
|
||||||
|
}
|
||||||
|
g_strfreev (split);
|
||||||
|
|
||||||
|
if (tokens->len == 0)
|
||||||
|
{
|
||||||
|
g_ptr_array_free (tokens, TRUE);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
g_ptr_array_add (tokens, NULL);
|
||||||
|
|
||||||
|
return (char **) g_ptr_array_free (tokens, FALSE);
|
||||||
|
}
|
||||||
|
|
||||||
|
gboolean
|
||||||
|
emoji_entry_matches (const EmojiEntry *entry, char *const *needle_tokens)
|
||||||
|
{
|
||||||
|
gsize i;
|
||||||
|
|
||||||
|
g_return_val_if_fail (entry != NULL, FALSE);
|
||||||
|
|
||||||
|
if (!needle_tokens || !needle_tokens[0])
|
||||||
|
return FALSE;
|
||||||
|
|
||||||
|
for (i = 0; needle_tokens[i]; i++)
|
||||||
|
{
|
||||||
|
if (!strstr (entry->search, needle_tokens[i]))
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *
|
||||||
|
emoji_data_unicode_version (void)
|
||||||
|
{
|
||||||
|
return emoji_data_unicode_version_str;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *
|
||||||
|
emoji_data_cldr_version (void)
|
||||||
|
{
|
||||||
|
return emoji_data_cldr_version_str;
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
/* ZoiteChat
|
||||||
|
* Copyright (C) 2026 deepend-tildeclub.
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation; either version 2 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program; if not, write to the Free Software
|
||||||
|
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* Bundled emoji catalog, generated at build time from pinned Unicode,
|
||||||
|
* CLDR and gemoji data (see data/emoji/ and tools/gen-emoji-data.py).
|
||||||
|
* Every entry is a fully-qualified Unicode sequence; nothing here depends
|
||||||
|
* on the GTK runtime's emoji data. The catalog only decides what users
|
||||||
|
* can find and insert - rendering stays with the platform's fonts, and
|
||||||
|
* IRC always receives the plain Unicode sequence. */
|
||||||
|
|
||||||
|
#ifndef ZOITECHAT_EMOJI_DATA_H
|
||||||
|
#define ZOITECHAT_EMOJI_DATA_H
|
||||||
|
|
||||||
|
#include <glib.h>
|
||||||
|
|
||||||
|
typedef enum
|
||||||
|
{
|
||||||
|
EMOJI_GROUP_SMILEYS_EMOTION,
|
||||||
|
EMOJI_GROUP_PEOPLE_BODY,
|
||||||
|
EMOJI_GROUP_ANIMALS_NATURE,
|
||||||
|
EMOJI_GROUP_FOOD_DRINK,
|
||||||
|
EMOJI_GROUP_TRAVEL_PLACES,
|
||||||
|
EMOJI_GROUP_ACTIVITIES,
|
||||||
|
EMOJI_GROUP_OBJECTS,
|
||||||
|
EMOJI_GROUP_SYMBOLS,
|
||||||
|
EMOJI_GROUP_FLAGS,
|
||||||
|
EMOJI_GROUP_COUNT
|
||||||
|
} EmojiGroup;
|
||||||
|
|
||||||
|
typedef enum
|
||||||
|
{
|
||||||
|
EMOJI_TONE_NONE,
|
||||||
|
EMOJI_TONE_LIGHT,
|
||||||
|
EMOJI_TONE_MEDIUM_LIGHT,
|
||||||
|
EMOJI_TONE_MEDIUM,
|
||||||
|
EMOJI_TONE_MEDIUM_DARK,
|
||||||
|
EMOJI_TONE_DARK,
|
||||||
|
EMOJI_TONE_COUNT
|
||||||
|
} EmojiTone;
|
||||||
|
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
const char *sequence;
|
||||||
|
const char *name;
|
||||||
|
const char *search;
|
||||||
|
const char *aliases;
|
||||||
|
guint8 group;
|
||||||
|
gint16 tone_set;
|
||||||
|
} EmojiEntry;
|
||||||
|
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
const char *sequence[EMOJI_TONE_COUNT - 1];
|
||||||
|
} EmojiToneSet;
|
||||||
|
|
||||||
|
gsize emoji_data_count (void);
|
||||||
|
const EmojiEntry *emoji_data_entry (gsize index);
|
||||||
|
|
||||||
|
const EmojiEntry *emoji_data_find_by_sequence (const char *sequence,
|
||||||
|
EmojiTone *tone_out);
|
||||||
|
|
||||||
|
const char *emoji_data_group_display_name (EmojiGroup group);
|
||||||
|
const char *emoji_data_group_icon_sequence (EmojiGroup group);
|
||||||
|
const char *emoji_data_tone_display_name (EmojiTone tone);
|
||||||
|
|
||||||
|
const char *emoji_entry_sequence_for_tone (const EmojiEntry *entry,
|
||||||
|
EmojiTone tone);
|
||||||
|
|
||||||
|
char **emoji_search_tokenize (const char *text);
|
||||||
|
gboolean emoji_entry_matches (const EmojiEntry *entry,
|
||||||
|
char *const *needle_tokens);
|
||||||
|
|
||||||
|
const char *emoji_data_unicode_version (void);
|
||||||
|
const char *emoji_data_cldr_version (void);
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -70,6 +70,9 @@ void fe_add_rawlog (struct server *serv, char *text, int len, int outbound);
|
|||||||
#define FE_MSG_ERROR 8
|
#define FE_MSG_ERROR 8
|
||||||
#define FE_MSG_MARKUP 16
|
#define FE_MSG_MARKUP 16
|
||||||
void fe_message (char *msg, int flags);
|
void fe_message (char *msg, int flags);
|
||||||
|
#ifdef __linux__
|
||||||
|
void fe_root_warning (void);
|
||||||
|
#endif
|
||||||
#define FIA_READ 1
|
#define FIA_READ 1
|
||||||
#define FIA_WRITE 2
|
#define FIA_WRITE 2
|
||||||
#define FIA_EX 4
|
#define FIA_EX 4
|
||||||
|
|||||||
@@ -858,8 +858,10 @@ path_depth_from_root (const char *base, const char *path)
|
|||||||
static gint
|
static gint
|
||||||
theme_root_candidate_compare (gconstpointer a, gconstpointer b)
|
theme_root_candidate_compare (gconstpointer a, gconstpointer b)
|
||||||
{
|
{
|
||||||
const ThemeRootCandidate *ca = a;
|
/* g_ptr_array_sort() passes pointers to the array slots, not the values
|
||||||
const ThemeRootCandidate *cb = b;
|
* stored in those slots. Dereference once before comparing candidates. */
|
||||||
|
const ThemeRootCandidate *ca = *(ThemeRootCandidate * const *) a;
|
||||||
|
const ThemeRootCandidate *cb = *(ThemeRootCandidate * const *) b;
|
||||||
if (ca->has_index_theme != cb->has_index_theme)
|
if (ca->has_index_theme != cb->has_index_theme)
|
||||||
return ca->has_index_theme ? -1 : 1;
|
return ca->has_index_theme ? -1 : 1;
|
||||||
if (ca->depth != cb->depth)
|
if (ca->depth != cb->depth)
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
/* ZoiteChat
|
||||||
|
* Parsing of \004 (hex color) formatting codes: RRGGBB[,RRGGBB].
|
||||||
|
* Kept free of other application dependencies so the xtext widget tests can
|
||||||
|
* compile it on its own.
|
||||||
|
* Distributed under GPL-2.0-or-later; see COPYING. */
|
||||||
|
|
||||||
|
#include "hexcolor.h"
|
||||||
|
|
||||||
|
static int
|
||||||
|
hexcolor_component (const char *s, int avail, guint32 *rgb)
|
||||||
|
{
|
||||||
|
int i;
|
||||||
|
guint32 value = 0;
|
||||||
|
|
||||||
|
if (avail < 6)
|
||||||
|
return FALSE;
|
||||||
|
for (i = 0; i < 6; i++)
|
||||||
|
{
|
||||||
|
if (!g_ascii_isxdigit (s[i]))
|
||||||
|
return FALSE;
|
||||||
|
value = (value << 4) | g_ascii_xdigit_value (s[i]);
|
||||||
|
}
|
||||||
|
if (rgb)
|
||||||
|
*rgb = value;
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
int
|
||||||
|
hexcolor_parse (const char *s, int avail, guint32 *fg, guint32 *bg, int *has_bg)
|
||||||
|
{
|
||||||
|
if (has_bg)
|
||||||
|
*has_bg = FALSE;
|
||||||
|
if (!hexcolor_component (s, avail, fg))
|
||||||
|
return 0;
|
||||||
|
if (avail >= 7 && s[6] == ',' && hexcolor_component (s + 7, avail - 7, bg))
|
||||||
|
{
|
||||||
|
if (has_bg)
|
||||||
|
*has_bg = TRUE;
|
||||||
|
return 13;
|
||||||
|
}
|
||||||
|
return 6;
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
/* ZoiteChat
|
||||||
|
* Parsing of \004 (hex color) formatting codes: RRGGBB[,RRGGBB].
|
||||||
|
* See https://modern.ircdocs.horse/formatting
|
||||||
|
* Distributed under GPL-2.0-or-later; see COPYING. */
|
||||||
|
|
||||||
|
#ifndef ZOITECHAT_HEXCOLOR_H
|
||||||
|
#define ZOITECHAT_HEXCOLOR_H
|
||||||
|
|
||||||
|
#include <glib.h>
|
||||||
|
|
||||||
|
/* Parses the parameters following a \004 byte. Returns the number of bytes
|
||||||
|
* consumed (6 or 13), or 0 if no valid RRGGBB follows. fg, bg and has_bg
|
||||||
|
* may be NULL. */
|
||||||
|
int hexcolor_parse (const char *s, int avail, guint32 *fg, guint32 *bg, int *has_bg);
|
||||||
|
|
||||||
|
#endif
|
||||||
+2
-2
@@ -271,7 +271,7 @@ ignore_read_next_entry (char *my_cfg, struct ignore *ignore)
|
|||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
ignore_load ()
|
ignore_load (void)
|
||||||
{
|
{
|
||||||
struct ignore *ignore;
|
struct ignore *ignore;
|
||||||
struct stat st;
|
struct stat st;
|
||||||
@@ -302,7 +302,7 @@ ignore_load ()
|
|||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
ignore_save ()
|
ignore_save (void)
|
||||||
{
|
{
|
||||||
char buf[1024];
|
char buf[1024];
|
||||||
int fh;
|
int fh;
|
||||||
|
|||||||
+74
-34
@@ -1880,38 +1880,51 @@ inbound_toggle_caps (server *serv, const char *extensions_str, gboolean enable)
|
|||||||
char **extensions;
|
char **extensions;
|
||||||
gsize i;
|
gsize i;
|
||||||
|
|
||||||
|
if (!extensions_str)
|
||||||
|
return;
|
||||||
|
|
||||||
extensions = g_strsplit (extensions_str, " ", 0);
|
extensions = g_strsplit (extensions_str, " ", 0);
|
||||||
|
|
||||||
for (i = 0; extensions[i]; i++)
|
for (i = 0; extensions[i]; i++)
|
||||||
{
|
{
|
||||||
const char *extension = extensions[i];
|
const char *extension = extensions[i];
|
||||||
|
gboolean cap_enable = enable;
|
||||||
|
|
||||||
|
/* ACK prefixes disabled capabilities with '-', unlike CAP DEL. */
|
||||||
|
if (*extension == '-')
|
||||||
|
{
|
||||||
|
cap_enable = FALSE;
|
||||||
|
extension++;
|
||||||
|
}
|
||||||
|
|
||||||
if (!strcmp (extension, "solanum.chat/identify-msg"))
|
if (!strcmp (extension, "solanum.chat/identify-msg"))
|
||||||
serv->have_idmsg = enable;
|
serv->have_idmsg = cap_enable;
|
||||||
else if (!strcmp (extension, "multi-prefix"))
|
else if (!strcmp (extension, "multi-prefix"))
|
||||||
serv->have_namesx = enable;
|
serv->have_namesx = cap_enable;
|
||||||
else if (!strcmp (extension, "account-notify"))
|
else if (!strcmp (extension, "account-notify"))
|
||||||
serv->have_accnotify = enable;
|
serv->have_accnotify = cap_enable;
|
||||||
else if (!strcmp (extension, "extended-join"))
|
else if (!strcmp (extension, "extended-join"))
|
||||||
serv->have_extjoin = enable;
|
serv->have_extjoin = cap_enable;
|
||||||
else if (!strcmp (extension, "userhost-in-names"))
|
else if (!strcmp (extension, "userhost-in-names"))
|
||||||
serv->have_uhnames = enable;
|
serv->have_uhnames = cap_enable;
|
||||||
else if (!strcmp (extension, "server-time")
|
else if (!strcmp (extension, "server-time")
|
||||||
|| !strcmp (extension, "znc.in/server-time")
|
|| !strcmp (extension, "znc.in/server-time")
|
||||||
|| !strcmp (extension, "znc.in/server-time-iso"))
|
|| !strcmp (extension, "znc.in/server-time-iso"))
|
||||||
serv->have_server_time = enable;
|
serv->have_server_time = cap_enable;
|
||||||
else if (!strcmp (extension, "away-notify"))
|
else if (!strcmp (extension, "away-notify"))
|
||||||
serv->have_awaynotify = enable;
|
serv->have_awaynotify = cap_enable;
|
||||||
else if (!strcmp (extension, "account-tag"))
|
else if (!strcmp (extension, "account-tag"))
|
||||||
serv->have_account_tag = enable;
|
serv->have_account_tag = cap_enable;
|
||||||
else if (!strcmp (extension, "message-tags"))
|
else if (!strcmp (extension, "message-tags"))
|
||||||
serv->have_message_tags = enable;
|
serv->have_message_tags = cap_enable;
|
||||||
else if (!strcmp (extension, "echo-message"))
|
else if (!strcmp (extension, "echo-message"))
|
||||||
serv->have_echo_message = enable;
|
serv->have_echo_message = cap_enable;
|
||||||
else if (!strcmp (extension, "sasl"))
|
else if (!strcmp (extension, "sasl"))
|
||||||
{
|
{
|
||||||
serv->have_sasl = enable;
|
gboolean start_auth = cap_enable && !serv->have_sasl;
|
||||||
if (enable)
|
|
||||||
|
serv->have_sasl = cap_enable;
|
||||||
|
if (start_auth)
|
||||||
{
|
{
|
||||||
#ifdef USE_OPENSSL
|
#ifdef USE_OPENSSL
|
||||||
if (serv->loginmethod == LOGIN_SASLEXTERNAL)
|
if (serv->loginmethod == LOGIN_SASLEXTERNAL)
|
||||||
@@ -1962,10 +1975,15 @@ inbound_cap_ack (server *serv, char *nick, char *extensions,
|
|||||||
inbound_toggle_caps (serv, extensions, TRUE);
|
inbound_toggle_caps (serv, extensions, TRUE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void inbound_request_new_caps (server *serv, const char *extensions_str,
|
||||||
|
const message_tags_data *tags_data);
|
||||||
|
|
||||||
void
|
void
|
||||||
inbound_cap_new (server *serv, char *nick, char *extensions,
|
inbound_cap_new (server *serv, char *nick, char *extensions,
|
||||||
const message_tags_data *tags_data)
|
const message_tags_data *tags_data)
|
||||||
{
|
{
|
||||||
|
gboolean sts_upgrade_triggered = FALSE;
|
||||||
|
|
||||||
if (extensions)
|
if (extensions)
|
||||||
{
|
{
|
||||||
char **tokens = g_strsplit (extensions, " ", 0);
|
char **tokens = g_strsplit (extensions, " ", 0);
|
||||||
@@ -1977,7 +1995,7 @@ inbound_cap_new (server *serv, char *nick, char *extensions,
|
|||||||
|
|
||||||
if (!g_strcmp0 (parts[0], "sts") && parts[1] && parts[1][0])
|
if (!g_strcmp0 (parts[0], "sts") && parts[1] && parts[1][0])
|
||||||
{
|
{
|
||||||
sts_handle_capability (serv, parts[1]);
|
sts_upgrade_triggered |= sts_handle_capability (serv, parts[1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
g_strfreev (parts);
|
g_strfreev (parts);
|
||||||
@@ -1986,38 +2004,22 @@ inbound_cap_new (server *serv, char *nick, char *extensions,
|
|||||||
g_strfreev (tokens);
|
g_strfreev (tokens);
|
||||||
}
|
}
|
||||||
|
|
||||||
EMIT_SIGNAL_TIMESTAMP (XP_TE_CAPACK, serv->server_session, nick, extensions,
|
EMIT_SIGNAL_TIMESTAMP (XP_TE_CAPLIST, serv->server_session, nick, extensions,
|
||||||
NULL, NULL, 0, tags_data->timestamp);
|
NULL, NULL, 0, tags_data->timestamp);
|
||||||
|
|
||||||
inbound_toggle_caps (serv, extensions, TRUE);
|
/* NEW advertises availability; only ACK confirms an enabled capability. */
|
||||||
|
if (!sts_upgrade_triggered)
|
||||||
|
inbound_request_new_caps (serv, extensions, tags_data);
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
inbound_cap_del (server *serv, char *nick, char *extensions,
|
inbound_cap_del (server *serv, char *nick, char *extensions,
|
||||||
const message_tags_data *tags_data)
|
const message_tags_data *tags_data)
|
||||||
{
|
{
|
||||||
if (extensions)
|
|
||||||
{
|
|
||||||
char **tokens = g_strsplit (extensions, " ", 0);
|
|
||||||
int i;
|
|
||||||
|
|
||||||
for (i = 0; tokens[i]; i++)
|
|
||||||
{
|
|
||||||
if (!g_strcmp0 (tokens[i], "sts") ||
|
|
||||||
g_str_has_prefix (tokens[i], "sts="))
|
|
||||||
{
|
|
||||||
/* STS cannot be disabled via CAP DEL. */
|
|
||||||
g_strfreev (tokens);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
g_strfreev (tokens);
|
|
||||||
}
|
|
||||||
|
|
||||||
EMIT_SIGNAL_TIMESTAMP (XP_TE_CAPDEL, serv->server_session, nick, extensions,
|
EMIT_SIGNAL_TIMESTAMP (XP_TE_CAPDEL, serv->server_session, nick, extensions,
|
||||||
NULL, NULL, 0, tags_data->timestamp);
|
NULL, NULL, 0, tags_data->timestamp);
|
||||||
|
|
||||||
|
/* STS has no toggle, but must not prevent other capabilities being removed. */
|
||||||
inbound_toggle_caps (serv, extensions, FALSE);
|
inbound_toggle_caps (serv, extensions, FALSE);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2053,6 +2055,44 @@ static const char * const supported_caps[] = {
|
|||||||
"solanum.chat/identify-msg",
|
"solanum.chat/identify-msg",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
static void
|
||||||
|
inbound_request_new_caps (server *serv, const char *extensions_str,
|
||||||
|
const message_tags_data *tags_data)
|
||||||
|
{
|
||||||
|
char **extensions;
|
||||||
|
gsize i, x;
|
||||||
|
|
||||||
|
if (!extensions_str)
|
||||||
|
return;
|
||||||
|
|
||||||
|
extensions = g_strsplit (extensions_str, " ", 0);
|
||||||
|
for (i = 0; extensions[i]; i++)
|
||||||
|
{
|
||||||
|
char *value = strchr (extensions[i], '=');
|
||||||
|
|
||||||
|
if (value)
|
||||||
|
*value = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Request each supported capability once, keeping every IRC line bounded.
|
||||||
|
* SASL is negotiated during login and is not in supported_caps. */
|
||||||
|
for (x = 0; x < G_N_ELEMENTS (supported_caps); x++)
|
||||||
|
{
|
||||||
|
for (i = 0; extensions[i]; i++)
|
||||||
|
{
|
||||||
|
if (!strcmp (extensions[i], supported_caps[x]))
|
||||||
|
{
|
||||||
|
EMIT_SIGNAL_TIMESTAMP (XP_TE_CAPREQ, serv->server_session,
|
||||||
|
extensions[i], NULL, NULL, NULL, 0,
|
||||||
|
tags_data->timestamp);
|
||||||
|
tcp_sendf (serv, "CAP REQ :%s\r\n", extensions[i]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
g_strfreev (extensions);
|
||||||
|
}
|
||||||
|
|
||||||
static int
|
static int
|
||||||
get_supported_mech (server *serv, const char *list)
|
get_supported_mech (server *serv, const char *list)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,127 @@
|
|||||||
|
/* Relocate resources before GTK, Fontconfig, GIO or OpenSSL initialize.
|
||||||
|
* GPL-2.0-or-later; see COPYING. */
|
||||||
|
#include "config.h"
|
||||||
|
#include "macos.h"
|
||||||
|
#include <gio/gio.h>
|
||||||
|
#include <mach-o/dyld.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
static char *locale_dir;
|
||||||
|
|
||||||
|
static void
|
||||||
|
set_path (const char *variable, const char *base, const char *relative)
|
||||||
|
{
|
||||||
|
char *path = g_build_filename (base, relative, NULL);
|
||||||
|
g_setenv (variable, path, TRUE);
|
||||||
|
g_free (path);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
relocate_cache (const char *resources, const char *name, const char *variable)
|
||||||
|
{
|
||||||
|
char *input = g_build_filename (resources, name, NULL);
|
||||||
|
char *contents = NULL;
|
||||||
|
GError *error = NULL;
|
||||||
|
|
||||||
|
if (g_file_get_contents (input, &contents, NULL, &error))
|
||||||
|
{
|
||||||
|
/* GTK cache files require absolute module paths. Write only data to
|
||||||
|
* the user cache, never into the signed/read-only application bundle. */
|
||||||
|
char **parts = g_strsplit (contents, "@ZOITECHAT_RESOURCES@", -1);
|
||||||
|
char *escaped = g_strescape (resources, NULL);
|
||||||
|
char *relocated = g_strjoinv (escaped, parts);
|
||||||
|
g_free (escaped);
|
||||||
|
char *digest = g_compute_checksum_for_string (G_CHECKSUM_SHA256, relocated, -1);
|
||||||
|
char *directory = g_build_filename (g_get_user_cache_dir (), "zoitechat", "modules", digest, NULL);
|
||||||
|
char *output = g_build_filename (directory, name, NULL);
|
||||||
|
if (g_mkdir_with_parents (directory, 0700) == 0 &&
|
||||||
|
g_file_set_contents (output, relocated, -1, &error))
|
||||||
|
g_setenv (variable, output, TRUE);
|
||||||
|
else
|
||||||
|
g_warning ("Cannot relocate %s: %s", name, error ? error->message : "cannot create cache directory");
|
||||||
|
g_free (output);
|
||||||
|
g_free (directory);
|
||||||
|
g_free (digest);
|
||||||
|
g_free (relocated);
|
||||||
|
g_strfreev (parts);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
g_warning ("Cannot read %s: %s", input, error->message);
|
||||||
|
g_clear_error (&error);
|
||||||
|
g_free (contents);
|
||||||
|
g_free (input);
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
zoitechat_macos_init (void)
|
||||||
|
{
|
||||||
|
uint32_t size = 0;
|
||||||
|
char *raw, *executable, *macos, *contents, *resources, *marker;
|
||||||
|
char *ca;
|
||||||
|
GTlsDatabase *database;
|
||||||
|
GError *error = NULL;
|
||||||
|
|
||||||
|
_NSGetExecutablePath (NULL, &size);
|
||||||
|
raw = g_malloc (size);
|
||||||
|
if (_NSGetExecutablePath (raw, &size) != 0)
|
||||||
|
{
|
||||||
|
g_free (raw);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
executable = realpath (raw, NULL);
|
||||||
|
g_free (raw);
|
||||||
|
if (!executable)
|
||||||
|
return;
|
||||||
|
macos = g_path_get_dirname (executable);
|
||||||
|
free (executable);
|
||||||
|
contents = g_path_get_dirname (macos);
|
||||||
|
resources = g_build_filename (contents, "Resources", NULL);
|
||||||
|
marker = g_build_filename (resources, "zoitechat-bundle", NULL);
|
||||||
|
g_free (macos);
|
||||||
|
g_free (contents);
|
||||||
|
if (!g_file_test (marker, G_FILE_TEST_IS_REGULAR))
|
||||||
|
{
|
||||||
|
g_free (marker);
|
||||||
|
g_free (resources);
|
||||||
|
return; /* Ordinary Meson installs and build-tree runs retain their paths. */
|
||||||
|
}
|
||||||
|
g_free (marker);
|
||||||
|
g_setenv ("GDK_BACKEND", "quartz", TRUE);
|
||||||
|
set_path ("ZOITECHAT_MACOS_SHARE", resources, "share");
|
||||||
|
set_path ("XDG_DATA_DIRS", resources, "share");
|
||||||
|
set_path ("GSETTINGS_SCHEMA_DIR", resources, "share/glib-2.0/schemas");
|
||||||
|
set_path ("FONTCONFIG_FILE", resources, "fonts.conf");
|
||||||
|
set_path ("GIO_EXTRA_MODULES", resources, "lib/gio/modules");
|
||||||
|
set_path ("GIO_MODULE_DIR", resources, "lib/gio/modules");
|
||||||
|
set_path ("ZOITECHAT_LIBDIR", resources, "lib/zoitechat/plugins");
|
||||||
|
set_path ("ZOITECHAT_EMOJI_FONT", resources, "share/fonts/zoitechat/NotoColorEmoji.ttf");
|
||||||
|
set_path ("ZOITECHAT_ENCHANT_LIBRARY", resources, "lib/libenchant-2.dylib");
|
||||||
|
g_setenv ("ZOITECHAT_ENCHANT_PREFIX", resources, TRUE);
|
||||||
|
set_path ("SSL_CERT_FILE", resources, "certs/ca-bundle.crt");
|
||||||
|
set_path ("OPENSSL_MODULES", resources, "lib/ossl-modules");
|
||||||
|
locale_dir = g_build_filename (resources, "share/locale", NULL);
|
||||||
|
relocate_cache (resources, "pixbuf-loaders.cache", "GDK_PIXBUF_MODULE_FILE");
|
||||||
|
relocate_cache (resources, "gtk-immodules.cache", "GTK_IM_MODULE_FILE");
|
||||||
|
|
||||||
|
/* HTTPS previews use GIO; IRC uses OpenSSL. Give both the same CA set. */
|
||||||
|
ca = g_build_filename (resources, "certs/ca-bundle.crt", NULL);
|
||||||
|
database = g_tls_file_database_new (ca, &error);
|
||||||
|
if (database)
|
||||||
|
{
|
||||||
|
g_tls_backend_set_default_database (g_tls_backend_get_default (), database);
|
||||||
|
g_object_unref (database);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
g_warning ("Cannot load bundled HTTPS trust database: %s", error->message);
|
||||||
|
g_clear_error (&error);
|
||||||
|
g_free (ca);
|
||||||
|
g_free (resources);
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *
|
||||||
|
zoitechat_macos_locale_dir (void)
|
||||||
|
{
|
||||||
|
return locale_dir ? locale_dir : LOCALEDIR;
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
/* macOS bundle runtime; GPL-2.0-or-later, see COPYING. */
|
||||||
|
#ifndef ZOITECHAT_MACOS_H
|
||||||
|
#define ZOITECHAT_MACOS_H
|
||||||
|
void zoitechat_macos_init (void);
|
||||||
|
const char *zoitechat_macos_locale_dir (void);
|
||||||
|
#endif
|
||||||
+60
-3
@@ -3,6 +3,7 @@ common_sources = [
|
|||||||
'chanopt.c',
|
'chanopt.c',
|
||||||
'ctcp.c',
|
'ctcp.c',
|
||||||
'dcc.c',
|
'dcc.c',
|
||||||
|
'emoji-data.c',
|
||||||
'gtk3-theme-service.c',
|
'gtk3-theme-service.c',
|
||||||
'zoitechat.c',
|
'zoitechat.c',
|
||||||
'history.c',
|
'history.c',
|
||||||
@@ -24,9 +25,14 @@ common_sources = [
|
|||||||
'tree.c',
|
'tree.c',
|
||||||
'url.c',
|
'url.c',
|
||||||
'userlist.c',
|
'userlist.c',
|
||||||
|
'hexcolor.c',
|
||||||
'util.c'
|
'util.c'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
if host_machine.system() == 'darwin'
|
||||||
|
common_sources += 'macos.c'
|
||||||
|
endif
|
||||||
|
|
||||||
secretstore_sources = files('secretstore.c')
|
secretstore_sources = files('secretstore.c')
|
||||||
|
|
||||||
common_sysinfo_deps = []
|
common_sysinfo_deps = []
|
||||||
@@ -109,6 +115,20 @@ public_suffix_data = custom_target('public_suffix_data_h',
|
|||||||
command: [python3, files('gen-public-suffix.py'), '@OUTPUT@']
|
command: [python3, files('gen-public-suffix.py'), '@OUTPUT@']
|
||||||
)
|
)
|
||||||
|
|
||||||
|
emoji_data_table = custom_target('emoji_data_table_h',
|
||||||
|
input: [
|
||||||
|
'../../data/emoji/emoji-test.txt',
|
||||||
|
'../../data/emoji/cldr-annotations-en.xml',
|
||||||
|
'../../data/emoji/cldr-annotations-derived-en.xml',
|
||||||
|
'../../data/emoji/gemoji.json',
|
||||||
|
'../../data/emoji/VERSIONS',
|
||||||
|
],
|
||||||
|
output: 'emoji-data-table.h',
|
||||||
|
command: [python3, files('../../tools/gen-emoji-data.py'),
|
||||||
|
'@INPUT0@', '@INPUT1@', '@INPUT2@', '@INPUT3@', '@INPUT4@',
|
||||||
|
'@OUTPUT@']
|
||||||
|
)
|
||||||
|
|
||||||
textevents = custom_target('textevents',
|
textevents = custom_target('textevents',
|
||||||
input: 'textevents.in',
|
input: 'textevents.in',
|
||||||
output: ['textevents.h', 'textenums.h'],
|
output: ['textevents.h', 'textenums.h'],
|
||||||
@@ -132,19 +152,26 @@ if get_option('plugin')
|
|||||||
endif
|
endif
|
||||||
|
|
||||||
zoitechat_common = static_library('zoitechatcommon',
|
zoitechat_common = static_library('zoitechatcommon',
|
||||||
sources: [textevents, public_suffix_data] + marshal + common_sources + secretstore_sources,
|
sources: [textevents, public_suffix_data, emoji_data_table] + marshal + common_sources + secretstore_sources,
|
||||||
include_directories: config_h_include,
|
include_directories: config_h_include,
|
||||||
dependencies: common_deps + common_sysinfo_deps,
|
dependencies: common_deps + common_sysinfo_deps,
|
||||||
c_args: common_cflags,
|
c_args: common_cflags,
|
||||||
pic: true
|
pic: true
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# zoitechat.h exposes OpenSSL types whenever TLS is enabled. Propagate its
|
||||||
|
# header requirements even to standalone consumers that do not call OpenSSL.
|
||||||
|
zoitechat_common_header_deps = [libgio_dep]
|
||||||
|
if libssl_dep.found()
|
||||||
|
zoitechat_common_header_deps += libssl_dep.partial_dependency(compile_args: true, includes: true)
|
||||||
|
endif
|
||||||
|
|
||||||
zoitechat_common_dep = declare_dependency(
|
zoitechat_common_dep = declare_dependency(
|
||||||
sources: [textevents, public_suffix_data] + marshal,
|
sources: [textevents, public_suffix_data] + marshal,
|
||||||
link_with: zoitechat_common,
|
link_with: zoitechat_common,
|
||||||
include_directories: common_includes,
|
include_directories: common_includes,
|
||||||
compile_args: common_cflags,
|
compile_args: common_cflags,
|
||||||
dependencies: libgio_dep,
|
dependencies: zoitechat_common_header_deps,
|
||||||
)
|
)
|
||||||
|
|
||||||
zoitechat_plugin_dep = declare_dependency(
|
zoitechat_plugin_dep = declare_dependency(
|
||||||
@@ -160,10 +187,40 @@ gtk3_theme_service_tests = executable('gtk3_theme_service_tests',
|
|||||||
'gtk3-theme-service.c',
|
'gtk3-theme-service.c',
|
||||||
],
|
],
|
||||||
include_directories: [config_h_include, include_directories('.')],
|
include_directories: [config_h_include, include_directories('.')],
|
||||||
dependencies: [libgio_dep] + (libarchive_dep.found() ? [libarchive_dep] : []),
|
dependencies: zoitechat_common_header_deps + (libarchive_dep.found() ? [libarchive_dep] : []),
|
||||||
)
|
)
|
||||||
|
|
||||||
test('GTK3 Theme Service Tests', gtk3_theme_service_tests,
|
test('GTK3 Theme Service Tests', gtk3_theme_service_tests,
|
||||||
protocol: 'tap',
|
protocol: 'tap',
|
||||||
timeout: 120,
|
timeout: 120,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
emoji_data_tests = executable('emoji_data_tests',
|
||||||
|
[
|
||||||
|
'tests/test-emoji-data.c',
|
||||||
|
'emoji-data.c',
|
||||||
|
emoji_data_table,
|
||||||
|
],
|
||||||
|
include_directories: [config_h_include, include_directories('.')],
|
||||||
|
dependencies: [libgio_dep],
|
||||||
|
)
|
||||||
|
|
||||||
|
test('Emoji Data Tests', emoji_data_tests,
|
||||||
|
protocol: 'tap',
|
||||||
|
timeout: 120,
|
||||||
|
)
|
||||||
|
|
||||||
|
hexcolor_tests = executable('hexcolor_tests',
|
||||||
|
[
|
||||||
|
'tests/test-hexcolor.c',
|
||||||
|
'hexcolor.c',
|
||||||
|
'util.c',
|
||||||
|
],
|
||||||
|
include_directories: [config_h_include, include_directories('.')],
|
||||||
|
dependencies: [libgio_dep] + (libssl_dep.found() ? [libssl_dep] : []),
|
||||||
|
)
|
||||||
|
|
||||||
|
test('Hex Color Tests', hexcolor_tests,
|
||||||
|
protocol: 'tap',
|
||||||
|
timeout: 120,
|
||||||
|
)
|
||||||
|
|||||||
+1
-1
@@ -662,7 +662,7 @@ notify_isnotify (struct session *sess, char *name)
|
|||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
notify_cleanup ()
|
notify_cleanup (void)
|
||||||
{
|
{
|
||||||
GSList *list = notify_list;
|
GSList *list = notify_list;
|
||||||
GSList *nslist, *srvlist;
|
GSList *nslist, *srvlist;
|
||||||
|
|||||||
@@ -2826,6 +2826,7 @@ static gboolean
|
|||||||
client_tag_allowed (server *serv, const char *tag)
|
client_tag_allowed (server *serv, const char *tag)
|
||||||
{
|
{
|
||||||
char **deny;
|
char **deny;
|
||||||
|
gboolean blocked = FALSE;
|
||||||
int i;
|
int i;
|
||||||
|
|
||||||
if (!serv->have_message_tags)
|
if (!serv->have_message_tags)
|
||||||
@@ -2837,15 +2838,16 @@ client_tag_allowed (server *serv, const char *tag)
|
|||||||
deny = g_strsplit (serv->clienttagdeny, ",", 0);
|
deny = g_strsplit (serv->clienttagdeny, ",", 0);
|
||||||
for (i = 0; deny[i]; i++)
|
for (i = 0; deny[i]; i++)
|
||||||
{
|
{
|
||||||
if (!strcmp (deny[i], "*") || !strcmp (deny[i], tag) || (deny[i][0] == '+' && !strcmp (deny[i] + 1, tag)))
|
if (!strcmp (deny[i], "*"))
|
||||||
{
|
blocked = TRUE;
|
||||||
g_strfreev (deny);
|
else if (deny[i][0] == '-' && !strcmp (deny[i] + 1, tag))
|
||||||
return FALSE;
|
blocked = FALSE;
|
||||||
}
|
else if (!strcmp (deny[i], tag) || (deny[i][0] == '+' && !strcmp (deny[i] + 1, tag)))
|
||||||
|
blocked = TRUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
g_strfreev (deny);
|
g_strfreev (deny);
|
||||||
return TRUE;
|
return !blocked;
|
||||||
}
|
}
|
||||||
|
|
||||||
static char *
|
static char *
|
||||||
|
|||||||
@@ -1288,6 +1288,11 @@ process_named_msg (session *sess, char *type, char *word[], char *word_eol[],
|
|||||||
char *user = net && net->user ? net->user : prefs.hex_irc_user_name;
|
char *user = net && net->user ? net->user : prefs.hex_irc_user_name;
|
||||||
|
|
||||||
response = challengeauth_response (user, serv->password, word[5]);
|
response = challengeauth_response (user, serv->password, word[5]);
|
||||||
|
if (response == NULL)
|
||||||
|
{
|
||||||
|
PrintTextf (serv->server_session, _("Error building CHALLENGEAUTH for %s"), user);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
tcp_sendf (serv, "PRIVMSG %s :CHALLENGEAUTH %s %s %s\r\n",
|
tcp_sendf (serv, "PRIVMSG %s :CHALLENGEAUTH %s %s %s\r\n",
|
||||||
CHALLENGEAUTH_NICK,
|
CHALLENGEAUTH_NICK,
|
||||||
|
|||||||
+3
-3
@@ -155,7 +155,7 @@ _SSL_get_cert_info (struct cert_info *cert_info, SSL * ssl)
|
|||||||
{
|
{
|
||||||
X509 *peer_cert;
|
X509 *peer_cert;
|
||||||
const X509_PUBKEY *key;
|
const X509_PUBKEY *key;
|
||||||
X509_ALGOR *algor = NULL;
|
ASN1_OBJECT *obj = NULL;
|
||||||
EVP_PKEY *peer_pkey;
|
EVP_PKEY *peer_pkey;
|
||||||
char notBefore[64];
|
char notBefore[64];
|
||||||
char notAfter[64];
|
char notAfter[64];
|
||||||
@@ -174,10 +174,10 @@ _SSL_get_cert_info (struct cert_info *cert_info, SSL * ssl)
|
|||||||
broke_oneline (cert_info->issuer, cert_info->issuer_word);
|
broke_oneline (cert_info->issuer, cert_info->issuer_word);
|
||||||
|
|
||||||
key = X509_get_X509_PUBKEY(peer_cert);
|
key = X509_get_X509_PUBKEY(peer_cert);
|
||||||
if (!X509_PUBKEY_get0_param(NULL, NULL, 0, &algor, key))
|
if (!X509_PUBKEY_get0_param(&obj, NULL, NULL, NULL, key))
|
||||||
return 1;
|
return 1;
|
||||||
|
|
||||||
alg = OBJ_obj2nid (algor->algorithm);
|
alg = OBJ_obj2nid (obj);
|
||||||
#ifndef HAVE_X509_GET_SIGNATURE_NID
|
#ifndef HAVE_X509_GET_SIGNATURE_NID
|
||||||
sign_alg = OBJ_obj2nid (peer_cert->sig_alg->algorithm);
|
sign_alg = OBJ_obj2nid (peer_cert->sig_alg->algorithm);
|
||||||
#else
|
#else
|
||||||
|
|||||||
@@ -0,0 +1,243 @@
|
|||||||
|
/* ZoiteChat
|
||||||
|
* Copyright (C) 2026 deepend-tildeclub.
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation; either version 2 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program; if not, write to the Free Software
|
||||||
|
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include <glib.h>
|
||||||
|
|
||||||
|
#include "../emoji-data.h"
|
||||||
|
|
||||||
|
#define GRINNING_FACE "\360\237\230\200" /* 😀 */
|
||||||
|
#define DUCK "\360\237\246\206" /* 🦆 */
|
||||||
|
#define THUMBS_UP "\360\237\221\215" /* 👍 */
|
||||||
|
#define WAVE_DARK "\360\237\221\213\360\237\217\277" /* 👋🏿 */
|
||||||
|
#define MEDIUM_TONE_MODIFIER "\360\237\217\275" /* U+1F3FD */
|
||||||
|
#define KEYCAP_HASH "#\357\270\217\342\203\243" /* #️⃣ */
|
||||||
|
|
||||||
|
static const EmojiEntry *
|
||||||
|
find_by_name (const char *name)
|
||||||
|
{
|
||||||
|
gsize i;
|
||||||
|
|
||||||
|
for (i = 0; i < emoji_data_count (); i++)
|
||||||
|
{
|
||||||
|
const EmojiEntry *entry = emoji_data_entry (i);
|
||||||
|
|
||||||
|
if (!strcmp (entry->name, name))
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
test_catalog_integrity (void)
|
||||||
|
{
|
||||||
|
gsize i;
|
||||||
|
|
||||||
|
g_assert_cmpuint (emoji_data_count (), >=, 1800);
|
||||||
|
|
||||||
|
for (i = 0; i < emoji_data_count (); i++)
|
||||||
|
{
|
||||||
|
const EmojiEntry *entry = emoji_data_entry (i);
|
||||||
|
|
||||||
|
g_assert_nonnull (entry->sequence);
|
||||||
|
g_assert_true (*entry->sequence != '\0');
|
||||||
|
g_assert_true (g_utf8_validate (entry->sequence, -1, NULL));
|
||||||
|
g_assert_nonnull (entry->name);
|
||||||
|
g_assert_true (*entry->name != '\0');
|
||||||
|
g_assert_true (g_utf8_validate (entry->name, -1, NULL));
|
||||||
|
g_assert_nonnull (entry->search);
|
||||||
|
g_assert_true (*entry->search != '\0');
|
||||||
|
g_assert_nonnull (entry->aliases);
|
||||||
|
g_assert_cmpuint (entry->group, <, EMOJI_GROUP_COUNT);
|
||||||
|
g_assert_cmpint (entry->tone_set, >=, -1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
test_cldr_order (void)
|
||||||
|
{
|
||||||
|
const EmojiEntry *first = emoji_data_entry (0);
|
||||||
|
|
||||||
|
g_assert_cmpstr (first->sequence, ==, GRINNING_FACE);
|
||||||
|
g_assert_cmpuint (first->group, ==, EMOJI_GROUP_SMILEYS_EMOTION);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
test_groups (void)
|
||||||
|
{
|
||||||
|
int group;
|
||||||
|
gboolean seen[EMOJI_GROUP_COUNT] = { FALSE };
|
||||||
|
gsize i;
|
||||||
|
|
||||||
|
for (group = 0; group < EMOJI_GROUP_COUNT; group++)
|
||||||
|
{
|
||||||
|
g_assert_true (*emoji_data_group_display_name (group) != '\0');
|
||||||
|
g_assert_true (*emoji_data_group_icon_sequence (group) != '\0');
|
||||||
|
}
|
||||||
|
|
||||||
|
for (i = 0; i < emoji_data_count (); i++)
|
||||||
|
seen[emoji_data_entry (i)->group] = TRUE;
|
||||||
|
for (group = 0; group < EMOJI_GROUP_COUNT; group++)
|
||||||
|
g_assert_true (seen[group]);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
test_search_by_keyword (void)
|
||||||
|
{
|
||||||
|
const EmojiEntry *duck = find_by_name ("duck");
|
||||||
|
char **tokens = emoji_search_tokenize ("Duck");
|
||||||
|
|
||||||
|
g_assert_nonnull (duck);
|
||||||
|
g_assert_cmpstr (duck->sequence, ==, DUCK);
|
||||||
|
g_assert_nonnull (tokens);
|
||||||
|
g_assert_true (emoji_entry_matches (duck, tokens));
|
||||||
|
g_strfreev (tokens);
|
||||||
|
|
||||||
|
tokens = emoji_search_tokenize ("bird");
|
||||||
|
g_assert_true (emoji_entry_matches (duck, tokens));
|
||||||
|
g_strfreev (tokens);
|
||||||
|
|
||||||
|
tokens = emoji_search_tokenize ("no-such-emoji-keyword");
|
||||||
|
g_assert_false (emoji_entry_matches (duck, tokens));
|
||||||
|
g_strfreev (tokens);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
test_search_by_alias (void)
|
||||||
|
{
|
||||||
|
const EmojiEntry *thumbs = find_by_name ("thumbs up");
|
||||||
|
char **tokens;
|
||||||
|
|
||||||
|
g_assert_nonnull (thumbs);
|
||||||
|
g_assert_nonnull (strstr (thumbs->aliases, "thumbsup"));
|
||||||
|
|
||||||
|
tokens = emoji_search_tokenize (":thumbsup:");
|
||||||
|
g_assert_nonnull (tokens);
|
||||||
|
g_assert_true (emoji_entry_matches (thumbs, tokens));
|
||||||
|
g_strfreev (tokens);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
test_multi_token_search (void)
|
||||||
|
{
|
||||||
|
const EmojiEntry *flag = find_by_name ("flag: Canada");
|
||||||
|
char **tokens = emoji_search_tokenize ("flag canada");
|
||||||
|
|
||||||
|
g_assert_nonnull (flag);
|
||||||
|
g_assert_cmpuint (flag->group, ==, EMOJI_GROUP_FLAGS);
|
||||||
|
g_assert_nonnull (tokens);
|
||||||
|
g_assert_true (emoji_entry_matches (flag, tokens));
|
||||||
|
g_strfreev (tokens);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
test_skin_tones (void)
|
||||||
|
{
|
||||||
|
const EmojiEntry *thumbs = find_by_name ("thumbs up");
|
||||||
|
const EmojiEntry *duck = find_by_name ("duck");
|
||||||
|
const char *toned;
|
||||||
|
|
||||||
|
g_assert_nonnull (thumbs);
|
||||||
|
g_assert_cmpint (thumbs->tone_set, >=, 0);
|
||||||
|
|
||||||
|
toned = emoji_entry_sequence_for_tone (thumbs, EMOJI_TONE_MEDIUM);
|
||||||
|
g_assert_cmpstr (toned, !=, thumbs->sequence);
|
||||||
|
g_assert_nonnull (strstr (toned, MEDIUM_TONE_MODIFIER));
|
||||||
|
g_assert_cmpstr (emoji_entry_sequence_for_tone (thumbs, EMOJI_TONE_NONE),
|
||||||
|
==, thumbs->sequence);
|
||||||
|
|
||||||
|
g_assert_cmpstr (emoji_entry_sequence_for_tone (thumbs, (EmojiTone) -1), ==, thumbs->sequence);
|
||||||
|
g_assert_cmpstr (emoji_entry_sequence_for_tone (thumbs, EMOJI_TONE_COUNT), ==, thumbs->sequence);
|
||||||
|
g_assert_nonnull (duck);
|
||||||
|
g_assert_cmpint (duck->tone_set, ==, -1);
|
||||||
|
g_assert_cmpstr (emoji_entry_sequence_for_tone (duck, EMOJI_TONE_DARK),
|
||||||
|
==, duck->sequence);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
test_find_by_sequence (void)
|
||||||
|
{
|
||||||
|
EmojiTone tone;
|
||||||
|
const EmojiEntry *entry;
|
||||||
|
|
||||||
|
entry = emoji_data_find_by_sequence (GRINNING_FACE, &tone);
|
||||||
|
g_assert_nonnull (entry);
|
||||||
|
g_assert_cmpstr (entry->name, ==, "grinning face");
|
||||||
|
g_assert_cmpuint (tone, ==, EMOJI_TONE_NONE);
|
||||||
|
|
||||||
|
entry = emoji_data_find_by_sequence (WAVE_DARK, &tone);
|
||||||
|
g_assert_nonnull (entry);
|
||||||
|
g_assert_cmpstr (entry->name, ==, "waving hand");
|
||||||
|
g_assert_cmpuint (tone, ==, EMOJI_TONE_DARK);
|
||||||
|
|
||||||
|
g_assert_null (emoji_data_find_by_sequence ("not an emoji", &tone));
|
||||||
|
g_assert_null (emoji_data_find_by_sequence ("", NULL));
|
||||||
|
g_assert_null (emoji_data_find_by_sequence (NULL, NULL));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
test_grapheme_sequences_intact (void)
|
||||||
|
{
|
||||||
|
const EmojiEntry *keycap = find_by_name ("keycap: #");
|
||||||
|
const EmojiEntry *family = find_by_name ("family: man, woman, boy");
|
||||||
|
|
||||||
|
g_assert_nonnull (keycap);
|
||||||
|
g_assert_cmpstr (keycap->sequence, ==, KEYCAP_HASH);
|
||||||
|
|
||||||
|
g_assert_nonnull (family);
|
||||||
|
g_assert_nonnull (strstr (family->sequence, "\342\200\215"));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
test_tokenize_edge_cases (void)
|
||||||
|
{
|
||||||
|
g_assert_null (emoji_search_tokenize (NULL));
|
||||||
|
g_assert_null (emoji_search_tokenize (""));
|
||||||
|
g_assert_null (emoji_search_tokenize (" "));
|
||||||
|
g_assert_null (emoji_search_tokenize (":::"));
|
||||||
|
g_assert_null (emoji_search_tokenize ("\377"));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
test_versions (void)
|
||||||
|
{
|
||||||
|
g_assert_true (*emoji_data_unicode_version () != '\0');
|
||||||
|
g_assert_true (*emoji_data_cldr_version () != '\0');
|
||||||
|
}
|
||||||
|
|
||||||
|
int
|
||||||
|
main (int argc, char **argv)
|
||||||
|
{
|
||||||
|
g_test_init (&argc, &argv, NULL);
|
||||||
|
g_test_add_func ("/emoji-data/catalog-integrity", test_catalog_integrity);
|
||||||
|
g_test_add_func ("/emoji-data/cldr-order", test_cldr_order);
|
||||||
|
g_test_add_func ("/emoji-data/groups", test_groups);
|
||||||
|
g_test_add_func ("/emoji-data/search-by-keyword", test_search_by_keyword);
|
||||||
|
g_test_add_func ("/emoji-data/search-by-alias", test_search_by_alias);
|
||||||
|
g_test_add_func ("/emoji-data/multi-token-search", test_multi_token_search);
|
||||||
|
g_test_add_func ("/emoji-data/skin-tones", test_skin_tones);
|
||||||
|
g_test_add_func ("/emoji-data/find-by-sequence", test_find_by_sequence);
|
||||||
|
g_test_add_func ("/emoji-data/grapheme-sequences-intact",
|
||||||
|
test_grapheme_sequences_intact);
|
||||||
|
g_test_add_func ("/emoji-data/tokenize-edge-cases",
|
||||||
|
test_tokenize_edge_cases);
|
||||||
|
g_test_add_func ("/emoji-data/versions", test_versions);
|
||||||
|
return g_test_run ();
|
||||||
|
}
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
/* ZoiteChat
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation; either version 2 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program; if not, write to the Free Software
|
||||||
|
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include <glib.h>
|
||||||
|
|
||||||
|
#include "../hexcolor.h"
|
||||||
|
#include "../util.h"
|
||||||
|
|
||||||
|
static void
|
||||||
|
test_parse_foreground (void)
|
||||||
|
{
|
||||||
|
guint32 fg = 0, bg = 0xDEAD;
|
||||||
|
int has_bg = TRUE;
|
||||||
|
|
||||||
|
g_assert_cmpint (hexcolor_parse ("FF8800text", 10, &fg, &bg, &has_bg), ==, 6);
|
||||||
|
g_assert_cmphex (fg, ==, 0xFF8800);
|
||||||
|
g_assert_false (has_bg);
|
||||||
|
g_assert_cmphex (bg, ==, 0xDEAD);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
test_parse_foreground_and_background (void)
|
||||||
|
{
|
||||||
|
guint32 fg = 0, bg = 0;
|
||||||
|
int has_bg = FALSE;
|
||||||
|
|
||||||
|
g_assert_cmpint (hexcolor_parse ("112233,a0b0c0x", 14, &fg, &bg, &has_bg), ==, 13);
|
||||||
|
g_assert_cmphex (fg, ==, 0x112233);
|
||||||
|
g_assert_cmphex (bg, ==, 0xA0B0C0);
|
||||||
|
g_assert_true (has_bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
test_parse_bad_background_keeps_foreground (void)
|
||||||
|
{
|
||||||
|
guint32 fg = 0;
|
||||||
|
int has_bg = TRUE;
|
||||||
|
|
||||||
|
/* ",12" is plain text after a valid foreground, not a background */
|
||||||
|
g_assert_cmpint (hexcolor_parse ("112233,12 hi", 12, &fg, NULL, &has_bg), ==, 6);
|
||||||
|
g_assert_cmphex (fg, ==, 0x112233);
|
||||||
|
g_assert_false (has_bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
test_parse_bare (void)
|
||||||
|
{
|
||||||
|
/* a bare \004 resets colors, like a bare \003 */
|
||||||
|
g_assert_cmpint (hexcolor_parse (" text", 5, NULL, NULL, NULL), ==, 0);
|
||||||
|
g_assert_cmpint (hexcolor_parse ("12345G", 6, NULL, NULL, NULL), ==, 0);
|
||||||
|
g_assert_cmpint (hexcolor_parse ("", 0, NULL, NULL, NULL), ==, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
test_parse_respects_length (void)
|
||||||
|
{
|
||||||
|
g_assert_cmpint (hexcolor_parse ("FF8800", 5, NULL, NULL, NULL), ==, 0);
|
||||||
|
g_assert_cmpint (hexcolor_parse ("FF8800,112233", 12, NULL, NULL, NULL), ==, 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
assert_stripped (const char *in, int flags, const char *expected)
|
||||||
|
{
|
||||||
|
char out[128];
|
||||||
|
|
||||||
|
strip_color2 (in, -1, out, flags);
|
||||||
|
g_assert_cmpstr (out, ==, expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
test_strip (void)
|
||||||
|
{
|
||||||
|
assert_stripped ("\004FF8800alice\017: hi", STRIP_ALL, "alice: hi");
|
||||||
|
assert_stripped ("\004112233,445566bg", STRIP_ALL, "bg");
|
||||||
|
assert_stripped ("a\004 b", STRIP_ALL, "a b");
|
||||||
|
assert_stripped ("\004FF8800\00312x", STRIP_ALL, "x");
|
||||||
|
/* a trailing code with too few digits leaves the rest as text */
|
||||||
|
assert_stripped ("x\004FF88", STRIP_ALL, "xFF88");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
test_strip_keeps_hex_without_strip_color (void)
|
||||||
|
{
|
||||||
|
assert_stripped ("\004FF8800red\002", STRIP_ATTRIB, "\004FF8800red");
|
||||||
|
}
|
||||||
|
|
||||||
|
int
|
||||||
|
main (int argc, char *argv[])
|
||||||
|
{
|
||||||
|
g_test_init (&argc, &argv, NULL);
|
||||||
|
|
||||||
|
g_test_add_func ("/hexcolor/parse/foreground", test_parse_foreground);
|
||||||
|
g_test_add_func ("/hexcolor/parse/foreground-and-background", test_parse_foreground_and_background);
|
||||||
|
g_test_add_func ("/hexcolor/parse/bad-background", test_parse_bad_background_keeps_foreground);
|
||||||
|
g_test_add_func ("/hexcolor/parse/bare", test_parse_bare);
|
||||||
|
g_test_add_func ("/hexcolor/parse/length", test_parse_respects_length);
|
||||||
|
g_test_add_func ("/hexcolor/strip/all", test_strip);
|
||||||
|
g_test_add_func ("/hexcolor/strip/attrib-only", test_strip_keeps_hex_without_strip_color);
|
||||||
|
|
||||||
|
return g_test_run ();
|
||||||
|
}
|
||||||
+3
-3
@@ -1808,7 +1808,7 @@ pevent_check_all_loaded (void)
|
|||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
load_text_events ()
|
load_text_events (void)
|
||||||
{
|
{
|
||||||
memset (&pntevts_text, 0, sizeof (char *) * (NUM_XP));
|
memset (&pntevts_text, 0, sizeof (char *) * (NUM_XP));
|
||||||
memset (&pntevts, 0, sizeof (char *) * (NUM_XP));
|
memset (&pntevts, 0, sizeof (char *) * (NUM_XP));
|
||||||
@@ -2488,7 +2488,7 @@ sound_load_event (char *evt, char *file)
|
|||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
sound_load ()
|
sound_load (void)
|
||||||
{
|
{
|
||||||
int fd;
|
int fd;
|
||||||
char buf[512];
|
char buf[512];
|
||||||
@@ -2521,7 +2521,7 @@ sound_load ()
|
|||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
sound_save ()
|
sound_save (void)
|
||||||
{
|
{
|
||||||
int fd, i;
|
int fd, i;
|
||||||
char buf[512];
|
char buf[512];
|
||||||
|
|||||||
+16
-2
@@ -47,6 +47,7 @@
|
|||||||
#include "zoitechatc.h"
|
#include "zoitechatc.h"
|
||||||
#include <ctype.h>
|
#include <ctype.h>
|
||||||
#include "util.h"
|
#include "util.h"
|
||||||
|
#include "hexcolor.h"
|
||||||
|
|
||||||
#if defined (__FreeBSD__) || defined (__APPLE__)
|
#if defined (__FreeBSD__) || defined (__APPLE__)
|
||||||
#include <sys/sysctl.h>
|
#include <sys/sysctl.h>
|
||||||
@@ -330,6 +331,14 @@ strip_color2 (const char *src, int len, char *dst, int flags)
|
|||||||
if (!(flags & STRIP_COLOR)) goto pass_char;
|
if (!(flags & STRIP_COLOR)) goto pass_char;
|
||||||
rcol = 2;
|
rcol = 2;
|
||||||
break;
|
break;
|
||||||
|
case '\004': /*ATTR_HEXCOLOR: */
|
||||||
|
if (!(flags & STRIP_COLOR)) goto pass_char;
|
||||||
|
{
|
||||||
|
int skip = hexcolor_parse (src + 1, len, NULL, NULL, NULL);
|
||||||
|
src += skip;
|
||||||
|
len -= skip;
|
||||||
|
}
|
||||||
|
break;
|
||||||
case HIDDEN_CHAR: /* CL: invisible text (for event formats only) */ /* this takes care of the topic */
|
case HIDDEN_CHAR: /* CL: invisible text (for event formats only) */ /* this takes care of the topic */
|
||||||
if (!(flags & STRIP_HIDDEN)) goto pass_char;
|
if (!(flags & STRIP_HIDDEN)) goto pass_char;
|
||||||
break;
|
break;
|
||||||
@@ -1444,7 +1453,7 @@ challengeauth_response (const char *username, const char *password, const char *
|
|||||||
char *passhash;
|
char *passhash;
|
||||||
char *key;
|
char *key;
|
||||||
char *keyhash;
|
char *keyhash;
|
||||||
unsigned char *digest;
|
unsigned char digest[SHA256_DIGEST_LENGTH];
|
||||||
GString *buf = g_string_new_len (NULL, SHA256_DIGEST_LENGTH * 2);
|
GString *buf = g_string_new_len (NULL, SHA256_DIGEST_LENGTH * 2);
|
||||||
|
|
||||||
user = rfc_strlower (username); /* convert username to lowercase as per the RFC */
|
user = rfc_strlower (username); /* convert username to lowercase as per the RFC */
|
||||||
@@ -1460,7 +1469,12 @@ challengeauth_response (const char *username, const char *password, const char *
|
|||||||
keyhash = str_sha256hash (key);
|
keyhash = str_sha256hash (key);
|
||||||
g_free (key);
|
g_free (key);
|
||||||
|
|
||||||
digest = HMAC (EVP_sha256 (), keyhash, strlen (keyhash), (unsigned char *) challenge, strlen (challenge), NULL, NULL);
|
if (HMAC (EVP_sha256 (), keyhash, strlen (keyhash), (unsigned char *) challenge, strlen (challenge), digest, NULL) == NULL)
|
||||||
|
{
|
||||||
|
g_free (keyhash);
|
||||||
|
g_string_free (buf, TRUE);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
g_free (keyhash);
|
g_free (keyhash);
|
||||||
|
|
||||||
for (i = 0; i < SHA256_DIGEST_LENGTH; i++)
|
for (i = 0; i < SHA256_DIGEST_LENGTH; i++)
|
||||||
|
|||||||
+29
-3
@@ -54,6 +54,9 @@
|
|||||||
#include "text.h"
|
#include "text.h"
|
||||||
#include "url.h"
|
#include "url.h"
|
||||||
#include "zoitechatc.h"
|
#include "zoitechatc.h"
|
||||||
|
#ifdef __APPLE__
|
||||||
|
#include "macos.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
#if ! GLIB_CHECK_VERSION (2, 36, 0)
|
#if ! GLIB_CHECK_VERSION (2, 36, 0)
|
||||||
#include <glib-object.h> /* for g_type_init() */
|
#include <glib-object.h> /* for g_type_init() */
|
||||||
@@ -95,6 +98,7 @@ int zoitechat_is_quitting = FALSE;
|
|||||||
/* command-line args */
|
/* command-line args */
|
||||||
int arg_dont_autoconnect = FALSE;
|
int arg_dont_autoconnect = FALSE;
|
||||||
int arg_skip_plugins = FALSE;
|
int arg_skip_plugins = FALSE;
|
||||||
|
gint arg_run_as_root = FALSE;
|
||||||
char *arg_url = NULL;
|
char *arg_url = NULL;
|
||||||
char **arg_urls = NULL;
|
char **arg_urls = NULL;
|
||||||
char *arg_command = NULL;
|
char *arg_command = NULL;
|
||||||
@@ -463,8 +467,7 @@ doover:
|
|||||||
while (list)
|
while (list)
|
||||||
{
|
{
|
||||||
sess = list->data;
|
sess = list->data;
|
||||||
if (!sess->server->have_awaynotify)
|
sess->done_away_check = FALSE;
|
||||||
sess->done_away_check = FALSE;
|
|
||||||
list = list->next;
|
list = list->next;
|
||||||
}
|
}
|
||||||
loop++;
|
loop++;
|
||||||
@@ -1172,10 +1175,33 @@ main (int argc, char *argv[])
|
|||||||
int i;
|
int i;
|
||||||
int ret;
|
int ret;
|
||||||
|
|
||||||
|
#ifdef __APPLE__
|
||||||
|
zoitechat_macos_init ();
|
||||||
|
#endif
|
||||||
|
|
||||||
#ifdef WIN32
|
#ifdef WIN32
|
||||||
HRESULT coinit_result;
|
HRESULT coinit_result;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifdef __linux__
|
||||||
|
for (i = 1; i < argc; i++)
|
||||||
|
{
|
||||||
|
if (strcmp (argv[i], "--") == 0)
|
||||||
|
break;
|
||||||
|
if (strcmp (argv[i], "--run-as-root") == 0)
|
||||||
|
{
|
||||||
|
arg_run_as_root = TRUE;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (geteuid () == 0 && !arg_run_as_root)
|
||||||
|
{
|
||||||
|
fe_root_warning ();
|
||||||
|
return EXIT_FAILURE;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
srand ((unsigned int) time (NULL)); /* CL: do this only once! */
|
srand ((unsigned int) time (NULL)); /* CL: do this only once! */
|
||||||
|
|
||||||
/* We must check for the config dir parameter, otherwise load_config() will behave incorrectly.
|
/* We must check for the config dir parameter, otherwise load_config() will behave incorrectly.
|
||||||
@@ -1264,7 +1290,7 @@ main (int argc, char *argv[])
|
|||||||
#ifndef WIN32
|
#ifndef WIN32
|
||||||
#ifndef __EMX__
|
#ifndef __EMX__
|
||||||
/* OS/2 uses UID 0 all the time */
|
/* OS/2 uses UID 0 all the time */
|
||||||
if (getuid () == 0)
|
if (getuid () == 0 && !arg_run_as_root)
|
||||||
fe_message (_("* Running IRC as root is stupid! You should\n"
|
fe_message (_("* Running IRC as root is stupid! You should\n"
|
||||||
" create a User Account and use that to login.\n"), FE_MSG_WARN|FE_MSG_WAIT);
|
" create a User Account and use that to login.\n"), FE_MSG_WARN|FE_MSG_WAIT);
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -204,6 +204,7 @@ struct zoitechatprefs
|
|||||||
unsigned int hex_net_auto_reconnect;
|
unsigned int hex_net_auto_reconnect;
|
||||||
unsigned int hex_net_auto_reconnectonfail;
|
unsigned int hex_net_auto_reconnectonfail;
|
||||||
unsigned int hex_net_proxy_auth;
|
unsigned int hex_net_proxy_auth;
|
||||||
|
unsigned int hex_net_remote_media;
|
||||||
unsigned int hex_net_throttle;
|
unsigned int hex_net_throttle;
|
||||||
unsigned int hex_notify_whois_online;
|
unsigned int hex_notify_whois_online;
|
||||||
unsigned int hex_perl_warnings;
|
unsigned int hex_perl_warnings;
|
||||||
@@ -260,6 +261,7 @@ struct zoitechatprefs
|
|||||||
int hex_gui_dialog_left;
|
int hex_gui_dialog_left;
|
||||||
int hex_gui_dialog_top;
|
int hex_gui_dialog_top;
|
||||||
int hex_gui_dialog_width;
|
int hex_gui_dialog_width;
|
||||||
|
int hex_gui_emoji_skin_tone;
|
||||||
int hex_gui_lagometer;
|
int hex_gui_lagometer;
|
||||||
int hex_gui_lang;
|
int hex_gui_lang;
|
||||||
int hex_gui_pane_divider_position;
|
int hex_gui_pane_divider_position;
|
||||||
@@ -304,6 +306,7 @@ struct zoitechatprefs
|
|||||||
int hex_net_reconnect_delay;
|
int hex_net_reconnect_delay;
|
||||||
int hex_notify_timeout;
|
int hex_notify_timeout;
|
||||||
int hex_text_max_indent;
|
int hex_text_max_indent;
|
||||||
|
int hex_text_wrap_align;
|
||||||
int hex_text_max_lines;
|
int hex_text_max_lines;
|
||||||
int hex_url_grabber_limit;
|
int hex_url_grabber_limit;
|
||||||
|
|
||||||
@@ -313,6 +316,7 @@ struct zoitechatprefs
|
|||||||
char hex_dcc_completed_dir[PATHLEN + 1];
|
char hex_dcc_completed_dir[PATHLEN + 1];
|
||||||
char hex_dcc_dir[PATHLEN + 1];
|
char hex_dcc_dir[PATHLEN + 1];
|
||||||
char hex_dcc_ip[DOMAINLEN + 1];
|
char hex_dcc_ip[DOMAINLEN + 1];
|
||||||
|
char hex_gui_tab_font[FONTNAMELEN + 1];
|
||||||
char hex_gui_ulist_doubleclick[256];
|
char hex_gui_ulist_doubleclick[256];
|
||||||
char hex_input_command_char[4];
|
char hex_input_command_char[4];
|
||||||
char hex_irc_extra_hilight[300];
|
char hex_irc_extra_hilight[300];
|
||||||
@@ -447,6 +451,7 @@ typedef struct session
|
|||||||
int mode_timeout_tag;
|
int mode_timeout_tag;
|
||||||
int typing_timeout_tag;
|
int typing_timeout_tag;
|
||||||
int typing_status;
|
int typing_status;
|
||||||
|
gint64 typing_last_sent;
|
||||||
int typing_animation_tag;
|
int typing_animation_tag;
|
||||||
int typing_animation_frame;
|
int typing_animation_frame;
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ extern struct zoitechatprefs prefs;
|
|||||||
extern int zoitechat_is_quitting;
|
extern int zoitechat_is_quitting;
|
||||||
extern gint arg_skip_plugins; /* command-line args */
|
extern gint arg_skip_plugins; /* command-line args */
|
||||||
extern gint arg_dont_autoconnect;
|
extern gint arg_dont_autoconnect;
|
||||||
|
extern gint arg_run_as_root;
|
||||||
extern char *arg_url;
|
extern char *arg_url;
|
||||||
extern char **arg_urls;
|
extern char **arg_urls;
|
||||||
extern char *arg_command;
|
extern char *arg_command;
|
||||||
|
|||||||
+3
-3
@@ -138,7 +138,7 @@ ascii_open (void)
|
|||||||
{
|
{
|
||||||
table_pos++;
|
table_pos++;
|
||||||
hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0);
|
hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0);
|
||||||
gtk_container_add (GTK_CONTAINER (vbox), hbox);
|
gtk_box_pack_start (GTK_BOX (vbox), hbox, TRUE, TRUE, 0);
|
||||||
gtk_widget_show (hbox);
|
gtk_widget_show (hbox);
|
||||||
i++;
|
i++;
|
||||||
continue;
|
continue;
|
||||||
@@ -155,14 +155,14 @@ ascii_open (void)
|
|||||||
G_CALLBACK (ascii_click), NULL);
|
G_CALLBACK (ascii_click), NULL);
|
||||||
g_signal_connect (G_OBJECT (but), "enter-notify-event",
|
g_signal_connect (G_OBJECT (but), "enter-notify-event",
|
||||||
G_CALLBACK (ascii_enter), label);
|
G_CALLBACK (ascii_enter), label);
|
||||||
gtk_box_pack_start (GTK_BOX (hbox), but, 0, 0, 0);
|
gtk_box_pack_start (GTK_BOX (hbox), but, TRUE, TRUE, 0);
|
||||||
gtk_widget_show (but);
|
gtk_widget_show (but);
|
||||||
|
|
||||||
table_pos += len;
|
table_pos += len;
|
||||||
}
|
}
|
||||||
|
|
||||||
frame = gtk_frame_new ("");
|
frame = gtk_frame_new ("");
|
||||||
gtk_container_add (GTK_CONTAINER (hbox), frame);
|
gtk_box_pack_start (GTK_BOX (hbox), frame, TRUE, TRUE, 0);
|
||||||
gtk_container_add (GTK_CONTAINER (frame), label);
|
gtk_container_add (GTK_CONTAINER (frame), label);
|
||||||
gtk_widget_show (label);
|
gtk_widget_show (label);
|
||||||
gtk_widget_show (frame);
|
gtk_widget_show (frame);
|
||||||
|
|||||||
@@ -19,6 +19,8 @@
|
|||||||
|
|
||||||
/* file included in chanview.c */
|
/* file included in chanview.c */
|
||||||
|
|
||||||
|
#include "theme/theme-css.h"
|
||||||
|
|
||||||
typedef struct
|
typedef struct
|
||||||
{
|
{
|
||||||
GtkWidget *outer; /* outer box */
|
GtkWidget *outer; /* outer box */
|
||||||
@@ -690,6 +692,7 @@ cv_tabs_add (chanview *cv, chan *ch, char *name, GtkTreeIter *parent)
|
|||||||
GtkWidget *label;
|
GtkWidget *label;
|
||||||
GtkWidget *close_button;
|
GtkWidget *close_button;
|
||||||
GtkWidget *close_icon;
|
GtkWidget *close_icon;
|
||||||
|
GtkCssProvider *close_css;
|
||||||
|
|
||||||
but = gtk_toggle_button_new ();
|
but = gtk_toggle_button_new ();
|
||||||
gtk_widget_set_name (but, "zoitechat-tab");
|
gtk_widget_set_name (but, "zoitechat-tab");
|
||||||
@@ -698,9 +701,27 @@ cv_tabs_add (chanview *cv, chan *ch, char *name, GtkTreeIter *parent)
|
|||||||
cv_add_scroll_events (but);
|
cv_add_scroll_events (but);
|
||||||
hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 2);
|
hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 2);
|
||||||
label = gtk_label_new (name);
|
label = gtk_label_new (name);
|
||||||
|
if (prefs.hex_gui_tab_font[0])
|
||||||
|
{
|
||||||
|
PangoFontDescription *font_desc;
|
||||||
|
|
||||||
|
/* Keep activity colours and the Smaller text scale in the label attributes. */
|
||||||
|
font_desc = pango_font_description_from_string (prefs.hex_gui_tab_font);
|
||||||
|
gtkutil_apply_palette (label, NULL, NULL, font_desc);
|
||||||
|
pango_font_description_free (font_desc);
|
||||||
|
}
|
||||||
close_button = gtk_button_new ();
|
close_button = gtk_button_new ();
|
||||||
cv_add_scroll_events (close_button);
|
cv_add_scroll_events (close_button);
|
||||||
gtk_style_context_add_class (gtk_widget_get_style_context (close_button), "flat");
|
gtk_style_context_add_class (gtk_widget_get_style_context (close_button), "flat");
|
||||||
|
gtk_style_context_add_class (gtk_widget_get_style_context (close_button), "zoitechat-tab-close");
|
||||||
|
/* Theme button minimums and padding must not determine the tab height. */
|
||||||
|
close_css = gtk_css_provider_new ();
|
||||||
|
gtk_css_provider_load_from_data (close_css,
|
||||||
|
".zoitechat-tab-close { min-width: 0; min-height: 0; padding: 1px; margin: 0; }",
|
||||||
|
-1, NULL);
|
||||||
|
theme_css_apply_widget_provider (close_button, GTK_STYLE_PROVIDER (close_css));
|
||||||
|
g_object_unref (close_css);
|
||||||
|
gtk_widget_set_valign (close_button, GTK_ALIGN_CENTER);
|
||||||
close_icon = gtk_image_new_from_icon_name ("window-close-symbolic", GTK_ICON_SIZE_MENU);
|
close_icon = gtk_image_new_from_icon_name ("window-close-symbolic", GTK_ICON_SIZE_MENU);
|
||||||
gtk_image_set_pixel_size (GTK_IMAGE (close_icon), 8);
|
gtk_image_set_pixel_size (GTK_IMAGE (close_icon), 8);
|
||||||
gtk_button_set_always_show_image (GTK_BUTTON (close_button), TRUE);
|
gtk_button_set_always_show_image (GTK_BUTTON (close_button), TRUE);
|
||||||
|
|||||||
+28
-19
@@ -301,26 +301,35 @@ cv_tree_focus (chan *ch)
|
|||||||
path = gtk_tree_model_get_path (model, &ch->iter);
|
path = gtk_tree_model_get_path (model, &ch->iter);
|
||||||
if (path)
|
if (path)
|
||||||
{
|
{
|
||||||
/* This full section does what
|
if (!gtk_widget_get_realized (GTK_WIDGET (tree)))
|
||||||
* gtk_tree_view_scroll_to_cell (tree, path, NULL, TRUE, 0.5, 0.5);
|
|
||||||
* does, except it only scrolls the window if the provided cell is
|
|
||||||
* not visible. Basic algorithm taken from gtktreeview.c */
|
|
||||||
|
|
||||||
/* obtain information to see if the cell is visible */
|
|
||||||
gtk_tree_view_get_background_area (tree, path, NULL, &cell_rect);
|
|
||||||
gtk_tree_view_get_visible_rect (tree, &vis_rect);
|
|
||||||
|
|
||||||
/* The cordinates aren't offset correctly */
|
|
||||||
gtk_tree_view_convert_widget_to_bin_window_coords ( tree, cell_rect.x, cell_rect.y, NULL, &cell_rect.y );
|
|
||||||
|
|
||||||
/* only need to scroll if out of bounds */
|
|
||||||
if (cell_rect.y < vis_rect.y ||
|
|
||||||
cell_rect.y + cell_rect.height > vis_rect.y + vis_rect.height)
|
|
||||||
{
|
{
|
||||||
dest_y = cell_rect.y - ((vis_rect.height - cell_rect.height) * 0.5);
|
/* Layout changes can restore focus before the new tree is realized.
|
||||||
if (dest_y < 0)
|
* Let GTK track the row and scroll once its geometry is available. */
|
||||||
dest_y = 0;
|
gtk_tree_view_scroll_to_cell (tree, path, NULL, TRUE, 0.5, 0.5);
|
||||||
gtk_tree_view_scroll_to_point (tree, -1, dest_y);
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
/* This full section does what
|
||||||
|
* gtk_tree_view_scroll_to_cell (tree, path, NULL, TRUE, 0.5, 0.5);
|
||||||
|
* does, except it only scrolls the window if the provided cell is
|
||||||
|
* not visible. Basic algorithm taken from gtktreeview.c */
|
||||||
|
|
||||||
|
/* obtain information to see if the cell is visible */
|
||||||
|
gtk_tree_view_get_background_area (tree, path, NULL, &cell_rect);
|
||||||
|
gtk_tree_view_get_visible_rect (tree, &vis_rect);
|
||||||
|
|
||||||
|
/* The cordinates aren't offset correctly */
|
||||||
|
gtk_tree_view_convert_widget_to_bin_window_coords ( tree, cell_rect.x, cell_rect.y, NULL, &cell_rect.y );
|
||||||
|
|
||||||
|
/* only need to scroll if out of bounds */
|
||||||
|
if (cell_rect.y < vis_rect.y ||
|
||||||
|
cell_rect.y + cell_rect.height > vis_rect.y + vis_rect.height)
|
||||||
|
{
|
||||||
|
dest_y = cell_rect.y - ((vis_rect.height - cell_rect.height) * 0.5);
|
||||||
|
if (dest_y < 0)
|
||||||
|
dest_y = 0;
|
||||||
|
gtk_tree_view_scroll_to_point (tree, -1, dest_y);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
/* theft done, now make it focused like */
|
/* theft done, now make it focused like */
|
||||||
gtk_tree_view_set_cursor (tree, path, NULL, FALSE);
|
gtk_tree_view_set_cursor (tree, path, NULL, FALSE);
|
||||||
|
|||||||
@@ -0,0 +1,117 @@
|
|||||||
|
/* ZoiteChat -- shared, process-private emoji font rendering.
|
||||||
|
* Distributed under GPL-2.0-or-later; see COPYING. */
|
||||||
|
|
||||||
|
#include "fe-gtk.h"
|
||||||
|
#include "emoji-font.h"
|
||||||
|
|
||||||
|
#include <fontconfig/fontconfig.h>
|
||||||
|
#include <pango/pangocairo.h>
|
||||||
|
#include <pango/pangofc-fontmap.h>
|
||||||
|
#ifdef G_OS_WIN32
|
||||||
|
#include <glib/gwin32.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
static char *
|
||||||
|
emoji_font_path (void)
|
||||||
|
{
|
||||||
|
const char *override = g_getenv ("ZOITECHAT_EMOJI_FONT");
|
||||||
|
|
||||||
|
if (override && *override)
|
||||||
|
return g_strdup (override);
|
||||||
|
|
||||||
|
#ifdef G_OS_WIN32
|
||||||
|
{
|
||||||
|
char *base = g_win32_get_package_installation_directory_of_module (NULL);
|
||||||
|
char *path;
|
||||||
|
|
||||||
|
if (!base)
|
||||||
|
return NULL;
|
||||||
|
path = g_build_filename (base, "share", "fonts", "zoitechat",
|
||||||
|
"NotoColorEmoji.ttf", NULL);
|
||||||
|
g_free (base);
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
return g_build_filename (ZOITECHAT_DATADIR, "fonts", "zoitechat",
|
||||||
|
"NotoColorEmoji.ttf", NULL);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
PangoFontMap *
|
||||||
|
emoji_font_get_map (void)
|
||||||
|
{
|
||||||
|
static PangoFontMap *map;
|
||||||
|
static gboolean attempted;
|
||||||
|
FcConfig *config;
|
||||||
|
char *path, *escaped, *rules;
|
||||||
|
gboolean registered;
|
||||||
|
|
||||||
|
/* GTK widgets are used on the main thread. Keep the map alive for every
|
||||||
|
* layout's lifetime, including detached chat windows and shutdown. */
|
||||||
|
if (attempted)
|
||||||
|
return map;
|
||||||
|
attempted = TRUE;
|
||||||
|
|
||||||
|
path = emoji_font_path ();
|
||||||
|
if (!path || !g_file_test (path, G_FILE_TEST_IS_REGULAR))
|
||||||
|
{
|
||||||
|
g_warning ("Bundled emoji font is missing: %s", path ? path : "(unknown path)");
|
||||||
|
g_free (path);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Keep the system configuration for normal fonts and user preferences.
|
||||||
|
* Give ONLY our application font a private family so an older system
|
||||||
|
* Noto cannot win the match. Never install fonts globally or override
|
||||||
|
* digits, '#' or '*' in the widget's ordinary text font. */
|
||||||
|
config = FcInitLoadConfigAndFonts ();
|
||||||
|
if (!config)
|
||||||
|
{
|
||||||
|
g_warning ("Unable to load font configuration for emoji rendering");
|
||||||
|
g_free (path);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
escaped = g_markup_escape_text (path, -1);
|
||||||
|
rules = g_strdup_printf (
|
||||||
|
"<fontconfig>"
|
||||||
|
"<match target='scan'><test name='file'><string>%s</string></test>"
|
||||||
|
"<edit name='family' mode='assign'><string>" ZOITECHAT_EMOJI_FAMILY
|
||||||
|
"</string></edit></match>"
|
||||||
|
"<match target='pattern'><test name='family'><string>emoji</string></test>"
|
||||||
|
/* System rules may already have prepended older emoji families.
|
||||||
|
* Put our bundled family before those, not just before 'emoji'. */
|
||||||
|
"<edit name='family' mode='prepend_first' binding='strong'><string>"
|
||||||
|
ZOITECHAT_EMOJI_FAMILY "</string></edit></match>"
|
||||||
|
"</fontconfig>", escaped);
|
||||||
|
registered = FcConfigParseAndLoadFromMemory (config, (const FcChar8 *) rules, FcTrue) &&
|
||||||
|
FcConfigAppFontAddFile (config, (const FcChar8 *) path);
|
||||||
|
g_free (rules);
|
||||||
|
g_free (escaped);
|
||||||
|
g_free (path);
|
||||||
|
|
||||||
|
if (registered)
|
||||||
|
{
|
||||||
|
/* GDI cannot load Noto's CBDT/CBLC color glyphs. FreeType works on
|
||||||
|
* Windows 7 too, and is shipped in all of our GTK dependency bundles. */
|
||||||
|
map = pango_cairo_font_map_new_for_font_type (CAIRO_FONT_TYPE_FT);
|
||||||
|
if (map && PANGO_IS_FC_FONT_MAP (map))
|
||||||
|
pango_fc_font_map_set_config (PANGO_FC_FONT_MAP (map), config);
|
||||||
|
else
|
||||||
|
g_clear_object (&map);
|
||||||
|
}
|
||||||
|
FcConfigDestroy (config);
|
||||||
|
if (!map)
|
||||||
|
g_warning ("Unable to initialize bundled emoji rendering; using system fonts");
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
emoji_font_apply (GtkWidget *widget)
|
||||||
|
{
|
||||||
|
PangoFontMap *map;
|
||||||
|
|
||||||
|
g_return_if_fail (GTK_IS_WIDGET (widget));
|
||||||
|
map = emoji_font_get_map ();
|
||||||
|
if (map && gtk_widget_get_font_map (widget) != map)
|
||||||
|
gtk_widget_set_font_map (widget, map);
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
#ifndef ZOITECHAT_EMOJI_FONT_H
|
||||||
|
#define ZOITECHAT_EMOJI_FONT_H
|
||||||
|
|
||||||
|
#include <gtk/gtk.h>
|
||||||
|
|
||||||
|
/* Application-private family, distinct from any system Noto version. */
|
||||||
|
#define ZOITECHAT_EMOJI_FAMILY "ZoiteChat Emoji"
|
||||||
|
|
||||||
|
/* Use the shared FreeType font map without changing the widget's font/style.
|
||||||
|
* Children inherit it. Call before creating layouts for the widget. */
|
||||||
|
void emoji_font_apply (GtkWidget *widget);
|
||||||
|
|
||||||
|
/* Borrowed reference, or NULL if registration failed. Also usable headlessly. */
|
||||||
|
PangoFontMap *emoji_font_get_map (void);
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,828 @@
|
|||||||
|
/* ZoiteChat
|
||||||
|
* Copyright (C) 2026 deepend-tildeclub.
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation; either version 2 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program; if not, write to the Free Software
|
||||||
|
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* ZoiteChat's own emoji picker. Replaces GTK's built-in "show-emoji-icon"
|
||||||
|
* entry machinery so that every build offers the same catalog (bundled at
|
||||||
|
* compile time from pinned Unicode/CLDR data, see src/common/emoji-data.h)
|
||||||
|
* regardless of the GTK runtime version or distribution emoji data.
|
||||||
|
* Selecting an emoji inserts the plain Unicode sequence at the cursor -
|
||||||
|
* nothing about what is sent over IRC changes. */
|
||||||
|
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include "fe-gtk.h"
|
||||||
|
|
||||||
|
#include "../common/zoitechat.h"
|
||||||
|
#include "../common/zoitechatc.h"
|
||||||
|
#include "../common/cfgfiles.h"
|
||||||
|
#include "../common/emoji-data.h"
|
||||||
|
|
||||||
|
#include "emoji-picker.h"
|
||||||
|
#include "emoji-font.h"
|
||||||
|
#include "preferences-persistence.h"
|
||||||
|
|
||||||
|
#define EMOJI_CATEGORY_RECENT -1
|
||||||
|
#define EMOJI_RECENTS_MAX 32
|
||||||
|
#define EMOJI_SEARCH_MAX_RESULTS 256
|
||||||
|
#define EMOJI_GRID_COLUMNS 9
|
||||||
|
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
GtkWidget *button;
|
||||||
|
GtkWidget *target; /* weak pointer to the input entry */
|
||||||
|
GtkWidget *popover;
|
||||||
|
GtkWidget *search_entry;
|
||||||
|
GtkWidget *stack;
|
||||||
|
GtkWidget *grid; /* GtkFlowBox */
|
||||||
|
GtkWidget *scroller;
|
||||||
|
GtkWidget *empty_label;
|
||||||
|
GtkWidget *category_buttons[EMOJI_GROUP_COUNT + 1]; /* [0] = recents */
|
||||||
|
GtkWidget *tone_buttons[EMOJI_TONE_COUNT];
|
||||||
|
int category; /* EMOJI_CATEGORY_RECENT or EmojiGroup */
|
||||||
|
gboolean populated;
|
||||||
|
gboolean updating_buttons;
|
||||||
|
} EmojiPicker;
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------------ *
|
||||||
|
* Recently used emoji, shared by all windows and persisted across runs
|
||||||
|
* ------------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
static GPtrArray *recent_emoji = NULL;
|
||||||
|
|
||||||
|
static void emoji_search_stop_cb (GtkSearchEntry *entry, gpointer userdata);
|
||||||
|
|
||||||
|
static char *
|
||||||
|
emoji_recents_filename (void)
|
||||||
|
{
|
||||||
|
return g_build_filename (get_xdir (), "emoji-recents.conf", NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
emoji_recents_load (void)
|
||||||
|
{
|
||||||
|
char *filename, *contents = NULL;
|
||||||
|
char **lines;
|
||||||
|
gsize i;
|
||||||
|
|
||||||
|
if (recent_emoji)
|
||||||
|
return;
|
||||||
|
|
||||||
|
recent_emoji = g_ptr_array_new_with_free_func (g_free);
|
||||||
|
|
||||||
|
filename = emoji_recents_filename ();
|
||||||
|
if (g_file_get_contents (filename, &contents, NULL, NULL))
|
||||||
|
{
|
||||||
|
lines = g_strsplit (contents, "\n", -1);
|
||||||
|
for (i = 0; lines[i] && recent_emoji->len < EMOJI_RECENTS_MAX; i++)
|
||||||
|
{
|
||||||
|
g_strstrip (lines[i]);
|
||||||
|
/* only keep sequences that exist in the bundled catalog */
|
||||||
|
if (*lines[i] && emoji_data_find_by_sequence (lines[i], NULL))
|
||||||
|
g_ptr_array_add (recent_emoji, g_strdup (lines[i]));
|
||||||
|
}
|
||||||
|
g_strfreev (lines);
|
||||||
|
g_free (contents);
|
||||||
|
}
|
||||||
|
g_free (filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
emoji_recents_save (void)
|
||||||
|
{
|
||||||
|
GString *contents = g_string_new (NULL);
|
||||||
|
char *filename;
|
||||||
|
gsize i;
|
||||||
|
|
||||||
|
for (i = 0; i < recent_emoji->len; i++)
|
||||||
|
{
|
||||||
|
g_string_append (contents, g_ptr_array_index (recent_emoji, i));
|
||||||
|
g_string_append_c (contents, '\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
filename = emoji_recents_filename ();
|
||||||
|
g_file_set_contents (filename, contents->str, contents->len, NULL);
|
||||||
|
g_free (filename);
|
||||||
|
g_string_free (contents, TRUE);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
emoji_recents_add (const char *sequence)
|
||||||
|
{
|
||||||
|
GPtrArray *updated;
|
||||||
|
gsize i;
|
||||||
|
|
||||||
|
emoji_recents_load ();
|
||||||
|
|
||||||
|
updated = g_ptr_array_new_with_free_func (g_free);
|
||||||
|
g_ptr_array_add (updated, g_strdup (sequence));
|
||||||
|
for (i = 0; i < recent_emoji->len && updated->len < EMOJI_RECENTS_MAX; i++)
|
||||||
|
{
|
||||||
|
const char *old = g_ptr_array_index (recent_emoji, i);
|
||||||
|
|
||||||
|
if (strcmp (old, sequence) != 0)
|
||||||
|
g_ptr_array_add (updated, g_strdup (old));
|
||||||
|
}
|
||||||
|
g_ptr_array_free (recent_emoji, TRUE);
|
||||||
|
recent_emoji = updated;
|
||||||
|
|
||||||
|
emoji_recents_save ();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------------ *
|
||||||
|
* Appearance helpers
|
||||||
|
* ------------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
/* One application-wide provider so every emoji cell prefers a color emoji
|
||||||
|
* font without changing the fonts of surrounding widgets. The private
|
||||||
|
* font map resolves ZoiteChat Emoji to the same bundled file everywhere. */
|
||||||
|
static void
|
||||||
|
emoji_picker_ensure_css (void)
|
||||||
|
{
|
||||||
|
static gboolean done = FALSE;
|
||||||
|
GtkCssProvider *provider;
|
||||||
|
|
||||||
|
if (done)
|
||||||
|
return;
|
||||||
|
done = TRUE;
|
||||||
|
|
||||||
|
provider = gtk_css_provider_new ();
|
||||||
|
gtk_css_provider_load_from_data (provider,
|
||||||
|
".zc-emoji-cell {"
|
||||||
|
" font-family: \"ZoiteChat Emoji\", \"Noto Color Emoji\","
|
||||||
|
" \"Apple Color Emoji\", \"Twemoji Mozilla\", \"EmojiOne Color\";"
|
||||||
|
" font-size: 15pt;"
|
||||||
|
" padding: 2px;"
|
||||||
|
"}"
|
||||||
|
".zc-emoji-cell-small {"
|
||||||
|
" font-family: \"ZoiteChat Emoji\", \"Noto Color Emoji\","
|
||||||
|
" \"Apple Color Emoji\", \"Twemoji Mozilla\", \"EmojiOne Color\";"
|
||||||
|
"}",
|
||||||
|
-1, NULL);
|
||||||
|
gtk_style_context_add_provider_for_screen (gdk_screen_get_default (),
|
||||||
|
GTK_STYLE_PROVIDER (provider), GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
|
||||||
|
g_object_unref (provider);
|
||||||
|
}
|
||||||
|
|
||||||
|
static GtkWidget *
|
||||||
|
emoji_label_new (const char *sequence, gboolean small_cell)
|
||||||
|
{
|
||||||
|
GtkWidget *label = gtk_label_new (sequence);
|
||||||
|
|
||||||
|
gtk_style_context_add_class (gtk_widget_get_style_context (label),
|
||||||
|
small_cell ? "zc-emoji-cell-small" : "zc-emoji-cell");
|
||||||
|
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
emoji_picker_set_a11y_name (GtkWidget *widget, const char *name)
|
||||||
|
{
|
||||||
|
AtkObject *accessible = gtk_widget_get_accessible (widget);
|
||||||
|
|
||||||
|
if (accessible)
|
||||||
|
atk_object_set_name (accessible, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
static EmojiTone
|
||||||
|
emoji_picker_current_tone (void)
|
||||||
|
{
|
||||||
|
int tone = prefs.hex_gui_emoji_skin_tone;
|
||||||
|
|
||||||
|
if (tone < EMOJI_TONE_NONE || tone >= EMOJI_TONE_COUNT)
|
||||||
|
return EMOJI_TONE_NONE;
|
||||||
|
|
||||||
|
return (EmojiTone) tone;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* "waving hand" / "waving hand (Dark skin tone)", with :shortcodes: on a
|
||||||
|
* second line - used for both tooltips and accessible names. */
|
||||||
|
static char *
|
||||||
|
emoji_picker_describe (const EmojiEntry *entry, EmojiTone tone)
|
||||||
|
{
|
||||||
|
GString *desc = g_string_new (NULL);
|
||||||
|
char **aliases;
|
||||||
|
gsize i;
|
||||||
|
|
||||||
|
if (tone != EMOJI_TONE_NONE && entry->tone_set >= 0)
|
||||||
|
g_string_append_printf (desc, "%s (%s)", entry->name,
|
||||||
|
emoji_data_tone_display_name (tone));
|
||||||
|
else
|
||||||
|
g_string_append (desc, entry->name);
|
||||||
|
|
||||||
|
if (*entry->aliases)
|
||||||
|
{
|
||||||
|
g_string_append_c (desc, '\n');
|
||||||
|
aliases = g_strsplit (entry->aliases, " ", -1);
|
||||||
|
for (i = 0; aliases[i]; i++)
|
||||||
|
{
|
||||||
|
if (i > 0)
|
||||||
|
g_string_append_c (desc, ' ');
|
||||||
|
g_string_append_printf (desc, ":%s:", aliases[i]);
|
||||||
|
}
|
||||||
|
g_strfreev (aliases);
|
||||||
|
}
|
||||||
|
|
||||||
|
return g_string_free (desc, FALSE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------------ *
|
||||||
|
* Insertion
|
||||||
|
* ------------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
/* GtkEntry can select all text when focused; preserve its selection and
|
||||||
|
* editability instead of temporarily making a read-only entry writable. */
|
||||||
|
static void
|
||||||
|
emoji_picker_focus_target (EmojiPicker *picker)
|
||||||
|
{
|
||||||
|
GtkEditable *editable;
|
||||||
|
int start, end, pos;
|
||||||
|
gboolean selected;
|
||||||
|
|
||||||
|
if (!picker->target)
|
||||||
|
return;
|
||||||
|
/* Closing a window also closes its popovers. Moving focus during that
|
||||||
|
* teardown can make GtkWindow reference an already destroyed widget. */
|
||||||
|
if (gtk_widget_in_destruction (picker->target) ||
|
||||||
|
gtk_widget_in_destruction (gtk_widget_get_toplevel (picker->target)))
|
||||||
|
return;
|
||||||
|
editable = GTK_EDITABLE (picker->target);
|
||||||
|
pos = gtk_editable_get_position (editable);
|
||||||
|
selected = gtk_editable_get_selection_bounds (editable, &start, &end);
|
||||||
|
gtk_widget_grab_focus (picker->target);
|
||||||
|
if (selected)
|
||||||
|
gtk_editable_select_region (editable, pos == start ? end : start, pos);
|
||||||
|
else
|
||||||
|
gtk_editable_set_position (editable, pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
emoji_picker_insert (EmojiPicker *picker, const char *sequence)
|
||||||
|
{
|
||||||
|
GtkEditable *editable;
|
||||||
|
int pos, start, end, max_length, length;
|
||||||
|
|
||||||
|
if (!picker->target || !sequence || !*sequence)
|
||||||
|
return;
|
||||||
|
editable = GTK_EDITABLE (picker->target);
|
||||||
|
if (!gtk_editable_get_editable (editable))
|
||||||
|
return;
|
||||||
|
|
||||||
|
/* GtkEntry otherwise truncates at a code point, possibly leaving half
|
||||||
|
* a flag, a dangling joiner or a skin-tone modifier. Reject as a unit. */
|
||||||
|
max_length = gtk_entry_get_max_length (GTK_ENTRY (editable));
|
||||||
|
length = gtk_entry_get_text_length (GTK_ENTRY (editable));
|
||||||
|
if (gtk_editable_get_selection_bounds (editable, &start, &end))
|
||||||
|
length -= end - start;
|
||||||
|
if (length + g_utf8_strlen (sequence, -1) > (max_length ? max_length : 65535))
|
||||||
|
{
|
||||||
|
gtk_widget_error_bell (picker->target);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
gtk_editable_delete_selection (editable);
|
||||||
|
pos = gtk_editable_get_position (editable);
|
||||||
|
gtk_editable_insert_text (editable, sequence, strlen (sequence), &pos);
|
||||||
|
gtk_editable_set_position (editable, pos);
|
||||||
|
emoji_recents_add (sequence);
|
||||||
|
gtk_popover_popdown (GTK_POPOVER (picker->popover));
|
||||||
|
emoji_picker_focus_target (picker);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
emoji_picker_activate_child (EmojiPicker *picker, GtkFlowBoxChild *child)
|
||||||
|
{
|
||||||
|
emoji_picker_insert (picker,
|
||||||
|
g_object_get_data (G_OBJECT (child), "emoji-sequence"));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
emoji_grid_child_activated_cb (GtkFlowBox *grid, GtkFlowBoxChild *child,
|
||||||
|
gpointer userdata)
|
||||||
|
{
|
||||||
|
emoji_picker_activate_child (userdata, child);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------------ *
|
||||||
|
* Grid contents
|
||||||
|
* ------------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
static void
|
||||||
|
emoji_grid_add (EmojiPicker *picker, const EmojiEntry *entry,
|
||||||
|
const char *sequence, EmojiTone tone)
|
||||||
|
{
|
||||||
|
GtkWidget *child, *label;
|
||||||
|
char *desc;
|
||||||
|
|
||||||
|
label = emoji_label_new (sequence, FALSE);
|
||||||
|
child = gtk_flow_box_child_new ();
|
||||||
|
gtk_container_add (GTK_CONTAINER (child), label);
|
||||||
|
g_object_set_data_full (G_OBJECT (child), "emoji-sequence",
|
||||||
|
g_strdup (sequence), g_free);
|
||||||
|
|
||||||
|
desc = emoji_picker_describe (entry, tone);
|
||||||
|
gtk_widget_set_tooltip_text (child, desc);
|
||||||
|
emoji_picker_set_a11y_name (child, desc);
|
||||||
|
g_free (desc);
|
||||||
|
|
||||||
|
gtk_container_add (GTK_CONTAINER (picker->grid), child);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
emoji_picker_refresh (EmojiPicker *picker)
|
||||||
|
{
|
||||||
|
const char *text = gtk_entry_get_text (GTK_ENTRY (picker->search_entry));
|
||||||
|
EmojiTone tone = emoji_picker_current_tone ();
|
||||||
|
GtkAdjustment *vadj;
|
||||||
|
gboolean any = FALSE;
|
||||||
|
gsize i, results = 0;
|
||||||
|
|
||||||
|
gtk_container_foreach (GTK_CONTAINER (picker->grid),
|
||||||
|
(GtkCallback) gtk_widget_destroy, NULL);
|
||||||
|
|
||||||
|
if (text && *text)
|
||||||
|
{
|
||||||
|
char **tokens = emoji_search_tokenize (text);
|
||||||
|
|
||||||
|
if (tokens)
|
||||||
|
{
|
||||||
|
for (i = 0; i < emoji_data_count () &&
|
||||||
|
results < EMOJI_SEARCH_MAX_RESULTS; i++)
|
||||||
|
{
|
||||||
|
const EmojiEntry *entry = emoji_data_entry (i);
|
||||||
|
|
||||||
|
if (!emoji_entry_matches (entry, tokens))
|
||||||
|
continue;
|
||||||
|
emoji_grid_add (picker, entry,
|
||||||
|
emoji_entry_sequence_for_tone (entry, tone), tone);
|
||||||
|
results++;
|
||||||
|
}
|
||||||
|
g_strfreev (tokens);
|
||||||
|
any = results > 0;
|
||||||
|
}
|
||||||
|
gtk_label_set_text (GTK_LABEL (picker->empty_label),
|
||||||
|
_("No emoji found"));
|
||||||
|
}
|
||||||
|
else if (picker->category == EMOJI_CATEGORY_RECENT)
|
||||||
|
{
|
||||||
|
emoji_recents_load ();
|
||||||
|
for (i = 0; i < recent_emoji->len; i++)
|
||||||
|
{
|
||||||
|
const char *sequence = g_ptr_array_index (recent_emoji, i);
|
||||||
|
EmojiTone seq_tone;
|
||||||
|
const EmojiEntry *entry =
|
||||||
|
emoji_data_find_by_sequence (sequence, &seq_tone);
|
||||||
|
|
||||||
|
if (!entry)
|
||||||
|
continue;
|
||||||
|
emoji_grid_add (picker, entry, sequence, seq_tone);
|
||||||
|
any = TRUE;
|
||||||
|
}
|
||||||
|
gtk_label_set_text (GTK_LABEL (picker->empty_label),
|
||||||
|
_("No recently used emoji"));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
for (i = 0; i < emoji_data_count (); i++)
|
||||||
|
{
|
||||||
|
const EmojiEntry *entry = emoji_data_entry (i);
|
||||||
|
|
||||||
|
if (entry->group != picker->category)
|
||||||
|
continue;
|
||||||
|
emoji_grid_add (picker, entry,
|
||||||
|
emoji_entry_sequence_for_tone (entry, tone), tone);
|
||||||
|
any = TRUE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
gtk_widget_show_all (picker->grid);
|
||||||
|
gtk_stack_set_visible_child_name (GTK_STACK (picker->stack),
|
||||||
|
any ? "grid" : "empty");
|
||||||
|
|
||||||
|
vadj = gtk_scrolled_window_get_vadjustment (
|
||||||
|
GTK_SCROLLED_WINDOW (picker->scroller));
|
||||||
|
gtk_adjustment_set_value (vadj, gtk_adjustment_get_lower (vadj));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------------ *
|
||||||
|
* Categories
|
||||||
|
* ------------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
static void
|
||||||
|
emoji_picker_sync_category_buttons (EmojiPicker *picker)
|
||||||
|
{
|
||||||
|
gsize i;
|
||||||
|
|
||||||
|
picker->updating_buttons = TRUE;
|
||||||
|
for (i = 0; i < G_N_ELEMENTS (picker->category_buttons); i++)
|
||||||
|
{
|
||||||
|
int category = (int) i - 1;
|
||||||
|
|
||||||
|
gtk_toggle_button_set_active (
|
||||||
|
GTK_TOGGLE_BUTTON (picker->category_buttons[i]),
|
||||||
|
category == picker->category);
|
||||||
|
}
|
||||||
|
picker->updating_buttons = FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
emoji_category_toggled_cb (GtkToggleButton *button, gpointer userdata)
|
||||||
|
{
|
||||||
|
EmojiPicker *picker = userdata;
|
||||||
|
int category = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (button),
|
||||||
|
"emoji-category"));
|
||||||
|
|
||||||
|
if (picker->updating_buttons)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!gtk_toggle_button_get_active (button))
|
||||||
|
{
|
||||||
|
/* the active category cannot be untoggled */
|
||||||
|
if (picker->category == category)
|
||||||
|
emoji_picker_sync_category_buttons (picker);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
picker->category = category;
|
||||||
|
emoji_picker_sync_category_buttons (picker);
|
||||||
|
|
||||||
|
/* leave search mode; clearing the entry triggers the refresh */
|
||||||
|
if (*gtk_entry_get_text (GTK_ENTRY (picker->search_entry)))
|
||||||
|
gtk_entry_set_text (GTK_ENTRY (picker->search_entry), "");
|
||||||
|
else
|
||||||
|
emoji_picker_refresh (picker);
|
||||||
|
}
|
||||||
|
|
||||||
|
static GtkWidget *
|
||||||
|
emoji_category_button_new (EmojiPicker *picker, int category,
|
||||||
|
const char *icon_sequence, const char *name)
|
||||||
|
{
|
||||||
|
GtkWidget *button = gtk_toggle_button_new ();
|
||||||
|
|
||||||
|
gtk_container_add (GTK_CONTAINER (button),
|
||||||
|
emoji_label_new (icon_sequence, TRUE));
|
||||||
|
gtk_button_set_relief (GTK_BUTTON (button), GTK_RELIEF_NONE);
|
||||||
|
gtk_widget_set_tooltip_text (button, name);
|
||||||
|
emoji_picker_set_a11y_name (button, name);
|
||||||
|
g_object_set_data (G_OBJECT (button), "emoji-category",
|
||||||
|
GINT_TO_POINTER (category));
|
||||||
|
g_signal_connect (G_OBJECT (button), "toggled",
|
||||||
|
G_CALLBACK (emoji_category_toggled_cb), picker);
|
||||||
|
|
||||||
|
return button;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------------ *
|
||||||
|
* Skin tones
|
||||||
|
* ------------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
#define EMOJI_TONE_SAMPLE "\342\234\213" /* ✋ raised hand */
|
||||||
|
|
||||||
|
static void
|
||||||
|
emoji_tone_toggled_cb (GtkToggleButton *button, gpointer userdata)
|
||||||
|
{
|
||||||
|
EmojiPicker *picker = userdata;
|
||||||
|
int tone = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (button),
|
||||||
|
"emoji-tone"));
|
||||||
|
|
||||||
|
if (!gtk_toggle_button_get_active (button))
|
||||||
|
return;
|
||||||
|
if (prefs.hex_gui_emoji_skin_tone == tone)
|
||||||
|
return;
|
||||||
|
|
||||||
|
prefs.hex_gui_emoji_skin_tone = tone;
|
||||||
|
preferences_persistence_save_all ();
|
||||||
|
emoji_picker_refresh (picker);
|
||||||
|
}
|
||||||
|
|
||||||
|
static GtkWidget *
|
||||||
|
emoji_tone_row_new (EmojiPicker *picker)
|
||||||
|
{
|
||||||
|
GtkWidget *box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0);
|
||||||
|
GtkWidget *button;
|
||||||
|
GSList *group = NULL;
|
||||||
|
const EmojiEntry *sample = emoji_data_find_by_sequence (EMOJI_TONE_SAMPLE,
|
||||||
|
NULL);
|
||||||
|
int tone;
|
||||||
|
|
||||||
|
gtk_widget_set_halign (box, GTK_ALIGN_CENTER);
|
||||||
|
|
||||||
|
for (tone = EMOJI_TONE_NONE; tone < EMOJI_TONE_COUNT; tone++)
|
||||||
|
{
|
||||||
|
const char *sequence = EMOJI_TONE_SAMPLE;
|
||||||
|
|
||||||
|
if (sample)
|
||||||
|
sequence = emoji_entry_sequence_for_tone (sample, tone);
|
||||||
|
|
||||||
|
button = gtk_radio_button_new (group);
|
||||||
|
group = gtk_radio_button_get_group (GTK_RADIO_BUTTON (button));
|
||||||
|
gtk_toggle_button_set_mode (GTK_TOGGLE_BUTTON (button), FALSE);
|
||||||
|
gtk_button_set_relief (GTK_BUTTON (button), GTK_RELIEF_NONE);
|
||||||
|
gtk_container_add (GTK_CONTAINER (button),
|
||||||
|
emoji_label_new (sequence, TRUE));
|
||||||
|
gtk_widget_set_tooltip_text (button,
|
||||||
|
emoji_data_tone_display_name (tone));
|
||||||
|
emoji_picker_set_a11y_name (button,
|
||||||
|
emoji_data_tone_display_name (tone));
|
||||||
|
g_object_set_data (G_OBJECT (button), "emoji-tone",
|
||||||
|
GINT_TO_POINTER (tone));
|
||||||
|
picker->tone_buttons[tone] = button;
|
||||||
|
|
||||||
|
if (emoji_picker_current_tone () == tone)
|
||||||
|
gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (button), TRUE);
|
||||||
|
|
||||||
|
g_signal_connect (G_OBJECT (button), "toggled",
|
||||||
|
G_CALLBACK (emoji_tone_toggled_cb), picker);
|
||||||
|
|
||||||
|
gtk_box_pack_start (GTK_BOX (box), button, FALSE, FALSE, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* the pref can also change externally (/set gui_emoji_skin_tone) */
|
||||||
|
static void
|
||||||
|
emoji_picker_sync_tone_buttons (EmojiPicker *picker)
|
||||||
|
{
|
||||||
|
EmojiTone tone = emoji_picker_current_tone ();
|
||||||
|
|
||||||
|
gtk_toggle_button_set_active (
|
||||||
|
GTK_TOGGLE_BUTTON (picker->tone_buttons[tone]), TRUE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------------ *
|
||||||
|
* Search
|
||||||
|
* ------------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
static void
|
||||||
|
emoji_search_changed_cb (GtkSearchEntry *entry, gpointer userdata)
|
||||||
|
{
|
||||||
|
emoji_picker_refresh (userdata);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
emoji_search_activate_cb (GtkEntry *entry, gpointer userdata)
|
||||||
|
{
|
||||||
|
EmojiPicker *picker = userdata;
|
||||||
|
GtkFlowBoxChild *child;
|
||||||
|
|
||||||
|
/* search-changed is delayed by GTK; Enter must use the current query. */
|
||||||
|
emoji_picker_refresh (picker);
|
||||||
|
child = gtk_flow_box_get_child_at_index (GTK_FLOW_BOX (picker->grid), 0);
|
||||||
|
|
||||||
|
/* Enter inserts the first hit */
|
||||||
|
if (child)
|
||||||
|
emoji_picker_activate_child (picker, child);
|
||||||
|
}
|
||||||
|
|
||||||
|
static gboolean
|
||||||
|
emoji_search_key_press_cb (GtkWidget *entry, GdkEventKey *event,
|
||||||
|
gpointer userdata)
|
||||||
|
{
|
||||||
|
EmojiPicker *picker = userdata;
|
||||||
|
GtkFlowBoxChild *child;
|
||||||
|
|
||||||
|
if (event->keyval == GDK_KEY_Down || event->keyval == GDK_KEY_KP_Down)
|
||||||
|
{
|
||||||
|
emoji_picker_refresh (picker);
|
||||||
|
child = gtk_flow_box_get_child_at_index (GTK_FLOW_BOX (picker->grid), 0);
|
||||||
|
if (child)
|
||||||
|
{
|
||||||
|
gtk_widget_grab_focus (GTK_WIDGET (child));
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------------ *
|
||||||
|
* Popover assembly
|
||||||
|
* ------------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
static void
|
||||||
|
emoji_picker_build_contents (EmojiPicker *picker)
|
||||||
|
{
|
||||||
|
GtkWidget *vbox, *categories, *button;
|
||||||
|
int group;
|
||||||
|
|
||||||
|
emoji_picker_ensure_css ();
|
||||||
|
|
||||||
|
vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 6);
|
||||||
|
g_object_set (vbox, "margin", 6, NULL);
|
||||||
|
|
||||||
|
picker->search_entry = gtk_search_entry_new ();
|
||||||
|
gtk_entry_set_placeholder_text (GTK_ENTRY (picker->search_entry),
|
||||||
|
_("Search emoji"));
|
||||||
|
emoji_picker_set_a11y_name (picker->search_entry, _("Search emoji"));
|
||||||
|
g_signal_connect (G_OBJECT (picker->search_entry), "search-changed",
|
||||||
|
G_CALLBACK (emoji_search_changed_cb), picker);
|
||||||
|
g_signal_connect (G_OBJECT (picker->search_entry), "stop-search",
|
||||||
|
G_CALLBACK (emoji_search_stop_cb), picker);
|
||||||
|
g_signal_connect (G_OBJECT (picker->search_entry), "activate",
|
||||||
|
G_CALLBACK (emoji_search_activate_cb), picker);
|
||||||
|
g_signal_connect (G_OBJECT (picker->search_entry), "key-press-event",
|
||||||
|
G_CALLBACK (emoji_search_key_press_cb), picker);
|
||||||
|
gtk_box_pack_start (GTK_BOX (vbox), picker->search_entry, FALSE, FALSE, 0);
|
||||||
|
|
||||||
|
categories = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0);
|
||||||
|
gtk_box_set_homogeneous (GTK_BOX (categories), TRUE);
|
||||||
|
|
||||||
|
picker->category_buttons[0] = emoji_category_button_new (picker,
|
||||||
|
EMOJI_CATEGORY_RECENT, "\360\237\225\230" /* 🕘 */,
|
||||||
|
_("Recently Used"));
|
||||||
|
gtk_box_pack_start (GTK_BOX (categories), picker->category_buttons[0],
|
||||||
|
TRUE, TRUE, 0);
|
||||||
|
for (group = 0; group < EMOJI_GROUP_COUNT; group++)
|
||||||
|
{
|
||||||
|
button = emoji_category_button_new (picker, group,
|
||||||
|
emoji_data_group_icon_sequence (group),
|
||||||
|
emoji_data_group_display_name (group));
|
||||||
|
picker->category_buttons[group + 1] = button;
|
||||||
|
gtk_box_pack_start (GTK_BOX (categories), button, TRUE, TRUE, 0);
|
||||||
|
}
|
||||||
|
gtk_box_pack_start (GTK_BOX (vbox), categories, FALSE, FALSE, 0);
|
||||||
|
|
||||||
|
picker->grid = gtk_flow_box_new ();
|
||||||
|
gtk_flow_box_set_selection_mode (GTK_FLOW_BOX (picker->grid),
|
||||||
|
GTK_SELECTION_NONE);
|
||||||
|
gtk_flow_box_set_activate_on_single_click (GTK_FLOW_BOX (picker->grid),
|
||||||
|
TRUE);
|
||||||
|
gtk_flow_box_set_min_children_per_line (GTK_FLOW_BOX (picker->grid),
|
||||||
|
EMOJI_GRID_COLUMNS);
|
||||||
|
gtk_flow_box_set_max_children_per_line (GTK_FLOW_BOX (picker->grid),
|
||||||
|
EMOJI_GRID_COLUMNS);
|
||||||
|
gtk_flow_box_set_homogeneous (GTK_FLOW_BOX (picker->grid), TRUE);
|
||||||
|
gtk_widget_set_valign (picker->grid, GTK_ALIGN_START);
|
||||||
|
g_signal_connect (G_OBJECT (picker->grid), "child-activated",
|
||||||
|
G_CALLBACK (emoji_grid_child_activated_cb), picker);
|
||||||
|
|
||||||
|
picker->scroller = gtk_scrolled_window_new (NULL, NULL);
|
||||||
|
gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (picker->scroller),
|
||||||
|
GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
|
||||||
|
gtk_scrolled_window_set_min_content_height (
|
||||||
|
GTK_SCROLLED_WINDOW (picker->scroller), 300);
|
||||||
|
gtk_container_add (GTK_CONTAINER (picker->scroller), picker->grid);
|
||||||
|
|
||||||
|
picker->empty_label = gtk_label_new (_("No emoji found"));
|
||||||
|
gtk_style_context_add_class (
|
||||||
|
gtk_widget_get_style_context (picker->empty_label), "dim-label");
|
||||||
|
|
||||||
|
picker->stack = gtk_stack_new ();
|
||||||
|
gtk_widget_set_size_request (picker->stack, 380, -1);
|
||||||
|
gtk_stack_add_named (GTK_STACK (picker->stack), picker->scroller, "grid");
|
||||||
|
gtk_stack_add_named (GTK_STACK (picker->stack), picker->empty_label,
|
||||||
|
"empty");
|
||||||
|
gtk_box_pack_start (GTK_BOX (vbox), picker->stack, TRUE, TRUE, 0);
|
||||||
|
|
||||||
|
gtk_box_pack_start (GTK_BOX (vbox), emoji_tone_row_new (picker),
|
||||||
|
FALSE, FALSE, 0);
|
||||||
|
|
||||||
|
gtk_container_add (GTK_CONTAINER (picker->popover), vbox);
|
||||||
|
gtk_widget_show_all (vbox);
|
||||||
|
|
||||||
|
emoji_picker_sync_category_buttons (picker);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
emoji_popover_show_cb (GtkWidget *popover, gpointer userdata)
|
||||||
|
{
|
||||||
|
EmojiPicker *picker = userdata;
|
||||||
|
|
||||||
|
if (!picker->populated)
|
||||||
|
{
|
||||||
|
picker->populated = TRUE;
|
||||||
|
emoji_picker_build_contents (picker);
|
||||||
|
}
|
||||||
|
else if (*gtk_entry_get_text (GTK_ENTRY (picker->search_entry)))
|
||||||
|
{
|
||||||
|
gtk_entry_set_text (GTK_ENTRY (picker->search_entry), "");
|
||||||
|
}
|
||||||
|
|
||||||
|
gtk_widget_grab_focus (picker->search_entry);
|
||||||
|
emoji_picker_sync_tone_buttons (picker);
|
||||||
|
emoji_picker_refresh (picker);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
emoji_search_stop_cb (GtkSearchEntry *entry, gpointer userdata)
|
||||||
|
{
|
||||||
|
EmojiPicker *picker = userdata;
|
||||||
|
|
||||||
|
gtk_popover_popdown (GTK_POPOVER (picker->popover));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
emoji_entry_insert_cb (GtkEntry *entry, GtkWidget *button)
|
||||||
|
{
|
||||||
|
/* Covers GtkEntry's context menu and Ctrl+period/Ctrl+semicolon. */
|
||||||
|
g_signal_stop_emission_by_name (entry, "insert-emoji");
|
||||||
|
gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (button), TRUE);
|
||||||
|
}
|
||||||
|
|
||||||
|
static const char *
|
||||||
|
emoji_picker_button_icon_name (void)
|
||||||
|
{
|
||||||
|
static const char *icon_names[] = {
|
||||||
|
"face-smile-symbolic",
|
||||||
|
"face-smile",
|
||||||
|
"insert-emoticon-symbolic",
|
||||||
|
"insert-emoticon",
|
||||||
|
"zc-menu-emoji",
|
||||||
|
};
|
||||||
|
GtkIconTheme *theme = gtk_icon_theme_get_default ();
|
||||||
|
gsize i;
|
||||||
|
|
||||||
|
if (!theme)
|
||||||
|
return NULL;
|
||||||
|
|
||||||
|
for (i = 0; i < G_N_ELEMENTS (icon_names); i++)
|
||||||
|
{
|
||||||
|
if (gtk_icon_theme_has_icon (theme, icon_names[i]))
|
||||||
|
return icon_names[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
emoji_picker_free (gpointer data)
|
||||||
|
{
|
||||||
|
EmojiPicker *picker = data;
|
||||||
|
|
||||||
|
if (picker->target)
|
||||||
|
g_object_remove_weak_pointer (G_OBJECT (picker->target),
|
||||||
|
(gpointer *) &picker->target);
|
||||||
|
g_free (picker);
|
||||||
|
}
|
||||||
|
|
||||||
|
GtkWidget *
|
||||||
|
emoji_picker_button_new (GtkWidget *target_entry)
|
||||||
|
{
|
||||||
|
EmojiPicker *picker;
|
||||||
|
const char *icon_name;
|
||||||
|
|
||||||
|
g_return_val_if_fail (GTK_IS_ENTRY (target_entry), NULL);
|
||||||
|
|
||||||
|
picker = g_new0 (EmojiPicker, 1);
|
||||||
|
picker->target = target_entry;
|
||||||
|
picker->category = EMOJI_GROUP_SMILEYS_EMOTION;
|
||||||
|
g_object_add_weak_pointer (G_OBJECT (target_entry),
|
||||||
|
(gpointer *) &picker->target);
|
||||||
|
|
||||||
|
picker->button = gtk_menu_button_new ();
|
||||||
|
emoji_font_apply (picker->button);
|
||||||
|
gtk_button_set_relief (GTK_BUTTON (picker->button), GTK_RELIEF_NONE);
|
||||||
|
gtk_widget_set_can_focus (picker->button, TRUE);
|
||||||
|
gtk_widget_set_focus_on_click (picker->button, FALSE);
|
||||||
|
gtk_widget_set_tooltip_text (picker->button, _("Insert Emoji"));
|
||||||
|
emoji_picker_set_a11y_name (picker->button, _("Insert Emoji"));
|
||||||
|
|
||||||
|
icon_name = emoji_picker_button_icon_name ();
|
||||||
|
if (icon_name)
|
||||||
|
{
|
||||||
|
gtk_button_set_image (GTK_BUTTON (picker->button),
|
||||||
|
gtk_image_new_from_icon_name (icon_name, GTK_ICON_SIZE_MENU));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
/* no themed icon anywhere: fall back to an emoji label so the
|
||||||
|
* button never turns into an unusable placeholder */
|
||||||
|
emoji_picker_ensure_css ();
|
||||||
|
gtk_container_add (GTK_CONTAINER (picker->button),
|
||||||
|
emoji_label_new ("\360\237\231\202" /* 🙂 */, TRUE));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* the popover shell is cheap; its contents are built on first open so
|
||||||
|
* startup cost stays unchanged */
|
||||||
|
picker->popover = gtk_popover_new (picker->button);
|
||||||
|
emoji_font_apply (picker->popover);
|
||||||
|
g_object_set_data_full (G_OBJECT (picker->popover), "emoji-picker",
|
||||||
|
picker, emoji_picker_free);
|
||||||
|
/* GtkEntry added this action in 3.22.27; earlier supported GTK 3.22
|
||||||
|
* runtimes still use the button without emitting an unknown signal. */
|
||||||
|
if (g_signal_lookup ("insert-emoji", GTK_TYPE_ENTRY))
|
||||||
|
g_signal_connect_object (target_entry, "insert-emoji",
|
||||||
|
G_CALLBACK (emoji_entry_insert_cb), picker->button, 0);
|
||||||
|
gtk_popover_set_position (GTK_POPOVER (picker->popover), GTK_POS_TOP);
|
||||||
|
g_signal_connect (G_OBJECT (picker->popover), "show",
|
||||||
|
G_CALLBACK (emoji_popover_show_cb), picker);
|
||||||
|
gtk_menu_button_set_popover (GTK_MENU_BUTTON (picker->button),
|
||||||
|
picker->popover);
|
||||||
|
|
||||||
|
return picker->button;
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
/* ZoiteChat
|
||||||
|
* Copyright (C) 2026 deepend-tildeclub.
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation; either version 2 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program; if not, write to the Free Software
|
||||||
|
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef ZOITECHAT_EMOJI_PICKER_H
|
||||||
|
#define ZOITECHAT_EMOJI_PICKER_H
|
||||||
|
|
||||||
|
#include <gtk/gtk.h>
|
||||||
|
|
||||||
|
/* Creates the emoji picker button for an input entry. The returned
|
||||||
|
* GtkMenuButton opens a popover with search, categories, recently used
|
||||||
|
* emoji and skin-tone selection; activating an emoji inserts its Unicode
|
||||||
|
* sequence into target_entry at the cursor. The popover contents are
|
||||||
|
* built lazily on first open. */
|
||||||
|
GtkWidget *emoji_picker_button_new (GtkWidget *target_entry);
|
||||||
|
|
||||||
|
#endif
|
||||||
+57
-2
@@ -21,6 +21,9 @@
|
|||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
|
|
||||||
#include "fe-gtk.h"
|
#include "fe-gtk.h"
|
||||||
|
#ifdef __APPLE__
|
||||||
|
#include "../common/macos.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
#ifdef GDK_WINDOWING_WIN32
|
#ifdef GDK_WINDOWING_WIN32
|
||||||
#include <gdk/gdkwin32.h>
|
#include <gdk/gdkwin32.h>
|
||||||
@@ -49,6 +52,7 @@
|
|||||||
#include "chanlist.h"
|
#include "chanlist.h"
|
||||||
#include "joind.h"
|
#include "joind.h"
|
||||||
#include "xtext.h"
|
#include "xtext.h"
|
||||||
|
#include "inline-image.h"
|
||||||
#include "theme/theme-gtk.h"
|
#include "theme/theme-gtk.h"
|
||||||
#include "menu.h"
|
#include "menu.h"
|
||||||
#include "notifygui.h"
|
#include "notifygui.h"
|
||||||
@@ -89,6 +93,9 @@ static const GOptionEntry gopt_entries[] =
|
|||||||
{"no-auto", 'a', 0, G_OPTION_ARG_NONE, &arg_dont_autoconnect, N_("Don't auto connect to servers"), NULL},
|
{"no-auto", 'a', 0, G_OPTION_ARG_NONE, &arg_dont_autoconnect, N_("Don't auto connect to servers"), NULL},
|
||||||
{"cfgdir", 'd', 0, G_OPTION_ARG_STRING, &arg_cfgdir, N_("Use a different config directory"), "PATH"},
|
{"cfgdir", 'd', 0, G_OPTION_ARG_STRING, &arg_cfgdir, N_("Use a different config directory"), "PATH"},
|
||||||
{"no-plugins", 'n', 0, G_OPTION_ARG_NONE, &arg_skip_plugins, N_("Don't auto load any plugins"), NULL},
|
{"no-plugins", 'n', 0, G_OPTION_ARG_NONE, &arg_skip_plugins, N_("Don't auto load any plugins"), NULL},
|
||||||
|
#ifdef __linux__
|
||||||
|
{"run-as-root", 0, 0, G_OPTION_ARG_NONE, &arg_run_as_root, N_("Allow ZoiteChat to run as root (unsafe)"), NULL},
|
||||||
|
#endif
|
||||||
{"plugindir", 'p', 0, G_OPTION_ARG_NONE, &arg_show_autoload, N_("Show plugin/script auto-load directory"), NULL},
|
{"plugindir", 'p', 0, G_OPTION_ARG_NONE, &arg_show_autoload, N_("Show plugin/script auto-load directory"), NULL},
|
||||||
{"configdir", 'u', 0, G_OPTION_ARG_NONE, &arg_show_config, N_("Show user config directory"), NULL},
|
{"configdir", 'u', 0, G_OPTION_ARG_NONE, &arg_show_config, N_("Show user config directory"), NULL},
|
||||||
{"url", 0, G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_STRING, &arg_url, N_("Open an irc://server:port/channel?key URL"), "URL"},
|
{"url", 0, G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_STRING, &arg_url, N_("Open an irc://server:port/channel?key URL"), "URL"},
|
||||||
@@ -102,6 +109,33 @@ static const GOptionEntry gopt_entries[] =
|
|||||||
{NULL}
|
{NULL}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#ifdef __linux__
|
||||||
|
void
|
||||||
|
fe_root_warning (void)
|
||||||
|
{
|
||||||
|
const char *message =
|
||||||
|
"ZoiteChat refuses to run as root on Linux.\n\n"
|
||||||
|
"Run it as a normal user, or pass --run-as-root to override this protection.";
|
||||||
|
GtkWidget *dialog;
|
||||||
|
|
||||||
|
g_printerr ("%s\n", message);
|
||||||
|
|
||||||
|
/* A graphical session may not be accessible to root, particularly under
|
||||||
|
* Wayland. Keep stderr as the fallback if GTK cannot open the display. */
|
||||||
|
if (!gtk_init_check (NULL, NULL))
|
||||||
|
return;
|
||||||
|
|
||||||
|
dialog = gtk_message_dialog_new (NULL,
|
||||||
|
GTK_DIALOG_MODAL,
|
||||||
|
GTK_MESSAGE_ERROR,
|
||||||
|
GTK_BUTTONS_CLOSE,
|
||||||
|
"%s", message);
|
||||||
|
gtk_window_set_title (GTK_WINDOW (dialog), "ZoiteChat");
|
||||||
|
gtk_dialog_run (GTK_DIALOG (dialog));
|
||||||
|
gtk_widget_destroy (dialog);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
#ifdef WIN32
|
#ifdef WIN32
|
||||||
static void
|
static void
|
||||||
create_msg_dialog (gchar *title, gchar *message)
|
create_msg_dialog (gchar *title, gchar *message)
|
||||||
@@ -341,7 +375,13 @@ fe_args (int argc, char *argv[])
|
|||||||
g_free (locale_path);
|
g_free (locale_path);
|
||||||
g_free (base_path);
|
g_free (base_path);
|
||||||
#else
|
#else
|
||||||
bindtextdomain (GETTEXT_PACKAGE, LOCALEDIR);
|
bindtextdomain (GETTEXT_PACKAGE,
|
||||||
|
#ifdef __APPLE__
|
||||||
|
zoitechat_macos_locale_dir ()
|
||||||
|
#else
|
||||||
|
LOCALEDIR
|
||||||
|
#endif
|
||||||
|
);
|
||||||
#endif
|
#endif
|
||||||
bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8");
|
bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8");
|
||||||
textdomain (GETTEXT_PACKAGE);
|
textdomain (GETTEXT_PACKAGE);
|
||||||
@@ -462,6 +502,15 @@ fe_args (int argc, char *argv[])
|
|||||||
#endif
|
#endif
|
||||||
gtk_init (&argc, &argv);
|
gtk_init (&argc, &argv);
|
||||||
|
|
||||||
|
#ifdef __APPLE__
|
||||||
|
if (g_getenv ("ZOITECHAT_MACOS_SHARE"))
|
||||||
|
{
|
||||||
|
char *icons = g_build_filename (g_getenv ("ZOITECHAT_MACOS_SHARE"), "icons", NULL);
|
||||||
|
gtk_icon_theme_prepend_search_path (gtk_icon_theme_get_default (), icons);
|
||||||
|
g_free (icons);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
#ifdef WIN32
|
#ifdef WIN32
|
||||||
win32_configure_icon_theme ();
|
win32_configure_icon_theme ();
|
||||||
#endif
|
#endif
|
||||||
@@ -943,7 +992,13 @@ void
|
|||||||
fe_print_text (struct session *sess, char *text, time_t stamp,
|
fe_print_text (struct session *sess, char *text, time_t stamp,
|
||||||
gboolean no_activity)
|
gboolean no_activity)
|
||||||
{
|
{
|
||||||
PrintTextRaw (sess->res->buffer, (unsigned char *)text, prefs.hex_text_indent, stamp);
|
/* image links become a "[view image]" placeholder with a hidden URL */
|
||||||
|
char *filtered = inline_image_filter_text (text);
|
||||||
|
|
||||||
|
PrintTextRaw (sess->res->buffer,
|
||||||
|
(unsigned char *)(filtered ? filtered : text),
|
||||||
|
prefs.hex_text_indent, stamp);
|
||||||
|
g_free (filtered);
|
||||||
|
|
||||||
if (no_activity || !sess->gui->is_tab)
|
if (no_activity || !sess->gui->is_tab)
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
</ClCompile>
|
</ClCompile>
|
||||||
<Link>
|
<Link>
|
||||||
<AdditionalLibraryDirectories>$(ArchiveLibDir);$(DepsRoot)\lib;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
|
<AdditionalLibraryDirectories>$(ArchiveLibDir);$(DepsRoot)\lib;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
|
||||||
<AdditionalDependencies>$(DepLibs);$(ZoiteChatLib)common.lib;wbemuuid.lib;dwmapi.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
<AdditionalDependencies>$(DepLibs);$(ZoiteChatLib)common.lib;wbemuuid.lib;dwmapi.lib;pangoft2-1.0.lib;fontconfig.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||||
<EntryPointSymbol>mainCRTStartup</EntryPointSymbol>
|
<EntryPointSymbol>mainCRTStartup</EntryPointSymbol>
|
||||||
</Link>
|
</Link>
|
||||||
</ItemDefinitionGroup>
|
</ItemDefinitionGroup>
|
||||||
@@ -59,6 +59,7 @@ powershell "Get-Content -Encoding UTF8 '$(ZoiteChatLib)zoitechat.rc.utf8' | Out-
|
|||||||
<ClInclude Include="fkeys.h" />
|
<ClInclude Include="fkeys.h" />
|
||||||
<ClInclude Include="gtkutil.h" />
|
<ClInclude Include="gtkutil.h" />
|
||||||
<ClInclude Include="icon-resolver.h" />
|
<ClInclude Include="icon-resolver.h" />
|
||||||
|
<ClInclude Include="inline-image.h" />
|
||||||
<ClInclude Include="joind.h" />
|
<ClInclude Include="joind.h" />
|
||||||
<ClInclude Include="maingui.h" />
|
<ClInclude Include="maingui.h" />
|
||||||
<ClInclude Include="preferences-persistence.h" />
|
<ClInclude Include="preferences-persistence.h" />
|
||||||
@@ -97,11 +98,14 @@ powershell "Get-Content -Encoding UTF8 '$(ZoiteChatLib)zoitechat.rc.utf8' | Out-
|
|||||||
<ClCompile Include="custom-list.c" />
|
<ClCompile Include="custom-list.c" />
|
||||||
<ClCompile Include="dccgui.c" />
|
<ClCompile Include="dccgui.c" />
|
||||||
<ClCompile Include="editlist.c" />
|
<ClCompile Include="editlist.c" />
|
||||||
|
<ClCompile Include="emoji-picker.c" />
|
||||||
|
<ClCompile Include="emoji-font.c" />
|
||||||
<ClCompile Include="fe-gtk.c" />
|
<ClCompile Include="fe-gtk.c" />
|
||||||
<ClCompile Include="fkeys.c" />
|
<ClCompile Include="fkeys.c" />
|
||||||
<ClCompile Include="gtkutil.c" />
|
<ClCompile Include="gtkutil.c" />
|
||||||
<ClCompile Include="icon-resolver.c" />
|
<ClCompile Include="icon-resolver.c" />
|
||||||
<ClCompile Include="ignoregui.c" />
|
<ClCompile Include="ignoregui.c" />
|
||||||
|
<ClCompile Include="inline-image.c" />
|
||||||
<ClCompile Include="joind.c" />
|
<ClCompile Include="joind.c" />
|
||||||
<ClCompile Include="maingui.c" />
|
<ClCompile Include="maingui.c" />
|
||||||
<ClCompile Include="menu.c" />
|
<ClCompile Include="menu.c" />
|
||||||
|
|||||||
@@ -45,6 +45,9 @@
|
|||||||
<ClInclude Include="icon-resolver.h">
|
<ClInclude Include="icon-resolver.h">
|
||||||
<Filter>Header Files</Filter>
|
<Filter>Header Files</Filter>
|
||||||
</ClInclude>
|
</ClInclude>
|
||||||
|
<ClInclude Include="inline-image.h">
|
||||||
|
<Filter>Header Files</Filter>
|
||||||
|
</ClInclude>
|
||||||
<ClInclude Include="joind.h">
|
<ClInclude Include="joind.h">
|
||||||
<Filter>Header Files</Filter>
|
<Filter>Header Files</Filter>
|
||||||
</ClInclude>
|
</ClInclude>
|
||||||
@@ -170,6 +173,9 @@
|
|||||||
<ClCompile Include="ignoregui.c">
|
<ClCompile Include="ignoregui.c">
|
||||||
<Filter>Source Files</Filter>
|
<Filter>Source Files</Filter>
|
||||||
</ClCompile>
|
</ClCompile>
|
||||||
|
<ClCompile Include="inline-image.c">
|
||||||
|
<Filter>Source Files</Filter>
|
||||||
|
</ClCompile>
|
||||||
<ClCompile Include="joind.c">
|
<ClCompile Include="joind.c">
|
||||||
<Filter>Source Files</Filter>
|
<Filter>Source Files</Filter>
|
||||||
</ClCompile>
|
</ClCompile>
|
||||||
|
|||||||
+15
-5
@@ -426,7 +426,7 @@ key_binding_is_builtin (struct key_binding *kb)
|
|||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
key_init ()
|
key_init (void)
|
||||||
{
|
{
|
||||||
if (key_load_kbs () == 1)
|
if (key_load_kbs () == 1)
|
||||||
{
|
{
|
||||||
@@ -1219,9 +1219,9 @@ key_dialog_load (GtkListStore *store)
|
|||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
key_dialog_show ()
|
key_dialog_show (void)
|
||||||
{
|
{
|
||||||
GtkWidget *vbox, *box;
|
GtkWidget *vbox, *box, *pane, *scroll;
|
||||||
GtkWidget *view, *xtext, *delete_button;
|
GtkWidget *view, *xtext, *delete_button;
|
||||||
GtkListStore *store;
|
GtkListStore *store;
|
||||||
XTextColor xtext_palette[XTEXT_COLS];
|
XTextColor xtext_palette[XTEXT_COLS];
|
||||||
@@ -1237,10 +1237,20 @@ key_dialog_show ()
|
|||||||
key_dialog = mg_create_generic_tab ("editkeys", buf, TRUE, FALSE, key_dialog_close,
|
key_dialog = mg_create_generic_tab ("editkeys", buf, TRUE, FALSE, key_dialog_close,
|
||||||
NULL, 600, 360, &vbox, 0);
|
NULL, 600, 360, &vbox, 0);
|
||||||
|
|
||||||
view = key_dialog_treeview_new (vbox);
|
pane = gtk_paned_new (GTK_ORIENTATION_VERTICAL);
|
||||||
|
gtk_box_pack_start (GTK_BOX (vbox), pane, TRUE, TRUE, 0);
|
||||||
|
box = gtkutil_box_new (GTK_ORIENTATION_VERTICAL, FALSE, 0);
|
||||||
|
gtk_paned_pack1 (GTK_PANED (pane), box, TRUE, FALSE);
|
||||||
|
view = key_dialog_treeview_new (box);
|
||||||
|
scroll = gtk_scrolled_window_new (NULL, NULL);
|
||||||
|
gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scroll),
|
||||||
|
GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
|
||||||
|
gtk_scrolled_window_set_min_content_height (GTK_SCROLLED_WINDOW (scroll), 80);
|
||||||
|
gtk_paned_pack2 (GTK_PANED (pane), scroll, TRUE, FALSE);
|
||||||
|
gtk_paned_set_position (GTK_PANED (pane), 220);
|
||||||
theme_get_xtext_colors (xtext_palette, XTEXT_COLS);
|
theme_get_xtext_colors (xtext_palette, XTEXT_COLS);
|
||||||
xtext = gtk_xtext_new (xtext_palette, 0);
|
xtext = gtk_xtext_new (xtext_palette, 0);
|
||||||
gtk_box_pack_start (GTK_BOX (vbox), xtext, FALSE, TRUE, 2);
|
gtk_container_add (GTK_CONTAINER (scroll), xtext);
|
||||||
gtk_xtext_set_font (GTK_XTEXT (xtext), prefs.hex_text_font);
|
gtk_xtext_set_font (GTK_XTEXT (xtext), prefs.hex_text_font);
|
||||||
|
|
||||||
g_object_set_data (G_OBJECT (key_dialog), "view", view);
|
g_object_set_data (G_OBJECT (key_dialog), "view", view);
|
||||||
|
|||||||
+47
-12
@@ -168,7 +168,36 @@ gtkutil_append_font_css (GString *css, const PangoFontDescription *font_desc)
|
|||||||
const char *family = pango_font_description_get_family (font_desc);
|
const char *family = pango_font_description_get_family (font_desc);
|
||||||
|
|
||||||
if (family && *family)
|
if (family && *family)
|
||||||
g_string_append_printf (css, " font-family: \"%s\";", family);
|
{
|
||||||
|
char **families = g_strsplit (family, ",", -1);
|
||||||
|
gboolean appended = FALSE;
|
||||||
|
int i;
|
||||||
|
|
||||||
|
for (i = 0; families[i]; i++)
|
||||||
|
{
|
||||||
|
char *name = g_strstrip (families[i]);
|
||||||
|
|
||||||
|
if (!*name)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
g_string_append (css, appended ? "," : " font-family:");
|
||||||
|
g_string_append (css, " \"");
|
||||||
|
for (; *name; name++)
|
||||||
|
{
|
||||||
|
if (*name == '\\' || *name == '\"')
|
||||||
|
g_string_append_c (css, '\\');
|
||||||
|
if (*name == '\n' || *name == '\r' || *name == '\f')
|
||||||
|
g_string_append_printf (css, "\\%x ", (unsigned char) *name);
|
||||||
|
else
|
||||||
|
g_string_append_c (css, *name);
|
||||||
|
}
|
||||||
|
g_string_append_c (css, '\"');
|
||||||
|
appended = TRUE;
|
||||||
|
}
|
||||||
|
if (appended)
|
||||||
|
g_string_append (css, ";");
|
||||||
|
g_strfreev (families);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mask & PANGO_FONT_MASK_STYLE)
|
if (mask & PANGO_FONT_MASK_STYLE)
|
||||||
@@ -612,9 +641,8 @@ gtkutil_get_str (char *msg, char *def, void *callback, void *userdata, gboolean
|
|||||||
if (parent_window && GTK_IS_WINDOW (parent_window))
|
if (parent_window && GTK_IS_WINDOW (parent_window))
|
||||||
gtk_window_set_transient_for (GTK_WINDOW (dialog), GTK_WINDOW (parent_window));
|
gtk_window_set_transient_for (GTK_WINDOW (dialog), GTK_WINDOW (parent_window));
|
||||||
gtkutil_place_dialog (dialog);
|
gtkutil_place_dialog (dialog);
|
||||||
gtk_box_set_homogeneous (GTK_BOX (gtk_dialog_get_content_area (GTK_DIALOG (dialog))), TRUE);
|
|
||||||
|
|
||||||
hbox = gtkutil_box_new (GTK_ORIENTATION_HORIZONTAL, TRUE, 0);
|
hbox = gtkutil_box_new (GTK_ORIENTATION_HORIZONTAL, FALSE, 6);
|
||||||
|
|
||||||
g_object_set_data (G_OBJECT (dialog), "cb", callback);
|
g_object_set_data (G_OBJECT (dialog), "cb", callback);
|
||||||
g_object_set_data (G_OBJECT (dialog), "ud", userdata);
|
g_object_set_data (G_OBJECT (dialog), "ud", userdata);
|
||||||
@@ -624,7 +652,8 @@ gtkutil_get_str (char *msg, char *def, void *callback, void *userdata, gboolean
|
|||||||
G_CALLBACK (gtkutil_str_enter), dialog);
|
G_CALLBACK (gtkutil_str_enter), dialog);
|
||||||
gtk_entry_set_text (GTK_ENTRY (entry), def ? def : "");
|
gtk_entry_set_text (GTK_ENTRY (entry), def ? def : "");
|
||||||
gtk_entry_set_visibility (GTK_ENTRY (entry), visible);
|
gtk_entry_set_visibility (GTK_ENTRY (entry), visible);
|
||||||
gtk_box_pack_end (GTK_BOX (hbox), entry, 0, 0, 0);
|
gtk_widget_set_valign (entry, GTK_ALIGN_CENTER);
|
||||||
|
gtk_box_pack_end (GTK_BOX (hbox), entry, TRUE, TRUE, 0);
|
||||||
|
|
||||||
label = gtk_label_new (msg);
|
label = gtk_label_new (msg);
|
||||||
gtk_box_pack_end (GTK_BOX (hbox), label, 0, 0, 0);
|
gtk_box_pack_end (GTK_BOX (hbox), label, 0, 0, 0);
|
||||||
@@ -632,7 +661,8 @@ gtkutil_get_str (char *msg, char *def, void *callback, void *userdata, gboolean
|
|||||||
g_signal_connect (G_OBJECT (dialog), "response",
|
g_signal_connect (G_OBJECT (dialog), "response",
|
||||||
G_CALLBACK (gtkutil_get_str_response), entry);
|
G_CALLBACK (gtkutil_get_str_response), entry);
|
||||||
|
|
||||||
gtk_container_add (GTK_CONTAINER (gtk_dialog_get_content_area (GTK_DIALOG (dialog))), hbox);
|
gtk_box_pack_start (GTK_BOX (gtk_dialog_get_content_area (GTK_DIALOG (dialog))),
|
||||||
|
hbox, TRUE, TRUE, 0);
|
||||||
|
|
||||||
gtk_widget_show_all (dialog);
|
gtk_widget_show_all (dialog);
|
||||||
}
|
}
|
||||||
@@ -713,9 +743,8 @@ fe_get_int (char *msg, int def, void *callback, void *userdata)
|
|||||||
if (parent_window && GTK_IS_WINDOW (parent_window))
|
if (parent_window && GTK_IS_WINDOW (parent_window))
|
||||||
gtk_window_set_transient_for (GTK_WINDOW (dialog), GTK_WINDOW (parent_window));
|
gtk_window_set_transient_for (GTK_WINDOW (dialog), GTK_WINDOW (parent_window));
|
||||||
gtkutil_place_dialog (dialog);
|
gtkutil_place_dialog (dialog);
|
||||||
gtk_box_set_homogeneous (GTK_BOX (gtk_dialog_get_content_area (GTK_DIALOG (dialog))), TRUE);
|
|
||||||
|
|
||||||
hbox = gtkutil_box_new (GTK_ORIENTATION_HORIZONTAL, TRUE, 0);
|
hbox = gtkutil_box_new (GTK_ORIENTATION_HORIZONTAL, FALSE, 6);
|
||||||
|
|
||||||
g_object_set_data (G_OBJECT (dialog), "cb", callback);
|
g_object_set_data (G_OBJECT (dialog), "cb", callback);
|
||||||
g_object_set_data (G_OBJECT (dialog), "ud", userdata);
|
g_object_set_data (G_OBJECT (dialog), "ud", userdata);
|
||||||
@@ -726,7 +755,8 @@ fe_get_int (char *msg, int def, void *callback, void *userdata)
|
|||||||
gtk_adjustment_set_upper (adj, 1024);
|
gtk_adjustment_set_upper (adj, 1024);
|
||||||
gtk_adjustment_set_step_increment (adj, 1);
|
gtk_adjustment_set_step_increment (adj, 1);
|
||||||
gtk_spin_button_set_value ((GtkSpinButton*)spin, def);
|
gtk_spin_button_set_value ((GtkSpinButton*)spin, def);
|
||||||
gtk_box_pack_end (GTK_BOX (hbox), spin, 0, 0, 0);
|
gtk_widget_set_valign (spin, GTK_ALIGN_CENTER);
|
||||||
|
gtk_box_pack_end (GTK_BOX (hbox), spin, TRUE, TRUE, 0);
|
||||||
|
|
||||||
label = gtk_label_new (msg);
|
label = gtk_label_new (msg);
|
||||||
gtk_box_pack_end (GTK_BOX (hbox), label, 0, 0, 0);
|
gtk_box_pack_end (GTK_BOX (hbox), label, 0, 0, 0);
|
||||||
@@ -734,7 +764,8 @@ fe_get_int (char *msg, int def, void *callback, void *userdata)
|
|||||||
g_signal_connect (G_OBJECT (dialog), "response",
|
g_signal_connect (G_OBJECT (dialog), "response",
|
||||||
G_CALLBACK (gtkutil_get_number_response), spin);
|
G_CALLBACK (gtkutil_get_number_response), spin);
|
||||||
|
|
||||||
gtk_container_add (GTK_CONTAINER (gtk_dialog_get_content_area (GTK_DIALOG (dialog))), hbox);
|
gtk_box_pack_start (GTK_BOX (gtk_dialog_get_content_area (GTK_DIALOG (dialog))),
|
||||||
|
hbox, TRUE, TRUE, 0);
|
||||||
|
|
||||||
gtk_widget_show_all (dialog);
|
gtk_widget_show_all (dialog);
|
||||||
}
|
}
|
||||||
@@ -754,17 +785,18 @@ fe_get_bool (char *title, char *prompt, void *callback, void *userdata)
|
|||||||
if (parent_window && GTK_IS_WINDOW (parent_window))
|
if (parent_window && GTK_IS_WINDOW (parent_window))
|
||||||
gtk_window_set_transient_for (GTK_WINDOW (dialog), GTK_WINDOW (parent_window));
|
gtk_window_set_transient_for (GTK_WINDOW (dialog), GTK_WINDOW (parent_window));
|
||||||
gtkutil_place_dialog (dialog);
|
gtkutil_place_dialog (dialog);
|
||||||
gtk_box_set_homogeneous (GTK_BOX (gtk_dialog_get_content_area (GTK_DIALOG (dialog))), TRUE);
|
|
||||||
|
|
||||||
g_object_set_data (G_OBJECT (dialog), "cb", callback);
|
g_object_set_data (G_OBJECT (dialog), "cb", callback);
|
||||||
g_object_set_data (G_OBJECT (dialog), "ud", userdata);
|
g_object_set_data (G_OBJECT (dialog), "ud", userdata);
|
||||||
|
|
||||||
prompt_label = gtk_label_new (prompt);
|
prompt_label = gtk_label_new (prompt);
|
||||||
|
gtk_label_set_line_wrap (GTK_LABEL (prompt_label), TRUE);
|
||||||
|
|
||||||
g_signal_connect (G_OBJECT (dialog), "response",
|
g_signal_connect (G_OBJECT (dialog), "response",
|
||||||
G_CALLBACK (gtkutil_get_bool_response), NULL);
|
G_CALLBACK (gtkutil_get_bool_response), NULL);
|
||||||
|
|
||||||
gtk_container_add (GTK_CONTAINER (gtk_dialog_get_content_area (GTK_DIALOG (dialog))), prompt_label);
|
gtk_box_pack_start (GTK_BOX (gtk_dialog_get_content_area (GTK_DIALOG (dialog))),
|
||||||
|
prompt_label, TRUE, TRUE, 0);
|
||||||
|
|
||||||
gtk_widget_show_all (dialog);
|
gtk_widget_show_all (dialog);
|
||||||
}
|
}
|
||||||
@@ -856,7 +888,10 @@ gtkutil_window_new (char *title, char *role, int width, int height, int flags)
|
|||||||
gtk_window_set_wmclass (GTK_WINDOW (win), "ZoiteChat", "zoitechat");
|
gtk_window_set_wmclass (GTK_WINDOW (win), "ZoiteChat", "zoitechat");
|
||||||
#endif
|
#endif
|
||||||
gtk_window_set_title (GTK_WINDOW (win), title);
|
gtk_window_set_title (GTK_WINDOW (win), title);
|
||||||
gtk_window_set_default_size (GTK_WINDOW (win), width, height);
|
/* Zero means content-sized to callers; GTK requires -1 for that. */
|
||||||
|
gtk_window_set_default_size (GTK_WINDOW (win),
|
||||||
|
width > 0 ? width : -1,
|
||||||
|
height > 0 ? height : -1);
|
||||||
gtk_window_set_role (GTK_WINDOW (win), role);
|
gtk_window_set_role (GTK_WINDOW (win), role);
|
||||||
if (flags & 1)
|
if (flags & 1)
|
||||||
gtk_window_set_position (GTK_WINDOW (win), GTK_WIN_POS_MOUSE);
|
gtk_window_set_position (GTK_WINDOW (win), GTK_WIN_POS_MOUSE);
|
||||||
|
|||||||
@@ -336,7 +336,7 @@ ignore_stats_entry (GtkWidget * box, char *label, int value)
|
|||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
ignore_gui_open ()
|
ignore_gui_open (void)
|
||||||
{
|
{
|
||||||
GtkWidget *vbox, *box, *stat_box, *frame;
|
GtkWidget *vbox, *box, *stat_box, *frame;
|
||||||
GtkWidget *view;
|
GtkWidget *view;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,45 @@
|
|||||||
|
/* ZoiteChat
|
||||||
|
* Copyright (C) 2026 ZoiteChat contributors.
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation; either version 2 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program; if not, write to the Free Software
|
||||||
|
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef ZOITECHAT_INLINE_IMAGE_H
|
||||||
|
#define ZOITECHAT_INLINE_IMAGE_H
|
||||||
|
|
||||||
|
#include "xtext.h"
|
||||||
|
|
||||||
|
/* does this look like a direct http(s) link to an image file? */
|
||||||
|
gboolean inline_image_is_image_url (const char *url);
|
||||||
|
|
||||||
|
/* show the image behind 'url' inline below 'ent', or hide it again if it is
|
||||||
|
* already shown. Errors are reported to 'sess'. */
|
||||||
|
void inline_image_toggle (session *sess, GtkXText *xtext, textentry *ent,
|
||||||
|
const char *url);
|
||||||
|
|
||||||
|
/* replace image links in a message with a clickable placeholder label that
|
||||||
|
* hides the URL. Returns a newly allocated string, or NULL if the text has
|
||||||
|
* no image links (or the feature is off). */
|
||||||
|
char *inline_image_filter_text (const char *text);
|
||||||
|
|
||||||
|
/* pop up a window with a larger version of the entry's inline image */
|
||||||
|
void inline_image_show_viewer (GtkXText *xtext, textentry *ent);
|
||||||
|
|
||||||
|
/* can downloads go through the proxy configured for ZoiteChat (or is no
|
||||||
|
* proxy configured)? FALSE means loading media would have to bypass the
|
||||||
|
* user's proxy, which is never done. */
|
||||||
|
gboolean inline_image_proxy_usable (void);
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -137,7 +137,6 @@ joind_show_dialog (server *serv)
|
|||||||
gtk_window_set_position (GTK_WINDOW (dialog1), GTK_WIN_POS_CENTER_ON_PARENT);
|
gtk_window_set_position (GTK_WINDOW (dialog1), GTK_WIN_POS_CENTER_ON_PARENT);
|
||||||
gtk_window_set_transient_for (GTK_WINDOW(dialog1), GTK_WINDOW(serv->front_session->gui->window));
|
gtk_window_set_transient_for (GTK_WINDOW(dialog1), GTK_WINDOW(serv->front_session->gui->window));
|
||||||
gtk_window_set_modal (GTK_WINDOW (dialog1), TRUE);
|
gtk_window_set_modal (GTK_WINDOW (dialog1), TRUE);
|
||||||
gtk_window_set_resizable (GTK_WINDOW (dialog1), FALSE);
|
|
||||||
|
|
||||||
dialog_vbox1 = gtk_dialog_get_content_area (GTK_DIALOG (dialog1));
|
dialog_vbox1 = gtk_dialog_get_content_area (GTK_DIALOG (dialog1));
|
||||||
gtk_widget_show (dialog_vbox1);
|
gtk_widget_show (dialog_vbox1);
|
||||||
|
|||||||
+311
-200
@@ -45,6 +45,8 @@
|
|||||||
#include "theme/theme-manager.h"
|
#include "theme/theme-manager.h"
|
||||||
#include "theme/theme-css.h"
|
#include "theme/theme-css.h"
|
||||||
#include "banlist.h"
|
#include "banlist.h"
|
||||||
|
#include "emoji-picker.h"
|
||||||
|
#include "emoji-font.h"
|
||||||
#include "gtkutil.h"
|
#include "gtkutil.h"
|
||||||
#include "icon-resolver.h"
|
#include "icon-resolver.h"
|
||||||
#include "joind.h"
|
#include "joind.h"
|
||||||
@@ -59,6 +61,7 @@
|
|||||||
#include "pixmaps.h"
|
#include "pixmaps.h"
|
||||||
#include "plugin-tray.h"
|
#include "plugin-tray.h"
|
||||||
#include "xtext.h"
|
#include "xtext.h"
|
||||||
|
#include "inline-image.h"
|
||||||
#include "sexy-spell-entry.h"
|
#include "sexy-spell-entry.h"
|
||||||
#include "gtkutil.h"
|
#include "gtkutil.h"
|
||||||
|
|
||||||
@@ -200,7 +203,6 @@ enum
|
|||||||
#define TAG_IRC 0 /* server, channel, dialog */
|
#define TAG_IRC 0 /* server, channel, dialog */
|
||||||
#define TAG_UTIL 1 /* dcc, notify, chanlist */
|
#define TAG_UTIL 1 /* dcc, notify, chanlist */
|
||||||
|
|
||||||
static void mg_apply_emoji_fallback_widget (GtkWidget *widget);
|
|
||||||
static void mg_reply_show_child (GtkWidget *widget, gpointer data);
|
static void mg_reply_show_child (GtkWidget *widget, gpointer data);
|
||||||
|
|
||||||
#define MG_CONFIG_SAVE_DEBOUNCE_MS 250
|
#define MG_CONFIG_SAVE_DEBOUNCE_MS 250
|
||||||
@@ -295,40 +297,6 @@ mg_color_component_to_pango (double value)
|
|||||||
return (guint16)(value * 65535.0 + 0.5);
|
return (guint16)(value * 65535.0 + 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void
|
|
||||||
mg_apply_font_css (GtkWidget *widget, const PangoFontDescription *desc,
|
|
||||||
const char *class_name, const char *provider_key)
|
|
||||||
{
|
|
||||||
GtkStyleContext *context;
|
|
||||||
GtkCssProvider *provider;
|
|
||||||
GString *css;
|
|
||||||
|
|
||||||
if (!widget || !desc)
|
|
||||||
return;
|
|
||||||
|
|
||||||
context = gtk_widget_get_style_context (widget);
|
|
||||||
if (!context)
|
|
||||||
return;
|
|
||||||
|
|
||||||
provider = g_object_get_data (G_OBJECT (widget), provider_key);
|
|
||||||
if (!provider)
|
|
||||||
{
|
|
||||||
provider = gtk_css_provider_new ();
|
|
||||||
g_object_set_data_full (G_OBJECT (widget), provider_key, provider, g_object_unref);
|
|
||||||
}
|
|
||||||
|
|
||||||
css = g_string_new (".");
|
|
||||||
g_string_append (css, class_name);
|
|
||||||
g_string_append (css, " {");
|
|
||||||
gtkutil_append_font_css (css, desc);
|
|
||||||
g_string_append (css, " }");
|
|
||||||
gtk_css_provider_load_from_data (provider, css->str, -1, NULL);
|
|
||||||
g_string_free (css, TRUE);
|
|
||||||
|
|
||||||
gtk_style_context_add_class (context, class_name);
|
|
||||||
theme_css_apply_widget_provider (widget, GTK_STYLE_PROVIDER (provider));
|
|
||||||
}
|
|
||||||
|
|
||||||
static void
|
static void
|
||||||
mg_set_label_alignment_start (GtkWidget *widget)
|
mg_set_label_alignment_start (GtkWidget *widget)
|
||||||
{
|
{
|
||||||
@@ -776,10 +744,13 @@ mg_inputbox_focus (GtkWidget *widget, GdkEventFocus *event, session_gui *gui)
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#define TYPING_THROTTLE_USEC (3 * G_USEC_PER_SEC)
|
||||||
|
|
||||||
static gboolean
|
static gboolean
|
||||||
mg_client_tag_allowed (server *serv, const char *tag)
|
mg_client_tag_allowed (server *serv, const char *tag)
|
||||||
{
|
{
|
||||||
char **deny;
|
char **deny;
|
||||||
|
gboolean blocked = FALSE;
|
||||||
int i;
|
int i;
|
||||||
|
|
||||||
if (!serv->have_message_tags)
|
if (!serv->have_message_tags)
|
||||||
@@ -791,30 +762,42 @@ mg_client_tag_allowed (server *serv, const char *tag)
|
|||||||
deny = g_strsplit (serv->clienttagdeny, ",", 0);
|
deny = g_strsplit (serv->clienttagdeny, ",", 0);
|
||||||
for (i = 0; deny[i]; i++)
|
for (i = 0; deny[i]; i++)
|
||||||
{
|
{
|
||||||
if (!strcmp (deny[i], "*") || !strcmp (deny[i], tag) || (deny[i][0] == '+' && !strcmp (deny[i] + 1, tag)))
|
if (!strcmp (deny[i], "*"))
|
||||||
{
|
blocked = TRUE;
|
||||||
g_strfreev (deny);
|
else if (deny[i][0] == '-' && !strcmp (deny[i] + 1, tag))
|
||||||
return FALSE;
|
blocked = FALSE;
|
||||||
}
|
else if (!strcmp (deny[i], tag) || (deny[i][0] == '+' && !strcmp (deny[i] + 1, tag)))
|
||||||
|
blocked = TRUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
g_strfreev (deny);
|
g_strfreev (deny);
|
||||||
return TRUE;
|
return !blocked;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void
|
static gboolean
|
||||||
|
mg_typing_can_send (session *sess)
|
||||||
|
{
|
||||||
|
gint64 now = g_get_monotonic_time ();
|
||||||
|
|
||||||
|
return sess->typing_last_sent == 0 ||
|
||||||
|
now - sess->typing_last_sent >= TYPING_THROTTLE_USEC;
|
||||||
|
}
|
||||||
|
|
||||||
|
static gboolean
|
||||||
mg_send_typing (session *sess, const char *state)
|
mg_send_typing (session *sess, const char *state)
|
||||||
{
|
{
|
||||||
char tags[32];
|
char tags[32];
|
||||||
|
|
||||||
if (!sess || !sess->server->connected || !mg_client_tag_allowed (sess->server, "typing") || !sess->channel[0])
|
if (!sess || !sess->server->connected || !mg_client_tag_allowed (sess->server, "typing") || !sess->channel[0])
|
||||||
return;
|
return FALSE;
|
||||||
|
|
||||||
if (sess->type != SESS_CHANNEL && sess->type != SESS_DIALOG)
|
if (sess->type != SESS_CHANNEL && sess->type != SESS_DIALOG)
|
||||||
return;
|
return FALSE;
|
||||||
|
|
||||||
g_snprintf (tags, sizeof (tags), "+typing=%s", state);
|
g_snprintf (tags, sizeof (tags), "+typing=%s", state);
|
||||||
sess->server->p_tagmsg (sess->server, tags, sess->channel);
|
sess->server->p_tagmsg (sess->server, tags, sess->channel);
|
||||||
|
sess->typing_last_sent = g_get_monotonic_time ();
|
||||||
|
return TRUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int
|
static int
|
||||||
@@ -843,17 +826,15 @@ mg_typing_update (session *sess, const char *text)
|
|||||||
|
|
||||||
if (!text || !*text || text[0] == prefs.hex_input_command_char[0])
|
if (!text || !*text || text[0] == prefs.hex_input_command_char[0])
|
||||||
{
|
{
|
||||||
if (sess->typing_status)
|
if (sess->typing_status && mg_typing_can_send (sess))
|
||||||
mg_send_typing (sess, "done");
|
mg_send_typing (sess, "done");
|
||||||
sess->typing_status = 0;
|
sess->typing_status = 0;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sess->typing_status != 1)
|
if (mg_typing_can_send (sess))
|
||||||
{
|
|
||||||
mg_send_typing (sess, "active");
|
mg_send_typing (sess, "active");
|
||||||
sess->typing_status = 1;
|
sess->typing_status = 1;
|
||||||
}
|
|
||||||
sess->typing_timeout_tag = fe_timeout_add_seconds (6, mg_typing_pause_cb, sess);
|
sess->typing_timeout_tag = fe_timeout_add_seconds (6, mg_typing_pause_cb, sess);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -925,10 +906,236 @@ mg_send_reply_or_text (session *sess, char *cmd)
|
|||||||
mg_reply_update (sess);
|
mg_reply_update (sess);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
gboolean bold;
|
||||||
|
gboolean italic;
|
||||||
|
gboolean underline;
|
||||||
|
gboolean strikethrough;
|
||||||
|
gboolean reverse;
|
||||||
|
gint foreground;
|
||||||
|
gint background;
|
||||||
|
gboolean pending_foreground;
|
||||||
|
gboolean pending_background;
|
||||||
|
} MgInputFormatState;
|
||||||
|
|
||||||
|
static void
|
||||||
|
mg_input_format_state_at_cursor (GtkEntry *entry, MgInputFormatState *state)
|
||||||
|
{
|
||||||
|
const char *text;
|
||||||
|
const char *cursor;
|
||||||
|
gsize limit;
|
||||||
|
gsize i;
|
||||||
|
gint cursor_pos;
|
||||||
|
|
||||||
|
memset (state, 0, sizeof (*state));
|
||||||
|
state->foreground = -1;
|
||||||
|
state->background = -1;
|
||||||
|
|
||||||
|
text = gtk_entry_get_text (entry);
|
||||||
|
cursor_pos = gtk_editable_get_position (GTK_EDITABLE (entry));
|
||||||
|
if (!text || cursor_pos < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
cursor = g_utf8_offset_to_pointer (text, cursor_pos);
|
||||||
|
limit = (gsize) (cursor - text);
|
||||||
|
|
||||||
|
for (i = 0; i < limit;)
|
||||||
|
{
|
||||||
|
guchar ch = (guchar) text[i++];
|
||||||
|
|
||||||
|
switch (ch)
|
||||||
|
{
|
||||||
|
case ATTR_BOLD:
|
||||||
|
state->bold = !state->bold;
|
||||||
|
break;
|
||||||
|
case ATTR_ITALICS:
|
||||||
|
state->italic = !state->italic;
|
||||||
|
break;
|
||||||
|
case ATTR_UNDERLINE:
|
||||||
|
state->underline = !state->underline;
|
||||||
|
break;
|
||||||
|
case ATTR_STRIKETHROUGH:
|
||||||
|
state->strikethrough = !state->strikethrough;
|
||||||
|
break;
|
||||||
|
case ATTR_REVERSE:
|
||||||
|
state->reverse = !state->reverse;
|
||||||
|
break;
|
||||||
|
case ATTR_RESET:
|
||||||
|
case '\n':
|
||||||
|
state->bold = FALSE;
|
||||||
|
state->italic = FALSE;
|
||||||
|
state->underline = FALSE;
|
||||||
|
state->strikethrough = FALSE;
|
||||||
|
state->reverse = FALSE;
|
||||||
|
state->foreground = -1;
|
||||||
|
state->background = -1;
|
||||||
|
state->pending_foreground = FALSE;
|
||||||
|
state->pending_background = FALSE;
|
||||||
|
break;
|
||||||
|
case ATTR_COLOR:
|
||||||
|
{
|
||||||
|
gint value = 0;
|
||||||
|
gint digits = 0;
|
||||||
|
|
||||||
|
state->pending_foreground = FALSE;
|
||||||
|
state->pending_background = FALSE;
|
||||||
|
|
||||||
|
if (i >= limit)
|
||||||
|
{
|
||||||
|
state->pending_foreground = TRUE;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!g_ascii_isdigit (text[i]))
|
||||||
|
{
|
||||||
|
state->foreground = -1;
|
||||||
|
state->background = -1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (i < limit && digits < 2 && g_ascii_isdigit (text[i]))
|
||||||
|
{
|
||||||
|
value = (value * 10) + (text[i] - '0');
|
||||||
|
i++;
|
||||||
|
digits++;
|
||||||
|
}
|
||||||
|
state->foreground = value;
|
||||||
|
state->background = -1;
|
||||||
|
|
||||||
|
if (i < limit && text[i] == ',')
|
||||||
|
{
|
||||||
|
i++;
|
||||||
|
if (i >= limit)
|
||||||
|
{
|
||||||
|
state->pending_background = TRUE;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
value = 0;
|
||||||
|
digits = 0;
|
||||||
|
while (i < limit && digits < 2 && g_ascii_isdigit (text[i]))
|
||||||
|
{
|
||||||
|
value = (value * 10) + (text[i] - '0');
|
||||||
|
i++;
|
||||||
|
digits++;
|
||||||
|
}
|
||||||
|
if (digits > 0)
|
||||||
|
state->background = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
mg_input_format_append (GString *summary, const char *text)
|
||||||
|
{
|
||||||
|
if (summary->len > 0)
|
||||||
|
g_string_append (summary, " · ");
|
||||||
|
g_string_append (summary, text);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
mg_input_format_color_value (char *buffer, gsize buffer_len, const char *label, gint color_index)
|
||||||
|
{
|
||||||
|
guint16 red;
|
||||||
|
guint16 green;
|
||||||
|
guint16 blue;
|
||||||
|
|
||||||
|
if (theme_get_mirc_color_rgb16 ((unsigned int) color_index, &red, &green, &blue))
|
||||||
|
{
|
||||||
|
g_snprintf (buffer, buffer_len,
|
||||||
|
"%s <b>%02d</b> <span foreground=\"#%02x%02x%02x\">■</span>",
|
||||||
|
label, color_index, (guint) (red >> 8), (guint) (green >> 8),
|
||||||
|
(guint) (blue >> 8));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
g_snprintf (buffer, buffer_len, "%s <b>%02d</b>", label, color_index);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
mg_input_format_update (GtkEntry *entry)
|
||||||
|
{
|
||||||
|
GtkWidget *format_box;
|
||||||
|
GtkWidget *format_label;
|
||||||
|
MgInputFormatState state;
|
||||||
|
GString *summary;
|
||||||
|
char color[96];
|
||||||
|
char *markup;
|
||||||
|
|
||||||
|
format_box = g_object_get_data (G_OBJECT (entry), "zoitechat-format-box");
|
||||||
|
format_label = g_object_get_data (G_OBJECT (entry), "zoitechat-format-label");
|
||||||
|
if (!format_box || !format_label)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!prefs.hex_gui_input_attr)
|
||||||
|
{
|
||||||
|
gtk_widget_hide (format_box);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mg_input_format_state_at_cursor (entry, &state);
|
||||||
|
summary = g_string_new (NULL);
|
||||||
|
|
||||||
|
if (state.bold)
|
||||||
|
mg_input_format_append (summary, "<b>Bold</b>");
|
||||||
|
if (state.italic)
|
||||||
|
mg_input_format_append (summary, "<i>Italic</i>");
|
||||||
|
if (state.underline)
|
||||||
|
mg_input_format_append (summary, "<u>Underline</u>");
|
||||||
|
if (state.strikethrough)
|
||||||
|
mg_input_format_append (summary, "<s>Strikethrough</s>");
|
||||||
|
if (state.reverse)
|
||||||
|
mg_input_format_append (summary, "Reverse");
|
||||||
|
|
||||||
|
if (state.foreground >= 0)
|
||||||
|
{
|
||||||
|
mg_input_format_color_value (color, sizeof (color), "Text color", state.foreground);
|
||||||
|
mg_input_format_append (summary, color);
|
||||||
|
}
|
||||||
|
if (state.background >= 0)
|
||||||
|
{
|
||||||
|
mg_input_format_color_value (color, sizeof (color), "Background", state.background);
|
||||||
|
mg_input_format_append (summary, color);
|
||||||
|
}
|
||||||
|
if (state.pending_foreground)
|
||||||
|
mg_input_format_append (summary, "Color: type foreground[,background]");
|
||||||
|
else if (state.pending_background)
|
||||||
|
mg_input_format_append (summary, "Background: type color");
|
||||||
|
|
||||||
|
if (summary->len == 0)
|
||||||
|
{
|
||||||
|
gtk_widget_hide (format_box);
|
||||||
|
g_string_free (summary, TRUE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
markup = g_strdup_printf ("<span foreground='#7d8790'>Formatting · %s</span>", summary->str);
|
||||||
|
gtk_label_set_markup (GTK_LABEL (format_label), markup);
|
||||||
|
gtk_widget_show (format_label);
|
||||||
|
gtk_widget_show (format_box);
|
||||||
|
g_free (markup);
|
||||||
|
g_string_free (summary, TRUE);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
mg_inputbox_cursor_changed (GObject *object, GParamSpec *pspec, gpointer userdata)
|
||||||
|
{
|
||||||
|
(void) pspec;
|
||||||
|
(void) userdata;
|
||||||
|
mg_input_format_update (GTK_ENTRY (object));
|
||||||
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
mg_inputbox_changed (GtkEditable *editable, session_gui *gui)
|
mg_inputbox_changed (GtkEditable *editable, session_gui *gui)
|
||||||
{
|
{
|
||||||
key_check_replace_on_change (editable, NULL);
|
key_check_replace_on_change (editable, NULL);
|
||||||
|
mg_input_format_update (GTK_ENTRY (editable));
|
||||||
if (current_sess && current_sess->gui == gui)
|
if (current_sess && current_sess->gui == gui)
|
||||||
mg_typing_update (current_sess, gtk_entry_get_text (GTK_ENTRY (editable)));
|
mg_typing_update (current_sess, gtk_entry_get_text (GTK_ENTRY (editable)));
|
||||||
}
|
}
|
||||||
@@ -2089,7 +2296,6 @@ mg_open_quit_dialog (gboolean minimize_button)
|
|||||||
gtk_container_set_border_width (GTK_CONTAINER (dialog), 6);
|
gtk_container_set_border_width (GTK_CONTAINER (dialog), 6);
|
||||||
gtk_window_set_title (GTK_WINDOW (dialog), _("Quit ZoiteChat?"));
|
gtk_window_set_title (GTK_WINDOW (dialog), _("Quit ZoiteChat?"));
|
||||||
gtk_window_set_transient_for (GTK_WINDOW (dialog), GTK_WINDOW (parent_window));
|
gtk_window_set_transient_for (GTK_WINDOW (dialog), GTK_WINDOW (parent_window));
|
||||||
gtk_window_set_resizable (GTK_WINDOW (dialog), FALSE);
|
|
||||||
|
|
||||||
dialog_vbox1 = gtk_dialog_get_content_area (GTK_DIALOG (dialog));
|
dialog_vbox1 = gtk_dialog_get_content_area (GTK_DIALOG (dialog));
|
||||||
gtk_widget_show (dialog_vbox1);
|
gtk_widget_show (dialog_vbox1);
|
||||||
@@ -3246,7 +3452,7 @@ mg_create_chanmodebuttons (session_gui *gui, GtkWidget *box)
|
|||||||
gtk_entry_set_max_length (GTK_ENTRY (gui->key_entry), 23);
|
gtk_entry_set_max_length (GTK_ENTRY (gui->key_entry), 23);
|
||||||
gtk_widget_set_size_request (gui->key_entry, 58, 11);
|
gtk_widget_set_size_request (gui->key_entry, 58, 11);
|
||||||
gtk_box_pack_start (GTK_BOX (box), gui->key_entry, 0, 0, 0);
|
gtk_box_pack_start (GTK_BOX (box), gui->key_entry, 0, 0, 0);
|
||||||
mg_apply_emoji_fallback_widget (gui->key_entry);
|
emoji_font_apply (gui->key_entry);
|
||||||
mg_apply_compact_mode_css (gui->key_entry);
|
mg_apply_compact_mode_css (gui->key_entry);
|
||||||
g_signal_connect (G_OBJECT (gui->key_entry), "activate",
|
g_signal_connect (G_OBJECT (gui->key_entry), "activate",
|
||||||
G_CALLBACK (mg_key_entry_cb), NULL);
|
G_CALLBACK (mg_key_entry_cb), NULL);
|
||||||
@@ -3264,7 +3470,7 @@ mg_create_chanmodebuttons (session_gui *gui, GtkWidget *box)
|
|||||||
gtk_entry_set_width_chars (GTK_ENTRY (gui->limit_entry), 5);
|
gtk_entry_set_width_chars (GTK_ENTRY (gui->limit_entry), 5);
|
||||||
gtk_widget_set_size_request (gui->limit_entry, 45, 11);
|
gtk_widget_set_size_request (gui->limit_entry, 45, 11);
|
||||||
gtk_box_pack_start (GTK_BOX (box), gui->limit_entry, 0, 0, 0);
|
gtk_box_pack_start (GTK_BOX (box), gui->limit_entry, 0, 0, 0);
|
||||||
mg_apply_emoji_fallback_widget (gui->limit_entry);
|
emoji_font_apply (gui->limit_entry);
|
||||||
mg_apply_compact_mode_css (gui->limit_entry);
|
mg_apply_compact_mode_css (gui->limit_entry);
|
||||||
g_signal_connect (G_OBJECT (gui->limit_entry), "activate",
|
g_signal_connect (G_OBJECT (gui->limit_entry), "activate",
|
||||||
G_CALLBACK (mg_limit_entry_cb), NULL);
|
G_CALLBACK (mg_limit_entry_cb), NULL);
|
||||||
@@ -3529,8 +3735,12 @@ mg_apply_session_font_prefs (session_gui *gui)
|
|||||||
mg_topicbar_update_height (gui->topic_entry);
|
mg_topicbar_update_height (gui->topic_entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (gui->input_box && prefs.hex_gui_input_style)
|
if (gui->input_box)
|
||||||
theme_manager_apply_entry_palette (gui->input_box, font);
|
{
|
||||||
|
if (prefs.hex_gui_input_style)
|
||||||
|
theme_manager_apply_entry_palette (gui->input_box, font);
|
||||||
|
emoji_font_apply (gui->input_box);
|
||||||
|
}
|
||||||
|
|
||||||
if (gui->chanview)
|
if (gui->chanview)
|
||||||
chanview_apply_theme (gui->chanview);
|
chanview_apply_theme (gui->chanview);
|
||||||
@@ -3687,7 +3897,13 @@ mg_word_clicked (GtkWidget *xtext, char *word, GdkEventButton *even)
|
|||||||
{
|
{
|
||||||
if (word == NULL)
|
if (word == NULL)
|
||||||
{
|
{
|
||||||
mg_focus (sess);
|
/* a click on a shown inline image opens the viewer */
|
||||||
|
textentry *img_ent = gtk_xtext_get_clicked_image_entry (GTK_XTEXT (xtext));
|
||||||
|
|
||||||
|
if (img_ent)
|
||||||
|
inline_image_show_viewer (GTK_XTEXT (xtext), img_ent);
|
||||||
|
else
|
||||||
|
mg_focus (sess);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3699,6 +3915,20 @@ mg_word_clicked (GtkWidget *xtext, char *word, GdkEventButton *even)
|
|||||||
case WORD_HOST6:
|
case WORD_HOST6:
|
||||||
case WORD_HOST:
|
case WORD_HOST:
|
||||||
word[end] = 0;
|
word[end] = 0;
|
||||||
|
/* image links toggle an inline preview instead
|
||||||
|
of opening the browser */
|
||||||
|
if (word_type == WORD_URL &&
|
||||||
|
prefs.hex_net_remote_media &&
|
||||||
|
inline_image_is_image_url (word + start))
|
||||||
|
{
|
||||||
|
textentry *ent = gtk_xtext_get_clicked_entry (GTK_XTEXT (xtext));
|
||||||
|
|
||||||
|
if (ent)
|
||||||
|
{
|
||||||
|
inline_image_toggle (sess, GTK_XTEXT (xtext), ent, word + start);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
fe_open_url (word + start);
|
fe_open_url (word + start);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3727,7 +3957,8 @@ mg_word_clicked (GtkWidget *xtext, char *word, GdkEventButton *even)
|
|||||||
case WORD_HOST:
|
case WORD_HOST:
|
||||||
word[end] = 0;
|
word[end] = 0;
|
||||||
word += start;
|
word += start;
|
||||||
menu_urlmenu (even, word);
|
menu_urlmenu_entry (even, word, xtext,
|
||||||
|
gtk_xtext_get_clicked_entry (GTK_XTEXT (xtext)));
|
||||||
break;
|
break;
|
||||||
case WORD_NICK:
|
case WORD_NICK:
|
||||||
word[end] = 0;
|
word[end] = 0;
|
||||||
@@ -4416,131 +4647,6 @@ mg_inputbox_rightclick (GtkEntry *entry, GtkWidget *menu)
|
|||||||
mg_create_color_menu (menu, NULL);
|
mg_create_color_menu (menu, NULL);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ------------------------------------------------------------------------- *
|
|
||||||
* Emoji font handling
|
|
||||||
*
|
|
||||||
* Goal: prefer color emoji fonts when available, without changing existing
|
|
||||||
* font size/style/weight, and without breaking user-configured fonts.
|
|
||||||
* ------------------------------------------------------------------------- */
|
|
||||||
|
|
||||||
static const char *mg_emoji_family_fallback =
|
|
||||||
#ifdef G_OS_WIN32
|
|
||||||
"Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji, Apple Color Emoji, Twemoji Mozilla, EmojiOne Color";
|
|
||||||
#else
|
|
||||||
"Noto Color Emoji, Segoe UI Emoji, Apple Color Emoji, Twemoji Mozilla, EmojiOne Color";
|
|
||||||
#endif
|
|
||||||
|
|
||||||
static const char *
|
|
||||||
mg_find_available_icon_name (const char *const *icon_names)
|
|
||||||
{
|
|
||||||
GtkIconTheme *theme;
|
|
||||||
int i;
|
|
||||||
|
|
||||||
theme = gtk_icon_theme_get_default ();
|
|
||||||
if (!theme || !icon_names)
|
|
||||||
return NULL;
|
|
||||||
|
|
||||||
for (i = 0; icon_names[i] != NULL; i++)
|
|
||||||
{
|
|
||||||
int action;
|
|
||||||
|
|
||||||
if (gtk_icon_theme_has_icon (theme, icon_names[i]))
|
|
||||||
return icon_names[i];
|
|
||||||
|
|
||||||
if (icon_resolver_menu_action_from_name (icon_names[i], &action))
|
|
||||||
{
|
|
||||||
char *resource_path = icon_resolver_resolve_path (ICON_RESOLVER_ROLE_MENU_ACTION, action, GTK_ICON_SIZE_MENU, "menu", ICON_RESOLVER_THEME_SYSTEM, NULL);
|
|
||||||
|
|
||||||
if (resource_path)
|
|
||||||
{
|
|
||||||
g_free (resource_path);
|
|
||||||
return icon_names[i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
static gboolean
|
|
||||||
mg_family_already_has_emoji (const gchar *family)
|
|
||||||
{
|
|
||||||
if (!family || !*family)
|
|
||||||
return FALSE;
|
|
||||||
|
|
||||||
/* cheap but effective */
|
|
||||||
return (strstr (family, "Noto Color Emoji") != NULL) ||
|
|
||||||
(strstr (family, "Segoe UI Emoji") != NULL) ||
|
|
||||||
(strstr (family, "Apple Color Emoji") != NULL) ||
|
|
||||||
(strstr (family, "Twemoji") != NULL) ||
|
|
||||||
(strstr (family, "EmojiOne") != NULL);
|
|
||||||
}
|
|
||||||
|
|
||||||
static PangoFontDescription *
|
|
||||||
mg_fontdesc_with_fallback (const PangoFontDescription *base_desc, gboolean emoji_first)
|
|
||||||
{
|
|
||||||
PangoFontDescription *desc;
|
|
||||||
const gchar *base_family;
|
|
||||||
gchar *family_list;
|
|
||||||
|
|
||||||
if (!base_desc)
|
|
||||||
return NULL;
|
|
||||||
|
|
||||||
desc = pango_font_description_copy (base_desc);
|
|
||||||
base_family = pango_font_description_get_family (desc);
|
|
||||||
|
|
||||||
if (mg_family_already_has_emoji (base_family))
|
|
||||||
return desc;
|
|
||||||
|
|
||||||
if (emoji_first)
|
|
||||||
{
|
|
||||||
family_list = g_strdup_printf ("%s, %s",
|
|
||||||
mg_emoji_family_fallback,
|
|
||||||
(base_family && *base_family) ? base_family : "Sans");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
family_list = g_strdup_printf ("%s, %s",
|
|
||||||
(base_family && *base_family) ? base_family : "Sans",
|
|
||||||
mg_emoji_family_fallback);
|
|
||||||
}
|
|
||||||
|
|
||||||
pango_font_description_set_family (desc, family_list);
|
|
||||||
g_free (family_list);
|
|
||||||
|
|
||||||
return desc;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void
|
|
||||||
mg_apply_emoji_fallback_widget (GtkWidget *widget)
|
|
||||||
{
|
|
||||||
PangoFontDescription *desc;
|
|
||||||
GtkStyleContext *context;
|
|
||||||
PangoFontDescription *base_desc = NULL;
|
|
||||||
|
|
||||||
if (!widget)
|
|
||||||
return;
|
|
||||||
|
|
||||||
context = gtk_widget_get_style_context (widget);
|
|
||||||
if (!context)
|
|
||||||
return;
|
|
||||||
|
|
||||||
gtk_style_context_get (context, GTK_STATE_FLAG_NORMAL,
|
|
||||||
"font", &base_desc,
|
|
||||||
NULL);
|
|
||||||
if (!base_desc)
|
|
||||||
return;
|
|
||||||
|
|
||||||
desc = mg_fontdesc_with_fallback (base_desc, FALSE);
|
|
||||||
pango_font_description_free (base_desc);
|
|
||||||
if (!desc)
|
|
||||||
return;
|
|
||||||
|
|
||||||
mg_apply_font_css (widget, desc, "zoitechat-emoji-font",
|
|
||||||
"zoitechat-emoji-font-provider");
|
|
||||||
pango_font_description_free (desc);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Search bar adapted from Conspire's by William Pitcock */
|
/* Search bar adapted from Conspire's by William Pitcock */
|
||||||
|
|
||||||
#define SEARCH_CHANGE 1
|
#define SEARCH_CHANGE 1
|
||||||
@@ -4745,7 +4851,7 @@ mg_create_search(session *sess, GtkWidget *box)
|
|||||||
gui->shentry = entry = gtk_entry_new();
|
gui->shentry = entry = gtk_entry_new();
|
||||||
gtk_box_pack_start(GTK_BOX(gui->shbox), entry, FALSE, FALSE, 0);
|
gtk_box_pack_start(GTK_BOX(gui->shbox), entry, FALSE, FALSE, 0);
|
||||||
gtk_widget_set_size_request (gui->shentry, 180, -1);
|
gtk_widget_set_size_request (gui->shentry, 180, -1);
|
||||||
mg_apply_emoji_fallback_widget (entry);
|
emoji_font_apply (entry);
|
||||||
mg_apply_entry_scroll_artifact_fix (entry);
|
mg_apply_entry_scroll_artifact_fix (entry);
|
||||||
gui->search_changed_signal = g_signal_connect(G_OBJECT(entry), "changed", G_CALLBACK(search_handle_change), sess);
|
gui->search_changed_signal = g_signal_connect(G_OBJECT(entry), "changed", G_CALLBACK(search_handle_change), sess);
|
||||||
g_signal_connect (G_OBJECT (entry), "key-press-event", G_CALLBACK (search_handle_keypress), sess);
|
g_signal_connect (G_OBJECT (entry), "key-press-event", G_CALLBACK (search_handle_keypress), sess);
|
||||||
@@ -4799,17 +4905,18 @@ mg_create_search(session *sess, GtkWidget *box)
|
|||||||
static void
|
static void
|
||||||
mg_create_entry (session *sess, GtkWidget *box)
|
mg_create_entry (session *sess, GtkWidget *box)
|
||||||
{
|
{
|
||||||
GtkWidget *hbox, *but, *entry;
|
GtkWidget *hbox, *but, *entry, *format_box, *format_label;
|
||||||
session_gui *gui = sess->gui;
|
session_gui *gui = sess->gui;
|
||||||
const char *emoji_fallback_icon_names[] = {
|
|
||||||
"face-smile-symbolic",
|
format_box = mg_box_new (GTK_ORIENTATION_HORIZONTAL, FALSE, 6);
|
||||||
"face-smile",
|
gtk_widget_set_name (format_box, "zoitechat-formatbar");
|
||||||
"insert-emoticon-symbolic",
|
gtk_widget_set_no_show_all (format_box, TRUE);
|
||||||
"insert-emoticon",
|
gtk_box_pack_start (GTK_BOX (box), format_box, 0, 0, 0);
|
||||||
"zc-menu-emoji",
|
format_label = gtk_label_new ("");
|
||||||
NULL
|
gtk_label_set_ellipsize (GTK_LABEL (format_label), PANGO_ELLIPSIZE_END);
|
||||||
};
|
gtk_box_pack_start (GTK_BOX (format_box), format_label, TRUE, TRUE, 8);
|
||||||
const char *emoji_fallback_icon_name;
|
gtk_widget_show (format_label);
|
||||||
|
gtk_widget_hide (format_box);
|
||||||
|
|
||||||
gui->reply_box = mg_box_new (GTK_ORIENTATION_HORIZONTAL, FALSE, 6);
|
gui->reply_box = mg_box_new (GTK_ORIENTATION_HORIZONTAL, FALSE, 6);
|
||||||
gtk_widget_set_name (gui->reply_box, "zoitechat-replybar");
|
gtk_widget_set_name (gui->reply_box, "zoitechat-replybar");
|
||||||
@@ -4841,12 +4948,16 @@ mg_create_entry (session *sess, GtkWidget *box)
|
|||||||
gui->input_box = entry = sexy_spell_entry_new ();
|
gui->input_box = entry = sexy_spell_entry_new ();
|
||||||
sexy_spell_entry_set_checked ((SexySpellEntry *)entry, prefs.hex_gui_input_spell);
|
sexy_spell_entry_set_checked ((SexySpellEntry *)entry, prefs.hex_gui_input_spell);
|
||||||
sexy_spell_entry_set_parse_attributes ((SexySpellEntry *)entry, prefs.hex_gui_input_attr);
|
sexy_spell_entry_set_parse_attributes ((SexySpellEntry *)entry, prefs.hex_gui_input_attr);
|
||||||
|
g_object_set_data (G_OBJECT (entry), "zoitechat-format-box", format_box);
|
||||||
|
g_object_set_data (G_OBJECT (entry), "zoitechat-format-label", format_label);
|
||||||
|
|
||||||
gtk_entry_set_max_length (GTK_ENTRY (gui->input_box), 0);
|
gtk_entry_set_max_length (GTK_ENTRY (gui->input_box), 0);
|
||||||
g_signal_connect (G_OBJECT (entry), "activate",
|
g_signal_connect (G_OBJECT (entry), "activate",
|
||||||
G_CALLBACK (mg_inputbox_cb), gui);
|
G_CALLBACK (mg_inputbox_cb), gui);
|
||||||
g_signal_connect (G_OBJECT (entry), "changed",
|
g_signal_connect (G_OBJECT (entry), "changed",
|
||||||
G_CALLBACK (mg_inputbox_changed), gui);
|
G_CALLBACK (mg_inputbox_changed), gui);
|
||||||
|
g_signal_connect (G_OBJECT (entry), "notify::cursor-position",
|
||||||
|
G_CALLBACK (mg_inputbox_cursor_changed), gui);
|
||||||
gtk_box_pack_start (GTK_BOX (hbox), entry, TRUE, TRUE, 0);
|
gtk_box_pack_start (GTK_BOX (hbox), entry, TRUE, TRUE, 0);
|
||||||
|
|
||||||
gtk_widget_set_name (entry, "zoitechat-inputbox");
|
gtk_widget_set_name (entry, "zoitechat-inputbox");
|
||||||
@@ -4862,16 +4973,14 @@ mg_create_entry (session *sess, GtkWidget *box)
|
|||||||
|
|
||||||
if (prefs.hex_gui_input_style)
|
if (prefs.hex_gui_input_style)
|
||||||
mg_apply_entry_style (entry);
|
mg_apply_entry_style (entry);
|
||||||
|
emoji_font_apply (entry);
|
||||||
mg_apply_entry_scroll_artifact_fix (entry);
|
mg_apply_entry_scroll_artifact_fix (entry);
|
||||||
|
|
||||||
g_object_set (G_OBJECT (entry), "show-emoji-icon", TRUE, NULL);
|
/* ZoiteChat's own emoji picker; deliberately not GTK's
|
||||||
|
* "show-emoji-icon" machinery, so the catalog is the same on every
|
||||||
if (gtk_entry_get_icon_storage_type (GTK_ENTRY (entry), GTK_ENTRY_ICON_SECONDARY) == GTK_IMAGE_EMPTY)
|
* GTK runtime and the button always has a working callback */
|
||||||
{
|
but = emoji_picker_button_new (entry);
|
||||||
emoji_fallback_icon_name = mg_find_available_icon_name (emoji_fallback_icon_names);
|
gtk_box_pack_start (GTK_BOX (hbox), but, FALSE, FALSE, 0);
|
||||||
if (emoji_fallback_icon_name)
|
|
||||||
gtk_entry_set_icon_from_icon_name (GTK_ENTRY (entry), GTK_ENTRY_ICON_SECONDARY, emoji_fallback_icon_name);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
@@ -5019,6 +5128,7 @@ mg_create_topwindow (session *sess)
|
|||||||
prefs.hex_gui_win_width,
|
prefs.hex_gui_win_width,
|
||||||
prefs.hex_gui_win_height, 0);
|
prefs.hex_gui_win_height, 0);
|
||||||
sess->gui->window = win;
|
sess->gui->window = win;
|
||||||
|
emoji_font_apply (win);
|
||||||
gtk_container_set_border_width (GTK_CONTAINER (win), GUI_BORDER);
|
gtk_container_set_border_width (GTK_CONTAINER (win), GUI_BORDER);
|
||||||
gtk_widget_set_opacity (win, (prefs.hex_gui_transparency / 255.));
|
gtk_widget_set_opacity (win, (prefs.hex_gui_transparency / 255.));
|
||||||
|
|
||||||
@@ -5223,6 +5333,7 @@ mg_create_tabwindow (session *sess)
|
|||||||
win = gtkutil_window_new ("ZoiteChat", NULL, prefs.hex_gui_win_width,
|
win = gtkutil_window_new ("ZoiteChat", NULL, prefs.hex_gui_win_width,
|
||||||
prefs.hex_gui_win_height, 0);
|
prefs.hex_gui_win_height, 0);
|
||||||
sess->gui->window = win;
|
sess->gui->window = win;
|
||||||
|
emoji_font_apply (win);
|
||||||
gtk_window_move (GTK_WINDOW (win), prefs.hex_gui_win_left,
|
gtk_window_move (GTK_WINDOW (win), prefs.hex_gui_win_left,
|
||||||
prefs.hex_gui_win_top);
|
prefs.hex_gui_win_top);
|
||||||
if (prefs.hex_gui_win_state)
|
if (prefs.hex_gui_win_state)
|
||||||
|
|||||||
+64
-5
@@ -45,6 +45,7 @@
|
|||||||
#include "../common/util.h"
|
#include "../common/util.h"
|
||||||
#include "../common/text.h"
|
#include "../common/text.h"
|
||||||
#include "xtext.h"
|
#include "xtext.h"
|
||||||
|
#include "inline-image.h"
|
||||||
#include "ascii.h"
|
#include "ascii.h"
|
||||||
#include "banlist.h"
|
#include "banlist.h"
|
||||||
#include "chanlist.h"
|
#include "chanlist.h"
|
||||||
@@ -1056,8 +1057,40 @@ open_url_cb (GtkWidget *item, char *url)
|
|||||||
handle_command (current_sess, buf, FALSE);
|
handle_command (current_sess, buf, FALSE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* the text entry the open URL menu was created for; the widget pointer is
|
||||||
|
nulled automatically if the widget dies while the menu is up */
|
||||||
|
static GtkXText *urlmenu_xtext = NULL;
|
||||||
|
static textentry *urlmenu_ent = NULL;
|
||||||
|
|
||||||
|
static void
|
||||||
|
menu_urlmenu_set_entry (GtkWidget *xtext, textentry *ent)
|
||||||
|
{
|
||||||
|
if (urlmenu_xtext)
|
||||||
|
g_object_remove_weak_pointer (G_OBJECT (urlmenu_xtext),
|
||||||
|
(gpointer *) &urlmenu_xtext);
|
||||||
|
urlmenu_xtext = xtext ? GTK_XTEXT (xtext) : NULL;
|
||||||
|
urlmenu_ent = ent;
|
||||||
|
if (urlmenu_xtext)
|
||||||
|
g_object_add_weak_pointer (G_OBJECT (urlmenu_xtext),
|
||||||
|
(gpointer *) &urlmenu_xtext);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
inline_image_menu_cb (GtkWidget *item, char *url)
|
||||||
|
{
|
||||||
|
if (urlmenu_xtext == NULL || urlmenu_ent == NULL)
|
||||||
|
return;
|
||||||
|
|
||||||
|
/* the line may have scrolled out of the buffer since the menu opened */
|
||||||
|
if (!gtk_xtext_buffer_contains (urlmenu_xtext, urlmenu_ent))
|
||||||
|
return;
|
||||||
|
|
||||||
|
inline_image_toggle (current_sess, urlmenu_xtext, urlmenu_ent, url);
|
||||||
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
menu_urlmenu (GdkEventButton *event, char *url)
|
menu_urlmenu_entry (GdkEventButton *event, char *url, GtkWidget *xtext,
|
||||||
|
textentry *ent)
|
||||||
{
|
{
|
||||||
GtkWidget *menu;
|
GtkWidget *menu;
|
||||||
char *tmp, *chop;
|
char *tmp, *chop;
|
||||||
@@ -1065,6 +1098,14 @@ menu_urlmenu (GdkEventButton *event, char *url)
|
|||||||
g_free (str_copy);
|
g_free (str_copy);
|
||||||
str_copy = g_strdup (url);
|
str_copy = g_strdup (url);
|
||||||
|
|
||||||
|
if (xtext == NULL || ent == NULL || !prefs.hex_net_remote_media ||
|
||||||
|
!inline_image_is_image_url (str_copy))
|
||||||
|
{
|
||||||
|
xtext = NULL;
|
||||||
|
ent = NULL;
|
||||||
|
}
|
||||||
|
menu_urlmenu_set_entry (xtext, ent);
|
||||||
|
|
||||||
menu = menu_new ();
|
menu = menu_new ();
|
||||||
/* more than 51 chars? Chop it */
|
/* more than 51 chars? Chop it */
|
||||||
if (g_utf8_strlen (str_copy, -1) >= 52)
|
if (g_utf8_strlen (str_copy, -1) >= 52)
|
||||||
@@ -1087,6 +1128,15 @@ menu_urlmenu (GdkEventButton *event, char *url)
|
|||||||
menu_quick_item_with_callback (open_url_cb, _("Connect"), menu, str_copy);
|
menu_quick_item_with_callback (open_url_cb, _("Connect"), menu, str_copy);
|
||||||
else
|
else
|
||||||
menu_quick_item_with_callback (open_url_cb, _("Open Link in Browser"), menu, str_copy);
|
menu_quick_item_with_callback (open_url_cb, _("Open Link in Browser"), menu, str_copy);
|
||||||
|
if (ent)
|
||||||
|
{
|
||||||
|
const char *shown = gtk_xtext_entry_get_image_url (GTK_XTEXT (xtext), ent);
|
||||||
|
|
||||||
|
menu_quick_item_with_callback (inline_image_menu_cb,
|
||||||
|
shown && strcmp (shown, str_copy) == 0 ?
|
||||||
|
_("Hide Inline Image") : _("Show Image Inline"),
|
||||||
|
menu, str_copy);
|
||||||
|
}
|
||||||
menu_quick_item_with_callback (copy_to_clipboard_cb, _("Copy Selected Link"), menu, str_copy);
|
menu_quick_item_with_callback (copy_to_clipboard_cb, _("Copy Selected Link"), menu, str_copy);
|
||||||
/* custom ones from urlhandlers.conf */
|
/* custom ones from urlhandlers.conf */
|
||||||
menu_create (menu, urlhandler_list, str_copy, TRUE);
|
menu_create (menu, urlhandler_list, str_copy, TRUE);
|
||||||
@@ -1094,6 +1144,12 @@ menu_urlmenu (GdkEventButton *event, char *url)
|
|||||||
menu_popup (menu, event, NULL);
|
menu_popup (menu, event, NULL);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
menu_urlmenu (GdkEventButton *event, char *url)
|
||||||
|
{
|
||||||
|
menu_urlmenu_entry (event, url, NULL, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
menu_chan_cycle (GtkWidget * menu, char *chan)
|
menu_chan_cycle (GtkWidget * menu, char *chan)
|
||||||
{
|
{
|
||||||
@@ -1539,16 +1595,16 @@ menu_join (GtkWidget * wid, gpointer none)
|
|||||||
NULL);
|
NULL);
|
||||||
theme_manager_attach_window (dialog);
|
theme_manager_attach_window (dialog);
|
||||||
content_area = gtk_dialog_get_content_area (GTK_DIALOG (dialog));
|
content_area = gtk_dialog_get_content_area (GTK_DIALOG (dialog));
|
||||||
gtk_box_set_homogeneous (GTK_BOX (content_area), TRUE);
|
|
||||||
gtk_window_set_position (GTK_WINDOW (dialog), GTK_WIN_POS_MOUSE);
|
gtk_window_set_position (GTK_WINDOW (dialog), GTK_WIN_POS_MOUSE);
|
||||||
hbox = gtkutil_box_new (GTK_ORIENTATION_HORIZONTAL, TRUE, 0);
|
hbox = gtkutil_box_new (GTK_ORIENTATION_HORIZONTAL, FALSE, 6);
|
||||||
|
|
||||||
entry = gtk_entry_new ();
|
entry = gtk_entry_new ();
|
||||||
gtk_editable_set_editable (GTK_EDITABLE (entry), FALSE); /* avoid auto-selection */
|
gtk_editable_set_editable (GTK_EDITABLE (entry), FALSE); /* avoid auto-selection */
|
||||||
gtk_entry_set_text (GTK_ENTRY (entry), "#");
|
gtk_entry_set_text (GTK_ENTRY (entry), "#");
|
||||||
g_signal_connect (G_OBJECT (entry), "activate",
|
g_signal_connect (G_OBJECT (entry), "activate",
|
||||||
G_CALLBACK (menu_join_entry_cb), dialog);
|
G_CALLBACK (menu_join_entry_cb), dialog);
|
||||||
gtk_box_pack_end (GTK_BOX (hbox), entry, 0, 0, 0);
|
gtk_widget_set_valign (entry, GTK_ALIGN_CENTER);
|
||||||
|
gtk_box_pack_end (GTK_BOX (hbox), entry, TRUE, TRUE, 0);
|
||||||
|
|
||||||
label = gtk_label_new (_("Enter Channel to Join:"));
|
label = gtk_label_new (_("Enter Channel to Join:"));
|
||||||
gtk_box_pack_end (GTK_BOX (hbox), label, 0, 0, 0);
|
gtk_box_pack_end (GTK_BOX (hbox), label, 0, 0, 0);
|
||||||
@@ -1556,7 +1612,7 @@ menu_join (GtkWidget * wid, gpointer none)
|
|||||||
g_signal_connect (G_OBJECT (dialog), "response",
|
g_signal_connect (G_OBJECT (dialog), "response",
|
||||||
G_CALLBACK (menu_join_cb), entry);
|
G_CALLBACK (menu_join_cb), entry);
|
||||||
|
|
||||||
gtk_container_add (GTK_CONTAINER (content_area), hbox);
|
gtk_box_pack_start (GTK_BOX (content_area), hbox, TRUE, TRUE, 0);
|
||||||
|
|
||||||
gtk_widget_show_all (dialog);
|
gtk_widget_show_all (dialog);
|
||||||
|
|
||||||
@@ -1906,7 +1962,10 @@ menu_about (GtkWidget *wid, gpointer sess)
|
|||||||
gtk_about_dialog_set_logo (dialog, pix_zoitechat);
|
gtk_about_dialog_set_logo (dialog, pix_zoitechat);
|
||||||
gtk_about_dialog_set_copyright (dialog, "\302\251 1998-2010 Peter \305\275elezn\303\275\n\302\251 2009-2014 Berke Viktor\n\302\251 2015-2025 Patrick Griffis\n\302\251 2026 deepend");
|
gtk_about_dialog_set_copyright (dialog, "\302\251 1998-2010 Peter \305\275elezn\303\275\n\302\251 2009-2014 Berke Viktor\n\302\251 2015-2025 Patrick Griffis\n\302\251 2026 deepend");
|
||||||
gtk_about_dialog_set_comments (dialog, comment);
|
gtk_about_dialog_set_comments (dialog, comment);
|
||||||
|
/* Preserve the About dialog's existing action-area button layout. */
|
||||||
|
G_GNUC_BEGIN_IGNORE_DEPRECATIONS
|
||||||
actions = gtk_dialog_get_action_area (GTK_DIALOG (dialog));
|
actions = gtk_dialog_get_action_area (GTK_DIALOG (dialog));
|
||||||
|
G_GNUC_END_IGNORE_DEPRECATIONS
|
||||||
children = gtk_container_get_children (GTK_CONTAINER (actions));
|
children = gtk_container_get_children (GTK_CONTAINER (actions));
|
||||||
for (child = children; child; child = child->next)
|
for (child = children; child; child = child->next)
|
||||||
gtk_widget_destroy (GTK_WIDGET (child->data));
|
gtk_widget_destroy (GTK_WIDGET (child->data));
|
||||||
|
|||||||
@@ -20,8 +20,11 @@
|
|||||||
#ifndef ZOITECHAT_MENU_H
|
#ifndef ZOITECHAT_MENU_H
|
||||||
#define ZOITECHAT_MENU_H
|
#define ZOITECHAT_MENU_H
|
||||||
|
|
||||||
|
#include "xtext.h"
|
||||||
|
|
||||||
GtkWidget *menu_create_main (void *accel_group, int bar, int away, int toplevel, GtkWidget **menu_widgets);
|
GtkWidget *menu_create_main (void *accel_group, int bar, int away, int toplevel, GtkWidget **menu_widgets);
|
||||||
void menu_urlmenu (GdkEventButton * event, char *url);
|
void menu_urlmenu (GdkEventButton * event, char *url);
|
||||||
|
void menu_urlmenu_entry (GdkEventButton *event, char *url, GtkWidget *xtext, textentry *ent);
|
||||||
void menu_chanmenu (session *sess, GdkEventButton * event, char *chan);
|
void menu_chanmenu (session *sess, GdkEventButton * event, char *chan);
|
||||||
void menu_addfavoritemenu (server *serv, GtkWidget *menu, char *channel, gboolean istree);
|
void menu_addfavoritemenu (server *serv, GtkWidget *menu, char *channel, gboolean istree);
|
||||||
void menu_addconnectmenu (server *serv, GtkWidget *menu);
|
void menu_addconnectmenu (server *serv, GtkWidget *menu);
|
||||||
|
|||||||
+53
-3
@@ -18,11 +18,14 @@ zoitechat_gtk_sources = [
|
|||||||
'custom-list.c',
|
'custom-list.c',
|
||||||
'dccgui.c',
|
'dccgui.c',
|
||||||
'editlist.c',
|
'editlist.c',
|
||||||
|
'emoji-picker.c',
|
||||||
|
'emoji-font.c',
|
||||||
'fe-gtk.c',
|
'fe-gtk.c',
|
||||||
'fkeys.c',
|
'fkeys.c',
|
||||||
'gtkutil.c',
|
'gtkutil.c',
|
||||||
'icon-resolver.c',
|
'icon-resolver.c',
|
||||||
'ignoregui.c',
|
'ignoregui.c',
|
||||||
|
'inline-image.c',
|
||||||
'joind.c',
|
'joind.c',
|
||||||
'menu.c',
|
'menu.c',
|
||||||
'maingui.c',
|
'maingui.c',
|
||||||
@@ -50,6 +53,15 @@ zoitechat_gtk_deps = [
|
|||||||
|
|
||||||
gtk_dep = dependency('gtk+-3.0', version: '>= 3.22')
|
gtk_dep = dependency('gtk+-3.0', version: '>= 3.22')
|
||||||
|
|
||||||
|
# Standalone GTK tests also include the common application's headers.
|
||||||
|
gtk_dep = declare_dependency(dependencies: [gtk_dep] + zoitechat_common_header_deps)
|
||||||
|
|
||||||
|
# fe-gtk.h exposes GtkosxApplication to every GTK consumer when enabled.
|
||||||
|
# Theme libraries and standalone UI tests need the same headers as the app.
|
||||||
|
if host_machine.system() == 'darwin' and gtk_mac_dep.found()
|
||||||
|
gtk_dep = declare_dependency(dependencies: [gtk_dep, gtk_mac_dep])
|
||||||
|
endif
|
||||||
|
|
||||||
zoitechat_theme_deps = [gtk_dep, zoitechat_common_dep]
|
zoitechat_theme_deps = [gtk_dep, zoitechat_common_dep]
|
||||||
|
|
||||||
if host_machine.system() != 'windows'
|
if host_machine.system() != 'windows'
|
||||||
@@ -72,7 +84,8 @@ if host_machine.system() != 'windows'
|
|||||||
endif
|
endif
|
||||||
endif
|
endif
|
||||||
|
|
||||||
zoitechat_gtk_deps += gtk_dep
|
emoji_font_deps = [gtk_dep, dependency('pangoft2', version: '>= 1.44'), dependency('fontconfig', version: '>= 2.13')]
|
||||||
|
zoitechat_gtk_deps += emoji_font_deps
|
||||||
|
|
||||||
if host_machine.system() != 'windows'
|
if host_machine.system() != 'windows'
|
||||||
gdk_x11_dep = dependency('gdk-x11-3.0', required: false)
|
gdk_x11_dep = dependency('gdk-x11-3.0', required: false)
|
||||||
@@ -87,12 +100,18 @@ if host_machine.system() != 'windows'
|
|||||||
endif
|
endif
|
||||||
|
|
||||||
zoitechat_gtk_ldflags = []
|
zoitechat_gtk_ldflags = []
|
||||||
|
zoitechat_gtk_objcflags = []
|
||||||
|
|
||||||
if host_machine.system() == 'windows'
|
if host_machine.system() == 'windows'
|
||||||
zoitechat_gtk_sources += 'notifications/notification-windows.c'
|
zoitechat_gtk_sources += 'notifications/notification-windows.c'
|
||||||
zoitechat_gtk_deps += cc.find_library('dwmapi', required: true)
|
zoitechat_gtk_deps += cc.find_library('dwmapi', required: true)
|
||||||
zoitechat_theme_deps += cc.find_library('dwmapi', required: true)
|
zoitechat_theme_deps += cc.find_library('dwmapi', required: true)
|
||||||
|
|
||||||
|
elif host_machine.system() == 'darwin'
|
||||||
|
add_languages('objc', required: true)
|
||||||
|
zoitechat_gtk_sources += 'notifications/notification-osx.m'
|
||||||
|
zoitechat_gtk_deps += dependency('appleframeworks', modules: ['Cocoa', 'UserNotifications'])
|
||||||
|
zoitechat_gtk_objcflags += '-fobjc-arc'
|
||||||
else
|
else
|
||||||
zoitechat_gtk_sources += 'notifications/notification-freedesktop.c'
|
zoitechat_gtk_sources += 'notifications/notification-freedesktop.c'
|
||||||
endif
|
endif
|
||||||
@@ -107,7 +126,7 @@ zoitechat_theme_lib = static_library('zoitechat_theme',
|
|||||||
iso_codes = dependency('iso-codes', required: false)
|
iso_codes = dependency('iso-codes', required: false)
|
||||||
if iso_codes.found()
|
if iso_codes.found()
|
||||||
zoitechat_gtk_sources += 'sexy-iso-codes.c'
|
zoitechat_gtk_sources += 'sexy-iso-codes.c'
|
||||||
iso_codes_prefix = iso_codes.get_pkgconfig_variable('prefix')
|
iso_codes_prefix = iso_codes.get_variable(pkgconfig : 'prefix')
|
||||||
zoitechat_gtk_cflags += '-DISO_CODES_PREFIX="@0@"'.format(iso_codes_prefix)
|
zoitechat_gtk_cflags += '-DISO_CODES_PREFIX="@0@"'.format(iso_codes_prefix)
|
||||||
zoitechat_gtk_cflags += '-DISO_CODES_LOCALEDIR="@0@"'.format(
|
zoitechat_gtk_cflags += '-DISO_CODES_LOCALEDIR="@0@"'.format(
|
||||||
join_paths(iso_codes_prefix, 'share/locale'))
|
join_paths(iso_codes_prefix, 'share/locale'))
|
||||||
@@ -133,10 +152,23 @@ executable('zoitechat',
|
|||||||
dependencies: zoitechat_gtk_deps,
|
dependencies: zoitechat_gtk_deps,
|
||||||
link_with: zoitechat_theme_lib,
|
link_with: zoitechat_theme_lib,
|
||||||
c_args: zoitechat_gtk_cflags,
|
c_args: zoitechat_gtk_cflags,
|
||||||
|
objc_args: zoitechat_gtk_objcflags,
|
||||||
link_args: zoitechat_gtk_ldflags,
|
link_args: zoitechat_gtk_ldflags,
|
||||||
pie: true,
|
pie: true,
|
||||||
install: true,
|
install: true,
|
||||||
gui_app: true,
|
win_subsystem: 'windows',
|
||||||
|
)
|
||||||
|
|
||||||
|
emoji_ui_tests = executable('emoji_ui_tests',
|
||||||
|
['tests/test-emoji-ui.c', 'emoji-font.c', 'emoji-picker.c',
|
||||||
|
'../common/emoji-data.c', emoji_data_table],
|
||||||
|
include_directories: [config_h_include, include_directories('../common')],
|
||||||
|
dependencies: emoji_font_deps,
|
||||||
|
)
|
||||||
|
test('Emoji UI Tests', emoji_ui_tests,
|
||||||
|
env: ['ZOITECHAT_EMOJI_FONT=' + join_paths(meson.source_root(), 'data/fonts/NotoColorEmoji.ttf')],
|
||||||
|
protocol: 'tap',
|
||||||
|
timeout: 120,
|
||||||
)
|
)
|
||||||
|
|
||||||
theme_manager_policy_tests = executable('theme_manager_policy_tests',
|
theme_manager_policy_tests = executable('theme_manager_policy_tests',
|
||||||
@@ -251,3 +283,21 @@ test('Theme Preferences GTK3 Populate Tests', theme_preferences_gtk3_populate_te
|
|||||||
protocol: 'tap',
|
protocol: 'tap',
|
||||||
timeout: 120,
|
timeout: 120,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
emoji_font_tests = executable('emoji_font_tests',
|
||||||
|
['tests/test-emoji-font.c', 'emoji-font.c', '../common/emoji-data.c', emoji_data_table],
|
||||||
|
include_directories: [config_h_include, include_directories('../common')],
|
||||||
|
dependencies: emoji_font_deps,
|
||||||
|
)
|
||||||
|
test('Emoji Font Tests', emoji_font_tests,
|
||||||
|
env: ['ZOITECHAT_EMOJI_FONT=' + join_paths(meson.source_root(), 'data/fonts/NotoColorEmoji.ttf')],
|
||||||
|
protocol: 'tap',
|
||||||
|
timeout: 120,
|
||||||
|
)
|
||||||
|
|
||||||
|
xtext_tests = executable('xtext_tests',
|
||||||
|
['tests/test-xtext.c', '../common/hexcolor.c'],
|
||||||
|
include_directories: [config_h_include],
|
||||||
|
dependencies: [gtk_dep],
|
||||||
|
)
|
||||||
|
test('XText Tests', xtext_tests, protocol: 'tap', timeout: 120)
|
||||||
|
|||||||
@@ -1,54 +1,67 @@
|
|||||||
/* ZoiteChat
|
/* Native macOS notifications; GPL-2.0-or-later, see COPYING. */
|
||||||
* Copyright (C) 2015 Patrick Griffis.
|
|
||||||
*
|
|
||||||
* This program is free software; you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation; either version 2 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program; if not, write to the Free Software
|
|
||||||
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
|
|
||||||
*/
|
|
||||||
|
|
||||||
#import <Cocoa/Cocoa.h>
|
#import <Cocoa/Cocoa.h>
|
||||||
#include <gtkosxapplication.h>
|
#import <UserNotifications/UserNotifications.h>
|
||||||
|
|
||||||
void
|
@interface ZoiteChatNotificationDelegate : NSObject <UNUserNotificationCenterDelegate>
|
||||||
notification_backend_show (const char *title, const char *text)
|
@end
|
||||||
|
@implementation ZoiteChatNotificationDelegate
|
||||||
|
- (void)userNotificationCenter:(UNUserNotificationCenter *)center
|
||||||
|
willPresentNotification:(UNNotification *)notification
|
||||||
|
withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler
|
||||||
{
|
{
|
||||||
NSString *str_title = [[NSString alloc] initWithUTF8String:title];
|
completionHandler(UNNotificationPresentationOptionBanner | UNNotificationPresentationOptionSound);
|
||||||
NSString *str_text = [[NSString alloc] initWithUTF8String:text];
|
}
|
||||||
|
@end
|
||||||
|
|
||||||
NSUserNotification *userNotification = [NSUserNotification new];
|
static ZoiteChatNotificationDelegate *notification_delegate;
|
||||||
userNotification.title = str_title;
|
|
||||||
userNotification.informativeText = str_text;
|
|
||||||
|
|
||||||
NSUserNotificationCenter *center = [NSUserNotificationCenter defaultUserNotificationCenter];
|
int
|
||||||
[center scheduleNotification:userNotification];
|
notification_backend_supported (void)
|
||||||
|
{
|
||||||
[str_title release];
|
return [[NSBundle mainBundle] bundleIdentifier] != nil;
|
||||||
[str_text release];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int
|
int
|
||||||
notification_backend_init (const char **error)
|
notification_backend_init (const char **error)
|
||||||
{
|
{
|
||||||
return 1;
|
if (!notification_backend_supported ())
|
||||||
|
{
|
||||||
|
*error = "macOS notifications require an application bundle";
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
notification_delegate = [ZoiteChatNotificationDelegate new];
|
||||||
|
[UNUserNotificationCenter currentNotificationCenter].delegate = notification_delegate;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
notification_backend_show (const char *title, const char *text)
|
||||||
|
{
|
||||||
|
UNMutableNotificationContent *content = [UNMutableNotificationContent new];
|
||||||
|
content.title = [NSString stringWithUTF8String:title] ?: @"ZoiteChat";
|
||||||
|
content.body = [NSString stringWithUTF8String:text] ?: @"";
|
||||||
|
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
|
||||||
|
/* Ask only when the user first needs a notification, without blocking GTK. */
|
||||||
|
[center requestAuthorizationWithOptions:UNAuthorizationOptionAlert | UNAuthorizationOptionSound
|
||||||
|
completionHandler:^(BOOL granted, NSError *error) {
|
||||||
|
if (granted)
|
||||||
|
{
|
||||||
|
UNNotificationRequest *request = [UNNotificationRequest
|
||||||
|
requestWithIdentifier:[[NSUUID UUID] UUIDString] content:content trigger:nil];
|
||||||
|
[center addNotificationRequest:request withCompletionHandler:^(NSError *deliveryError) {
|
||||||
|
if (deliveryError)
|
||||||
|
NSLog(@"ZoiteChat notification failed: %@", deliveryError);
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
else if (error)
|
||||||
|
NSLog(@"ZoiteChat notification authorization failed: %@", error);
|
||||||
|
}];
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
notification_backend_deinit (void)
|
notification_backend_deinit (void)
|
||||||
{
|
{
|
||||||
}
|
if (notification_delegate)
|
||||||
|
[UNUserNotificationCenter currentNotificationCenter].delegate = nil;
|
||||||
int
|
notification_delegate = nil;
|
||||||
notification_backend_supported (void)
|
|
||||||
{
|
|
||||||
return gtkosx_application_get_bundle_id () != NULL;
|
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-6
@@ -391,34 +391,39 @@ fe_notify_ask (char *nick, char *networks)
|
|||||||
gtk_container_set_border_width (GTK_CONTAINER (table), 12);
|
gtk_container_set_border_width (GTK_CONTAINER (table), 12);
|
||||||
gtk_grid_set_row_spacing (GTK_GRID (table), 3);
|
gtk_grid_set_row_spacing (GTK_GRID (table), 3);
|
||||||
gtk_grid_set_column_spacing (GTK_GRID (table), 8);
|
gtk_grid_set_column_spacing (GTK_GRID (table), 8);
|
||||||
gtk_container_add (GTK_CONTAINER (gtk_dialog_get_content_area (GTK_DIALOG (dialog))), table);
|
gtk_box_pack_start (GTK_BOX (gtk_dialog_get_content_area (GTK_DIALOG (dialog))),
|
||||||
|
table, TRUE, TRUE, 0);
|
||||||
|
|
||||||
label = gtk_label_new (msg);
|
label = gtk_label_new (msg);
|
||||||
gtkutil_grid_attach_defaults (table, label, 0, 1, 0, 1);
|
gtk_label_set_xalign (GTK_LABEL (label), 0.0f);
|
||||||
|
gtkutil_grid_attach (table, label, 0, 1, 0, 1, GTKUTIL_ATTACH_FILL, 0, 0, 0);
|
||||||
|
|
||||||
entry = gtk_entry_new ();
|
entry = gtk_entry_new ();
|
||||||
gtk_entry_set_text (GTK_ENTRY (entry), nick);
|
gtk_entry_set_text (GTK_ENTRY (entry), nick);
|
||||||
g_signal_connect (G_OBJECT (entry), "activate",
|
g_signal_connect (G_OBJECT (entry), "activate",
|
||||||
G_CALLBACK (notifygui_add_enter), dialog);
|
G_CALLBACK (notifygui_add_enter), dialog);
|
||||||
gtkutil_grid_attach_defaults (table, entry, 1, 2, 0, 1);
|
gtkutil_grid_attach (table, entry, 1, 2, 0, 1,
|
||||||
|
GTKUTIL_ATTACH_EXPAND | GTKUTIL_ATTACH_FILL, 0, 0, 0);
|
||||||
|
|
||||||
g_signal_connect (G_OBJECT (dialog), "response",
|
g_signal_connect (G_OBJECT (dialog), "response",
|
||||||
G_CALLBACK (notifygui_add_cb), entry);
|
G_CALLBACK (notifygui_add_cb), entry);
|
||||||
|
|
||||||
label = gtk_label_new (_("Notify on these networks:"));
|
label = gtk_label_new (_("Notify on these networks:"));
|
||||||
gtkutil_grid_attach_defaults (table, label, 0, 1, 2, 3);
|
gtk_label_set_xalign (GTK_LABEL (label), 0.0f);
|
||||||
|
gtkutil_grid_attach (table, label, 0, 1, 2, 3, GTKUTIL_ATTACH_FILL, 0, 0, 0);
|
||||||
|
|
||||||
wid = gtk_entry_new ();
|
wid = gtk_entry_new ();
|
||||||
g_object_set_data (G_OBJECT (entry), "net", wid);
|
g_object_set_data (G_OBJECT (entry), "net", wid);
|
||||||
g_signal_connect (G_OBJECT (wid), "activate",
|
g_signal_connect (G_OBJECT (wid), "activate",
|
||||||
G_CALLBACK (notifygui_add_enter), dialog);
|
G_CALLBACK (notifygui_add_enter), dialog);
|
||||||
gtk_entry_set_text (GTK_ENTRY (wid), networks ? networks : "ALL");
|
gtk_entry_set_text (GTK_ENTRY (wid), networks ? networks : "ALL");
|
||||||
gtkutil_grid_attach_defaults (table, wid, 1, 2, 2, 3);
|
gtkutil_grid_attach (table, wid, 1, 2, 2, 3,
|
||||||
|
GTKUTIL_ATTACH_EXPAND | GTKUTIL_ATTACH_FILL, 0, 0, 0);
|
||||||
|
|
||||||
label = gtk_label_new (NULL);
|
label = gtk_label_new (NULL);
|
||||||
g_snprintf (buf, sizeof (buf), "<i><span size=\"smaller\">%s</span></i>", _("Comma separated list of networks is accepted."));
|
g_snprintf (buf, sizeof (buf), "<i><span size=\"smaller\">%s</span></i>", _("Comma separated list of networks is accepted."));
|
||||||
gtk_label_set_markup (GTK_LABEL (label), buf);
|
gtk_label_set_markup (GTK_LABEL (label), buf);
|
||||||
gtkutil_grid_attach_defaults (table, label, 1, 2, 3, 4);
|
gtkutil_grid_attach (table, label, 1, 2, 3, 4, GTKUTIL_ATTACH_FILL, 0, 0, 0);
|
||||||
|
|
||||||
gtk_widget_show_all (dialog);
|
gtk_widget_show_all (dialog);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2423,6 +2423,8 @@ servlist_open_edit (GtkWidget *parent, ircnet *net)
|
|||||||
GtkWidget *comboboxentry_charset;
|
GtkWidget *comboboxentry_charset;
|
||||||
GtkWidget *combobox_logintypes;
|
GtkWidget *combobox_logintypes;
|
||||||
GtkWidget *hbox1;
|
GtkWidget *hbox1;
|
||||||
|
GtkWidget *content_scroll;
|
||||||
|
GtkWidget *content_box;
|
||||||
GtkWidget *scrolledwindow2;
|
GtkWidget *scrolledwindow2;
|
||||||
GtkWidget *scrolledwindow4;
|
GtkWidget *scrolledwindow4;
|
||||||
GtkWidget *scrolledwindow5;
|
GtkWidget *scrolledwindow5;
|
||||||
@@ -2449,7 +2451,9 @@ servlist_open_edit (GtkWidget *parent, ircnet *net)
|
|||||||
gtk_container_set_border_width (GTK_CONTAINER (editwindow), 4);
|
gtk_container_set_border_width (GTK_CONTAINER (editwindow), 4);
|
||||||
g_snprintf (buf, sizeof (buf), _("Edit %s - %s"), net->name, _(DISPLAY_NAME));
|
g_snprintf (buf, sizeof (buf), _("Edit %s - %s"), net->name, _(DISPLAY_NAME));
|
||||||
gtk_window_set_title (GTK_WINDOW (editwindow), buf);
|
gtk_window_set_title (GTK_WINDOW (editwindow), buf);
|
||||||
gtk_window_set_default_size (GTK_WINDOW (editwindow), netedit_win_width, netedit_win_height);
|
gtk_window_set_default_size (GTK_WINDOW (editwindow),
|
||||||
|
netedit_win_width > 0 ? netedit_win_width : -1,
|
||||||
|
netedit_win_height > 0 ? netedit_win_height : -1);
|
||||||
gtk_window_set_transient_for (GTK_WINDOW (editwindow), GTK_WINDOW (parent));
|
gtk_window_set_transient_for (GTK_WINDOW (editwindow), GTK_WINDOW (parent));
|
||||||
gtk_window_set_modal (GTK_WINDOW (editwindow), TRUE);
|
gtk_window_set_modal (GTK_WINDOW (editwindow), TRUE);
|
||||||
gtk_window_set_type_hint (GTK_WINDOW (editwindow), GDK_WINDOW_TYPE_HINT_DIALOG);
|
gtk_window_set_type_hint (GTK_WINDOW (editwindow), GDK_WINDOW_TYPE_HINT_DIALOG);
|
||||||
@@ -2458,10 +2462,20 @@ servlist_open_edit (GtkWidget *parent, ircnet *net)
|
|||||||
vbox5 = gtkutil_box_new (GTK_ORIENTATION_VERTICAL, FALSE, 0);
|
vbox5 = gtkutil_box_new (GTK_ORIENTATION_VERTICAL, FALSE, 0);
|
||||||
gtk_container_add (GTK_CONTAINER (editwindow), vbox5);
|
gtk_container_add (GTK_CONTAINER (editwindow), vbox5);
|
||||||
|
|
||||||
|
/* Keep Close accessible on small screens, while the lists still receive
|
||||||
|
all spare space when the complete form fits in the window. */
|
||||||
|
content_scroll = gtk_scrolled_window_new (NULL, NULL);
|
||||||
|
gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (content_scroll),
|
||||||
|
GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
|
||||||
|
gtk_scrolled_window_set_propagate_natural_width (GTK_SCROLLED_WINDOW (content_scroll), TRUE);
|
||||||
|
gtk_scrolled_window_set_propagate_natural_height (GTK_SCROLLED_WINDOW (content_scroll), TRUE);
|
||||||
|
gtk_box_pack_start (GTK_BOX (vbox5), content_scroll, TRUE, TRUE, 0);
|
||||||
|
content_box = gtkutil_box_new (GTK_ORIENTATION_VERTICAL, FALSE, 0);
|
||||||
|
gtk_container_add (GTK_CONTAINER (content_scroll), content_box);
|
||||||
|
|
||||||
/* Tabs and buttons */
|
/* Tabs and buttons */
|
||||||
hbox1 = gtkutil_box_new (GTK_ORIENTATION_HORIZONTAL, FALSE, 0);
|
hbox1 = gtkutil_box_new (GTK_ORIENTATION_HORIZONTAL, FALSE, 0);
|
||||||
gtk_box_pack_start (GTK_BOX (vbox5), hbox1, TRUE, TRUE, 4);
|
gtk_box_pack_start (GTK_BOX (content_box), hbox1, TRUE, TRUE, 4);
|
||||||
|
|
||||||
scrolledwindow2 = gtk_scrolled_window_new (NULL, NULL);
|
scrolledwindow2 = gtk_scrolled_window_new (NULL, NULL);
|
||||||
scrolledwindow4 = gtk_scrolled_window_new (NULL, NULL);
|
scrolledwindow4 = gtk_scrolled_window_new (NULL, NULL);
|
||||||
@@ -2599,7 +2613,7 @@ servlist_open_edit (GtkWidget *parent, ircnet *net)
|
|||||||
|
|
||||||
/* Checkboxes and entries */
|
/* Checkboxes and entries */
|
||||||
table3 = gtkutil_grid_new (19, 2, FALSE);
|
table3 = gtkutil_grid_new (19, 2, FALSE);
|
||||||
gtk_box_pack_start (GTK_BOX (vbox5), table3, FALSE, FALSE, 0);
|
gtk_box_pack_start (GTK_BOX (content_box), table3, FALSE, FALSE, 0);
|
||||||
gtk_grid_set_row_spacing (GTK_GRID (table3), 2);
|
gtk_grid_set_row_spacing (GTK_GRID (table3), 2);
|
||||||
gtk_grid_set_column_spacing (GTK_GRID (table3), 8);
|
gtk_grid_set_column_spacing (GTK_GRID (table3), 8);
|
||||||
|
|
||||||
@@ -2828,7 +2842,9 @@ servlist_open_networks (void)
|
|||||||
gtk_container_set_border_width (GTK_CONTAINER (servlist), 4);
|
gtk_container_set_border_width (GTK_CONTAINER (servlist), 4);
|
||||||
g_snprintf(buf, sizeof(buf), _("Network List - %s"), _(DISPLAY_NAME));
|
g_snprintf(buf, sizeof(buf), _("Network List - %s"), _(DISPLAY_NAME));
|
||||||
gtk_window_set_title (GTK_WINDOW (servlist), buf);
|
gtk_window_set_title (GTK_WINDOW (servlist), buf);
|
||||||
gtk_window_set_default_size (GTK_WINDOW (servlist), netlist_win_width, netlist_win_height);
|
gtk_window_set_default_size (GTK_WINDOW (servlist),
|
||||||
|
netlist_win_width > 0 ? netlist_win_width : -1,
|
||||||
|
netlist_win_height > 0 ? netlist_win_height : -1);
|
||||||
gtk_window_set_role (GTK_WINDOW (servlist), "servlist");
|
gtk_window_set_role (GTK_WINDOW (servlist), "servlist");
|
||||||
gtk_window_set_type_hint (GTK_WINDOW (servlist), GDK_WINDOW_TYPE_HINT_DIALOG);
|
gtk_window_set_type_hint (GTK_WINDOW (servlist), GDK_WINDOW_TYPE_HINT_DIALOG);
|
||||||
if (current_sess)
|
if (current_sess)
|
||||||
|
|||||||
+63
-8
@@ -59,6 +59,8 @@ static struct zoitechatprefs setup_prefs;
|
|||||||
static GtkWidget *cancel_button;
|
static GtkWidget *cancel_button;
|
||||||
static GtkWidget *font_dialog = NULL;
|
static GtkWidget *font_dialog = NULL;
|
||||||
static GtkWidget *setup_topicbar_multiline_toggle = NULL;
|
static GtkWidget *setup_topicbar_multiline_toggle = NULL;
|
||||||
|
static GtkWidget *setup_wrap_align_label = NULL;
|
||||||
|
static GtkWidget *setup_wrap_align_combo = NULL;
|
||||||
|
|
||||||
enum
|
enum
|
||||||
{
|
{
|
||||||
@@ -157,6 +159,14 @@ static const char *const langsmenu[] =
|
|||||||
};
|
};
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
static const char *const wrapalignmenu[] =
|
||||||
|
{
|
||||||
|
N_("Start of line (default)"),
|
||||||
|
N_("Prefix (nick name)"),
|
||||||
|
N_("Message"),
|
||||||
|
NULL
|
||||||
|
};
|
||||||
|
|
||||||
static const setting appearance_settings[] =
|
static const setting appearance_settings[] =
|
||||||
{
|
{
|
||||||
{ST_HEADER, N_("General"),0,0,0},
|
{ST_HEADER, N_("General"),0,0,0},
|
||||||
@@ -172,6 +182,7 @@ static const setting appearance_settings[] =
|
|||||||
{ST_TOGGLR, N_("Indent nick names"), P_OFFINTNL(hex_text_indent), N_("Make nick names right-justified"),0,0},
|
{ST_TOGGLR, N_("Indent nick names"), P_OFFINTNL(hex_text_indent), N_("Make nick names right-justified"),0,0},
|
||||||
{ST_TOGGLE, N_ ("Show marker line"), P_OFFINTNL (hex_text_show_marker), N_ ("Insert a red line after the last read text."), 0, 0},
|
{ST_TOGGLE, N_ ("Show marker line"), P_OFFINTNL (hex_text_show_marker), N_ ("Insert a red line after the last read text."), 0, 0},
|
||||||
{ST_TOGGLE, N_("Keep search position when closing"), P_OFFINTNL(hex_text_search_keep_position), 0, 0, 0},
|
{ST_TOGGLE, N_("Keep search position when closing"), P_OFFINTNL(hex_text_search_keep_position), 0, 0, 0},
|
||||||
|
{ST_MENU, N_("Align wrapped lines under:"), P_OFFINTNL(hex_text_wrap_align), N_("Where wrapped lines start when nick names are not indented"), wrapalignmenu, 0},
|
||||||
|
|
||||||
{ST_HEADER, N_("Timestamps"),0,0,0},
|
{ST_HEADER, N_("Timestamps"),0,0,0},
|
||||||
{ST_TOGGLE, N_("Enable timestamps"), P_OFFINTNL(hex_stamp_text),0,0,1},
|
{ST_TOGGLE, N_("Enable timestamps"), P_OFFINTNL(hex_stamp_text),0,0,1},
|
||||||
@@ -340,6 +351,9 @@ static const setting tabs_settings[] =
|
|||||||
{ST_TOGGLE, N_("Show close button on tabs"), P_OFFINTNL(hex_gui_tab_closebuttons), 0, 0, 0},
|
{ST_TOGGLE, N_("Show close button on tabs"), P_OFFINTNL(hex_gui_tab_closebuttons), 0, 0, 0},
|
||||||
{ST_TOGGLE, N_("Middle click to close tab"), P_OFFINTNL(hex_gui_tab_middleclose), 0, 0, 0},
|
{ST_TOGGLE, N_("Middle click to close tab"), P_OFFINTNL(hex_gui_tab_middleclose), 0, 0, 0},
|
||||||
{ST_TOGGLE, N_("Smaller text"), P_OFFINTNL(hex_gui_tab_small), 0, 0, 0},
|
{ST_TOGGLE, N_("Smaller text"), P_OFFINTNL(hex_gui_tab_small), 0, 0, 0},
|
||||||
|
{ST_EFONT, N_("Tab font:"), P_OFFSETNL(hex_gui_tab_font),
|
||||||
|
N_("Font for server and channel tabs in the Tabs layout. Leave blank to use the theme font. Smaller text still applies."),
|
||||||
|
0, sizeof prefs.hex_gui_tab_font},
|
||||||
{ST_MENU, N_("Focus new tabs:"), P_OFFINTNL(hex_gui_tab_newtofront), 0, focusnewtabsmenu, 0},
|
{ST_MENU, N_("Focus new tabs:"), P_OFFINTNL(hex_gui_tab_newtofront), 0, focusnewtabsmenu, 0},
|
||||||
{ST_MENU, N_("Placement of notices:"), P_OFFINTNL(hex_irc_notice_pos), 0, noticeposmenu, 0},
|
{ST_MENU, N_("Placement of notices:"), P_OFFINTNL(hex_irc_notice_pos), 0, noticeposmenu, 0},
|
||||||
{ST_MENU, N_("Show channel switcher at:"), P_OFFINTNL(hex_gui_tab_pos), 0, cspos, 1},
|
{ST_MENU, N_("Show channel switcher at:"), P_OFFINTNL(hex_gui_tab_pos), 0, cspos, 1},
|
||||||
@@ -674,6 +688,11 @@ static const setting network_settings[] =
|
|||||||
{ST_ENTRY, N_("Username:"), P_OFFSETNL(hex_net_proxy_user), 0, 0, sizeof prefs.hex_net_proxy_user},
|
{ST_ENTRY, N_("Username:"), P_OFFSETNL(hex_net_proxy_user), 0, 0, sizeof prefs.hex_net_proxy_user},
|
||||||
{ST_ENTRY, N_("Password:"), P_OFFSETNL(hex_net_proxy_pass), 0, GINT_TO_POINTER(1), sizeof prefs.hex_net_proxy_pass},
|
{ST_ENTRY, N_("Password:"), P_OFFSETNL(hex_net_proxy_pass), 0, GINT_TO_POINTER(1), sizeof prefs.hex_net_proxy_pass},
|
||||||
|
|
||||||
|
{ST_HEADER, N_("Remote Media"), 0, 0, 0, 0},
|
||||||
|
{ST_TOGGLE, N_("Allow loading remote media (inline images) when clicked"),
|
||||||
|
P_OFFINTNL(hex_net_remote_media),
|
||||||
|
N_("Loading remote media contacts the site hosting it, which can reveal your IP address and that you viewed the message. You are asked for confirmation before each new domain is contacted."), 0, 0},
|
||||||
|
|
||||||
{ST_END, 0, 0, 0, 0, 0}
|
{ST_END, 0, 0, 0, 0, 0}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -849,6 +868,20 @@ setup_topicbar_inline_toggled_cb (GtkToggleButton *but, gpointer userdata)
|
|||||||
!gtk_toggle_button_get_active (but));
|
!gtk_toggle_button_get_active (but));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
setup_text_indent_toggled_cb (GtkToggleButton *but, gpointer userdata)
|
||||||
|
{
|
||||||
|
gboolean sens = !gtk_toggle_button_get_active (but);
|
||||||
|
|
||||||
|
(void) userdata;
|
||||||
|
|
||||||
|
/* wrapped line alignment only applies without nick indentation */
|
||||||
|
if (setup_wrap_align_label)
|
||||||
|
gtk_widget_set_sensitive (setup_wrap_align_label, sens);
|
||||||
|
if (setup_wrap_align_combo)
|
||||||
|
gtk_widget_set_sensitive (setup_wrap_align_combo, sens);
|
||||||
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
setup_create_toggleR (GtkWidget *tab, int row, const setting *set)
|
setup_create_toggleR (GtkWidget *tab, int row, const setting *set)
|
||||||
{
|
{
|
||||||
@@ -859,6 +892,9 @@ setup_create_toggleR (GtkWidget *tab, int row, const setting *set)
|
|||||||
setup_get_int (&setup_prefs, set));
|
setup_get_int (&setup_prefs, set));
|
||||||
g_signal_connect (G_OBJECT (wid), "toggled",
|
g_signal_connect (G_OBJECT (wid), "toggled",
|
||||||
G_CALLBACK (setup_toggle_cb), (gpointer)set);
|
G_CALLBACK (setup_toggle_cb), (gpointer)set);
|
||||||
|
if (set->offset == STRUCT_OFFSET_INT (struct zoitechatprefs, hex_text_indent))
|
||||||
|
g_signal_connect (G_OBJECT (wid), "toggled",
|
||||||
|
G_CALLBACK (setup_text_indent_toggled_cb), NULL);
|
||||||
if (set->tooltip)
|
if (set->tooltip)
|
||||||
gtk_widget_set_tooltip_text (wid, _(set->tooltip));
|
gtk_widget_set_tooltip_text (wid, _(set->tooltip));
|
||||||
setup_table_attach (tab, wid, 4, 5, row, row + 1, TRUE, FALSE,
|
setup_table_attach (tab, wid, 4, 5, row, row + 1, TRUE, FALSE,
|
||||||
@@ -1248,7 +1284,7 @@ setup_create_id_menu (GtkWidget *table, char *label, int row, char *dest)
|
|||||||
static void
|
static void
|
||||||
setup_create_menu (GtkWidget *table, int row, const setting *set)
|
setup_create_menu (GtkWidget *table, int row, const setting *set)
|
||||||
{
|
{
|
||||||
GtkWidget *wid, *cbox, *box;
|
GtkWidget *wid, *cbox;
|
||||||
const char **text = (const char **)set->list;
|
const char **text = (const char **)set->list;
|
||||||
int i;
|
int i;
|
||||||
|
|
||||||
@@ -1269,10 +1305,18 @@ setup_create_menu (GtkWidget *table, int row, const setting *set)
|
|||||||
g_signal_connect (G_OBJECT (cbox), "changed",
|
g_signal_connect (G_OBJECT (cbox), "changed",
|
||||||
G_CALLBACK (setup_menu_cb), (gpointer)set);
|
G_CALLBACK (setup_menu_cb), (gpointer)set);
|
||||||
|
|
||||||
box = gtkutil_box_new (GTK_ORIENTATION_HORIZONTAL, FALSE, 0);
|
setup_table_attach (table, cbox, 3, 6, row, row + 1, TRUE, FALSE,
|
||||||
gtk_box_pack_start (GTK_BOX (box), cbox, 0, 0, 0);
|
|
||||||
setup_table_attach (table, box, 3, 4, row, row + 1, TRUE, FALSE,
|
|
||||||
SETUP_ALIGN_FILL, SETUP_ALIGN_FILL, 0, 0);
|
SETUP_ALIGN_FILL, SETUP_ALIGN_FILL, 0, 0);
|
||||||
|
|
||||||
|
if (set->offset == STRUCT_OFFSET_INT (struct zoitechatprefs, hex_text_wrap_align))
|
||||||
|
{
|
||||||
|
setup_wrap_align_label = wid;
|
||||||
|
setup_wrap_align_combo = cbox;
|
||||||
|
g_object_add_weak_pointer (G_OBJECT (wid), (gpointer *)&setup_wrap_align_label);
|
||||||
|
g_object_add_weak_pointer (G_OBJECT (cbox), (gpointer *)&setup_wrap_align_combo);
|
||||||
|
gtk_widget_set_sensitive (wid, !setup_prefs.hex_text_indent);
|
||||||
|
gtk_widget_set_sensitive (cbox, !setup_prefs.hex_text_indent);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
@@ -1955,7 +1999,8 @@ setup_add_page (const char *title, GtkWidget *book, GtkWidget *tab)
|
|||||||
|
|
||||||
sw = GTK_SCROLLED_WINDOW(gtk_scrolled_window_new (NULL, NULL));
|
sw = GTK_SCROLLED_WINDOW(gtk_scrolled_window_new (NULL, NULL));
|
||||||
gtk_scrolled_window_set_shadow_type (sw, GTK_SHADOW_IN);
|
gtk_scrolled_window_set_shadow_type (sw, GTK_SHADOW_IN);
|
||||||
gtk_scrolled_window_set_policy (sw, GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
|
gtk_scrolled_window_set_policy (sw, GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
|
||||||
|
gtk_scrolled_window_set_propagate_natural_width (sw, TRUE);
|
||||||
gtk_container_add (GTK_CONTAINER (sw), vvbox);
|
gtk_container_add (GTK_CONTAINER (sw), vvbox);
|
||||||
|
|
||||||
viewport = gtk_bin_get_child (GTK_BIN(sw));
|
viewport = gtk_bin_get_child (GTK_BIN(sw));
|
||||||
@@ -1999,7 +2044,9 @@ setup_ensure_page_created (int page)
|
|||||||
}
|
}
|
||||||
|
|
||||||
page_widget = setup_page_factories[page] (setup_page_settings[page]);
|
page_widget = setup_page_factories[page] (setup_page_settings[page]);
|
||||||
gtk_container_add (GTK_CONTAINER (container), page_widget);
|
/* Lazy pages must pass the notebook's spare space to their content,
|
||||||
|
particularly the independently scrolling Sounds list. */
|
||||||
|
gtk_box_pack_start (GTK_BOX (container), page_widget, TRUE, TRUE, 0);
|
||||||
gtk_widget_show_all (container);
|
gtk_widget_show_all (container);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2177,9 +2224,14 @@ setup_create_tree (GtkWidget *box, GtkWidget *book)
|
|||||||
-1, _("Categories"), renderer, "text", 0, NULL);
|
-1, _("Categories"), renderer, "text", 0, NULL);
|
||||||
gtk_tree_view_expand_all (GTK_TREE_VIEW (tree));
|
gtk_tree_view_expand_all (GTK_TREE_VIEW (tree));
|
||||||
|
|
||||||
frame = gtk_frame_new (NULL);
|
frame = gtk_scrolled_window_new (NULL, NULL);
|
||||||
|
gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (frame),
|
||||||
|
GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
|
||||||
|
gtk_scrolled_window_set_shadow_type (GTK_SCROLLED_WINDOW (frame), GTK_SHADOW_IN);
|
||||||
|
gtk_scrolled_window_set_propagate_natural_width (GTK_SCROLLED_WINDOW (frame), TRUE);
|
||||||
|
gtk_widget_set_vexpand (frame, TRUE);
|
||||||
gtk_container_add (GTK_CONTAINER (frame), tree);
|
gtk_container_add (GTK_CONTAINER (frame), tree);
|
||||||
gtk_box_pack_start (GTK_BOX (box), frame, 0, 0, 0);
|
gtk_box_pack_start (GTK_BOX (box), frame, FALSE, TRUE, 0);
|
||||||
gtk_box_reorder_child (GTK_BOX (box), frame, 0);
|
gtk_box_reorder_child (GTK_BOX (box), frame, 0);
|
||||||
|
|
||||||
if (sel_iter)
|
if (sel_iter)
|
||||||
@@ -2330,6 +2382,9 @@ setup_apply (struct zoitechatprefs *pr)
|
|||||||
if (DIFF (hex_gui_tab_icons) || DIFF (hex_gui_tab_closebuttons) ||
|
if (DIFF (hex_gui_tab_icons) || DIFF (hex_gui_tab_closebuttons) ||
|
||||||
DIFF (hex_gui_tab_small))
|
DIFF (hex_gui_tab_small))
|
||||||
live_changes.chanview = TRUE;
|
live_changes.chanview = TRUE;
|
||||||
|
if (pr->hex_gui_tab_layout == 0 &&
|
||||||
|
strcmp (pr->hex_gui_tab_font, prefs.hex_gui_tab_font) != 0)
|
||||||
|
live_changes.chanview = TRUE;
|
||||||
if (DIFF (hex_gui_tab_sort))
|
if (DIFF (hex_gui_tab_sort))
|
||||||
live_changes.tab_resort = TRUE;
|
live_changes.tab_resort = TRUE;
|
||||||
if (DIFF (hex_gui_tab_trunc))
|
if (DIFF (hex_gui_tab_trunc))
|
||||||
|
|||||||
@@ -215,7 +215,22 @@ initialize_enchant (void)
|
|||||||
#endif
|
#endif
|
||||||
};
|
};
|
||||||
|
|
||||||
for (i = 0; i < G_N_ELEMENTS(libnames); ++i)
|
#ifdef __APPLE__
|
||||||
|
const char *bundled_enchant = g_getenv ("ZOITECHAT_ENCHANT_LIBRARY");
|
||||||
|
if (bundled_enchant && *bundled_enchant)
|
||||||
|
{
|
||||||
|
enchant = g_module_open (bundled_enchant, 0);
|
||||||
|
if (enchant)
|
||||||
|
{
|
||||||
|
void (*set_prefix) (const char *) = NULL;
|
||||||
|
const char *prefix = g_getenv ("ZOITECHAT_ENCHANT_PREFIX");
|
||||||
|
if (prefix && g_module_symbol (enchant, "enchant_set_prefix_dir", (gpointer *) &set_prefix))
|
||||||
|
set_prefix (prefix);
|
||||||
|
have_enchant = TRUE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
for (i = 0; !have_enchant && i < G_N_ELEMENTS(libnames); ++i)
|
||||||
{
|
{
|
||||||
enchant = g_module_open(libnames[i], 0);
|
enchant = g_module_open(libnames[i], 0);
|
||||||
if (enchant)
|
if (enchant)
|
||||||
@@ -1266,8 +1281,6 @@ attr_list_has_attrs (PangoAttrList *attrs)
|
|||||||
static void
|
static void
|
||||||
sexy_spell_entry_recheck_all(SexySpellEntry *entry)
|
sexy_spell_entry_recheck_all(SexySpellEntry *entry)
|
||||||
{
|
{
|
||||||
GdkRectangle rect;
|
|
||||||
GtkAllocation allocation;
|
|
||||||
GtkWidget *widget = GTK_WIDGET(entry);
|
GtkWidget *widget = GTK_WIDGET(entry);
|
||||||
int length, i, text_len;
|
int length, i, text_len;
|
||||||
const char *text;
|
const char *text;
|
||||||
@@ -1299,15 +1312,8 @@ sexy_spell_entry_recheck_all(SexySpellEntry *entry)
|
|||||||
|
|
||||||
gtk_entry_set_attributes (GTK_ENTRY (entry), attr_list_has_attrs (entry->priv->attr_list) ? entry->priv->attr_list : NULL);
|
gtk_entry_set_attributes (GTK_ENTRY (entry), attr_list_has_attrs (entry->priv->attr_list) ? entry->priv->attr_list : NULL);
|
||||||
|
|
||||||
if (gtk_widget_get_realized (GTK_WIDGET(entry)))
|
/* GtkEntry shares its parent's GdkWindow in GTK3; redraw the widget area. */
|
||||||
{
|
gtk_widget_queue_draw (widget);
|
||||||
gtk_widget_get_allocation (GTK_WIDGET(entry), &allocation);
|
|
||||||
|
|
||||||
rect.x = 0; rect.y = 0;
|
|
||||||
rect.width = allocation.width;
|
|
||||||
rect.height = allocation.height;
|
|
||||||
gdk_window_invalidate_rect(gtk_widget_get_window (widget), &rect, TRUE);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static gboolean
|
static gboolean
|
||||||
|
|||||||
@@ -0,0 +1,174 @@
|
|||||||
|
/* Shared headless font coverage and ordinary text regression tests.
|
||||||
|
* Distributed under GPL-2.0-or-later; see COPYING. */
|
||||||
|
#include "../emoji-font.h"
|
||||||
|
#include "../../common/emoji-data.h"
|
||||||
|
#include <pango/pangofc-fontmap.h>
|
||||||
|
#include <pango/pangofc-font.h>
|
||||||
|
|
||||||
|
static GHashTable *font_checksums;
|
||||||
|
static char *bundled_checksum;
|
||||||
|
|
||||||
|
static const char *
|
||||||
|
font_checksum (const char *path)
|
||||||
|
{
|
||||||
|
const char *cached = g_hash_table_lookup (font_checksums, path);
|
||||||
|
char *contents, *checksum;
|
||||||
|
gsize length;
|
||||||
|
|
||||||
|
if (cached)
|
||||||
|
return cached;
|
||||||
|
g_assert_true (g_file_get_contents (path, &contents, &length, NULL));
|
||||||
|
checksum = g_compute_checksum_for_data (G_CHECKSUM_SHA256,
|
||||||
|
(const guchar *) contents, length);
|
||||||
|
g_free (contents);
|
||||||
|
g_hash_table_insert (font_checksums, g_strdup (path), checksum);
|
||||||
|
return checksum;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
assert_bundled_font (PangoFont *font, gboolean expected)
|
||||||
|
{
|
||||||
|
FcPattern *pattern;
|
||||||
|
FcChar8 *path;
|
||||||
|
const char *checksum;
|
||||||
|
|
||||||
|
/* Pango can describe a face by its embedded name rather than its
|
||||||
|
* Fontconfig alias. Verify the resolved file's bytes, not that name.
|
||||||
|
* Cache by filename so catalog coverage does not reread the font for
|
||||||
|
* every glyph. An older system Noto must still fail this assertion. */
|
||||||
|
g_assert_true (PANGO_IS_FC_FONT (font));
|
||||||
|
#if PANGO_VERSION_CHECK(1, 48, 0)
|
||||||
|
pattern = pango_fc_font_get_pattern (PANGO_FC_FONT (font));
|
||||||
|
#else
|
||||||
|
pattern = PANGO_FC_FONT (font)->font_pattern;
|
||||||
|
#endif
|
||||||
|
g_assert_cmpint (FcPatternGetString (pattern, FC_FILE, 0, &path), ==, FcResultMatch);
|
||||||
|
checksum = font_checksum ((const char *) path);
|
||||||
|
if (expected)
|
||||||
|
g_assert_cmpstr (checksum, ==, bundled_checksum);
|
||||||
|
else
|
||||||
|
g_assert_cmpstr (checksum, !=, bundled_checksum);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
test_font_catalog (void)
|
||||||
|
{
|
||||||
|
PangoFontMap *map = emoji_font_get_map ();
|
||||||
|
PangoContext *context;
|
||||||
|
PangoLayout *layout;
|
||||||
|
PangoFontDescription *desc;
|
||||||
|
gsize i;
|
||||||
|
int tone;
|
||||||
|
|
||||||
|
g_assert_nonnull (map);
|
||||||
|
context = pango_font_map_create_context (map);
|
||||||
|
g_assert_true (PANGO_IS_FC_FONT_MAP (map));
|
||||||
|
layout = pango_layout_new (context);
|
||||||
|
desc = pango_font_description_from_string (ZOITECHAT_EMOJI_FAMILY " 15");
|
||||||
|
pango_layout_set_font_description (layout, desc);
|
||||||
|
for (i = 0; i < emoji_data_count (); i++)
|
||||||
|
{
|
||||||
|
const EmojiEntry *entry = emoji_data_entry (i);
|
||||||
|
for (tone = 0; tone < (entry->tone_set < 0 ? 1 : EMOJI_TONE_COUNT); tone++)
|
||||||
|
{
|
||||||
|
PangoLayoutIter *iter;
|
||||||
|
pango_layout_set_text (layout, emoji_entry_sequence_for_tone (entry, tone), -1);
|
||||||
|
g_assert_cmpint (pango_layout_get_unknown_glyphs_count (layout), ==, 0);
|
||||||
|
iter = pango_layout_get_iter (layout);
|
||||||
|
do
|
||||||
|
{
|
||||||
|
PangoLayoutRun *run = pango_layout_iter_get_run_readonly (iter);
|
||||||
|
if (run)
|
||||||
|
{
|
||||||
|
assert_bundled_font (run->item->analysis.font, TRUE);
|
||||||
|
}
|
||||||
|
} while (pango_layout_iter_next_run (iter));
|
||||||
|
pango_layout_iter_free (iter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pango_font_description_free (desc);
|
||||||
|
g_object_unref (layout);
|
||||||
|
g_object_unref (context);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
test_plain_text_font (void)
|
||||||
|
{
|
||||||
|
PangoFontMap *map = emoji_font_get_map ();
|
||||||
|
PangoContext *context;
|
||||||
|
PangoLayout *layout;
|
||||||
|
PangoLayoutIter *iter;
|
||||||
|
PangoFontDescription *desc;
|
||||||
|
|
||||||
|
g_assert_nonnull (map);
|
||||||
|
context = pango_font_map_create_context (map);
|
||||||
|
layout = pango_layout_new (context);
|
||||||
|
pango_layout_set_text (layout, "/join #channel 0123456789 *", -1);
|
||||||
|
desc = pango_font_description_from_string ("Monospace 12");
|
||||||
|
pango_layout_set_font_description (layout, desc);
|
||||||
|
iter = pango_layout_get_iter (layout);
|
||||||
|
do
|
||||||
|
{
|
||||||
|
PangoLayoutRun *run = pango_layout_iter_get_run_readonly (iter);
|
||||||
|
if (run)
|
||||||
|
{
|
||||||
|
assert_bundled_font (run->item->analysis.font, FALSE);
|
||||||
|
}
|
||||||
|
} while (pango_layout_iter_next_run (iter));
|
||||||
|
pango_layout_iter_free (iter);
|
||||||
|
pango_font_description_free (desc);
|
||||||
|
g_object_unref (layout);
|
||||||
|
g_object_unref (context);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
test_implicit_emoji_font (void)
|
||||||
|
{
|
||||||
|
PangoFontMap *map = emoji_font_get_map ();
|
||||||
|
PangoContext *context = pango_font_map_create_context (map);
|
||||||
|
PangoLayout *layout = pango_layout_new (context);
|
||||||
|
PangoFontDescription *desc = pango_font_description_from_string ("Monospace 12");
|
||||||
|
const char *samples[] = { "\360\237\230\200", "#\357\270\217\342\203\243", "\360\237\253\252" };
|
||||||
|
gsize i;
|
||||||
|
|
||||||
|
pango_layout_set_font_description (layout, desc);
|
||||||
|
for (i = 0; i < G_N_ELEMENTS (samples); i++)
|
||||||
|
{
|
||||||
|
PangoLayoutIter *iter;
|
||||||
|
pango_layout_set_text (layout, samples[i], -1);
|
||||||
|
g_assert_cmpint (pango_layout_get_unknown_glyphs_count (layout), ==, 0);
|
||||||
|
iter = pango_layout_get_iter (layout);
|
||||||
|
do
|
||||||
|
{
|
||||||
|
PangoLayoutRun *run = pango_layout_iter_get_run_readonly (iter);
|
||||||
|
if (run)
|
||||||
|
{
|
||||||
|
assert_bundled_font (run->item->analysis.font, TRUE);
|
||||||
|
}
|
||||||
|
} while (pango_layout_iter_next_run (iter));
|
||||||
|
pango_layout_iter_free (iter);
|
||||||
|
}
|
||||||
|
pango_font_description_free (desc);
|
||||||
|
g_object_unref (layout);
|
||||||
|
g_object_unref (context);
|
||||||
|
}
|
||||||
|
|
||||||
|
int
|
||||||
|
main (int argc, char **argv)
|
||||||
|
{
|
||||||
|
const char *path;
|
||||||
|
int result;
|
||||||
|
|
||||||
|
g_test_init (&argc, &argv, NULL);
|
||||||
|
path = g_getenv ("ZOITECHAT_EMOJI_FONT");
|
||||||
|
g_assert_nonnull (path);
|
||||||
|
font_checksums = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
|
||||||
|
bundled_checksum = g_strdup (font_checksum (path));
|
||||||
|
g_test_add_func ("/emoji-font/catalog", test_font_catalog);
|
||||||
|
g_test_add_func ("/emoji-font/plain-text", test_plain_text_font);
|
||||||
|
g_test_add_func ("/emoji-font/implicit-emoji", test_implicit_emoji_font);
|
||||||
|
result = g_test_run ();
|
||||||
|
g_free (bundled_checksum);
|
||||||
|
g_hash_table_destroy (font_checksums);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user