[
  {
    "path": ".gitattributes",
    "content": "# 统一使用 LF 换行符\n* text=auto eol=lf\n\n# Windows 特定文件保持 CRLF\n*.bat text eol=crlf\n*.cmd text eol=crlf\n*.ps1 text eol=crlf\n\n# LFS for large files\ndist/* filter=lfs diff=lfs merge=lfs -text\n"
  },
  {
    "path": ".github/workflows/auto-tag.yml",
    "content": "name: Auto Tag\n\non:\n  push:\n    branches:\n      - main                 # 正式环境\n\npermissions:\n  contents: write\n\njobs:\n  auto-tag:\n    runs-on: ubuntu-latest\n    outputs:\n      should_release: ${{ steps.version_check.outputs.should_release }}\n      version: ${{ steps.get_version.outputs.version }}\n      tag_name: ${{ steps.create_tag.outputs.tag_name }}\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v4\n        with:\n          fetch-depth: 0  # 获取所有历史，用于 git tag 操作\n\n      - name: Get version from pyproject.toml\n        id: get_version\n        run: |\n          VERSION=$(python3 -c \"import tomllib; print(tomllib.load(open('pyproject.toml', 'rb'))['project']['version'])\")\n          echo \"version=$VERSION\" >> $GITHUB_OUTPUT\n\n      - name: Get latest tag\n        id: get_latest_tag\n        run: |\n          # 获取所有 tag 并按版本排序，取最新的\n          LATEST=$(git describe --tags --abbrev=0 2>/dev/null || echo \"v0.0.0\")\n          LATEST=${LATEST#v}\n          echo \"latest=$LATEST\" >> $GITHUB_OUTPUT\n\n      - name: Compare versions\n        id: version_check\n        run: |\n          python - <<EOF\n          from packaging import version\n          pyproject_ver = \"${{ steps.get_version.outputs.version }}\"\n          latest_ver = \"${{ steps.get_latest_tag.outputs.latest }}\"\n          should_release = version.parse(pyproject_ver) > version.parse(latest_ver)\n          with open(\"$GITHUB_OUTPUT\", \"a\") as f:\n              f.write(f\"should_release={str(should_release).lower()}\\n\")\n          print(f\"PyProject version: {pyproject_ver}\")\n          print(f\"Latest tag: {latest_ver}\")\n          print(f\"Should release: {should_release}\")\n          EOF\n\n      - name: Create and push tag\n        id: create_tag\n        if: steps.version_check.outputs.should_release == 'true'\n        run: |\n          TAG_NAME=\"v${{ steps.get_version.outputs.version }}\"\n          git config user.name \"github-actions[bot]\"\n          git config user.email \"github-actions[bot]@users.noreply.github.com\"\n          git tag -a \"$TAG_NAME\" -m \"Release $TAG_NAME\"\n          git push origin \"$TAG_NAME\"\n          echo \"tag_name=$TAG_NAME\" >> $GITHUB_OUTPUT\n          echo \"✓ Created tag: $TAG_NAME\"\n\n  build:\n    needs: auto-tag\n    if: needs.auto-tag.outputs.should_release == 'true'\n    runs-on: windows-latest\n    strategy:\n      matrix:\n        architecture: [x64]\n        python-version: ['3.11']\n\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v4\n\n      - name: Set up Python\n        uses: actions/setup-python@v5\n        with:\n          python-version: ${{ matrix.python-version }}\n\n      - name: Install build dependencies\n        run: |\n          python -m pip install --upgrade pip\n          pip install -e \".[dev]\"\n\n      - name: Run tests\n        run: |\n          pytest -v --cov=remark --cov-report=term-missing\n\n      - name: Install PyInstaller\n        run: |\n          pip install pyinstaller\n\n      - name: Build executable\n        run: |\n          pyinstaller remark.spec --clean\n\n      - name: Rename executable with version\n        run: |\n          $version = \"${{ needs.auto-tag.outputs.version }}\"\n          Copy-Item \"dist\\windows-folder-remark.exe\" \"dist\\windows-folder-remark-$version.exe\"\n\n      - name: Generate checksum\n        run: |\n          $version = \"${{ needs.auto-tag.outputs.version }}\"\n          $file = \"dist\\windows-folder-remark-$version.exe\"\n          certutil -hashfile $file SHA256 > \"$file.sha256\"\n\n      - name: Upload artifact\n        uses: actions/upload-artifact@v4\n        with:\n          name: windows-folder-remark-${{ matrix.architecture }}\n          path: |\n            dist/*.exe\n            dist/*.sha256\n\n  release:\n    needs: [auto-tag, build]\n    if: needs.auto-tag.outputs.should_release == 'true'\n    runs-on: windows-latest\n    permissions:\n      contents: write\n\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v4\n\n      - name: Download all artifacts\n        uses: actions/download-artifact@v4\n        with:\n          path: artifacts\n\n      - name: Create Release\n        uses: softprops/action-gh-release@v2\n        with:\n          tag_name: ${{ needs.auto-tag.outputs.tag_name }}\n          draft: false\n          prerelease: false\n          generate_release_notes: true\n          files: |\n            artifacts/**/*.exe\n            artifacts/**/*.sha256\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n"
  },
  {
    "path": ".github/workflows/deploy-docs.yml",
    "content": "name: Deploy VitePress site to Pages\n\non:\n  push:\n    branches: [main]\n  workflow_dispatch:\n\npermissions:\n  contents: read\n  pages: write\n  id-token: write\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n\n      - name: Setup Node.js\n        uses: actions/setup-node@v4\n        with:\n          node-version: '20'\n          cache: 'npm'\n          cache-dependency-path: package-lock.json\n\n      - name: Install dependencies\n        run: npm ci\n\n      - name: Build VitePress site\n        run: npm run docs:build\n\n      - name: Upload artifact\n        uses: actions/upload-pages-artifact@v3\n        with:\n          path: docs/.vitepress/dist\n\n  deploy:\n    environment:\n      name: github-pages\n      url: ${{ steps.deployment.outputs.page_url }}\n    runs-on: ubuntu-latest\n    needs: build\n    steps:\n      - name: Deploy to GitHub Pages\n        id: deployment\n        uses: actions/deploy-pages@v4\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "# -*- coding: utf-8 -*-\nname: Build and Release\n\non:\n  push:\n    tags:\n      - 'v*.*.*'\n  workflow_dispatch:\n    inputs:\n      tag_name:\n        description: 'Tag name for testing (e.g. v2.0.1)'\n        required: false\n        type: string\n\njobs:\n  build:\n    runs-on: windows-latest\n    strategy:\n      matrix:\n        architecture: [x64]\n        python-version: ['3.11']\n\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v4\n\n      - name: Set up Python\n        uses: actions/setup-python@v5\n        with:\n          python-version: ${{ matrix.python-version }}\n\n      - name: Install build dependencies\n        run: |\n          python -m pip install --upgrade pip\n          pip install -e \".[dev]\"\n\n      - name: Run tests\n        run: |\n          pytest -v --cov=remark --cov-report=term-missing\n\n      - name: Install PyInstaller\n        run: |\n          pip install pyinstaller\n\n      - name: Build executable\n        run: |\n          pyinstaller remark.spec --clean\n\n      - name: Rename executable with version\n        run: |\n          if ($env:GITHUB_REF -match \"refs/tags/v(.*)\") {\n            $version = $matches[1]\n          } else {\n            $version = \"dev\"\n          }\n          Copy-Item \"dist\\windows-folder-remark.exe\" \"dist\\windows-folder-remark-$version.exe\"\n\n      - name: Generate checksum\n        run: |\n          if ($env:GITHUB_REF -match \"refs/tags/v(.*)\") {\n            $version = $matches[1]\n          } else {\n            $version = \"dev\"\n          }\n          $file = \"dist\\windows-folder-remark-$version.exe\"\n          certutil -hashfile $file SHA256 > \"$file.sha256\"\n\n      - name: Upload artifact\n        uses: actions/upload-artifact@v4\n        with:\n          name: windows-folder-remark-${{ matrix.architecture }}\n          path: |\n            dist/*.exe\n            dist/*.sha256\n\n  release:\n    needs: build\n    runs-on: windows-latest\n    permissions:\n      contents: write\n\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v4\n\n      - name: Download all artifacts\n        uses: actions/download-artifact@v4\n        with:\n          path: artifacts\n\n      - name: Create Release\n        uses: softprops/action-gh-release@v2\n        with:\n          tag_name: ${{ inputs.tag_name || github.ref_name }}\n          draft: ${{ github.event_name == 'workflow_dispatch' }}\n          prerelease: false\n          generate_release_notes: true\n          files: |\n            artifacts/**/*.exe\n            artifacts/**/*.sha256\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n"
  },
  {
    "path": ".github/workflows/test.yml",
    "content": "name: Test and Build\n\non:\n  pull_request:\n    branches: [main]\n  workflow_dispatch:\n\njobs:\n  test:\n    runs-on: windows-latest\n    strategy:\n      matrix:\n        python-version: [\"3.11\", \"3.12\", \"3.13\"]\n\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v4\n\n      - name: Set up Python ${{ matrix.python-version }}\n        uses: actions/setup-python@v5\n        with:\n          python-version: ${{ matrix.python-version }}\n\n      - name: Install dependencies\n        run: |\n          python -m pip install --upgrade pip\n          pip install -e \".[dev]\"\n\n      - name: Run tests\n        run: pytest -v --cov=remark --cov-report=term-missing\n\n  build-verify:\n    runs-on: windows-latest\n    needs: test\n\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v4\n\n      - name: Set up Python\n        uses: actions/setup-python@v5\n        with:\n          python-version: \"3.11\"\n\n      - name: Install dependencies\n        run: |\n          python -m pip install --upgrade pip\n          pip install -e \".[dev]\"\n\n      - name: Install PyInstaller\n        run: pip install pyinstaller\n\n      - name: Build executable\n        run: pyinstaller remark.spec --clean\n\n      - name: Verify executable\n        run: |\n          $exePath = \"dist\\windows-folder-remark.exe\"\n          if (-not (Test-Path $exePath)) {\n            Write-Error \"Executable not found!\"\n            exit 1\n          }\n          & $exePath --help\n          if ($LASTEXITCODE -ne 0) {\n            Write-Error \"Executable failed to run!\"\n            exit 1\n          }\n          Write-Output \"Build successful!\"\n        shell: pwsh\n"
  },
  {
    "path": ".gitignore",
    "content": "# IDE\n.idea\n\n# Build\nbuild/\ndist/\n# PyInstaller spec files (keep remark.spec)\n# *.spec\n\n# Python\n__pycache__/\n*.py[cod]\n*$py.class\n*.egg-info/\n.eggs/\n.venv/\nvenv/\n*.egg\n\n# Testing\n.pytest_cache/\n.coverage\nhtmlcov/\n*.cover\n\n# Type checking\n.mypy_cache/\n.dmypy.json\ndmypy.json\n.ruff_cache/\n\n# Distribution\n*.whl\n*.tar.gz\n/tools/upx/\n\n# i18n - 不再忽略 .mo 文件，需要提交到仓库以便 CI 使用\n# .mo 文件由 pybabel compile 生成，从 .po 文件编译而来\n\n# bv (beads viewer) local config and caches\n.bv/\n\n# Node.js\nnode_modules/\n"
  },
  {
    "path": ".pre-commit-config.yaml",
    "content": "# Pre-commit configuration\n# https://pre-commit.com/\n\ndefault_language_version:\n  python: python3.9\n\ndefault_stages: [pre-commit]\n\nrepos:\n  # Ruff - 代码检查和格式化\n  - repo: https://github.com/astral-sh/ruff-pre-commit\n    rev: v0.8.4\n    hooks:\n      - id: ruff\n        types: [python]\n        args: [--fix, --exit-non-zero-on-fix]\n      - id: ruff-format\n        types: [python]\n\n  # Pre-commit hooks for general checks\n  - repo: https://github.com/pre-commit/pre-commit-hooks\n    rev: v5.0.0\n    hooks:\n      - id: trailing-whitespace     # 删除行尾空格\n      - id: end-of-file-fixer       # 文件末尾添加换行\n      - id: mixed-line-ending       # 强制统一行尾符为 LF\n        args: ['--fix=lf']\n        exclude: ^(\\.bat|\\.cmd|\\.ps1)$\n      - id: check-yaml              # 检查 YAML 语法\n      - id: check-toml              # 检查 TOML 语法\n      - id: check-added-large-files # 防止大文件提交\n        args: ['--maxkb=1000']\n      - id: check-merge-conflict    # 检查合并冲突标记\n      - id: check-case-conflict     # 检查大小写冲突\n      - id: check-docstring-first   # 检查 docstring 是否在代码前\n\n  # Mypy - 静态类型检查\n  - repo: https://github.com/pre-commit/mirrors-mypy\n    rev: v1.14.1\n    hooks:\n      - id: mypy\n        types: [python]\n        additional_dependencies:\n          - types-setuptools\n          - types-requests>=2.31.0\n          - types-tqdm>=4.66.0\n        exclude: ^remark\\.py$\n\n  # Local hooks - pre-commit 和 pre-push 阶段\n  - repo: local\n    hooks:\n      - id: run-tests\n        name: Run tests\n        entry: pytest -v -m \"not slow\" --cov=remark --cov-report=term-missing\n        language: system\n        types: [python]\n        stages: [pre-commit, pre-push]\n        pass_filenames: false\n\n      - id: build-exe\n        name: Build exe (Windows only)\n        entry: bash -c 'set -e; if [[ \"$OSTYPE\" == \"msys\" || \"$OSTYPE\" == \"win32\" ]]; then pyinstaller remark.spec --clean && ./dist/windows-folder-remark.exe --help; else echo \"Skip build on non-Windows\"; fi'\n        language: system\n        files: '^(.*\\.py|.*\\.spec)$'\n        stages: [pre-push]\n        pass_filenames: false\n\n      # i18n 相关检查\n      - id: extract-translations\n        name: Extract i18n messages\n        entry: bash -c 'pybabel extract -k \"_\" -k \"gettext\" -k \"ngettext\" -o messages.pot remark/ && echo \"Translations extracted successfully\"'\n        language: system\n        types: [python]\n        stages: [pre-commit]\n        pass_filenames: false\n        require_serial: true\n\n      - id: check-i18n-completeness\n        name: Check i18n completeness\n        entry: bash -c 'pybabel update -i messages.pot -d locale && python scripts/check_i18n.py locale/*/LC_MESSAGES/messages.po'\n        language: system\n        types: [python]\n        stages: [pre-commit]\n        pass_filenames: false\n\n      - id: check-po-files\n        name: Check PO files with polint\n        entry: bash -c 'if compgen -G \"locale/*/LC_MESSAGES/*.po\" > /dev/null; then polint locale/*/LC_MESSAGES/*.po || true; fi'\n        language: system\n        types: [po]\n        stages: [pre-commit]\n\n# 全局排除\nexclude: |\n  ^(?:\n      \\.venv/|\n      \\.git/|\n      \\.mypy_cache/|\n      __pycache__/|\n      build/|\n      dist/|\n      *.egg-info/\n    )\n"
  },
  {
    "path": ".python-version",
    "content": "3.11.7\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Changelog\n\nAll notable changes to this project will be documented in this file.\n\nThe format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),\nand this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).\n\n## [Unreleased]\n\n### Added\n- GitHub Actions CI/CD for automated releases\n- PyInstaller configuration for Windows executable builds\n- Version management script for release automation\n- Support for both 32-bit and 64-bit Windows builds\n\n## [2.0.0] - Unreleased\n\n### Added\n- UTF-16 encoding detection and conversion for desktop.ini\n- User confirmation prompt before encoding conversion\n- EncodingConversionCanceled exception for safer error handling\n- Smart delete logic: removes InfoTip while preserving other desktop.ini settings\n- Top-level exception handling in CLI main()\n- Windows platform check before running\n\n### Changed\n- Improved desktop.ini read/write operations with encoding safety\n- Better error handling with exception-based flow control\n- Enhanced folder comment handling with dedicated storage layer\n- Refactored command-line argument parsing with argparse\n\n### Fixed\n- Fixed desktop.ini encoding issues\n- Path handling with spaces using os.path.join()\n- Exception handling for encoding conversion\n\n### Removed\n- File comment functionality (COM component and Property Store)\n- notify_shell_update function (no longer needed)\n- File-related imports and handlers\n\n## [1.0] - 2022-05-03\n\n### Added\n- Interactive mode with continuous loop for batch processing\n- Help system with usage instructions\n- Comment length validation\n- Complete exception handling mechanism\n\n### Fixed\n- Path with spaces handling issue\n- Command injection vulnerability (subprocess replaced os.system)\n- Encoding conversion exception handling\n- Explicit file write encoding specification\n\n### Changed\n- Improved help messages and usage prompts\n- Packaged as Windows executable\n\n[Unreleased]: https://github.com/piratf/windows-folder-remark/compare/v2.0.0...HEAD\n[2.0.0]: https://github.com/piratf/windows-folder-remark/compare/v1.0...v2.0.0\n[1.0]: https://github.com/piratf/windows-folder-remark/releases/tag/v1.0\n"
  },
  {
    "path": "LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2016 潘\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.en.md",
    "content": "# Windows Folder Remark/Comment Tool\n\n**[English](README.en.md)** | [中文文档](README.md)\n\n[![PyPI](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/)\n[![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)\n[![en](https://img.shields.io/badge/lang-en-blue.svg)](README.en.md)\n[![zh](https://img.shields.io/badge/lang-zh-red.svg)](README.md)\n\nA lightweight CLI tool to add remarks/comments to Windows folders via Desktop.ini. No system residency, no data upload, safe and secure, use it when you need it. Perfect for organizing your files with custom descriptions.\n\n## ⭐ Star Us\n\nIf you find this tool helpful, please consider giving it a star on GitHub!\n\n## Why This Tool\n\n- **Use and Go**: Runs when needed, exits when done — no system residency\n- **Safe & Secure**: Completely local operation, no data upload, privacy protected\n- **Portable**: Single-file exe packaging, no installation required\n\n## Features\n\n- Multi-language character support (UTF-16 encoding)\n- Multi-language interface support (English, Chinese)\n- Command-line and interactive modes\n- Automatic encoding detection and repair\n- Automatic update checking to stay current\n- Right-click menu integration for quick access\n- Single-file exe packaging, no Python environment required\n\n## Installation\n\n### Method 1: Using exe file (Recommended)\n\nDownload `windows-folder-remark.exe` from [releases](https://github.com/piratf/windows-folder-remark/releases) and use directly.\n\n### Method 2: Install from source\n\n```bash\n# Clone repository\ngit clone https://github.com/piratf/windows-folder-remark.git\ncd windows-folder-remark\n\n# Install dependencies (no external dependencies)\npip install -e .\n\n# Run\npython -m remark.cli --help\n```\n\n## Usage\n\n### Command-line Mode\n\n```bash\n# Add remark\nwindows-folder-remark.exe \"C:\\MyFolder\" \"This is my folder\"\n\n# View remark\nwindows-folder-remark.exe --view \"C:\\MyFolder\"\n\n# Delete remark\nwindows-folder-remark.exe --delete \"C:\\MyFolder\"\n\n# Check updates\nwindows-folder-remark.exe --update\n\n# Install right-click menu\nwindows-folder-remark.exe --install\n\n# Uninstall right-click menu\nwindows-folder-remark.exe --uninstall\n```\n\n### Interactive Mode\n\n```bash\n# Follow prompts after running\nwindows-folder-remark.exe\n```\n\n### Right-click Menu (Recommended)\n\nAfter installing the right-click menu, you can add remarks directly in File Explorer by right-clicking folders:\n\n```bash\n# Install right-click menu\nwindows-folder-remark.exe --install\n```\n\n- **Windows 10**: Right-click folder to see \"Add Folder Remark\"\n- **Windows 11**: Right-click folder → Click \"Show more options\" → Add Folder Remark\n\n### Auto Update\n\nThe program automatically checks for updates on exit (once every 24 hours) and prompts if a new version is available.\n\nYou can also manually check for updates:\n\n```bash\nwindows-folder-remark.exe --update\n```\n\n## Encoding Detection\n\nWhen viewing remarks with `--view`, if the `desktop.ini` file is not in standard UTF-16 encoding, the tool will prompt you:\n\n```\nWarning: desktop.ini file encoding is utf-8, not standard UTF-16.\nThis may cause Chinese and other special characters to display abnormally.\nFix encoding to UTF-16? [Y/n]:\n```\n\nSelect `Y` to automatically fix the encoding.\n\n## Development\n\n```bash\n# Install development dependencies\npip install -e \".[dev]\"\n\n# Run tests\npytest\n\n# Code check\nruff check .\nruff format .\n\n# Type check\nmypy remark/\n\n# Build exe locally\npython -m scripts.build\n```\n\n## How It Works\n\nThis tool implements folder remarks through these steps:\n\n1. Create/modify `Desktop.ini` file in the folder\n2. Write `[.ShellClassInfo]` section and `InfoTip` property\n3. Save file with UTF-16 encoding\n4. Set `Desktop.ini` as hidden and system attributes\n5. Set folder as read-only (makes Windows read `Desktop.ini`)\n\nReference: [Microsoft Official Documentation](https://learn.microsoft.com/en-us/windows/win32/shell/how-to-customize-folders-with-desktop-ini)\n\n## Notes\n\n- May take a few minutes to display in File Explorer after modification\n- Some file managers may not support folder remarks\n- The tool modifies system attributes of folders\n\n## License\n\nMIT License\n"
  },
  {
    "path": "README.md",
    "content": "# Windows Folder Remark/Comment Tool - Windows 文件夹备注工具\n\n**[English Documentation](README.en.md)** | [中文文档](README.md)\n\n[![PyPI](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/)\n[![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)\n[![en](https://img.shields.io/badge/lang-en-blue.svg)](README.en.md)\n[![zh](https://img.shields.io/badge/lang-zh-red.svg)](README.md)\n\nA lightweight CLI tool to add remarks/comments to Windows folders via Desktop.ini. No system residency, no data upload, safe and secure, use it when you need it. / 一个轻量级的命令行工具，通过 Desktop.ini 为 Windows 文件夹添加备注/评论。无系统驻留，无数据上传，安全放心，用完即走。\n\n**Documentation**: [Full Documentation](https://piratf.github.io/windows-folder-remark/en/) | [完整文档](https://piratf.github.io/windows-folder-remark/zh/)\n\n## ⭐ 支持\n\n如果这个工具对你有帮助，请在 GitHub 上给个 Star！\n\n## 工具优势\n\n- **用完即走**：需要时运行，用完即退出，无系统驻留\n- **安全放心**：完全本地运行，无数据上传，保护隐私\n- **轻量便携**：单文件 exe 打包，无需安装，随处可用\n\n## 特性\n\n- 支持中文等多语言字符（UTF-16 编码）\n- 支持中英文界面切换\n- 命令行模式和交互模式\n- 自动编码检测和修复\n- 自动更新检查，保持最新版本\n- 右键菜单集成，快速访问\n- 单文件 exe 打包，无需 Python 环境\n\n## 安装\n\n### 方式一：使用 exe 文件（推荐）\n\n下载 [releases](https://github.com/piratf/windows-folder-remark/releases) 中的 `windows-folder-remark.exe`，直接使用。\n\n### 方式二：从源码安装\n\n```bash\n# 克隆仓库\ngit clone https://github.com/piratf/windows-folder-remark.git\ncd windows-folder-remark\n\n# 安装依赖（无外部依赖）\npip install -e .\n\n# 运行\npython -m remark.cli --help\n```\n\n## 使用方法\n\n### 命令行模式\n\n```bash\n# 添加备注\nwindows-folder-remark.exe \"C:\\MyFolder\" \"这是我的文件夹\"\n\n# 查看备注\nwindows-folder-remark.exe --view \"C:\\MyFolder\"\n\n# 删除备注\nwindows-folder-remark.exe --delete \"C:\\MyFolder\"\n\n# 检查更新\nwindows-folder-remark.exe --update\n\n# 安装右键菜单\nwindows-folder-remark.exe --install\n\n# 卸载右键菜单\nwindows-folder-remark.exe --uninstall\n```\n\n### 交互模式\n\n```bash\n# 运行后根据提示操作\nwindows-folder-remark.exe\n```\n\n### 右键菜单（推荐）\n\n安装右键菜单后，可以直接在文件资源管理器中右键文件夹添加备注：\n\n```bash\n# 安装右键菜单\nwindows-folder-remark.exe --install\n```\n\n- **Windows 10**：右键文件夹可直接看到「添加文件夹备注」\n- **Windows 11**：右键文件夹 → 点击「显示更多选项」→ 添加文件夹备注\n\n### 自动更新\n\n程序会在退出时自动检查更新（每 24 小时一次），如有新版本会提示是否立即更新。\n\n也可以手动检查更新：\n\n```bash\nwindows-folder-remark.exe --update\n```\n\n## 编码检测\n\n当使用 `--view` 查看备注时，如果检测到 `desktop.ini` 文件不是标准的 UTF-16 编码，工具会提醒你：\n\n```\n警告: desktop.ini 文件编码为 utf-8，不是标准的 UTF-16。\n这可能导致中文等特殊字符显示异常。\n是否修复编码为 UTF-16？[Y/n]:\n```\n\n选择 `Y` 可自动修复编码。\n\n## 开发\n\n```bash\n# 安装开发依赖\npip install -e \".[dev]\"\n\n# 运行测试\npytest\n\n# 代码检查\nruff check .\nruff format .\n\n# 类型检查\nmypy remark/\n\n# 本地打包 exe\npython -m scripts.build\n```\n\n## 原理说明\n\n该工具通过以下步骤实现文件夹备注：\n\n1. 在文件夹中创建/修改 `Desktop.ini` 文件\n2. 写入 `[.ShellClassInfo]` 段落和 `InfoTip` 属性\n3. 使用 UTF-16 编码保存文件\n4. 将 `Desktop.ini` 设置为隐藏和系统属性\n5. 将文件夹设置为只读属性（使 Windows 读取 `Desktop.ini`）\n\n参考：[Microsoft 官方文档](https://learn.microsoft.com/en-us/windows/win32/shell/how-to-customize-folders-with-desktop-ini)\n\n## 注意事项\n\n- 修改后可能需要几分钟才能在资源管理器中显示\n- 某些文件管理器可能不支持显示文件夹备注\n- 工具会修改文件夹的系统属性\n\n## 许可证\n\nMIT License\n"
  },
  {
    "path": "babel.cfg",
    "content": "# Babel extraction configuration\n# https://babel.pocoo.org/en/latest/messages.html\n\n# Extract translatable strings from Python files\n[python: remark/**/*.py]\n\n# Keyword list - recognize strings in these function calls as translatable\nkeywords = _, gettext, ngettext\n\n# Encoding\nencoding = utf-8\n"
  },
  {
    "path": "docs/.vitepress/config.mjs",
    "content": "import { defineConfig } from 'vitepress'\n\nexport default defineConfig({\n  title: 'Windows 文件夹备注工具',\n  description: '一个轻量级的命令行工具，通过 Desktop.ini 为 Windows 文件夹添加备注/评论。无系统驻留，无数据上传，安全放心，用完即走。',\n  base: '/windows-folder-remark/',\n  lang: 'zh-CN',\n\n  locales: {\n    root: {\n      label: '简体中文',\n      lang: 'zh-CN'\n    },\n    en: {\n      label: 'English',\n      lang: 'en-US',\n      link: '/en/'\n    }\n  },\n\n  head: [\n    ['link', { rel: 'alternate', hreflang: 'zh-CN', href: 'https://piratf.github.io/windows-folder-remark/' }],\n    ['link', { rel: 'alternate', hreflang: 'en-US', href: 'https://piratf.github.io/windows-folder-remark/en/' }],\n    ['link', { rel: 'alternate', hreflang: 'x-default', href: 'https://piratf.github.io/windows-folder-remark/en/' }]\n  ],\n\n  sitemap: {\n    hostname: 'https://piratf.github.io',\n    transformItems(items) {\n      return items.map((item) => {\n        return {\n          url: '/windows-folder-remark' + (item.url.startsWith('/') ? '' : '/') + item.url,\n          changefreq: 'weekly',\n        }\n      })\n    }\n  },\n\n  themeConfig: {\n    nav: () => [\n      { text: '指南', link: '/guide/' },\n      { text: 'English', link: '/en/' }\n    ],\n\n    sidebar: {\n      '/': [\n        {\n          text: '指南',\n          items: [\n            { text: '介绍', link: '/' },\n            { text: '快速开始', link: '/guide/getting-started' },\n            { text: '使用方法', link: '/guide/usage' },\n            { text: 'API 参考', link: '/guide/api' }\n          ]\n        }\n      ],\n      '/en/': [\n        {\n          text: 'Guide',\n          items: [\n            { text: 'Introduction', link: '/en/' },\n            { text: 'Getting Started', link: '/en/guide/getting-started' },\n            { text: 'Usage', link: '/en/guide/usage' },\n            { text: 'API Reference', link: '/en/guide/api' }\n          ]\n        }\n      ]\n    }\n  }\n})\n"
  },
  {
    "path": "docs/en/guide/api.md",
    "content": "# API Reference\n\n## Command-line Arguments\n\n| Argument | Short | Description |\n|---|---|---|\n| `--help` | `-h` | Show help information |\n| `--install` | | Install right-click menu |\n| `--uninstall` | | Uninstall right-click menu |\n| `--update` | | Check for updates |\n| `--view <path>` | | View folder remark |\n| `--delete <path>` | | Delete folder remark |\n| `--gui <path>` | | GUI mode |\n| `--lang <lang>` | `-L` | Set language (en, zh) |\n\n## Exit Codes\n\n| Code | Description |\n|---|---|\n| 0 | Success |\n| 1 | Error |\n"
  },
  {
    "path": "docs/en/guide/getting-started.md",
    "content": "# Getting Started\n\n## Download\n\nDownload `windows-folder-remark.exe` from [GitHub Releases](https://github.com/piratf/windows-folder-remark/releases).\n\n## Basic Usage\n\n### Add Remark\n\n```bash\nwindows-folder-remark.exe \"C:\\MyFolder\" \"This is my folder\"\n```\n\n### View Remark\n\n```bash\nwindows-folder-remark.exe --view \"C:\\MyFolder\"\n```\n\n### Delete Remark\n\n```bash\nwindows-folder-remark.exe --delete \"C:\\MyFolder\"\n```\n\n## Install Right-click Menu\n\n```bash\nwindows-folder-remark.exe --install\n```\n\nAfter installation, you can add remarks directly in File Explorer by right-clicking folders.\n"
  },
  {
    "path": "docs/en/guide/usage.md",
    "content": "# Usage\n\n## Command-line Mode\n\n```bash\n# Add remark\nwindows-folder-remark.exe \"C:\\MyFolder\" \"My Folder\"\n\n# View remark\nwindows-folder-remark.exe --view \"C:\\MyFolder\"\n\n# Delete remark\nwindows-folder-remark.exe --delete \"C:\\MyFolder\"\n```\n\n## Interactive Mode\n\n```bash\nwindows-folder-remark.exe\n```\n\n## Language Switch\n\n```bash\n# Use English\nwindows-folder-remark.exe --lang en\n\n# Use Chinese\nwindows-folder-remark.exe --lang zh\n```\n\n## Auto Update\n\nThe program automatically checks for updates on exit (once every 24 hours).\n\nManual update check:\n\n```bash\nwindows-folder-remark.exe --update\n```\n"
  },
  {
    "path": "docs/en/index.md",
    "content": "---\nlayout: home\n\nhead:\n  - - script\n    - type: application/ld+json\n    - |\n      {\n        \"@context\": \"https://schema.org\",\n        \"@type\": \"SoftwareSourceCode\",\n        \"name\": \"Windows Folder Remark Tool\",\n        \"description\": \"A lightweight CLI tool to add remarks/comments to Windows folders via Desktop.ini. No system residency, no data upload, safe and secure, use it when you need it.\",\n        \"programmingLanguage\": \"Python\",\n        \"codeRepository\": \"https://github.com/piratf/windows-folder-remark\",\n        \"url\": \"https://piratf.github.io/windows-folder-remark/en/\",\n        \"version\": \"2.0.7\",\n        \"license\": \"MIT\",\n        \"offers\": {\n          \"@type\": \"Offer\",\n          \"price\": \"0\",\n          \"priceCurrency\": \"USD\"\n        }\n      }\n\nhero:\n  name: Windows Folder Remark Tool\n  text: A Lightweight CLI Tool for Windows Folder Remarks\n  tagline: Add remarks/comments to Windows folders via Desktop.ini\n  actions:\n    - theme: brand\n      text: Get Started\n      link: /en/guide/getting-started\n    - theme: alt\n      text: GitHub\n      link: https://github.com/piratf/windows-folder-remark\n\nfeatures:\n  - title: Use and Go\n    details: Runs when needed, exits when done — no system residency\n  - title: Safe & Secure\n    details: Completely local operation, no data upload, privacy protected\n  - title: Portable\n    details: Single-file exe packaging, no installation required\n  - title: Multi-language Support\n    details: English and Chinese interface with auto language detection\n  - title: UTF-16 Encoding\n    details: Full support for Chinese and other special characters\n  - title: Auto Update\n    details: Built-in update checking to stay current\n---\n\n## ⭐ Star Us\n\nIf you find this tool helpful, please give it a star on [GitHub](https://github.com/piratf/windows-folder-remark)!\n"
  },
  {
    "path": "docs/guide/api.md",
    "content": "# API 参考\n\n## 命令行参数\n\n| 参数 | 简写 | 说明 |\n|---|---|---|\n| `--help` | `-h` | 显示帮助信息 |\n| `--install` | | 安装右键菜单 |\n| `--uninstall` | | 卸载右键菜单 |\n| `--update` | | 检查更新 |\n| `--view <path>` | | 查看文件夹备注 |\n| `--delete <path>` | | 删除文件夹备注 |\n| `--gui <path>` | | GUI 模式 |\n| `--lang <lang>` | `-L` | 设置语言 (en, zh) |\n\n## 退出码\n\n| 代码 | 说明 |\n|---|---|\n| 0 | 成功 |\n| 1 | 错误 |\n"
  },
  {
    "path": "docs/guide/getting-started.md",
    "content": "# 快速开始\n\n## 下载\n\n从 [GitHub Releases](https://github.com/piratf/windows-folder-remark/releases) 下载 `windows-folder-remark.exe`。\n\n## 基本使用\n\n### 添加备注\n\n```bash\nwindows-folder-remark.exe \"C:\\MyFolder\" \"这是我的文件夹\"\n```\n\n### 查看备注\n\n```bash\nwindows-folder-remark.exe --view \"C:\\MyFolder\"\n```\n\n### 删除备注\n\n```bash\nwindows-folder-remark.exe --delete \"C:\\MyFolder\"\n```\n\n## 安装右键菜单\n\n```bash\nwindows-folder-remark.exe --install\n```\n\n安装后可以在文件资源管理器中右键文件夹直接添加备注。\n"
  },
  {
    "path": "docs/guide/usage.md",
    "content": "# 使用方法\n\n## 命令行模式\n\n```bash\n# 添加备注\nwindows-folder-remark.exe \"C:\\MyFolder\" \"我的文件夹\"\n\n# 查看备注\nwindows-folder-remark.exe --view \"C:\\MyFolder\"\n\n# 删除备注\nwindows-folder-remark.exe --delete \"C:\\MyFolder\"\n```\n\n## 交互模式\n\n```bash\nwindows-folder-remark.exe\n```\n\n## 语言切换\n\n```bash\n# 使用中文\nwindows-folder-remark.exe --lang zh\n\n# 使用英文\nwindows-folder-remark.exe --lang en\n```\n\n## 自动更新\n\n程序会在退出时自动检查更新（每 24 小时一次）。\n\n手动检查更新：\n\n```bash\nwindows-folder-remark.exe --update\n```\n"
  },
  {
    "path": "docs/index.md",
    "content": "---\nlayout: home\n\nhead:\n  - - script\n    - type: application/ld+json\n    - |\n      {\n        \"@context\": \"https://schema.org\",\n        \"@type\": \"SoftwareSourceCode\",\n        \"name\": \"Windows 文件夹备注工具\",\n        \"description\": \"一个轻量级的命令行工具，通过 Desktop.ini 为 Windows 文件夹添加备注/评论。无系统驻留，无数据上传，安全放心，用完即走。\",\n        \"programmingLanguage\": \"Python\",\n        \"codeRepository\": \"https://github.com/piratf/windows-folder-remark\",\n        \"url\": \"https://piratf.github.io/windows-folder-remark/\",\n        \"version\": \"2.0.7\",\n        \"license\": \"MIT\",\n        \"offers\": {\n          \"@type\": \"Offer\",\n          \"price\": \"0\",\n          \"priceCurrency\": \"USD\"\n        }\n      }\n\nhero:\n  name: Windows 文件夹备注工具\n  text: 轻量级 Windows 文件夹备注工具\n  tagline: 通过 Desktop.ini 为 Windows 文件夹添加备注/评论\n  actions:\n    - theme: brand\n      text: 快速开始\n      link: /guide/getting-started\n    - theme: alt\n      text: GitHub\n      link: https://github.com/piratf/windows-folder-remark\n\nfeatures:\n  - title: 用完即走\n    details: 需要时运行，用完即退出，无系统驻留\n  - title: 安全放心\n    details: 完全本地运行，无数据上传，保护隐私\n  - title: 轻量便携\n    details: 单文件 exe 打包，无需安装，随处可用\n  - title: 多语言支持\n    details: 支持中英文界面，自动检测系统语言\n  - title: UTF-16 编码\n    details: 支持中文等多语言字符\n  - title: 自动更新\n    details: 内置更新检查，保持最新版本\n---\n\n## ⭐ 支持\n\n如果这个工具对你有帮助，请在 [GitHub](https://github.com/piratf/windows-folder-remark) 上给个 Star！\n"
  },
  {
    "path": "docs/robots.txt",
    "content": "User-agent: *\nAllow: /\n\nSitemap: https://piratf.github.io/windows-folder-remark/sitemap.xml\n"
  },
  {
    "path": "locale/zh/LC_MESSAGES/messages.po",
    "content": "# Chinese translations for PROJECT.\n# Copyright (C) 2026 ORGANIZATION\n# This file is distributed under the same license as the PROJECT project.\n# FIRST AUTHOR <EMAIL@ADDRESS>, 2026.\n#\nmsgid \"\"\nmsgstr \"\"\n\"Project-Id-Version: PROJECT VERSION\\n\"\n\"Report-Msgid-Bugs-To: EMAIL@ADDRESS\\n\"\n\"POT-Creation-Date: 2026-01-31 14:42+0800\\n\"\n\"PO-Revision-Date: 2026-01-29 22:35+0800\\n\"\n\"Last-Translator: FULL NAME <EMAIL@ADDRESS>\\n\"\n\"Language: zh\\n\"\n\"Language-Team: zh <LL@li.org>\\n\"\n\"Plural-Forms: nplurals=1; plural=0;\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"Generated-By: Babel 2.17.0\\n\"\n\n#: remark/cli/commands.py:73\n#, python-brace-format\nmsgid \"Path does not exist: {path}\"\nmsgstr \"路径不存在: {path}\"\n\n#: remark/cli/commands.py:76\n#, python-brace-format\nmsgid \"Path is not a folder: {path}\"\nmsgstr \"路径不是文件夹: {path}\"\n\n#: remark/cli/commands.py:98\n#, python-brace-format\nmsgid \"Current version: {version}\"\nmsgstr \"当前版本: {version}\"\n\n#: remark/cli/commands.py:99\nmsgid \"Checking for updates...\"\nmsgstr \"正在检查更新...\"\n\n#: remark/cli/commands.py:104\n#, python-brace-format\nmsgid \"\"\n\"\\n\"\n\"New version found: {tag_name}\"\nmsgstr \"\"\n\"\\n\"\n\"发现新版本: {tag_name}\"\n\n#: remark/cli/commands.py:105 remark/cli/commands.py:133\n#, python-brace-format\nmsgid \"Update notes: {notes}\"\nmsgstr \"更新说明: {notes}\"\n\n#: remark/cli/commands.py:106 remark/cli/commands.py:134\n#, python-brace-format\nmsgid \"Full changelog: {url}\"\nmsgstr \"完整更新日志: {url}\"\n\n#: remark/cli/commands.py:107\nmsgid \"\"\n\"\\n\"\n\"Update now? [Y/n]: \"\nmsgstr \"\"\n\"\\n\"\n\"是否立即更新? [Y/n]: \"\n\n#: remark/cli/commands.py:112\nmsgid \"Already at the latest version\"\nmsgstr \"已是最新版本\"\n\n#: remark/cli/commands.py:129\n#, python-brace-format\nmsgid \"\"\n\"\\n\"\n\"New version available: {tag_name} (Current version: {version})\"\nmsgstr \"\"\n\"\\n\"\n\"发现新版本: {tag_name} (当前版本: {version})\"\n\n#: remark/cli/commands.py:135\nmsgid \"Update now? [Y/n]: \"\nmsgstr \"是否立即更新? [Y/n]: \"\n\n#: remark/cli/commands.py:142\nmsgid \"Downloading new version...\"\nmsgstr \"正在下载新版本...\"\n\n#: remark/cli/commands.py:149\nmsgid \"Download complete, preparing update...\"\nmsgstr \"下载完成，准备更新...\"\n\n#: remark/cli/commands.py:153\nmsgid \"Update program has started, the application will exit...\"\nmsgstr \"更新程序已启动，程序即将退出...\"\n\n#: remark/cli/commands.py:154\nmsgid \"Please wait a few moments, the update will complete automatically.\"\nmsgstr \"请等待几秒钟，更新将自动完成。\"\n\n#: remark/cli/commands.py:160\nmsgid \"Download failed: Connection reset by server\"\nmsgstr \"下载失败：连接被服务器断开\"\n\n#: remark/cli/commands.py:161\nmsgid \"Please try again later, or visit the following link to download manually:\"\nmsgstr \"请稍后重试，或访问以下链接手动下载：\"\n\n#: remark/cli/commands.py:164\nmsgid \"Download failed: Request timeout\"\nmsgstr \"下载失败：请求超时\"\n\n#: remark/cli/commands.py:165 remark/cli/commands.py:169\nmsgid \"\"\n\"Please check your network connection, or visit the following link to \"\n\"download manually:\"\nmsgstr \"请检查网络连接，或访问以下链接手动下载：\"\n\n#: remark/cli/commands.py:168\nmsgid \"Download failed: Unable to connect to server\"\nmsgstr \"下载失败：无法连接到服务器\"\n\n#: remark/cli/commands.py:172\nmsgid \"Download failed, please check your network or download manually\"\nmsgstr \"下载失败，请检查网络连接或手动下载更新\"\n\n#: remark/cli/commands.py:175\n#, python-brace-format\nmsgid \"Update failed: {error}\"\nmsgstr \"更新失败: {error}\"\n\n#: remark/cli/commands.py:176\n#, python-brace-format\nmsgid \"Manual download: {url}\"\nmsgstr \"手动下载: {url}\"\n\n#: remark/cli/commands.py:193\nmsgid \"Right-click menu installed successfully\"\nmsgstr \"右键菜单安装成功\"\n\n#: remark/cli/commands.py:195\nmsgid \"Usage Instructions:\"\nmsgstr \"使用说明:\"\n\n#: remark/cli/commands.py:196\nmsgid \"  Windows 10: Right-click folder to see 'Add Folder Remark'\"\nmsgstr \"  Windows 10: 右键文件夹可直接看到「添加文件夹备注」\"\n\n#: remark/cli/commands.py:197\nmsgid \"\"\n\"  Windows 11: Right-click folder → Click 'Show more options' → Add Folder\"\n\" Remark\"\nmsgstr \"  Windows 11: 右键文件夹 → 点击「显示更多选项」→ 添加文件夹备注\"\n\n#: remark/cli/commands.py:200\nmsgid \"Right-click menu installation failed\"\nmsgstr \"右键菜单安装失败\"\n\n#: remark/cli/commands.py:206\nmsgid \"Right-click menu uninstalled\"\nmsgstr \"右键菜单已卸载\"\n\n#: remark/cli/commands.py:209\nmsgid \"Right-click menu uninstallation failed\"\nmsgstr \"右键菜单卸载失败\"\n\n#: remark/cli/commands.py:236 remark/storage/desktop_ini.py:309\n#, python-brace-format\nmsgid \"Warning: desktop.ini file encoding is {encoding}, not standard UTF-16.\"\nmsgstr \"警告: desktop.ini 文件编码为 {encoding}，不是标准的 UTF-16。\"\n\n#: remark/cli/commands.py:237 remark/storage/desktop_ini.py:310\nmsgid \"unknown\"\nmsgstr \"未知\"\n\n#: remark/cli/commands.py:239\nmsgid \"This may cause Chinese and other special characters to display abnormally.\"\nmsgstr \"这可能导致中文等特殊字符显示异常。\"\n\n#: remark/cli/commands.py:243\nmsgid \"Fix encoding to UTF-16? [Y/n]: \"\nmsgstr \"是否修复编码为 UTF-16？[Y/n]: \"\n\n#: remark/cli/commands.py:246\nmsgid \"Fixed to UTF-16 encoding\"\nmsgstr \"已修复为 UTF-16 编码\"\n\n#: remark/cli/commands.py:248\nmsgid \"Failed to fix encoding\"\nmsgstr \"修复失败\"\n\n#: remark/cli/commands.py:251\nmsgid \"Skip encoding fix\"\nmsgstr \"跳过编码修复\"\n\n#: remark/cli/commands.py:254 remark/storage/desktop_ini.py:335\nmsgid \"Please enter Y or n\"\nmsgstr \"请输入 Y 或 n\"\n\n#: remark/cli/commands.py:259\n#, python-brace-format\nmsgid \"Current remark: {remark}\"\nmsgstr \"当前备注: {remark}\"\n\n#: remark/cli/commands.py:261 remark/core/folder_handler.py:85\nmsgid \"This folder has no remark\"\nmsgstr \"该文件夹没有备注\"\n\n#: remark/cli/commands.py:266\n#, python-brace-format\nmsgid \"Windows Folder Remark Tool v{version}\"\nmsgstr \"Windows 文件夹备注工具 v{version}\"\n\n#: remark/cli/commands.py:267\nmsgid \"Tip: Press Ctrl + C to exit\"\nmsgstr \"提示: 按 Ctrl + C 退出程序\"\n\n#: remark/cli/commands.py:268\nmsgid \"Tip: Use #help to see available commands\"\nmsgstr \"提示: 使用 #help 查看可用命令\"\n\n#: remark/cli/commands.py:276\nmsgid \"Enter folder path (or drag here): \"\nmsgstr \"请输入文件夹路径(或拖动到这里): \"\n\n#: remark/cli/commands.py:277\nmsgid \"Enter remark:\"\nmsgstr \"请输入备注:\"\n\n#: remark/cli/commands.py:296\nmsgid \"Path does not exist, please re-enter\"\nmsgstr \"路径不存在，请重新输入\"\n\n#: remark/cli/commands.py:300\nmsgid \"This is a 'file', currently only supports adding remarks to 'folders'\"\nmsgstr \"这是一个「文件」，当前仅支持为「文件夹」添加备注\"\n\n#: remark/cli/commands.py:305\nmsgid \"Remark cannot be empty\"\nmsgstr \"备注不要为空哦\"\n\n#: remark/cli/commands.py:311\nmsgid \" ❤ Thank you for using\"\nmsgstr \" ❤ 感谢使用\"\n\n#: remark/cli/commands.py:313\nmsgid \"Continue processing or press Ctrl + C to exit\"\nmsgstr \"继续处理或按 Ctrl + C 退出程序\"\n\n#: remark/cli/commands.py:327\nmsgid \"Available commands:\"\nmsgstr \"可用命令:\"\n\n#: remark/cli/commands.py:331 remark/cli/commands.py:345\nmsgid \"Tip: Press Tab to complete commands\"\nmsgstr \"提示: 按 Tab 补全命令\"\n\n#: remark/cli/commands.py:333 remark/cli/commands.py:347\nmsgid \"Tip: Install pyreadline3 for Tab completion (pip install pyreadline3)\"\nmsgstr \"提示: 安装 pyreadline3 以使用 Tab 补全（pip install pyreadline3）\"\n\n#: remark/cli/commands.py:337\nmsgid \"Interactive Commands:\"\nmsgstr \"交互命令:\"\n\n#: remark/cli/commands.py:338\nmsgid \"  #help     Show this help message\"\nmsgstr \"  #help     显示此帮助信息\"\n\n#: remark/cli/commands.py:339\nmsgid \"  #install  Install right-click menu\"\nmsgstr \"  #install  安装右键菜单\"\n\n#: remark/cli/commands.py:340\nmsgid \"  #uninstall Uninstall right-click menu\"\nmsgstr \"  #uninstall 卸载右键菜单\"\n\n#: remark/cli/commands.py:341\nmsgid \"  #update   Check for updates\"\nmsgstr \"  #update   检查更新\"\n\n#: remark/cli/commands.py:343\nmsgid \"Or simply enter a folder path to add remarks\"\nmsgstr \"或者直接输入文件夹路径来添加备注\"\n\n#: remark/cli/commands.py:351\nmsgid \"Windows Folder Remark Tool\"\nmsgstr \"Windows 文件夹备注工具\"\n\n#: remark/cli/commands.py:352\nmsgid \"Usage:\"\nmsgstr \"使用方法:\"\n\n#: remark/cli/commands.py:353\nmsgid \"  Interactive mode: python remark.py\"\nmsgstr \"  交互模式: python remark.py\"\n\n#: remark/cli/commands.py:354\nmsgid \"  Command line mode: python remark.py [options] [arguments]\"\nmsgstr \"  命令行模式: python remark.py [选项] [参数]\"\n\n#: remark/cli/commands.py:355\nmsgid \"Options:\"\nmsgstr \"选项:\"\n\n#: remark/cli/commands.py:356\nmsgid \"  --install          Install right-click menu\"\nmsgstr \"  --install          安装右键菜单\"\n\n#: remark/cli/commands.py:357\nmsgid \"  --uninstall        Uninstall right-click menu\"\nmsgstr \"  --uninstall        卸载右键菜单\"\n\n#: remark/cli/commands.py:358\nmsgid \"  --update           Check for updates\"\nmsgstr \"  --update           检查更新\"\n\n#: remark/cli/commands.py:359\nmsgid \"  --gui <path>        GUI mode (called from right-click menu)\"\nmsgstr \"  --gui <路径>       GUI 模式（右键菜单调用）\"\n\n#: remark/cli/commands.py:360\nmsgid \"  --delete <path>     Delete remark\"\nmsgstr \"  --delete <路径>    删除备注\"\n\n#: remark/cli/commands.py:361\n#, fuzzy\nmsgid \"  --view <path>       View remark\"\nmsgstr \"  --view <路径>      查看备注\"\n\n#: remark/cli/commands.py:362\nmsgid \"  --help, -h         Show help information\"\nmsgstr \"  --help, -h         显示帮助信息\"\n\n#: remark/cli/commands.py:363\nmsgid \"Interactive Commands (available in interactive mode):\"\nmsgstr \"交互命令（在交互模式中可用）:\"\n\n#: remark/cli/commands.py:364\nmsgid \"  #help              Show interactive help\"\nmsgstr \"  #help              显示交互帮助\"\n\n#: remark/cli/commands.py:365\nmsgid \"  #install           Install right-click menu\"\nmsgstr \"  #install           安装右键菜单\"\n\n#: remark/cli/commands.py:366\nmsgid \"  #uninstall         Uninstall right-click menu\"\nmsgstr \"  #uninstall         卸载右键菜单\"\n\n#: remark/cli/commands.py:367\nmsgid \"  #update            Check for updates\"\nmsgstr \"  #update            检查更新\"\n\n#: remark/cli/commands.py:368\nmsgid \"Examples:\"\nmsgstr \"示例:\"\n\n#: remark/cli/commands.py:369\nmsgid \" [Add remark] python remark.py \\\"C:\\\\\\\\MyFolder\\\" \\\"My Folder\\\"\"\nmsgstr \" [添加备注] python remark.py \\\"C:\\\\\\\\MyFolder\\\" \\\"这是我的文件夹\\\"\"\n\n#: remark/cli/commands.py:370\nmsgid \" [Delete remark] python remark.py --delete \\\"C:\\\\\\\\MyFolder\\\"\"\nmsgstr \" [删除备注] python remark.py --delete \\\"C:\\\\\\\\MyFolder\\\"\"\n\n#: remark/cli/commands.py:371\nmsgid \" [View current remark] python remark.py --view \\\"C:\\\\\\\\MyFolder\\\"\"\nmsgstr \" [查看当前备注] python remark.py --view \\\"C:\\\\\\\\MyFolder\\\"\"\n\n#: remark/cli/commands.py:372\nmsgid \" [Install right-click menu] python remark.py --install\"\nmsgstr \" [安装右键菜单] python remark.py --install\"\n\n#: remark/cli/commands.py:373\nmsgid \" [Check for updates] python remark.py --update\"\nmsgstr \" [检查更新] python remark.py --update\"\n\n#: remark/cli/commands.py:430\nmsgid \"Error: Path does not exist or not quoted\"\nmsgstr \"错误: 路径不存在或未使用引号\"\n\n#: remark/cli/commands.py:431\nmsgid \"Hint: Use quotes when path contains spaces\"\nmsgstr \"提示: 路径包含空格时请使用引号\"\n\n#: remark/cli/commands.py:432\nmsgid \"  windows-folder-remark \\\"C:\\\\\\\\My Documents\\\" \\\"Remark content\\\"\"\nmsgstr \"  windows-folder-remark \\\"C:\\\\\\\\My Documents\\\" \\\"备注内容\\\"\"\n\n#: remark/cli/commands.py:437\n#, python-brace-format\nmsgid \"Detected path: {path}\"\nmsgstr \"检测到路径: {path}\"\n\n#: remark/cli/commands.py:440 remark/cli/commands.py:482\nmsgid \"Error: This is a file, the tool can only set remarks for folders\"\nmsgstr \"错误: 这是一个文件，工具只能为文件夹设置备注\"\n\n#: remark/cli/commands.py:445\n#, python-brace-format\nmsgid \"Remark content: {remark}\"\nmsgstr \"备注内容: {remark}\"\n\n#: remark/cli/commands.py:447\nmsgid \"(Will view existing remark)\"\nmsgstr \"(将查看现有备注)\"\n\n#: remark/cli/commands.py:449\nmsgid \"Continue? [Y/n]: \"\nmsgstr \"是否继续? [Y/n]: \"\n\n#: remark/cli/commands.py:568\nmsgid \"\"\n\"\\n\"\n\"Operation cancelled\"\nmsgstr \"\"\n\"\\n\"\n\"操作已取消\"\n\n#: remark/cli/commands.py:571\n#, python-brace-format\nmsgid \"An error occurred: {error}\"\nmsgstr \"发生错误: {error}\"\n\n#: remark/core/folder_handler.py:24\n#, python-brace-format\nmsgid \"Path is not a folder: {folder_path}\"\nmsgstr \"路径不是文件夹: {folder_path}\"\n\n#: remark/core/folder_handler.py:29\n#, python-brace-format\nmsgid \"Remark length exceeds limit, maximum length is {length} characters\"\nmsgstr \"备注长度超过限制，最大长度为 {length} 个字符\"\n\n#: remark/core/folder_handler.py:47 remark/core/folder_handler.py:90\nmsgid \"Failed to clear file attributes\"\nmsgstr \"清除文件属性失败\"\n\n#: remark/core/folder_handler.py:52\nmsgid \"Failed to write desktop.ini\"\nmsgstr \"写入 desktop.ini 失败\"\n\n#: remark/core/folder_handler.py:57\nmsgid \"Failed to set file attributes\"\nmsgstr \"设置文件属性失败\"\n\n#: remark/core/folder_handler.py:62\nmsgid \"Failed to set folder attributes\"\nmsgstr \"设置文件夹属性失败\"\n\n#: remark/core/folder_handler.py:66\n#, python-brace-format\nmsgid \"Remark [{remark}] has been set for folder [{folder_path}]\"\nmsgstr \"已经为文件夹 [{folder_path}] 设置备注 [{remark}]\"\n\n#: remark/core/folder_handler.py:70\nmsgid \"Remark added successfully, may take a few minutes to display\"\nmsgstr \"备注添加成功，可能需要过几分钟才会显示\"\n\n#: remark/core/folder_handler.py:73\n#, python-brace-format\nmsgid \"Failed to set remark: {error}\"\nmsgstr \"设置备注失败: {error}\"\n\n#: remark/core/folder_handler.py:95\nmsgid \"Failed to remove remark\"\nmsgstr \"移除备注失败\"\n\n#: remark/core/folder_handler.py:102\nmsgid \"Failed to restore file attributes\"\nmsgstr \"恢复文件属性失败\"\n\n#: remark/core/folder_handler.py:105\nmsgid \"Remark deleted successfully\"\nmsgstr \"备注删除成功\"\n\n#: remark/storage/desktop_ini.py:313\nmsgid \"This file needs to be converted to UTF-16 encoding before modification.\"\nmsgstr \"修改此文件前需要先转换为 UTF-16 编码。\"\n\n#: remark/storage/desktop_ini.py:314\nmsgid \"\"\n\"The original content will be preserved, only the encoding format will \"\n\"change.\"\nmsgstr \"原内容会被保留，仅改变编码格式。\"\n\n#: remark/storage/desktop_ini.py:321\nmsgid \"\"\n\"\\n\"\n\"Current file content:\"\nmsgstr \"\"\n\"\\n\"\n\"当前文件内容:\"\n\n#: remark/storage/desktop_ini.py:328\nmsgid \"\"\n\"\\n\"\n\"Convert to UTF-16 encoding and continue? [Y/n]: \"\nmsgstr \"\"\n\"\\n\"\n\"是否转换为 UTF-16 编码后继续？[Y/n]: \"\n\n#: remark/storage/desktop_ini.py:332 remark/storage/desktop_ini.py:347\nmsgid \"Operation cancelled.\"\nmsgstr \"操作已取消。\"\n\n#: remark/storage/desktop_ini.py:341\nmsgid \"Converted to UTF-16 encoding.\"\nmsgstr \"已转换为 UTF-16 编码。\"\n\n#: remark/storage/desktop_ini.py:346\n#, python-brace-format\nmsgid \"Conversion failed: {error}\"\nmsgstr \"转换失败: {error}\"\n\n#: remark/utils/platform.py:13\nmsgid \"\"\n\"Error: This tool adds remarks to files/folders on Windows, other systems \"\n\"are not supported.\"\nmsgstr \"错误: 此工具为 Windows 系统中的文件/文件夹添加备注，暂不支持其他系统。\"\n\n#: remark/utils/platform.py:14\n#, python-brace-format\nmsgid \"Current system: {system}\"\nmsgstr \"当前系统: {system}\"\n\n#~ msgid \"Detected multiple possible paths, please select:\"\n#~ msgstr \"检测到多个可能的路径，请选择:\"\n\n#~ msgid \" [file]\"\n#~ msgstr \" [文件]\"\n\n#~ msgid \"\"\n#~ \"\\n\"\n#~ \"[{index}] Path: {path}{type_mark}\"\n#~ msgstr \"\"\n#~ \"\\n\"\n#~ \"[{index}] 路径: {path}{type_mark}\"\n\n#~ msgid \"    Remaining remarks: {remarks}\"\n#~ msgstr \"    剩余备注: {remarks}\"\n\n#~ msgid \"    (Will view existing remark)\"\n#~ msgstr \"    (将查看现有备注)\"\n\n#~ msgid \"\"\n#~ \"\\n\"\n#~ \"[0] Cancel\"\n#~ msgstr \"\"\n#~ \"\\n\"\n#~ \"[0] 取消\"\n\n#~ msgid \"\"\n#~ \"\\n\"\n#~ \"Please select [0-{max}]: \"\n#~ msgstr \"\"\n#~ \"\\n\"\n#~ \"请选择 [0-{max}]: \"\n\n#~ msgid \"\"\n#~ \"\\n\"\n#~ \"Error: This is a file, the tool\"\n#~ \" can only set remarks for folders,\"\n#~ \" please reselect\"\n#~ msgstr \"\"\n#~ \"\\n\"\n#~ \"错误: 这是一个文件，工具只能为文件夹设置备注，请重新选择\"\n\n#~ msgid \"Invalid selection, please try again\"\n#~ msgstr \"无效选择，请重试\"\n\n"
  },
  {
    "path": "messages.pot",
    "content": "# Translations template for PROJECT.\n# Copyright (C) 2026 ORGANIZATION\n# This file is distributed under the same license as the PROJECT project.\n# FIRST AUTHOR <EMAIL@ADDRESS>, 2026.\n#\n#, fuzzy\nmsgid \"\"\nmsgstr \"\"\n\"Project-Id-Version: PROJECT VERSION\\n\"\n\"Report-Msgid-Bugs-To: EMAIL@ADDRESS\\n\"\n\"POT-Creation-Date: 2026-01-31 14:42+0800\\n\"\n\"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\\n\"\n\"Last-Translator: FULL NAME <EMAIL@ADDRESS>\\n\"\n\"Language-Team: LANGUAGE <LL@li.org>\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=utf-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"Generated-By: Babel 2.17.0\\n\"\n\n#: remark/cli/commands.py:73\n#, python-brace-format\nmsgid \"Path does not exist: {path}\"\nmsgstr \"\"\n\n#: remark/cli/commands.py:76\n#, python-brace-format\nmsgid \"Path is not a folder: {path}\"\nmsgstr \"\"\n\n#: remark/cli/commands.py:98\n#, python-brace-format\nmsgid \"Current version: {version}\"\nmsgstr \"\"\n\n#: remark/cli/commands.py:99\nmsgid \"Checking for updates...\"\nmsgstr \"\"\n\n#: remark/cli/commands.py:104\n#, python-brace-format\nmsgid \"\"\n\"\\n\"\n\"New version found: {tag_name}\"\nmsgstr \"\"\n\n#: remark/cli/commands.py:105 remark/cli/commands.py:133\n#, python-brace-format\nmsgid \"Update notes: {notes}\"\nmsgstr \"\"\n\n#: remark/cli/commands.py:106 remark/cli/commands.py:134\n#, python-brace-format\nmsgid \"Full changelog: {url}\"\nmsgstr \"\"\n\n#: remark/cli/commands.py:107\nmsgid \"\"\n\"\\n\"\n\"Update now? [Y/n]: \"\nmsgstr \"\"\n\n#: remark/cli/commands.py:112\nmsgid \"Already at the latest version\"\nmsgstr \"\"\n\n#: remark/cli/commands.py:129\n#, python-brace-format\nmsgid \"\"\n\"\\n\"\n\"New version available: {tag_name} (Current version: {version})\"\nmsgstr \"\"\n\n#: remark/cli/commands.py:135\nmsgid \"Update now? [Y/n]: \"\nmsgstr \"\"\n\n#: remark/cli/commands.py:142\nmsgid \"Downloading new version...\"\nmsgstr \"\"\n\n#: remark/cli/commands.py:149\nmsgid \"Download complete, preparing update...\"\nmsgstr \"\"\n\n#: remark/cli/commands.py:153\nmsgid \"Update program has started, the application will exit...\"\nmsgstr \"\"\n\n#: remark/cli/commands.py:154\nmsgid \"Please wait a few moments, the update will complete automatically.\"\nmsgstr \"\"\n\n#: remark/cli/commands.py:160\nmsgid \"Download failed: Connection reset by server\"\nmsgstr \"\"\n\n#: remark/cli/commands.py:161\nmsgid \"Please try again later, or visit the following link to download manually:\"\nmsgstr \"\"\n\n#: remark/cli/commands.py:164\nmsgid \"Download failed: Request timeout\"\nmsgstr \"\"\n\n#: remark/cli/commands.py:165 remark/cli/commands.py:169\nmsgid \"\"\n\"Please check your network connection, or visit the following link to \"\n\"download manually:\"\nmsgstr \"\"\n\n#: remark/cli/commands.py:168\nmsgid \"Download failed: Unable to connect to server\"\nmsgstr \"\"\n\n#: remark/cli/commands.py:172\nmsgid \"Download failed, please check your network or download manually\"\nmsgstr \"\"\n\n#: remark/cli/commands.py:175\n#, python-brace-format\nmsgid \"Update failed: {error}\"\nmsgstr \"\"\n\n#: remark/cli/commands.py:176\n#, python-brace-format\nmsgid \"Manual download: {url}\"\nmsgstr \"\"\n\n#: remark/cli/commands.py:193\nmsgid \"Right-click menu installed successfully\"\nmsgstr \"\"\n\n#: remark/cli/commands.py:195\nmsgid \"Usage Instructions:\"\nmsgstr \"\"\n\n#: remark/cli/commands.py:196\nmsgid \"  Windows 10: Right-click folder to see 'Add Folder Remark'\"\nmsgstr \"\"\n\n#: remark/cli/commands.py:197\nmsgid \"\"\n\"  Windows 11: Right-click folder → Click 'Show more options' → Add Folder\"\n\" Remark\"\nmsgstr \"\"\n\n#: remark/cli/commands.py:200\nmsgid \"Right-click menu installation failed\"\nmsgstr \"\"\n\n#: remark/cli/commands.py:206\nmsgid \"Right-click menu uninstalled\"\nmsgstr \"\"\n\n#: remark/cli/commands.py:209\nmsgid \"Right-click menu uninstallation failed\"\nmsgstr \"\"\n\n#: remark/cli/commands.py:236 remark/storage/desktop_ini.py:309\n#, python-brace-format\nmsgid \"Warning: desktop.ini file encoding is {encoding}, not standard UTF-16.\"\nmsgstr \"\"\n\n#: remark/cli/commands.py:237 remark/storage/desktop_ini.py:310\nmsgid \"unknown\"\nmsgstr \"\"\n\n#: remark/cli/commands.py:239\nmsgid \"This may cause Chinese and other special characters to display abnormally.\"\nmsgstr \"\"\n\n#: remark/cli/commands.py:243\nmsgid \"Fix encoding to UTF-16? [Y/n]: \"\nmsgstr \"\"\n\n#: remark/cli/commands.py:246\nmsgid \"Fixed to UTF-16 encoding\"\nmsgstr \"\"\n\n#: remark/cli/commands.py:248\nmsgid \"Failed to fix encoding\"\nmsgstr \"\"\n\n#: remark/cli/commands.py:251\nmsgid \"Skip encoding fix\"\nmsgstr \"\"\n\n#: remark/cli/commands.py:254 remark/storage/desktop_ini.py:335\nmsgid \"Please enter Y or n\"\nmsgstr \"\"\n\n#: remark/cli/commands.py:259\n#, python-brace-format\nmsgid \"Current remark: {remark}\"\nmsgstr \"\"\n\n#: remark/cli/commands.py:261 remark/core/folder_handler.py:85\nmsgid \"This folder has no remark\"\nmsgstr \"\"\n\n#: remark/cli/commands.py:266\n#, python-brace-format\nmsgid \"Windows Folder Remark Tool v{version}\"\nmsgstr \"\"\n\n#: remark/cli/commands.py:267\nmsgid \"Tip: Press Ctrl + C to exit\"\nmsgstr \"\"\n\n#: remark/cli/commands.py:268\nmsgid \"Tip: Use #help to see available commands\"\nmsgstr \"\"\n\n#: remark/cli/commands.py:276\nmsgid \"Enter folder path (or drag here): \"\nmsgstr \"\"\n\n#: remark/cli/commands.py:277\nmsgid \"Enter remark:\"\nmsgstr \"\"\n\n#: remark/cli/commands.py:296\nmsgid \"Path does not exist, please re-enter\"\nmsgstr \"\"\n\n#: remark/cli/commands.py:300\nmsgid \"This is a 'file', currently only supports adding remarks to 'folders'\"\nmsgstr \"\"\n\n#: remark/cli/commands.py:305\nmsgid \"Remark cannot be empty\"\nmsgstr \"\"\n\n#: remark/cli/commands.py:311\nmsgid \" ❤ Thank you for using\"\nmsgstr \"\"\n\n#: remark/cli/commands.py:313\nmsgid \"Continue processing or press Ctrl + C to exit\"\nmsgstr \"\"\n\n#: remark/cli/commands.py:327\nmsgid \"Available commands:\"\nmsgstr \"\"\n\n#: remark/cli/commands.py:331 remark/cli/commands.py:345\nmsgid \"Tip: Press Tab to complete commands\"\nmsgstr \"\"\n\n#: remark/cli/commands.py:333 remark/cli/commands.py:347\nmsgid \"Tip: Install pyreadline3 for Tab completion (pip install pyreadline3)\"\nmsgstr \"\"\n\n#: remark/cli/commands.py:337\nmsgid \"Interactive Commands:\"\nmsgstr \"\"\n\n#: remark/cli/commands.py:338\nmsgid \"  #help     Show this help message\"\nmsgstr \"\"\n\n#: remark/cli/commands.py:339\nmsgid \"  #install  Install right-click menu\"\nmsgstr \"\"\n\n#: remark/cli/commands.py:340\nmsgid \"  #uninstall Uninstall right-click menu\"\nmsgstr \"\"\n\n#: remark/cli/commands.py:341\nmsgid \"  #update   Check for updates\"\nmsgstr \"\"\n\n#: remark/cli/commands.py:343\nmsgid \"Or simply enter a folder path to add remarks\"\nmsgstr \"\"\n\n#: remark/cli/commands.py:351\nmsgid \"Windows Folder Remark Tool\"\nmsgstr \"\"\n\n#: remark/cli/commands.py:352\nmsgid \"Usage:\"\nmsgstr \"\"\n\n#: remark/cli/commands.py:353\nmsgid \"  Interactive mode: python remark.py\"\nmsgstr \"\"\n\n#: remark/cli/commands.py:354\nmsgid \"  Command line mode: python remark.py [options] [arguments]\"\nmsgstr \"\"\n\n#: remark/cli/commands.py:355\nmsgid \"Options:\"\nmsgstr \"\"\n\n#: remark/cli/commands.py:356\nmsgid \"  --install          Install right-click menu\"\nmsgstr \"\"\n\n#: remark/cli/commands.py:357\nmsgid \"  --uninstall        Uninstall right-click menu\"\nmsgstr \"\"\n\n#: remark/cli/commands.py:358\nmsgid \"  --update           Check for updates\"\nmsgstr \"\"\n\n#: remark/cli/commands.py:359\nmsgid \"  --gui <path>        GUI mode (called from right-click menu)\"\nmsgstr \"\"\n\n#: remark/cli/commands.py:360\nmsgid \"  --delete <path>     Delete remark\"\nmsgstr \"\"\n\n#: remark/cli/commands.py:361\nmsgid \"  --view <path>       View remark\"\nmsgstr \"\"\n\n#: remark/cli/commands.py:362\nmsgid \"  --help, -h         Show help information\"\nmsgstr \"\"\n\n#: remark/cli/commands.py:363\nmsgid \"Interactive Commands (available in interactive mode):\"\nmsgstr \"\"\n\n#: remark/cli/commands.py:364\nmsgid \"  #help              Show interactive help\"\nmsgstr \"\"\n\n#: remark/cli/commands.py:365\nmsgid \"  #install           Install right-click menu\"\nmsgstr \"\"\n\n#: remark/cli/commands.py:366\nmsgid \"  #uninstall         Uninstall right-click menu\"\nmsgstr \"\"\n\n#: remark/cli/commands.py:367\nmsgid \"  #update            Check for updates\"\nmsgstr \"\"\n\n#: remark/cli/commands.py:368\nmsgid \"Examples:\"\nmsgstr \"\"\n\n#: remark/cli/commands.py:369\nmsgid \" [Add remark] python remark.py \\\"C:\\\\\\\\MyFolder\\\" \\\"My Folder\\\"\"\nmsgstr \"\"\n\n#: remark/cli/commands.py:370\nmsgid \" [Delete remark] python remark.py --delete \\\"C:\\\\\\\\MyFolder\\\"\"\nmsgstr \"\"\n\n#: remark/cli/commands.py:371\nmsgid \" [View current remark] python remark.py --view \\\"C:\\\\\\\\MyFolder\\\"\"\nmsgstr \"\"\n\n#: remark/cli/commands.py:372\nmsgid \" [Install right-click menu] python remark.py --install\"\nmsgstr \"\"\n\n#: remark/cli/commands.py:373\nmsgid \" [Check for updates] python remark.py --update\"\nmsgstr \"\"\n\n#: remark/cli/commands.py:430\nmsgid \"Error: Path does not exist or not quoted\"\nmsgstr \"\"\n\n#: remark/cli/commands.py:431\nmsgid \"Hint: Use quotes when path contains spaces\"\nmsgstr \"\"\n\n#: remark/cli/commands.py:432\nmsgid \"  windows-folder-remark \\\"C:\\\\\\\\My Documents\\\" \\\"Remark content\\\"\"\nmsgstr \"\"\n\n#: remark/cli/commands.py:437\n#, python-brace-format\nmsgid \"Detected path: {path}\"\nmsgstr \"\"\n\n#: remark/cli/commands.py:440 remark/cli/commands.py:482\nmsgid \"Error: This is a file, the tool can only set remarks for folders\"\nmsgstr \"\"\n\n#: remark/cli/commands.py:445\n#, python-brace-format\nmsgid \"Remark content: {remark}\"\nmsgstr \"\"\n\n#: remark/cli/commands.py:447\nmsgid \"(Will view existing remark)\"\nmsgstr \"\"\n\n#: remark/cli/commands.py:449\nmsgid \"Continue? [Y/n]: \"\nmsgstr \"\"\n\n#: remark/cli/commands.py:568\nmsgid \"\"\n\"\\n\"\n\"Operation cancelled\"\nmsgstr \"\"\n\n#: remark/cli/commands.py:571\n#, python-brace-format\nmsgid \"An error occurred: {error}\"\nmsgstr \"\"\n\n#: remark/core/folder_handler.py:24\n#, python-brace-format\nmsgid \"Path is not a folder: {folder_path}\"\nmsgstr \"\"\n\n#: remark/core/folder_handler.py:29\n#, python-brace-format\nmsgid \"Remark length exceeds limit, maximum length is {length} characters\"\nmsgstr \"\"\n\n#: remark/core/folder_handler.py:47 remark/core/folder_handler.py:90\nmsgid \"Failed to clear file attributes\"\nmsgstr \"\"\n\n#: remark/core/folder_handler.py:52\nmsgid \"Failed to write desktop.ini\"\nmsgstr \"\"\n\n#: remark/core/folder_handler.py:57\nmsgid \"Failed to set file attributes\"\nmsgstr \"\"\n\n#: remark/core/folder_handler.py:62\nmsgid \"Failed to set folder attributes\"\nmsgstr \"\"\n\n#: remark/core/folder_handler.py:66\n#, python-brace-format\nmsgid \"Remark [{remark}] has been set for folder [{folder_path}]\"\nmsgstr \"\"\n\n#: remark/core/folder_handler.py:70\nmsgid \"Remark added successfully, may take a few minutes to display\"\nmsgstr \"\"\n\n#: remark/core/folder_handler.py:73\n#, python-brace-format\nmsgid \"Failed to set remark: {error}\"\nmsgstr \"\"\n\n#: remark/core/folder_handler.py:95\nmsgid \"Failed to remove remark\"\nmsgstr \"\"\n\n#: remark/core/folder_handler.py:102\nmsgid \"Failed to restore file attributes\"\nmsgstr \"\"\n\n#: remark/core/folder_handler.py:105\nmsgid \"Remark deleted successfully\"\nmsgstr \"\"\n\n#: remark/storage/desktop_ini.py:313\nmsgid \"This file needs to be converted to UTF-16 encoding before modification.\"\nmsgstr \"\"\n\n#: remark/storage/desktop_ini.py:314\nmsgid \"\"\n\"The original content will be preserved, only the encoding format will \"\n\"change.\"\nmsgstr \"\"\n\n#: remark/storage/desktop_ini.py:321\nmsgid \"\"\n\"\\n\"\n\"Current file content:\"\nmsgstr \"\"\n\n#: remark/storage/desktop_ini.py:328\nmsgid \"\"\n\"\\n\"\n\"Convert to UTF-16 encoding and continue? [Y/n]: \"\nmsgstr \"\"\n\n#: remark/storage/desktop_ini.py:332 remark/storage/desktop_ini.py:347\nmsgid \"Operation cancelled.\"\nmsgstr \"\"\n\n#: remark/storage/desktop_ini.py:341\nmsgid \"Converted to UTF-16 encoding.\"\nmsgstr \"\"\n\n#: remark/storage/desktop_ini.py:346\n#, python-brace-format\nmsgid \"Conversion failed: {error}\"\nmsgstr \"\"\n\n#: remark/utils/platform.py:13\nmsgid \"\"\n\"Error: This tool adds remarks to files/folders on Windows, other systems \"\n\"are not supported.\"\nmsgstr \"\"\n\n#: remark/utils/platform.py:14\n#, python-brace-format\nmsgid \"Current system: {system}\"\nmsgstr \"\"\n\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"windows-folder-remark-docs\",\n  \"version\": \"1.0.0\",\n  \"type\": \"module\",\n  \"description\": \"Documentation for Windows Folder Remark Tool\",\n  \"scripts\": {\n    \"docs:dev\": \"vitepress dev docs\",\n    \"docs:build\": \"vitepress build docs\",\n    \"docs:preview\": \"vitepress preview docs\"\n  },\n  \"devDependencies\": {\n    \"vitepress\": \"^1.0.0\"\n  }\n}\n"
  },
  {
    "path": "pyproject.toml",
    "content": "# pyproject.toml - 现代 Python 项目配置\n# https://packaging.python.org/en/latest/guides/writing-pyproject-toml/\n\n[project]\nname = \"windows-folder-remark\"\nversion = \"2.0.7\"\ndescription = \"Windows 文件夹备注工具\"\nreadme = \"README.md\"\nrequires-python = \">=3.11\"\nlicense = {text = \"MIT\"}\nauthors = [\n    {name = \"Piratf\"}\n]\nkeywords = [\"windows\", \"folder\", \"remark\", \"comment\", \"desktop.ini\"]\nclassifiers = [\n    \"Programming Language :: Python :: 3\",\n    \"License :: OSI Approved :: MIT License\",\n    \"Operating System :: Microsoft :: Windows\",\n    \"Programming Language :: Python :: 3.11\",\n    \"Programming Language :: Python :: 3.12\",\n    \"Programming Language :: Python :: 3.13\",\n]\n\n# 运行时依赖\n# 注意：仅使用 Python 标准库，无外部依赖\ndependencies = []\n\n# 开发依赖\n[project.optional-dependencies]\ndev = [\n    \"ruff>=0.8.0\",\n    \"mypy>=1.14.0\",\n    \"pytest>=7.0.0\",\n    \"pytest-cov>=4.0.0\",\n    \"pytest-xdist>=3.0.0\",\n    \"pytest-mock>=3.10.0\",\n    \"pyfakefs>=5.0.0\",\n    \"pre-commit>=3.0.0\",\n    \"pyinstaller>=6.0.0\",\n    \"babel>=2.16.0\",\n    \"polint>=0.3.0\",\n]\n\n# 命令行入口\n[project.scripts]\nremark = \"remark.cli.commands:main\"\nremark-build = \"scripts.build:main\"\n\n# 构建系统配置\n[build-system]\nrequires = [\"setuptools>=61.0\"]\nbuild-backend = \"setuptools.build_meta\"\n\n[tool.setuptools.packages.find]\ninclude = [\"remark*\"]\n\n\n# =============================================================================\n# 工具配置\n# =============================================================================\n\n# Ruff - 代码检查和格式化\n# https://docs.astral.sh/ruff/configuration/\n[tool.ruff]\ntarget-version = \"py311\"\nline-length = 100\n\n[tool.ruff.lint]\nselect = [\n    \"E\",   # pycodestyle errors\n    \"W\",   # pycodestyle warnings\n    \"F\",   # pyflakes\n    \"I\",   # isort\n    \"N\",   # pep8-naming\n    \"UP\",  # pyupgrade\n    \"B\",   # flake8-bugbear\n    \"C4\",  # flake8-comprehensions\n    \"SIM\", # flake8-simplify\n    \"RUF\", # ruff-specific rules\n]\n\nignore = [\n    \"E501\",   # line too long (由 formatter 处理)\n    \"B008\",   # do not perform function calls in argument defaults\n    \"SIM108\", # use ternary operator (可读性考虑)\n    \"RUF001\", # 中文全角字符误报\n    \"RUF002\", # 中文全角字符误报\n    \"RUF003\", # 中文全角字符误报\n]\n\nfixable = [\"ALL\"]\nunfixable = []\n\n[tool.ruff.lint.per-file-ignores]\n\"tests/**/*.py\" = [\"S101\"]  # 允许测试中使用 assert\n\n[tool.ruff.format]\nquote-style = \"double\"\nindent-style = \"space\"\nskip-magic-trailing-comma = false\nline-ending = \"auto\"\n\n[tool.ruff.lint.isort]\nknown-first-party = [\"remark\"]\n\n\n# Mypy - 静态类型检查\n# https://mypy.readthedocs.io/\n[tool.mypy]\npython_version = \"3.11\"\nwarn_return_any = true\nwarn_unused_configs = true\ndisallow_untyped_defs = false\ncheck_untyped_defs = true\nshow_error_codes = true\nshow_column_numbers = true\ncolor_output = true\nerror_summary = true\nexclude = [\n    '^remark\\\\.py$',\n]\n\n[[tool.mypy.overrides]]\nmodule = []\nignore_missing_imports = true\n\n\n# Pytest - 测试框架\n# https://docs.pytest.org/\n[tool.pytest.ini_options]\ntestpaths = [\"tests\"]\npython_files = [\"test_*.py\", \"*_test.py\"]\npython_classes = [\"Test*\"]\npython_functions = [\"test_*\"]\nminversion = \"7.0\"\n\naddopts = [\n    \"-v\",                    # 详细输出\n    \"-l\",                    # 显示本地变量（失败时）\n    \"-s\",                    # 显示 print 输出\n]\n\nmarkers = [\n    \"unit: 单元测试\",\n    \"integration: 集成测试\",\n    \"windows: 仅在 Windows 上运行的测试\",\n    \"slow: 慢速测试\",\n]\n\nfilterwarnings = [\n    \"ignore::DeprecationWarning\",\n    \"ignore::PendingDeprecationWarning\",\n]\n\n# Coverage - 覆盖率配置\n# https://coverage.readthedocs.io/\n[tool.coverage.run]\nsource = [\"remark\"]\nomit = [\n    \"*/tests/*\",\n    \"*/__pycache__/*\",\n    \"*/site-packages/*\",\n]\n\n[tool.coverage.report]\nexclude_lines = [\n    \"pragma: no cover\",\n    \"def __repr__\",\n    \"raise AssertionError\",\n    \"raise NotImplementedError\",\n    \"if __name__ == .__main__.:\",\n    \"if TYPE_CHECKING:\",\n    \"@abstractmethod\",\n]\n\n\n# Babel - 国际化配置\n# https://babel.pocoo.org/\n[tool.babel]\n# 目录配置\nlocale_dir = \"locale\"\ndomain = \"messages\"\n# 源文件编码\ninput_encoding = \"utf-8\"\n# 输出文件编码\noutput_encoding = \"utf-8\"\n"
  },
  {
    "path": "remark/__init__.py",
    "content": "\"\"\"\nWindows Folder Remark - 为 Windows 文件夹添加备注工具\n\"\"\"\n\n__author__ = \"Piratf\"\n\nfrom remark.core.base import CommentHandler\nfrom remark.core.folder_handler import FolderCommentHandler\n\n__all__ = [\n    \"CommentHandler\",\n    \"FolderCommentHandler\",\n]\n"
  },
  {
    "path": "remark/cli/__init__.py",
    "content": "\"\"\"\n命令行接口模块\n\"\"\"\n\nfrom remark.cli.commands import CLI\n\n__all__ = [\"CLI\"]\n"
  },
  {
    "path": "remark/cli/__main__.py",
    "content": "\"\"\"\nMain entry point for running remark.cli as a module.\n\"\"\"\n\nfrom remark.cli.commands import main\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "remark/cli/commands.py",
    "content": "\"\"\"\n命令行接口\n\"\"\"\n\nimport argparse\nimport os\nimport sys\nimport tempfile\nimport threading\nimport urllib.error\n\nfrom remark.core.folder_handler import FolderCommentHandler\nfrom remark.gui import remark_dialog\nfrom remark.i18n import _ as _, set_language\nfrom remark.utils import registry\nfrom remark.utils.path_resolver import find_candidates\nfrom remark.utils.platform import check_platform\nfrom remark.utils.updater import (\n    check_updates_auto,\n    check_updates_manual,\n    create_update_script,\n    download_update,\n    get_executable_path,\n    should_check_update,\n    trigger_update,\n)\n\n\ndef get_version():\n    \"\"\"动态获取版本号\"\"\"\n    try:\n        from importlib.metadata import version\n\n        return version(\"windows-folder-remark\")\n    except Exception:\n        return \"unknown\"\n\n\nclass CLI:\n    \"\"\"命令行接口\"\"\"\n\n    def __init__(self):\n        self.handler = FolderCommentHandler()\n        self.pending_update = None\n        self._update_check_done = threading.Event()\n        # 初始化交互模式命令列表\n        self._interactive_commands_list = [\"#help\", \"#install\", \"#uninstall\", \"#update\"]\n        self._interactive_commands = {\n            \"#help\": self._interactive_help,\n            \"#install\": self.install_menu,\n            \"#uninstall\": self.uninstall_menu,\n            \"#update\": self.check_update_now,\n        }\n        # 先检查缓存，只有在需要检查时才启动后台线程\n        if should_check_update():\n            self._start_update_checker()\n        else:\n            self._update_check_done.set()  # 不需要检查，直接标记完成\n\n    def _validate_folder(self, path: str) -> bool:\n        \"\"\"验证路径是否为文件夹\"\"\"\n        if not os.path.exists(path):\n            print(_(\"Path does not exist: {path}\").format(path=path))\n            return False\n        if not self.handler.supports(path):\n            print(_(\"Path is not a folder: {path}\").format(path=path))\n            return False\n        return True\n\n    def _start_update_checker(self):\n        \"\"\"后台线程检查更新，不阻塞主流程\"\"\"\n        thread = threading.Thread(target=self._run_update_check, daemon=True)\n        thread.start()\n\n    def _run_update_check(self):\n        \"\"\"实际执行更新检查\"\"\"\n        try:\n            self.pending_update = check_updates_auto(get_version())\n        finally:\n            self._update_check_done.set()\n\n    def check_update_now(self) -> bool:\n        \"\"\"强制检查更新（用于 --update 命令，绕过缓存）\n\n        Returns:\n            True 如果有新版本，False 否则\n        \"\"\"\n        print(_(\"Current version: {version}\").format(version=get_version()))\n        print(_(\"Checking for updates...\"))\n\n        update = check_updates_manual(get_version())\n\n        if update:\n            print(_(\"\\nNew version found: {tag_name}\").format(tag_name=update[\"tag_name\"]))\n            print(_(\"Update notes: {notes}\").format(notes=update[\"body\"][:300]))\n            print(_(\"Full changelog: {url}\").format(url=update[\"html_url\"]))\n            response = input(_(\"\\nUpdate now? [Y/n]: \")).lower()\n            if response in (\"\", \"y\", \"yes\"):\n                self._perform_update(update)\n            return True\n        else:\n            print(_(\"Already at the latest version\"))\n            return False\n\n    def _wait_for_update_check(self, timeout: float = 2.0) -> None:\n        \"\"\"等待后台检测完成\n\n        Args:\n            timeout: 超时时间（秒）\n        \"\"\"\n        self._update_check_done.wait(timeout=timeout)\n\n    def _prompt_update(self) -> None:\n        \"\"\"提示用户有新版本可用\"\"\"\n        update = self.pending_update\n        if update is None:\n            return\n        print(\n            _(\"\\nNew version available: {tag_name} (Current version: {version})\").format(\n                tag_name=update[\"tag_name\"], version=get_version()\n            )\n        )\n        print(_(\"Update notes: {notes}\").format(notes=update[\"body\"][:200]))\n        print(_(\"Full changelog: {url}\").format(url=update[\"html_url\"]))\n        response = input(_(\"Update now? [Y/n]: \")).lower()\n        if response in (\"\", \"y\", \"yes\"):\n            self._perform_update(update)\n\n    def _perform_update(self, update: dict) -> None:\n        \"\"\"执行更新流程\"\"\"\n        try:\n            print(_(\"Downloading new version...\"))\n            # 下载到临时目录\n            new_exe = os.path.join(\n                tempfile.gettempdir(), f\"windows-folder-remark-{update['tag_name']}.exe\"\n            )\n            download_update(update[\"download_url\"], new_exe)\n\n            print(_(\"Download complete, preparing update...\"))\n            old_exe = get_executable_path()\n            script_path = create_update_script(old_exe, new_exe)\n\n            print(_(\"Update program has started, the application will exit...\"))\n            print(_(\"Please wait a few moments, the update will complete automatically.\"))\n            trigger_update(script_path)\n            sys.exit(0)\n        except urllib.error.URLError as e:\n            err_msg = str(e)\n            if \"closed connection\" in err_msg.lower() or \"connection reset\" in err_msg.lower():\n                print(_(\"Download failed: Connection reset by server\"))\n                print(_(\"Please try again later, or visit the following link to download manually:\"))\n                print(f\"  {update['html_url']}\")\n            elif \"timeout\" in err_msg.lower():\n                print(_(\"Download failed: Request timeout\"))\n                print(_(\"Please check your network connection, or visit the following link to download manually:\"))\n                print(f\"  {update['html_url']}\")\n            elif \"no route to host\" in err_msg.lower() or \"hostname\" in err_msg.lower():\n                print(_(\"Download failed: Unable to connect to server\"))\n                print(_(\"Please check your network connection, or visit the following link to download manually:\"))\n                print(f\"  {update['html_url']}\")\n            else:\n                print(_(\"Download failed, please check your network or download manually\"))\n                print(f\"  {update['html_url']}\")\n        except Exception as e:\n            print(_(\"Update failed: {error}\").format(error=e))\n            print(_(\"Manual download: {url}\").format(url=update[\"html_url\"]))\n\n    def add_comment(self, path, comment):\n        \"\"\"添加备注\"\"\"\n        if self._validate_folder(path):\n            return self.handler.set_comment(path, comment)\n        return False\n\n    def delete_comment(self, path):\n        \"\"\"删除备注\"\"\"\n        if self._validate_folder(path):\n            return self.handler.delete_comment(path)\n        return False\n\n    def install_menu(self) -> bool:\n        \"\"\"安装右键菜单\"\"\"\n        if registry.install_context_menu():\n            print(_(\"Right-click menu installed successfully\"))\n            print(\"\")\n            print(_(\"Usage Instructions:\"))\n            print(_(\"  Windows 10: Right-click folder to see 'Add Folder Remark'\"))\n            print(_(\"  Windows 11: Right-click folder → Click 'Show more options' → Add Folder Remark\"))\n            return True\n        else:\n            print(_(\"Right-click menu installation failed\"))\n            return False\n\n    def uninstall_menu(self) -> bool:\n        \"\"\"卸载右键菜单\"\"\"\n        if registry.uninstall_context_menu():\n            print(_(\"Right-click menu uninstalled\"))\n            return True\n        else:\n            print(_(\"Right-click menu uninstallation failed\"))\n            return False\n\n    def gui_mode(self, folder_path: str) -> bool:\n        \"\"\"GUI 模式（右键菜单调用）\"\"\"\n        if not self._validate_folder(folder_path):\n            return False\n\n        # 显示对话框\n        comment = remark_dialog.show_remark_dialog(folder_path)\n        if comment:\n            result = self.add_comment(folder_path, comment)\n            return result is not False\n        return False\n\n    def view_comment(self, path: str) -> None:\n        \"\"\"查看备注\"\"\"\n        if self._validate_folder(path):\n            # 检查 desktop.ini 编码\n            from remark.storage.desktop_ini import DesktopIniHandler\n\n            if DesktopIniHandler.exists(path):\n                desktop_ini_path = DesktopIniHandler.get_path(path)\n                detected_encoding, is_utf16 = DesktopIniHandler.detect_encoding(desktop_ini_path)\n                if not is_utf16:\n                    print(\n                        _(\n                            \"Warning: desktop.ini file encoding is {encoding}, not standard UTF-16.\"\n                        ).format(encoding=detected_encoding or _(\"unknown\"))\n                    )\n                    print(_(\"This may cause Chinese and other special characters to display abnormally.\"))\n\n                    # 询问是否修复\n                    while True:\n                        response = input(_(\"Fix encoding to UTF-16? [Y/n]: \")).strip().lower()\n                        if response in (\"\", \"y\", \"yes\"):\n                            if DesktopIniHandler.fix_encoding(desktop_ini_path, detected_encoding):\n                                print(_(\"Fixed to UTF-16 encoding\"))\n                            else:\n                                print(_(\"Failed to fix encoding\"))\n                            break\n                        elif response in (\"n\", \"no\"):\n                            print(_(\"Skip encoding fix\"))\n                            break\n                        else:\n                            print(_(\"Please enter Y or n\"))\n                    print()  # 空行分隔\n\n            comment = self.handler.get_comment(path)\n            if comment:\n                print(_(\"Current remark: {remark}\").format(remark=comment))\n            else:\n                print(_(\"This folder has no remark\"))\n\n    def interactive_mode(self) -> None:\n        \"\"\"交互模式\"\"\"\n        version = get_version()\n        print(_(\"Windows Folder Remark Tool v{version}\").format(version=version))\n        print(_(\"Tip: Press Ctrl + C to exit\"))\n        print(_(\"Tip: Use #help to see available commands\"))\n\n        input_path_msg = \"\\n\" + _(\"Enter folder path (or drag here): \")\n        input_comment_msg = _(\"Enter remark:\")\n\n        while True:\n            try:\n                user_input = input(input_path_msg).replace('\"', \"\").strip()\n\n                # 处理交互命令\n                if user_input in self._interactive_commands:\n                    self._interactive_commands[user_input]()\n                    print()\n                    continue\n\n                # 用户输入单独的 #，显示可用命令列表\n                if user_input == \"#\":\n                    self._show_command_list()\n                    print()\n                    continue\n\n                if not os.path.exists(user_input):\n                    print(_(\"Path does not exist, please re-enter\"))\n                    continue\n\n                if not os.path.isdir(user_input):\n                    print(_(\"This is a 'file', currently only supports adding remarks to 'folders'\"))\n                    continue\n\n                comment = input(input_comment_msg)\n                while not comment:\n                    print(_(\"Remark cannot be empty\"))\n                    comment = input(input_comment_msg)\n\n                self.add_comment(user_input, comment)\n\n            except KeyboardInterrupt:\n                print(\"\\n\" + _(\" ❤ Thank you for using\"))\n                break\n            print(os.linesep + _(\"Continue processing or press Ctrl + C to exit\") + os.linesep)\n\n    def _show_command_list(self) -> None:\n        \"\"\"显示可用命令列表\"\"\"\n        print(_(\"Available commands:\"))\n        for cmd in self._interactive_commands_list:\n            print(f\"  {cmd}\")\n\n    def _interactive_help(self) -> None:\n        \"\"\"显示交互模式帮助信息\"\"\"\n        print(_(\"Interactive Commands:\"))\n        print(_(\"  #help     Show this help message\"))\n        print(_(\"  #install  Install right-click menu\"))\n        print(_(\"  #uninstall Uninstall right-click menu\"))\n        print(_(\"  #update   Check for updates\"))\n        print(os.linesep)\n        print(_(\"Or simply enter a folder path to add remarks\"))\n\n    def show_help(self) -> None:\n        \"\"\"显示帮助信息\"\"\"\n        print(_(\"Windows Folder Remark Tool\"))\n        print(_(\"Usage:\"))\n        print(_(\"  Interactive mode: python remark.py\"))\n        print(_(\"  Command line mode: python remark.py [options] [arguments]\"))\n        print(_(\"Options:\"))\n        print(_(\"  --install          Install right-click menu\"))\n        print(_(\"  --uninstall        Uninstall right-click menu\"))\n        print(_(\"  --update           Check for updates\"))\n        print(_(\"  --gui <path>        GUI mode (called from right-click menu)\"))\n        print(_(\"  --delete <path>     Delete remark\"))\n        print(_(\"  --view <path>       View remark\"))\n        print(_(\"  --help, -h         Show help information\"))\n        print(_(\"Interactive Commands (available in interactive mode):\"))\n        print(_(\"  #help              Show interactive help\"))\n        print(_(\"  #install           Install right-click menu\"))\n        print(_(\"  #uninstall         Uninstall right-click menu\"))\n        print(_(\"  #update            Check for updates\"))\n        print(_(\"Examples:\"))\n        print(_(' [Add remark] python remark.py \"C:\\\\\\\\MyFolder\" \"My Folder\"'))\n        print(_(' [Delete remark] python remark.py --delete \"C:\\\\\\\\MyFolder\"'))\n        print(_(' [View current remark] python remark.py --view \"C:\\\\\\\\MyFolder\"'))\n        print(_(\" [Install right-click menu] python remark.py --install\"))\n        print(_(\" [Check for updates] python remark.py --update\"))\n\n    def _select_from_multiple_candidates(\n        self, candidates: list, show_remaining: bool = False\n    ) -> tuple[str, list[str]] | None:\n        \"\"\"\n        从多个候选路径中选择\n\n        Args:\n            candidates: 候选路径列表，每个元素为 (path, remaining, type)\n            show_remaining: 是否显示剩余参数（备注内容）\n\n        Returns:\n            (path_str, remaining) 或 None 如果用户取消\n        \"\"\"\n\n        # 转换 candidates 中的 Path 对象为字符串\n        str_candidates: list[tuple[str, list[str], str]] = []\n        for path, remaining, path_type in candidates:\n            str_candidates.append((str(path), remaining, path_type))\n\n        print(\"检测到多个可能的路径，请选择:\")\n        for i, (p, r, t) in enumerate(str_candidates, 1):\n            type_mark = \" [文件]\" if t == \"file\" else \"\"\n            print(f\"\\n[{i}] {p}{type_mark}\")\n            if show_remaining and r:\n                print(f\"    剩余备注: {' '.join(r)}\")\n            elif not show_remaining:\n                print(\"    (将查看现有备注)\")\n        print(\"\\n[0] 取消\")\n\n        while True:\n            choice = input(f\"\\n请选择 [0-{len(str_candidates)}]: \").strip()\n            if choice == \"0\":\n                return None\n            if choice.isdigit() and 1 <= int(choice) <= len(str_candidates):\n                path, remaining, path_type = str_candidates[int(choice) - 1]\n                if path_type == \"file\":\n                    print(\"\\n错误: 这是一个文件，工具只能为文件夹设置备注，请重新选择\")\n                    continue\n                return path, remaining\n            print(\"无效选择，请重试\")\n\n    def _handle_ambiguous_path(self, args_list: list[str]) -> tuple[str | None, str | None]:\n        \"\"\"\n        处理模糊路径，返回 (最终路径, 备注内容)\n\n        Args:\n            args_list: 位置参数列表\n\n        Returns:\n            (path, comment) 或 (None, None) 如果用户取消\n        \"\"\"\n\n        candidates = find_candidates(args_list)\n\n        if not candidates:\n            print(_(\"Error: Path does not exist or not quoted\"))\n            print(_(\"Hint: Use quotes when path contains spaces\"))\n            print(_('  windows-folder-remark \"C:\\\\\\\\My Documents\" \"Remark content\"'))\n            return None, None\n\n        if len(candidates) == 1:\n            path, remaining, path_type = candidates[0]\n            print(_(\"Detected path: {path}\").format(path=path))\n\n            if path_type == \"file\":\n                print(_(\"Error: This is a file, the tool can only set remarks for folders\"))\n                return None, None\n\n            if remaining:\n                comment = \" \".join(remaining)\n                print(_(\"Remark content: {remark}\").format(remark=comment))\n            else:\n                print(_(\"(Will view existing remark)\"))\n\n            if input(_(\"Continue? [Y/n]: \")).lower() in (\"\", \"y\", \"yes\"):\n                return str(path), \" \".join(remaining) if remaining else None\n\n            return None, None\n\n        result = self._select_from_multiple_candidates(candidates, show_remaining=True)\n        if result:\n            path_str, remaining = result\n            return path_str, \" \".join(remaining) if remaining else None\n        return None, None\n\n    def _resolve_path_from_ambiguous_args(self, args_list: list[str]) -> str | None:\n        \"\"\"\n        从可能被空格分割的参数列表中解析出有效路径\n\n        适用于 --view, --delete, --gui 等只接收路径的命令。\n\n        Args:\n            args_list: 可能包含路径片段的参数列表\n\n        Returns:\n            解析出的路径字符串，如果无法解析则返回 None\n        \"\"\"\n        candidates = find_candidates(args_list)\n\n        if not candidates:\n            return None\n\n        if len(candidates) == 1:\n            path, remaining, path_type = candidates[0]\n            if path_type == \"folder\":\n                return str(path)\n            else:\n                print(_(\"Error: This is a file, the tool can only set remarks for folders\"))\n                return None\n\n        result = self._select_from_multiple_candidates(candidates, show_remaining=False)\n        if result:\n            return result[0]\n        return None\n\n    def run(self, argv=None) -> None:\n        \"\"\"运行 CLI\"\"\"\n        if not check_platform():\n            sys.exit(1)\n\n        parser = argparse.ArgumentParser(description=\"Windows 文件夹备注工具\", add_help=False)\n        parser.add_argument(\"args\", nargs=\"*\", help=\"位置参数（路径和备注）\")\n        parser.add_argument(\"--install\", action=\"store_true\", help=\"安装右键菜单\")\n        parser.add_argument(\"--uninstall\", action=\"store_true\", help=\"卸载右键菜单\")\n        parser.add_argument(\"--update\", action=\"store_true\", help=\"检查更新\")\n        parser.add_argument(\"--gui\", metavar=\"PATH\", help=\"GUI 模式（右键菜单调用）\")\n        parser.add_argument(\"--delete\", metavar=\"PATH\", help=\"删除备注\")\n        parser.add_argument(\"--view\", metavar=\"PATH\", help=\"查看备注\")\n        parser.add_argument(\"--help\", \"-h\", action=\"store_true\", help=\"显示帮助信息\")\n        parser.add_argument(\"--lang\", \"-L\", metavar=\"LANG\", help=\"设置语言 (en, zh)\", dest=\"lang\")\n\n        args = parser.parse_args(argv)\n\n        # 设置语言\n        if args.lang:\n            set_language(args.lang)\n\n        if args.help:\n            self.show_help()\n        elif args.install:\n            self.install_menu()\n        elif args.uninstall:\n            self.uninstall_menu()\n        elif args.update:\n            self.check_update_now()\n            sys.exit(0)\n        elif args.gui:\n            path = self._resolve_path_from_ambiguous_args([args.gui, *args.args])\n            if path:\n                self.gui_mode(path)\n            else:\n                print(\"错误: 路径不存在或未使用引号\")\n        elif args.delete:\n            path = self._resolve_path_from_ambiguous_args([args.delete, *args.args])\n            if path:\n                self.delete_comment(path)\n            else:\n                print(\"错误: 路径不存在或未使用引号\")\n        elif args.view:\n            path = self._resolve_path_from_ambiguous_args([args.view, *args.args])\n            if path:\n                self.view_comment(path)\n            else:\n                print(\"错误: 路径不存在或未使用引号\")\n        elif args.args:\n            # 处理位置参数\n            path, comment = self._handle_ambiguous_path(args.args)\n            if path:\n                if comment:\n                    self.add_comment(path, comment)\n                else:\n                    self.view_comment(path)\n            else:\n                # 用户取消或解析失败，显示帮助\n                self.show_help()\n        else:\n            # 无参数，进入交互模式\n            self.interactive_mode()\n\n\ndef main() -> None:\n    \"\"\"主入口\"\"\"\n    # 强制设置控制台编码为 UTF-8，支持中文等特殊字符输出\n    # 这对于 Windows 系统特别重要，因为默认控制台编码可能是 GBK\n    if hasattr(sys.stdout, \"reconfigure\"):\n        sys.stdout.reconfigure(encoding=\"utf-8\", errors=\"replace\")\n    if hasattr(sys.stderr, \"reconfigure\"):\n        sys.stderr.reconfigure(encoding=\"utf-8\", errors=\"replace\")\n\n    cli = CLI()\n    try:\n        cli.run()\n    except KeyboardInterrupt:\n        print(_(\"\\nOperation cancelled\"))\n        sys.exit(0)\n    except Exception as e:\n        print(_(\"An error occurred: {error}\").format(error=str(e)))\n        sys.exit(1)\n    finally:\n        # 等待后台检测完成（最多等待 2 秒）\n        cli._wait_for_update_check(timeout=2.0)\n        # 退出前检查更新\n        if cli.pending_update:\n            cli._prompt_update()\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "remark/core/__init__.py",
    "content": "\"\"\"\n核心功能模块\n\"\"\"\n\nfrom remark.core.base import CommentHandler\nfrom remark.core.folder_handler import FolderCommentHandler\n\n__all__ = [\n    \"CommentHandler\",\n    \"FolderCommentHandler\",\n]\n"
  },
  {
    "path": "remark/core/base.py",
    "content": "\"\"\"\n基础接口定义\n\"\"\"\n\nfrom abc import ABC, abstractmethod\n\n\nclass CommentHandler(ABC):\n    \"\"\"备注处理器基类\"\"\"\n\n    @abstractmethod\n    def set_comment(self, path, comment):\n        \"\"\"设置备注\"\"\"\n        pass\n\n    @abstractmethod\n    def get_comment(self, path):\n        \"\"\"获取备注\"\"\"\n        pass\n\n    @abstractmethod\n    def delete_comment(self, path):\n        \"\"\"删除备注\"\"\"\n        pass\n\n    @abstractmethod\n    def supports(self, path):\n        \"\"\"检查是否支持该路径类型\"\"\"\n        pass\n"
  },
  {
    "path": "remark/core/folder_handler.py",
    "content": "\"\"\"\n文件夹备注处理器 - 使用 desktop.ini\n\n使用 Microsoft 官方支持的 desktop.ini 方式设置文件夹备注。\n\n参考文档:\nhttps://learn.microsoft.com/en-us/windows/win32/shell/how-to-customize-folders-with-desktop-ini\n\"\"\"\n\nimport os\n\nfrom remark.core.base import CommentHandler\nfrom remark.i18n import _ as _\nfrom remark.storage.desktop_ini import DesktopIniHandler\nfrom remark.utils.constants import MAX_COMMENT_LENGTH\n\n\nclass FolderCommentHandler(CommentHandler):\n    \"\"\"文件夹备注处理器\"\"\"\n\n    def set_comment(self, folder_path: str, comment: str) -> bool:\n        \"\"\"设置文件夹备注\"\"\"\n        if not os.path.isdir(folder_path):\n            print(_(\"Path is not a folder: {folder_path}\").format(folder_path=folder_path))\n            return False\n\n        if len(comment) > MAX_COMMENT_LENGTH:\n            print(\n                _(\"Remark length exceeds limit, maximum length is {length} characters\").format(\n                    length=MAX_COMMENT_LENGTH\n                )\n            )\n            comment = comment[:MAX_COMMENT_LENGTH]\n\n        return self._set_comment_desktop_ini(folder_path, comment)\n\n    @staticmethod\n    def _set_comment_desktop_ini(folder_path: str, comment: str) -> bool:\n        \"\"\"使用 desktop.ini 设置备注\"\"\"\n        desktop_ini_path = DesktopIniHandler.get_path(folder_path)\n\n        try:\n            # 清除文件属性以便修改\n            if DesktopIniHandler.exists(\n                folder_path\n            ) and not DesktopIniHandler.clear_file_attributes(desktop_ini_path):\n                print(_(\"Failed to clear file attributes\"))\n                return False\n\n            # 使用 UTF-16 编码写入 desktop.ini\n            if not DesktopIniHandler.write_info_tip(folder_path, comment):\n                print(_(\"Failed to write desktop.ini\"))\n                return False\n\n            # 设置 desktop.ini 文件为隐藏和系统属性\n            if not DesktopIniHandler.set_file_hidden_system_attributes(desktop_ini_path):\n                print(_(\"Failed to set file attributes\"))\n                return False\n\n            # 设置文件夹为只读属性（使 desktop.ini 生效）\n            if not DesktopIniHandler.set_folder_system_attributes(folder_path):\n                print(_(\"Failed to set folder attributes\"))\n                return False\n\n            print(\n                _(\"Remark [{remark}] has been set for folder [{folder_path}]\").format(\n                    remark=comment, folder_path=folder_path\n                )\n            )\n            print(_(\"Remark added successfully, may take a few minutes to display\"))\n            return True\n        except Exception as e:\n            print(_(\"Failed to set remark: {error}\").format(error=str(e)))\n            return False\n\n    def get_comment(self, folder_path: str) -> str | None:\n        \"\"\"获取文件夹备注\"\"\"\n        return DesktopIniHandler.read_info_tip(folder_path)\n\n    def delete_comment(self, folder_path: str) -> bool:\n        \"\"\"删除文件夹备注\"\"\"\n        desktop_ini_path = DesktopIniHandler.get_path(folder_path)\n\n        if not DesktopIniHandler.exists(folder_path):\n            print(_(\"This folder has no remark\"))\n            return True\n\n        # 清除文件属性以便修改\n        if not DesktopIniHandler.clear_file_attributes(desktop_ini_path):\n            print(_(\"Failed to clear file attributes\"))\n            return False\n\n        # 移除 InfoTip 行（保留其他设置如 IconResource）\n        if not DesktopIniHandler.remove_info_tip(folder_path):\n            print(_(\"Failed to remove remark\"))\n            return False\n\n        # 如果 desktop.ini 仍存在，恢复文件属性\n        if DesktopIniHandler.exists(\n            folder_path\n        ) and not DesktopIniHandler.set_file_hidden_system_attributes(desktop_ini_path):\n            print(_(\"Failed to restore file attributes\"))\n            return False\n\n        print(_(\"Remark deleted successfully\"))\n        return True\n\n    def supports(self, path: str) -> bool:\n        \"\"\"检查是否支持该路径\"\"\"\n        return os.path.isdir(path)\n"
  },
  {
    "path": "remark/gui/__init__.py",
    "content": "\"\"\"GUI 模块\"\"\"\n"
  },
  {
    "path": "remark/gui/remark_dialog.py",
    "content": "\"\"\"\n备注输入对话框\n\n使用 tkinter 实现的简单 GUI 对话框，用于右键菜单集成。\n\"\"\"\n\nimport tkinter as tk\nfrom tkinter import messagebox, ttk\n\nfrom remark.core.folder_handler import FolderCommentHandler\n\n\ndef show_remark_dialog(folder_path: str) -> str | None:\n    \"\"\"\n    显示备注输入对话框\n\n    界面:\n    - 标题: \"添加文件夹备注\"\n    - 文件夹路径显示 (只读)\n    - 当前备注显示 (如果有)\n    - 备注输入框\n    - 确定/取消按钮\n\n    Args:\n        folder_path: 文件夹完整路径\n\n    Returns:\n        用户输入的备注内容（非空字符串），用户点击取消返回 None\n    \"\"\"\n    root = tk.Tk()\n    root.title(\"添加文件夹备注\")\n\n    # 设置窗口大小和居中\n    window_width = 500\n    window_height = 250\n    screen_width = root.winfo_screenwidth()\n    screen_height = root.winfo_screenheight()\n    x = (screen_width - window_width) // 2\n    y = (screen_height - window_height) // 2\n    root.geometry(f\"{window_width}x{window_height}+{x}+{y}\")\n\n    # 禁止调整窗口大小\n    root.resizable(False, False)\n\n    # 使用 ttk 样式\n    style = ttk.Style()\n    style.theme_use(\"clam\")\n\n    # 结果存储\n    result: dict[str, str | None] = {\"comment\": None}\n\n    # =============================================================================\n    # 界面元素\n    # =============================================================================\n\n    # 主框架\n    main_frame = ttk.Frame(root, padding=\"20\")\n    main_frame.pack(fill=tk.BOTH, expand=True)\n\n    # 文件夹路径标签\n    path_label = ttk.Label(main_frame, text=\"文件夹路径:\")\n    path_label.grid(row=0, column=0, sticky=tk.W, pady=(0, 5))\n\n    path_entry = ttk.Entry(main_frame, width=60)\n    path_entry.insert(0, folder_path)\n    path_entry.configure(state=\"readonly\")\n    path_entry.grid(row=1, column=0, columnspan=2, sticky=tk.EW, pady=(0, 15))\n\n    # 当前备注显示（如果存在）\n    handler = FolderCommentHandler()\n    current_comment = handler.get_comment(folder_path)\n\n    if current_comment:\n        current_label = ttk.Label(main_frame, text=\"当前备注:\")\n        current_label.grid(row=2, column=0, sticky=tk.W, pady=(0, 5))\n\n        current_value = ttk.Entry(main_frame, width=60)\n        current_value.insert(0, current_comment)\n        current_value.configure(state=\"readonly\")\n        current_value.grid(row=3, column=0, columnspan=2, sticky=tk.EW, pady=(0, 15))\n\n        next_row = 4\n    else:\n        next_row = 2\n\n    # 备注输入标签\n    comment_label = ttk.Label(main_frame, text=\"备注内容:\")\n    comment_label.grid(row=next_row, column=0, sticky=tk.W, pady=(0, 5))\n\n    # 备注输入框\n    comment_entry = ttk.Entry(main_frame, width=60)\n    if current_comment:\n        comment_entry.insert(0, current_comment)\n    comment_entry.grid(row=next_row + 1, column=0, columnspan=2, sticky=tk.EW, pady=(0, 20))\n\n    # 按钮框架\n    button_frame = ttk.Frame(main_frame)\n    button_frame.grid(row=next_row + 2, column=0, columnspan=2, sticky=tk.EW)\n\n    # 确定按钮\n    def on_ok():\n        comment = comment_entry.get().strip()\n        if not comment:\n            messagebox.showwarning(\"警告\", \"备注不能为空\")\n            return\n        result[\"comment\"] = comment\n        root.destroy()\n\n    def on_cancel():\n        result[\"comment\"] = None\n        root.destroy()\n\n    ok_button = ttk.Button(button_frame, text=\"确定\", command=on_ok, width=10)\n    ok_button.pack(side=tk.RIGHT, padx=(5, 0))\n\n    # 取消按钮\n    cancel_button = ttk.Button(button_frame, text=\"取消\", command=on_cancel, width=10)\n    cancel_button.pack(side=tk.RIGHT)\n\n    # =============================================================================\n    # 键盘快捷键\n    # =============================================================================\n\n    root.bind(\"<Return>\", lambda e: on_ok())\n    root.bind(\"<Escape>\", lambda e: on_cancel())\n\n    # =============================================================================\n    # 焦点设置\n    # =============================================================================\n\n    comment_entry.focus_set()\n    if current_comment:\n        # 如果有现有备注，全选文本方便修改\n        comment_entry.select_range(0, tk.END)\n\n    # =============================================================================\n    # 运行\n    # =============================================================================\n\n    root.wait_window()\n    return result[\"comment\"]\n"
  },
  {
    "path": "remark/i18n.py",
    "content": "\"\"\"\nInternationalization (i18n) module for Windows Folder Remark tool.\n\nThis module provides translation support using gettext.\n\"\"\"\nfrom __future__ import annotations\n\nimport ctypes\nimport gettext\nimport locale\nimport os\nimport platform\nimport sys\nfrom pathlib import Path\nfrom typing import Final\n\n# 翻译域\nDOMAIN: Final = \"messages\"\n\n# 支持的语言列表\nSUPPORTED_LANGUAGES: Final = (\"en\", \"zh\")\n\n\ndef _get_locale_dir() -> Path:\n    \"\"\"\n    获取翻译文件目录路径.\n\n    支持 PyInstaller 打包环境：\n    - 打包后：sys._MEIPASS/locale（文件解压到临时目录的 locale 子目录）\n    - 开发环境：使用项目根目录下的 locale 目录\n\n    Returns:\n        locale 目录的路径\n    \"\"\"\n    # PyInstaller 打包后的临时目录，文件被解压到 _MEIPASS/locale/\n    if getattr(sys, \"frozen\", False) and hasattr(sys, \"_MEIPASS\"):\n        return Path(sys._MEIPASS) / \"locale\"\n\n    # 开发环境：使用项目根目录\n    return Path(__file__).parent.parent / \"locale\"\n\n\n# 翻译文件目录（运行时计算）\nLOCALE_DIR: Final = _get_locale_dir()\n\n\ndef _get_windows_locale() -> str | None:\n    \"\"\"\n    在 Windows 上使用 Windows API 获取用户默认区域设置名称.\n\n    Returns:\n        区域设置名称（如 'zh-CN', 'en-US'），如果获取失败则返回 None\n    \"\"\"\n    try:\n        # GetUserDefaultLocaleName 返回 locale 名称（如 'zh-CN', 'en-US'）\n        # 缓冲区大小为 LOCALE_NAME_MAX_LENGTH (85)\n        buffer_size: int = 85\n        buffer: ctypes.Array[ctypes.c_wchar] = ctypes.create_unicode_buffer(buffer_size)\n\n        # kernel32.dll 中的 GetUserDefaultLocaleName 函数\n        # 原型: int GetUserDefaultLocaleName(LPWSTR lpLocaleName, int cchLocaleName)\n        kernel32 = ctypes.windll.kernel32\n        kernel32.GetUserDefaultLocaleName.restype = ctypes.c_int\n        kernel32.GetUserDefaultLocaleName.argtypes = [\n            ctypes.POINTER(ctypes.c_wchar),\n            ctypes.c_int,\n        ]\n\n        result: int = kernel32.GetUserDefaultLocaleName(buffer, buffer_size)\n\n        if result > 0:\n            return buffer.value.strip()\n    except (AttributeError, OSError, ValueError):\n        pass\n\n    return None\n\n\ndef get_system_language() -> str:\n    \"\"\"\n    获取系统语言设置.\n\n    优先级顺序：\n    1. 环境变量 LANG（最容易被测试控制）\n    2. Windows 平台的 Windows API\n    3. locale.getlocale()\n    4. 默认返回英文\n\n    Returns:\n        语言代码（如 'en', 'zh'），如果不支持则返回默认的 'en'\n    \"\"\"\n    # 优先从环境变量获取（便于测试控制）\n    lang = os.environ.get(\"LANG\", \"\")\n    if lang:\n        # 提取语言代码（如 zh_CN.UTF-8 -> zh）\n        lang_code = lang.split(\".\")[0].split(\"_\")[0]\n        if lang_code in SUPPORTED_LANGUAGES:\n            return lang_code\n        # 处理完整语言代码（如 zh_CN -> zh）\n        if \"_\" in lang:\n            full_lang = lang.split(\".\")[0]\n            if full_lang in SUPPORTED_LANGUAGES:\n                return full_lang\n\n    # Windows 平台使用 Windows API\n    if platform.system() == \"Windows\":\n        windows_locale = _get_windows_locale()\n        if windows_locale:\n            # Windows locale 格式为 'zh-CN', 'en-US' 等\n            # 提取语言部分（zh-CN -> zh）\n            lang_code = windows_locale.split(\"-\")[0]\n            if lang_code in SUPPORTED_LANGUAGES:\n                return lang_code\n\n    # 尝试从 locale 获取\n    try:\n        loc = locale.getlocale()[0]\n        if loc:\n            # locale 格式可能是 'zh_CN', 'zh-CN', 'Chinese_China' 等\n            normalized = loc.replace(\"-\", \"_\")\n            if normalized in SUPPORTED_LANGUAGES:\n                return normalized\n            # 尝试只取语言部分\n            lang_code = normalized.split(\"_\")[0]\n            if lang_code in SUPPORTED_LANGUAGES:\n                return lang_code\n            # 处理 Windows 特殊格式（如 'Chinese_China' -> 'zh'）\n            if normalized.startswith(\"Chinese\"):\n                return \"zh\"\n    except (ValueError, AttributeError):\n        pass\n\n    # 默认返回英文\n    return \"en\"\n\n\ndef init_translation(language: str | None = None) -> gettext.GNUTranslations:\n    \"\"\"\n    初始化翻译.\n\n    Args:\n        language: 语言代码，如果为 None 则使用系统语言\n\n    Returns:\n        翻译函数\n    \"\"\"\n    if language is None:\n        language = get_system_language()\n\n    # 确保语言受支持\n    if language not in SUPPORTED_LANGUAGES:\n        language = \"en\"\n\n    # 尝试加载翻译\n    try:\n        translator = gettext.translation(\n            domain=DOMAIN,\n            localedir=str(LOCALE_DIR),\n            languages=[language],\n            fallback=True,\n        )\n        return translator\n    except Exception:\n        # 如果加载失败，使用空翻译（返回原字符串）\n        return gettext.NullTranslations()\n\n\n# 全局翻译函数\n_translator: gettext.GNUTranslations | gettext.NullTranslations | None = None\n\n\ndef get_translator() -> gettext.GNUTranslations | gettext.NullTranslations:\n    \"\"\"\n    获取当前翻译器.\n\n    Returns:\n        翻译器实例\n    \"\"\"\n    global _translator\n    if _translator is None:\n        _translator = init_translation()\n    return _translator\n\n\ndef set_language(language: str) -> None:\n    \"\"\"\n    设置当前语言.\n\n    Args:\n        language: 语言代码（如 'en', 'zh_CN'）\n    \"\"\"\n    global _translator\n    _translator = init_translation(language)\n\n\ndef gettext_function(message: str) -> str:\n    \"\"\"\n    翻译函数（用于在代码中标记可翻译字符串）.\n\n    Args:\n        message: 要翻译的字符串\n\n    Returns:\n        翻译后的字符串\n    \"\"\"\n    return get_translator().gettext(message)\n\n\ndef ngettext_function(singular: str, plural: str, n: int) -> str:\n    \"\"\"\n    复数形式翻译函数.\n\n    Args:\n        singular: 单数形式\n        plural: 复数形式\n        n: 数量\n\n    Returns:\n        翻译后的字符串\n    \"\"\"\n    return get_translator().ngettext(singular, plural, n)\n\n\n# 默认导出的翻译函数\n_ = gettext_function\n"
  },
  {
    "path": "remark/storage/__init__.py",
    "content": "\"\"\"\n存储层模块 - 提供统一的存储接口\n\"\"\"\n\nfrom .desktop_ini import DesktopIniHandler, EncodingConversionCanceled\n\n__all__ = [\"DesktopIniHandler\", \"EncodingConversionCanceled\"]\n"
  },
  {
    "path": "remark/storage/desktop_ini.py",
    "content": "\"\"\"\nDesktop.ini 交互层\n\n根据 Microsoft 官方文档要求，desktop.ini 文件必须使用 Unicode 格式\n才能正确存储和显示本地化字符串。\n\n参考文档:\nhttps://learn.microsoft.com/en-us/windows/win32/shell/how-to-customize-folders-with-desktop-ini\n\n引用: \"Make sure the Desktop.ini file that you create is in the Unicode format.\nThis is necessary to store the localized strings that can be displayed to users.\"\n\"\"\"\n\nimport codecs\nimport os\n\nfrom remark.i18n import _ as _\n\n\nclass EncodingConversionCanceled(Exception):  # noqa: N818\n    \"\"\"编码转换被用户取消\"\"\"\n\n    pass\n\n\n# Windows desktop.ini 标准编码格式\n# 使用 'utf-16' 编码，codecs 会自动添加 UTF-16 LE BOM (0xFF 0xFE)\nDESKTOP_INI_ENCODING = \"utf-16\"\n# Windows 行尾符\nLINE_ENDING = \"\\r\\n\"\n\n\nclass DesktopIniHandler:\n    \"\"\"\n    Desktop.ini 处理器\n\n    提供对 desktop.ini 文件的读写操作，确保使用正确的编码格式\n    以支持资源管理器正确显示中文等非 ASCII 字符。\n    \"\"\"\n\n    # desktop.ini 文件名\n    FILENAME = \"desktop.ini\"\n    # ShellClassInfo 段落\n    SECTION_SHELL_CLASS_INFO = \"[.ShellClassInfo]\"\n    # InfoTip 属性\n    PROPERTY_INFOTIP = \"InfoTip\"\n\n    @staticmethod\n    def get_path(folder_path):\n        \"\"\"\n        获取 desktop.ini 文件路径\n\n        Args:\n            folder_path: 文件夹路径\n\n        Returns:\n            desktop.ini 文件的完整路径\n        \"\"\"\n        return os.path.join(folder_path, DesktopIniHandler.FILENAME)\n\n    @staticmethod\n    def exists(folder_path):\n        \"\"\"\n        检查 desktop.ini 是否存在\n\n        Args:\n            folder_path: 文件夹路径\n\n        Returns:\n            bool: desktop.ini 是否存在\n        \"\"\"\n        return os.path.exists(DesktopIniHandler.get_path(folder_path))\n\n    @staticmethod\n    def read_info_tip(folder_path):\n        \"\"\"\n        读取 desktop.ini 中的 InfoTip 值\n\n        使用 UTF-16 编码读取（与写入逻辑一致），支持中文等非 ASCII 字符。\n        如果 UTF-16 读取失败，会尝试其他编码以处理外部程序创建的文件。\n\n        Args:\n            folder_path: 文件夹路径\n\n        Returns:\n            str: InfoTip 值，如果不存在或读取失败返回 None\n        \"\"\"\n        desktop_ini_path = DesktopIniHandler.get_path(folder_path)\n\n        if not os.path.exists(desktop_ini_path):\n            return None\n\n        # 优先使用标准编码 UTF-16（与写入逻辑一致）\n        # 降级编码用于处理外部程序创建的文件\n        encodings = [DESKTOP_INI_ENCODING, \"utf-16-le\", \"utf-8-sig\", \"utf-8\", \"gbk\", \"mbcs\"]\n\n        for encoding in encodings:\n            try:\n                with codecs.open(desktop_ini_path, \"r\", encoding=encoding) as f:\n                    content = f.read()\n\n                # 验证是否是合法的 desktop.ini 结构（必须包含 [.ShellClassInfo]）\n                # 如果不包含，说明编码不对，继续尝试下一个\n                if DesktopIniHandler.SECTION_SHELL_CLASS_INFO not in content:\n                    continue\n\n                # 解析 InfoTip\n                if DesktopIniHandler.PROPERTY_INFOTIP in content:\n                    # 找到 InfoTip= 的位置\n                    start = content.index(DesktopIniHandler.PROPERTY_INFOTIP + \"=\")\n                    start += len(DesktopIniHandler.PROPERTY_INFOTIP + \"=\")\n\n                    # 找到行尾\n                    end = len(content)\n                    for line_ending in [\"\\r\\n\", \"\\n\", \"\\r\"]:\n                        pos = content.find(line_ending, start)\n                        if pos != -1 and pos < end:\n                            end = pos\n                            break\n\n                    value = content[start:end].strip()\n                    if value:\n                        return value\n                # 成功读取且结构正确，但没有 InfoTip\n                return None\n            except (UnicodeDecodeError, UnicodeError):\n                # 当前编码失败，尝试下一个\n                continue\n            except Exception:\n                # 其他错误（文件不存在、权限问题等），直接返回\n                break\n\n        return None\n\n    @staticmethod\n    def write_info_tip(folder_path, info_tip):\n        \"\"\"\n        写入 InfoTip 到 desktop.ini\n\n        使用 UTF-16 编码写入（自动添加 BOM），符合 Microsoft 官方文档要求。\n        这确保中文等非 ASCII 字符在资源管理器中正确显示。\n\n        如果 desktop.ini 已存在且包含其他设置（如 IconResource），会保留这些设置。\n\n        Args:\n            folder_path: 文件夹路径\n            info_tip: 要写入的 InfoTip 值\n\n        Returns:\n            bool: 写入是否成功\n\n        Raises:\n            EncodingConversionCanceled: 用户拒绝编码转换\n        \"\"\"\n        if not info_tip:\n            return False\n\n        desktop_ini_path = DesktopIniHandler.get_path(folder_path)\n\n        try:\n            # 如果文件已存在，读取并更新\n            if os.path.exists(desktop_ini_path):\n                # 确保是 UTF-16 编码（用户拒绝会抛出异常）\n                DesktopIniHandler.ensure_utf16_encoding(desktop_ini_path)\n\n                with codecs.open(desktop_ini_path, \"r\", encoding=DESKTOP_INI_ENCODING) as f:\n                    content = f.read()\n\n                # 检查是否已有 InfoTip\n                lines = content.splitlines()\n                new_lines = []\n                info_tip_updated = False\n\n                for line in lines:\n                    stripped = line.strip()\n                    # 更新现有 InfoTip 行\n                    if stripped.startswith(\n                        DesktopIniHandler.PROPERTY_INFOTIP + \"=\"\n                    ) or stripped.startswith(DesktopIniHandler.PROPERTY_INFOTIP + \" \"):\n                        new_lines.append(DesktopIniHandler.PROPERTY_INFOTIP + \"=\" + info_tip)\n                        info_tip_updated = True\n                    else:\n                        new_lines.append(line)\n\n                # 如果没有 InfoTip，添加它\n                if not info_tip_updated:\n                    # 找到 [.ShellClassInfo] 后插入\n                    inserted = False\n                    for i, line in enumerate(new_lines):\n                        if line.strip().startswith(\"[.ShellClassInfo]\"):\n                            new_lines.insert(\n                                i + 1, DesktopIniHandler.PROPERTY_INFOTIP + \"=\" + info_tip\n                            )\n                            inserted = True\n                            break\n                    if not inserted:\n                        # 没找到 section，添加整个 section\n                        new_lines = [\n                            DesktopIniHandler.SECTION_SHELL_CLASS_INFO,\n                            DesktopIniHandler.PROPERTY_INFOTIP + \"=\" + info_tip,\n                        ]\n\n                new_content = LINE_ENDING.join(new_lines)\n            else:\n                # 新建文件\n                new_content = (\n                    DesktopIniHandler.SECTION_SHELL_CLASS_INFO\n                    + LINE_ENDING\n                    + DesktopIniHandler.PROPERTY_INFOTIP\n                    + \"=\"\n                    + info_tip\n                    + LINE_ENDING\n                )\n\n            # 使用 UTF-16 编码写入\n            with codecs.open(desktop_ini_path, \"w\", encoding=DESKTOP_INI_ENCODING) as f:\n                f.write(new_content)\n\n            return True\n\n        except EncodingConversionCanceled:\n            return False\n        except Exception:\n            return False\n\n    @staticmethod\n    def detect_encoding(file_path):\n        \"\"\"\n        检测文件编码\n\n        Args:\n            file_path: 文件路径\n\n        Returns:\n            tuple: (encoding_name, is_utf16)\n                - encoding_name: 检测到的编码名称\n                - is_utf16: 是否为 UTF-16 编码\n        \"\"\"\n        # 检查 BOM\n        try:\n            with open(file_path, \"rb\") as f:\n                bom = f.read(4)\n\n            if bom[:2] == b\"\\xff\\xfe\":  # UTF-16 LE BOM\n                return \"utf-16-le\", True\n            elif bom[:2] == b\"\\xfe\\xff\":  # UTF-16 BE BOM\n                return \"utf-16-be\", True\n            elif bom[:3] == b\"\\xef\\xbb\\xbf\":  # UTF-8 BOM\n                return \"utf-8-sig\", False\n        except Exception:\n            pass\n\n        # 尝试检测其他编码\n        for encoding in [\"utf-8\", \"gbk\", \"mbcs\"]:\n            try:\n                with codecs.open(file_path, \"r\", encoding=encoding) as f:\n                    f.read()\n                return encoding, False\n            except (UnicodeDecodeError, UnicodeError):\n                continue\n\n        return None, False\n\n    @staticmethod\n    def fix_encoding(file_path, current_encoding):\n        \"\"\"\n        修复文件编码为 UTF-16\n\n        Args:\n            file_path: 文件路径\n            current_encoding: 当前编码名称\n\n        Returns:\n            bool: 修复是否成功\n        \"\"\"\n        try:\n            # 读取当前内容\n            with codecs.open(file_path, \"r\", encoding=current_encoding or \"utf-8\") as f:\n                content = f.read()\n\n            # 写入 UTF-16 编码\n            with codecs.open(file_path, \"w\", encoding=DESKTOP_INI_ENCODING) as f:\n                f.write(content)\n\n            return True\n        except Exception:\n            return False\n\n    @staticmethod\n    def ensure_utf16_encoding(file_path):\n        \"\"\"\n        确保文件是 UTF-16 编码，如果不是则提示用户确认转换\n\n        如果用户拒绝转换，抛出 EncodingConversionCanceled 异常。\n\n        Args:\n            file_path: 文件路径\n\n        Raises:\n            EncodingConversionCanceled: 用户拒绝转换\n        \"\"\"\n        encoding, is_utf16 = DesktopIniHandler.detect_encoding(file_path)\n\n        if is_utf16:\n            return  # 已经是 UTF-16\n\n        # 文件不是 UTF-16，需要用户确认\n        print(\n            _(\"Warning: desktop.ini file encoding is {encoding}, not standard UTF-16.\").format(\n                encoding=encoding or _(\"unknown\")\n            )\n        )\n        print(_(\"This file needs to be converted to UTF-16 encoding before modification.\"))\n        print(_(\"The original content will be preserved, only the encoding format will change.\"))\n\n        try:\n            # 显示文件预览\n            with codecs.open(file_path, \"r\", encoding=encoding or \"utf-8\") as f:\n                content = f.read()\n\n            print(_(\"\\nCurrent file content:\"))\n            print(\"-\" * 40)\n            print(content)\n            print(\"-\" * 40)\n\n            # 用户确认\n            while True:\n                response = input(_(\"\\nConvert to UTF-16 encoding and continue? [Y/n]: \")).strip().lower()\n                if response in (\"\", \"y\", \"yes\"):\n                    break\n                elif response in (\"n\", \"no\"):\n                    print(_(\"Operation cancelled.\"))\n                    raise EncodingConversionCanceled(\"用户拒绝编码转换\")\n                else:\n                    print(_(\"Please enter Y or n\"))\n\n            # 执行转换\n            with codecs.open(file_path, \"w\", encoding=DESKTOP_INI_ENCODING) as f:\n                f.write(content)\n\n            print(_(\"Converted to UTF-16 encoding.\"))\n\n        except EncodingConversionCanceled:\n            raise\n        except Exception as e:\n            print(_(\"Conversion failed: {error}\").format(error=e))\n            print(_(\"Operation cancelled.\"))\n            raise EncodingConversionCanceled(f\"编码转换失败: {e}\") from e\n\n    @staticmethod\n    def remove_info_tip(folder_path):\n        \"\"\"\n        移除 desktop.ini 中的 InfoTip\n\n        只删除 InfoTip 行，保留其他设置（如 IconResource, Logo 等）。\n\n        Args:\n            folder_path: 文件夹路径\n\n        Returns:\n            bool: 操作是否成功\n\n        Raises:\n            EncodingConversionCanceled: 用户拒绝编码转换\n        \"\"\"\n        desktop_ini_path = DesktopIniHandler.get_path(folder_path)\n\n        if not os.path.exists(desktop_ini_path):\n            return True\n\n        try:\n            # 确保文件是 UTF-16 编码\n            DesktopIniHandler.ensure_utf16_encoding(desktop_ini_path)\n\n            # 读取内容（UTF-16）\n            with codecs.open(desktop_ini_path, \"r\", encoding=DESKTOP_INI_ENCODING) as f:\n                content = f.read()\n\n            # 移除 InfoTip 行\n            lines = content.splitlines()\n            new_lines = []\n            for line in lines:\n                # 跳过 InfoTip 行（支持 = 前后有/无空格）\n                stripped = line.strip()\n                if stripped.startswith(\n                    DesktopIniHandler.PROPERTY_INFOTIP + \"=\"\n                ) or stripped.startswith(DesktopIniHandler.PROPERTY_INFOTIP + \" \"):\n                    continue\n                new_lines.append(line)\n\n            # 检查是否还有有效内容\n            has_content = False\n            for line in new_lines:\n                stripped = line.strip()\n                if stripped and not stripped.startswith(\"[.ShellClassInfo]\"):\n                    has_content = True\n                    break\n\n            # 如果没有其他内容，删除文件\n            if not has_content:\n                os.remove(desktop_ini_path)\n                return True\n\n            # 用 UTF-16 写回\n            new_content = LINE_ENDING.join(new_lines)\n            with codecs.open(desktop_ini_path, \"w\", encoding=DESKTOP_INI_ENCODING) as f:\n                f.write(new_content)\n\n            return True\n\n        except EncodingConversionCanceled:\n            return False\n        except Exception:\n            return False\n\n    @staticmethod\n    def delete(folder_path):\n        \"\"\"\n        删除 desktop.ini 文件\n\n        Args:\n            folder_path: 文件夹路径\n\n        Returns:\n            bool: 删除是否成功\n        \"\"\"\n        desktop_ini_path = DesktopIniHandler.get_path(folder_path)\n\n        if not os.path.exists(desktop_ini_path):\n            return True\n\n        try:\n            os.remove(desktop_ini_path)\n            return True\n        except Exception:\n            return False\n\n    @staticmethod\n    def set_folder_system_attributes(folder_path):\n        \"\"\"\n        设置文件夹为只读属性\n\n        根据 Microsoft 文档和社区讨论，文件夹必须设置为只读属性\n        Windows 才会读取 desktop.ini 中的自定义设置。\n\n        参考: \"Apply the read-only attribute for each folder.\n        This will make Explorer process the desktop.ini file for that folder.\"\n        https://superuser.com/questions/1117824/how-to-get-windows-to-read-copied-desktop-ini-file\n\n        Args:\n            folder_path: 文件夹路径\n\n        Returns:\n            bool: 设置是否成功\n        \"\"\"\n        try:\n            import ctypes\n            import subprocess\n\n            # 使用 Windows API 检查文件夹是否已有只读属性\n            FILE_ATTRIBUTE_READONLY = 0x01  # noqa: N806 - Windows API 常量\n            GetFileAttributesW = ctypes.windll.kernel32.GetFileAttributesW  # noqa: N806 - Windows API\n\n            attrs = GetFileAttributesW(folder_path)\n            if attrs == 0xFFFFFFFF:  # INVALID_FILE_ATTRIBUTES\n                return False\n\n            # 如果已有只读属性，无需再次设置\n            if attrs & FILE_ATTRIBUTE_READONLY:\n                return True\n\n            # 设置文件夹为只读属性\n            result = subprocess.call(\n                'attrib +r \"' + folder_path + '\"',\n                shell=True,\n                stdout=subprocess.DEVNULL,  # 抑制输出\n                stderr=subprocess.DEVNULL,\n            )\n            return result == 0\n        except Exception:\n            return False\n\n    @staticmethod\n    def set_file_hidden_system_attributes(file_path):\n        \"\"\"\n        设置 desktop.ini 文件为隐藏和系统属性\n\n        根据 Microsoft 文档，desktop.ini 应该被标记为隐藏和系统文件\n        以防止普通用户看到或修改它。\n\n        Args:\n            file_path: desktop.ini 文件路径\n\n        Returns:\n            bool: 设置是否成功\n        \"\"\"\n        try:\n            import subprocess\n\n            result = subprocess.call('attrib +h +s \"' + file_path + '\"', shell=True)\n            return result == 0\n        except Exception:\n            return False\n\n    @staticmethod\n    def clear_file_attributes(file_path):\n        \"\"\"\n        清除文件的隐藏和系统属性\n\n        在修改 desktop.ini 之前需要调用，以便能够写入文件\n\n        Args:\n            file_path: 文件路径\n\n        Returns:\n            bool: 清除是否成功\n        \"\"\"\n        try:\n            import subprocess\n\n            result = subprocess.call('attrib -s -h \"' + file_path + '\"', shell=True)\n            return result == 0\n        except Exception:\n            return False\n"
  },
  {
    "path": "remark/utils/__init__.py",
    "content": "\"\"\"\n工具模块\n\"\"\"\n\nfrom remark.utils.constants import MAX_COMMENT_LENGTH\nfrom remark.utils.platform import check_platform\n\n__all__ = [\n    \"MAX_COMMENT_LENGTH\",\n    \"check_platform\",\n]\n"
  },
  {
    "path": "remark/utils/constants.py",
    "content": "\"\"\"\n常量定义\n\"\"\"\n\nMAX_COMMENT_LENGTH = 260\n\n# GitHub 仓库配置\nGITHUB_REPO = \"piratf/windows-folder-remark\"\nGITHUB_API_RELEASES = \"https://api.github.com/repos/piratf/windows-folder-remark/releases/latest\"\n\n# 更新配置\nUPDATE_CHECK_INTERVAL = 86400  # 检查间隔（秒），默认 24 小时\nUPDATE_CACHE_FILE = \"update_check_cache.txt\"  # 缓存下次检查时间\n"
  },
  {
    "path": "remark/utils/encoding.py",
    "content": "\"\"\"\n编码处理工具\n\"\"\"\n"
  },
  {
    "path": "remark/utils/path_resolver.py",
    "content": "\"\"\"\n路径解析模块\n\n处理未加引号的含空格路径，智能重建完整路径。\n\"\"\"\n\nimport posixpath\nimport re\nfrom collections import deque\nfrom copy import deepcopy\nfrom dataclasses import dataclass\nfrom enum import Enum\nfrom pathlib import Path, PureWindowsPath\n\n\nclass NextResult(Enum):\n    \"\"\"Cursor.next() 的返回类型枚举\"\"\"\n\n    SEPARATOR = \"separator\"  # 找到路径分隔符\n    END_OF_ARG = \"end_of_arg\"  # 找到参数末尾\n\n\n@dataclass\nclass Cursor:\n    \"\"\"\n    路径解析游标，跟踪当前解析位置\n\n    注意：Cursor 在 posix 格式分隔符 (/) 上工作，因为 normalized_args 使用 posix 格式\n\n    Attributes:\n        arg_index: 当前指向第几个参数（从 0 开始）\n        char_index: 当前指向参数中第几个字符（路径归一化后的参数为准，从 0 开始）\n    \"\"\"\n\n    arg_index: int\n    char_index: int\n\n    def jump_to_last_separator(self, normalized_args: list[str]) -> None:\n        \"\"\"\n        跳转到当前参数的最后一个系统分隔符位置\n        如果找不到，则留在当前位置 (后面没有分隔符)\n\n        :param normalized_args: 归一化后的参数列表\n        \"\"\"\n        norm_path = normalized_args[self.arg_index]\n        last_sep = norm_path.rfind(posixpath.sep)\n        if last_sep >= 0:\n            self.char_index = last_sep\n        else:\n            self.char_index = -1\n\n    def next(self, normalized_args: list[str]) -> tuple[\"Cursor\", NextResult] | None:\n        \"\"\"\n        从当前位置向后查找，找到下一个路径分隔符或参数末尾\n\n        搜索从 char_index + 1 开始（跳过当前位置），找到下一个分隔符或参数末尾。\n        找到分隔符时，end cursor 停在分隔符上（与 jump_to_last_separator 保持一致）。\n\n        :param normalized_args: 归一化后的参数列表\n        :return: (新 cursor, NextResult) 或 None（如果无法继续）\n        \"\"\"\n        new_cursor = Cursor(self.arg_index, self.char_index)\n\n        while new_cursor.arg_index < len(normalized_args):\n            current_arg = normalized_args[new_cursor.arg_index]\n            arg_len = len(current_arg)\n\n            # 如果当前位置已在参数末尾，跳到下一个参数开头\n            if new_cursor.char_index >= arg_len:\n                new_cursor.arg_index += 1\n                new_cursor.char_index = 0\n                continue\n\n            # 从 char_index + 1 开始查找分隔符（跳过当前位置）\n            search_start = new_cursor.char_index + 1\n            sep_pos = current_arg.find(posixpath.sep, search_start)\n\n            if sep_pos >= 0:\n                # 找到分隔符，新 cursor 停在分隔符上\n                new_cursor.char_index = sep_pos\n                return new_cursor, NextResult.SEPARATOR\n\n            # 没有找到分隔符，跳到当前参数末尾\n            new_cursor.char_index = arg_len\n            return new_cursor, NextResult.END_OF_ARG\n\n        # 已经到达最后一个参数的末尾，无法继续\n        return None\n\n\ndef get_between(begin: Cursor, end: Cursor, normalized_args: list[str]) -> list[str]:\n    \"\"\"\n    获取两个 cursor 之间的内容，可能跨多个参数\n\n    :param begin: 起始 cursor（不包含）\n    :param end: 结束 cursor（不包含）\n    :param normalized_args: 归一化后的参数列表\n    :return: 字符串片段列表\n    \"\"\"\n    if begin.arg_index == end.arg_index:\n        # 同一个参数内，从 begin.char_index + 1 开始（不包含 begin 位置）\n        arg = normalized_args[begin.arg_index]\n        return [arg[begin.char_index + 1 : end.char_index]]\n\n    # 跨多个参数\n    result = []\n\n    # 第一个参数的部分，从 begin.char_index + 1 开始（不包含 begin 位置）\n    first_arg = normalized_args[begin.arg_index]\n    result.append(first_arg[begin.char_index + 1 :])\n\n    # 中间的完整参数\n    for i in range(begin.arg_index + 1, end.arg_index):\n        result.append(normalized_args[i])\n\n    # 最后一个参数的部分\n    if end.arg_index < len(normalized_args):\n        last_arg = normalized_args[end.arg_index]\n        result.append(last_arg[: end.char_index])\n\n    return result\n\n\ndef build_pattern(parts: list[str]) -> re.Pattern:\n    r\"\"\"\n    将多个字符串片段构建为宽容搜索的正则表达式\n\n    由于终端用空格分割参数，用户输入的 \"My Folder\" 会被分割成 [\"My\", \"Folder\"]。\n    此函数构建一个宽容的正则表达式来匹配可能的文件名。\n\n    宽容规则：\n    - 片段之间允许任意空白字符（用 \\s+ 连接）\n    - 转义所有正则表达式特殊字符，用户输入不含正则表达式\n    - 使用 re.IGNORECASE 忽略大小写（Windows 文件系统不区分大小写）\n\n    :param parts: 字符串片段列表\n    :return: 编译后的正则表达式模式\n    \"\"\"\n    if not parts:\n        return re.compile(r\"\")\n\n    # 转义每个片段中的正则表达式特殊字符\n    escaped_parts = [re.escape(part) for part in parts]\n\n    # 用 \\s+ 连接片段，允许片段之间有任意空白\n    pattern = r\"\\s+\".join(escaped_parts)\n\n    # 首尾精确匹配\n    pattern = r\"^\" + pattern + r\"$\"\n\n    # 忽略大小写匹配\n    return re.compile(pattern, re.IGNORECASE)\n\n\ndef get_current_working_path(\n    first_arg: str, cursor: Cursor | None = None, normalized_args: list[str] | None = None\n) -> tuple[PureWindowsPath, Cursor]:\n    \"\"\"\n    从第一个参数中提取工作目录和剩余内容\n\n    规则：\n    - 使用 PureWindowsPath 规范化路径并获取父目录\n    - 空字符串 → (\".\", \"\")\n\n    :param first_arg: 一个路径\n    :param cursor: 游标，如果为 None 则初始化为 (0, 0)\n    :param normalized_args: 归一化后的参数列表，如果为 None 则使用 first_arg 初始化\n    :return: (工作目录, Cursor)\n    \"\"\"\n    # 如果 cursor 为 None，初始化为 (0, 0)\n    if cursor is None:\n        cursor = Cursor(0, 0)\n\n    # 如果 normalized_args 为 None，归一化 first_arg\n    if normalized_args is None:\n        normalized_args = [PureWindowsPath(first_arg).as_posix()]\n\n    # 空字符串处理\n    if not first_arg:\n        return PureWindowsPath(), cursor\n\n    # 使用 pathlib 获取父目录\n    path_obj = PureWindowsPath(first_arg)\n    parent = path_obj.parent\n\n    # 跳转到最后一个分隔符位置，因为 parent 可能是 \".\" 等特殊情况，根据最后一个分隔符判断是安全的\n    cursor.jump_to_last_separator(normalized_args)\n    return parent if parent else PureWindowsPath(), cursor\n\n\ndef get_inner_items_list(current_working_path: Path) -> list[Path]:\n    \"\"\"\n    获取指定路径下的所有文件和文件夹列表\n\n    :param current_working_path: 当前工作目录路径\n    :return: 文件和文件夹名称列表，如果路径不存在或不是目录则返回空列表\n    \"\"\"\n    return list(current_working_path.iterdir())\n\n\ndef find_candidates(\n    args_list: list[str],\n) -> list[tuple[Path, list[str], str]]:\n    \"\"\"\n    递归查找所有可能的路径重建候选\n\n    返回所有候选，按优先级排序（消耗更多 args 的优先）\n\n    Args:\n        args_list: argparse 解析后的位置参数列表\n                   例如: [\"C:\\\\Program\", \"Files\", \"App\"] 或 [\"My\", \"Folder/App\", \"备注\"]\n\n    Returns:\n        List[Tuple[full_path, remaining_args, type]]: 所有候选\n        - full_path: 完整路径\n        - remaining_args: 剩余参数（作为备注内容）\n        - type: \"folder\" 或 \"file\"\n\n    \"\"\"\n    if not args_list:\n        return []\n\n    # 归一化所有参数\n    normalized_args = [PureWindowsPath(arg).as_posix() for arg in args_list]\n\n    # 构建游标\n    cursor = Cursor(0, 0)\n\n    # 根据第一个参数，判断当前工作目录\n    current_working_path, cursor = get_current_working_path(args_list[0], cursor, normalized_args)\n\n    # 处理剩余内容\n    # 接下来是一个经典的 BFS 搜索问题，我们使用队列来保存当前工作目录和游标作为搜索起点（值拷贝）\n    # - 如果队列为空，结束搜索\n    # 获取当前队头的工作目录对应的文件列表\n    #   - 如果为空，则工作目录加入候选，弹出队列\n    #   - 否则继续\n    # 接下来使用三指针策略，一个新的 next_ 指针沿着当前 cursor 向后找，一个 last_ 保存上一次找到的位置，一个 start_ 保存起始位置，每次\n    #   - 找到下一个路径分隔符\n    #   - 或者找到下一个参数末尾\n    # Cursor 应当提供一个 next 接口，返回新的指针和以上两种类型之一，但是不要修改当前 cursor 的值\n    # Cursor 应当提供一个 get_between 接口，返回两个指针之间的全部字符串内容(可能跨多个参数，因此可能有多个字符串)\n    # 当前模块应当提供一个把多个字符串组合成一个正则表达式的函数\n    # 如果找到的是路径分隔符\n    #   - 将当前找到的内容()**放到一个正则表达式**中(抽象为一个函数)，然后在文件列表中搜索匹配\n    #       - 如果匹配成功\n    #           - 将成功的一个或多个匹配作为新的工作目录，带着新 cursor 值，进行深拷贝并加入候选项\n    #       - 如果没有匹配成功\n    #           - 结束搜索，返回当前可选项\n    #   - 弹出当前工作目录\n    # 如果找到的是参数末尾\n    #   - 将当前找到的内容**放到一个正则表达式**中，然后在文件列表中搜索匹配\n    #   - 如果匹配成功\n    #       - 将成功的一个或多个匹配作为新的工作目录，带着新 cursor 值，进行深拷贝并加入候选项\n    #   - 如果没有匹配成功\n    #       - 当前 cursor 不变，队列也不变，新 Cursor 继续向后找\n    #   - 无论匹配是否成功，当前工作目录都不变\n    # 如果没有下一个参数，新 Cursor 无法前进\n    #   - 弹出队列中的当前工作目录\n\n    candidates: list[tuple[Path, list[str], str]] = []\n    # 队列元素: (current_working_path, cursor)\n    queue: deque[tuple[Path, Cursor, Cursor]] = deque()\n    # working_path, start, last\n    queue.append((Path(current_working_path), deepcopy(cursor), deepcopy(cursor)))\n\n    while queue:\n        work_path, start_cursor, cur = queue.popleft()\n        if not work_path.is_dir():\n            continue\n\n        # 尝试从当前 cursor 向后推进\n        next_result = cur.next(normalized_args)\n        if next_result is None:\n            # 无法继续，弹出队列中的当前工作目录（已处理）\n            continue\n\n        next_cursor, result_type = next_result\n\n        # 获取当前 cursor 和 next_cursor 之间的内容\n        parts = get_between(start_cursor, next_cursor, normalized_args)\n\n        # 构建正则表达式\n        pattern = build_pattern(parts)\n\n        # 获取当前工作目录的文件列表\n        inner_items = get_inner_items_list(work_path)\n\n        if not inner_items:\n            # 工作目录为空，说明某个 A\\\\B 的路径不正确 (A 是空目录)\n            # 搜索失败\n            continue\n\n        # 在文件列表中搜索匹配\n        matches = [item.name for item in inner_items if pattern.search(item.name)]\n\n        if result_type == NextResult.SEPARATOR:\n            # 找到分隔符\n            if matches:\n                # 匹配成功，将匹配项作为新的工作目录加入队列\n                # 需要将 cursor 推进到分隔符之后\n                for match in matches:\n                    new_work_path = work_path / match\n                    queue.append((new_work_path, next_cursor, next_cursor))\n            else:\n                # 匹配失败，结束搜索，返回当前候选\n                break\n\n        elif result_type == NextResult.END_OF_ARG:\n            # 找到参数末尾\n            if matches:\n                # 匹配成功，将匹配项加入候选\n                for match in matches:\n                    full_path = work_path / match\n                    is_folder = full_path.is_dir()\n                    entry_type = \"folder\" if is_folder else \"file\"\n                    remaining = get_remaining_args(next_cursor, normalized_args)\n                    candidates.append((full_path, remaining, entry_type))\n\n            # 无论匹配是否成功，当前工作目录不变，继续尝试向前推进\n            # 将当前工作目录和 next_cursor 重新加入队列\n            queue.append((work_path, start_cursor, next_cursor))\n\n    def _candidate_key(item: tuple[Path, list[str], str]) -> tuple[bool, int]:\n        \"\"\"候选排序键函数：folder 优先，路径越长越优先\"\"\"\n        return (\n            item[2] != \"folder\",  # folder 优先\n            -len(str(item[0])),  # 路径越长越优先\n        )\n\n    # 匹配到的路径越长越优先（消耗的参数越多，剩余参数越少）\n    candidates.sort(key=_candidate_key)\n\n    return candidates if candidates else []\n\n\ndef get_remaining_args(cursor: Cursor, normalized_args: list[str]) -> list[str]:\n    \"\"\"\n    获取 cursor 之后的所有剩余参数，作为备注内容\n\n    :param cursor: 当前游标\n    :param normalized_args: 归一化后的参数列表\n    :return: 剩余参数拼接的字符串\n    \"\"\"\n    remaining = []\n\n    # 当前参数的剩余部分\n    if cursor.arg_index < len(normalized_args):\n        current_arg = normalized_args[cursor.arg_index]\n        if cursor.char_index < len(current_arg):\n            remaining.append(current_arg[cursor.char_index :])\n\n    # 后续完整参数\n    for i in range(cursor.arg_index + 1, len(normalized_args)):\n        remaining.append(normalized_args[i])\n\n    return remaining\n"
  },
  {
    "path": "remark/utils/platform.py",
    "content": "\"\"\"\n平台检查工具\n\"\"\"\n\nimport platform\n\nfrom remark.i18n import _ as _\n\n\ndef check_platform() -> bool:\n    \"\"\"检查是否为 Windows 系统\"\"\"\n    if platform.system() != \"Windows\":\n        print(_(\"Error: This tool adds remarks to files/folders on Windows, other systems are not supported.\"))\n        print(_(\"Current system: {system}\").format(system=platform.system()))\n        return False\n    return True\n"
  },
  {
    "path": "remark/utils/registry.py",
    "content": "\"\"\"\nWindows 注册表操作工具\n\n用于安装/卸载右键菜单到 Windows 资源管理器。\n\"\"\"\n\nimport contextlib\nimport os\nimport sys\nimport winreg\n\n# =============================================================================\n# 常量定义\n# =============================================================================\n\nREGISTRY_ROOT = winreg.HKEY_CURRENT_USER\nREGISTRY_PATH = r\"Software\\Classes\\Directory\\shell\\WindowsFolderRemark\"\nMENU_NAME = \"添加文件夹备注\"\nICON_INDEX = 0\n\n\n# =============================================================================\n# 工具函数\n# =============================================================================\n\n\ndef get_executable_path() -> str:\n    \"\"\"\n    获取可执行文件路径\n\n    Returns:\n        开发环境: 返回 python 脚本路径（用于测试）\n        打包后: 返回 exe 文件完整路径\n    \"\"\"\n    if getattr(sys, \"frozen\", False):\n        # PyInstaller 打包后，sys.executable 指向 exe 文件\n        return sys.executable\n    else:\n        # 开发环境，从 __file__ 推导脚本路径\n        current_file = os.path.abspath(__file__)\n        # remark/utils/registry.py -> remark.py\n        script_path = os.path.join(os.path.dirname(os.path.dirname(current_file)), \"remark.py\")\n        return os.path.abspath(script_path)\n\n\ndef install_context_menu() -> bool:\n    r\"\"\"\n    安装右键菜单到注册表\n\n    创建的注册表结构:\n    HKCU\\Software\\Classes\\Directory\\shell\\WindowsFolderRemark\n        @=\"添加文件夹备注\"\n        Icon=\"[exe_path],0\"\n        \\command\n            @=\"[exe_path] --gui \"%1\"\"\n\n    Returns:\n        成功返回 True，失败返回 False\n    \"\"\"\n    exe_path = get_executable_path()\n\n    try:\n        # 创建主键\n        key = winreg.CreateKey(REGISTRY_ROOT, REGISTRY_PATH)\n\n        # 设置默认值（菜单显示文本）\n        winreg.SetValueEx(key, \"\", 0, winreg.REG_SZ, MENU_NAME)\n\n        # 设置图标（使用 exe 文件的第一个图标）\n        icon_value = f'\"{exe_path}\",{ICON_INDEX}'\n        winreg.SetValueEx(key, \"Icon\", 0, winreg.REG_SZ, icon_value)\n\n        winreg.CloseKey(key)\n\n        # 创建 command 子键\n        command_path = f\"{REGISTRY_PATH}\\\\command\"\n        command_key = winreg.CreateKey(REGISTRY_ROOT, command_path)\n\n        # 设置命令\n        command_value = f'\"{exe_path}\" --gui \"%1\"'\n        winreg.SetValueEx(command_key, \"\", 0, winreg.REG_SZ, command_value)\n\n        winreg.CloseKey(command_key)\n\n        return True\n\n    except PermissionError:\n        return False\n    except OSError:\n        return False\n\n\ndef uninstall_context_menu() -> bool:\n    \"\"\"\n    从注册表卸载右键菜单\n\n    Returns:\n        成功返回 True（包括键不存在的情况），失败返回 False\n    \"\"\"\n    try:\n        # 先删除 command 子键\n        command_path = f\"{REGISTRY_PATH}\\\\command\"\n        with contextlib.suppress(FileNotFoundError):\n            winreg.DeleteKey(REGISTRY_ROOT, command_path)\n\n        # 删除主键\n        with contextlib.suppress(FileNotFoundError):\n            winreg.DeleteKey(REGISTRY_ROOT, REGISTRY_PATH)\n\n        return True\n\n    except PermissionError:\n        return False\n    except OSError:\n        return False\n"
  },
  {
    "path": "remark/utils/updater.py",
    "content": "\"\"\"\n自动更新模块\n\n提供版本检测、下载更新、创建更新脚本等功能。\n\"\"\"\n\nimport json\nimport os\nimport sys\nimport tempfile\nimport urllib.error\nimport urllib.request\nfrom typing import Any\n\nfrom packaging import version\n\nfrom remark.utils.constants import (\n    GITHUB_API_RELEASES,\n    UPDATE_CACHE_FILE,\n    UPDATE_CHECK_INTERVAL,\n)\n\n\ndef _get_proxies() -> dict[str, str] | None:\n    \"\"\"从环境变量获取代理配置\"\"\"\n    proxies = []\n    http_proxy = os.environ.get(\"HTTP_PROXY\") or os.environ.get(\"http_proxy\")\n    https_proxy = os.environ.get(\"HTTPS_PROXY\") or os.environ.get(\"https_proxy\")\n\n    if http_proxy:\n        proxies.append((\"http\", http_proxy))\n    if https_proxy:\n        proxies.append((\"https\", https_proxy))\n\n    return dict(proxies) if proxies else None\n\n\ndef _create_opener():\n    \"\"\"创建带代理的 URL opener\"\"\"\n    proxies = _get_proxies()\n    if proxies:\n        proxy_handler = urllib.request.ProxyHandler(proxies)\n        return urllib.request.build_opener(proxy_handler)\n    return urllib.request.build_opener()\n\n\ndef _get_cache_file_path() -> str:\n    \"\"\"获取缓存文件的完整路径（放在临时目录）\"\"\"\n    return os.path.join(tempfile.gettempdir(), UPDATE_CACHE_FILE)\n\n\ndef get_executable_path() -> str:\n    \"\"\"获取当前可执行文件路径\"\"\"\n    if getattr(sys, \"frozen\", False):\n        # PyInstaller 打包后的 exe\n        return sys.executable\n    else:\n        # 开发环境下的 Python 脚本\n        return os.path.abspath(__file__)\n\n\ndef get_latest_release() -> dict[str, Any] | None:\n    \"\"\"\n    从 GitHub API 获取最新 release 信息\n    使用 urllib 而不是 requests 是为了减少打包体积，减轻用户下载负担\n\n    Returns:\n        包含 tag_name, html_url, body, download_url 的字典，如果获取失败则返回 None\n    \"\"\"\n    try:\n        request = urllib.request.Request(\n            GITHUB_API_RELEASES,\n            headers={\n                \"Accept\": \"application/vnd.github.v3+json\",\n                \"User-Agent\": \"windows-folder-remark\",\n            },\n        )\n        opener = _create_opener()\n        with opener.open(request, timeout=10) as response:\n            if response.status != 200:\n                return None\n            data = json.load(response)\n\n        # 过滤掉 prerelease 和 draft\n        if data.get(\"prerelease\") or data.get(\"draft\"):\n            return None\n\n        # 查找 windows-folder-remark-*.exe 文件\n        download_url = None\n        for asset in data.get(\"assets\", []):\n            name = asset.get(\"name\", \"\")\n            if name.startswith(\"windows-folder-remark-\") and name.endswith(\".exe\"):\n                download_url = asset.get(\"browser_download_url\")\n                break\n\n        if not download_url:\n            return None\n\n        return {\n            \"tag_name\": data.get(\"tag_name\", \"\").lstrip(\"v\"),\n            \"html_url\": data.get(\"html_url\", \"\"),\n            \"body\": data.get(\"body\", \"\"),\n            \"download_url\": download_url,\n        }\n    except (urllib.error.HTTPError, urllib.error.URLError, json.JSONDecodeError, OSError):\n        return None\n\n\ndef should_check_update() -> bool:\n    \"\"\"\n    检查是否应该进行更新检查\n\n    Returns:\n        True 表示应该检查，False 表示还没到检查时间\n    \"\"\"\n    cache_file = _get_cache_file_path()\n    if not os.path.exists(cache_file):\n        return True  # 没有记录，进行第一次检查\n\n    try:\n        import time\n\n        with open(cache_file, encoding=\"utf-8\") as f:\n            next_check_time = float(f.read().strip())\n        return time.time() >= next_check_time\n    except (ValueError, OSError):\n        return True\n\n\ndef update_next_check_time() -> None:\n    \"\"\"更新下次检查时间为当前时间 + 24小时\"\"\"\n    cache_file = _get_cache_file_path()\n    try:\n        import time\n\n        next_check = time.time() + UPDATE_CHECK_INTERVAL\n        with open(cache_file, \"w\", encoding=\"utf-8\") as f:\n            f.write(str(next_check))\n    except OSError:\n        pass\n\n\ndef check_updates_auto(current_version: str) -> dict[str, Any] | None:\n    \"\"\"\n    自动检查更新（CLI 启动时调用，尊重缓存）\n\n    此函数用于后台自动检查更新，会先检查缓存文件，只有在需要时才访问网络。\n    检查成功后会更新缓存文件的下次检查时间。\n\n    Args:\n        current_version: 当前版本号\n\n    Returns:\n        最新 release 信息字典，如果没有新版本则返回 None\n    \"\"\"\n    if not should_check_update():\n        return None\n\n    # 决定检查，立即更新下次检查时间\n    update_next_check_time()\n\n    latest = get_latest_release()\n    if not latest:\n        return None\n\n    try:\n        if version.parse(latest[\"tag_name\"]) > version.parse(current_version):\n            return latest\n    except version.InvalidVersion:\n        return None\n\n    return None\n\n\ndef check_updates_manual(current_version: str) -> dict[str, Any] | None:\n    \"\"\"\n    手动检查更新（--update 命令调用，绕过缓存）\n\n    此函数用于用户手动触发更新检查，会直接访问 GitHub API 获取最新版本信息，\n    不受缓存文件影响。\n\n    Args:\n        current_version: 当前版本号\n\n    Returns:\n        最新 release 信息字典，如果没有新版本则返回 None\n    \"\"\"\n    latest = get_latest_release()\n    if not latest:\n        return None\n\n    try:\n        if version.parse(latest[\"tag_name\"]) > version.parse(current_version):\n            return latest\n    except version.InvalidVersion:\n        return None\n\n    return None\n\n\ndef download_update(url: str, dest: str) -> str:\n    \"\"\"\n    下载新版本 exe\n\n    Args:\n        url: 下载 URL\n        dest: 目标路径（文件名）\n\n    Returns:\n        下载的文件路径\n    \"\"\"\n    request = urllib.request.Request(\n        url,\n        headers={\"User-Agent\": \"windows-folder-remark\"},\n    )\n    opener = _create_opener()\n\n    with opener.open(request, timeout=30) as response:\n        total_size = int(response.headers.get(\"content-length\", 0))\n        downloaded = 0\n        chunk_size = 8192\n\n        with open(dest, \"wb\") as f:\n            while True:\n                chunk = response.read(chunk_size)\n                if not chunk:\n                    break\n                f.write(chunk)\n                downloaded += len(chunk)\n\n                # 显示进度\n                if total_size > 0:\n                    percent = min(downloaded * 100 / total_size, 100)\n                    downloaded_mb = downloaded / 1024 / 1024\n                    total_mb = total_size / 1024 / 1024\n                    print(\n                        f\"\\r下载进度: {percent:.1f}% ({downloaded_mb:.1f}MB / {total_mb:.1f}MB)\",\n                        end=\"\",\n                    )\n        print()  # 换行\n\n    return dest\n\n\ndef create_update_script(old_exe: str, new_exe: str) -> str:\n    \"\"\"\n    创建更新批处理脚本\n\n    Args:\n        old_exe: 旧 exe 路径\n        new_exe: 新 exe 路径\n\n    Returns:\n        批处理脚本路径\n    \"\"\"\n    script_content = f\"\"\"@echo off\nREM 等待主进程退出\ntimeout /t 3 /nobreak >nul\n\nREM 替换 exe\nmove /Y \"{new_exe}\" \"{old_exe}\"\n\nREM 删除自己\ndel \"%~f0\"\n\nREM 更新完成\necho 更新完成！请手动启动新版本程序。\npause\n\"\"\"\n\n    # 创建临时脚本文件\n    temp_dir = tempfile.gettempdir()\n    script_path = os.path.join(temp_dir, \"update_windows_folder_remark.bat\")\n\n    with open(script_path, \"w\", encoding=\"gbk\") as f:\n        f.write(script_content)\n\n    return script_path\n\n\ndef trigger_update(script_path: str) -> None:\n    \"\"\"\n    触发更新流程：启动批处理脚本并退出程序\n\n    Args:\n        script_path: 批处理脚本路径\n    \"\"\"\n    import subprocess\n\n    # 启动批处理脚本（新窗口，不阻塞）\n    subprocess.Popen(\n        [\"cmd.exe\", \"/c\", script_path],\n        shell=True,\n        creationflags=subprocess.CREATE_NEW_CONSOLE,\n    )\n"
  },
  {
    "path": "remark.py",
    "content": "\"\"\"\nWindows 文件/文件夹备注工具 - 主入口\n\"\"\"\n\nfrom remark.cli.commands import main\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "remark.spec",
    "content": "# -*- mode: python ; coding: utf-8 -*-\n\"\"\"\nPyInstaller spec file for windows-folder-remark\n\nUsage:\n    pyinstaller remark.spec\n\"\"\"\n\nimport os\nimport re\nimport sys\nimport subprocess\n\nfrom PyInstaller.utils.hooks import collect_submodules\n\nif sys.platform == \"win32\":\n    sys.stdout.reconfigure(encoding=\"utf-8\")\n    sys.stderr.reconfigure(encoding=\"utf-8\")\n\n\ndef get_upx_dir():\n    local_upx_dir = os.path.join(SPECPATH, \"tools\", \"upx\")\n    upx_exe = os.path.join(local_upx_dir, \"upx.exe\")\n\n    if os.path.exists(upx_exe):\n        return local_upx_dir\n\n    print(\"WARNING: UPX not available, compression disabled (exe will be larger)\")\n    print(\"HINT: Run 'python scripts/ensure_upx.py' to install UPX automatically\")\n    return None\n\n# =============================================================================\n# Configuration\n# =============================================================================\n\nblock_cipher = None\napp_name = \"windows-folder-remark\"\n\n\ndef get_app_version():\n    \"\"\"从 pyproject.toml 的 [project] 部分读取版本号，确保版本同步\"\"\"\n    # SPECPATH 是 PyInstaller 提供的内置全局变量，指向 spec 文件所在目录\n    toml_file = os.path.join(SPECPATH, \"pyproject.toml\")\n    with open(toml_file, encoding=\"utf-8\") as f:\n        lines = f.readlines()\n\n    # 只查找 [project] 部分的 version 字段\n    in_project_section = False\n    for line in lines:\n        if line.strip() == \"[project]\":\n            in_project_section = True\n        elif line.startswith(\"[\") and not line.startswith(\"[[\"):\n            in_project_section = False\n        elif in_project_section and line.strip().startswith(\"version\"):\n            match = re.search(r'=\\s*[\"\\']([^\"\\']+)[\"\\']', line)\n            if match:\n                return match.group(1)\n    return \"unknown\"\n\n\napp_version = get_app_version()\napp_description = \"Windows 文件夹备注工具\"\n\n# =============================================================================\n# Python Interpreter Options\n# =============================================================================\n\n# 强制启用 UTF-8 模式，支持中文等特殊字符输出\n# See: https://pyinstaller.org/en/stable/spec-files.html#specifying-python-interpreter-options\noptions = [\n    ('X utf8', None, 'OPTION'),\n]\n\n# =============================================================================\n# Analysis\n# =============================================================================\n\n# Collect all submodules from remark package\nhiddenimports = collect_submodules('remark') + [\n    'tkinter',\n    'packaging',\n    'packaging.version',\n]\n\n# Collect locale translation files (.mo)\n# PyInstaller 会将这些文件复制到 exe 的临时目录\nlocale_datas = []\nfor lang in ['zh', 'en']:\n    lang_dir = os.path.join('locale', lang, 'LC_MESSAGES')\n    mo_file = os.path.join(lang_dir, 'messages.mo')\n    if os.path.exists(mo_file):\n        locale_datas.append((mo_file, lang_dir))\n\na = Analysis(\n    [os.path.join(\"remark\", \"cli\", \"commands.py\")],\n    pathex=[],\n    binaries=[],\n    datas=locale_datas,\n    hiddenimports=hiddenimports,\n    hookspath=[],\n    hooksconfig={},\n    runtime_hooks=[],\n    excludes=[\n        'setuptools',\n        'setuptools.*',\n        'distutils',\n        'distutils.*',\n        'unittest',\n        'pydoc',\n        'pydoc_data',\n    ],\n    win_no_prefer_redirects=False,\n    win_private_assemblies=False,\n    cipher=block_cipher,\n    noarchive=False,\n)\n\n# =============================================================================\n# PYZ\n# =============================================================================\n\npyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)\n\n# =============================================================================\n# EXE\n# =============================================================================\n\nupx_dir = get_upx_dir()\n\nexe = EXE(\n    pyz,\n    a.scripts,\n    a.binaries,\n    a.zipfiles,\n    a.datas,\n    options,  # Python 解释器选项：启用 UTF-8 模式\n    [],\n    name=app_name,\n    debug=False,\n    bootloader_ignore_signals=False,\n    strip=False,\n    upx=upx_dir is not None,\n    upx_dir=upx_dir if upx_dir is not None else \"\",\n    upx_exclude=[],\n    runtime_tmpdir=None,\n    console=True,  # Console application for interactive mode\n    disable_windowed_traceback=False,\n    argv_emulation=False,\n    target_arch=None,\n    codesign_identity=None,\n    entitlements_file=None,\n)\n"
  },
  {
    "path": "scripts/__init__.py",
    "content": "\"\"\"Scripts package.\"\"\"\n"
  },
  {
    "path": "scripts/analyze_exe_size.py",
    "content": "\"\"\"\nPyInstaller EXE 大小分析工具\n\n使用方法:\n    python scripts/analyze_exe_size.py\n\n功能:\n    1. 运行 pyi-archive_viewer -r 获取 exe 内容\n    2. 分析各组件大小\n    3. 生成详细报告到 tmp/ 目录\n\"\"\"\n\nimport os\nimport re\nimport subprocess\nfrom datetime import datetime\n\n\ndef run_archive_viewer(exe_path: str) -> str:\n    \"\"\"运行 pyi-archive_viewer -r 并获取输出\"\"\"\n    print(f\"Analyzing {exe_path}...\")\n\n    result = subprocess.run(\n        [\"pyi-archive_viewer\", \"-r\", exe_path],\n        capture_output=True,\n        text=True,\n    )\n\n    return result.stdout\n\n\ndef parse_archive_content(content: str) -> dict[str, int]:\n    \"\"\"解析 archive_viewer 输出，返回 {name: size} 字典\"\"\"\n    components: dict[str, int] = {}\n    for line in content.split(\"\\n\"):\n        if not line.strip() or line.startswith(\"position\") or line.startswith(\"Options\"):\n            continue\n        match = re.match(r\"^\\s*\\d+,\\s*(\\d+),\\s*\\d+,\\s*\\d+,\\s*'[^']+',\\s*'([^']+)'\", line)\n        if match:\n            size = int(match.group(1))\n            name = match.group(2)\n            components[name] = components.get(name, 0) + size\n    return components\n\n\ndef parse_pyz_content(content: str) -> dict[str, int]:\n    \"\"\"解析 PYZ.pyz 内部内容\"\"\"\n    if \"Contents of 'PYZ.pyz'\" not in content:\n        return {}\n\n    pyz_section = content.split(\"Contents of 'PYZ.pyz'\")[1]\n    lines = pyz_section.split(\"\\n\")[:5000]\n\n    packages: dict[str, int] = {}\n    for line in lines:\n        if not line.strip() or line.startswith(\"Contents\") or line.startswith(\"typecode\"):\n            continue\n        parts = line.split(\",\")\n        if len(parts) >= 4:\n            try:\n                length = int(parts[2].strip())\n                name = parts[3].strip().strip(\"'\")\n\n                # 按包分组\n                if \".\" in name:\n                    pkg = name.split(\".\")[0]\n                else:\n                    pkg = name\n\n                packages[pkg] = packages.get(pkg, 0) + length\n            except (ValueError, IndexError):\n                pass\n    return packages\n\n\ndef generate_report(\n    exe_path: str,\n    components: dict[str, int],\n    pyz_packages: dict[str, int],\n    output_path: str,\n) -> None:\n    \"\"\"生成分析报告\"\"\"\n    exe_size = os.path.getsize(exe_path)\n\n    lines = []\n    lines.append(\"=\" * 80)\n    lines.append(\"PyInstaller EXE Size Analysis Report\")\n    lines.append(\"=\" * 80)\n    lines.append(\"\")\n    lines.append(f\"EXE: {exe_path}\")\n    lines.append(f\"Current EXE Size: {exe_size / (1024 * 1024):.2f} MB ({exe_size:,} bytes)\")\n    lines.append(f\"Analysis Time: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\")\n    lines.append(\"\")\n\n    # 1. 最大的文件\n    lines.append(\"=\" * 80)\n    lines.append(\"1. Top 50 Largest Files/Components\")\n    lines.append(\"=\" * 80)\n    lines.append(\"\")\n    sorted_all = sorted(components.items(), key=lambda x: x[1], reverse=True)\n    for i, (name, size) in enumerate(sorted_all[:50], 1):\n        name_short = name[:47] + \"...\" if len(name) > 50 else name\n        lines.append(f\"{i:3d}. {name_short:<50} {size // 1024:6d} KB\")\n\n    # 2. DLL 文件\n    lines.append(\"\")\n    lines.append(\"=\" * 80)\n    lines.append(\"2. DLL Files (Windows System Libraries)\")\n    lines.append(\"=\" * 80)\n    lines.append(\"\")\n    dlls = [(n, s) for n, s in components.items() if n.endswith(\".dll\")]\n    dlls.sort(key=lambda x: x[1], reverse=True)\n    for name, size in dlls[:30]:\n        lines.append(f\"{name:<60} {size // 1024:6d} KB\")\n\n    # 3. PYD 文件\n    lines.append(\"\")\n    lines.append(\"=\" * 80)\n    lines.append(\"3. PYD Files (Python Extension Modules)\")\n    lines.append(\"=\" * 80)\n    lines.append(\"\")\n    pyds = [(n, s) for n, s in components.items() if n.endswith(\".pyd\")]\n    pyds.sort(key=lambda x: x[1], reverse=True)\n    for name, size in pyds[:20]:\n        lines.append(f\"{name:<60} {size // 1024:6d} KB\")\n\n    # 4. Tcl/Tk 文件\n    lines.append(\"\")\n    lines.append(\"=\" * 80)\n    lines.append(\"4. Tcl/Tk Data Files\")\n    lines.append(\"=\" * 80)\n    lines.append(\"\")\n    tcl_files = [(n, s) for n, s in components.items() if \"_tcl_data\" in n or \"tcl8\" in n]\n    tcl_files.sort(key=lambda x: x[1], reverse=True)\n    for name, size in tcl_files[:30]:\n        lines.append(f\"{name:<60} {size // 1024:6d} KB\")\n\n    # 5. PYZ.pyz 内部模块\n    if pyz_packages:\n        lines.append(\"\")\n        lines.append(\"=\" * 80)\n        lines.append(\"PYZ.pyz Internal Python Modules Analysis\")\n        lines.append(\"=\" * 80)\n        lines.append(\"\")\n        lines.append(f\"Total PYZ.pyz size: {sum(pyz_packages.values()) // 1024} KB\")\n        lines.append(\"\")\n\n        lines.append(\"=\" * 80)\n        lines.append(\"5. Python Packages by Size (Top 50)\")\n        lines.append(\"=\" * 80)\n        lines.append(\"\")\n        sorted_packages = sorted(pyz_packages.items(), key=lambda x: x[1], reverse=True)\n        for pkg, size in sorted_packages[:50]:\n            lines.append(f\"{pkg:<30} {size // 1024:6d} KB\")\n\n        # 可移除的模块\n        excludable = [\"setuptools\", \"unittest\", \"email\", \"distutils\", \"pydoc\", \"pydoc_data\", \"test\"]\n        lines.append(\"\")\n        lines.append(\"=\" * 80)\n        lines.append(\"6. Potentially Removable Modules\")\n        lines.append(\"=\" * 80)\n        lines.append(\"\")\n        excludable_packages = [\n            (p, s) for p, s in sorted_packages if any(e in p for e in excludable)\n        ]\n        total_removable = sum(s for p, s in excludable_packages)\n        for pkg, size in excludable_packages:\n            lines.append(f\"{pkg:<30} {size // 1024:6d} KB\")\n        lines.append(\"\")\n        lines.append(\n            f\"Total potentially removable: {total_removable // 1024} KB ({total_removable // 1024 / 1024:.2f} MB)\"\n        )\n\n    # 写入文件\n    with open(output_path, \"w\", encoding=\"utf-8\") as f:\n        f.write(\"\\n\".join(lines))\n\n\ndef main():\n    # 获取 exe 路径\n    script_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))\n    exe_path = os.path.join(script_dir, \"dist\", \"windows-folder-remark.exe\")\n\n    if not os.path.exists(exe_path):\n        print(f\"Error: {exe_path} not found!\")\n        print(\"Please build the exe first with: pyinstaller remark.spec\")\n        return 1\n\n    # 运行 archive_viewer\n    recursive_content = run_archive_viewer(exe_path)\n\n    # 解析内容\n    components = parse_archive_content(recursive_content)\n    pyz_packages = parse_pyz_content(recursive_content)\n\n    # 生成带时间戳的输出文件\n    timestamp = datetime.now().strftime(\"%Y%m%d_%H%M%S\")\n    tmp_dir = os.path.join(script_dir, \"tmp\")\n    os.makedirs(tmp_dir, exist_ok=True)\n    output_path = os.path.join(tmp_dir, f\"exe_size_analysis_{timestamp}.txt\")\n\n    # 生成报告\n    generate_report(exe_path, components, pyz_packages, output_path)\n\n    print(f\"\\nReport saved to: {output_path}\")\n\n    # 打印摘要\n    exe_size = os.path.getsize(exe_path)\n    print(\"\\nSummary:\")\n    print(f\"  EXE Size: {exe_size / (1024 * 1024):.2f} MB\")\n    print(f\"  Largest component: {max(components.items(), key=lambda x: x[1])[0]}\")\n    if pyz_packages and \"setuptools\" in pyz_packages:\n        print(f\"  setuptools size: {pyz_packages['setuptools'] // 1024} KB (removable)\")\n\n    return 0\n\n\nif __name__ == \"__main__\":\n    exit(main())\n"
  },
  {
    "path": "scripts/build.py",
    "content": "\"\"\"\n本地打包脚本\n\n使用方法:\n    # 打包为单文件 exe\n    python -m scripts.build\n\n    # 清理构建文件\n    python -m scripts.build --clean\n\"\"\"\n\nimport argparse\nimport os\nimport shutil\nimport subprocess\nimport sys\nfrom collections.abc import Callable\n\n# 设置 UTF-8 输出编码（Windows 兼容）\nif sys.platform == \"win32\":\n    if hasattr(sys.stdout, \"reconfigure\"):\n        sys.stdout.reconfigure(encoding=\"utf-8\", errors=\"replace\")\n    if hasattr(sys.stderr, \"reconfigure\"):\n        sys.stderr.reconfigure(encoding=\"utf-8\", errors=\"replace\")\n\n# 项目根目录\nROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))\n\n# 导入 ensure_upx 模块\ndo_ensure_upx: Callable[[], str | None] | None = None\nHAS_UPX_SCRIPT = False\ntry:\n    from scripts.ensure_upx import ensure_upx as do_ensure_upx\n\n    HAS_UPX_SCRIPT = True\nexcept ImportError:\n    pass\n\n\ndef get_project_version():\n    \"\"\"获取项目版本号\"\"\"\n    toml_file = os.path.join(ROOT_DIR, \"pyproject.toml\")\n    with open(toml_file, encoding=\"utf-8\") as f:\n        content = f.read()\n        import re\n\n        match = re.search(r'version\\s*=\\s*[\"\\']([^\"\\']+)[\"\\']', content)\n        if match:\n            return match.group(1)\n    return \"unknown\"\n\n\ndef clean_build_files():\n    \"\"\"清理构建文件\"\"\"\n    print(\"清理构建文件...\")\n\n    dirs_to_remove = [\"build\", \"dist\", \"spec\"]\n\n    for dir_name in dirs_to_remove:\n        dir_path = os.path.join(ROOT_DIR, dir_name)\n        if os.path.exists(dir_path):\n            shutil.rmtree(dir_path)\n            print(f\"  已删除: {dir_name}/\")\n\n    print(\"✓ 清理完成\")\n\n\ndef ensure_upx():\n    \"\"\"确保 UPX 压缩工具可用\"\"\"\n    print(\"检查 UPX 压缩工具...\")\n\n    if not HAS_UPX_SCRIPT or do_ensure_upx is None:\n        print(\"警告: UPX 安装脚本不存在，跳过\")\n        return True\n\n    try:\n        upx_path = do_ensure_upx()\n        return upx_path is not None\n    except Exception as e:\n        print(f\"警告: UPX 检查失败: {e}\")\n        print(\"将继续打包，但压缩可能被禁用\")\n        return True\n\n\ndef build_exe():\n    \"\"\"使用 PyInstaller 打包为单文件 exe\"\"\"\n    print(\"开始打包...\")\n\n    spec_file = os.path.join(ROOT_DIR, \"remark.spec\")\n    if not os.path.exists(spec_file):\n        print(f\"错误: 找不到 {spec_file}\")\n        return False\n\n    # 检查 PyInstaller 是否安装\n    try:\n        import PyInstaller\n\n        print(f\"PyInstaller 版本: {PyInstaller.__version__}\")\n    except ImportError:\n        print(\"错误: 未安装 PyInstaller\")\n        print(\"请运行: pip install pyinstaller\")\n        return False\n\n    # 确保 UPX 可用\n    if not ensure_upx():\n        print(\"警告: UPX 不可用，exe 体积会更大\")\n\n    # 运行 PyInstaller\n    try:\n        subprocess.run(\n            [\"pyinstaller\", spec_file, \"--clean\"],\n            cwd=ROOT_DIR,\n            check=True,\n        )\n        print(\"✓ 打包完成\")\n        print(\"\\n输出文件: dist/windows-folder-remark.exe\")\n        return True\n    except subprocess.CalledProcessError as e:\n        print(f\"✗ 打包失败: {e}\")\n        return False\n    except FileNotFoundError:\n        print(\"错误: 找不到 pyinstaller 命令\")\n        print(\"请运行: pip install pyinstaller\")\n        return False\n\n\ndef main():\n    parser = argparse.ArgumentParser(description=\"本地打包工具\")\n    parser.add_argument(\"--clean\", \"-c\", action=\"store_true\", help=\"仅清理构建文件，不进行打包\")\n\n    args = parser.parse_args()\n\n    if args.clean:\n        clean_build_files()\n        return\n\n    # 打包前先清理\n    clean_build_files()\n    print()\n\n    # 开始打包\n    version = get_project_version()\n    print(f\"项目版本: {version}\")\n    print()\n\n    if build_exe():\n        print(\"\\n\" + \"=\" * 50)\n        print(\"打包成功!\")\n        print(f\"版本: {version}\")\n        print(\"位置: dist/windows-folder-remark.exe\")\n        print(\"=\" * 50)\n    else:\n        sys.exit(1)\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "scripts/check_i18n.py",
    "content": "#!/usr/bin/env python\n\"\"\"检查翻译文件完整性\"\"\"\n\nimport re\nimport sys\n\n\ndef check_po_file(path: str) -> bool:\n    \"\"\"检查单个 .po 文件是否有空翻译\"\"\"\n    with open(path, \"r\", encoding=\"utf-8\") as f:\n        lines = f.readlines()\n\n    i = 0\n    found_issue = False\n    while i < len(lines):\n        line = lines[i]\n        # 检查单行 msgid 后跟空 msgstr 的情况\n        if re.match(r'^msgid \".+\"$', line):\n            if i + 1 < len(lines):\n                next_line = lines[i + 1].strip()\n                # 如果下一行是 msgstr \"\" 且不是多行字符串的开始\n                if next_line == 'msgstr \"\"':\n                    # 检查是否真的是单行（再下一行不是以引号开头）\n                    if i + 2 >= len(lines) or not lines[i + 2].strip().startswith('\"'):\n                        print(f\"ERROR: {path}:{i + 2}: Empty translation: {line.strip()}\")\n                        found_issue = True\n        i += 1\n\n    return found_issue\n\n\nif __name__ == \"__main__\":\n    all_ok = True\n    for path in sys.argv[1:]:\n        if check_po_file(path):\n            all_ok = False\n\n    if not all_ok:\n        print(\"\\nERROR: Empty translations found! Please add translations.\", file=sys.stderr)\n        sys.exit(1)\n\n    print(\"Translation check PASSED\")\n    sys.exit(0)\n"
  },
  {
    "path": "scripts/ensure_upx.py",
    "content": "\"\"\"\nUPX 下载脚本\n\n自动从 GitHub releases 下载最新版本的 UPX 压缩工具。\n\n使用方法:\n    python scripts/ensure_upx.py\n\"\"\"\n\nimport json\nimport os\nimport platform\nimport shutil\nimport subprocess\nimport sys\nimport tempfile\nimport urllib.error\nimport urllib.request\nimport zipfile\n\n# 设置 UTF-8 输出编码（Windows 兼容）\nif sys.platform == \"win32\":\n    if hasattr(sys.stdout, \"reconfigure\"):\n        sys.stdout.reconfigure(encoding=\"utf-8\", errors=\"replace\")\n    if hasattr(sys.stderr, \"reconfigure\"):\n        sys.stderr.reconfigure(encoding=\"utf-8\", errors=\"replace\")\n\n# 项目根目录\nROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))\n\n# UPX 安装目录\nUPX_DIR = os.path.join(ROOT_DIR, \"tools\", \"upx\")\n\n# GitHub API 配\nGITHUB_API_RELEASES = \"https://api.github.com/repos/upx/upx/releases/latest\"\n\n\ndef get_proxies() -> dict[str, str] | None:\n    \"\"\"从环境变量获取代理配置\"\"\"\n    proxies = []\n    http_proxy = os.environ.get(\"HTTP_PROXY\") or os.environ.get(\"http_proxy\")\n    https_proxy = os.environ.get(\"HTTPS_PROXY\") or os.environ.get(\"https_proxy\")\n\n    if http_proxy:\n        proxies.append((\"http\", http_proxy))\n    if https_proxy:\n        proxies.append((\"https\", https_proxy))\n\n    return dict(proxies) if proxies else None\n\n\ndef create_opener():\n    \"\"\"创建带代理的 URL opener\"\"\"\n    proxies = get_proxies()\n    if proxies:\n        proxy_handler = urllib.request.ProxyHandler(proxies)\n        return urllib.request.build_opener(proxy_handler)\n    return urllib.request.build_opener()\n\n\ndef get_system_info():\n    \"\"\"获取系统信息\"\"\"\n    system = platform.system().lower()\n    machine = platform.machine().lower()\n\n    if system == \"windows\":\n        if machine in (\"amd64\", \"x86_64\"):\n            return \"win_amd64\"\n        elif machine in (\"i386\", \"i686\", \"x86\"):\n            return \"win32\"\n    return None\n\n\ndef get_latest_upx_version() -> tuple[str, str, str] | None:\n    \"\"\"从 GitHub API 获取最新 UPX 版本和下载信息\n\n    Returns:\n        (version, download_url, asset_name) 或 None\n    \"\"\"\n    try:\n        proxies = get_proxies()\n        if proxies:\n            print(f\"  使用代理: {proxies.get('https', proxies.get('http'))}\")\n\n        request = urllib.request.Request(\n            GITHUB_API_RELEASES,\n            headers={\n                \"Accept\": \"application/vnd.github.v3+json\",\n                \"User-Agent\": \"windows-folder-remark\",\n            },\n        )\n        opener = create_opener()\n        with opener.open(request, timeout=10) as response:\n            if response.status != 200:\n                return None\n            data = json.load(response)\n\n        version = data[\"tag_name\"].lstrip(\"v\")\n\n        # 获取目标平台\n        target_platform = get_system_info()\n        if not target_platform:\n            return None\n\n        # 查找匹配的 asset\n        platform_keywords = {\n            \"win_amd64\": [\"win\", \"64\"],\n            \"win32\": [\"win\", \"32\"],\n        }\n\n        keywords = platform_keywords.get(target_platform, [])\n\n        for asset in data.get(\"assets\", []):\n            name = asset[\"name\"].lower()\n            # 检查是否匹配平台关键词\n            if all(kw in name for kw in keywords):\n                return version, asset[\"browser_download_url\"], asset[\"name\"]\n\n        print(f\"错误: 找不到适合 {target_platform} 的 UPX 包\")\n        print(\"可用的包:\")\n        for asset in data.get(\"assets\", []):\n            print(f\"  - {asset['name']}\")\n        return None\n\n    except (urllib.error.HTTPError, urllib.error.URLError, json.JSONDecodeError) as e:\n        print(f\"警告: 无法获取 UPX 版本信息: {e}\")\n        return None\n\n\ndef find_upx_executable() -> str | None:\n    \"\"\"查找已存在的 UPX 可执行文件\"\"\"\n    # 优先使用项目本地的 UPX\n    local_upx = os.path.join(UPX_DIR, \"upx.exe\")\n    if os.path.exists(local_upx):\n        return local_upx\n\n    # 检查系统 PATH 中是否有 UPX\n    try:\n        result = subprocess.run(\n            [\"where\", \"upx\"],\n            capture_output=True,\n            text=True,\n            check=False,\n        )\n        if result.returncode == 0:\n            return result.stdout.strip().split(\"\\n\")[0]\n    except FileNotFoundError:\n        pass\n\n    return None\n\n\ndef download_upx(version: str, download_url: str, asset_name: str) -> str | None:\n    \"\"\"下载 UPX 并解压到指定目录\n\n    Args:\n        version: UPX 版本号\n        download_url: 实际的下载 URL（从 API 获取）\n        asset_name: 资源文件名\n\n    Returns:\n        UPX 可执行文件路径，失败返回 None\n    \"\"\"\n    print(f\"正在下载 UPX {version}...\")\n    print(f\"  文件: {asset_name}\")\n    print(f\"  URL: {download_url}\")\n\n    # 创建临时目录\n    with tempfile.TemporaryDirectory() as temp_dir:\n        zip_path = os.path.join(temp_dir, asset_name)\n\n        # 下载文件\n        try:\n            request = urllib.request.Request(\n                download_url,\n                headers={\"User-Agent\": \"windows-folder-remark\"},\n            )\n            opener = create_opener()\n\n            with opener.open(request, timeout=60) as response:\n                total_size = int(response.headers.get(\"content-length\", 0))\n                downloaded = 0\n                chunk_size = 8192\n\n                with open(zip_path, \"wb\") as f:\n                    while True:\n                        chunk = response.read(chunk_size)\n                        if not chunk:\n                            break\n                        f.write(chunk)\n                        downloaded += len(chunk)\n\n                        # 显示进度\n                        if total_size > 0:\n                            progress = int(50 * downloaded / total_size)\n                            downloaded_mb = downloaded / 1024 / 1024\n                            print(\n                                f\"\\r  进度: [{'#' * progress}{'.' * (50 - progress)}] {downloaded_mb:.1f}MB\",\n                                end=\"\",\n                            )\n\n            print()  # 换行\n\n        except (urllib.error.HTTPError, urllib.error.URLError) as e:\n            print(f\"\\n错误: 下载失败: {e}\")\n            return None\n\n        # 解压文件\n        print(\"正在解压...\")\n        try:\n            with zipfile.ZipFile(zip_path, \"r\") as zip_ref:\n                zip_ref.extractall(temp_dir)\n        except zipfile.BadZipFile as e:\n            print(f\"错误: 解压失败: {e}\")\n            return None\n\n        # 查找 upx.exe\n        for root, _, files in os.walk(temp_dir):\n            for file in files:\n                if file == \"upx.exe\":\n                    src = os.path.join(root, file)\n                    # 创建目标目录\n                    os.makedirs(UPX_DIR, exist_ok=True)\n                    dst = os.path.join(UPX_DIR, file)\n                    shutil.copy2(src, dst)\n                    return dst\n\n        print(\"错误: 在下载的包中找不到 upx.exe\")\n        return None\n\n\ndef verify_upx(upx_path: str) -> bool:\n    \"\"\"验证 UPX 是否可用\"\"\"\n    try:\n        result = subprocess.run(\n            [upx_path, \"--version\"],\n            capture_output=True,\n            text=True,\n            check=True,\n        )\n        # 检查输出是否包含 UPX\n        if \"upx\" in result.stdout.lower():\n            version_line = result.stdout.split(\"\\n\")[0]\n            print(f\"  版本: {version_line.strip()}\")\n            return True\n    except (subprocess.CalledProcessError, FileNotFoundError):\n        pass\n    return False\n\n\ndef ensure_upx() -> str | None:\n    \"\"\"确保 UPX 可用，返回 UPX 可执行文件路径\"\"\"\n    # 检查平台\n    target_platform = get_system_info()\n    if not target_platform:\n        print(\"错误: 不支持的平台\")\n        print(\"  UPX 自动下载仅支持 Windows\")\n        return None\n\n    print(\"检查 UPX 压缩工具...\")\n\n    # 查找已存在的 UPX\n    upx_path = find_upx_executable()\n    if upx_path:\n        print(f\"  找到 UPX: {upx_path}\")\n        if verify_upx(upx_path):\n            return upx_path\n        print(\"  警告: 现有 UPX 不可用\")\n\n    # 获取最新版本和下载信息\n    release_info = get_latest_upx_version()\n    if not release_info:\n        print(\"错误: 无法获取 UPX 最新版本\")\n        return None\n\n    version, download_url, asset_name = release_info\n    print(f\"  最新版本: {version}\")\n\n    # 下载 UPX\n    upx_path = download_upx(version, download_url, asset_name)\n    if not upx_path:\n        return None\n\n    # 验证下载的 UPX\n    if verify_upx(upx_path):\n        print(f\"✓ UPX 已安装到: {upx_path}\")\n        return upx_path\n\n    print(\"错误: UPX 验证失败\")\n    return None\n\n\ndef main():\n    \"\"\"主入口\"\"\"\n    upx_path = ensure_upx()\n    if upx_path:\n        print(f\"\\nUPX 路径: {upx_path}\")\n        return 0\n    return 1\n\n\nif __name__ == \"__main__\":\n    sys.exit(main())\n"
  },
  {
    "path": "scripts/release.py",
    "content": "\"\"\"\n版本发布脚本\n\n使用方法:\n    # 查看当前版本\n    python scripts/release.py\n\n    # 递增补丁版本 (2.0.0 -> 2.0.1)\n    python scripts/release.py patch\n\n    # 递增次版本 (2.0.0 -> 2.1.0)\n    python scripts/release.py minor\n\n    # 递增主版本 (2.0.0 -> 3.0.0)\n    python scripts/release.py major\n\n    # 设置特定版本\n    python scripts/release.py 2.1.0\n\n    # 创建并推送 release tag\n    python scripts/release.py patch --push\n\"\"\"\n\nimport argparse\nimport os\nimport re\nimport subprocess\nimport sys\n\n# 项目根目录\nROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))\n\n\ndef get_current_version():\n    \"\"\"获取当前版本号（从 pyproject.toml）\"\"\"\n    toml_file = os.path.join(ROOT_DIR, \"pyproject.toml\")\n    with open(toml_file, encoding=\"utf-8\") as f:\n        content = f.read()\n        match = re.search(r'version\\s*=\\s*[\"\\']([^\"\\']+)[\"\\']', content)\n        if match:\n            return match.group(1)\n    raise ValueError(\"无法在 pyproject.toml 中找到版本号\")\n\n\ndef update_version(new_version):\n    \"\"\"更新 pyproject.toml 中的版本号（仅 [project] 部分）\"\"\"\n    toml_file = os.path.join(ROOT_DIR, \"pyproject.toml\")\n    with open(toml_file, encoding=\"utf-8\") as f:\n        lines = f.readlines()\n\n    # 只更新 [project] 部分的 version = 行\n    in_project_section = False\n    for i, line in enumerate(lines):\n        if line.strip() == \"[project]\":\n            in_project_section = True\n        elif line.startswith(\"[\") and not line.startswith(\"[[\"):\n            in_project_section = False\n        elif in_project_section and line.strip().startswith(\"version\"):\n            lines[i] = re.sub(\n                r'(version\\s*=\\s*[\"\\'])([^\"\\']+)([\"\\'])', rf\"\\g<1>{new_version}\\g<3>\", line\n            )\n            break\n\n    # 写入时使用 LF 换行符，避免 mixed-line-ending hook 修改文件\n    with open(toml_file, \"w\", encoding=\"utf-8\", newline=\"\\n\") as f:\n        f.writelines(lines)\n    return new_version\n\n\ndef bump_version(current, part=\"patch\"):\n    \"\"\"递增版本号\"\"\"\n    major, minor, patch = map(int, current.split(\".\"))\n\n    if part == \"major\":\n        major += 1\n        minor = 0\n        patch = 0\n    elif part == \"minor\":\n        minor += 1\n        patch = 0\n    else:  # patch\n        patch += 1\n\n    return f\"{major}.{minor}.{patch}\"\n\n\ndef create_tag(version, override=False):\n    \"\"\"创建 git tag\"\"\"\n    tag_name = f\"v{version}\"\n    # 如果使用 override 且 tag 已存在，先删除\n    if override:\n        try:\n            subprocess.run([\"git\", \"tag\", \"-d\", tag_name], check=True, capture_output=True)\n            print(f\"已删除本地 tag: {tag_name}\")\n        except subprocess.CalledProcessError:\n            pass  # tag 不存在，忽略\n    subprocess.run([\"git\", \"tag\", \"-a\", tag_name, \"-m\", f\"Release {tag_name}\"], check=True)\n    print(f\"已创建 tag: {tag_name}\")\n    return tag_name\n\n\ndef push_tag(tag_name, force=False):\n    \"\"\"推送 tag 到远程仓库\"\"\"\n    args = [\"git\", \"push\", \"origin\"]\n    if force:\n        args.append(\"--force\")\n    args.append(tag_name)\n    subprocess.run(args, check=True)\n    print(f\"已推送 tag: {tag_name}\")\n\n\ndef commit_version_changes():\n    \"\"\"提交版本变更\"\"\"\n    current_version = get_current_version()\n    subprocess.run([\"git\", \"add\", \"pyproject.toml\"], check=True)\n    subprocess.run([\"git\", \"commit\", \"-m\", f\"bump: version to {current_version}\"], check=True)\n    print(f\"已提交版本变更: {current_version}\")\n\n\ndef check_branch():\n    \"\"\"检查当前分支是否为主分支\"\"\"\n    result = subprocess.run(\n        [\"git\", \"branch\", \"--show-current\"],\n        capture_output=True,\n        text=True,\n        check=True,\n    )\n    current_branch = result.stdout.strip()\n    return current_branch\n\n\ndef check_working_directory_clean():\n    \"\"\"检查工作目录是否有未提交的改动\"\"\"\n    result = subprocess.run(\n        [\"git\", \"status\", \"--porcelain\"],\n        capture_output=True,\n        text=True,\n        check=True,\n    )\n    return result.stdout.strip() == \"\"\n\n\ndef check_remote_sync():\n    \"\"\"检查本地是否与远程同步\"\"\"\n    result = subprocess.run(\n        [\"git\", \"status\", \"-sb\"],\n        capture_output=True,\n        text=True,\n        check=True,\n    )\n    status_line = result.stdout.split(\"\\n\")[0]\n    # 检查是否包含 \"behind\" 字样\n    return \"behind\" not in status_line.lower()\n\n\ndef get_latest_tag() -> str | None:\n    \"\"\"获取最新的 git tag 版本号\"\"\"\n    try:\n        result = subprocess.run(\n            [\"git\", \"describe\", \"--tags\", \"--abbrev=0\"],\n            capture_output=True,\n            text=True,\n            check=True,\n        )\n        tag = result.stdout.strip()\n        # 移除 v 前缀\n        return tag.lstrip(\"v\") if tag else None\n    except subprocess.CalledProcessError:\n        # 没有任何 tag\n        return None\n\n\ndef validate_version_increment(current: str, new: str) -> bool:\n    \"\"\"验证新版本号是否大于当前版本号\"\"\"\n    curr_major, curr_minor, curr_patch = map(int, current.split(\".\"))\n    new_major, new_minor, new_patch = map(int, new.split(\".\"))\n\n    return (\n        new_major > curr_major\n        or (new_major == curr_major and new_minor > curr_minor)\n        or (new_major == curr_major and new_minor == curr_minor and new_patch > curr_patch)\n    )\n\n\ndef is_version_releasable(version: str) -> bool:\n    \"\"\"检查版本是否可发布（大于已有最新 tag）\"\"\"\n    latest_tag = get_latest_tag()\n    if latest_tag is None:\n        return True  # 没有任何 tag，可以发布\n\n    return validate_version_increment(latest_tag, version)\n\n\ndef main():\n    parser = argparse.ArgumentParser(description=\"版本发布管理工具\")\n    parser.add_argument(\n        \"version\", nargs=\"?\", help=\"新版本号 (如: 2.1.0) 或递增类型: patch/minor/major\"\n    )\n    parser.add_argument(\n        \"--push\", \"-p\", action=\"store_true\", help=\"创建并推送 tag 到远程仓库（触发 GitHub Actions）\"\n    )\n    parser.add_argument(\"--commit\", \"-c\", action=\"store_true\", help=\"提交版本变更到 git\")\n    parser.add_argument(\n        \"--dry-run\", \"-n\", action=\"store_true\", help=\"只显示将要执行的操作，不实际执行\"\n    )\n    parser.add_argument(\n        \"--skip-branch-check\",\n        action=\"store_true\",\n        help=\"跳过分支检查（不推荐）\",\n    )\n    parser.add_argument(\n        \"--override\",\n        action=\"store_true\",\n        help=\"强制使用当前版本发布，跳过版本检查（用于重新发布）\",\n    )\n\n    args = parser.parse_args()\n\n    # --push 自动包含 --commit（确保 tag 指向包含版本变更的提交）\n    if args.push and not args.commit:\n        args.commit = True\n\n    current = get_current_version()\n    print(f\"当前版本: {current}\")\n\n    # 确定目标版本\n    if not args.version:\n        # 未指定版本，使用当前版本\n        new_version = current\n        version_changed = False\n        if is_version_releasable(current):\n            print(f\"发布当前版本: {new_version}\")\n        elif args.override:\n            print(f\"强制重新发布当前版本: {new_version}\")\n        else:\n            print(f\"当前版本 {current} 已发布或不是最新版本\")\n            print(\"请指定新版本号: patch, minor, major 或具体版本号\")\n            print(\"或使用 --override 强制重新发布当前版本\")\n            return\n    elif args.version in (\"patch\", \"minor\", \"major\"):\n        new_version = bump_version(current, args.version)\n        version_changed = True\n    else:\n        # 验证版本号格式\n        if not re.match(r\"^\\d+\\.\\d+\\.\\d+$\", args.version):\n            print(\"错误: 版本号格式应为 x.y.z\")\n            sys.exit(1)\n        new_version = args.version\n        version_changed = new_version != current\n\n    print(f\"目标版本: {new_version}\")\n\n    # 检查版本是否可发布（大于已有最新 tag），除非使用 --override\n    if not args.override and not is_version_releasable(new_version):\n        latest_tag = get_latest_tag()\n        print(f\"错误: 版本 {new_version} 不大于已有最新 tag v{latest_tag}\")\n        print(\"提示: 使用 --override 强制发布当前版本（会覆盖已有 tag）\")\n        sys.exit(1)\n\n    # 分支检查\n    if not args.skip_branch_check:\n        current_branch = check_branch()\n        if current_branch not in (\"main\", \"master\"):\n            print(f\"警告: 当前分支 '{current_branch}' 不是主分支\")\n            print(\"建议在 main 或 master 分支进行发布\")\n            response = input(\"是否继续? (yes/no): \")\n            if response.lower() not in (\"yes\", \"y\"):\n                print(\"已取消\")\n                sys.exit(1)\n\n    # 工作目录状态检查\n    if not check_working_directory_clean():\n        print(\"错误: 工作目录有未提交的改动\")\n        print(\"请先提交或暂存所有改动后再进行发布\")\n        sys.exit(1)\n\n    # 远程同步检查\n    if not check_remote_sync():\n        print(\"警告: 本地分支落后于远程分支\")\n        print(\"建议先执行 'git pull' 同步最新代码\")\n        response = input(\"是否继续? (yes/no): \")\n        if response.lower() not in (\"yes\", \"y\"):\n            print(\"已取消\")\n            sys.exit(1)\n\n    if args.dry_run:\n        print(\"\\n[DRY RUN] 将执行以下操作:\")\n        if version_changed:\n            print(f\"  1. 更新版本号: {current} -> {new_version}\")\n        else:\n            print(f\"  1. 使用当前版本: {new_version}\")\n        if args.commit and version_changed:\n            print(\"  2. 提交版本变更\")\n        if args.push:\n            print(\n                f\"  {'2' if version_changed or not args.commit else '1'}. 创建并推送 tag v{new_version}\"\n            )\n        return\n\n    # 只有版本变更时才更新 pyproject.toml\n    if version_changed:\n        update_version(new_version)\n        print(f\"已更新版本号到: {new_version}\")\n    else:\n        print(f\"使用当前版本: {new_version}\")\n\n    # 提交变更（仅当版本有变更时）\n    if args.commit and version_changed:\n        commit_version_changes()\n\n    # 创建并推送 tag\n    if args.push:\n        tag_name = create_tag(new_version, override=args.override)\n        push_tag(tag_name, force=args.override)\n        print(f\"\\n✓ Release v{new_version} 已准备就绪!\")\n        print(\"  GitHub Actions 将自动构建并发布\")\n    else:\n        print(\"\\n提示: 使用 --push 参数创建并推送 tag 以触发 release\")\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "tests/__init__.py",
    "content": "\"\"\"\n测试模块\n\"\"\"\n"
  },
  {
    "path": "tests/conftest.py",
    "content": "\"\"\"共享测试配置\"\"\"\n\nimport os\nimport sys\n\nfrom pathlib import Path\n\nimport pytest\n\n# 项目根目录添加到路径\nsys.path.insert(0, str(Path(__file__).parent.parent))\n\n# 在导入任何模块之前设置语言环境变量，确保翻译使用中文\nos.environ[\"LANG\"] = \"zh\"\n\n# 立即设置翻译器为中文，确保在导入任何模块之前生效\nfrom remark.i18n import set_language\nset_language(\"zh\")\n\n\ndef pytest_configure(config):\n    \"\"\"pytest 配置钩子 - 定义 markers\"\"\"\n    import sys\n    config.addinivalue_line(\"markers\", \"unit: 单元测试（使用 mock）\")\n    config.addinivalue_line(\"markers\", \"integration: 集成测试（真实文件系统）\")\n    config.addinivalue_line(\"markers\", \"windows: 仅在 Windows 上运行\")\n    config.addinivalue_line(\"markers\", \"slow: 慢速测试\")\n\n    # 强制使用 UTF-8 编码输出，支持 emoji 等特殊字符\n    import io\n\n    if sys.stdout.encoding.lower() not in (\"utf-8\", \"utf-16\", \"utf-16-le\", \"utf-16-be\"):\n        sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding=\"utf-8\", errors=\"replace\")\n    if sys.stderr.encoding.lower() not in (\"utf-8\", \"utf-16\", \"utf-16-le\", \"utf-16-be\"):\n        sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding=\"utf-8\", errors=\"replace\")\n\n    # 强制重新初始化翻译器为中文\n    from remark.i18n import set_language\n    set_language(\"zh\")\n\n\ndef pytest_collection_modifyitems(config, items):\n    \"\"\"自动跳过非 Windows 平台上的 Windows 测试\"\"\"\n    if sys.platform != \"win32\":\n        skip_windows = pytest.mark.skip(reason=\"Windows only test\")\n        for item in items:\n            if \"windows\" in item.keywords:\n                item.add_marker(skip_windows)\n\n\n@pytest.fixture(autouse=True, scope=\"session\")\ndef set_chinese_language():\n    \"\"\"在整个测试会话开始时设置语言为中文\"\"\"\n    from remark.i18n import set_language\n    set_language(\"zh\")\n    yield\n"
  },
  {
    "path": "tests/integration/__init__.py",
    "content": "\"\"\"集成测试模块\"\"\"\n"
  },
  {
    "path": "tests/integration/conftest.py",
    "content": "\"\"\"集成测试专用 fixtures\"\"\"\n\nimport codecs\n\nimport pytest\n\n\n@pytest.fixture\ndef utf16_encoded_file(tmp_path):\n    \"\"\"创建 UTF-16 编码的测试文件\"\"\"\n    file_path = tmp_path / \"utf16_test.ini\"\n    content = \"[.ShellClassInfo]\\r\\nInfoTip=UTF-16 测试\\r\\n\"\n\n    with codecs.open(str(file_path), \"w\", encoding=\"utf-16\") as f:\n        f.write(content)\n\n    return str(file_path)\n\n\n@pytest.fixture\ndef utf8_encoded_file(tmp_path):\n    \"\"\"创建 UTF-8 编码的测试文件\"\"\"\n    file_path = tmp_path / \"utf8_test.ini\"\n    content = \"[.ShellClassInfo]\\r\\nInfoTip=UTF-8 测试\\r\\n\"\n\n    with open(str(file_path), \"w\", encoding=\"utf-8\") as f:\n        f.write(content)\n\n    return str(file_path)\n"
  },
  {
    "path": "tests/integration/test_encoding_handling.py",
    "content": "\"\"\"编码处理集成测试\"\"\"\n\nimport codecs\nimport os\n\nimport pytest\n\nfrom remark.storage.desktop_ini import DesktopIniHandler\n\n\n@pytest.mark.integration\nclass TestEncodingHandling:\n    \"\"\"编码处理集成测试\"\"\"\n\n    def test_write_and_read_utf16(self, tmp_path):\n        \"\"\"测试 UTF-16 编码读写\"\"\"\n        folder = str(tmp_path / \"test\")\n        os.makedirs(folder)\n\n        # 写入\n        result = DesktopIniHandler.write_info_tip(folder, \"UTF-16 测试\")\n        assert result is True\n\n        # 验证文件存在\n        ini_path = os.path.join(folder, \"desktop.ini\")\n        assert os.path.exists(ini_path)\n\n        # 读取\n        read_result = DesktopIniHandler.read_info_tip(folder)\n        assert read_result == \"UTF-16 测试\"\n\n    def test_read_gbk_encoded_file(self, tmp_path):\n        \"\"\"测试读取 GBK 编码的文件（降级兼容）\"\"\"\n        folder = str(tmp_path / \"gbk_test\")\n        os.makedirs(folder)\n        ini_path = os.path.join(folder, \"desktop.ini\")\n\n        # 使用 codecs.open 确保行尾符正确处理\n        with codecs.open(ini_path, \"w\", encoding=\"gbk\") as f:\n            f.write(\"[.ShellClassInfo]\\r\\nInfoTip=GBK Test\\r\\n\")\n\n        result = DesktopIniHandler.read_info_tip(folder)\n        assert result == \"GBK Test\"\n\n    def test_read_utf8_encoded_file(self, tmp_path):\n        \"\"\"测试读取 UTF-8 编码的文件\"\"\"\n        folder = str(tmp_path / \"utf8_test\")\n        os.makedirs(folder)\n        ini_path = os.path.join(folder, \"desktop.ini\")\n\n        # 使用 codecs.open 确保 UTF-8 编码正确\n        with codecs.open(ini_path, \"w\", encoding=\"utf-8\") as f:\n            f.write(\"[.ShellClassInfo]\\r\\nInfoTip=UTF-8 Test\\r\\n\")\n\n        result = DesktopIniHandler.read_info_tip(folder)\n        assert result == \"UTF-8 Test\"\n\n    def test_encoding_detection_utf16(self, utf16_encoded_file):\n        \"\"\"测试编码检测 - UTF-16\"\"\"\n        encoding, is_utf16 = DesktopIniHandler.detect_encoding(utf16_encoded_file)\n        assert is_utf16 is True\n        assert \"utf-16\" in encoding\n\n    def test_encoding_detection_utf8(self, utf8_encoded_file):\n        \"\"\"测试编码检测 - UTF-8\"\"\"\n        encoding, is_utf16 = DesktopIniHandler.detect_encoding(utf8_encoded_file)\n        assert is_utf16 is False\n        assert encoding == \"utf-8\"\n\n    @pytest.mark.parametrize(\n        \"comment\",\n        [\n            \"简体中文\",\n            \"繁體中文\",\n            \"日本語\",\n            \"한국어\",\n            \"Emoji 🔥\",\n            \"Mixed 中英文 Mixed\",\n            \"Special chars: !@#$%^&*()\",\n        ],\n    )\n    def test_write_various_characters(self, tmp_path, comment):\n        \"\"\"测试写入各种字符\"\"\"\n        folder = str(tmp_path / \"chinese\")\n        os.makedirs(folder)\n\n        result = DesktopIniHandler.write_info_tip(folder, comment)\n        assert result is True\n\n        read_result = DesktopIniHandler.read_info_tip(folder)\n        assert read_result == comment\n\n    def test_write_long_comment(self, tmp_path):\n        \"\"\"测试写入长备注\"\"\"\n        folder = str(tmp_path / \"long\")\n        os.makedirs(folder)\n\n        # 260 字符（MAX_COMMENT_LENGTH）\n        long_comment = \"A\" * 260\n        result = DesktopIniHandler.write_info_tip(folder, long_comment)\n        assert result is True\n\n        read_result = DesktopIniHandler.read_info_tip(folder)\n        assert read_result == long_comment\n\n    def test_update_preserves_encoding(self, tmp_path):\n        \"\"\"测试更新备注保持编码\"\"\"\n        folder = str(tmp_path / \"update\")\n        os.makedirs(folder)\n\n        # 第一次写入\n        DesktopIniHandler.write_info_tip(folder, \"初始备注\")\n\n        # 获取文件编码\n        ini_path = os.path.join(folder, \"desktop.ini\")\n        encoding1, is_utf16_1 = DesktopIniHandler.detect_encoding(ini_path)\n        assert is_utf16_1 is True\n\n        # 更新备注\n        DesktopIniHandler.write_info_tip(folder, \"更新备注\")\n\n        # 验证编码仍然是 UTF-16\n        encoding2, is_utf16_2 = DesktopIniHandler.detect_encoding(ini_path)\n        assert is_utf16_2 is True\n        assert encoding1.split(\"-\")[0] == encoding2.split(\"-\")[0]\n\n    def test_write_new_line_endings(self, tmp_path):\n        \"\"\"测试写入使用 Windows 行尾符\"\"\"\n        folder = str(tmp_path / \"line_ending\")\n        os.makedirs(folder)\n\n        DesktopIniHandler.write_info_tip(folder, \"行尾测试\")\n\n        ini_path = os.path.join(folder, \"desktop.ini\")\n\n        # UTF-16 LE 编码中，\\r\\n 被存储为 \\x00\\r\\x00\\n（每个字符前有 null byte）\n        # 或者可以简单地读取文本内容验证行尾符\n        with codecs.open(ini_path, \"r\", encoding=\"utf-16\") as f:\n            text_content = f.read()\n\n        # 验证文本内容包含 CRLF\n        assert \"\\r\\n\" in text_content\n\n    def test_read_without_bom(self, tmp_path):\n        \"\"\"测试读取没有 BOM 的文件（降级到 utf-8）\"\"\"\n        folder = str(tmp_path / \"no_bom\")\n        os.makedirs(folder)\n\n        ini_path = os.path.join(folder, \"desktop.ini\")\n        # 使用 codecs.open 确保编码正确\n        with codecs.open(ini_path, \"w\", encoding=\"utf-8\") as f:\n            f.write(\"[.ShellClassInfo]\\r\\nInfoTip=No BOM Test\\r\\n\")\n\n        result = DesktopIniHandler.read_info_tip(folder)\n        assert result == \"No BOM Test\"\n\n    def test_empty_folder(self, tmp_path):\n        \"\"\"测试空文件夹\"\"\"\n        folder = str(tmp_path / \"empty\")\n        os.makedirs(folder)\n\n        result = DesktopIniHandler.read_info_tip(folder)\n        assert result is None\n\n    def test_corrupted_ini_file(self, tmp_path):\n        \"\"\"测试损坏的 ini 文件\"\"\"\n        folder = str(tmp_path / \"corrupted\")\n        os.makedirs(folder)\n\n        ini_path = os.path.join(folder, \"desktop.ini\")\n        with open(ini_path, \"wb\") as f:\n            f.write(b\"\\x00\\x01\\x02\\x03\\x04\\x05\")  # 二进制垃圾数据\n\n        # 应该返回 None 而不是崩溃\n        result = DesktopIniHandler.read_info_tip(folder)\n        # 可能成功解码（某些编码会接受）或返回 None\n        assert result is None or isinstance(result, str)\n"
  },
  {
    "path": "tests/unit/__init__.py",
    "content": "\"\"\"单元测试模块\"\"\"\n"
  },
  {
    "path": "tests/unit/test_cli_commands.py",
    "content": "\"\"\"CLI 命令单元测试\"\"\"\n\nimport os\n\nimport pytest\n\nfrom remark.cli.commands import CLI, get_version\n\n\n@pytest.mark.unit\nclass TestCLI:\n    \"\"\"CLI 命令测试\"\"\"\n\n    @pytest.fixture(autouse=True)\n    def disable_background_update_check(self, monkeypatch):\n        \"\"\"禁用后台更新检查，避免 pyfakefs 隔离被后台线程破坏\"\"\"\n        monkeypatch.setattr(\n            \"remark.cli.commands.CLI._start_update_checker\",\n            lambda self: None,\n        )\n\n    def test_init(self):\n        \"\"\"测试 CLI 初始化\"\"\"\n        cli = CLI()\n        assert cli.handler is not None\n\n    def test_validate_folder_not_exists(self, capsys):\n        \"\"\"测试验证不存在的路径\"\"\"\n        cli = CLI()\n        result = cli._validate_folder(\"/invalid/path\")\n        assert result is False\n        captured = capsys.readouterr()\n        assert \"路径不存在\" in captured.out\n\n    @pytest.mark.skipif(os.name != \"nt\", reason=\"Windows only\")\n    def test_validate_folder_not_dir(self, fs, capsys):\n        \"\"\"测试验证非文件夹路径\"\"\"\n        fs.create_file(\"/file.txt\")\n        cli = CLI()\n        result = cli._validate_folder(\"/file.txt\")\n        assert result is False\n        captured = capsys.readouterr()\n        assert \"不是文件夹\" in captured.out\n\n    @pytest.mark.skipif(os.name != \"nt\", reason=\"Windows only\")\n    def test_validate_folder_success(self, fs):\n        \"\"\"测试验证有效文件夹\"\"\"\n        fs.create_dir(\"/valid/folder\")\n        cli = CLI()\n        result = cli._validate_folder(\"/valid/folder\")\n        assert result is True\n\n    @pytest.mark.skipif(os.name != \"nt\", reason=\"Windows only\")\n    def test_add_comment_success(self, fs):\n        \"\"\"测试添加备注成功\"\"\"\n        fs.create_dir(\"/test/folder\")\n        cli = CLI()\n        result = cli.add_comment(\"/test/folder\", \"测试备注\")\n        assert result is True\n\n    def test_add_comment_invalid_folder(self):\n        \"\"\"测试添加备注到无效路径\"\"\"\n        cli = CLI()\n        result = cli.add_comment(\"/invalid/path\", \"备注\")\n        assert result is False\n\n    @pytest.mark.skipif(os.name != \"nt\", reason=\"Windows only\")\n    def test_delete_comment_success(self, fs):\n        \"\"\"测试删除备注成功\"\"\"\n        fs.create_dir(\"/test/folder\")\n        cli = CLI()\n        result = cli.delete_comment(\"/test/folder\")\n        assert result is True\n\n    def test_delete_comment_invalid_folder(self):\n        \"\"\"测试删除备注失败（无效路径）\"\"\"\n        cli = CLI()\n        result = cli.delete_comment(\"/invalid/path\")\n        assert result is False\n\n    @pytest.mark.skipif(os.name != \"nt\", reason=\"Windows only\")\n    def test_view_comment_with_content(self, fs, capsys):\n        \"\"\"测试查看有备注的文件夹\"\"\"\n        fs.create_dir(\"/test/folder\")\n        from remark.core.folder_handler import FolderCommentHandler\n\n        with pytest.MonkeyPatch().context() as m:\n            m.setattr(FolderCommentHandler, \"get_comment\", lambda self, path: \"测试备注\")\n            cli = CLI()\n            cli.view_comment(\"/test/folder\")\n            captured = capsys.readouterr()\n            assert \"测试备注\" in captured.out\n\n    @pytest.mark.skipif(os.name != \"nt\", reason=\"Windows only\")\n    def test_view_comment_without_content(self, fs, capsys):\n        \"\"\"测试查看无备注的文件夹\"\"\"\n        fs.create_dir(\"/test/folder\")\n        from remark.core.folder_handler import FolderCommentHandler\n\n        with pytest.MonkeyPatch().context() as m:\n            m.setattr(FolderCommentHandler, \"get_comment\", lambda self, path: None)\n            cli = CLI()\n            cli.view_comment(\"/test/folder\")\n            captured = capsys.readouterr()\n            assert \"没有备注\" in captured.out\n\n    @pytest.mark.skipif(os.name != \"nt\", reason=\"Windows only\")\n    def test_view_comment_with_encoding_issue_and_fix(self, fs, capsys):\n        \"\"\"测试查看有编码问题的文件夹并选择修复\"\"\"\n        from remark.core.folder_handler import FolderCommentHandler\n        from remark.storage.desktop_ini import DesktopIniHandler\n\n        fs.create_dir(\"/test/folder\")\n        fs.create_file(\"/test/folder/desktop.ini\", contents=\"test content\")\n\n        with pytest.MonkeyPatch().context() as m:\n            m.setattr(DesktopIniHandler, \"detect_encoding\", lambda file_path: (\"utf-8\", False))\n            m.setattr(DesktopIniHandler, \"fix_encoding\", lambda file_path, current_encoding: True)\n            m.setattr(FolderCommentHandler, \"get_comment\", lambda self, path: \"测试备注\")\n            m.setattr(\"builtins.input\", lambda *args, **kwargs: \"y\")\n\n            cli = CLI()\n            cli.view_comment(\"/test/folder\")\n            captured = capsys.readouterr()\n            assert \"编码为 utf-8\" in captured.out\n            assert \"已修复为 UTF-16 编码\" in captured.out\n            assert \"测试备注\" in captured.out\n\n    @pytest.mark.skipif(os.name != \"nt\", reason=\"Windows only\")\n    def test_view_comment_with_encoding_issue_and_skip(self, fs, capsys):\n        \"\"\"测试查看有编码问题的文件夹但选择跳过修复\"\"\"\n        from remark.core.folder_handler import FolderCommentHandler\n        from remark.storage.desktop_ini import DesktopIniHandler\n\n        fs.create_dir(\"/test/folder\")\n        fs.create_file(\"/test/folder/desktop.ini\", contents=\"test content\")\n\n        with pytest.MonkeyPatch().context() as m:\n            m.setattr(DesktopIniHandler, \"detect_encoding\", lambda file_path: (\"gbk\", False))\n            m.setattr(FolderCommentHandler, \"get_comment\", lambda self, path: \"测试备注\")\n            m.setattr(\"builtins.input\", lambda *args, **kwargs: \"n\")\n\n            cli = CLI()\n            cli.view_comment(\"/test/folder\")\n            captured = capsys.readouterr()\n            assert \"编码为 gbk\" in captured.out\n            assert \"跳过编码修复\" in captured.out\n            assert \"测试备注\" in captured.out\n\n    @pytest.mark.skipif(os.name != \"nt\", reason=\"Windows only\")\n    def test_view_comment_with_correct_encoding(self, fs, capsys):\n        \"\"\"测试查看编码正确的文件夹\"\"\"\n        from remark.core.folder_handler import FolderCommentHandler\n        from remark.storage.desktop_ini import DesktopIniHandler\n\n        fs.create_dir(\"/test/folder\")\n        fs.create_file(\"/test/folder/desktop.ini\", contents=\"test content\")\n\n        with pytest.MonkeyPatch().context() as m:\n            m.setattr(\n                DesktopIniHandler,\n                \"detect_encoding\",\n                lambda file_path: (\"utf-16-le\", True),\n            )\n            m.setattr(FolderCommentHandler, \"get_comment\", lambda self, path: \"测试备注\")\n\n            cli = CLI()\n            cli.view_comment(\"/test/folder\")\n            captured = capsys.readouterr()\n            # 不应该显示编码警告\n            assert \"编码\" not in captured.out\n            assert \"测试备注\" in captured.out\n\n    @pytest.mark.skipif(os.name != \"nt\", reason=\"Windows only\")\n    def test_interactive_mode_valid_input(self, fs, monkeypatch):\n        \"\"\"测试交互模式有效输入\"\"\"\n        fs.create_dir(\"/folder\")\n\n        input_sequence = [\"/folder\", \"测试备注\"]\n\n        def mock_input(prompt):\n            if input_sequence:\n                return input_sequence.pop(0)\n            raise KeyboardInterrupt()\n\n        cli = CLI()\n\n        # Mock input to control user input and exit after first iteration\n        monkeypatch.setattr(\"builtins.input\", mock_input)\n\n        # Mock add_comment to verify it was called\n        original_add_comment = cli.add_comment\n        calls = []\n\n        def mock_add_comment(path, comment):\n            calls.append((path, comment))\n            return original_add_comment(path, comment)\n\n        monkeypatch.setattr(cli, \"add_comment\", mock_add_comment)\n\n        cli.interactive_mode()\n\n        # 验证 add_comment 被正确调用\n        assert len(calls) == 1\n        assert calls[0] == (\"/folder\", \"测试备注\")\n\n    @pytest.mark.skipif(os.name != \"nt\", reason=\"Windows only\")\n    def test_interactive_mode_invalid_path_then_valid(self, fs, monkeypatch, capsys):\n        \"\"\"测试交互模式先输入无效路径再输入有效路径\"\"\"\n        fs.create_dir(\"/valid_folder\")\n\n        input_sequence = [\"/invalid\", \"/valid_folder\", \"备注内容\"]\n\n        def mock_input(prompt):\n            if input_sequence:\n                return input_sequence.pop(0)\n            raise KeyboardInterrupt()\n\n        cli = CLI()\n        monkeypatch.setattr(\"builtins.input\", mock_input)\n\n        # Mock add_comment to verify it was called\n        original_add_comment = cli.add_comment\n        calls = []\n\n        def mock_add_comment(path, comment):\n            calls.append((path, comment))\n            return original_add_comment(path, comment)\n\n        monkeypatch.setattr(cli, \"add_comment\", mock_add_comment)\n\n        cli.interactive_mode()\n\n        # 验证无效路径被提示，最终有效路径被处理\n        captured = capsys.readouterr()\n        assert \"路径不存在\" in captured.out\n        assert len(calls) == 1\n        assert calls[0] == (\"/valid_folder\", \"备注内容\")\n\n    @pytest.mark.skipif(os.name != \"nt\", reason=\"Windows only\")\n    def test_interactive_mode_empty_comment_retry(self, fs, monkeypatch, capsys):\n        \"\"\"测试交互模式空备注重试\"\"\"\n        fs.create_dir(\"/folder\")\n\n        input_sequence = [\"/folder\", \"\", \"有效备注\"]\n\n        def mock_input(prompt):\n            if input_sequence:\n                return input_sequence.pop(0)\n            raise KeyboardInterrupt()\n\n        cli = CLI()\n        monkeypatch.setattr(\"builtins.input\", mock_input)\n\n        # Mock add_comment to verify it was called\n        original_add_comment = cli.add_comment\n        calls = []\n\n        def mock_add_comment(path, comment):\n            calls.append((path, comment))\n            return original_add_comment(path, comment)\n\n        monkeypatch.setattr(cli, \"add_comment\", mock_add_comment)\n\n        cli.interactive_mode()\n\n        # 验证空备注被提示重新输入，最终有效备注被处理\n        captured = capsys.readouterr()\n        assert \"备注不要为空\" in captured.out\n        assert len(calls) == 1\n        assert calls[0] == (\"/folder\", \"有效备注\")\n\n    def test_show_help(self, capsys):\n        \"\"\"测试帮助信息\"\"\"\n        cli = CLI()\n        cli.show_help()\n        captured = capsys.readouterr()\n        assert \"Windows 文件夹备注工具\" in captured.out\n        assert \"使用方法\" in captured.out\n        assert \"交互模式\" in captured.out\n\n    def test_run_with_help(self, capsys):\n        \"\"\"测试运行 --help 参数\"\"\"\n        cli = CLI()\n        cli.run([\"--help\"])\n        captured = capsys.readouterr()\n        assert \"Windows 文件夹备注工具\" in captured.out\n\n    @pytest.mark.skipif(os.name != \"nt\", reason=\"Windows only\")\n    def test_run_with_delete(self, fs):\n        \"\"\"测试运行 --delete 参数\"\"\"\n        fs.create_dir(\"/test/folder\")\n        cli = CLI()\n        cli.run([\"--delete\", \"/test/folder\"])\n\n    @pytest.mark.skipif(os.name != \"nt\", reason=\"Windows only\")\n    def test_run_with_view(self, fs):\n        \"\"\"测试运行 --view 参数\"\"\"\n        fs.create_dir(\"/test/folder\")\n        cli = CLI()\n        cli.run([\"--view\", \"/test/folder\"])\n\n    @pytest.mark.skipif(os.name != \"nt\", reason=\"Windows only\")\n    def test_run_with_path_and_comment(self, fs, monkeypatch):\n        \"\"\"测试运行带路径和备注参数\"\"\"\n        fs.create_dir(\"/folder\")\n        # Mock input to auto-confirm when path is detected\n        monkeypatch.setattr(\"builtins.input\", lambda *args, **kwargs: \"y\")\n        cli = CLI()\n        cli.run([\"/folder\", \"备注\"])\n\n    def test_run_interactive_mode(self, monkeypatch):\n        \"\"\"测试运行进入交互模式\"\"\"\n        # Mock interactive_mode to avoid actually entering interactive mode\n        monkeypatch.setattr(CLI, \"interactive_mode\", lambda cli: None)\n        cli = CLI()\n        cli.run([])\n\n    def test_run_platform_check_fail(self):\n        \"\"\"测试非 Windows 平台\"\"\"\n        # check_platform 在 CLI.run 开始时被调用，如果返回 False 会 sys.exit(1)\n        # 由于 sys.exit 会抛出 SystemExit，我们需要捕获它或 mock 它\n        with pytest.MonkeyPatch().context() as m:\n            m.setattr(\"remark.cli.commands.check_platform\", lambda: False)\n            cli = CLI()\n            with pytest.raises(SystemExit) as exc_info:\n                cli.run([])\n            # sys.exit(1) 会被调用\n            assert exc_info.value.code == 1\n\n\n@pytest.mark.unit\nclass TestResolvePathAmbiguousArgs:\n    \"\"\"测试 _resolve_path_from_ambiguous_args 方法\"\"\"\n\n    @pytest.fixture(autouse=True)\n    def disable_background_update_check(self, monkeypatch):\n        \"\"\"禁用后台更新检查，避免 pyfakefs 隔离被后台线程破坏\"\"\"\n        monkeypatch.setattr(\n            \"remark.cli.commands.CLI._start_update_checker\",\n            lambda self: None,\n        )\n\n    @pytest.mark.skipif(os.name != \"nt\", reason=\"Windows only\")\n    def test_resolve_path_valid_folder(self, fs):\n        \"\"\"测试解析有效文件夹路径\"\"\"\n        fs.create_dir(\"/My Documents/folder\")\n        cli = CLI()\n        path = cli._resolve_path_from_ambiguous_args([\"My\", \"Documents\", \"folder\"])\n        assert path is not None\n        assert \"My Documents\" in path\n\n    @pytest.mark.skipif(os.name != \"nt\", reason=\"Windows only\")\n    def test_resolve_path_file_rejected(self, fs):\n        \"\"\"测试拒绝文件路径\"\"\"\n        fs.create_file(\"/file.txt\")\n        cli = CLI()\n        path = cli._resolve_path_from_ambiguous_args([\"file\"])\n        assert path is None  # 文件应被拒绝\n\n    @pytest.mark.skipif(os.name != \"nt\", reason=\"Windows only\")\n    def test_resolve_path_no_candidates(self, fs):\n        \"\"\"测试无候选路径时返回 None\"\"\"\n        cli = CLI()\n        path = cli._resolve_path_from_ambiguous_args([\"nonexistent\"])\n        assert path is None\n\n    @pytest.mark.skipif(os.name != \"nt\", reason=\"Windows only\")\n    def test_resolve_path_single_candidate(self, fs, monkeypatch, capsys):\n        \"\"\"测试单个候选路径直接返回\"\"\"\n        fs.create_dir(\"/My Folder\")\n        cli = CLI()\n        # Mock input 模拟用户确认\n        monkeypatch.setattr(\"builtins.input\", lambda *args, **kwargs: \"y\")\n\n        path = cli._resolve_path_from_ambiguous_args([\"My\", \"Folder\"])\n        assert path is not None\n        assert \"My Folder\" in path\n\n    @pytest.mark.skipif(os.name != \"nt\", reason=\"Windows only\")\n    def test_resolve_path_multiple_candidates_user_cancels(self, fs, monkeypatch):\n        \"\"\"测试多个候选路径用户取消\"\"\"\n        fs.create_dir(\"/folder1\")\n        fs.create_dir(\"/folder2\")\n        cli = CLI()\n        # Mock input 模拟用户选择取消\n        monkeypatch.setattr(\"builtins.input\", lambda *args, **kwargs: \"0\")\n\n        path = cli._resolve_path_from_ambiguous_args([\"folder\"])\n        assert path is None\n\n    @pytest.mark.skipif(os.name != \"nt\", reason=\"Windows only\")\n    def test_resolve_path_multiple_candidates_user_selects(self, fs, monkeypatch):\n        \"\"\"测试多个候选路径用户选择\"\"\"\n        # 在同一目录下创建多个同名的文件夹（不同路径）\n        fs.create_dir(\"/parent1/folder\")\n        fs.create_dir(\"/parent2/folder\")\n        cli = CLI()\n        # Mock input 模拟用户选择第一个选项\n        inputs = iter([\"1\"])\n        monkeypatch.setattr(\"builtins.input\", lambda *args, **kwargs: next(inputs))\n\n        path = cli._resolve_path_from_ambiguous_args([\"parent1\", \"folder\"])\n        assert path is not None\n        assert \"parent1\" in path\n\n\n@pytest.mark.unit\nclass TestGetVersion:\n    \"\"\"get_version 函数测试\"\"\"\n\n    def test_get_version_from_package(self):\n        \"\"\"测试从包获取版本\"\"\"\n        with pytest.MonkeyPatch().context() as m:\n            m.setattr(\"importlib.metadata.version\", lambda *args, **kwargs: \"2.0.0\")\n            version = get_version()\n            assert version == \"2.0.0\"\n\n    def test_get_version_fallback(self):\n        \"\"\"测试获取版本失败时返回 unknown\"\"\"\n        with pytest.MonkeyPatch().context() as m:\n            m.setattr(\n                \"importlib.metadata.version\",\n                lambda *args, **kwargs: (_ for _ in ()).throw(Exception()),\n            )\n            version = get_version()\n            assert version == \"unknown\"\n\n\n@pytest.mark.unit\nclass TestInteractiveCommands:\n    \"\"\"交互模式命令测试\"\"\"\n\n    @pytest.fixture(autouse=True)\n    def disable_background_update_check(self, monkeypatch):\n        \"\"\"禁用后台更新检查\"\"\"\n        monkeypatch.setattr(\n            \"remark.cli.commands.CLI._start_update_checker\",\n            lambda self: None,\n        )\n\n    def test_interactive_commands_list_initialized(self):\n        \"\"\"测试交互命令列表正确初始化\"\"\"\n        cli = CLI()\n        # 进入交互模式会初始化命令列表\n        assert hasattr(cli, \"_interactive_commands_list\")\n        assert hasattr(cli, \"_interactive_commands\")\n        expected_commands = [\"#help\", \"#install\", \"#uninstall\", \"#update\"]\n        assert cli._interactive_commands_list == expected_commands\n\n    def test_show_command_list(self, capsys):\n        \"\"\"测试显示命令列表\"\"\"\n        cli = CLI()\n        cli._show_command_list()\n        captured = capsys.readouterr()\n        # 检查中文或英文输出\n        assert \"Available commands\" in captured.out or \"可用命令\" in captured.out\n        assert \"#help\" in captured.out\n        assert \"#install\" in captured.out\n        assert \"#uninstall\" in captured.out\n        assert \"#update\" in captured.out\n\n    def test_interactive_help_shows_commands(self, capsys):\n        \"\"\"测试 #help 命令显示所有可用命令\"\"\"\n        cli = CLI()\n        cli._interactive_help()\n        captured = capsys.readouterr()\n        # 检查中文或英文输出\n        assert \"Interactive Commands\" in captured.out or \"交互命令\" in captured.out\n        assert \"#help\" in captured.out\n        assert \"#install\" in captured.out\n        assert \"#uninstall\" in captured.out\n        assert \"#update\" in captured.out\n\n    @pytest.mark.skipif(os.name != \"nt\", reason=\"Windows only\")\n    def test_interactive_mode_handles_hash_only(self, fs, monkeypatch, capsys):\n        \"\"\"测试交互模式处理单独的 # 输入\"\"\"\n        fs.create_dir(\"/test/folder\")\n        cli = CLI()\n\n        # Mock input: 先输入 # 然后输入 Ctrl+C 退出\n        input_count = [0]\n\n        def mock_input(prompt):\n            input_count[0] += 1\n            if input_count[0] == 1:\n                return \"#\"\n            else:\n                raise KeyboardInterrupt()\n\n        monkeypatch.setattr(\"builtins.input\", mock_input)\n\n        cli.interactive_mode()\n\n        captured = capsys.readouterr()\n        # 应该显示可用命令列表（中文或英文）\n        assert \"Available commands\" in captured.out or \"可用命令\" in captured.out\n"
  },
  {
    "path": "tests/unit/test_desktop_ini.py",
    "content": "\"\"\"desktop.ini 读写单元测试\"\"\"\n\nimport os\nfrom unittest.mock import MagicMock, patch\n\nimport pytest\n\nfrom remark.storage.desktop_ini import (\n    DESKTOP_INI_ENCODING,\n    LINE_ENDING,\n    DesktopIniHandler,\n    EncodingConversionCanceled,\n)\n\n\n@pytest.mark.unit\nclass TestDesktopIniHandler:\n    \"\"\"desktop.ini 处理器测试\"\"\"\n\n    def test_get_path(self):\n        \"\"\"测试获取路径\"\"\"\n        result = DesktopIniHandler.get_path(\"/test/folder\")\n        # 使用 normpath 比较以兼容不同平台的路径分隔符\n        assert os.path.normpath(result) == os.path.normpath(\"/test/folder/desktop.ini\")\n\n    @pytest.mark.parametrize(\n        \"exists_return,expected\",\n        [(True, True), (False, False)],\n    )\n    def test_exists(self, exists_return, expected):\n        \"\"\"测试文件存在检测\"\"\"\n        with patch(\"os.path.exists\", return_value=exists_return):\n            result = DesktopIniHandler.exists(\"/folder\")\n            assert result is expected\n\n    def test_read_info_tip_no_file(self):\n        \"\"\"测试读取不存在的文件\"\"\"\n        with patch(\"os.path.exists\", return_value=False):\n            result = DesktopIniHandler.read_info_tip(\"/folder\")\n            assert result is None\n\n    @pytest.mark.parametrize(\n        \"content,expected\",\n        [\n            (\"[.ShellClassInfo]\\r\\nInfoTip=测试备注\\r\\n\", \"测试备注\"),\n            (\"[.ShellClassInfo]\\nInfoTip=测试备注\\n\", \"测试备注\"),\n            (\"[.ShellClassInfo]\\r\\nInfoTip=English Comment\\r\\n\", \"English Comment\"),\n            (\"[.ShellClassInfo]\\r\\nInfoTip=备注\\r\\nIconResource=icon.dll\\r\\n\", \"备注\"),\n            (\"[.ShellClassInfo]\\r\\nIconResource=icon.dll\\r\\n\", None),\n            (\"[.ShellClassInfo]\\r\\n\", None),\n            (\"No InfoTip here\", None),\n        ],\n    )\n    def test_read_info_tip_with_content(self, content, expected):\n        \"\"\"测试读取各种内容格式\"\"\"\n        with patch(\"os.path.exists\", return_value=True), patch(\"codecs.open\") as mock_open_func:\n            mock_file = MagicMock()\n            mock_file.read.return_value = content\n            mock_open_func.return_value.__enter__.return_value = mock_file\n\n            result = DesktopIniHandler.read_info_tip(\"/folder\")\n            assert result == expected\n\n    def test_write_info_tip_empty(self):\n        \"\"\"测试写入空备注\"\"\"\n        result = DesktopIniHandler.write_info_tip(\"/folder\", \"\")\n        assert result is False\n\n    def test_write_info_tip_new_file(self):\n        \"\"\"测试写入新文件\"\"\"\n        with patch(\"os.path.exists\", return_value=False), patch(\"codecs.open\") as mock_open_func:\n            mock_file = MagicMock()\n            mock_open_func.return_value.__enter__.return_value = mock_file\n\n            result = DesktopIniHandler.write_info_tip(\"/folder\", \"新备注\")\n            assert result is True\n            mock_file.write.assert_called_once()\n\n    def test_write_info_tip_update_existing(self):\n        \"\"\"测试更新已有文件\"\"\"\n        existing_content = \"[.ShellClassInfo]\\r\\nInfoTip=旧备注\\r\\n\"\n        with (\n            patch(\"os.path.exists\", return_value=True),\n            patch(\"remark.storage.desktop_ini.DesktopIniHandler.ensure_utf16_encoding\"),\n            patch(\"codecs.open\") as mock_open_func,\n        ):\n            mock_file_read = MagicMock()\n            mock_file_read.read.return_value = existing_content\n            mock_file_write = MagicMock()\n            mock_open_func.side_effect = [mock_file_read, mock_file_write]\n\n            result = DesktopIniHandler.write_info_tip(\"/folder\", \"新备注\")\n            assert result is True\n\n    def test_detect_encoding_utf16_le(self):\n        \"\"\"测试检测 UTF-16 LE 编码\"\"\"\n        with patch(\"builtins.open\") as mock_open_func:\n            mock_file = MagicMock()\n            mock_file.read.return_value = b\"\\xff\\xfe\\x00\\x00\"\n            mock_open_func.return_value.__enter__.return_value = mock_file\n\n            encoding, is_utf16 = DesktopIniHandler.detect_encoding(\"/test.ini\")\n            assert encoding == \"utf-16-le\"\n            assert is_utf16 is True\n\n    def test_detect_encoding_utf16_be(self):\n        \"\"\"测试检测 UTF-16 BE 编码\"\"\"\n        with patch(\"builtins.open\") as mock_open_func:\n            mock_file = MagicMock()\n            mock_file.read.return_value = b\"\\xfe\\xff\\x00\\x00\"\n            mock_open_func.return_value.__enter__.return_value = mock_file\n\n            encoding, is_utf16 = DesktopIniHandler.detect_encoding(\"/test.ini\")\n            assert encoding == \"utf-16-be\"\n            assert is_utf16 is True\n\n    def test_detect_encoding_utf8_bom(self):\n        \"\"\"测试检测 UTF-8 BOM 编码\"\"\"\n        with patch(\"builtins.open\") as mock_open_func:\n            mock_file = MagicMock()\n            mock_file.read.return_value = b\"\\xef\\xbb\\xbf\"\n            mock_open_func.return_value.__enter__.return_value = mock_file\n\n            encoding, is_utf16 = DesktopIniHandler.detect_encoding(\"/test.ini\")\n            assert encoding == \"utf-8-sig\"\n            assert is_utf16 is False\n\n    def test_set_file_hidden_system_attributes(self):\n        \"\"\"测试设置文件隐藏系统属性\"\"\"\n        with patch(\"subprocess.call\", return_value=0) as mock_call:\n            result = DesktopIniHandler.set_file_hidden_system_attributes(\"/file\")\n            assert result is True\n            mock_call.assert_called_once()\n            args = mock_call.call_args[0][0]\n            assert \"attrib +h +s\" in args\n            assert \"/file\" in args or '\"file\"' in args or \"file\" in args\n\n    def test_clear_file_attributes(self):\n        \"\"\"测试清除文件属性\"\"\"\n        with patch(\"subprocess.call\", return_value=0) as mock_call:\n            result = DesktopIniHandler.clear_file_attributes(\"/file\")\n            assert result is True\n            mock_call.assert_called_once()\n            args = mock_call.call_args[0][0]\n            assert \"attrib -s -h\" in args\n\n    def test_delete_file_exists(self):\n        \"\"\"测试删除存在的文件\"\"\"\n        with patch(\"os.path.exists\", return_value=True), patch(\"os.remove\"):\n            result = DesktopIniHandler.delete(\"/folder\")\n            assert result is True\n\n    def test_delete_no_file(self):\n        \"\"\"测试删除不存在的文件\"\"\"\n        with patch(\"os.path.exists\", return_value=False):\n            result = DesktopIniHandler.delete(\"/folder\")\n            assert result is True\n\n    def test_constants(self):\n        \"\"\"测试常量定义\"\"\"\n        assert DESKTOP_INI_ENCODING == \"utf-16\"\n        assert LINE_ENDING == \"\\r\\n\"\n\n\n@pytest.mark.unit\nclass TestEncodingConversionCanceled:\n    \"\"\"编码转换取消异常测试\"\"\"\n\n    def test_exception_creation(self):\n        \"\"\"测试异常创建\"\"\"\n        exc = EncodingConversionCanceled(\"用户取消\")\n        assert str(exc) == \"用户取消\"\n        assert isinstance(exc, Exception)\n"
  },
  {
    "path": "tests/unit/test_folder_handler.py",
    "content": "\"\"\"核心业务逻辑单元测试\"\"\"\n\nfrom unittest.mock import patch\n\nimport pytest\n\nfrom remark.core.folder_handler import MAX_COMMENT_LENGTH, FolderCommentHandler\n\n\n@pytest.mark.unit\nclass TestFolderCommentHandler:\n    \"\"\"文件夹备注处理器测试\"\"\"\n\n    def test_init(self):\n        \"\"\"测试初始化\"\"\"\n        handler = FolderCommentHandler()\n        assert handler is not None\n\n    @pytest.mark.parametrize(\n        \"is_dir,expected\",\n        [(True, True), (False, False)],\n    )\n    def test_supports(self, is_dir, expected):\n        \"\"\"测试支持检测\"\"\"\n        with patch(\"os.path.isdir\", return_value=is_dir):\n            handler = FolderCommentHandler()\n            result = handler.supports(\"/folder\")\n            assert result is expected\n\n    def test_set_comment_not_folder(self, capsys):\n        \"\"\"测试对非文件夹路径设置备注\"\"\"\n        with patch(\"os.path.isdir\", return_value=False):\n            handler = FolderCommentHandler()\n            result = handler.set_comment(\"/file.txt\", \"备注\")\n            assert result is False\n            captured = capsys.readouterr()\n            assert \"不是文件夹\" in captured.out\n\n    def test_set_comment_too_long(self, capsys):\n        \"\"\"测试备注长度超过限制\"\"\"\n        with (\n            patch(\"os.path.isdir\", return_value=True),\n            patch.object(\n                FolderCommentHandler, \"_set_comment_desktop_ini\", return_value=True\n            ) as mock_set,\n        ):\n            handler = FolderCommentHandler()\n            long_comment = \"A\" * 300  # 超过 MAX_COMMENT_LENGTH\n\n            handler.set_comment(\"/folder\", long_comment)\n\n            # 应该被截断到 MAX_COMMENT_LENGTH\n            mock_set.assert_called_once()\n            args = mock_set.call_args[0]\n            assert len(args[1]) == MAX_COMMENT_LENGTH\n            assert args[1] == \"A\" * MAX_COMMENT_LENGTH\n\n    def test_set_comment_success(self):\n        \"\"\"测试成功设置备注\"\"\"\n        with (\n            patch(\"os.path.isdir\", return_value=True),\n            patch.object(FolderCommentHandler, \"_set_comment_desktop_ini\", return_value=True),\n        ):\n            handler = FolderCommentHandler()\n            result = handler.set_comment(\"/folder\", \"测试备注\")\n            assert result is True\n\n    @pytest.mark.parametrize(\n        \"read_return,expected\",\n        [\n            (\"测试备注\", \"测试备注\"),\n            (\"English Comment\", \"English Comment\"),\n            (None, None),\n        ],\n    )\n    def test_get_comment(self, read_return, expected):\n        \"\"\"测试获取备注\"\"\"\n        with patch(\n            \"remark.storage.desktop_ini.DesktopIniHandler.read_info_tip\",\n            return_value=read_return,\n        ):\n            handler = FolderCommentHandler()\n            result = handler.get_comment(\"/folder\")\n            assert result == expected\n\n    def test_delete_comment_no_ini(self, capsys):\n        \"\"\"测试删除不存在的备注\"\"\"\n        with patch(\"remark.storage.desktop_ini.DesktopIniHandler.exists\", return_value=False):\n            handler = FolderCommentHandler()\n            result = handler.delete_comment(\"/folder\")\n            assert result is True\n            captured = capsys.readouterr()\n            assert \"没有备注\" in captured.out\n\n    def test_delete_comment_with_ini(self):\n        \"\"\"测试删除存在的备注\"\"\"\n        with (\n            patch(\"remark.storage.desktop_ini.DesktopIniHandler.exists\", return_value=True),\n            patch(\n                \"remark.storage.desktop_ini.DesktopIniHandler.clear_file_attributes\",\n                return_value=True,\n            ),\n            patch(\n                \"remark.storage.desktop_ini.DesktopIniHandler.remove_info_tip\",\n                return_value=True,\n            ),\n        ):\n            handler = FolderCommentHandler()\n            result = handler.delete_comment(\"/folder\")\n            assert result is True\n\n    def test_delete_comment_clear_failure(self):\n        \"\"\"测试删除时清除属性失败\"\"\"\n        with (\n            patch(\"remark.storage.desktop_ini.DesktopIniHandler.exists\", return_value=True),\n            patch(\n                \"remark.storage.desktop_ini.DesktopIniHandler.clear_file_attributes\",\n                return_value=False,\n            ),\n        ):\n            handler = FolderCommentHandler()\n            result = handler.delete_comment(\"/folder\")\n            assert result is False\n\n    def test_set_comment_desktop_ini(self):\n        \"\"\"测试 desktop.ini 设置备注的内部方法\"\"\"\n        with (\n            patch(\n                \"remark.storage.desktop_ini.DesktopIniHandler.write_info_tip\",\n                return_value=True,\n            ),\n            patch(\n                \"remark.storage.desktop_ini.DesktopIniHandler.clear_file_attributes\",\n                return_value=True,\n            ),\n            patch(\n                \"remark.storage.desktop_ini.DesktopIniHandler.set_file_hidden_system_attributes\",\n                return_value=True,\n            ),\n            patch(\n                \"remark.storage.desktop_ini.DesktopIniHandler.set_folder_system_attributes\",\n                return_value=True,\n            ),\n        ):\n            handler = FolderCommentHandler()\n            result = handler._set_comment_desktop_ini(\"/folder\", \"备注\")\n            assert result is True\n\n    def test_max_comment_length_constant(self):\n        \"\"\"测试最大备注长度常量\"\"\"\n        assert MAX_COMMENT_LENGTH == 260\n"
  },
  {
    "path": "tests/unit/test_i18n.py",
    "content": "\"\"\"国际化 (i18n) 单元测试\"\"\"\n\nfrom unittest.mock import MagicMock, patch\n\nimport pytest\n\nfrom remark.i18n import (\n    SUPPORTED_LANGUAGES,\n    _get_windows_locale,\n    get_system_language,\n    gettext_function,\n    init_translation,\n    ngettext_function,\n    set_language,\n)\n\n\n@pytest.mark.unit\nclass TestGetWindowsLocale:\n    \"\"\"Windows locale 获取测试\"\"\"\n\n    @pytest.mark.parametrize(\n        \"locale_name,expected\",\n        [\n            (\"zh-CN\", \"zh-CN\"),\n            (\"en-US\", \"en-US\"),\n            (\"zh-TW\", \"zh-TW\"),\n        ],\n    )\n    def test_get_windows_locale_success(self, locale_name, expected):\n        \"\"\"测试成功获取 Windows locale\"\"\"\n        mock_buffer = MagicMock()\n        mock_buffer.value = locale_name\n        mock_buffer.strip.return_value = locale_name\n\n        with patch(\"ctypes.create_unicode_buffer\", return_value=mock_buffer):\n            with patch(\"ctypes.windll.kernel32.GetUserDefaultLocaleName\", return_value=len(locale_name)):\n                result = _get_windows_locale()\n                assert result == expected\n\n    def test_get_windows_locale_api_fails(self):\n        \"\"\"测试 Windows API 调用失败\"\"\"\n        with patch(\"ctypes.windll.kernel32.GetUserDefaultLocaleName\", return_value=0):\n            result = _get_windows_locale()\n            assert result is None\n\n    def test_get_windows_locale_exception(self):\n        \"\"\"测试 Windows API 抛出异常\"\"\"\n        with patch(\"ctypes.windll.kernel32.GetUserDefaultLocaleName\", side_effect=OSError):\n            result = _get_windows_locale()\n            assert result is None\n\n\n@pytest.mark.unit\nclass TestGetSystemLanguage:\n    \"\"\"系统语言获取测试\"\"\"\n\n    @pytest.mark.parametrize(\n        \"windows_locale,expected\",\n        [\n            (\"zh-CN\", \"zh\"),\n            (\"en-US\", \"en\"),\n            (\"zh-TW\", \"zh\"),\n        ],\n    )\n    def test_windows_platform_priority(self, windows_locale, expected):\n        \"\"\"测试 Windows 平台优先使用 Windows API（当 LANG 未设置时）\"\"\"\n        with patch(\"remark.i18n.platform.system\", return_value=\"Windows\"):\n            with patch(\"remark.i18n._get_windows_locale\", return_value=windows_locale):\n                with patch.dict(\"os.environ\", {}, clear=True):\n                    result = get_system_language()\n                    assert result in SUPPORTED_LANGUAGES\n                    assert result == expected\n\n    @pytest.mark.parametrize(\n        \"windows_locale\",\n        [\"ja-JP\", \"ko-KR\", \"fr-FR\"],\n    )\n    def test_windows_unsupported_locale_fallback(self, windows_locale):\n        \"\"\"测试不支持的语言回退到默认\"\"\"\n        with patch(\"remark.i18n.platform.system\", return_value=\"Windows\"):\n            with patch(\"remark.i18n._get_windows_locale\", return_value=windows_locale):\n                with patch.dict(\"os.environ\", {}, clear=True):\n                    with patch(\"remark.i18n.locale.getlocale\", return_value=(None, None)):\n                        result = get_system_language()\n                        assert result == \"en\"\n\n    def test_windows_api_null_fallback_to_locale(self):\n        \"\"\"测试 Windows API 返回 None 时回退到 locale.getlocale()\"\"\"\n        with patch(\"remark.i18n.platform.system\", return_value=\"Windows\"):\n            with patch(\"remark.i18n._get_windows_locale\", return_value=None):\n                with patch.dict(\"os.environ\", {}, clear=True):\n                    with patch(\"remark.i18n.locale.getlocale\", return_value=(\"zh_CN\", \"cp1252\")):\n                        result = get_system_language()\n                        assert result == \"zh\"\n\n    @pytest.mark.parametrize(\n        \"locale_value,expected\",\n        [\n            (\"zh_CN\", \"zh\"),\n            (\"zh-CN\", \"zh\"),\n            (\"Chinese_China\", \"zh\"),\n            (\"en_US\", \"en\"),\n            (\"en-GB\", \"en\"),\n            (\"English_United States\", \"en\"),\n        ],\n    )\n    def test_locale_getlocale_variations(self, locale_value, expected):\n        \"\"\"测试 locale.getlocale() 的各种返回值格式\"\"\"\n        with patch(\"remark.i18n.platform.system\", return_value=\"Linux\"):\n            with patch(\"remark.i18n._get_windows_locale\", return_value=None):\n                with patch.dict(\"os.environ\", {}, clear=True):\n                    with patch(\"remark.i18n.locale.getlocale\", return_value=(locale_value, \"cp1252\")):\n                        result = get_system_language()\n                        assert result == expected\n\n    @pytest.mark.parametrize(\n        \"env_lang,expected\",\n        [\n            (\"zh_CN.UTF-8\", \"zh\"),\n            (\"en_US.UTF-8\", \"en\"),\n            (\"zh.UTF-8\", \"zh\"),\n            (\"en\", \"en\"),\n        ],\n    )\n    def test_lang_environment_variable(self, env_lang, expected):\n        \"\"\"测试 LANG 环境变量\"\"\"\n        with patch(\"remark.i18n.platform.system\", return_value=\"Linux\"):\n            with patch(\"remark.i18n._get_windows_locale\", return_value=None):\n                with patch.dict(\"os.environ\", {\"LANG\": env_lang}):\n                    with patch(\"remark.i18n.locale.getlocale\", return_value=(None, None)):\n                        result = get_system_language()\n                        assert result == expected\n\n    def test_all_methods_fallback_to_default(self):\n        \"\"\"测试所有方法都失败时回退到默认语言\"\"\"\n        with patch(\"remark.i18n.platform.system\", return_value=\"Linux\"):\n            with patch(\"remark.i18n._get_windows_locale\", return_value=None):\n                with patch.dict(\"os.environ\", {}, clear=True):\n                    with patch(\"remark.i18n.locale.getlocale\", return_value=(None, None)):\n                        result = get_system_language()\n                        assert result == \"en\"\n\n\n@pytest.mark.unit\nclass TestInitTranslation:\n    \"\"\"翻译初始化测试\"\"\"\n\n    @pytest.mark.parametrize(\n        \"language,expected_domain\",\n        [\n            (\"en\", \"messages\"),\n            (\"zh\", \"messages\"),\n        ],\n    )\n    def test_init_translation_supported_language(self, language, expected_domain):\n        \"\"\"测试支持的语言初始化\"\"\"\n        translator = init_translation(language)\n        assert translator is not None\n        assert translator.gettext(\"test\") == \"test\"\n\n    def test_init_translation_unsupported_language_fallback(self):\n        \"\"\"测试不支持的语言回退到英文\"\"\"\n        translator = init_translation(\"fr\")\n        assert translator is not None\n        # 应该回退到 NullTranslations 或英文翻译\n        assert translator.gettext(\"test\") == \"test\"\n\n    def test_init_translation_none_uses_system(self):\n        \"\"\"测试 None 作为参数使用系统语言\"\"\"\n        with patch(\"remark.i18n.get_system_language\", return_value=\"zh\"):\n            translator = init_translation(None)\n            assert translator is not None\n\n\n@pytest.mark.unit\nclass TestSetLanguage:\n    \"\"\"设置语言测试\"\"\"\n\n    def test_set_language_updates_translator(self):\n        \"\"\"测试设置语言更新翻译器\"\"\"\n        set_language(\"zh\")\n        # 验证语言已被设置（通过调用 gettext_function）\n        result = gettext_function(\"Windows Folder Remark Tool\")\n        # 如果成功加载中文翻译，应该返回中文字符串\n        # 否则返回原字符串\n        assert isinstance(result, str)\n\n\n@pytest.mark.unit\nclass TestGetTextFunction:\n    \"\"\"翻译函数测试\"\"\"\n\n    def test_gettext_function_returns_string(self):\n        \"\"\"测试 gettext_function 返回字符串\"\"\"\n        result = gettext_function(\"test message\")\n        assert isinstance(result, str)\n\n    def test_ngettext_function_singular(self):\n        \"\"\"测试 ngettext 单数形式\"\"\"\n        result = ngettext_function(\"one item\", \"many items\", 1)\n        assert isinstance(result, str)\n\n    def test_ngettext_function_plural(self):\n        \"\"\"测试 ngettext 复数形式\"\"\"\n        result = ngettext_function(\"one item\", \"many items\", 10)\n        assert isinstance(result, str)\n"
  },
  {
    "path": "tests/unit/test_path_resolver.py",
    "content": "\"\"\"\n路径解析模块单元测试\n\"\"\"\n\nimport os\nfrom pathlib import Path, PureWindowsPath\n\nimport pytest\n\nfrom remark.utils.path_resolver import find_candidates\n\n\nclass TestFindCandidates:\n    \"\"\"测试 find_candidates 函数\"\"\"\n\n    @pytest.mark.skipif(os.name != \"nt\", reason=\"Windows only\")\n    def test_single_space_split(self, fs):\n        \"\"\"\n        用例: 单次空格分割\n\n        输入: [\"D:\\\\My\", \"Documents\"]\n        模拟环境:\n            D:\\\\ 目录下存在文件夹 \"My Documents\"\n            listdir(\"D:\\\\\") -> [\"My Documents\", \"Users\", \"Windows\", \"test.txt\"]\n\n        预期输出:\n            [(\"D:\\\\My Documents\", [], \"folder\")]\n        \"\"\"\n        fs.create_dir(\"D:\\\\My Documents\")\n        fs.create_dir(\"D:\\\\Users\")\n        fs.create_dir(\"D:\\\\Windows\")\n        fs.create_file(\"D:\\\\test.txt\")\n\n        result = find_candidates([\"D:\\\\My\", \"Documents\"])\n\n        assert len(result) == 1\n        path, remaining, type_ = result[0]\n        assert path == Path(\"D:\\\\My Documents\")\n        assert remaining == []\n        assert type_ == \"folder\"\n\n    @pytest.mark.skipif(os.name != \"nt\", reason=\"Windows only\")\n    def test_multi_space_folder_name_with_comment(self, fs):\n        \"\"\"\n        用例: 文件夹名包含多个空格（有备注）\n\n        输入: [\"D:\\\\Here\", \"Is\", \"My\", \"Folder\", \"备注\", \"内容\"]\n        模拟环境:\n            D:\\\\ 目录下存在文件夹 \"Here Is My Folder\"\n            listdir(\"D:\\\\\") -> [\"Here Is My Folder\", \"Users\"]\n\n        预期输出:\n            [(\"D:\\\\Here Is My Folder\", [\"备注\", \"内容\"], \"folder\")]\n\n        说明: \"Here Is My Folder\" 被空格分割成 4 个参数，剩余 2 个参数作为备注\n        \"\"\"\n        fs.create_dir(\"D:\\\\Here Is My Folder\")\n        fs.create_dir(\"D:\\\\Users\")\n\n        result = find_candidates([\"D:\\\\Here\", \"Is\", \"My\", \"Folder\", \"备注\", \"内容\"])\n\n        assert len(result) == 1\n        path, remaining, type_ = result[0]\n        assert path == Path(\"D:\\\\Here Is My Folder\")\n        assert remaining == [\"备注\", \"内容\"]\n        assert type_ == \"folder\"\n\n    @pytest.mark.skipif(os.name != \"nt\", reason=\"Windows only\")\n    def test_valid_folder_with_comment(self, fs):\n        \"\"\"\n        用例: 路径有效 + 备注\n\n        输入: [\"D:\\\\ValidFolder\", \"备注\"]\n        模拟环境:\n            D:\\\\ValidFolder 目录存在且是文件夹\n\n        预期输出:\n            [(\"D:\\\\ValidFolder\", [\"备注\"], \"folder\")]\n\n        说明: 路径本身完整，剩余参数作为备注\n        \"\"\"\n        fs.create_dir(\"D:\\\\ValidFolder\")\n\n        result = find_candidates([\"D:\\\\ValidFolder\", \"备注\"])\n\n        assert len(result) == 1\n        path, remaining, type_ = result[0]\n        assert path == Path(\"D:\\\\ValidFolder\")\n        assert remaining == [\"备注\"]\n        assert type_ == \"folder\"\n\n    @pytest.mark.skipif(os.name != \"nt\", reason=\"Windows only\")\n    def test_no_match(self, fs):\n        \"\"\"\n        用例: 无匹配\n\n        输入: [\"D:\\\\Invalid\", \"Path\"]\n        模拟环境:\n            D:\\\\ 目录下只有 \"Users\", \"Windows\" 文件夹\n            listdir(\"D:\\\\\") -> [\"Users\", \"Windows\"]\n            \"D:\\\\Invalid\" 和 \"D:\\\\Invalid Path\" 都不存在\n\n        预期输出:\n            [] (空列表)\n\n        说明: 无法匹配任何路径，返回空列表\n        \"\"\"\n        fs.create_dir(\"D:\\\\Users\")\n        fs.create_dir(\"D:\\\\Windows\")\n\n        result = find_candidates([\"D:\\\\Invalid\", \"Path\"])\n\n        assert result == []\n\n    @pytest.mark.skipif(os.name != \"nt\", reason=\"Windows only\")\n    def test_both_single_name_and_extended_exist(self, fs):\n        \"\"\"\n        用例: 单个文件夹名和扩展路径都存在\n\n        输入: [\"My\", \"Files\", \"App\"]\n        模拟环境:\n            当前目录下同时存在 \"My\" 和 \"My Files\" 两个文件夹\n            listdir(\".\") -> [\"My\", \"My Files\", \"Other\"]\n\n        预期输出 (按优先级排序):\n            [\n                (包含 \"My Files\" 的路径, [\"App\"], \"folder\"),      # 消耗 2 个参数，剩余 1 个\n                (包含 \"My\" 的路径, [\"Files\", \"App\"], \"folder\"),   # 消耗 1 个参数，剩余 2 个\n            ]\n\n        说明: 同时匹配 \"My Files\" 和 \"My\"，按剩余参数数量升序排序（剩余越少优先级越高）\n        \"\"\"\n        fs.create_dir(\"My\")\n        fs.create_dir(\"My Files\")\n        fs.create_dir(\"Other\")\n\n        result = find_candidates([\"My\", \"Files\", \"App\"])\n\n        assert len(result) == 2\n        path1, remaining1, type1 = result[0]\n        path2, remaining2, type2 = result[1]\n\n        # 验证第一个候选是 \"My Files\"（包含完整路径）\n        assert path1 == Path(\"My Files\")\n        assert remaining1 == [\"App\"]\n        assert type1 == \"folder\"\n\n        # 验证第二个候选是 \"My\"\n        assert path2 == Path(\"My\")\n        assert remaining2 == [\"Files\", \"App\"]\n        assert type2 == \"folder\"\n\n    @pytest.mark.skipif(os.name != \"nt\", reason=\"Windows only\")\n    def test_subdirectory_recursive_matching(self, fs):\n        \"\"\"\n        用例: 子目录递归匹配（跨参数的子目录链）\n\n        输入: [\"My\", \"Folder/App\", \"Folder/New\", \"Folder\", \"测试内容\"]\n        模拟环境:\n            ./My                                (文件夹)\n            ./My Folder/                        (文件夹)\n            ./My Folder/App                     (文件夹)\n            ./My Folder/App Folder/             (文件夹)\n            ./My Folder/App Folder1/            (文件夹)\n            ./My Folder/App Folder/New Folder/  (文件夹)\n            ./Other                             (文件夹)\n\n            listdir(\".\")            -> [\"My\", \"My Folder\", \"Other\"]\n            listdir(\"./My Folder\") -> [\"App\", \"App Folder\", \"App Folder1\"]\n            listdir(\"./My Folder/App Folder\") -> [\"New Folder\", \"Other\"]\n\n        预期输出 (按优先级排序，按剩余参数数量升序):\n            [\n                (\"My Folder/App Folder/New Folder\", [\"测试内容\"], \"folder\"),      # 剩余 1 个\n                (\"My Folder/App\", [\"Folder/New\", \"Folder\", \"测试内容\"], \"folder\"), # 剩余 3 个\n                (\"My\", [\"Folder/App\", \"Folder/New\", \"Folder\", \"测试内容\"], \"folder\"), # 剩余 4 个\n            ]\n        \"\"\"\n        fs.create_dir(\"My\")\n        fs.create_dir(\"My Folder/App\")\n        fs.create_dir(\"My Folder/App Folder/New Folder\")\n        fs.create_dir(\"My Folder/App Folder1\")\n        fs.create_dir(\"Other\")\n\n        result = find_candidates([\"My\", \"Folder/App\", \"Folder/New\", \"Folder\", \"测试内容\"])\n\n        # 严格验证所有候选（按优先级排序）\n        assert len(result) == 3\n\n        # 候选 1: 最大匹配\n        path1, remaining1, type1 = result[0]\n        assert path1 == Path(\"My Folder/App Folder/New Folder\")\n        assert remaining1 == [\"测试内容\"]\n        assert type1 == \"folder\"\n\n        # 候选 2: 中间匹配\n        path2, remaining2, type2 = result[1]\n        assert path2 == Path(\"My Folder/App\")\n        assert remaining2 == [\"Folder/New\", \"Folder\", \"测试内容\"]\n        assert type2 == \"folder\"\n\n        # 候选 3: 基础匹配\n        path3, remaining3, type3 = result[2]\n        assert path3 == Path(\"My\")\n        assert remaining3 == [\"Folder/App\", \"Folder/New\", \"Folder\", \"测试内容\"]\n        assert type3 == \"folder\"\n\n    def test_empty_args(self):\n        \"\"\"\n        用例: 空参数列表\n\n        输入: []\n        模拟环境: (无需模拟)\n\n        预期输出:\n            [] (空列表)\n        \"\"\"\n        result = find_candidates([])\n        assert result == []\n\n    @pytest.mark.skipif(os.name != \"nt\", reason=\"Windows only\")\n    def test_multiple_slashes_in_single_arg(self, fs):\n        \"\"\"\n        用例: 单个参数包含多个路径分隔符\n\n        输入: [\"My\", \"Folder/App/Deep/New\", \"备注\"]\n        模拟环境:\n            ./My                       (文件夹)\n            ./My Folder/               (文件夹)\n            ./My Folder/App/           (文件夹)\n            ./My Folder/App/Deep/      (文件夹)\n            ./My Folder/App/Deep/New/  (文件夹)\n\n            listdir(\".\")          -> [\"My\", \"My Folder\"]\n            listdir(\"./My Folder\") -> [\"App\"]\n            listdir(\"./My Folder/App\") -> [\"Deep\"]\n            listdir(\"./My Folder/App/Deep\") -> [\"New\"]\n\n        预期输出 (按优先级排序，按剩余参数数量升序):\n            [\n                (\"My Folder/App/Deep/New\", [\"备注\"], \"folder\"),      # 剩余 1 个\n                (\"My\", [\"Folder/App/Deep/New\", \"备注\"], \"folder\"),   # 剩余 2 个\n            ]\n\n        说明:\n            - \"Folder/App/Deep/New\" 是单个参数，包含 3 个 /，表示 4 级子目录\n            - _find_subpath_candidates 应该递归处理每一级\n        \"\"\"\n        fs.create_dir(\"My\")\n        fs.create_dir(\"My Folder/App/Deep/New\")\n\n        result = find_candidates([\"My\", \"Folder/App/Deep/New\", \"备注\"])\n\n        # 严格验证所有候选\n        assert len(result) == 2\n\n        # 候选 1: 最大匹配\n        path1, remaining1, type1 = result[0]\n        assert path1 == Path(\"My Folder/App/Deep/New\")\n        assert remaining1 == [\"备注\"]\n        assert type1 == \"folder\"\n\n        # 候选 2: 基础匹配\n        path2, remaining2, type2 = result[1]\n        assert path2 == Path(\"My\")\n        assert remaining2 == [\"Folder/App/Deep/New\", \"备注\"]\n        assert type2 == \"folder\"\n\n    @pytest.mark.skipif(os.name != \"nt\", reason=\"Windows only\")\n    def test_absolute_path_with_slash_arg(self, fs):\n        \"\"\"\n        用例: 绝对路径 + 含 / 的参数（跨参数空格匹配）\n\n        输入: [\"D:\\\\My\", \"Folder/Sub\", \"备注\"]\n        模拟环境:\n            D:\\\\My            (文件夹)\n            D:\\\\My Folder\\\\    (文件夹)\n            D:\\\\My Folder\\\\Sub\\\\ (文件夹)\n\n            listdir(\"D:\\\\\") -> [\"My\", \"My Folder\"]\n            listdir(\"D:\\\\My Folder\") -> [\"Sub\"]\n\n        预期输出 (按优先级排序，按剩余参数数量升序):\n            [\n                (\"D:\\\\My Folder\\\\Sub\", [\"备注\"], \"folder\"),      # 剩余 1 个\n                (\"D:\\\\My\", [\"Folder/Sub\", \"备注\"], \"folder\"),    # 剩余 2 个\n            ]\n\n        说明:\n            - \"D:\\\\My\" 是绝对路径，\"Folder/Sub\" 的 \"Folder\" 与 \"My\" 正则匹配 → \"My Folder\"\n            - \"Sub\" 在 \"My Folder\" 中匹配子目录\n            - 测试绝对路径与跨参数 / 处理的组合场景\n        \"\"\"\n        fs.create_dir(\"D:\\\\My\")\n        fs.create_dir(\"D:\\\\My Folder\\\\Sub\")\n\n        result = find_candidates([\"D:\\\\My\", \"Folder/Sub\", \"备注\"])\n\n        # 严格验证所有候选\n        assert len(result) == 2\n\n        # 候选 1: 最大匹配\n        path1, remaining1, type1 = result[0]\n        assert path1 == Path(\"D:\\\\My Folder\\\\Sub\")\n        assert remaining1 == [\"备注\"]\n        assert type1 == \"folder\"\n\n        # 候选 2: 基础匹配\n        path2, remaining2, type2 = result[1]\n        assert path2 == Path(\"D:\\\\My\")\n        assert remaining2 == [\"Folder/Sub\", \"备注\"]\n        assert type2 == \"folder\"\n\n    @pytest.mark.skipif(os.name != \"nt\", reason=\"Windows only\")\n    def test_empty_directory(self, fs):\n        \"\"\"\n        用例: 空目录处理\n\n        输入: [\"D:\\\\Empty\", \"Folder\", \"备注\"]\n        模拟环境:\n            D:\\\\ 目录下存在空文件夹 \"Empty Folder\"\n            listdir(\"D:\\\\\") -> [\"Empty Folder\"]\n            listdir(\"D:\\\\Empty Folder\") -> []\n\n        预期输出:\n            [(\"D:\\\\Empty Folder\\\\App\", [\"备注\"], \"folder\")]\n\n        说明: 匹配到 \"Empty Folder\" 后继续搜索，发现目录为空，加入候选\n        \"\"\"\n        fs.create_dir(\"D:\\\\Empty Folder\")\n\n        result = find_candidates([\"D:\\\\Empty\", \"Folder\\\\App\", \"备注\"])\n\n        assert len(result) == 0\n\n    @pytest.mark.skipif(os.name != \"nt\", reason=\"Windows only\")\n    def test_separator_no_match(self, fs):\n        \"\"\"\n        用例: 分隔符匹配失败\n\n        输入: [\"D:\\\\My\", \"Invalid/Sub\", \"备注\"]\n        模拟环境:\n            D:\\\\My            (文件夹)\n            D:\\\\My Folder\\\\    (文件夹)\n            listdir(\"D:\\\\\") -> [\"My\", \"My Folder\"]\n            listdir(\"D:\\\\My Folder\") -> [\"Sub\"]\n\n        预期输出:\n            [(\"D:\\\\My\", [\"Invalid/Sub\", \"备注\"], \"folder\")]\n\n        说明: \"My\" 匹配后，\"Invalid\" 在 \"My Folder\" 中无匹配，搜索结束\n        \"\"\"\n        fs.create_dir(\"D:\\\\My\")\n        fs.create_dir(\"D:\\\\My Folder\\\\Sub\")\n\n        result = find_candidates([\"D:\\\\My\", \"Invalid/Sub\", \"备注\"])\n\n        assert len(result) == 1\n        path, remaining, type_ = result[0]\n        assert path == Path(\"D:\\\\My\")\n        assert remaining == [\"Invalid/Sub\", \"备注\"]\n        assert type_ == \"folder\"\n\n    @pytest.mark.skipif(os.name != \"nt\", reason=\"Windows only\")\n    def test_file_skipped(self, fs):\n        \"\"\"\n        用例: 跳过文件（非目录路径）\n\n        输入: [\"D:\\\\My\", \"File\", \"Folder\", \"备注内容\"]\n        模拟环境:\n            D:\\\\My File          (文件，应被跳过)\n            D:\\\\My File Folder\\\\  (文件夹)\n            listdir(\"D:\\\\\") -> [\"My File\", \"My File Folder\"]\n\n        预期输出:\n            [(\"D:\\\\My File Folder\", [\"备注内容\"], \"folder\")]\n\n        说明: \"My File\" 和 \"My File Folder\" 都匹配 \"My File\"，但 \"My File\" 是文件被跳过\n        \"\"\"\n        fs.create_file(\"D:\\\\My File\")\n        fs.create_dir(\"D:\\\\My File Folder\")\n\n        result = find_candidates([\"D:\\\\My\", \"File\\\\Folder\", \"备注内容\"])\n\n        assert len(result) == 0\n\n\n@pytest.mark.unit\nclass TestGetCurrentWorkingPath:\n    \"\"\"测试 get_current_working_path 函数\"\"\"\n\n    def test_empty_string(self):\n        \"\"\"空字符串返回 (\".\", Cursor(0, 0))\"\"\"\n        from remark.utils.path_resolver import get_current_working_path\n\n        working, cursor = get_current_working_path(\"\")\n        assert working == PureWindowsPath()\n        assert cursor.arg_index == 0\n        assert cursor.char_index == 0\n\n    def test_absolute_root(self):\n        r\"\"\"根目录 C:\\ 返回 (C:\\, Cursor(0, 2))\"\"\"\n        from remark.utils.path_resolver import get_current_working_path\n\n        working, cursor = get_current_working_path(\"C:\\\\\")\n        assert working == PureWindowsPath(\"C:\\\\\")\n        assert cursor.arg_index == 0\n        assert cursor.char_index == 2\n\n    def test_absolute_with_trailing_slash(self):\n        r\"\"\"C:\\MyFolder\\ 返回 (C:\\, Cursor(0, 2))\"\"\"\n        from remark.utils.path_resolver import get_current_working_path\n\n        working, cursor = get_current_working_path(\"C:\\\\MyFolder\\\\\")\n        assert working == PureWindowsPath(\"C:\\\\\")\n        assert cursor.arg_index == 0\n        assert cursor.char_index == 2\n\n    def test_absolute_without_trailing_slash(self):\n        r\"\"\"C:\\MyFolder 返回 (C:, Cursor(0, 2))\"\"\"\n        from remark.utils.path_resolver import get_current_working_path\n\n        working, cursor = get_current_working_path(\"C:\\\\MyFolder\")\n        assert working == PureWindowsPath(\"C:\\\\\")\n        assert cursor.arg_index == 0\n        assert cursor.char_index == 2\n\n    def test_absolute_multi_level(self):\n        r\"\"\"C:\\MyFolder\\Other 返回 (C:\\MyFolder, Cursor(0, 11))\"\"\"\n        from remark.utils.path_resolver import get_current_working_path\n\n        working, cursor = get_current_working_path(\"C:\\\\MyFolder\\\\Other\")\n        assert working == PureWindowsPath(\"C:\\\\MyFolder\")\n        assert cursor.arg_index == 0\n        assert cursor.char_index == 11\n\n    def test_forward_slash_normalization(self):\n        r\"\"\"C:/My/Folder 规范化后返回 (C:\\My, Cursor(0, 5))\"\"\"\n        from remark.utils.path_resolver import get_current_working_path\n\n        working, cursor = get_current_working_path(\"C:/My/Folder\")\n        assert working == PureWindowsPath(\"C:\\\\My\")\n        assert cursor.arg_index == 0\n        assert cursor.char_index == 5\n\n    def test_relative_single(self):\n        \"\"\"MyFolder 返回 (., Cursor(0, 0))\"\"\"\n        from remark.utils.path_resolver import get_current_working_path\n\n        working, cursor = get_current_working_path(\"MyFolder\")\n        assert working == PureWindowsPath()\n        assert cursor.arg_index == 0\n        assert cursor.char_index == -1\n\n    def test_relative_with_backslash(self):\n        r\"\"\"My\\Folder 返回 (My, Cursor(0, 2))\"\"\"\n        from remark.utils.path_resolver import get_current_working_path\n\n        working, cursor = get_current_working_path(\"My\\\\Folder\")\n        assert working == PureWindowsPath(\"My\")\n        assert cursor.arg_index == 0\n        assert cursor.char_index == 2\n"
  },
  {
    "path": "tests/unit/test_platform.py",
    "content": "\"\"\"平台检测单元测试\"\"\"\n\nfrom unittest.mock import patch\n\nimport pytest\n\nfrom remark.utils.platform import check_platform\n\n\n@pytest.mark.unit\nclass TestPlatform:\n    \"\"\"平台检测测试\"\"\"\n\n    @pytest.mark.parametrize(\n        \"system_name,expected\",\n        [\n            (\"Windows\", True),\n            # 注意：check_platform 使用 == \"Windows\" 比较，是大小写敏感的\n            # (\"windows\", True),   # 小写会失败\n            # (\"WINDOWS\", True),   # 大写会失败\n        ],\n    )\n    def test_check_platform_windows(self, system_name, expected):\n        \"\"\"测试 Windows 平台检测\"\"\"\n        with patch(\"remark.utils.platform.platform.system\", return_value=system_name):\n            result = check_platform()\n            assert result is expected\n\n    @pytest.mark.parametrize(\n        \"system_name\",\n        [\"Linux\", \"Darwin\", \"FreeBSD\", \"SunOS\"],\n    )\n    def test_check_platform_non_windows(self, system_name, capsys):\n        \"\"\"测试非 Windows 平台\"\"\"\n        with patch(\"remark.utils.platform.platform.system\", return_value=system_name):\n            result = check_platform()\n            assert result is False\n            captured = capsys.readouterr()\n            assert \"此工具为 Windows 系统\" in captured.out\n\n    @pytest.mark.parametrize(\n        \"system_name\",\n        [\"windows\", \"WINDOWS\", \"WiNdOwS\"],  # 大小写不匹配\n    )\n    def test_check_platform_case_sensitive(self, system_name, capsys):\n        \"\"\"测试平台检测大小写敏感\"\"\"\n        with patch(\"remark.utils.platform.platform.system\", return_value=system_name):\n            result = check_platform()\n            # 这些会返回 False 因为不等于 \"Windows\"\n            assert result is False\n            captured = capsys.readouterr()\n            assert \"此工具为 Windows 系统\" in captured.out\n"
  },
  {
    "path": "tests/unit/test_registry.py",
    "content": "\"\"\"\n注册表操作单元测试\n\"\"\"\n\nfrom unittest.mock import MagicMock, patch\n\nimport pytest\n\nfrom remark.utils import registry\n\n\n@pytest.mark.unit\nclass TestRegistry:\n    \"\"\"注册表操作测试\"\"\"\n\n    @patch(\"remark.utils.registry.sys\")\n    def test_get_executable_path_frozen(self, mock_sys):\n        \"\"\"测试获取打包后的 exe 路径\"\"\"\n        mock_sys.frozen = True\n        mock_sys.executable = r\"C:\\Program Files\\windows-folder-remark.exe\"\n\n        result = registry.get_executable_path()\n\n        assert result == r\"C:\\Program Files\\windows-folder-remark.exe\"\n\n    @patch(\"remark.utils.registry.sys\")\n    @patch(\"remark.utils.registry.os.path\")\n    def test_get_executable_path_dev(self, mock_path, mock_sys):\n        \"\"\"测试获取开发环境的脚本路径\"\"\"\n        mock_sys.frozen = False\n        mock_path.abspath.side_effect = lambda x: x.replace(\"..\", \"abs\")\n        mock_path.dirname.side_effect = lambda x: x.replace(\"registry.py\", \"\").replace(\n            \"utils\\\\\", \"\"\n        )\n        mock_path.join.return_value = \"remark.py\"\n\n        with patch(\"remark.utils.registry.__file__\", \"remark/utils/registry.py\"):\n            result = registry.get_executable_path()\n\n        assert result == \"remark.py\"\n\n    @patch(\"remark.utils.registry.winreg\")\n    @patch(\"remark.utils.registry.get_executable_path\")\n    def test_install_context_menu_success(self, mock_get_exe, mock_winreg):\n        \"\"\"测试成功安装右键菜单\"\"\"\n        mock_get_exe.return_value = r\"C:\\test.exe\"\n        mock_key = MagicMock()\n        mock_winreg.CreateKey.return_value = mock_key\n\n        result = registry.install_context_menu()\n\n        assert result is True\n        # 验证注册表键被创建\n        assert mock_winreg.CreateKey.call_count == 2\n        # 验证 SetValueEx 被调用（设置默认值和图标）\n        assert mock_winreg.SetValueEx.call_count == 3\n\n    @patch(\"remark.utils.registry.winreg\")\n    def test_install_context_menu_permission_error(self, mock_winreg):\n        \"\"\"测试权限不足时的安装\"\"\"\n        mock_winreg.CreateKey.side_effect = PermissionError()\n\n        result = registry.install_context_menu()\n\n        assert result is False\n\n    @patch(\"remark.utils.registry.winreg\")\n    @patch(\"remark.utils.registry.get_executable_path\")\n    def test_uninstall_context_menu_success(self, mock_get_exe, mock_winreg):\n        \"\"\"测试成功卸载右键菜单\"\"\"\n        mock_get_exe.return_value = r\"C:\\test.exe\"\n\n        result = registry.uninstall_context_menu()\n\n        assert result is True\n        # 验证 DeleteKey 被调用两次（command 和主键）\n        assert mock_winreg.DeleteKey.call_count == 2\n\n    @patch(\"remark.utils.registry.winreg\")\n    def test_uninstall_context_menu_not_installed(self, mock_winreg):\n        \"\"\"测试卸载未安装的菜单（键不存在）\"\"\"\n        mock_winreg.DeleteKey.side_effect = FileNotFoundError()\n\n        result = registry.uninstall_context_menu()\n\n        assert result is True\n\n    @patch(\"remark.utils.registry.winreg\")\n    def test_uninstall_context_menu_permission_error(self, mock_winreg):\n        \"\"\"测试权限不足时的卸载\"\"\"\n        mock_winreg.DeleteKey.side_effect = PermissionError()\n\n        result = registry.uninstall_context_menu()\n\n        assert result is False\n"
  },
  {
    "path": "tests/unit/test_release.py",
    "content": "\"\"\"release.py 脚本单元测试\"\"\"\n\nfrom unittest.mock import Mock, patch\n\nimport pytest\n\nfrom scripts.release import (\n    check_branch,\n    check_remote_sync,\n    check_working_directory_clean,\n    validate_version_increment,\n)\n\n\nclass TestValidateVersionIncrement:\n    \"\"\"测试版本号递增验证\"\"\"\n\n    def test_patch_increment(self):\n        \"\"\"测试补丁版本递增\"\"\"\n        assert validate_version_increment(\"1.0.0\", \"1.0.1\")\n        assert validate_version_increment(\"2.3.4\", \"2.3.5\")\n\n    def test_minor_increment(self):\n        \"\"\"测试次版本递增\"\"\"\n        assert validate_version_increment(\"1.0.0\", \"1.1.0\")\n        assert validate_version_increment(\"2.3.4\", \"2.4.0\")\n\n    def test_major_increment(self):\n        \"\"\"测试主版本递增\"\"\"\n        assert validate_version_increment(\"1.0.0\", \"2.0.0\")\n        assert validate_version_increment(\"2.3.4\", \"3.0.0\")\n\n    def test_same_version_fails(self):\n        \"\"\"测试相同版本号应失败\"\"\"\n        assert not validate_version_increment(\"1.0.0\", \"1.0.0\")\n\n    def test_lower_version_fails(self):\n        \"\"\"测试更低版本号应失败\"\"\"\n        assert not validate_version_increment(\"2.0.0\", \"1.9.9\")\n        assert not validate_version_increment(\"1.2.3\", \"1.2.2\")\n        assert not validate_version_increment(\"2.5.0\", \"2.4.9\")\n\n\n@pytest.mark.parametrize(\n    (\"status_output\", \"expected_result\"),\n    [\n        (\"\", True),  # 无输出表示干净\n        (\"M file.txt\", False),  # 有修改\n        (\"?? new.txt\", False),  # 有新文件\n        (\"M modified.txt\\n?? new.txt\", False),  # 多种改动\n    ],\n)\ndef test_check_working_directory_clean(status_output, expected_result):\n    \"\"\"测试工作目录状态检查\"\"\"\n    with patch(\"subprocess.run\") as mock_run:\n        mock_result = Mock()\n        mock_result.stdout = status_output\n        mock_run.return_value = mock_result\n\n        result = check_working_directory_clean()\n        assert result is expected_result\n\n\n@pytest.mark.parametrize(\n    (\"branch_name\", \"is_main\"),\n    [\n        (\"main\", True),\n        (\"master\", True),\n        (\"develop\", False),\n        (\"feat-new-feature\", False),\n    ],\n)\ndef test_check_branch(branch_name, is_main):\n    \"\"\"测试分支检查\"\"\"\n    with patch(\"subprocess.run\") as mock_run:\n        mock_result = Mock()\n        mock_result.stdout = branch_name\n        mock_run.return_value = mock_result\n\n        result = check_branch()\n        assert result == branch_name\n\n\n@pytest.mark.parametrize(\n    (\"status_output\", \"is_synced\"),\n    [\n        (\"## main...origin/main\", True),\n        (\"## master...origin/master\", True),\n        (\"## main...origin/main [behind 3]\", False),\n        (\"## main...origin/main [ahead 2]\", True),\n        (\"## main...origin/main [ahead 1, behind 1]\", False),\n    ],\n)\ndef test_check_remote_sync(status_output, is_synced):\n    \"\"\"测试远程同步检查\"\"\"\n    with patch(\"subprocess.run\") as mock_run:\n        mock_result = Mock()\n        mock_result.stdout = status_output\n        mock_run.return_value = mock_result\n\n        result = check_remote_sync()\n        assert result is is_synced\n\n\nclass TestPushCommitInteraction:\n    \"\"\"测试 --push 和 --commit 的联动\"\"\"\n\n    def test_push_implies_commit(self, capsys):\n        \"\"\"测试 --push 自动包含 --commit\"\"\"\n        with (\n            patch(\n                \"sys.argv\", [\"release.py\", \"patch\", \"--push\", \"--dry-run\", \"--skip-branch-check\"]\n            ),\n            patch(\"scripts.release.check_working_directory_clean\", return_value=True),\n            patch(\"scripts.release.check_remote_sync\", return_value=True),\n            patch(\"scripts.release.get_current_version\", return_value=\"1.0.0\"),\n            patch(\"scripts.release.check_branch\", return_value=\"main\"),\n            patch(\"scripts.release.get_latest_tag\", return_value=\"1.0.0\"),\n        ):\n            from scripts.release import main\n\n            main()\n            captured = capsys.readouterr()\n            # 应该包含 \"提交版本变更\" 因为 --push 自动启用 --commit\n            assert \"提交版本变更\" in captured.out\n"
  },
  {
    "path": "tests/unit/test_updater.py",
    "content": "\"\"\"updater.py 单元测试\"\"\"\n\nimport json\nimport os\nimport tempfile\nfrom unittest.mock import Mock, patch\n\nimport pytest\n\nfrom remark.utils.constants import UPDATE_CACHE_FILE, UPDATE_CHECK_INTERVAL\nfrom remark.utils.updater import (\n    _create_opener,\n    _get_cache_file_path,\n    _get_proxies,\n    check_updates_auto,\n    check_updates_manual,\n    create_update_script,\n    download_update,\n    get_executable_path,\n    get_latest_release,\n    should_check_update,\n    trigger_update,\n    update_next_check_time,\n)\n\n\n@pytest.mark.unit\nclass TestProxyFunctions:\n    \"\"\"测试代理相关函数\"\"\"\n\n    def test_get_proxies_no_env(self, monkeypatch):\n        \"\"\"无环境变量时返回 None\"\"\"\n        for key in [\"HTTP_PROXY\", \"http_proxy\", \"HTTPS_PROXY\", \"https_proxy\"]:\n            monkeypatch.delenv(key, raising=False)\n\n        result = _get_proxies()\n        assert result is None\n\n    def test_get_proxies_http_only(self, monkeypatch):\n        \"\"\"仅 HTTP_PROXY 时返回 http\"\"\"\n        for key in [\"http_proxy\", \"HTTPS_PROXY\", \"https_proxy\"]:\n            monkeypatch.delenv(key, raising=False)\n        monkeypatch.setenv(\"HTTP_PROXY\", \"http://proxy.example.com:8080\")\n\n        result = _get_proxies()\n        assert result == {\"http\": \"http://proxy.example.com:8080\"}\n\n    def test_get_proxies_http_lowercase(self, monkeypatch):\n        \"\"\"小写 http_proxy 也能识别\"\"\"\n        for key in [\"HTTP_PROXY\", \"HTTPS_PROXY\", \"https_proxy\"]:\n            monkeypatch.delenv(key, raising=False)\n        monkeypatch.setenv(\"http_proxy\", \"http://proxy.example.com:8080\")\n\n        result = _get_proxies()\n        assert result == {\"http\": \"http://proxy.example.com:8080\"}\n\n    def test_get_proxies_https_only(self, monkeypatch):\n        \"\"\"仅 HTTPS_PROXY 时返回 https\"\"\"\n        for key in [\"HTTP_PROXY\", \"http_proxy\", \"https_proxy\"]:\n            monkeypatch.delenv(key, raising=False)\n        monkeypatch.setenv(\"HTTPS_PROXY\", \"https://proxy.example.com:8443\")\n\n        result = _get_proxies()\n        assert result == {\"https\": \"https://proxy.example.com:8443\"}\n\n    def test_get_proxies_both(self, monkeypatch):\n        \"\"\"两者都有时返回两者\"\"\"\n        for key in [\"http_proxy\", \"https_proxy\"]:\n            monkeypatch.delenv(key, raising=False)\n        monkeypatch.setenv(\"HTTP_PROXY\", \"http://proxy.example.com:8080\")\n        monkeypatch.setenv(\"HTTPS_PROXY\", \"https://proxy.example.com:8443\")\n\n        result = _get_proxies()\n        assert result == {\n            \"http\": \"http://proxy.example.com:8080\",\n            \"https\": \"https://proxy.example.com:8443\",\n        }\n\n    def test_create_opener_no_proxy(self, monkeypatch):\n        \"\"\"无代理时创建默认 opener\"\"\"\n        for key in [\"HTTP_PROXY\", \"http_proxy\", \"HTTPS_PROXY\", \"https_proxy\"]:\n            monkeypatch.delenv(key, raising=False)\n\n        opener = _create_opener()\n        assert opener is not None\n\n    def test_create_opener_with_proxy(self, monkeypatch):\n        \"\"\"有代理时创建带 ProxyHandler 的 opener\"\"\"\n        for key in [\"http_proxy\", \"HTTPS_PROXY\", \"https_proxy\"]:\n            monkeypatch.delenv(key, raising=False)\n        monkeypatch.setenv(\"HTTP_PROXY\", \"http://proxy.example.com:8080\")\n\n        opener = _create_opener()\n        assert opener is not None\n\n\n@pytest.mark.unit\nclass TestCacheFilePath:\n    \"\"\"测试缓存文件路径获取\"\"\"\n\n    def test_get_cache_file_path(self):\n        \"\"\"返回临时目录下的缓存文件路径\"\"\"\n        result = _get_cache_file_path()\n        expected = os.path.join(tempfile.gettempdir(), UPDATE_CACHE_FILE)\n        assert result == expected\n\n\n@pytest.mark.unit\nclass TestExecutablePath:\n    \"\"\"测试可执行文件路径获取\"\"\"\n\n    def test_get_executable_path_frozen(self, monkeypatch):\n        \"\"\"frozen 状态下返回 sys.executable\"\"\"\n        monkeypatch.setattr(\"sys.frozen\", True, raising=False)\n        monkeypatch.setattr(\"sys.executable\", \"/path/to/app.exe\")\n\n        result = get_executable_path()\n        assert result == \"/path/to/app.exe\"\n\n    def test_get_executable_path_not_frozen(self, monkeypatch):\n        \"\"\"非 frozen 状态下返回 __file__ 路径\"\"\"\n        monkeypatch.delattr(\"sys\", \"frozen\", raising=False)\n        # 在实际测试中，__file__ 会是实际路径，所以我们不 mock 它\n        # 只验证在 frozen=False 时的行为\n        result = get_executable_path()\n        # 结果应该是 remark/utils/updater.py 的路径\n        assert result.endswith(\"updater.py\")\n\n\n@pytest.mark.unit\nclass TestGitHubAPI:\n    \"\"\"测试 GitHub API 相关函数\"\"\"\n\n    def test_get_latest_release_success(self):\n        \"\"\"成功获取最新 release\"\"\"\n        mock_data = {\n            \"tag_name\": \"v2.0.3\",\n            \"html_url\": \"https://github.com/piratf/windows-folder-remark/releases/tag/v2.0.3\",\n            \"body\": \"New release\",\n            \"prerelease\": False,\n            \"draft\": False,\n            \"assets\": [\n                {\n                    \"name\": \"windows-folder-remark-2.0.3.exe\",\n                    \"browser_download_url\": \"https://github.com/.../exe\",\n                }\n            ],\n        }\n\n        mock_response = Mock()\n        mock_response.status = 200\n        mock_response.headers = {}\n        mock_response.__enter__ = Mock(return_value=mock_response)\n        mock_response.__exit__ = Mock(return_value=False)\n\n        with (\n            patch(\"urllib.request.OpenerDirector.open\", return_value=mock_response),\n            patch(\"json.load\", return_value=mock_data),\n        ):\n            result = get_latest_release()\n\n        assert result is not None\n        assert result[\"tag_name\"] == \"2.0.3\"\n        assert (\n            result[\"html_url\"]\n            == \"https://github.com/piratf/windows-folder-remark/releases/tag/v2.0.3\"\n        )\n        assert result[\"body\"] == \"New release\"\n\n    def test_get_latest_release_prerelease_filtered(self):\n        \"\"\"过滤掉 prerelease 版本\"\"\"\n        mock_data = {\n            \"tag_name\": \"v2.1.0-beta\",\n            \"prerelease\": True,\n            \"draft\": False,\n            \"assets\": [\n                {\n                    \"name\": \"windows-folder-remark-2.1.0-beta.exe\",\n                    \"browser_download_url\": \"https://github.com/.../exe\",\n                }\n            ],\n        }\n\n        mock_response = Mock()\n        mock_response.status = 200\n        mock_response.headers = {}\n        mock_response.__enter__ = Mock(return_value=mock_response)\n        mock_response.__exit__ = Mock(return_value=False)\n\n        with (\n            patch(\"urllib.request.OpenerDirector.open\", return_value=mock_response),\n            patch(\"json.load\", return_value=mock_data),\n        ):\n            result = get_latest_release()\n\n        assert result is None\n\n    def test_get_latest_release_draft_filtered(self):\n        \"\"\"过滤掉 draft 版本\"\"\"\n        mock_data = {\n            \"tag_name\": \"v2.1.0\",\n            \"prerelease\": False,\n            \"draft\": True,\n            \"assets\": [\n                {\n                    \"name\": \"windows-folder-remark-2.1.0.exe\",\n                    \"browser_download_url\": \"https://github.com/.../exe\",\n                }\n            ],\n        }\n\n        mock_response = Mock()\n        mock_response.status = 200\n        mock_response.headers = {}\n        mock_response.__enter__ = Mock(return_value=mock_response)\n        mock_response.__exit__ = Mock(return_value=False)\n\n        with (\n            patch(\"urllib.request.OpenerDirector.open\", return_value=mock_response),\n            patch(\"json.load\", return_value=mock_data),\n        ):\n            result = get_latest_release()\n\n        assert result is None\n\n    def test_get_latest_release_no_exe_asset(self):\n        \"\"\"无 exe 文件时返回 None\"\"\"\n        mock_data = {\n            \"tag_name\": \"v2.0.3\",\n            \"prerelease\": False,\n            \"draft\": False,\n            \"assets\": [\n                {\"name\": \"README.md\", \"browser_download_url\": \"https://github.com/.../README.md\"}\n            ],\n        }\n\n        mock_response = Mock()\n        mock_response.status = 200\n        mock_response.headers = {}\n        mock_response.__enter__ = Mock(return_value=mock_response)\n        mock_response.__exit__ = Mock(return_value=False)\n\n        with (\n            patch(\"urllib.request.OpenerDirector.open\", return_value=mock_response),\n            patch(\"json.load\", return_value=mock_data),\n        ):\n            result = get_latest_release()\n\n        assert result is None\n\n    def test_get_latest_release_api_error(self):\n        \"\"\"API 请求失败时返回 None\"\"\"\n        # 使用 parse_headers 创建空的 HTTPMessage\n        # HTTPError(url, code, msg, hdrs, fp)\n        import http.client\n        import urllib.error\n        from io import BytesIO\n\n        hdrs = http.client.parse_headers(BytesIO())\n        with patch(\n            \"urllib.request.OpenerDirector.open\",\n            side_effect=urllib.error.HTTPError(\"url\", 500, \"Error\", hdrs, BytesIO(b\"error body\")),\n        ):\n            result = get_latest_release()\n\n        assert result is None\n\n    def test_get_latest_release_json_error(self):\n        \"\"\"JSON 解析失败时返回 None\"\"\"\n        with patch(\"json.load\", side_effect=json.JSONDecodeError(\"error\", \"\", 0)):\n            result = get_latest_release()\n\n        assert result is None\n\n    def test_get_latest_release_non_200_status(self):\n        \"\"\"HTTP 状态码不是 200 时返回 None\"\"\"\n        mock_response = Mock()\n        mock_response.status = 404\n        mock_response.headers = {}\n        mock_response.__enter__ = Mock(return_value=mock_response)\n        mock_response.__exit__ = Mock(return_value=False)\n\n        with patch(\"urllib.request.OpenerDirector.open\", return_value=mock_response):\n            result = get_latest_release()\n\n        assert result is None\n\n\n@pytest.mark.unit\nclass TestCacheFunctions:\n    \"\"\"测试缓存相关函数（核心逻辑）\"\"\"\n\n    def test_should_check_update_no_cache_file(self, fs):\n        \"\"\"无缓存文件时返回 True（首次检查）\"\"\"\n        result = should_check_update()\n        assert result is True\n\n    def test_should_check_update_invalid_content(self, fs):\n        \"\"\"缓存文件内容无效时返回 True\"\"\"\n        cache_file = _get_cache_file_path()\n        fs.create_file(cache_file, contents=\"invalid_content\")\n\n        result = should_check_update()\n        assert result is True\n\n    def test_should_check_update_cache_expired(self, fs):\n        \"\"\"缓存过期时返回 True\"\"\"\n        # 设置一个过去的时间戳（2024-01-01）\n        past_time = 1704067200.0\n        cache_file = _get_cache_file_path()\n        fs.create_file(cache_file, contents=str(past_time))\n\n        # Mock 当前时间为 2024-01-02\n        with patch(\"time.time\", return_value=1704153600.0):\n            result = should_check_update()\n\n        assert result is True\n\n    def test_should_check_update_cache_valid(self, fs):\n        \"\"\"缓存未过期时返回 False\"\"\"\n        current_time = 1704067200.0\n        cache_file = _get_cache_file_path()\n        # 缓存设置为未来时间\n        next_check = current_time + UPDATE_CHECK_INTERVAL\n        fs.create_file(cache_file, contents=str(next_check))\n\n        with patch(\"time.time\", return_value=current_time):\n            result = should_check_update()\n\n        assert result is False\n\n    def test_update_next_check_time(self, fs):\n        \"\"\"更新下次检查时间为当前时间 + 24 小时\"\"\"\n        current_time = 1704067200.0\n\n        with patch(\"time.time\", return_value=current_time):\n            update_next_check_time()\n\n        cache_file = _get_cache_file_path()\n        with open(cache_file, encoding=\"utf-8\") as f:\n            cached_time = float(f.read().strip())\n\n        expected = current_time + UPDATE_CHECK_INTERVAL\n        assert cached_time == expected\n\n    def test_update_next_check_time_silent_failure(self, fs):\n        \"\"\"写入失败时静默处理（不抛异常）\"\"\"\n        current_time = 1704067200.0\n\n        with (\n            patch(\"time.time\", return_value=current_time),\n            patch(\"builtins.open\", side_effect=OSError(\"Permission denied\")),\n        ):\n            update_next_check_time()\n\n    def test_check_updates_auto_skips_when_not_needed(self, fs):\n        \"\"\"不需要检查时直接返回 None，不调用 API\"\"\"\n        current_time = 1704067200.0\n        cache_file = _get_cache_file_path()\n        next_check = current_time + UPDATE_CHECK_INTERVAL\n        fs.create_file(cache_file, contents=str(next_check))\n\n        with (\n            patch(\"time.time\", return_value=current_time),\n            patch(\"remark.utils.updater.get_latest_release\") as mock_api,\n        ):\n            result = check_updates_auto(\"2.0.2\")\n\n        assert result is None\n        mock_api.assert_not_called()\n\n    def test_check_updates_auto_updates_cache_after_check(self, fs):\n        \"\"\"检查后立即更新缓存时间\"\"\"\n        current_time = 1704067200.0\n\n        with (\n            patch(\"time.time\", return_value=current_time),\n            patch(\"remark.utils.updater.get_latest_release\", return_value=None),\n        ):\n            check_updates_auto(\"2.0.2\")\n\n        cache_file = _get_cache_file_path()\n        with open(cache_file, encoding=\"utf-8\") as f:\n            cached_time = float(f.read().strip())\n\n        expected = current_time + UPDATE_CHECK_INTERVAL\n        assert cached_time == expected\n\n    def test_check_updates_auto_has_new_version(self, fs):\n        \"\"\"有新版本时返回 release 信息\"\"\"\n        current_time = 1704067200.0\n        mock_release = {\n            \"tag_name\": \"2.0.3\",\n            \"html_url\": \"https://github.com/.../2.0.3\",\n            \"body\": \"New version\",\n            \"download_url\": \"https://github.com/.../exe\",\n        }\n\n        with (\n            patch(\"time.time\", return_value=current_time),\n            patch(\"remark.utils.updater.get_latest_release\", return_value=mock_release),\n        ):\n            result = check_updates_auto(\"2.0.2\")\n\n        assert result is not None\n        assert result[\"tag_name\"] == \"2.0.3\"\n\n    def test_check_updates_auto_no_new_version(self, fs):\n        \"\"\"无新版本时返回 None\"\"\"\n        current_time = 1704067200.0\n        mock_release = {\n            \"tag_name\": \"2.0.2\",\n            \"html_url\": \"https://github.com/.../2.0.2\",\n            \"body\": \"Current version\",\n            \"download_url\": \"https://github.com/.../exe\",\n        }\n\n        with (\n            patch(\"time.time\", return_value=current_time),\n            patch(\"remark.utils.updater.get_latest_release\", return_value=mock_release),\n        ):\n            result = check_updates_auto(\"2.0.2\")\n\n        assert result is None\n\n    def test_check_updates_auto_api_failure(self, fs):\n        \"\"\"API 失败时返回 None\"\"\"\n        current_time = 1704067200.0\n\n        with (\n            patch(\"time.time\", return_value=current_time),\n            patch(\"remark.utils.updater.get_latest_release\", return_value=None),\n        ):\n            result = check_updates_auto(\"2.0.2\")\n\n        assert result is None\n\n    def test_check_updates_manual_ignores_cache(self, fs):\n        \"\"\"check_updates_manual 忽略缓存，直接调用 API\"\"\"\n        current_time = 1704067200.0\n        cache_file = _get_cache_file_path()\n        next_check = current_time + UPDATE_CHECK_INTERVAL\n        fs.create_file(cache_file, contents=str(next_check))\n\n        mock_release = {\n            \"tag_name\": \"2.0.3\",\n            \"html_url\": \"https://github.com/.../2.0.3\",\n            \"body\": \"New version\",\n            \"download_url\": \"https://github.com/.../exe\",\n        }\n\n        with patch(\n            \"remark.utils.updater.get_latest_release\", return_value=mock_release\n        ) as mock_api:\n            result = check_updates_manual(\"2.0.2\")\n\n        mock_api.assert_called_once()\n        assert result is not None\n        assert result[\"tag_name\"] == \"2.0.3\"\n\n    def test_check_updates_manual_no_new_version(self):\n        \"\"\"无新版本时返回 None\"\"\"\n        mock_release = {\n            \"tag_name\": \"2.0.2\",\n            \"html_url\": \"https://github.com/.../2.0.2\",\n            \"body\": \"Current version\",\n            \"download_url\": \"https://github.com/.../exe\",\n        }\n\n        with patch(\"remark.utils.updater.get_latest_release\", return_value=mock_release):\n            result = check_updates_manual(\"2.0.2\")\n\n        assert result is None\n\n    def test_check_updates_manual_api_returns_none(self):\n        \"\"\"API 返回 None 时返回 None\"\"\"\n        with patch(\"remark.utils.updater.get_latest_release\", return_value=None):\n            result = check_updates_manual(\"2.0.2\")\n\n        assert result is None\n\n\n@pytest.mark.unit\nclass TestDownloadUpdate:\n    \"\"\"测试下载更新函数\"\"\"\n\n    def test_download_update_success(self, tmp_path):\n        \"\"\"正常下载文件\"\"\"\n        dest = str(tmp_path / \"test.exe\")\n\n        mock_response = Mock()\n        mock_response.headers = {\"content-length\": \"100\"}\n        mock_response.read.side_effect = [b\"chunk1\", b\"chunk2\", b\"\"]\n        mock_response.__enter__ = Mock(return_value=mock_response)\n        mock_response.__exit__ = Mock(return_value=False)\n\n        with patch(\"urllib.request.OpenerDirector.open\", return_value=mock_response):\n            result = download_update(\"http://example.com/test.exe\", dest)\n\n        assert result == dest\n\n    def test_download_update_progress(self, tmp_path, capsys):\n        \"\"\"显示下载进度\"\"\"\n        dest = str(tmp_path / \"test.exe\")\n\n        mock_response = Mock()\n        mock_response.headers = {\"content-length\": \"100\"}\n        mock_response.read.side_effect = [b\"x\" * 25, b\"x\" * 25, b\"x\" * 25, b\"x\" * 25, b\"\"]\n        mock_response.__enter__ = Mock(return_value=mock_response)\n        mock_response.__exit__ = Mock(return_value=False)\n\n        with patch(\"urllib.request.OpenerDirector.open\", return_value=mock_response):\n            download_update(\"http://example.com/test.exe\", dest)\n\n        captured = capsys.readouterr()\n        assert \"下载进度\" in captured.out\n\n    def test_download_update_network_error(self):\n        \"\"\"网络错误时抛出异常\"\"\"\n        import urllib.error\n\n        with (\n            patch(\n                \"urllib.request.OpenerDirector.open\",\n                side_effect=urllib.error.URLError(\"Network error\"),\n            ),\n            pytest.raises(urllib.error.URLError),\n        ):\n            download_update(\"http://example.com/test.exe\", \"/tmp/test.exe\")\n\n\n@pytest.mark.unit\nclass TestUpdateScript:\n    \"\"\"测试更新脚本相关函数\"\"\"\n\n    def test_create_update_script(self, tmp_path):\n        \"\"\"创建批处理脚本\"\"\"\n        old_exe = \"C:/Program Files/old.exe\"\n        new_exe = str(tmp_path / \"new.exe\")\n\n        result = create_update_script(old_exe, new_exe)\n\n        assert os.path.exists(result)\n        with open(result, encoding=\"gbk\") as f:\n            content = f.read()\n\n        assert old_exe in content\n        assert new_exe in content\n        assert \"move\" in content\n\n    def test_trigger_update(self):\n        \"\"\"使用 subprocess.Popen 启动\"\"\"\n        with patch(\"subprocess.Popen\") as mock_popen:\n            trigger_update(\"/tmp/update.bat\")\n\n            mock_popen.assert_called_once()\n            call_args = mock_popen.call_args\n            assert call_args[0][0] == [\"cmd.exe\", \"/c\", \"/tmp/update.bat\"]\n            assert call_args[1][\"shell\"] is True\n            assert \"creationflags\" in call_args[1]\n\n\n@pytest.mark.unit\nclass TestInvalidVersion:\n    \"\"\"测试无效版本号处理\"\"\"\n\n    def test_check_updates_auto_invalid_version(self, fs):\n        \"\"\"当前版本号无效时返回 None\"\"\"\n        current_time = 1704067200.0\n        mock_release = {\n            \"tag_name\": \"invalid-version\",\n            \"html_url\": \"https://github.com/.../invalid\",\n            \"body\": \"Invalid\",\n            \"download_url\": \"https://github.com/.../exe\",\n        }\n\n        with (\n            patch(\"time.time\", return_value=current_time),\n            patch(\"remark.utils.updater.get_latest_release\", return_value=mock_release),\n        ):\n            result = check_updates_auto(\"2.0.2\")\n\n        assert result is None\n\n    def test_check_updates_manual_invalid_version(self):\n        \"\"\"强制检查时，版本号无效返回 None\"\"\"\n        mock_release = {\n            \"tag_name\": \"invalid-version\",\n            \"html_url\": \"https://github.com/.../invalid\",\n            \"body\": \"Invalid\",\n            \"download_url\": \"https://github.com/.../exe\",\n        }\n\n        with patch(\"remark.utils.updater.get_latest_release\", return_value=mock_release):\n            result = check_updates_manual(\"2.0.2\")\n\n        assert result is None\n"
  },
  {
    "path": "tests/windows/__init__.py",
    "content": "\"\"\"Windows 特定测试模块\"\"\"\n"
  },
  {
    "path": "tests/windows/test_context_menu.py",
    "content": "\"\"\"\n右键菜单集成测试\n\n这些测试需要在 Windows 系统上运行，并且会实际操作注册表。\n\"\"\"\n\nimport sys\nimport winreg\n\nimport pytest\n\nfrom remark.utils import registry\n\nREGISTRY_ROOT = winreg.HKEY_CURRENT_USER\nREGISTRY_PATH = r\"Software\\Classes\\Directory\\shell\\WindowsFolderRemark\"\n\n\n@pytest.mark.windows\n@pytest.mark.integration\nclass TestContextMenu:\n    \"\"\"右键菜单集成测试（仅 Windows）\"\"\"\n\n    def setup_method(self):\n        \"\"\"每个测试方法前执行：确保菜单未安装\"\"\"\n        registry.uninstall_context_menu()\n\n    def teardown_method(self):\n        \"\"\"每个测试方法后执行：清理注册表\"\"\"\n        registry.uninstall_context_menu()\n\n    def test_install_and_uninstall(self):\n        \"\"\"测试完整的安装和卸载流程\"\"\"\n        # 1. 安装\n        assert registry.install_context_menu() is True\n\n        # 2. 验证注册表键存在\n        key = winreg.OpenKey(REGISTRY_ROOT, REGISTRY_PATH)\n        assert key is not None\n        winreg.CloseKey(key)\n\n        # 3. 验证 command 子键存在\n        command_path = f\"{REGISTRY_PATH}\\\\command\"\n        command_key = winreg.OpenKey(REGISTRY_ROOT, command_path)\n        assert command_key is not None\n        winreg.CloseKey(command_key)\n\n        # 4. 卸载\n        assert registry.uninstall_context_menu() is True\n\n        # 5. 验证注册表键不存在\n        with pytest.raises(FileNotFoundError):\n            winreg.OpenKey(REGISTRY_ROOT, REGISTRY_PATH)\n\n    def test_install_twice(self):\n        \"\"\"测试重复安装的幂等性\"\"\"\n        # 第一次安装\n        assert registry.install_context_menu() is True\n\n        # 第二次安装应该成功（覆盖）\n        assert registry.install_context_menu() is True\n\n    def test_uninstall_not_installed(self):\n        \"\"\"测试卸载未安装的菜单\"\"\"\n        # 未安装时卸载应该返回 True（幂等）\n        assert registry.uninstall_context_menu() is True\n\n    def test_get_executable_path(self):\n        \"\"\"测试获取可执行文件路径\"\"\"\n        path = registry.get_executable_path()\n        assert path is not None\n        assert isinstance(path, str)\n        # 验证路径是有效的\n        import os\n\n        if hasattr(sys, \"frozen\"):\n            # 打包后，路径应该是 exe 文件\n            assert os.path.isfile(path)\n        else:\n            # 开发环境，路径应该是 remark.py\n            assert \"remark.py\" in path\n"
  },
  {
    "path": "tests/windows/test_full_workflow.py",
    "content": "\"\"\"完整工作流测试（仅 Windows）\"\"\"\n\nimport ctypes\nimport os\nimport sys\n\nimport pytest\n\nfrom remark.core.folder_handler import FolderCommentHandler\nfrom remark.storage.desktop_ini import DesktopIniHandler\n\n\n@pytest.mark.windows\n@pytest.mark.integration\n@pytest.mark.slow\nclass TestFullWorkflow:\n    \"\"\"完整工作流测试\"\"\"\n\n    def test_complete_add_workflow(self, tmp_path):\n        \"\"\"测试完整的添加工作流\"\"\"\n        if sys.platform != \"win32\":\n            pytest.skip(\"Windows only test\")\n\n        handler = FolderCommentHandler()\n        folder = str(tmp_path / \"workflow_test\")\n        os.makedirs(folder)\n\n        # 1. 设置备注\n        result = handler.set_comment(folder, \"完整工作流测试\")\n        assert result is True\n\n        # 2. 验证文件存在\n        ini_path = os.path.join(folder, \"desktop.ini\")\n        assert os.path.exists(ini_path)\n\n        # 3. 读取备注\n        comment = handler.get_comment(folder)\n        assert comment == \"完整工作流测试\"\n\n        # 4. 验证文件属性（Windows API）\n        GetFileAttributesW = ctypes.windll.kernel32.GetFileAttributesW  # noqa: N806\n        FILE_ATTRIBUTE_HIDDEN = 0x02  # noqa: N806\n        FILE_ATTRIBUTE_SYSTEM = 0x04  # noqa: N806\n\n        attrs = GetFileAttributesW(ini_path)\n        assert attrs != 0xFFFFFFFF\n        assert attrs & FILE_ATTRIBUTE_HIDDEN\n        assert attrs & FILE_ATTRIBUTE_SYSTEM\n\n    def test_complete_delete_workflow(self, tmp_path):\n        \"\"\"测试完整的删除工作流\"\"\"\n        if sys.platform != \"win32\":\n            pytest.skip(\"Windows only test\")\n\n        handler = FolderCommentHandler()\n        folder = str(tmp_path / \"delete_test\")\n        os.makedirs(folder)\n\n        # 1. 先添加备注\n        handler.set_comment(folder, \"待删除的备注\")\n\n        # 2. 验证备注存在\n        assert handler.get_comment(folder) == \"待删除的备注\"\n\n        # 3. 删除备注\n        result = handler.delete_comment(folder)\n        assert result is True\n\n        # 4. 验证备注已删除\n        comment = handler.get_comment(folder)\n        assert comment is None\n\n    def test_update_existing_comment(self, tmp_path):\n        \"\"\"测试更新已有备注\"\"\"\n        if sys.platform != \"win32\":\n            pytest.skip(\"Windows only test\")\n\n        handler = FolderCommentHandler()\n        folder = str(tmp_path / \"update_test\")\n        os.makedirs(folder)\n\n        # 1. 设置初始备注\n        handler.set_comment(folder, \"原始备注\")\n        assert handler.get_comment(folder) == \"原始备注\"\n\n        # 2. 更新备注\n        result = handler.set_comment(folder, \"更新后的备注\")\n        assert result is True\n\n        # 3. 验证更新\n        comment = handler.get_comment(folder)\n        assert comment == \"更新后的备注\"\n\n        # 验证文件只有一个 InfoTip\n        ini_path = os.path.join(folder, \"desktop.ini\")\n        with open(ini_path, encoding=\"utf-16\") as f:\n            content = f.read()\n        # 计算出现次数\n        count = content.count(\"InfoTip=\")\n        assert count == 1, \"应该只有一个 InfoTip 行\"\n\n    def test_multiple_folders(self, tmp_path):\n        \"\"\"测试多个文件夹\"\"\"\n        if sys.platform != \"win32\":\n            pytest.skip(\"Windows only test\")\n\n        handler = FolderCommentHandler()\n\n        folders = []\n        for i in range(5):\n            folder = str(tmp_path / f\"folder_{i}\")\n            os.makedirs(folder)\n            folders.append(folder)\n\n        # 为所有文件夹设置备注\n        for i, folder in enumerate(folders):\n            result = handler.set_comment(folder, f\"备注 {i}\")\n            assert result is True\n\n        # 验证所有备注\n        for i, folder in enumerate(folders):\n            comment = handler.get_comment(folder)\n            assert comment == f\"备注 {i}\"\n\n    def test_preserve_other_settings(self, tmp_path):\n        \"\"\"测试保留其他 desktop.ini 设置\"\"\"\n        if sys.platform != \"win32\":\n            pytest.skip(\"Windows only test\")\n\n        import codecs\n\n        folder = str(tmp_path / \"preserve_test\")\n        os.makedirs(folder)\n\n        # 创建包含 IconResource 的 desktop.ini\n        ini_path = os.path.join(folder, \"desktop.ini\")\n        content = \"[.ShellClassInfo]\\r\\nIconResource=C:\\\\icon.ico,0\\r\\n\"\n        with codecs.open(ini_path, \"w\", encoding=\"utf-16\") as f:\n            f.write(content)\n\n        # 添加 InfoTip\n        handler = FolderCommentHandler()\n        handler.set_comment(folder, \"备注\")\n\n        # 验证 IconResource 仍然存在\n        with codecs.open(ini_path, \"r\", encoding=\"utf-16\") as f:\n            new_content = f.read()\n\n        assert \"InfoTip=备注\" in new_content\n        assert \"IconResource\" in new_content\n\n    def test_empty_comment_removal(self, tmp_path):\n        \"\"\"测试删除备注后保留文件（如果有其他设置）\"\"\"\n        if sys.platform != \"win32\":\n            pytest.skip(\"Windows only test\")\n\n        import codecs\n\n        folder = str(tmp_path / \"empty_removal\")\n        os.makedirs(folder)\n\n        # 创建包含 InfoTip 和 IconResource 的文件\n        ini_path = os.path.join(folder, \"desktop.ini\")\n        content = \"[.ShellClassInfo]\\r\\nInfoTip=备注\\r\\nIconResource=C:\\\\icon.ico,0\\r\\n\"\n        with codecs.open(ini_path, \"w\", encoding=\"utf-16\") as f:\n            f.write(content)\n\n        # 删除备注\n        handler = FolderCommentHandler()\n        handler.delete_comment(folder)\n\n        # 验证文件仍存在（因为有 IconResource）\n        assert os.path.exists(ini_path)\n\n        # 验证 InfoTip 已删除\n        comment = handler.get_comment(folder)\n        assert comment is None\n\n    def test_special_characters_in_comment(self, tmp_path):\n        \"\"\"测试备注中的特殊字符\"\"\"\n        if sys.platform != \"win32\":\n            pytest.skip(\"Windows only test\")\n\n        handler = FolderCommentHandler()\n        folder = str(tmp_path / \"special\")\n        os.makedirs(folder)\n\n        special_comments = [\n            \"换行\\n测试\",  # 包含换行符（会被处理）\n            \"制表符\\t测试\",\n            '引号 \"测试\"',\n            \"单引号 '测试'\",\n            \"反斜杠 \\\\测试\",\n            \"斜杠 /测试\",\n        ]\n\n        for comment in special_comments:\n            result = handler.set_comment(folder, comment)\n            assert result is True\n\n            read_result = handler.get_comment(folder)\n            assert read_result == comment\n\n    def test_comment_length_truncation(self, tmp_path):\n        \"\"\"测试备注长度截断\"\"\"\n        if sys.platform != \"win32\":\n            pytest.skip(\"Windows only test\")\n\n        from remark.core.folder_handler import MAX_COMMENT_LENGTH\n\n        handler = FolderCommentHandler()\n        folder = str(tmp_path / \"truncation\")\n        os.makedirs(folder)\n\n        # 超长备注\n        long_comment = \"A\" * 300\n        handler.set_comment(folder, long_comment)\n\n        # 验证被截断\n        read_result = handler.get_comment(folder)\n        assert len(read_result) == MAX_COMMENT_LENGTH\n        assert read_result == \"A\" * MAX_COMMENT_LENGTH\n\n\n@pytest.mark.windows\nclass TestDesktopIniIntegration:\n    \"\"\"desktop.ini 集成测试\"\"\"\n\n    def test_write_read_cycle(self, tmp_path):\n        \"\"\"测试写入读取循环\"\"\"\n        if sys.platform != \"win32\":\n            pytest.skip(\"Windows only test\")\n\n        folder = str(tmp_path / \"cycle\")\n        os.makedirs(folder)\n\n        test_comments = [\"第一次\", \"第二次\", \"第三次\"]\n        for comment in test_comments:\n            DesktopIniHandler.write_info_tip(folder, comment)\n            read = DesktopIniHandler.read_info_tip(folder)\n            assert read == comment\n\n    def test_remove_info_tip(self, tmp_path):\n        \"\"\"测试移除 InfoTip\"\"\"\n        if sys.platform != \"win32\":\n            pytest.skip(\"Windows only test\")\n\n        folder = str(tmp_path / \"remove\")\n        os.makedirs(folder)\n\n        # 写入备注\n        DesktopIniHandler.write_info_tip(folder, \"待移除\")\n        assert DesktopIniHandler.read_info_tip(folder) == \"待移除\"\n\n        # 移除备注\n        result = DesktopIniHandler.remove_info_tip(folder)\n        assert result is True\n\n        # 验证已移除\n        assert DesktopIniHandler.read_info_tip(folder) is None\n\n    def test_file_attributes_workflow(self, tmp_path):\n        \"\"\"测试文件属性工作流\"\"\"\n        if sys.platform != \"win32\":\n            pytest.skip(\"Windows only test\")\n\n        folder = str(tmp_path / \"attributes\")\n        os.makedirs(folder)\n\n        DesktopIniHandler.write_info_tip(folder, \"属性测试\")\n\n        ini_path = os.path.join(folder, \"desktop.ini\")\n\n        # 测试清除属性\n        assert DesktopIniHandler.clear_file_attributes(ini_path) is True\n\n        # 测试设置隐藏系统属性\n        assert DesktopIniHandler.set_file_hidden_system_attributes(ini_path) is True\n\n        # 验证属性\n        GetFileAttributesW = ctypes.windll.kernel32.GetFileAttributesW  # noqa: N806\n        FILE_ATTRIBUTE_HIDDEN = 0x02  # noqa: N806\n        FILE_ATTRIBUTE_SYSTEM = 0x04  # noqa: N806\n\n        attrs = GetFileAttributesW(ini_path)\n        assert attrs & FILE_ATTRIBUTE_HIDDEN\n        assert attrs & FILE_ATTRIBUTE_SYSTEM\n\n    def test_folder_readonly_workflow(self, tmp_path):\n        \"\"\"测试文件夹只读属性工作流\"\"\"\n        if sys.platform != \"win32\":\n            pytest.skip(\"Windows only test\")\n\n        folder = str(tmp_path / \"readonly\")\n        os.makedirs(folder)\n\n        # 设置只读属性\n        result = DesktopIniHandler.set_folder_system_attributes(folder)\n        assert result is True\n\n        # 验证只读属性\n        FILE_ATTRIBUTE_READONLY = 0x01  # noqa: N806\n        GetFileAttributesW = ctypes.windll.kernel32.GetFileAttributesW  # noqa: N806\n\n        attrs = GetFileAttributesW(folder)\n        assert attrs != 0xFFFFFFFF\n        assert attrs & FILE_ATTRIBUTE_READONLY\n\n        # 再次设置（应该跳过，已经设置）\n        result2 = DesktopIniHandler.set_folder_system_attributes(folder)\n        assert result2 is True\n"
  }
]