Full Code of piratf/windows-folder-remark for AI

main 33630c306733 cached
74 files
258.8 KB
70.9k tokens
342 symbols
1 requests
Download .txt
Showing preview only (305K chars total). Download the full file or copy to clipboard to get everything.
Repository: piratf/windows-folder-remark
Branch: main
Commit: 33630c306733
Files: 74
Total size: 258.8 KB

Directory structure:
gitextract__x7cup28/

├── .gitattributes
├── .github/
│   └── workflows/
│       ├── auto-tag.yml
│       ├── deploy-docs.yml
│       ├── release.yml
│       └── test.yml
├── .gitignore
├── .pre-commit-config.yaml
├── .python-version
├── CHANGELOG.md
├── LICENSE
├── README.en.md
├── README.md
├── babel.cfg
├── desktop.ini
├── docs/
│   ├── .vitepress/
│   │   └── config.mjs
│   ├── en/
│   │   ├── guide/
│   │   │   ├── api.md
│   │   │   ├── getting-started.md
│   │   │   └── usage.md
│   │   └── index.md
│   ├── guide/
│   │   ├── api.md
│   │   ├── getting-started.md
│   │   └── usage.md
│   ├── index.md
│   └── robots.txt
├── locale/
│   └── zh/
│       └── LC_MESSAGES/
│           ├── messages.mo
│           └── messages.po
├── messages.pot
├── package.json
├── pyproject.toml
├── remark/
│   ├── __init__.py
│   ├── cli/
│   │   ├── __init__.py
│   │   ├── __main__.py
│   │   └── commands.py
│   ├── core/
│   │   ├── __init__.py
│   │   ├── base.py
│   │   └── folder_handler.py
│   ├── gui/
│   │   ├── __init__.py
│   │   └── remark_dialog.py
│   ├── i18n.py
│   ├── storage/
│   │   ├── __init__.py
│   │   └── desktop_ini.py
│   └── utils/
│       ├── __init__.py
│       ├── constants.py
│       ├── encoding.py
│       ├── path_resolver.py
│       ├── platform.py
│       ├── registry.py
│       └── updater.py
├── remark.py
├── remark.spec
├── scripts/
│   ├── __init__.py
│   ├── analyze_exe_size.py
│   ├── build.py
│   ├── check_i18n.py
│   ├── ensure_upx.py
│   └── release.py
└── tests/
    ├── __init__.py
    ├── conftest.py
    ├── integration/
    │   ├── __init__.py
    │   ├── conftest.py
    │   └── test_encoding_handling.py
    ├── unit/
    │   ├── __init__.py
    │   ├── test_cli_commands.py
    │   ├── test_desktop_ini.py
    │   ├── test_folder_handler.py
    │   ├── test_i18n.py
    │   ├── test_path_resolver.py
    │   ├── test_platform.py
    │   ├── test_registry.py
    │   ├── test_release.py
    │   └── test_updater.py
    └── windows/
        ├── __init__.py
        ├── test_context_menu.py
        └── test_full_workflow.py

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitattributes
================================================
# 统一使用 LF 换行符
* text=auto eol=lf

# Windows 特定文件保持 CRLF
*.bat text eol=crlf
*.cmd text eol=crlf
*.ps1 text eol=crlf

# LFS for large files
dist/* filter=lfs diff=lfs merge=lfs -text


================================================
FILE: .github/workflows/auto-tag.yml
================================================
name: Auto Tag

on:
  push:
    branches:
      - main                 # 正式环境

permissions:
  contents: write

jobs:
  auto-tag:
    runs-on: ubuntu-latest
    outputs:
      should_release: ${{ steps.version_check.outputs.should_release }}
      version: ${{ steps.get_version.outputs.version }}
      tag_name: ${{ steps.create_tag.outputs.tag_name }}
    steps:
      - name: Checkout code
        uses: actions/checkout@v4
        with:
          fetch-depth: 0  # 获取所有历史,用于 git tag 操作

      - name: Get version from pyproject.toml
        id: get_version
        run: |
          VERSION=$(python3 -c "import tomllib; print(tomllib.load(open('pyproject.toml', 'rb'))['project']['version'])")
          echo "version=$VERSION" >> $GITHUB_OUTPUT

      - name: Get latest tag
        id: get_latest_tag
        run: |
          # 获取所有 tag 并按版本排序,取最新的
          LATEST=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0")
          LATEST=${LATEST#v}
          echo "latest=$LATEST" >> $GITHUB_OUTPUT

      - name: Compare versions
        id: version_check
        run: |
          python - <<EOF
          from packaging import version
          pyproject_ver = "${{ steps.get_version.outputs.version }}"
          latest_ver = "${{ steps.get_latest_tag.outputs.latest }}"
          should_release = version.parse(pyproject_ver) > version.parse(latest_ver)
          with open("$GITHUB_OUTPUT", "a") as f:
              f.write(f"should_release={str(should_release).lower()}\n")
          print(f"PyProject version: {pyproject_ver}")
          print(f"Latest tag: {latest_ver}")
          print(f"Should release: {should_release}")
          EOF

      - name: Create and push tag
        id: create_tag
        if: steps.version_check.outputs.should_release == 'true'
        run: |
          TAG_NAME="v${{ steps.get_version.outputs.version }}"
          git config user.name "github-actions[bot]"
          git config user.email "github-actions[bot]@users.noreply.github.com"
          git tag -a "$TAG_NAME" -m "Release $TAG_NAME"
          git push origin "$TAG_NAME"
          echo "tag_name=$TAG_NAME" >> $GITHUB_OUTPUT
          echo "✓ Created tag: $TAG_NAME"

  build:
    needs: auto-tag
    if: needs.auto-tag.outputs.should_release == 'true'
    runs-on: windows-latest
    strategy:
      matrix:
        architecture: [x64]
        python-version: ['3.11']

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python-version }}

      - name: Install build dependencies
        run: |
          python -m pip install --upgrade pip
          pip install -e ".[dev]"

      - name: Run tests
        run: |
          pytest -v --cov=remark --cov-report=term-missing

      - name: Install PyInstaller
        run: |
          pip install pyinstaller

      - name: Build executable
        run: |
          pyinstaller remark.spec --clean

      - name: Rename executable with version
        run: |
          $version = "${{ needs.auto-tag.outputs.version }}"
          Copy-Item "dist\windows-folder-remark.exe" "dist\windows-folder-remark-$version.exe"

      - name: Generate checksum
        run: |
          $version = "${{ needs.auto-tag.outputs.version }}"
          $file = "dist\windows-folder-remark-$version.exe"
          certutil -hashfile $file SHA256 > "$file.sha256"

      - name: Upload artifact
        uses: actions/upload-artifact@v4
        with:
          name: windows-folder-remark-${{ matrix.architecture }}
          path: |
            dist/*.exe
            dist/*.sha256

  release:
    needs: [auto-tag, build]
    if: needs.auto-tag.outputs.should_release == 'true'
    runs-on: windows-latest
    permissions:
      contents: write

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Download all artifacts
        uses: actions/download-artifact@v4
        with:
          path: artifacts

      - name: Create Release
        uses: softprops/action-gh-release@v2
        with:
          tag_name: ${{ needs.auto-tag.outputs.tag_name }}
          draft: false
          prerelease: false
          generate_release_notes: true
          files: |
            artifacts/**/*.exe
            artifacts/**/*.sha256
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}


================================================
FILE: .github/workflows/deploy-docs.yml
================================================
name: Deploy VitePress site to Pages

on:
  push:
    branches: [main]
  workflow_dispatch:

permissions:
  contents: read
  pages: write
  id-token: write

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
          cache-dependency-path: package-lock.json

      - name: Install dependencies
        run: npm ci

      - name: Build VitePress site
        run: npm run docs:build

      - name: Upload artifact
        uses: actions/upload-pages-artifact@v3
        with:
          path: docs/.vitepress/dist

  deploy:
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}
    runs-on: ubuntu-latest
    needs: build
    steps:
      - name: Deploy to GitHub Pages
        id: deployment
        uses: actions/deploy-pages@v4


================================================
FILE: .github/workflows/release.yml
================================================
# -*- coding: utf-8 -*-
name: Build and Release

on:
  push:
    tags:
      - 'v*.*.*'
  workflow_dispatch:
    inputs:
      tag_name:
        description: 'Tag name for testing (e.g. v2.0.1)'
        required: false
        type: string

jobs:
  build:
    runs-on: windows-latest
    strategy:
      matrix:
        architecture: [x64]
        python-version: ['3.11']

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python-version }}

      - name: Install build dependencies
        run: |
          python -m pip install --upgrade pip
          pip install -e ".[dev]"

      - name: Run tests
        run: |
          pytest -v --cov=remark --cov-report=term-missing

      - name: Install PyInstaller
        run: |
          pip install pyinstaller

      - name: Build executable
        run: |
          pyinstaller remark.spec --clean

      - name: Rename executable with version
        run: |
          if ($env:GITHUB_REF -match "refs/tags/v(.*)") {
            $version = $matches[1]
          } else {
            $version = "dev"
          }
          Copy-Item "dist\windows-folder-remark.exe" "dist\windows-folder-remark-$version.exe"

      - name: Generate checksum
        run: |
          if ($env:GITHUB_REF -match "refs/tags/v(.*)") {
            $version = $matches[1]
          } else {
            $version = "dev"
          }
          $file = "dist\windows-folder-remark-$version.exe"
          certutil -hashfile $file SHA256 > "$file.sha256"

      - name: Upload artifact
        uses: actions/upload-artifact@v4
        with:
          name: windows-folder-remark-${{ matrix.architecture }}
          path: |
            dist/*.exe
            dist/*.sha256

  release:
    needs: build
    runs-on: windows-latest
    permissions:
      contents: write

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Download all artifacts
        uses: actions/download-artifact@v4
        with:
          path: artifacts

      - name: Create Release
        uses: softprops/action-gh-release@v2
        with:
          tag_name: ${{ inputs.tag_name || github.ref_name }}
          draft: ${{ github.event_name == 'workflow_dispatch' }}
          prerelease: false
          generate_release_notes: true
          files: |
            artifacts/**/*.exe
            artifacts/**/*.sha256
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}


================================================
FILE: .github/workflows/test.yml
================================================
name: Test and Build

on:
  pull_request:
    branches: [main]
  workflow_dispatch:

jobs:
  test:
    runs-on: windows-latest
    strategy:
      matrix:
        python-version: ["3.11", "3.12", "3.13"]

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up Python ${{ matrix.python-version }}
        uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python-version }}

      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip
          pip install -e ".[dev]"

      - name: Run tests
        run: pytest -v --cov=remark --cov-report=term-missing

  build-verify:
    runs-on: windows-latest
    needs: test

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: "3.11"

      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip
          pip install -e ".[dev]"

      - name: Install PyInstaller
        run: pip install pyinstaller

      - name: Build executable
        run: pyinstaller remark.spec --clean

      - name: Verify executable
        run: |
          $exePath = "dist\windows-folder-remark.exe"
          if (-not (Test-Path $exePath)) {
            Write-Error "Executable not found!"
            exit 1
          }
          & $exePath --help
          if ($LASTEXITCODE -ne 0) {
            Write-Error "Executable failed to run!"
            exit 1
          }
          Write-Output "Build successful!"
        shell: pwsh


================================================
FILE: .gitignore
================================================
# IDE
.idea

# Build
build/
dist/
# PyInstaller spec files (keep remark.spec)
# *.spec

# Python
__pycache__/
*.py[cod]
*$py.class
*.egg-info/
.eggs/
.venv/
venv/
*.egg

# Testing
.pytest_cache/
.coverage
htmlcov/
*.cover

# Type checking
.mypy_cache/
.dmypy.json
dmypy.json
.ruff_cache/

# Distribution
*.whl
*.tar.gz
/tools/upx/

# i18n - 不再忽略 .mo 文件,需要提交到仓库以便 CI 使用
# .mo 文件由 pybabel compile 生成,从 .po 文件编译而来

# bv (beads viewer) local config and caches
.bv/

# Node.js
node_modules/


================================================
FILE: .pre-commit-config.yaml
================================================
# Pre-commit configuration
# https://pre-commit.com/

default_language_version:
  python: python3.9

default_stages: [pre-commit]

repos:
  # Ruff - 代码检查和格式化
  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.8.4
    hooks:
      - id: ruff
        types: [python]
        args: [--fix, --exit-non-zero-on-fix]
      - id: ruff-format
        types: [python]

  # Pre-commit hooks for general checks
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v5.0.0
    hooks:
      - id: trailing-whitespace     # 删除行尾空格
      - id: end-of-file-fixer       # 文件末尾添加换行
      - id: mixed-line-ending       # 强制统一行尾符为 LF
        args: ['--fix=lf']
        exclude: ^(\.bat|\.cmd|\.ps1)$
      - id: check-yaml              # 检查 YAML 语法
      - id: check-toml              # 检查 TOML 语法
      - id: check-added-large-files # 防止大文件提交
        args: ['--maxkb=1000']
      - id: check-merge-conflict    # 检查合并冲突标记
      - id: check-case-conflict     # 检查大小写冲突
      - id: check-docstring-first   # 检查 docstring 是否在代码前

  # Mypy - 静态类型检查
  - repo: https://github.com/pre-commit/mirrors-mypy
    rev: v1.14.1
    hooks:
      - id: mypy
        types: [python]
        additional_dependencies:
          - types-setuptools
          - types-requests>=2.31.0
          - types-tqdm>=4.66.0
        exclude: ^remark\.py$

  # Local hooks - pre-commit 和 pre-push 阶段
  - repo: local
    hooks:
      - id: run-tests
        name: Run tests
        entry: pytest -v -m "not slow" --cov=remark --cov-report=term-missing
        language: system
        types: [python]
        stages: [pre-commit, pre-push]
        pass_filenames: false

      - id: build-exe
        name: Build exe (Windows only)
        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'
        language: system
        files: '^(.*\.py|.*\.spec)$'
        stages: [pre-push]
        pass_filenames: false

      # i18n 相关检查
      - id: extract-translations
        name: Extract i18n messages
        entry: bash -c 'pybabel extract -k "_" -k "gettext" -k "ngettext" -o messages.pot remark/ && echo "Translations extracted successfully"'
        language: system
        types: [python]
        stages: [pre-commit]
        pass_filenames: false
        require_serial: true

      - id: check-i18n-completeness
        name: Check i18n completeness
        entry: bash -c 'pybabel update -i messages.pot -d locale && python scripts/check_i18n.py locale/*/LC_MESSAGES/messages.po'
        language: system
        types: [python]
        stages: [pre-commit]
        pass_filenames: false

      - id: check-po-files
        name: Check PO files with polint
        entry: bash -c 'if compgen -G "locale/*/LC_MESSAGES/*.po" > /dev/null; then polint locale/*/LC_MESSAGES/*.po || true; fi'
        language: system
        types: [po]
        stages: [pre-commit]

# 全局排除
exclude: |
  ^(?:
      \.venv/|
      \.git/|
      \.mypy_cache/|
      __pycache__/|
      build/|
      dist/|
      *.egg-info/
    )


================================================
FILE: .python-version
================================================
3.11.7


================================================
FILE: CHANGELOG.md
================================================
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added
- GitHub Actions CI/CD for automated releases
- PyInstaller configuration for Windows executable builds
- Version management script for release automation
- Support for both 32-bit and 64-bit Windows builds

## [2.0.0] - Unreleased

### Added
- UTF-16 encoding detection and conversion for desktop.ini
- User confirmation prompt before encoding conversion
- EncodingConversionCanceled exception for safer error handling
- Smart delete logic: removes InfoTip while preserving other desktop.ini settings
- Top-level exception handling in CLI main()
- Windows platform check before running

### Changed
- Improved desktop.ini read/write operations with encoding safety
- Better error handling with exception-based flow control
- Enhanced folder comment handling with dedicated storage layer
- Refactored command-line argument parsing with argparse

### Fixed
- Fixed desktop.ini encoding issues
- Path handling with spaces using os.path.join()
- Exception handling for encoding conversion

### Removed
- File comment functionality (COM component and Property Store)
- notify_shell_update function (no longer needed)
- File-related imports and handlers

## [1.0] - 2022-05-03

### Added
- Interactive mode with continuous loop for batch processing
- Help system with usage instructions
- Comment length validation
- Complete exception handling mechanism

### Fixed
- Path with spaces handling issue
- Command injection vulnerability (subprocess replaced os.system)
- Encoding conversion exception handling
- Explicit file write encoding specification

### Changed
- Improved help messages and usage prompts
- Packaged as Windows executable

[Unreleased]: https://github.com/piratf/windows-folder-remark/compare/v2.0.0...HEAD
[2.0.0]: https://github.com/piratf/windows-folder-remark/compare/v1.0...v2.0.0
[1.0]: https://github.com/piratf/windows-folder-remark/releases/tag/v1.0


================================================
FILE: LICENSE
================================================
The MIT License (MIT)

Copyright (c) 2016 潘

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.en.md
================================================
# Windows Folder Remark/Comment Tool

**[English](README.en.md)** | [中文文档](README.md)

