diff --git a/.changeset/auth-sso-addaccount.md b/.changeset/auth-sso-addaccount.md new file mode 100644 index 0000000000..c2b2ece77a --- /dev/null +++ b/.changeset/auth-sso-addaccount.md @@ -0,0 +1,5 @@ +--- +default: patch +--- + +Preserve addAccount parameter through SSO redirect. diff --git a/.changeset/developer-tools.md b/.changeset/developer-tools.md new file mode 100644 index 0000000000..b2bcf52296 --- /dev/null +++ b/.changeset/developer-tools.md @@ -0,0 +1,5 @@ +--- +default: minor +--- + +Add build-time experiment flag injection, typed deterministic bucketing helpers, and a DevTools panel to force-rotate Megolm encryption sessions per room. diff --git a/.changeset/dm-list-group-avatars.md b/.changeset/dm-list-group-avatars.md new file mode 100644 index 0000000000..8eff7f5c3c --- /dev/null +++ b/.changeset/dm-list-group-avatars.md @@ -0,0 +1,5 @@ +--- +default: minor +--- + +Show group DM composite avatars in the sidebar DM list. diff --git a/.changeset/dm-routing.md b/.changeset/dm-routing.md new file mode 100644 index 0000000000..019d56853b --- /dev/null +++ b/.changeset/dm-routing.md @@ -0,0 +1,5 @@ +--- +default: patch +--- + +Route DM rooms to /direct before checking space parents to fix incorrect navigation. diff --git a/.changeset/edit-in-input.md b/.changeset/edit-in-input.md new file mode 100644 index 0000000000..9068b7cae9 --- /dev/null +++ b/.changeset/edit-in-input.md @@ -0,0 +1,5 @@ +--- +default: minor +--- + +Add experimental Discord-style edit-in-input toggle. diff --git a/.changeset/edit-message-keybind.md b/.changeset/edit-message-keybind.md new file mode 100644 index 0000000000..52245431de --- /dev/null +++ b/.changeset/edit-message-keybind.md @@ -0,0 +1,5 @@ +--- +default: minor +--- + +Add Ctrl+Alt+Up/Down keyboard shortcuts to cycle through editable messages. diff --git a/.changeset/emoji-board-focustrap.md b/.changeset/emoji-board-focustrap.md new file mode 100644 index 0000000000..fa6d55dc73 --- /dev/null +++ b/.changeset/emoji-board-focustrap.md @@ -0,0 +1,5 @@ +--- +default: patch +--- + +Skip displayCheck on FocusTrap activation in the emoji board to prevent focus trap errors. diff --git a/.changeset/encrypted-search-idb.md b/.changeset/encrypted-search-idb.md new file mode 100644 index 0000000000..2b8b14a87b --- /dev/null +++ b/.changeset/encrypted-search-idb.md @@ -0,0 +1,5 @@ +--- +default: minor +--- + +Add IndexedDB-backed persistent search index for all rooms via a MiniSearch web worker with multi-tab write safety and LRU eviction. Note: indexed message text is stored unencrypted in IndexedDB. diff --git a/.changeset/encrypted-search-memory.md b/.changeset/encrypted-search-memory.md new file mode 100644 index 0000000000..8b4a3ae00c --- /dev/null +++ b/.changeset/encrypted-search-memory.md @@ -0,0 +1,5 @@ +--- +default: minor +--- + +Add in-memory encrypted message search with room/DM scoping, member picker avatars, and > quick-switcher prefix. diff --git a/.changeset/fix-phantom-unreads.md b/.changeset/fix-phantom-unreads.md new file mode 100644 index 0000000000..a461295f9a --- /dev/null +++ b/.changeset/fix-phantom-unreads.md @@ -0,0 +1,5 @@ +--- +default: patch +--- + +Fix phantom unread dot badges when server reports zero unreads or when you sent the latest message. diff --git a/.changeset/media-cache.md b/.changeset/media-cache.md new file mode 100644 index 0000000000..d829a25fee --- /dev/null +++ b/.changeset/media-cache.md @@ -0,0 +1,5 @@ +--- +default: minor +--- + +Add persistent Cache API for media storage; skip caching failed media requests. diff --git a/.changeset/message-bookmarks.md b/.changeset/message-bookmarks.md new file mode 100644 index 0000000000..9ca1cf6c36 --- /dev/null +++ b/.changeset/message-bookmarks.md @@ -0,0 +1,5 @@ +--- +default: minor +--- + +Add message bookmarks (MSC4438). Users can bookmark messages for easy retrieval via a new Bookmarks section in the home sidebar. Gated by an operator `config.json` experiment flag (`experiments.messageBookmarks`) and a per-user experimental settings toggle. diff --git a/.changeset/message-grouping.md b/.changeset/message-grouping.md new file mode 100644 index 0000000000..c38516112b --- /dev/null +++ b/.changeset/message-grouping.md @@ -0,0 +1,5 @@ +--- +default: minor +--- + +Add configurable message grouping time threshold setting. diff --git a/.changeset/mobile.md b/.changeset/mobile.md new file mode 100644 index 0000000000..501b1bbd16 --- /dev/null +++ b/.changeset/mobile.md @@ -0,0 +1,5 @@ +--- +default: patch +--- + +Fix iOS sync resume, network latency retries, iPad layout, and connecting banner during fast-path sync. Add missing items to mobile long-press message menu; fix bookmark toggle and iOS keyboard cover. diff --git a/.changeset/notifications.md b/.changeset/notifications.md new file mode 100644 index 0000000000..13b1d7f7a2 --- /dev/null +++ b/.changeset/notifications.md @@ -0,0 +1,5 @@ +--- +default: patch +--- + +Fix notification routing, in-app banners for All Messages rooms, and background audio for loud non-DM rooms. diff --git a/.changeset/polls.md b/.changeset/polls.md new file mode 100644 index 0000000000..286be77287 --- /dev/null +++ b/.changeset/polls.md @@ -0,0 +1,5 @@ +--- +default: minor +--- + +Add poll duration picker with hour presets and custom date input to PollCreator. diff --git a/.changeset/presence.md b/.changeset/presence.md new file mode 100644 index 0000000000..959d7a3468 --- /dev/null +++ b/.changeset/presence.md @@ -0,0 +1,5 @@ +--- +default: patch +--- + +Fix presence: optimistic badge updates, own-status sync, idle timer on desktop, and REST API fallback. diff --git a/.changeset/room-message-preview.md b/.changeset/room-message-preview.md new file mode 100644 index 0000000000..0eaca26cfe --- /dev/null +++ b/.changeset/room-message-preview.md @@ -0,0 +1,5 @@ +--- +default: minor +--- + +Show topic and last-message preview below room and DM names in the sidebar. diff --git a/.changeset/settings-sync.md b/.changeset/settings-sync.md new file mode 100644 index 0000000000..5b5b1cf495 --- /dev/null +++ b/.changeset/settings-sync.md @@ -0,0 +1,5 @@ +--- +default: patch +--- + +Exclude enterForNewline from cross-device settings sync. diff --git a/.changeset/sliding-sync-prefetch.md b/.changeset/sliding-sync-prefetch.md new file mode 100644 index 0000000000..3a3c058a19 --- /dev/null +++ b/.changeset/sliding-sync-prefetch.md @@ -0,0 +1,5 @@ +--- +default: minor +--- + +Prefetch recently-visited rooms on sliding sync complete. diff --git a/.changeset/timeline.md b/.changeset/timeline.md new file mode 100644 index 0000000000..88969644ed --- /dev/null +++ b/.changeset/timeline.md @@ -0,0 +1,5 @@ +--- +default: patch +--- + +Fix timeline scroll recovery, loading spinner position, autopag loop, blank notification room, and ArrowUp-to-edit routing. diff --git a/.changeset/tiptap-composer.md b/.changeset/tiptap-composer.md new file mode 100644 index 0000000000..f624c20c23 --- /dev/null +++ b/.changeset/tiptap-composer.md @@ -0,0 +1,5 @@ +--- +default: minor +--- + +Add experimental Tiptap-based message composer behind a settings toggle. diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000000..f97cbc6fc1 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,152 @@ +// Codespace configuration — lives on personal/config (not ephemeral dev/feat branches). +// This file intentionally targets browser-based use on iPad. +{ + "name": "Sable", + // Using base + node feature instead of javascript-node: to avoid + // tag availability issues on newer Node versions. + "image": "mcr.microsoft.com/devcontainers/base:bookworm", + + "features": { + "ghcr.io/devcontainers/features/node:1": { "version": "24" }, + // Keep git up-to-date for SSH signing support (git ≥ 2.34). + "ghcr.io/devcontainers/features/git:1": {}, + "ghcr.io/devcontainers/features/github-cli:1": {} + }, + + // ── Codespace user secrets ────────────────────────────────────────────────── + // Configure these at: github.com/settings/codespaces > Secrets + // + // GIT_SIGNING_KEY — passphrase-free SSH private key (ed25519 recommended). + // Add the matching public key to your GitHub account as a + // "signing key": github.com/settings/keys + // postCreate.sh will wire up git automatically if set. + // + // SSH_AUTH_KEY — passphrase-free SSH private key (ed25519 recommended). + // Add the matching public key to ~/.ssh/authorized_keys on + // any server you want to SSH into from the Codespace. + // + // GIT_USER_NAME — e.g. "Evie" + // GIT_USER_EMAIL — e.g. "evie@gauthier.id" + // ─────────────────────────────────────────────────────────────────────────── + + "remoteEnv": { + // Pin the pnpm store to a known path so the volume mount works across rebuilds. + "PNPM_STORE_DIR": "/home/vscode/.pnpm-store" + }, + + "customizations": { + "vscode": { + "settings": { + // ── Layout — tuned for iPad browser (vscode.dev / Codespaces web) ───── + // Move the activity bar to the top so it isn't hidden by the iOS Safari + // toolbar or the browser's combined title/status bar. + "workbench.activityBar.location": "top", + // Use a menu for the layout control — fewer tiny hit targets on touch. + "workbench.layoutControl.type": "menu", + // Place the panel (Terminal, Problems, Copilot Chat history) on the + // right so it doesn't fight with the keyboard on small screens. + "workbench.panel.defaultLocation": "right", + // Keep editor tabs visible and wrap them so none are hidden off-screen. + "workbench.editor.showTabs": "multiple", + "workbench.editor.wrapTabs": true, + // Disable minimap — saves horizontal space, improves touch accuracy. + "editor.minimap.enabled": false, + "editor.scrollBeyondLastLine": false, + // Larger default fonts for retina/HiDPI iPad displays. + // Fira Code is loaded as a web font by the tonsky.font-fira-code extension. + // This works for the Monaco *editor* (HTML/CSS rendered), but xterm.js uses + // canvas drawing — it does NOT reliably inherit CSS @font-face on iOS Safari. + // MesloLGS NF / Monaco / Meslo are not iOS system fonts either. + // → Editor: Fira Code via extension is fine. + // → Terminal: use Menlo only (ships with iOS since iOS 7, always available). + "editor.fontSize": 14, + "editor.fontFamily": "'MesloLGS NF', 'Fira Code', Menlo, 'Courier New', monospace", + "editor.fontLigatures": true, + "terminal.integrated.fontSize": 14, + "terminal.integrated.fontFamily": "Menlo, 'Courier New', monospace", + "terminal.integrated.fontLigatures.enabled": false, + "terminal.integrated.gpuAcceleration": "off", + + // Use zsh (installed in onCreate) as the default terminal shell. + // Explicit profile with -l (login shell) ensures nvm / PATH additions + // from the devcontainer node feature are loaded inside the terminal. + "terminal.integrated.defaultProfile.linux": "zsh", + "terminal.integrated.profiles.linux": { + "zsh": { "path": "/bin/zsh", "args": ["-l"] } + }, + + // Shell integration MUST be enabled for Copilot Chat to run terminal + // commands. We set it explicitly because Powerlevel10k instant prompt + // can fire before VS Code injects its integration script and suppress + // the markers — postCreate.sh patches .zshrc to guard against this. + "terminal.integrated.shellIntegration.enabled": true, + + // ── Git signing ─────────────────────────────────────────────────────── + // postCreate.sh configures gpg.format and user.signingkey if + // GIT_SIGNING_KEY secret is present. This just keeps VS Code's git + // UI in sync. + "git.enableCommitSigning": true, + "git.confirmSync": false, + + // ── Copilot Chat ────────────────────────────────────────────────────── + // Always show follow-ups and keep chat history accessible. + "github.copilot.chat.followUps": "always", + // Disable auto-discovery of extension-provided MCP servers (e.g. the + // io.github.github/github-mcp-server registered by vscode-pull-request-github). + // Our explicit HTTP server in .vscode/mcp.json is unaffected and handles all + // GitHub MCP calls without requiring a token prompt. + "chat.mcp.discovery.enabled": false + }, + "extensions": [ + // ── Copilot ─────────────────────────────────────────────────────────── + "GitHub.copilot", + "GitHub.copilot-chat", + "GitHub.vscode-pull-request-github", + // ── Font (web font — required for terminal in browser/iPad) ─────────── + "tonsky.font-fira-code", + // ── Theme ───────────────────────────────────────────────────────────── + "GitHub.github-vscode-theme", + // ── Formatting & linting ────────────────────────────────────────────── + "esbenp.prettier-vscode", + "dbaeumer.vscode-eslint", + "streetsidesoftware.code-spell-checker", + "davidanson.vscode-markdownlint", + // ── Testing ─────────────────────────────────────────────────────────── + "vitest.explorer", + // ── TypeScript / React ──────────────────────────────────────────────── + "bradlc.vscode-tailwindcss", + "styled-components.vscode-styled-components", + "dsznajder.es7-react-js-snippets", + "formulahendry.auto-rename-tag", + "wix.vscode-import-cost", + // ── Utilities ───────────────────────────────────────────────────────── + "christian-kohler.path-intellisense", + "usernamehw.errorlens", + "gruntfuggly.todo-tree", + "wayou.vscode-todo-highlight", + "webpro.vscode-knip", + "lokalise.i18n-ally", + // ── Infrastructure ──────────────────────────────────────────────────── + "hashicorp.terraform", + "zamerick.vscode-caddyfile-syntax" + ] + } + }, + + // ── Port forwarding ───────────────────────────────────────────────────────── + "forwardPorts": [5173, 4173], + "portsAttributes": { + "5173": { "label": "Vite dev", "onAutoForward": "notify" }, + "4173": { "label": "Vite preview", "onAutoForward": "notify" } + }, + + // ── Persistence ───────────────────────────────────────────────────────────── + // Named volume keeps the pnpm content-addressable store across rebuilds. + // Combined with the PNPM_STORE_DIR env var above so postCreate can also + // point pnpm at the same path. + "mounts": ["source=sable-pnpm-store,target=/home/vscode/.pnpm-store,type=volume"], + + "postCreateCommand": "bash .devcontainer/postCreate.sh", + "onCreateCommand": "bash .devcontainer/onCreate.sh", + "remoteUser": "vscode" +} diff --git a/.devcontainer/onCreate.sh b/.devcontainer/onCreate.sh new file mode 100644 index 0000000000..2f2943fa96 --- /dev/null +++ b/.devcontainer/onCreate.sh @@ -0,0 +1,60 @@ +#!/bin/bash +# onCreate.sh — runs during prebuild AND on first Codespace creation. +# No user secrets are available here — keep this purely about dependencies. +# Everything here is cached in the prebuild snapshot. +set -euo pipefail + +# ── Ensure the node feature's PATH additions are active ────────────────────── +# The devcontainers node feature installs via nvm; source it so `node`/`pnpm` +# resolve correctly even in non-login, non-interactive shells. +export NVM_DIR="${NVM_DIR:-/usr/local/share/nvm}" +# shellcheck source=/dev/null +[ -s "${NVM_DIR}/nvm.sh" ] && source "${NVM_DIR}/nvm.sh" --no-use +# Activate the version pinned in .nvmrc / package.json engines. +nvm use 24 2>/dev/null || nvm use node + +# ── Fix named-volume ownership ──────────────────────────────────────────────── +# Docker mounts named volumes as root; fix ownership so the vscode user can write. +if [ -d "${PNPM_STORE_DIR:-}" ]; then + sudo chown -R "$(id -u):$(id -g)" "${PNPM_STORE_DIR}" +fi + +# ── pnpm ────────────────────────────────────────────────────────────────────── +# Suppress corepack's interactive download-confirmation prompt in CI/prebuild. +export COREPACK_ENABLE_DOWNLOAD_PROMPT=0 + +# Enable corepack so the exact pnpm version from package.json#packageManager is used. +corepack enable + +# Point pnpm at the persistent named-volume store so packages survive rebuilds. +if [ -n "${PNPM_STORE_DIR:-}" ]; then + pnpm config set store-dir "${PNPM_STORE_DIR}" +fi + +pnpm install + +# ── Zsh + Oh My Zsh + Powerlevel10k ────────────────────────────────────────── +# Install these during prebuild so the first Codespace start is fast. +# The dotfiles checkout in postCreate.sh will provide .zshrc / .p10k.zsh. + +# Install zsh and tmux if not already present (base:bookworm ships zsh, but be safe). +if ! command -v zsh &>/dev/null || ! command -v tmux &>/dev/null; then + sudo apt-get update -qq && sudo apt-get install -y -qq zsh tmux +fi + +# Install Oh My Zsh non-interactively (KEEP_ZSHRC=yes preserves any existing .zshrc). +if [ ! -d "${HOME}/.oh-my-zsh" ]; then + KEEP_ZSHRC=yes CHSH=no RUNZSH=no \ + sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" +fi + +# Install Powerlevel10k as an OMZ custom theme. +P10K_DIR="${ZSH_CUSTOM:-${HOME}/.oh-my-zsh/custom}/themes/powerlevel10k" +if [ ! -d "${P10K_DIR}" ]; then + git clone --depth=1 https://github.com/romkatv/powerlevel10k.git "${P10K_DIR}" +fi + +# Make zsh the default shell for this user. +sudo chsh -s "$(command -v zsh)" "$(whoami)" + +echo "✓ onCreate complete" diff --git a/.devcontainer/postCreate.sh b/.devcontainer/postCreate.sh new file mode 100644 index 0000000000..58dec301cb --- /dev/null +++ b/.devcontainer/postCreate.sh @@ -0,0 +1,157 @@ +#!/bin/bash +# postCreate.sh — runs once after the Codespace container is created (NOT during prebuild). +# Secrets (GIT_SIGNING_KEY, GIT_USER_NAME, GIT_USER_EMAIL) are available here. +set -euo pipefail + +# ── Dotfiles (bare git repo, MacStudio branch) ──────────────────────────────── +# The dotfiles repo uses the "bare repo in $HOME" pattern. +# We clone a specific branch so we get the VS Code / Codespace-aware config +# (e.g. the P10k instant-prompt guard for $TERM_PROGRAM == "vscode"). +DOTFILES_REPO="https://github.com/Just-Insane/dotfiles.git" +DOTFILES_BRANCH="codespaces" +DOTFILES_DIR="${HOME}/.cfg" + +if [ ! -d "${DOTFILES_DIR}" ]; then + git clone --bare --branch "${DOTFILES_BRANCH}" "${DOTFILES_REPO}" "${DOTFILES_DIR}" + + # Check out dotfiles to $HOME. Use --force to overwrite any stub files + # created by the devcontainer (e.g. a default .bashrc). + git --git-dir="${DOTFILES_DIR}" --work-tree="${HOME}" checkout --force "${DOTFILES_BRANCH}" + + # Don't show untracked files (the whole home dir) in status. + git --git-dir="${DOTFILES_DIR}" --work-tree="${HOME}" \ + config --local status.showUntrackedFiles no + + echo "✓ Dotfiles checked out from ${DOTFILES_BRANCH}" +else + # Already exists (e.g. Codespace resumed) — just pull latest. + git --git-dir="${DOTFILES_DIR}" --work-tree="${HOME}" \ + fetch origin "${DOTFILES_BRANCH}" && \ + git --git-dir="${DOTFILES_DIR}" --work-tree="${HOME}" \ + checkout --force "${DOTFILES_BRANCH}" + echo "✓ Dotfiles updated" +fi + +# ── Powerlevel10k — browser-compatible glyph mode ──────────────────────────── +# MesloLGS NF / Nerd Font glyphs are unavailable in browser-based Codespaces. +# Patch .p10k.zsh to use the 'compatible' Unicode symbol set instead, which +# renders correctly with any modern monospace font (e.g. Fira Code via extension). +# The POWERLEVEL9K_MODE line has no quotes: POWERLEVEL9K_MODE=nerdfont-complete +if [ -f "${HOME}/.p10k.zsh" ]; then + sed -i "s/POWERLEVEL9K_MODE=.*/POWERLEVEL9K_MODE=compatible/" \ + "${HOME}/.p10k.zsh" + echo "✓ p10k mode set to compatible" +else + echo "⚠ ~/.p10k.zsh not found — skipping p10k patch (add it to your dotfiles repo)" +fi + +# ── Powerlevel10k — disable instant prompt in Codespace terminal ────────────── +# Instant prompt outputs to the terminal before VS Code injects its shell +# integration script. This breaks the integration markers that Copilot Chat +# relies on to run commands. +# We unconditionally disable it here because: +# - In a Codespace, VS Code shell integration is always needed for Copilot Chat. +# - $TERM_PROGRAM is NOT reliably set to "vscode" in browser-based Codespaces +# (e.g. iPad / vscode.dev), so a conditional guard can silently fail. +# The check is idempotent — safe to run on Codespace resume. +if [ -f "${HOME}/.zshrc" ]; then + if ! grep -q 'POWERLEVEL9K_INSTANT_PROMPT=off' "${HOME}/.zshrc"; then + tmp=$(mktemp) + { + printf '# Disable P10k instant prompt — it fires before VS Code shell\n' + printf '# integration is injected, breaking Copilot Chat terminal access.\n' + printf '# Unconditional: $TERM_PROGRAM is not reliable in browser Codespaces.\n' + printf 'typeset -g POWERLEVEL9K_INSTANT_PROMPT=off\n\n' + cat "${HOME}/.zshrc" + } > "$tmp" + mv "$tmp" "${HOME}/.zshrc" + echo "✓ P10k instant prompt unconditionally disabled" + else + echo "✓ P10k instant prompt already disabled" + fi +else + echo "⚠ ~/.zshrc not found — skipping instant-prompt patch (dotfiles not checked out?)" +fi + +# ── Git identity ────────────────────────────────────────────────────────────── +# Populate from Codespace user secrets if they aren't already set by dotfiles. +if [ -n "${GIT_USER_NAME:-}" ] && [ -z "$(git config --global user.name 2>/dev/null)" ]; then + git config --global user.name "${GIT_USER_NAME}" +fi + +if [ -n "${GIT_USER_EMAIL:-}" ] && [ -z "$(git config --global user.email 2>/dev/null)" ]; then + git config --global user.email "${GIT_USER_EMAIL}" +fi + +# ── Git SSH commit signing ──────────────────────────────────────────────────── +# Requires a Codespace user secret named GIT_SIGNING_KEY containing a +# passphrase-free SSH private key (ed25519 recommended). +# +# To set up: +# 1. Generate a key: ssh-keygen -t ed25519 -C "codespace signing" -N "" -f ~/.ssh/signing_key +# 2. Copy the private key into a GitHub Codespace secret called GIT_SIGNING_KEY: +# github.com/settings/codespaces > Secrets > New secret +# 3. Add the *public* key to your GitHub account as a signing key (not auth key): +# github.com/settings/keys > New signing key +# ---------------------------------------------------------------------------- +if [ -n "${GIT_SIGNING_KEY:-}" ]; then + SSH_DIR="${HOME}/.ssh" + mkdir -p "${SSH_DIR}" + chmod 700 "${SSH_DIR}" + + KEY_FILE="${SSH_DIR}/git_signing_key" + printf '%s\n' "${GIT_SIGNING_KEY}" > "${KEY_FILE}" + chmod 600 "${KEY_FILE}" + + # Derive the public key from the private key so the user only stores one secret. + ssh-keygen -y -f "${KEY_FILE}" > "${KEY_FILE}.pub" + chmod 644 "${KEY_FILE}.pub" + + # Configure git to use SSH signing. + git config --global gpg.format ssh + git config --global user.signingkey "${KEY_FILE}.pub" + git config --global commit.gpgsign true + git config --global tag.gpgsign true + + # Allow this key when verifying signatures locally. + ALLOWED_SIGNERS="${SSH_DIR}/allowed_signers" + EMAIL="$(git config --global user.email 2>/dev/null || echo "evie@gauthier.id")" + echo "${EMAIL} $(cat "${KEY_FILE}.pub")" > "${ALLOWED_SIGNERS}" + git config --global gpg.ssh.allowedSignersFile "${ALLOWED_SIGNERS}" + + # Load the key into the ssh-agent so it's available for signing and SSH auth. + eval "$(ssh-agent -s)" &>/dev/null || true + ssh-add "${KEY_FILE}" + + echo "✓ Git SSH commit signing configured (${KEY_FILE}.pub)" +fi + +# ── SSH auth key ────────────────────────────────────────────────────────────── +# Requires a Codespace user secret named SSH_AUTH_KEY containing a +# passphrase-free SSH private key (ed25519 recommended). +# +# To set up: +# 1. Generate a key: ssh-keygen -t ed25519 -C "codespace auth" -N "" -f ~/.ssh/id_ed25519 +# 2. Copy the private key into a GitHub Codespace secret called SSH_AUTH_KEY: +# github.com/settings/codespaces > Secrets > New secret +# 3. Add the *public* key to ~/.ssh/authorized_keys on your server. +# ---------------------------------------------------------------------------- +if [ -n "${SSH_AUTH_KEY:-}" ]; then + SSH_DIR="${HOME}/.ssh" + mkdir -p "${SSH_DIR}" + chmod 700 "${SSH_DIR}" + + AUTH_KEY_FILE="${SSH_DIR}/id_ed25519" + printf '%s\n' "${SSH_AUTH_KEY}" > "${AUTH_KEY_FILE}" + chmod 600 "${AUTH_KEY_FILE}" + + ssh-keygen -y -f "${AUTH_KEY_FILE}" > "${AUTH_KEY_FILE}.pub" + chmod 644 "${AUTH_KEY_FILE}.pub" + + eval "$(ssh-agent -s)" &>/dev/null || true + ssh-add "${AUTH_KEY_FILE}" + + echo "✓ SSH auth key loaded (${AUTH_KEY_FILE}.pub)" +fi + +echo "✓ postCreate complete" diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml index 9b4c9acbb3..d9a365eebf 100644 --- a/.github/actions/setup/action.yml +++ b/.github/actions/setup/action.yml @@ -34,6 +34,36 @@ runs: env: INPUTS_INSTALL_COMMAND: ${{ inputs.install-command }} + - name: Inject runtime config overrides + if: ${{ env.CLIENT_CONFIG_OVERRIDES_JSON != '' }} + shell: bash + working-directory: ${{ github.workspace }} + run: node scripts/inject-client-config.js + env: + CLIENT_CONFIG_OVERRIDES_JSON: ${{ env.CLIENT_CONFIG_OVERRIDES_JSON }} + CLIENT_CONFIG_OVERRIDES_STRICT: ${{ env.CLIENT_CONFIG_OVERRIDES_STRICT }} + + - name: Display injected config + if: ${{ env.CLIENT_CONFIG_OVERRIDES_JSON != '' }} + shell: bash + working-directory: ${{ github.workspace }} + run: | + summary_file="${GITHUB_STEP_SUMMARY:-}" + echo "::group::Injected Client Config" + experiments_json="$(jq -c '.experiments // "No experiments configured"' config.json 2>/dev/null || echo 'config.json not readable')" + echo "$experiments_json" + echo "::endgroup::" + + if [[ -n "$summary_file" ]]; then + { + echo "### Injected client config" + echo + echo "\`\`\`json" + echo "$experiments_json" + echo "\`\`\`" + } >> "$summary_file" + fi + - name: Build app if: ${{ inputs.build == 'true' }} shell: bash diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000000..401bc55cb6 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,16 @@ +# Sable – GitHub Copilot Instructions + +Universal rules that apply to every session. Detailed guidance lives in `.github/instructions/` and `AGENTS.md`. + +## Core Rules + +- **Never commit directly to `dev` or `integration`.** All work goes on a dedicated branch (`fix/…`, `feat/…`, `chore/…`, etc.). +- Use short, scoped commit messages: `type(scope): description` (e.g. `fix(timeline): correct scroll anchor on bulk load`). +- Run quality gates in order and fix all failures before committing: + ``` + pnpm lint && pnpm fmt:check && pnpm typecheck && pnpm test:run && pnpm knip && pnpm build + ``` +- No `any` casts without an inline comment explaining why it's unavoidable. +- **No over-engineering**: only make changes directly requested or clearly necessary. Don't add abstractions for one-off operations. +- **Reversible actions only**: ask before deleting files/branches, force-pushing, dropping data, or running `pnpm install` to add/remove packages. +- Do not log or expose access tokens, room keys, or other secrets. diff --git a/.github/instructions/security.instructions.md b/.github/instructions/security.instructions.md new file mode 100644 index 0000000000..684d3083fd --- /dev/null +++ b/.github/instructions/security.instructions.md @@ -0,0 +1,10 @@ +--- +applyTo: 'src/**,Caddyfile,Dockerfile' +--- + +## Security + +- Follow OWASP Top 10 guidance. +- No `innerHTML`, no `eval`; sanitise all user-supplied and Matrix-sourced content before rendering. +- Do not log or expose access tokens, room keys, or other secrets. +- Content Security Policy headers in `Caddyfile` and `Dockerfile` must not be weakened without a documented reason. diff --git a/.github/instructions/typescript.instructions.md b/.github/instructions/typescript.instructions.md new file mode 100644 index 0000000000..f1fd190726 --- /dev/null +++ b/.github/instructions/typescript.instructions.md @@ -0,0 +1,29 @@ +--- +applyTo: 'src/**' +--- + +## TypeScript & React + +- Functional components and hooks only. No class components. +- Proper dependency arrays on `useEffect`, `useCallback`, and `useMemo`. +- Prefer explicit types over inferred types for public/exported function signatures. +- No `any` casts without an inline comment explaining why it's unavoidable. + +## Comments & Documentation + +- Comments must be **short and purposeful** — explain _why_, not _what_. +- No decorative separator lines (`//------`), no block comments restating the code. +- Do not add docstrings, comments, or type annotations to code that was not changed in the current task. +- Add concise docstrings, comments, and/or type annotations to new or updated code. + +## Testing + +- Test files live alongside source in `src/` (e.g. `foo.test.ts`). Match the naming convention of existing tests. +- Write Vitest tests for any new utility function, hook, or non-trivial logic. +- Bug fixes should include a regression test where feasible. + +## Feature Flags + +- Every user-visible new feature must be gated behind a feature flag in `config.json` / `useClientConfig`. +- Flags default to `false` (opt-in) unless the feature is a bug fix or a non-breaking improvement with no regressions. +- Document the flag in `config.json` and in the Sable-Docs documentation repo. diff --git a/.github/prompts/architecture-review.prompt.md b/.github/prompts/architecture-review.prompt.md new file mode 100644 index 0000000000..e2c30f26d2 --- /dev/null +++ b/.github/prompts/architecture-review.prompt.md @@ -0,0 +1,112 @@ +# Sable Architecture Review + +You are acting as a senior software architect reviewing a proposed feature or architectural change. + +Do NOT implement code. + +Your goal is to understand the current architecture and propose the cleanest integration approach. + +## Feature / Proposal + +{{FEATURE_DESCRIPTION}} + +## Investigation Requirements + +Review: + +- current architecture +- existing components +- state management +- routing/navigation +- data flow +- mobile behavior +- desktop behavior +- existing UX patterns + +Identify: + +- reusable components +- reusable hooks +- reusable state +- reusable infrastructure + +## Evaluation Criteria + +Assess: + +### User Experience + +- discoverability +- usability +- consistency +- accessibility + +### Technical Design + +- complexity +- maintainability +- scalability +- performance + +### Integration + +- impact on existing systems +- migration concerns +- compatibility with current architecture + +## Deliverables + +### Current Architecture Summary + +Describe: + +- relevant subsystems +- important files +- existing patterns + +### Design Options + +For each option: + +- description +- advantages +- disadvantages +- implementation complexity + +### Recommended Approach + +Provide: + +- preferred solution +- rationale +- tradeoffs + +### Implementation Roadmap + +Break implementation into phases. + +For each phase: + +- goals +- affected files +- dependencies + +### Risks + +Identify: + +- technical risks +- UX risks +- maintenance risks + +### Implementation Prompt + +Produce a follow-up implementation prompt suitable for a smaller coding model. + +## Important + +Prefer solutions that fit naturally into the existing architecture. + +Avoid introducing new frameworks, state management systems, or architectural patterns unless clearly justified. + +Favor incremental adoption over large rewrites. diff --git a/.github/prompts/implement-plan.prompt.md b/.github/prompts/implement-plan.prompt.md new file mode 100644 index 0000000000..a498a2a138 --- /dev/null +++ b/.github/prompts/implement-plan.prompt.md @@ -0,0 +1,81 @@ +# Sable Implementation Task + +You are implementing an approved plan in the Sable codebase. + +Your goal is to make the smallest, safest set of changes required. + +## Requirements + +Implement the plan below. + +Follow existing patterns and conventions already present in the repository. + +Prefer: + +- existing hooks +- existing utilities +- existing state management +- existing styling patterns +- existing navigation patterns + +Avoid: + +- introducing new architecture +- large refactors +- unrelated cleanup +- speculative improvements + +## Plan + +{{IMPLEMENTATION_PLAN}} + +## Expectations + +Before making changes: + +1. Locate the relevant files. +2. Verify the plan matches the current implementation. +3. Identify any discrepancies. + +If the code differs significantly from the plan: + +- stop +- explain the discrepancy +- propose an updated implementation approach + +## Deliverables + +### Implementation Summary + +- what changed +- why + +### Files Modified + +For each file: + +- reason for modification +- summary of changes + +### Risks + +- potential regressions +- edge cases + +### Validation + +Provide: + +- manual test plan +- automated tests to run +- areas requiring additional verification + +## Important + +Keep changes minimal. + +Do not redesign systems unless absolutely necessary. + +Do not make unrelated changes. + +Prefer maintainability and consistency over cleverness. diff --git a/.github/prompts/investigate-bug.prompt.md b/.github/prompts/investigate-bug.prompt.md new file mode 100644 index 0000000000..18a62ab441 --- /dev/null +++ b/.github/prompts/investigate-bug.prompt.md @@ -0,0 +1,112 @@ +# Sable Bug Investigation + +You are investigating a bug in the Sable codebase. + +Do NOT implement changes yet. + +Your task is to perform a root-cause analysis and produce an implementation plan. + +## Investigation Requirements + +For the issue described below: + +1. Identify the relevant code paths. +2. Trace execution flow end-to-end. +3. Identify all major components, hooks, state, events, and side effects involved. +4. Identify likely root causes. +5. Continue investigating beyond the first plausible explanation. +6. Look for: + - race conditions + - lifecycle issues + - state synchronization issues + - React rendering issues + - timeline virtualization issues + - scroll anchoring issues + - Sliding Sync interactions + - Progressive Prefetch interactions + - mobile browser behavior + - iOS PWA behavior + - routing/navigation issues + - event ordering issues + +## Architecture Guidelines + +Prefer existing patterns already present in the codebase. + +Avoid: + +- large rewrites +- introducing new architecture unnecessarily +- duplicate logic +- bandaid fixes + +Reuse: + +- existing hooks +- existing state management +- existing routing/navigation patterns +- existing UI patterns +- existing utility functions + +## Issue To Investigate + +{{ISSUE_DESCRIPTION}} + +## Deliverables + +### Summary + +- user-visible behavior +- relevant architecture +- affected subsystems + +### Root Cause Analysis + +For each suspected root cause: + +- description +- supporting evidence +- confidence level +- files involved + +### Recommended Solution + +- preferred solution +- alternative solutions +- tradeoffs + +### Implementation Plan + +- ordered implementation steps +- minimal architectural changes +- existing patterns to reuse + +### Files + +List: + +- files inspected +- files likely requiring changes +- important functions/classes + +### Risks + +List: + +- edge cases +- testing considerations +- migration concerns + +### Implementation Prompt + +Produce a follow-up implementation prompt suitable for a smaller coding model. + +## Important + +Do NOT implement changes. + +If information is missing, ask clarifying questions before making assumptions. + +If assumptions are necessary, clearly identify them. + +Do not stop after finding the first plausible explanation. diff --git a/.github/prompts/normalize-integration.prompt.md b/.github/prompts/normalize-integration.prompt.md new file mode 100644 index 0000000000..9efb5a59f4 --- /dev/null +++ b/.github/prompts/normalize-integration.prompt.md @@ -0,0 +1,76 @@ +--- +name: normalize integration commits +description: Move unique integration commits onto proper branches based on dev +--- + +Normalize the `integration` branch so it contains no unique feature work. + +## Goal + +Ensure that all meaningful commits currently present only on `integration` are moved onto appropriate feature/fix/personal branches based on updated `dev`. + +`integration` should become a rebuildable branch composed only from `dev` plus explicitly merged branches. + +## Required Process + +1. Inspect the current git state. +2. Ensure the working tree is clean. +3. Fetch all remotes. +4. Update local `dev` from `upstream/dev`. +5. Push updated `dev` to `origin/dev`. +6. Compare: + - `integration` vs `dev` + - `origin/integration` vs `origin/dev` + - local branches vs `integration` + +7. Identify commits that exist only on `integration`. +8. Group unique commits by feature, fix, or purpose. +9. Determine whether each unique commit already belongs to an existing branch. + +## Branch Normalization Plan + +Before making changes, produce a plan showing: + +- unique commits found on `integration` +- suggested destination branch for each commit +- whether to create a new branch or update an existing one +- branch base commit +- cherry-pick order +- likely conflicts +- validation needed per branch + +Do not move commits until I confirm the plan. + +## Implementation Rules + +When approved: + +- create a backup branch of `integration` before making changes +- create/update destination branches from latest `dev` +- cherry-pick or recreate commits carefully +- preserve commit history where reasonable +- resolve conflicts intentionally +- do not drop commits silently +- do not make unrelated cleanup changes +- push each resulting branch to `origin` + +## Validation + +For each created or updated branch: + +1. Run appropriate tests/checks. +2. Report failures. +3. Push to `origin` only after validation passes or after I explicitly approve. + +After all unique commits are moved: + +1. Rebuild `integration` from `dev`. +2. Merge the normalized branches back into `integration`. +3. Confirm `integration` has no unique commits not present on component branches. +4. Push `integration` to `origin/integration` only after approval. + +## Important + +Do not delete or overwrite unique work. + +If any commit purpose is unclear, stop and ask me where it belongs. diff --git a/.github/prompts/rebuild-integration.prompt.md b/.github/prompts/rebuild-integration.prompt.md new file mode 100644 index 0000000000..f54fcb99bc --- /dev/null +++ b/.github/prompts/rebuild-integration.prompt.md @@ -0,0 +1,65 @@ +--- +name: rebuild integration +description: When asked to rebuild integration, or if there are large numbers of changes to branches +--- + +Rebuild the `integration` branch safely. + +## Goal + +Create a fresh `integration` branch from the latest `dev`, then merge selected branches into it in a clean, deliberate order. + +`integration` must be treated as disposable. It should not contain unique work that is not present on a feature/fix/personal branch. + +## Required Process + +1. Inspect the current git state. +2. Ensure the working tree is clean before making changes. +3. Fetch all remotes. +4. Update local `dev` from `upstream/dev`. +5. Push updated `dev` to `origin/dev`. +6. Delete and recreate local `integration` from updated `dev`. +7. Ask me which branches should be included. +8. Always include `personal/config`. +9. Before merging, propose the cleanest merge order. + +## Merge Planning + +Before performing merges, inspect selected branches and produce a merge plan that considers: + +- branch dependencies +- overlapping files +- likely conflict areas +- whether some branches should be merged before others +- whether any branches appear stale and should first be rebased or merged from `dev` +- whether any branch appears to duplicate changes from another branch + +Do not merge until you have shown the plan and I confirm. + +## Merge Requirements + +When merging selected branches: + +- preserve feature branch commits where practical +- avoid dropping code or features silently +- resolve conflicts carefully +- explain each conflict before resolving it +- prefer existing branch intent over accidental integration-only state +- do not make unrelated cleanup changes + +## Validation + +After rebuilding `integration`: + +1. Show the final branch graph summary. +2. Show which branches were merged. +3. Show any conflicts resolved. +4. Run appropriate tests/checks. +5. Report failures clearly. +6. Push `integration` to `origin/integration` only after validation passes or after I explicitly approve pushing with known failures. + +## Important + +Do not create unique feature work directly on `integration`. + +If you discover commits that exist only on `integration`, stop and report them instead of deleting them silently. diff --git a/.github/prompts/review-prs-against-upstream.md b/.github/prompts/review-prs-against-upstream.md new file mode 100644 index 0000000000..75a742a8ae --- /dev/null +++ b/.github/prompts/review-prs-against-upstream.md @@ -0,0 +1,69 @@ +--- +name: review open PRs against `upstream` +description: When asked to review open PRs against `upstream` +--- + +Review open pull requests against the upstream repository. + +## Goal + +Identify upstream PRs that may be useful, relevant, risky, or conflicting for Sable. + +Do not merge anything yet. + +## Required Process + +1. Inspect configured git remotes. +2. Identify the upstream repository. +3. Fetch latest upstream branches and pull request refs if available. +4. List open upstream PRs. +5. For each relevant PR, inspect: + - title + - description + - changed files + - commits + - affected subsystems + - merge status + - whether it touches areas Sable has modified + +## Review Criteria + +For each PR, classify it as: + +- likely useful for Sable +- potentially useful but needs review +- risky due to conflicts with Sable changes +- not relevant +- already effectively included +- should be avoided + +Pay special attention to PRs touching: + +- Matrix SDK usage +- Sliding Sync +- timeline rendering +- room navigation +- mobile behavior +- notifications +- encryption +- build tooling +- dependency updates +- security fixes + +## Deliverables + +Produce a report with: + +1. Summary of open upstream PRs reviewed. +2. Recommended PRs to cherry-pick, merge, or monitor. +3. PRs likely to conflict with Sable. +4. PRs touching files modified by Sable. +5. Suggested order of adoption. +6. Risks and testing requirements. +7. Follow-up implementation prompt for applying selected PRs. + +## Important + +Do not merge or cherry-pick PRs unless I explicitly ask. + +If GitHub CLI or network access is unavailable, explain what information is missing and suggest the exact commands I should run. diff --git a/.github/workflows/cloudflare-dev-deploy.yml b/.github/workflows/cloudflare-dev-deploy.yml new file mode 100644 index 0000000000..5bc6421e0f --- /dev/null +++ b/.github/workflows/cloudflare-dev-deploy.yml @@ -0,0 +1,105 @@ +name: Cloudflare Worker Dev Deploy + +on: + push: + branches: + - dev + paths: + - 'src/**' + - 'config.json' + - 'index.html' + - 'package.json' + - 'package-lock.json' + - 'scripts/inject-client-config.js' + - 'vite.config.ts' + - 'tsconfig.json' + - '.github/workflows/cloudflare-dev-deploy.yml' + - '.github/actions/setup/**' + +concurrency: + group: cloudflare-worker-dev-deploy + cancel-in-progress: true + +jobs: + deploy: + runs-on: ubuntu-latest + environment: production + permissions: + contents: read + env: + CLIENT_CONFIG_OVERRIDES_JSON: ${{ vars.CLIENT_CONFIG_OVERRIDES_JSON }} + CLIENT_CONFIG_OVERRIDES_STRICT: ${{ vars.CLIENT_CONFIG_OVERRIDES_STRICT || 'false' }} + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Prepare preview metadata + id: metadata + shell: bash + run: | + preview_message="$(git log -1 --pretty=%s)" + preview_message="$(printf '%s' "$preview_message" | head -c 100)" + + { + echo 'preview_message<> "$GITHUB_OUTPUT" + + - name: Set Sentry build environment + env: + VITE_SENTRY_DSN: ${{ secrets.VITE_SENTRY_DSN }} + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + SENTRY_ORG: ${{ secrets.SENTRY_ORG }} + SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }} + shell: bash + run: | + echo "VITE_SENTRY_DSN=$VITE_SENTRY_DSN" >> "$GITHUB_ENV" + echo "VITE_SENTRY_ENVIRONMENT=production" >> "$GITHUB_ENV" + echo "SENTRY_AUTH_TOKEN=$SENTRY_AUTH_TOKEN" >> "$GITHUB_ENV" + echo "SENTRY_ORG=$SENTRY_ORG" >> "$GITHUB_ENV" + echo "SENTRY_PROJECT=$SENTRY_PROJECT" >> "$GITHUB_ENV" + + - name: Setup app and build + uses: ./.github/actions/setup + with: + build: 'true' + + - name: Upload Worker preview + id: deploy + uses: cloudflare/wrangler-action@da0e0dfe58b7a431659754fdf3f186c529afbe65 # v3.14.1 + env: + PREVIEW_MESSAGE: ${{ steps.metadata.outputs.preview_message }} + with: + apiToken: ${{ secrets.TF_CLOUDFLARE_API_TOKEN }} + accountId: ${{ secrets.TF_VAR_ACCOUNT_ID }} + command: > + versions upload + -c dist/wrangler.json + --preview-alias dev + --message "$PREVIEW_MESSAGE" + + - name: Publish summary + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + DEPLOYMENT_URL: ${{ steps.deploy.outputs.deployment-url }} + SHORT_SHA: ${{ github.sha }} + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const deploymentUrl = process.env.DEPLOYMENT_URL; + const shortSha = process.env.SHORT_SHA?.slice(0, 7); + const now = new Date().toUTCString().replace(':00 GMT', ' UTC'); + + const tableRow = "| ✅ Dev deployment successful! | " + deploymentUrl + " | " + shortSha + " | `dev` | " + now + " |"; + const comment = [ + `## Deploying with  Cloudflare Workers  Cloudflare Workers (dev → production config)`, + ``, + `| Status | URL | Commit | Alias | Updated (UTC) |`, + `| - | - | - | - | - |`, + tableRow, + ].join("\n"); + + await core.summary.addRaw(comment).write(); diff --git a/.github/workflows/cloudflare-web-deploy.yml b/.github/workflows/cloudflare-web-deploy.yml index d3d2c44614..e32dbf68e4 100644 --- a/.github/workflows/cloudflare-web-deploy.yml +++ b/.github/workflows/cloudflare-web-deploy.yml @@ -40,6 +40,10 @@ jobs: plan: if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository runs-on: ubuntu-latest + environment: preview + env: + CLIENT_CONFIG_OVERRIDES_JSON: ${{ vars.CLIENT_CONFIG_OVERRIDES_JSON }} + CLIENT_CONFIG_OVERRIDES_STRICT: ${{ vars.CLIENT_CONFIG_OVERRIDES_STRICT || 'false' }} permissions: contents: read pull-requests: write @@ -73,6 +77,10 @@ jobs: apply: if: (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')) || github.event_name == 'workflow_dispatch' runs-on: ubuntu-latest + environment: production + env: + CLIENT_CONFIG_OVERRIDES_JSON: ${{ vars.CLIENT_CONFIG_OVERRIDES_JSON }} + CLIENT_CONFIG_OVERRIDES_STRICT: ${{ vars.CLIENT_CONFIG_OVERRIDES_STRICT || 'false' }} permissions: contents: read defaults: diff --git a/.github/workflows/cloudflare-web-preview.yml b/.github/workflows/cloudflare-web-preview.yml index 8b93a4bb9b..762c8fe9d6 100644 --- a/.github/workflows/cloudflare-web-preview.yml +++ b/.github/workflows/cloudflare-web-preview.yml @@ -4,21 +4,25 @@ on: pull_request: paths: - 'src/**' + - 'config.json' - 'index.html' - 'package.json' - 'package-lock.json' + - 'scripts/inject-client-config.js' - 'vite.config.ts' - 'tsconfig.json' - '.github/workflows/cloudflare-web-preview.yml' - '.github/actions/setup/**' push: branches: - - dev + - integration paths: - 'src/**' + - 'config.json' - 'index.html' - 'package.json' - 'package-lock.json' + - 'scripts/inject-client-config.js' - 'vite.config.ts' - 'tsconfig.json' - '.github/workflows/cloudflare-web-preview.yml' @@ -32,9 +36,13 @@ jobs: deploy: if: github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'push' runs-on: ubuntu-latest + environment: preview permissions: contents: read pull-requests: write + env: + CLIENT_CONFIG_OVERRIDES_JSON: ${{ vars.CLIENT_CONFIG_OVERRIDES_JSON }} + CLIENT_CONFIG_OVERRIDES_STRICT: ${{ vars.CLIENT_CONFIG_OVERRIDES_STRICT || 'false' }} steps: - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index cb87c2d14e..5badb90a42 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -2,7 +2,7 @@ name: Build and publish Docker image on: push: - branches: [dev] + branches: [dev, integration] tags: - 'v*' pull_request: @@ -23,12 +23,16 @@ env: jobs: build-and-push: runs-on: ubuntu-latest + environment: ${{ github.event_name == 'pull_request' && (github.base_ref == 'dev' && 'production' || github.base_ref == 'integration' && 'preview' || 'preview') || github.ref == 'refs/heads/dev' && 'production' || github.ref == 'refs/heads/integration' && 'preview' || 'preview' }} permissions: contents: read packages: write attestations: write artifact-metadata: write id-token: write + env: + CLIENT_CONFIG_OVERRIDES_JSON: ${{ vars.CLIENT_CONFIG_OVERRIDES_JSON }} + CLIENT_CONFIG_OVERRIDES_STRICT: ${{ vars.CLIENT_CONFIG_OVERRIDES_STRICT || 'false' }} steps: - name: Checkout repository @@ -70,12 +74,16 @@ jobs: flavor: | latest=false tags: | - # dev branch or manual dispatch without a tag: short commit SHA + dev - type=sha,prefix=,format=short,enable=${{ github.ref == 'refs/heads/dev' || (github.event_name == 'workflow_dispatch' && inputs.git_tag == '') }} - type=raw,value=dev,enable=${{ github.ref == 'refs/heads/dev' || (github.event_name == 'workflow_dispatch' && inputs.git_tag == '') }} + # dev/integration branch or manual dispatch without a tag: short commit SHA + type=sha,prefix=,format=short,enable=${{ github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/integration' || (github.event_name == 'workflow_dispatch' && inputs.git_tag == '') }} - # git tags (push or manual dispatch with a tag): semver breakdown + latest - type=raw,value=latest,enable=${{ steps.release_tag.outputs.is_release == 'true' }} + # dev branch or manual dispatch without a tag: latest tag + type=raw,value=latest,enable=${{ github.ref == 'refs/heads/dev' || (github.event_name == 'workflow_dispatch' && inputs.git_tag == '') }} + + # integration branch: stable integration tag + type=raw,value=integration,enable=${{ github.ref == 'refs/heads/integration' }} + + # git tags (push or manual dispatch with a tag): semver breakdown type=semver,pattern={{version}},value=${{ steps.release_tag.outputs.value }},enable=${{ steps.release_tag.outputs.is_release == 'true' }} type=semver,pattern={{major}}.{{minor}},value=${{ steps.release_tag.outputs.value }},enable=${{ steps.release_tag.outputs.is_release == 'true' }} type=semver,pattern={{major}},value=${{ steps.release_tag.outputs.value }},enable=${{ steps.release_tag.outputs.is_release == 'true' && !startsWith(steps.release_tag.outputs.value, 'v0.') }} @@ -91,6 +99,12 @@ jobs: env: VITE_BUILD_HASH: ${{ steps.vars.outputs.short_sha }} VITE_IS_RELEASE_TAG: ${{ steps.release_tag.outputs.is_release }} + VITE_SENTRY_DSN: ${{ secrets.VITE_SENTRY_DSN }} + VITE_SENTRY_ENVIRONMENT: ${{ (steps.release_tag.outputs.is_release == 'true' || github.ref == 'refs/heads/dev') && 'production' || 'preview' }} + VITE_APP_VERSION: ${{ github.ref_name }} + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + SENTRY_ORG: ${{ secrets.SENTRY_ORG }} + SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }} run: | NODE_OPTIONS=--max_old_space_size=4096 pnpm run build diff --git a/.github/workflows/flake-lock-fix.yml b/.github/workflows/flake-lock-fix.yml index 39447cfa5c..6ec90be358 100644 --- a/.github/workflows/flake-lock-fix.yml +++ b/.github/workflows/flake-lock-fix.yml @@ -1,7 +1,6 @@ name: Fix flake hashes on: - workflow_dispatch: push: branches: [dev] paths: ['pnpm-lock.yaml'] @@ -14,20 +13,11 @@ jobs: id-token: write contents: write steps: - - name: Generate bot token - id: app-token - uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 - with: - app-id: ${{ secrets.APP_ID }} - private-key: ${{ secrets.APP_PRIVATE_KEY }} - permission-contents: write - - name: Checkout dev uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: dev persist-credentials: true - token: ${{ steps.app-token.outputs.token }} - name: Install Determinate Nix uses: DeterminateSystems/determinate-nix-action@bafaa638b9d5ec0e7e3ac1a7fc80453ef1fd265f # v3.20.0 @@ -40,20 +30,6 @@ jobs: if: steps.check.outcome == 'failure' run: determinate-nixd fix hashes --auto-apply - - name: Get GitHub App User ID - id: get-user-id - run: echo "user-id=$(gh api "/users/${{ steps.app-token.outputs.app-slug }}[bot]" --jq .id)" >> "$GITHUB_OUTPUT" - env: - GH_TOKEN: ${{ steps.app-token.outputs.token }} - - - name: Configure bot git identity - run: | - git config --global user.name '${{ steps.app-token.outputs.app-slug }}[bot]' - git config --global user.email '${{ steps.get-user-id.outputs.user-id }}+${{ steps.app-token.outputs.app-slug }}[bot]@users.noreply.github.com' - gh auth setup-git - env: - GH_TOKEN: ${{ steps.app-token.outputs.token }} - - name: Commit and push if changed if: steps.check.outcome == 'failure' shell: bash @@ -63,6 +39,8 @@ jobs: echo "No changes after fix hashes; nothing to commit." exit 0 fi + git config user.name "sable-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" git add --update --ignore-removal . git commit -m "chore(nix): auto-fix nix hashes" git push origin HEAD:dev --force-with-lease diff --git a/.gitignore b/.gitignore index 76af755426..7ad97f72fa 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,7 @@ devAssets .DS_Store .idea - +*.code-workspace .env .wrangler @@ -24,14 +24,25 @@ build.sh # the following line was added with the "git ignore" tool by itsrye.dev, version 0.1.0 .lh -# the following line was added with nvim by Shea because its annoying to clear every so often -.vscode/bookmarks.json - -# Nix things -## nix build output -result -## direnv config + local state -.envrc -.direnv -## auto-generated pre-commit config -.pre-commit-config.yaml +# Tauri +/build +/target/ +/gen/schemas +dist-js + +# Gradle / Android build artifacts +**/android/build/ +**/android/.tauri/ +**/android/.gradle/ +**/.gradle/ + +# Rust build artifacts +src-tauri/target/ +# Tauri Android build outputs +src-tauri/gen/android/app/build/ + +# Tauri-typegen cache +.typecache + +# Worktrees +.worktrees \ No newline at end of file diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000000..93534783c1 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,11 @@ +dist +node_modules +package.json +pnpm-lock.yaml +LICENSE +README.md +CHANGELOG.md +./changeset +src/app/generated +crates/ +src-tauri/ \ No newline at end of file diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 434432fea2..c58bff3e39 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,3 +1,28 @@ { - "recommendations": ["webpro.vscode-knip", "oxc.oxc-vscode"] + "recommendations": [ + // JS/TS toolchain + "webpro.vscode-knip", + "oxc.oxc-vscode", + "usernamehw.errorlens", + "christian-kohler.path-intellisense", + "styled-components.vscode-styled-components", + "bradlc.vscode-tailwindcss", + // React/TypeScript + "dsznajder.es7-react-js-snippets", + "formulahendry.auto-rename-tag", + "wix.vscode-import-cost", + // i18n + "lokalise.i18n-ally", + // Testing + "vitest.explorer", + // Infrastructure + "hashicorp.terraform", + "zamerick.vscode-caddyfile-syntax", + // Documentation + "streetsidesoftware.code-spell-checker", + "davidanson.vscode-markdownlint", + // Quality of Life + "gruntfuggly.todo-tree", + "wayou.vscode-todo-highlight" + ] } diff --git a/.vscode/mcp.json b/.vscode/mcp.json new file mode 100644 index 0000000000..b2bc0a4e87 --- /dev/null +++ b/.vscode/mcp.json @@ -0,0 +1,10 @@ +{ + // GitHub MCP server — uses existing Copilot auth, no token prompt needed. + // Works in browser-based Codespaces (no vscode:// redirect required). + "servers": { + "github": { + "type": "http", + "url": "https://api.githubcopilot.com/mcp/" + } + } +} diff --git a/.vscode/settings.json b/.vscode/settings.json index d8071aa979..a22d11d161 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -11,17 +11,9 @@ "editor.defaultFormatter": "oxc.oxc-vscode" }, "[json]": { - "editor.defaultFormatter": "oxc.oxc-vscode" + "editor.defaultFormatter": "esbenp.prettier-vscode" }, - "nixEnvSelector.nixFile": "${workspaceFolder}/flake.nix", - "nixEnvSelector.useFlakes": true, - "explorer.fileNesting.enabled": true, - "explorer.fileNesting.patterns": { - "package.json": "pnpm-*.yaml, yarn.lock, package-lock.json, .npmrc, .nvmrc, .node-version", - "tsconfig.json": "tsconfig.*.json", - "Dockerfile": ".dockerignore", - ".env": ".env.*", - "flake.nix": "flake.lock", - ".gitattributes": ".gitignore" + "[rust]": { + "editor.defaultFormatter": "rust-lang.rust-analyzer" } } diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000000..c44ee70522 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,74 @@ +# Sable – Agent Instructions + +Workflow and process rules for AI agents. These complement the universal rules in `.github/copilot-instructions.md`. + +--- + +## Git & Branching + +- Never commit directly to `dev` or `integration`. +- When creating a branch, first sync `upstream/dev` to `origin/dev` and local `dev`, then branch from `dev`, with `origin/dev` as the remote: + ``` + git fetch upstream + git checkout dev && git reset --hard upstream/dev + git push origin dev + git checkout -b feat/your-branch dev + ``` +- Before building `integration`, always force-update `origin/dev` from `upstream/dev`, then force-update `dev`: + ``` + git fetch upstream && git push origin upstream/dev:dev --force && git fetch origin && git checkout dev && git reset --hard origin/dev + ``` +- When asked to build `integration`, always prompt for which feature/fix/chore branches to include. In general, include all non-`dev` branches. + +## Quality Gates + +Run these in order and fix all failures before committing: + +``` +pnpm lint # ESLint +pnpm fmt:check # Prettier +pnpm typecheck # TypeScript +pnpm test:run # Vitest unit tests +pnpm knip # Dead-code / unused exports check +pnpm build # Production build — must succeed with no errors +``` + +## Pull Requests + +- Use the PR template (`.github/PULL_REQUEST_TEMPLATE.md`) in full — all checkboxes must be present. +- Descriptions should be short, clear, and human-readable. +- Each PR gets one changeset line (or `fix:` + `feat:` if both are genuinely present; prefer separate PRs otherwise). + +### Pre-PR Research + +1. Search for related open **and** merged PRs on `upstream` (`SableClient/Sable` and `cinnyapp/cinny`) and `origin`. Summarise findings and ask how to proceed if there is overlap or conflict. +2. Search for related open **issues** on `upstream` and `origin`. Confirm with the user, then link any related ones in the PR description (`Closes #N` / `Related to #N`). +3. If the PR has a corresponding `SableClient/docs` PR, link both PRs to each other. + +## Matrix Spec Compliance + +- New features and fixes must match the current Matrix spec, or the relevant MSC if the spec change is pending. +- Check how Element Web, FluffyChat, or Nheko implement the same thing before diverging from established client patterns. +- Link the relevant spec section or MSC in the PR description when the change is spec-driven. + +## Documentation + +- When a new feature is added (or an existing one materially changed), update the Sable-Docs repo (`/Users/evie/git/Sable-Docs`). Add or update the relevant page under `content/features/` or `content/general/`. +- Keep docs concise — match the style of existing pages. + +## Dependency Changes + +- Adding or removing packages requires explicit user confirmation before running `pnpm install`. + +## Merge Conflicts + +- When resolving merge conflicts, prefer the version from the feature branch; ask if the intent is ambiguous. + +## Destructive Actions + +Always ask before: + +- Deleting files or branches (`git branch -D`, `rm`, etc.) +- Force-pushing (`git push --force`) +- Hard-resetting local branches other than `dev`/`integration` (`git reset --hard`) +- Dropping or truncating data diff --git a/Caddyfile b/Caddyfile index d32ef3bb99..c30deb74fb 100644 --- a/Caddyfile +++ b/Caddyfile @@ -14,6 +14,18 @@ precompressed br } + # Exclude static assets from the index.html fallback. If a JS/CSS/font file + # doesn't exist (e.g., stale worker URL from an old cached index.html), return + # 404 instead of serving index.html (which causes "text/html is not a valid + # JavaScript MIME type" errors when the browser tries to execute it). + @assets path /assets/* /font/* /res/* /sound/* + handle @assets { + file_server { + precompressed br + } + } + + # All other requests fall back to index.html for client-side routing. try_files {path} /index.html # Content-hashed assets can be cached indefinitely — the filename changes on every build. diff --git a/config.json b/config.json index 2809e4f68e..82e54d1bdb 100644 --- a/config.json +++ b/config.json @@ -10,7 +10,8 @@ "pushNotificationDetails": { "pushNotifyUrl": "https://sygnal.sable.moe/_matrix/push/v1/notify", "vapidPublicKey": "BCnS4SbHjeOaqVFW4wjt5xDt_pYIL62qMzKePfYF9fl9PQU14RieIaObh7nLR_9dQf4sykZa-CTrcjkgMIE1mcg", - "webPushAppID": "moe.sable.app.sygnal" + "webPushAppID": "moe.sable.app.sygnal", + "unifiedPushAppID": "moe.sable.up" }, "themeCatalogBaseUrl": "https://raw.githubusercontent.com/SableClient/themes/main/", @@ -44,5 +45,9 @@ "hashRouter": { "enabled": false, "basename": "/" + }, + + "features": { + "encryptedSearch": true } } diff --git a/docs/LOGGING_STANDARDS.md b/docs/LOGGING_STANDARDS.md new file mode 100644 index 0000000000..29008cf76e --- /dev/null +++ b/docs/LOGGING_STANDARDS.md @@ -0,0 +1,310 @@ +# Sable Logging Standards + +Comprehensive guide for logging practices across the Sable codebase. + +## Overview + +Sable uses a structured logging system with two primary utilities: + +1. **Simple Logger** (`createLogger`) - For basic debug logging +2. **Enhanced Logger** (`createDebugLogger`) - For categorized logging with Sentry integration + +## When to Use Each Logger + +### Use `createLogger` for: + +- Simple debug output during development +- Performance-critical code where overhead matters +- Service Worker logging (SW context doesn't have full debugLogger) +- Quick troubleshooting that won't be committed + +### Use `createDebugLogger` for: + +- Production logging that helps diagnose user issues +- Error tracking and warning messages +- Feature-specific debug logs +- Any log that should integrate with Sentry + +## Standard Log Prefixes + +### Service Worker Logs + +All Service Worker logs use `console.debug` with the `[SW]` prefix: + +```typescript +console.debug('[SW] Prefetching sliding sync data...'); +console.debug('[SW] Media fetch failed, falling through:', error); +console.warn('[SW fetchRawEvent] HTTP', res.status, 'for', eventId); +``` + +**Format:** `[SW]` or `[SW functionName]` + +### Search Worker Logs + +Search worker logs use `[SearchWorker]`: + +```typescript +console.error('[SearchWorker] INIT failed:', err); +console.warn('[SearchWorker] IDB connection closed:', dbName); +``` + +### Application Logs + +Application logs should use `createDebugLogger` with appropriate categories: + +```typescript +import { createDebugLogger } from '$utils/debugLogger'; + +const debugLog = createDebugLogger('featureName'); + +// Usage +debugLog.log('info', 'sync', 'Connection established', { userId }); +debugLog.log('error', 'network', 'Request failed', error); +debugLog.log('warn', 'timeline', 'Event missing', { roomId, eventId }); +``` + +### Debug Logs (Temporary) + +Temporary debug logs for development use consistent category prefixes: + +```typescript +console.log('[CATEGORY-DEBUG:component]', { data }); +``` + +**Examples:** + +- `[BADGE-DEBUG:SpaceTabs]` - Space badge calculation +- `[BADGE-DEBUG:getRoomsUnread]` - Unread map queries +- `[BADGE-DEBUG:init]` - Atom initialization + +**Important:** These should be removed before merging to `dev`. + +## Log Categories + +The `createDebugLogger` system uses these categories (defined in `debugLogger.ts`): + +- `sync` - Sync state, sliding sync, classic sync +- `network` - HTTP requests, API calls, connectivity +- `notification` - Push notifications, notification handling +- `message` - Message sending, receiving, rendering +- `call` - Voice/video calls, call widgets +- `ui` - UI interactions, rendering issues +- `timeline` - Timeline loading, pagination, events +- `search` - Search indexing, querying +- `idb` - IndexedDB operations +- `worker` - Web worker communication +- `storage` - LocalStorage, storage management +- `crypto` - E2E encryption, verification +- `media` - Media loading, thumbnails, attachments +- `auth` - Authentication, login, session management +- `error` - Unhandled errors, exceptions +- `general` - Uncategorized logs + +## Best Practices + +### DO + +✅ Use appropriate log levels: + +- `debug` - Verbose information for troubleshooting +- `info` - Normal operation, state changes +- `warn` - Unexpected but recoverable situations +- `error` - Errors that require attention + +✅ Include relevant context data: + +```typescript +debugLog.log('error', 'timeline', 'Failed to load events', { + roomId, + eventCount, + error: error.message, +}); +``` + +✅ Use descriptive messages: + +```typescript +// Good +console.debug('[SW] Prefetching sliding sync data...'); + +// Bad +console.debug('[SW] Fetch...'); +``` + +✅ Scrub sensitive data before logging: + +```typescript +// Use the scrubbing utilities from sentryScrubbers.ts +import { scrubMatrixIds, scrubMatrixUrl } from '$utils/sentryScrubbers'; +``` + +### DON'T + +❌ Log access tokens, passwords, or encryption keys +❌ Log full Matrix event content without scrubbing +❌ Use inconsistent prefixes (`[component]`, `[Component]`, `component:`) +❌ Log in tight loops without rate limiting +❌ Mix log utilities in the same file + +## Migration Guide + +### Migrating from Raw Console Logs + +**Before:** + +```typescript +console.log('Loading room', roomId); +console.warn('Failed to load:', error); +``` + +**After:** + +```typescript +const debugLog = createDebugLogger('roomLoader'); + +debugLog.log('info', 'timeline', 'Loading room', { roomId }); +debugLog.log('warn', 'timeline', 'Failed to load', { roomId, error }); +``` + +### Migrating from Simple Logger + +**Before:** + +```typescript +const log = createLogger('featureName'); +log.warn('Something went wrong'); +``` + +**After:** + +```typescript +const debugLog = createDebugLogger('featureName'); +debugLog.log('warn', 'general', 'Something went wrong'); +``` + +## Sentry Integration + +Logs from `createDebugLogger` automatically integrate with Sentry: + +- **Errors** - Always sent to Sentry as events +- **Warnings** - Sampled at 10% to avoid quota exhaustion +- **Info/Debug** - Included as breadcrumbs (can be filtered per-category in Settings) + +Users can control which categories create Sentry breadcrumbs in: +**Settings → Developer Tools → Error Tracking (Sentry)** + +## Temporary Debug Logging + +When adding temporary debug logs for investigating specific issues: + +1. Use a consistent `[CATEGORY-DEBUG:component]` prefix +2. Document the purpose in a comment +3. Remove before merging to `dev` (or create a follow-up task) + +Example: + +```typescript +// TODO: Remove after investigating space badge visibility issue +console.log('[BADGE-DEBUG:SpaceTabs]', { + spaceName: space.name, + allUnread, + loudUnread, +}); +``` + +## Performance Considerations + +- `createLogger` logs are no-op in production unless `sable_debug` is enabled +- `createDebugLogger` only logs errors/warnings in production unless `sable_internal_debug` is enabled +- Service Worker logs use `console.debug` which can be filtered in DevTools +- Sentry breadcrumbs have minimal overhead (~1-2ms per log) + +## Testing Logs + +When writing tests, you can suppress console output: + +```typescript +import { vi } from 'vitest'; + +vi.spyOn(console, 'log').mockImplementation(() => {}); +vi.spyOn(console, 'warn').mockImplementation(() => {}); +``` + +Or assert on log calls: + +```typescript +const logSpy = vi.spyOn(console, 'log'); +// ... perform action ... +expect(logSpy).toHaveBeenCalledWith('[BADGE-DEBUG:init]', expect.any(Object)); +``` + +## Examples from Codebase + +### Service Worker + +```typescript +// src/sw.ts +console.debug('[SW] Prefetching sliding sync data...'); +console.warn('[SW fetchRawEvent] HTTP', res.status, 'for', eventId); +``` + +### Matrix Client Init + +```typescript +// src/client/initMatrix.ts +const log = createLogger('initMatrix'); +const debugLog = createDebugLogger('initMatrix'); + +log.log('Starting Matrix client...'); +debugLog.log('info', 'sync', 'Client started', { transport }); +``` + +### Timeline Component + +```typescript +// Example component +const debugLog = createDebugLogger('Timeline'); + +useEffect(() => { + debugLog.log('info', 'timeline', 'Loading room timeline', { roomId }); + + try { + // ... load timeline ... + } catch (error) { + debugLog.log('error', 'timeline', 'Failed to load timeline', { roomId, error }); + } +}, [roomId]); +``` + +## Configuration + +### Enable Debug Logging + +**Simple Logger:** + +```javascript +localStorage.setItem('sable_debug', '1'); +location.reload(); +``` + +**Enhanced Logger:** + +```javascript +localStorage.setItem('sable_internal_debug', '1'); +location.reload(); +``` + +**Or via Settings UI:** +Settings → Developer Tools → Debug Logs → Enable Enhanced Debug Logger + +### Disable Sentry Breadcrumb Categories + +Settings → Developer Tools → Error Tracking (Sentry) → Breadcrumb Categories + +## References + +- `src/app/utils/debug.ts` - Simple logger implementation +- `src/app/utils/debugLogger.ts` - Enhanced logger implementation +- `src/app/utils/sentryScrubbers.ts` - Data scrubbing utilities +- `src/instrument.ts` - Sentry configuration +- `docs/SENTRY_INTEGRATION.md` - Sentry integration guide diff --git a/docs/SENTRY_INTEGRATION.md b/docs/SENTRY_INTEGRATION.md index df5511e574..da07e6b47d 100644 --- a/docs/SENTRY_INTEGRATION.md +++ b/docs/SENTRY_INTEGRATION.md @@ -150,7 +150,7 @@ Configure Sentry via environment variables: VITE_SENTRY_DSN=https://your-sentry-dsn@sentry.io/project-id # Required: Environment name - controls sampling rates -# - "production" = 10% trace/replay sampling (cost-effective for production) +# - "production" = 50% trace/replay sampling (balanced quota usage for production) # - "preview" = 100% trace/replay sampling (full debugging for PR previews) # - "development" = 100% trace/replay sampling (full debugging for local dev) VITE_SENTRY_ENVIRONMENT=production @@ -239,8 +239,8 @@ docker build \ **Production deployment (from `dev` branch):** - Set `VITE_SENTRY_ENVIRONMENT=production` -- Gets 10% sampling for traces and session replay -- Cost-effective for production usage +- Gets 50% sampling for traces and session replay +- Balanced quota usage for production - Configured in `.github/workflows/cloudflare-web-deploy.yml` **Preview deployments (PR previews, Cloudflare Pages):** @@ -261,7 +261,7 @@ docker build \ ``` Environment | Traces | Profiles | Session Replay | Error Replay ---------------|--------|----------|----------------|------------- -production | 10% | 10% | 10% | 100% +production | 50% | 50% | 50% | 100% preview | 100% | 100% | 100% | 100% development | 100% | 100% | 100% | 100% ``` @@ -363,8 +363,8 @@ the Sentry issue timeline and can be filtered in the developer settings panel. ### Sentry Configuration -- **Tracing sample rate**: 100% in development, 10% in production -- **Session replay sample rate**: 10% of all sessions, 100% of error sessions +- **Tracing sample rate**: 100% in development, 50% in production +- **Session replay sample rate**: 50% of all sessions, 100% of error sessions - **Warning capture rate**: 10% to avoid overwhelming Sentry - **Breadcrumb retention**: All breadcrumbs retained for context - **Log attachment limit**: Last 100 debug log entries diff --git a/docs/SENTRY_ISSUES_ANALYSIS.md b/docs/SENTRY_ISSUES_ANALYSIS.md new file mode 100644 index 0000000000..7cffb947d5 --- /dev/null +++ b/docs/SENTRY_ISSUES_ANALYSIS.md @@ -0,0 +1,165 @@ +# Sentry Issues Analysis & Fixes + +Analysis and fixes for active Sentry issues. Commit: `514f067c6` + +--- + +## Issue 1: Presence Rate Limiting (SABLE-59) ✅ FIXED + +### Problem + +PUT requests to `/_matrix/client/v3/presence/{userId}/status` returning HTTP 429 Too Many Requests. + +### Root Cause + +The existing code had 429 detection and retry-after parsing, but **failed to retry** after the backoff. When a 429 occurred: + +1. Error caught ✓ +2. Retry-After header parsed ✓ +3. Sleep for backoff duration ✓ +4. Update `lastSentTimestamp` ✓ +5. **Return immediately** ✗ ← presence update was lost + +### Fix + +Added retry loop with exponential backoff (up to 3 retries): + +- Detects 429 responses +- Respects `retry_after_ms` from server response +- Retries the request after backoff +- Updates Sentry telemetry with retry count +- Falls back to 5000ms if server doesn't provide retry-after + +**Code location**: [src/app/hooks/usePresenceSync.ts](src/app/hooks/usePresenceSync.ts#L342-L386) + +**Behavior**: + +- Retry 1: Wait for server-provided `retry_after_ms`, retry +- Retry 2: Wait again, retry +- Retry 3: Wait again, retry +- If all retries exhausted: log error and give up (prevents infinite loop) + +--- + +## Issue 2: Worker MIME Type Error (SABLE-5K) ⚠️ NO CODE CHANGES + +### Problem + +`TypeError: 'text/html' is not a valid JavaScript MIME type` when loading the search worker. + +### Root Cause + +**Stale cached index.html** referencing old worker URLs after deployment. When: + +1. New version deployed with updated worker script paths +2. User has old `index.html` cached in browser +3. Old HTML tries to load worker from old path → 404 +4. Server returns HTML error page instead of JS +5. Browser tries to parse HTML as JavaScript → MIME type error + +### Analysis + +Existing error handling is appropriate: + +- Worker instantiation wrapped in try-catch ✓ +- MIME errors detected and flagged ✓ +- Sentry telemetry includes detailed context ✓ +- Error doesn't crash app (search just unavailable) ✓ + +**Code location**: [src/app/hooks/useSearchIndex.tsx](src/app/hooks/useSearchIndex.tsx#L662-L703) + +### Recommendation + +**This is a cache invalidation issue, not a code bug**. Cannot be fixed in code. + +**User mitigation**: Hard reload (Cmd+Shift+R / Ctrl+Shift+R) + +**Server mitigation options** (not implemented in this commit): + +1. Set aggressive cache headers on `index.html` (`Cache-Control: no-cache`) +2. Implement service worker cache busting for HTML +3. Add build-time hash to worker script URLs (Vite already does this via `?worker` suffix) + +--- + +## Issue 3: Crypto Store IndexedDB Errors ⚠️ MITIGATED (SDK-level issue) + +### Problem + +Intermittent IndexedDB transaction errors causing `/sync` to fail: + +1. `DomException UnknownError (0): Attempt to get a record from database without an in-progress transaction` +2. `DomException InvalidStateError (11): Failed to execute 'transaction' on 'IDBDatabase': The database connection is closed` + +### Root Cause + +Errors originate in the **matrix-rust-sdk-crypto WASM layer** (part of matrix-js-sdk), which we don't control. Suspected race conditions: + +- Transaction timing issues in WASM crypto operations +- IDB connection closed prematurely during crypto operations +- Concurrent crypto store access from multiple async paths + +### Fix + +**Enhanced error detection and telemetry**: + +- Added crypto-store-specific error detection in sync error handlers (both classic and sliding sync) +- Errors are now categorized and tagged in Sentry for better debugging +- Metrics track `crypto_store_error: true` for filtering +- Extra context includes recovery recommendations + +**Code locations**: + +- Classic sync: [src/client/initMatrix.ts](src/client/initMatrix.ts#L663-L712) +- Sliding sync: [src/client/slidingSync.ts](src/client/slidingSync.ts#L469-L515) + +### Behavior + +When these errors occur: + +1. Detected via error message pattern matching +2. Logged to Sentry with `component: crypto-store` tag +3. Sync enters `SyncState.Error` or `SyncState.Reconnecting` +4. Matrix SDK automatically retries sync after backoff +5. User sees temporary connection disruption, then recovers + +### Limitations + +**Cannot fix root cause** (SDK-level WASM issue). This fix only improves visibility and monitoring. + +### Potential Future Work + +If errors persist at high volume, consider: + +1. Matrix SDK upgrade (check if newer versions fix WASM crypto issues) +2. Force sync restart on repeated crypto store errors +3. Add IDB connection health checks before crypto operations +4. Report to matrix-js-sdk maintainers with Sentry data + +--- + +## Testing Recommendations + +### Presence Rate Limiting + +Monitor Sentry for: + +- `Presence rate limited` message frequency +- Check `retryCount` in extra context (should be 1-3) +- Verify successful retry after backoff (no more "dropped update" patterns) + +### Crypto Store Errors + +Monitor Sentry for: + +- `Crypto store IndexedDB error during sync` message +- Check `error_type` tag (transaction_error vs connection_closed) +- Verify sync recovery (users shouldn't get stuck in error state) + +### Worker MIME Type + +Monitor Sentry for: + +- `Search worker failed to instantiate` errors +- Check `is_mime_error: true` and `likely_stale_cache: true` +- High spike after deployments suggests cache invalidation strategy needed diff --git a/docs/SENTRY_PRIVACY.md b/docs/SENTRY_PRIVACY.md index 39be916ce7..a268195799 100644 --- a/docs/SENTRY_PRIVACY.md +++ b/docs/SENTRY_PRIVACY.md @@ -231,7 +231,7 @@ Sample rates for replay: | Trigger | Production | Preview / Dev | | -------------------- | ---------- | ------------- | -| Regular sessions | 10% | 100% | +| Regular sessions | 50% | 100% | | Sessions with errors | 100% | 100% | **Code:** `src/instrument.ts` — `replayIntegration` call with `maskAllText`, diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000000..24204b41f4 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,181 @@ +import path from 'node:path'; + +import { includeIgnoreFile } from '@eslint/compat'; +import js from '@eslint/js'; +import { defineConfig } from 'eslint/config'; +import { configs, helpers, plugins } from 'eslint-config-airbnb-extended'; +import { rules as prettierConfigRules } from 'eslint-config-prettier'; +import prettierPlugin from 'eslint-plugin-prettier'; +import reactPlugin from 'eslint-plugin-react'; +import globals from 'globals'; + +const gitignorePath = path.resolve('.', '.gitignore'); +const { jsFiles, tsFiles } = helpers.extensions; + +const jsConfig = defineConfig([ + // ESLint recommended config + { + name: 'js/config', + ...js.configs.recommended, + }, + // Stylistic plugin + plugins.stylistic, + // Import X plugin + plugins.importX, + // Airbnb base recommended config + ...configs.base.recommended, +]); + +const reactConfig = defineConfig([ + // React plugin + plugins.react, + // React hooks plugin + plugins.reactHooks, + // React JSX A11y plugin + plugins.reactA11y, + // Airbnb React recommended config + ...configs.react.recommended, + // React 17+ automatic JSX runtime + reactPlugin.configs.flat['jsx-runtime'], +]); + +const typescriptConfig = defineConfig([ + // TypeScript ESLint plugin + plugins.typescriptEslint, + // Airbnb base TypeScript config + ...configs.base.typescript, + // Airbnb React TypeScript config + ...configs.react.typescript, +]); + +const prettierConfig = defineConfig([ + // Prettier plugin + { + name: 'prettier/plugin/config', + plugins: { + prettier: prettierPlugin, + }, + }, + // Prettier config + { + name: 'prettier/config', + rules: { + ...prettierConfigRules, + 'prettier/prettier': 'error', + }, + }, +]); + +const projectOverrides = defineConfig([ + { + name: 'project/rule-overrides', + files: [...jsFiles, ...tsFiles], + languageOptions: { + globals: { + JSX: 'readonly', + ...globals.browser, + }, + }, + rules: { + 'linebreak-style': 'off', + 'no-underscore-dangle': 'off', + 'no-shadow': 'off', + 'import-x/prefer-default-export': 'off', + 'import-x/extensions': 'off', + 'import-x/no-unresolved': 'off', + 'import-x/no-extraneous-dependencies': [ + 'error', + { + devDependencies: true, + optionalDependencies: false, + peerDependencies: true, + bundledDependencies: true, + }, + ], + 'react/no-unstable-nested-components': ['error', { allowAsProps: true }], + 'react/jsx-filename-extension': [ + 'error', + { + extensions: ['.tsx', '.jsx'], + }, + ], + // obsolete in a React 19 + 'react/prop-types': 'off', + 'react/require-default-props': 'off', + 'react/jsx-props-no-spreading': 'off', + 'react-hooks/rules-of-hooks': 'error', + 'react-hooks/exhaustive-deps': 'error', + }, + }, + { + name: 'project/fetch-wrapper-enforcement', + files: ['src/**/*.{js,jsx,ts,tsx}'], + ignores: ['src/sw.ts', 'src/**/*.test.*', 'src/app/utils/fetch.ts'], + rules: { + 'no-restricted-globals': [ + 'error', + { + name: 'fetch', + message: 'Import fetch from $utils/fetch.', + }, + ], + 'no-restricted-properties': [ + 'error', + { + object: 'window', + property: 'fetch', + message: 'Import fetch from $utils/fetch.', + }, + { + object: 'globalThis', + property: 'fetch', + message: 'Import fetch from $utils/fetch.', + }, + ], + 'no-restricted-imports': [ + 'error', + { + paths: [ + { + name: '@tauri-apps/plugin-http', + message: 'Use $utils/fetch instead of importing plugin-http directly.', + }, + ], + }, + ], + }, + }, + { + name: 'project/typescript-rule-overrides', + files: tsFiles, + rules: { + // disabled for now to get eslint to pass + '@typescript-eslint/consistent-type-definitions': 'off', + '@typescript-eslint/no-unsafe-enum-comparison': 'off', + '@typescript-eslint/only-throw-error': 'off', + '@typescript-eslint/array-type': 'off', + '@typescript-eslint/no-unused-vars': [ + 'error', + { + args: 'after-used', + enableAutofixRemoval: { + imports: true, + }, + ignoreRestSiblings: true, + vars: 'all', + }, + ], + '@typescript-eslint/no-shadow': 'error', + 'no-undef': 'off', + }, + }, +]); + +export default defineConfig([ + includeIgnoreFile(gitignorePath), + ...jsConfig, + ...reactConfig, + ...typescriptConfig, + ...prettierConfig, + ...projectOverrides, +]); diff --git a/flake.lock b/flake.lock index 536adabd72..a70fe3c7aa 100644 --- a/flake.lock +++ b/flake.lock @@ -1,89 +1,105 @@ { "nodes": { - "flake-compat": { - "flake": false, + "android-nixpkgs": { + "inputs": { + "devshell": "devshell", + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + }, "locked": { - "lastModified": 1767039857, - "narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=", - "owner": "NixOS", - "repo": "flake-compat", - "rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab", + "lastModified": 1773434018, + "narHash": "sha256-g/OmqUpsyUg3lw6QNkJudtsE2XVydkCKq724KIrr4no=", + "owner": "tadfisher", + "repo": "android-nixpkgs", + "rev": "b12e333a75f25fde82adad28acf47f57eb1b3dc4", "type": "github" }, "original": { - "owner": "NixOS", - "repo": "flake-compat", + "owner": "tadfisher", + "repo": "android-nixpkgs", "type": "github" } }, - "flake-parts": { + "devshell": { "inputs": { - "nixpkgs-lib": "nixpkgs-lib" + "nixpkgs": [ + "android-nixpkgs", + "nixpkgs" + ] }, "locked": { - "lastModified": 1777988971, - "narHash": "sha256-qIoWPDs+0/8JecyYgE3gpKQxW/4bLW/gp45vow9ioCQ=", - "owner": "hercules-ci", - "repo": "flake-parts", - "rev": "0678d8986be1661af6bb555f3489f2fdfc31f6ff", + "lastModified": 1768818222, + "narHash": "sha256-460jc0+CZfyaO8+w8JNtlClB2n4ui1RbHfPTLkpwhU8=", + "owner": "numtide", + "repo": "devshell", + "rev": "255a2b1725a20d060f566e4755dbf571bbbb5f76", "type": "github" }, "original": { - "owner": "hercules-ci", - "repo": "flake-parts", + "owner": "numtide", + "repo": "devshell", "type": "github" } }, - "git-hooks": { + "flake-utils": { "inputs": { - "flake-compat": "flake-compat", - "gitignore": "gitignore", - "nixpkgs": [ - "nixpkgs" - ] + "systems": "systems" }, "locked": { - "lastModified": 1776796298, - "narHash": "sha256-PcRvlWayisPSjd0UcRQbhG8Oqw78AcPE6x872cPRHN8=", - "owner": "cachix", - "repo": "git-hooks.nix", - "rev": "3cfd774b0a530725a077e17354fbdb87ea1c4aad", + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", "type": "github" }, "original": { - "owner": "cachix", - "repo": "git-hooks.nix", + "owner": "numtide", + "repo": "flake-utils", "type": "github" } }, - "gitignore": { + "flake-utils_2": { "inputs": { - "nixpkgs": [ - "git-hooks", - "nixpkgs" - ] + "systems": "systems_2" }, "locked": { - "lastModified": 1709087332, - "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=", - "owner": "hercules-ci", - "repo": "gitignore.nix", - "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", "type": "github" }, "original": { - "owner": "hercules-ci", - "repo": "gitignore.nix", + "owner": "numtide", + "repo": "flake-utils", "type": "github" } }, "nixpkgs": { "locked": { - "lastModified": 1777954456, - "narHash": "sha256-hGdgeU2Nk87RAuZyYjyDjFL6LK7dAZN5RE9+hrDTkDU=", + "lastModified": 1773282481, + "narHash": "sha256-b/GV2ysM8mKHhinse2wz+uP37epUrSE+sAKXy/xvBY4=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "fe416aaedd397cacb33a610b33d60ff2b431b127", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_2": { + "locked": { + "lastModified": 1773282481, + "narHash": "sha256-b/GV2ysM8mKHhinse2wz+uP37epUrSE+sAKXy/xvBY4=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "549bd84d6279f9852cae6225e372cc67fb91a4c1", + "rev": "fe416aaedd397cacb33a610b33d60ff2b431b127", "type": "github" }, "original": { @@ -93,46 +109,75 @@ "type": "github" } }, - "nixpkgs-lib": { + "nixpkgs_3": { "locked": { - "lastModified": 1777168982, - "narHash": "sha256-GOkGPcboWE9BmGCRMLX3worL4EMnsnG8MyKmXNeYuhQ=", - "owner": "nix-community", - "repo": "nixpkgs.lib", - "rev": "f5901329dade4a6ea039af1433fb087bd9c1fe14", + "lastModified": 1744536153, + "narHash": "sha256-awS2zRgF4uTwrOKwwiJcByDzDOdo3Q1rPZbiHQg/N38=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "18dd725c29603f582cf1900e0d25f9f1063dbf11", "type": "github" }, "original": { - "owner": "nix-community", - "repo": "nixpkgs.lib", + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", "type": "github" } }, "root": { "inputs": { - "flake-parts": "flake-parts", - "git-hooks": "git-hooks", - "nixpkgs": "nixpkgs", - "treefmt-nix": "treefmt-nix" + "android-nixpkgs": "android-nixpkgs", + "flake-utils": "flake-utils_2", + "nixpkgs": "nixpkgs_2", + "rust-overlay": "rust-overlay" } }, - "treefmt-nix": { + "rust-overlay": { "inputs": { - "nixpkgs": [ - "nixpkgs" - ] + "nixpkgs": "nixpkgs_3" }, "locked": { - "lastModified": 1775636079, - "narHash": "sha256-pc20NRoMdiar8oPQceQT47UUZMBTiMdUuWrYu2obUP0=", - "owner": "numtide", - "repo": "treefmt-nix", - "rev": "790751ff7fd3801feeaf96d7dc416a8d581265ba", + "lastModified": 1773371126, + "narHash": "sha256-SGnZQO8hnynR90Lo/1MVrTScsOPx9i26XjqSqoFOZ4E=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "475826b105eb52f39bd3281f60c052299e64d085", "type": "github" }, "original": { - "owner": "numtide", - "repo": "treefmt-nix", + "owner": "oxalica", + "repo": "rust-overlay", + "type": "github" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "systems_2": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", "type": "github" } } diff --git a/flake.nix b/flake.nix index c19856c22d..84c7fd5365 100644 --- a/flake.nix +++ b/flake.nix @@ -1,181 +1,136 @@ -# SPDX-License-Identifier: AGPL-3.0 +# https://github.com/PleahMaCaka/svelte-tauri-template/blob/a598c33a34489b07680e14bb0d82ac01b0a9a4be/flake.nix { + description = "sable dev environment"; + inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; - flake-parts.url = "github:hercules-ci/flake-parts"; - git-hooks.url = "github:cachix/git-hooks.nix"; - git-hooks.inputs.nixpkgs.follows = "nixpkgs"; - treefmt-nix.url = "github:numtide/treefmt-nix"; - treefmt-nix.inputs.nixpkgs.follows = "nixpkgs"; + flake-utils.url = "github:numtide/flake-utils"; + rust-overlay.url = "github:oxalica/rust-overlay"; + android-nixpkgs.url = "github:tadfisher/android-nixpkgs"; }; - outputs = - inputs@{ flake-parts, ... }: - flake-parts.lib.mkFlake { inherit inputs; } { - imports = [ - inputs.git-hooks.flakeModule - inputs.treefmt-nix.flakeModule - ]; - - systems = [ - "x86_64-linux" - "aarch64-linux" - "x86_64-darwin" - "aarch64-darwin" - ]; - - perSystem = - { - config, - pkgs, - lib, - ... - }: - let - self = inputs.self; + nixConfig = { + bash-prompt = "\\[\\e[1;35m\\]sable\\[\\e[0m\\]"; # bold magenta + bash-prompt-suffix = " \\[\\e[0;36m\\]>\\[\\e[0m\\] "; # cyan > + }; - packageJson = builtins.fromJSON (builtins.readFile ./package.json); + outputs = + { + nixpkgs, + flake-utils, + rust-overlay, + android-nixpkgs, + ... + }: + flake-utils.lib.eachDefaultSystem ( + system: + let + pkgs = import nixpkgs { + inherit system; + overlays = [ rust-overlay.overlays.default ]; + config = { + android_sdk.accept_license = true; + allowUnfree = true; + }; + }; - nodejs = pkgs.nodejs_24; - pnpm = pkgs.pnpm_10; - pnpmConfigHook = pkgs.pnpmConfigHook.override { inherit pnpm; }; + rust = pkgs.rust-bin.stable.latest.default.override { + targets = [ + "aarch64-linux-android" + "armv7-linux-androideabi" + "i686-linux-android" + "x86_64-linux-android" + ]; + }; - pnpmNativeBuildInputs = [ + androidSdk = android-nixpkgs.sdk.${system} ( + sdkPkgs: with sdkPkgs; [ + build-tools-34-0-0 + platforms-android-34 + ndk-26-1-10909125 + cmdline-tools-latest + platform-tools + emulator + ] + ); + + baseShell = { + nativeBuildInputs = with pkgs; [ + pkg-config + gobject-introspection + cargo + cargo-tauri nodejs - pnpm - pnpmConfigHook + xdg-utils + desktop-file-utils ]; - mkPnpmDeps = - { - src, - version, - pnpmInstallFlags, - }: - pkgs.fetchPnpmDeps { - inherit - pnpm - src - version - pnpmInstallFlags - ; - pname = "sable"; - fetcherVersion = 3; - hash = "sha256-IJrBo2/PsHiMBbN7eUu46U6V8flL9KYFDphz5cirfrU="; - }; - - mkPnpmCheck = - name: script: - pkgs.stdenv.mkDerivation (finalAttrs: { - pname = "sable-${name}"; - inherit (packageJson) version; - src = lib.cleanSource ./.; - - pnpmInstallFlags = [ "--ignore-scripts" ]; - - pnpmDeps = mkPnpmDeps { - inherit (finalAttrs) src version pnpmInstallFlags; - }; - - nativeBuildInputs = pnpmNativeBuildInputs; - - buildPhase = '' - runHook preBuild - pnpm run ${script} - runHook postBuild - ''; + buildInputs = with pkgs; [ + at-spi2-atk + atkmm + cairo + gdk-pixbuf + glib + glib-networking + gtk3 + gsettings-desktop-schemas + harfbuzz + librsvg + libsoup_3 + pango + webkitgtk_4_1 + openssl + libayatana-appindicator + gst_all_1.gstreamer + gst_all_1.gst-plugins-base + gst_all_1.gst-plugins-good + gst_all_1.gst-plugins-bad + gst_all_1.gst-plugins-ugly + gst_all_1.gst-libav + ]; - installPhase = '' - runHook preInstall - touch $out - runHook postInstall - ''; + packages = with pkgs; [ + coreutils + nix + pnpm + rust + ]; - doCheck = false; - }); - in - { - treefmt = { - projectRootFile = "flake.nix"; - programs = { - nixfmt.enable = true; - oxfmt.enable = true; - }; - settings.global.excludes = [ - "dist" - "node_modules" - "pnpm-lock.yaml" - "pnpm-workspace.yaml" - "package.json" - "LICENSE" - "CHANGELOG.md" - "./changeset" + shellHook = '' + export XDG_DATA_DIRS="$GSETTINGS_SCHEMAS_PATH:$XDG_DATA_DIRS" + export GIO_EXTRA_MODULES="${pkgs.glib-networking}/lib/gio/modules" + export GST_PLUGIN_SYSTEM_PATH="${pkgs.gst_all_1.gst-plugins-base}/lib/gstreamer-1.0:${pkgs.gst_all_1.gst-plugins-good}/lib/gstreamer-1.0:${pkgs.gst_all_1.gst-plugins-bad}/lib/gstreamer-1.0" + alias tauri="pnpm exec tauri" + echo "Done! Now make sable stable :3"; + ''; + }; + in + { + devShells = { + default = pkgs.mkShell { + name = "sable dev environment"; + inherit (baseShell) nativeBuildInputs buildInputs packages shellHook; + + LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath [ + pkgs.libayatana-appindicator ]; }; - pre-commit.settings.hooks = { - treefmt = { - enable = true; - package = config.treefmt.build.wrapper; - }; - }; - - packages.sable = pkgs.stdenv.mkDerivation (finalAttrs: { - pname = "sable"; - inherit (packageJson) version; - src = lib.cleanSource ./.; - - # ignoring knope for building - pnpmInstallFlags = [ "--ignore-scripts" ]; - - pnpmDeps = mkPnpmDeps { - inherit (finalAttrs) src version pnpmInstallFlags; - }; - nativeBuildInputs = pnpmNativeBuildInputs; + android = pkgs.mkShell { + name = "sable dev environment (android)"; + inherit (baseShell) nativeBuildInputs buildInputs; # <-- shellHook removed here - env.VITE_BUILD_HASH = self.shortRev or self.dirtyShortRev or ""; - env.VITE_IS_RELEASE_TAG = "false"; + packages = baseShell.packages ++ (with pkgs; [ + jdk + androidSdk + ]); - buildPhase = '' - runHook preBuild - pnpm run build - runHook postBuild + shellHook = baseShell.shellHook + '' + export NDK_HOME=${androidSdk}/share/android-sdk/ndk/26.1.10909125 + echo "Android SDK ready!"; ''; - - installPhase = '' - runHook preInstall - cp -r dist $out - runHook postInstall - ''; - }); - - packages.default = config.packages.sable; - - checks = { - build = config.packages.sable; - lint = mkPnpmCheck "lint" "lint"; - fmt = mkPnpmCheck "fmt" "fmt:check"; - test = mkPnpmCheck "test" "test:run"; - typecheck = mkPnpmCheck "typecheck" "typecheck"; - knip = mkPnpmCheck "knip" "knip"; - }; - - devShells.default = pkgs.mkShell { - packages = [ - nodejs - pnpm - pkgs.corepack - pkgs.vitejs - pkgs.oxlint - pkgs.oxfmt - pkgs.knope - pkgs.typescript - pkgs.typescript-language-server - pkgs.nil - pkgs.nixd - ]; - shellHook = config.pre-commit.installationScript; }; }; - }; + } + ); } diff --git a/index.html b/index.html index 5cca4f0388..60120a1625 100644 --- a/index.html +++ b/index.html @@ -25,6 +25,79 @@ /> + + @@ -116,6 +189,20 @@ sizes="512x512" href="./public/res/logo-maskable/logo-maskable-512x512.png" /> + + + + + + + + @@ -123,7 +210,6 @@ window.global ||= window;
-
diff --git a/infra/web/variables.tf b/infra/web/variables.tf index 3569c9822b..8ddd72ae44 100644 --- a/infra/web/variables.tf +++ b/infra/web/variables.tf @@ -7,7 +7,7 @@ variable "account_id" { variable "custom_domain" { description = "Custom domain attached to the Worker" type = string - default = "app.sable.moe" + default = "dev.cloudhub.social" } variable "worker_name" { diff --git a/knip.json b/knip.json index 6cc8c8581c..1c2f48a61f 100644 --- a/knip.json +++ b/knip.json @@ -1,11 +1,12 @@ { "$schema": "https://unpkg.com/knip@5/schema.json", - "entry": ["src/sw.ts", "scripts/normalize-imports.js"], - "ignore": ["oxlint.config.ts", "oxfmt.config.ts"], + "entry": ["src/sw.ts", "scripts/**/*.js"], "ignoreExportsUsedInFile": { "interface": true, "type": true }, + "ignore": ["eslint.config.js", "oxlint.config.ts", "oxfmt.config.ts"], + "ignoreFiles": ["src/app/generated/**/*"], "ignoreDependencies": [ "buffer", "@sableclient/sable-call-embedded", diff --git a/knope.toml b/knope.toml index fc533824c8..8c3fea4097 100644 --- a/knope.toml +++ b/knope.toml @@ -1,5 +1,5 @@ [package] -versioned_files = ["package.json"] +versioned_files = ["package.json", "src-tauri/tauri.conf.json"] changelog = "CHANGELOG.md" extra_changelog_sections = [ { name = "Documentation", types = [ @@ -62,7 +62,7 @@ help_text = "Create a new change file to be included in the next release" type = "CreateChangeFile" [github] -owner = "SableClient" +owner = "Just-Insane" repo = "Sable" [release_notes] diff --git a/package.json b/package.json index 8288680305..a515f8f8a1 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,9 @@ "fmt": "oxfmt .", "fmt:check": "oxfmt --check .", "typecheck": "tsc", + "tauri": "tauri", + "tauri:cef": "node scripts/tauri.js cef", + "tauri:wry": "node scripts/tauri.js wry", "test": "vitest", "test:ui": "vitest --ui", "test:run": "vitest run", @@ -25,7 +28,10 @@ "tunnel": "cloudflared tunnel --url http://localhost:8080", "knope": "knope", "document-change": "knope document-change", - "postinstall": "node scripts/install-knope.js" + "postinstall": "node scripts/install-knope.js", + "setup:knope": "node scripts/install-knope.js", + "setup:tauri-icons": "node scripts/symlink-tauri-icons.js", + "lint:normalize-imports": "node scripts/normalize-imports.js" }, "keywords": [], "author": "7w1", @@ -38,10 +44,23 @@ "@fontsource-variable/nunito": "5.2.7", "@fontsource/space-mono": "5.2.9", "@phosphor-icons/react": "^2.1.10", + "@sableclient/tauri-plugin-notifications-api": "git+https://github.com/SableClient/tauri-plugin-notifications.git#9d15b2b2c8798dc29ce723d743f0f3b4921d6a54", "@sentry/react": "^10.43.0", "@tanstack/react-query": "^5.90.21", "@tanstack/react-query-devtools": "^5.91.3", "@tanstack/react-virtual": "^3.13.19", + "@tiptap/core": "3.23.4", + "@tiptap/extension-link": "^3.23.4", + "@tiptap/extension-mention": "^3.23.4", + "@tiptap/extension-placeholder": "^3.23.4", + "@tiptap/react": "^3.23.4", + "@tiptap/starter-kit": "^3.23.4", + "@tauri-apps/api": "2.11.0", + "@tauri-apps/plugin-deep-link": "2.4.9", + "@tauri-apps/plugin-http": "2.5.9", + "@tauri-apps/plugin-opener": "2.5.4", + "@tauri-apps/plugin-os": "2.3.2", + "@tauri-apps/plugin-store": "^2.4.3", "@use-gesture/react": "10.3.1", "@vanilla-extract/css": "^1.18.0", "@vanilla-extract/recipes": "^0.5.7", @@ -61,7 +80,6 @@ "file-saver": "^2.0.5", "focus-trap-react": "^10.3.1", "folds": "^2.6.2", - "framer-motion": "12.34.3", "html-dom-parser": "^5.1.8", "html-react-parser": "^4.2.10", "i18next": "^25.8.13", @@ -76,6 +94,8 @@ "marked": "^18.0.2", "matrix-js-sdk": "^38.4.0", "matrix-widget-api": "^1.16.1", + "minisearch": "7.2.0", + "motion": "12.35.0", "pdfjs-dist": "^5.4.624", "react": "^18.3.1", "react-aria": "^3.46.0", @@ -101,6 +121,7 @@ "@rollup/plugin-wasm": "^6.2.2", "@sableclient/sable-call-embedded": "v1.1.4", "@sentry/vite-plugin": "^5.1.1", + "@tauri-apps/cli": "2.11.0", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", "@testing-library/user-event": "^14.6.1", @@ -132,4 +153,4 @@ "vitest": "^4.1.0", "wrangler": "^4.70.0" } -} \ No newline at end of file +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ee78361853..be877cb0e5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -39,6 +39,9 @@ importers: '@phosphor-icons/react': specifier: ^2.1.10 version: 2.1.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@sableclient/tauri-plugin-notifications-api': + specifier: git+https://github.com/SableClient/tauri-plugin-notifications.git#9d15b2b2c8798dc29ce723d743f0f3b4921d6a54 + version: https://codeload.github.com/SableClient/tauri-plugin-notifications/tar.gz/9d15b2b2c8798dc29ce723d743f0f3b4921d6a54 '@sentry/react': specifier: ^10.43.0 version: 10.43.0(react@18.3.1) @@ -51,6 +54,42 @@ importers: '@tanstack/react-virtual': specifier: ^3.13.19 version: 3.13.21(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@tauri-apps/api': + specifier: 2.11.0 + version: 2.11.0 + '@tauri-apps/plugin-deep-link': + specifier: 2.4.9 + version: 2.4.9 + '@tauri-apps/plugin-http': + specifier: 2.5.9 + version: 2.5.9 + '@tauri-apps/plugin-opener': + specifier: 2.5.4 + version: 2.5.4 + '@tauri-apps/plugin-os': + specifier: 2.3.2 + version: 2.3.2 + '@tauri-apps/plugin-store': + specifier: ^2.4.3 + version: 2.4.3 + '@tiptap/core': + specifier: 3.23.4 + version: 3.23.4(@tiptap/pm@3.23.4) + '@tiptap/extension-link': + specifier: ^3.23.4 + version: 3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4) + '@tiptap/extension-mention': + specifier: ^3.23.4 + version: 3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4)(@tiptap/suggestion@3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4)) + '@tiptap/extension-placeholder': + specifier: ^3.23.4 + version: 3.23.4(@tiptap/extensions@3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4)) + '@tiptap/react': + specifier: ^3.23.4 + version: 3.23.4(@floating-ui/dom@1.7.6)(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@tiptap/starter-kit': + specifier: ^3.23.4 + version: 3.23.4 '@use-gesture/react': specifier: 10.3.1 version: 10.3.1(react@18.3.1) @@ -108,9 +147,6 @@ importers: folds: specifier: ^2.6.2 version: 2.6.2(@vanilla-extract/css@1.18.0)(@vanilla-extract/recipes@0.5.7(@vanilla-extract/css@1.18.0))(classnames@2.5.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - framer-motion: - specifier: 12.34.3 - version: 12.34.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) html-dom-parser: specifier: ^5.1.8 version: 5.1.8 @@ -153,6 +189,12 @@ importers: matrix-widget-api: specifier: ^1.16.1 version: 1.17.0 + minisearch: + specifier: 7.2.0 + version: 7.2.0 + motion: + specifier: 12.35.0 + version: 12.35.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) pdfjs-dist: specifier: ^5.4.624 version: 5.5.207 @@ -223,6 +265,9 @@ importers: '@sentry/vite-plugin': specifier: ^5.1.1 version: 5.1.1(rollup@4.59.0) + '@tauri-apps/cli': + specifier: 2.11.0 + version: 2.11.0 '@testing-library/jest-dom': specifier: ^6.9.1 version: 6.9.1 @@ -1154,6 +1199,15 @@ packages: '@noble/hashes': optional: true + '@floating-ui/core@1.7.5': + resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} + + '@floating-ui/dom@1.7.6': + resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==} + + '@floating-ui/utils@0.2.11': + resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} + '@fontsource-variable/nunito@5.2.7': resolution: {integrity: sha512-2N8QhatkyKgSUbAGZO2FYLioxA32+RyI1EplVLawbpkGjUeui9Qg9VMrpkCaik1ydjFjfLV+kzQ0cGEsMrMenQ==} @@ -2624,6 +2678,10 @@ packages: '@sableclient/sable-call-embedded@1.1.4': resolution: {integrity: sha512-XLRcbUPcn7i3QKZAPjIfUkUEXP0E4DOr0dyRoVCWMjHWj28kq+T7jeB2fRr5lB77olBwNHMjIuoTwrv02xiepQ==} + '@sableclient/tauri-plugin-notifications-api@https://codeload.github.com/SableClient/tauri-plugin-notifications/tar.gz/9d15b2b2c8798dc29ce723d743f0f3b4921d6a54': + resolution: {tarball: https://codeload.github.com/SableClient/tauri-plugin-notifications/tar.gz/9d15b2b2c8798dc29ce723d743f0f3b4921d6a54} + version: 0.4.3 + '@sentry-internal/browser-utils@10.43.0': resolution: {integrity: sha512-8zYTnzhAPvNkVH1Irs62wl0J/c+0QcJ62TonKnzpSFUUD3V5qz8YDZbjIDGfxy+1EB9fO0sxtddKCzwTHF/MbQ==} engines: {node: '>=18'} @@ -2916,6 +2974,100 @@ packages: '@tanstack/virtual-core@3.13.21': resolution: {integrity: sha512-ww+fmLHyCbPSf7JNbWZP3g7wl6SdNo3ah5Aiw+0e9FDErkVHLKprYUrwTm7dF646FtEkN/KkAKPYezxpmvOjxw==} + '@tauri-apps/api@2.11.0': + resolution: {integrity: sha512-7CinYODhky9lmO23xHnUFv0Xt43fbtWMyxZcLcRBlFkcgXKuEirBvHpmtJ89YMhyeGcq20Wuc47Fa4XjyniywA==} + + '@tauri-apps/cli-darwin-arm64@2.11.0': + resolution: {integrity: sha512-UfMeDNlgIP252rm/KSTuu8yHatPua5TjtUEUf+jyIzVwBNcIl7Ywkdpfj+e5jVVg3EfCTp+4gwuL1dNpgF8clg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@tauri-apps/cli-darwin-x64@2.11.0': + resolution: {integrity: sha512-lY1+aPlgyMN7vgjtCdQ3+WODfZkebAcxnrCrO0HjqDpKSXieDkrJbimqeaoM4RwhTSrCLRHfVYiYrfE5E131tg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@tauri-apps/cli-linux-arm-gnueabihf@2.11.0': + resolution: {integrity: sha512-5uCP0AusgN3NrKC8EpkuJwjek1k8pEffBdugJSpXPey/QGbPEb8vZ542n/giJ2mZPjMSllDkdhG2QIDpBY4PpQ==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@tauri-apps/cli-linux-arm64-gnu@2.11.0': + resolution: {integrity: sha512-loDPqtRHMSbIcrH2VBd4GgHoQlF7jJnrZj7MxA2lj1cixS/jEgMAPFqj83U6Wvjete4HfYplbE/gCpSFifA9jw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@tauri-apps/cli-linux-arm64-musl@2.11.0': + resolution: {integrity: sha512-DtSE8ZBlB9H+L+eHkfZ3myt00EVEyAB3e41juEHoE2qT88fgVlJvyrwa9SZYc/xTwCS9TnmK+R84tpg+ZsAg7Q==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@tauri-apps/cli-linux-riscv64-gnu@2.11.0': + resolution: {integrity: sha512-5QdgS4LD+kntClI1aj2JmwjW38LosNXxwCe8viIHEwqYIWuMPdNEIau6/cLogI38Yzx9DnfCPRfEWLyI+5li8Q==} + engines: {node: '>= 10'} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@tauri-apps/cli-linux-x64-gnu@2.11.0': + resolution: {integrity: sha512-5UynPXo3Zq9khjVdAbD+YogeLltdVUeOah2ioSIM3tu6H7wY9vMy6rgGJhv9r5R8ZXmk9GttMippdqYJWrnLnA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@tauri-apps/cli-linux-x64-musl@2.11.0': + resolution: {integrity: sha512-CNz7fHbApz1Zyhhq73jtGn9JqgNEV/lIWnTnUo6h6ujw+mHsTmkLszvJSM8W6JBaDjNpTTFr/RSNoVL5FMwcTg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@tauri-apps/cli-win32-arm64-msvc@2.11.0': + resolution: {integrity: sha512-K+br+VXZ+Xx0n/9FdWohpW5Ugq+2FQUpJScqcPl1hTxXfh3fgjYgt4qA2NgrjlJo+zZPNrmUMl+NLvm0ufEqBQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@tauri-apps/cli-win32-ia32-msvc@2.11.0': + resolution: {integrity: sha512-OFV+s3MLZnd75zl0ZAFU5riMpGK4waUEA8ZDuijDsnkU0btz/gHhqh5jVlOn8thyvgdtT3Xyoxqo099MMifH3g==} + engines: {node: '>= 10'} + cpu: [ia32] + os: [win32] + + '@tauri-apps/cli-win32-x64-msvc@2.11.0': + resolution: {integrity: sha512-AeDTWBd2cOZ6TX133BWsoo+LutG9o0JRcgjMsIfLE13ZugpgCMv/2dJbUiBGeRvbPOGin5A3aYmsArPVV6ZSHQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@tauri-apps/cli@2.11.0': + resolution: {integrity: sha512-W5Wbuqsb2pHFPTj4TaRNKTj5rwXhDShPiLSY9T18y4ouSR/NNCptAEFxFsBtyNRgL6Vs1a/q9LzfqqYzEwC+Jw==} + engines: {node: '>= 10'} + hasBin: true + + '@tauri-apps/plugin-deep-link@2.4.9': + resolution: {integrity: sha512-u0SKOUHnJ1wqeqXsDFq2+kASCBj9xxbG0g9XZWPy9SOmU4wXtp6b/wiYpm6oH6/5fBTQsLqnLhIvqLBRpgHJlA==} + + '@tauri-apps/plugin-http@2.5.9': + resolution: {integrity: sha512-lCiY0+vs4HvIUSvZrBs8TC3TiCB0MOPRmiUjTq4prW7SlcJE2jdLeT6KBsJrT9Tlplufl7W1pY6SFAO3gCWxDA==} + + '@tauri-apps/plugin-opener@2.5.4': + resolution: {integrity: sha512-1HnPkb+AmgO29HBazm4uPLKB+r7zzcTBW1d0fyYp1uP+jwtpoiNDGKMMzz58SFp49nOIrxdE3aUJtT57lfO9CQ==} + + '@tauri-apps/plugin-os@2.3.2': + resolution: {integrity: sha512-n+nXWeuSeF9wcEsSPmRnBEGrRgOy6jjkSU+UVCOV8YUGKb2erhDOxis7IqRXiRVHhY8XMKks00BJ0OAdkpf6+A==} + + '@tauri-apps/plugin-store@2.4.3': + resolution: {integrity: sha512-9LWPj9yMphRi9czEtUv87XHbl1b6xgd9EXpPrUnq6nG7+nbtoF84d4Kwz9xhAv/Hf30sr58pq7EOlyI936y8qw==} + '@testing-library/dom@10.4.1': resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} engines: {node: '>=18'} @@ -2945,6 +3097,173 @@ packages: peerDependencies: '@testing-library/dom': '>=7.21.4' + '@tiptap/core@3.23.4': + resolution: {integrity: sha512-ni2LWE52bVeSt3L2HVBSmbBw+elc32ATej9C68EyKzN/8vR5ILxFn6RCdDTKm4asmwZyq2jys12dKmBdWMr9QA==} + peerDependencies: + '@tiptap/pm': 3.23.4 + + '@tiptap/extension-blockquote@3.23.4': + resolution: {integrity: sha512-7YjSibNlPcy9eGK+tHt5G/Njr7nPxl+rZ3rCC6TwtLIRLSHPnoGDsfFOgTPkXxaQcE1a/VQwemnYfWc3kdIjDQ==} + peerDependencies: + '@tiptap/core': 3.23.4 + + '@tiptap/extension-bold@3.23.4': + resolution: {integrity: sha512-3L9tnZ12i+98u5df2nV2zGu/sc3rhI87E3ocn1YYAO8PJUAgZnMwdet8JawCrS1uut5sRKlxo3SXEmdNfRVm/w==} + peerDependencies: + '@tiptap/core': 3.23.4 + + '@tiptap/extension-bubble-menu@3.23.4': + resolution: {integrity: sha512-EPTpL/IFp/aTGZErBq/Mc3dKznj6G/qNEkVYWjueOn1oKApyT0P6WVHGvu/vpMdErhzmoGDuFPPGVS6T8Upx2Q==} + peerDependencies: + '@tiptap/core': 3.23.4 + '@tiptap/pm': 3.23.4 + + '@tiptap/extension-bullet-list@3.23.4': + resolution: {integrity: sha512-mXB2KZOz1R+E6VNTZ3vzdAk7ZDGKjPmsJEZIQg1B5qRycTKg49/rCCkLA2QnqAwX6BzS3mLLH1RWE2W0oXD7vg==} + peerDependencies: + '@tiptap/extension-list': 3.23.4 + + '@tiptap/extension-code-block@3.23.4': + resolution: {integrity: sha512-UEU1w/85CSNKktbhESnIRmtjKcH7DeschReZA8err1wAnYLTKzid5ucnJSJ25iRg2V5Fnuws5gnPT5CVgdfXCQ==} + peerDependencies: + '@tiptap/core': 3.23.4 + '@tiptap/pm': 3.23.4 + + '@tiptap/extension-code@3.23.4': + resolution: {integrity: sha512-C0TeRipMycUEBnV+Mzx6eLp/yZb6Vi/waP3Tkb0lO5/ikg7LWLB7AlmMunjIXEUcR/pJHID/aEh5PfJFpysUDg==} + peerDependencies: + '@tiptap/core': 3.23.4 + + '@tiptap/extension-document@3.23.4': + resolution: {integrity: sha512-YC4G6VkxT629rlqUTwD6XvOpxjvghn7fxrK4RbyKVJY2C6E1vgmX0won1Ast6v+qTE6iONOMS6f6VyPxSGjg4w==} + peerDependencies: + '@tiptap/core': 3.23.4 + + '@tiptap/extension-dropcursor@3.23.4': + resolution: {integrity: sha512-ujJQUIENk0RwVFCh5g/TOSEv1a7Pnam/cjHmSUqHWUNZkYS9aOqjm+JfURJPCinRS2oHvo3AARul5mkKgDJYcA==} + peerDependencies: + '@tiptap/extensions': 3.23.4 + + '@tiptap/extension-floating-menu@3.23.4': + resolution: {integrity: sha512-eAc72bKM26yIPx0jsU8qdjE71vFNVu5R9jGbdItBMFc0SPLS4qY8g+8RJ+iWoLwbcSEpgooLS9D9sLfdAU+Tvw==} + peerDependencies: + '@floating-ui/dom': ^1.0.0 + '@tiptap/core': 3.23.4 + '@tiptap/pm': 3.23.4 + + '@tiptap/extension-gapcursor@3.23.4': + resolution: {integrity: sha512-RuyvOlIGP6UpVOc0Lw0L63jKLtYM49CNhPV2OMSfwwwbBZ3pJGos2/SqpYg71d3sn+qpsAopS4Pfr8iPZog73A==} + peerDependencies: + '@tiptap/extensions': 3.23.4 + + '@tiptap/extension-hard-break@3.23.4': + resolution: {integrity: sha512-ODlpZCi7n136BH9luM09EFL8Pg+bbRCd0tzCQM5BKMXRkLitYZA8Gl/f5DLmGJ50wzFsDPXK2Br2g9UvZK7COg==} + peerDependencies: + '@tiptap/core': 3.23.4 + + '@tiptap/extension-heading@3.23.4': + resolution: {integrity: sha512-8W9Hqi0J69Xbqg08nPf4xRMJXMccaKFAgUE1tvy5PAWJSQxOMwkKQXgZXxwe+80sOMUnV8qveBqUy/ODMPgAxQ==} + peerDependencies: + '@tiptap/core': 3.23.4 + + '@tiptap/extension-horizontal-rule@3.23.4': + resolution: {integrity: sha512-EA4kK8ywZ4dQNOdxeZbplmDDs5T5LjMgHpqxRwukj9wwKiILOK5E3fcKm1fCKh9Q02w96jax6YVccHwmgJP3sQ==} + peerDependencies: + '@tiptap/core': 3.23.4 + '@tiptap/pm': 3.23.4 + + '@tiptap/extension-italic@3.23.4': + resolution: {integrity: sha512-jUAHi+HZlg47BzgVIy6y/UH5vev7vPQ95jddhB5K3hC122kvWFMXlken7UOnqzbxNcHs2+4Oi/ZJirYMpT4P5w==} + peerDependencies: + '@tiptap/core': 3.23.4 + + '@tiptap/extension-link@3.23.4': + resolution: {integrity: sha512-XjxltY7MomwfTs6jmN6Bw5bb/upb34lpyqv2RiXppFTK25Br7ipksRjUpWpB4/csZeg30qwrLGVKxCol38ffrw==} + peerDependencies: + '@tiptap/core': 3.23.4 + '@tiptap/pm': 3.23.4 + + '@tiptap/extension-list-item@3.23.4': + resolution: {integrity: sha512-Q/JXosShD5oyDwukE6igdrZD2lb0ZgyoQTHYchk0pzU4frClFbn3RI1wKP+XeqKLhdO6KH2WZ9rERGH7PtDi7Q==} + peerDependencies: + '@tiptap/extension-list': 3.23.4 + + '@tiptap/extension-list-keymap@3.23.4': + resolution: {integrity: sha512-9FezifCfuoc0o+5K6l4QNOOfelqxnDGg/f9oL1D/LFZPC54bPxpWWft9QCWOqyqZgyLCLjbCjciAlbgkrFUmmw==} + peerDependencies: + '@tiptap/extension-list': 3.23.4 + + '@tiptap/extension-list@3.23.4': + resolution: {integrity: sha512-yuauDm6qW/7q+ZO0YJBKQEGdnUm6DDTJM8AMp9bMZrT4jRf/zyUtNcZ91QEfFvBcyVuI+10PIOXtNPevhQ741Q==} + peerDependencies: + '@tiptap/core': 3.23.4 + '@tiptap/pm': 3.23.4 + + '@tiptap/extension-mention@3.23.4': + resolution: {integrity: sha512-4Fq4shW/XQ8h4wyaudOP4HWze9NWN4MTCQAQb8BSHWaMOosVRzve+WnTQL53axWj0pbYqM+d9iYpMgvdMmMm9g==} + peerDependencies: + '@tiptap/core': 3.23.4 + '@tiptap/pm': 3.23.4 + '@tiptap/suggestion': 3.23.4 + + '@tiptap/extension-ordered-list@3.23.4': + resolution: {integrity: sha512-+3ofyssYnOTa1+nFWEmCAY1ngn8nAV1xo25JnNNC87NMU9WkSgr93jB7/uUJP0uui1C2dBLlaup3XXm108yarw==} + peerDependencies: + '@tiptap/extension-list': 3.23.4 + + '@tiptap/extension-paragraph@3.23.4': + resolution: {integrity: sha512-KbhXjCFzWphvFn5VU7E4dtmYDm+bssI1i0+CnXPWCXkjdaaX88ck68Xp1fKz8/bbI/CqlgiNDO/3TvqgtZ6woQ==} + peerDependencies: + '@tiptap/core': 3.23.4 + + '@tiptap/extension-placeholder@3.23.4': + resolution: {integrity: sha512-yHtAZkFR9M2AQmCi555w4ns1BBCqwRyYDYMtd10DBvqPX7T3TmGerPdUfI6sLr74GxnZ5zHOnOYdwAbeG5JzNw==} + peerDependencies: + '@tiptap/extensions': 3.23.4 + + '@tiptap/extension-strike@3.23.4': + resolution: {integrity: sha512-Vnq5vW801zPbu1LtKeA5k4R241jY+hRjXeijYwIPxy15KzIiipY12518HiCf6/8kkRbMxgOfdYg9X4BRV3HV3g==} + peerDependencies: + '@tiptap/core': 3.23.4 + + '@tiptap/extension-text@3.23.4': + resolution: {integrity: sha512-q9kxver/MR18p66aWZHSPycnr9hcBFyVGeGj8gf+BQCzn5hpvtSYTfLvk1nq8GFhygdQ9/e3f7B5ovrm/jnpvw==} + peerDependencies: + '@tiptap/core': 3.23.4 + + '@tiptap/extension-underline@3.23.4': + resolution: {integrity: sha512-F1ocPT10LV+seky25R1TMCRdc/Iof99jLcDSYDGr6mNEDY4ct2RvOeSM8aDdYq6CkH+vXt3i3JDeRwV23KzswQ==} + peerDependencies: + '@tiptap/core': 3.23.4 + + '@tiptap/extensions@3.23.4': + resolution: {integrity: sha512-SlGPXauW8iKWG7wwuwC/0y/smLImp0h6GBIGgNnTBgIP/ThXQnjLMSZH0mW/REO87dQxkku01V3ARRywi+juhg==} + peerDependencies: + '@tiptap/core': 3.23.4 + '@tiptap/pm': 3.23.4 + + '@tiptap/pm@3.23.4': + resolution: {integrity: sha512-+C5ngcoza47n3MjtjVBqBEBICPC0McdbwzJ+X6SSCviCLoqnSYanv5mIX9HWG0Q4fJ4BkdNM3VibZUxQaTbKyQ==} + + '@tiptap/react@3.23.4': + resolution: {integrity: sha512-mb5aIY9PuLreOVLExqs+8BAI20I/8+jCUBfEIqheuFY2GRRuBiwczejSlYuADfVDBbPVN5uPw4UMADCaH5wueQ==} + peerDependencies: + '@tiptap/core': 3.23.4 + '@tiptap/pm': 3.23.4 + '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 + '@types/react-dom': ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0 + + '@tiptap/starter-kit@3.23.4': + resolution: {integrity: sha512-3VhU+NO6/ec9DMj/5Ej0nzARSq42cXnqW+QHCmTL3FNXkXQz+tw1KlfruT5GGJ3M0RssjWjRC0a39N/4S3qxeA==} + + '@tiptap/suggestion@3.23.4': + resolution: {integrity: sha512-KvrHKQcGpEKPPuetH2N4K21kA7hc31n5WDzw3FM+fNpMKdJOToYoNZzS9rmuBBHmNZ9wyK2sWmzi09enmv6wbg==} + peerDependencies: + '@tiptap/core': 3.23.4 + '@tiptap/pm': 3.23.4 + '@tybys/wasm-util@0.10.1': resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} @@ -3013,6 +3332,9 @@ packages: '@types/ua-parser-js@0.7.39': resolution: {integrity: sha512-P/oDfpofrdtF5xw433SPALpdSchtJmY7nsJItf8h3KXqOslkbySh8zq4dSWXH2oTjRvJ5PczVEoCZPow6GicLg==} + '@types/use-sync-external-store@0.0.6': + resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==} + '@use-gesture/core@10.3.1': resolution: {integrity: sha512-WcINiDt8WjqBdUXye25anHiNxPc0VOrlT8F6LLkU6cycrOGUDyY/yyFmsg3k8i5OLvv25llc0QC45GhR/C8llw==} @@ -3582,6 +3904,10 @@ packages: fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + fast-equals@5.4.0: + resolution: {integrity: sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==} + engines: {node: '>=6.0.0'} + fast-glob@3.3.3: resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} @@ -3659,8 +3985,8 @@ packages: engines: {node: '>=18.3.0'} hasBin: true - framer-motion@12.34.3: - resolution: {integrity: sha512-v81ecyZKYO/DfpTwHivqkxSUBzvceOpoI+wLfgCgoUIKxlFKEXdg0oR9imxwXumT4SFy8vRk9xzJ5l3/Du/55Q==} + framer-motion@12.35.2: + resolution: {integrity: sha512-dhfuEMaNo0hc+AEqyHiIfiJRNb9U9UQutE9FoKm5pjf7CMitp9xPEF1iWZihR1q86LBmo6EJ7S8cN8QXEy49AA==} peerDependencies: '@emotion/is-prop-valid': '*' react: ^18.0.0 || ^19.0.0 @@ -4108,6 +4434,9 @@ packages: linkifyjs@4.3.2: resolution: {integrity: sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA==} + linkifyjs@4.3.3: + resolution: {integrity: sha512-P8aEP5U/D1/IlTY2OeYsErdwh9bGuLE30NcXtKEjgdHcahveQoQwM2yZNsioQHsWFz0P7KKudisbrzCgR0sDHg==} + locate-path@6.0.0: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} @@ -4223,6 +4552,9 @@ packages: resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} engines: {node: '>=16 || 14 >=14.17'} + minisearch@7.2.0: + resolution: {integrity: sha512-dqT2XBYUOZOiC5t2HRnwADjhNS2cecp9u+TJRiJ1Qp/f5qjkeT5APcGPjHw+bz89Ms8Jp+cG4AlE+QZ/QnDglg==} + mlly@1.8.1: resolution: {integrity: sha512-SnL6sNutTwRWWR/vcmCYHSADjiEesp5TGQQ0pXyLhW5IoeibRlF/CbSLailbB3CNqJUk9cVJ9dUDnbD7GrcHBQ==} @@ -4235,6 +4567,20 @@ packages: motion-utils@12.29.2: resolution: {integrity: sha512-G3kc34H2cX2gI63RqU+cZq+zWRRPSsNIOjpdl9TN4AQwC4sgwYPl/Q/Obf/d53nOm569T0fYK+tcoSV50BWx8A==} + motion@12.35.0: + resolution: {integrity: sha512-BQUhNUIGvUcwXCzwmnT1JpjUqab34lIwxHnXUyWRht1WC1vAyp7/4qgMiUXxN3K6hgUhyoR+HNnLeQMwUZjVjw==} + peerDependencies: + '@emotion/is-prop-valid': '*' + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/is-prop-valid': + optional: true + react: + optional: true + react-dom: + optional: true + mrmime@2.0.1: resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} engines: {node: '>=10'} @@ -4295,6 +4641,9 @@ packages: resolution: {integrity: sha512-jNdst/U28Iasukx/L5MP6b274Vr7ftQs6qAhPBCvz6Wt5rPCA+Q/tUmCzfCHHWweWw5szeMy2Gfrm1rITwUKrw==} engines: {node: '>=18'} + orderedmap@2.1.1: + resolution: {integrity: sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==} + own-keys@1.0.1: resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} engines: {node: '>= 0.4'} @@ -4425,6 +4774,42 @@ packages: prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + prosemirror-changeset@2.4.1: + resolution: {integrity: sha512-96WBLhOaYhJ+kPhLg3uW359Tz6I/MfcrQfL4EGv4SrcqKEMC1gmoGrXHecPE8eOwTVCJ4IwgfzM8fFad25wNfw==} + + prosemirror-commands@1.7.1: + resolution: {integrity: sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w==} + + prosemirror-dropcursor@1.8.2: + resolution: {integrity: sha512-CCk6Gyx9+Tt2sbYk5NK0nB1ukHi2ryaRgadV/LvyNuO3ena1payM2z6Cg0vO1ebK8cxbzo41ku2DE5Axj1Zuiw==} + + prosemirror-gapcursor@1.4.1: + resolution: {integrity: sha512-pMdYaEnjNMSwl11yjEGtgTmLkR08m/Vl+Jj443167p9eB3HVQKhYCc4gmHVDsLPODfZfjr/MmirsdyZziXbQKw==} + + prosemirror-history@1.5.0: + resolution: {integrity: sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg==} + + prosemirror-keymap@1.2.3: + resolution: {integrity: sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==} + + prosemirror-model@1.25.6: + resolution: {integrity: sha512-RIm+e9BiqAaJ1mRECv3vR3C+VG8ELoTTI+47tVudGi82yLnFOx3G/p/iSPK1HmHQdKhkkrJ68NJqxh7S+FBVmQ==} + + prosemirror-schema-list@1.5.1: + resolution: {integrity: sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==} + + prosemirror-state@1.4.4: + resolution: {integrity: sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==} + + prosemirror-tables@1.8.5: + resolution: {integrity: sha512-V/0cDCsHKHe/tfWkeCmthNUcEp1IVO3p6vwN8XtwE9PZQLAZJigbw3QoraAdfJPir4NKJtNvOB8oYGKRl+t0Dw==} + + prosemirror-transform@1.12.0: + resolution: {integrity: sha512-GxboyN4AMIsoHNtz5uf2r2Ru551i5hWeCMD6E2Ib4Eogqoub0NflniaBPVQ4MrGE5yZ8JV9tUHg9qcZTTrcN4w==} + + prosemirror-view@1.41.8: + resolution: {integrity: sha512-TnKDdohEatgyZNGCDWIdccOHXhYloJwbwU+phw/a23KBvJIR9lWQWW7WHHK3vBdOLDNuF7TaX98GObUZOWkOnA==} + proxy-from-env@1.1.0: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} @@ -4582,6 +4967,9 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + rope-sequence@1.3.4: + resolution: {integrity: sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==} + run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} @@ -5098,6 +5486,9 @@ packages: resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==} engines: {node: '>=0.10.0'} + w3c-keyname@2.2.8: + resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} + w3c-xmlserializer@5.0.0: resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} engines: {node: '>=18'} @@ -6160,6 +6551,20 @@ snapshots: '@exodus/bytes@1.15.0': {} + '@floating-ui/core@1.7.5': + dependencies: + '@floating-ui/utils': 0.2.11 + optional: true + + '@floating-ui/dom@1.7.6': + dependencies: + '@floating-ui/core': 1.7.5 + '@floating-ui/utils': 0.2.11 + optional: true + + '@floating-ui/utils@0.2.11': + optional: true + '@fontsource-variable/nunito@5.2.7': {} '@fontsource/space-mono@5.2.9': {} @@ -7728,6 +8133,10 @@ snapshots: '@sableclient/sable-call-embedded@1.1.4': {} + '@sableclient/tauri-plugin-notifications-api@https://codeload.github.com/SableClient/tauri-plugin-notifications/tar.gz/9d15b2b2c8798dc29ce723d743f0f3b4921d6a54': + dependencies: + '@tauri-apps/api': 2.11.0 + '@sentry-internal/browser-utils@10.43.0': dependencies: '@sentry/core': 10.43.0 @@ -8004,6 +8413,75 @@ snapshots: '@tanstack/virtual-core@3.13.21': {} + '@tauri-apps/api@2.11.0': {} + + '@tauri-apps/cli-darwin-arm64@2.11.0': + optional: true + + '@tauri-apps/cli-darwin-x64@2.11.0': + optional: true + + '@tauri-apps/cli-linux-arm-gnueabihf@2.11.0': + optional: true + + '@tauri-apps/cli-linux-arm64-gnu@2.11.0': + optional: true + + '@tauri-apps/cli-linux-arm64-musl@2.11.0': + optional: true + + '@tauri-apps/cli-linux-riscv64-gnu@2.11.0': + optional: true + + '@tauri-apps/cli-linux-x64-gnu@2.11.0': + optional: true + + '@tauri-apps/cli-linux-x64-musl@2.11.0': + optional: true + + '@tauri-apps/cli-win32-arm64-msvc@2.11.0': + optional: true + + '@tauri-apps/cli-win32-ia32-msvc@2.11.0': + optional: true + + '@tauri-apps/cli-win32-x64-msvc@2.11.0': + optional: true + + '@tauri-apps/cli@2.11.0': + optionalDependencies: + '@tauri-apps/cli-darwin-arm64': 2.11.0 + '@tauri-apps/cli-darwin-x64': 2.11.0 + '@tauri-apps/cli-linux-arm-gnueabihf': 2.11.0 + '@tauri-apps/cli-linux-arm64-gnu': 2.11.0 + '@tauri-apps/cli-linux-arm64-musl': 2.11.0 + '@tauri-apps/cli-linux-riscv64-gnu': 2.11.0 + '@tauri-apps/cli-linux-x64-gnu': 2.11.0 + '@tauri-apps/cli-linux-x64-musl': 2.11.0 + '@tauri-apps/cli-win32-arm64-msvc': 2.11.0 + '@tauri-apps/cli-win32-ia32-msvc': 2.11.0 + '@tauri-apps/cli-win32-x64-msvc': 2.11.0 + + '@tauri-apps/plugin-deep-link@2.4.9': + dependencies: + '@tauri-apps/api': 2.11.0 + + '@tauri-apps/plugin-http@2.5.9': + dependencies: + '@tauri-apps/api': 2.11.0 + + '@tauri-apps/plugin-opener@2.5.4': + dependencies: + '@tauri-apps/api': 2.11.0 + + '@tauri-apps/plugin-os@2.3.2': + dependencies: + '@tauri-apps/api': 2.11.0 + + '@tauri-apps/plugin-store@2.4.3': + dependencies: + '@tauri-apps/api': 2.11.0 + '@testing-library/dom@10.4.1': dependencies: '@babel/code-frame': 7.29.0 @@ -8038,6 +8516,192 @@ snapshots: dependencies: '@testing-library/dom': 10.4.1 + '@tiptap/core@3.23.4(@tiptap/pm@3.23.4)': + dependencies: + '@tiptap/pm': 3.23.4 + + '@tiptap/extension-blockquote@3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))': + dependencies: + '@tiptap/core': 3.23.4(@tiptap/pm@3.23.4) + + '@tiptap/extension-bold@3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))': + dependencies: + '@tiptap/core': 3.23.4(@tiptap/pm@3.23.4) + + '@tiptap/extension-bubble-menu@3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4)': + dependencies: + '@floating-ui/dom': 1.7.6 + '@tiptap/core': 3.23.4(@tiptap/pm@3.23.4) + '@tiptap/pm': 3.23.4 + optional: true + + '@tiptap/extension-bullet-list@3.23.4(@tiptap/extension-list@3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4))': + dependencies: + '@tiptap/extension-list': 3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4) + + '@tiptap/extension-code-block@3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4)': + dependencies: + '@tiptap/core': 3.23.4(@tiptap/pm@3.23.4) + '@tiptap/pm': 3.23.4 + + '@tiptap/extension-code@3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))': + dependencies: + '@tiptap/core': 3.23.4(@tiptap/pm@3.23.4) + + '@tiptap/extension-document@3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))': + dependencies: + '@tiptap/core': 3.23.4(@tiptap/pm@3.23.4) + + '@tiptap/extension-dropcursor@3.23.4(@tiptap/extensions@3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4))': + dependencies: + '@tiptap/extensions': 3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4) + + '@tiptap/extension-floating-menu@3.23.4(@floating-ui/dom@1.7.6)(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4)': + dependencies: + '@floating-ui/dom': 1.7.6 + '@tiptap/core': 3.23.4(@tiptap/pm@3.23.4) + '@tiptap/pm': 3.23.4 + optional: true + + '@tiptap/extension-gapcursor@3.23.4(@tiptap/extensions@3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4))': + dependencies: + '@tiptap/extensions': 3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4) + + '@tiptap/extension-hard-break@3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))': + dependencies: + '@tiptap/core': 3.23.4(@tiptap/pm@3.23.4) + + '@tiptap/extension-heading@3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))': + dependencies: + '@tiptap/core': 3.23.4(@tiptap/pm@3.23.4) + + '@tiptap/extension-horizontal-rule@3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4)': + dependencies: + '@tiptap/core': 3.23.4(@tiptap/pm@3.23.4) + '@tiptap/pm': 3.23.4 + + '@tiptap/extension-italic@3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))': + dependencies: + '@tiptap/core': 3.23.4(@tiptap/pm@3.23.4) + + '@tiptap/extension-link@3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4)': + dependencies: + '@tiptap/core': 3.23.4(@tiptap/pm@3.23.4) + '@tiptap/pm': 3.23.4 + linkifyjs: 4.3.3 + + '@tiptap/extension-list-item@3.23.4(@tiptap/extension-list@3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4))': + dependencies: + '@tiptap/extension-list': 3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4) + + '@tiptap/extension-list-keymap@3.23.4(@tiptap/extension-list@3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4))': + dependencies: + '@tiptap/extension-list': 3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4) + + '@tiptap/extension-list@3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4)': + dependencies: + '@tiptap/core': 3.23.4(@tiptap/pm@3.23.4) + '@tiptap/pm': 3.23.4 + + '@tiptap/extension-mention@3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4)(@tiptap/suggestion@3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4))': + dependencies: + '@tiptap/core': 3.23.4(@tiptap/pm@3.23.4) + '@tiptap/pm': 3.23.4 + '@tiptap/suggestion': 3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4) + + '@tiptap/extension-ordered-list@3.23.4(@tiptap/extension-list@3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4))': + dependencies: + '@tiptap/extension-list': 3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4) + + '@tiptap/extension-paragraph@3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))': + dependencies: + '@tiptap/core': 3.23.4(@tiptap/pm@3.23.4) + + '@tiptap/extension-placeholder@3.23.4(@tiptap/extensions@3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4))': + dependencies: + '@tiptap/extensions': 3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4) + + '@tiptap/extension-strike@3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))': + dependencies: + '@tiptap/core': 3.23.4(@tiptap/pm@3.23.4) + + '@tiptap/extension-text@3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))': + dependencies: + '@tiptap/core': 3.23.4(@tiptap/pm@3.23.4) + + '@tiptap/extension-underline@3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))': + dependencies: + '@tiptap/core': 3.23.4(@tiptap/pm@3.23.4) + + '@tiptap/extensions@3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4)': + dependencies: + '@tiptap/core': 3.23.4(@tiptap/pm@3.23.4) + '@tiptap/pm': 3.23.4 + + '@tiptap/pm@3.23.4': + dependencies: + prosemirror-changeset: 2.4.1 + prosemirror-commands: 1.7.1 + prosemirror-dropcursor: 1.8.2 + prosemirror-gapcursor: 1.4.1 + prosemirror-history: 1.5.0 + prosemirror-keymap: 1.2.3 + prosemirror-model: 1.25.6 + prosemirror-schema-list: 1.5.1 + prosemirror-state: 1.4.4 + prosemirror-tables: 1.8.5 + prosemirror-transform: 1.12.0 + prosemirror-view: 1.41.8 + + '@tiptap/react@3.23.4(@floating-ui/dom@1.7.6)(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@tiptap/core': 3.23.4(@tiptap/pm@3.23.4) + '@tiptap/pm': 3.23.4 + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + '@types/use-sync-external-store': 0.0.6 + fast-equals: 5.4.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + use-sync-external-store: 1.6.0(react@18.3.1) + optionalDependencies: + '@tiptap/extension-bubble-menu': 3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4) + '@tiptap/extension-floating-menu': 3.23.4(@floating-ui/dom@1.7.6)(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4) + transitivePeerDependencies: + - '@floating-ui/dom' + + '@tiptap/starter-kit@3.23.4': + dependencies: + '@tiptap/core': 3.23.4(@tiptap/pm@3.23.4) + '@tiptap/extension-blockquote': 3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4)) + '@tiptap/extension-bold': 3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4)) + '@tiptap/extension-bullet-list': 3.23.4(@tiptap/extension-list@3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4)) + '@tiptap/extension-code': 3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4)) + '@tiptap/extension-code-block': 3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4) + '@tiptap/extension-document': 3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4)) + '@tiptap/extension-dropcursor': 3.23.4(@tiptap/extensions@3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4)) + '@tiptap/extension-gapcursor': 3.23.4(@tiptap/extensions@3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4)) + '@tiptap/extension-hard-break': 3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4)) + '@tiptap/extension-heading': 3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4)) + '@tiptap/extension-horizontal-rule': 3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4) + '@tiptap/extension-italic': 3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4)) + '@tiptap/extension-link': 3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4) + '@tiptap/extension-list': 3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4) + '@tiptap/extension-list-item': 3.23.4(@tiptap/extension-list@3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4)) + '@tiptap/extension-list-keymap': 3.23.4(@tiptap/extension-list@3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4)) + '@tiptap/extension-ordered-list': 3.23.4(@tiptap/extension-list@3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4)) + '@tiptap/extension-paragraph': 3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4)) + '@tiptap/extension-strike': 3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4)) + '@tiptap/extension-text': 3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4)) + '@tiptap/extension-underline': 3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4)) + '@tiptap/extensions': 3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4) + '@tiptap/pm': 3.23.4 + + '@tiptap/suggestion@3.23.4(@tiptap/core@3.23.4(@tiptap/pm@3.23.4))(@tiptap/pm@3.23.4)': + dependencies: + '@tiptap/core': 3.23.4(@tiptap/pm@3.23.4) + '@tiptap/pm': 3.23.4 + '@tybys/wasm-util@0.10.1': dependencies: tslib: 2.8.1 @@ -8110,6 +8774,8 @@ snapshots: '@types/ua-parser-js@0.7.39': {} + '@types/use-sync-external-store@0.0.6': {} + '@use-gesture/core@10.3.1': {} '@use-gesture/react@10.3.1(react@18.3.1)': @@ -8802,6 +9468,8 @@ snapshots: fast-deep-equal@3.1.3: {} + fast-equals@5.4.0: {} + fast-glob@3.3.3: dependencies: '@nodelib/fs.stat': 2.0.5 @@ -8878,7 +9546,7 @@ snapshots: dependencies: fd-package-json: 2.0.0 - framer-motion@12.34.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + framer-motion@12.35.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: motion-dom: 12.35.2 motion-utils: 12.29.2 @@ -9343,6 +10011,8 @@ snapshots: linkifyjs@4.3.2: {} + linkifyjs@4.3.3: {} + locate-path@6.0.0: dependencies: p-locate: 5.0.0 @@ -9462,6 +10132,8 @@ snapshots: minipass@7.1.3: {} + minisearch@7.2.0: {} + mlly@1.8.1: dependencies: acorn: 8.16.0 @@ -9477,6 +10149,14 @@ snapshots: motion-utils@12.29.2: {} + motion@12.35.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + framer-motion: 12.35.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + tslib: 2.8.1 + optionalDependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + mrmime@2.0.1: {} ms@2.1.3: {} @@ -9522,6 +10202,8 @@ snapshots: dependencies: jwt-decode: 4.0.0 + orderedmap@2.1.1: {} + own-keys@1.0.1: dependencies: get-intrinsic: 1.3.0 @@ -9700,6 +10382,75 @@ snapshots: object-assign: 4.1.1 react-is: 16.13.1 + prosemirror-changeset@2.4.1: + dependencies: + prosemirror-transform: 1.12.0 + + prosemirror-commands@1.7.1: + dependencies: + prosemirror-model: 1.25.6 + prosemirror-state: 1.4.4 + prosemirror-transform: 1.12.0 + + prosemirror-dropcursor@1.8.2: + dependencies: + prosemirror-state: 1.4.4 + prosemirror-transform: 1.12.0 + prosemirror-view: 1.41.8 + + prosemirror-gapcursor@1.4.1: + dependencies: + prosemirror-keymap: 1.2.3 + prosemirror-model: 1.25.6 + prosemirror-state: 1.4.4 + prosemirror-view: 1.41.8 + + prosemirror-history@1.5.0: + dependencies: + prosemirror-state: 1.4.4 + prosemirror-transform: 1.12.0 + prosemirror-view: 1.41.8 + rope-sequence: 1.3.4 + + prosemirror-keymap@1.2.3: + dependencies: + prosemirror-state: 1.4.4 + w3c-keyname: 2.2.8 + + prosemirror-model@1.25.6: + dependencies: + orderedmap: 2.1.1 + + prosemirror-schema-list@1.5.1: + dependencies: + prosemirror-model: 1.25.6 + prosemirror-state: 1.4.4 + prosemirror-transform: 1.12.0 + + prosemirror-state@1.4.4: + dependencies: + prosemirror-model: 1.25.6 + prosemirror-transform: 1.12.0 + prosemirror-view: 1.41.8 + + prosemirror-tables@1.8.5: + dependencies: + prosemirror-keymap: 1.2.3 + prosemirror-model: 1.25.6 + prosemirror-state: 1.4.4 + prosemirror-transform: 1.12.0 + prosemirror-view: 1.41.8 + + prosemirror-transform@1.12.0: + dependencies: + prosemirror-model: 1.25.6 + + prosemirror-view@1.41.8: + dependencies: + prosemirror-model: 1.25.6 + prosemirror-state: 1.4.4 + prosemirror-transform: 1.12.0 + proxy-from-env@1.1.0: {} punycode@2.3.1: {} @@ -9919,6 +10670,8 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.59.0 fsevents: 2.3.3 + rope-sequence@1.3.4: {} + run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 @@ -10476,6 +11229,8 @@ snapshots: void-elements@3.1.0: {} + w3c-keyname@2.2.8: {} + w3c-xmlserializer@5.0.0: dependencies: xml-name-validator: 5.0.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index fbcf9139d9..34a45e8733 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,4 +1,5 @@ allowBuilds: + '@sableclient/tauri-plugin-notifications-api': true '@sentry/cli': true '@swc/core': true cloudflared: true diff --git a/public/full_res_sable_square.png b/public/full_res_sable_square.png new file mode 100644 index 0000000000..1c3980e228 Binary files /dev/null and b/public/full_res_sable_square.png differ diff --git a/sable.code-workspace b/sable.code-workspace new file mode 100644 index 0000000000..f937d83cae --- /dev/null +++ b/sable.code-workspace @@ -0,0 +1,27 @@ +{ + "folders": [ + { + "path": ".", + "name": "Sable", + }, + { + "path": "../Sable-Docs", + "name": "Sable-Docs", + }, + ], + "settings": { + "editor.formatOnSave": true, + "typescript.tsdk": "Sable/node_modules/typescript/lib", + }, + "extensions": { + "recommendations": [ + "dbaeumer.vscode-eslint", + "esbenp.prettier-vscode", + "webpro.vscode-knip", + "tamasfe.even-better-toml", + "yzhang.markdown-all-in-one", + "github.vscode-pull-request-github", + "eamodio.gitlens", + ], + }, +} diff --git a/scripts/git-hooks/README.md b/scripts/git-hooks/README.md new file mode 100644 index 0000000000..2793d19218 --- /dev/null +++ b/scripts/git-hooks/README.md @@ -0,0 +1,28 @@ +# Git Hooks + +This directory contains git hooks that enforce quality standards before pushing code. + +## Installation + +Run the installation script from the repository root: + +```bash +./scripts/install-git-hooks.sh +``` + +This will copy the hooks to `.git/hooks/` and make them executable. + +## Hooks + +### pre-push + +Runs before every `git push` and enforces: +- TypeScript type checking (`npm run typecheck`) +- ESLint checks (`npm run lint`) +- Prettier formatting (`npm run fmt:check`) + +If any check fails, the push is blocked. To bypass in emergencies: `git push --no-verify` + +## Maintenance + +This directory is tracked on the `personal/config` branch to persist across `dev` pulls and merges. diff --git a/scripts/git-hooks/pre-push b/scripts/git-hooks/pre-push new file mode 100644 index 0000000000..d4c02c37a6 --- /dev/null +++ b/scripts/git-hooks/pre-push @@ -0,0 +1,35 @@ +#!/bin/zsh +# Pre-push hook: Run quality checks before allowing push +# This prevents pushing code that will fail CI checks + +set -e + +echo "🔍 Running pre-push quality checks..." + +# Run typecheck +echo " → Running typecheck..." +if ! npm run typecheck > /dev/null 2>&1; then + echo "❌ Typecheck failed. Fix errors before pushing." + npm run typecheck + exit 1 +fi +echo " ✓ Typecheck passed" + +# Run lint +echo " → Running lint..." +if ! npm run lint > /dev/null 2>&1; then + echo "❌ Lint failed. Fix errors before pushing." + npm run lint + exit 1 +fi +echo " ✓ Lint passed" + +# Run format check +echo " → Running format check..." +if ! npm run fmt:check > /dev/null 2>&1; then + echo "❌ Format check failed. Run 'npm run fmt' to fix." + exit 1 +fi +echo " ✓ Format check passed" + +echo "✅ All quality checks passed. Proceeding with push..." diff --git a/scripts/inject-client-config.js b/scripts/inject-client-config.js new file mode 100644 index 0000000000..0b5fcd3adb --- /dev/null +++ b/scripts/inject-client-config.js @@ -0,0 +1,75 @@ +import { readFile, writeFile } from 'node:fs/promises'; +import process from 'node:process'; +import { PrefixedLogger } from './utils/console-style.js'; + +const CONFIG_PATH = 'config.json'; +const OVERRIDES_ENV = 'CLIENT_CONFIG_OVERRIDES_JSON'; +const STRICT_ENV = 'CLIENT_CONFIG_OVERRIDES_STRICT'; +const logger = new PrefixedLogger('[config-inject]'); + +const formatError = (error) => { + if (error instanceof Error) return error.stack ?? error.message; + return String(error); +}; + +const isPlainObject = (value) => + typeof value === 'object' && value !== null && !Array.isArray(value); + +// Keys that could trigger prototype pollution via bracket assignment. +const UNSAFE_KEYS = new Set(['__proto__', 'constructor', 'prototype']); + +const deepMerge = (target, source) => { + if (!isPlainObject(target) || !isPlainObject(source)) return source; + + const merged = { ...target }; + Object.entries(source).forEach(([key, value]) => { + if (UNSAFE_KEYS.has(key)) return; + const targetValue = merged[key]; + merged[key] = + isPlainObject(targetValue) && isPlainObject(value) ? deepMerge(targetValue, value) : value; + }); + return merged; +}; + +const failOnError = process.env[STRICT_ENV] === 'true'; +const overridesRaw = process.env[OVERRIDES_ENV]; + +if (!overridesRaw) { + logger.info(`No ${OVERRIDES_ENV} provided; leaving ${CONFIG_PATH} unchanged.`); + process.exit(0); +} + +let fileConfig; +let overrides; + +try { + const file = await readFile(CONFIG_PATH, 'utf8'); + fileConfig = JSON.parse(file); +} catch (error) { + logger.error(`Failed reading ${CONFIG_PATH}: ${formatError(error)}`); + process.exit(1); +} + +try { + overrides = JSON.parse(overridesRaw); + if (!isPlainObject(overrides)) { + throw new Error(`${OVERRIDES_ENV} must be a JSON object.`); + } +} catch (error) { + const message = `[config-inject] Invalid ${OVERRIDES_ENV}; ${ + failOnError ? 'failing build' : 'skipping overrides' + }.`; + if (failOnError) { + logger.error(`${message} ${formatError(error)}`); + process.exit(1); + } + logger.info(`[warning] ${message} ${formatError(error)}`); + process.exit(0); +} + +const mergedConfig = deepMerge(fileConfig, overrides); + +await writeFile(CONFIG_PATH, `${JSON.stringify(mergedConfig, null, 2)}\n`, 'utf8'); +logger.info( + `Applied overrides to ${CONFIG_PATH}. Top-level keys: ${Object.keys(overrides).join(', ')}` +); diff --git a/scripts/install-git-hooks.sh b/scripts/install-git-hooks.sh new file mode 100644 index 0000000000..c90efc8190 --- /dev/null +++ b/scripts/install-git-hooks.sh @@ -0,0 +1,25 @@ +#!/bin/zsh +# Setup script: Install git hooks from scripts/git-hooks/ + +set -e + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +HOOKS_DIR="$REPO_ROOT/.git/hooks" +SOURCE_DIR="$REPO_ROOT/scripts/git-hooks" + +echo "🔧 Installing git hooks..." + +# Install pre-push hook +if [ -f "$SOURCE_DIR/pre-push" ]; then + cp "$SOURCE_DIR/pre-push" "$HOOKS_DIR/pre-push" + chmod +x "$HOOKS_DIR/pre-push" + echo " ✓ Installed pre-push hook" +else + echo " ⚠ pre-push hook not found in $SOURCE_DIR" +fi + +echo "✅ Git hooks installation complete!" +echo "" +echo "The pre-push hook will now run quality checks (typecheck, lint, format)" +echo "before every git push. To bypass in emergencies, use: git push --no-verify" diff --git a/scripts/symlink-tauri-icons.js b/scripts/symlink-tauri-icons.js new file mode 100644 index 0000000000..25674f335c --- /dev/null +++ b/scripts/symlink-tauri-icons.js @@ -0,0 +1,163 @@ +#!/usr/bin/env node +/* eslint-disable no-console */ + +import fs from 'node:fs'; +import path from 'node:path'; +import process from 'node:process'; +import { createTextHelpers } from './utils/console-style.js'; + +const ANDROID_ICONS = [ + 'mipmap-hdpi', + 'mipmap-mdpi', + 'mipmap-xhdpi', + 'mipmap-xxhdpi', + 'mipmap-xxxhdpi', +] + .flatMap((dir) => [ + `${dir}/ic_launcher.png`, + `${dir}/ic_launcher_round.png`, + `${dir}/ic_launcher_foreground.png`, + `${dir}/ic_notification.png`, + ]) + .concat(['mipmap-anydpi-v26/ic_launcher.xml', 'values/ic_launcher_background.xml']) + .concat(['drawable/ic_notification.xml', 'drawable/notification_icon.xml']); + +const IOS_ICONS = [ + 'AppIcon-20x20@1x.png', + 'AppIcon-20x20@2x.png', + 'AppIcon-20x20@2x-1.png', + 'AppIcon-20x20@3x.png', + 'AppIcon-29x29@1x.png', + 'AppIcon-29x29@2x.png', + 'AppIcon-29x29@2x-1.png', + 'AppIcon-29x29@3x.png', + 'AppIcon-40x40@1x.png', + 'AppIcon-40x40@2x.png', + 'AppIcon-40x40@2x-1.png', + 'AppIcon-40x40@3x.png', + 'AppIcon-60x60@2x.png', + 'AppIcon-60x60@3x.png', + 'AppIcon-76x76@1x.png', + 'AppIcon-76x76@2x.png', + 'AppIcon-83.5x83.5@2x.png', + 'AppIcon-512@2x.png', +]; + +function parseArgs(argv) { + let write = false; + let force = false; + + argv.forEach((arg) => { + if (arg === '--write') write = true; + if (arg === '--force') force = true; + if (arg === '--help' || arg === '-h') { + console.log( + [ + 'Usage: node scripts/symlink-icons.js [--write] [--force]', + '', + 'Default mode is dry-run.', + '--write Apply changes (create symlinks).', + '--force Overwrite existing symlinks.', + ].join('\n') + ); + process.exit(0); + } + }); + + return { write, force }; +} + +function createSymlink(src, dest, write, force, helpers) { + const { dim, red, green } = helpers; + + if (!fs.existsSync(src)) { + console.log(` ${red('!')} ${dim('source missing, skipping:')} ${src}`); + return false; + } + + const exists = fs.existsSync(dest); + if (exists && !force) { + console.log( + ` ${dim('~')} ${dim(path.relative(process.cwd(), dest))} ${dim('(exists, skipping)')}` + ); + return false; + } + + if (write) { + fs.mkdirSync(path.dirname(dest), { recursive: true }); + + try { + fs.unlinkSync(dest); + } catch { + // dest does not exist — nothing to remove + } + + const rel = path.relative(path.dirname(dest), src).split(path.sep).join('/'); + fs.symlinkSync(rel, dest); + } + + console.log(` ${green('+')} ${dim(path.relative(process.cwd(), dest))}`); + return true; +} + +function processGroup(label, srcDir, destDir, files, write, force, helpers) { + const { dim, red } = helpers; + + if (!fs.existsSync(destDir)) { + console.log(`\n${label}: ${red('destination not found')}, skipping.\n ${dim(destDir)}`); + return; + } + + console.log(`\n${label}`); + + const results = files.map((file) => { + try { + return createSymlink( + path.join(srcDir, file), + path.join(destDir, file), + write, + force, + helpers + ); + } catch (err) { + console.log(` ${red('-')} ${file}: ${err.message}`); + return false; + } + }); + + const ok = results.filter(Boolean).length; + const verb = write ? 'created' : 'would create'; + console.log(` ${dim(`→ ${ok}/${files.length} symlinks ${verb}.`)}`); +} + +function main() { + const ROOT = process.cwd(); + const { write, force } = parseArgs(process.argv.slice(2)); + const helpers = createTextHelpers(); + + processGroup( + 'Android', + path.join(ROOT, 'src-tauri', 'icons', 'android'), + path.join(ROOT, 'src-tauri', 'gen', 'android', 'app', 'src', 'main', 'res'), + ANDROID_ICONS, + write, + force, + helpers + ); + + processGroup( + 'iOS', + path.join(ROOT, 'src-tauri', 'icons', 'ios'), + path.join(ROOT, 'src-tauri', 'gen', 'apple', 'Assets.xcassets', 'AppIcon.appiconset'), + IOS_ICONS, + write, + force, + helpers + ); + + const mode = write ? 'Applied' : 'Dry run'; + console.log(`\n${mode}.`); + if (!write) console.log('Re-run with --write to apply changes.'); +} + +main(); diff --git a/scripts/tauri.js b/scripts/tauri.js new file mode 100644 index 0000000000..b4ac819b8b --- /dev/null +++ b/scripts/tauri.js @@ -0,0 +1,76 @@ +#!/usr/bin/env node + +/** + * Script to run tauri commands for a given runtime (wry or cef) with prepended CLI args. + * + * Usage: + * script/tauri [prepended-args...] + * + * Examples: + * script/tauri cef dev --verbose + * script/tauri cef dev -- --verbose + * Both will run: cargo tauri dev --features cef -- --verbose --no-default-features + */ + +import { spawn } from 'child_process'; +import { fileURLToPath } from 'url'; +import { dirname, join } from 'path'; +import process from 'node:process'; +import { PrefixedLogger, createTextHelpers } from './utils/console-style.js'; + +const logger = new PrefixedLogger('[tauri]'); +const { dim } = createTextHelpers({ useColor: logger.useColor }); + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +const cmdlineArgs = process.argv.slice(2); + +if (cmdlineArgs.length < 2) { + logger.error('Usage: node scripts/tauri [prepended-args...]'); + logger.error(` ${dim('command:')} dev or build`); + logger.error(` ${dim('runtime:')} wry or cef`); + logger.error( + ` ${dim('prepended-args:')} Arguments to prepend to the cargo args (-- separator is optional)` + ); + process.exit(1); +} + +const [runtime, cmd, ...tauriArgs] = cmdlineArgs; + +if (!['wry', 'cef'].includes(runtime)) { + logger.error(`Invalid runtime: ${runtime}. Must be 'wry' or 'cef'`); + process.exit(1); +} +if (!['dev', 'build'].includes(cmd)) { + logger.error(`Invalid command: ${cmd}. Must be 'dev' or 'build'`); + process.exit(1); +} + +tauriArgs.unshift('--features', runtime); +if (!tauriArgs.includes('--')) { + tauriArgs.push('--'); +} +tauriArgs.push('--no-default-features'); + +const args = ['tauri', cmd, ...tauriArgs]; + +const command = 'cargo'; + +logger.info(`${dim('Running:')} ${command} ${args.join(' ')}`); + +// Spawn the tauri process +const proc = spawn(command, args, { + stdio: 'inherit', + shell: false, + cwd: join(__dirname, '..'), +}); + +proc.on('error', (error) => { + logger.error(`Failed to start process: ${error.message}`); + process.exit(1); +}); + +proc.on('exit', (code) => { + process.exit(code ?? 0); +}); diff --git a/src-tauri/.gitignore b/src-tauri/.gitignore new file mode 100644 index 0000000000..489be3277b --- /dev/null +++ b/src-tauri/.gitignore @@ -0,0 +1,4 @@ +# Generated by Cargo +# will have compiled files and executables +/target/ +/gen/schemas diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock new file mode 100644 index 0000000000..3c83abb322 --- /dev/null +++ b/src-tauri/Cargo.lock @@ -0,0 +1,6924 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom 0.2.17", + "once_cell", + "version_check", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "android_log-sys" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84521a3cf562bc62942e294181d9eef17eb38ceb8c68677bc49f144e4c3d4f8d" + +[[package]] +name = "android_logger" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbb4e440d04be07da1f1bf44fb4495ebd58669372fe0cffa6e48595ac5bd88a3" +dependencies = [ + "android_log-sys", + "env_filter", + "log", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-process" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "async-signal" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix", + "signal-hook-registry", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "atk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b" +dependencies = [ + "atk-sys", + "glib", + "libc", +] + +[[package]] +name = "atk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" +dependencies = [ + "serde_core", +] + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + +[[package]] +name = "blocking" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + +[[package]] +name = "borsh" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfd1e3f8955a5d7de9fab72fc8373fade9fb8a703968cb200ae3dc6cf08e185a" +dependencies = [ + "borsh-derive", + "bytes", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfcfdc083699101d5a7965e49925975f2f55060f94f9a05e7187be95d530ca59" +dependencies = [ + "once_cell", + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "brotli" +version = "8.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bstr" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "byte-unit" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c6d47a4e2961fb8721bcfc54feae6455f2f64e7054f9bc67e875f0e77f4c58d" +dependencies = [ + "rust_decimal", + "schemars 1.2.1", + "serde", + "utf8-width", +] + +[[package]] +name = "bytecheck" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2" +dependencies = [ + "bytecheck_derive", + "ptr_meta", + "simdutf8", +] + +[[package]] +name = "bytecheck_derive" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +dependencies = [ + "serde", +] + +[[package]] +name = "cairo-rs" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" +dependencies = [ + "bitflags 2.11.1", + "cairo-sys-rs", + "glib", + "libc", + "once_cell", + "thiserror 1.0.69", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "camino" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror 2.0.18", +] + +[[package]] +name = "cargo_toml" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374b7c592d9c00c1f4972ea58390ac6b18cbb6ab79011f3bdc90a0b82ca06b77" +dependencies = [ + "serde", + "toml 0.9.12+spec-1.1.0", +] + +[[package]] +name = "cc" +version = "1.2.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfb" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" +dependencies = [ + "byteorder", + "fnv", + "uuid", +] + +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chacha20" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.1", +] + +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link 0.2.1", +] + +[[package]] +name = "chrono-tz" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93698b29de5e97ad0ae26447b344c482a7284c737d9ddc5f9e52b74a336671bb" +dependencies = [ + "chrono", + "chrono-tz-build", + "phf 0.11.3", +] + +[[package]] +name = "chrono-tz-build" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c088aee841df9c3041febbb73934cfc39708749bf96dc827e3359cd39ef11b1" +dependencies = [ + "parse-zoneinfo", + "phf 0.11.3", + "phf_codegen 0.11.3", +] + +[[package]] +name = "clap" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "console" +version = "0.15.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" +dependencies = [ + "encode_unicode", + "libc", + "once_cell", + "unicode-width", + "windows-sys 0.59.0", +] + +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom 0.2.17", + "once_cell", + "tiny-keccak", +] + +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + +[[package]] +name = "cookie_store" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15b2c103cf610ec6cae3da84a766285b42fd16aad564758459e6ecf128c75206" +dependencies = [ + "cookie", + "document-features", + "idna", + "log", + "publicsuffix", + "serde", + "serde_derive", + "serde_json", + "time", + "url", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" +dependencies = [ + "bitflags 2.11.1", + "core-foundation 0.10.1", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +dependencies = [ + "bitflags 2.11.1", + "core-foundation 0.10.1", + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "cssparser" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dae61cf9c0abb83bd659dab65b7e4e38d8236824c85f0f804f173567bda257d2" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "phf 0.13.1", + "smallvec", +] + +[[package]] +name = "cssparser-macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +dependencies = [ + "quote", + "syn 2.0.117", +] + +[[package]] +name = "ctor" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "352d39c2f7bef1d6ad73db6f5160efcaed66d94ef8c6c573a8410c00bf909a98" +dependencies = [ + "ctor-proc-macro", + "dtor", +] + +[[package]] +name = "ctor-proc-macro" +version = "0.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1" + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.117", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "data-url" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376" + +[[package]] +name = "dbus" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b942602992bb7acfd1f51c49811c58a610ef9181b6e66f3e519d79b540a3bf73" +dependencies = [ + "libc", + "libdbus-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", + "serde_core", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.117", +] + +[[package]] +name = "deunicode" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abd57806937c9cc163efc8ea3910e00a62e2aeb0b8119f1793a978088f8f6b04" + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", +] + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.11.1", + "block2", + "libc", + "objc2", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "dlopen2" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4" +dependencies = [ + "dlopen2_derive", + "libc", + "once_cell", + "winapi", +] + +[[package]] +name = "dlopen2_derive" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "dlv-list" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f" +dependencies = [ + "const-random", +] + +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + +[[package]] +name = "dom_query" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521e380c0c8afb8d9a1e83a1822ee03556fc3e3e7dbc1fd30be14e37f9cb3f89" +dependencies = [ + "bit-set", + "cssparser", + "foldhash 0.2.0", + "html5ever", + "precomputed-hash", + "selectors", + "tendril", +] + +[[package]] +name = "dpi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" +dependencies = [ + "serde", +] + +[[package]] +name = "dtoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" + +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", +] + +[[package]] +name = "dtor" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1057d6c64987086ff8ed0fd3fbf377a6b7d205cc7715868cd401705f715cbe4" +dependencies = [ + "dtor-proc-macro", +] + +[[package]] +name = "dtor-proc-macro" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5" + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "embed-resource" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31a88c8d26de40ed18fe748c547845aa39de1db3afd958f8cb91579f3644bcb" +dependencies = [ + "cc", + "memchr", + "rustc_version", + "toml 1.1.2+spec-1.1.0", + "vswhom", + "winreg", +] + +[[package]] +name = "embed_plist" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" + +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "endi" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" + +[[package]] +name = "enigo" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71744ff36f35a4276e8827add8102d0e792378c574fd93cb4e1c8e0505f96b7c" +dependencies = [ + "core-foundation 0.10.1", + "core-graphics", + "foreign-types-shared", + "libc", + "log", + "nom", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "windows", + "x11rb", + "xkbcommon", + "xkeysym", +] + +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "env_filter" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "erased-serde" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "fern" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4316185f709b23713e41e3195f90edef7fb00c3ed4adc79769cf09cc762a3b29" +dependencies = [ + "log", +] + +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "rustc_version", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "gdk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691" +dependencies = [ + "cairo-rs", + "gdk-pixbuf", + "gdk-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" +dependencies = [ + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", + "once_cell", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkwayland-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69" +dependencies = [ + "gdk-sys", + "glib-sys", + "gobject-sys", + "libc", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkx11" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3caa00e14351bebbc8183b3c36690327eb77c49abc2268dd4bd36b856db3fbfe" +dependencies = [ + "gdk", + "gdkx11-sys", + "gio", + "glib", + "libc", + "x11", +] + +[[package]] +name = "gdkx11-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d" +dependencies = [ + "gdk-sys", + "glib-sys", + "libc", + "system-deps", + "x11", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "gethostname" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" +dependencies = [ + "rustix", + "windows-link 0.2.1", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 5.3.0", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "rand_core 0.10.1", + "wasip2", + "wasip3", +] + +[[package]] +name = "gio" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "once_cell", + "pin-project-lite", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "gio-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "winapi", +] + +[[package]] +name = "glib" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" +dependencies = [ + "bitflags 2.11.1", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "once_cell", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "glib-macros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" +dependencies = [ + "heck 0.4.1", + "proc-macro-crate 2.0.2", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "glib-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "globset" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "globwalk" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757" +dependencies = [ + "bitflags 2.11.1", + "ignore", + "walkdir", +] + +[[package]] +name = "gobject-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gtk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a" +dependencies = [ + "atk", + "cairo-rs", + "field-offset", + "futures-channel", + "gdk", + "gdk-pixbuf", + "gio", + "glib", + "gtk-sys", + "gtk3-macros", + "libc", + "pango", + "pkg-config", +] + +[[package]] +name = "gtk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414" +dependencies = [ + "atk-sys", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "gtk3-macros" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "h2" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "171fefbc92fe4a4de27e0698d6a5b392d6a0e333506bc49133760b3bcf948733" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap 2.14.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "html5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1054432bae2f14e0061e33d23402fbaa67a921d319d56adc6bcf887ddad1cbc2" +dependencies = [ + "log", + "markup5ever", +] + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "humansize" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7" +dependencies = [ + "libm", +] + +[[package]] +name = "hyper" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "system-configuration", + "tokio", + "tower-service", + "tracing", + "windows-registry 0.6.1", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core 0.62.2", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ico" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371" +dependencies = [ + "byteorder", + "png 0.17.16", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "ignore" +version = "0.4.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3d782a365a015e0f5c04902246139249abf769125006fbe7649e2ee88169b4a" +dependencies = [ + "crossbeam-deque", + "globset", + "log", + "memchr", + "regex-automata", + "same-file", + "walkdir", + "winapi-util", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.0", + "serde", + "serde_core", +] + +[[package]] +name = "indicatif" +version = "0.17.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "183b3088984b400f4cfac3620d5e076c84da5364016b4f49473de574b2586235" +dependencies = [ + "console", + "number_prefix", + "portable-atomic", + "unicode-width", + "web-time", +] + +[[package]] +name = "infer" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7" +dependencies = [ + "cfb", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "iri-string" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25e659a4bb38e810ebc252e53b5814ff908a8c58c2a9ce2fae1bbec24cbf4e20" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "is-docker" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" +dependencies = [ + "once_cell", +] + +[[package]] +name = "is-wsl" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" +dependencies = [ + "is-docker", + "once_cell", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "javascriptcore-rs" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc" +dependencies = [ + "bitflags 1.3.2", + "glib", + "javascriptcore-rs-sys", +] + +[[package]] +name = "javascriptcore-rs-sys" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys 0.3.1", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.117", +] + +[[package]] +name = "js-sys" +version = "0.3.97" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1840c94c045fbcf8ba2812c95db44499f7c64910a912551aaaa541decebcacf" +dependencies = [ + "cfg-if", + "futures-util", + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "json-patch" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "863726d7afb6bc2590eeff7135d923545e5e964f004c2ccf8716c25e70a86f08" +dependencies = [ + "jsonptr", + "serde", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] +name = "jsonptr" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dea2b27dd239b2556ed7a25ba842fe47fd602e7fc7433c2a8d6106d4d9edd70" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "keyboard-types" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" +dependencies = [ + "bitflags 2.11.1", + "serde", + "unicode-segmentation", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libappindicator" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a" +dependencies = [ + "glib", + "gtk", + "gtk-sys", + "libappindicator-sys", + "log", +] + +[[package]] +name = "libappindicator-sys" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" +dependencies = [ + "gtk-sys", + "libloading", + "once_cell", +] + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libdbus-sys" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043" +dependencies = [ + "pkg-config", +] + +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libredox" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" +dependencies = [ + "libc", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +dependencies = [ + "value-bag", +] + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "mac-notification-sys" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29a16783dd1a47849b8c8133c9cd3eb2112cfbc6901670af3dba47c8bbfb07d3" +dependencies = [ + "cc", + "objc2", + "objc2-foundation", + "time", +] + +[[package]] +name = "markup5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8983d30f2915feeaaab2d6babdd6bc7e9ed1a00b66b5e6d74df19aa9c0e91862" +dependencies = [ + "log", + "tendril", + "web_atoms", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "memmap2" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3" +dependencies = [ + "libc", +] + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "muda" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ae8844f63b5b118e334e205585b8c5c17b984121dbdb179d44aeb087ffad3cb" +dependencies = [ + "crossbeam-channel", + "dpi", + "gtk", + "keyboard-types", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "once_cell", + "png 0.18.1", + "serde", + "thiserror 2.0.18", + "windows-sys 0.61.2", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.11.1", + "jni-sys 0.3.1", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys 0.3.1", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +dependencies = [ + "bitflags 2.11.1", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + +[[package]] +name = "notify-rust" +version = "4.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50ff2e74231b72c832d82982193b417f230945be6bdb5575b251d941d31adb00" +dependencies = [ + "futures-lite", + "log", + "mac-notification-sys", + "serde", + "tauri-winrt-notification", + "zbus", +] + +[[package]] +name = "num-conv" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + +[[package]] +name = "number_prefix" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", + "objc2-exception-helper", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.11.1", + "block2", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" +dependencies = [ + "bitflags 2.11.1", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-data" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.11.1", + "dispatch2", + "objc2", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.11.1", + "dispatch2", + "objc2", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-core-image" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-location" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-text" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" +dependencies = [ + "bitflags 2.11.1", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-exception-helper" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a1c5fbb72d7735b076bb47b578523aedc40f3c439bea6dfd595c089d79d98a" +dependencies = [ + "cc", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.11.1", + "block2", + "libc", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.11.1", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags 2.11.1", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" +dependencies = [ + "bitflags 2.11.1", + "block2", + "objc2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image", + "objc2-core-location", + "objc2-core-text", + "objc2-foundation", + "objc2-quartz-core", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-web-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" +dependencies = [ + "bitflags 2.11.1", + "block2", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "open" +version = "5.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f3bab717c29a857abf75fcef718d441ec7cb2725f937343c734740a985d37fd" +dependencies = [ + "dunce", + "is-wsl", + "libc", + "pathdiff", +] + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "ordered-multimap" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79" +dependencies = [ + "dlv-list", + "hashbrown 0.14.5", +] + +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "os_info" +version = "3.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4022a17595a00d6a369236fdae483f0de7f0a339960a53118b818238e132224" +dependencies = [ + "android_system_properties", + "log", + "nix", + "objc2", + "objc2-foundation", + "objc2-ui-kit", + "serde", + "windows-sys 0.61.2", +] + +[[package]] +name = "pango" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" +dependencies = [ + "gio", + "glib", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link 0.2.1", +] + +[[package]] +name = "parse-zoneinfo" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f2a05b18d44e2957b88f96ba460715e295bc1d7510468a2f3d3b44535d26c24" +dependencies = [ + "regex", +] + +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pest" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "pest_meta" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" +dependencies = [ + "pest", + "sha2", +] + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_macros 0.11.3", + "phf_shared 0.11.3", +] + +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros 0.13.1", + "phf_shared 0.13.1", + "serde", +] + +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", +] + +[[package]] +name = "phf_codegen" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" +dependencies = [ + "phf_generator 0.13.1", + "phf_shared 0.13.1", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared 0.11.3", + "rand 0.8.6", +] + +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared 0.13.1", +] + +[[package]] +name = "phf_macros" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator 0.13.1", + "phf_shared 0.13.1", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "piper" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "plist" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "092791278e026273c1b65bbdcfbba3a300f2994c896bd01ab01da613c29c46f1" +dependencies = [ + "base64 0.22.1", + "indexmap 2.14.0", + "quick-xml 0.39.3", + "serde", + "time", +] + +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.11.1", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.117", +] + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" +dependencies = [ + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit 0.25.11+spec-1.1.0", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "psl-types" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac" + +[[package]] +name = "ptr_meta" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "publicsuffix" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42ea446cab60335f76979ec15e12619a2165b5ae2c12166bef27d283a9fadf" +dependencies = [ + "idna", + "psl-types", +] + +[[package]] +name = "quick-xml" +version = "0.37.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" +dependencies = [ + "memchr", +] + +[[package]] +name = "quick-xml" +version = "0.39.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "721da970c312655cde9b4ffe0547f20a8494866a4af5ff51f18b7c633d0c870b" +dependencies = [ + "memchr", +] + +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.18", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +dependencies = [ + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.4", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.60.2", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" +dependencies = [ + "chacha20", + "getrandom 0.4.2", + "rand_core 0.10.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.11.1", +] + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 2.0.18", +] + +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "rend" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c" +dependencies = [ + "bytecheck", +] + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64 0.22.1", + "bytes", + "cookie", + "cookie_store", + "encoding_rs", + "futures-core", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "mime", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", +] + +[[package]] +name = "reqwest" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62e0021ea2c22aed41653bc7e1419abb2c97e038ff2c33d0e1309e49a97deec0" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "sync_wrapper", + "tokio", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rkyv" +version = "0.7.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2297bf9c81a3f0dc96bc9521370b88f054168c29826a75e89c55ff196e7ed6a1" +dependencies = [ + "bitvec", + "bytecheck", + "bytes", + "hashbrown 0.12.3", + "ptr_meta", + "rend", + "rkyv_derive", + "seahash", + "tinyvec", + "uuid", +] + +[[package]] +name = "rkyv_derive" +version = "0.7.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84d7b42d4b8d06048d3ac8db0eb31bcb942cbeb709f0b5f2b2ebde398d3038f5" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "rust-ini" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "796e8d2b6696392a43bea58116b667fb4c29727dc5abd27d6acf338bb4f688c7" +dependencies = [ + "cfg-if", + "ordered-multimap", +] + +[[package]] +name = "rust_decimal" +version = "1.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ce901f9a19d251159075a4c37af514c3b8ef99c22e02dd8c19161cf397ee94a" +dependencies = [ + "arrayvec", + "borsh", + "bytes", + "num-traits", + "rand 0.8.6", + "rkyv", + "serde", + "serde_json", + "wasm-bindgen", +] + +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.11.1", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "sable" +version = "0.1.0" +dependencies = [ + "enigo", + "log", + "serde", + "serde_json", + "tauri", + "tauri-build", + "tauri-plugin-deep-link", + "tauri-plugin-edge-to-edge", + "tauri-plugin-http", + "tauri-plugin-log", + "tauri-plugin-notifications", + "tauri-plugin-opener", + "tauri-plugin-os", + "tauri-plugin-single-instance", + "tauri-plugin-store", + "tauri-plugin-window-state", + "tauri-typegen", + "tokio", + "ts-rs", + "windows", +] + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schemars" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +dependencies = [ + "dyn-clone", + "indexmap 1.9.3", + "schemars_derive", + "serde", + "serde_json", + "url", + "uuid", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.117", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + +[[package]] +name = "selectors" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c" +dependencies = [ + "bitflags 2.11.1", + "cssparser", + "derive_more", + "log", + "new_debug_unreachable", + "phf 0.13.1", + "phf_codegen 0.13.1", + "precomputed-hash", + "rustc-hash", + "servo_arc", + "smallvec", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-rename-rule" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8a059d895f1a31dd928f40abbea4e7177e3d8ff3aa4152fdb7a396ae1ef63a3" + +[[package]] +name = "serde-untagged" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" +dependencies = [ + "erased-serde", + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05839ce67618e14a09b286535c0d9c94e85ef25469b0e13cb4f844e5593eb19" +dependencies = [ + "base64 0.22.1", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.14.0", + "schemars 0.9.0", + "schemars 1.2.1", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf2ebbe86054f9b45bc3881e865683ccfaccce97b9b4cb53f3039d67f355a334" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serialize-to-javascript" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04f3666a07a197cdb77cdf306c32be9b7f598d7060d50cfd4d5aa04bfd92f6c5" +dependencies = [ + "serde", + "serde_json", + "serialize-to-javascript-impl", +] + +[[package]] +name = "serialize-to-javascript-impl" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "servo_arc" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "slug" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "882a80f72ee45de3cc9a5afeb2da0331d58df69e4e7d8eeb5d3c7784ae67e724" +dependencies = [ + "deunicode", + "wasm-bindgen", +] + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "softbuffer" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3" +dependencies = [ + "bytemuck", + "js-sys", + "ndk", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "objc2-quartz-core", + "raw-window-handle", + "redox_syscall", + "tracing", + "wasm-bindgen", + "web-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "soup3" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f" +dependencies = [ + "futures-channel", + "gio", + "glib", + "libc", + "soup3-sys", +] + +[[package]] +name = "soup3-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "string_cache" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared 0.13.1", + "precomputed-hash", +] + +[[package]] +name = "string_cache_codegen" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" +dependencies = [ + "phf_generator 0.13.1", + "phf_shared 0.13.1", + "proc-macro2", + "quote", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "swift-bridge" +version = "0.1.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384ed39ea10f1cefabb197b7d8e67f0034b15a94ccbb1038b8e020da59bfb0be" +dependencies = [ + "once_cell", + "swift-bridge-build", + "swift-bridge-macro", + "tokio", +] + +[[package]] +name = "swift-bridge-build" +version = "0.1.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71b36df21e7f8a8b5eeb718d2e71f9cfc308477bfb705981cca705de9767dcb7" +dependencies = [ + "proc-macro2", + "swift-bridge-ir", + "syn 1.0.109", + "tempfile", +] + +[[package]] +name = "swift-bridge-ir" +version = "0.1.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c73bd16155df50708b92306945656e57d62d321290a7db490f299f709fb31c83" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "swift-bridge-macro" +version = "0.1.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a13dc0dc875d85341dec5b5344a7d713f20eb5650b71086b27d09a6ece272f" +dependencies = [ + "proc-macro2", + "quote", + "swift-bridge-ir", + "syn 1.0.109", +] + +[[package]] +name = "swift-rs" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4057c98e2e852d51fdcfca832aac7b571f6b351ad159f9eda5db1655f8d0c4d7" +dependencies = [ + "base64 0.21.7", + "serde", + "serde_json", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "sys-locale" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eab9a99a024a169fe8a903cf9d4a3b3601109bcc13bd9e3c6fff259138626c4" +dependencies = [ + "libc", +] + +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags 2.11.1", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck 0.5.0", + "pkg-config", + "toml 0.8.2", + "version-compare", +] + +[[package]] +name = "tao" +version = "0.35.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a33f7f9e486ade65fcf1e45c440f9236c904f5c1002cdc7fc6ae582777345ce4" +dependencies = [ + "bitflags 2.11.1", + "block2", + "core-foundation 0.10.1", + "core-graphics", + "crossbeam-channel", + "dbus", + "dispatch2", + "dlopen2", + "dpi", + "gdkwayland-sys", + "gdkx11-sys", + "gtk", + "jni", + "libc", + "log", + "ndk", + "ndk-sys", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "once_cell", + "parking_lot", + "percent-encoding", + "raw-window-handle", + "tao-macros", + "unicode-segmentation", + "url", + "windows", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "tao-macros" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "tauri" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d059f2527558d9dba6f186dec4772610e1aecfd3f94002397613e7e648752b66" +dependencies = [ + "anyhow", + "bytes", + "cookie", + "dirs", + "dunce", + "embed_plist", + "getrandom 0.3.4", + "glob", + "gtk", + "heck 0.5.0", + "http", + "jni", + "libc", + "log", + "mime", + "muda", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "percent-encoding", + "plist", + "raw-window-handle", + "reqwest 0.13.3", + "serde", + "serde_json", + "serde_repr", + "serialize-to-javascript", + "swift-rs", + "tauri-build", + "tauri-macros", + "tauri-runtime", + "tauri-runtime-wry", + "tauri-utils", + "thiserror 2.0.18", + "tokio", + "tray-icon", + "url", + "webkit2gtk", + "webview2-com", + "window-vibrancy", + "windows", +] + +[[package]] +name = "tauri-build" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be9aa8c59a894f76c29a002501c589de5eb4987a5913d62a6e0a47f320901988" +dependencies = [ + "anyhow", + "cargo_toml", + "dirs", + "glob", + "heck 0.5.0", + "json-patch", + "schemars 0.8.22", + "semver", + "serde", + "serde_json", + "tauri-utils", + "tauri-winres", + "walkdir", +] + +[[package]] +name = "tauri-codegen" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e4e8230d565106aa19dfbaa01a7ed01abf78047fe0577a83377224bd1bf20e" +dependencies = [ + "base64 0.22.1", + "brotli", + "ico", + "json-patch", + "plist", + "png 0.17.16", + "proc-macro2", + "quote", + "semver", + "serde", + "serde_json", + "sha2", + "syn 2.0.117", + "tauri-utils", + "thiserror 2.0.18", + "time", + "url", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc8de2cddbbc33dbdf4c84f170121886595efdbcc9cb4b3d76342b79d082cedc" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", + "tauri-codegen", + "tauri-utils", +] + +[[package]] +name = "tauri-plugin" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8d5f58bfd0cdcfdbc0a68dc08b354eea2afc551b421de91b07b69e0dd769d57" +dependencies = [ + "anyhow", + "glob", + "plist", + "schemars 0.8.22", + "serde", + "serde_json", + "tauri-utils", + "walkdir", +] + +[[package]] +name = "tauri-plugin-deep-link" +version = "2.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ee75bc5627f77bfdf40c913255ebc258117b10ebe2b2239a1a1cf40b0b58aa" +dependencies = [ + "dunce", + "plist", + "rust-ini", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "tauri-utils", + "thiserror 2.0.18", + "tracing", + "url", + "windows-registry 0.5.3", + "windows-result 0.3.4", +] + +[[package]] +name = "tauri-plugin-edge-to-edge" +version = "0.3.3" +source = "git+https://github.com/SableClient/tauri-plugin-edge-to-edge.git?rev=33c6116c27be28c06df5a9d02231ecc5fdeb93c5#33c6116c27be28c06df5a9d02231ecc5fdeb93c5" +dependencies = [ + "serde", + "tauri", + "tauri-plugin", + "thiserror 2.0.18", +] + +[[package]] +name = "tauri-plugin-fs" +version = "2.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7ecc274121aca0c036a2b42d1cbe83d368d348f54e0bb8a735c2b1548e8f371" +dependencies = [ + "anyhow", + "dunce", + "glob", + "log", + "objc2-foundation", + "percent-encoding", + "schemars 0.8.22", + "serde", + "serde_json", + "serde_repr", + "tauri", + "tauri-plugin", + "tauri-utils", + "thiserror 2.0.18", + "toml 1.1.2+spec-1.1.0", + "url", +] + +[[package]] +name = "tauri-plugin-http" +version = "2.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5bd512048e1985b7ec78f96d99083e2ddaf7e0d906b2b63c44ce5bb8b894067" +dependencies = [ + "bytes", + "cookie_store", + "data-url", + "http", + "regex", + "reqwest 0.12.28", + "schemars 0.8.22", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "tauri-plugin-fs", + "thiserror 2.0.18", + "tokio", + "url", + "urlpattern", +] + +[[package]] +name = "tauri-plugin-log" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7545bd67f070a4500432c826e2e0682146a1d6712aee22a2786490156b574d93" +dependencies = [ + "android_logger", + "byte-unit", + "fern", + "log", + "objc2", + "objc2-foundation", + "serde", + "serde_json", + "serde_repr", + "swift-rs", + "tauri", + "tauri-plugin", + "thiserror 2.0.18", + "time", +] + +[[package]] +name = "tauri-plugin-notifications" +version = "0.4.3" +source = "git+https://github.com/SableClient/tauri-plugin-notifications.git?rev=9d15b2b2c8798dc29ce723d743f0f3b4921d6a54#9d15b2b2c8798dc29ce723d743f0f3b4921d6a54" +dependencies = [ + "log", + "notify-rust", + "rand 0.10.1", + "serde", + "serde_json", + "serde_repr", + "swift-bridge", + "swift-bridge-build", + "tauri", + "tauri-plugin", + "thiserror 2.0.18", + "time", + "url", +] + +[[package]] +name = "tauri-plugin-opener" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17e1bea14edce6b793a04e2417e3fd924b9bc4faae83cdee7d714156cceeed29" +dependencies = [ + "dunce", + "glob", + "objc2-app-kit", + "objc2-foundation", + "open", + "schemars 0.8.22", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror 2.0.18", + "url", + "windows", + "zbus", +] + +[[package]] +name = "tauri-plugin-os" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8f08346c8deb39e96f86973da0e2d76cbb933d7ac9b750f6dc4daf955a6f997" +dependencies = [ + "gethostname", + "log", + "os_info", + "serde", + "serde_json", + "serialize-to-javascript", + "sys-locale", + "tauri", + "tauri-plugin", + "thiserror 2.0.18", +] + +[[package]] +name = "tauri-plugin-single-instance" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c8f29386f5e9fdc699182388a33ee80a56de436d91b67459e86afef426282af" +dependencies = [ + "serde", + "serde_json", + "tauri", + "tauri-plugin-deep-link", + "thiserror 2.0.18", + "tracing", + "windows-sys 0.60.2", + "zbus", +] + +[[package]] +name = "tauri-plugin-store" +version = "2.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c72dda16786eb4a3f903e43a17b64d8d78dc0f00fe2aa4b757c28f617a8630b" +dependencies = [ + "dunce", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror 2.0.18", + "tokio", + "tracing", +] + +[[package]] +name = "tauri-plugin-window-state" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73736611e14142408d15353e21e3cca2f12a3cfb523ad0ce85999b6d2ef1a704" +dependencies = [ + "bitflags 2.11.1", + "log", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror 2.0.18", +] + +[[package]] +name = "tauri-runtime" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e42bbcb76237351fbaa02f08d808c537dc12eb5a6eabbf3e517b50056334d95" +dependencies = [ + "cookie", + "dpi", + "gtk", + "http", + "jni", + "objc2", + "objc2-ui-kit", + "objc2-web-kit", + "raw-window-handle", + "serde", + "serde_json", + "tauri-utils", + "thiserror 2.0.18", + "url", + "webkit2gtk", + "webview2-com", + "windows", +] + +[[package]] +name = "tauri-runtime-wry" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cadb13dad0c681e1e0a2c49ae488f0e2906ded3d57e7a0017f4aaf46e387117" +dependencies = [ + "gtk", + "http", + "jni", + "log", + "objc2", + "objc2-app-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "softbuffer", + "tao", + "tauri-runtime", + "tauri-utils", + "url", + "webkit2gtk", + "webview2-com", + "windows", + "wry", +] + +[[package]] +name = "tauri-typegen" +version = "0.5.0" +source = "git+https://github.com/SableClient/tauri-typegen?branch=fix%2Fnondeterministic-generation-cache#645ac948bbd940704cda4419a203e307e7f0d429" +dependencies = [ + "chrono", + "clap", + "indicatif", + "proc-macro2", + "quote", + "regex", + "serde", + "serde-rename-rule", + "serde_json", + "syn 2.0.117", + "tera", + "thiserror 2.0.18", + "walkdir", +] + +[[package]] +name = "tauri-utils" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55f61d2bf7188fbcf2b0ed095b67a6bc498f713c939314bb19eb700118a573b7" +dependencies = [ + "anyhow", + "brotli", + "cargo_metadata", + "ctor", + "dom_query", + "dunce", + "glob", + "http", + "infer", + "json-patch", + "log", + "memchr", + "phf 0.11.3", + "plist", + "proc-macro2", + "quote", + "regex", + "schemars 0.8.22", + "semver", + "serde", + "serde-untagged", + "serde_json", + "serde_with", + "swift-rs", + "thiserror 2.0.18", + "toml 1.1.2+spec-1.1.0", + "url", + "urlpattern", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-winres" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc65d45c68858bfe420dd29e834b5d15dbecf8a07a8a16cf4d532c7b1f69d4b6" +dependencies = [ + "dunce", + "embed-resource", + "toml 1.1.2+spec-1.1.0", +] + +[[package]] +name = "tauri-winrt-notification" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b1e66e07de489fe43a46678dd0b8df65e0c973909df1b60ba33874e297ba9b9" +dependencies = [ + "quick-xml 0.37.5", + "thiserror 2.0.18", + "windows", + "windows-version", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.2", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "tendril" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4790fc369d5a530f4b544b094e31388b9b3a37c0f4652ade4505945f5660d24" +dependencies = [ + "new_debug_unreachable", + "utf-8", +] + +[[package]] +name = "tera" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8004bca281f2d32df3bacd59bc67b312cb4c70cea46cbd79dbe8ac5ed206722" +dependencies = [ + "chrono", + "chrono-tz", + "globwalk", + "humansize", + "lazy_static", + "percent-encoding", + "pest", + "pest_derive", + "rand 0.8.6", + "regex", + "serde", + "serde_json", + "slug", + "unicode-segmentation", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "libc", + "num-conv", + "num_threads", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.52.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "110a78583f19d5cdb2c5ccf321d1290344e71313c6c37d43520d386027d18386" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" +dependencies = [ + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "toml" +version = "0.9.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +dependencies = [ + "indexmap 2.14.0", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 0.7.15", +] + +[[package]] +name = "toml" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" +dependencies = [ + "indexmap 2.14.0", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 1.0.2", +] + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap 2.14.0", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +dependencies = [ + "indexmap 2.14.0", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.25.11+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b" +dependencies = [ + "indexmap 2.14.0", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.2", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow 1.0.2", +] + +[[package]] +name = "toml_writer" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags 2.11.1", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tray-icon" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15edbb0d80583e85ee8df283410038e17314df5cba30da2087a54a85216c0773" +dependencies = [ + "crossbeam-channel", + "dirs", + "libappindicator", + "muda", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "once_cell", + "png 0.18.1", + "serde", + "thiserror 2.0.18", + "windows-sys 0.61.2", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "ts-rs" +version = "12.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "756050066659291d47a554a9f558125db17428b073c5ffce1daf5dcb0f7231d8" +dependencies = [ + "thiserror 2.0.18", + "ts-rs-macros", +] + +[[package]] +name = "ts-rs-macros" +version = "12.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38d90eea51bc7988ef9e674bf80a85ba6804739e535e9cab48e4bb34a8b652aa" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "termcolor", +] + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "typenum" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + +[[package]] +name = "uds_windows" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" +dependencies = [ + "memoffset", + "tempfile", + "windows-sys 0.61.2", +] + +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range", +] + +[[package]] +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" + +[[package]] +name = "unic-ucd-ident" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", + "serde_derive", +] + +[[package]] +name = "urlpattern" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70acd30e3aa1450bc2eece896ce2ad0d178e9c079493819301573dae3c37ba6d" +dependencies = [ + "regex", + "serde", + "unic-ucd-ident", + "url", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8-width" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1292c0d970b54115d14f2492fe0170adf21d68a1de108eebc51c1df4f346a091" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" +dependencies = [ + "getrandom 0.4.2", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "value-bag" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ba6f5989077681266825251a52748b8c1d8a4ad098cc37e440103d0ea717fc0" + +[[package]] +name = "version-compare" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.120" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df52b6d9b87e0c74c9edfa1eb2d9bf85e5d63515474513aa50fa181b3c4f5db1" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af934872acec734c2d80e6617bbb5ff4f12b052dd8e6332b0817bce889516084" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.120" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b1041f495fb322e64aca85f5756b2172e35cd459376e67f2a6c9dffcedb103" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.120" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dcd0ff20416988a18ac686d4d4d0f6aae9ebf08a389ff5d29012b05af2a1b41" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.117", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.120" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49757b3c82ebf16c57d69365a142940b384176c24df52a087fb748e2085359ea" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap 2.14.0", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasm-streams" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.11.1", + "hashbrown 0.15.5", + "indexmap 2.14.0", + "semver", +] + +[[package]] +name = "web-sys" +version = "0.3.97" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eadbac71025cd7b0834f20d1fe8472e8495821b4e9801eb0a60bd1f19827602" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web_atoms" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7cff6eef815df1834fd250e3a2ff436044d82a9f1bc1980ca1dbdf07effc538" +dependencies = [ + "phf 0.13.1", + "phf_codegen 0.13.1", + "string_cache", + "string_cache_codegen", +] + +[[package]] +name = "webkit2gtk" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1027150013530fb2eaf806408df88461ae4815a45c541c8975e61d6f2fc4793" +dependencies = [ + "bitflags 1.3.2", + "cairo-rs", + "gdk", + "gdk-sys", + "gio", + "gio-sys", + "glib", + "glib-sys", + "gobject-sys", + "gtk", + "gtk-sys", + "javascriptcore-rs", + "libc", + "once_cell", + "soup3", + "webkit2gtk-sys", +] + +[[package]] +name = "webkit2gtk-sys" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916a5f65c2ef0dfe12fff695960a2ec3d4565359fdbb2e9943c974e06c734ea5" +dependencies = [ + "bitflags 1.3.2", + "cairo-sys-rs", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "gtk-sys", + "javascriptcore-rs-sys", + "libc", + "pkg-config", + "soup3-sys", + "system-deps", +] + +[[package]] +name = "webpki-roots" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "webview2-com" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a" +dependencies = [ + "webview2-com-macros", + "webview2-com-sys", + "windows", + "windows-core 0.61.2", + "windows-implement", + "windows-interface", +] + +[[package]] +name = "webview2-com-macros" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "webview2-com-sys" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c" +dependencies = [ + "thiserror 2.0.18", + "windows", + "windows-core 0.61.2", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "window-vibrancy" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c" +dependencies = [ + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "raw-window-handle", + "windows-sys 0.59.0", + "windows-version", +] + +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections", + "windows-core 0.61.2", + "windows-future", + "windows-link 0.1.3", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.2", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-registry" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" +dependencies = [ + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-version" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" + +[[package]] +name = "winnow" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.55.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97" +dependencies = [ + "cfg-if", + "windows-sys 0.59.0", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck 0.5.0", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck 0.5.0", + "indexmap 2.14.0", + "prettyplease", + "syn 2.0.117", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.117", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.11.1", + "indexmap 2.14.0", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap 2.14.0", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "wry" +version = "0.55.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186f9871daa55fd9c016578b810d149de58367113db7fb72b462d2323ce19514" +dependencies = [ + "base64 0.22.1", + "block2", + "cookie", + "crossbeam-channel", + "dirs", + "dom_query", + "dpi", + "dunce", + "gdkx11", + "gtk", + "http", + "javascriptcore-rs", + "jni", + "libc", + "ndk", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "sha2", + "soup3", + "tao-macros", + "thiserror 2.0.18", + "url", + "webkit2gtk", + "webkit2gtk-sys", + "webview2-com", + "windows", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "x11rb" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" +dependencies = [ + "gethostname", + "rustix", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" + +[[package]] +name = "xkbcommon" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d66ca9352cbd4eecbbc40871d8a11b4ac8107cfc528a6e14d7c19c69d0e1ac9" +dependencies = [ + "libc", + "memmap2", + "xkeysym", +] + +[[package]] +name = "xkeysym" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" + +[[package]] +name = "yoke" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "zbus" +version = "5.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3bcbf15c8708d7fc1be0c993622e0a5cbd5e8b52bfa40afa4c3e0cd8d724ac1" +dependencies = [ + "async-broadcast", + "async-executor", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener", + "futures-core", + "futures-lite", + "hex", + "libc", + "ordered-stream", + "rustix", + "serde", + "serde_repr", + "tracing", + "uds_windows", + "uuid", + "windows-sys 0.61.2", + "winnow 1.0.2", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "5.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51fa5406ad9175a8c825a931f8cf347116b531b3634fcb0b627c290f1f2516ff" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", + "zbus_names", + "zvariant", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "4.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7074f3e50b894eac91750142016d30d0a89be8e67dbfd9704fb875825760e52d" +dependencies = [ + "serde", + "winnow 1.0.2", + "zvariant", +] + +[[package]] +name = "zerocopy" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zerofrom" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zvariant" +version = "5.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c1567a6ec68df868cbbfde844cfc6d81649fe5109a62b116b19fabd53e618ee" +dependencies = [ + "endi", + "enumflags2", + "serde", + "winnow 1.0.2", + "zvariant_derive", + "zvariant_utils", +] + +[[package]] +name = "zvariant_derive" +version = "5.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7d5b780599bbde114e39d9a0799577fad1ced5105d38515745f7b3099d8ceda" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "3.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d464f5733ffa07a3164d656f18533caace9d0638596721355d73256a410d691" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn 2.0.117", + "winnow 1.0.2", +] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml new file mode 100644 index 0000000000..2cb4ce6137 --- /dev/null +++ b/src-tauri/Cargo.toml @@ -0,0 +1,92 @@ +[package] +name = "sable" +version = "0.1.0" +description = "Yet another matrix client but better" +authors = ["you"] +license = "" +repository = "" +edition = "2021" +rust-version = "1.88.0" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +name = "app_lib" +crate-type = ["staticlib", "cdylib", "rlib"] + +[build-dependencies] +tauri-build = { version = "2.5.6", features = [] } +tauri-typegen = "0.5" + +[dependencies] +serde_json = "1.0" +serde = { version = "1.0", features = ["derive"] } +log = "0.4" +tokio = { version = "1", features = ["time", "sync"] } +ts-rs = "12.0.0" + +tauri = { version = "2.11.0", default-features = false, features = ["tray-icon", "dynamic-acl", "compression", "devtools", "x11", "common-controls-v6", "dbus"] } + +tauri-plugin-log = "2.8.0" +tauri-plugin-opener = "2.5.4" +tauri-plugin-os = "2.3.2" +tauri-plugin-deep-link = "2.4.9" +tauri-plugin-http = "2.5.9" +tauri-plugin-store = "2.4.3" + +[target.'cfg(target_os = "windows")'.dependencies] +enigo = "0.5.0" +windows = { version = "0.61", features = [ + "Win32_Foundation", + "Win32_UI_WindowsAndMessaging", + "Win32_System_Threading", + "Win32_System_ProcessStatus" +] } + +[target.'cfg(any(target_os = "macos", windows, target_os = "linux"))'.dependencies] +tauri-plugin-single-instance = { version = "2.4.2", features = ["deep-link"] } + +[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] +tauri-plugin-window-state = "2.4.1" + +[target.'cfg(any(target_os = "android", target_os = "ios"))'.dependencies] +tauri-plugin-notifications = { git = "https://github.com/SableClient/tauri-plugin-notifications.git", rev = "9d15b2b2c8798dc29ce723d743f0f3b4921d6a54", features = ["push-notifications", "unified-push"] } +tauri-plugin-edge-to-edge = { git = "https://github.com/SableClient/tauri-plugin-edge-to-edge.git", rev = "33c6116c27be28c06df5a9d02231ecc5fdeb93c5" } + +[features] +default = ["wry"] +custom-protocol = ["tauri/custom-protocol"] +wry = ["tauri/wry"] +cef = [] +[patch.crates-io] +tauri-typegen = { git = "https://github.com/SableClient/tauri-typegen", branch = "fix/nondeterministic-generation-cache" } + +# To test unreleased tauri features +# tauri = { git = "https://github.com/tauri-apps/tauri", branch = "dev" } +# tauri-build = { git = "https://github.com/tauri-apps/tauri", branch = "dev" } +# tauri-plugin = { git = "https://github.com/tauri-apps/tauri", branch = "dev" } +# tauri-utils = { git = "https://github.com/tauri-apps/tauri", branch = "dev" } +# tauri-macros = { git = "https://github.com/tauri-apps/tauri", branch = "dev" } +# tauri-runtime = { git = "https://github.com/tauri-apps/tauri", branch = "dev" } +# tauri-runtime-wry = { git = "https://github.com/tauri-apps/tauri", branch = "dev" } + +# To test the CEF runtime path, switch this back to `cef = ["tauri/cef"]` +# and uncomment the patch scaffold below. +# cef = ["tauri/cef"] +# +# [patch.crates-io] +# tauri = { git = "https://github.com/tauri-apps/tauri", branch = "feat/cef" } +# tauri-build = { git = "https://github.com/tauri-apps/tauri", branch = "feat/cef" } +# tauri-plugin = { git = "https://github.com/tauri-apps/tauri", branch = "feat/cef" } +# tauri-utils = { git = "https://github.com/tauri-apps/tauri", branch = "feat/cef" } +# tauri-macros = { git = "https://github.com/tauri-apps/tauri", branch = "feat/cef" } +# tauri-runtime = { git = "https://github.com/tauri-apps/tauri", branch = "feat/cef" } +# tauri-runtime-wry = { git = "https://github.com/tauri-apps/tauri", branch = "feat/cef" } +# tauri-plugin-log = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "feat/cef" } +# tauri-plugin-opener = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "feat/cef" } +# tauri-plugin-os = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "feat/cef" } +# tauri-plugin-deep-link = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "feat/cef" } +# tauri-plugin-http = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "feat/cef" } +# tauri-plugin-store = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "feat/cef" } +# tauri-plugin-single-instance = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "feat/cef" } +# tauri-plugin-window-state = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "feat/cef" } diff --git a/src-tauri/build.rs b/src-tauri/build.rs new file mode 100644 index 0000000000..c72d822f61 --- /dev/null +++ b/src-tauri/build.rs @@ -0,0 +1,8 @@ +fn main() { + println!("cargo:rustc-env=TS_RS_EXPORT_DIR=../src/app/generated/tauri"); + + tauri_typegen::BuildSystem::generate_at_build_time() + .expect("Failed to generate TypeScript bindings"); + + tauri_build::build() +} diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json new file mode 100644 index 0000000000..5e83a44b9e --- /dev/null +++ b/src-tauri/capabilities/default.json @@ -0,0 +1,34 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "default", + "description": "enables the default permissions", + "windows": ["main"], + "permissions": [ + "core:default", + "core:window:allow-close", + "core:window:allow-minimize", + "core:window:allow-maximize", + "core:window:allow-set-focus", + "core:window:allow-show", + "core:window:allow-is-maximized", + "core:window:allow-toggle-maximize", + "core:window:allow-start-dragging", + "deep-link:default", + "os:default", + "opener:default", + "opener:allow-default-urls", + { + "identifier": "http:default", + "allow": [{ "url": "http://*" }, { "url": "https://*" }] + }, + { + "identifier": "opener:allow-open-url", + "allow": [ + { + "url": "https://*", + "app": "inAppBrowser" + } + ] + } + ] +} diff --git a/src-tauri/capabilities/desktop.json b/src-tauri/capabilities/desktop.json new file mode 100644 index 0000000000..0e665f7fdf --- /dev/null +++ b/src-tauri/capabilities/desktop.json @@ -0,0 +1,7 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "desktop-capability", + "platforms": ["macOS", "windows", "linux"], + "windows": ["main"], + "permissions": ["window-state:default", "store:default"] +} diff --git a/src-tauri/capabilities/mobile.json b/src-tauri/capabilities/mobile.json new file mode 100644 index 0000000000..9a46cf55d3 --- /dev/null +++ b/src-tauri/capabilities/mobile.json @@ -0,0 +1,17 @@ +{ + "$schema": "../gen/schemas/mobile-schema.json", + "identifier": "mobile-capability", + "platforms": ["android", "iOS"], + "windows": ["main"], + "permissions": [ + "core:default", + "deep-link:default", + "edge-to-edge:default", + "notifications:default", + "notifications:allow-register-for-unified-push", + "notifications:allow-unregister-from-unified-push", + "notifications:allow-get-unified-push-distributors", + "notifications:allow-save-unified-push-distributor", + "notifications:allow-get-unified-push-distributor" + ] +} diff --git a/src-tauri/gen/android/.editorconfig b/src-tauri/gen/android/.editorconfig new file mode 100644 index 0000000000..ebe51d3bfa --- /dev/null +++ b/src-tauri/gen/android/.editorconfig @@ -0,0 +1,12 @@ +# EditorConfig is awesome: https://EditorConfig.org + +# top-most EditorConfig file +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = false +insert_final_newline = false \ No newline at end of file diff --git a/src-tauri/gen/android/.gitignore b/src-tauri/gen/android/.gitignore new file mode 100644 index 0000000000..026c5af237 --- /dev/null +++ b/src-tauri/gen/android/.gitignore @@ -0,0 +1,21 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +.kotlin/ +build +/captures +.externalNativeBuild +.cxx +local.properties +key.properties +keystore.properties + +/.tauri +/tauri.settings.gradle \ No newline at end of file diff --git a/src-tauri/gen/android/app/.gitignore b/src-tauri/gen/android/app/.gitignore new file mode 100644 index 0000000000..6c4d56b4a2 --- /dev/null +++ b/src-tauri/gen/android/app/.gitignore @@ -0,0 +1,6 @@ +/src/main/**/generated +/src/main/jniLibs/**/*.so +/src/main/assets/tauri.conf.json +/tauri.build.gradle.kts +/proguard-tauri.pro +/tauri.properties \ No newline at end of file diff --git a/src-tauri/gen/android/app/build.gradle.kts b/src-tauri/gen/android/app/build.gradle.kts new file mode 100644 index 0000000000..ae08822e2a --- /dev/null +++ b/src-tauri/gen/android/app/build.gradle.kts @@ -0,0 +1,86 @@ +import java.util.Properties +import java.io.FileInputStream + +plugins { + id("com.android.application") + id("org.jetbrains.kotlin.android") + id("rust") +} + +val tauriProperties = Properties().apply { + val propFile = file("tauri.properties") + if (propFile.exists()) { + propFile.inputStream().use { load(it) } + } +} + +android { + compileSdk = 36 + namespace = "moe.sable.client" + defaultConfig { + manifestPlaceholders["usesCleartextTraffic"] = "false" + applicationId = "moe.sable.client" + minSdk = 24 + targetSdk = 36 + versionCode = tauriProperties.getProperty("tauri.android.versionCode", "1").toInt() + versionName = tauriProperties.getProperty("tauri.android.versionName", "1.0") + } + signingConfigs { + create("release") { + val keystorePropertiesFile = rootProject.file("keystore.properties") + val keystoreProperties = Properties() + if (keystorePropertiesFile.exists()) { + keystoreProperties.load(FileInputStream(keystorePropertiesFile)) + } + + keyAlias = keystoreProperties["keyAlias"] as String + keyPassword = keystoreProperties["password"] as String + storeFile = file(keystoreProperties["storeFile"] as String) + storePassword = keystoreProperties["password"] as String + } + } + buildTypes { + getByName("debug") { + manifestPlaceholders["usesCleartextTraffic"] = "true" + isDebuggable = true + isJniDebuggable = true + isMinifyEnabled = false + packaging { jniLibs.keepDebugSymbols.add("*/arm64-v8a/*.so") + jniLibs.keepDebugSymbols.add("*/armeabi-v7a/*.so") + jniLibs.keepDebugSymbols.add("*/x86/*.so") + jniLibs.keepDebugSymbols.add("*/x86_64/*.so") + } + } + getByName("release") { + signingConfig = signingConfigs.getByName("release") + isMinifyEnabled = true + proguardFiles( + *fileTree(".") { include("**/*.pro") } + .plus(getDefaultProguardFile("proguard-android-optimize.txt")) + .toList().toTypedArray() + ) + } + } + kotlinOptions { + jvmTarget = "1.8" + } + buildFeatures { + buildConfig = true + } +} + +rust { + rootDirRel = "../../../" +} + +dependencies { + implementation("androidx.webkit:webkit:1.14.0") + implementation("androidx.appcompat:appcompat:1.7.1") + implementation("androidx.activity:activity-ktx:1.10.1") + implementation("com.google.android.material:material:1.12.0") + testImplementation("junit:junit:4.13.2") + androidTestImplementation("androidx.test.ext:junit:1.1.4") + androidTestImplementation("androidx.test.espresso:espresso-core:3.5.0") +} + +apply(from = "tauri.build.gradle.kts") diff --git a/src-tauri/gen/android/app/proguard-rules.pro b/src-tauri/gen/android/app/proguard-rules.pro new file mode 100644 index 0000000000..b63990750b --- /dev/null +++ b/src-tauri/gen/android/app/proguard-rules.pro @@ -0,0 +1,56 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile + +# Keep Android bridge symbols used by Wry/Tauri JNI on release builds. +# Without these, R8 can rename/remove methods like WryActivity.getId(), +# which tao resolves by method name via JNI. +-keep class moe.sable.client.* { + native ; +} + +-keep class moe.sable.client.WryActivity { + public (...); + + void setWebView(moe.sable.client.RustWebView); + java.lang.Class getAppClass(...); + java.lang.String getVersion(); + int startActivity(...); + int getId(); +} + +-keep class moe.sable.client.Ipc { + public (...); + + @android.webkit.JavascriptInterface public ; +} + +-keep class moe.sable.client.RustWebView { + public (...); + + void loadUrlMainThread(...); + void loadHTMLMainThread(...); + void evalScript(...); +} + +-keep class moe.sable.client.RustWebChromeClient,moe.sable.client.RustWebViewClient { + public (...); +} \ No newline at end of file diff --git a/src-tauri/gen/android/app/src/main/AndroidManifest.xml b/src-tauri/gen/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..2e50b687b1 --- /dev/null +++ b/src-tauri/gen/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src-tauri/gen/android/app/src/main/java/moe/sable/client/MainActivity.kt b/src-tauri/gen/android/app/src/main/java/moe/sable/client/MainActivity.kt new file mode 100644 index 0000000000..016cf63168 --- /dev/null +++ b/src-tauri/gen/android/app/src/main/java/moe/sable/client/MainActivity.kt @@ -0,0 +1,11 @@ +package moe.sable.client + +import android.os.Bundle +import androidx.activity.enableEdgeToEdge + +class MainActivity : TauriActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + enableEdgeToEdge() + super.onCreate(savedInstanceState) + } +} diff --git a/src-tauri/gen/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/src-tauri/gen/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000000..cc14f035a6 --- /dev/null +++ b/src-tauri/gen/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src-tauri/gen/android/app/src/main/res/drawable/ic_launcher_background.xml b/src-tauri/gen/android/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000000..a4f78de59d --- /dev/null +++ b/src-tauri/gen/android/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src-tauri/gen/android/app/src/main/res/drawable/ic_notification.xml b/src-tauri/gen/android/app/src/main/res/drawable/ic_notification.xml new file mode 120000 index 0000000000..94a4ffc173 --- /dev/null +++ b/src-tauri/gen/android/app/src/main/res/drawable/ic_notification.xml @@ -0,0 +1 @@ +../../../../../../../icons/android/drawable/ic_notification.xml \ No newline at end of file diff --git a/src-tauri/gen/android/app/src/main/res/drawable/notification_icon.xml b/src-tauri/gen/android/app/src/main/res/drawable/notification_icon.xml new file mode 120000 index 0000000000..f363fcecdc --- /dev/null +++ b/src-tauri/gen/android/app/src/main/res/drawable/notification_icon.xml @@ -0,0 +1 @@ +../../../../../../../icons/android/drawable/notification_icon.xml \ No newline at end of file diff --git a/src-tauri/gen/android/app/src/main/res/layout/activity_main.xml b/src-tauri/gen/android/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000000..4fc244418b --- /dev/null +++ b/src-tauri/gen/android/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,18 @@ + + + + + + \ No newline at end of file diff --git a/src-tauri/gen/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/src-tauri/gen/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 120000 index 0000000000..eb916ab9de --- /dev/null +++ b/src-tauri/gen/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1 @@ +../../../../../../../icons/android/mipmap-anydpi-v26/ic_launcher.xml \ No newline at end of file diff --git a/src-tauri/gen/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/src-tauri/gen/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 120000 index 0000000000..e4011dc876 --- /dev/null +++ b/src-tauri/gen/android/app/src/main/res/mipmap-hdpi/ic_launcher.png @@ -0,0 +1 @@ +../../../../../../../icons/android/mipmap-hdpi/ic_launcher.png \ No newline at end of file diff --git a/src-tauri/gen/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/src-tauri/gen/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png new file mode 120000 index 0000000000..2bb464f07c --- /dev/null +++ b/src-tauri/gen/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png @@ -0,0 +1 @@ +../../../../../../../icons/android/mipmap-hdpi/ic_launcher_foreground.png \ No newline at end of file diff --git a/src-tauri/gen/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/src-tauri/gen/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 120000 index 0000000000..0e1d1415ef --- /dev/null +++ b/src-tauri/gen/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png @@ -0,0 +1 @@ +../../../../../../../icons/android/mipmap-hdpi/ic_launcher_round.png \ No newline at end of file diff --git a/src-tauri/gen/android/app/src/main/res/mipmap-hdpi/ic_notification.png b/src-tauri/gen/android/app/src/main/res/mipmap-hdpi/ic_notification.png new file mode 120000 index 0000000000..38bf128b24 --- /dev/null +++ b/src-tauri/gen/android/app/src/main/res/mipmap-hdpi/ic_notification.png @@ -0,0 +1 @@ +../../../../../../../icons/android/mipmap-hdpi/ic_notification.png \ No newline at end of file diff --git a/src-tauri/gen/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/src-tauri/gen/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 120000 index 0000000000..c5c8af7d59 --- /dev/null +++ b/src-tauri/gen/android/app/src/main/res/mipmap-mdpi/ic_launcher.png @@ -0,0 +1 @@ +../../../../../../../icons/android/mipmap-mdpi/ic_launcher.png \ No newline at end of file diff --git a/src-tauri/gen/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/src-tauri/gen/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png new file mode 120000 index 0000000000..1dc8bd6601 --- /dev/null +++ b/src-tauri/gen/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png @@ -0,0 +1 @@ +../../../../../../../icons/android/mipmap-mdpi/ic_launcher_foreground.png \ No newline at end of file diff --git a/src-tauri/gen/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/src-tauri/gen/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 120000 index 0000000000..de6c001a7e --- /dev/null +++ b/src-tauri/gen/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png @@ -0,0 +1 @@ +../../../../../../../icons/android/mipmap-mdpi/ic_launcher_round.png \ No newline at end of file diff --git a/src-tauri/gen/android/app/src/main/res/mipmap-mdpi/ic_notification.png b/src-tauri/gen/android/app/src/main/res/mipmap-mdpi/ic_notification.png new file mode 120000 index 0000000000..04dcb612b3 --- /dev/null +++ b/src-tauri/gen/android/app/src/main/res/mipmap-mdpi/ic_notification.png @@ -0,0 +1 @@ +../../../../../../../icons/android/mipmap-mdpi/ic_notification.png \ No newline at end of file diff --git a/src-tauri/gen/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/src-tauri/gen/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 120000 index 0000000000..c244d43ac8 --- /dev/null +++ b/src-tauri/gen/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png @@ -0,0 +1 @@ +../../../../../../../icons/android/mipmap-xhdpi/ic_launcher.png \ No newline at end of file diff --git a/src-tauri/gen/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/src-tauri/gen/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png new file mode 120000 index 0000000000..a9ba70dfaa --- /dev/null +++ b/src-tauri/gen/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png @@ -0,0 +1 @@ +../../../../../../../icons/android/mipmap-xhdpi/ic_launcher_foreground.png \ No newline at end of file diff --git a/src-tauri/gen/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/src-tauri/gen/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 120000 index 0000000000..9756cb1f2e --- /dev/null +++ b/src-tauri/gen/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png @@ -0,0 +1 @@ +../../../../../../../icons/android/mipmap-xhdpi/ic_launcher_round.png \ No newline at end of file diff --git a/src-tauri/gen/android/app/src/main/res/mipmap-xhdpi/ic_notification.png b/src-tauri/gen/android/app/src/main/res/mipmap-xhdpi/ic_notification.png new file mode 120000 index 0000000000..d4b91921d9 --- /dev/null +++ b/src-tauri/gen/android/app/src/main/res/mipmap-xhdpi/ic_notification.png @@ -0,0 +1 @@ +../../../../../../../icons/android/mipmap-xhdpi/ic_notification.png \ No newline at end of file diff --git a/src-tauri/gen/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/src-tauri/gen/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 120000 index 0000000000..1832021dc3 --- /dev/null +++ b/src-tauri/gen/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png @@ -0,0 +1 @@ +../../../../../../../icons/android/mipmap-xxhdpi/ic_launcher.png \ No newline at end of file diff --git a/src-tauri/gen/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/src-tauri/gen/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png new file mode 120000 index 0000000000..20afdb7e59 --- /dev/null +++ b/src-tauri/gen/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png @@ -0,0 +1 @@ +../../../../../../../icons/android/mipmap-xxhdpi/ic_launcher_foreground.png \ No newline at end of file diff --git a/src-tauri/gen/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/src-tauri/gen/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 120000 index 0000000000..f209887ca9 --- /dev/null +++ b/src-tauri/gen/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png @@ -0,0 +1 @@ +../../../../../../../icons/android/mipmap-xxhdpi/ic_launcher_round.png \ No newline at end of file diff --git a/src-tauri/gen/android/app/src/main/res/mipmap-xxhdpi/ic_notification.png b/src-tauri/gen/android/app/src/main/res/mipmap-xxhdpi/ic_notification.png new file mode 120000 index 0000000000..dea0cf0353 --- /dev/null +++ b/src-tauri/gen/android/app/src/main/res/mipmap-xxhdpi/ic_notification.png @@ -0,0 +1 @@ +../../../../../../../icons/android/mipmap-xxhdpi/ic_notification.png \ No newline at end of file diff --git a/src-tauri/gen/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/src-tauri/gen/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 120000 index 0000000000..2c20016b0d --- /dev/null +++ b/src-tauri/gen/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png @@ -0,0 +1 @@ +../../../../../../../icons/android/mipmap-xxxhdpi/ic_launcher.png \ No newline at end of file diff --git a/src-tauri/gen/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/src-tauri/gen/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png new file mode 120000 index 0000000000..18a184d9f9 --- /dev/null +++ b/src-tauri/gen/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png @@ -0,0 +1 @@ +../../../../../../../icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png \ No newline at end of file diff --git a/src-tauri/gen/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/src-tauri/gen/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 120000 index 0000000000..c7eff28348 --- /dev/null +++ b/src-tauri/gen/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png @@ -0,0 +1 @@ +../../../../../../../icons/android/mipmap-xxxhdpi/ic_launcher_round.png \ No newline at end of file diff --git a/src-tauri/gen/android/app/src/main/res/mipmap-xxxhdpi/ic_notification.png b/src-tauri/gen/android/app/src/main/res/mipmap-xxxhdpi/ic_notification.png new file mode 120000 index 0000000000..78d475ccc7 --- /dev/null +++ b/src-tauri/gen/android/app/src/main/res/mipmap-xxxhdpi/ic_notification.png @@ -0,0 +1 @@ +../../../../../../../icons/android/mipmap-xxxhdpi/ic_notification.png \ No newline at end of file diff --git a/src-tauri/gen/android/app/src/main/res/values-night/themes.xml b/src-tauri/gen/android/app/src/main/res/values-night/themes.xml new file mode 100644 index 0000000000..fbc12a6cbb --- /dev/null +++ b/src-tauri/gen/android/app/src/main/res/values-night/themes.xml @@ -0,0 +1,6 @@ + + + + diff --git a/src-tauri/gen/android/app/src/main/res/values/colors.xml b/src-tauri/gen/android/app/src/main/res/values/colors.xml new file mode 100644 index 0000000000..f8c6127d32 --- /dev/null +++ b/src-tauri/gen/android/app/src/main/res/values/colors.xml @@ -0,0 +1,10 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + \ No newline at end of file diff --git a/src-tauri/gen/android/app/src/main/res/values/ic_launcher_background.xml b/src-tauri/gen/android/app/src/main/res/values/ic_launcher_background.xml new file mode 120000 index 0000000000..3aa4ae6f10 --- /dev/null +++ b/src-tauri/gen/android/app/src/main/res/values/ic_launcher_background.xml @@ -0,0 +1 @@ +../../../../../../../icons/android/values/ic_launcher_background.xml \ No newline at end of file diff --git a/src-tauri/gen/android/app/src/main/res/values/strings.xml b/src-tauri/gen/android/app/src/main/res/values/strings.xml new file mode 100644 index 0000000000..8e9c7417a3 --- /dev/null +++ b/src-tauri/gen/android/app/src/main/res/values/strings.xml @@ -0,0 +1,4 @@ + + sable + sable + \ No newline at end of file diff --git a/src-tauri/gen/android/app/src/main/res/values/themes.xml b/src-tauri/gen/android/app/src/main/res/values/themes.xml new file mode 100644 index 0000000000..fbc12a6cbb --- /dev/null +++ b/src-tauri/gen/android/app/src/main/res/values/themes.xml @@ -0,0 +1,6 @@ + + + + diff --git a/src-tauri/gen/android/app/src/main/res/xml/file_paths.xml b/src-tauri/gen/android/app/src/main/res/xml/file_paths.xml new file mode 100644 index 0000000000..782d63b993 --- /dev/null +++ b/src-tauri/gen/android/app/src/main/res/xml/file_paths.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/src-tauri/gen/android/build.gradle.kts b/src-tauri/gen/android/build.gradle.kts new file mode 100644 index 0000000000..3373810c7b --- /dev/null +++ b/src-tauri/gen/android/build.gradle.kts @@ -0,0 +1,22 @@ +buildscript { + repositories { + google() + mavenCentral() + } + dependencies { + classpath("com.android.tools.build:gradle:8.11.0") + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:2.2.0") + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +tasks.register("clean").configure { + delete("build") +} + diff --git a/src-tauri/gen/android/buildSrc/build.gradle.kts b/src-tauri/gen/android/buildSrc/build.gradle.kts new file mode 100644 index 0000000000..5c55bba71c --- /dev/null +++ b/src-tauri/gen/android/buildSrc/build.gradle.kts @@ -0,0 +1,23 @@ +plugins { + `kotlin-dsl` +} + +gradlePlugin { + plugins { + create("pluginsForCoolKids") { + id = "rust" + implementationClass = "RustPlugin" + } + } +} + +repositories { + google() + mavenCentral() +} + +dependencies { + compileOnly(gradleApi()) + implementation("com.android.tools.build:gradle:8.11.0") +} + diff --git a/src-tauri/gen/android/buildSrc/src/main/java/moe/sable/client/kotlin/BuildTask.kt b/src-tauri/gen/android/buildSrc/src/main/java/moe/sable/client/kotlin/BuildTask.kt new file mode 100644 index 0000000000..f764e2ad9b --- /dev/null +++ b/src-tauri/gen/android/buildSrc/src/main/java/moe/sable/client/kotlin/BuildTask.kt @@ -0,0 +1,68 @@ +import java.io.File +import org.apache.tools.ant.taskdefs.condition.Os +import org.gradle.api.DefaultTask +import org.gradle.api.GradleException +import org.gradle.api.logging.LogLevel +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.TaskAction + +open class BuildTask : DefaultTask() { + @Input + var rootDirRel: String? = null + @Input + var target: String? = null + @Input + var release: Boolean? = null + + @TaskAction + fun assemble() { + val executable = """pnpm"""; + try { + runTauriCli(executable) + } catch (e: Exception) { + if (Os.isFamily(Os.FAMILY_WINDOWS)) { + // Try different Windows-specific extensions + val fallbacks = listOf( + "$executable.exe", + "$executable.cmd", + "$executable.bat", + ) + + var lastException: Exception = e + for (fallback in fallbacks) { + try { + runTauriCli(fallback) + return + } catch (fallbackException: Exception) { + lastException = fallbackException + } + } + throw lastException + } else { + throw e; + } + } + } + + fun runTauriCli(executable: String) { + val rootDirRel = rootDirRel ?: throw GradleException("rootDirRel cannot be null") + val target = target ?: throw GradleException("target cannot be null") + val release = release ?: throw GradleException("release cannot be null") + val args = listOf("tauri", "android", "android-studio-script"); + + project.exec { + workingDir(File(project.projectDir, rootDirRel)) + executable(executable) + args(args) + if (project.logger.isEnabled(LogLevel.DEBUG)) { + args("-vv") + } else if (project.logger.isEnabled(LogLevel.INFO)) { + args("-v") + } + if (release) { + args("--release") + } + args(listOf("--target", target)) + }.assertNormalExitValue() + } +} \ No newline at end of file diff --git a/src-tauri/gen/android/buildSrc/src/main/java/moe/sable/client/kotlin/RustPlugin.kt b/src-tauri/gen/android/buildSrc/src/main/java/moe/sable/client/kotlin/RustPlugin.kt new file mode 100644 index 0000000000..4aa7fcaf67 --- /dev/null +++ b/src-tauri/gen/android/buildSrc/src/main/java/moe/sable/client/kotlin/RustPlugin.kt @@ -0,0 +1,85 @@ +import com.android.build.api.dsl.ApplicationExtension +import org.gradle.api.DefaultTask +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.kotlin.dsl.configure +import org.gradle.kotlin.dsl.get + +const val TASK_GROUP = "rust" + +open class Config { + lateinit var rootDirRel: String +} + +open class RustPlugin : Plugin { + private lateinit var config: Config + + override fun apply(project: Project) = with(project) { + config = extensions.create("rust", Config::class.java) + + val defaultAbiList = listOf("arm64-v8a", "armeabi-v7a", "x86", "x86_64"); + val abiList = (findProperty("abiList") as? String)?.split(',') ?: defaultAbiList + + val defaultArchList = listOf("arm64", "arm", "x86", "x86_64"); + val archList = (findProperty("archList") as? String)?.split(',') ?: defaultArchList + + val targetsList = (findProperty("targetList") as? String)?.split(',') ?: listOf("aarch64", "armv7", "i686", "x86_64") + + extensions.configure { + @Suppress("UnstableApiUsage") + flavorDimensions.add("abi") + productFlavors { + create("universal") { + dimension = "abi" + ndk { + abiFilters += abiList + } + } + defaultArchList.forEachIndexed { index, arch -> + create(arch) { + dimension = "abi" + ndk { + abiFilters.add(defaultAbiList[index]) + } + } + } + } + } + + afterEvaluate { + for (profile in listOf("debug", "release")) { + val profileCapitalized = profile.replaceFirstChar { it.uppercase() } + val buildTask = tasks.maybeCreate( + "rustBuildUniversal$profileCapitalized", + DefaultTask::class.java + ).apply { + group = TASK_GROUP + description = "Build dynamic library in $profile mode for all targets" + } + + tasks["mergeUniversal${profileCapitalized}JniLibFolders"].dependsOn(buildTask) + + for (targetPair in targetsList.withIndex()) { + val targetName = targetPair.value + val targetArch = archList[targetPair.index] + val targetArchCapitalized = targetArch.replaceFirstChar { it.uppercase() } + val targetBuildTask = project.tasks.maybeCreate( + "rustBuild$targetArchCapitalized$profileCapitalized", + BuildTask::class.java + ).apply { + group = TASK_GROUP + description = "Build dynamic library in $profile mode for $targetArch" + rootDirRel = config.rootDirRel + target = targetName + release = profile == "release" + } + + buildTask.dependsOn(targetBuildTask) + tasks["merge$targetArchCapitalized${profileCapitalized}JniLibFolders"].dependsOn( + targetBuildTask + ) + } + } + } + } +} \ No newline at end of file diff --git a/src-tauri/gen/android/gradle.properties b/src-tauri/gen/android/gradle.properties new file mode 100644 index 0000000000..2a7ec6959d --- /dev/null +++ b/src-tauri/gen/android/gradle.properties @@ -0,0 +1,24 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app"s APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true +# Kotlin code style for this project: "official" or "obsolete": +kotlin.code.style=official +# Enables namespacing of each library's R class so that its R class includes only the +# resources declared in the library itself and none from the library's dependencies, +# thereby reducing the size of the R class for that library +android.nonTransitiveRClass=true +android.nonFinalResIds=false \ No newline at end of file diff --git a/src-tauri/gen/android/gradle/wrapper/gradle-wrapper.jar b/src-tauri/gen/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000..e708b1c023 Binary files /dev/null and b/src-tauri/gen/android/gradle/wrapper/gradle-wrapper.jar differ diff --git a/src-tauri/gen/android/gradle/wrapper/gradle-wrapper.properties b/src-tauri/gen/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000..c5f9a53c27 --- /dev/null +++ b/src-tauri/gen/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Tue May 10 19:22:52 CST 2022 +distributionBase=GRADLE_USER_HOME +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip +distributionPath=wrapper/dists +zipStorePath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME diff --git a/src-tauri/gen/android/gradlew b/src-tauri/gen/android/gradlew new file mode 100755 index 0000000000..4f906e0c81 --- /dev/null +++ b/src-tauri/gen/android/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/src-tauri/gen/android/gradlew.bat b/src-tauri/gen/android/gradlew.bat new file mode 100644 index 0000000000..107acd32c4 --- /dev/null +++ b/src-tauri/gen/android/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/src-tauri/gen/android/settings.gradle b/src-tauri/gen/android/settings.gradle new file mode 100644 index 0000000000..39391166fb --- /dev/null +++ b/src-tauri/gen/android/settings.gradle @@ -0,0 +1,3 @@ +include ':app' + +apply from: 'tauri.settings.gradle' diff --git a/src-tauri/icons/128x128.png b/src-tauri/icons/128x128.png new file mode 100644 index 0000000000..474e29a1e8 Binary files /dev/null and b/src-tauri/icons/128x128.png differ diff --git a/src-tauri/icons/128x128@2x.png b/src-tauri/icons/128x128@2x.png new file mode 100644 index 0000000000..3144762669 Binary files /dev/null and b/src-tauri/icons/128x128@2x.png differ diff --git a/src-tauri/icons/32x32.png b/src-tauri/icons/32x32.png new file mode 100644 index 0000000000..f4a91f8360 Binary files /dev/null and b/src-tauri/icons/32x32.png differ diff --git a/src-tauri/icons/64x64.png b/src-tauri/icons/64x64.png new file mode 100644 index 0000000000..abdfaf20db Binary files /dev/null and b/src-tauri/icons/64x64.png differ diff --git a/src-tauri/icons/Square107x107Logo.png b/src-tauri/icons/Square107x107Logo.png new file mode 100644 index 0000000000..2861b265c2 Binary files /dev/null and b/src-tauri/icons/Square107x107Logo.png differ diff --git a/src-tauri/icons/Square142x142Logo.png b/src-tauri/icons/Square142x142Logo.png new file mode 100644 index 0000000000..c8cbf42169 Binary files /dev/null and b/src-tauri/icons/Square142x142Logo.png differ diff --git a/src-tauri/icons/Square150x150Logo.png b/src-tauri/icons/Square150x150Logo.png new file mode 100644 index 0000000000..0f66d8bd19 Binary files /dev/null and b/src-tauri/icons/Square150x150Logo.png differ diff --git a/src-tauri/icons/Square284x284Logo.png b/src-tauri/icons/Square284x284Logo.png new file mode 100644 index 0000000000..7ba98ce9a5 Binary files /dev/null and b/src-tauri/icons/Square284x284Logo.png differ diff --git a/src-tauri/icons/Square30x30Logo.png b/src-tauri/icons/Square30x30Logo.png new file mode 100644 index 0000000000..07b3f5f9dd Binary files /dev/null and b/src-tauri/icons/Square30x30Logo.png differ diff --git a/src-tauri/icons/Square310x310Logo.png b/src-tauri/icons/Square310x310Logo.png new file mode 100644 index 0000000000..c3eed74b50 Binary files /dev/null and b/src-tauri/icons/Square310x310Logo.png differ diff --git a/src-tauri/icons/Square44x44Logo.png b/src-tauri/icons/Square44x44Logo.png new file mode 100644 index 0000000000..039dfd2af0 Binary files /dev/null and b/src-tauri/icons/Square44x44Logo.png differ diff --git a/src-tauri/icons/Square71x71Logo.png b/src-tauri/icons/Square71x71Logo.png new file mode 100644 index 0000000000..f16a689dfa Binary files /dev/null and b/src-tauri/icons/Square71x71Logo.png differ diff --git a/src-tauri/icons/Square89x89Logo.png b/src-tauri/icons/Square89x89Logo.png new file mode 100644 index 0000000000..e0163af577 Binary files /dev/null and b/src-tauri/icons/Square89x89Logo.png differ diff --git a/src-tauri/icons/StoreLogo.png b/src-tauri/icons/StoreLogo.png new file mode 100644 index 0000000000..9ae41f1273 Binary files /dev/null and b/src-tauri/icons/StoreLogo.png differ diff --git a/src-tauri/icons/android/drawable/ic_notification.xml b/src-tauri/icons/android/drawable/ic_notification.xml new file mode 100644 index 0000000000..696a13d0cb --- /dev/null +++ b/src-tauri/icons/android/drawable/ic_notification.xml @@ -0,0 +1,7 @@ + + + + diff --git a/src-tauri/icons/android/drawable/notification_icon.xml b/src-tauri/icons/android/drawable/notification_icon.xml new file mode 100644 index 0000000000..afa6af3542 --- /dev/null +++ b/src-tauri/icons/android/drawable/notification_icon.xml @@ -0,0 +1,7 @@ + + + + diff --git a/src-tauri/icons/android/mipmap-anydpi-v26/ic_launcher.xml b/src-tauri/icons/android/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000000..2ffbf24b68 --- /dev/null +++ b/src-tauri/icons/android/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png b/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000..c398901836 Binary files /dev/null and b/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png differ diff --git a/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png b/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000..dc2a6d3a8b Binary files /dev/null and b/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png differ diff --git a/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png b/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000000..4564946982 Binary files /dev/null and b/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png differ diff --git a/src-tauri/icons/android/mipmap-hdpi/ic_notification.png b/src-tauri/icons/android/mipmap-hdpi/ic_notification.png new file mode 100644 index 0000000000..e2d71f7d2a Binary files /dev/null and b/src-tauri/icons/android/mipmap-hdpi/ic_notification.png differ diff --git a/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png b/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000000..12d2fc6d4a Binary files /dev/null and b/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png differ diff --git a/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png b/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000..c5348bee7e Binary files /dev/null and b/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png differ diff --git a/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png b/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 0000000000..e1e69bc260 Binary files /dev/null and b/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png differ diff --git a/src-tauri/icons/android/mipmap-mdpi/ic_notification.png b/src-tauri/icons/android/mipmap-mdpi/ic_notification.png new file mode 100644 index 0000000000..2550840251 Binary files /dev/null and b/src-tauri/icons/android/mipmap-mdpi/ic_notification.png differ diff --git a/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png b/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000000..eb83edbf09 Binary files /dev/null and b/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png differ diff --git a/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png b/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000..51ec437c7f Binary files /dev/null and b/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png differ diff --git a/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png b/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000000..0abd89e7c1 Binary files /dev/null and b/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/src-tauri/icons/android/mipmap-xhdpi/ic_notification.png b/src-tauri/icons/android/mipmap-xhdpi/ic_notification.png new file mode 100644 index 0000000000..bea0f2d250 Binary files /dev/null and b/src-tauri/icons/android/mipmap-xhdpi/ic_notification.png differ diff --git a/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png b/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000..54b6f0e3f3 Binary files /dev/null and b/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png b/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000..61c130179c Binary files /dev/null and b/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png differ diff --git a/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png b/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000000..4df39fe7f4 Binary files /dev/null and b/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/src-tauri/icons/android/mipmap-xxhdpi/ic_notification.png b/src-tauri/icons/android/mipmap-xxhdpi/ic_notification.png new file mode 100644 index 0000000000..8a3e2a860c Binary files /dev/null and b/src-tauri/icons/android/mipmap-xxhdpi/ic_notification.png differ diff --git a/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png b/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000..71533fb0d1 Binary files /dev/null and b/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png b/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000..46b30f412a Binary files /dev/null and b/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png differ diff --git a/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png b/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000000..204c18c53a Binary files /dev/null and b/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/src-tauri/icons/android/mipmap-xxxhdpi/ic_notification.png b/src-tauri/icons/android/mipmap-xxxhdpi/ic_notification.png new file mode 100644 index 0000000000..334958c5d9 Binary files /dev/null and b/src-tauri/icons/android/mipmap-xxxhdpi/ic_notification.png differ diff --git a/src-tauri/icons/android/values/ic_launcher_background.xml b/src-tauri/icons/android/values/ic_launcher_background.xml new file mode 100644 index 0000000000..ea9c223a6c --- /dev/null +++ b/src-tauri/icons/android/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #fff + \ No newline at end of file diff --git a/src-tauri/icons/icon.icns b/src-tauri/icons/icon.icns new file mode 100644 index 0000000000..e15e8b580c Binary files /dev/null and b/src-tauri/icons/icon.icns differ diff --git a/src-tauri/icons/icon.ico b/src-tauri/icons/icon.ico new file mode 100644 index 0000000000..fabc3e5529 Binary files /dev/null and b/src-tauri/icons/icon.ico differ diff --git a/src-tauri/icons/icon.png b/src-tauri/icons/icon.png new file mode 100644 index 0000000000..8103a9145c Binary files /dev/null and b/src-tauri/icons/icon.png differ diff --git a/src-tauri/icons/ios/AppIcon-20x20@1x.png b/src-tauri/icons/ios/AppIcon-20x20@1x.png new file mode 100644 index 0000000000..465b5e67e0 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-20x20@1x.png differ diff --git a/src-tauri/icons/ios/AppIcon-20x20@2x-1.png b/src-tauri/icons/ios/AppIcon-20x20@2x-1.png new file mode 100644 index 0000000000..46648af089 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-20x20@2x-1.png differ diff --git a/src-tauri/icons/ios/AppIcon-20x20@2x.png b/src-tauri/icons/ios/AppIcon-20x20@2x.png new file mode 100644 index 0000000000..46648af089 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-20x20@2x.png differ diff --git a/src-tauri/icons/ios/AppIcon-20x20@3x.png b/src-tauri/icons/ios/AppIcon-20x20@3x.png new file mode 100644 index 0000000000..d517fc389e Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-20x20@3x.png differ diff --git a/src-tauri/icons/ios/AppIcon-29x29@1x.png b/src-tauri/icons/ios/AppIcon-29x29@1x.png new file mode 100644 index 0000000000..58983c65a2 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-29x29@1x.png differ diff --git a/src-tauri/icons/ios/AppIcon-29x29@2x-1.png b/src-tauri/icons/ios/AppIcon-29x29@2x-1.png new file mode 100644 index 0000000000..d74dd27303 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-29x29@2x-1.png differ diff --git a/src-tauri/icons/ios/AppIcon-29x29@2x.png b/src-tauri/icons/ios/AppIcon-29x29@2x.png new file mode 100644 index 0000000000..d74dd27303 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-29x29@2x.png differ diff --git a/src-tauri/icons/ios/AppIcon-29x29@3x.png b/src-tauri/icons/ios/AppIcon-29x29@3x.png new file mode 100644 index 0000000000..01a606e328 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-29x29@3x.png differ diff --git a/src-tauri/icons/ios/AppIcon-40x40@1x.png b/src-tauri/icons/ios/AppIcon-40x40@1x.png new file mode 100644 index 0000000000..46648af089 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-40x40@1x.png differ diff --git a/src-tauri/icons/ios/AppIcon-40x40@2x-1.png b/src-tauri/icons/ios/AppIcon-40x40@2x-1.png new file mode 100644 index 0000000000..089502002a Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-40x40@2x-1.png differ diff --git a/src-tauri/icons/ios/AppIcon-40x40@2x.png b/src-tauri/icons/ios/AppIcon-40x40@2x.png new file mode 100644 index 0000000000..089502002a Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-40x40@2x.png differ diff --git a/src-tauri/icons/ios/AppIcon-40x40@3x.png b/src-tauri/icons/ios/AppIcon-40x40@3x.png new file mode 100644 index 0000000000..d759b0beaf Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-40x40@3x.png differ diff --git a/src-tauri/icons/ios/AppIcon-512@2x.png b/src-tauri/icons/ios/AppIcon-512@2x.png new file mode 100644 index 0000000000..bfce1e7f89 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-512@2x.png differ diff --git a/src-tauri/icons/ios/AppIcon-60x60@2x.png b/src-tauri/icons/ios/AppIcon-60x60@2x.png new file mode 100644 index 0000000000..d759b0beaf Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-60x60@2x.png differ diff --git a/src-tauri/icons/ios/AppIcon-60x60@3x.png b/src-tauri/icons/ios/AppIcon-60x60@3x.png new file mode 100644 index 0000000000..d020c68b07 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-60x60@3x.png differ diff --git a/src-tauri/icons/ios/AppIcon-76x76@1x.png b/src-tauri/icons/ios/AppIcon-76x76@1x.png new file mode 100644 index 0000000000..94f38d735d Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-76x76@1x.png differ diff --git a/src-tauri/icons/ios/AppIcon-76x76@2x.png b/src-tauri/icons/ios/AppIcon-76x76@2x.png new file mode 100644 index 0000000000..f5a7391ff7 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-76x76@2x.png differ diff --git a/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png b/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png new file mode 100644 index 0000000000..42194746b9 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png differ diff --git a/src-tauri/src/desktop/mod.rs b/src-tauri/src/desktop/mod.rs new file mode 100644 index 0000000000..657ecd98e0 --- /dev/null +++ b/src-tauri/src/desktop/mod.rs @@ -0,0 +1,6 @@ +pub mod runtime_state; +pub mod settings; +pub mod tray; + +#[cfg(windows)] +pub mod windows; diff --git a/src-tauri/src/desktop/runtime_state.rs b/src-tauri/src/desktop/runtime_state.rs new file mode 100644 index 0000000000..f81982b826 --- /dev/null +++ b/src-tauri/src/desktop/runtime_state.rs @@ -0,0 +1,38 @@ +use serde::{Deserialize, Serialize}; +use ts_rs::TS; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export, export_to = "desktop/", rename_all = "camelCase")] +pub struct DesktopRuntimeState { + pub tray_available: bool, +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + use ts_rs::{Config, TS}; + + #[test] + fn desktop_runtime_state_serializes_with_camel_case_keys() { + let state = DesktopRuntimeState { + tray_available: true, + }; + + assert_eq!( + serde_json::to_value(state).unwrap(), + json!({ + "trayAvailable": true, + }) + ); + } + + #[test] + fn desktop_runtime_state_exports_to_typescript() { + let output = DesktopRuntimeState::export_to_string(&Config::new()).unwrap(); + + assert!(output.contains("type DesktopRuntimeState")); + assert!(output.contains("trayAvailable: boolean")); + } +} diff --git a/src-tauri/src/desktop/settings.rs b/src-tauri/src/desktop/settings.rs new file mode 100644 index 0000000000..6f9c58e129 --- /dev/null +++ b/src-tauri/src/desktop/settings.rs @@ -0,0 +1,101 @@ +use serde::{Deserialize, Serialize}; +use ts_rs::TS; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export, export_to = "desktop/", rename_all = "camelCase")] +pub struct DesktopSettings { + pub close_to_background_on_close: bool, + pub show_system_tray_icon: bool, +} + +pub(crate) const DESKTOP_SETTINGS_PATH: &str = "desktop-preferences.json"; +pub(crate) const CLOSE_TO_BACKGROUND_ON_CLOSE_KEY: &str = "closeToBackgroundOnClose"; +pub(crate) const SHOW_SYSTEM_TRAY_ICON_KEY: &str = "showSystemTrayIcon"; +pub(crate) const LEGACY_KEEP_BACKGROUND_RUNNING_KEY: &str = "keepBackgroundRunning"; + +pub(crate) fn tray_available_for_session(settings: DesktopSettings, tray_created: bool) -> bool { + settings.show_system_tray_icon && tray_created +} + +pub(crate) fn desktop_settings_from_values( + close_to_background_on_close: Option, + show_system_tray_icon: Option, + keep_background_running: Option, +) -> DesktopSettings { + DesktopSettings { + close_to_background_on_close: close_to_background_on_close.unwrap_or(true) + || keep_background_running.unwrap_or(false), + show_system_tray_icon: show_system_tray_icon.unwrap_or(true), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + use ts_rs::{Config, TS}; + + #[test] + fn desktop_settings_serialize_with_camel_case_keys() { + let settings = DesktopSettings { + close_to_background_on_close: true, + show_system_tray_icon: false, + }; + + assert_eq!( + serde_json::to_value(settings).unwrap(), + json!({ + "closeToBackgroundOnClose": true, + "showSystemTrayIcon": false, + }) + ); + } + + #[test] + fn desktop_settings_deserialize_from_camel_case_keys() { + assert_eq!( + serde_json::from_value::(json!({ + "closeToBackgroundOnClose": true, + "showSystemTrayIcon": false, + })) + .unwrap(), + DesktopSettings { + close_to_background_on_close: true, + show_system_tray_icon: false, + } + ); + } + + #[test] + fn desktop_settings_export_to_typescript_with_camel_case_keys() { + let output = DesktopSettings::export_to_string(&Config::new()).unwrap(); + + assert!(output.contains("type DesktopSettings")); + assert!(output.contains("closeToBackgroundOnClose: boolean")); + assert!(output.contains("showSystemTrayIcon: boolean")); + assert!(!output.contains("keepBackgroundRunning: boolean")); + } + + #[test] + fn legacy_background_setting_keeps_close_behavior_enabled() { + assert_eq!( + desktop_settings_from_values(Some(false), Some(false), Some(true)), + DesktopSettings { + close_to_background_on_close: true, + show_system_tray_icon: false, + } + ); + } + + #[test] + fn explicit_close_setting_stays_disabled_when_legacy_background_is_off() { + assert_eq!( + desktop_settings_from_values(Some(false), Some(true), Some(false)), + DesktopSettings { + close_to_background_on_close: false, + show_system_tray_icon: true, + } + ); + } +} diff --git a/src-tauri/src/desktop/tray.rs b/src-tauri/src/desktop/tray.rs new file mode 100644 index 0000000000..c756f03ca4 --- /dev/null +++ b/src-tauri/src/desktop/tray.rs @@ -0,0 +1,378 @@ +use std::sync::atomic::{AtomicBool, Ordering}; + +use crate::desktop::runtime_state::DesktopRuntimeState; +use crate::desktop::settings::{ + desktop_settings_from_values, tray_available_for_session, DesktopSettings, + CLOSE_TO_BACKGROUND_ON_CLOSE_KEY, DESKTOP_SETTINGS_PATH, LEGACY_KEEP_BACKGROUND_RUNNING_KEY, + SHOW_SYSTEM_TRAY_ICON_KEY, +}; +use serde_json::json; +use tauri::{ + menu::{Menu, MenuEvent, MenuItem}, + tray::TrayIconBuilder, + AppHandle, Manager, RunEvent, +}; +use tauri_plugin_store::StoreExt; + +#[cfg(not(target_os = "linux"))] +use tauri::tray::{MouseButton, TrayIconEvent}; + +const MAIN_TRAY_ID: &str = "main"; +const TRAY_MENU_SHOW_ID: &str = "tray_show"; +const TRAY_MENU_QUIT_ID: &str = "tray_quit"; + +pub struct DesktopSettingsState { + close_to_background_on_close: AtomicBool, + show_system_tray_icon: AtomicBool, + tray_available: AtomicBool, +} + +impl Default for DesktopSettingsState { + fn default() -> Self { + Self { + close_to_background_on_close: AtomicBool::new(true), + show_system_tray_icon: AtomicBool::new(true), + tray_available: AtomicBool::new(false), + } + } +} + +#[cfg(not(target_os = "linux"))] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum TrayDoubleClickAction { + Ignore, + ShowOrCreateMainWindow, + CloseMainWindow, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum ExitRequestAction { + AllowExit, + CloseWindowsToBackground, +} + +#[cfg(not(target_os = "linux"))] +fn tray_double_click_action(window_exists: bool, event: &TrayIconEvent) -> TrayDoubleClickAction { + match event { + TrayIconEvent::DoubleClick { + button: MouseButton::Left, + .. + } => { + if window_exists { + TrayDoubleClickAction::CloseMainWindow + } else { + TrayDoubleClickAction::ShowOrCreateMainWindow + } + } + _ => TrayDoubleClickAction::Ignore, + } +} + +fn exit_request_action(settings: DesktopSettings, code: Option) -> ExitRequestAction { + match (settings.close_to_background_on_close, code) { + (_, Some(_)) => ExitRequestAction::AllowExit, + (true, None) => ExitRequestAction::CloseWindowsToBackground, + (false, None) => ExitRequestAction::AllowExit, + } +} + +fn load_desktop_settings(app: &AppHandle) -> tauri::Result { + let store = app + .store_builder(DESKTOP_SETTINGS_PATH) + .defaults(std::collections::HashMap::from([ + (CLOSE_TO_BACKGROUND_ON_CLOSE_KEY.into(), json!(true)), + (SHOW_SYSTEM_TRAY_ICON_KEY.into(), json!(true)), + ])) + .build() + .map_err(|error| tauri::Error::PluginInitialization("store".into(), error.to_string()))?; + + Ok(desktop_settings_from_values( + store + .get(CLOSE_TO_BACKGROUND_ON_CLOSE_KEY) + .and_then(|value| value.as_bool()), + store + .get(SHOW_SYSTEM_TRAY_ICON_KEY) + .and_then(|value| value.as_bool()), + store + .get(LEGACY_KEEP_BACKGROUND_RUNNING_KEY) + .and_then(|value| value.as_bool()), + )) +} + +fn current_desktop_settings(app: &AppHandle) -> DesktopSettings { + let state = app.state::(); + DesktopSettings { + close_to_background_on_close: state.close_to_background_on_close.load(Ordering::Relaxed), + show_system_tray_icon: state.show_system_tray_icon.load(Ordering::Relaxed), + } +} + +fn desktop_runtime_state(app: &AppHandle) -> DesktopRuntimeState { + DesktopRuntimeState { + tray_available: app + .state::() + .tray_available + .load(Ordering::Relaxed), + } +} + +#[tauri::command] +pub fn get_desktop_runtime_state(app: AppHandle) -> DesktopRuntimeState { + desktop_runtime_state(&app) +} + +#[tauri::command] +pub fn sync_desktop_settings( + app: AppHandle, + settings: DesktopSettings, +) -> Result { + apply_desktop_settings(&app, settings).map_err(|error| error.to_string()) +} + +pub(crate) fn sync_desktop_settings_inner( + app: &AppHandle, +) -> tauri::Result { + let settings = load_desktop_settings(app)?; + apply_desktop_settings(app, settings) +} + +fn apply_desktop_settings( + app: &AppHandle, + settings: DesktopSettings, +) -> tauri::Result { + let state = app.state::(); + + state + .close_to_background_on_close + .store(settings.close_to_background_on_close, Ordering::Relaxed); + state + .show_system_tray_icon + .store(settings.show_system_tray_icon, Ordering::Relaxed); + + if settings.show_system_tray_icon { + if app.tray_by_id(MAIN_TRAY_ID).is_none() { + match create_system_tray(app) { + Ok(()) => state.tray_available.store( + tray_available_for_session(settings, true), + Ordering::Relaxed, + ), + Err(error) => { + log::warn!("Failed to initialize system tray: {error}"); + state.tray_available.store( + tray_available_for_session(settings, false), + Ordering::Relaxed, + ); + } + } + } else { + state.tray_available.store( + tray_available_for_session(settings, true), + Ordering::Relaxed, + ); + } + } else { + let _ = app.remove_tray_by_id(MAIN_TRAY_ID); + state.tray_available.store(false, Ordering::Relaxed); + } + + Ok(desktop_runtime_state(app)) +} + +#[cfg(not(target_os = "linux"))] +fn main_window_exists(app: &AppHandle) -> bool { + app.get_webview_window(crate::MAIN_WINDOW_LABEL).is_some() +} + +#[cfg(not(target_os = "linux"))] +fn close_main_window(app: &AppHandle) { + if let Some(window) = app.get_webview_window(crate::MAIN_WINDOW_LABEL) { + let _ = window.close(); + } +} + +fn close_all_windows(app: &AppHandle) { + for (_label, window) in app.webview_windows() { + let _ = window.close(); + } +} + +fn handle_exit_request( + app: &AppHandle, + code: Option, + api: &tauri::ExitRequestApi, +) { + let settings = current_desktop_settings(app); + if exit_request_action(settings, code) == ExitRequestAction::CloseWindowsToBackground { + api.prevent_exit(); + close_all_windows(app); + } +} + +#[cfg(not(target_os = "linux"))] +fn handle_tray_double_click(app: &AppHandle, event: &TrayIconEvent) { + match tray_double_click_action(main_window_exists(app), event) { + TrayDoubleClickAction::ShowOrCreateMainWindow => { + let _ = crate::show_or_create_main_window(app); + } + TrayDoubleClickAction::CloseMainWindow => { + close_main_window(app); + } + TrayDoubleClickAction::Ignore => {} + } +} + +pub fn handle_run_event(app: &AppHandle, event: RunEvent) { + if let RunEvent::ExitRequested { code, api, .. } = event { + handle_exit_request(app, code, &api); + } +} + +#[cfg(not(target_os = "linux"))] +fn configure_tray_icon_interactions( + builder: TrayIconBuilder, +) -> TrayIconBuilder { + builder + .show_menu_on_left_click(false) + .on_tray_icon_event(|tray, event| { + let app = tray.app_handle(); + handle_tray_double_click(app, &event); + }) +} + +#[cfg(target_os = "linux")] +fn configure_tray_icon_interactions( + builder: TrayIconBuilder, +) -> TrayIconBuilder { + builder +} + +pub fn create_system_tray(app: &AppHandle) -> tauri::Result<()> { + let show_item = MenuItem::with_id(app, TRAY_MENU_SHOW_ID, "Show", true, None::<&str>)?; + let quit_item = MenuItem::with_id(app, TRAY_MENU_QUIT_ID, "Quit", true, None::<&str>)?; + let tray_menu = Menu::with_items(app, &[&show_item, &quit_item])?; + + let mut tray_builder = configure_tray_icon_interactions( + TrayIconBuilder::with_id(MAIN_TRAY_ID) + .menu(&tray_menu) + .on_menu_event(|app, event: MenuEvent| match event.id().as_ref() { + TRAY_MENU_SHOW_ID => { + let _ = crate::show_or_create_main_window(app); + } + TRAY_MENU_QUIT_ID => { + app.exit(0); + } + _ => {} + }), + ); + + if let Some(icon) = app.default_window_icon() { + tray_builder = tray_builder.icon(icon.clone()); + } + + tray_builder.build(app)?; + Ok(()) +} + +#[cfg(test)] +fn tray_icon_events_supported() -> bool { + !cfg!(target_os = "linux") +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::desktop::settings::{ + desktop_settings_from_values, tray_available_for_session, DesktopSettings, + }; + + #[test] + fn close_behavior_keeps_sable_running() { + let settings = DesktopSettings { + close_to_background_on_close: true, + show_system_tray_icon: false, + }; + + assert_eq!( + exit_request_action(settings, None), + ExitRequestAction::CloseWindowsToBackground + ); + } + + #[test] + fn tray_setting_does_not_keep_sable_running_on_its_own() { + let settings = DesktopSettings { + show_system_tray_icon: true, + close_to_background_on_close: false, + }; + + assert_eq!( + exit_request_action(settings, None), + ExitRequestAction::AllowExit + ); + } + + #[test] + fn tray_failure_still_closes_to_background_when_requested() { + let settings = DesktopSettings { + close_to_background_on_close: true, + show_system_tray_icon: true, + }; + + assert_eq!( + exit_request_action(settings, None), + ExitRequestAction::CloseWindowsToBackground + ); + assert!(!tray_available_for_session(settings, false)); + } + + #[test] + fn explicit_quit_bypasses_background_mode() { + let settings = DesktopSettings { + close_to_background_on_close: true, + show_system_tray_icon: true, + }; + + assert_eq!( + exit_request_action(settings, Some(0)), + ExitRequestAction::AllowExit + ); + } + + #[test] + fn missing_store_values_default_to_enabled() { + assert_eq!( + desktop_settings_from_values(None, None, None), + DesktopSettings { + close_to_background_on_close: true, + show_system_tray_icon: true, + } + ); + } + + #[test] + fn legacy_background_store_value_migrates_to_close_behavior() { + assert_eq!( + desktop_settings_from_values(Some(false), Some(false), Some(true)), + DesktopSettings { + close_to_background_on_close: true, + show_system_tray_icon: false, + } + ); + } + + #[test] + fn explicit_store_values_are_preserved_when_legacy_background_is_off() { + assert_eq!( + desktop_settings_from_values(Some(false), Some(false), Some(false)), + DesktopSettings { + show_system_tray_icon: false, + close_to_background_on_close: false, + } + ); + } + + #[test] + fn tray_icon_event_support_matches_platform() { + assert_eq!(tray_icon_events_supported(), !cfg!(target_os = "linux")); + } +} diff --git a/src-tauri/src/desktop/windows/mod.rs b/src-tauri/src/desktop/windows/mod.rs new file mode 100644 index 0000000000..f5ac1c7f6e --- /dev/null +++ b/src-tauri/src/desktop/windows/mod.rs @@ -0,0 +1,5 @@ +#[cfg(windows)] +pub mod snap_overlay; + +#[cfg(windows)] +pub mod window_tracking; diff --git a/src-tauri/src/desktop/windows/snap_overlay.rs b/src-tauri/src/desktop/windows/snap_overlay.rs new file mode 100644 index 0000000000..0dc27aa1fe --- /dev/null +++ b/src-tauri/src/desktop/windows/snap_overlay.rs @@ -0,0 +1,23 @@ +use enigo::{ + Direction::{Click, Press, Release}, + Enigo, Key, Keyboard, Settings, +}; + +#[tauri::command] +pub async fn show_snap_overlay() { + let mut enigo = Enigo::new(&Settings::default()).unwrap(); + + enigo.key(Key::Meta, Press).unwrap(); + enigo.key(Key::Unicode('z'), Click).unwrap(); + enigo.key(Key::Meta, Release).unwrap(); + + std::thread::sleep(std::time::Duration::from_millis(100)); + + enigo.key(Key::Alt, Click).unwrap(); +} + +#[tauri::command] +pub async fn hide_snap_overlay() { + let mut enigo = Enigo::new(&Settings::default()).unwrap(); + enigo.key(Key::Escape, Click).unwrap(); +} diff --git a/src-tauri/src/desktop/windows/window_tracking.rs b/src-tauri/src/desktop/windows/window_tracking.rs new file mode 100644 index 0000000000..fa00afc7cb --- /dev/null +++ b/src-tauri/src/desktop/windows/window_tracking.rs @@ -0,0 +1,294 @@ +use std::{ + ffi::OsString, + os::windows::ffi::OsStringExt, + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, + }, + time::{Duration, Instant}, +}; + +use serde::{Deserialize, Serialize}; +use tauri::{AppHandle, Emitter, State}; +use windows::Win32::{ + Foundation::{CloseHandle, HWND, POINT}, + System::{ + ProcessStatus::GetModuleBaseNameW, + Threading::{OpenProcess, PROCESS_QUERY_INFORMATION, PROCESS_VM_READ}, + }, + UI::WindowsAndMessaging::{ + GetClassNameW, GetCursorPos, GetWindowThreadProcessId, WindowFromPoint, + }, +}; + +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] +pub struct WindowInfo { + pub mouse_x: i32, + pub mouse_y: i32, + pub window_class: Option, + pub exe_name: Option, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] +pub struct WindowTarget { + pub window_class: Option, + pub exe_name: Option, +} + +impl WindowTarget { + pub fn new(window_class: Option, exe_name: Option) -> Self { + Self { + window_class, + exe_name, + } + } + + pub fn matches(&self, window_info: &WindowInfo) -> bool { + let class_matches = match &self.window_class { + Some(target_class) => window_info + .window_class + .as_ref() + .map(|class| class.eq_ignore_ascii_case(target_class)) + .unwrap_or(false), + None => true, + }; + + let exe_matches = match &self.exe_name { + Some(target_exe) => window_info + .exe_name + .as_ref() + .map(|exe| exe.eq_ignore_ascii_case(target_exe)) + .unwrap_or(false), + None => true, + }; + + class_matches && exe_matches + } +} + +impl WindowInfo { + pub fn matches_target(&self, target: &WindowTarget) -> bool { + target.matches(self) + } +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] +pub enum EventType { + Started, + TargetLost, + Timeout, + Stopped, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] +pub struct TrackingEvent { + pub event_type: EventType, + pub window_info: Option, + pub target: WindowTarget, + pub message: String, +} + +pub struct TrackingState { + active: AtomicBool, +} + +impl TrackingState { + pub fn new() -> Self { + Self { + active: AtomicBool::new(false), + } + } + + pub fn is_active(&self) -> bool { + self.active.load(Ordering::Relaxed) + } + + pub fn set_active(&self, active: bool) { + self.active.store(active, Ordering::Relaxed); + } +} + +impl Default for TrackingState { + fn default() -> Self { + Self::new() + } +} + +fn get_window_class_name(hwnd: HWND) -> Result> { + let mut buffer = [0u16; 256]; + let len = unsafe { GetClassNameW(hwnd, &mut buffer) }; + if len == 0 { + return Err("Failed to get class name".into()); + } + Ok(OsString::from_wide(&buffer[..len as usize]) + .to_string_lossy() + .into_owned()) +} + +fn get_exe_name_from_window(hwnd: HWND) -> Result> { + let mut process_id = 0; + unsafe { + GetWindowThreadProcessId(hwnd, Some(&mut process_id)); + } + if process_id == 0 { + return Err("Failed to get process id".into()); + } + + let process_handle = unsafe { + OpenProcess( + PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, + false, + process_id, + ) + }?; + + let mut buffer = [0u16; 260]; + let len = unsafe { GetModuleBaseNameW(process_handle, None, &mut buffer) }; + let _ = unsafe { CloseHandle(process_handle) }; + + if len == 0 { + return Err("Failed to get process name".into()); + } + + Ok(OsString::from_wide(&buffer[..len as usize]) + .to_string_lossy() + .into_owned()) +} + +fn get_window_info() -> Result> { + let mut point = POINT { x: 0, y: 0 }; + unsafe { GetCursorPos(&raw mut point) }?; + + let hwnd = unsafe { WindowFromPoint(point) }; + + let window_class = if !hwnd.0.is_null() { + get_window_class_name(hwnd).ok() + } else { + None + }; + + let exe_name = if !hwnd.0.is_null() { + get_exe_name_from_window(hwnd).ok() + } else { + None + }; + + Ok(WindowInfo { + mouse_x: point.x, + mouse_y: point.y, + window_class, + exe_name, + }) +} + +async fn track_window_hover_with_target( + app_handle: AppHandle, + target: WindowTarget, + tracking_state: Arc, +) { + let start_time = Instant::now(); + let timeout_duration = Duration::from_millis(200); + let check_interval = Duration::from_millis(100); + + let mut was_on_target = false; + + while tracking_state.is_active() { + if start_time.elapsed() >= timeout_duration && !was_on_target { + let event = TrackingEvent { + event_type: EventType::Timeout, + window_info: None, + target: target.clone(), + message: "Tracking timed out before reaching target".to_owned(), + }; + + let _ = app_handle.emit("window-tracking", &event); + tracking_state.set_active(false); + break; + } + + if let Ok(window_info) = get_window_info() { + let is_on_target = window_info.matches_target(&target); + + if was_on_target && !is_on_target { + let event = TrackingEvent { + event_type: EventType::TargetLost, + window_info: Some(window_info), + target: target.clone(), + message: "Pointer moved away from snap popup".to_owned(), + }; + let _ = app_handle.emit("window-tracking", &event); + tracking_state.set_active(false); + break; + } + + was_on_target = is_on_target; + } + + tokio::time::sleep(check_interval).await; + } +} + +#[tauri::command] +pub async fn start_window_tracking_with_target( + app_handle: AppHandle, + target: WindowTarget, + tracking_state: State<'_, Arc>, +) -> Result<(), String> { + if tracking_state.is_active() { + return Ok(()); + } + + tracking_state.set_active(true); + + let event = TrackingEvent { + event_type: EventType::Started, + window_info: None, + target: target.clone(), + message: "Window tracking started".to_owned(), + }; + + app_handle + .emit("window-tracking", &event) + .map_err(|error| format!("Failed to emit start event: {error}"))?; + + let app_handle_clone = app_handle.clone(); + let tracking_state_clone = tracking_state.inner().clone(); + + tauri::async_runtime::spawn(async move { + track_window_hover_with_target(app_handle_clone, target, tracking_state_clone).await; + }); + + Ok(()) +} + +#[tauri::command] +pub async fn stop_window_tracking( + app_handle: AppHandle, + tracking_state: State<'_, Arc>, +) -> Result<(), String> { + if !tracking_state.is_active() { + return Ok(()); + } + + tracking_state.set_active(false); + + let event = TrackingEvent { + event_type: EventType::Stopped, + window_info: None, + target: WindowTarget::new(None, None), + message: "Window tracking stopped".to_owned(), + }; + + app_handle + .emit("window-tracking", &event) + .map_err(|error| format!("Failed to emit stop event: {error}"))?; + + Ok(()) +} + +#[tauri::command] +pub async fn is_window_tracking_active( + tracking_state: State<'_, Arc>, +) -> Result { + Ok(tracking_state.is_active()) +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs new file mode 100644 index 0000000000..4232017eb6 --- /dev/null +++ b/src-tauri/src/lib.rs @@ -0,0 +1,156 @@ +#[cfg(desktop)] +mod desktop; +mod network; + +use tauri::{AppHandle, Manager}; +#[cfg(desktop)] +use tauri_plugin_window_state::StateFlags; + +// Runtime selection: CEF or Wry +#[cfg(feature = "cef")] +type BrowserEngine = tauri::Wry; +#[cfg(all(not(feature = "cef"), feature = "wry"))] +use tauri::Wry as BrowserEngine; + +pub const MAIN_WINDOW_LABEL: &str = "main"; + +pub fn show_or_create_main_window(app: &AppHandle) -> tauri::Result<()> { + if let Some(window) = app.get_webview_window(MAIN_WINDOW_LABEL) { + #[cfg(desktop)] + { + window.unminimize()?; + window.show()?; + window.set_focus()?; + } + + return Ok(()); + } + + log::info!("Main window not found, creating a new one."); + + let builder = + tauri::WebviewWindowBuilder::new(app, MAIN_WINDOW_LABEL, tauri::WebviewUrl::default()); + + #[cfg(desktop)] + let builder = builder + .title(app.package_info().name.clone()) + .resizable(true) + .fullscreen(false) + .inner_size(1280.0, 720.0) + .visible(false); + + #[cfg(target_os = "windows")] + let builder = builder.decorations(false); + + builder.build()?; + Ok(()) +} + +#[cfg_attr(mobile, tauri::mobile_entry_point)] +pub fn run() { + let builder = tauri::Builder::::new(); + + #[cfg(desktop)] + let builder = builder.plugin( + tauri_plugin_window_state::Builder::default() + .with_state_flags(StateFlags::all() & !StateFlags::VISIBLE & !StateFlags::DECORATIONS) + .build(), + ); + + #[cfg(desktop)] + let builder = builder + .plugin(tauri_plugin_store::Builder::default().build()) + .manage(desktop::tray::DesktopSettingsState::default()); + + #[cfg(windows)] + let builder = builder.manage(std::sync::Arc::new( + desktop::windows::window_tracking::TrackingState::new(), + )); + + #[cfg(desktop)] + let builder = builder.plugin(tauri_plugin_single_instance::init(|app, _argv, _cwd| { + let _ = show_or_create_main_window(app); + })); + + #[cfg(mobile)] + let builder = builder.plugin(tauri_plugin_notifications::init()); + + #[cfg(mobile)] + let builder = builder.plugin(tauri_plugin_edge_to_edge::init()); + + let app = builder + .plugin(tauri_plugin_http::init()) + .plugin(tauri_plugin_opener::init()) + .plugin(tauri_plugin_deep_link::init()) + .plugin(tauri_plugin_os::init()) + .setup(|app| { + #[cfg(any(target_os = "linux", all(debug_assertions, windows)))] + { + use tauri_plugin_deep_link::DeepLinkExt; + app.deep_link().register_all()?; + } + + show_or_create_main_window(app.handle())?; + + #[cfg(desktop)] + desktop::tray::sync_desktop_settings_inner(app.handle())?; + + if cfg!(debug_assertions) { + app.handle().plugin( + tauri_plugin_log::Builder::default() + .level(log::LevelFilter::Info) + .build(), + )?; + } + Ok(()) + }) + .invoke_handler(tauri::generate_handler![ + network::loopback_http::abort_loopback_fetch, + network::loopback_http::loopback_fetch, + #[cfg(desktop)] + desktop::tray::get_desktop_runtime_state, + #[cfg(desktop)] + desktop::tray::sync_desktop_settings, + #[cfg(windows)] + desktop::windows::snap_overlay::show_snap_overlay, + #[cfg(windows)] + desktop::windows::snap_overlay::hide_snap_overlay, + #[cfg(windows)] + desktop::windows::window_tracking::start_window_tracking_with_target, + #[cfg(windows)] + desktop::windows::window_tracking::stop_window_tracking, + #[cfg(windows)] + desktop::windows::window_tracking::is_window_tracking_active, + ]) + .build(tauri::generate_context!()); + + let Ok(app) = app else { + // On Android this function is called through JNI. Panicking here causes + // "panic_cannot_unwind" and aborts the whole process without a useful + // message in logcat. + eprintln!("failed to build tauri application: {app:?}"); + return; + }; + + app.run(|app, event| { + #[cfg(desktop)] + desktop::tray::handle_run_event(app, event); + + #[cfg(not(desktop))] + let _ = (app, event); + }); +} + +#[cfg(test)] +mod tests { + #[test] + fn desktop_modules_are_grouped_under_desktop() { + let _ = crate::desktop::settings::DesktopSettings { + close_to_background_on_close: true, + show_system_tray_icon: true, + }; + let _ = crate::desktop::runtime_state::DesktopRuntimeState { + tray_available: true, + }; + } +} diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs new file mode 100644 index 0000000000..d42d2ed28f --- /dev/null +++ b/src-tauri/src/main.rs @@ -0,0 +1,22 @@ +// Prevents additional console window on Windows in release, DO NOT REMOVE!! +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +fn main() { + #[cfg(target_os = "linux")] + unsafe { + // Tao/Tauri Wayland decorations are don't respect server side decorations, forcing GTK onto X11/XWayland for now. + // https://github.com/tauri-apps/tao/issues/1046 + // https://github.com/tauri-apps/tauri/issues/11856 + // https://github.com/tauri-apps/tauri/issues/14251 + std::env::set_var("GDK_BACKEND", "x11"); + + // NVIDIA explicit sync is another upstream WebKitGTK/Wayland failure mode. Prefer this lower-cost workaround over WEBKIT_DISABLE_DMABUF_RENDERER=1, but don't stomp an explicit user override. + // https://github.com/tauri-apps/tauri/issues/10702 + // https://github.com/tauri-apps/tauri/issues/9394 + if std::env::var_os("__NV_DISABLE_EXPLICIT_SYNC").is_none() { + std::env::set_var("__NV_DISABLE_EXPLICIT_SYNC", "1"); + } + } + + app_lib::run(); +} diff --git a/src-tauri/src/network/loopback_http.rs b/src-tauri/src/network/loopback_http.rs new file mode 100644 index 0000000000..a599b2196d --- /dev/null +++ b/src-tauri/src/network/loopback_http.rs @@ -0,0 +1,196 @@ +use std::{ + collections::HashMap, + sync::{LazyLock, Mutex}, +}; + +use serde::{Deserialize, Serialize}; +use tauri_plugin_http::reqwest::{ + header::{HeaderMap, HeaderName, HeaderValue}, + ClientBuilder, Method, Url, +}; +use tokio::sync::watch; + +static LOOPBACK_ABORT_SENDERS: LazyLock>>> = + LazyLock::new(|| Mutex::new(HashMap::new())); + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct LoopbackFetchRequest { + request_id: String, + method: String, + url: String, + headers: Vec<(String, String)>, + body: Option>, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct LoopbackFetchResponse { + status: u16, + status_text: String, + url: String, + headers: Vec<(String, String)>, + body: Vec, +} + +fn validate_loopback_url(url: &str) -> Result { + let parsed = Url::parse(url.trim()).map_err(|err| err.to_string())?; + + if parsed.scheme() != "http" && parsed.scheme() != "https" { + return Err("loopback_fetch only allows loopback http:// or https:// URLs".into()); + } + + match parsed.host_str() { + Some("localhost") | Some("127.0.0.1") | Some("::1") | Some("[::1]") => Ok(parsed), + _ => Err("loopback_fetch only allows loopback http:// or https:// URLs".into()), + } +} + +fn build_headers(headers: Vec<(String, String)>) -> Result { + let mut header_map = HeaderMap::new(); + + for (name, value) in headers { + let header_name = HeaderName::from_bytes(name.as_bytes()).map_err(|err| err.to_string())?; + let header_value = HeaderValue::from_str(&value).map_err(|err| err.to_string())?; + header_map.append(header_name, header_value); + } + + Ok(header_map) +} + +fn register_abort_sender(request_id: &str) -> watch::Receiver { + let (sender, receiver) = watch::channel(false); + LOOPBACK_ABORT_SENDERS + .lock() + .expect("loopback abort senders poisoned") + .insert(request_id.to_owned(), sender); + receiver +} + +fn remove_abort_sender(request_id: &str) { + LOOPBACK_ABORT_SENDERS + .lock() + .expect("loopback abort senders poisoned") + .remove(request_id); +} + +async fn wait_for_abort_signal(receiver: &mut watch::Receiver) { + if *receiver.borrow() { + return; + } + + while receiver.changed().await.is_ok() { + if *receiver.borrow() { + return; + } + } + + std::future::pending::<()>().await; +} + +#[tauri::command] +pub fn abort_loopback_fetch(request_id: String) { + if let Some(sender) = LOOPBACK_ABORT_SENDERS + .lock() + .expect("loopback abort senders poisoned") + .remove(&request_id) + { + let _ = sender.send(true); + } +} + +#[tauri::command] +pub async fn loopback_fetch( + request: LoopbackFetchRequest, +) -> Result { + let request_id = request.request_id.clone(); + let mut abort_receiver = register_abort_sender(&request_id); + let url = validate_loopback_url(&request.url)?; + + let result = async { + let method = Method::from_bytes(request.method.as_bytes()).map_err(|err| err.to_string())?; + let headers = build_headers(request.headers)?; + let mut req = ClientBuilder::new() + .no_proxy() + .build() + .map_err(|err| err.to_string())? + .request(method, url) + .headers(headers); + + if let Some(body) = request.body { + req = req.body(body); + } + + let response = tokio::select! { + response = req.send() => response.map_err(|err| err.to_string())?, + _ = wait_for_abort_signal(&mut abort_receiver) => return Err("Loopback request aborted".into()), + }; + let status = response.status(); + let status_text = status + .canonical_reason() + .map(str::to_owned) + .unwrap_or_else(|| status.as_str().to_owned()); + let url = response.url().to_string(); + let headers = response + .headers() + .iter() + .filter_map(|(name, value)| value.to_str().ok().map(|text| (name.to_string(), text.to_owned()))) + .collect(); + let body = tokio::select! { + body = response.bytes() => body.map_err(|err| err.to_string())?.to_vec(), + _ = wait_for_abort_signal(&mut abort_receiver) => return Err("Loopback request aborted".into()), + }; + + Ok(LoopbackFetchResponse { + status: status.as_u16(), + status_text, + url, + headers, + body, + }) + } + .await; + + remove_abort_sender(&request_id); + result +} + +#[cfg(test)] +mod tests { + use super::{abort_loopback_fetch, register_abort_sender, validate_loopback_url}; + use tokio::time::{timeout, Duration}; + + #[test] + fn allows_loopback_http_hosts() { + assert!(validate_loopback_url("http://localhost:8008").is_ok()); + assert!(validate_loopback_url("https://localhost:8448").is_ok()); + assert!(validate_loopback_url("http://127.0.0.1:8008").is_ok()); + assert!(validate_loopback_url("https://127.0.0.1:8448").is_ok()); + assert!(validate_loopback_url("http://[::1]:8008").is_ok()); + assert!(validate_loopback_url("https://[::1]:8448").is_ok()); + assert!(validate_loopback_url("http://localhost/_matrix/client/versions").is_ok()); + } + + #[test] + fn rejects_non_loopback_or_non_http_urls() { + assert!(validate_loopback_url("http://192.168.1.5:8008").is_err()); + assert!(validate_loopback_url("https://matrix.example.org").is_err()); + assert!(validate_loopback_url("http://localhost.evil.example.org").is_err()); + assert!(validate_loopback_url("http://localhost:8008@evil.example.org").is_err()); + } + + #[test] + fn abort_command_signals_registered_requests() { + tauri::async_runtime::block_on(async { + let mut receiver = register_abort_sender("request-1"); + abort_loopback_fetch("request-1".into()); + + timeout(Duration::from_secs(1), receiver.changed()) + .await + .expect("abort signal timed out") + .expect("abort receiver unexpectedly closed"); + + assert!(*receiver.borrow()); + }); + } +} diff --git a/src-tauri/src/network/mod.rs b/src-tauri/src/network/mod.rs new file mode 100644 index 0000000000..443e0d6f89 --- /dev/null +++ b/src-tauri/src/network/mod.rs @@ -0,0 +1 @@ +pub mod loopback_http; diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json new file mode 100644 index 0000000000..1b6314f727 --- /dev/null +++ b/src-tauri/tauri.conf.json @@ -0,0 +1,56 @@ +{ + "$schema": "../node_modules/@tauri-apps/cli/config.schema.json", + "productName": "sable", + "version": "0.1.0", + "identifier": "moe.sable.client", + "build": { + "frontendDist": "../dist", + "devUrl": "http://localhost:8080", + "beforeDevCommand": "pnpm dev", + "beforeBuildCommand": "pnpm build" + }, + "app": { + "windows": [], + "security": { + "csp": "default-src 'self' blob: data: filesystem: ws: wss: http: https: tauri:; script-src 'self' 'unsafe-eval' 'unsafe-inline' blob: data: filesystem: ws: wss: http: https: tauri:; img-src 'self' data: blob: filesystem: http: https:; connect-src 'self' ipc: ws: wss: http: https: http://ipc.localhost" + } + }, + "bundle": { + "active": true, + "targets": "all", + "linux": { + "deb": { + "depends": ["xwayland"] + }, + "rpm": { + "depends": ["xorg-x11-server-Xwayland"] + } + }, + "icon": [ + "icons/32x32.png", + "icons/128x128.png", + "icons/128x128@2x.png", + "icons/icon.icns", + "icons/icon.ico" + ] + }, + "plugins": { + "typegen": { + "projectPath": ".", + "outputPath": "../src/app/generated/tauri", + "validationLibrary": "none", + "verbose": false + }, + "deep-link": { + "desktop": { + "schemes": ["sable"] + }, + "mobile": [ + { + "scheme": ["sable"], + "appLink": false + } + ] + } + } +} diff --git a/src/app/components/AccountDataEditor.tsx b/src/app/components/AccountDataEditor.tsx index 131ae7fab3..c0b903cd71 100644 --- a/src/app/components/AccountDataEditor.tsx +++ b/src/app/components/AccountDataEditor.tsx @@ -197,8 +197,9 @@ type AccountDataViewProps = { type: string; defaultContent: string; onEdit: () => void; + onDelete?: () => void; }; -function AccountDataView({ type, defaultContent, onEdit }: AccountDataViewProps) { +function AccountDataView({ type, defaultContent, onEdit, onDelete }: AccountDataViewProps) { return ( Edit + {onDelete && ( + + )} JSON Content @@ -246,6 +252,7 @@ export type AccountDataEditorProps = { type?: string; content?: object; submitChange: AccountDataSubmitCallback; + onDelete?: () => void; requestClose: () => void; }; @@ -253,6 +260,7 @@ export function AccountDataEditor({ type, content, submitChange, + onDelete, requestClose, }: AccountDataEditorProps) { const [data, setData] = useState({ @@ -315,6 +323,7 @@ export function AccountDataEditor({ type={data.type} defaultContent={contentJSONStr} onEdit={() => setEdit(true)} + onDelete={onDelete} /> )} diff --git a/src/app/components/AuthFlowsLoader.tsx b/src/app/components/AuthFlowsLoader.tsx index 28478acf57..68eb0e3823 100644 --- a/src/app/components/AuthFlowsLoader.tsx +++ b/src/app/components/AuthFlowsLoader.tsx @@ -7,6 +7,7 @@ import { useAutoDiscoveryInfo } from '$hooks/useAutoDiscoveryInfo'; import { promiseFulfilledResult, promiseRejectedResult } from '$utils/common'; import type { AuthFlows, RegisterFlowsResponse } from '$hooks/useAuthFlows'; import { RegisterFlowStatus, parseRegisterErrResp } from '$hooks/useAuthFlows'; +import { fetch } from '$utils/fetch'; type AuthFlowsLoaderProps = { fallback?: () => ReactNode; @@ -17,7 +18,7 @@ export function AuthFlowsLoader({ fallback, error, children }: AuthFlowsLoaderPr const autoDiscoveryInfo = useAutoDiscoveryInfo(); const baseUrl = autoDiscoveryInfo['m.homeserver'].base_url; - const mx = useMemo(() => createClient({ baseUrl }), [baseUrl]); + const mx = useMemo(() => createClient({ baseUrl, fetchFn: fetch }), [baseUrl]); const [state, load] = useAsyncCallback( useCallback(async () => { diff --git a/src/app/components/AuthenticatedImg.test.tsx b/src/app/components/AuthenticatedImg.test.tsx new file mode 100644 index 0000000000..dfde5206ea --- /dev/null +++ b/src/app/components/AuthenticatedImg.test.tsx @@ -0,0 +1,50 @@ +/* oxlint-disable vitest/require-mock-type-parameters */ +import { render, screen } from '@testing-library/react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { AuthenticatedImg } from './AuthenticatedImg'; + +const renderableMedia = vi.hoisted(() => ({ + useRenderableMediaUrl: vi.fn(), +})); + +vi.mock('$hooks/useRenderableMediaUrl', () => renderableMedia); + +describe('AuthenticatedImg', () => { + beforeEach(() => { + renderableMedia.useRenderableMediaUrl.mockReset(); + }); + + it('renders the image when the hook resolves a safe media url', () => { + renderableMedia.useRenderableMediaUrl.mockReturnValue('https://example.org/image.png'); + + render(); + + expect(renderableMedia.useRenderableMediaUrl).toHaveBeenCalledWith( + 'https://example.org/image.png' + ); + expect(screen.getByAltText('safe media')).toHaveAttribute( + 'src', + 'https://example.org/image.png' + ); + }); + + it('drops unsafe image urls when media resolution does not provide a safe replacement', () => { + renderableMedia.useRenderableMediaUrl.mockReturnValue(undefined); + const unsafeUrl = ['javascript', 'alert(1)'].join(':'); + const { container } = render(); + + expect(screen.queryByAltText('unsafe media')).not.toBeInTheDocument(); + expect(container.querySelector('img')).toBeNull(); + }); + + it('renders the fallback when no safe image url is available', () => { + renderableMedia.useRenderableMediaUrl.mockReturnValue(undefined); + const unsafeUrl = ['javascript', 'alert(1)'].join(':'); + + render( + fallback media} /> + ); + + expect(screen.getByText('fallback media')).toBeInTheDocument(); + }); +}); diff --git a/src/app/components/AuthenticatedImg.tsx b/src/app/components/AuthenticatedImg.tsx new file mode 100644 index 0000000000..fb96795d59 --- /dev/null +++ b/src/app/components/AuthenticatedImg.tsx @@ -0,0 +1,27 @@ +/* eslint-disable jsx-a11y/alt-text */ +import type { ReactEventHandler, ReactNode } from 'react'; +import { useEffect, useState } from 'react'; +import { useRenderableMediaUrl } from '$hooks/useRenderableMediaUrl'; + +type AuthenticatedImgProps = React.ImgHTMLAttributes & { + src?: string; + fallback?: ReactNode; +}; + +export function AuthenticatedImg({ src, fallback, onError, ...props }: AuthenticatedImgProps) { + const [failed, setFailed] = useState(false); + const imageSrc = useRenderableMediaUrl(src); + + useEffect(() => { + setFailed(false); + }, [imageSrc]); + + const handleError: ReactEventHandler = (event) => { + setFailed(true); + onError?.(event); + }; + + if (failed || !imageSrc) return <>{fallback}; + + return ; +} diff --git a/src/app/components/ClientConfigLoader.test.tsx b/src/app/components/ClientConfigLoader.test.tsx index b80e408847..69c2ee4b6a 100644 --- a/src/app/components/ClientConfigLoader.test.tsx +++ b/src/app/components/ClientConfigLoader.test.tsx @@ -20,7 +20,15 @@ afterEach(() => { }); const mockFetch = (config: object) => - vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ json: () => Promise.resolve(config) })); + vi.stubGlobal( + 'fetch', + vi.fn().mockResolvedValue({ + ok: true, + status: 200, + statusText: 'OK', + json: () => Promise.resolve(config), + }) + ); describe('ClientConfigLoader + matrix-to wiring', () => { it('generates a standard matrix.to URL when no custom base is configured', async () => { diff --git a/src/app/components/ClientConfigLoader.tsx b/src/app/components/ClientConfigLoader.tsx index f03b8398e0..cbfc04f86d 100644 --- a/src/app/components/ClientConfigLoader.tsx +++ b/src/app/components/ClientConfigLoader.tsx @@ -1,13 +1,79 @@ import type { ReactNode } from 'react'; import { useCallback, useEffect, useState } from 'react'; +import * as Sentry from '@sentry/react'; import { AsyncStatus, useAsyncCallback } from '$hooks/useAsyncCallback'; import type { ClientConfig } from '$hooks/useClientConfig'; import { trimTrailingSlash } from '$utils/common'; +import { fetch } from '$utils/fetch'; +const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +/** + * Fetch the client config with retry logic and exponential backoff. + * config.json is a static asset served locally, so transient failures are likely + * caused by service worker issues or deploy races. Retrying ensures the app + * doesn't start with incorrect configuration. + */ const getClientConfig = async (): Promise => { const url = `${trimTrailingSlash(import.meta.env.BASE_URL)}/config.json`; - const config = await fetch(url, { method: 'GET' }); - return config.json(); + const maxAttempts = 3; + + // Sequential retries with exponential backoff are intentional — we need to wait + // before retrying, so parallel Promise.all would be incorrect here. + // eslint-disable-next-line no-await-in-loop + for (let attempt = 0; attempt < maxAttempts; attempt++) { + try { + Sentry.addBreadcrumb({ + category: 'config', + message: `Fetching config.json (attempt ${attempt + 1}/${maxAttempts})`, + level: 'info', + }); + + const config = await fetch(url, { method: 'GET' }); + if (!config.ok) { + throw new Error(`HTTP ${config.status}: ${config.statusText}`); + } + + const data = await config.json(); + + Sentry.addBreadcrumb({ + category: 'config', + message: 'config.json loaded successfully', + level: 'info', + data: { attempt: attempt + 1 }, + }); + + return data; + } catch (err) { + const isLastAttempt = attempt === maxAttempts - 1; + const errorMessage = err instanceof Error ? err.message : String(err); + + Sentry.addBreadcrumb({ + category: 'config', + message: `config.json fetch failed (attempt ${attempt + 1}/${maxAttempts})`, + level: isLastAttempt ? 'error' : 'warning', + data: { error: errorMessage }, + }); + + if (isLastAttempt) { + Sentry.captureMessage('Failed to load config.json after all retries', { + level: 'error', + extra: { attempts: maxAttempts, lastError: errorMessage }, + }); + throw new Error( + `Failed to load app configuration after ${maxAttempts} attempts: ${errorMessage}`, + { cause: err } + ); + } + + // Exponential backoff: 500ms, 1000ms, 2000ms + const backoffMs = 500 * Math.pow(2, attempt); + await sleep(backoffMs); + } + } + + // TypeScript exhaustiveness check (unreachable) + throw new Error('Unreachable: config fetch loop exited without return or throw'); }; type ClientConfigLoaderProps = { diff --git a/src/app/components/GlobalKeyboardShortcuts.tsx b/src/app/components/GlobalKeyboardShortcuts.tsx index 7246219a4c..29438d7f47 100644 --- a/src/app/components/GlobalKeyboardShortcuts.tsx +++ b/src/app/components/GlobalKeyboardShortcuts.tsx @@ -2,14 +2,14 @@ * Global keyboard shortcuts for navigation and accessibility. * * Shortcuts provided: - * Alt+N — jump to the highest-priority unread room - * Alt+Shift+Down — cycle forward through unread rooms - * Alt+Shift+Up — cycle backward through unread rooms - * Ctrl+Down / Ctrl+Up: cycle through messages to reply to + * Alt+N — jump to the highest-priority unread room + * Alt+Shift+Down/Up — cycle forward/backward through unread rooms + * Ctrl+Down / Ctrl+Up — cycle through messages to reply to + * Ctrl+Alt+Down/Up — cycle through your own messages to edit */ import { useCallback, useRef } from 'react'; import { useNavigate, useLocation, matchPath } from 'react-router-dom'; -import { useAtomValue, useSetAtom } from 'jotai'; +import { useAtomValue, useSetAtom, atom } from 'jotai'; import { isKeyHotkey } from 'is-hotkey'; import { useMatrixClient } from '$hooks/useMatrixClient'; import { roomToParentsAtom } from '$state/room/roomToParents'; @@ -20,9 +20,17 @@ import { getDirectRoomPath, getHomeRoomPath, getSpaceRoomPath } from '$pages/pat import { HOME_ROOM_PATH, DIRECT_ROOM_PATH, SPACE_ROOM_PATH } from '$pages/paths'; import { getCanonicalAliasOrRoomId } from '$utils/matrix'; import { announce } from '$utils/announce'; -import { roomIdToReplyDraftAtomFamily } from '$state/room/roomInputDrafts'; +import { + roomIdToReplyDraftAtomFamily, + roomIdToEditNavRequestAtomFamily, + type IEditNavRequest, +} from '$state/room/roomInputDrafts'; import type { Room } from '$types/matrix-sdk'; +// Stable fallback atom used when no room is active — prevents atomFamily from +// creating a spurious entry under the empty-string key ''. +const _noopEditNavAtom = atom(undefined); + export function GlobalKeyboardShortcuts() { const navigate = useNavigate(); const location = useLocation(); @@ -52,6 +60,11 @@ export function GlobalKeyboardShortcuts() { const replyDraft = useAtomValue(replyDraftAtomFamily); const setReplyDraft = useSetAtom(replyDraftAtomFamily); + const setEditNavRequest = useSetAtom( + currentRoom?.roomId ? roomIdToEditNavRequestAtomFamily(currentRoom.roomId) : _noopEditNavAtom + ); + const editNavNonceRef = useRef(0); + /** Navigate to a room by ID and announce it to screen readers. */ const navigateToRoom = useCallback( (roomId: string, remaining: number) => { @@ -151,9 +164,24 @@ export function GlobalKeyboardShortcuts() { [currentRoom, replyDraft, setReplyDraft] ); + /** Ctrl+Alt+Down / Ctrl+Alt+Up: cycle through the current user's editable messages. */ + const handleEditKeyDown = useCallback( + (evt: KeyboardEvent) => { + const isDown = isKeyHotkey('mod+alt+down', evt); + const isUp = isKeyHotkey('mod+alt+up', evt); + if (!isDown && !isUp) return; + if (currentRoom === null) return; + evt.preventDefault(); + editNavNonceRef.current += 1; + setEditNavRequest({ dir: isDown ? 'next' : 'prev', nonce: editNavNonceRef.current }); + }, + [currentRoom, setEditNavRequest] + ); + useKeyDown(window, handleNextUnreadKeyDown); useKeyDown(window, handleUnreadNavKeyDown); useKeyDown(window, handleReplyKeyDown); + useKeyDown(window, handleEditKeyDown); return null; } diff --git a/src/app/components/SpecVersionsLoader.tsx b/src/app/components/SpecVersionsLoader.tsx index 0a92aa2a33..0a8da6e938 100644 --- a/src/app/components/SpecVersionsLoader.tsx +++ b/src/app/components/SpecVersionsLoader.tsx @@ -1,6 +1,7 @@ import type { ReactNode } from 'react'; import { useCallback, useEffect, useState } from 'react'; import { AsyncStatus, useAsyncCallback } from '$hooks/useAsyncCallback'; +import { fetch } from '$utils/fetch'; import type { SpecVersions } from '../cs-api'; import { specVersions } from '../cs-api'; diff --git a/src/app/components/SwipeableChatWrapper.tsx b/src/app/components/SwipeableChatWrapper.tsx index d4a5472989..66045316c5 100644 --- a/src/app/components/SwipeableChatWrapper.tsx +++ b/src/app/components/SwipeableChatWrapper.tsx @@ -1,5 +1,5 @@ import type { ReactNode } from 'react'; -import { motion, useMotionValue, useSpring } from 'framer-motion'; +import { motion, useMotionValue, useSpring } from 'motion/react'; import { useDrag } from '@use-gesture/react'; import { useAtomValue } from 'jotai'; import { settingsAtom, RightSwipeAction } from '$state/settings'; @@ -44,8 +44,8 @@ export function SwipeableChatWrapper({ if (active) { x.set(val); } else { - const swipeThreshold = 120; - const velocityThreshold = 0.5; + const swipeThreshold = 180; + const velocityThreshold = 1.2; if (val > swipeThreshold || (vx > velocityThreshold && dx > 0 && val > 0)) { onOpenSidebar?.(); diff --git a/src/app/components/SwipeableMessageWrapper.tsx b/src/app/components/SwipeableMessageWrapper.tsx index 58fc9293e5..656bf11c22 100644 --- a/src/app/components/SwipeableMessageWrapper.tsx +++ b/src/app/components/SwipeableMessageWrapper.tsx @@ -1,4 +1,4 @@ -import { useMotionValue, useSpring, useTransform, motion } from 'framer-motion'; +import { useMotionValue, useSpring, useTransform, motion } from 'motion/react'; import { useDrag } from '@use-gesture/react'; import type { ReactNode } from 'react'; import { useMemo, useState } from 'react'; @@ -18,9 +18,9 @@ function ActiveSwipeWrapper({ children, onReply }: { children: ReactNode; onRepl if (active) { const val = mx < 0 ? mx : 0; x.set(Math.max(-80, val)); - if (mx < -50 !== isReady) setIsReady(mx < -50); + if (mx < -80 !== isReady) setIsReady(mx < -80); } else { - if (mx < -50) onReply(); + if (mx < -80) onReply(); x.set(0); setIsReady(false); } diff --git a/src/app/components/SwipeableOverlayWrapper.tsx b/src/app/components/SwipeableOverlayWrapper.tsx index a77b802f5d..f9420120a0 100644 --- a/src/app/components/SwipeableOverlayWrapper.tsx +++ b/src/app/components/SwipeableOverlayWrapper.tsx @@ -1,5 +1,5 @@ import type { ReactNode } from 'react'; -import { motion, useMotionValue, useSpring } from 'framer-motion'; +import { motion, useMotionValue, useSpring } from 'motion/react'; import { useDrag } from '@use-gesture/react'; import { useAtomValue } from 'jotai'; import { settingsAtom } from '$state/settings'; @@ -40,8 +40,8 @@ export function SwipeableOverlayWrapper({ if (active) { x.set(val); } else { - const swipeThreshold = 100; - const velocityThreshold = 0.5; + const swipeThreshold = 150; + const velocityThreshold = 1.2; const swipedLeft = direction === 'left' && (val < -swipeThreshold || (vx > velocityThreshold && dx < 0)); diff --git a/src/app/components/SyncConnectionStatus.tsx b/src/app/components/SyncConnectionStatus.tsx new file mode 100644 index 0000000000..387b1a7383 --- /dev/null +++ b/src/app/components/SyncConnectionStatus.tsx @@ -0,0 +1,162 @@ +import classNames from 'classnames'; +import { Box, config, Line, Text } from 'folds'; +import { AnimatePresence, motion, useReducedMotion } from 'motion/react'; +import { SyncState } from '$types/matrix-sdk'; +import { type TitlebarStatusView } from '$state/titlebarStatus'; +import { ContainerColor } from '$styles/ContainerColor.css'; + +const TITLEBAR_EASE_OUT: [number, number, number, number] = [0.32, 0.72, 0, 1]; +const TITLEBAR_EASE_OUT_SOFT: [number, number, number, number] = [0.24, 0.72, 0.08, 1]; + +type SyncConnectionStatusProps = { + status: TitlebarStatusView | null; +}; + +export function getSyncConnectionStatusView( + current: SyncState | null, + previous: SyncState | null | undefined +): TitlebarStatusView | null { + if ( + (current === SyncState.Prepared || + current === SyncState.Syncing || + current === SyncState.Catchup) && + previous !== SyncState.Syncing + ) { + return { text: 'Connecting...', variant: 'Success' }; + } + + if (current === SyncState.Reconnecting) { + return { text: 'Connection Lost! Reconnecting...', variant: 'Warning' }; + } + + if (current === SyncState.Error) { + return { text: 'Connection Lost!', variant: 'Critical' }; + } + + return null; +} + +export function SyncConnectionStatusBanner({ status }: SyncConnectionStatusProps) { + if (!status) return null; + + return ( + + + {status.text} + + + + ); +} + +export function SyncConnectionStatusTitlebar({ status }: SyncConnectionStatusProps) { + const shouldReduceMotion = useReducedMotion(); + const pillVariants = shouldReduceMotion + ? { + hidden: { opacity: 0 }, + visible: { + opacity: 1, + transition: { duration: 0.18, ease: TITLEBAR_EASE_OUT }, + }, + exit: { + opacity: 0, + transition: { + when: 'afterChildren' as const, + opacity: { duration: 0.1, delay: 0.08, ease: TITLEBAR_EASE_OUT_SOFT }, + }, + }, + } + : { + hidden: { + y: -2, + scaleX: 0.98, + scaleY: 0.96, + opacity: 0, + clipPath: 'inset(0 50% 0 50% round 999px)', + }, + visible: { + y: 0, + scaleX: 1, + scaleY: 1, + opacity: 1, + clipPath: 'inset(0 0% 0 0% round 999px)', + transition: { duration: 0.2, ease: TITLEBAR_EASE_OUT }, + }, + exit: { + y: -2, + scaleX: 0.98, + scaleY: 0.96, + opacity: 0, + clipPath: 'inset(0 50% 0 50% round 999px)', + transition: { + when: 'afterChildren' as const, + y: { duration: 0.2, ease: TITLEBAR_EASE_OUT }, + scaleX: { duration: 0.2, ease: TITLEBAR_EASE_OUT }, + scaleY: { duration: 0.2, ease: TITLEBAR_EASE_OUT }, + clipPath: { duration: 0.2, ease: TITLEBAR_EASE_OUT }, + opacity: { duration: 0.1, delay: 0.08, ease: TITLEBAR_EASE_OUT_SOFT }, + }, + }, + }; + + const textVariants = shouldReduceMotion + ? { + hidden: { opacity: 0 }, + visible: { + opacity: 1, + transition: { duration: 0.12, ease: TITLEBAR_EASE_OUT }, + }, + exit: { + opacity: 0, + transition: { duration: 0.1, ease: TITLEBAR_EASE_OUT_SOFT }, + }, + } + : { + hidden: { opacity: 0 }, + visible: { + opacity: 1, + transition: { duration: 0.12, delay: 0.04, ease: TITLEBAR_EASE_OUT_SOFT }, + }, + exit: { + opacity: 0, + transition: { duration: 0.1, ease: TITLEBAR_EASE_OUT_SOFT }, + }, + }; + + return ( + + {status && ( + + + {status.text} + + + )} + + ); +} diff --git a/src/app/components/app-shell/AppShell.tsx b/src/app/components/app-shell/AppShell.tsx new file mode 100644 index 0000000000..0a5589502f --- /dev/null +++ b/src/app/components/app-shell/AppShell.tsx @@ -0,0 +1,81 @@ +import { type ReactNode, Suspense, lazy, useState } from 'react'; +import { Provider as JotaiProvider } from 'jotai'; +import { OverlayContainerProvider, PopOutContainerProvider, TooltipContainerProvider } from 'folds'; +import { QueryClientProvider } from '@tanstack/react-query'; +import { isTauri } from '@tauri-apps/api/core'; +import { type as osType } from '@tauri-apps/plugin-os'; + +import { TauriFrontendReady } from '$components/tauri/TauriFrontendReady'; +import { WindowsTitleBar } from '$components/tauri/WindowsTitleBar'; +import type { ScreenSize } from '$hooks/useScreenSize'; +import { ScreenSizeProvider } from '$hooks/useScreenSize'; +import { isReactQueryDevtoolsEnabled } from '$pages/reactQueryDevtoolsGate'; +import { SystemBarShell } from './SystemBarShell'; + +const ReactQueryDevtools = lazy(async () => { + const { ReactQueryDevtools: Devtools } = await import('@tanstack/react-query-devtools'); + + return { default: Devtools }; +}); + +type AppShellProps = { + children: ReactNode; + queryClient: Parameters[0]['client']; + screenSize: ScreenSize; +}; + +export function AppShell({ children, queryClient, screenSize }: AppShellProps) { + const tauriOs = isTauri() ? osType() : undefined; + const useCustomWindowsTitleBar = tauriOs === 'windows'; + const reactQueryDevtoolsEnabled = isReactQueryDevtoolsEnabled(); + const contentHeight = useCustomWindowsTitleBar + ? 'calc(100% - var(--tauri-titlebar-height))' + : '100%'; + const [portalContainer, setPortalContainer] = useState(null); + + return ( + + + + + + + +
+ {useCustomWindowsTitleBar && } +
+ + {children} + +
+
+
+ {reactQueryDevtoolsEnabled && ( + + + + )} +
+
+
+
+
+ ); +} diff --git a/src/app/components/app-shell/SystemBarShell.tsx b/src/app/components/app-shell/SystemBarShell.tsx new file mode 100644 index 0000000000..cf5828fc66 --- /dev/null +++ b/src/app/components/app-shell/SystemBarShell.tsx @@ -0,0 +1,85 @@ +import { type ReactNode } from 'react'; +import { isTauri } from '@tauri-apps/api/core'; +import { type as osType } from '@tauri-apps/plugin-os'; +import { mobileOrTablet } from '$utils/user-agent'; + +const safeAreaTop = 'var(--safe-area-inset-top, env(safe-area-inset-top, 0px))'; +const safeAreaBottom = 'var(--safe-area-inset-bottom, env(safe-area-inset-bottom, 0px))'; +const safeAreaLeft = 'var(--safe-area-inset-left, env(safe-area-inset-left, 0px))'; +const safeAreaRight = 'var(--safe-area-inset-right, env(safe-area-inset-right, 0px))'; + +type SystemBarStripProps = { + position: 'top' | 'bottom'; + size: string; +}; + +function SystemBarStrip({ position, size }: SystemBarStripProps) { + return ( +
+
+
+ ); +} + +type SystemBarShellProps = { + children: ReactNode; + onPortalContainerChange: (node: HTMLDivElement | null) => void; +}; + +export function SystemBarShell({ children, onPortalContainerChange }: SystemBarShellProps) { + const tauriOs = isTauri() ? osType() : undefined; + const isTauriMobile = tauriOs === 'android' || tauriOs === 'ios'; + const needsBottomSystemBar = tauriOs === 'android'; + const isBrowserMobile = !isTauri() && mobileOrTablet(); + const enabled = isTauriMobile || isBrowserMobile; + + return ( + <> + {enabled && } + +
+
+ {children} +
+ +
+
+ + {needsBottomSystemBar && } + + ); +} diff --git a/src/app/components/app-shell/index.ts b/src/app/components/app-shell/index.ts new file mode 100644 index 0000000000..d9728686de --- /dev/null +++ b/src/app/components/app-shell/index.ts @@ -0,0 +1,2 @@ +export { AppShell } from './AppShell'; +export { SystemBarShell } from './SystemBarShell'; diff --git a/src/app/components/editor-tiptap/TiptapEditor.css.ts b/src/app/components/editor-tiptap/TiptapEditor.css.ts new file mode 100644 index 0000000000..bd0a560652 --- /dev/null +++ b/src/app/components/editor-tiptap/TiptapEditor.css.ts @@ -0,0 +1,122 @@ +import { style, globalStyle } from '@vanilla-extract/css'; +import { color, config, DefaultReset, toRem } from 'folds'; + +export const TiptapEditorRoot = style([ + DefaultReset, + { + backgroundColor: color.SurfaceVariant.Container, + color: color.SurfaceVariant.OnContainer, + boxShadow: `inset 0 0 0 ${config.borderWidth.B300} ${color.SurfaceVariant.ContainerLine}`, + borderRadius: config.radii.R400, + overflow: 'hidden', + width: '100%', + }, +]); + +export const TiptapEditorRow = style({ + gridTemplateColumns: 'auto 1fr auto', + alignItems: 'center', +}); + +export const TiptapEditorRowMultiline = style({ + gridTemplateColumns: 'auto 1fr', + gridTemplateAreas: ` + "before textarea" + "before after" + `, + alignItems: 'start', +}); + +export const TiptapEditorOptions = style([ + DefaultReset, + { + padding: config.space.S200, + }, +]); + +export const TiptapEditorOptionsMultiline = style({ + gridArea: 'before', + alignSelf: 'end', +}); + +export const TiptapEditorOptionsAfterMultiline = style({ + gridArea: 'after', + justifySelf: 'end', +}); + +export const TiptapEditorScrollArea = style({ + minWidth: 0, +}); + +export const TiptapEditorScrollAreaMultiline = style({ + gridArea: 'textarea', +}); + +export const TiptapEditorContent = style([ + DefaultReset, + { + flexGrow: 1, + height: 'auto', + padding: `${toRem(13)} 0 0`, + selectors: { + [`${TiptapEditorScrollArea}:first-child &`]: { + paddingLeft: toRem(13), + }, + [`${TiptapEditorScrollArea}:last-child &`]: { + paddingRight: toRem(13), + }, + '&:focus': { + outline: 'none', + }, + }, + }, +]); + +/** Wraps the ProseMirror editable div — resets prose styles from host page. */ +export const TiptapProseMirrorWrapper = style({}); + +globalStyle(`${TiptapProseMirrorWrapper} .ProseMirror`, { + outline: 'none', + minHeight: toRem(20), + paddingBottom: toRem(13), + wordBreak: 'break-word', + overflowWrap: 'break-word', +}); + +globalStyle(`${TiptapProseMirrorWrapper} .ProseMirror p`, { + margin: 0, +}); + +globalStyle(`${TiptapProseMirrorWrapper} .ProseMirror p.is-editor-empty:first-child::before`, { + content: 'attr(data-placeholder)', + float: 'left', + color: color.SurfaceVariant.OnContainer, + opacity: config.opacity.Placeholder, + pointerEvents: 'none', + height: '0', +}); + +globalStyle(`${TiptapProseMirrorWrapper} [data-mention]`, { + display: 'inline', + borderRadius: config.radii.R300, + padding: `0 ${toRem(2)}`, + backgroundColor: color.Secondary.Container, + color: color.Secondary.OnContainer, + cursor: 'default', + userSelect: 'none', +}); + +globalStyle(`${TiptapProseMirrorWrapper} [data-emoticon] img`, { + height: toRem(20), + verticalAlign: 'middle', +}); + +globalStyle(`${TiptapProseMirrorWrapper} [data-command]`, { + display: 'inline', + borderRadius: config.radii.R300, + padding: `0 ${toRem(2)}`, + backgroundColor: color.Primary.Container, + color: color.Primary.OnContainer, + cursor: 'default', + userSelect: 'none', +}); diff --git a/src/app/components/editor-tiptap/TiptapEditor.tsx b/src/app/components/editor-tiptap/TiptapEditor.tsx new file mode 100644 index 0000000000..e5e89a60d3 --- /dev/null +++ b/src/app/components/editor-tiptap/TiptapEditor.tsx @@ -0,0 +1,234 @@ +import type { ReactNode, KeyboardEventHandler, ClipboardEventHandler } from 'react'; +import { forwardRef, useCallback, useImperativeHandle, useRef, useState, useEffect } from 'react'; +import { Box, Scroll } from 'folds'; +import { useEditor, EditorContent } from '@tiptap/react'; +import StarterKit from '@tiptap/starter-kit'; +import Placeholder from '@tiptap/extension-placeholder'; +import Link from '@tiptap/extension-link'; +import type { Editor as TiptapEditorInstance } from '@tiptap/core'; + +import { mobileOrTablet } from '$utils/user-agent'; +import { MatrixMentionExtension } from './extensions/MentionExtension'; +import { EmoticonExtension } from './extensions/EmoticonExtension'; +import { CommandExtension } from './extensions/CommandExtension'; +import * as css from './TiptapEditor.css'; + +export type { TiptapEditorInstance }; + +/** Imperative handle exposed via ref for parent components. */ +export type TiptapEditorHandle = { + // eslint-disable-next-line typescript-eslint/no-redundant-type-constituents + editor: TiptapEditorInstance | null; + focus: () => void; + reset: () => void; + isEmpty: () => boolean; +}; + +type TiptapEditorProps = { + editableName?: string; + top?: ReactNode; + bottom?: ReactNode; + before?: ReactNode; + after?: ReactNode; + responsiveAfter?: ReactNode; + forceMultilineLayout?: boolean; + maxHeight?: string; + placeholder?: string; + onKeyDown?: KeyboardEventHandler; + onKeyUp?: KeyboardEventHandler; + onChange?: (editor: TiptapEditorInstance) => void; + onPaste?: ClipboardEventHandler; + className?: string; + variant?: 'Surface' | 'SurfaceVariant' | 'Background'; +}; + +export const TiptapEditor = forwardRef( + ( + { + editableName, + top, + bottom, + before, + after, + responsiveAfter, + forceMultilineLayout = false, + maxHeight = '50vh', + placeholder, + onKeyDown, + onKeyUp, + onChange, + onPaste, + className, + variant = 'SurfaceVariant', + }, + ref + ) => { + const [isMultiline, setIsMultiline] = useState(false); + const rootRef = useRef(null); + + const editor = useEditor({ + extensions: [ + StarterKit.configure({ + // Disable block-level elements we don't need in a chat composer + heading: false, + bulletList: false, + orderedList: false, + listItem: false, + blockquote: false, + codeBlock: false, + horizontalRule: false, + hardBreak: { + // Shift+Enter inserts a hard break (newline within paragraph) + keepMarks: true, + }, + }), + Link.configure({ + openOnClick: false, + autolink: true, + }), + Placeholder.configure({ + placeholder: placeholder ?? '', + }), + MatrixMentionExtension.configure({ + suggestion: { + // Disable the built-in suggestion popup — we handle autocomplete at the + // RoomInputTiptap level using our own React-based UI. + render: () => ({ + onStart: () => {}, + onUpdate: () => {}, + onKeyDown: () => false, + onExit: () => {}, + }), + }, + }), + EmoticonExtension, + CommandExtension, + ], + + onUpdate({ editor: updatedEditor }) { + // Detect multiline (more than one paragraph or hard break in first paragraph) + const { doc } = updatedEditor.state; + let multiline = doc.childCount > 1; + if (!multiline) { + doc.forEach((node) => { + if (!multiline) { + node.forEach((child) => { + if (child.type.name === 'hardBreak') multiline = true; + }); + } + }); + } + setIsMultiline(multiline || forceMultilineLayout); + onChange?.(updatedEditor); + }, + + editorProps: { + attributes: { + ...(editableName ? { 'data-editable-name': editableName } : {}), + class: css.TiptapEditorContent, + autocapitalize: 'sentences', + }, + handleKeyDown(_, event) { + onKeyDown?.(event as unknown as React.KeyboardEvent); + return false; // let Tiptap handle the event normally as well + }, + handleDOMEvents: { + keyup: (_, event) => { + onKeyUp?.(event as unknown as React.KeyboardEvent); + return false; + }, + paste: (_, event) => { + onPaste?.(event as unknown as React.ClipboardEvent); + return false; + }, + blur: () => { + if (mobileOrTablet() && editor) { + editor.commands.focus(); + } + return false; + }, + }, + }, + }); + + // Keep multiline in sync with forceMultilineLayout changes + useEffect(() => { + if (forceMultilineLayout && !isMultiline) setIsMultiline(true); + }, [forceMultilineLayout, isMultiline]); + + useImperativeHandle( + ref, + () => ({ + editor, + focus: () => editor?.commands.focus(), + reset: () => editor?.commands.clearContent(true), + isEmpty: () => editor?.isEmpty ?? true, + }), + [editor] + ); + + const layoutIsMultiline = isMultiline || forceMultilineLayout; + const hasBefore = Boolean(before); + const hasAfter = Boolean(after); + const hasResponsiveAfter = Boolean(responsiveAfter); + const showResponsiveAfterInFooter = hasResponsiveAfter && layoutIsMultiline; + const showResponsiveAfterInline = hasResponsiveAfter && !showResponsiveAfterInFooter; + + const handlePaste = useCallback( + (e) => { + onPaste?.(e); + }, + [onPaste] + ); + + return ( +
+ {top} + + {hasBefore && ( + + {before} + + )} + +
+ +
+
+ {(hasAfter || showResponsiveAfterInline) && ( + + {showResponsiveAfterInline && responsiveAfter} + {after} + + )} +
+ {bottom} +
+ ); + } +); diff --git a/src/app/components/editor-tiptap/extensions/CommandExtension.ts b/src/app/components/editor-tiptap/extensions/CommandExtension.ts new file mode 100644 index 0000000000..6eff2878cc --- /dev/null +++ b/src/app/components/editor-tiptap/extensions/CommandExtension.ts @@ -0,0 +1,40 @@ +import { Node, mergeAttributes } from '@tiptap/core'; + +/** + * Inline void node representing a /command inserted at the beginning of the message. + */ +export const CommandExtension = Node.create({ + name: 'command', + group: 'inline', + inline: true, + selectable: true, + atom: true, + + addAttributes() { + return { + command: { default: null }, + }; + }, + + parseHTML() { + return [{ tag: 'span[data-command]' }]; + }, + + renderHTML({ HTMLAttributes }) { + return ['span', mergeAttributes({ 'data-command': '' }, HTMLAttributes)]; + }, + + renderText({ node }) { + return `/${node.attrs.command ?? ''}`; + }, + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + addCommands(): any { + return { + insertCommand: + (command: string) => + ({ commands }: { commands: { insertContent: (c: unknown) => boolean } }) => + commands.insertContent({ type: this.name, attrs: { command } }), + }; + }, +}); diff --git a/src/app/components/editor-tiptap/extensions/EmoticonExtension.ts b/src/app/components/editor-tiptap/extensions/EmoticonExtension.ts new file mode 100644 index 0000000000..ef248dad66 --- /dev/null +++ b/src/app/components/editor-tiptap/extensions/EmoticonExtension.ts @@ -0,0 +1,45 @@ +import { Node, mergeAttributes } from '@tiptap/core'; + +/** + * Inline void node representing a custom emoticon (mxc:// image or plain emoji char). + */ +export const EmoticonExtension = Node.create({ + name: 'emoticon', + group: 'inline', + inline: true, + selectable: true, + draggable: false, + atom: true, + + addAttributes() { + return { + /** mxc:// URL or plain emoji character / shortcode key */ + key: { default: null }, + shortcode: { default: null }, + }; + }, + + parseHTML() { + return [{ tag: 'span[data-emoticon]' }]; + }, + + renderHTML({ HTMLAttributes }) { + return ['span', mergeAttributes({ 'data-emoticon': '' }, HTMLAttributes)]; + }, + + renderText({ node }) { + const { key, shortcode } = node.attrs as { key: string | null; shortcode: string | null }; + if (!key) return ''; + return key.startsWith('mxc://') ? `:${shortcode ?? ''}:` : key; + }, + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + addCommands(): any { + return { + insertEmoticon: + (attrs: { key: string; shortcode: string }) => + ({ commands }: { commands: { insertContent: (c: unknown) => boolean } }) => + commands.insertContent({ type: this.name, attrs }), + }; + }, +}); diff --git a/src/app/components/editor-tiptap/extensions/MentionExtension.ts b/src/app/components/editor-tiptap/extensions/MentionExtension.ts new file mode 100644 index 0000000000..f1ec20632f --- /dev/null +++ b/src/app/components/editor-tiptap/extensions/MentionExtension.ts @@ -0,0 +1,35 @@ +import Mention from '@tiptap/extension-mention'; + +/** + * Extended Mention node that stores the extra Matrix-specific attrs we need + * (nodeType, highlight, viaServers, eventId) alongside the base id/label. + */ +export const MatrixMentionExtension = Mention.extend({ + addAttributes() { + return { + id: { default: null }, + label: { default: null }, + /** 'user' | 'room' */ + nodeType: { default: 'user' }, + highlight: { default: false }, + viaServers: { default: null }, + eventId: { default: null }, + }; + }, + + renderHTML({ node, HTMLAttributes }) { + return [ + 'span', + { + 'data-mention': '', + 'data-mention-type': node.attrs.nodeType, + ...HTMLAttributes, + }, + `${node.attrs.nodeType === 'room' ? '' : '@'}${node.attrs.label ?? node.attrs.id ?? ''}`, + ]; + }, + + renderText({ node }) { + return node.attrs.id ?? node.attrs.label ?? ''; + }, +}); diff --git a/src/app/components/editor-tiptap/index.ts b/src/app/components/editor-tiptap/index.ts new file mode 100644 index 0000000000..c83e201214 --- /dev/null +++ b/src/app/components/editor-tiptap/index.ts @@ -0,0 +1,10 @@ +export { TiptapEditor } from './TiptapEditor'; +export type { TiptapEditorHandle, TiptapEditorInstance } from './TiptapEditor'; +export { + tiptapToMatrixCustomHTML, + tiptapToPlainText, + tiptapCustomHtmlEqualsPlainText, +} from './output'; +export { MatrixMentionExtension } from './extensions/MentionExtension'; +export { EmoticonExtension } from './extensions/EmoticonExtension'; +export { CommandExtension } from './extensions/CommandExtension'; diff --git a/src/app/components/editor-tiptap/output.ts b/src/app/components/editor-tiptap/output.ts new file mode 100644 index 0000000000..461a8e3e8d --- /dev/null +++ b/src/app/components/editor-tiptap/output.ts @@ -0,0 +1,201 @@ +/** + * Tiptap output serializer — converts a Tiptap editor's document to the formats + * required by the Matrix spec (custom HTML for formatted_body, plain text for body). + * + * Approach: walk the ProseMirror JSON tree, emit markdown-like text for formatted + * content, then run it through the existing markdownToHtml() pipeline so that the + * output is identical in structure to the Slate-based serializer. + */ + +import type { Editor as TiptapEditorInstance, JSONContent } from '@tiptap/core'; +import type { Room } from '$types/matrix-sdk'; +import { sanitizeText } from '$utils/sanitize'; +import { markdownToHtml, injectDataMd } from '$plugins/markdown'; +import { getMxIdLocalPart, isUserId } from '$utils/matrix'; +import { getMemberDisplayName } from '$utils/room'; +import { MATRIX_TO_BASE } from '$plugins/matrix-to'; + +export type TiptapOutputOptions = { + forEmote?: boolean; + room?: Room; +}; + +// ───────────────────────────────────────────────────────────────────────────── +// Helpers +// ───────────────────────────────────────────────────────────────────────────── + +const escapeMarkdownInline = (text: string): string => + // Escape characters that could accidentally trigger markdown formatting + text.replace(/([\\`*_~[\]])/g, '\\$1'); + +function userMentionLabel(userId: string, room: Room | undefined): string { + const fallback = getMxIdLocalPart(userId) ?? userId; + if (!room) return fallback; + const fromMembership = getMemberDisplayName(room, userId); + if (!fromMembership) return fallback; + const t = fromMembership.trim(); + if (!t || t.includes(']')) return fallback; + for (let i = 0; i < t.length; i++) { + if (t.charCodeAt(i) <= 0x1f) return fallback; + } + return t; +} + +// ───────────────────────────────────────────────────────────────────────────── +// Node → markdown text +// ───────────────────────────────────────────────────────────────────────────── + +function marksForNode(node: JSONContent): { + bold?: boolean; + italic?: boolean; + strike?: boolean; + code?: boolean; +} { + const marks: Record = {}; + for (const m of node.marks ?? []) { + if (typeof m === 'string') marks[m] = true; + else marks[m.type] = true; + } + return marks; +} + +function inlineNodeToMarkdown(node: JSONContent, opts: TiptapOutputOptions): string { + switch (node.type) { + case 'text': { + let text = node.text ?? ''; + const { bold, italic, strike, code } = marksForNode(node); + + if (code) { + // Don't escape inside code spans + return `\`${text}\``; + } + + text = escapeMarkdownInline(text); + if (bold) text = `**${text}**`; + if (italic) text = `*${text}*`; + if (strike) text = `~~${text}~~`; + return text; + } + + case 'mention': { + const { id, nodeType, viaServers, eventId } = node.attrs ?? {}; + if (!id) return ''; + + let fragment = String(id); + if (eventId) fragment += `/${String(eventId)}`; + if (viaServers && (viaServers as string[]).length > 0) + fragment += `?${(viaServers as string[]).map((s) => `via=${s}`).join('&')}`; + + const matrixTo = `${MATRIX_TO_BASE}#/${fragment}`; + + if (id === '@room') return `[@room](${encodeURI(matrixTo)})`; + if (nodeType === 'user' || isUserId(String(id))) { + const mdLabel = userMentionLabel(String(id), opts.room); + return `[${mdLabel}](${encodeURI(matrixTo)})`; + } + return sanitizeText(matrixTo); + } + + case 'emoticon': { + const { key, shortcode } = node.attrs ?? {}; + if (!key) return ''; + if (String(key).startsWith('mxc://')) { + return `${sanitizeText(String(shortcode ?? ''))}`; + } + return sanitizeText(String(key)); + } + + case 'command': { + const { command } = node.attrs ?? {}; + return `/${sanitizeText(String(command ?? ''))}`; + } + + case 'hardBreak': + return '\n'; + + default: + return ''; + } +} + +function paragraphToMarkdown(paragraph: JSONContent, opts: TiptapOutputOptions): string { + const parts = (paragraph.content ?? []).map((n) => inlineNodeToMarkdown(n, opts)); + return parts.join(''); +} + +function docToMarkdown(doc: JSONContent, opts: TiptapOutputOptions): string { + const paragraphs = doc.content ?? []; + const lines = paragraphs.map((p) => { + if (p.type === 'paragraph') return paragraphToMarkdown(p, opts); + return ''; + }); + // Join with newline; trailing newline will be stripped by trimCustomHtml + return lines.join('\n'); +} + +// ───────────────────────────────────────────────────────────────────────────── +// Public API +// ───────────────────────────────────────────────────────────────────────────── + +/** + * Convert a Tiptap editor's content to Matrix custom HTML (formatted_body). + */ +export function tiptapToMatrixCustomHTML( + editor: TiptapEditorInstance, + opts: TiptapOutputOptions = {} +): string { + const doc = editor.getJSON() as unknown as JSONContent; + const markdown = docToMarkdown(doc, opts); + const html = markdownToHtml(markdown, { emote: opts.forEmote }); + return injectDataMd(html); +} + +/** + * Convert a Tiptap editor's content to a plain-text string (body). + */ +export function tiptapToPlainText(editor: TiptapEditorInstance): string { + const doc = editor.getJSON() as unknown as JSONContent; + const paragraphs = doc.content ?? []; + const lines = paragraphs.map((p) => { + if (p.type !== 'paragraph') return ''; + return (p.content ?? []) + .map((n) => { + switch (n.type) { + case 'text': + return n.text ?? ''; + case 'mention': + return n.attrs?.id === '@room' ? '@room' : String(n.attrs?.id ?? ''); + case 'emoticon': { + const { key, shortcode } = n.attrs ?? {}; + if (!key) return ''; + return String(key).startsWith('mxc://') ? `:${String(shortcode ?? '')}:` : String(key); + } + case 'command': + return `/${String(n.attrs?.command ?? '')}`; + case 'hardBreak': + return '\n'; + default: + return ''; + } + }) + .join(''); + }); + return lines.join('\n').replace(/\n$/, ''); +} + +/** + * Returns true when the HTML and plain-text representations are equivalent + * (i.e., no actual formatting — just send plain text). + */ +export function tiptapCustomHtmlEqualsPlainText( + editor: TiptapEditorInstance, + opts: TiptapOutputOptions = {} +): boolean { + const plain = tiptapToPlainText(editor); + const html = tiptapToMatrixCustomHTML(editor, opts); + // Use DOMParser to extract text content safely — avoids manual tag-stripping + // regex and chained entity unescaping (which can double-decode). + const doc = new DOMParser().parseFromString(html, 'text/html'); + const stripped = doc.body.textContent ?? ''; + return stripped.trim() === plain.trim(); +} diff --git a/src/app/components/editor/Editor.css.ts b/src/app/components/editor/Editor.css.ts index 077519d177..0d465d6bc2 100644 --- a/src/app/components/editor/Editor.css.ts +++ b/src/app/components/editor/Editor.css.ts @@ -65,6 +65,9 @@ export const EditorTextarea = style([ { flexGrow: 1, height: 'auto', + // Detect text direction per-keystroke so RTL text right-aligns automatically. + // Matches the unicodeBidi: 'plaintext' pattern used by MessageTextBody. + unicodeBidi: 'plaintext', padding: `${toRem(13)} 0 0`, selectors: { [`${EditorTextareaScroll}:first-child &`]: { diff --git a/src/app/components/editor/Editor.tsx b/src/app/components/editor/Editor.tsx index e76cce520b..888610cc05 100644 --- a/src/app/components/editor/Editor.tsx +++ b/src/app/components/editor/Editor.tsx @@ -6,7 +6,7 @@ import { Node, createEditor } from 'slate'; import type { RenderLeafProps, RenderElementProps, RenderPlaceholderProps } from 'slate-react'; import { Slate, Editable, withReact, ReactEditor } from 'slate-react'; import { withHistory } from 'slate-history'; -import { mobileOrTablet } from '$utils/user-agent'; +import { isPhone, mobileOrTablet } from '$utils/user-agent'; import { BlockType } from './types'; import { RenderElement, RenderLeaf } from './Elements'; import type { CustomElement } from './slate'; @@ -114,6 +114,9 @@ export const CustomEditor = forwardRef( const singleLineWidthOffsetRef = useRef(0); const latestValueRef = useRef(editor.children); const isMultilineRef = useRef(false); + // Tracks whether a triggerAutoCapitalize rAF is already queued to avoid stacking + // multiple rAFs when content changes fire rapidly (e.g. IME composition). + const autocapPendingRef = useRef(false); const [isMultiline, setIsMultiline] = useState(false); const [measurementVersion, setMeasurementVersion] = useState(0); const hasBefore = Boolean(before); @@ -348,8 +351,29 @@ export const CustomEditor = forwardRef( updateMultilineLayout(latestValueRef.current); }, [measurementVersion, updateMultilineLayout]); + // Mobile OSes (iOS and Android) do not reliably capitalise the first letter in an empty + // contenteditable. Both platforms render a zero-width placeholder character (\uFEFF) + // inside the Slate DOM node to maintain the cursor, and their keyboards interpret this + // as existing content — so they don't apply sentence-case to the next keystroke. + // Toggling the autocapitalize attribute from 'none' → 'sentences' on the focused + // contenteditable forces the keyboard to re-evaluate capitalisation state with no + // content changes, no focus shifts, and no keyboard dismissal. + const triggerAutoCapitalize = useCallback(() => { + if (!mobileOrTablet()) return; + if (autocapPendingRef.current) return; + const el = editableRef.current; + if (!el) return; + autocapPendingRef.current = true; + el.setAttribute('autocapitalize', 'none'); + requestAnimationFrame(() => { + el.setAttribute('autocapitalize', 'sentences'); + autocapPendingRef.current = false; + }); + }, []); + const handleChange = useCallback( (value: Descendant[]) => { + const prevText = latestValueRef.current.map((node) => Node.string(node)).join(''); latestValueRef.current = value; measurementCacheRef.current = null; if (multilineMeasureFrameRef.current !== null) { @@ -358,8 +382,15 @@ export const CustomEditor = forwardRef( } setMeasurementVersion((version) => version + 1); onChange?.(value); + // After a send, content goes from non-empty to empty while the editor stays focused. + // Trigger the autocap attribute toggle so the next message starts capitalised. + // onBlur keeps focus on the editor so isFocused() is true when this fires. + const nextText = value.map((node) => Node.string(node)).join(''); + if (prevText.length > 0 && nextText.length === 0 && ReactEditor.isFocused(editor)) { + triggerAutoCapitalize(); + } }, - [onChange] + [onChange, editor, triggerAutoCapitalize] ); const renderElement = useCallback( @@ -371,8 +402,10 @@ export const CustomEditor = forwardRef( const handleKeydown: KeyboardEventHandler = useCallback( (evt) => { - // mobile ignores config option - if (mobileOrTablet() && evt.key === 'Enter' && !evt.shiftKey) { + // Phones (on-screen keyboard) ignore the enter-to-send config option. + // Tablets with an external keyboard should still forward Enter to onKeyDown + // so RoomInput can honour the enterForNewline / mod+enter settings. + if (isPhone() && evt.key === 'Enter' && !evt.shiftKey) { return; } @@ -440,6 +473,20 @@ export const CustomEditor = forwardRef( onPaste={onPaste} // Defer to OS capitalization setting (respects iOS sentence-case toggle). autoCapitalize="sentences" + // Detect text direction per-message so RTL languages (Arabic, Hebrew, etc.) + // automatically right-align without any toggle. + dir="auto" + // Trigger autocap re-evaluation when the editor gains focus empty. + // This handles the initial tap-to-focus case: Slate's DOM contains a + // \uFEFF placeholder that the keyboard sees as existing content and so + // skips sentence-case. The attribute toggle forces a re-evaluation. + // autocapPendingRef prevents double-fire if handleChange also fires + // (e.g. the send clears content while focus is transferred). + onFocus={() => { + if (mobileOrTablet() && Node.string(editor).length === 0) { + triggerAutoCapitalize(); + } + }} // keeps focus after pressing send. onBlur={() => { if (mobileOrTablet()) ReactEditor.focus(editor); diff --git a/src/app/components/editor/Elements.tsx b/src/app/components/editor/Elements.tsx index 6d8cbb2975..fb29f19b01 100644 --- a/src/app/components/editor/Elements.tsx +++ b/src/app/components/editor/Elements.tsx @@ -7,6 +7,7 @@ import * as css from '$styles/CustomHtml.css'; import { useMatrixClient } from '$hooks/useMatrixClient'; import { mxcUrlToHttp } from '$utils/matrix'; import { useMediaAuthentication } from '$hooks/useMediaAuthentication'; +import { AuthenticatedImg } from '$components/AuthenticatedImg'; import { nicknamesAtom } from '$state/nicknames'; import { BlockType } from './types'; import { formatMentionElementDisplayName, getBeginCommand } from './utils'; @@ -91,9 +92,9 @@ function RenderEmoticonElement({ contentEditable={false} > {element.key.startsWith('mxc://') ? ( - {element.shortcode} ) : ( diff --git a/src/app/components/editor/autocomplete/AutocompleteMenu.tsx b/src/app/components/editor/autocomplete/AutocompleteMenu.tsx index 938c27dcd4..72decd4620 100644 --- a/src/app/components/editor/autocomplete/AutocompleteMenu.tsx +++ b/src/app/components/editor/autocomplete/AutocompleteMenu.tsx @@ -60,6 +60,7 @@ export function AutocompleteMenu({ isKeyForward: (evt: KeyboardEvent) => isKeyHotkey('arrowdown', evt), isKeyBackward: (evt: KeyboardEvent) => isKeyHotkey('arrowup', evt), escapeDeactivates: stopPropagation, + tabbableOptions: { displayCheck: 'none' }, }} > void; returnFocusOnDeactivate?: boolean; + /** Controls whether the FocusTrap is active. Pass false when rendering but hiding the board. */ + active?: boolean; onEmojiSelect?: (unicode: string, shortcode: string) => void; onCustomEmojiSelect?: (mxc: string, shortcode: string) => void; onStickerSelect?: (mxc: string, shortcode: string, label: string) => void; @@ -393,6 +395,7 @@ export function EmojiBoard({ imagePackRooms, requestClose, returnFocusOnDeactivate, + active = true, onEmojiSelect, onCustomEmojiSelect, onStickerSelect, @@ -534,6 +537,7 @@ export function EmojiBoard({ return ( !editableActiveElement() && isKeyHotkey(['arrowup', 'arrowleft'], evt), escapeDeactivates: stopPropagation, + tabbableOptions: { displayCheck: 'none' }, }} > { +): string | undefined => { const preserveAnimation = isAnimatedPackImage(image); return preserveAnimation || !saveStickerEmojiBandwidth - ? (mxcUrlToHttp(mx, image.url, useAuthentication) ?? '') - : (mxcUrlToHttp(mx, image.url, useAuthentication, width, height) ?? ''); + ? (mxcUrlToHttp(mx, image.url, useAuthentication) ?? undefined) + : (mxcUrlToHttp(mx, image.url, useAuthentication, width, height) ?? undefined); }; export const getEmojiItemInfo = (element: Element): EmojiItemInfo | undefined => { @@ -95,7 +96,7 @@ export function CustomEmojiItem({ data-emoji-data={image.url} data-emoji-shortcode={image.shortcode} > - {image.body - {image.body {key.startsWith('mxc://') ? ( - {shortcode} ) : ( diff --git a/src/app/components/image-pack-view/ImageTile.tsx b/src/app/components/image-pack-view/ImageTile.tsx index 5692c7364d..f2f3bf0e86 100644 --- a/src/app/components/image-pack-view/ImageTile.tsx +++ b/src/app/components/image-pack-view/ImageTile.tsx @@ -10,6 +10,7 @@ import type { TUploadAtom } from '$state/upload'; import { createUploadAtom } from '$state/upload'; import { replaceSpaceWithDash } from '$utils/common'; import { SettingTile } from '$components/setting-tile'; +import { AuthenticatedImg } from '$components/AuthenticatedImg'; import * as css from './style.css'; import { UsageSwitcher, useUsageStr } from './UsageSwitcher'; @@ -39,9 +40,9 @@ export function ImageTile({ return ( @@ -163,9 +164,9 @@ export function ImageTileEdit({ return ( diff --git a/src/app/components/image-pack-view/PackMeta.tsx b/src/app/components/image-pack-view/PackMeta.tsx index bc3b53d14b..f02cfb6ddd 100644 --- a/src/app/components/image-pack-view/PackMeta.tsx +++ b/src/app/components/image-pack-view/PackMeta.tsx @@ -25,6 +25,7 @@ import { useObjectURL } from '$hooks/useObjectURL'; import type { UploadSuccess } from '$state/upload'; import { createUploadAtom } from '$state/upload'; import { useMediaAuthentication } from '$hooks/useMediaAuthentication'; +import { useRenderableMediaUrl } from '$hooks/useRenderableMediaUrl'; import { PackMetaReader } from '$plugins/custom-emoji'; import { CompactUploadCardRenderer } from '$components/upload-card'; @@ -33,10 +34,11 @@ type ImagePackAvatarProps = { name?: string; }; function ImagePackAvatar({ url, name }: ImagePackAvatarProps) { + const resolvedUrl = useRenderableMediaUrl(url); return ( {url ? ( - + ) : ( {nameInitials(name ?? 'Unknown')} diff --git a/src/app/components/image-viewer/ImageViewer.test.tsx b/src/app/components/image-viewer/ImageViewer.test.tsx new file mode 100644 index 0000000000..b4ab92d50e --- /dev/null +++ b/src/app/components/image-viewer/ImageViewer.test.tsx @@ -0,0 +1,52 @@ +/* oxlint-disable vitest/require-mock-type-parameters */ +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; +import FileSaver from 'file-saver'; +import { ImageViewer } from './ImageViewer'; + +const downloadMedia = vi.fn(); + +vi.mock('$hooks/useImageGestures', () => ({ + useImageGestures: () => ({ + transforms: { zoom: 1, pan: { x: 0, y: 0 } }, + cursor: 'grab', + handleWheel: vi.fn(), + onPointerDown: vi.fn(), + resetTransforms: vi.fn(), + zoomIn: vi.fn(), + zoomOut: vi.fn(), + setZoom: vi.fn(), + fitRatio: 1, + imageRef: { current: null }, + containerRef: { current: null }, + handleImageLoad: vi.fn(), + enableResizeWithWindow: vi.fn(), + }), +})); + +vi.mock('$utils/matrix', () => ({ + downloadMedia: (...args: unknown[]) => downloadMedia(...args), +})); + +vi.mock('file-saver', () => ({ + default: { + saveAs: vi.fn(), + }, +})); + +describe('ImageViewer', () => { + it('downloads media without passing a media token argument', async () => { + downloadMedia.mockResolvedValue(new Blob(['image'])); + + render( + + ); + + fireEvent.click(screen.getByText('Download')); + + await waitFor(() => { + expect(downloadMedia).toHaveBeenCalledWith('https://example.org/kitten.png'); + }); + expect(FileSaver.saveAs).toHaveBeenCalledWith(expect.any(Blob), 'kitten.png'); + }); +}); diff --git a/src/app/components/image-viewer/ImageViewer.tsx b/src/app/components/image-viewer/ImageViewer.tsx index 573ef0cfa4..f4796ed41d 100644 --- a/src/app/components/image-viewer/ImageViewer.tsx +++ b/src/app/components/image-viewer/ImageViewer.tsx @@ -69,8 +69,12 @@ export const ImageViewer = as<'div', ImageViewerProps>( }, [isEditingZoom]); const handleDownload = async () => { - const fileContent = await downloadMedia(src); - FileSaver.saveAs(fileContent, alt); + try { + const fileContent = await downloadMedia(src); + FileSaver.saveAs(fileContent, alt); + } catch { + // Download failed (e.g. network error or non-2xx response) - silently ignore. + } }; return ( diff --git a/src/app/components/member-tile/MemberTile.tsx b/src/app/components/member-tile/MemberTile.tsx index c74433eb7e..6fc30af878 100644 --- a/src/app/components/member-tile/MemberTile.tsx +++ b/src/app/components/member-tile/MemberTile.tsx @@ -4,11 +4,12 @@ import type { MatrixClient, Room, RoomMember } from '$types/matrix-sdk'; import { getMemberDisplayName } from '$utils/room'; import { getMxIdLocalPart } from '$utils/matrix'; import { useSableCosmetics } from '$hooks/useSableCosmetics'; +import { useCachedMxcConverter } from '$hooks/useCachedMxcConverter'; import { useAtomValue } from 'jotai'; import { nicknamesAtom } from '$state/nicknames'; import { UserAvatar } from '$components/user-avatar'; -import { useUserPresence } from '$hooks/useUserPresence'; -import { PresenceBadge } from '$components/presence'; +import { Presence, useUserPresence } from '$hooks/useUserPresence'; +import { AvatarPresence, PresenceBadge } from '$components/presence'; import * as css from './style.css'; const getName = (room: Room, member: RoomMember, nicknames: Record) => @@ -26,12 +27,13 @@ type MemberTileProps = { export const MemberTile = as<'button', MemberTileProps>( ({ as: AsMemberTile = 'button', mx, room, member, useAuthentication, after, ...props }, ref) => { const nicknames = useAtomValue(nicknamesAtom); + const convertMxc = useCachedMxcConverter(); const name = getName(room, member, nicknames); const presence = useUserPresence(member.userId ?? ''); const avatarMxcUrl = member.getMxcAvatarUrl() ?? mx.getUser(member.userId)?.avatarUrl; const avatarUrl = avatarMxcUrl - ? mx.mxcUrlToHttp(avatarMxcUrl, 100, 100, 'crop', undefined, false, useAuthentication) + ? (convertMxc(mx, avatarMxcUrl, useAuthentication, 100, 100, 'crop') ?? undefined) : undefined; // Sable username color and fonts @@ -39,25 +41,30 @@ export const MemberTile = as<'button', MemberTileProps>( return ( - - } - /> - + + ) : undefined + } + > + + } + /> + + {name} - {presence && presence.status && ( - - - - {presence.status} - - + {presence?.status && ( + + {presence.status} + )} {after} diff --git a/src/app/components/message/FileHeader.tsx b/src/app/components/message/FileHeader.tsx index 92c41d37ec..725eb8802d 100644 --- a/src/app/components/message/FileHeader.tsx +++ b/src/app/components/message/FileHeader.tsx @@ -7,7 +7,12 @@ import { mimeTypeToExt } from '$utils/mimeTypes'; import { useMatrixClient } from '$hooks/useMatrixClient'; import { useMediaAuthentication } from '$hooks/useMediaAuthentication'; import { AsyncStatus, useAsyncCallback } from '$hooks/useAsyncCallback'; -import { decryptFile, downloadEncryptedMedia, downloadMedia, mxcUrlToHttp } from '$utils/matrix'; +import { + decryptFileSafe, + downloadEncryptedMedia, + downloadMedia, + mxcUrlToHttp, +} from '$utils/matrix'; const badgeStyles = { maxWidth: toRem(100) }; @@ -26,8 +31,12 @@ export function FileDownloadButton({ filename, url, mimeType, encInfo }: FileDow const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication); if (!mediaUrl) throw new Error('Invalid media URL'); const fileContent = encInfo - ? await downloadEncryptedMedia(mediaUrl, (encBuf) => decryptFile(encBuf, mimeType, encInfo)) - : await downloadMedia(mediaUrl); + ? await downloadEncryptedMedia( + mediaUrl, + (encBuf) => decryptFileSafe(encBuf, mimeType, encInfo, { mediaUrl }), + mx.getAccessToken() + ) + : await downloadMedia(mediaUrl, mx.getAccessToken()); const fileURL = URL.createObjectURL(fileContent); FileSaver.saveAs(fileURL, filename); diff --git a/src/app/components/message/MsgTypeRenderers.tsx b/src/app/components/message/MsgTypeRenderers.tsx index 9ef884a43d..d5da417025 100644 --- a/src/app/components/message/MsgTypeRenderers.tsx +++ b/src/app/components/message/MsgTypeRenderers.tsx @@ -96,52 +96,59 @@ const getUrlsFromContent = ( content: Record, renderUrlsPreview?: (urls: string[]) => ReactNode ): { urls?: string[]; bundleContent?: BundleContent[] } => { - const body = typeof content.body === 'string' ? content.body : ''; - const customBody = - typeof content.formatted_body === 'string' ? content.formatted_body : undefined; - const trimmedBody = trimReplyFromBody(body); - - const urlsMatch = trimmedBody.match(LINKINPUTREGEX); - let urls = urlsMatch ? [...new Set(urlsMatch)] : undefined; - urls = urls?.map( - (url) => - (url.startsWith('(') && url.endsWith(')') && url.substring(1, url.length - 1)) || - (url.startsWith('(') && url.substring(1)) || - (url.endsWith('/)') && url.substring(0, url.length - 1)) || - url - ); - - if (urls && customBody) { - // Filter out URLs that only appear inside or
 tags in the formatted body
-    const safeHtml = customBody
-      .replace(/]*>.*?<\/pre>/gs, '')
-      .replace(/]*>.*?<\/code>/gs, '');
-    const safeText = safeHtml.replace(/<[^a][^>]*>/g, '');
-    const safeUrlsMatch = safeText.match(LINKINPUTREGEX);
-    let safeUrls = safeUrlsMatch ? [...new Set(safeUrlsMatch)] : [];
-    safeUrls = safeUrls.map(
+  try {
+    const body = typeof content.body === 'string' ? content.body : '';
+    const customBody =
+      typeof content.formatted_body === 'string' ? content.formatted_body : undefined;
+    const trimmedBody = trimReplyFromBody(body);
+
+    const urlsMatch = trimmedBody.match(LINKINPUTREGEX);
+    let urls = urlsMatch ? [...new Set(urlsMatch)] : undefined;
+    urls = urls?.map(
       (url) =>
         (url.startsWith('(') && url.endsWith(')') && url.substring(1, url.length - 1)) ||
         (url.startsWith('(') && url.substring(1)) ||
         (url.endsWith('/)') && url.substring(0, url.length - 1)) ||
         url
     );
-    const safeUrlsSet = new Set(safeUrls);
-    urls = urls.filter((url) => safeUrlsSet.has(url) && !url.startsWith(MATRIX_TO_BASE));
-  }
 
-  let bundleContent = content[
-    prefix.MATRIX_UNSTABLE_EMBEDDED_LINK_PREVIEW_PROPERTY_NAME
-  ] as BundleContent[];
-  try {
-    bundleContent = bundleContent?.filter((bundle) => !!urls?.includes(bundle.matched_url));
-    if (renderUrlsPreview && bundleContent)
-      urls = bundleContent.map((bundle) => bundle.matched_url);
-  } catch {
-    urls = [];
-  }
+    if (urls && customBody) {
+      // Filter out URLs that only appear inside  or 
 tags in the formatted body
+      const safeHtml = customBody
+        .replace(/]*>.*?<\/pre>/gs, '')
+        .replace(/]*>.*?<\/code>/gs, '');
+      const safeText = safeHtml.replace(/<[^a][^>]*>/g, '');
+      const safeUrlsMatch = safeText.match(LINKINPUTREGEX);
+      let safeUrls = safeUrlsMatch ? [...new Set(safeUrlsMatch)] : [];
+      safeUrls = safeUrls.map(
+        (url) =>
+          (url.startsWith('(') && url.endsWith(')') && url.substring(1, url.length - 1)) ||
+          (url.startsWith('(') && url.substring(1)) ||
+          (url.endsWith('/)') && url.substring(0, url.length - 1)) ||
+          url
+      );
+      const safeUrlsSet = new Set(safeUrls);
+      urls = urls.filter((url) => safeUrlsSet.has(url) && !url.startsWith(MATRIX_TO_BASE));
+    }
 
-  return { urls, bundleContent };
+    let bundleContent = content[
+      prefix.MATRIX_UNSTABLE_EMBEDDED_LINK_PREVIEW_PROPERTY_NAME
+    ] as BundleContent[];
+    try {
+      bundleContent = bundleContent?.filter((bundle) => !!urls?.includes(bundle.matched_url));
+      if (renderUrlsPreview && bundleContent)
+        urls = bundleContent.map((bundle) => bundle.matched_url);
+    } catch (innerError) {
+      console.warn('[getUrlsFromContent] Failed to process bundleContent:', innerError);
+      urls = [];
+    }
+
+    return { urls, bundleContent };
+  } catch (error) {
+    console.warn('[getUrlsFromContent] Failed to extract URLs from message content:', error);
+    // Return empty to allow message to render without link previews
+    return { urls: undefined, bundleContent: undefined };
+  }
 };
 
 export function MText({
diff --git a/src/app/components/message/Reaction.tsx b/src/app/components/message/Reaction.tsx
index c6d212ec46..58d8fee0d1 100644
--- a/src/app/components/message/Reaction.tsx
+++ b/src/app/components/message/Reaction.tsx
@@ -7,6 +7,7 @@ import { getMemberDisplayName } from '$utils/room';
 import { eventWithShortcode, getMxIdLocalPart, mxcUrlToHttp } from '$utils/matrix';
 import { useAtomValue } from 'jotai';
 import { nicknamesAtom } from '$state/nicknames';
+import { AuthenticatedImg } from '$components/AuthenticatedImg';
 import * as css from './Reaction.css';
 
 export const Reaction = as<
@@ -43,9 +44,9 @@ export const Reaction = as<
                 
               );
             return (
-              {reaction} setImgError(true)}
               />
diff --git a/src/app/components/message/Reply.test.tsx b/src/app/components/message/Reply.test.tsx
index def400b461..a92a627042 100644
--- a/src/app/components/message/Reply.test.tsx
+++ b/src/app/components/message/Reply.test.tsx
@@ -48,6 +48,10 @@ vi.mock('$hooks/useMediaAuthentication', () => ({
   useMediaAuthentication: () => false,
 }));
 
+vi.mock('$hooks/useRenderableMediaUrl', () => ({
+  useRenderableMediaUrl: (src: string | undefined) => src,
+}));
+
 vi.mock('$hooks/useIgnoredUsers', () => ({
   useIgnoredUsers: () => [],
 }));
diff --git a/src/app/components/message/content/AudioContent.tsx b/src/app/components/message/content/AudioContent.tsx
index 89674edd98..6b498d8dc7 100644
--- a/src/app/components/message/content/AudioContent.tsx
+++ b/src/app/components/message/content/AudioContent.tsx
@@ -17,7 +17,12 @@ import {
 } from '$hooks/media';
 import { useThrottle } from '$hooks/useThrottle';
 import { secondsToMinutesAndSeconds } from '$utils/common';
-import { decryptFile, downloadEncryptedMedia, downloadMedia, mxcUrlToHttp } from '$utils/matrix';
+import {
+  decryptFileSafe,
+  downloadEncryptedMedia,
+  downloadMedia,
+  mxcUrlToHttp,
+} from '$utils/matrix';
 import { useMediaAuthentication } from '$hooks/useMediaAuthentication';
 import { MEDIA_VOLUME_KEY } from '$components/media';
 
@@ -54,8 +59,12 @@ export function AudioContent({
       const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication);
       if (!mediaUrl) throw new Error('Invalid media URL');
       const fileContent = encInfo
-        ? await downloadEncryptedMedia(mediaUrl, (encBuf) => decryptFile(encBuf, mimeType, encInfo))
-        : await downloadMedia(mediaUrl);
+        ? await downloadEncryptedMedia(
+            mediaUrl,
+            (encBuf) => decryptFileSafe(encBuf, mimeType, encInfo, { mediaUrl }),
+            mx.getAccessToken()
+          )
+        : await downloadMedia(mediaUrl, mx.getAccessToken());
       return URL.createObjectURL(fileContent);
     }, [mx, url, useAuthentication, mimeType, encInfo])
   );
diff --git a/src/app/components/message/content/FileContent.tsx b/src/app/components/message/content/FileContent.tsx
index 73b11613f0..1f3194d7fb 100644
--- a/src/app/components/message/content/FileContent.tsx
+++ b/src/app/components/message/content/FileContent.tsx
@@ -29,7 +29,12 @@ import {
   mimeTypeToExt,
 } from '$utils/mimeTypes';
 import { stopPropagation } from '$utils/keyboard';
-import { decryptFile, downloadEncryptedMedia, downloadMedia, mxcUrlToHttp } from '$utils/matrix';
+import {
+  decryptFileSafe,
+  downloadEncryptedMedia,
+  downloadMedia,
+  mxcUrlToHttp,
+} from '$utils/matrix';
 import { useMediaAuthentication } from '$hooks/useMediaAuthentication';
 import { ModalWide } from '$styles/Modal.css';
 
@@ -85,8 +90,12 @@ export function ReadTextFile({ body, mimeType, url, encInfo, renderViewer }: Rea
       const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication);
       if (!mediaUrl) throw new Error('Invalid media URL');
       const fileContent = encInfo
-        ? await downloadEncryptedMedia(mediaUrl, (encBuf) => decryptFile(encBuf, mimeType, encInfo))
-        : await downloadMedia(mediaUrl);
+        ? await downloadEncryptedMedia(
+            mediaUrl,
+            (encBuf) => decryptFileSafe(encBuf, mimeType, encInfo, { mediaUrl }),
+            mx.getAccessToken()
+          )
+        : await downloadMedia(mediaUrl, mx.getAccessToken());
 
       const text = fileContent.text();
       setTextViewer(true);
@@ -176,8 +185,12 @@ export function ReadPdfFile({ body, mimeType, url, encInfo, renderViewer }: Read
       const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication);
       if (!mediaUrl) throw new Error('Invalid media URL');
       const fileContent = encInfo
-        ? await downloadEncryptedMedia(mediaUrl, (encBuf) => decryptFile(encBuf, mimeType, encInfo))
-        : await downloadMedia(mediaUrl);
+        ? await downloadEncryptedMedia(
+            mediaUrl,
+            (encBuf) => decryptFileSafe(encBuf, mimeType, encInfo, { mediaUrl }),
+            mx.getAccessToken()
+          )
+        : await downloadMedia(mediaUrl, mx.getAccessToken());
       setPdfViewer(true);
       return URL.createObjectURL(fileContent);
     }, [mx, url, useAuthentication, mimeType, encInfo])
@@ -254,8 +267,12 @@ export function DownloadFile({ body, mimeType, url, info, encInfo }: DownloadFil
       const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication);
       if (!mediaUrl) throw new Error('Invalid media URL');
       const fileContent = encInfo
-        ? await downloadEncryptedMedia(mediaUrl, (encBuf) => decryptFile(encBuf, mimeType, encInfo))
-        : await downloadMedia(mediaUrl);
+        ? await downloadEncryptedMedia(
+            mediaUrl,
+            (encBuf) => decryptFileSafe(encBuf, mimeType, encInfo, { mediaUrl }),
+            mx.getAccessToken()
+          )
+        : await downloadMedia(mediaUrl, mx.getAccessToken());
 
       const fileURL = URL.createObjectURL(fileContent);
       FileSaver.saveAs(fileURL, body);
diff --git a/src/app/components/message/content/ImageContent.tsx b/src/app/components/message/content/ImageContent.tsx
index 5907367608..eb18a9e65d 100644
--- a/src/app/components/message/content/ImageContent.tsx
+++ b/src/app/components/message/content/ImageContent.tsx
@@ -1,5 +1,5 @@
 import type { ReactNode } from 'react';
-import { useCallback, useEffect, useState } from 'react';
+import { useCallback, useEffect, useMemo, useState } from 'react';
 import {
   Badge,
   Box,
@@ -28,10 +28,17 @@ import type { EncryptedAttachmentInfo } from 'browser-encrypt-attachment';
 import type { IImageInfo } from '$types/matrix/common';
 import { AsyncStatus, useAsyncCallback } from '$hooks/useAsyncCallback';
 import { useMatrixClient } from '$hooks/useMatrixClient';
+import { useMediaUrlCacheContext } from '$hooks/useMediaUrlCacheContext';
 import { bytesToSize } from '$utils/common';
 import { FALLBACK_MIMETYPE } from '$utils/mimeTypes';
 import { stopPropagation } from '$utils/keyboard';
-import { decryptFile, downloadEncryptedMedia, mxcUrlToHttp } from '$utils/matrix';
+import {
+  decryptFileSafe,
+  downloadEncryptedMedia,
+  downloadMedia,
+  mxcUrlToHttp,
+} from '$utils/matrix';
+import { getDecryptedBlob, storeDecryptedBlob } from '$hooks/useBlobCache';
 import { useMediaAuthentication } from '$hooks/useMediaAuthentication';
 import { ModalWide } from '$styles/Modal.css';
 import { validBlurHash } from '$utils/blurHash';
@@ -65,6 +72,8 @@ type RenderImageProps = {
   alt: string;
   title: string;
   src: string;
+  width?: number;
+  height?: number;
   onLoad: () => void;
   onError: () => void;
   onClick: () => void;
@@ -85,6 +94,8 @@ export type ImageContentProps = {
   mediaLayout?: 'default' | 'contained';
   containedStripMinPx?: number;
   fillsPreviewSlot?: boolean;
+  onError?: () => void;
+  suppressErrorUI?: boolean;
 };
 export const ImageContent = as<'div', ImageContentProps>(
   (
@@ -105,12 +116,15 @@ export const ImageContent = as<'div', ImageContentProps>(
       mediaLayout = 'default',
       containedStripMinPx,
       fillsPreviewSlot,
+      onError,
+      suppressErrorUI,
       ...props
     },
     ref
   ) => {
     const mx = useMatrixClient();
     const useAuthentication = useMediaAuthentication();
+    const mediaUrlCache = useMediaUrlCacheContext();
     const blurHash = validBlurHash(info?.[MATRIX_UNSTABLE_BLUR_HASH_PROPERTY_NAME]);
 
     const [load, setLoad] = useState(false);
@@ -120,32 +134,81 @@ export const ImageContent = as<'div', ImageContentProps>(
     const [blurred, setBlurred] = useState(markedAsSpoiler ?? false);
     const [isHovered, setIsHovered] = useState(false);
 
+    const rawMediaUrl = useMemo(() => {
+      if (url.startsWith('http')) return url;
+      return mxcUrlToHttp(mx, url, useAuthentication) ?? undefined;
+    }, [mx, url, useAuthentication]);
+
     const [srcState, loadSrc] = useAsyncCallback(
       useCallback(async () => {
         if (url.startsWith('http')) return url;
 
         if (typeof matrixThumbnailMaxEdge === 'number' && matrixThumbnailMaxEdge > 0 && !encInfo) {
           const { tw, th } = thumbnailDimsForMaxEdge(matrixThumbnailMaxEdge, info?.w, info?.h);
-          const thumbUrl = mxcUrlToHttp(mx, url, useAuthentication, tw, th, 'scale', false);
-          if (thumbUrl) return thumbUrl;
+          const thumbUrl = mediaUrlCache.get(mx, url, useAuthentication, tw, th, 'scale', false);
+          if (thumbUrl) {
+            const cachedBlob = mediaUrlCache.getBlob(url, false, `thumb:${tw}x${th}:scale`);
+            if (cachedBlob) return cachedBlob;
+
+            const thumbContent = await downloadMedia(thumbUrl, mx.getAccessToken());
+            const thumbBlobUrl = URL.createObjectURL(thumbContent);
+            mediaUrlCache.setBlob(url, false, thumbBlobUrl, `thumb:${tw}x${th}:scale`);
+            return thumbBlobUrl;
+          }
         }
 
-        const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication);
+        const mediaUrl = rawMediaUrl;
         if (!mediaUrl) throw new Error('Invalid media URL');
+
         if (encInfo) {
-          const fileContent = await downloadEncryptedMedia(mediaUrl, (encBuf) =>
-            decryptFile(encBuf, mimeType ?? FALLBACK_MIMETYPE, encInfo)
+          // Check blob cache first to avoid redundant downloads/decryption
+          const cachedBlob = mediaUrlCache.getBlob(url, true, mimeType);
+          if (cachedBlob) return cachedBlob;
+
+          const persistedBlob = await getDecryptedBlob(url);
+          if (persistedBlob) {
+            const blobUrl = URL.createObjectURL(persistedBlob);
+            mediaUrlCache.setBlob(url, true, blobUrl, mimeType);
+            return blobUrl;
+          }
+
+          const fileContent = await downloadEncryptedMedia(
+            mediaUrl,
+            (encBuf) =>
+              decryptFileSafe(encBuf, mimeType ?? FALLBACK_MIMETYPE, encInfo, { mediaUrl }),
+            mx.getAccessToken()
           );
-          return URL.createObjectURL(fileContent);
+          const blobUrl = URL.createObjectURL(fileContent);
+          mediaUrlCache.setBlob(url, true, blobUrl, mimeType);
+          // Persist the decrypted blob so subsequent loads skip decrypt
+          void storeDecryptedBlob(url, fileContent);
+          return blobUrl;
         }
-        return mediaUrl;
-      }, [mx, url, useAuthentication, mimeType, encInfo, matrixThumbnailMaxEdge, info?.w, info?.h])
+        const cachedBlob = mediaUrlCache.getBlob(url, false, mimeType);
+        if (cachedBlob) return cachedBlob;
+
+        const fileContent = await downloadMedia(mediaUrl, mx.getAccessToken());
+        const blobUrl = URL.createObjectURL(fileContent);
+        mediaUrlCache.setBlob(url, false, blobUrl, mimeType);
+        return blobUrl;
+      }, [
+        encInfo,
+        info?.h,
+        info?.w,
+        matrixThumbnailMaxEdge,
+        mediaUrlCache,
+        mimeType,
+        mx,
+        rawMediaUrl,
+        url,
+        useAuthentication,
+      ])
     );
 
     useEffect(() => {
       if (!viewer) {
         setViewerFullSrc(null);
-        return undefined;
+        return;
       }
       if (
         typeof matrixThumbnailMaxEdge !== 'number' ||
@@ -153,25 +216,27 @@ export const ImageContent = as<'div', ImageContentProps>(
         encInfo ||
         url.startsWith('http')
       ) {
-        return undefined;
+        return;
       }
-      let cancelled = false;
-      void (async () => {
-        const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication);
-        if (!mediaUrl || cancelled) return;
-        setViewerFullSrc(mediaUrl);
-      })();
-      return () => {
-        cancelled = true;
-      };
-    }, [viewer, matrixThumbnailMaxEdge, encInfo, url, mx, useAuthentication]);
+      setViewerFullSrc(rawMediaUrl ?? null);
+    }, [viewer, matrixThumbnailMaxEdge, encInfo, url, rawMediaUrl]);
+
+    // When the source download succeeds, reset image-element error state so the
+    // Retry button doesn't flash before the  has had a chance to load.
+    useEffect(() => {
+      if (srcState.status === AsyncStatus.Success) {
+        setError(false);
+      }
+    }, [srcState.status]);
 
     const handleLoad = () => {
       setLoad(true);
+      setError(false);
     };
     const handleError = () => {
       setLoad(false);
       setError(true);
+      onError?.();
     };
 
     const handleRetry = () => {
@@ -183,6 +248,22 @@ export const ImageContent = as<'div', ImageContentProps>(
       if (autoPlay) loadSrc();
     }, [autoPlay, loadSrc]);
 
+    // Safety timeout: if the image src is ready but hasn't loaded within 30s,
+    // treat it as an error. This prevents infinite spinners when the browser
+    // silently fails to load the image (e.g. bad URL, CORS issue).
+    useEffect(() => {
+      if (srcState.status !== AsyncStatus.Success || load || error) return undefined;
+      const timeoutId = setTimeout(() => {
+        if (!load && !error) {
+          // eslint-disable-next-line no-console
+          console.warn('[ImageContent] Image load timeout after 30s:', url);
+          setError(true);
+          onError?.();
+        }
+      }, 30000);
+      return () => clearTimeout(timeoutId);
+    }, [srcState.status, load, error, url, onError]);
+
     const imageW = info?.w;
     const imageH = info?.h;
     const hasDimensions = typeof imageW === 'number' && typeof imageH === 'number';
@@ -290,6 +371,8 @@ export const ImageContent = as<'div', ImageContentProps>(
               alt: body,
               title: body,
               src: srcState.data,
+              ...(typeof info?.w === 'number' && Number.isFinite(info.w) ? { width: info.w } : {}),
+              ...(typeof info?.h === 'number' && Number.isFinite(info.h) ? { height: info.h } : {}),
               onLoad: handleLoad,
               onError: handleError,
               onClick: () => {
@@ -334,12 +417,13 @@ export const ImageContent = as<'div', ImageContentProps>(
         )}
         {(srcState.status === AsyncStatus.Loading || srcState.status === AsyncStatus.Success) &&
           !load &&
+          !error &&
           !blurred && (
             
               
             
           )}
-        {(error || srcState.status === AsyncStatus.Error) && (
+        {!suppressErrorUI && (error || srcState.status === AsyncStatus.Error) && (
            {
+    if (typeof thumbMxcUrl !== 'string') return undefined;
+    return mxcUrlToHttp(mx, thumbMxcUrl, useAuthentication) ?? undefined;
+  }, [mx, thumbMxcUrl, useAuthentication]);
 
   const [thumbSrcState, loadThumbSrc] = useAsyncCallback(
     useCallback(async () => {
       const thumbInfo = info.thumbnail_info;
-      const thumbMxcUrl = info.thumbnail_file?.url ?? info.thumbnail_url;
-      const encInfo = info.thumbnail_file;
       if (typeof thumbMxcUrl !== 'string' || typeof thumbInfo?.mimetype !== 'string') {
-        throw new Error('Failed to load thumbnail');
+        return null;
       }
-
-      const mediaUrl = mxcUrlToHttp(mx, thumbMxcUrl, useAuthentication);
-      if (!mediaUrl) throw new Error('Invalid media URL');
       if (encInfo) {
-        const fileContent = await downloadEncryptedMedia(mediaUrl, (encBuf) =>
-          decryptFile(encBuf, thumbInfo.mimetype ?? FALLBACK_MIMETYPE, encInfo)
-        );
-        return URL.createObjectURL(fileContent);
+        if (!rawMediaUrl) return null;
+
+        // Check blob cache first
+        const cachedBlob = mediaUrlCache.getBlob(thumbMxcUrl, true, thumbInfo.mimetype);
+        if (cachedBlob) return cachedBlob;
+
+        try {
+          const fileContent = await downloadEncryptedMedia(
+            rawMediaUrl,
+            (encBuf) =>
+              decryptFileSafe(encBuf, thumbInfo.mimetype ?? FALLBACK_MIMETYPE, encInfo, {
+                mediaUrl: rawMediaUrl,
+              }),
+            mx.getAccessToken()
+          );
+          const blobUrl = URL.createObjectURL(fileContent);
+          mediaUrlCache.setBlob(thumbMxcUrl, true, blobUrl, thumbInfo.mimetype);
+          return blobUrl;
+        } catch {
+          // Network-level media fetch failed (timeout, 404, 401, etc.).
+          // Return null so the component renders nothing instead of propagating to error boundary.
+          return null;
+        }
       }
+      if (!rawMediaUrl) return null;
+
+      const cachedBlob = mediaUrlCache.getBlob(thumbMxcUrl, false, thumbInfo.mimetype);
+      if (cachedBlob) return cachedBlob;
 
-      return mediaUrl;
-    }, [mx, info, useAuthentication])
+      try {
+        const fileContent = await downloadMedia(rawMediaUrl, mx.getAccessToken());
+        const blobUrl = URL.createObjectURL(fileContent);
+        mediaUrlCache.setBlob(thumbMxcUrl, false, blobUrl, thumbInfo.mimetype);
+        return blobUrl;
+      } catch {
+        return null;
+      }
+    }, [encInfo, info.thumbnail_info, mediaUrlCache, mx, rawMediaUrl, thumbMxcUrl])
   );
 
   useEffect(() => {
     loadThumbSrc();
   }, [loadThumbSrc]);
 
-  return thumbSrcState.status === AsyncStatus.Success ? renderImage(thumbSrcState.data) : null;
+  return thumbSrcState.status === AsyncStatus.Success && thumbSrcState.data
+    ? renderImage(thumbSrcState.data)
+    : null;
 }
diff --git a/src/app/components/message/content/VideoContent.tsx b/src/app/components/message/content/VideoContent.tsx
index 2b1166084b..95a2ae558b 100644
--- a/src/app/components/message/content/VideoContent.tsx
+++ b/src/app/components/message/content/VideoContent.tsx
@@ -21,9 +21,10 @@ import { BlurhashCanvas } from 'react-blurhash';
 import type { EncryptedAttachmentInfo } from 'browser-encrypt-attachment';
 import type { IThumbnailContent, IVideoInfo } from '$types/matrix/common';
 import { useMatrixClient } from '$hooks/useMatrixClient';
+import { useMediaUrlCacheContext } from '$hooks/useMediaUrlCacheContext';
 import { AsyncStatus, useAsyncCallback } from '$hooks/useAsyncCallback';
 import { bytesToSize, millisecondsToMinutesAndSeconds } from '$utils/common';
-import { decryptFile, downloadEncryptedMedia, downloadMedia, mxcUrlToHttp } from '$utils/matrix';
+import { decryptFileSafe, downloadEncryptedMedia, downloadMedia } from '$utils/matrix';
 import { useMediaAuthentication } from '$hooks/useMediaAuthentication';
 import { validBlurHash } from '$utils/blurHash';
 import * as css from './style.css';
@@ -69,6 +70,7 @@ export const VideoContent = as<'div', VideoContentProps>(
   ) => {
     const mx = useMatrixClient();
     const useAuthentication = useMediaAuthentication();
+    const mediaUrlCache = useMediaUrlCacheContext();
     const blurHash = validBlurHash(info.thumbnail_info?.[MATRIX_UNSTABLE_BLUR_HASH_PROPERTY_NAME]);
 
     const [load, setLoad] = useState(false);
@@ -80,23 +82,48 @@ export const VideoContent = as<'div', VideoContentProps>(
       useCallback(async () => {
         if (url.startsWith('http')) return url;
 
-        const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication);
+        const mediaUrl = mediaUrlCache.get(mx, url, useAuthentication);
         if (!mediaUrl) throw new Error('Invalid media URL');
+
+        // Check blob cache first
+        const isEncrypted = !!encInfo;
+        const cachedBlob = mediaUrlCache.getBlob(url, isEncrypted, mimeType);
+        if (cachedBlob) return cachedBlob;
+
         const fileContent = encInfo
-          ? await downloadEncryptedMedia(mediaUrl, (encBuf) =>
-              decryptFile(encBuf, mimeType, encInfo)
+          ? await downloadEncryptedMedia(
+              mediaUrl,
+              (encBuf) => decryptFileSafe(encBuf, mimeType, encInfo, { mediaUrl }),
+              mx.getAccessToken()
             )
-          : await downloadMedia(mediaUrl);
-        return URL.createObjectURL(fileContent);
-      }, [mx, url, useAuthentication, mimeType, encInfo])
+          : await downloadMedia(mediaUrl, mx.getAccessToken());
+
+        const blobUrl = URL.createObjectURL(fileContent);
+        mediaUrlCache.setBlob(url, isEncrypted, blobUrl, mimeType);
+        return blobUrl;
+      }, [mx, url, useAuthentication, mimeType, encInfo, mediaUrlCache])
     );
 
+    // When the source download succeeds, reset video-element error state so the
+    // Retry button doesn't flash before the