[![PyPI](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/)
[![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
[![en](https://img.shields.io/badge/lang-en-blue.svg)](README.en.md)
[![zh](https://img.shields.io/badge/lang-zh-red.svg)](README.md)

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. Perfect for organizing your files with custom descriptions.

## ⭐ Star Us

If you find this tool helpful, please consider giving it a star on GitHub!

## Why This Tool

- **Use and Go**: Runs when needed, exits when done — no system residency
- **Safe & Secure**: Completely local operation, no data upload, privacy protected
- **Portable**: Single-file exe packaging, no installation required

## Features

- Multi-language character support (UTF-16 encoding)
- Multi-language interface support (English, Chinese)
- Command-line and interactive modes
- Automatic encoding detection and repair
- Automatic update checking to stay current
- Right-click menu integration for quick access
- Single-file exe packaging, no Python environment required

## Installation

### Method 1: Using exe file (Recommended)

Download `windows-folder-remark.exe` from [releases](https://github.com/piratf/windows-folder-remark/releases) and use directly.

### Method 2: Install from source

```bash
# Clone repository
git clone https://github.com/piratf/windows-folder-remark.git
cd windows-folder-remark

# Install dependencies (no external dependencies)
pip install -e .

# Run
python -m remark.cli --help
```

## Usage

### Command-line Mode

```bash
# Add remark
windows-folder-remark.exe "C:\MyFolder" "This is my folder"

# View remark
windows-folder-remark.exe --view "C:\MyFolder"

# Delete remark
windows-folder-remark.exe --delete "C:\MyFolder"

# Check updates
windows-folder-remark.exe --update

# Install right-click menu
windows-folder-remark.exe --install

# Uninstall right-click menu
windows-folder-remark.exe --uninstall
```

### Interactive Mode

```bash
# Follow prompts after running
windows-folder-remark.exe
```

### Right-click Menu (Recommended)

After installing the right-click menu, you can add remarks directly in File Explorer by right-clicking folders:

```bash
# Install right-click menu
windows-folder-remark.exe --install
```

- **Windows 10**: Right-click folder to see "Add Folder Remark"
- **Windows 11**: Right-click folder → Click "Show more options" → Add Folder Remark

### Auto Update

The program automatically checks for updates on exit (once every 24 hours) and prompts if a new version is available.

You can also manually check for updates:

```bash
windows-folder-remark.exe --update
```

## Encoding Detection

When viewing remarks with `--view`, if the `desktop.ini` file is not in standard UTF-16 encoding, the tool will prompt you:

```
Warning: desktop.ini file encoding is utf-8, not standard UTF-16.
This may cause Chinese and other special characters to display abnormally.
Fix encoding to UTF-16? [Y/n]:
```

Select `Y` to automatically fix the encoding.

## Development

```bash
# Install development dependencies
pip install -e ".[dev]"

# Run tests
pytest

# Code check
ruff check .
ruff format .

# Type check
mypy remark/

# Build exe locally
python -m scripts.build
```

## How It Works

This tool implements folder remarks through these steps:

1. Create/modify `Desktop.ini` file in the folder
2. Write `[.ShellClassInfo]` section and `InfoTip` property
3. Save file with UTF-16 encoding
4. Set `Desktop.ini` as hidden and system attributes
5. Set folder as read-only (makes Windows read `Desktop.ini`)

Reference: [Microsoft Official Documentation](https://learn.microsoft.com/en-us/windows/win32/shell/how-to-customize-folders-with-desktop-ini)

## Notes

- May take a few minutes to display in File Explorer after modification
- Some file managers may not support folder remarks
- The tool modifies system attributes of folders

## License

MIT License


================================================
FILE: README.md
================================================
# Windows Folder Remark/Comment Tool - Windows 文件夹备注工具

**[English Documentation](README.en.md)** | [中文文档](README.md)

[![PyPI](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/)
[![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
[![en](https://img.shields.io/badge/lang-en-blue.svg)](README.en.md)
[![zh](https://img.shields.io/badge/lang-zh-red.svg)](README.md)

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. / 一个轻量级的命令行工具,通过 Desktop.ini 为 Windows 文件夹添加备注/评论。无系统驻留,无数据上传,安全放心,用完即走。

**Documentation**: [Full Documentation](https://piratf.github.io/windows-folder-remark/en/) | [完整文档](https://piratf.github.io/windows-folder-remark/zh/)

## ⭐ 支持

如果这个工具对你有帮助,请在 GitHub 上给个 Star!

## 工具优势

- **用完即走**:需要时运行,用完即退出,无系统驻留
- **安全放心**:完全本地运行,无数据上传,保护隐私
- **轻量便携**:单文件 exe 打包,无需安装,随处可用

## 特性

- 支持中文等多语言字符(UTF-16 编码)
- 支持中英文界面切换
- 命令行模式和交互模式
- 自动编码检测和修复
- 自动更新检查,保持最新版本
- 右键菜单集成,快速访问
- 单文件 exe 打包,无需 Python 环境

## 安装

### 方式一:使用 exe 文件(推荐)

下载 [releases](https://github.com/piratf/windows-folder-remark/releases) 中的 `windows-folder-remark.exe`,直接使用。

### 方式二:从源码安装

```bash
# 克隆仓库
git clone https://github.com/piratf/windows-folder-remark.git
cd windows-folder-remark

# 安装依赖(无外部依赖)
pip install -e .

# 运行
python -m remark.cli --help
```

## 使用方法

### 命令行模式

```bash
# 添加备注
windows-folder-remark.exe "C:\MyFolder" "这是我的文件夹"

# 查看备注
windows-folder-remark.exe --view "C:\MyFolder"

# 删除备注
windows-folder-remark.exe --delete "C:\MyFolder"

# 检查更新
windows-folder-remark.exe --update

# 安装右键菜单
windows-folder-remark.exe --install

# 卸载右键菜单
windows-folder-remark.exe --uninstall
```

### 交互模式

```bash
# 运行后根据提示操作
windows-folder-remark.exe
```

### 右键菜单(推荐)

安装右键菜单后,可以直接在文件资源管理器中右键文件夹添加备注:

```bash
# 安装右键菜单
windows-folder-remark.exe --install
```

- **Windows 10**:右键文件夹可直接看到「添加文件夹备注」
- **Windows 11**:右键文件夹 → 点击「显示更多选项」→ 添加文件夹备注

### 自动更新

程序会在退出时自动检查更新(每 24 小时一次),如有新版本会提示是否立即更新。

也可以手动检查更新:

```bash
windows-folder-remark.exe --update
```

## 编码检测

当使用 `--view` 查看备注时,如果检测到 `desktop.ini` 文件不是标准的 UTF-16 编码,工具会提醒你:

```
警告: desktop.ini 文件编码为 utf-8,不是标准的 UTF-16。
这可能导致中文等特殊字符显示异常。
是否修复编码为 UTF-16?[Y/n]:
```

选择 `Y` 可自动修复编码。

## 开发

```bash
# 安装开发依赖
pip install -e ".[dev]"

# 运行测试
pytest

# 代码检查
ruff check .
ruff format .

# 类型检查
mypy remark/

# 本地打包 exe
python -m scripts.build
```

## 原理说明

该工具通过以下步骤实现文件夹备注:

1. 在文件夹中创建/修改 `Desktop.ini` 文件
2. 写入 `[.ShellClassInfo]` 段落和 `InfoTip` 属性
3. 使用 UTF-16 编码保存文件
4. 将 `Desktop.ini` 设置为隐藏和系统属性
5. 将文件夹设置为只读属性(使 Windows 读取 `Desktop.ini`)

参考:[Microsoft 官方文档](https://learn.microsoft.com/en-us/windows/win32/shell/how-to-customize-folders-with-desktop-ini)

## 注意事项

- 修改后可能需要几分钟才能在资源管理器中显示
- 某些文件管理器可能不支持显示文件夹备注
- 工具会修改文件夹的系统属性

## 许可证

MIT License


================================================
FILE: babel.cfg
================================================
# Babel extraction configuration
# https://babel.pocoo.org/en/latest/messages.html

# Extract translatable strings from Python files
[python: remark/**/*.py]

# Keyword list - recognize strings in these function calls as translatable
keywords = _, gettext, ngettext

# Encoding
encoding = utf-8


================================================
FILE: docs/.vitepress/config.mjs
================================================
import { defineConfig } from 'vitepress'

export default defineConfig({
  title: 'Windows 文件夹备注工具',
  description: '一个轻量级的命令行工具,通过 Desktop.ini 为 Windows 文件夹添加备注/评论。无系统驻留,无数据上传,安全放心,用完即走。',
  base: '/windows-folder-remark/',
  lang: 'zh-CN',

  locales: {
    root: {
      label: '简体中文',
      lang: 'zh-CN'
    },
    en: {
      label: 'English',
      lang: 'en-US',
      link: '/en/'
    }
  },

  head: [
    ['link', { rel: 'alternate', hreflang: 'zh-CN', href: 'https://piratf.github.io/windows-folder-remark/' }],
    ['link', { rel: 'alternate', hreflang: 'en-US', href: 'https://piratf.github.io/windows-folder-remark/en/' }],
    ['link', { rel: 'alternate', hreflang: 'x-default', href: 'https://piratf.github.io/windows-folder-remark/en/' }]
  ],

  sitemap: {
    hostname: 'https://piratf.github.io',
    transformItems(items) {
      return items.map((item) => {
        return {
          url: '/windows-folder-remark' + (item.url.startsWith('/') ? '' : '/') + item.url,
          changefreq: 'weekly',
        }
      })
    }
  },

  themeConfig: {
    nav: () => [
      { text: '指南', link: '/guide/' },
      { text: 'English', link: '/en/' }
    ],

    sidebar: {
      '/': [
        {
          text: '指南',
          items: [
            { text: '介绍', link: '/' },
            { text: '快速开始', link: '/guide/getting-started' },
            { text: '使用方法', link: '/guide/usage' },
            { text: 'API 参考', link: '/guide/api' }
          ]
        }
      ],
      '/en/': [
        {
          text: 'Guide',
          items: [
            { text: 'Introduction', link: '/en/' },
            { text: 'Getting Started', link: '/en/guide/getting-started' },
            { text: 'Usage', link: '/en/guide/usage' },
            { text: 'API Reference', link: '/en/guide/api' }
          ]
        }
      ]
    }
  }
})


================================================
FILE: docs/en/guide/api.md
================================================
# API Reference

## Command-line Arguments

| Argument | Short | Description |
|---|---|---|
| `--help` | `-h` | Show help information |
| `--install` | | Install right-click menu |
| `--uninstall` | | Uninstall right-click menu |
| `--update` | | Check for updates |
| `--view <path>` | | View folder remark |
| `--delete <path>` | | Delete folder remark |
| `--gui <path>` | | GUI mode |
| `--lang <lang>` | `-L` | Set language (en, zh) |

## Exit Codes

| Code | Description |
|---|---|
| 0 | Success |
| 1 | Error |


================================================
FILE: docs/en/guide/getting-started.md
================================================
# Getting Started

## Download

Download `windows-folder-remark.exe` from [GitHub Releases](https://github.com/piratf/windows-folder-remark/releases).

## Basic Usage

### Add Remark

```bash
windows-folder-remark.exe "C:\MyFolder" "This is my folder"
```

### View Remark

```bash
windows-folder-remark.exe --view "C:\MyFolder"
```

### Delete Remark

```bash
windows-folder-remark.exe --delete "C:\MyFolder"
```

## Install Right-click Menu

```bash
windows-folder-remark.exe --install
```

After installation, you can add remarks directly in File Explorer by right-clicking folders.


================================================
FILE: docs/en/guide/usage.md
================================================
# Usage

## Command-line Mode

```bash
# Add remark
windows-folder-remark.exe "C:\MyFolder" "My Folder"

# View remark
windows-folder-remark.exe --view "C:\MyFolder"

# Delete remark
windows-folder-remark.exe --delete "C:\MyFolder"
```

## Interactive Mode

```bash
windows-folder-remark.exe
```

## Language Switch

```bash
# Use English
windows-folder-remark.exe --lang en

# Use Chinese
windows-folder-remark.exe --lang zh
```

## Auto Update

The program automatically checks for updates on exit (once every 24 hours).

Manual update check:

```bash
windows-folder-remark.exe --update
```


================================================
FILE: docs/en/index.md
================================================
---
layout: home

head:
  - - script
    - type: application/ld+json
    - |
      {
        "@context": "https://schema.org",
        "@type": "SoftwareSourceCode",
        "name": "Windows Folder Remark Tool",
        "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.",
        "programmingLanguage": "Python",
        "codeRepository": "https://github.com/piratf/windows-folder-remark",
        "url": "https://piratf.github.io/windows-folder-remark/en/",
        "version": "2.0.7",
        "license": "MIT",
        "offers": {
          "@type": "Offer",
          "price": "0",
          "priceCurrency": "USD"
        }
      }

hero:
  name: Windows Folder Remark Tool
  text: A Lightweight CLI Tool for Windows Folder Remarks
  tagline: Add remarks/comments to Windows folders via Desktop.ini
  actions:
    - theme: brand
      text: Get Started
      link: /en/guide/getting-started
    - theme: alt
      text: GitHub
      link: https://github.com/piratf/windows-folder-remark

features:
  - title: Use and Go
    details: Runs when needed, exits when done — no system residency
  - title: Safe & Secure
    details: Completely local operation, no data upload, privacy protected
  - title: Portable
    details: Single-file exe packaging, no installation required
  - title: Multi-language Support
    details: English and Chinese interface with auto language detection
  - title: UTF-16 Encoding
    details: Full support for Chinese and other special characters
  - title: Auto Update
    details: Built-in update checking to stay current
---

## ⭐ Star Us

If you find this tool helpful, please give it a star on [GitHub](https://github.com/piratf/windows-folder-remark)!


================================================
FILE: docs/guide/api.md
================================================
# API 参考

## 命令行参数

| 参数 | 简写 | 说明 |
|---|---|---|
| `--help` | `-h` | 显示帮助信息 |
| `--install` | | 安装右键菜单 |
| `--uninstall` | | 卸载右键菜单 |
| `--update` | | 检查更新 |
| `--view <path>` | | 查看文件夹备注 |
| `--delete <path>` | | 删除文件夹备注 |
| `--gui <path>` | | GUI 模式 |
| `--lang <lang>` | `-L` | 设置语言 (en, zh) |

## 退出码

| 代码 | 说明 |
|---|---|
| 0 | 成功 |
| 1 | 错误 |


================================================
FILE: docs/guide/getting-started.md
================================================
# 快速开始

## 下载

从 [GitHub Releases](https://github.com/piratf/windows-folder-remark/releases) 下载 `windows-folder-remark.exe`。

## 基本使用

### 添加备注

```bash
windows-folder-remark.exe "C:\MyFolder" "这是我的文件夹"
```

### 查看备注

```bash
windows-folder-remark.exe --view "C:\MyFolder"
```

### 删除备注

```bash
windows-folder-remark.exe --delete "C:\MyFolder"
```

## 安装右键菜单

```bash
windows-folder-remark.exe --install
```

安装后可以在文件资源管理器中右键文件夹直接添加备注。


================================================
FILE: docs/guide/usage.md
================================================
# 使用方法

## 命令行模式

```bash
# 添加备注
windows-folder-remark.exe "C:\MyFolder" "我的文件夹"

# 查看备注
windows-folder-remark.exe --view "C:\MyFolder"

# 删除备注
windows-folder-remark.exe --delete "C:\MyFolder"
```

## 交互模式

```bash
windows-folder-remark.exe
```

## 语言切换

```bash
# 使用中文
windows-folder-remark.exe --lang zh

# 使用英文
windows-folder-remark.exe --lang en
```

## 自动更新

程序会在退出时自动检查更新(每 24 小时一次)。

手动检查更新:

```bash
windows-folder-remark.exe --update
```


================================================
FILE: docs/index.md
================================================
---
layout: home

head:
  - - script
    - type: application/ld+json
    - |
      {
        "@context": "https://schema.org",
        "@type": "SoftwareSourceCode",
        "name": "Windows 文件夹备注工具",
        "description": "一个轻量级的命令行工具,通过 Desktop.ini 为 Windows 文件夹添加备注/评论。无系统驻留,无数据上传,安全放心,用完即走。",
        "programmingLanguage": "Python",
        "codeRepository": "https://github.com/piratf/windows-folder-remark",
        "url": "https://piratf.github.io/windows-folder-remark/",
        "version": "2.0.7",
        "license": "MIT",
        "offers": {
          "@type": "Offer",
          "price": "0",
          "priceCurrency": "USD"
        }
      }

hero:
  name: Windows 文件夹备注工具
  text: 轻量级 Windows 文件夹备注工具
  tagline: 通过 Desktop.ini 为 Windows 文件夹添加备注/评论
  actions:
    - theme: brand
      text: 快速开始
      link: /guide/getting-started
    - theme: alt
      text: GitHub
      link: https://github.com/piratf/windows-folder-remark

features:
  - title: 用完即走
    details: 需要时运行,用完即退出,无系统驻留
  - title: 安全放心
    details: 完全本地运行,无数据上传,保护隐私
  - title: 轻量便携
    details: 单文件 exe 打包,无需安装,随处可用
  - title: 多语言支持
    details: 支持中英文界面,自动检测系统语言
  - title: UTF-16 编码
    details: 支持中文等多语言字符
  - title: 自动更新
    details: 内置更新检查,保持最新版本
---

## ⭐ 支持

如果这个工具对你有帮助,请在 [GitHub](https://github.com/piratf/windows-folder-remark) 上给个 Star!


================================================
FILE: docs/robots.txt
================================================
User-agent: *
Allow: /

Sitemap: https://piratf.github.io/windows-folder-remark/sitemap.xml


================================================
FILE: locale/zh/LC_MESSAGES/messages.po
================================================
# Chinese translations for PROJECT.
# Copyright (C) 2026 ORGANIZATION
# This file is distributed under the same license as the PROJECT project.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2026.
#
msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2026-01-31 14:42+0800\n"
"PO-Revision-Date: 2026-01-29 22:35+0800\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: zh\n"
"Language-Team: zh <LL@li.org>\n"
"Plural-Forms: nplurals=1; plural=0;\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.17.0\n"

#: remark/cli/commands.py:73
#, python-brace-format
msgid "Path does not exist: {path}"
msgstr "路径不存在: {path}"

#: remark/cli/commands.py:76
#, python-brace-format
msgid "Path is not a folder: {path}"
msgstr "路径不是文件夹: {path}"

#: remark/cli/commands.py:98
#, python-brace-format
msgid "Current version: {version}"
msgstr "当前版本: {version}"

#: remark/cli/commands.py:99
msgid "Checking for updates..."
msgstr "正在检查更新..."

#: remark/cli/commands.py:104
#, python-brace-format
msgid ""
"\n"
"New version found: {tag_name}"
msgstr ""
"\n"
"发现新版本: {tag_name}"

#: remark/cli/commands.py:105 remark/cli/commands.py:133
#, python-brace-format
msgid "Update notes: {notes}"
msgstr "更新说明: {notes}"

#: remark/cli/commands.py:106 remark/cli/commands.py:134
#, python-brace-format
msgid "Full changelog: {url}"
msgstr "完整更新日志: {url}"

#: remark/cli/commands.py:107
msgid ""
"\n"
"Update now? [Y/n]: "
msgstr ""
"\n"
"是否立即更新? [Y/n]: "

#: remark/cli/commands.py:112
msgid "Already at the latest version"
msgstr "已是最新版本"

#: remark/cli/commands.py:129
#, python-brace-format
msgid ""
"\n"
"New version available: {tag_name} (Current version: {version})"
msgstr ""
"\n"
"发现新版本: {tag_name} (当前版本: {version})"

#: remark/cli/commands.py:135
msgid "Update now? [Y/n]: "
msgstr "是否立即更新? [Y/n]: "

#: remark/cli/commands.py:142
msgid "Downloading new version..."
msgstr "正在下载新版本..."

#: remark/cli/commands.py:149
msgid "Download complete, preparing update..."
msgstr "下载完成,准备更新..."

#: remark/cli/commands.py:153
msgid "Update program has started, the application will exit..."
msgstr "更新程序已启动,程序即将退出..."

#: remark/cli/commands.py:154
msgid "Please wait a few moments, the update will complete automatically."
msgstr "请等待几秒钟,更新将自动完成。"

#: remark/cli/commands.py:160
msgid "Download failed: Connection reset by server"
msgstr "下载失败:连接被服务器断开"

#: remark/cli/commands.py:161
msgid "Please try again later, or visit the following link to download manually:"
msgstr "请稍后重试,或访问以下链接手动下载:"

#: remark/cli/commands.py:164
msgid "Download failed: Request timeout"
msgstr "下载失败:请求超时"

#: remark/cli/commands.py:165 remark/cli/commands.py:169
msgid ""
"Please check your network connection, or visit the following link to "
"download manually:"
msgstr "请检查网络连接,或访问以下链接手动下载:"

#: remark/cli/commands.py:168
msgid "Download failed: Unable to connect to server"
msgstr "下载失败:无法连接到服务器"

#: remark/cli/commands.py:172
msgid "Download failed, please check your network or download manually"
msgstr "下载失败,请检查网络连接或手动下载更新"

#: remark/cli/commands.py:175
#, python-brace-format
msgid "Update failed: {error}"
msgstr "更新失败: {error}"

#: remark/cli/commands.py:176
#, python-brace-format
msgid "Manual download: {url}"
msgstr "手动下载: {url}"

#: remark/cli/commands.py:193
msgid "Right-click menu installed successfully"
msgstr "右键菜单安装成功"

#: remark/cli/commands.py:195
msgid "Usage Instructions:"
msgstr "使用说明:"

#: remark/cli/commands.py:196
msgid "  Windows 10: Right-click folder to see 'Add Folder Remark'"
msgstr "  Windows 10: 右键文件夹可直接看到「添加文件夹备注」"

#: remark/cli/commands.py:197
msgid ""
"  Windows 11: Right-click folder → Click 'Show more options' → Add Folder"
" Remark"
msgstr "  Windows 11: 右键文件夹 → 点击「显示更多选项」→ 添加文件夹备注"

#: remark/cli/commands.py:200
msgid "Right-click menu installation failed"
msgstr "右键菜单安装失败"

#: remark/cli/commands.py:206
msgid "Right-click menu uninstalled"
msgstr "右键菜单已卸载"

#: remark/cli/commands.py:209
msgid "Right-click menu uninstallation failed"
msgstr "右键菜单卸载失败"

#: remark/cli/commands.py:236 remark/storage/desktop_ini.py:309
#, python-brace-format
msgid "Warning: desktop.ini file encoding is {encoding}, not standard UTF-16."
msgstr "警告: desktop.ini 文件编码为 {encoding},不是标准的 UTF-16。"

#: remark/cli/commands.py:237 remark/storage/desktop_ini.py:310
msgid "unknown"
msgstr "未知"

#: remark/cli/commands.py:239
msgid "This may cause Chinese and other special characters to display abnormally."
msgstr "这可能导致中文等特殊字符显示异常。"

#: remark/cli/commands.py:243
msgid "Fix encoding to UTF-16? [Y/n]: "
msgstr "是否修复编码为 UTF-16?[Y/n]: "

#: remark/cli/commands.py:246
msgid "Fixed to UTF-16 encoding"
msgstr "已修复为 UTF-16 编码"

#: remark/cli/commands.py:248
msgid "Failed to fix encoding"
msgstr "修复失败"

#: remark/cli/commands.py:251
msgid "Skip encoding fix"
msgstr "跳过编码修复"

#: remark/cli/commands.py:254 remark/storage/desktop_ini.py:335
msgid "Please enter Y or n"
msgstr "请输入 Y 或 n"

#: remark/cli/commands.py:259
#, python-brace-format
msgid "Current remark: {remark}"
msgstr "当前备注: {remark}"

#: remark/cli/commands.py:261 remark/core/folder_handler.py:85
msgid "This folder has no remark"
msgstr "该文件夹没有备注"

#: remark/cli/commands.py:266
#, python-brace-format
msgid "Windows Folder Remark Tool v{version}"
msgstr "Windows 文件夹备注工具 v{version}"

#: remark/cli/commands.py:267
msgid "Tip: Press Ctrl + C to exit"
msgstr "提示: 按 Ctrl + C 退出程序"

#: remark/cli/commands.py:268
msgid "Tip: Use #help to see available commands"
msgstr "提示: 使用 #help 查看可用命令"

#: remark/cli/commands.py:276
msgid "Enter folder path (or drag here): "
msgstr "请输入文件夹路径(或拖动到这里): "

#: remark/cli/commands.py:277
msgid "Enter remark:"
msgstr "请输入备注:"

#: remark/cli/commands.py:296
msgid "Path does not exist, please re-enter"
msgstr "路径不存在,请重新输入"

#: remark/cli/commands.py:300
msgid "This is a 'file', currently only supports adding remarks to 'folders'"
msgstr "这是一个「文件」,当前仅支持为「文件夹」添加备注"

#: remark/cli/commands.py:305
msgid "Remark cannot be empty"
msgstr "备注不要为空哦"

#: remark/cli/commands.py:311
msgid " ❤ Thank you for using"
msgstr " ❤ 感谢使用"

#: remark/cli/commands.py:313
msgid "Continue processing or press Ctrl + C to exit"
msgstr "继续处理或按 Ctrl + C 退出程序"

#: remark/cli/commands.py:327
msgid "Available commands:"
msgstr "可用命令:"

#: remark/cli/commands.py:331 remark/cli/commands.py:345
msgid "Tip: Press Tab to complete commands"
msgstr "提示: 按 Tab 补全命令"

#: remark/cli/commands.py:333 remark/cli/commands.py:347
msgid "Tip: Install pyreadline3 for Tab completion (pip install pyreadline3)"
msgstr "提示: 安装 pyreadline3 以使用 Tab 补全(pip install pyreadline3)"

#: remark/cli/commands.py:337
msgid "Interactive Commands:"
msgstr "交互命令:"

#: remark/cli/commands.py:338
msgid "  #help     Show this help message"
msgstr "  #help     显示此帮助信息"

#: remark/cli/commands.py:339
msgid "  #install  Install right-click menu"
msgstr "  #install  安装右键菜单"

#: remark/cli/commands.py:340
msgid "  #uninstall Uninstall right-click menu"
msgstr "  #uninstall 卸载右键菜单"

#: remark/cli/commands.py:341
msgid "  #update   Check for updates"
msgstr "  #update   检查更新"

#: remark/cli/commands.py:343
msgid "Or simply enter a folder path to add remarks"
msgstr "或者直接输入文件夹路径来添加备注"

#: remark/cli/commands.py:351
msgid "Windows Folder Remark Tool"
msgstr "Windows 文件夹备注工具"

#: remark/cli/commands.py:352
msgid "Usage:"
msgstr "使用方法:"

#: remark/cli/commands.py:353
msgid "  Interactive mode: python remark.py"
msgstr "  交互模式: python remark.py"

#: remark/cli/commands.py:354
msgid "  Command line mode: python remark.py [options] [arguments]"
msgstr "  命令行模式: python remark.py [选项] [参数]"

#: remark/cli/commands.py:355
msgid "Options:"
msgstr "选项:"

#: remark/cli/commands.py:356
msgid "  --install          Install right-click menu"
msgstr "  --install          安装右键菜单"

#: remark/cli/commands.py:357
msgid "  --uninstall        Uninstall right-click menu"
msgstr "  --uninstall        卸载右键菜单"

#: remark/cli/commands.py:358
msgid "  --update           Check for updates"
msgstr "  --update           检查更新"

#: remark/cli/commands.py:359
msgid "  --gui <path>        GUI mode (called from right-click menu)"
msgstr "  --gui <路径>       GUI 模式(右键菜单调用)"

#: remark/cli/commands.py:360
msgid "  --delete <path>     Delete remark"
msgstr "  --delete <路径>    删除备注"

#: remark/cli/commands.py:361
#, fuzzy
msgid "  --view <path>       View remark"
msgstr "  --view <路径>      查看备注"

#: remark/cli/commands.py:362
msgid "  --help, -h         Show help information"
msgstr "  --help, -h         显示帮助信息"

#: remark/cli/commands.py:363
msgid "Interactive Commands (available in interactive mode):"
msgstr "交互命令(在交互模式中可用):"

#: remark/cli/commands.py:364
msgid "  #help              Show interactive help"
msgstr "  #help              显示交互帮助"

#: remark/cli/commands.py:365
msgid "  #install           Install right-click menu"
msgstr "  #install           安装右键菜单"

#: remark/cli/commands.py:366
msgid "  #uninstall         Uninstall right-click menu"
msgstr "  #uninstall         卸载右键菜单"

#: remark/cli/commands.py:367
msgid "  #update            Check for updates"
msgstr "  #update            检查更新"

#: remark/cli/commands.py:368
msgid "Examples:"
msgstr "示例:"

#: remark/cli/commands.py:369
msgid " [Add remark] python remark.py \"C:\\\\MyFolder\" \"My Folder\""
msgstr " [添加备注] python remark.py \"C:\\\\MyFolder\" \"这是我的文件夹\""

#: remark/cli/commands.py:370
msgid " [Delete remark] python remark.py --delete \"C:\\\\MyFolder\""
msgstr " [删除备注] python remark.py --delete \"C:\\\\MyFolder\""

#: remark/cli/commands.py:371
msgid " [View current remark] python remark.py --view \"C:\\\\MyFolder\""
msgstr " [查看当前备注] python remark.py --view \"C:\\\\MyFolder\""

#: remark/cli/commands.py:372
msgid " [Install right-click menu] python remark.py --install"
msgstr " [安装右键菜单] python remark.py --install"

#: remark/cli/commands.py:373
msgid " [Check for updates] python remark.py --update"
msgstr " [检查更新] python remark.py --update"

#: remark/cli/commands.py:430
msgid "Error: Path does not exist or not quoted"
msgstr "错误: 路径不存在或未使用引号"

#: remark/cli/commands.py:431
msgid "Hint: Use quotes when path contains spaces"
msgstr "提示: 路径包含空格时请使用引号"

#: remark/cli/commands.py:432
msgid "  windows-folder-remark \"C:\\\\My Documents\" \"Remark content\""
msgstr "  windows-folder-remark \"C:\\\\My Documents\" \"备注内容\""

#: remark/cli/commands.py:437
#, python-brace-format
msgid "Detected path: {path}"
msgstr "检测到路径: {path}"

#: remark/cli/commands.py:440 remark/cli/commands.py:482
msgid "Error: This is a file, the tool can only set remarks for folders"
msgstr "错误: 这是一个文件,工具只能为文件夹设置备注"

#: remark/cli/commands.py:445
#, python-brace-format
msgid "Remark content: {remark}"
msgstr "备注内容: {remark}"

#: remark/cli/commands.py:447
msgid "(Will view existing remark)"
msgstr "(将查看现有备注)"

#: remark/cli/commands.py:449
msgid "Continue? [Y/n]: "
msgstr "是否继续? [Y/n]: "

#: remark/cli/commands.py:568
msgid ""
"\n"
"Operation cancelled"
msgstr ""
"\n"
"操作已取消"

#: remark/cli/commands.py:571
#, python-brace-format
msgid "An error occurred: {error}"
msgstr "发生错误: {error}"

#: remark/core/folder_handler.py:24
#, python-brace-format
msgid "Path is not a folder: {folder_path}"
msgstr "路径不是文件夹: {folder_path}"

#: remark/core/folder_handler.py:29
#, python-brace-format
msgid "Remark length exceeds limit, maximum length is {length} characters"
msgstr "备注长度超过限制,最大长度为 {length} 个字符"

#: remark/core/folder_handler.py:47 remark/core/folder_handler.py:90
msgid "Failed to clear file attributes"
msgstr "清除文件属性失败"

#: remark/core/folder_handler.py:52
msgid "Failed to write desktop.ini"
msgstr "写入 desktop.ini 失败"

#: remark/core/folder_handler.py:57
msgid "Failed to set file attributes"
msgstr "设置文件属性失败"

#: remark/core/folder_handler.py:62
msgid "Failed to set folder attributes"
msgstr "设置文件夹属性失败"

#: remark/core/folder_handler.py:66
#, python-brace-format
msgid "Remark [{remark}] has been set for folder [{folder_path}]"
msgstr "已经为文件夹 [{folder_path}] 设置备注 [{remark}]"

#: remark/core/folder_handler.py:70
msgid "Remark added successfully, may take a few minutes to display"
msgstr "备注添加成功,可能需要过几分钟才会显示"

#: remark/core/folder_handler.py:73
#, python-brace-format
msgid "Failed to set remark: {error}"
msgstr "设置备注失败: {error}"

#: remark/core/folder_handler.py:95
msgid "Failed to remove remark"
msgstr "移除备注失败"

#: remark/core/folder_handler.py:102
msgid "Failed to restore file attributes"
msgstr "恢复文件属性失败"

#: remark/core/folder_handler.py:105
msgid "Remark deleted successfully"
msgstr "备注删除成功"

#: remark/storage/desktop_ini.py:313
msgid "This file needs to be converted to UTF-16 encoding before modification."
msgstr "修改此文件前需要先转换为 UTF-16 编码。"

#: remark/storage/desktop_ini.py:314
msgid ""
"The original content will be preserved, only the encoding format will "
"change."
msgstr "原内容会被保留,仅改变编码格式。"

#: remark/storage/desktop_ini.py:321
msgid ""
"\n"
"Current file content:"
msgstr ""
"\n"
"当前文件内容:"

#: remark/storage/desktop_ini.py:328
msgid ""
"\n"
"Convert to UTF-16 encoding and continue? [Y/n]: "
msgstr ""
"\n"
"是否转换为 UTF-16 编码后继续?[Y/n]: "

#: remark/storage/desktop_ini.py:332 remark/storage/desktop_ini.py:347
msgid "Operation cancelled."
msgstr "操作已取消。"

#: remark/storage/desktop_ini.py:341
msgid "Converted to UTF-16 encoding."
msgstr "已转换为 UTF-16 编码。"

#: remark/storage/desktop_ini.py:346
#, python-brace-format
msgid "Conversion failed: {error}"
msgstr "转换失败: {error}"

#: remark/utils/platform.py:13
msgid ""
"Error: This tool adds remarks to files/folders on Windows, other systems "
"are not supported."
msgstr "错误: 此工具为 Windows 系统中的文件/文件夹添加备注,暂不支持其他系统。"

#: remark/utils/platform.py:14
#, python-brace-format
msgid "Current system: {system}"
msgstr "当前系统: {system}"

#~ msgid "Detected multiple possible paths, please select:"
#~ msgstr "检测到多个可能的路径,请选择:"

#~ msgid " [file]"
#~ msgstr " [文件]"

#~ msgid ""
#~ "\n"
#~ "[{index}] Path: {path}{type_mark}"
#~ msgstr ""
#~ "\n"
#~ "[{index}] 路径: {path}{type_mark}"

#~ msgid "    Remaining remarks: {remarks}"
#~ msgstr "    剩余备注: {remarks}"

#~ msgid "    (Will view existing remark)"
#~ msgstr "    (将查看现有备注)"

#~ msgid ""
#~ "\n"
#~ "[0] Cancel"
#~ msgstr ""
#~ "\n"
#~ "[0] 取消"

#~ msgid ""
#~ "\n"
#~ "Please select [0-{max}]: "
#~ msgstr ""
#~ "\n"
#~ "请选择 [0-{max}]: "

#~ msgid ""
#~ "\n"
#~ "Error: This is a file, the tool"
#~ " can only set remarks for folders,"
#~ " please reselect"
#~ msgstr ""
#~ "\n"
#~ "错误: 这是一个文件,工具只能为文件夹设置备注,请重新选择"

#~ msgid "Invalid selection, please try again"
#~ msgstr "无效选择,请重试"



================================================
FILE: messages.pot
================================================
# Translations template for PROJECT.
# Copyright (C) 2026 ORGANIZATION
# This file is distributed under the same license as the PROJECT project.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2026.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2026-01-31 14:42+0800\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.17.0\n"

#: remark/cli/commands.py:73
#, python-brace-format
msgid "Path does not exist: {path}"
msgstr ""

#: remark/cli/commands.py:76
#, python-brace-format
msgid "Path is not a folder: {path}"
msgstr ""

#: remark/cli/commands.py:98
#, python-brace-format
msgid "Current version: {version}"
msgstr ""

#: remark/cli/commands.py:99
msgid "Checking for updates..."
msgstr ""

#: remark/cli/commands.py:104
#, python-brace-format
msgid ""
"\n"
"New version found: {tag_name}"
msgstr ""

#: remark/cli/commands.py:105 remark/cli/commands.py:133
#, python-brace-format
msgid "Update notes: {notes}"
msgstr ""

#: remark/cli/commands.py:106 remark/cli/commands.py:134
#, python-brace-format
msgid "Full changelog: {url}"
msgstr ""

#: remark/cli/commands.py:107
msgid ""
"\n"
"Update now? [Y/n]: "
msgstr ""

#: remark/cli/commands.py:112
msgid "Already at the latest version"
msgstr ""

#: remark/cli/commands.py:129
#, python-brace-format
msgid ""
"\n"
"New version available: {tag_name} (Current version: {version})"
msgstr ""

#: remark/cli/commands.py:135
msgid "Update now? [Y/n]: "
msgstr ""

#: remark/cli/commands.py:142
msgid "Downloading new version..."
msgstr ""

#: remark/cli/commands.py:149
msgid "Download complete, preparing update..."
msgstr ""

#: remark/cli/commands.py:153
msgid "Update program has started, the application will exit..."
msgstr ""

#: remark/cli/commands.py:154
msgid "Please wait a few moments, the update will complete automatically."
msgstr ""

#: remark/cli/commands.py:160
msgid "Download failed: Connection reset by server"
msgstr ""

#: remark/cli/commands.py:161
msgid "Please try again later, or visit the following link to download manually:"
msgstr ""

#: remark/cli/commands.py:164
msgid "Download failed: Request timeout"
msgstr ""

#: remark/cli/commands.py:165 remark/cli/commands.py:169
msgid ""
"Please check your network connection, or visit the following link to "
"download manually:"
msgstr ""

#: remark/cli/commands.py:168
msgid "Download failed: Unable to connect to server"
msgstr ""

#: remark/cli/commands.py:172
msgid "Download failed, please check your network or download manually"
msgstr ""

#: remark/cli/commands.py:175
#, python-brace-format
msgid "Update failed: {error}"
msgstr ""

#: remark/cli/commands.py:176
#, python-brace-format
msgid "Manual download: {url}"
msgstr ""

#: remark/cli/commands.py:193
msgid "Right-click menu installed successfully"
msgstr ""

#: remark/cli/commands.py:195
msgid "Usage Instructions:"
msgstr ""

#: remark/cli/commands.py:196
msgid "  Windows 10: Right-click folder to see 'Add Folder Remark'"
msgstr ""

#: remark/cli/commands.py:197
msgid ""
"  Windows 11: Right-click folder → Click 'Show more options' → Add Folder"
" Remark"
msgstr ""

#: remark/cli/commands.py:200
msgid "Right-click menu installation failed"
msgstr ""

#: remark/cli/commands.py:206
msgid "Right-click menu uninstalled"
msgstr ""

#: remark/cli/commands.py:209
msgid "Right-click menu uninstallation failed"
msgstr ""

#: remark/cli/commands.py:236 remark/storage/desktop_ini.py:309
#, python-brace-format
msgid "Warning: desktop.ini file encoding is {encoding}, not standard UTF-16."
msgstr ""

#: remark/cli/commands.py:237 remark/storage/desktop_ini.py:310
msgid "unknown"
msgstr ""

#: remark/cli/commands.py:239
msgid "This may cause Chinese and other special characters to display abnormally."
msgstr ""

#: remark/cli/commands.py:243
msgid "Fix encoding to UTF-16? [Y/n]: "
msgstr ""

#: remark/cli/commands.py:246
msgid "Fixed to UTF-16 encoding"
msgstr ""

#: remark/cli/commands.py:248
msgid "Failed to fix encoding"
msgstr ""

#: remark/cli/commands.py:251
msgid "Skip encoding fix"
msgstr ""

#: remark/cli/commands.py:254 remark/storage/desktop_ini.py:335
msgid "Please enter Y or n"
msgstr ""

#: remark/cli/commands.py:259
#, python-brace-format
msgid "Current remark: {remark}"
msgstr ""

#: remark/cli/commands.py:261 remark/core/folder_handler.py:85
msgid "This folder has no remark"
msgstr ""

#: remark/cli/commands.py:266
#, python-brace-format
msgid "Windows Folder Remark Tool v{version}"
msgstr ""

#: remark/cli/commands.py:267
msgid "Tip: Press Ctrl + C to exit"
msgstr ""

#: remark/cli/commands.py:268
msgid "Tip: Use #help to see available commands"
msgstr ""

#: remark/cli/commands.py:276
msgid "Enter folder path (or drag here): "
msgstr ""

#: remark/cli/commands.py:277
msgid "Enter remark:"
msgstr ""

#: remark/cli/commands.py:296
msgid "Path does not exist, please re-enter"
msgstr ""

#: remark/cli/commands.py:300
msgid "This is a 'file', currently only supports adding remarks to 'folders'"
msgstr ""

#: remark/cli/commands.py:305
msgid "Remark cannot be empty"
msgstr ""

#: remark/cli/commands.py:311
msgid " ❤ Thank you for using"
msgstr ""

#: remark/cli/commands.py:313
msgid "Continue processing or press Ctrl + C to exit"
msgstr ""

#: remark/cli/commands.py:327
msgid "Available commands:"
msgstr ""

#: remark/cli/commands.py:331 remark/cli/commands.py:345
msgid "Tip: Press Tab to complete commands"
msgstr ""

#: remark/cli/commands.py:333 remark/cli/commands.py:347
msgid "Tip: Install pyreadline3 for Tab completion (pip install pyreadline3)"
msgstr ""

#: remark/cli/commands.py:337
msgid "Interactive Commands:"
msgstr ""

#: remark/cli/commands.py:338
msgid "  #help     Show this help message"
msgstr ""

#: remark/cli/commands.py:339
msgid "  #install  Install right-click menu"
msgstr ""

#: remark/cli/commands.py:340
msgid "  #uninstall Uninstall right-click menu"
msgstr ""

#: remark/cli/commands.py:341
msgid "  #update   Check for updates"
msgstr ""

#: remark/cli/commands.py:343
msgid "Or simply enter a folder path to add remarks"
msgstr ""

#: remark/cli/commands.py:351
msgid "Windows Folder Remark Tool"
msgstr ""

#: remark/cli/commands.py:352
msgid "Usage:"
msgstr ""

#: remark/cli/commands.py:353
msgid "  Interactive mode: python remark.py"
msgstr ""

#: remark/cli/commands.py:354
msgid "  Command line mode: python remark.py [options] [arguments]"
msgstr ""

#: remark/cli/commands.py:355
msgid "Options:"
msgstr ""

#: remark/cli/commands.py:356
msgid "  --install          Install right-click menu"
msgstr ""

#: remark/cli/commands.py:357
msgid "  --uninstall        Uninstall right-click menu"
msgstr ""

#: remark/cli/commands.py:358
msgid "  --update           Check for updates"
msgstr ""

#: remark/cli/commands.py:359
msgid "  --gui <path>        GUI mode (called from right-click menu)"
msgstr ""

#: remark/cli/commands.py:360
msgid "  --delete <path>     Delete remark"
msgstr ""

#: remark/cli/commands.py:361
msgid "  --view <path>       View remark"
msgstr ""

#: remark/cli/commands.py:362
msgid "  --help, -h         Show help information"
msgstr ""

#: remark/cli/commands.py:363
msgid "Interactive Commands (available in interactive mode):"
msgstr ""

#: remark/cli/commands.py:364
msgid "  #help              Show interactive help"
msgstr ""

#: remark/cli/commands.py:365
msgid "  #install           Install right-click menu"
msgstr ""

#: remark/cli/commands.py:366
msgid "  #uninstall         Uninstall right-click menu"
msgstr ""

#: remark/cli/commands.py:367
msgid "  #update            Check for updates"
msgstr ""

#: remark/cli/commands.py:368
msgid "Examples:"
msgstr ""

#: remark/cli/commands.py:369
msgid " [Add remark] python remark.py \"C:\\\\MyFolder\" \"My Folder\""
msgstr ""

#: remark/cli/commands.py:370
msgid " [Delete remark] python remark.py --delete \"C:\\\\MyFolder\""
msgstr ""

#: remark/cli/commands.py:371
msgid " [View current remark] python remark.py --view \"C:\\\\MyFolder\""
msgstr ""

#: remark/cli/commands.py:372
msgid " [Install right-click menu] python remark.py --install"
msgstr ""

#: remark/cli/commands.py:373
msgid " [Check for updates] python remark.py --update"
msgstr ""

#: remark/cli/commands.py:430
msgid "Error: Path does not exist or not quoted"
msgstr ""

#: remark/cli/commands.py:431
msgid "Hint: Use quotes when path contains spaces"
msgstr ""

#: remark/cli/commands.py:432
msgid "  windows-folder-remark \"C:\\\\My Documents\" \"Remark content\""
msgstr ""

#: remark/cli/commands.py:437
#, python-brace-format
msgid "Detected path: {path}"
msgstr ""

#: remark/cli/commands.py:440 remark/cli/commands.py:482
msgid "Error: This is a file, the tool can only set remarks for folders"
msgstr ""

#: remark/cli/commands.py:445
#, python-brace-format
msgid "Remark content: {remark}"
msgstr ""

#: remark/cli/commands.py:447
msgid "(Will view existing remark)"
msgstr ""

#: remark/cli/commands.py:449
msgid "Continue? [Y/n]: "
msgstr ""

#: remark/cli/commands.py:568
msgid ""
"\n"
"Operation cancelled"
msgstr ""

#: remark/cli/commands.py:571
#, python-brace-format
msgid "An error occurred: {error}"
msgstr ""

#: remark/core/folder_handler.py:24
#, python-brace-format
msgid "Path is not a folder: {folder_path}"
msgstr ""

#: remark/core/folder_handler.py:29
#, python-brace-format
msgid "Remark length exceeds limit, maximum length is {length} characters"
msgstr ""

#: remark/core/folder_handler.py:47 remark/core/folder_handler.py:90
msgid "Failed to clear file attributes"
msgstr ""

#: remark/core/folder_handler.py:52
msgid "Failed to write desktop.ini"
msgstr ""

#: remark/core/folder_handler.py:57
msgid "Failed to set file attributes"
msgstr ""

#: remark/core/folder_handler.py:62
msgid "Failed to set folder attributes"
msgstr ""

#: remark/core/folder_handler.py:66
#, python-brace-format
msgid "Remark [{remark}] has been set for folder [{folder_path}]"
msgstr ""

#: remark/core/folder_handler.py:70
msgid "Remark added successfully, may take a few minutes to display"
msgstr ""

#: remark/core/folder_handler.py:73
#, python-brace-format
msgid "Failed to set remark: {error}"
msgstr ""

#: remark/core/folder_handler.py:95
msgid "Failed to remove remark"
msgstr ""

#: remark/core/folder_handler.py:102
msgid "Failed to restore file attributes"
msgstr ""

#: remark/core/folder_handler.py:105
msgid "Remark deleted successfully"
msgstr ""

#: remark/storage/desktop_ini.py:313
msgid "This file needs to be converted to UTF-16 encoding before modification."
msgstr ""

#: remark/storage/desktop_ini.py:314
msgid ""
"The original content will be preserved, only the encoding format will "
"change."
msgstr ""

#: remark/storage/desktop_ini.py:321
msgid ""
"\n"
"Current file content:"
msgstr ""

#: remark/storage/desktop_ini.py:328
msgid ""
"\n"
"Convert to UTF-16 encoding and continue? [Y/n]: "
msgstr ""

#: remark/storage/desktop_ini.py:332 remark/storage/desktop_ini.py:347
msgid "Operation cancelled."
msgstr ""

#: remark/storage/desktop_ini.py:341
msgid "Converted to UTF-16 encoding."
msgstr ""

#: remark/storage/desktop_ini.py:346
#, python-brace-format
msgid "Conversion failed: {error}"
msgstr ""

#: remark/utils/platform.py:13
msgid ""
"Error: This tool adds remarks to files/folders on Windows, other systems "
"are not supported."
msgstr ""

#: remark/utils/platform.py:14
#, python-brace-format
msgid "Current system: {system}"
msgstr ""



================================================
FILE: package.json
================================================
{
  "name": "windows-folder-remark-docs",
  "version": "1.0.0",
  "type": "module",
  "description": "Documentation for Windows Folder Remark Tool",
  "scripts": {
    "docs:dev": "vitepress dev docs",
    "docs:build": "vitepress build docs",
    "docs:preview": "vitepress preview docs"
  },
  "devDependencies": {
    "vitepress": "^1.0.0"
  }
}


================================================
FILE: pyproject.toml
================================================
# pyproject.toml - 现代 Python 项目配置
# https://packaging.python.org/en/latest/guides/writing-pyproject-toml/

[project]
name = "windows-folder-remark"
version = "2.0.7"
description = "Windows 文件夹备注工具"
readme = "README.md"
requires-python = ">=3.11"
license = {text = "MIT"}
authors = [
    {name = "Piratf"}
]
keywords = ["windows", "folder", "remark", "comment", "desktop.ini"]
classifiers = [
    "Programming Language :: Python :: 3",
    "License :: OSI Approved :: MIT License",
    "Operating System :: Microsoft :: Windows",
    "Programming Language :: Python :: 3.11",
    "Programming Language :: Python :: 3.12",
    "Programming Language :: Python :: 3.13",
]

# 运行时依赖
# 注意:仅使用 Python 标准库,无外部依赖
dependencies = []

# 开发依赖
[project.optional-dependencies]
dev = [
    "ruff>=0.8.0",
    "mypy>=1.14.0",
    "pytest>=7.0.0",
    "pytest-cov>=4.0.0",
    "pytest-xdist>=3.0.0",
    "pytest-mock>=3.10.0",
    "pyfakefs>=5.0.0",
    "pre-commit>=3.0.0",
    "pyinstaller>=6.0.0",
    "babel>=2.16.0",
    "polint>=0.3.0",
]

# 命令行入口
[project.scripts]
remark = "remark.cli.commands:main"
remark-build = "scripts.build:main"

# 构建系统配置
[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"

[tool.setuptools.packages.find]
include = ["remark*"]


# =============================================================================
# 工具配置
# =============================================================================

# Ruff - 代码检查和格式化
# https://docs.astral.sh/ruff/configuration/
[tool.ruff]
target-version = "py311"
line-length = 100

[tool.ruff.lint]
select = [
    "E",   # pycodestyle errors
    "W",   # pycodestyle warnings
    "F",   # pyflakes
    "I",   # isort
    "N",   # pep8-naming
    "UP",  # pyupgrade
    "B",   # flake8-bugbear
    "C4",  # flake8-comprehensions
    "SIM", # flake8-simplify
    "RUF", # ruff-specific rules
]

ignore = [
    "E501",   # line too long (由 formatter 处理)
    "B008",   # do not perform function calls in argument defaults
    "SIM108", # use ternary operator (可读性考虑)
    "RUF001", # 中文全角字符误报
    "RUF002", # 中文全角字符误报
    "RUF003", # 中文全角字符误报
]

fixable = ["ALL"]
unfixable = []

[tool.ruff.lint.per-file-ignores]
"tests/**/*.py" = ["S101"]  # 允许测试中使用 assert

[tool.ruff.format]
quote-style = "double"
indent-style = "space"
skip-magic-trailing-comma = false
line-ending = "auto"

[tool.ruff.lint.isort]
known-first-party = ["remark"]


# Mypy - 静态类型检查
# https://mypy.readthedocs.io/
[tool.mypy]
python_version = "3.11"
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = false
check_untyped_defs = true
show_error_codes = true
show_column_numbers = true
color_output = true
error_summary = true
exclude = [
    '^remark\\.py$',
]

[[tool.mypy.overrides]]
module = []
ignore_missing_imports = true


# Pytest - 测试框架
# https://docs.pytest.org/
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py", "*_test.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]
minversion = "7.0"

addopts = [
    "-v",                    # 详细输出
    "-l",                    # 显示本地变量(失败时)
    "-s",                    # 显示 print 输出
]

markers = [
    "unit: 单元测试",
    "integration: 集成测试",
    "windows: 仅在 Windows 上运行的测试",
    "slow: 慢速测试",
]

filterwarnings = [
    "ignore::DeprecationWarning",
    "ignore::PendingDeprecationWarning",
]

# Coverage - 覆盖率配置
# https://coverage.readthedocs.io/
[tool.coverage.run]
source = ["remark"]
omit = [
    "*/tests/*",
    "*/__pycache__/*",
    "*/site-packages/*",
]

[tool.coverage.report]
exclude_lines = [
    "pragma: no cover",
    "def __repr__",
    "raise AssertionError",
    "raise NotImplementedError",
    "if __name__ == .__main__.:",
    "if TYPE_CHECKING:",
    "@abstractmethod",
]


# Babel - 国际化配置
# https://babel.pocoo.org/
[tool.babel]
# 目录配置
locale_dir = "locale"
domain = "messages"
# 源文件编码
input_encoding = "utf-8"
# 输出文件编码
output_encoding = "utf-8"


================================================
FILE: remark/__init__.py
================================================
"""
Windows Folder Remark - 为 Windows 文件夹添加备注工具
"""

__author__ = "Piratf"

from remark.core.base import CommentHandler
from remark.core.folder_handler import FolderCommentHandler

__all__ = [
    "CommentHandler",
    "FolderCommentHandler",
]


================================================
FILE: remark/cli/__init__.py
================================================
"""
命令行接口模块
"""

from remark.cli.commands import CLI

__all__ = ["CLI"]


================================================
FILE: remark/cli/__main__.py
================================================
"""
Main entry point for running remark.cli as a module.
"""

from remark.cli.commands import main

if __name__ == "__main__":
    main()


================================================
FILE: remark/cli/commands.py
================================================
"""
命令行接口
"""

import argparse
import os
import sys
import tempfile
import threading
import urllib.error

from remark.core.folder_handler import FolderCommentHandler
from remark.gui import remark_dialog
from remark.i18n import _ as _, set_language
from remark.utils import registry
from remark.utils.path_resolver import find_candidates
from remark.utils.platform import check_platform
from remark.utils.updater import (
    check_updates_auto,
    check_updates_manual,
    create_update_script,
    download_update,
    get_executable_path,
    should_check_update,
    trigger_update,
)


def get_version():
    """动态获取版本号"""
    try:
        from importlib.metadata import version

        return version("windows-folder-remark")
    except Exception:
        return "unknown"


class CLI:
    """命令行接口"""

    def __init__(self):
        self.handler = FolderCommentHandler()
        self.pending_update = None
        self._update_check_done = threading.Event()
        # 初始化交互模式命令列表
        self._interactive_commands_list = ["#help", "#install", "#uninstall", "#update"]
        self._interactive_commands = {
            "#help": self._interactive_help,
            "#install": self.install_menu,
            "#uninstall": self.uninstall_menu,
            "#update": self.check_update_now,
        }
        # 先检查缓存,只有在需要检查时才启动后台线程
        if should_check_update():
            self._start_update_checker()
        else:
            self._update_check_done.set()  # 不需要检查,直接标记完成

    def _validate_folder(self, path: str) -> bool:
        """验证路径是否为文件夹"""
        if not os.path.exists(path):
            print(_("Path does not exist: {path}").format(path=path))
            return False
        if not self.handler.supports(path):
            print(_("Path is not a folder: {path}").format(path=path))
            return False
        return True

    def _start_update_checker(self):
        """后台线程检查更新,不阻塞主流程"""
        thread = threading.Thread(target=self._run_update_check, daemon=True)
        thread.start()

    def _run_update_check(self):
        """实际执行更新检查"""
        try:
            self.pending_update = check_updates_auto(get_version())
        finally:
            self._update_check_done.set()

    def check_update_now(self) -> bool:
        """强制检查更新(用于 --update 命令,绕过缓存)

        Returns:
            True 如果有新版本,False 否则
        """
        print(_("Current version: {version}").format(version=get_version()))
        print(_("Checking for updates..."))

        update = check_updates_manual(get_version())

        if update:
            print(_("\nNew version found: {tag_name}").format(tag_name=update["tag_name"]))
            print(_("Update notes: {notes}").format(notes=update["body"][:300]))
            print(_("Full changelog: {url}").format(url=update["html_url"]))
            response = input(_("\nUpdate now? [Y/n]: ")).lower()
            if response in ("", "y", "yes"):
                self._perform_update(update)
            return True
        else:
            print(_("Already at the latest version"))
            return False

    def _wait_for_update_check(self, timeout: float = 2.0) -> None:
        """等待后台检测完成

        Args:
            timeout: 超时时间(秒)
        """
        self._update_check_done.wait(timeout=timeout)

    def _prompt_update(self) -> None:
        """提示用户有新版本可用"""
        update = self.pending_update
        if update is None:
            return
        print(
            _("\nNew version available: {tag_name} (Current version: {version})").format(
                tag_name=update["tag_name"], version=get_version()
            )
        )
        print(_("Update notes: {notes}").format(notes=update["body"][:200]))
        print(_("Full changelog: {url}").format(url=update["html_url"]))
        response = input(_("Update now? [Y/n]: ")).lower()
        if response in ("", "y", "yes"):
            self._perform_update(update)

    def _perform_update(self, update: dict) -> None:
        """执行更新流程"""
        try:
            print(_("Downloading new version..."))
            # 下载到临时目录
            new_exe = os.path.join(
                tempfile.gettempdir(), f"windows-folder-remark-{update['tag_name']}.exe"
            )
            download_update(update["download_url"], new_exe)

            print(_("Download complete, preparing update..."))
            old_exe = get_executable_path()
            script_path = create_update_script(old_exe, new_exe)

            print(_("Update program has started, the application will exit..."))
            print(_("Please wait a few moments, the update will complete automatically."))
            trigger_update(script_path)
            sys.exit(0)
        except urllib.error.URLError as e:
            err_msg = str(e)
            if "closed connection" in err_msg.lower() or "connection reset" in err_msg.lower():
                print(_("Download failed: Connection reset by server"))
                print(_("Please try again later, or visit the following link to download manually:"))
                print(f"  {update['html_url']}")
            elif "timeout" in err_msg.lower():
                print(_("Download failed: Request timeout"))
                print(_("Please check your network connection, or visit the following link to download manually:"))
                print(f"  {update['html_url']}")
            elif "no route to host" in err_msg.lower() or "hostname" in err_msg.lower():
                print(_("Download failed: Unable to connect to server"))
                print(_("Please check your network connection, or visit the following link to download manually:"))
                print(f"  {update['html_url']}")
            else:
                print(_("Download failed, please check your network or download manually"))
                print(f"  {update['html_url']}")
        except Exception as e:
            print(_("Update failed: {error}").format(error=e))
            print(_("Manual download: {url}").format(url=update["html_url"]))

    def add_comment(self, path, comment):
        """添加备注"""
        if self._validate_folder(path):
            return self.handler.set_comment(path, comment)
        return False

    def delete_comment(self, path):
        """删除备注"""
        if self._validate_folder(path):
            return self.handler.delete_comment(path)
        return False

    def install_menu(self) -> bool:
        """安装右键菜单"""
        if registry.install_context_menu():
            print(_("Right-click menu installed successfully"))
            print("")
            print(_("Usage Instructions:"))
            print(_("  Windows 10: Right-click folder to see 'Add Folder Remark'"))
            print(_("  Windows 11: Right-click folder → Click 'Show more options' → Add Folder Remark"))
            return True
        else:
            print(_("Right-click menu installation failed"))
            return False

    def uninstall_menu(self) -> bool:
        """卸载右键菜单"""
        if registry.uninstall_context_menu():
            print(_("Right-click menu uninstalled"))
            return True
        else:
            print(_("Right-click menu uninstallation failed"))
            return False

    def gui_mode(self, folder_path: str) -> bool:
        """GUI 模式(右键菜单调用)"""
        if not self._validate_folder(folder_path):
            return False

        # 显示对话框
        comment = remark_dialog.show_remark_dialog(folder_path)
        if comment:
            result = self.add_comment(folder_path, comment)
            return result is not False
        return False

    def view_comment(self, path: str) -> None:
        """查看备注"""
        if self._validate_folder(path):
            # 检查 desktop.ini 编码
            from remark.storage.desktop_ini import DesktopIniHandler

            if DesktopIniHandler.exists(path):
                desktop_ini_path = DesktopIniHandler.get_path(path)
                detected_encoding, is_utf16 = DesktopIniHandler.detect_encoding(desktop_ini_path)
                if not is_utf16:
                    print(
                        _(
                            "Warning: desktop.ini file encoding is {encoding}, not standard UTF-16."
                        ).format(encoding=detected_encoding or _("unknown"))
                    )
                    print(_("This may cause Chinese and other special characters to display abnormally."))

                    # 询问是否修复
                    while True:
                        response = input(_("Fix encoding to UTF-16? [Y/n]: ")).strip().lower()
                        if response in ("", "y", "yes"):
                            if DesktopIniHandler.fix_encoding(desktop_ini_path, detected_encoding):
                                print(_("Fixed to UTF-16 encoding"))
                            else:
                                print(_("Failed to fix encoding"))
                            break
                        elif response in ("n", "no"):
                            print(_("Skip encoding fix"))
                            break
                        else:
                            print(_("Please enter Y or n"))
                    print()  # 空行分隔

            comment = self.handler.get_comment(path)
            if comment:
                print(_("Current remark: {remark}").format(remark=comment))
            else:
                print(_("This folder has no remark"))

    def interactive_mode(self) -> None:
        """交互模式"""
        version = get_version()
        print(_("Windows Folder Remark Tool v{version}").format(version=version))
        print(_("Tip: Press Ctrl + C to exit"))
        print(_("Tip: Use #help to see available commands"))

        input_path_msg = "\n" + _("Enter folder path (or drag here): ")
        input_comment_msg = _("Enter remark:")

        while True:
            try:
                user_input = input(input_path_msg).replace('"', "").strip()

                # 处理交互命令
                if user_input in self._interactive_commands:
                    self._interactive_commands[user_input]()
                    print()
                    continue

                # 用户输入单独的 #,显示可用命令列表
                if user_input == "#":
                    self._show_command_list()
                    print()
                    continue

                if not os.path.exists(user_input):
                    print(_("Path does not exist, please re-enter"))
                    continue

                if not os.path.isdir(user_input):
                    print(_("This is a 'file', currently only supports adding remarks to 'folders'"))
                    continue

                comment = input(input_comment_msg)
                while not comment:
                    print(_("Remark cannot be empty"))
                    comment = input(input_comment_msg)

                self.add_comment(user_input, comment)

            except KeyboardInterrupt:
                print("\n" + _(" ❤ Thank you for using"))
                break
            print(os.linesep + _("Continue processing or press Ctrl + C to exit") + os.linesep)

    def _show_command_list(self) -> None:
        """显示可用命令列表"""
        print(_("Available commands:"))
        for cmd in self._interactive_commands_list:
            print(f"  {cmd}")

    def _interactive_help(self) -> None:
        """显示交互模式帮助信息"""
        print(_("Interactive Commands:"))
        print(_("  #help     Show this help message"))
        print(_("  #install  Install right-click menu"))
        print(_("  #uninstall Uninstall right-click menu"))
        print(_("  #update   Check for updates"))
        print(os.linesep)
        print(_("Or simply enter a folder path to add remarks"))

    def show_help(self) -> None:
        """显示帮助信息"""
        print(_("Windows Folder Remark Tool"))
        print(_("Usage:"))
        print(_("  Interactive mode: python remark.py"))
        print(_("  Command line mode: python remark.py [options] [arguments]"))
        print(_("Options:"))
        print(_("  --install          Install right-click menu"))
        print(_("  --uninstall        Uninstall right-click menu"))
        print(_("  --update           Check for updates"))
        print(_("  --gui <path>        GUI mode (called from right-click menu)"))
        print(_("  --delete <path>     Delete remark"))
        print(_("  --view <path>       View remark"))
        print(_("  --help, -h         Show help information"))
        print(_("Interactive Commands (available in interactive mode):"))
        print(_("  #help              Show interactive help"))
        print(_("  #install           Install right-click menu"))
        print(_("  #uninstall         Uninstall right-click menu"))
        print(_("  #update            Check for updates"))
        print(_("Examples:"))
        print(_(' [Add remark] python remark.py "C:\\\\MyFolder" "My Folder"'))
        print(_(' [Delete remark] python remark.py --delete "C:\\\\MyFolder"'))
        print(_(' [View current remark] python remark.py --view "C:\\\\MyFolder"'))
        print(_(" [Install right-click menu] python remark.py --install"))
        print(_(" [Check for updates] python remark.py --update"))

    def _select_from_multiple_candidates(
        self, candidates: list, show_remaining: bool = False
    ) -> tuple[str, list[str]] | None:
        """
        从多个候选路径中选择

        Args:
            candidates: 候选路径列表,每个元素为 (path, remaining, type)
            show_remaining: 是否显示剩余参数(备注内容)

        Returns:
            (path_str, remaining) 或 None 如果用户取消
        """

        # 转换 candidates 中的 Path 对象为字符串
        str_candidates: list[tuple[str, list[str], str]] = []
        for path, remaining, path_type in candidates:
            str_candidates.append((str(path), remaining, path_type))

        print("检测到多个可能的路径,请选择:")
        for i, (p, r, t) in enumerate(str_candidates, 1):
            type_mark = " [文件]" if t == "file" else ""
            print(f"\n[{i}] {p}{type_mark}")
            if show_remaining and r:
                print(f"    剩余备注: {' '.join(r)}")
            elif not show_remaining:
                print("    (将查看现有备注)")
        print("\n[0] 取消")

        while True:
            choice = input(f"\n请选择 [0-{len(str_candidates)}]: ").strip()
            if choice == "0":
                return None
            if choice.isdigit() and 1 <= int(choice) <= len(str_candidates):
                path, remaining, path_type = str_candidates[int(choice) - 1]
                if path_type == "file":
                    print("\n错误: 这是一个文件,工具只能为文件夹设置备注,请重新选择")
                    continue
                return path, remaining
            print("无效选择,请重试")

    def _handle_ambiguous_path(self, args_list: list[str]) -> tuple[str | None, str | None]:
        """
        处理模糊路径,返回 (最终路径, 备注内容)

        Args:
            args_list: 位置参数列表

        Returns:
            (path, comment) 或 (None, None) 如果用户取消
        """

        candidates = find_candidates(args_list)

        if not candidates:
            print(_("Error: Path does not exist or not quoted"))
            print(_("Hint: Use quotes when path contains spaces"))
            print(_('  windows-folder-remark "C:\\\\My Documents" "Remark content"'))
            return None, None

        if len(candidates) == 1:
            path, remaining, path_type = candidates[0]
            print(_("Detected path: {path}").format(path=path))

            if path_type == "file":
                print(_("Error: This is a file, the tool can only set remarks for folders"))
                return None, None

            if remaining:
                comment = " ".join(remaining)
                print(_("Remark content: {remark}").format(remark=comment))
            else:
                print(_("(Will view existing remark)"))

            if input(_("Continue? [Y/n]: ")).lower() in ("", "y", "yes"):
                return str(path), " ".join(remaining) if remaining else None

            return None, None

        result = self._select_from_multiple_candidates(candidates, show_remaining=True)
        if result:
            path_str, remaining = result
            return path_str, " ".join(remaining) if remaining else None
        return None, None

    def _resolve_path_from_ambiguous_args(self, args_list: list[str]) -> str | None:
        """
        从可能被空格分割的参数列表中解析出有效路径

        适用于 --view, --delete, --gui 等只接收路径的命令。

        Args:
            args_list: 可能包含路径片段的参数列表

        Returns:
            解析出的路径字符串,如果无法解析则返回 None
        """
        candidates = find_candidates(args_list)

        if not candidates:
            return None

        if len(candidates) == 1:
            path, remaining, path_type = candidates[0]
            if path_type == "folder":
                return str(path)
            else:
                print(_("Error: This is a file, the tool can only set remarks for folders"))
                return None

        result = self._select_from_multiple_candidates(candidates, show_remaining=False)
        if result:
            return result[0]
        return None

    def run(self, argv=None) -> None:
        """运行 CLI"""
        if not check_platform():
            sys.exit(1)

        parser = argparse.ArgumentParser(description="Windows 文件夹备注工具", add_help=False)
        parser.add_argument("args", nargs="*", help="位置参数(路径和备注)")
        parser.add_argument("--install", action="store_true", help="安装右键菜单")
        parser.add_argument("--uninstall", action="store_true", help="卸载右键菜单")
        parser.add_argument("--update", action="store_true", help="检查更新")
        parser.add_argument("--gui", metavar="PATH", help="GUI 模式(右键菜单调用)")
        parser.add_argument("--delete", metavar="PATH", help="删除备注")
        parser.add_argument("--view", metavar="PATH", help="查看备注")
        parser.add_argument("--help", "-h", action="store_true", help="显示帮助信息")
        parser.add_argument("--lang", "-L", metavar="LANG", help="设置语言 (en, zh)", dest="lang")

        args = parser.parse_args(argv)

        # 设置语言
        if args.lang:
            set_language(args.lang)

        if args.help:
            self.show_help()
        elif args.install:
            self.install_menu()
        elif args.uninstall:
            self.uninstall_menu()
        elif args.update:
            self.check_update_now()
            sys.exit(0)
        elif args.gui:
            path = self._resolve_path_from_ambiguous_args([args.gui, *args.args])
            if path:
                self.gui_mode(path)
            else:
                print("错误: 路径不存在或未使用引号")
        elif args.delete:
            path = self._resolve_path_from_ambiguous_args([args.delete, *args.args])
            if path:
                self.delete_comment(path)
            else:
                print("错误: 路径不存在或未使用引号")
        elif args.view:
            path = self._resolve_path_from_ambiguous_args([args.view, *args.args])
            if path:
                self.view_comment(path)
            else:
                print("错误: 路径不存在或未使用引号")
        elif args.args:
            # 处理位置参数
            path, comment = self._handle_ambiguous_path(args.args)
            if path:
                if comment:
                    self.add_comment(path, comment)
                else:
                    self.view_comment(path)
            else:
                # 用户取消或解析失败,显示帮助
                self.show_help()
        else:
            # 无参数,进入交互模式
            self.interactive_mode()


def main() -> None:
    """主入口"""
    # 强制设置控制台编码为 UTF-8,支持中文等特殊字符输出
    # 这对于 Windows 系统特别重要,因为默认控制台编码可能是 GBK
    if hasattr(sys.stdout, "reconfigure"):
        sys.stdout.reconfigure(encoding="utf-8", errors="replace")
    if hasattr(sys.stderr, "reconfigure"):
        sys.stderr.reconfigure(encoding="utf-8", errors="replace")

    cli = CLI()
    try:
        cli.run()
    except KeyboardInterrupt:
        print(_("\nOperation cancelled"))
        sys.exit(0)
    except Exception as e:
        print(_("An error occurred: {error}").format(error=str(e)))
        sys.exit(1)
    finally:
        # 等待后台检测完成(最多等待 2 秒)
        cli._wait_for_update_check(timeout=2.0)
        # 退出前检查更新
        if cli.pending_update:
            cli._prompt_update()


if __name__ == "__main__":
    main()


================================================
FILE: remark/core/__init__.py
================================================
"""
核心功能模块
"""

from remark.core.base import CommentHandler
from remark.core.folder_handler import FolderCommentHandler

__all__ = [
    "CommentHandler",
    "FolderCommentHandler",
]


================================================
FILE: remark/core/base.py
================================================
"""
基础接口定义
"""

from abc import ABC, abstractmethod


class CommentHandler(ABC):
    """备注处理器基类"""

    @abstractmethod
    def set_comment(self, path, comment):
        """设置备注"""
        pass

    @abstractmethod
    def get_comment(self, path):
        """获取备注"""
        pass

    @abstractmethod
    def delete_comment(self, path):
        """删除备注"""
        pass

    @abstractmethod
    def supports(self, path):
        """检查是否支持该路径类型"""
        pass


================================================
FILE: remark/core/folder_handler.py
================================================
"""
文件夹备注处理器 - 使用 desktop.ini

使用 Microsoft 官方支持的 desktop.ini 方式设置文件夹备注。

参考文档:
https://learn.microsoft.com/en-us/windows/win32/shell/how-to-customize-folders-with-desktop-ini
"""

import os

from remark.core.base import CommentHandler
from remark.i18n import _ as _
from remark.storage.desktop_ini import DesktopIniHandler
from remark.utils.constants import MAX_COMMENT_LENGTH


class FolderCommentHandler(CommentHandler):
    """文件夹备注处理器"""

    def set_comment(self, folder_path: str, comment: str) -> bool:
        """设置文件夹备注"""
        if not os.path.isdir(folder_path):
            print(_("Path is not a folder: {folder_path}").format(folder_path=folder_path))
            return False

        if len(comment) > MAX_COMMENT_LENGTH:
            print(
                _("Remark length exceeds limit, maximum length is {length} characters").format(
                    length=MAX_COMMENT_LENGTH
                )
            )
            comment = comment[:MAX_COMMENT_LENGTH]

        return self._set_comment_desktop_ini(folder_path, comment)

    @staticmethod
    def _set_comment_desktop_ini(folder_path: str, comment: str) -> bool:
        """使用 desktop.ini 设置备注"""
        desktop_ini_path = DesktopIniHandler.get_path(folder_path)

        try:
            # 清除文件属性以便修改
            if DesktopIniHandler.exists(
                folder_path
            ) and not DesktopIniHandler.clear_file_attributes(desktop_ini_path):
                print(_("Failed to clear file attributes"))
                return False

            # 使用 UTF-16 编码写入 desktop.ini
            if not DesktopIniHandler.write_info_tip(folder_path, comment):
                print(_("Failed to write desktop.ini"))
                return False

            # 设置 desktop.ini 文件为隐藏和系统属性
            if not DesktopIniHandler.set_file_hidden_system_attributes(desktop_ini_path):
                print(_("Failed to set file attributes"))
                return False

            # 设置文件夹为只读属性(使 desktop.ini 生效)
            if not DesktopIniHandler.set_folder_system_attributes(folder_path):
                print(_("Failed to set folder attributes"))
                return False

            print(
                _("Remark [{remark}] has been set for folder [{folder_path}]").format(
                    remark=comment, folder_path=folder_path
                )
            )
            print(_("Remark added successfully, may take a few minutes to display"))
            return True
        except Exception as e:
            print(_("Failed to set remark: {error}").format(error=str(e)))
            return False

    def get_comment(self, folder_path: str) -> str | None:
        """获取文件夹备注"""
        return DesktopIniHandler.read_info_tip(folder_path)

    def delete_comment(self, folder_path: str) -> bool:
        """删除文件夹备注"""
        desktop_ini_path = DesktopIniHandler.get_path(folder_path)

        if not DesktopIniHandler.exists(folder_path):
            print(_("This folder has no remark"))
            return True

        # 清除文件属性以便修改
        if not DesktopIniHandler.clear_file_attributes(desktop_ini_path):
            print(_("Failed to clear file attributes"))
            return False

        # 移除 InfoTip 行(保留其他设置如 IconResource)
        if not DesktopIniHandler.remove_info_tip(folder_path):
            print(_("Failed to remove remark"))
            return False

        # 如果 desktop.ini 仍存在,恢复文件属性
        if DesktopIniHandler.exists(
            folder_path
        ) and not DesktopIniHandler.set_file_hidden_system_attributes(desktop_ini_path):
            print(_("Failed to restore file attributes"))
            return False

        print(_("Remark deleted successfully"))
        return True

    def supports(self, path: str) -> bool:
        """检查是否支持该路径"""
        return os.path.isdir(path)


================================================
FILE: remark/gui/__init__.py
================================================
"""GUI 模块"""


================================================
FILE: remark/gui/remark_dialog.py
================================================
"""
备注输入对话框

使用 tkinter 实现的简单 GUI 对话框,用于右键菜单集成。
"""

import tkinter as tk
from tkinter import messagebox, ttk

from remark.core.folder_handler import FolderCommentHandler


def show_remark_dialog(folder_path: str) -> str | None:
    """
    显示备注输入对话框

    界面:
    - 标题: "添加文件夹备注"
    - 文件夹路径显示 (只读)
    - 当前备注显示 (如果有)
    - 备注输入框
    - 确定/取消按钮

    Args:
        folder_path: 文件夹完整路径

    Returns:
        用户输入的备注内容(非空字符串),用户点击取消返回 None
    """
    root = tk.Tk()
    root.title("添加文件夹备注")

    # 设置窗口大小和居中
    window_width = 500
    window_height = 250
    screen_width = root.winfo_screenwidth()
    screen_height = root.winfo_screenheight()
    x = (screen_width - window_width) // 2
    y = (screen_height - window_height) // 2
    root.geometry(f"{window_width}x{window_height}+{x}+{y}")

    # 禁止调整窗口大小
    root.resizable(False, False)

    # 使用 ttk 样式
    style = ttk.Style()
    style.theme_use("clam")

    # 结果存储
    result: dict[str, str | None] = {"comment": None}

    # =============================================================================
    # 界面元素
    # =============================================================================

    # 主框架
    main_frame = ttk.Frame(root, padding="20")
    main_frame.pack(fill=tk.BOTH, expand=True)

    # 文件夹路径标签
    path_label = ttk.Label(main_frame, text="文件夹路径:")
    path_label.grid(row=0, column=0, sticky=tk.W, pady=(0, 5))

    path_entry = ttk.Entry(main_frame, width=60)
    path_entry.insert(0, folder_path)
    path_entry.configure(state="readonly")
    path_entry.grid(row=1, column=0, columnspan=2, sticky=tk.EW, pady=(0, 15))

    # 当前备注显示(如果存在)
    handler = FolderCommentHandler()
    current_comment = handler.get_comment(folder_path)

    if current_comment:
        current_label = ttk.Label(main_frame, text="当前备注:")
        current_label.grid(row=2, column=0, sticky=tk.W, pady=(0, 5))

        current_value = ttk.Entry(main_frame, width=60)
        current_value.insert(0, current_comment)
        current_value.configure(state="readonly")
        current_value.grid(row=3, column=0, columnspan=2, sticky=tk.EW, pady=(0, 15))

        next_row = 4
    else:
        next_row = 2

    # 备注输入标签
    comment_label = ttk.Label(main_frame, text="备注内容:")
    comment_label.grid(row=next_row, column=0, sticky=tk.W, pady=(0, 5))

    # 备注输入框
    comment_entry = ttk.Entry(main_frame, width=60)
    if current_comment:
        comment_entry.insert(0, current_comment)
    comment_entry.grid(row=next_row + 1, column=0, columnspan=2, sticky=tk.EW, pady=(0, 20))

    # 按钮框架
    button_frame = ttk.Frame(main_frame)
    button_frame.grid(row=next_row + 2, column=0, columnspan=2, sticky=tk.EW)

    # 确定按钮
    def on_ok():
        comment = comment_entry.get().strip()
        if not comment:
            messagebox.showwarning("警告", "备注不能为空")
            return
        result["comment"] = comment
        root.destroy()

    def on_cancel():
        result["comment"] = None
        root.destroy()

    ok_button = ttk.Button(button_frame, text="确定", command=on_ok, width=10)
    ok_button.pack(side=tk.RIGHT, padx=(5, 0))

    # 取消按钮
    cancel_button = ttk.Button(button_frame, text="取消", command=on_cancel, width=10)
    cancel_button.pack(side=tk.RIGHT)

    # =============================================================================
    # 键盘快捷键
    # =============================================================================

    root.bind("<Return>", lambda e: on_ok())
    root.bind("<Escape>", lambda e: on_cancel())

    # =============================================================================
    # 焦点设置
    # =============================================================================

    comment_entry.focus_set()
    if current_comment:
        # 如果有现有备注,全选文本方便修改
        comment_entry.select_range(0, tk.END)

    # =============================================================================
    # 运行
    # =============================================================================

    root.wait_window()
    return result["comment"]


================================================
FILE: remark/i18n.py
================================================
"""
Internationalization (i18n) module for Windows Folder Remark tool.

This module provides translation support using gettext.
"""
from __future__ import annotations

import ctypes
import gettext
import locale
import os
import platform
import sys
from pathlib import Path
from typing import Final

# 翻译域
DOMAIN: Final = "messages"

# 支持的语言列表
SUPPORTED_LANGUAGES: Final = ("en", "zh")


def _get_locale_dir() -> Path:
    """
    获取翻译文件目录路径.

    支持 PyInstaller 打包环境:
    - 打包后:sys._MEIPASS/locale(文件解压到临时目录的 locale 子目录)
    - 开发环境:使用项目根目录下的 locale 目录

    Returns:
        locale 目录的路径
    """
    # PyInstaller 打包后的临时目录,文件被解压到 _MEIPASS/locale/
    if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"):
        return Path(sys._MEIPASS) / "locale"

    # 开发环境:使用项目根目录
    return Path(__file__).parent.parent / "locale"


# 翻译文件目录(运行时计算)
LOCALE_DIR: Final = _get_locale_dir()


def _get_windows_locale() -> str | None:
    """
    在 Windows 上使用 Windows API 获取用户默认区域设置名称.

    Returns:
        区域设置名称(如 'zh-CN', 'en-US'),如果获取失败则返回 None
    """
    try:
        # GetUserDefaultLocaleName 返回 locale 名称(如 'zh-CN', 'en-US')
        # 缓冲区大小为 LOCALE_NAME_MAX_LENGTH (85)
        buffer_size: int = 85
        buffer: ctypes.Array[ctypes.c_wchar] = ctypes.create_unicode_buffer(buffer_size)

        # kernel32.dll 中的 GetUserDefaultLocaleName 函数
        # 原型: int GetUserDefaultLocaleName(LPWSTR lpLocaleName, int cchLocaleName)
        kernel32 = ctypes.windll.kernel32
        kernel32.GetUserDefaultLocaleName.restype = ctypes.c_int
        kernel32.GetUserDefaultLocaleName.argtypes = [
            ctypes.POINTER(ctypes.c_wchar),
            ctypes.c_int,
        ]

        result: int = kernel32.GetUserDefaultLocaleName(buffer, buffer_size)

        if result > 0:
            return buffer.value.strip()
    except (AttributeError, OSError, ValueError):
        pass

    return None


def get_system_language() -> str:
    """
    获取系统语言设置.

    优先级顺序:
    1. 环境变量 LANG(最容易被测试控制)
    2. Windows 平台的 Windows API
    3. locale.getlocale()
    4. 默认返回英文

    Returns:
        语言代码(如 'en', 'zh'),如果不支持则返回默认的 'en'
    """
    # 优先从环境变量获取(便于测试控制)
    lang = os.environ.get("LANG", "")
    if lang:
        # 提取语言代码(如 zh_CN.UTF-8 -> zh)
        lang_code = lang.split(".")[0].split("_")[0]
        if lang_code in SUPPORTED_LANGUAGES:
            return lang_code
        # 处理完整语言代码(如 zh_CN -> zh)
        if "_" in lang:
            full_lang = lang.split(".")[0]
            if full_lang in SUPPORTED_LANGUAGES:
                return full_lang

    # Windows 平台使用 Windows API
    if platform.system() == "Windows":
        windows_locale = _get_windows_locale()
        if windows_locale:
            # Windows locale 格式为 'zh-CN', 'en-US' 等
            # 提取语言部分(zh-CN -> zh)
            lang_code = windows_locale.split("-")[0]
            if lang_code in SUPPORTED_LANGUAGES:
                return lang_code

    # 尝试从 locale 获取
    try:
        loc = locale.getlocale()[0]
        if loc:
            # locale 格式可能是 'zh_CN', 'zh-CN', 'Chinese_China' 等
            normalized = loc.replace("-", "_")
            if normalized in SUPPORTED_LANGUAGES:
                return normalized
            # 尝试只取语言部分
            lang_code = normalized.split("_")[0]
            if lang_code in SUPPORTED_LANGUAGES:
                return lang_code
            # 处理 Windows 特殊格式(如 'Chinese_China' -> 'zh')
            if normalized.startswith("Chinese"):
                return "zh"
    except (ValueError, AttributeError):
        pass

    # 默认返回英文
    return "en"


def init_translation(language: str | None = None) -> gettext.GNUTranslations:
    """
    初始化翻译.

    Args:
        language: 语言代码,如果为 None 则使用系统语言

    Returns:
        翻译函数
    """
    if language is None:
        language = get_system_language()

    # 确保语言受支持
    if language not in SUPPORTED_LANGUAGES:
        language = "en"

    # 尝试加载翻译
    try:
        translator = gettext.translation(
            domain=DOMAIN,
            localedir=str(LOCALE_DIR),
            languages=[language],
            fallback=True,
        )
        return translator
    except Exception:
        # 如果加载失败,使用空翻译(返回原字符串)
        return gettext.NullTranslations()


# 全局翻译函数
_translator: gettext.GNUTranslations | gettext.NullTranslations | None = None


def get_translator() -> gettext.GNUTranslations | gettext.NullTranslations:
    """
    获取当前翻译器.

    Returns:
        翻译器实例
    """
    global _translator
    if _translator is None:
        _translator = init_translation()
    return _translator


def set_language(language: str) -> None:
    """
    设置当前语言.

    Args:
        language: 语言代码(如 'en', 'zh_CN')
    """
    global _translator
    _translator = init_translation(language)


def gettext_function(message: str) -> str:
    """
    翻译函数(用于在代码中标记可翻译字符串).

    Args:
        message: 要翻译的字符串

    Returns:
        翻译后的字符串
    """
    return get_translator().gettext(message)


def ngettext_function(singular: str, plural: str, n: int) -> str:
    """
    复数形式翻译函数.

    Args:
        singular: 单数形式
        plural: 复数形式
        n: 数量

    Returns:
        翻译后的字符串
    """
    return get_translator().ngettext(singular, plural, n)


# 默认导出的翻译函数
_ = gettext_function


================================================
FILE: remark/storage/__init__.py
================================================
"""
存储层模块 - 提供统一的存储接口
"""

from .desktop_ini import DesktopIniHandler, EncodingConversionCanceled

__all__ = ["DesktopIniHandler", "EncodingConversionCanceled"]


================================================
FILE: remark/storage/desktop_ini.py
================================================
"""
Desktop.ini 交互层

根据 Microsoft 官方文档要求,desktop.ini 文件必须使用 Unicode 格式
才能正确存储和显示本地化字符串。

参考文档:
https://learn.microsoft.com/en-us/windows/win32/shell/how-to-customize-folders-with-desktop-ini

引用: "Make sure the Desktop.ini file that you create is in the Unicode format.
This is necessary to store the localized strings that can be displayed to users."
"""

import codecs
import os

from remark.i18n import _ as _


class EncodingConversionCanceled(Exception):  # noqa: N818
    """编码转换被用户取消"""

    pass


# Windows desktop.ini 标准编码格式
# 使用 'utf-16' 编码,codecs 会自动添加 UTF-16 LE BOM (0xFF 0xFE)
DESKTOP_INI_ENCODING = "utf-16"
# Windows 行尾符
LINE_ENDING = "\r\n"


class DesktopIniHandler:
    """
    Desktop.ini 处理器

    提供对 desktop.ini 文件的读写操作,确保使用正确的编码格式
    以支持资源管理器正确显示中文等非 ASCII 字符。
    """

    # desktop.ini 文件名
    FILENAME = "desktop.ini"
    # ShellClassInfo 段落
    SECTION_SHELL_CLASS_INFO = "[.ShellClassInfo]"
    # InfoTip 属性
    PROPERTY_INFOTIP = "InfoTip"

    @staticmethod
    def get_path(folder_path):
        """
        获取 desktop.ini 文件路径

        Args:
            folder_path: 文件夹路径

        Returns:
            desktop.ini 文件的完整路径
        """
        return os.path.join(folder_path, DesktopIniHandler.FILENAME)

    @staticmethod
    def exists(folder_path):
        """
        检查 desktop.ini 是否存在

        Args:
            folder_path: 文件夹路径

        Returns:
            bool: desktop.ini 是否存在
        """
        return os.path.exists(DesktopIniHandler.get_path(folder_path))

    @staticmethod
    def read_info_tip(folder_path):
        """
        读取 desktop.ini 中的 InfoTip 值

        使用 UTF-16 编码读取(与写入逻辑一致),支持中文等非 ASCII 字符。
        如果 UTF-16 读取失败,会尝试其他编码以处理外部程序创建的文件。

        Args:
            folder_path: 文件夹路径

        Returns:
            str: InfoTip 值,如果不存在或读取失败返回 None
        """
        desktop_ini_path = DesktopIniHandler.get_path(folder_path)

        if not os.path.exists(desktop_ini_path):
            return None

        # 优先使用标准编码 UTF-16(与写入逻辑一致)
        # 降级编码用于处理外部程序创建的文件
        encodings = [DESKTOP_INI_ENCODING, "utf-16-le", "utf-8-sig", "utf-8", "gbk", "mbcs"]

        for encoding in encodings:
            try:
                with codecs.open(desktop_ini_path, "r", encoding=encoding) as f:
                    content = f.read()

                # 验证是否是合法的 desktop.ini 结构(必须包含 [.ShellClassInfo])
                # 如果不包含,说明编码不对,继续尝试下一个
                if DesktopIniHandler.SECTION_SHELL_CLASS_INFO not in content:
                    continue

                # 解析 InfoTip
                if DesktopIniHandler.PROPERTY_INFOTIP in content:
                    # 找到 InfoTip= 的位置
                    start = content.index(DesktopIniHandler.PROPERTY_INFOTIP + "=")
                    start += len(DesktopIniHandler.PROPERTY_INFOTIP + "=")

                    # 找到行尾
                    end = len(content)
                    for line_ending in ["\r\n", "\n", "\r"]:
                        pos = content.find(line_ending, start)
                        if pos != -1 and pos < end:
                            end = pos
                            break

                    value = content[start:end].strip()
                    if value:
                        return value
                # 成功读取且结构正确,但没有 InfoTip
                return None
            except (UnicodeDecodeError, UnicodeError):
                # 当前编码失败,尝试下一个
                continue
            except Exception:
                # 其他错误(文件不存在、权限问题等),直接返回
                break

        return None

    @staticmethod
    def write_info_tip(folder_path, info_tip):
        """
        写入 InfoTip 到 desktop.ini

        使用 UTF-16 编码写入(自动添加 BOM),符合 Microsoft 官方文档要求。
        这确保中文等非 ASCII 字符在资源管理器中正确显示。

        如果 desktop.ini 已存在且包含其他设置(如 IconResource),会保留这些设置。

        Args:
            folder_path: 文件夹路径
            info_tip: 要写入的 InfoTip 值

        Returns:
            bool: 写入是否成功

        Raises:
            EncodingConversionCanceled: 用户拒绝编码转换
        """
        if not info_tip:
            return False

        desktop_ini_path = DesktopIniHandler.get_path(folder_path)

        try:
            # 如果文件已存在,读取并更新
            if os.path.exists(desktop_ini_path):
                # 确保是 UTF-16 编码(用户拒绝会抛出异常)
                DesktopIniHandler.ensure_utf16_encoding(desktop_ini_path)

                with codecs.open(desktop_ini_path, "r", encoding=DESKTOP_INI_ENCODING) as f:
                    content = f.read()

                # 检查是否已有 InfoTip
                lines = content.splitlines()
                new_lines = []
                info_tip_updated = False

                for line in lines:
                    stripped = line.strip()
                    # 更新现有 InfoTip 行
                    if stripped.startswith(
                        DesktopIniHandler.PROPERTY_INFOTIP + "="
                    ) or stripped.startswith(DesktopIniHandler.PROPERTY_INFOTIP + " "):
                        new_lines.append(DesktopIniHandler.PROPERTY_INFOTIP + "=" + info_tip)
                        info_tip_updated = True
                    else:
                        new_lines.append(line)

                # 如果没有 InfoTip,添加它
                if not info_tip_updated:
                    # 找到 [.ShellClassInfo] 后插入
                    inserted = False
                    for i, line in enumerate(new_lines):
                        if line.strip().startswith("[.ShellClassInfo]"):
                            new_lines.insert(
                                i + 1, DesktopIniHandler.PROPERTY_INFOTIP + "=" + info_tip
                            )
                            inserted = True
                            break
                    if not inserted:
                        # 没找到 section,添加整个 section
                        new_lines = [
                            DesktopIniHandler.SECTION_SHELL_CLASS_INFO,
                            DesktopIniHandler.PROPERTY_INFOTIP + "=" + info_tip,
                        ]

                new_content = LINE_ENDING.join(new_lines)
            else:
                # 新建文件
                new_content = (
                    DesktopIniHandler.SECTION_SHELL_CLASS_INFO
                    + LINE_ENDING
                    + DesktopIniHandler.PROPERTY_INFOTIP
                    + "="
                    + info_tip
                    + LINE_ENDING
                )

            # 使用 UTF-16 编码写入
            with codecs.open(desktop_ini_path, "w", encoding=DESKTOP_INI_ENCODING) as f:
                f.write(new_content)

            return True

        except EncodingConversionCanceled:
            return False
        except Exception:
            return False

    @staticmethod
    def detect_encoding(file_path):
        """
        检测文件编码

        Args:
            file_path: 文件路径

        Returns:
            tuple: (encoding_name, is_utf16)
                - encoding_name: 检测到的编码名称
                - is_utf16: 是否为 UTF-16 编码
        """
        # 检查 BOM
        try:
            with open(file_path, "rb") as f:
                bom = f.read(4)

            if bom[:2] == b"\xff\xfe":  # UTF-16 LE BOM
                return "utf-16-le", True
            elif bom[:2] == b"\xfe\xff":  # UTF-16 BE BOM
                return "utf-16-be", True
            elif bom[:3] == b"\xef\xbb\xbf":  # UTF-8 BOM
                return "utf-8-sig", False
        except Exception:
            pass

        # 尝试检测其他编码
        for encoding in ["utf-8", "gbk", "mbcs"]:
            try:
                with codecs.open(file_path, "r", encoding=encoding) as f:
                    f.read()
                return encoding, False
            except (UnicodeDecodeError, UnicodeError):
                continue

        return None, False

    @staticmethod
    def fix_encoding(file_path, current_encoding):
        """
        修复文件编码为 UTF-16

        Args:
            file_path: 文件路径
            current_encoding: 当前编码名称

        Returns:
            bool: 修复是否成功
        """
        try:
            # 读取当前内容
            with codecs.open(file_path, "r", encoding=current_encoding or "utf-8") as f:
                content = f.read()

            # 写入 UTF-16 编码
            with codecs.open(file_path, "w", encoding=DESKTOP_INI_ENCODING) as f:
                f.write(content)

            return True
        except Exception:
            return False

    @staticmethod
    def ensure_utf16_encoding(file_path):
        """
        确保文件是 UTF-16 编码,如果不是则提示用户确认转换

        如果用户拒绝转换,抛出 EncodingConversionCanceled 异常。

        Args:
            file_path: 文件路径

        Raises:
            EncodingConversionCanceled: 用户拒绝转换
        """
        encoding, is_utf16 = DesktopIniHandler.detect_encoding(file_path)

        if is_utf16:
            return  # 已经是 UTF-16

        # 文件不是 UTF-16,需要用户确认
        print(
            _("Warning: desktop.ini file encoding is {encoding}, not standard UTF-16.").format(
                encoding=encoding or _("unknown")
            )
        )
        print(_("This file needs to be converted to UTF-16 encoding before modification."))
        print(_("The original content will be preserved, only the encoding format will change."))

        try:
            # 显示文件预览
            with codecs.open(file_path, "r", encoding=encoding or "utf-8") as f:
                content = f.read()

            print(_("\nCurrent file content:"))
            print("-" * 40)
            print(content)
            print("-" * 40)

            # 用户确认
            while True:
                response = input(_("\nConvert to UTF-16 encoding and continue? [Y/n]: ")).strip().lower()
                if response in ("", "y", "yes"):
                    break
                elif response in ("n", "no"):
                    print(_("Operation cancelled."))
                    raise EncodingConversionCanceled("用户拒绝编码转换")
                else:
                    print(_("Please enter Y or n"))

            # 执行转换
            with codecs.open(file_path, "w", encoding=DESKTOP_INI_ENCODING) as f:
                f.write(content)

            print(_("Converted to UTF-16 encoding."))

        except EncodingConversionCanceled:
            raise
        except Exception as e:
            print(_("Conversion failed: {error}").format(error=e))
            print(_("Operation cancelled."))
            raise EncodingConversionCanceled(f"编码转换失败: {e}") from e

    @staticmethod
    def remove_info_tip(folder_path):
        """
        移除 desktop.ini 中的 InfoTip

        只删除 InfoTip 行,保留其他设置(如 IconResource, Logo 等)。

        Args:
            folder_path: 文件夹路径

        Returns:
            bool: 操作是否成功

        Raises:
            EncodingConversionCanceled: 用户拒绝编码转换
        """
        desktop_ini_path = DesktopIniHandler.get_path(folder_path)

        if not os.path.exists(desktop_ini_path):
            return True

        try:
            # 确保文件是 UTF-16 编码
            DesktopIniHandler.ensure_utf16_encoding(desktop_ini_path)

            # 读取内容(UTF-16)
            with codecs.open(desktop_ini_path, "r", encoding=DESKTOP_INI_ENCODING) as f:
                content = f.read()

            # 移除 InfoTip 行
            lines = content.splitlines()
            new_lines = []
            for line in lines:
                # 跳过 InfoTip 行(支持 = 前后有/无空格)
                stripped = line.strip()
                if stripped.startswith(
                    DesktopIniHandler.PROPERTY_INFOTIP + "="
                ) or stripped.startswith(DesktopIniHandler.PROPERTY_INFOTIP + " "):
                    continue
                new_lines.append(line)

            # 检查是否还有有效内容
            has_content = False
            for line in new_lines:
                stripped = line.strip()
                if stripped and not stripped.startswith("[.ShellClassInfo]"):
                    has_content = True
                    break

            # 如果没有其他内容,删除文件
            if not has_content:
                os.remove(desktop_ini_path)
                return True

            # 用 UTF-16 写回
            new_content = LINE_ENDING.join(new_lines)
            with codecs.open(desktop_ini_path, "w", encoding=DESKTOP_INI_ENCODING) as f:
                f.write(new_content)

            return True

        except EncodingConversionCanceled:
            return False
        except Exception:
            return False

    @staticmethod
    def delete(folder_path):
        """
        删除 desktop.ini 文件

        Args:
            folder_path: 文件夹路径

        Returns:
            bool: 删除是否成功
        """
        desktop_ini_path = DesktopIniHandler.get_path(folder_path)

        if not os.path.exists(desktop_ini_path):
            return True

        try:
            os.remove(desktop_ini_path)
            return True
        except Exception:
            return False

    @staticmethod
    def set_folder_system_attributes(folder_path):
        """
        设置文件夹为只读属性

        根据 Microsoft 文档和社区讨论,文件夹必须设置为只读属性
        Windows 才会读取 desktop.ini 中的自定义设置。

        参考: "Apply the read-only attribute for each folder.
        This will make Explorer process the desktop.ini file for that folder."
        https://superuser.com/questions/1117824/how-to-get-windows-to-read-copied-desktop-ini-file

        Args:
            folder_path: 文件夹路径

        Returns:
            bool: 设置是否成功
        """
        try:
            import ctypes
            import subprocess

            # 使用 Windows API 检查文件夹是否已有只读属性
            FILE_ATTRIBUTE_READONLY = 0x01  # noqa: N806 - Windows API 常量
            GetFileAttributesW = ctypes.windll.kernel32.GetFileAttributesW  # noqa: N806 - Windows API

            attrs = GetFileAttributesW(folder_path)
            if attrs == 0xFFFFFFFF:  # INVALID_FILE_ATTRIBUTES
                return False

            # 如果已有只读属性,无需再次设置
            if attrs & FILE_ATTRIBUTE_READONLY:
                return True

            # 设置文件夹为只读属性
            result = subprocess.call(
                'attrib +r "' + folder_path + '"',
                shell=True,
                stdout=subprocess.DEVNULL,  # 抑制输出
                stderr=subprocess.DEVNULL,
            )
            return result == 0
        except Exception:
            return False

    @staticmethod
    def set_file_hidden_system_attributes(file_path):
        """
        设置 desktop.ini 文件为隐藏和系统属性

        根据 Microsoft 文档,desktop.ini 应该被标记为隐藏和系统文件
        以防止普通用户看到或修改它。

        Args:
            file_path: desktop.ini 文件路径

        Returns:
            bool: 设置是否成功
        """
        try:
            import subprocess

            result = subprocess.call('attrib +h +s "' + file_path + '"', shell=True)
            return result == 0
        except Exception:
            return False

    @staticmethod
    def clear_file_attributes(file_path):
        """
        清除文件的隐藏和系统属性

        在修改 desktop.ini 之前需要调用,以便能够写入文件

        Args:
            file_path: 文件路径

        Returns:
            bool: 清除是否成功
        """
        try:
            import subprocess

            result = subprocess.call('attrib -s -h "' + file_path + '"', shell=True)
            return result == 0
        except Exception:
            return False


================================================
FILE: remark/utils/__init__.py
================================================
"""
工具模块
"""

from remark.utils.constants import MAX_COMMENT_LENGTH
from remark.utils.platform import check_platform

__all__ = [
    "MAX_COMMENT_LENGTH",
    "check_platform",
]


================================================
FILE: remark/utils/constants.py
================================================
"""
常量定义
"""

MAX_COMMENT_LENGTH = 260

# GitHub 仓库配置
GITHUB_REPO = "piratf/windows-folder-remark"
GITHUB_API_RELEASES = "https://api.github.com/repos/piratf/windows-folder-remark/releases/latest"

# 更新配置
UPDATE_CHECK_INTERVAL = 86400  # 检查间隔(秒),默认 24 小时
UPDATE_CACHE_FILE = "update_check_cache.txt"  # 缓存下次检查时间


================================================
FILE: remark/utils/encoding.py
================================================
"""
编码处理工具
"""


================================================
FILE: remark/utils/path_resolver.py
================================================
"""
路径解析模块

处理未加引号的含空格路径,智能重建完整路径。
"""

import posixpath
import re
from collections import deque
from copy import deepcopy
from dataclasses import dataclass
from enum import Enum
from pathlib import Path, PureWindowsPath


class NextResult(Enum):
    """Cursor.next() 的返回类型枚举"""

    SEPARATOR = "separator"  # 找到路径分隔符
    END_OF_ARG = "end_of_arg"  # 找到参数末尾


@dataclass
class Cursor:
    """
    路径解析游标,跟踪当前解析位置

    注意:Cursor 在 posix 格式分隔符 (/) 上工作,因为 normalized_args 使用 posix 格式

    Attributes:
        arg_index: 当前指向第几个参数(从 0 开始)
        char_index: 当前指向参数中第几个字符(路径归一化后的参数为准,从 0 开始)
    """

    arg_index: int
    char_index: int

    def jump_to_last_separator(self, normalized_args: list[str]) -> None:
        """
        跳转到当前参数的最后一个系统分隔符位置
        如果找不到,则留在当前位置 (后面没有分隔符)

        :param normalized_args: 归一化后的参数列表
        """
        norm_path = normalized_args[self.arg_index]
        last_sep = norm_path.rfind(posixpath.sep)
        if last_sep >= 0:
            self.char_index = last_sep
        else:
            self.char_index = -1

    def next(self, normalized_args: list[str]) -> tuple["Cursor", NextResult] | None:
        """
        从当前位置向后查找,找到下一个路径分隔符或参数末尾

        搜索从 char_index + 1 开始(跳过当前位置),找到下一个分隔符或参数末尾。
        找到分隔符时,end cursor 停在分隔符上(与 jump_to_last_separator 保持一致)。

        :param normalized_args: 归一化后的参数列表
        :return: (新 cursor, NextResult) 或 None(如果无法继续)
        """
        new_cursor = Cursor(self.arg_index, self.char_index)

        while new_cursor.arg_index < len(normalized_args):
            current_arg = normalized_args[new_cursor.arg_index]
            arg_len = len(current_arg)

            # 如果当前位置已在参数末尾,跳到下一个参数开头
            if new_cursor.char_index >= arg_len:
                new_cursor.arg_index += 1
                new_cursor.char_index = 0
                continue

            # 从 char_index + 1 开始查找分隔符(跳过当前位置)
            search_start = new_cursor.char_index + 1
            sep_pos = current_arg.find(posixpath.sep, search_start)

            if sep_pos >= 0:
                # 找到分隔符,新 cursor 停在分隔符上
                new_cursor.char_index = sep_pos
                return new_cursor, NextResult.SEPARATOR

            # 没有找到分隔符,跳到当前参数末尾
            new_cursor.char_index = arg_len
            return new_cursor, NextResult.END_OF_ARG

        # 已经到达最后一个参数的末尾,无法继续
        return None


def get_between(begin: Cursor, end: Cursor, normalized_args: list[str]) -> list[str]:
    """
    获取两个 cursor 之间的内容,可能跨多个参数

    :param begin: 起始 cursor(不包含)
    :param end: 结束 cursor(不包含)
    :param normalized_args: 归一化后的参数列表
    :return: 字符串片段列表
    """
    if begin.arg_index == end.arg_index:
        # 同一个参数内,从 begin.char_index + 1 开始(不包含 begin 位置)
        arg = normalized_args[begin.arg_index]
        return [arg[begin.char_index + 1 : end.char_index]]

    # 跨多个参数
    result = []

    # 第一个参数的部分,从 begin.char_index + 1 开始(不包含 begin 位置)
    first_arg = normalized_args[begin.arg_index]
    result.append(first_arg[begin.char_index + 1 :])

    # 中间的完整参数
    for i in range(begin.arg_index + 1, end.arg_index):
        result.append(normalized_args[i])

    # 最后一个参数的部分
    if end.arg_index < len(normalized_args):
        last_arg = normalized_args[end.arg_index]
        result.append(last_arg[: end.char_index])

    return result


def build_pattern(parts: list[str]) -> re.Pattern:
    r"""
    将多个字符串片段构建为宽容搜索的正则表达式

    由于终端用空格分割参数,用户输入的 "My Folder" 会被分割成 ["My", "Folder"]。
    此函数构建一个宽容的正则表达式来匹配可能的文件名。

    宽容规则:
    - 片段之间允许任意空白字符(用 \s+ 连接)
    - 转义所有正则表达式特殊字符,用户输入不含正则表达式
    - 使用 re.IGNORECASE 忽略大小写(Windows 文件系统不区分大小写)

    :param parts: 字符串片段列表
    :return: 编译后的正则表达式模式
    """
    if not parts:
        return re.compile(r"")

    # 转义每个片段中的正则表达式特殊字符
    escaped_parts = [re.escape(part) for part in parts]

    # 用 \s+ 连接片段,允许片段之间有任意空白
    pattern = r"\s+".join(escaped_parts)

    # 首尾精确匹配
    pattern = r"^" + pattern + r"$"

    # 忽略大小写匹配
    return re.compile(pattern, re.IGNORECASE)


def get_current_working_path(
    first_arg: str, cursor: Cursor | None = None, normalized_args: list[str] | None = None
) -> tuple[PureWindowsPath, Cursor]:
    """
    从第一个参数中提取工作目录和剩余内容

    规则:
    - 使用 PureWindowsPath 规范化路径并获取父目录
    - 空字符串 → (".", "")

    :param first_arg: 一个路径
    :param cursor: 游标,如果为 None 则初始化为 (0, 0)
    :param normalized_args: 归一化后的参数列表,如果为 None 则使用 first_arg 初始化
    :return: (工作目录, Cursor)
    """
    # 如果 cursor 为 None,初始化为 (0, 0)
    if cursor is None:
        cursor = Cursor(0, 0)

    # 如果 normalized_args 为 None,归一化 first_arg
    if normalized_args is None:
        normalized_args = [PureWindowsPath(first_arg).as_posix()]

    # 空字符串处理
    if not first_arg:
        return PureWindowsPath(), cursor

    # 使用 pathlib 获取父目录
    path_obj = PureWindowsPath(first_arg)
    parent = path_obj.parent

    # 跳转到最后一个分隔符位置,因为 parent 可能是 "." 等特殊情况,根据最后一个分隔符判断是安全的
    cursor.jump_to_last_separator(normalized_args)
    return parent if parent else PureWindowsPath(), cursor


def get_inner_items_list(current_working_path: Path) -> list[Path]:
    """
    获取指定路径下的所有文件和文件夹列表

    :param current_working_path: 当前工作目录路径
    :return: 文件和文件夹名称列表,如果路径不存在或不是目录则返回空列表
    """
    return list(current_working_path.iterdir())


def find_candidates(
    args_list: list[str],
) -> list[tuple[Path, list[str], str]]:
    """
    递归查找所有可能的路径重建候选

    返回所有候选,按优先级排序(消耗更多 args 的优先)

    Args:
        args_list: argparse 解析后的位置参数列表
                   例如: ["C:\\Program", "Files", "App"] 或 ["My", "Folder/App", "备注"]

    Returns:
        List[Tuple[full_path, remaining_args, type]]: 所有候选
        - full_path: 完整路径
        - remaining_args: 剩余参数(作为备注内容)
        - type: "folder" 或 "file"

    """
    if not args_list:
        return []

    # 归一化所有参数
    normalized_args = [PureWindowsPath(arg).as_posix() for arg in args_list]

    # 构建游标
    cursor = Cursor(0, 0)

    # 根据第一个参数,判断当前工作目录
    current_working_path, cursor = get_current_working_path(args_list[0], cursor, normalized_args)

    # 处理剩余内容
    # 接下来是一个经典的 BFS 搜索问题,我们使用队列来保存当前工作目录和游标作为搜索起点(值拷贝)
    # - 如果队列为空,结束搜索
    # 获取当前队头的工作目录对应的文件列表
    #   - 如果为空,则工作目录加入候选,弹出队列
    #   - 否则继续
    # 接下来使用三指针策略,一个新的 next_ 指针沿着当前 cursor 向后找,一个 last_ 保存上一次找到的位置,一个 start_ 保存起始位置,每次
    #   - 找到下一个路径分隔符
    #   - 或者找到下一个参数末尾
    # Cursor 应当提供一个 next 接口,返回新的指针和以上两种类型之一,但是不要修改当前 cursor 的值
    # Cursor 应当提供一个 get_between 接口,返回两个指针之间的全部字符串内容(可能跨多个参数,因此可能有多个字符串)
    # 当前模块应当提供一个把多个字符串组合成一个正则表达式的函数
    # 如果找到的是路径分隔符
    #   - 将当前找到的内容()**放到一个正则表达式**中(抽象为一个函数),然后在文件列表中搜索匹配
    #       - 如果匹配成功
    #           - 将成功的一个或多个匹配作为新的工作目录,带着新 cursor 值,进行深拷贝并加入候选项
    #       - 如果没有匹配成功
    #           - 结束搜索,返回当前可选项
    #   - 弹出当前工作目录
    # 如果找到的是参数末尾
    #   - 将当前找到的内容**放到一个正则表达式**中,然后在文件列表中搜索匹配
    #   - 如果匹配成功
    #       - 将成功的一个或多个匹配作为新的工作目录,带着新 cursor 值,进行深拷贝并加入候选项
    #   - 如果没有匹配成功
    #       - 当前 cursor 不变,队列也不变,新 Cursor 继续向后找
    #   - 无论匹配是否成功,当前工作目录都不变
    # 如果没有下一个参数,新 Cursor 无法前进
    #   - 弹出队列中的当前工作目录

    candidates: list[tuple[Path, list[str], str]] = []
    # 队列元素: (current_working_path, cursor)
    queue: deque[tuple[Path, Cursor, Cursor]] = deque()
    # working_path, start, last
    queue.append((Path(current_working_path), deepcopy(cursor), deepcopy(cursor)))

    while queue:
        work_path, start_cursor, cur = queue.popleft()
        if not work_path.is_dir():
            continue

        # 尝试从当前 cursor 向后推进
        next_result = cur.next(normalized_args)
        if next_result is None:
            # 无法继续,弹出队列中的当前工作目录(已处理)
            continue

        next_cursor, result_type = next_result

        # 获取当前 cursor 和 next_cursor 之间的内容
        parts = get_between(start_cursor, next_cursor, normalized_args)

        # 构建正则表达式
        pattern = build_pattern(parts)

        # 获取当前工作目录的文件列表
        inner_items = get_inner_items_list(work_path)

        if not inner_items:
            # 工作目录为空,说明某个 A\\B 的路径不正确 (A 是空目录)
            # 搜索失败
            continue

        # 在文件列表中搜索匹配
        matches = [item.name for item in inner_items if pattern.search(item.name)]

        if result_type == NextResult.SEPARATOR:
            # 找到分隔符
            if matches:
                # 匹配成功,将匹配项作为新的工作目录加入队列
                # 需要将 cursor 推进到分隔符之后
                for match in matches:
                    new_work_path = work_path / match
                    queue.append((new_work_path, next_cursor, next_cursor))
            else:
                # 匹配失败,结束搜索,返回当前候选
                break

        elif result_type == NextResult.END_OF_ARG:
            # 找到参数末尾
            if matches:
                # 匹配成功,将匹配项加入候选
                for match in matches:
                    full_path = work_path / match
                    is_folder = full_path.is_dir()
                    entry_type = "folder" if is_folder else "file"
                    remaining = get_remaining_args(next_cursor, normalized_args)
                    candidates.append((full_path, remaining, entry_type))

            # 无论匹配是否成功,当前工作目录不变,继续尝试向前推进
            # 将当前工作目录和 next_cursor 重新加入队列
            queue.append((work_path, start_cursor, next_cursor))

    def _candidate_key(item: tuple[Path, list[str], str]) -> tuple[bool, int]:
        """候选排序键函数:folder 优先,路径越长越优先"""
        return (
            item[2] != "folder",  # folder 优先
            -len(str(item[0])),  # 路径越长越优先
        )

    # 匹配到的路径越长越优先(消耗的参数越多,剩余参数越少)
    candidates.sort(key=_candidate_key)

    return candidates if candidates else []


def get_remaining_args(cursor: Cursor, normalized_args: list[str]) -> list[str]:
    """
    获取 cursor 之后的所有剩余参数,作为备注内容

    :param cursor: 当前游标
    :param normalized_args: 归一化后的参数列表
    :return: 剩余参数拼接的字符串
    """
    remaining = []

    # 当前参数的剩余部分
    if cursor.arg_index < len(normalized_args):
        current_arg = normalized_args[cursor.arg_index]
        if cursor.char_index < len(current_arg):
            remaining.append(current_arg[cursor.char_index :])

    # 后续完整参数
    for i in range(cursor.arg_index + 1, len(normalized_args)):
        remaining.append(normalized_args[i])

    return remaining


================================================
FILE: remark/utils/platform.py
================================================
"""
平台检查工具
"""

import platform

from remark.i18n import _ as _


def check_platform() -> bool:
    """检查是否为 Windows 系统"""
    if platform.system() != "Windows":
        print(_("Error: This tool adds remarks to files/folders on Windows, other systems are not supported."))
        print(_("Current system: {system}").format(system=platform.system()))
        return False
    return True


================================================
FILE: remark/utils/registry.py
================================================
"""
Windows 注册表操作工具

用于安装/卸载右键菜单到 Windows 资源管理器。
"""

import contextlib
import os
import sys
import winreg

# =============================================================================
# 常量定义
# =============================================================================

REGISTRY_ROOT = winreg.HKEY_CURRENT_USER
REGISTRY_PATH = r"Software\Classes\Directory\shell\WindowsFolderRemark"
MENU_NAME = "添加文件夹备注"
ICON_INDEX = 0


# =============================================================================
# 工具函数
# =============================================================================


def get_executable_path() -> str:
    """
    获取可执行文件路径

    Returns:
        开发环境: 返回 python 脚本路径(用于测试)
        打包后: 返回 exe 文件完整路径
    """
    if getattr(sys, "frozen", False):
        # PyInstaller 打包后,sys.executable 指向 exe 文件
        return sys.executable
    else:
        # 开发环境,从 __file__ 推导脚本路径
        current_file = os.path.abspath(__file__)
        # remark/utils/registry.py -> remark.py
        script_path = os.path.join(os.path.dirname(os.path.dirname(current_file)), "remark.py")
        return os.path.abspath(script_path)


def install_context_menu() -> bool:
    r"""
    安装右键菜单到注册表

    创建的注册表结构:
    HKCU\Software\Classes\Directory\shell\WindowsFolderRemark
        @="添加文件夹备注"
        Icon="[exe_path],0"
        \command
            @="[exe_path] --gui "%1""

    Returns:
        成功返回 True,失败返回 False
    """
    exe_path = get_executable_path()

    try:
        # 创建主键
        key = winreg.CreateKey(REGISTRY_ROOT, REGISTRY_PATH)

        # 设置默认值(菜单显示文本)
        winreg.SetValueEx(key, "", 0, winreg.REG_SZ, MENU_NAME)

        # 设置图标(使用 exe 文件的第一个图标)
        icon_value = f'"{exe_path}",{ICON_INDEX}'
        winreg.SetValueEx(key, "Icon", 0, winreg.REG_SZ, icon_value)

        winreg.CloseKey(key)

        # 创建 command 子键
        command_path = f"{REGISTRY_PATH}\\command"
        command_key = winreg.CreateKey(REGISTRY_ROOT, command_path)

        # 设置命令
        command_value = f'"{exe_path}" --gui "%1"'
        winreg.SetValueEx(command_key, "", 0, winreg.REG_SZ, command_value)

        winreg.CloseKey(command_key)

        return True

    except PermissionError:
        return False
    except OSError:
        return False


def uninstall_context_menu() -> bool:
    """
    从注册表卸载右键菜单

    Returns:
        成功返回 True(包括键不存在的情况),失败返回 False
    """
    try:
        # 先删除 command 子键
        command_path = f"{REGISTRY_PATH}\\command"
        with contextlib.suppress(FileNotFoundError):
            winreg.DeleteKey(REGISTRY_ROOT, command_path)

        # 删除主键
        with contextlib.suppress(FileNotFoundError):
            winreg.DeleteKey(REGISTRY_ROOT, REGISTRY_PATH)

        return True

    except PermissionError:
        return False
    except OSError:
        return False


================================================
FILE: remark/utils/updater.py
================================================
"""
自动更新模块

提供版本检测、下载更新、创建更新脚本等功能。
"""

import json
import os
import sys
import tempfile
import urllib.error
import urllib.request
from typing import Any

from packaging import version

from remark.utils.constants import (
    GITHUB_API_RELEASES,
    UPDATE_CACHE_FILE,
    UPDATE_CHECK_INTERVAL,
)


def _get_proxies() -> dict[str, str] | None:
    """从环境变量获取代理配置"""
    proxies = []
    http_proxy = os.environ.get("HTTP_PROXY") or os.environ.get("http_proxy")
    https_proxy = os.environ.get("HTTPS_PROXY") or os.environ.get("https_proxy")

    if http_proxy:
        proxies.append(("http", http_proxy))
    if https_proxy:
        proxies.append(("https", https_proxy))

    return dict(proxies) if proxies else None


def _create_opener():
    """创建带代理的 URL opener"""
    proxies = _get_proxies()
    if proxies:
        proxy_handler = urllib.request.ProxyHandler(proxies)
        return urllib.request.build_opener(proxy_handler)
    return urllib.request.build_opener()


def _get_cache_file_path() -> str:
    """获取缓存文件的完整路径(放在临时目录)"""
    return os.path.join(tempfile.gettempdir(), UPDATE_CACHE_FILE)


def get_executable_path() -> str:
    """获取当前可执行文件路径"""
    if getattr(sys, "frozen", False):
        # PyInstaller 打包后的 exe
        return sys.executable
    else:
        # 开发环境下的 Python 脚本
        return os.path.abspath(__file__)


def get_latest_release() -> dict[str, Any] | None:
    """
    从 GitHub API 获取最新 release 信息
    使用 urllib 而不是 requests 是为了减少打包体积,减轻用户下载负担

    Returns:
        包含 tag_name, html_url, body, download_url 的字典,如果获取失败则返回 None
    """
    try:
        request = urllib.request.Request(
            GITHUB_API_RELEASES,
            headers={
                "Accept": "application/vnd.github.v3+json",
                "User-Agent": "windows-folder-remark",
            },
        )
        opener = _create_opener()
        with opener.open(request, timeout=10) as response:
            if response.status != 200:
                return None
            data = json.load(response)

        # 过滤掉 prerelease 和 draft
        if data.get("prerelease") or data.get("draft"):
            return None

        # 查找 windows-folder-remark-*.exe 文件
        download_url = None
        for asset in data.get("assets", []):
            name = asset.get("name", "")
            if name.startswith("windows-folder-remark-") and name.endswith(".exe"):
                download_url = asset.get("browser_download_url")
                break

        if not download_url:
            return None

        return {
            "tag_name": data.get("tag_name", "").lstrip("v"),
            "html_url": data.get("html_url", ""),
            "body": data.get("body", ""),
            "download_url": download_url,
        }
    except (urllib.error.HTTPError, urllib.error.URLError, json.JSONDecodeError, OSError):
        return None


def should_check_update() -> bool:
    """
    检查是否应该进行更新检查

    Returns:
        True 表示应该检查,False 表示还没到检查时间
    """
    cache_file = _get_cache_file_path()
    if not os.path.exists(cache_file):
        return True  # 没有记录,进行第一次检查

    try:
        import time

        with open(cache_file, encoding="utf-8") as f:
            next_check_time = float(f.read().strip())
        return time.time() >= next_check_time
    except (ValueError, OSError):
        return True


def update_next_check_time() -> None:
    """更新下次检查时间为当前时间 + 24小时"""
    cache_file = _get_cache_file_path()
    try:
        import time

        next_check = time.time() + UPDATE_CHECK_INTERVAL
        with open(cache_file, "w", encoding="utf-8") as f:
            f.write(str(next_check))
    except OSError:
        pass


def check_updates_auto(current_version: str) -> dict[str, Any] | None:
    """
    自动检查更新(CLI 启动时调用,尊重缓存)

    此函数用于后台自动检查更新,会先检查缓存文件,只有在需要时才访问网络。
    检查成功后会更新缓存文件的下次检查时间。

    Args:
        current_version: 当前版本号

    Returns:
        最新 release 信息字典,如果没有新版本则返回 None
    """
    if not should_check_update():
        return None

    # 决定检查,立即更新下次检查时间
    update_next_check_time()

    latest = get_latest_release()
    if not latest:
        return None

    try:
        if version.parse(latest["tag_name"]) > version.parse(current_version):
            return latest
    except version.InvalidVersion:
        return None

    return None


def check_updates_manual(current_version: str) -> dict[str, Any] | None:
    """
    手动检查更新(--update 命令调用,绕过缓存)

    此函数用于用户手动触发更新检查,会直接访问 GitHub API 获取最新版本信息,
    不受缓存文件影响。

    Args:
        current_version: 当前版本号

    Returns:
        最新 release 信息字典,如果没有新版本则返回 None
    """
    latest = get_latest_release()
    if not latest:
        return None

    try:
        if version.parse(latest["tag_name"]) > version.parse(current_version):
            return latest
    except version.InvalidVersion:
        return None

    return None


def download_update(url: str, dest: str) -> str:
    """
    下载新版本 exe

    Args:
        url: 下载 URL
        dest: 目标路径(文件名)

    Returns:
        下载的文件路径
    """
    request = urllib.request.Request(
        url,
        headers={"User-Agent": "windows-folder-remark"},
    )
    opener = _create_opener()

    with opener.open(request, timeout=30) as response:
        total_size = int(response.headers.get("content-length", 0))
        downloaded = 0
        chunk_size = 8192

        with open(dest, "wb") as f:
            while True:
                chunk = response.read(chunk_size)
                if not chunk:
                    break
                f.write(chunk)
                downloaded += len(chunk)

                # 显示进度
                if total_size > 0:
                    percent = min(downloaded * 100 / total_size, 100)
                    downloaded_mb = downloaded / 1024 / 1024
                    total_mb = total_size / 1024 / 1024
                    print(
                        f"\r下载进度: {percent:.1f}% ({downloaded_mb:.1f}MB / {total_mb:.1f}MB)",
                        end="",
                    )
        print()  # 换行

    return dest


def create_update_script(old_exe: str, new_exe: str) -> str:
    """
    创建更新批处理脚本

    Args:
        old_exe: 旧 exe 路径
        new_exe: 新 exe 路径

    Returns:
        批处理脚本路径
    """
    script_content = f"""@echo off
REM 等待主进程退出
timeout /t 3 /nobreak >nul

REM 替换 exe
move /Y "{new_exe}" "{old_exe}"

REM 删除自己
del "%~f0"

REM 更新完成
echo 更新完成!请手动启动新版本程序。
pause
"""

    # 创建临时脚本文件
    temp_dir = tempfile.gettempdir()
    script_path = os.path.join(temp_dir, "update_windows_folder_remark.bat")

    with open(script_path, "w", encoding="gbk") as f:
        f.write(script_content)

    return script_path


def trigger_update(script_path: str) -> None:
    """
    触发更新流程:启动批处理脚本并退出程序

    Args:
        script_path: 批处理脚本路径
    """
    import subprocess

    # 启动批处理脚本(新窗口,不阻塞)
    subprocess.Popen(
        ["cmd.exe", "/c", script_path],
        shell=True,
        creationflags=subprocess.CREATE_NEW_CONSOLE,
    )


================================================
FILE: remark.py
================================================
"""
Windows 文件/文件夹备注工具 - 主入口
"""

from remark.cli.commands import main

if __name__ == "__main__":
    main()


================================================
FILE: remark.spec
================================================
# -*- mode: python ; coding: utf-8 -*-
"""
PyInstaller spec file for windows-folder-remark

Usage:
    pyinstaller remark.spec
"""

import os
import re
import sys
import subprocess

from PyInstaller.utils.hooks import collect_submodules

if sys.platform == "win32":
    sys.stdout.reconfigure(encoding="utf-8")
    sys.stderr.reconfigure(encoding="utf-8")


def get_upx_dir():
    local_upx_dir = os.path.join(SPECPATH, "tools", "upx")
    upx_exe = os.path.join(local_upx_dir, "upx.exe")

    if os.path.exists(upx_exe):
        return local_upx_dir

    print("WARNING: UPX not available, compression disabled (exe will be larger)")
    print("HINT: Run 'python scripts/ensure_upx.py' to install UPX automatically")
    return None

# =============================================================================
# Configuration
# =============================================================================

block_cipher = None
app_name = "windows-folder-remark"


def get_app_version():
    """从 pyproject.toml 的 [project] 部分读取版本号,确保版本同步"""
    # SPECPATH 是 PyInstaller 提供的内置全局变量,指向 spec 文件所在目录
    toml_file = os.path.join(SPECPATH, "pyproject.toml")
    with open(toml_file, encoding="utf-8") as f:
        lines = f.readlines()

    # 只查找 [project] 部分的 version 字段
    in_project_section = False
    for line in lines:
        if line.strip() == "[project]":
            in_project_section = True
        elif line.startswith("[") and not line.startswith("[["):
            in_project_section = False
        elif in_project_section and line.strip().startswith("version"):
            match = re.search(r'=\s*["\']([^"\']+)["\']', line)
            if match:
                return match.group(1)
    return "unknown"


app_version = get_app_version()
app_description = "Windows 文件夹备注工具"

# =============================================================================
# Python Interpreter Options
# =============================================================================

# 强制启用 UTF-8 模式,支持中文等特殊字符输出
# See: https://pyinstaller.org/en/stable/spec-files.html#specifying-python-interpreter-options
options = [
    ('X utf8', None, 'OPTION'),
]

# =============================================================================
# Analysis
# =============================================================================

# Collect all submodules from remark package
hiddenimports = collect_submodules('remark') + [
    'tkinter',
    'packaging',
    'packaging.version',
]

# Collect locale translation files (.mo)
# PyInstaller 会将这些文件复制到 exe 的临时目录
locale_datas = []
for lang in ['zh', 'en']:
    lang_dir = os.path.join('locale', lang, 'LC_MESSAGES')
    mo_file = os.path.join(lang_dir, 'messages.mo')
    if os.path.exists(mo_file):
        locale_datas.append((mo_file, lang_dir))

a = Analysis(
    [os.path.join("remark", "cli", "commands.py")],
    pathex=[],
    binaries=[],
    datas=locale_datas,
    hiddenimports=hiddenimports,
    hookspath=[],
    hooksconfig={},
    runtime_hooks=[],
    excludes=[
        'setuptools',
        'setuptools.*',
        'distutils',
        'distutils.*',
        'unittest',
        'pydoc',
        'pydoc_data',
    ],
    win_no_prefer_redirects=False,
    win_private_assemblies=False,
    cipher=block_cipher,
    noarchive=False,
)

# =============================================================================
# PYZ
# =============================================================================

pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)

# =============================================================================
# EXE
# =============================================================================

upx_dir = get_upx_dir()

exe = EXE(
    pyz,
    a.scripts,
    a.binaries,
    a.zipfiles,
    a.datas,
    options,  # Python 解释器选项:启用 UTF-8 模式
    [],
    name=app_name,
    debug=False,
    bootloader_ignore_signals=False,
    strip=False,
    upx=upx_dir is not None,
    upx_dir=upx_dir if upx_dir is not None else "",
    upx_exclude=[],
    runtime_tmpdir=None,
    console=True,  # Console application for interactive mode
    disable_windowed_traceback=False,
    argv_emulation=False,
    target_arch=None,
    codesign_identity=None,
    entitlements_file=None,
)


================================================
FILE: scripts/__init__.py
================================================
"""Scripts package."""


================================================
FILE: scripts/analyze_exe_size.py
================================================
"""
PyInstaller EXE 大小分析工具

使用方法:
    python scripts/analyze_exe_size.py

功能:
    1. 运行 pyi-archive_viewer -r 获取 exe 内容
    2. 分析各组件大小
    3. 生成详细报告到 tmp/ 目录
"""

import os
import re
import subprocess
from datetime import datetime


def run_archive_viewer(exe_path: str) -> str:
    """运行 pyi-archive_viewer -r 并获取输出"""
    print(f"Analyzing {exe_path}...")

    result = subprocess.run(
        ["pyi-archive_viewer", "-r", exe_path],
        capture_output=True,
        text=True,
    )

    return result.stdout


def parse_archive_content(content: str) -> dict[str, int]:
    """解析 archive_viewer 输出,返回 {name: size} 字典"""
    components: dict[str, int] = {}
    for line in content.split("\n"):
        if not line.strip() or line.startswith("position") or line.startswith("Options"):
            continue
        match = re.match(r"^\s*\d+,\s*(\d+),\s*\d+,\s*\d+,\s*'[^']+',\s*'([^']+)'", line)
        if match:
            size = int(match.group(1))
            name = match.group(2)
            components[name] = components.get(name, 0) + size
    return components


def parse_pyz_content(content: str) -> dict[str, int]:
    """解析 PYZ.pyz 内部内容"""
    if "Contents of 'PYZ.pyz'" not in content:
        return {}

    pyz_section = content.split("Contents of 'PYZ.pyz'")[1]
    lines = pyz_section.split("\n")[:5000]

    packages: dict[str, int] = {}
    for line in lines:
        if not line.strip() or line.startswith("Contents") or line.startswith("typecode"):
            continue
        parts = line.split(",")
        if len(parts) >= 4:
            try:
                length = int(parts[2].strip())
                name = parts[3].strip().strip("'")

                # 按包分组
                if "." in name:
                    pkg = name.split(".")[0]
                else:
                    pkg = name

                packages[pkg] = packages.get(pkg, 0) + length
            except (ValueError, IndexError):
                pass
    return packages


def generate_report(
    exe_path: str,
    components: dict[str, int],
    pyz_packages: dict[str, int],
    output_path: str,
) -> None:
    """生成分析报告"""
    exe_size = os.path.getsize(exe_path)

    lines = []
    lines.append("=" * 80)
    lines.append("PyInstaller EXE Size Analysis Report")
    lines.append("=" * 80)
    lines.append("")
    lines.append(f"EXE: {exe_path}")
    lines.append(f"Current EXE Size: {exe_size / (1024 * 1024):.2f} MB ({exe_size:,} bytes)")
    lines.append(f"Analysis Time: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
    lines.append("")

    # 1. 最大的文件
    lines.append("=" * 80)
    lines.append("1. Top 50 Largest Files/Components")
    lines.append("=" * 80)
    lines.append("")
    sorted_all = sorted(components.items(), key=lambda x: x[1], reverse=True)
    for i, (name, size) in enumerate(sorted_all[:50], 1):
        name_short = name[:47] + "..." if len(name) > 50 else name
        lines.append(f"{i:3d}. {name_short:<50} {size // 1024:6d} KB")

    # 2. DLL 文件
    lines.append("")
    lines.append("=" * 80)
    lines.append("2. DLL Files (Windows System Libraries)")
    lines.append("=" * 80)
    lines.append("")
    dlls = [(n, s) for n, s in components.items() if n.endswith(".dll")]
    dlls.sort(key=lambda x: x[1], reverse=True)
    for name, size in dlls[:30]:
        lines.append(f"{name:<60} {size // 1024:6d} KB")

    # 3. PYD 文件
    lines.append("")
    lines.append("=" * 80)
    lines.append("3. PYD Files (Python Extension Modules)")
    lines.append("=" * 80)
    lines.append("")
    pyds = [(n, s) for n, s in components.items() if n.endswith(".pyd")]
    pyds.sort(key=lambda x: x[1], reverse=True)
    for name, size in pyds[:20]:
        lines.append(f"{name:<60} {size // 1024:6d} KB")

    # 4. Tcl/Tk 文件
    lines.append("")
    lines.append("=" * 80)
    lines.append("4. Tcl/Tk Data Files")
    lines.append("=" * 80)
    lines.append("")
    tcl_files = [(n, s) for n, s in components.items() if "_tcl_data" in n or "tcl8" in n]
    tcl_files.sort(key=lambda x: x[1], reverse=True)
    for name, size in tcl_files[:30]:
        lines.append(f"{name:<60} {size // 1024:6d} KB")

    # 5. PYZ.pyz 内部模块
    if pyz_packages:
        lines.append("")
        lines.append("=" * 80)
        lines.append("PYZ.pyz Internal Python Modules Analysis")
        lines.append("=" * 80)
        lines.append("")
        lines.append(f"Total PYZ.pyz size: {sum(pyz_packages.values()) // 1024} KB")
        lines.append("")

        lines.append("=" * 80)
        lines.append("5. Python Packages by Size (Top 50)")
        lines.append("=" * 80)
        lines.append("")
        sorted_packages = sorted(pyz_packages.items(), key=lambda x: x[1], reverse=True)
        for pkg, size in sorted_packages[:50]:
            lines.append(f"{pkg:<30} {size // 1024:6d} KB")

        # 可移除的模块
        excludable = ["setuptools", "unittest", "email", "distutils", "pydoc", "pydoc_data", "test"]
        lines.append("")
        lines.append("=" * 80)
        lines.append("6. Potentially Removable Modules")
        lines.append("=" * 80)
        lines.append("")
        excludable_packages = [
            (p, s) for p, s in sorted_packages if any(e in p for e in excludable)
        ]
        total_removable = sum(s for p, s in excludable_packages)
        for pkg, size in excludable_packages:
            lines.append(f"{pkg:<30} {size // 1024:6d} KB")
        lines.append("")
        lines.append(
            f"Total potentially removable: {total_removable // 1024} KB ({total_removable // 1024 / 1024:.2f} MB)"
        )

    # 写入文件
    with open(output_path, "w", encoding="utf-8") as f:
        f.write("\n".join(lines))


def main():
    # 获取 exe 路径
    script_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
    exe_path = os.path.join(script_dir, "dist", "windows-folder-remark.exe")

    if not os.path.exists(exe_path):
        print(f"Error: {exe_path} not found!")
        print("Please build the exe first with: pyinstaller remark.spec")
        return 1

    # 运行 archive_viewer
    recursive_content = run_archive_viewer(exe_path)

    # 解析内容
    components = parse_archive_content(recursive_content)
    pyz_packages = parse_pyz_content(recursive_content)

    # 生成带时间戳的输出文件
    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
    tmp_dir = os.path.join(script_dir, "tmp")
    os.makedirs(tmp_dir, exist_ok=True)
    output_path = os.path.join(tmp_dir, f"exe_size_analysis_{timestamp}.txt")

    # 生成报告
    generate_report(exe_path, components, pyz_packages, output_path)

    print(f"\nReport saved to: {output_path}")

    # 打印摘要
    exe_size = os.path.getsize(exe_path)
    print("\nSummary:")
    print(f"  EXE Size: {exe_size / (1024 * 1024):.2f} MB")
    print(f"  Largest component: {max(components.items(), key=lambda x: x[1])[0]}")
    if pyz_packages and "setuptools" in pyz_packages:
        print(f"  setuptools size: {pyz_packages['setuptools'] // 1024} KB (removable)")

    return 0


if __name__ == "__main__":
    exit(main())


================================================
FILE: scripts/build.py
================================================
"""
本地打包脚本

使用方法:
    # 打包为单文件 exe
    python -m scripts.build

    # 清理构建文件
    python -m scripts.build --clean
"""

import argparse
import os
import shutil
import subprocess
import sys
from collections.abc import Callable

# 设置 UTF-8 输出编码(Windows 兼容)
if sys.platform == "win32":
    if hasattr(sys.stdout, "reconfigure"):
        sys.stdout.reconfigure(encoding="utf-8", errors="replace")
    if hasattr(sys.stderr, "reconfigure"):
        sys.stderr.reconfigure(encoding="utf-8", errors="replace")

# 项目根目录
ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

# 导入 ensure_upx 模块
do_ensure_upx: Callable[[], str | None] | None = None
HAS_UPX_SCRIPT = False
try:
    from scripts.ensure_upx import ensure_upx as do_ensure_upx

    HAS_UPX_SCRIPT = True
except ImportError:
    pass


def get_project_version():
    """获取项目版本号"""
    toml_file = os.path.join(ROOT_DIR, "pyproject.toml")
    with open(toml_file, encoding="utf-8") as f:
        content = f.read()
        import re

        match = re.search(r'version\s*=\s*["\']([^"\']+)["\']', content)
        if match:
            return match.group(1)
    return "unknown"


def clean_build_files():
    """清理构建文件"""
    print("清理构建文件...")

    dirs_to_remove = ["build", "dist", "spec"]

    for dir_name in dirs_to_remove:
        dir_path = os.path.join(ROOT_DIR, dir_name)
        if os.path.exists(dir_path):
            shutil.rmtree(dir_path)
            print(f"  已删除: {dir_name}/")

    print("✓ 清理完成")


def ensure_upx():
    """确保 UPX 压缩工具可用"""
    print("检查 UPX 压缩工具...")

    if not HAS_UPX_SCRIPT or do_ensure_upx is None:
        print("警告: UPX 安装脚本不存在,跳过")
        return True

    try:
        upx_path = do_ensure_upx()
        return upx_path is not None
    except Exception as e:
        print(f"警告: UPX 检查失败: {e}")
        print("将继续打包,但压缩可能被禁用")
        return True


def build_exe():
    """使用 PyInstaller 打包为单文件 exe"""
    print("开始打包...")

    spec_file = os.path.join(ROOT_DIR, "remark.spec")
    if not os.path.exists(spec_file):
        print(f"错误: 找不到 {spec_file}")
        return False

    # 检查 PyInstaller 是否安装
    try:
        import PyInstaller

        print(f"PyInstaller 版本: {PyInstaller.__version__}")
    except ImportError:
        print("错误: 未安装 PyInstaller")
        print("请运行: pip install pyinstaller")
        return False

    # 确保 UPX 可用
    if not ensure_upx():
        print("警告: UPX 不可用,exe 体积会更大")

    # 运行 PyInstaller
    try:
        subprocess.run(
            ["pyinstaller", spec_file, "--clean"],
            cwd=ROOT_DIR,
            check=True,
        )
        print("✓ 打包完成")
        print("\n输出文件: dist/windows-folder-remark.exe")
        return True
    except subprocess.CalledProcessError as e:
        print(f"✗ 打包失败: {e}")
        return False
    except FileNotFoundError:
        print("错误: 找不到 pyinstaller 命令")
        print("请运行: pip install pyinstaller")
        return False


def main():
    parser = argparse.ArgumentParser(description="本地打包工具")
    parser.add_argument("--clean", "-c", action="store_true", help="仅清理构建文件,不进行打包")

    args = parser.parse_args()

    if args.clean:
        clean_build_files()
        return

    # 打包前先清理
    clean_build_files()
    print()

    # 开始打包
    version = get_project_version()
    print(f"项目版本: {version}")
    print()

    if build_exe():
        print("\n" + "=" * 50)
        print("打包成功!")
        print(f"版本: {version}")
        print("位置: dist/windows-folder-remark.exe")
        print("=" * 50)
    else:
        sys.exit(1)


if __name__ == "__main__":
    main()


================================================
FILE: scripts/check_i18n.py
================================================
#!/usr/bin/env python
"""检查翻译文件完整性"""

import re
import sys


def check_po_file(path: str) -> bool:
    """检查单个 .po 文件是否有空翻译"""
    with open(path, "r", encoding="utf-8") as f:
        lines = f.readlines()

    i = 0
    found_issue = False
    while i < len(lines):
        line = lines[i]
        # 检查单行 msgid 后跟空 msgstr 的情况
        if re.match(r'^msgid ".+"$', line):
            if i + 1 < len(lines):
                next_line = lines[i + 1].strip()
                # 如果下一行是 msgstr "" 且不是多行字符串的开始
                if next_line == 'msgstr ""':
                    # 检查是否真的是单行(再下一行不是以引号开头)
                    if i + 2 >= len(lines) or not lines[i + 2].strip().startswith('"'):
                        print(f"ERROR: {path}:{i + 2}: Empty translation: {line.strip()}")
                        found_issue = True
        i += 1

    return found_issue


if __name__ == "__main__":
    all_ok = True
    for path in sys.argv[1:]:
        if check_po_file(path):
            all_ok = False

    if not all_ok:
        print("\nERROR: Empty translations found! Please add translations.", file=sys.stderr)
        sys.exit(1)

    print("Translation check PASSED")
    sys.exit(0)


================================================
FILE: scripts/ensure_upx.py
================================================
"""
UPX 下载脚本

自动从 GitHub releases 下载最新版本的 UPX 压缩工具。

使用方法:
    python scripts/ensure_upx.py
"""

import json
import os
import platform
import shutil
import subprocess
import sys
import tempfile
import urllib.error
import urllib.request
import zipfile

# 设置 UTF-8 输出编码(Windows 兼容)
if sys.platform == "win32":
    if hasattr(sys.stdout, "reconfigure"):
        sys.stdout.reconfigure(encoding="utf-8", errors="replace")
    if hasattr(sys.stderr, "reconfigure"):
        sys.stderr.reconfigure(encoding="utf-8", errors="replace")

# 项目根目录
ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

# UPX 安装目录
UPX_DIR = os.path.join(ROOT_DIR, "tools", "upx")

# GitHub API 配
GITHUB_API_RELEASES = "https://api.github.com/repos/upx/upx/releases/latest"


def get_proxies() -> dict[str, str] | None:
    """从环境变量获取代理配置"""
    proxies = []
    http_proxy = os.environ.get("HTTP_PROXY") or os.environ.get("http_proxy")
    https_proxy = os.environ.get("HTTPS_PROXY") or os.environ.get("https_proxy")

    if http_proxy:
        proxies.append(("http", http_proxy))
    if https_proxy:
        proxies.append(("https", https_proxy))

    return dict(proxies) if proxies else None


def create_opener():
    """创建带代理的 URL opener"""
    proxies = get_proxies()
    if proxies:
        proxy_handler = urllib.request.ProxyHandler(proxies)
        return urllib.request.build_opener(proxy_handler)
    return urllib.request.build_opener()


def get_system_info():
    """获取系统信息"""
    system = platform.system().lower()
    machine = platform.machine().lower()

    if system == "windows":
        if machine in ("amd64", "x86_64"):
            return "win_amd64"
        elif machine in ("i386", "i686", "x86"):
            return "win32"
    return None


def get_latest_upx_version() -> tuple[str, str, str] | None:
    """从 GitHub API 获取最新 UPX 版本和下载信息

    Returns:
        (version, download_url, asset_name) 或 None
    """
    try:
        proxies = get_proxies()
        if proxies:
            print(f"  使用代理: {proxies.get('https', proxies.get('http'))}")

        request = urllib.request.Request(
            GITHUB_API_RELEASES,
            headers={
                "Accept": "application/vnd.github.v3+json",
                "User-Agent": "windows-folder-remark",
            },
        )
        opener = create_opener()
        with opener.open(request, timeout=10) as response:
            if response.status != 200:
                return None
            data = json.load(response)

        version = data["tag_name"].lstrip("v")

        # 获取目标平台
        target_platform = get_system_info()
        if not target_platform:
            return None

        # 查找匹配的 asset
        platform_keywords = {
            "win_amd64": ["win", "64"],
            "win32": ["win", "32"],
        }

        keywords = platform_keywords.get(target_platform, [])

        for asset in data.get("assets", []):
            name = asset["name"].lower()
            # 检查是否匹配平台关键词
            if all(kw in name for kw in keywords):
                return version, asset["browser_download_url"], asset["name"]

        print(f"错误: 找不到适合 {target_platform} 的 UPX 包")
        print("可用的包:")
        for asset in data.get("assets", []):
            print(f"  - {asset['name']}")
        return None

    except (urllib.error.HTTPError, urllib.error.URLError, json.JSONDecodeError) as e:
        print(f"警告: 无法获取 UPX 版本信息: {e}")
        return None


def find_upx_executable() -> str | None:
    """查找已存在的 UPX 可执行文件"""
    # 优先使用项目本地的 UPX
    local_upx = os.path.join(UPX_DIR, "upx.exe")
    if os.path.exists(local_upx):
        return local_upx

    # 检查系统 PATH 中是否有 UPX
    try:
        result = subprocess.run(
            ["where", "upx"],
            capture_output=True,
            text=True,
            check=False,
        )
        if result.returncode == 0:
            return result.stdout.strip().split("\n")[0]
    except FileNotFoundError:
        pass

    return None


def download_upx(version: str, download_url: str, asset_name: str) -> str | None:
    """下载 UPX 并解压到指定目录

    Args:
        version: UPX 版本号
        download_url: 实际的下载 URL(从 API 获取)
        asset_name: 资源文件名

    Returns:
        UPX 可执行文件路径,失败返回 None
    """
    print(f"正在下载 UPX {version}...")
    print(f"  文件: {asset_name}")
    print(f"  URL: {download_url}")

    # 创建临时目录
    with tempfile.TemporaryDirectory() as temp_dir:
        zip_path = os.path.join(temp_dir, asset_name)

        # 下载文件
        try:
            request = urllib.request.Request(
                download_url,
                headers={"User-Agent": "windows-folder-remark"},
            )
            opener = create_opener()

            with opener.open(request, timeout=60) as response:
                total_size = int(response.headers.get("content-length", 0))
                downloaded = 0
                chunk_size = 8192

                with open(zip_path, "wb") as f:
                    while True:
                        chunk = response.read(chunk_size)
                        if not chunk:
                            break
                        f.write(chunk)
                        downloaded += len(chunk)

                        # 显示进度
                        if total_size > 0:
                            progress = int(50 * downloaded / total_size)
                            downloaded_mb = downloaded / 1024 / 1024
                            print(
                                f"\r  进度: [{'#' * progress}{'.' * (50 - progress)}] {downloaded_mb:.1f}MB",
                                end="",
                            )

            print()  # 换行

        except (urllib.error.HTTPError, urllib.error.URLError) as e:
            print(f"\n错误: 下载失败: {e}")
            return None

        # 解压文件
        print("正在解压...")
        try:
            with zipfile.ZipFile(zip_path, "r") as zip_ref:
                zip_ref.extractall(temp_dir)
        except zipfile.BadZipFile as e:
            print(f"错误: 解压失败: {e}")
            return None

        # 查找 upx.exe
        for root, _, files in os.walk(temp_dir):
            for file in files:
                if file == "upx.exe":
                    src = os.path.join(root, file)
                    # 创建目标目录
                    os.makedirs(UPX_DIR, exist_ok=True)
                    dst = os.path.join(UPX_DIR, file)
                    shutil.copy2(src, dst)
                    return dst

        print("错误: 在下载的包中找不到 upx.exe")
        return None


def verify_upx(upx_path: str) -> bool:
    """验证 UPX 是否可用"""
    try:
        result = subprocess.run(
            [upx_path, "--version"],
            capture_output=True,
            text=True,
            check=True,
        )
        # 检查输出是否包含 UPX
        if "upx" in result.stdout.lower():
            version_line = result.stdout.split("\n")[0]
            print(f"  版本: {version_line.strip()}")
            return True
    except (subprocess.CalledProcessError, FileNotFoundError):
        pass
    return False


def ensure_upx() -> str | None:
    """确保 UPX 可用,返回 UPX 可执行文件路径"""
    # 检查平台
    target_platform = get_system_info()
    if not target_platform:
        print("错误: 不支持的平台")
        print("  UPX 自动下载仅支持 Windows")
        return None

    print("检查 UPX 压缩工具...")

    # 查找已存在的 UPX
    upx_path = find_upx_executable()
    if upx_path:
        print(f"  找到 UPX: {upx_path}")
        if verify_upx(upx_path):
            return upx_path
        print("  警告: 现有 UPX 不可用")

    # 获取最新版本和下载信息
    release_info = get_latest_upx_version()
    if not release_info:
        print("错误: 无法获取 UPX 最新版本")
        return None

    version, download_url, asset_name = release_info
    print(f"  最新版本: {version}")

    # 下载 UPX
    upx_path = download_upx(version, download_url, asset_name)
    if not upx_path:
        return None

    # 验证下载的 UPX
    if verify_upx(upx_path):
        print(f"✓ UPX 已安装到: {upx_path}")
        return upx_path

    print("错误: UPX 验证失败")
    return None


def main():
    """主入口"""
    upx_path = ensure_upx()
    if upx_path:
        print(f"\nUPX 路径: {upx_path}")
        return 0
    return 1


if __name__ == "__main__":
    sys.exit(main())


================================================
FILE: scripts/release.py
================================================
"""
版本发布脚本

使用方法:
    # 查看当前版本
    python scripts/release.py

    # 递增补丁版本 (2.0.0 -> 2.0.1)
    python scripts/release.py patch

    # 递增次版本 (2.0.0 -> 2.1.0)
    python scripts/release.py minor

    # 递增主版本 (2.0.0 -> 3.0.0)
    python scripts/release.py major

    # 设置特定版本
    python scripts/release.py 2.1.0

    # 创建并推送 release tag
    python scripts/release.py patch --push
"""

import argparse
import os
import re
import subprocess
import sys

# 项目根目录
ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))


def get_current_version():
    """获取当前版本号(从 pyproject.toml)"""
    toml_file = os.path.join(ROOT_DIR, "pyproject.toml")
    with open(toml_file, encoding="utf-8") as f:
        content = f.read()
        match = re.search(r'version\s*=\s*["\']([^"\']+)["\']', content)
        if match:
            return match.group(1)
    raise ValueError("无法在 pyproject.toml 中找到版本号")


def update_version(new_version):
    """更新 pyproject.toml 中的版本号(仅 [project] 部分)"""
    toml_file = os.path.join(ROOT_DIR, "pyproject.toml")
    with open(toml_file, encoding="utf-8") as f:
        lines = f.readlines()

    # 只更新 [project] 部分的 version = 行
    in_project_section = False
    for i, line in enumerate(lines):
        if line.strip() == "[project]":
            in_project_section = True
        elif line.startswith("[") and not line.startswith("[["):
            in_project_section = False
        elif in_project_section and line.strip().startswith("version"):
            lines[i] = re.sub(
                r'(version\s*=\s*["\'])([^"\']+)(["\'])', rf"\g<1>{new_version}\g<3>", line
            )
            break

    # 写入时使用 LF 换行符,避免 mixed-line-ending hook 修改文件
    with open(toml_file, "w", encoding="utf-8", newline="\n") as f:
        f.writelines(lines)
    return new_version


def bump_version(current, part="patch"):
    """递增版本号"""
    major, minor, patch = map(int, current.split("."))

    if part == "major":
        major += 1
        minor = 0
        patch = 0
    elif part == "minor":
        minor += 1
        patch = 0
    else:  # patch
        patch += 1

    return f"{major}.{minor}.{patch}"


def create_tag(version, override=False):
    """创建 git tag"""
    tag_name = f"v{version}"
    # 如果使用 override 且 tag 已存在,先删除
    if override:
        try:
            subprocess.run(["git", "tag", "-d", tag_name], check=True, capture_output=True)
            print(f"已删除本地 tag: {tag_name}")
        except subprocess.CalledProcessError:
            pass  # tag 不存在,忽略
    subprocess.run(["git", "tag", "-a", tag_name, "-m", f"Release {tag_name}"], check=True)
    print(f"已创建 tag: {tag_name}")
    return tag_name


def push_tag(tag_name, force=False):
    """推送 tag 到远程仓库"""
    args = ["git", "push", "origin"]
    if force:
        args.append("--force")
    args.append(tag_name)
    subprocess.run(args, check=True)
    print(f"已推送 tag: {tag_name}")


def commit_version_changes():
    """提交版本变更"""
    current_version = get_current_version()
    subprocess.run(["git", "add", "pyproject.toml"], check=True)
    subprocess.run(["git", "commit", "-m", f"bump: version to {current_version}"], check=True)
    print(f"已提交版本变更: {current_version}")


def check_branch():
    """检查当前分支是否为主分支"""
    result = subprocess.run(
        ["git", "branch", "--show-current"],
        capture_output=True,
        text=True,
        check=True,
    )
    current_branch = result.stdout.strip()
    return current_branch


def check_working_directory_clean():
    """检查工作目录是否有未提交的改动"""
    result = subprocess.run(
        ["git", "status", "--porcelain"],
        capture_output=True,
        text=True,
        check=True,
    )
    return result.stdout.strip() == ""


def check_remote_sync():
    """检查本地是否与远程同步"""
    result = subprocess.run(
        ["git", "status", "-sb"],
        capture_output=True,
        text=True,
        check=True,
    )
    status_line = result.stdout.split("\n")[0]
    # 检查是否包含 "behind" 字样
    return "behind" not in status_line.lower()


def get_latest_tag() -> str | None:
    """获取最新的 git tag 版本号"""
    try:
        result = subprocess.run(
            ["git", "describe", "--tags", "--abbrev=0"],
            capture_output=True,
            text=True,
            check=True,
        )
        tag = result.stdout.strip()
        # 移除 v 前缀
        return tag.lstrip("v") if tag else None
    except subprocess.CalledProcessError:
        # 没有任何 tag
        return None


def validate_version_increment(current: str, new: str) -> bool:
    """验证新版本号是否大于当前版本号"""
    curr_major, curr_minor, curr_patch = map(int, current.split("."))
    new_major, new_minor, new_patch = map(int, new.split("."))

    return (
        new_major > curr_major
        or (new_major == curr_major and new_minor > curr_minor)
        or (new_major == curr_major and new_minor == curr_minor and new_patch > curr_patch)
    )


def is_version_releasable(version: str) -> bool:
    """检查版本是否可发布(大于已有最新 tag)"""
    latest_tag = get_latest_tag()
    if latest_tag is None:
        return True  # 没有任何 tag,可以发布

    return validate_version_increment(latest_tag, version)


def main():
    parser = argparse.ArgumentParser(description="版本发布管理工具")
    parser.add_argument(
        "version", nargs="?", help="新版本号 (如: 2.1.0) 或递增类型: patch/minor/major"
    )
    parser.add_argument(
        "--push", "-p", action="store_true", help="创建并推送 tag 到远程仓库(触发 GitHub Actions)"
    )
    parser.add_argument("--commit", "-c", action="store_true", help="提交版本变更到 git")
    parser.add_argument(
        "--dry-run", "-n", action="store_true", help="只显示将要执行的操作,不实际执行"
    )
    parser.add_argument(
        "--skip-branch-check",
        action="store_true",
        help="跳过分支检查(不推荐)",
    )
    parser.add_argument(
        "--override",
        action="store_true",
        help="强制使用当前版本发布,跳过版本检查(用于重新发布)",
    )

    args = parser.parse_args()

    # --push 自动包含 --commit(确保 tag 指向包含版本变更的提交)
    if args.push and not args.commit:
        args.commit = True

    current = get_current_version()
    print(f"当前版本: {current}")

    # 确定目标版本
    if not args.version:
        # 未指定版本,使用当前版本
        new_version = current
        version_changed = False
        if is_version_releasable(current):
            print(f"发布当前版本: {new_version}")
        elif args.override:
            print(f"强制重新发布当前版本: {new_version}")
        else:
            print(f"当前版本 {current} 已发布或不是最新版本")
            print("请指定新版本号: patch, minor, major 或具体版本号")
            print("或使用 --override 强制重新发布当前版本")
            return
    elif args.version in ("patch", "minor", "major"):
        new_version = bump_version(current, args.version)
        version_changed = True
    else:
        # 验证版本号格式
        if not re.match(r"^\d+\.\d+\.\d+$", args.version):
            print("错误: 版本号格式应为 x.y.z")
            sys.exit(1)
        new_version = args.version
        version_changed = new_version != current

    print(f"目标版本: {new_version}")

    # 检查版本是否可发布(大于已有最新 tag),除非使用 --override
    if not args.override and not is_version_releasable(new_version):
        latest_tag = get_latest_tag()
        print(f"错误: 版本 {new_version} 不大于已有最新 tag v{latest_tag}")
        print("提示: 使用 --override 强制发布当前版本(会覆盖已有 tag)")
        sys.exit(1)

    # 分支检查
    if not args.skip_branch_check:
        current_branch = check_branch()
        if current_branch not in ("main", "master"):
            print(f"警告: 当前分支 '{current_branch}' 不是主分支")
            print("建议在 main 或 master 分支进行发布")
            response = input("是否继续? (yes/no): ")
            if response.lower() not in ("yes", "y"):
                print("已取消")
                sys.exit(1)

    # 工作目录状态检查
    if not check_working_directory_clean():
        print("错误: 工作目录有未提交的改动")
        print("请先提交或暂存所有改动后再进行发布")
        sys.exit(1)

    # 远程同步检查
    if not check_remote_sync():
        print("警告: 本地分支落后于远程分支")
        print("建议先执行 'git pull' 同步最新代码")
        response = input("是否继续? (yes/no): ")
        if response.lower() not in ("yes", "y"):
            print("已取消")
            sys.exit(1)

    if args.dry_run:
        print("\n[DRY RUN] 将执行以下操作:")
        if version_changed:
            print(f"  1. 更新版本号: {current} -> {new_version}")
        else:
            print(f"  1. 使用当前版本: {new_version}")
        if args.commit and version_changed:
            print("  2. 提交版本变更")
        if args.push:
            print(
                f"  {'2' if version_changed or not args.commit else '1'}. 创建并推送 tag v{new_version}"
            )
        return

    # 只有版本变更时才更新 pyproject.toml
    if version_changed:
        update_version(new_version)
        print(f"已更新版本号到: {new_version}")
    else:
        print(f"使用当前版本: {new_version}")

    # 提交变更(仅当版本有变更时)
    if args.commit and version_changed:
        commit_version_changes()

    # 创建并推送 tag
    if args.push:
        tag_name = create_tag(new_version, override=args.override)
        push_tag(tag_name, force=args.override)
        print(f"\n✓ Release v{new_version} 已准备就绪!")
        print("  GitHub Actions 将自动构建并发布")
    else:
        print("\n提示: 使用 --push 参数创建并推送 tag 以触发 release")


if __name__ == "__main__":
    main()


================================================
FILE: tests/__init__.py
================================================
"""
测试模块
"""


================================================
FILE: tests/conftest.py
================================================
"""共享测试配置"""

import os
import sys

from pathlib import Path

import pytest

# 项目根目录添加到路径
sys.path.insert(0, str(Path(__file__).parent.parent))

# 在导入任何模块之前设置语言环境变量,确保翻译使用中文
os.environ["LANG"] = "zh"

# 立即设置翻译器为中文,确保在导入任何模块之前生效
from remark.i18n import set_language
set_language("zh")


def pytest_configure(config):
    """pytest 配置钩子 - 定义 markers"""
    import sys
    config.addinivalue_line("markers", "unit: 单元测试(使用 mock)")
    config.addinivalue_line("markers", "integration: 集成测试(真实文件系统)")
    config.addinivalue_line("markers", "windows: 仅在 Windows 上运行")
    config.addinivalue_line("markers", "slow: 慢速测试")

    # 强制使用 UTF-8 编码输出,支持 emoji 等特殊字符
    import io

    if sys.stdout.encoding.lower() not in ("utf-8", "utf-16", "utf-16-le", "utf-16-be"):
        sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
    if sys.stderr.encoding.lower() not in ("utf-8", "utf-16", "utf-16-le", "utf-16-be"):
        sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8", errors="replace")

    # 强制重新初始化翻译器为中文
    from remark.i18n import set_language
    set_language("zh")


def pytest_collection_modifyitems(config, items):
    """自动跳过非 Windows 平台上的 Windows 测试"""
    if sys.platform != "win32":
        skip_windows = pytest.mark.skip(reason="Windows only test")
        for item in items:
            if "windows" in item.keywords:
                item.add_marker(skip_windows)


@pytest.fixture(autouse=True, scope="session")
def set_chinese_language():
    """在整个测试会话开始时设置语言为中文"""
    from remark.i18n import set_language
    set_language("zh")
    yield


================================================
FILE: tests/integration/__init__.py
================================================
"""集成测试模块"""


================================================
FILE: tests/integration/conftest.py
================================================
"""集成测试专用 fixtures"""

import codecs

import pytest


@pytest.fixture
def utf16_encoded_file(tmp_path):
    """创建 UTF-16 编码的测试文件"""
    file_path = tmp_path / "utf16_test.ini"
    content = "[.ShellClassInfo]\r\nInfoTip=UTF-16 测试\r\n"

    with codecs.open(str(file_path), "w", encoding="utf-16") as f:
        f.write(content)

    return str(file_path)


@pytest.fixture
def utf8_encoded_file(tmp_path):
    """创建 UTF-8 编码的测试文件"""
    file_path = tmp_path / "utf8_test.ini"
    content = "[.ShellClassInfo]\r\nInfoTip=UTF-8 测试\r\n"

    with open(str(file_path), "w", encoding="utf-8") as f:
        f.write(content)

    return str(file_path)


================================================
FILE: tests/integration/test_encoding_handling.py
================================================
"""编码处理集成测试"""

import codecs
import os

import pytest

from remark.storage.desktop_ini import DesktopIniHandler


@pytest.mark.integration
class TestEncodingHandling:
    """编码处理集成测试"""

    def test_write_and_read_utf16(self, tmp_path):
        """测试 UTF-16 编码读写"""
        folder = str(tmp_path / "test")
        os.makedirs(folder)

        # 写入
        result = DesktopIniHandler.write_info_tip(folder, "UTF-16 测试")
        assert result is True

        # 验证文件存在
        ini_path = os.path.join(folder, "desktop.ini")
        assert os.path.exists(ini_path)

        # 读取
        read_result = DesktopIniHandler.read_info_tip(folder)
        assert read_result == "UTF-16 测试"

    def test_read_gbk_encoded_file(self, tmp_path):
        """测试读取 GBK 编码的文件(降级兼容)"""
        folder = str(tmp_path / "gbk_test")
        os.makedirs(folder)
        ini_path = os.path.join(folder, "desktop.ini")

        # 使用 codecs.open 确保行尾符正确处理
        with codecs.open(ini_path, "w", encoding="gbk") as f:
            f.write("[.ShellClassInfo]\r\nInfoTip=GBK Test\r\n")

        result = DesktopIniHandler.read_info_tip(folder)
        assert result == "GBK Test"

    def test_read_utf8_encoded_file(self, tmp_path):
        """测试读取 UTF-8 编码的文件"""
        folder = str(tmp_path / "utf8_test")
        os.makedirs(folder)
        ini_path = os.path.join(folder, "desktop.ini")

        # 使用 codecs.open 确保 UTF-8 编码正确
        with codecs.open(ini_path, "w", encoding="utf-8") as f:
            f.write("[.ShellClassInfo]\r\nInfoTip=UTF-8 Test\r\n")

        result = DesktopIniHandler.read_info_tip(folder)
        assert result == "UTF-8 Test"

    def test_encoding_detection_utf16(self, utf16_encoded_file):
        """测试编码检测 - UTF-16"""
        encoding, is_utf16 = DesktopIniHandler.detect_encoding(utf16_encoded_file)
        assert is_utf16 is True
        assert "utf-16" in encoding

    def test_encoding_detection_utf8(self, utf8_encoded_file):
        """测试编码检测 - UTF-8"""
        encoding, is_utf16 = DesktopIniHandler.detect_encoding(utf8_encoded_file)
        assert is_utf16 is False
        assert encoding == "utf-8"

    @pytest.mark.parametrize(
        "comment",
        [
            "简体中文",
            "繁體中文",
            "日本語",
            "한국어",
            "Emoji 🔥",
            "Mixed 中英文 Mixed",
            "Special chars: !@#$%^&*()",
        ],
    )
    def test_write_various_characters(self, tmp_path, comment):
        """测试写入各种字符"""
        folder = str(tmp_path / "chinese")
        os.makedirs(folder)

        result = DesktopIniHandler.write_info_tip(folder, comment)
        assert result is True

        read_result = DesktopIniHandler.read_info_tip(folder)
        assert read_result == comment

    def test_write_long_comment(self, tmp_path):
        """测试写入长备注"""
        folder = str(tmp_path / "long")
        os.makedirs(folder)

        # 260 字符(MAX_COMMENT_LENGTH)
        long_comment = "A" * 260
        result = DesktopIniHandler.write_info_tip(folder, long_comment)
        assert result is True

        read_result = DesktopIniHandler.read_info_tip(folder)
        assert read_result == long_comment

    def test_update_preserves_encoding(self, tmp_path):
        """测试更新备注保持编码"""
        folder = str(tmp_path / "update")
        os.makedirs(folder)

        # 第一次写入
        DesktopIniHandler.write_info_tip(folder, "初始备注")

        # 获取文件编码
        ini_path = os.path.join(folder, "desktop.ini")
        encoding1, is_utf16_1 = DesktopIniHandler.detect_encoding(ini_path)
        assert is_utf16_1 is True

        # 更新备注
        DesktopIniHandler.write_info_tip(folder, "更新备注")

        # 验证编码仍然是 UTF-16
        encoding2, is_utf16_2 = DesktopIniHandler.detect_encoding(ini_path)
        assert is_utf16_2 is True
        assert encoding1.split("-")[0] == encoding2.split("-")[0]

    def test_write_new_line_endings(self, tmp_path):
        """测试写入使用 Windows 行尾符"""
        folder = str(tmp_path / "line_ending")
        os.makedirs(folder)

        DesktopIniHandler.write_info_tip(folder, "行尾测试")

        ini_path = os.path.join(folder, "desktop.ini")

        # UTF-16 LE 编码中,\r\n 被存储为 \x00\r\x00\n(每个字符前有 null byte)
        # 或者可以简单地读取文本内容验证行尾符
        with codecs.open(ini_path, "r", encoding="utf-16") as f:
            text_content = f.read()

        # 验证文本内容包含 CRLF
        assert "\r\n" in text_content

    def test_read_without_bom(self, tmp_path):
        """测试读取没有 BOM 的文件(降级到 utf-8)"""
        folder = str(tmp_path / "no_bom")
        os.makedirs(folder)

        ini_path = os.path.join(folder, "desktop.ini")
        # 使用 codecs.open 确保编码正确
        with codecs.open(ini_path, "w", encoding="utf-8") as f:
            f.write("[.ShellClassInfo]\r\nInfoTip=No BOM Test\r\n")

        result = DesktopIniHandler.read_info_tip(folder)
        assert result == "No BOM Test"

    def test_empty_folder(self, tmp_path):
        """测试空文件夹"""
        folder = str(tmp_path / "empty")
        os.makedirs(folder)

        result = DesktopIniHandler.read_info_tip(folder)
        assert result is None

    def test_corrupted_ini_file(self, tmp_path):
        """测试损坏的 ini 文件"""
        folder = str(tmp_path / "corrupted")
        os.makedirs(folder)

        ini_path = os.path.join(folder, "desktop.ini")
        with open(ini_path, "wb") as f:
            f.write(b"\x00\x01\x02\x03\x04\x05")  # 二进制垃圾数据

        # 应该返回 None 而不是崩溃
        result = DesktopIniHandler.read_info_tip(folder)
        # 可能成功解码(某些编码会接受)或返回 None
        assert result is None or isinstance(result, str)


================================================
FILE: tests/unit/__init__.py
================================================
"""单元测试模块"""


================================================
FILE: tests/unit/test_cli_commands.py
================================================
"""CLI 命令单元测试"""

import os

import pytest

from remark.cli.commands import CLI, get_version


@pytest.mark.unit
class TestCLI:
    """CLI 命令测试"""

    @pytest.fixture(autouse=True)
    def disable_background_update_check(self, monkeypatch):
        """禁用后台更新检查,避免 pyfakefs 隔离被后台线程破坏"""
        monkeypatch.setattr(
            "remark.cli.commands.CLI._start_update_checker",
            lambda self: None,
        )

    def test_init(self):
        """测试 CLI 初始化"""
        cli = CLI()
        assert cli.handler is not None

    def test_validate_folder_not_exists(self, capsys):
        """测试验证不存在的路径"""
        cli = CLI()
        result = cli._validate_folder("/invalid/path")
        assert result is False
        captured = capsys.readouterr()
        assert "路径不存在" in captured.out

    @pytest.mark.skipif(os.name != "nt", reason="Windows only")
    def test_validate_folder_not_dir(self, fs, capsys):
        """测试验证非文件夹路径"""
        fs.create_file("/file.txt")
        cli = CLI()
        result = cli._validate_folder("/file.txt")
        assert result is False
        captured = capsys.readouterr()
        assert "不是文件夹" in captured.out

    @pytest.mark.skipif(os.name != "nt", reason="Windows only")
    def test_validate_folder_success(self, fs):
        """测试验证有效文件夹"""
        fs.create_dir("/valid/folder")
        cli = CLI()
        result = cli._validate_folder("/valid/folder")
        assert result is True

    @pytest.mark.skipif(os.name != "nt", reason="Windows only")
    def test_add_comment_success(self, fs):
        """测试添加备注成功"""
        fs.create_dir("/test/folder")
        cli = CLI()
        result = cli.add_comment("/test/folder", "测试备注")
        assert result is True

    def test_add_comment_invalid_folder(self):
        """测试添加备注到无效路径"""
        cli = CLI()
        result = cli.add_comment("/invalid/path", "备注")
        assert result is False

    @pytest.mark.skipif(os.name != "nt", reason="Windows only")
    def test_delete_comment_success(self, fs):
        """测试删除备注成功"""
        fs.create_dir("/test/folder")
        cli = CLI()
        result = cli.delete_comment("/test/folder")
        assert result is True

    def test_delete_comment_invalid_folder(self):
        """测试删除备注失败(无效路径)"""
        cli = CLI()
        result = cli.delete_comment("/invalid/path")
        assert result is False

    @pytest.mark.skipif(os.name != "nt", reason="Windows only")
    def test_view_comment_with_content(self, fs, capsys):
        """测试查看有备注的文件夹"""
        fs.create_dir("/test/folder")
        from remark.core.folder_handler import FolderCommentHandler

        with pytest.MonkeyPatch().context() as m:
            m.setattr(FolderCommentHandler, "get_comment", lambda self, path: "测试备注")
            cli = CLI()
            cli.view_comment("/test/folder")
            captured = capsys.readouterr()
            assert "测试备注" in captured.out

    @pytest.mark.skipif(os.name != "nt", reason="Windows only")
    def test_view_comment_without_content(self, fs, capsys):
        """测试查看无备注的文件夹"""
        fs.create_dir("/test/folder")
        from remark.core.folder_handler import FolderCommentHandler

        with pytest.MonkeyPatch().context() as m:
            m.setattr(FolderCommentHandler, "get_comment", lambda self, path: None)
            cli = CLI()
            cli.view_comment("/test/folder")
            captured = capsys.readouterr()
            assert "没有备注" in captured.out

    @pytest.mark.skipif(os.name != "nt", reason="Windows only")
    def test_view_comment_with_encoding_issue_and_fix(self, fs, capsys):
        """测试查看有编码问题的文件夹并选择修复"""
        from remark.core.folder_handler import FolderCommentHandler
        from remark.storage.desktop_ini import DesktopIniHandler

        fs.create_dir("/test/folder")
        fs.create_file("/test/folder/desktop.ini", contents="test content")

        with pytest.MonkeyPatch().context() as m:
            m.setattr(DesktopIniHandler, "detect_encoding", lambda file_path: ("utf-8", False))
            m.setattr(DesktopIniHandler, "fix_encoding", lambda file_path, current_encoding: True)
            m.setattr(FolderCommentHandler, "get_comment", lambda self, path: "测试备注")
            m.setattr("builtins.input", lambda *args, **kwargs: "y")

            cli = CLI()
            cli.view_comment("/test/folder")
            captured = capsys.readouterr()
            assert "编码为 utf-8" in captured.out
            assert "已修复为 UTF-16 编码" in captured.out
            assert "测试备注" in captured.out

    @pytest.mark.skipif(os.name != "nt", reason="Windows only")
    def test_view_comment_with_encoding_issue_and_skip(self, fs, capsys):
        """测试查看有编码问题的文件夹但选择跳过修复"""
        from remark.core.folder_handler import FolderCommentHandler
        from remark.storage.desktop_ini import DesktopIniHandler

        fs.create_dir("/test/folder")
        fs.create_file("/test/folder/desktop.ini", contents="test content")

        with pytest.MonkeyPatch().context() as m:
            m.setattr(DesktopIniHandler, "detect_encoding", lambda file_path: ("gbk", False))
            m.setattr(FolderCommentHandler, "get_comment", lambda self, path: "测试备注")
            m.setattr("builtins.input", lambda *args, **kwargs: "n")

            cli = CLI()
            cli.view_comment("/test/folder")
            captured = capsys.readouterr()
            assert "编码为 gbk" in captured.out
            assert "跳过编码修复" in captured.out
            assert "测试备注" in captured.out

    @pytest.mark.skipif(os.name != "nt", reason="Windows only")
    def test_view_comment_with_correct_encoding(self, fs, capsys):
        """测试查看编码正确的文件夹"""
        from remark.core.folder_handler import FolderCommentHandler
        from remark.storage.desktop_ini import DesktopIniHandler

        fs.create_dir("/test/folder")
        fs.create_file("/test/folder/desktop.ini", contents="test content")

        with pytest.MonkeyPatch().context() as m:
            m.setattr(
                DesktopIniHandler,
                "detect_encoding",
                lambda file_path: ("utf-16-le", True),
            )
            m.setattr(FolderCommentHandler, "get_comment", lambda self, path: "测试备注")

            cli = CLI()
            cli.view_comment("/test/folder")
            captured = capsys.readouterr()
            # 不应该显示编码警告
            assert "编码" not in captured.out
            assert "测试备注" in captured.out

    @pytest.mark.skipif(os.name != "nt", reason="Windows only")
    def test_interactive_mode_valid_input(self, fs, monkeypatch):
        """测试交互模式有效输入"""
        fs.create_dir("/folder")

        input_sequence = ["/folder", "测试备注"]

        def mock_input(prompt):
            if input_sequence:
                return input_sequence.pop(0)
            raise KeyboardInterrupt()

        cli = CLI()

        # Mock input to control user input and exit after first iteration
        monkeypatch.setattr("builtins.input", mock_input)

        # Mock add_comment to verify it was called
        original_add_comment = cli.add_comment
        calls = []

        def mock_add_comment(path, comment):
            calls.append((path, comment))
            return original_add_comment(path, comment)

        monkeypatch.setattr(cli, "add_comment", mock_add_comment)

        cli.interactive_mode()

        # 验证 add_comment 被正确调用
        assert len(calls) == 1
        assert calls[0] == ("/folder", "测试备注")

    @pytest.mark.skipif(os.name != "nt", reason="Windows only")
    def test_interactive_mode_invalid_path_then_valid(self, fs, monkeypatch, capsys):
        """测试交互模式先输入无效路径再输入有效路径"""
        fs.create_dir("/valid_folder")

        input_sequence = ["/invalid", "/valid_folder", "备注内容"]

        def mock_input(prompt):
            if input_sequence:
                return input_sequence.pop(0)
            raise KeyboardInterrupt()

        cli = CLI()
        monkeypatch.setattr("builtins.input", mock_input)

        # Mock add_comment to verify it was called
        original_add_comment = cli.add_comment
        calls = []

        def mock_add_comment(path, comment):
            calls.append((path, comment))
            return original_add_comment(path, comment)

        monkeypatch.setattr(cli, "add_comment", mock_add_comment)

        cli.interactive_mode()

        # 验证无效路径被提示,最终有效路径被处理
        captured = capsys.readouterr()
        assert "路径不存在" in captured.out
        assert len(calls) == 1
        assert calls[0] == ("/valid_folder", "备注内容")

    @pytest.mark.skipif(os.name != "nt", reason="Windows only")
    def test_interactive_mode_empty_comment_retry(self, fs, monkeypatch, capsys):
        """测试交互模式空备注重试"""
        fs.create_dir("/folder")

        input_sequence = ["/folder", "", "有效备注"]

        def mock_input(prompt):
            if input_sequence:
                return input_sequence.pop(0)
            raise KeyboardInterrupt()

        cli = CLI()
        monkeypatch.setattr("builtins.input", mock_input)

        # Mock add_comment to verify it was called
        original_add_comment = cli.add_comment
        calls = []

        def mock_add_comment(path, comment):
            calls.append((path, comment))
            return original_add_comment(path, comment)

        monkeypatch.setattr(cli, "add_comment", mock_add_comment)

        cli.interactive_mode()

        # 验证空备注被提示重新输入,最终有效备注被处理
        captured = capsys.readouterr()
        assert "备注不要为空" in captured.out
        assert len(calls) == 1
        assert calls[0] == ("/folder", "有效备注")

    def test_show_help(self, capsys):
        """测试帮助信息"""
        cli = CLI()
        cli.show_help()
        captured = capsys.readouterr()
        assert "Windows 文件夹备注工具" in captured.out
        assert "使用方法" in captured.out
        assert "交互模式" in captured.out

    def test_run_with_help(self, capsys):
        """测试运行 --help 参数"""
        cli = CLI()
        cli.run(["--help"])
        captured = capsys.readouterr()
        assert "Windows 文件夹备注工具" in captured.out

    @pytest.mark.skipif(os.name != "nt", reason="Windows only")
    def test_run_with_delete(self, fs):
        """测试运行 --delete 参数"""
        fs.create_dir("/test/folder")
        cli = CLI()
        cli.run(["--delete", "/test/folder"])

    @pytest.mark.skipif(os.name != "nt", reason="Windows only")
    def test_run_with_view(self, fs):
        """测试运行 --view 参数"""
        fs.create_dir("/test/folder")
        cli = CLI()
        cli.run(["--view", "/test/folder"])

    @pytest.mark.skipif(os.name != "nt", reason="Windows only")
    def test_run_with_path_and_comment(self, fs, monkeypatch):
        """测试运行带路径和备注参数"""
        fs.create_dir("/folder")
        # Mock input to auto-confirm when path is detected
        monkeypatch.setattr("builtins.input", lambda *args, **kwargs: "y")
        cli = CLI()
        cli.run(["/folder", "备注"])

    def test_run_interactive_mode(self, monkeypatch):
        """测试运行进入交互模式"""
        # Mock interactive_mode to avoid actually entering interactive mode
        monkeypatch.setattr(CLI, "interactive_mode", lambda cli: None)
        cli = CLI()
        cli.run([])

    def test_run_platform_check_fail(self):
        """测试非 Windows 平台"""
        # check_platform 在 CLI.run 开始时被调用,如果返回 False 会 sys.exit(1)
        # 由于 sys.exit 会抛出 SystemExit,我们需要捕获它或 mock 它
        with pytest.MonkeyPatch().context() as m:
            m.setattr("remark.cli.commands.check_platform", lambda: False)
            cli = CLI()
            with pytest.raises(SystemExit) as exc_info:
                cli.run([])
            # sys.exit(1) 会被调用
            assert exc_info.value.code == 1


@pytest.mark.unit
class TestResolvePathAmbiguousArgs:
    """测试 _resolve_path_from_ambiguous_args 方法"""

    @pytest.fixture(autouse=True)
    def disable_background_update_check(self, monkeypatch):
        """禁用后台更新检查,避免 pyfakefs 隔离被后台线程破坏"""
        monkeypatch.setattr(
            "remark.cli.commands.CLI._start_update_checker",
            lambda self: None,
        )

    @pytest.mark.skipif(os.name != "nt", reason="Windows only")
    def test_resolve_path_valid_folder(self, fs):
        """测试解析有效文件夹路径"""
        fs.create_dir("/My Documents/folder")
        cli = CLI()
        path = cli._resolve_path_from_ambiguous_args(["My", "Documents", "folder"])
        assert path is not None
        assert "My Documents" in path

    @pytest.mark.skipif(os.name != "nt", reason="Windows only")
    def test_resolve_path_file_rejected(self, fs):
        """测试拒绝文件路径"""
        fs.create_file("/file.txt")
        cli = CLI()
        path = cli._resolve_path_from_ambiguous_args(["file"])
        assert path is None  # 文件应被拒绝

    @pytest.mark.skipif(os.name != "nt", reason="Windows only")
    def test_resolve_path_no_candidates(self, fs):
        """测试无候选路径时返回 None"""
        cli = CLI()
        path = cli._resolve_path_from_ambiguous_args(["nonexistent"])
        assert path is None

    @pytest.mark.skipif(os.name != "nt", reason="Windows only")
    def test_resolve_path_single_candidate(self, fs, monkeypatch, capsys):
        """测试单个候选路径直接返回"""
        fs.create_dir("/My Folder")
        cli = CLI()
        # Mock input 模拟用户确认
        monkeypatch.setattr("builtins.input", lambda *args, **kwargs: "y")

        path = cli._resolve_path_from_ambiguous_args(["My", "Folder"])
        assert path is not None
        assert "My Folder" in path

    @pytest.mark.skipif(os.name != "nt", reason="Windows only")
    def test_resolve_path_multiple_candidates_user_cancels(self, fs, monkeypatch):
        """测试多个候选路径用户取消"""
        fs.create_dir("/folder1")
        fs.create_dir("/folder2")
        cli = CLI()
        # Mock input 模拟用户选择取消
        monkeypatch.setattr("builtins.input", lambda *args, **kwargs: "0")

        path = cli._resolve_path_from_ambiguous_args(["folder"])
        assert path is None

    @pytest.mark.skipif(os.name != "nt", reason="Windows only")
    def test_resolve_path_multiple_candidates_user_selects(self, fs, monkeypatch):
        """测试多个候选路径用户选择"""
        # 在同一目录下创建多个同名的文件夹(不同路径)
        fs.create_dir("/parent1/folder")
        fs.create_dir("/parent2/folder")
        cli = CLI()
        # Mock input 模拟用户选择第一个选项
        inputs = iter(["1"])
        monkeypatch.setattr("builtins.input", lambda *args, **kwargs: next(inputs))

        path = cli._resolve_path_from_ambiguous_args(["parent1", "folder"])
        assert path is not None
        assert "parent1" in path


@pytest.mark.unit
class TestGetVersion:
    """get_version 函数测试"""

    def test_get_version_from_package(self):
        """测试从包获取版本"""
        with pytest.MonkeyPatch().context() as m:
            m.setattr("importlib.metadata.version", lambda *args, **kwargs: "2.0.0")
            version = get_version()
            assert version == "2.0.0"

    def test_get_version_fallback(self):
        """测试获取版本失败时返回 unknown"""
        with pytest.MonkeyPatch().context() as m:
            m.setattr(
                "importlib.metadata.version",
                lambda *args, **kwargs: (_ for _ in ()).throw(Exception()),
            )
            version = get_version()
            assert version == "unknown"


@pytest.mark.unit
class TestInteractiveCommands:
    """交互模式命令测试"""

    @pytest.fixture(autouse=True)
    def disable_background_update_check(self, monkeypatch):
        """禁用后台更新检查"""
        monkeypatch.setattr(
            "remark.cli.commands.CLI._start_update_checker",
            lambda self: None,
        )

    def test_interactive_commands_list_initialized(self):
        """测试交互命令列表正确初始化"""
        cli = CLI()
        # 进入交互模式会初始化命令列表
        assert hasattr(cli, "_interactive_commands_list")
        assert hasattr(cli, "_interactive_commands")
        expected_commands = ["#help", "#install", "#uninstall", "#update"]
        assert cli._interactive_commands_list == expected_commands

    def test_show_command_list(self, capsys):
        """测试显示命令列表"""
        cli = CLI()
        cli._show_command_list()
        captured = capsys.readouterr()
        # 检查中文或英文输出
        assert "Available commands" in captured.out or "可用命令" in captured.out
        assert "#help" in captured.out
        assert "#install" in captured.out
        assert "#uninstall" in captured.out
        assert "#update" in captured.out

    def test_interactive_help_shows_commands(self, capsys):
        """测试 #help 命令显示所有可用命令"""
        cli = CLI()
        cli._interactive_help()
        captured = capsys.readouterr()
        # 检查中文或英文输出
        assert "Interactive Commands" in captured.out or "交互命令" in captured.out
        assert "#help" in captured.out
        assert "#install" in captured.out
        assert "#uninstall" in captured.out
        assert "#update" in captured.out

    @pytest.mark.skipif(os.name != "nt", reason="Windows only")
    def test_interactive_mode_handles_hash_only(self, fs, monkeypatch, capsys):
        """测试交互模式处理单独的 # 输入"""
        fs.create_dir("/test/folder")
        cli = CLI()

        # Mock input: 先输入 # 然后输入 Ctrl+C 退出
        input_count = [0]

        def mock_input(prompt):
            input_count[0] += 1
            if input_count[0] == 1:
                return "#"
            else:
                raise KeyboardInterrupt()

        monkeypatch.setattr("builtins.input", mock_input)

        cli.interactive_mode()

        captured = capsys.readouterr()
        # 应该显示可用命令列表(中文或英文)
        assert "Available commands" in captured.out or "可用命令" in captured.out


================================================
FILE: tests/unit/test_desktop_ini.py
================================================
"""desktop.ini 读写单元测试"""

import os
from unittest.mock import MagicMock, patch

import pytest

from remark.storage.desktop_ini import (
    DESKTOP_INI_ENCODING,
    LINE_ENDING,
    DesktopIniHandler,
    EncodingConversionCanceled,
)


@pytest.mark.unit
class TestDesktopIniHandler:
    """desktop.ini 处理器测试"""

    def t
Download .txt
gitextract__x7cup28/

├── .gitattributes
├── .github/
│   └── workflows/
│       ├── auto-tag.yml
│       ├── deploy-docs.yml
│       ├── release.yml
│       └── test.yml
├── .gitignore
├── .pre-commit-config.yaml
├── .python-version
├── CHANGELOG.md
├── LICENSE
├── README.en.md
├── README.md
├── babel.cfg
├── desktop.ini
├── docs/
│   ├── .vitepress/
│   │   └── config.mjs
│   ├── en/
│   │   ├── guide/
│   │   │   ├── api.md
│   │   │   ├── getting-started.md
│   │   │   └── usage.md
│   │   └── index.md
│   ├── guide/
│   │   ├── api.md
│   │   ├── getting-started.md
│   │   └── usage.md
│   ├── index.md
│   └── robots.txt
├── locale/
│   └── zh/
│       └── LC_MESSAGES/
│           ├── messages.mo
│           └── messages.po
├── messages.pot
├── package.json
├── pyproject.toml
├── remark/
│   ├── __init__.py
│   ├── cli/
│   │   ├── __init__.py
│   │   ├── __main__.py
│   │   └── commands.py
│   ├── core/
│   │   ├── __init__.py
│   │   ├── base.py
│   │   └── folder_handler.py
│   ├── gui/
│   │   ├── __init__.py
│   │   └── remark_dialog.py
│   ├── i18n.py
│   ├── storage/
│   │   ├── __init__.py
│   │   └── desktop_ini.py
│   └── utils/
│       ├── __init__.py
│       ├── constants.py
│       ├── encoding.py
│       ├── path_resolver.py
│       ├── platform.py
│       ├── registry.py
│       └── updater.py
├── remark.py
├── remark.spec
├── scripts/
│   ├── __init__.py
│   ├── analyze_exe_size.py
│   ├── build.py
│   ├── check_i18n.py
│   ├── ensure_upx.py
│   └── release.py
└── tests/
    ├── __init__.py
    ├── conftest.py
    ├── integration/
    │   ├── __init__.py
    │   ├── conftest.py
    │   └── test_encoding_handling.py
    ├── unit/
    │   ├── __init__.py
    │   ├── test_cli_commands.py
    │   ├── test_desktop_ini.py
    │   ├── test_folder_handler.py
    │   ├── test_i18n.py
    │   ├── test_path_resolver.py
    │   ├── test_platform.py
    │   ├── test_registry.py
    │   ├── test_release.py
    │   └── test_updater.py
    └── windows/
        ├── __init__.py
        ├── test_context_menu.py
        └── test_full_workflow.py
Download .txt
SYMBOL INDEX (342 symbols across 30 files)

FILE: docs/.vitepress/config.mjs
  method transformItems (line 29) | transformItems(items) {

FILE: remark/cli/commands.py
  function get_version (line 29) | def get_version():
  class CLI (line 39) | class CLI:
    method __init__ (line 42) | def __init__(self):
    method _validate_folder (line 60) | def _validate_folder(self, path: str) -> bool:
    method _start_update_checker (line 70) | def _start_update_checker(self):
    method _run_update_check (line 75) | def _run_update_check(self):
    method check_update_now (line 82) | def check_update_now(self) -> bool:
    method _wait_for_update_check (line 105) | def _wait_for_update_check(self, timeout: float = 2.0) -> None:
    method _prompt_update (line 113) | def _prompt_update(self) -> None:
    method _perform_update (line 129) | def _perform_update(self, update: dict) -> None:
    method add_comment (line 168) | def add_comment(self, path, comment):
    method delete_comment (line 174) | def delete_comment(self, path):
    method install_menu (line 180) | def install_menu(self) -> bool:
    method uninstall_menu (line 193) | def uninstall_menu(self) -> bool:
    method gui_mode (line 202) | def gui_mode(self, folder_path: str) -> bool:
    method view_comment (line 214) | def view_comment(self, path: str) -> None:
    method interactive_mode (line 253) | def interactive_mode(self) -> None:
    method _show_command_list (line 299) | def _show_command_list(self) -> None:
    method _interactive_help (line 305) | def _interactive_help(self) -> None:
    method show_help (line 315) | def show_help(self) -> None:
    method _select_from_multiple_candidates (line 341) | def _select_from_multiple_candidates(
    method _handle_ambiguous_path (line 382) | def _handle_ambiguous_path(self, args_list: list[str]) -> tuple[str | ...
    method _resolve_path_from_ambiguous_args (line 426) | def _resolve_path_from_ambiguous_args(self, args_list: list[str]) -> s...
    method run (line 456) | def run(self, argv=None) -> None:
  function main (line 521) | def main() -> None:

FILE: remark/core/base.py
  class CommentHandler (line 8) | class CommentHandler(ABC):
    method set_comment (line 12) | def set_comment(self, path, comment):
    method get_comment (line 17) | def get_comment(self, path):
    method delete_comment (line 22) | def delete_comment(self, path):
    method supports (line 27) | def supports(self, path):

FILE: remark/core/folder_handler.py
  class FolderCommentHandler (line 18) | class FolderCommentHandler(CommentHandler):
    method set_comment (line 21) | def set_comment(self, folder_path: str, comment: str) -> bool:
    method _set_comment_desktop_ini (line 38) | def _set_comment_desktop_ini(folder_path: str, comment: str) -> bool:
    method get_comment (line 76) | def get_comment(self, folder_path: str) -> str | None:
    method delete_comment (line 80) | def delete_comment(self, folder_path: str) -> bool:
    method supports (line 108) | def supports(self, path: str) -> bool:

FILE: remark/gui/remark_dialog.py
  function show_remark_dialog (line 13) | def show_remark_dialog(folder_path: str) -> str | None:

FILE: remark/i18n.py
  function _get_locale_dir (line 24) | def _get_locale_dir() -> Path:
  function _get_windows_locale (line 47) | def _get_windows_locale() -> str | None:
  function get_system_language (line 79) | def get_system_language() -> str:
  function init_translation (line 137) | def init_translation(language: str | None = None) -> gettext.GNUTranslat...
  function get_translator (line 172) | def get_translator() -> gettext.GNUTranslations | gettext.NullTranslations:
  function set_language (line 185) | def set_language(language: str) -> None:
  function gettext_function (line 196) | def gettext_function(message: str) -> str:
  function ngettext_function (line 209) | def ngettext_function(singular: str, plural: str, n: int) -> str:

FILE: remark/storage/desktop_ini.py
  class EncodingConversionCanceled (line 20) | class EncodingConversionCanceled(Exception):  # noqa: N818
  class DesktopIniHandler (line 33) | class DesktopIniHandler:
    method get_path (line 49) | def get_path(folder_path):
    method exists (line 62) | def exists(folder_path):
    method read_info_tip (line 75) | def read_info_tip(folder_path):
    method write_info_tip (line 136) | def write_info_tip(folder_path, info_tip):
    method detect_encoding (line 227) | def detect_encoding(file_path):
    method fix_encoding (line 265) | def fix_encoding(file_path, current_encoding):
    method ensure_utf16_encoding (line 290) | def ensure_utf16_encoding(file_path):
    method remove_info_tip (line 351) | def remove_info_tip(folder_path):
    method delete (line 417) | def delete(folder_path):
    method set_folder_system_attributes (line 439) | def set_folder_system_attributes(folder_path):
    method set_file_hidden_system_attributes (line 484) | def set_file_hidden_system_attributes(file_path):
    method clear_file_attributes (line 506) | def clear_file_attributes(file_path):

FILE: remark/utils/path_resolver.py
  class NextResult (line 16) | class NextResult(Enum):
  class Cursor (line 24) | class Cursor:
    method jump_to_last_separator (line 38) | def jump_to_last_separator(self, normalized_args: list[str]) -> None:
    method next (line 52) | def next(self, normalized_args: list[str]) -> tuple["Cursor", NextResu...
  function get_between (line 91) | def get_between(begin: Cursor, end: Cursor, normalized_args: list[str]) ...
  function build_pattern (line 124) | def build_pattern(parts: list[str]) -> re.Pattern:
  function get_current_working_path (line 155) | def get_current_working_path(
  function get_inner_items_list (line 191) | def get_inner_items_list(current_working_path: Path) -> list[Path]:
  function find_candidates (line 201) | def find_candidates(
  function get_remaining_args (line 337) | def get_remaining_args(cursor: Cursor, normalized_args: list[str]) -> li...

FILE: remark/utils/platform.py
  function check_platform (line 10) | def check_platform() -> bool:

FILE: remark/utils/registry.py
  function get_executable_path (line 27) | def get_executable_path() -> str:
  function install_context_menu (line 46) | def install_context_menu() -> bool:
  function uninstall_context_menu (line 93) | def uninstall_context_menu() -> bool:

FILE: remark/utils/updater.py
  function _get_proxies (line 24) | def _get_proxies() -> dict[str, str] | None:
  function _create_opener (line 38) | def _create_opener():
  function _get_cache_file_path (line 47) | def _get_cache_file_path() -> str:
  function get_executable_path (line 52) | def get_executable_path() -> str:
  function get_latest_release (line 62) | def get_latest_release() -> dict[str, Any] | None:
  function should_check_update (line 109) | def should_check_update() -> bool:
  function update_next_check_time (line 130) | def update_next_check_time() -> None:
  function check_updates_auto (line 143) | def check_updates_auto(current_version: str) -> dict[str, Any] | None:
  function check_updates_manual (line 175) | def check_updates_manual(current_version: str) -> dict[str, Any] | None:
  function download_update (line 201) | def download_update(url: str, dest: str) -> str:
  function create_update_script (line 245) | def create_update_script(old_exe: str, new_exe: str) -> str:
  function trigger_update (line 281) | def trigger_update(script_path: str) -> None:

FILE: scripts/analyze_exe_size.py
  function run_archive_viewer (line 19) | def run_archive_viewer(exe_path: str) -> str:
  function parse_archive_content (line 32) | def parse_archive_content(content: str) -> dict[str, int]:
  function parse_pyz_content (line 46) | def parse_pyz_content(content: str) -> dict[str, int]:
  function generate_report (line 76) | def generate_report(
  function main (line 179) | def main():

FILE: scripts/build.py
  function get_project_version (line 40) | def get_project_version():
  function clean_build_files (line 53) | def clean_build_files():
  function ensure_upx (line 68) | def ensure_upx():
  function build_exe (line 85) | def build_exe():
  function main (line 127) | def main():

FILE: scripts/check_i18n.py
  function check_po_file (line 8) | def check_po_file(path: str) -> bool:

FILE: scripts/ensure_upx.py
  function get_proxies (line 38) | def get_proxies() -> dict[str, str] | None:
  function create_opener (line 52) | def create_opener():
  function get_system_info (line 61) | def get_system_info():
  function get_latest_upx_version (line 74) | def get_latest_upx_version() -> tuple[str, str, str] | None:
  function find_upx_executable (line 130) | def find_upx_executable() -> str | None:
  function download_upx (line 153) | def download_upx(version: str, download_url: str, asset_name: str) -> st...
  function verify_upx (line 232) | def verify_upx(upx_path: str) -> bool:
  function ensure_upx (line 251) | def ensure_upx() -> str | None:
  function main (line 293) | def main():

FILE: scripts/release.py
  function get_current_version (line 34) | def get_current_version():
  function update_version (line 45) | def update_version(new_version):
  function bump_version (line 70) | def bump_version(current, part="patch"):
  function create_tag (line 87) | def create_tag(version, override=False):
  function push_tag (line 102) | def push_tag(tag_name, force=False):
  function commit_version_changes (line 112) | def commit_version_changes():
  function check_branch (line 120) | def check_branch():
  function check_working_directory_clean (line 132) | def check_working_directory_clean():
  function check_remote_sync (line 143) | def check_remote_sync():
  function get_latest_tag (line 156) | def get_latest_tag() -> str | None:
  function validate_version_increment (line 173) | def validate_version_increment(current: str, new: str) -> bool:
  function is_version_releasable (line 185) | def is_version_releasable(version: str) -> bool:
  function main (line 194) | def main():

FILE: tests/conftest.py
  function pytest_configure (line 21) | def pytest_configure(config):
  function pytest_collection_modifyitems (line 42) | def pytest_collection_modifyitems(config, items):
  function set_chinese_language (line 52) | def set_chinese_language():

FILE: tests/integration/conftest.py
  function utf16_encoded_file (line 9) | def utf16_encoded_file(tmp_path):
  function utf8_encoded_file (line 21) | def utf8_encoded_file(tmp_path):

FILE: tests/integration/test_encoding_handling.py
  class TestEncodingHandling (line 12) | class TestEncodingHandling:
    method test_write_and_read_utf16 (line 15) | def test_write_and_read_utf16(self, tmp_path):
    method test_read_gbk_encoded_file (line 32) | def test_read_gbk_encoded_file(self, tmp_path):
    method test_read_utf8_encoded_file (line 45) | def test_read_utf8_encoded_file(self, tmp_path):
    method test_encoding_detection_utf16 (line 58) | def test_encoding_detection_utf16(self, utf16_encoded_file):
    method test_encoding_detection_utf8 (line 64) | def test_encoding_detection_utf8(self, utf8_encoded_file):
    method test_write_various_characters (line 82) | def test_write_various_characters(self, tmp_path, comment):
    method test_write_long_comment (line 93) | def test_write_long_comment(self, tmp_path):
    method test_update_preserves_encoding (line 106) | def test_update_preserves_encoding(self, tmp_path):
    method test_write_new_line_endings (line 127) | def test_write_new_line_endings(self, tmp_path):
    method test_read_without_bom (line 144) | def test_read_without_bom(self, tmp_path):
    method test_empty_folder (line 157) | def test_empty_folder(self, tmp_path):
    method test_corrupted_ini_file (line 165) | def test_corrupted_ini_file(self, tmp_path):

FILE: tests/unit/test_cli_commands.py
  class TestCLI (line 11) | class TestCLI:
    method disable_background_update_check (line 15) | def disable_background_update_check(self, monkeypatch):
    method test_init (line 22) | def test_init(self):
    method test_validate_folder_not_exists (line 27) | def test_validate_folder_not_exists(self, capsys):
    method test_validate_folder_not_dir (line 36) | def test_validate_folder_not_dir(self, fs, capsys):
    method test_validate_folder_success (line 46) | def test_validate_folder_success(self, fs):
    method test_add_comment_success (line 54) | def test_add_comment_success(self, fs):
    method test_add_comment_invalid_folder (line 61) | def test_add_comment_invalid_folder(self):
    method test_delete_comment_success (line 68) | def test_delete_comment_success(self, fs):
    method test_delete_comment_invalid_folder (line 75) | def test_delete_comment_invalid_folder(self):
    method test_view_comment_with_content (line 82) | def test_view_comment_with_content(self, fs, capsys):
    method test_view_comment_without_content (line 95) | def test_view_comment_without_content(self, fs, capsys):
    method test_view_comment_with_encoding_issue_and_fix (line 108) | def test_view_comment_with_encoding_issue_and_fix(self, fs, capsys):
    method test_view_comment_with_encoding_issue_and_skip (line 130) | def test_view_comment_with_encoding_issue_and_skip(self, fs, capsys):
    method test_view_comment_with_correct_encoding (line 151) | def test_view_comment_with_correct_encoding(self, fs, capsys):
    method test_interactive_mode_valid_input (line 175) | def test_interactive_mode_valid_input(self, fs, monkeypatch):
    method test_interactive_mode_invalid_path_then_valid (line 208) | def test_interactive_mode_invalid_path_then_valid(self, fs, monkeypatc...
    method test_interactive_mode_empty_comment_retry (line 241) | def test_interactive_mode_empty_comment_retry(self, fs, monkeypatch, c...
    method test_show_help (line 273) | def test_show_help(self, capsys):
    method test_run_with_help (line 282) | def test_run_with_help(self, capsys):
    method test_run_with_delete (line 290) | def test_run_with_delete(self, fs):
    method test_run_with_view (line 297) | def test_run_with_view(self, fs):
    method test_run_with_path_and_comment (line 304) | def test_run_with_path_and_comment(self, fs, monkeypatch):
    method test_run_interactive_mode (line 312) | def test_run_interactive_mode(self, monkeypatch):
    method test_run_platform_check_fail (line 319) | def test_run_platform_check_fail(self):
  class TestResolvePathAmbiguousArgs (line 333) | class TestResolvePathAmbiguousArgs:
    method disable_background_update_check (line 337) | def disable_background_update_check(self, monkeypatch):
    method test_resolve_path_valid_folder (line 345) | def test_resolve_path_valid_folder(self, fs):
    method test_resolve_path_file_rejected (line 354) | def test_resolve_path_file_rejected(self, fs):
    method test_resolve_path_no_candidates (line 362) | def test_resolve_path_no_candidates(self, fs):
    method test_resolve_path_single_candidate (line 369) | def test_resolve_path_single_candidate(self, fs, monkeypatch, capsys):
    method test_resolve_path_multiple_candidates_user_cancels (line 381) | def test_resolve_path_multiple_candidates_user_cancels(self, fs, monke...
    method test_resolve_path_multiple_candidates_user_selects (line 393) | def test_resolve_path_multiple_candidates_user_selects(self, fs, monke...
  class TestGetVersion (line 409) | class TestGetVersion:
    method test_get_version_from_package (line 412) | def test_get_version_from_package(self):
    method test_get_version_fallback (line 419) | def test_get_version_fallback(self):
  class TestInteractiveCommands (line 431) | class TestInteractiveCommands:
    method disable_background_update_check (line 435) | def disable_background_update_check(self, monkeypatch):
    method test_interactive_commands_list_initialized (line 442) | def test_interactive_commands_list_initialized(self):
    method test_show_command_list (line 451) | def test_show_command_list(self, capsys):
    method test_interactive_help_shows_commands (line 463) | def test_interactive_help_shows_commands(self, capsys):
    method test_interactive_mode_handles_hash_only (line 476) | def test_interactive_mode_handles_hash_only(self, fs, monkeypatch, cap...

FILE: tests/unit/test_desktop_ini.py
  class TestDesktopIniHandler (line 17) | class TestDesktopIniHandler:
    method test_get_path (line 20) | def test_get_path(self):
    method test_exists (line 30) | def test_exists(self, exists_return, expected):
    method test_read_info_tip_no_file (line 36) | def test_read_info_tip_no_file(self):
    method test_read_info_tip_with_content (line 54) | def test_read_info_tip_with_content(self, content, expected):
    method test_write_info_tip_empty (line 64) | def test_write_info_tip_empty(self):
    method test_write_info_tip_new_file (line 69) | def test_write_info_tip_new_file(self):
    method test_write_info_tip_update_existing (line 79) | def test_write_info_tip_update_existing(self):
    method test_detect_encoding_utf16_le (line 95) | def test_detect_encoding_utf16_le(self):
    method test_detect_encoding_utf16_be (line 106) | def test_detect_encoding_utf16_be(self):
    method test_detect_encoding_utf8_bom (line 117) | def test_detect_encoding_utf8_bom(self):
    method test_set_file_hidden_system_attributes (line 128) | def test_set_file_hidden_system_attributes(self):
    method test_clear_file_attributes (line 138) | def test_clear_file_attributes(self):
    method test_delete_file_exists (line 147) | def test_delete_file_exists(self):
    method test_delete_no_file (line 153) | def test_delete_no_file(self):
    method test_constants (line 159) | def test_constants(self):
  class TestEncodingConversionCanceled (line 166) | class TestEncodingConversionCanceled:
    method test_exception_creation (line 169) | def test_exception_creation(self):

FILE: tests/unit/test_folder_handler.py
  class TestFolderCommentHandler (line 11) | class TestFolderCommentHandler:
    method test_init (line 14) | def test_init(self):
    method test_supports (line 23) | def test_supports(self, is_dir, expected):
    method test_set_comment_not_folder (line 30) | def test_set_comment_not_folder(self, capsys):
    method test_set_comment_too_long (line 39) | def test_set_comment_too_long(self, capsys):
    method test_set_comment_success (line 58) | def test_set_comment_success(self):
    method test_get_comment (line 76) | def test_get_comment(self, read_return, expected):
    method test_delete_comment_no_ini (line 86) | def test_delete_comment_no_ini(self, capsys):
    method test_delete_comment_with_ini (line 95) | def test_delete_comment_with_ini(self):
    method test_delete_comment_clear_failure (line 112) | def test_delete_comment_clear_failure(self):
    method test_set_comment_desktop_ini (line 125) | def test_set_comment_desktop_ini(self):
    method test_max_comment_length_constant (line 149) | def test_max_comment_length_constant(self):

FILE: tests/unit/test_i18n.py
  class TestGetWindowsLocale (line 19) | class TestGetWindowsLocale:
    method test_get_windows_locale_success (line 30) | def test_get_windows_locale_success(self, locale_name, expected):
    method test_get_windows_locale_api_fails (line 41) | def test_get_windows_locale_api_fails(self):
    method test_get_windows_locale_exception (line 47) | def test_get_windows_locale_exception(self):
  class TestGetSystemLanguage (line 55) | class TestGetSystemLanguage:
    method test_windows_platform_priority (line 66) | def test_windows_platform_priority(self, windows_locale, expected):
    method test_windows_unsupported_locale_fallback (line 79) | def test_windows_unsupported_locale_fallback(self, windows_locale):
    method test_windows_api_null_fallback_to_locale (line 88) | def test_windows_api_null_fallback_to_locale(self):
    method test_locale_getlocale_variations (line 108) | def test_locale_getlocale_variations(self, locale_value, expected):
    method test_lang_environment_variable (line 126) | def test_lang_environment_variable(self, env_lang, expected):
    method test_all_methods_fallback_to_default (line 135) | def test_all_methods_fallback_to_default(self):
  class TestInitTranslation (line 146) | class TestInitTranslation:
    method test_init_translation_supported_language (line 156) | def test_init_translation_supported_language(self, language, expected_...
    method test_init_translation_unsupported_language_fallback (line 162) | def test_init_translation_unsupported_language_fallback(self):
    method test_init_translation_none_uses_system (line 169) | def test_init_translation_none_uses_system(self):
  class TestSetLanguage (line 177) | class TestSetLanguage:
    method test_set_language_updates_translator (line 180) | def test_set_language_updates_translator(self):
  class TestGetTextFunction (line 191) | class TestGetTextFunction:
    method test_gettext_function_returns_string (line 194) | def test_gettext_function_returns_string(self):
    method test_ngettext_function_singular (line 199) | def test_ngettext_function_singular(self):
    method test_ngettext_function_plural (line 204) | def test_ngettext_function_plural(self):

FILE: tests/unit/test_path_resolver.py
  class TestFindCandidates (line 13) | class TestFindCandidates:
    method test_single_space_split (line 17) | def test_single_space_split(self, fs):
    method test_multi_space_folder_name_with_comment (line 43) | def test_multi_space_folder_name_with_comment(self, fs):
    method test_valid_folder_with_comment (line 69) | def test_valid_folder_with_comment(self, fs):
    method test_no_match (line 93) | def test_no_match(self, fs):
    method test_both_single_name_and_extended_exist (line 116) | def test_both_single_name_and_extended_exist(self, fs):
    method test_subdirectory_recursive_matching (line 154) | def test_subdirectory_recursive_matching(self, fs):
    method test_empty_args (line 208) | def test_empty_args(self):
    method test_multiple_slashes_in_single_arg (line 222) | def test_multiple_slashes_in_single_arg(self, fs):
    method test_absolute_path_with_slash_arg (line 270) | def test_absolute_path_with_slash_arg(self, fs):
    method test_empty_directory (line 315) | def test_empty_directory(self, fs):
    method test_separator_no_match (line 337) | def test_separator_no_match(self, fs):
    method test_file_skipped (line 365) | def test_file_skipped(self, fs):
  class TestGetCurrentWorkingPath (line 389) | class TestGetCurrentWorkingPath:
    method test_empty_string (line 392) | def test_empty_string(self):
    method test_absolute_root (line 401) | def test_absolute_root(self):
    method test_absolute_with_trailing_slash (line 410) | def test_absolute_with_trailing_slash(self):
    method test_absolute_without_trailing_slash (line 419) | def test_absolute_without_trailing_slash(self):
    method test_absolute_multi_level (line 428) | def test_absolute_multi_level(self):
    method test_forward_slash_normalization (line 437) | def test_forward_slash_normalization(self):
    method test_relative_single (line 446) | def test_relative_single(self):
    method test_relative_with_backslash (line 455) | def test_relative_with_backslash(self):

FILE: tests/unit/test_platform.py
  class TestPlatform (line 11) | class TestPlatform:
    method test_check_platform_windows (line 23) | def test_check_platform_windows(self, system_name, expected):
    method test_check_platform_non_windows (line 33) | def test_check_platform_non_windows(self, system_name, capsys):
    method test_check_platform_case_sensitive (line 45) | def test_check_platform_case_sensitive(self, system_name, capsys):

FILE: tests/unit/test_registry.py
  class TestRegistry (line 13) | class TestRegistry:
    method test_get_executable_path_frozen (line 17) | def test_get_executable_path_frozen(self, mock_sys):
    method test_get_executable_path_dev (line 28) | def test_get_executable_path_dev(self, mock_path, mock_sys):
    method test_install_context_menu_success (line 44) | def test_install_context_menu_success(self, mock_get_exe, mock_winreg):
    method test_install_context_menu_permission_error (line 59) | def test_install_context_menu_permission_error(self, mock_winreg):
    method test_uninstall_context_menu_success (line 69) | def test_uninstall_context_menu_success(self, mock_get_exe, mock_winreg):
    method test_uninstall_context_menu_not_installed (line 80) | def test_uninstall_context_menu_not_installed(self, mock_winreg):
    method test_uninstall_context_menu_permission_error (line 89) | def test_uninstall_context_menu_permission_error(self, mock_winreg):

FILE: tests/unit/test_release.py
  class TestValidateVersionIncrement (line 15) | class TestValidateVersionIncrement:
    method test_patch_increment (line 18) | def test_patch_increment(self):
    method test_minor_increment (line 23) | def test_minor_increment(self):
    method test_major_increment (line 28) | def test_major_increment(self):
    method test_same_version_fails (line 33) | def test_same_version_fails(self):
    method test_lower_version_fails (line 37) | def test_lower_version_fails(self):
  function test_check_working_directory_clean (line 53) | def test_check_working_directory_clean(status_output, expected_result):
  function test_check_branch (line 73) | def test_check_branch(branch_name, is_main):
  function test_check_remote_sync (line 94) | def test_check_remote_sync(status_output, is_synced):
  class TestPushCommitInteraction (line 105) | class TestPushCommitInteraction:
    method test_push_implies_commit (line 108) | def test_push_implies_commit(self, capsys):

FILE: tests/unit/test_updater.py
  class TestProxyFunctions (line 28) | class TestProxyFunctions:
    method test_get_proxies_no_env (line 31) | def test_get_proxies_no_env(self, monkeypatch):
    method test_get_proxies_http_only (line 39) | def test_get_proxies_http_only(self, monkeypatch):
    method test_get_proxies_http_lowercase (line 48) | def test_get_proxies_http_lowercase(self, monkeypatch):
    method test_get_proxies_https_only (line 57) | def test_get_proxies_https_only(self, monkeypatch):
    method test_get_proxies_both (line 66) | def test_get_proxies_both(self, monkeypatch):
    method test_create_opener_no_proxy (line 79) | def test_create_opener_no_proxy(self, monkeypatch):
    method test_create_opener_with_proxy (line 87) | def test_create_opener_with_proxy(self, monkeypatch):
  class TestCacheFilePath (line 98) | class TestCacheFilePath:
    method test_get_cache_file_path (line 101) | def test_get_cache_file_path(self):
  class TestExecutablePath (line 109) | class TestExecutablePath:
    method test_get_executable_path_frozen (line 112) | def test_get_executable_path_frozen(self, monkeypatch):
    method test_get_executable_path_not_frozen (line 120) | def test_get_executable_path_not_frozen(self, monkeypatch):
  class TestGitHubAPI (line 131) | class TestGitHubAPI:
    method test_get_latest_release_success (line 134) | def test_get_latest_release_success(self):
    method test_get_latest_release_prerelease_filtered (line 170) | def test_get_latest_release_prerelease_filtered(self):
    method test_get_latest_release_draft_filtered (line 198) | def test_get_latest_release_draft_filtered(self):
    method test_get_latest_release_no_exe_asset (line 226) | def test_get_latest_release_no_exe_asset(self):
    method test_get_latest_release_api_error (line 251) | def test_get_latest_release_api_error(self):
    method test_get_latest_release_json_error (line 268) | def test_get_latest_release_json_error(self):
    method test_get_latest_release_non_200_status (line 275) | def test_get_latest_release_non_200_status(self):
  class TestCacheFunctions (line 290) | class TestCacheFunctions:
    method test_should_check_update_no_cache_file (line 293) | def test_should_check_update_no_cache_file(self, fs):
    method test_should_check_update_invalid_content (line 298) | def test_should_check_update_invalid_content(self, fs):
    method test_should_check_update_cache_expired (line 306) | def test_should_check_update_cache_expired(self, fs):
    method test_should_check_update_cache_valid (line 319) | def test_should_check_update_cache_valid(self, fs):
    method test_update_next_check_time (line 332) | def test_update_next_check_time(self, fs):
    method test_update_next_check_time_silent_failure (line 346) | def test_update_next_check_time_silent_failure(self, fs):
    method test_check_updates_auto_skips_when_not_needed (line 356) | def test_check_updates_auto_skips_when_not_needed(self, fs):
    method test_check_updates_auto_updates_cache_after_check (line 372) | def test_check_updates_auto_updates_cache_after_check(self, fs):
    method test_check_updates_auto_has_new_version (line 389) | def test_check_updates_auto_has_new_version(self, fs):
    method test_check_updates_auto_no_new_version (line 408) | def test_check_updates_auto_no_new_version(self, fs):
    method test_check_updates_auto_api_failure (line 426) | def test_check_updates_auto_api_failure(self, fs):
    method test_check_updates_manual_ignores_cache (line 438) | def test_check_updates_manual_ignores_cache(self, fs):
    method test_check_updates_manual_no_new_version (line 461) | def test_check_updates_manual_no_new_version(self):
    method test_check_updates_manual_api_returns_none (line 475) | def test_check_updates_manual_api_returns_none(self):
  class TestDownloadUpdate (line 484) | class TestDownloadUpdate:
    method test_download_update_success (line 487) | def test_download_update_success(self, tmp_path):
    method test_download_update_progress (line 502) | def test_download_update_progress(self, tmp_path, capsys):
    method test_download_update_network_error (line 518) | def test_download_update_network_error(self):
  class TestUpdateScript (line 533) | class TestUpdateScript:
    method test_create_update_script (line 536) | def test_create_update_script(self, tmp_path):
    method test_trigger_update (line 551) | def test_trigger_update(self):
  class TestInvalidVersion (line 564) | class TestInvalidVersion:
    method test_check_updates_auto_invalid_version (line 567) | def test_check_updates_auto_invalid_version(self, fs):
    method test_check_updates_manual_invalid_version (line 585) | def test_check_updates_manual_invalid_version(self):

FILE: tests/windows/test_context_menu.py
  class TestContextMenu (line 20) | class TestContextMenu:
    method setup_method (line 23) | def setup_method(self):
    method teardown_method (line 27) | def teardown_method(self):
    method test_install_and_uninstall (line 31) | def test_install_and_uninstall(self):
    method test_install_twice (line 54) | def test_install_twice(self):
    method test_uninstall_not_installed (line 62) | def test_uninstall_not_installed(self):
    method test_get_executable_path (line 67) | def test_get_executable_path(self):

FILE: tests/windows/test_full_workflow.py
  class TestFullWorkflow (line 16) | class TestFullWorkflow:
    method test_complete_add_workflow (line 19) | def test_complete_add_workflow(self, tmp_path):
    method test_complete_delete_workflow (line 50) | def test_complete_delete_workflow(self, tmp_path):
    method test_update_existing_comment (line 73) | def test_update_existing_comment(self, tmp_path):
    method test_multiple_folders (line 102) | def test_multiple_folders(self, tmp_path):
    method test_preserve_other_settings (line 125) | def test_preserve_other_settings(self, tmp_path):
    method test_empty_comment_removal (line 152) | def test_empty_comment_removal(self, tmp_path):
    method test_special_characters_in_comment (line 179) | def test_special_characters_in_comment(self, tmp_path):
    method test_comment_length_truncation (line 204) | def test_comment_length_truncation(self, tmp_path):
  class TestDesktopIniIntegration (line 226) | class TestDesktopIniIntegration:
    method test_write_read_cycle (line 229) | def test_write_read_cycle(self, tmp_path):
    method test_remove_info_tip (line 243) | def test_remove_info_tip(self, tmp_path):
    method test_file_attributes_workflow (line 262) | def test_file_attributes_workflow(self, tmp_path):
    method test_folder_readonly_workflow (line 289) | def test_folder_readonly_workflow(self, tmp_path):
Condensed preview — 74 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (314K chars).
[
  {
    "path": ".gitattributes",
    "chars": 182,
    "preview": "# 统一使用 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# L"
  },
  {
    "path": ".github/workflows/auto-tag.yml",
    "chars": 4416,
    "preview": "name: Auto Tag\n\non:\n  push:\n    branches:\n      - main                 # 正式环境\n\npermissions:\n  contents: write\n\njobs:\n  a"
  },
  {
    "path": ".github/workflows/deploy-docs.yml",
    "chars": 960,
    "preview": "name: Deploy VitePress site to Pages\n\non:\n  push:\n    branches: [main]\n  workflow_dispatch:\n\npermissions:\n  contents: re"
  },
  {
    "path": ".github/workflows/release.yml",
    "chars": 2562,
    "preview": "# -*- coding: utf-8 -*-\nname: Build and Release\n\non:\n  push:\n    tags:\n      - 'v*.*.*'\n  workflow_dispatch:\n    inputs:"
  },
  {
    "path": ".github/workflows/test.yml",
    "chars": 1625,
    "preview": "name: Test and Build\n\non:\n  pull_request:\n    branches: [main]\n  workflow_dispatch:\n\njobs:\n  test:\n    runs-on: windows-"
  },
  {
    "path": ".gitignore",
    "chars": 486,
    "preview": "# 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"
  },
  {
    "path": ".pre-commit-config.yaml",
    "chars": 3141,
    "preview": "# Pre-commit configuration\n# https://pre-commit.com/\n\ndefault_language_version:\n  python: python3.9\n\ndefault_stages: [pr"
  },
  {
    "path": ".python-version",
    "chars": 7,
    "preview": "3.11.7\n"
  },
  {
    "path": "CHANGELOG.md",
    "chars": 2153,
    "preview": "# Changelog\n\nAll notable changes to this project will be documented in this file.\n\nThe format is based on [Keep a Change"
  },
  {
    "path": "LICENSE",
    "chars": 1068,
    "preview": "The MIT License (MIT)\n\nCopyright (c) 2016 潘\n\nPermission is hereby granted, free of charge, to any person obtaining a cop"
  },
  {
    "path": "README.en.md",
    "chars": 4158,
    "preview": "# Windows Folder Remark/Comment Tool\n\n**[English](README.en.md)** | [中文文档](README.md)\n\n[![PyPI](https://img.shields.io/b"
  },
  {
    "path": "README.md",
    "chars": 2859,
    "preview": "# Windows Folder Remark/Comment Tool - Windows 文件夹备注工具\n\n**[English Documentation](README.en.md)** | [中文文档](README.md)\n\n["
  },
  {
    "path": "babel.cfg",
    "chars": 295,
    "preview": "# Babel extraction configuration\n# https://babel.pocoo.org/en/latest/messages.html\n\n# Extract translatable strings from "
  },
  {
    "path": "docs/.vitepress/config.mjs",
    "chars": 1845,
    "preview": "import { defineConfig } from 'vitepress'\n\nexport default defineConfig({\n  title: 'Windows 文件夹备注工具',\n  description: '一个轻量"
  },
  {
    "path": "docs/en/guide/api.md",
    "chars": 520,
    "preview": "# API Reference\n\n## Command-line Arguments\n\n| Argument | Short | Description |\n|---|---|---|\n| `--help` | `-h` | Show he"
  },
  {
    "path": "docs/en/guide/getting-started.md",
    "chars": 586,
    "preview": "# Getting Started\n\n## Download\n\nDownload `windows-folder-remark.exe` from [GitHub Releases](https://github.com/piratf/wi"
  },
  {
    "path": "docs/en/guide/usage.md",
    "chars": 593,
    "preview": "# Usage\n\n## Command-line Mode\n\n```bash\n# Add remark\nwindows-folder-remark.exe \"C:\\MyFolder\" \"My Folder\"\n\n# View remark\nw"
  },
  {
    "path": "docs/en/index.md",
    "chars": 1817,
    "preview": "---\nlayout: home\n\nhead:\n  - - script\n    - type: application/ld+json\n    - |\n      {\n        \"@context\": \"https://schema"
  },
  {
    "path": "docs/guide/api.md",
    "chars": 352,
    "preview": "# API 参考\n\n## 命令行参数\n\n| 参数 | 简写 | 说明 |\n|---|---|---|\n| `--help` | `-h` | 显示帮助信息 |\n| `--install` | | 安装右键菜单 |\n| `--uninstal"
  },
  {
    "path": "docs/guide/getting-started.md",
    "chars": 437,
    "preview": "# 快速开始\n\n## 下载\n\n从 [GitHub Releases](https://github.com/piratf/windows-folder-remark/releases) 下载 `windows-folder-remark.e"
  },
  {
    "path": "docs/guide/usage.md",
    "chars": 447,
    "preview": "# 使用方法\n\n## 命令行模式\n\n```bash\n# 添加备注\nwindows-folder-remark.exe \"C:\\MyFolder\" \"我的文件夹\"\n\n# 查看备注\nwindows-folder-remark.exe --vie"
  },
  {
    "path": "docs/index.md",
    "chars": 1330,
    "preview": "---\nlayout: home\n\nhead:\n  - - script\n    - type: application/ld+json\n    - |\n      {\n        \"@context\": \"https://schema"
  },
  {
    "path": "docs/robots.txt",
    "chars": 92,
    "preview": "User-agent: *\nAllow: /\n\nSitemap: https://piratf.github.io/windows-folder-remark/sitemap.xml\n"
  },
  {
    "path": "locale/zh/LC_MESSAGES/messages.po",
    "chars": 14560,
    "preview": "# Chinese translations for PROJECT.\n# Copyright (C) 2026 ORGANIZATION\n# This file is distributed under the same license "
  },
  {
    "path": "messages.pot",
    "chars": 11611,
    "preview": "# Translations template for PROJECT.\n# Copyright (C) 2026 ORGANIZATION\n# This file is distributed under the same license"
  },
  {
    "path": "package.json",
    "chars": 349,
    "preview": "{\n  \"name\": \"windows-folder-remark-docs\",\n  \"version\": \"1.0.0\",\n  \"type\": \"module\",\n  \"description\": \"Documentation for "
  },
  {
    "path": "pyproject.toml",
    "chars": 3932,
    "preview": "# pyproject.toml - 现代 Python 项目配置\n# https://packaging.python.org/en/latest/guides/writing-pyproject-toml/\n\n[project]\nnam"
  },
  {
    "path": "remark/__init__.py",
    "chars": 245,
    "preview": "\"\"\"\nWindows Folder Remark - 为 Windows 文件夹添加备注工具\n\"\"\"\n\n__author__ = \"Piratf\"\n\nfrom remark.core.base import CommentHandler\n"
  },
  {
    "path": "remark/cli/__init__.py",
    "chars": 72,
    "preview": "\"\"\"\n命令行接口模块\n\"\"\"\n\nfrom remark.cli.commands import CLI\n\n__all__ = [\"CLI\"]\n"
  },
  {
    "path": "remark/cli/__main__.py",
    "chars": 138,
    "preview": "\"\"\"\nMain entry point for running remark.cli as a module.\n\"\"\"\n\nfrom remark.cli.commands import main\n\nif __name__ == \"__ma"
  },
  {
    "path": "remark/cli/commands.py",
    "chars": 20269,
    "preview": "\"\"\"\n命令行接口\n\"\"\"\n\nimport argparse\nimport os\nimport sys\nimport tempfile\nimport threading\nimport urllib.error\n\nfrom remark.co"
  },
  {
    "path": "remark/core/__init__.py",
    "chars": 185,
    "preview": "\"\"\"\n核心功能模块\n\"\"\"\n\nfrom remark.core.base import CommentHandler\nfrom remark.core.folder_handler import FolderCommentHandler\n"
  },
  {
    "path": "remark/core/base.py",
    "chars": 459,
    "preview": "\"\"\"\n基础接口定义\n\"\"\"\n\nfrom abc import ABC, abstractmethod\n\n\nclass CommentHandler(ABC):\n    \"\"\"备注处理器基类\"\"\"\n\n    @abstractmethod\n"
  },
  {
    "path": "remark/core/folder_handler.py",
    "chars": 3814,
    "preview": "\"\"\"\n文件夹备注处理器 - 使用 desktop.ini\n\n使用 Microsoft 官方支持的 desktop.ini 方式设置文件夹备注。\n\n参考文档:\nhttps://learn.microsoft.com/en-us/window"
  },
  {
    "path": "remark/gui/__init__.py",
    "chars": 13,
    "preview": "\"\"\"GUI 模块\"\"\"\n"
  },
  {
    "path": "remark/gui/remark_dialog.py",
    "chars": 4052,
    "preview": "\"\"\"\n备注输入对话框\n\n使用 tkinter 实现的简单 GUI 对话框,用于右键菜单集成。\n\"\"\"\n\nimport tkinter as tk\nfrom tkinter import messagebox, ttk\n\nfrom rema"
  },
  {
    "path": "remark/i18n.py",
    "chars": 5240,
    "preview": "\"\"\"\nInternationalization (i18n) module for Windows Folder Remark tool.\n\nThis module provides translation support using g"
  },
  {
    "path": "remark/storage/__init__.py",
    "chars": 161,
    "preview": "\"\"\"\n存储层模块 - 提供统一的存储接口\n\"\"\"\n\nfrom .desktop_ini import DesktopIniHandler, EncodingConversionCanceled\n\n__all__ = [\"DesktopIn"
  },
  {
    "path": "remark/storage/desktop_ini.py",
    "chars": 15160,
    "preview": "\"\"\"\nDesktop.ini 交互层\n\n根据 Microsoft 官方文档要求,desktop.ini 文件必须使用 Unicode 格式\n才能正确存储和显示本地化字符串。\n\n参考文档:\nhttps://learn.microsoft.c"
  },
  {
    "path": "remark/utils/__init__.py",
    "chars": 180,
    "preview": "\"\"\"\n工具模块\n\"\"\"\n\nfrom remark.utils.constants import MAX_COMMENT_LENGTH\nfrom remark.utils.platform import check_platform\n\n__"
  },
  {
    "path": "remark/utils/constants.py",
    "chars": 312,
    "preview": "\"\"\"\n常量定义\n\"\"\"\n\nMAX_COMMENT_LENGTH = 260\n\n# GitHub 仓库配置\nGITHUB_REPO = \"piratf/windows-folder-remark\"\nGITHUB_API_RELEASES ="
  },
  {
    "path": "remark/utils/encoding.py",
    "chars": 15,
    "preview": "\"\"\"\n编码处理工具\n\"\"\"\n"
  },
  {
    "path": "remark/utils/path_resolver.py",
    "chars": 10070,
    "preview": "\"\"\"\n路径解析模块\n\n处理未加引号的含空格路径,智能重建完整路径。\n\"\"\"\n\nimport posixpath\nimport re\nfrom collections import deque\nfrom copy import deepco"
  },
  {
    "path": "remark/utils/platform.py",
    "chars": 389,
    "preview": "\"\"\"\n平台检查工具\n\"\"\"\n\nimport platform\n\nfrom remark.i18n import _ as _\n\n\ndef check_platform() -> bool:\n    \"\"\"检查是否为 Windows 系统\""
  },
  {
    "path": "remark/utils/registry.py",
    "chars": 2821,
    "preview": "\"\"\"\nWindows 注册表操作工具\n\n用于安装/卸载右键菜单到 Windows 资源管理器。\n\"\"\"\n\nimport contextlib\nimport os\nimport sys\nimport winreg\n\n# =========="
  },
  {
    "path": "remark/utils/updater.py",
    "chars": 6949,
    "preview": "\"\"\"\n自动更新模块\n\n提供版本检测、下载更新、创建更新脚本等功能。\n\"\"\"\n\nimport json\nimport os\nimport sys\nimport tempfile\nimport urllib.error\nimport urll"
  },
  {
    "path": "remark.py",
    "chars": 110,
    "preview": "\"\"\"\nWindows 文件/文件夹备注工具 - 主入口\n\"\"\"\n\nfrom remark.cli.commands import main\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "remark.spec",
    "chars": 4263,
    "preview": "# -*- mode: python ; coding: utf-8 -*-\n\"\"\"\nPyInstaller spec file for windows-folder-remark\n\nUsage:\n    pyinstaller remar"
  },
  {
    "path": "scripts/__init__.py",
    "chars": 23,
    "preview": "\"\"\"Scripts package.\"\"\"\n"
  },
  {
    "path": "scripts/analyze_exe_size.py",
    "chars": 7031,
    "preview": "\"\"\"\nPyInstaller EXE 大小分析工具\n\n使用方法:\n    python scripts/analyze_exe_size.py\n\n功能:\n    1. 运行 pyi-archive_viewer -r 获取 exe 内容\n"
  },
  {
    "path": "scripts/build.py",
    "chars": 3570,
    "preview": "\"\"\"\n本地打包脚本\n\n使用方法:\n    # 打包为单文件 exe\n    python -m scripts.build\n\n    # 清理构建文件\n    python -m scripts.build --clean\n\"\"\"\n\nim"
  },
  {
    "path": "scripts/check_i18n.py",
    "chars": 1179,
    "preview": "#!/usr/bin/env python\n\"\"\"检查翻译文件完整性\"\"\"\n\nimport re\nimport sys\n\n\ndef check_po_file(path: str) -> bool:\n    \"\"\"检查单个 .po 文件是否"
  },
  {
    "path": "scripts/ensure_upx.py",
    "chars": 8192,
    "preview": "\"\"\"\nUPX 下载脚本\n\n自动从 GitHub releases 下载最新版本的 UPX 压缩工具。\n\n使用方法:\n    python scripts/ensure_upx.py\n\"\"\"\n\nimport json\nimport os\ni"
  },
  {
    "path": "scripts/release.py",
    "chars": 9139,
    "preview": "\"\"\"\n版本发布脚本\n\n使用方法:\n    # 查看当前版本\n    python scripts/release.py\n\n    # 递增补丁版本 (2.0.0 -> 2.0.1)\n    python scripts/release.p"
  },
  {
    "path": "tests/__init__.py",
    "chars": 13,
    "preview": "\"\"\"\n测试模块\n\"\"\"\n"
  },
  {
    "path": "tests/conftest.py",
    "chars": 1604,
    "preview": "\"\"\"共享测试配置\"\"\"\n\nimport os\nimport sys\n\nfrom pathlib import Path\n\nimport pytest\n\n# 项目根目录添加到路径\nsys.path.insert(0, str(Path(__"
  },
  {
    "path": "tests/integration/__init__.py",
    "chars": 13,
    "preview": "\"\"\"集成测试模块\"\"\"\n"
  },
  {
    "path": "tests/integration/conftest.py",
    "chars": 646,
    "preview": "\"\"\"集成测试专用 fixtures\"\"\"\n\nimport codecs\n\nimport pytest\n\n\n@pytest.fixture\ndef utf16_encoded_file(tmp_path):\n    \"\"\"创建 UTF-16"
  },
  {
    "path": "tests/integration/test_encoding_handling.py",
    "chars": 5547,
    "preview": "\"\"\"编码处理集成测试\"\"\"\n\nimport codecs\nimport os\n\nimport pytest\n\nfrom remark.storage.desktop_ini import DesktopIniHandler\n\n\n@pyte"
  },
  {
    "path": "tests/unit/__init__.py",
    "chars": 13,
    "preview": "\"\"\"单元测试模块\"\"\"\n"
  },
  {
    "path": "tests/unit/test_cli_commands.py",
    "chars": 17460,
    "preview": "\"\"\"CLI 命令单元测试\"\"\"\n\nimport os\n\nimport pytest\n\nfrom remark.cli.commands import CLI, get_version\n\n\n@pytest.mark.unit\nclass T"
  },
  {
    "path": "tests/unit/test_desktop_ini.py",
    "chars": 6465,
    "preview": "\"\"\"desktop.ini 读写单元测试\"\"\"\n\nimport os\nfrom unittest.mock import MagicMock, patch\n\nimport pytest\n\nfrom remark.storage.deskt"
  },
  {
    "path": "tests/unit/test_folder_handler.py",
    "chars": 5197,
    "preview": "\"\"\"核心业务逻辑单元测试\"\"\"\n\nfrom unittest.mock import patch\n\nimport pytest\n\nfrom remark.core.folder_handler import MAX_COMMENT_LEN"
  },
  {
    "path": "tests/unit/test_i18n.py",
    "chars": 7451,
    "preview": "\"\"\"国际化 (i18n) 单元测试\"\"\"\n\nfrom unittest.mock import MagicMock, patch\n\nimport pytest\n\nfrom remark.i18n import (\n    SUPPORTE"
  },
  {
    "path": "tests/unit/test_path_resolver.py",
    "chars": 14651,
    "preview": "\"\"\"\n路径解析模块单元测试\n\"\"\"\n\nimport os\nfrom pathlib import Path, PureWindowsPath\n\nimport pytest\n\nfrom remark.utils.path_resolver "
  },
  {
    "path": "tests/unit/test_platform.py",
    "chars": 1665,
    "preview": "\"\"\"平台检测单元测试\"\"\"\n\nfrom unittest.mock import patch\n\nimport pytest\n\nfrom remark.utils.platform import check_platform\n\n\n@pyte"
  },
  {
    "path": "tests/unit/test_registry.py",
    "chars": 3035,
    "preview": "\"\"\"\n注册表操作单元测试\n\"\"\"\n\nfrom unittest.mock import MagicMock, patch\n\nimport pytest\n\nfrom remark.utils import registry\n\n\n@pytes"
  },
  {
    "path": "tests/unit/test_release.py",
    "chars": 3825,
    "preview": "\"\"\"release.py 脚本单元测试\"\"\"\n\nfrom unittest.mock import Mock, patch\n\nimport pytest\n\nfrom scripts.release import (\n    check_b"
  },
  {
    "path": "tests/unit/test_updater.py",
    "chars": 19746,
    "preview": "\"\"\"updater.py 单元测试\"\"\"\n\nimport json\nimport os\nimport tempfile\nfrom unittest.mock import Mock, patch\n\nimport pytest\n\nfrom "
  },
  {
    "path": "tests/windows/__init__.py",
    "chars": 21,
    "preview": "\"\"\"Windows 特定测试模块\"\"\"\n"
  },
  {
    "path": "tests/windows/test_context_menu.py",
    "chars": 2040,
    "preview": "\"\"\"\n右键菜单集成测试\n\n这些测试需要在 Windows 系统上运行,并且会实际操作注册表。\n\"\"\"\n\nimport sys\nimport winreg\n\nimport pytest\n\nfrom remark.utils import r"
  },
  {
    "path": "tests/windows/test_full_workflow.py",
    "chars": 9217,
    "preview": "\"\"\"完整工作流测试(仅 Windows)\"\"\"\n\nimport ctypes\nimport os\nimport sys\n\nimport pytest\n\nfrom remark.core.folder_handler import Fold"
  }
]

// ... and 2 more files (download for full content)

About this extraction

This page contains the full source code of the piratf/windows-folder-remark GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 74 files (258.8 KB), approximately 70.9k tokens, and a symbol index with 342 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!