diff --git a/examples/excel-copilot/.gitignore b/examples/excel-copilot/.gitignore new file mode 100644 index 00000000..a547bf36 --- /dev/null +++ b/examples/excel-copilot/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/examples/excel-copilot/.vscode/extensions.json b/examples/excel-copilot/.vscode/extensions.json new file mode 100644 index 00000000..24d7cc6d --- /dev/null +++ b/examples/excel-copilot/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["tauri-apps.tauri-vscode", "rust-lang.rust-analyzer"] +} diff --git a/examples/excel-copilot/README.md b/examples/excel-copilot/README.md new file mode 100644 index 00000000..2a00969e --- /dev/null +++ b/examples/excel-copilot/README.md @@ -0,0 +1,153 @@ +# Excel Copilot + +This is an example application demonstrating the Terminator SDK, showcasing how to automate Microsoft Excel using UI automation. It also integrates Google Gemini AI for enhanced data analysis and **Microsoft Excel Copilot** for advanced Excel features. Built with Tauri, React, TypeScript, and Rust. + +## Overview + +Excel Copilot enables AI-powered Excel automation through direct window manipulation. The application uses the Terminator library for precise UI automation, Google Gemini for intelligent data analysis and formula generation, and **can interact with Microsoft Excel Copilot** for advanced formatting, chart creation, and data analysis. + +## Features + +- **Excel Automation**: Read/write cells, set formulas, read ranges via UI automation +- **AI Integration**: Chat with Gemini to analyze and manipulate Excel data +- **Microsoft Excel Copilot Integration**: Advanced formatting, charts, conditional formatting +- **TSV Batch Operations**: Efficient bulk data import and processing +- **PDF Processing**: Attach PDF files for data extraction and analysis +- **Real-time Operations**: Direct interaction with Excel window +- **UI Context Awareness**: Precise targeting of Excel UI elements + +## Prerequisites + +- Windows 10/11 +- Node.js 18+ +- Rust (latest stable) +- Microsoft Excel (Copilot features are optional; requires Microsoft 365 subscription if you wish to use Copilot) +- Google Gemini API key +- **OneDrive account** (only required if using Copilot features) + +## Installation + +0. Install [webview2](https://developer.microsoft.com/en-us/microsoft-edge/webview2/?form=MA13LH) +1. Clone the repository +2. Install dependencies: + ```bash + cd examples/excel-copilot + npm install + ``` +3. Run the application: + ```bash + npm run tauri:dev + ``` + +## Configuration + +1. Obtain a Gemini API key from [Google AI Studio](https://makersuite.google.com/app/apikey) +2. Launch the application +3. Click the settings button and enter your API key +4. Ensure that the status state is "ready" +5. Click "new" to create a new file or "open" to use an existing file +6. Start chatting with Excel data + +## Usage + +### Basic Commands +- "Read cell A1" +- "Write 'Sales' to cell B1" +- "Set formula =SUM(A1:A10) in cell C1" +- "Read range A1:C5" + +### Data Analysis +- "Analyze the data in columns A through D" +- "Find the maximum value in this sheet" +- "Create a summary of this data" + +### PDF Integration +- Attach PDF files to extract data into Excel +- "Extract table data from this PDF" +- "Create Excel sheet from this invoice" + +## Microsoft Excel Copilot Requirements + +**⚠️ IMPORTANT: Microsoft Excel Copilot has specific requirements that must be met for it to function properly.** + +### Essential Requirements + +1. **Microsoft 365 Subscription**: Excel Copilot requires an active Microsoft 365 subscription with Copilot features enabled. + +2. **OneDrive Storage**: + - Files **MUST** be saved in OneDrive (not local storage) + - Use **"Open"** button to select existing OneDrive files (not "New") + - Check file path contains "OneDrive" to verify + +3. **Auto-Save Enabled**: + - Enable auto-save in Excel for best results + - Ensures changes are immediately synced to OneDrive + +4. **Data Structure Requirements**: + - Data range must have **at least 3 rows and 2 columns** + - **Headers are required** in the first row + - Headers must have different names (no duplicates) + - Data should be properly structured in table format + +### Copilot Features Available + +When requirements are met, you can use: + +- **Advanced Formatting**: "Format this data with bold headers, currency format for values, and alternating row colors" +- **Chart Creation**: "Create a column chart showing sales by month with title 'Monthly Sales Report'" +- **Conditional Formatting**: "Highlight values greater than 1000 in red and less than 500 in yellow" +- **Data Analysis**: "Create a summary table showing totals, averages, and trends from this data" + +### Enabling/Disabling Copilot + +- Use the toggle switch in the application to enable/disable Copilot features +- When disabled, only basic Excel automation tools are available +- When enabled, ensure all requirements above are met + +### Troubleshooting Copilot Issues + +- **"Copilot not working"** → Check if file is saved in OneDrive +- **"No Copilot button"** → Verify Microsoft 365 subscription and Excel version +- **"Insufficient data" error from copilot** → Ensure at least 3 rows and 2 columns with headers +- **"Auto-save required"** → Enable auto-save in Excel settings + +## Technical Details + +### Architecture +- **Frontend**: React + TypeScript for the user interface +- **Backend**: Rust + Tauri for system integration and Excel automation +- **Automation**: Terminator library for Windows UI automation +- **AI**: Google Gemini API with function calling capabilities + +### Available Functions +- `excel_read_cell` - Read value from specific cell +- `excel_write_cell` - Write value to specific cell +- `excel_read_range` - Read data from cell range +- `excel_set_formula` - Set Excel formula with validation +- `get_excel_sheet_overview` - Get complete sheet status +- `get_excel_ui_context` - Get UI element tree for precise targeting +- `paste_tsv_batch_data` - Efficient bulk data import via TSV format +- `send_request_to_excel_copilot` - Send requests to Microsoft Excel Copilot +- `format_cells_with_copilot` - Advanced cell formatting via Copilot +- `create_chart_with_copilot` - Chart creation via Copilot +- `apply_conditional_formatting_with_copilot` - Conditional formatting via Copilot + +## Build + +Development: +```bash +npm run tauri:dev +``` + +Production: +```bash +npm run tauri:build +``` + +## Requirements + +- Windows operating system (required for Excel automation) +- Microsoft Excel installed and running +- Valid Google Gemini API key +- Node.js and Rust development environment +- WebView2 : https://developer.microsoft.com/en-us/microsoft-edge/webview2/?form=MA13LH diff --git a/examples/excel-copilot/bin/terminator-excel-example.exe b/examples/excel-copilot/bin/terminator-excel-example.exe new file mode 100644 index 00000000..d680ba4c Binary files /dev/null and b/examples/excel-copilot/bin/terminator-excel-example.exe differ diff --git a/examples/excel-copilot/company_expense_statement_extract_example.pdf b/examples/excel-copilot/company_expense_statement_extract_example.pdf new file mode 100644 index 00000000..ff944814 Binary files /dev/null and b/examples/excel-copilot/company_expense_statement_extract_example.pdf differ diff --git a/examples/excel-copilot/index.html b/examples/excel-copilot/index.html new file mode 100644 index 00000000..ff93803b --- /dev/null +++ b/examples/excel-copilot/index.html @@ -0,0 +1,14 @@ + + + + + + + Tauri + React + Typescript + + + +
+ + + diff --git a/examples/excel-copilot/package-lock.json b/examples/excel-copilot/package-lock.json new file mode 100644 index 00000000..b9e4a4d4 --- /dev/null +++ b/examples/excel-copilot/package-lock.json @@ -0,0 +1,2022 @@ +{ + "name": "excel-copilot", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "excel-copilot", + "version": "0.1.0", + "dependencies": { + "@tauri-apps/api": "^2.5.0", + "@tauri-apps/plugin-dialog": "^2.2.2", + "@tauri-apps/plugin-fs": "^2.3.0", + "@tauri-apps/plugin-opener": "^2", + "lucide-react": "^0.511.0", + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@tauri-apps/cli": "^2", + "@types/react": "^18.3.1", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react": "^4.3.4", + "typescript": "~5.6.2", + "vite": "^6.0.3" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.3.tgz", + "integrity": "sha512-V42wFfx1ymFte+ecf6iXghnnP8kWTO+ZLXIyZq+1LAXHHvTZdVxicn4yiVYdYMGaCO3tmqub11AorKkv+iodqw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.3.tgz", + "integrity": "sha512-hyrN8ivxfvJ4i0fIJuV4EOlV0WDMz5Ui4StRTgVaAvWeiRCilXgwVvxJKtFQ3TKtHgJscB2YiXKGNJuVwhQMtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.27.3", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.27.3", + "@babel/helpers": "^7.27.3", + "@babel/parser": "^7.27.3", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.27.3", + "@babel/types": "^7.27.3", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.3.tgz", + "integrity": "sha512-xnlJYj5zepml8NXtjkG0WquFUv8RskFqyFcVgTBp5k+NaA/8uw/K+OSVf8AMGw5e9HKP2ETd5xpK5MLZQD6b4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.27.3", + "@babel/types": "^7.27.3", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz", + "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.3.tgz", + "integrity": "sha512-h/eKy9agOya1IGuLaZ9tEUgz+uIRXcbtOhRtUyyMf8JFmn1iT13vnl/IGVWSkdOCG/pC57U4S1jnAabAavTMwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.3.tgz", + "integrity": "sha512-xyYxRj6+tLNDTWi0KCBcZ9V7yg3/lwL9DWh9Uwh/RIVlIfFidggcgxKX3GCXwCiswwcGRawBKbEg2LG/Y8eJhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.3" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.3.tgz", + "integrity": "sha512-lId/IfN/Ye1CIu8xG7oKBHXd2iNb2aW1ilPszzGcJug6M8RCKfVNcYhpI5+bMvFYjK7lXIM0R+a+6r8xhHp2FQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.27.3", + "@babel/parser": "^7.27.3", + "@babel/template": "^7.27.2", + "@babel/types": "^7.27.3", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.3.tgz", + "integrity": "sha512-Y1GkI4ktrtvmawoSq+4FCVHNryea6uR+qUQy0AGxLSsjCX0nVmkYQMBLHDkXZuo5hGx7eYdnIaslsdBFm7zbUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz", + "integrity": "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.5.tgz", + "integrity": "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz", + "integrity": "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.5.tgz", + "integrity": "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz", + "integrity": "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz", + "integrity": "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz", + "integrity": "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz", + "integrity": "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz", + "integrity": "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz", + "integrity": "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz", + "integrity": "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz", + "integrity": "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz", + "integrity": "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz", + "integrity": "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz", + "integrity": "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz", + "integrity": "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz", + "integrity": "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.5.tgz", + "integrity": "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz", + "integrity": "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.5.tgz", + "integrity": "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz", + "integrity": "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz", + "integrity": "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz", + "integrity": "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz", + "integrity": "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz", + "integrity": "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.9", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.9.tgz", + "integrity": "sha512-e9MeMtVWo186sgvFFJOPGy7/d2j2mZhLJIdVW0C/xDluuOvymEATqz6zKsP0ZmXGzQtqlyjz5sC1sYQUoJG98w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.41.1.tgz", + "integrity": "sha512-NELNvyEWZ6R9QMkiytB4/L4zSEaBC03KIXEghptLGLZWJ6VPrL63ooZQCOnlx36aQPGhzuOMwDerC1Eb2VmrLw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.41.1.tgz", + "integrity": "sha512-DXdQe1BJ6TK47ukAoZLehRHhfKnKg9BjnQYUu9gzhI8Mwa1d2fzxA1aw2JixHVl403bwp1+/o/NhhHtxWJBgEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.41.1.tgz", + "integrity": "sha512-5afxvwszzdulsU2w8JKWwY8/sJOLPzf0e1bFuvcW5h9zsEg+RQAojdW0ux2zyYAz7R8HvvzKCjLNJhVq965U7w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.41.1.tgz", + "integrity": "sha512-egpJACny8QOdHNNMZKf8xY0Is6gIMz+tuqXlusxquWu3F833DcMwmGM7WlvCO9sB3OsPjdC4U0wHw5FabzCGZg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.41.1.tgz", + "integrity": "sha512-DBVMZH5vbjgRk3r0OzgjS38z+atlupJ7xfKIDJdZZL6sM6wjfDNo64aowcLPKIx7LMQi8vybB56uh1Ftck/Atg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.41.1.tgz", + "integrity": "sha512-3FkydeohozEskBxNWEIbPfOE0aqQgB6ttTkJ159uWOFn42VLyfAiyD9UK5mhu+ItWzft60DycIN1Xdgiy8o/SA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.41.1.tgz", + "integrity": "sha512-wC53ZNDgt0pqx5xCAgNunkTzFE8GTgdZ9EwYGVcg+jEjJdZGtq9xPjDnFgfFozQI/Xm1mh+D9YlYtl+ueswNEg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.41.1.tgz", + "integrity": "sha512-jwKCca1gbZkZLhLRtsrka5N8sFAaxrGz/7wRJ8Wwvq3jug7toO21vWlViihG85ei7uJTpzbXZRcORotE+xyrLA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.41.1.tgz", + "integrity": "sha512-g0UBcNknsmmNQ8V2d/zD2P7WWfJKU0F1nu0k5pW4rvdb+BIqMm8ToluW/eeRmxCared5dD76lS04uL4UaNgpNA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.41.1.tgz", + "integrity": "sha512-XZpeGB5TKEZWzIrj7sXr+BEaSgo/ma/kCgrZgL0oo5qdB1JlTzIYQKel/RmhT6vMAvOdM2teYlAaOGJpJ9lahg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.41.1.tgz", + "integrity": "sha512-bkCfDJ4qzWfFRCNt5RVV4DOw6KEgFTUZi2r2RuYhGWC8WhCA8lCAJhDeAmrM/fdiAH54m0mA0Vk2FGRPyzI+tw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.41.1.tgz", + "integrity": "sha512-3mr3Xm+gvMX+/8EKogIZSIEF0WUu0HL9di+YWlJpO8CQBnoLAEL/roTCxuLncEdgcfJcvA4UMOf+2dnjl4Ut1A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.41.1.tgz", + "integrity": "sha512-3rwCIh6MQ1LGrvKJitQjZFuQnT2wxfU+ivhNBzmxXTXPllewOF7JR1s2vMX/tWtUYFgphygxjqMl76q4aMotGw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.41.1.tgz", + "integrity": "sha512-LdIUOb3gvfmpkgFZuccNa2uYiqtgZAz3PTzjuM5bH3nvuy9ty6RGc/Q0+HDFrHrizJGVpjnTZ1yS5TNNjFlklw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.41.1.tgz", + "integrity": "sha512-oIE6M8WC9ma6xYqjvPhzZYk6NbobIURvP/lEbh7FWplcMO6gn7MM2yHKA1eC/GvYwzNKK/1LYgqzdkZ8YFxR8g==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.41.1.tgz", + "integrity": "sha512-cWBOvayNvA+SyeQMp79BHPK8ws6sHSsYnK5zDcsC3Hsxr1dgTABKjMnMslPq1DvZIp6uO7kIWhiGwaTdR4Og9A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.41.1.tgz", + "integrity": "sha512-y5CbN44M+pUCdGDlZFzGGBSKCA4A/J2ZH4edTYSSxFg7ce1Xt3GtydbVKWLlzL+INfFIZAEg1ZV6hh9+QQf9YQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.41.1.tgz", + "integrity": "sha512-lZkCxIrjlJlMt1dLO/FbpZbzt6J/A8p4DnqzSa4PWqPEUUUnzXLeki/iyPLfV0BmHItlYgHUqJe+3KiyydmiNQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.41.1.tgz", + "integrity": "sha512-+psFT9+pIh2iuGsxFYYa/LhS5MFKmuivRsx9iPJWNSGbh2XVEjk90fmpUEjCnILPEPJnikAU6SFDiEUyOv90Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.41.1.tgz", + "integrity": "sha512-Wq2zpapRYLfi4aKxf2Xff0tN+7slj2d4R87WEzqw7ZLsVvO5zwYCIuEGSZYiK41+GlwUo1HiR+GdkLEJnCKTCw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tauri-apps/api": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.5.0.tgz", + "integrity": "sha512-Ldux4ip+HGAcPUmuLT8EIkk6yafl5vK0P0c0byzAKzxJh7vxelVtdPONjfgTm96PbN24yjZNESY8CKo8qniluA==", + "license": "Apache-2.0 OR MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tauri" + } + }, + "node_modules/@tauri-apps/cli": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.5.0.tgz", + "integrity": "sha512-rAtHqG0Gh/IWLjN2zTf3nZqYqbo81oMbqop56rGTjrlWk9pTTAjkqOjSL9XQLIMZ3RbeVjveCqqCA0s8RnLdMg==", + "dev": true, + "license": "Apache-2.0 OR MIT", + "bin": { + "tauri": "tauri.js" + }, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tauri" + }, + "optionalDependencies": { + "@tauri-apps/cli-darwin-arm64": "2.5.0", + "@tauri-apps/cli-darwin-x64": "2.5.0", + "@tauri-apps/cli-linux-arm-gnueabihf": "2.5.0", + "@tauri-apps/cli-linux-arm64-gnu": "2.5.0", + "@tauri-apps/cli-linux-arm64-musl": "2.5.0", + "@tauri-apps/cli-linux-riscv64-gnu": "2.5.0", + "@tauri-apps/cli-linux-x64-gnu": "2.5.0", + "@tauri-apps/cli-linux-x64-musl": "2.5.0", + "@tauri-apps/cli-win32-arm64-msvc": "2.5.0", + "@tauri-apps/cli-win32-ia32-msvc": "2.5.0", + "@tauri-apps/cli-win32-x64-msvc": "2.5.0" + } + }, + "node_modules/@tauri-apps/cli-darwin-arm64": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.5.0.tgz", + "integrity": "sha512-VuVAeTFq86dfpoBDNYAdtQVLbP0+2EKCHIIhkaxjeoPARR0sLpFHz2zs0PcFU76e+KAaxtEtAJAXGNUc8E1PzQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-darwin-x64": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.5.0.tgz", + "integrity": "sha512-hUF01sC06cZVa8+I0/VtsHOk9BbO75rd+YdtHJ48xTdcYaQ5QIwL4yZz9OR1AKBTaUYhBam8UX9Pvd5V2/4Dpw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm-gnueabihf": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.5.0.tgz", + "integrity": "sha512-LQKqttsK252LlqYyX8R02MinUsfFcy3+NZiJwHFgi5Y3+ZUIAED9cSxJkyNtuY5KMnR4RlpgWyLv4P6akN1xhg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm64-gnu": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.5.0.tgz", + "integrity": "sha512-mTQufsPcpdHg5RW0zypazMo4L55EfeE5snTzrPqbLX4yCK2qalN7+rnP8O8GT06xhp6ElSP/Ku1M2MR297SByQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm64-musl": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.5.0.tgz", + "integrity": "sha512-rQO1HhRUQqyEaal5dUVOQruTRda/TD36s9kv1hTxZiFuSq3558lsTjAcUEnMAtBcBkps20sbyTJNMT0AwYIk8Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-riscv64-gnu": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.5.0.tgz", + "integrity": "sha512-7oS18FN46yDxyw1zX/AxhLAd7T3GrLj3Ai6s8hZKd9qFVzrAn36ESL7d3G05s8wEtsJf26qjXnVF4qleS3dYsA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-x64-gnu": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.5.0.tgz", + "integrity": "sha512-SG5sFNL7VMmDBdIg3nO3EzNRT306HsiEQ0N90ILe3ZABYAVoPDO/ttpCO37ApLInTzrq/DLN+gOlC/mgZvLw1w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-x64-musl": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.5.0.tgz", + "integrity": "sha512-QXDM8zp/6v05PNWju5ELsVwF0VH1n6b5pk2E6W/jFbbiwz80Vs1lACl9pv5kEHkrxBj+aWU/03JzGuIj2g3SkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-arm64-msvc": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.5.0.tgz", + "integrity": "sha512-pFSHFK6b+o9y4Un8w0gGLwVyFTZaC3P0kQ7umRt/BLDkzD5RnQ4vBM7CF8BCU5nkwmEBUCZd7Wt3TWZxe41o6Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-ia32-msvc": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.5.0.tgz", + "integrity": "sha512-EArv1IaRlogdLAQyGlKmEqZqm5RfHCUMhJoedWu7GtdbOMUfSAz6FMX2boE1PtEmNO4An+g188flLeVErrxEKg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-x64-msvc": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.5.0.tgz", + "integrity": "sha512-lj43EFYbnAta8pd9JnUq87o+xRUR0odz+4rixBtTUwUgdRdwQ2V9CzFtsMu6FQKpFQ6mujRK6P1IEwhL6ADRsQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/plugin-dialog": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-dialog/-/plugin-dialog-2.2.2.tgz", + "integrity": "sha512-Pm9qnXQq8ZVhAMFSEPwxvh+nWb2mk7LASVlNEHYaksHvcz8P6+ElR5U5dNL9Ofrm+uwhh1/gYKWswK8JJJAh6A==", + "license": "MIT OR Apache-2.0", + "dependencies": { + "@tauri-apps/api": "^2.0.0" + } + }, + "node_modules/@tauri-apps/plugin-fs": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-fs/-/plugin-fs-2.3.0.tgz", + "integrity": "sha512-G9gEyYVUaaxhdRJBgQTTLmzAe0vtHYxYyN1oTQzU3zwvb8T+tVLcAqCdFMWHq0qGeGbmynI5whvYpcXo5LvZ1w==", + "license": "MIT OR Apache-2.0", + "dependencies": { + "@tauri-apps/api": "^2.0.0" + } + }, + "node_modules/@tauri-apps/plugin-opener": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-opener/-/plugin-opener-2.2.7.tgz", + "integrity": "sha512-uduEyvOdjpPOEeDRrhwlCspG/f9EQalHumWBtLBnp3fRp++fKGLqDOyUhSIn7PzX45b/rKep//ZQSAQoIxobLA==", + "license": "MIT OR Apache-2.0", + "dependencies": { + "@tauri-apps/api": "^2.0.0" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.7.tgz", + "integrity": "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/estree": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", + "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.14", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz", + "integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.23", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.23.tgz", + "integrity": "sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.5.0.tgz", + "integrity": "sha512-JuLWaEqypaJmOJPLWwO335Ig6jSgC1FTONCWAxnqcQthLTK/Yc9aH6hr9z/87xciejbQcnP3GnA1FWUSWeXaeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.26.10", + "@babel/plugin-transform-react-jsx-self": "^7.25.9", + "@babel/plugin-transform-react-jsx-source": "^7.25.9", + "@rolldown/pluginutils": "1.0.0-beta.9", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.0.tgz", + "integrity": "sha512-PJ8gYKeS5e/whHBh8xrwYK+dAvEj7JXtz6uTucnMRB8OiGTsKccFekoRrjajPBHV8oOY+2tI4uxeceSimKwMFA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001718", + "electron-to-chromium": "^1.5.160", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001720", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001720.tgz", + "integrity": "sha512-Ec/2yV2nNPwb4DnTANEV99ZWwm3ZWfdlfkQbWSDDt+PsXEVYwlhPH8tdMaPunYTKKmz7AnHi2oNEi1GcmKCD8g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.161", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.161.tgz", + "integrity": "sha512-hwtetwfKNZo/UlwHIVBlKZVdy7o8bIZxxKs0Mv/ROPiQQQmDgdm5a+KvKtBsxM8ZjFzTaCeLoodZ8jiBE3o9rA==", + "dev": true, + "license": "ISC" + }, + "node_modules/esbuild": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz", + "integrity": "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.5", + "@esbuild/android-arm": "0.25.5", + "@esbuild/android-arm64": "0.25.5", + "@esbuild/android-x64": "0.25.5", + "@esbuild/darwin-arm64": "0.25.5", + "@esbuild/darwin-x64": "0.25.5", + "@esbuild/freebsd-arm64": "0.25.5", + "@esbuild/freebsd-x64": "0.25.5", + "@esbuild/linux-arm": "0.25.5", + "@esbuild/linux-arm64": "0.25.5", + "@esbuild/linux-ia32": "0.25.5", + "@esbuild/linux-loong64": "0.25.5", + "@esbuild/linux-mips64el": "0.25.5", + "@esbuild/linux-ppc64": "0.25.5", + "@esbuild/linux-riscv64": "0.25.5", + "@esbuild/linux-s390x": "0.25.5", + "@esbuild/linux-x64": "0.25.5", + "@esbuild/netbsd-arm64": "0.25.5", + "@esbuild/netbsd-x64": "0.25.5", + "@esbuild/openbsd-arm64": "0.25.5", + "@esbuild/openbsd-x64": "0.25.5", + "@esbuild/sunos-x64": "0.25.5", + "@esbuild/win32-arm64": "0.25.5", + "@esbuild/win32-ia32": "0.25.5", + "@esbuild/win32-x64": "0.25.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fdir": { + "version": "6.4.5", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.5.tgz", + "integrity": "sha512-4BG7puHpVsIYxZUbiUE3RqGloLaSSwzYie5jvasC4LWuBWzZawynvYouhjbQKw2JuIGYdm0DzIxl8iVidKlUEw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "0.511.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.511.0.tgz", + "integrity": "sha512-VK5a2ydJ7xm8GvBeKLS9mu1pVK6ucef9780JVUjw6bAjJL/QXnd4Y0p7SPeOUMC27YhzNCZvm5d/QX0Tp3rc0w==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.4", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.4.tgz", + "integrity": "sha512-QSa9EBe+uwlGTFmHsPKokv3B/oEMQZxfqW0QqNCyhpa6mB1afzulwn8hihglqAb2pOw+BJgNlmXQ8la2VeHB7w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.41.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.41.1.tgz", + "integrity": "sha512-cPmwD3FnFv8rKMBc1MxWCwVQFxwf1JEmSX3iQXrRVVG15zerAIXRjMFVWnd5Q5QvgKF7Aj+5ykXFhUl+QGnyOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.7" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.41.1", + "@rollup/rollup-android-arm64": "4.41.1", + "@rollup/rollup-darwin-arm64": "4.41.1", + "@rollup/rollup-darwin-x64": "4.41.1", + "@rollup/rollup-freebsd-arm64": "4.41.1", + "@rollup/rollup-freebsd-x64": "4.41.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.41.1", + "@rollup/rollup-linux-arm-musleabihf": "4.41.1", + "@rollup/rollup-linux-arm64-gnu": "4.41.1", + "@rollup/rollup-linux-arm64-musl": "4.41.1", + "@rollup/rollup-linux-loongarch64-gnu": "4.41.1", + "@rollup/rollup-linux-powerpc64le-gnu": "4.41.1", + "@rollup/rollup-linux-riscv64-gnu": "4.41.1", + "@rollup/rollup-linux-riscv64-musl": "4.41.1", + "@rollup/rollup-linux-s390x-gnu": "4.41.1", + "@rollup/rollup-linux-x64-gnu": "4.41.1", + "@rollup/rollup-linux-x64-musl": "4.41.1", + "@rollup/rollup-win32-arm64-msvc": "4.41.1", + "@rollup/rollup-win32-ia32-msvc": "4.41.1", + "@rollup/rollup-win32-x64-msvc": "4.41.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", + "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.4.4", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/typescript": { + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", + "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vite": { + "version": "6.3.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", + "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/examples/excel-copilot/package.json b/examples/excel-copilot/package.json new file mode 100644 index 00000000..87075b50 --- /dev/null +++ b/examples/excel-copilot/package.json @@ -0,0 +1,32 @@ +{ + "name": "excel-copilot", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview", + "tauri": "tauri", + "tauri:dev": "tauri dev", + "tauri:build": "tauri build" + }, + "dependencies": { + "@tauri-apps/api": "^2.5.0", + "@types/node": "^20.11.24", + "@tauri-apps/plugin-dialog": "^2.2.2", + "@tauri-apps/plugin-fs": "^2.3.0", + "@tauri-apps/plugin-opener": "^2", + "lucide-react": "^0.511.0", + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@tauri-apps/cli": "^2", + "@types/react": "^18.3.1", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react": "^4.3.4", + "typescript": "~5.6.2", + "vite": "^6.0.3" + } +} diff --git a/examples/excel-copilot/public/tauri.svg b/examples/excel-copilot/public/tauri.svg new file mode 100644 index 00000000..31b62c92 --- /dev/null +++ b/examples/excel-copilot/public/tauri.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/examples/excel-copilot/public/vite.svg b/examples/excel-copilot/public/vite.svg new file mode 100644 index 00000000..e7b8dfb1 --- /dev/null +++ b/examples/excel-copilot/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/excel-copilot/src-tauri/.gitignore b/examples/excel-copilot/src-tauri/.gitignore new file mode 100644 index 00000000..b21bd681 --- /dev/null +++ b/examples/excel-copilot/src-tauri/.gitignore @@ -0,0 +1,7 @@ +# Generated by Cargo +# will have compiled files and executables +/target/ + +# Generated by Tauri +# will have schema files for capabilities auto-completion +/gen/schemas diff --git a/examples/excel-copilot/src-tauri/Cargo.lock b/examples/excel-copilot/src-tauri/Cargo.lock new file mode 100644 index 00000000..726b9855 --- /dev/null +++ b/examples/excel-copilot/src-tauri/Cargo.lock @@ -0,0 +1,7608 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "accessibility" +version = "0.2.0" +source = "git+https://github.com/eiz/accessibility.git?branch=master#63a4a9c4ecdc143bed1d65b989aa18bbc5fd8129" +dependencies = [ + "accessibility-sys", + "cocoa", + "core-foundation 0.10.1", + "objc", + "thiserror 1.0.69", +] + +[[package]] +name = "accessibility-sys" +version = "0.2.0" +source = "git+https://github.com/eiz/accessibility.git?branch=master#63a4a9c4ecdc143bed1d65b989aa18bbc5fd8129" +dependencies = [ + "core-foundation-sys", +] + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "aligned-vec" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4aa90d7ce82d4be67b64039a3d588d38dbcc6736577de4a847025ce5b0c468d1" + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "annotate-snippets" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccaf7e9dfbb6ab22c82e473cd1a8a7bd313c19a5b7e40970f3d89ef5a5c9e81e" +dependencies = [ + "unicode-width", + "yansi-term", +] + +[[package]] +name = "anyhow" +version = "1.0.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" + +[[package]] +name = "arbitrary" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" +dependencies = [ + "derive_arbitrary", +] + +[[package]] +name = "arboard" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1df21f715862ede32a0c525ce2ca4d52626bb0007f8c18b87a384503ac33e70" +dependencies = [ + "clipboard-win", + "image", + "log", + "objc2 0.6.1", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation 0.3.1", + "parking_lot", + "percent-encoding", + "windows-sys 0.59.0", + "x11rb", +] + +[[package]] +name = "arg_enum_proc_macro" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "ashpd" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cbdf310d77fd3aaee6ea2093db7011dc2d35d2eb3481e5607f1f8d942ed99df" +dependencies = [ + "enumflags2", + "futures-channel", + "futures-util", + "rand 0.9.1", + "raw-window-handle", + "serde", + "serde_repr", + "tokio", + "url", + "zbus", +] + +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-channel" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb812ffb58524bdd10860d7d974e2f01cc0950c2438a74ee5ec2e2280c6c4ffa" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-io" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1237c0ae75a0f3765f58910ff9cdd0a12eeb39ab2f4c7de23262f337f0aacbb3" +dependencies = [ + "async-lock", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix 1.0.7", + "slab", + "tracing", + "windows-sys 0.59.0", +] + +[[package]] +name = "async-lock" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-process" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cde3f4e40e6021d7acffc90095cbd6dc54cb593903d1de5832f435eb274b85dc" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix 1.0.7", + "tracing", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "async-signal" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7605a4e50d4b06df3898d5a70bf5fde51ed9059b0434b73105193bc27acce0d" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix 1.0.7", + "signal-hook-registry", + "slab", + "windows-sys 0.59.0", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-trait" +version = "0.1.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "atk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b" +dependencies = [ + "atk-sys", + "glib", + "libc", +] + +[[package]] +name = "atk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "av1-grain" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f3efb2ca85bc610acfa917b5aaa36f3fcbebed5b3182d7f877b02531c4b80c8" +dependencies = [ + "anyhow", + "arrayvec", + "log", + "nom", + "num-rational", + "v_frame", +] + +[[package]] +name = "avif-serialize" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98922d6a4cfbcb08820c69d8eeccc05bb1f29bfa06b4f5b1dbfe9a868bd7608e" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "backtrace" +version = "0.3.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets 0.52.6", +] + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bindgen" +version = "0.69.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" +dependencies = [ + "annotate-snippets", + "bitflags 2.9.1", + "cexpr", + "clang-sys", + "itertools", + "lazy_static", + "lazycell", + "proc-macro2", + "quote", + "regex", + "rustc-hash 1.1.0", + "shlex", + "syn 2.0.101", +] + +[[package]] +name = "bit_field" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +dependencies = [ + "serde", +] + +[[package]] +name = "bitstream-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6099cdc01846bc367c4e7dd630dc5966dccf36b652fae7a74e17b640411a91b2" + +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block2" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" +dependencies = [ + "objc2 0.5.2", +] + +[[package]] +name = "block2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "340d2f0bdb2a43c1d3cd40513185b2bd7def0aa1052f956455114bc98f82dcf2" +dependencies = [ + "objc2 0.6.1", +] + +[[package]] +name = "blocking" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + +[[package]] +name = "brotli" +version = "7.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc97b8f16f944bba54f0433f07e30be199b6dc2bd25937444bbad560bcea29bd" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "4.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a334ef7c9e23abf0ce748e8cd309037da93e606ad52eb372e4ce327a0dcfbdfd" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "built" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56ed6191a7e78c36abdb16ab65341eefd73d64d303fffccdbb00d51e4205967b" + +[[package]] +name = "bumpalo" +version = "3.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" + +[[package]] +name = "bytemuck" +version = "1.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9134a6ef01ce4b366b50689c94f82c14bc72bc5d0386829828a2e2752ef7958c" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ecc273b49b3205b83d648f0690daa588925572cc5063745bfe547fe7ec8e1a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +dependencies = [ + "serde", +] + +[[package]] +name = "cairo-rs" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" +dependencies = [ + "bitflags 2.9.1", + "cairo-sys-rs", + "glib", + "libc", + "once_cell", + "thiserror 1.0.69", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "calamine" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138646b9af2c5d7f1804ea4bf93afc597737d2bd4f7341d67c48b03316976eb1" +dependencies = [ + "byteorder", + "codepage", + "encoding_rs", + "log", + "quick-xml 0.31.0", + "serde", + "zip", +] + +[[package]] +name = "camino" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror 2.0.12", +] + +[[package]] +name = "cargo_toml" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02260d489095346e5cafd04dea8e8cb54d1d74fcd759022a9b72986ebe9a1257" +dependencies = [ + "serde", + "toml", +] + +[[package]] +name = "cc" +version = "1.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16595d3be041c03b09d08d0858631facccee9221e579704070e6e9e4915d3bc7" +dependencies = [ + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfb" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" +dependencies = [ + "byteorder", + "fnv", + "uuid", +] + +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chrono" +version = "0.4.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "cidre" +version = "0.5.0" +source = "git+https://github.com/mediar-ai/cidre.git?rev=efb9e060c6f8edc48551365c2e80d3e8c6887433#efb9e060c6f8edc48551365c2e80d3e8c6887433" +dependencies = [ + "cidre-macros", + "parking_lot", + "tokio", +] + +[[package]] +name = "cidre-macros" +version = "0.1.0" +source = "git+https://github.com/mediar-ai/cidre.git?rev=efb9e060c6f8edc48551365c2e80d3e8c6887433#efb9e060c6f8edc48551365c2e80d3e8c6887433" + +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading 0.8.8", +] + +[[package]] +name = "clipboard-win" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15efe7a882b08f34e38556b14f2fb3daa98769d06c7f0c1b076dfd0d983bc892" +dependencies = [ + "error-code", +] + +[[package]] +name = "cocoa" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad36507aeb7e16159dfe68db81ccc27571c3ccd4b76fb2fb72fc59e7a4b1b64c" +dependencies = [ + "bitflags 2.9.1", + "block", + "cocoa-foundation", + "core-foundation 0.10.1", + "core-graphics", + "foreign-types 0.5.0", + "libc", + "objc", +] + +[[package]] +name = "cocoa-foundation" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81411967c50ee9a1fc11365f8c585f863a22a9697c89239c452292c40ba79b0d" +dependencies = [ + "bitflags 2.9.1", + "block", + "core-foundation 0.10.1", + "core-graphics-types", + "objc", +] + +[[package]] +name = "codepage" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48f68d061bc2828ae826206326e61251aca94c1e4a5305cf52d9138639c918b4" +dependencies = [ + "encoding_rs", +] + +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "time", + "version_check", +] + +[[package]] +name = "cookie-factory" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9885fa71e26b8ab7855e2ec7cae6e9b380edff76cd052e07c683a0319d51b3a2" +dependencies = [ + "futures", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" +dependencies = [ + "bitflags 2.9.1", + "core-foundation 0.10.1", + "core-graphics-types", + "foreign-types 0.5.0", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +dependencies = [ + "bitflags 2.9.1", + "core-foundation 0.10.1", + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "cssparser" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "754b69d351cdc2d8ee09ae203db831e005560fc6030da058f86ad60c92a9cb0a" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa 0.4.8", + "matches", + "phf 0.8.0", + "proc-macro2", + "quote", + "smallvec", + "syn 1.0.109", +] + +[[package]] +name = "cssparser-macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +dependencies = [ + "quote", + "syn 2.0.101", +] + +[[package]] +name = "ctor" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" +dependencies = [ + "quote", + "syn 2.0.101", +] + +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.101", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "deranged" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +dependencies = [ + "powerfmt", + "serde", +] + +[[package]] +name = "derive_arbitrary" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "derive_more" +version = "0.99.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" +dependencies = [ + "convert_case 0.4.0", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.101", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys 0.4.1", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys 0.5.0", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users 0.4.6", + "windows-sys 0.48.0", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users 0.5.0", + "windows-sys 0.59.0", +] + +[[package]] +name = "dispatch" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" + +[[package]] +name = "dispatch2" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a0d569e003ff27784e0e14e4a594048698e0c0f0b66cabcb51511be55a7caa0" +dependencies = [ + "bitflags 2.9.1", + "block2 0.6.1", + "libc", + "objc2 0.6.1", +] + +[[package]] +name = "dispatch2" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" +dependencies = [ + "bitflags 2.9.1", + "block2 0.6.1", + "libc", + "objc2 0.6.1", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "dlib" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" +dependencies = [ + "libloading 0.8.8", +] + +[[package]] +name = "dlopen2" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1297103d2bbaea85724fcee6294c2d50b1081f9ad47d0f6f6f61eda65315a6" +dependencies = [ + "dlopen2_derive", + "libc", + "once_cell", + "winapi", +] + +[[package]] +name = "dlopen2_derive" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "788160fb30de9cdd857af31c6a2675904b16ece8fc2737b2c7127ba368c9d0f4" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + +[[package]] +name = "dpi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" +dependencies = [ + "serde", +] + +[[package]] +name = "drm" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80bc8c5c6c2941f70a55c15f8d9f00f9710ebda3ffda98075f996a0e6c92756f" +dependencies = [ + "bitflags 2.9.1", + "bytemuck", + "drm-ffi", + "drm-fourcc", + "libc", + "rustix 0.38.44", +] + +[[package]] +name = "drm-ffi" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8e41459d99a9b529845f6d2c909eb9adf3b6d2f82635ae40be8de0601726e8b" +dependencies = [ + "drm-sys", + "rustix 0.38.44", +] + +[[package]] +name = "drm-fourcc" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0aafbcdb8afc29c1a7ee5fbe53b5d62f4565b35a042a662ca9fecd0b54dae6f4" + +[[package]] +name = "drm-sys" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bafb66c8dbc944d69e15cfcc661df7e703beffbaec8bd63151368b06c5f9858c" +dependencies = [ + "libc", + "linux-raw-sys 0.6.5", +] + +[[package]] +name = "dtoa" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6add3b8cff394282be81f3fc1a0605db594ed69890078ca6e2cab1c408bcf04" + +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dyn-clone" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c7a8fb8a9fbf66c1f703fe16184d10ca0ee9d23be5b4436400408ba54a95005" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "embed-resource" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8fe7d068ca6b3a5782ca5ec9afc244acd99dd441e4686a83b1c3973aba1d489" +dependencies = [ + "cc", + "memchr", + "rustc_version", + "toml", + "vswhom", + "winreg", +] + +[[package]] +name = "embed_plist" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "endi" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf" + +[[package]] +name = "enumflags2" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba2f4b465f5318854c6f8dd686ede6c0a9dc67d4b1ac241cf0eb51521a309147" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc4caf64a58d7a6d65ab00639b046ff54399a39f5f2554728895ace4b297cd79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "erased-serde" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e004d887f51fcb9fef17317a2f3525c887d8aa3f4f50fed920816a688284a5b7" +dependencies = [ + "serde", + "typeid", +] + +[[package]] +name = "errno" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "error-code" +version = "3.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" + +[[package]] +name = "event-listener" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3492acde4c3fc54c845eaab3eed8bd00c7a7d881f78bfc801e43a93dec1331ae" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + +[[package]] +name = "exr" +version = "1.73.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83197f59927b46c04a183a619b7c29df34e63e63c7869320862268c0ef687e0" +dependencies = [ + "bit_field", + "half", + "lebe", + "miniz_oxide", + "rayon-core", + "smallvec", + "zune-inflate", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "rustc_version", +] + +[[package]] +name = "filetime" +version = "0.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586" +dependencies = [ + "cfg-if", + "libc", + "libredox", + "windows-sys 0.59.0", +] + +[[package]] +name = "flate2" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared 0.1.1", +] + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared 0.3.1", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" +dependencies = [ + "mac", + "new_debug_unreachable", +] + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-lite" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5edaec856126859abb19ed65f39e90fea3a9574b9707f13539acf4abf7eb532" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + +[[package]] +name = "gbm" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce852e998d3ca5e4a97014fb31c940dc5ef344ec7d364984525fd11e8a547e6a" +dependencies = [ + "bitflags 2.9.1", + "drm", + "drm-fourcc", + "gbm-sys", + "libc", + "wayland-backend", + "wayland-server", +] + +[[package]] +name = "gbm-sys" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c13a5f2acc785d8fb6bf6b7ab6bfb0ef5dad4f4d97e8e70bb8e470722312f76f" +dependencies = [ + "libc", +] + +[[package]] +name = "gdk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691" +dependencies = [ + "cairo-rs", + "gdk-pixbuf", + "gdk-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" +dependencies = [ + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", + "once_cell", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkwayland-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69" +dependencies = [ + "gdk-sys", + "glib-sys", + "gobject-sys", + "libc", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkx11" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3caa00e14351bebbc8183b3c36690327eb77c49abc2268dd4bd36b856db3fbfe" +dependencies = [ + "gdk", + "gdkx11-sys", + "gio", + "glib", + "libc", + "x11", +] + +[[package]] +name = "gdkx11-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d" +dependencies = [ + "gdk-sys", + "glib-sys", + "libc", + "system-deps", + "x11", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "gethostname" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0176e0459c2e4a1fe232f984bca6890e681076abb9934f6cea7c326f3fc47818" +dependencies = [ + "libc", + "windows-targets 0.48.5", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", + "wasm-bindgen", +] + +[[package]] +name = "gif" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fb2d69b19215e18bb912fa30f7ce15846e301408695e44e0ef719f1da9e19f2" +dependencies = [ + "color_quant", + "weezl", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "gio" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "once_cell", + "pin-project-lite", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "gio-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "winapi", +] + +[[package]] +name = "gl" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a94edab108827d67608095e269cf862e60d920f144a5026d3dbcfd8b877fb404" +dependencies = [ + "gl_generator", +] + +[[package]] +name = "gl_generator" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a95dfc23a2b4a9a2f5ab41d194f8bfda3cabec42af4e39f08c339eb2a0c124d" +dependencies = [ + "khronos_api", + "log", + "xml-rs", +] + +[[package]] +name = "glib" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" +dependencies = [ + "bitflags 2.9.1", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "once_cell", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "glib-macros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" +dependencies = [ + "heck 0.4.1", + "proc-macro-crate 2.0.0", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "glib-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "glob" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" + +[[package]] +name = "gobject-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gtk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a" +dependencies = [ + "atk", + "cairo-rs", + "field-offset", + "futures-channel", + "gdk", + "gdk-pixbuf", + "gio", + "glib", + "gtk-sys", + "gtk3-macros", + "libc", + "pango", + "pkg-config", +] + +[[package]] +name = "gtk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414" +dependencies = [ + "atk-sys", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "gtk3-macros" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "h2" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9421a676d1b147b16b82c9225157dc629087ef8ec4d5e2960f9437a90dac0a5" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap 2.9.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "half" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9" +dependencies = [ + "cfg-if", + "crunchy", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f154ce46856750ed433c8649605bf7ed2de3bc35fd9d2a9f30cddd873c80cb08" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "home" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "html5ever" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bea68cab48b8459f17cf1c944c67ddc572d272d9f2b274140f223ecb1da4a3b7" +dependencies = [ + "log", + "mac", + "markup5ever", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "http" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +dependencies = [ + "bytes", + "fnv", + "itoa 1.0.15", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "itoa 1.0.15", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03a01595e11bdcec50946522c32dde3fc6914743000a68b93000965f2f02406d" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c293b6b3d21eca78250dc7dbebd6b9210ec5530e038cbfe0661b5c47ab06e8" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "system-configuration", + "tokio", + "tower-service", + "tracing", + "windows-registry", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core 0.61.2", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ico" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc50b891e4acf8fe0e71ef88ec43ad82ee07b3810ad09de10f1d01f072ed4b98" +dependencies = [ + "byteorder", + "png", +] + +[[package]] +name = "icu_collections" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" + +[[package]] +name = "icu_properties" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "potential_utf", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" + +[[package]] +name = "icu_provider" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +dependencies = [ + "displaydoc", + "icu_locale_core", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "image" +version = "0.25.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db35664ce6b9810857a38a906215e75a9c879f0696556a39f59c62829710251a" +dependencies = [ + "bytemuck", + "byteorder-lite", + "color_quant", + "exr", + "gif", + "image-webp", + "num-traits", + "png", + "qoi", + "ravif", + "rayon", + "rgb", + "tiff", + "zune-core", + "zune-jpeg", +] + +[[package]] +name = "image-webp" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b77d01e822461baa8409e156015a1d91735549f0f2c17691bd2d996bef238f7f" +dependencies = [ + "byteorder-lite", + "quick-error", +] + +[[package]] +name = "imgref" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0263a3d970d5c054ed9312c0057b4f3bde9c0b33836d3637361d4a9e6e7a408" + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" +dependencies = [ + "equivalent", + "hashbrown 0.15.3", + "serde", +] + +[[package]] +name = "infer" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7" +dependencies = [ + "cfb", +] + +[[package]] +name = "interpolate_name" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iri-string" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "javascriptcore-rs" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc" +dependencies = [ + "bitflags 1.3.2", + "glib", + "javascriptcore-rs-sys", +] + +[[package]] +name = "javascriptcore-rs-sys" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[package]] +name = "jobserver" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" +dependencies = [ + "getrandom 0.3.3", + "libc", +] + +[[package]] +name = "jpeg-decoder" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0" + +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "json-patch" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "863726d7afb6bc2590eeff7135d923545e5e964f004c2ccf8716c25e70a86f08" +dependencies = [ + "jsonptr", + "serde", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] +name = "jsonptr" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dea2b27dd239b2556ed7a25ba842fe47fd602e7fc7433c2a8d6106d4d9edd70" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "keyboard-types" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" +dependencies = [ + "bitflags 2.9.1", + "serde", + "unicode-segmentation", +] + +[[package]] +name = "khronos-egl" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "khronos_api" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" + +[[package]] +name = "kuchikiki" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f29e4755b7b995046f510a7520c42b2fed58b77bd94d5a87a8eb43d2fd126da8" +dependencies = [ + "cssparser", + "html5ever", + "indexmap 1.9.3", + "matches", + "selectors", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + +[[package]] +name = "lebe" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" + +[[package]] +name = "libappindicator" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a" +dependencies = [ + "glib", + "gtk", + "gtk-sys", + "libappindicator-sys", + "log", +] + +[[package]] +name = "libappindicator-sys" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" +dependencies = [ + "gtk-sys", + "libloading 0.7.4", + "once_cell", +] + +[[package]] +name = "libc" +version = "0.2.172" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" + +[[package]] +name = "libfuzzer-sys" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf78f52d400cf2d84a3a973a78a592b4adc535739e0a5597a0da6f0c357adc75" +dependencies = [ + "arbitrary", + "cc", +] + +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "libloading" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" +dependencies = [ + "cfg-if", + "windows-targets 0.53.0", +] + +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags 2.9.1", + "libc", + "redox_syscall", +] + +[[package]] +name = "libspa" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65f3a4b81b2a2d8c7f300643676202debd1b7c929dbf5c9bb89402ea11d19810" +dependencies = [ + "bitflags 2.9.1", + "cc", + "convert_case 0.6.0", + "cookie-factory", + "libc", + "libspa-sys", + "nix 0.27.1", + "nom", + "system-deps", +] + +[[package]] +name = "libspa-sys" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf0d9716420364790e85cbb9d3ac2c950bde16a7dd36f3209b7dfdfc4a24d01f" +dependencies = [ + "bindgen", + "cc", + "system-deps", +] + +[[package]] +name = "libwayshot-xcap" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "558a3a7ca16a17a14adf8f051b3adcd7766d397532f5f6d6a48034db11e54c22" +dependencies = [ + "drm", + "gbm", + "gl", + "image", + "khronos-egl", + "memmap2", + "rustix 1.0.7", + "thiserror 2.0.12", + "tracing", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-protocols-wlr", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a385b1be4e5c3e362ad2ffa73c392e53f031eaa5b7d648e64cd87f27f6063d7" + +[[package]] +name = "linux-raw-sys" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" + +[[package]] +name = "litemap" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" + +[[package]] +name = "lock_api" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + +[[package]] +name = "loop9" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062" +dependencies = [ + "imgref", +] + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + +[[package]] +name = "markup5ever" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2629bb1404f3d34c2e921f21fd34ba00b206124c81f65c50b43b6aaefeb016" +dependencies = [ + "log", + "phf 0.10.1", + "phf_codegen 0.10.0", + "string_cache", + "string_cache_codegen", + "tendril", +] + +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + +[[package]] +name = "matches" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" + +[[package]] +name = "maybe-rayon" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" +dependencies = [ + "cfg-if", + "rayon", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "memmap2" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3f7eed9d3848f8b98834af67102b720745c4ec028fcd0aa0239277e7de374f" +dependencies = [ + "libc", +] + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "minisign-verify" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e856fdd13623a2f5f2f54676a4ee49502a96a80ef4a62bcedd23d52427c44d43" + +[[package]] +name = "miniz_oxide" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +dependencies = [ + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.59.0", +] + +[[package]] +name = "muda" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4de14a9b5d569ca68d7c891d613b390cf5ab4f851c77aaa2f9e435555d3d9492" +dependencies = [ + "crossbeam-channel", + "dpi", + "gtk", + "keyboard-types", + "objc2 0.6.1", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation 0.3.1", + "once_cell", + "png", + "serde", + "thiserror 2.0.12", + "windows-sys 0.59.0", +] + +[[package]] +name = "native-tls" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.9.1", + "jni-sys", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nix" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" +dependencies = [ + "bitflags 2.9.1", + "cfg-if", + "libc", +] + +[[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +dependencies = [ + "bitflags 2.9.1", + "cfg-if", + "cfg_aliases", + "libc", + "memoffset", +] + +[[package]] +name = "nodrop" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "noop_proc_macro" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" + +[[package]] +name = "ntapi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4" +dependencies = [ + "winapi", +] + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e613fc340b2220f734a8595782c551f1250e969d87d3be1ae0579e8d4065179" +dependencies = [ + "num_enum_derive", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56" +dependencies = [ + "proc-macro-crate 3.3.0", + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", +] + +[[package]] +name = "objc-foundation" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" +dependencies = [ + "block", + "objc", + "objc_id", +] + +[[package]] +name = "objc-sys" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" + +[[package]] +name = "objc2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" +dependencies = [ + "objc-sys", + "objc2-encode", +] + +[[package]] +name = "objc2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88c6597e14493ab2e44ce58f2fdecf095a51f12ca57bec060a11c57332520551" +dependencies = [ + "objc2-encode", + "objc2-exception-helper", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6f29f568bec459b0ddff777cec4fe3fd8666d82d5a40ebd0ff7e66134f89bcc" +dependencies = [ + "bitflags 2.9.1", + "block2 0.6.1", + "libc", + "objc2 0.6.1", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image", + "objc2-foundation 0.3.1", + "objc2-quartz-core 0.3.1", +] + +[[package]] +name = "objc2-av-foundation" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e085a2e16c61dadbad7a808fc9d5b5f8472b1b825b53d529c9f64ccac78e722" +dependencies = [ + "bitflags 2.9.1", + "block2 0.6.1", + "dispatch2 0.3.0", + "objc2 0.6.1", + "objc2-avf-audio", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image", + "objc2-core-video", + "objc2-foundation 0.3.1", + "objc2-quartz-core 0.3.1", +] + +[[package]] +name = "objc2-avf-audio" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfc1d11521c211a7ebe17739fc806719da41f56c6b3f949d9861b459188ce910" +dependencies = [ + "objc2 0.6.1", + "objc2-foundation 0.3.1", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17614fdcd9b411e6ff1117dfb1d0150f908ba83a7df81b1f118005fe0a8ea15d" +dependencies = [ + "bitflags 2.9.1", + "objc2 0.6.1", + "objc2-foundation 0.3.1", +] + +[[package]] +name = "objc2-core-audio" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca44961e888e19313b808f23497073e3f6b3c22bb485056674c8b49f3b025c82" +dependencies = [ + "dispatch2 0.3.0", + "objc2 0.6.1", + "objc2-core-audio-types", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-core-audio-types" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f1cc99bb07ad2ddb6527ddf83db6a15271bb036b3eb94b801cd44fdc666ee1" +dependencies = [ + "bitflags 2.9.1", + "objc2 0.6.1", +] + +[[package]] +name = "objc2-core-data" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291fbbf7d29287518e8686417cf7239c74700fd4b607623140a7d4a3c834329d" +dependencies = [ + "bitflags 2.9.1", + "objc2 0.6.1", + "objc2-foundation 0.3.1", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166" +dependencies = [ + "bitflags 2.9.1", + "block2 0.6.1", + "dispatch2 0.3.0", + "libc", + "objc2 0.6.1", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "989c6c68c13021b5c2d6b71456ebb0f9dc78d752e86a98da7c716f4f9470f5a4" +dependencies = [ + "bitflags 2.9.1", + "block2 0.6.1", + "dispatch2 0.3.0", + "libc", + "objc2 0.6.1", + "objc2-core-foundation", + "objc2-io-surface", + "objc2-metal 0.3.1", +] + +[[package]] +name = "objc2-core-image" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79b3dc0cc4386b6ccf21c157591b34a7f44c8e75b064f85502901ab2188c007e" +dependencies = [ + "objc2 0.6.1", + "objc2-foundation 0.3.1", +] + +[[package]] +name = "objc2-core-media" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0b7afa6822e2fa20dfc88d10186b2432bf8560b5ed73ec9d31efd78277bc878" +dependencies = [ + "bitflags 2.9.1", + "block2 0.6.1", + "dispatch2 0.3.0", + "objc2 0.6.1", + "objc2-core-audio", + "objc2-core-audio-types", + "objc2-core-foundation", + "objc2-core-video", +] + +[[package]] +name = "objc2-core-video" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1989c3e76c7e978cab0ba9e6f4961cd00ed14ca21121444cc26877403bfb6303" +dependencies = [ + "bitflags 2.9.1", + "block2 0.6.1", + "objc2 0.6.1", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-io-surface", + "objc2-metal 0.3.1", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-exception-helper" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a1c5fbb72d7735b076bb47b578523aedc40f3c439bea6dfd595c089d79d98a" +dependencies = [ + "cc", +] + +[[package]] +name = "objc2-foundation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" +dependencies = [ + "bitflags 2.9.1", + "block2 0.5.1", + "libc", + "objc2 0.5.2", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900831247d2fe1a09a683278e5384cfb8c80c79fe6b166f9d14bfdde0ea1b03c" +dependencies = [ + "bitflags 2.9.1", + "block2 0.6.1", + "libc", + "objc2 0.6.1", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7282e9ac92529fa3457ce90ebb15f4ecbc383e8338060960760fa2cf75420c3c" +dependencies = [ + "bitflags 2.9.1", + "objc2 0.6.1", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-metal" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" +dependencies = [ + "bitflags 2.9.1", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-metal" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f246c183239540aab1782457b35ab2040d4259175bd1d0c58e46ada7b47a874" +dependencies = [ + "bitflags 2.9.1", + "objc2 0.6.1", + "objc2-foundation 0.3.1", +] + +[[package]] +name = "objc2-osa-kit" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26bb88504b5a050dbba515d2414607bf5e57dd56b107bc5f0351197a3e7bdc5d" +dependencies = [ + "bitflags 2.9.1", + "objc2 0.6.1", + "objc2-app-kit", + "objc2-foundation 0.3.1", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" +dependencies = [ + "bitflags 2.9.1", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-metal 0.2.2", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90ffb6a0cd5f182dc964334388560b12a57f7b74b3e2dec5e2722aa2dfb2ccd5" +dependencies = [ + "bitflags 2.9.1", + "objc2 0.6.1", + "objc2-foundation 0.3.1", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25b1312ad7bc8a0e92adae17aa10f90aae1fb618832f9b993b022b591027daed" +dependencies = [ + "bitflags 2.9.1", + "objc2 0.6.1", + "objc2-core-foundation", + "objc2-foundation 0.3.1", +] + +[[package]] +name = "objc2-web-kit" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91672909de8b1ce1c2252e95bbee8c1649c9ad9d14b9248b3d7b4c47903c47ad" +dependencies = [ + "bitflags 2.9.1", + "block2 0.6.1", + "objc2 0.6.1", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation 0.3.1", +] + +[[package]] +name = "objc_id" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" +dependencies = [ + "objc", +] + +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "openssl" +version = "0.10.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" +dependencies = [ + "bitflags 2.9.1", + "cfg-if", + "foreign-types 0.3.2", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "openssl-sys" +version = "0.9.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "osakit" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "732c71caeaa72c065bb69d7ea08717bd3f4863a4f451402fc9513e29dbd5261b" +dependencies = [ + "objc2 0.6.1", + "objc2-foundation 0.3.1", + "objc2-osa-kit", + "serde", + "serde_json", + "thiserror 2.0.12", +] + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "pango" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" +dependencies = [ + "gio", + "glib", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.6", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "phf" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" +dependencies = [ + "phf_macros 0.8.0", + "phf_shared 0.8.0", + "proc-macro-hack", +] + +[[package]] +name = "phf" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" +dependencies = [ + "phf_shared 0.10.0", +] + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_macros 0.11.3", + "phf_shared 0.11.3", +] + +[[package]] +name = "phf_codegen" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815" +dependencies = [ + "phf_generator 0.8.0", + "phf_shared 0.8.0", +] + +[[package]] +name = "phf_codegen" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb1c3a8bc4dd4e5cfce29b44ffc14bedd2ee294559a294e2a4d4c9e9a6a13cd" +dependencies = [ + "phf_generator 0.10.0", + "phf_shared 0.10.0", +] + +[[package]] +name = "phf_generator" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526" +dependencies = [ + "phf_shared 0.8.0", + "rand 0.7.3", +] + +[[package]] +name = "phf_generator" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" +dependencies = [ + "phf_shared 0.10.0", + "rand 0.8.5", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared 0.11.3", + "rand 0.8.5", +] + +[[package]] +name = "phf_macros" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6fde18ff429ffc8fe78e2bf7f8b7a5a5a6e2a8b58bc5a9ac69198bbda9189c" +dependencies = [ + "phf_generator 0.8.0", + "phf_shared 0.8.0", + "proc-macro-hack", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "phf_macros" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "phf_shared" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7" +dependencies = [ + "siphasher 0.3.11", +] + +[[package]] +name = "phf_shared" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" +dependencies = [ + "siphasher 0.3.11", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher 1.0.1", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "piper" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + +[[package]] +name = "pipewire" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08e645ba5c45109106d56610b3ee60eb13a6f2beb8b74f8dc8186cf261788dda" +dependencies = [ + "anyhow", + "bitflags 2.9.1", + "libc", + "libspa", + "libspa-sys", + "nix 0.27.1", + "once_cell", + "pipewire-sys", + "thiserror 1.0.69", +] + +[[package]] +name = "pipewire-sys" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "849e188f90b1dda88fe2bfe1ad31fe5f158af2c98f80fb5d13726c44f3f01112" +dependencies = [ + "bindgen", + "libspa-sys", + "system-deps", +] + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "plist" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac26e981c03a6e53e0aee43c113e3202f5581d5360dae7bd2c70e800dd0451d" +dependencies = [ + "base64 0.22.1", + "indexmap 2.9.0", + "quick-xml 0.32.0", + "serde", + "time", +] + +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "polling" +version = "3.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b53a684391ad002dd6a596ceb6c74fd004fdce75f4be2e3f615068abbea5fd50" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix 1.0.7", + "tracing", + "windows-sys 0.59.0", +] + +[[package]] +name = "potential_utf" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e8366a6159044a37876a2b9817124296703c586a5c92e2c53751fa06d8d43e8" +dependencies = [ + "toml_edit 0.20.7", +] + +[[package]] +name = "proc-macro-crate" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" +dependencies = [ + "toml_edit 0.22.26", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro-hack" +version = "0.5.20+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" + +[[package]] +name = "proc-macro2" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "profiling" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afbdc74edc00b6f6a218ca6a5364d6226a259d4b8ea1af4a0ea063f27e179f4d" +dependencies = [ + "profiling-procmacros", +] + +[[package]] +name = "profiling-procmacros" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a65f2e60fbf1063868558d69c6beacf412dc755f9fc020f514b7955fc914fe30" +dependencies = [ + "quote", + "syn 2.0.101", +] + +[[package]] +name = "qoi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + +[[package]] +name = "quick-xml" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eff6510e86862b57b210fd8cbe8ed3f0d7d600b9c2863cd4549a2e033c66e956" +dependencies = [ + "memchr", +] + +[[package]] +name = "quick-xml" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33" +dependencies = [ + "encoding_rs", + "memchr", +] + +[[package]] +name = "quick-xml" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d3a6e5838b60e0e8fa7a43f22ade549a37d61f8bdbe636d0d7816191de969c2" +dependencies = [ + "memchr", +] + +[[package]] +name = "quick-xml" +version = "0.37.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" +dependencies = [ + "memchr", +] + +[[package]] +name = "quinn" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "626214629cda6781b6dc1d316ba307189c85ba657213ce642d9c77670f8202c8" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash 2.1.1", + "rustls", + "socket2", + "thiserror 2.0.12", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49df843a9161c85bb8aae55f101bc0bac8bcafd637a620d9122fd7e0b2f7422e" +dependencies = [ + "bytes", + "getrandom 0.3.3", + "lru-slab", + "rand 0.9.1", + "ring", + "rustc-hash 2.1.1", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.12", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee4e529991f949c5e25755532370b8af5d114acae52326361d68d47af64aa842" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.59.0", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc", + "rand_pcg", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.16", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.3", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "rand_pcg" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "rav1e" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd87ce80a7665b1cce111f8a16c1f3929f6547ce91ade6addf4ec86a8dda5ce9" +dependencies = [ + "arbitrary", + "arg_enum_proc_macro", + "arrayvec", + "av1-grain", + "bitstream-io", + "built", + "cfg-if", + "interpolate_name", + "itertools", + "libc", + "libfuzzer-sys", + "log", + "maybe-rayon", + "new_debug_unreachable", + "noop_proc_macro", + "num-derive", + "num-traits", + "once_cell", + "paste", + "profiling", + "rand 0.8.5", + "rand_chacha 0.3.1", + "simd_helpers", + "system-deps", + "thiserror 1.0.69", + "v_frame", + "wasm-bindgen", +] + +[[package]] +name = "ravif" +version = "0.11.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6a5f31fcf7500f9401fea858ea4ab5525c99f2322cfcee732c0e6c74208c0c6" +dependencies = [ + "avif-serialize", + "imgref", + "loop9", + "quick-error", + "rav1e", + "rayon", + "rgb", +] + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "rayon" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redox_syscall" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af" +dependencies = [ + "bitflags 2.9.1", +] + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.16", + "libredox", + "thiserror 1.0.69", +] + +[[package]] +name = "redox_users" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" +dependencies = [ + "getrandom 0.2.16", + "libredox", + "thiserror 2.0.12", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata 0.4.9", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "reqwest" +version = "0.12.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e98ff6b0dbbe4d5a37318f433d4fc82babd21631f194d370409ceb2e40b2f0b5" +dependencies = [ + "base64 0.22.1", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "mime_guess", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tokio-rustls", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "webpki-roots", +] + +[[package]] +name = "rfd" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80c844748fdc82aae252ee4594a89b6e7ebef1063de7951545564cbc4e57075d" +dependencies = [ + "ashpd", + "block2 0.6.1", + "dispatch2 0.2.0", + "glib-sys", + "gobject-sys", + "gtk-sys", + "js-sys", + "log", + "objc2 0.6.1", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation 0.3.1", + "raw-window-handle", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-sys 0.59.0", +] + +[[package]] +name = "rgb" +version = "0.8.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57397d16646700483b67d2dd6511d79318f9d057fdbd21a4066aeac8b41d310a" + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rust_xlsxwriter" +version = "0.79.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c743cb9f2a4524676020e26ee5f298445a82d882b09956811b1e78ca7e42b440" +dependencies = [ + "zip", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.9.1", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" +dependencies = [ + "bitflags 2.9.1", + "errno", + "libc", + "linux-raw-sys 0.9.4", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustls" +version = "0.23.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "730944ca083c1c233a75c09f199e973ca499344a2b7ba9e755c457e86fb4a321" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" + +[[package]] +name = "rusty-tesseract" +version = "1.1.10" +source = "git+https://github.com/louis030195/rusty-tesseract.git?branch=main#08346c1de08d122c7121425abc79081c30dbf778" +dependencies = [ + "dirs 5.0.1", + "image", + "log", + "subprocess", + "substring", + "tempfile", + "thiserror 1.0.69", + "which", +] + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "schemars" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +dependencies = [ + "dyn-clone", + "indexmap 1.9.3", + "schemars_derive", + "serde", + "serde_json", + "url", + "uuid", +] + +[[package]] +name = "schemars_derive" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.101", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.9.1", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "selectors" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df320f1889ac4ba6bc0cdc9c9af7af4bd64bb927bccdf32d81140dc1f9be12fe" +dependencies = [ + "bitflags 1.3.2", + "cssparser", + "derive_more", + "fxhash", + "log", + "matches", + "phf 0.8.0", + "phf_codegen 0.8.0", + "precomputed-hash", + "servo_arc", + "smallvec", + "thin-slice", +] + +[[package]] +name = "semver" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" +dependencies = [ + "serde", +] + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde-untagged" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "299d9c19d7d466db4ab10addd5703e4c615dec2a5a16dbbafe191045e87ee66e" +dependencies = [ + "erased-serde", + "serde", + "typeid", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "serde_json" +version = "1.0.140" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +dependencies = [ + "itoa 1.0.15", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "serde_spanned" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa 1.0.15", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "3.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6b6f7f2fcb69f747921f79f3926bd1e203fce4fef62c268dd3abfb6d86029aa" +dependencies = [ + "base64 0.22.1", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.9.0", + "serde", + "serde_derive", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d00caa5193a3c8362ac2b73be6b9e768aa5a4b2f721d8f4b339600c3cb51f8e" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "serialize-to-javascript" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9823f2d3b6a81d98228151fdeaf848206a7855a7a042bbf9bf870449a66cafb" +dependencies = [ + "serde", + "serde_json", + "serialize-to-javascript-impl", +] + +[[package]] +name = "serialize-to-javascript-impl" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74064874e9f6a15f04c1f3cb627902d0e6b410abbf36668afa873c61889f1763" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "servo_arc" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98238b800e0d1576d8b6e3de32827c2d74bee68bb97748dcf5071fb53965432" +dependencies = [ + "nodrop", + "stable_deref_trait", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" +dependencies = [ + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + +[[package]] +name = "simd_helpers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6" +dependencies = [ + "quote", +] + +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" + +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "softbuffer" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18051cdd562e792cad055119e0cdb2cfc137e44e3987532e0f9659a77931bb08" +dependencies = [ + "bytemuck", + "cfg_aliases", + "core-graphics", + "foreign-types 0.5.0", + "js-sys", + "log", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-quartz-core 0.2.2", + "raw-window-handle", + "redox_syscall", + "wasm-bindgen", + "web-sys", + "windows-sys 0.59.0", +] + +[[package]] +name = "soup3" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f" +dependencies = [ + "futures-channel", + "gio", + "glib", + "libc", + "soup3-sys", +] + +[[package]] +name = "soup3-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "string_cache" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared 0.11.3", + "precomputed-hash", + "serde", +] + +[[package]] +name = "string_cache_codegen" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", + "proc-macro2", + "quote", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subprocess" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c2e86926081dda636c546d8c5e641661049d7562a68f5488be4a1f7f66f6086" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "substring" +version = "1.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ee6433ecef213b2e72f587ef64a2f5943e7cd16fbd82dbe8bc07486c534c86" +dependencies = [ + "autocfg", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "swift-rs" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4057c98e2e852d51fdcfca832aac7b571f6b351ad159f9eda5db1655f8d0c4d7" +dependencies = [ + "base64 0.21.7", + "serde", + "serde_json", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "sysinfo" +version = "0.34.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4b93974b3d3aeaa036504b8eefd4c039dced109171c1ae973f1dc63b2c7e4b2" +dependencies = [ + "libc", + "memchr", + "ntapi", + "objc2-core-foundation", + "windows 0.57.0", +] + +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags 2.9.1", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck 0.5.0", + "pkg-config", + "toml", + "version-compare", +] + +[[package]] +name = "tao" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e59c1f38e657351a2e822eadf40d6a2ad4627b9c25557bc1180ec1b3295ef82" +dependencies = [ + "bitflags 2.9.1", + "core-foundation 0.10.1", + "core-graphics", + "crossbeam-channel", + "dispatch", + "dlopen2", + "dpi", + "gdkwayland-sys", + "gdkx11-sys", + "gtk", + "jni", + "lazy_static", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-sys", + "objc2 0.6.1", + "objc2-app-kit", + "objc2-foundation 0.3.1", + "once_cell", + "parking_lot", + "raw-window-handle", + "scopeguard", + "tao-macros", + "unicode-segmentation", + "url", + "windows 0.61.1", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "tao-macros" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "tar" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a" +dependencies = [ + "filetime", + "libc", + "xattr", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "tauri" +version = "2.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7b0bc1aec81bda6bc455ea98fcaed26b3c98c1648c627ad6ff1c704e8bf8cbc" +dependencies = [ + "anyhow", + "bytes", + "dirs 6.0.0", + "dunce", + "embed_plist", + "futures-util", + "getrandom 0.2.16", + "glob", + "gtk", + "heck 0.5.0", + "http", + "jni", + "libc", + "log", + "mime", + "muda", + "objc2 0.6.1", + "objc2-app-kit", + "objc2-foundation 0.3.1", + "objc2-ui-kit", + "percent-encoding", + "plist", + "raw-window-handle", + "reqwest", + "serde", + "serde_json", + "serde_repr", + "serialize-to-javascript", + "swift-rs", + "tauri-build", + "tauri-macros", + "tauri-runtime", + "tauri-runtime-wry", + "tauri-utils", + "thiserror 2.0.12", + "tokio", + "tray-icon", + "url", + "urlpattern", + "webkit2gtk", + "webview2-com", + "window-vibrancy", + "windows 0.61.1", +] + +[[package]] +name = "tauri-build" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7a0350f0df1db385ca5c02888a83e0e66655c245b7443db8b78a70da7d7f8fc" +dependencies = [ + "anyhow", + "cargo_toml", + "dirs 6.0.0", + "glob", + "heck 0.5.0", + "json-patch", + "schemars", + "semver", + "serde", + "serde_json", + "tauri-utils", + "tauri-winres", + "toml", + "walkdir", +] + +[[package]] +name = "tauri-codegen" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93f035551bf7b11b3f51ad9bc231ebbe5e085565527991c16cf326aa38cdf47" +dependencies = [ + "base64 0.22.1", + "brotli", + "ico", + "json-patch", + "plist", + "png", + "proc-macro2", + "quote", + "semver", + "serde", + "serde_json", + "sha2", + "syn 2.0.101", + "tauri-utils", + "thiserror 2.0.12", + "time", + "url", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-macros" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8db4df25e2d9d45de0c4c910da61cd5500190da14ae4830749fee3466dddd112" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.101", + "tauri-codegen", + "tauri-utils", +] + +[[package]] +name = "tauri-plugin" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37a5ebe6a610d1b78a94650896e6f7c9796323f408800cef436e0fa0539de601" +dependencies = [ + "anyhow", + "glob", + "plist", + "schemars", + "serde", + "serde_json", + "tauri-utils", + "toml", + "walkdir", +] + +[[package]] +name = "tauri-plugin-dialog" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a33318fe222fc2a612961de8b0419e2982767f213f54a4d3a21b0d7b85c41df8" +dependencies = [ + "log", + "raw-window-handle", + "rfd", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "tauri-plugin-fs", + "thiserror 2.0.12", + "url", +] + +[[package]] +name = "tauri-plugin-fs" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ead0daec5d305adcefe05af9d970fc437bcc7996052d564e7393eb291252da" +dependencies = [ + "anyhow", + "dunce", + "glob", + "percent-encoding", + "schemars", + "serde", + "serde_json", + "serde_repr", + "tauri", + "tauri-plugin", + "tauri-utils", + "thiserror 2.0.12", + "toml", + "url", +] + +[[package]] +name = "tauri-plugin-updater" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f05c38afd77a4b8fd98e8fb6f1cdbb5fbb8a46ba181eb2758b05321e3c6209" +dependencies = [ + "base64 0.22.1", + "dirs 6.0.0", + "flate2", + "futures-util", + "http", + "infer", + "log", + "minisign-verify", + "osakit", + "percent-encoding", + "reqwest", + "semver", + "serde", + "serde_json", + "tar", + "tauri", + "tauri-plugin", + "tempfile", + "thiserror 2.0.12", + "time", + "tokio", + "url", + "windows-sys 0.59.0", + "zip", +] + +[[package]] +name = "tauri-runtime" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00f004905d549854069e6774533d742b03cacfd6f03deb08940a8677586cbe39" +dependencies = [ + "cookie", + "dpi", + "gtk", + "http", + "jni", + "objc2 0.6.1", + "objc2-ui-kit", + "raw-window-handle", + "serde", + "serde_json", + "tauri-utils", + "thiserror 2.0.12", + "url", + "windows 0.61.1", +] + +[[package]] +name = "tauri-runtime-wry" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f85d056f4d4b014fe874814034f3416d57114b617a493a4fe552580851a3f3a2" +dependencies = [ + "gtk", + "http", + "jni", + "log", + "objc2 0.6.1", + "objc2-app-kit", + "objc2-foundation 0.3.1", + "once_cell", + "percent-encoding", + "raw-window-handle", + "softbuffer", + "tao", + "tauri-runtime", + "tauri-utils", + "url", + "webkit2gtk", + "webview2-com", + "windows 0.61.1", + "wry", +] + +[[package]] +name = "tauri-utils" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2900399c239a471bcff7f15c4399eb1a8c4fe511ba2853e07c996d771a5e0a4" +dependencies = [ + "anyhow", + "brotli", + "cargo_metadata", + "ctor", + "dunce", + "glob", + "html5ever", + "http", + "infer", + "json-patch", + "kuchikiki", + "log", + "memchr", + "phf 0.11.3", + "proc-macro2", + "quote", + "regex", + "schemars", + "semver", + "serde", + "serde-untagged", + "serde_json", + "serde_with", + "swift-rs", + "thiserror 2.0.12", + "toml", + "url", + "urlpattern", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-winres" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8d321dbc6f998d825ab3f0d62673e810c861aac2d0de2cc2c395328f1d113b4" +dependencies = [ + "embed-resource", + "indexmap 2.9.0", + "toml", +] + +[[package]] +name = "tempfile" +version = "3.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" +dependencies = [ + "fastrand", + "getrandom 0.3.3", + "once_cell", + "rustix 1.0.7", + "windows-sys 0.59.0", +] + +[[package]] +name = "tendril" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" +dependencies = [ + "futf", + "mac", + "utf-8", +] + +[[package]] +name = "terminator" +version = "0.3.5" +dependencies = [ + "accessibility", + "accessibility-sys", + "anyhow", + "async-trait", + "base64 0.22.1", + "core-foundation 0.10.1", + "core-graphics", + "image", + "objc", + "objc-foundation", + "serde", + "serde_json", + "sysinfo", + "tempfile", + "thiserror 2.0.12", + "tokio", + "tracing", + "tracing-subscriber", + "uiautomation", + "uni-ocr", + "windows 0.61.1", + "xcap", +] + +[[package]] +name = "terminator-excel-example" +version = "0.0.0" +dependencies = [ + "anyhow", + "arboard", + "base64 0.22.1", + "calamine", + "chrono", + "reqwest", + "rust_xlsxwriter", + "serde", + "serde_json", + "tauri", + "tauri-build", + "tauri-plugin-dialog", + "tauri-plugin-fs", + "tauri-plugin-updater", + "terminator", + "thiserror 1.0.69", + "tokio", + "tokio-util", + "uuid", + "windows 0.58.0", +] + +[[package]] +name = "thin-slice" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaa81235c7058867fa8c0e7314f33dcce9c215f535d1913822a2b3f5e289f3c" + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl 2.0.12", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "tiff" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba1310fcea54c6a9a4fd1aad794ecc02c31682f6bfbecdf460bf19533eed1e3e" +dependencies = [ + "flate2", + "jpeg-decoder", + "weezl", +] + +[[package]] +name = "time" +version = "0.3.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +dependencies = [ + "deranged", + "itoa 1.0.15", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" + +[[package]] +name = "time-macros" +version = "0.2.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.45.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "tracing", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05ae329d1f08c4d17a59bed7ff5b5a769d062e64a62d34a3261b219e62cd5aae" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit 0.22.26", +] + +[[package]] +name = "toml_datetime" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap 2.9.0", + "toml_datetime", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.20.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70f427fce4d84c72b5b732388bf4a9f4531b53f74e2887e3ecb2481f68f66d81" +dependencies = [ + "indexmap 2.9.0", + "toml_datetime", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.22.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e" +dependencies = [ + "indexmap 2.9.0", + "serde", + "serde_spanned", + "toml_datetime", + "toml_write", + "winnow 0.7.10", +] + +[[package]] +name = "toml_write" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfb942dfe1d8e29a7ee7fcbde5bd2b9a25fb89aa70caea2eba3bee836ff41076" + +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fdb0c213ca27a9f57ab69ddb290fd80d970922355b83ae380b395d3986b8a2e" +dependencies = [ + "bitflags 2.9.1", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "tracing-core" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "tray-icon" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7eee98ec5c90daf179d55c20a49d8c0d043054ce7c26336c09a24d31f14fa0" +dependencies = [ + "crossbeam-channel", + "dirs 6.0.0", + "libappindicator", + "muda", + "objc2 0.6.1", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation 0.3.1", + "once_cell", + "png", + "serde", + "thiserror 2.0.12", + "windows-sys 0.59.0", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "typenum" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" + +[[package]] +name = "uds_windows" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" +dependencies = [ + "memoffset", + "tempfile", + "winapi", +] + +[[package]] +name = "uiautomation" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3379780cb40c35ca95daaaf75a6085006fa5c4b6d5320971ed58906e74a52a39" +dependencies = [ + "arboard", + "chrono", + "uiautomation_derive", + "windows 0.61.1", + "windows-core 0.61.2", +] + +[[package]] +name = "uiautomation_derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "124d2f62d6d5bf4c0fccac93800105816037faa9c155e57a9952a7d43664975f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "uni-ocr" +version = "0.1.0" +source = "git+https://github.com/mediar-ai/uniOCR?branch=main#51993e2f08ec0144ccc111abbee656a70cdd4c13" +dependencies = [ + "anyhow", + "base64 0.22.1", + "cidre", + "image", + "libc", + "reqwest", + "rusty-tesseract", + "serde", + "serde_json", + "tokio", + "tracing", + "url", + "windows 0.61.1", +] + +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range", +] + +[[package]] +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" + +[[package]] +name = "unic-ucd-ident" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", +] + +[[package]] +name = "unicase" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "urlpattern" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70acd30e3aa1450bc2eece896ce2ad0d178e9c079493819301573dae3c37ba6d" +dependencies = [ + "regex", + "serde", + "unic-ucd-ident", + "url", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" +dependencies = [ + "getrandom 0.3.3", + "js-sys", + "serde", + "wasm-bindgen", +] + +[[package]] +name = "v_frame" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f32aaa24bacd11e488aa9ba66369c7cd514885742c9fe08cfe85884db3e92b" +dependencies = [ + "aligned-vec", + "num-traits", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version-compare" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn 2.0.101", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "wayland-backend" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe770181423e5fc79d3e2a7f4410b7799d5aab1de4372853de3c6aa13ca24121" +dependencies = [ + "cc", + "downcast-rs", + "rustix 0.38.44", + "scoped-tls", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978fa7c67b0847dbd6a9f350ca2569174974cd4082737054dbb7fbb79d7d9a61" +dependencies = [ + "bitflags 2.9.1", + "rustix 0.38.44", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "779075454e1e9a521794fed15886323ea0feda3f8b0fc1390f5398141310422a" +dependencies = [ + "bitflags 2.9.1", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-wlr" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cb6cdc73399c0e06504c437fe3cf886f25568dd5454473d565085b36d6a8bbf" +dependencies = [ + "bitflags 2.9.1", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "896fdafd5d28145fce7958917d69f2fd44469b1d4e861cb5961bcbeebc6d1484" +dependencies = [ + "proc-macro2", + "quick-xml 0.37.5", + "quote", +] + +[[package]] +name = "wayland-server" +version = "0.31.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "485dfb8ccf0daa0d34625d34e6ac15f99e550a7999b6fd88a0835ccd37655785" +dependencies = [ + "bitflags 2.9.1", + "downcast-rs", + "rustix 0.38.44", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-sys" +version = "0.31.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbcebb399c77d5aa9fa5db874806ee7b4eba4e73650948e8f93963f128896615" +dependencies = [ + "dlib", + "libc", + "log", + "memoffset", + "pkg-config", +] + +[[package]] +name = "web-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webkit2gtk" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76b1bc1e54c581da1e9f179d0b38512ba358fb1af2d634a1affe42e37172361a" +dependencies = [ + "bitflags 1.3.2", + "cairo-rs", + "gdk", + "gdk-sys", + "gio", + "gio-sys", + "glib", + "glib-sys", + "gobject-sys", + "gtk", + "gtk-sys", + "javascriptcore-rs", + "libc", + "once_cell", + "soup3", + "webkit2gtk-sys", +] + +[[package]] +name = "webkit2gtk-sys" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62daa38afc514d1f8f12b8693d30d5993ff77ced33ce30cd04deebc267a6d57c" +dependencies = [ + "bitflags 1.3.2", + "cairo-sys-rs", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "gtk-sys", + "javascriptcore-rs-sys", + "libc", + "pkg-config", + "soup3-sys", + "system-deps", +] + +[[package]] +name = "webpki-roots" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2853738d1cc4f2da3a225c18ec6c3721abb31961096e9dbf5ab35fa88b19cfdb" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "webview2-com" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b542b5cfbd9618c46c2784e4d41ba218c336ac70d44c55e47b251033e7d85601" +dependencies = [ + "webview2-com-macros", + "webview2-com-sys", + "windows 0.61.1", + "windows-core 0.61.2", + "windows-implement 0.60.0", + "windows-interface 0.59.1", +] + +[[package]] +name = "webview2-com-macros" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d228f15bba3b9d56dde8bddbee66fa24545bd17b48d5128ccf4a8742b18e431" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "webview2-com-sys" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae2d11c4a686e4409659d7891791254cf9286d3cfe0eef54df1523533d22295" +dependencies = [ + "thiserror 2.0.12", + "windows 0.61.1", + "windows-core 0.61.2", +] + +[[package]] +name = "weezl" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a751b3277700db47d3e574514de2eced5e54dc8a5436a3bf7a0b248b2cee16f3" + +[[package]] +name = "which" +version = "6.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ee928febd44d98f2f459a4a79bd4d928591333a494a10a868418ac1b39cf1f" +dependencies = [ + "either", + "home", + "rustix 0.38.44", + "winsafe", +] + +[[package]] +name = "widestring" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd7cf3379ca1aac9eea11fba24fd7e315d621f8dfe35c8d7d2be8b793726e07d" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "window-vibrancy" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c" +dependencies = [ + "objc2 0.6.1", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation 0.3.1", + "raw-window-handle", + "windows-sys 0.59.0", + "windows-version", +] + +[[package]] +name = "windows" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143" +dependencies = [ + "windows-core 0.57.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" +dependencies = [ + "windows-core 0.58.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows" +version = "0.61.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5ee8f3d025738cb02bad7868bbb5f8a6327501e870bf51f1b455b0a2454a419" +dependencies = [ + "windows-collections", + "windows-core 0.61.2", + "windows-future", + "windows-link", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.2", +] + +[[package]] +name = "windows-core" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d" +dependencies = [ + "windows-implement 0.57.0", + "windows-interface 0.57.0", + "windows-result 0.1.2", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" +dependencies = [ + "windows-implement 0.58.0", + "windows-interface 0.58.0", + "windows-result 0.2.0", + "windows-strings 0.1.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement 0.60.0", + "windows-interface 0.59.1", + "windows-link", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core 0.61.2", + "windows-link", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "windows-implement" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "windows-interface" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "windows-interface" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "windows-link" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" + +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link", +] + +[[package]] +name = "windows-registry" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" +dependencies = [ + "windows-result 0.3.4", + "windows-strings 0.3.1", + "windows-targets 0.53.0", +] + +[[package]] +name = "windows-result" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result 0.2.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-strings" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b" +dependencies = [ + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", +] + +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-version" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e04a5c6627e310a23ad2358483286c7df260c964eb2d003d8efd6d0f4e79265c" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06928c8748d81b05c9be96aad92e1b6ff01833332f281e8cfca3be4b35fc9ec" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.55.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97" +dependencies = [ + "cfg-if", + "windows-sys 0.59.0", +] + +[[package]] +name = "winsafe" +version = "0.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" + +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags 2.9.1", +] + +[[package]] +name = "writeable" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" + +[[package]] +name = "wry" +version = "0.51.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c886a0a9d2a94fd90cfa1d929629b79cfefb1546e2c7430c63a47f0664c0e4e2" +dependencies = [ + "base64 0.22.1", + "block2 0.6.1", + "cookie", + "crossbeam-channel", + "dpi", + "dunce", + "gdkx11", + "gtk", + "html5ever", + "http", + "javascriptcore-rs", + "jni", + "kuchikiki", + "libc", + "ndk", + "objc2 0.6.1", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation 0.3.1", + "objc2-ui-kit", + "objc2-web-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "sha2", + "soup3", + "tao-macros", + "thiserror 2.0.12", + "url", + "webkit2gtk", + "webkit2gtk-sys", + "webview2-com", + "windows 0.61.1", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "x11rb" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d91ffca73ee7f68ce055750bf9f6eca0780b8c85eff9bc046a3b0da41755e12" +dependencies = [ + "gethostname", + "rustix 0.38.44", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec107c4503ea0b4a98ef47356329af139c0a4f7750e621cf2973cd3385ebcb3d" + +[[package]] +name = "xattr" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d65cbf2f12c15564212d48f4e3dfb87923d25d611f2aed18f4cb23f0413d89e" +dependencies = [ + "libc", + "rustix 1.0.7", +] + +[[package]] +name = "xcap" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef2d00d06bcbe2c8e00092190fbd54f85a181473a5ccff8ccc8e01fffef94454" +dependencies = [ + "dispatch2 0.3.0", + "image", + "lazy_static", + "libwayshot-xcap", + "log", + "objc2 0.6.1", + "objc2-app-kit", + "objc2-av-foundation", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-media", + "objc2-core-video", + "objc2-foundation 0.3.1", + "percent-encoding", + "pipewire", + "rand 0.9.1", + "scopeguard", + "serde", + "thiserror 2.0.12", + "url", + "widestring", + "windows 0.61.1", + "xcb", + "zbus", +] + +[[package]] +name = "xcb" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1e2f212bb1a92cd8caac8051b829a6582ede155ccb60b5d5908b81b100952be" +dependencies = [ + "bitflags 1.3.2", + "libc", + "quick-xml 0.30.0", +] + +[[package]] +name = "xml-rs" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a62ce76d9b56901b19a74f19431b0d8b3bc7ca4ad685a746dfd78ca8f4fc6bda" + +[[package]] +name = "yansi-term" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe5c30ade05e61656247b2e334a031dfd0cc466fadef865bdcdea8d537951bf1" +dependencies = [ + "winapi", +] + +[[package]] +name = "yoke" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", + "synstructure", +] + +[[package]] +name = "zbus" +version = "5.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3a7c7cee313d044fca3f48fa782cb750c79e4ca76ba7bc7718cd4024cdf6f68" +dependencies = [ + "async-broadcast", + "async-executor", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener", + "futures-core", + "futures-lite", + "hex", + "nix 0.30.1", + "ordered-stream", + "serde", + "serde_repr", + "tokio", + "tracing", + "uds_windows", + "windows-sys 0.59.0", + "winnow 0.7.10", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "5.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a17e7e5eec1550f747e71a058df81a9a83813ba0f6a95f39c4e218bdc7ba366a" +dependencies = [ + "proc-macro-crate 3.3.0", + "proc-macro2", + "quote", + "syn 2.0.101", + "zbus_names", + "zvariant", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7be68e64bf6ce8db94f63e72f0c7eb9a60d733f7e0499e628dfab0f84d6bcb97" +dependencies = [ + "serde", + "static_assertions", + "winnow 0.7.10", + "zvariant", +] + +[[package]] +name = "zerocopy" +version = "0.8.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + +[[package]] +name = "zerotrie" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "zip" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fabe6324e908f85a1c52063ce7aa26b68dcb7eb6dbc83a2d148403c9bc3eba50" +dependencies = [ + "arbitrary", + "crc32fast", + "crossbeam-utils", + "displaydoc", + "flate2", + "indexmap 2.9.0", + "memchr", + "thiserror 2.0.12", + "zopfli", +] + +[[package]] +name = "zopfli" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edfc5ee405f504cd4984ecc6f14d02d55cfda60fa4b689434ef4102aae150cd7" +dependencies = [ + "bumpalo", + "crc32fast", + "log", + "simd-adler32", +] + +[[package]] +name = "zune-core" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" + +[[package]] +name = "zune-inflate" +version = "0.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "zune-jpeg" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99a5bab8d7dedf81405c4bb1f2b83ea057643d9cb28778cea9eecddeedd2e028" +dependencies = [ + "zune-core", +] + +[[package]] +name = "zvariant" +version = "5.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d30786f75e393ee63a21de4f9074d4c038d52c5b1bb4471f955db249f9dffb1" +dependencies = [ + "endi", + "enumflags2", + "serde", + "url", + "winnow 0.7.10", + "zvariant_derive", + "zvariant_utils", +] + +[[package]] +name = "zvariant_derive" +version = "5.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75fda702cd42d735ccd48117b1630432219c0e9616bf6cb0f8350844ee4d9580" +dependencies = [ + "proc-macro-crate 3.3.0", + "proc-macro2", + "quote", + "syn 2.0.101", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e16edfee43e5d7b553b77872d99bc36afdda75c223ca7ad5e3fbecd82ca5fc34" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "static_assertions", + "syn 2.0.101", + "winnow 0.7.10", +] diff --git a/examples/excel-copilot/src-tauri/Cargo.toml b/examples/excel-copilot/src-tauri/Cargo.toml new file mode 100644 index 00000000..498d20ab --- /dev/null +++ b/examples/excel-copilot/src-tauri/Cargo.toml @@ -0,0 +1,50 @@ +[workspace] + +[package] +name = "terminator-excel-example" +version = "0.0.0" +description = "A Tauri App" +authors = ["you"] +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +# The `_lib` suffix may seem redundant but it is necessary +# to make the lib name unique and wouldn't conflict with the bin name. +# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519 +name = "excel_copilot_lib" +crate-type = ["staticlib", "cdylib", "rlib"] + +[build-dependencies] +tauri-build = { version = "2", features = [] } + +[dependencies] +tauri = { version = "2.0", features = ["devtools"] } +tauri-plugin-dialog = "2.0" +tauri-plugin-fs = "2" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +terminator = { path = "../../../terminator" } +tokio = { version = "1.0", features = ["full"] } +reqwest = { version = "0.12", features = ["json", "multipart"] } +calamine = "0.26" +rust_xlsxwriter = "0.79" +uuid = { version = "1", features = ["v4"] } +chrono = { version = "0.4", features = ["serde"] } +thiserror = "1" +anyhow = "1.0" +arboard = "3.4" +base64 = "0.22.0" +tokio-util = "0.7" + +[target.'cfg(windows)'.dependencies] +windows = { version = "0.58", features = ["Win32_Globalization"] } + +[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] +tauri-plugin-updater = "2.0" + +[features] +# This feature is used for production builds or when a dev server is not specified, DO NOT REMOVE!! +custom-protocol = ["tauri/custom-protocol"] + diff --git a/examples/excel-copilot/src-tauri/build.rs b/examples/excel-copilot/src-tauri/build.rs new file mode 100644 index 00000000..d860e1e6 --- /dev/null +++ b/examples/excel-copilot/src-tauri/build.rs @@ -0,0 +1,3 @@ +fn main() { + tauri_build::build() +} diff --git a/examples/excel-copilot/src-tauri/capabilities/default.json b/examples/excel-copilot/src-tauri/capabilities/default.json new file mode 100644 index 00000000..5bf446e6 --- /dev/null +++ b/examples/excel-copilot/src-tauri/capabilities/default.json @@ -0,0 +1,11 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "default", + "description": "Capability for the main window", + "windows": ["main"], + "permissions": [ + "core:default", + "dialog:default", + "fs:default" + ] +} diff --git a/examples/excel-copilot/src-tauri/icons/128x128.png b/examples/excel-copilot/src-tauri/icons/128x128.png new file mode 100644 index 00000000..6be5e50e Binary files /dev/null and b/examples/excel-copilot/src-tauri/icons/128x128.png differ diff --git a/examples/excel-copilot/src-tauri/icons/128x128@2x.png b/examples/excel-copilot/src-tauri/icons/128x128@2x.png new file mode 100644 index 00000000..e81becee Binary files /dev/null and b/examples/excel-copilot/src-tauri/icons/128x128@2x.png differ diff --git a/examples/excel-copilot/src-tauri/icons/32x32.png b/examples/excel-copilot/src-tauri/icons/32x32.png new file mode 100644 index 00000000..a437dd51 Binary files /dev/null and b/examples/excel-copilot/src-tauri/icons/32x32.png differ diff --git a/examples/excel-copilot/src-tauri/icons/Square107x107Logo.png b/examples/excel-copilot/src-tauri/icons/Square107x107Logo.png new file mode 100644 index 00000000..0ca4f271 Binary files /dev/null and b/examples/excel-copilot/src-tauri/icons/Square107x107Logo.png differ diff --git a/examples/excel-copilot/src-tauri/icons/Square142x142Logo.png b/examples/excel-copilot/src-tauri/icons/Square142x142Logo.png new file mode 100644 index 00000000..b81f8203 Binary files /dev/null and b/examples/excel-copilot/src-tauri/icons/Square142x142Logo.png differ diff --git a/examples/excel-copilot/src-tauri/icons/Square150x150Logo.png b/examples/excel-copilot/src-tauri/icons/Square150x150Logo.png new file mode 100644 index 00000000..624c7bfb Binary files /dev/null and b/examples/excel-copilot/src-tauri/icons/Square150x150Logo.png differ diff --git a/examples/excel-copilot/src-tauri/icons/Square284x284Logo.png b/examples/excel-copilot/src-tauri/icons/Square284x284Logo.png new file mode 100644 index 00000000..c021d2ba Binary files /dev/null and b/examples/excel-copilot/src-tauri/icons/Square284x284Logo.png differ diff --git a/examples/excel-copilot/src-tauri/icons/Square30x30Logo.png b/examples/excel-copilot/src-tauri/icons/Square30x30Logo.png new file mode 100644 index 00000000..62197002 Binary files /dev/null and b/examples/excel-copilot/src-tauri/icons/Square30x30Logo.png differ diff --git a/examples/excel-copilot/src-tauri/icons/Square310x310Logo.png b/examples/excel-copilot/src-tauri/icons/Square310x310Logo.png new file mode 100644 index 00000000..f9bc0483 Binary files /dev/null and b/examples/excel-copilot/src-tauri/icons/Square310x310Logo.png differ diff --git a/examples/excel-copilot/src-tauri/icons/Square44x44Logo.png b/examples/excel-copilot/src-tauri/icons/Square44x44Logo.png new file mode 100644 index 00000000..d5fbfb2a Binary files /dev/null and b/examples/excel-copilot/src-tauri/icons/Square44x44Logo.png differ diff --git a/examples/excel-copilot/src-tauri/icons/Square71x71Logo.png b/examples/excel-copilot/src-tauri/icons/Square71x71Logo.png new file mode 100644 index 00000000..63440d79 Binary files /dev/null and b/examples/excel-copilot/src-tauri/icons/Square71x71Logo.png differ diff --git a/examples/excel-copilot/src-tauri/icons/Square89x89Logo.png b/examples/excel-copilot/src-tauri/icons/Square89x89Logo.png new file mode 100644 index 00000000..f3f705af Binary files /dev/null and b/examples/excel-copilot/src-tauri/icons/Square89x89Logo.png differ diff --git a/examples/excel-copilot/src-tauri/icons/StoreLogo.png b/examples/excel-copilot/src-tauri/icons/StoreLogo.png new file mode 100644 index 00000000..45563882 Binary files /dev/null and b/examples/excel-copilot/src-tauri/icons/StoreLogo.png differ diff --git a/examples/excel-copilot/src-tauri/icons/icon.icns b/examples/excel-copilot/src-tauri/icons/icon.icns new file mode 100644 index 00000000..12a5bcee Binary files /dev/null and b/examples/excel-copilot/src-tauri/icons/icon.icns differ diff --git a/examples/excel-copilot/src-tauri/icons/icon.ico b/examples/excel-copilot/src-tauri/icons/icon.ico new file mode 100644 index 00000000..b3636e4b Binary files /dev/null and b/examples/excel-copilot/src-tauri/icons/icon.ico differ diff --git a/examples/excel-copilot/src-tauri/icons/icon.png b/examples/excel-copilot/src-tauri/icons/icon.png new file mode 100644 index 00000000..e1cd2619 Binary files /dev/null and b/examples/excel-copilot/src-tauri/icons/icon.png differ diff --git a/examples/excel-copilot/src-tauri/src/commands.rs b/examples/excel-copilot/src-tauri/src/commands.rs new file mode 100644 index 00000000..e459532d --- /dev/null +++ b/examples/excel-copilot/src-tauri/src/commands.rs @@ -0,0 +1,1293 @@ +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::sync::Mutex; +use tauri::{State, AppHandle}; +use tauri_plugin_dialog::DialogExt; +use tokio_util::sync::CancellationToken; + +use crate::excel::{ExcelWorkbook, ExcelSheet}; +use crate::gemini::GeminiClient; +use crate::openai::OpenAIClient; +use crate::excel_interaction::{get_excel_automation}; +use crate::locale_utils::{normalize_number_for_excel, get_locale_info as get_system_locale_info}; +use anyhow::Result; +use serde_json::Value; + +/// Convert column index to Excel letter notation (A, B, C, ..., Z, AA, AB, ...) +fn column_index_to_letter(mut index: usize) -> String { + let mut result = String::new(); + loop { + result.insert(0, (b'A' + (index % 26) as u8) as char); + if index < 26 { + break; + } + index = index / 26 - 1; + } + result +} + +/// Global state for storing the current Excel workbook +pub type AppState = Mutex>; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ExcelData { + pub file_path: Option, + pub sheets: Vec, + pub summary: String, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct ToolCall { + pub function_name: String, + pub arguments: Value, + pub result: String, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct ResponseDetails { + pub has_tool_calls: bool, + pub iterations: i32, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct ChatMessage { + pub role: String, + pub content: String, + pub timestamp: u64, + pub tool_calls: Option>, + pub response_details: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GeminiResponse { + pub success: bool, + pub message: String, + pub error: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CellRangeData { + pub range_notation: String, + pub cells: Vec, + pub display_text: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CellData { + pub notation: String, + pub value: String, + pub row: usize, + pub column: usize, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ColumnData { + pub column_notation: String, + pub cells: Vec, + pub display_text: String, +} + +impl From for ExcelData { + fn from(workbook: ExcelWorkbook) -> Self { + Self { + file_path: workbook.file_path.clone(), + sheets: workbook.sheets.iter().cloned().collect(), + summary: workbook.get_content_summary(), + } + } +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub enum LlmProvider { + Gemini, + OpenAI, +} + +impl Default for LlmProvider { + fn default() -> Self { + LlmProvider::Gemini + } +} + +#[derive(Default)] +pub struct AppStateStruct { + pub excel_workbook: Mutex>, + pub gemini_client: Mutex>, + pub openai_client: Mutex>, + pub chat_history: Mutex>, + pub current_file_path: Mutex>, + pub selected_llm: Mutex, + pub cancellation_token: Mutex>, +} + +/// Open an Excel file using the system dialog +#[tauri::command] +pub async fn open_excel_file( + app_handle: AppHandle, + state: State<'_, AppStateStruct>, +) -> Result { + let files = app_handle.dialog().file() + .set_title("Open Excel file") + .add_filter("Excel Files", &["xlsx", "xls"]) + .blocking_pick_file(); + + if let Some(file_path) = files { + let path_str = file_path.to_string(); + + // Update current file path + { + let mut current_path = state.current_file_path.lock().unwrap(); + *current_path = Some(path_str.clone()); + } + + // Try to open Excel using the system default application + match std::process::Command::new("cmd") + .arg("/C") + .arg("start") + .arg("") + .arg(&path_str) + .spawn() + { + Ok(_) => { + // Also load for internal representation if possible + match ExcelWorkbook::from_file(&path_str) { + Ok(workbook) => { + let mut excel_state = state.excel_workbook.lock().unwrap(); + *excel_state = Some(workbook); + } + Err(_) => { + // This is fine - we opened in Excel but couldn't parse internally + } + } + Ok(format!("✅ Excel file opened: {}", path_str)) + } + Err(e) => Err(format!("❌ Error opening file: {}", e)) + } + } else { + Err("❌ No file selected".to_string()) + } +} + +/// Create a new Excel workbook +#[tauri::command] +pub async fn create_new_excel( + state: State<'_, AppStateStruct>, +) -> Result { + let mut excel_state = state.excel_workbook.lock().unwrap(); + + let workbook = ExcelWorkbook::new(); + *excel_state = Some(workbook); + + // Create a temporary file and open it in Excel + let temp_dir = std::env::temp_dir(); + let temp_file = temp_dir.join("new_excel_sheet.xlsx"); + + { + let mut current_path = state.current_file_path.lock().unwrap(); + *current_path = Some(temp_file.to_string_lossy().to_string()); + } + + // Save the empty workbook + if let Some(ref workbook) = *excel_state { + workbook.save_to_file(&temp_file) + .map_err(|e| format!("Error creating file: {}", e))?; + } + + // Open in Excel + match std::process::Command::new("cmd") + .arg("/C") + .arg("start") + .arg("") + .arg(&temp_file.to_string_lossy().to_string()) + .spawn() + { + Ok(_) => Ok("✅ New Excel file created and opened".to_string()), + Err(e) => Err(format!("❌ Error opening file: {}", e)) + } +} + +/// Save the current Excel workbook +#[tauri::command] +pub async fn save_excel_file( + state: State<'_, AppStateStruct>, + file_path: Option, +) -> Result { + let excel_state = state.excel_workbook.lock().unwrap(); + if let Some(workbook) = excel_state.as_ref() { + let path = if let Some(path) = file_path { + path + } else { + let current_path = state.current_file_path.lock().unwrap(); + if let Some(ref path) = *current_path { + path.clone() + } else { + return Err("❌ No file path specified".to_string()); + } + }; + + workbook.save_to_file(&path) + .map_err(|e| format!("Error saving file: {}", e))?; + + { + let mut current_path = state.current_file_path.lock().unwrap(); + *current_path = Some(path.clone()); + } + + Ok(format!("File saved: {}", path)) + } else { + Err("No Excel file loaded".to_string()) + } +} + +/// Get the content of the current Excel workbook +#[tauri::command] +pub async fn get_excel_content( + state: State<'_, AppStateStruct>, +) -> Result>>, String> { + let excel_state = state.excel_workbook.lock().unwrap(); + if let Some(workbook) = excel_state.as_ref() { + let mut content = HashMap::new(); + + for sheet in &workbook.sheets { + content.insert(sheet.name.clone(), sheet.data.clone()); + } + + Ok(content) + } else { + Err("No Excel file loaded".to_string()) + } +} + +/// Configure the Gemini AI client with API key +#[tauri::command] +pub async fn setup_gemini_client( + state: State<'_, AppStateStruct>, + api_key: String, + copilot_enabled: Option, +) -> Result { + let mut gemini_state = state.gemini_client.lock().unwrap(); + + let use_copilot = copilot_enabled.unwrap_or(false); + + let client = GeminiClient::new(api_key, use_copilot) + .map_err(|e| format!("Error creating Gemini client: {}", e))?; + + *gemini_state = Some(client); + + let status_msg = if use_copilot { + "✅ Gemini client configured successfully with Copilot support" + } else { + "✅ Gemini client configured successfully (Copilot disabled)" + }; + + Ok(status_msg.to_string()) +} + +/// Configure the OpenAI client with API key +#[tauri::command] +pub async fn setup_openai_client( + state: State<'_, AppStateStruct>, + api_key: String, + copilot_enabled: Option, +) -> Result { + let mut openai_state = state.openai_client.lock().unwrap(); + + let use_copilot = copilot_enabled.unwrap_or(false); + + let client = OpenAIClient::new(api_key, use_copilot); + + *openai_state = Some(client); + + let status_msg = if use_copilot { + "✅ OpenAI client configured successfully with Copilot support" + } else { + "✅ OpenAI client configured successfully (Copilot disabled)" + }; + + Ok(status_msg.to_string()) +} + +/// Set the LLM provider (Gemini or OpenAI) +#[tauri::command] +pub async fn set_llm_provider( + state: State<'_, AppStateStruct>, + provider: String, +) -> Result { + let mut selected_llm = state.selected_llm.lock().unwrap(); + + match provider.to_lowercase().as_str() { + "gemini" => { + *selected_llm = LlmProvider::Gemini; + Ok("✅ Switched to Gemini".to_string()) + } + "openai" => { + *selected_llm = LlmProvider::OpenAI; + Ok("✅ Switched to OpenAI".to_string()) + } + _ => Err("❌ Invalid provider. Use 'gemini' or 'openai'".to_string()) + } +} + +/// Get the current LLM provider +#[tauri::command] +pub async fn get_llm_provider( + state: State<'_, AppStateStruct>, +) -> Result { + let selected_llm = state.selected_llm.lock().unwrap(); + match *selected_llm { + LlmProvider::Gemini => Ok("gemini".to_string()), + LlmProvider::OpenAI => Ok("openai".to_string()), + } +} + +/// Stop the current LLM request +#[tauri::command] +pub async fn stop_llm_request( + state: State<'_, AppStateStruct>, +) -> Result { + let mut cancellation_token = state.cancellation_token.lock().unwrap(); + + if let Some(token) = cancellation_token.take() { + token.cancel(); + println!("🛑 Request cancellation signal sent"); + Ok("Request stopped successfully".to_string()) + } else { + Ok("No active request to stop".to_string()) + } +} + +/// Send a chat message to the selected LLM with tool calling support +#[tauri::command] +pub async fn chat_with_llm( + state: State<'_, AppStateStruct>, + message: String, +) -> Result { + // Create and store cancellation token + let cancellation_token = CancellationToken::new(); + { + let mut token_guard = state.cancellation_token.lock().unwrap(); + *token_guard = Some(cancellation_token.clone()); + } + + let selected_llm = { + let llm = state.selected_llm.lock().unwrap(); + llm.clone() + }; + + let result = match selected_llm { + LlmProvider::Gemini => chat_with_gemini_with_cancellation(&state, message, cancellation_token.clone()).await, + LlmProvider::OpenAI => chat_with_openai_with_cancellation(&state, message, cancellation_token.clone()).await, + }; + + // Clear the cancellation token + { + let mut token_guard = state.cancellation_token.lock().unwrap(); + *token_guard = None; + } + + result +} + +/// Send a chat message to OpenAI with tool calling support and cancellation +async fn chat_with_openai_with_cancellation( + state: &State<'_, AppStateStruct>, + message: String, + cancellation_token: CancellationToken, +) -> Result { + // Clone the client to avoid holding the lock across await + let client_opt = { + let openai_state = state.openai_client.lock().unwrap(); + openai_state.clone() + }; + + if let Some(mut client) = client_opt { + // Add user message to history + { + let mut chat_history = state.chat_history.lock().unwrap(); + chat_history.push(ChatMessage { + role: "user".to_string(), + content: message.clone(), + timestamp: std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs(), + tool_calls: None, + response_details: None, + }); + } + + // Create tool executor that uses the original state directly + let tool_executor = |function_name: &str, args: &Value| { + let function_name_owned = function_name.to_string(); + let args_owned = args.clone(); + + // Get the current file path directly from state when needed + let current_file_path = { + let current_path = state.current_file_path.lock().unwrap(); + current_path.as_ref().map(|p| p.to_string()) + }; + + Box::pin(async move { + // Execute tool with just the file path instead of full state + execute_excel_tool_with_path(&function_name_owned, &args_owned, current_file_path.as_deref()).await + .map_err(|e| -> Box { format!("{}", e).into() }) + }) as std::pin::Pin>> + Send>> + }; + + + match client.send_message_with_tools_detailed_cancellable(&message, tool_executor, Some(cancellation_token)).await { + Ok(response) => { + // Convert tool calls to our format + let tool_calls: Vec = response.tool_calls.iter().map(|tc| ToolCall { + function_name: tc.function_name.clone(), + arguments: tc.arguments.clone(), + result: tc.result.clone(), + }).collect(); + + // Add final assistant response to history + { + let mut chat_history = state.chat_history.lock().unwrap(); + chat_history.push(ChatMessage { + role: "model".to_string(), + content: response.content.clone(), + timestamp: std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs(), + tool_calls: if tool_calls.is_empty() { None } else { Some(tool_calls) }, + response_details: Some(ResponseDetails { + has_tool_calls: response.has_tool_calls, + iterations: response.iterations, + }), + }); + } + + // Update the client state with the updated client that contains conversation history + { + let mut openai_state = state.openai_client.lock().unwrap(); + *openai_state = Some(client); + } + + Ok(response.content) + } + Err(e) => { + // Don't add error to chat history - just return the error + // The error will be handled by the frontend + Err(format!("OpenAI API error: {}", e)) + } + } + } else { + Err("OpenAI client not configured. Please set up your API key first.".to_string()) + } +} + +/// Send a chat message to OpenAI with tool calling support +#[tauri::command] +pub async fn chat_with_openai( + state: State<'_, AppStateStruct>, + message: String, +) -> Result { + let cancellation_token = CancellationToken::new(); + chat_with_openai_with_cancellation(&state, message, cancellation_token).await +} + +/// Send a chat message to Gemini with tool calling support and cancellation +async fn chat_with_gemini_with_cancellation( + state: &State<'_, AppStateStruct>, + message: String, + cancellation_token: CancellationToken, +) -> Result { + // Clone the client to avoid holding the lock across await + let client_opt = { + let gemini_state = state.gemini_client.lock().unwrap(); + gemini_state.clone() + }; + + if let Some(mut client) = client_opt { + // Add user message to history + { + let mut chat_history = state.chat_history.lock().unwrap(); + chat_history.push(ChatMessage { + role: "user".to_string(), + content: message.clone(), + timestamp: std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs(), + tool_calls: None, + response_details: None, + }); + } + + // Create tool executor that uses the original state directly + let tool_executor = |function_name: &str, args: &Value| { + let function_name_owned = function_name.to_string(); + let args_owned = args.clone(); + + // Get the current file path directly from state when needed + let current_file_path = { + let current_path = state.current_file_path.lock().unwrap(); + current_path.as_ref().map(|p| p.to_string()) + }; + + Box::pin(async move { + execute_excel_tool_with_path(&function_name_owned, &args_owned, current_file_path.as_deref()).await + .map_err(|e| -> Box { format!("{}", e).into() }) + }) as std::pin::Pin>> + Send>> + }; + + + match client.send_message_with_tools_detailed_cancellable(&message, tool_executor, Some(cancellation_token)).await { + Ok(response) => { + // Convert tool calls to our format + let tool_calls: Vec = response.tool_calls.iter().map(|tc| ToolCall { + function_name: tc.function_name.clone(), + arguments: tc.arguments.clone(), + result: tc.result.clone(), + }).collect(); + + // Add final assistant response to history + { + let mut chat_history = state.chat_history.lock().unwrap(); + chat_history.push(ChatMessage { + role: "model".to_string(), + content: response.content.clone(), + timestamp: std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs(), + tool_calls: if tool_calls.is_empty() { None } else { Some(tool_calls) }, + response_details: Some(ResponseDetails { + has_tool_calls: response.has_tool_calls, + iterations: response.iterations, + }), + }); + } + + // Update the client state with the updated client that contains conversation history + { + let mut gemini_state = state.gemini_client.lock().unwrap(); + *gemini_state = Some(client); + } + + Ok(response.content) + } + Err(e) => { + // Don't add error to chat history - just return the error + // The error will be handled by the frontend + Err(format!("Gemini API error: {}", e)) + } + } + } else { + Err("Gemini client not configured. Please set up your API key first.".to_string()) + } +} + +/// Send a chat message to Gemini with tool calling support +#[tauri::command] +pub async fn chat_with_gemini( + state: State<'_, AppStateStruct>, + message: String, +) -> Result { + let cancellation_token = CancellationToken::new(); + chat_with_gemini_with_cancellation(&state, message, cancellation_token).await +} + +/// Execute Excel tools via automation with file path +async fn execute_excel_tool_with_path( + function_name: &str, + args: &Value, + file_path: Option<&str> +) -> Result> { + let automation = get_excel_automation().await + .map_err(|e| format!("Failed to get Excel automation: {}", e))?; + + let result = match function_name { + "read_excel_cell" => { + let cell_address = args["cell_address"].as_str() + .ok_or("Missing or invalid cell_address parameter")?; + + let cell = automation.read_cell(cell_address).await + .map_err(|e| format!("Failed to read cell: {}", e))?; + + format!("SUCCESS: Read cell {} - Value: '{}'. The cell contains the value '{}' and is ready for further operations.", + cell.address, cell.value, cell.value) + } + "write_excel_cell" => { + let cell_address = args["cell_address"].as_str() + .ok_or("Missing or invalid cell_address parameter")?; + let value = args["value"].as_str() + .ok_or("Missing or invalid value parameter")?; + + // Normalize the value if it looks like a number + let normalized_value = if value.chars().any(|c| c.is_ascii_digit()) && + (value.contains('.') || value.contains(',') || value.contains('+') || value.contains('-')) { + let normalized = normalize_number_for_excel(value); + println!("Tool number normalization: '{}' -> '{}'", value, normalized); + normalized + } else { + value.to_string() + }; + + automation.write_cell(cell_address, &normalized_value).await + .map_err(|e| format!("Failed to write cell: {}", e))?; + + // Save the file after writing + automation.save_current_file().await + .map_err(|e| format!("Failed to save file after write: {}", e))?; + + // Verify the write by reading back the value + let verification = automation.read_cell(cell_address).await + .map_err(|e| format!("Failed to verify write: {}", e))?; + + if normalized_value != value { + format!("SUCCESS: Wrote '{}' to cell {} (normalized from '{}'). VERIFICATION: Cell {} now contains '{}'. The write operation is complete and verified with locale-aware number formatting.", + normalized_value, cell_address, value, verification.address, verification.value) + } else { + format!("SUCCESS: Wrote '{}' to cell {}. VERIFICATION: Cell {} now contains '{}'. The write operation is complete and verified.", + normalized_value, cell_address, verification.address, verification.value) + } + } + "read_excel_range" => { + let start_cell = args["start_cell"].as_str() + .ok_or("Missing or invalid start_cell parameter")?; + let end_cell = args["end_cell"].as_str() + .ok_or("Missing or invalid end_cell parameter")?; + + let range = automation.read_range(start_cell, end_cell).await + .map_err(|e| format!("Failed to read range: {}", e))?; + + if range.values.is_empty() { + format!("SUCCESS: Read range {}:{} - The range is empty (0 rows). No data found in the specified range.", start_cell, end_cell) + } else { + let mut result = format!("SUCCESS: Read range {}:{} - Found {} rows of data:\n", start_cell, end_cell, range.values.len()); + for (row_idx, row) in range.values.iter().enumerate() { + result.push_str(&format!("Row {}: {}\n", row_idx + 1, row.join(" | "))); + } + result.push_str(&format!("Range contains {} rows and {} columns.", range.values.len(), range.values.get(0).map(|r| r.len()).unwrap_or(0))); + result + } + } + "set_excel_formula" => { + let cell_address = args["cell_address"].as_str() + .ok_or("Missing or invalid cell_address parameter")?; + let formula = args["formula"].as_str() + .ok_or("Missing or invalid formula parameter")?; + + automation.set_formula(cell_address, formula).await + .map_err(|e| format!("Failed to set formula: {}", e))?; + + automation.save_current_file().await + .map_err(|e| format!("Failed to save file after formula: {}", e))?; + + tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; + + let result_cell = automation.read_cell(cell_address).await + .map_err(|e| format!("Failed to read formula result: {}", e))?; + + if result_cell.value.starts_with('#') { + format!("ERROR DETECTED: Formula '{}' in cell {} resulted in '{}' error. This indicates a problem with the formula that needs to be fixed.", + formula, cell_address, result_cell.value) + } else { + format!("SUCCESS: Set formula '{}' in cell {}. RESULT: The formula calculated to '{}'. Formula operation completed successfully.", + formula, cell_address, result_cell.value) + } + } + "get_excel_sheet_overview" => { + match get_complete_excel_status_from_file(file_path).await { + Ok(overview) => overview, + Err(e) => { + // Return a helpful error message to the model instead of throwing an error + if file_path.is_none() { + "ERROR: No Excel file is currently open. Please ask the user to open an Excel file first using 'Open Excel File' or create a new one with 'New Excel File'. Cannot read sheet overview without an active file.".to_string() + } else { + format!("ERROR: Failed to read Excel file: {}. The file might be corrupted, locked by another application, or inaccessible. Please ask the user to check if the file is still available and not being used by another program.", e) + } + } + } + } + "get_excel_ui_context" => { + let ui_context = automation.get_excel_ui_context().await + .map_err(|e| format!("Failed to get Excel UI context: {}", e))?; + + format!("SUCCESS: Retrieved Excel UI context. {}", ui_context) + } + "paste_tsv_batch_data" => { + let start_cell = args["start_cell"].as_str() + .ok_or("Missing or invalid start_cell parameter")?; + let tsv_data = args["tsv_data"].as_str() + .ok_or("Missing or invalid tsv_data parameter")?; + let verify_safe = args["verify_safe"].as_bool().unwrap_or(true); + + let result = automation.paste_tsv_data(start_cell, tsv_data, verify_safe).await + .map_err(|e| format!("Failed to paste TSV data: {}", e))?; + + // Save the file after batch paste + automation.save_current_file().await + .map_err(|e| format!("Failed to save file after TSV paste: {}", e))?; + + format!("SUCCESS: {}", result) + } + "send_request_to_excel_copilot" => { + let request = args["request"].as_str() + .ok_or("Missing or invalid request parameter")?; + + let result = automation.send_request_to_excel_copilot(request).await + .map_err(|e| format!("Failed to send request to Excel Copilot: {}", e))?; + + // Save the file after Copilot operation + automation.save_current_file().await + .map_err(|e| format!("Failed to save file after Copilot operation: {}", e))?; + + format!("SUCCESS: Excel Copilot executed '{}'. Result: {}", request, result) + } + "format_cells_with_copilot" => { + let range = args["range"].as_str() + .ok_or("Missing or invalid range parameter")?; + let format_description = args["format_description"].as_str() + .ok_or("Missing or invalid format_description parameter")?; + + let result = automation.format_cells_with_copilot(range, format_description).await + .map_err(|e| format!("Failed to format cells with Copilot: {}", e))?; + + // Save the file after formatting + automation.save_current_file().await + .map_err(|e| format!("Failed to save file after formatting: {}", e))?; + + format!("SUCCESS: Excel Copilot formatted range {} with '{}'. Result: {}", range, format_description, result) + } + "create_chart_with_copilot" => { + let data_range = args["data_range"].as_str() + .ok_or("Missing or invalid data_range parameter")?; + let chart_type = args["chart_type"].as_str() + .ok_or("Missing or invalid chart_type parameter")?; + + let result = automation.create_chart_with_copilot(data_range, chart_type).await + .map_err(|e| format!("Failed to create chart with Copilot: {}", e))?; + + automation.save_current_file().await + .map_err(|e| format!("Failed to save file after chart creation: {}", e))?; + + format!("SUCCESS: Excel Copilot created {} chart from range {}. Result: {}", chart_type, data_range, result) + } + "apply_conditional_formatting_with_copilot" => { + let range = args.get("range") + .and_then(|v| v.as_str()) + .ok_or("Missing 'range' parameter")?; + + let condition = args.get("condition") + .and_then(|v| v.as_str()) + .ok_or("Missing 'condition' parameter")?; + + let automation = get_excel_automation().await?; + let result = automation.apply_conditional_formatting_with_copilot(range, condition).await + .map_err(|e| format!("Failed to apply conditional formatting with Copilot: {}", e))?; + + // Save the file after conditional formatting + automation.save_current_file().await + .map_err(|e| format!("Failed to save file after conditional formatting: {}", e))?; + + format!("SUCCESS: Excel Copilot applied conditional formatting to range {} with condition '{}'. Result: {}", range, condition, result) + } + // Google Sheets commands + "open_google_sheets_app" => { + let automation = get_excel_automation().await?; + let result = automation.open_google_sheets_app().await + .map_err(|e| format!("Failed to open Google Sheets: {}", e))?; + format!("SUCCESS: {}", result) + } + + "check_google_sheets_availability" => { + let automation = get_excel_automation().await?; + let result = automation.check_google_sheets_availability().await + .map_err(|e| format!("Failed to check Google Sheets: {}", e))?; + format!("AVAILABILITY: {}", result) + } + + "send_request_to_google_sheets_gemini" => { + let request = args.get("request") + .and_then(|v| v.as_str()) + .ok_or("Missing 'request' parameter")?; + + let automation = get_excel_automation().await?; + let result = automation.interact_with_google_sheets_gemini(request).await + .map_err(|e| format!("Failed to send request to Google Sheets Gemini: {}", e))?; + format!("SUCCESS: Google Sheets Gemini executed '{}'. Result: {}", request, result) + } + + "send_data_to_google_sheets_gemini" => { + let data = args.get("data") + .and_then(|v| v.as_str()) + .ok_or("Missing 'data' parameter")?; + + let description = args.get("description") + .and_then(|v| v.as_str()) + .ok_or("Missing 'description' parameter")?; + + let automation = get_excel_automation().await?; + let result = automation.send_data_to_google_sheets_gemini(data, description).await + .map_err(|e| format!("Failed to send data to Google Sheets Gemini: {}", e))?; + format!("SUCCESS: Google Sheets Gemini processed data. Description: '{}'. Result: {}", description, result) + } + + // Default case + _ => format!("ERROR: Unknown function '{}'. Available functions: read_excel_cell, write_excel_cell, read_excel_range, get_excel_sheet_overview, get_excel_ui_context, paste_tsv_batch_data, set_excel_formula, send_request_to_excel_copilot, format_cells_with_copilot, create_chart_with_copilot, apply_conditional_formatting_with_copilot, open_google_sheets_app, check_google_sheets_availability, send_request_to_google_sheets_gemini, send_data_to_google_sheets_gemini", function_name) + }; + + // Always append complete Excel status after every operation + let mut final_result = result; + + // For all operations, always append current Excel state + match get_complete_excel_status_from_file(file_path).await { + Ok(excel_status) => { + final_result.push_str("\n\n"); + final_result.push_str(&excel_status); + } + Err(e) => { + final_result.push_str(&format!("\n\n Warning: Could not read current Excel file status: {}", e)); + } + } + + Ok(final_result) +} + +/// Get complete Excel status by reading the file directly from disk +async fn get_complete_excel_status_from_file(file_path: Option<&str>) -> Result> { + if let Some(path) = file_path { + match crate::excel::ExcelWorkbook::from_file(path) { + Ok(workbook) => { + let mut status = String::from("📊 CURRENT EXCEL FILE STATUS:\n"); + status.push_str("=====================================\n"); + + if workbook.sheets.is_empty() { + status.push_str("No sheets found in the workbook.\n"); + return Ok(status); + } + + for (sheet_idx, sheet) in workbook.sheets.iter().enumerate() { + status.push_str(&format!("📋 SHEET {} - '{}'\n", sheet_idx + 1, sheet.name)); + status.push_str("-------------------------------------\n"); + + let mut non_empty_cells = Vec::new(); + + // Find all non-empty cells in this sheet + for (row_idx, row) in sheet.data.iter().enumerate() { + for (col_idx, cell_value) in row.iter().enumerate() { + if !cell_value.trim().is_empty() { + let col_letter = column_index_to_letter(col_idx); + let cell_address = format!("{}{}", col_letter, row_idx + 1); + non_empty_cells.push((cell_address, cell_value.clone())); + } + } + } + + if non_empty_cells.is_empty() { + status.push_str("Sheet is empty - no data found.\n"); + } else { + status.push_str(&format!("Found {} non-empty cells:\n", non_empty_cells.len())); + + for (address, value) in non_empty_cells.iter() { + // Trim long values for readability + let display_value = if value.len() > 50 { + format!("{}...", &value[..47]) + } else { + value.clone() + }; + status.push_str(&format!(" {}: '{}'\n", address, display_value)); + } + } + + status.push_str("\n"); + } + + // Summary + let total_sheets = workbook.sheets.len(); + let total_cells: usize = workbook.sheets.iter() + .map(|sheet| { + sheet.data.iter() + .map(|row| row.iter().filter(|cell| !cell.trim().is_empty()).count()) + .sum::() + }) + .sum(); + + status.push_str("=====================================\n"); + status.push_str(&format!("SUMMARY: {} sheet(s), {} non-empty cell(s) total\n", total_sheets, total_cells)); + status.push_str("====================================="); + + Ok(status) + } + Err(e) => { + Err(format!("Failed to read Excel file '{}': {}", path, e).into()) + } + } + } else { + Err("No file path provided - cannot read Excel status".into()) + } +} + +/// Get chat history +#[tauri::command] +pub async fn get_chat_history( + state: State<'_, AppStateStruct>, +) -> Result, String> { + let chat_history = state.chat_history.lock().unwrap(); + Ok(chat_history.clone()) +} + +/// Clear chat history and reset conversation +#[tauri::command] +pub async fn clear_chat_history( + state: State<'_, AppStateStruct>, +) -> Result { + { + let mut chat_history = state.chat_history.lock().unwrap(); + chat_history.clear(); + } + + { + let mut gemini_state = state.gemini_client.lock().unwrap(); + if let Some(client) = gemini_state.as_mut() { + client.clear_conversation(); + } + } + + Ok("✅ Chat history cleared".to_string()) +} + +/// Send a chat message to Gemini with PDF attachments +#[tauri::command] +pub async fn chat_with_gemini_pdf( + state: State<'_, AppStateStruct>, + message: String, + pdf_files: Vec, +) -> Result { + // Clone the client to avoid holding the lock across await + let client_opt = { + let gemini_state = state.gemini_client.lock().unwrap(); + gemini_state.clone() + }; + + if let Some(mut client) = client_opt { + // Create tool executor + let tool_executor = |function_name: &str, args: &Value| { + let function_name_owned = function_name.to_string(); + let args_owned = args.clone(); + + let current_file_path = { + let current_path = state.current_file_path.lock().unwrap(); + current_path.as_ref().map(|p| p.to_string()) + }; + + Box::pin(async move { + execute_excel_tool_with_path(&function_name_owned, &args_owned, current_file_path.as_deref()).await + .map_err(|e| -> Box { format!("{}", e).into() }) + }) as std::pin::Pin>> + Send>> + }; + + // Clone pdf_files to avoid move issues + let pdf_files_clone = pdf_files.clone(); + + // Use PDF-enabled method + match client.send_message_with_pdf(&message, pdf_files_clone, tool_executor).await { + Ok(response) => { + // Convert tool calls to our format + let tool_calls: Vec = response.tool_calls.iter().map(|tc| ToolCall { + function_name: tc.function_name.clone(), + arguments: tc.arguments.clone(), + result: tc.result.clone(), + }).collect(); + + // Add user message to history + { + let mut chat_history = state.chat_history.lock().unwrap(); + chat_history.push(ChatMessage { + role: "user".to_string(), + content: format!("{} [with {} PDF attachment(s)]", message, pdf_files.len()), + timestamp: std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs(), + tool_calls: None, + response_details: None, + }); + + // Add assistant response + chat_history.push(ChatMessage { + role: "model".to_string(), + content: response.content.clone(), + timestamp: std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs(), + tool_calls: if tool_calls.is_empty() { None } else { Some(tool_calls) }, + response_details: Some(ResponseDetails { + has_tool_calls: response.has_tool_calls, + iterations: response.iterations, + }), + }); + } + + // Update the client state + { + let mut gemini_state = state.gemini_client.lock().unwrap(); + *gemini_state = Some(client); + } + + Ok(response.content) + } + Err(e) => Err(format!("Gemini PDF API error: {}", e)) + } + } else { + Err("Gemini client not configured. Please set up your API key first.".to_string()) + } +} + +/// Send a chat message to OpenAI with PDF attachments +#[tauri::command] +pub async fn chat_with_openai_pdf( + state: State<'_, AppStateStruct>, + message: String, + pdf_files: Vec, +) -> Result { + // Clone the client to avoid holding the lock across await + let client_opt = { + let openai_state = state.openai_client.lock().unwrap(); + openai_state.clone() + }; + + if let Some(mut client) = client_opt { + // Create tool executor + let tool_executor = |function_name: &str, args: &Value| { + let function_name_owned = function_name.to_string(); + let args_owned = args.clone(); + + let current_file_path = { + let current_path = state.current_file_path.lock().unwrap(); + current_path.as_ref().map(|p| p.to_string()) + }; + + Box::pin(async move { + execute_excel_tool_with_path(&function_name_owned, &args_owned, current_file_path.as_deref()).await + .map_err(|e| -> Box { format!("{}", e).into() }) + }) as std::pin::Pin>> + Send>> + }; + + // Clone pdf_files to avoid move issues + let pdf_files_clone = pdf_files.clone(); + + // Use PDF-enabled method + match client.send_message_with_pdf(&message, pdf_files_clone, tool_executor).await { + Ok(response) => { + // Convert tool calls to our format + let tool_calls: Vec = response.tool_calls.iter().map(|tc| ToolCall { + function_name: tc.function_name.clone(), + arguments: tc.arguments.clone(), + result: tc.result.clone(), + }).collect(); + + // Add user message to history + { + let mut chat_history = state.chat_history.lock().unwrap(); + chat_history.push(ChatMessage { + role: "user".to_string(), + content: format!("{} [with {} PDF attachment(s)]", message, pdf_files.len()), + timestamp: std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs(), + tool_calls: None, + response_details: None, + }); + + // Add assistant response + chat_history.push(ChatMessage { + role: "model".to_string(), + content: response.content.clone(), + timestamp: std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs(), + tool_calls: if tool_calls.is_empty() { None } else { Some(tool_calls) }, + response_details: Some(ResponseDetails { + has_tool_calls: response.has_tool_calls, + iterations: response.iterations, + }), + }); + } + + // Update the client state + { + let mut openai_state = state.openai_client.lock().unwrap(); + *openai_state = Some(client); + } + + Ok(response.content) + } + Err(e) => Err(format!("OpenAI PDF API error: {}", e)) + } + } else { + Err("OpenAI client not configured. Please set up your API key first.".to_string()) + } +} + +/// Send a chat message to the selected LLM with PDF attachments +#[tauri::command] +pub async fn chat_with_llm_pdf( + state: State<'_, AppStateStruct>, + message: String, + pdf_files: Vec, +) -> Result { + // Create and store cancellation token + let cancellation_token = CancellationToken::new(); + { + let mut token_guard = state.cancellation_token.lock().unwrap(); + *token_guard = Some(cancellation_token.clone()); + } + + let selected_llm = { + let llm = state.selected_llm.lock().unwrap(); + llm.clone() + }; + + let result = match selected_llm { + LlmProvider::Gemini => chat_with_gemini_pdf(state.clone(), message, pdf_files).await, + LlmProvider::OpenAI => chat_with_openai_pdf(state.clone(), message, pdf_files).await, + }; + + // Clear the cancellation token + { + let mut token_guard = state.cancellation_token.lock().unwrap(); + *token_guard = None; + } + + result +} + +/// Select PDF files for attachment +#[tauri::command] +pub async fn select_pdf_files(app_handle: AppHandle) -> Result, String> { + use std::sync::mpsc; + let (tx, rx) = mpsc::channel(); + + app_handle.dialog().file() + .set_title("Select PDF files") + .add_filter("PDF Files", &["pdf"]) + .pick_files(move |file_paths| { + let _ = tx.send(file_paths); + }); + + let files = rx.recv() + .map_err(|e| format!("Failed to receive file dialog result: {}", e))?; + + if let Some(file_paths) = files { + let paths: Vec = file_paths.iter() + .map(|path| path.to_string()) + .collect(); + Ok(paths) + } else { + Ok(Vec::new()) + } +} + +// Excel interaction commands using Terminator +#[tauri::command] +pub async fn excel_read_cell( + cell_address: String, +) -> Result { + let automation = get_excel_automation().await + .map_err(|e| format!("Excel automation error: {}", e))?; + + let cell = automation.read_cell(&cell_address).await + .map_err(|e| format!("Error reading cell: {}", e))?; + + Ok(format!("Cell {}: '{}'", cell.address, cell.value)) +} + +#[tauri::command] +pub async fn excel_write_cell( + cell_address: String, + value: String, +) -> Result { + let automation = get_excel_automation().await + .map_err(|e| format!("Excel automation error: {}", e))?; + + // Normalize the value if it looks like a number + let normalized_value = if value.chars().any(|c| c.is_ascii_digit()) && + (value.contains('.') || value.contains(',') || value.contains('+') || value.contains('-')) { + let normalized = normalize_number_for_excel(&value); + println!("Number normalization: '{}' -> '{}'", value, normalized); + normalized + } else { + value.clone() + }; + + automation.write_cell(&cell_address, &normalized_value).await + .map_err(|e| format!("Error writing to cell: {}", e))?; + + Ok(format!("Written '{}' to cell {} (original: '{}')", normalized_value, cell_address, value)) +} + +#[tauri::command] +pub async fn excel_read_range( + start_cell: String, + end_cell: String, +) -> Result>, String> { + let automation = get_excel_automation().await + .map_err(|e| format!("Excel automation error: {}", e))?; + + let range = automation.read_range(&start_cell, &end_cell).await + .map_err(|e| format!("Error reading range: {}", e))?; + + Ok(range.values) +} + +#[tauri::command] +pub async fn excel_set_formula( + cell_address: String, + formula: String, +) -> Result { + let automation = get_excel_automation().await + .map_err(|e| format!("Excel automation error: {}", e))?; + + automation.set_formula(&cell_address, &formula).await + .map_err(|e| format!("Error setting formula: {}", e))?; + + Ok(format!("Formula '{}' set in cell {}", formula, cell_address)) +} + +/// Get system locale information for number formatting +#[tauri::command] +pub async fn get_locale_info() -> Result { + Ok(get_system_locale_info()) +} + +// Google Sheets specific Tauri commands +#[tauri::command] +pub async fn check_google_sheets_availability() -> Result { + let excel = get_excel_automation().await.map_err(|e| e.to_string())?; + excel.check_google_sheets_availability().await.map_err(|e| e.to_string()) +} + +#[tauri::command] +pub async fn open_google_sheets() -> Result { + let excel = get_excel_automation().await.map_err(|e| e.to_string())?; + excel.open_google_sheets_app().await.map_err(|e| e.to_string()) +} + +#[tauri::command] +pub async fn google_sheets_send_prompt(prompt: String) -> Result { + let excel = get_excel_automation().await.map_err(|e| e.to_string())?; + excel.send_prompt_to_google_sheets_gemini(&prompt).await.map_err(|e| e.to_string()) +} + +#[tauri::command] +pub async fn google_sheets_send_data(data: String, description: String) -> Result { + let excel = get_excel_automation().await.map_err(|e| e.to_string())?; + excel.send_data_to_google_sheets_gemini(&data, &description).await.map_err(|e| e.to_string()) +} + +#[tauri::command] +pub async fn google_sheets_interact(task: String) -> Result { + let excel = get_excel_automation().await.map_err(|e| e.to_string())?; + excel.interact_with_google_sheets_gemini(&task).await.map_err(|e| e.to_string()) +} \ No newline at end of file diff --git a/examples/excel-copilot/src-tauri/src/excel.rs b/examples/excel-copilot/src-tauri/src/excel.rs new file mode 100644 index 00000000..d7eae0a8 --- /dev/null +++ b/examples/excel-copilot/src-tauri/src/excel.rs @@ -0,0 +1,487 @@ +use anyhow::{Result, anyhow}; +use calamine::{Reader, Xlsx, open_workbook}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::path::Path; +use rust_xlsxwriter::Workbook; + +/// Represents a cell position in Excel format (e.g., A1, B2, etc.) +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CellPosition { + pub column: usize, + pub row: usize, +} + +/// Represents a range of cells (e.g., A1:C10) +#[derive(Debug, Clone)] +pub struct CellRange { + pub start: CellPosition, + pub end: CellPosition, +} + +/// Individual Excel sheet with enhanced functionality +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ExcelSheet { + pub name: String, + pub data: Vec>, +} + +/// Main Excel workbook with enhanced functionality +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ExcelWorkbook { + pub file_path: Option, + pub sheets: Vec, +} + +/// Cell range utilities and helper functions +impl CellPosition { + /// Create a new cell position from column and row indices (0-based) + pub fn new(column: usize, row: usize) -> Self { + Self { column, row } + } + + /// Parse Excel cell notation (e.g., "A1", "B2") to CellPosition + pub fn from_excel_notation(notation: &str) -> Result { + let notation = notation.trim().to_uppercase(); + if notation.is_empty() { + return Err(anyhow!("Empty cell notation")); + } + + let mut column = 0; + let mut row_str = String::new(); + let mut found_digit = false; + + for ch in notation.chars() { + if ch.is_ascii_digit() { + found_digit = true; + row_str.push(ch); + } else if ch.is_ascii_alphabetic() && !found_digit { + column = column * 26 + (ch as usize - 'A' as usize + 1); + } else { + return Err(anyhow!("Invalid cell notation: {}", notation)); + } + } + + if row_str.is_empty() { + return Err(anyhow!("No row number found in notation: {}", notation)); + } + + let row = row_str.parse::() + .map_err(|_| anyhow!("Invalid row number: {}", row_str))?; + + if row == 0 { + return Err(anyhow!("Row numbers start from 1")); + } + + Ok(Self::new(column - 1, row - 1)) // Convert to 0-based indexing + } + + /// Convert to Excel notation (e.g., A1, B2) + pub fn to_excel_notation(&self) -> String { + let mut column_str = String::new(); + let mut col = self.column + 1; // Convert to 1-based + + while col > 0 { + col -= 1; + column_str.insert(0, (b'A' + (col % 26) as u8) as char); + col /= 26; + } + + format!("{}{}", column_str, self.row + 1) + } +} + +impl CellRange { + /// Create a new cell range + pub fn new(start: CellPosition, end: CellPosition) -> Self { + Self { start, end } + } + + /// Parse Excel range notation (e.g., "A1:C10") to CellRange + pub fn from_excel_notation(notation: &str) -> Result { + let notation = notation.trim().to_uppercase(); + + if let Some(colon_pos) = notation.find(':') { + let start_str = ¬ation[..colon_pos]; + let end_str = ¬ation[colon_pos + 1..]; + + let start = CellPosition::from_excel_notation(start_str)?; + let end = CellPosition::from_excel_notation(end_str)?; + + Ok(Self::new(start, end)) + } else { + // Single cell range + let pos = CellPosition::from_excel_notation(¬ation)?; + Ok(Self::new(pos.clone(), pos)) + } + } + + /// Convert to Excel notation (e.g., A1:C10) + pub fn to_excel_notation(&self) -> String { + if self.start == self.end { + self.start.to_excel_notation() + } else { + format!("{}:{}", self.start.to_excel_notation(), self.end.to_excel_notation()) + } + } + + /// Check if the range contains a specific position + pub fn contains(&self, pos: &CellPosition) -> bool { + pos.column >= self.start.column && pos.column <= self.end.column && + pos.row >= self.start.row && pos.row <= self.end.row + } + + /// Get all positions within this range + pub fn positions(&self) -> Vec { + let mut positions = Vec::new(); + for row in self.start.row..=self.end.row { + for col in self.start.column..=self.end.column { + positions.push(CellPosition::new(col, row)); + } + } + positions + } +} + +impl ExcelSheet { + /// Create a new empty sheet + pub fn new(name: String) -> Self { + Self { + name, + data: Vec::new(), + } + } + + /// Create a sheet with sample data + pub fn with_blank_data(name: String) -> Self { + Self { + name, + data: Vec::new(), + } + } + + /// Get cell value at specific position + pub fn get_cell(&self, pos: &CellPosition) -> Option<&String> { + self.data.get(pos.row)?.get(pos.column) + } + + /// Set cell value at specific position + pub fn set_cell(&mut self, pos: &CellPosition, value: String) { + // Ensure the data grid is large enough + while self.data.len() <= pos.row { + self.data.push(Vec::new()); + } + + let row = &mut self.data[pos.row]; + while row.len() <= pos.column { + row.push(String::new()); + } + + row[pos.column] = value; + } + + /// Get all cells in a range + pub fn get_range(&self, range: &CellRange) -> Vec<(CellPosition, String)> { + let mut cells = Vec::new(); + for pos in range.positions() { + if let Some(value) = self.get_cell(&pos) { + cells.push((pos, value.clone())); + } + } + cells + } + + /// Get a specific column (all cells in that column) + pub fn get_column(&self, column_index: usize) -> Vec<(usize, String)> { + let mut column_data = Vec::new(); + for (row_index, row) in self.data.iter().enumerate() { + if let Some(value) = row.get(column_index) { + column_data.push((row_index, value.clone())); + } + } + column_data + } + + /// Get a specific row (all cells in that row) + pub fn get_row(&self, row_index: usize) -> Option<&Vec> { + self.data.get(row_index) + } + + /// Display cells in a range with Excel notation + pub fn display_range(&self, range_notation: &str) -> Result { + let range = CellRange::from_excel_notation(range_notation)?; + let cells = self.get_range(&range); + + let mut result = format!("Range {} in sheet '{}':\n", range_notation, self.name); + + if cells.is_empty() { + result.push_str(" (empty)\n"); + return Ok(result); + } + + // Group by rows for better display + let mut rows: HashMap> = HashMap::new(); + for (pos, value) in cells { + rows.entry(pos.row).or_insert_with(Vec::new).push((pos.column, value)); + } + + let mut sorted_rows: Vec<_> = rows.into_iter().collect(); + sorted_rows.sort_by_key(|(row, _)| *row); + + for (row_index, mut columns) in sorted_rows { + columns.sort_by_key(|(col, _)| *col); + result.push_str(&format!(" Row {}: ", row_index + 1)); + + for (col_index, value) in columns { + let pos = CellPosition::new(col_index, row_index); + result.push_str(&format!("{}={} ", pos.to_excel_notation(), value)); + } + result.push('\n'); + } + + Ok(result) + } + + /// Get sheet dimensions (rows, columns) + pub fn dimensions(&self) -> (usize, usize) { + let rows = self.data.len(); + let cols = self.data.iter().map(|row| row.len()).max().unwrap_or(0); + (rows, cols) + } + + /// Get cell range data as Vec> + pub fn get_cell_range(&self, range: &CellRange) -> Result>> { + let start_row = range.start.row; + let end_row = range.end.row; + let start_col = range.start.column; + let end_col = range.end.column; + + let mut result = Vec::new(); + for row_idx in start_row..=end_row { + let mut row_data = Vec::new(); + for col_idx in start_col..=end_col { + let pos = CellPosition::new(col_idx, row_idx); + let value = self.get_cell(&pos).unwrap_or(&String::new()).clone(); + row_data.push(value); + } + result.push(row_data); + } + Ok(result) + } + + /// Get column data by column notation (e.g., "A", "B", "C") + pub fn get_column_data(&self, column_notation: &str) -> Result> { + let pos = CellPosition::from_excel_notation(&format!("{}1", column_notation.to_uppercase()))?; + let column_data = self.get_column(pos.column); + Ok(column_data.into_iter().map(|(_, value)| value).collect()) + } + + /// Set cell value by position + pub fn set_cell_value(&mut self, pos: &CellPosition, value: &str) -> Result<()> { + self.set_cell(pos, value.to_string()); + Ok(()) + } + + /// Get cell value by position + pub fn get_cell_value(&self, pos: &CellPosition) -> Result { + Ok(self.get_cell(pos).unwrap_or(&String::new()).clone()) + } +} + +/// Helper function to convert DataType to String +fn datatype_to_string(cell: &T) -> String { + format!("{}", cell) +} + +impl ExcelWorkbook { + /// Create a new empty workbook + pub fn new() -> Self { + Self { + file_path: None, + sheets: vec![ExcelSheet::with_blank_data("Sheet1".to_string())], + } + } + + /// Load workbook from file + pub fn from_file>(path: P) -> Result { + let path = path.as_ref(); + let mut workbook: Xlsx<_> = open_workbook(path) + .map_err(|e| anyhow!("Failed to open Excel file: {}", e))?; + + let mut sheets = Vec::new(); + + for sheet_name in workbook.sheet_names().to_vec() { + if let Ok(range) = workbook.worksheet_range(&sheet_name) { + let mut data = Vec::new(); + + for row in range.rows() { + let row_data: Vec = row.iter() + .map(|cell| datatype_to_string(cell)) + .collect(); + data.push(row_data); + } + + sheets.push(ExcelSheet { + name: sheet_name.to_string(), + data, + }); + } + } + + Ok(Self { + file_path: Some(path.to_string_lossy().to_string()), + sheets, + }) + } + + /// Save workbook to Excel file using rust_xlsxwriter + pub fn save_to_file>(&self, path: P) -> Result<()> { + let mut workbook = Workbook::new(); + + for sheet in &self.sheets { + let worksheet = workbook.add_worksheet() + .set_name(&sheet.name) + .map_err(|e| anyhow!("Failed to add worksheet '{}': {}", sheet.name, e))?; + + // Write data to worksheet + for (row_index, row) in sheet.data.iter().enumerate() { + for (col_index, value) in row.iter().enumerate() { + if !value.is_empty() { + // Try to parse as number first, then write as string + if let Ok(num) = value.parse::() { + worksheet.write_number(row_index as u32, col_index as u16, num) + .map_err(|e| anyhow!("Failed to write number at {}:{}: {}", row_index, col_index, e))?; + } else { + worksheet.write_string(row_index as u32, col_index as u16, value) + .map_err(|e| anyhow!("Failed to write string at {}:{}: {}", row_index, col_index, e))?; + } + } + } + } + } + + workbook.save(path.as_ref()) + .map_err(|e| anyhow!("Failed to save workbook: {}", e))?; + + Ok(()) + } + + /// Get sheet by name + pub fn get_sheet(&self, name: &str) -> Option<&ExcelSheet> { + self.sheets.iter().find(|sheet| sheet.name == name) + } + + /// Get mutable sheet by name + pub fn get_sheet_mut(&mut self, name: &str) -> Option<&mut ExcelSheet> { + self.sheets.iter_mut().find(|sheet| sheet.name == name) + } + + /// Add a new sheet + pub fn add_sheet(&mut self, sheet: ExcelSheet) { + self.sheets.push(sheet); + } + + /// Display specific range across all sheets + pub fn display_range_all_sheets(&self, range_notation: &str) -> Result { + let mut result = format!("Range {} across all sheets:\n\n", range_notation); + + for sheet in &self.sheets { + match sheet.display_range(range_notation) { + Ok(sheet_display) => result.push_str(&sheet_display), + Err(e) => result.push_str(&format!("Error in sheet '{}': {}\n", sheet.name, e)), + } + result.push('\n'); + } + + Ok(result) + } + + /// Display specific column across all sheets + pub fn display_column_all_sheets(&self, column_notation: &str) -> Result { + let pos = CellPosition::from_excel_notation(&format!("{}1", column_notation.to_uppercase()))?; + let mut result = format!("Column {} across all sheets:\n\n", column_notation.to_uppercase()); + + for sheet in &self.sheets { + result.push_str(&format!("Sheet '{}':\n", sheet.name)); + let column_data = sheet.get_column(pos.column); + + if column_data.is_empty() { + result.push_str(" (empty column)\n"); + } else { + for (row_index, value) in column_data { + if !value.is_empty() { + let cell_pos = CellPosition::new(pos.column, row_index); + result.push_str(&format!(" {} = {}\n", cell_pos.to_excel_notation(), value)); + } + } + } + result.push('\n'); + } + + Ok(result) + } + + /// Convert to CSV string (for backward compatibility) + pub fn to_csv_string(&self) -> String { + let mut result = String::new(); + + for (sheet_idx, sheet) in self.sheets.iter().enumerate() { + if sheet_idx > 0 { + result.push_str("\n\n--- Sheet: "); + result.push_str(&sheet.name); + result.push_str(" ---\n"); + } + + for row in &sheet.data { + let csv_row = row.join(","); + result.push_str(&csv_row); + result.push('\n'); + } + } + + result + } + + /// Get content summary with enhanced information + pub fn get_content_summary(&self) -> String { + let mut summary = String::new(); + summary.push_str(&format!("Excel workbook with {} sheet(s):\n\n", self.sheets.len())); + + for sheet in &self.sheets { + let (rows, cols) = sheet.dimensions(); + summary.push_str(&format!("Sheet '{}': {} rows, {} columns\n", + sheet.name, rows, cols + )); + + // Show first few rows as sample + for (i, row) in sheet.data.iter().take(5).enumerate() { + summary.push_str(&format!(" Row {}: {}\n", i + 1, row.join(" | "))); + } + + if sheet.data.len() > 5 { + summary.push_str(&format!(" ... and {} more rows\n", sheet.data.len() - 5)); + } + summary.push('\n'); + } + + summary + } + + /// Quick access functions for common operations + pub fn get_cell_value(&self, sheet_name: &str, cell_notation: &str) -> Result> { + let sheet = self.get_sheet(sheet_name) + .ok_or_else(|| anyhow!("Sheet '{}' not found", sheet_name))?; + + let pos = CellPosition::from_excel_notation(cell_notation)?; + Ok(sheet.get_cell(&pos).cloned()) + } + + pub fn set_cell_value(&mut self, sheet_name: &str, cell_notation: &str, value: String) -> Result<()> { + let sheet = self.get_sheet_mut(sheet_name) + .ok_or_else(|| anyhow!("Sheet '{}' not found", sheet_name))?; + + let pos = CellPosition::from_excel_notation(cell_notation)?; + sheet.set_cell(&pos, value); + Ok(()) + } +} \ No newline at end of file diff --git a/examples/excel-copilot/src-tauri/src/excel_interaction.rs b/examples/excel-copilot/src-tauri/src/excel_interaction.rs new file mode 100644 index 00000000..6aca6357 --- /dev/null +++ b/examples/excel-copilot/src-tauri/src/excel_interaction.rs @@ -0,0 +1,1357 @@ +use anyhow::{Result, anyhow}; +use serde::{Deserialize, Serialize}; +use std::time::Duration; +use terminator::{Desktop, Selector}; + +/// Represents an Excel cell with its address, value, and optional formula +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ExcelCell { + pub address: String, + pub value: String, + pub formula: Option, +} + +/// Represents a range of Excel cells with their values +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ExcelRange { + pub start_cell: String, + pub end_cell: String, + pub values: Vec>, +} + +/// Excel automation interface using the Terminator UI automation library +#[derive(Clone)] +pub struct ExcelAutomation { + desktop: Desktop, +} + +impl ExcelAutomation { + /// Create a new Excel automation instance + pub async fn new() -> Result { + let desktop = Desktop::new(false, false)?; + Ok(Self { desktop }) + } + + /// Get or open Excel application + pub async fn get_excel_application(&self) -> Result { + // First try to find existing Excel window + match self.desktop.application("Excel") { + Ok(excel_app) => { + println!("Found existing Excel application"); + Ok(excel_app) + } + Err(_) => { + // If not found, try to open Excel + println!("Opening Excel application..."); + self.desktop.open_application("excel") + .map_err(|e| anyhow!("Failed to open Excel: {}", e))?; + + // Wait a bit for Excel to fully load + tokio::time::sleep(Duration::from_millis(2000)).await; + + // Try to get it again + self.desktop.application("Excel") + .map_err(|e| anyhow!("Failed to find Excel after opening: {}", e)) + } + } + } + + /// Open an Excel file + pub async fn open_excel_file(&self, file_path: &str) -> Result<()> { + println!("Opening Excel file: {}", file_path); + + // Open the file using system default (Excel) + self.desktop.open_file(file_path)?; + + // Wait for the file to open + tokio::time::sleep(Duration::from_millis(3000)).await; + + Ok(()) + } + + /// Create a new Excel workbook + pub async fn create_new_workbook(&self) -> Result<()> { + println!("Creating new Excel workbook"); + + let excel_app = self.get_excel_application().await?; + + // Try to use Ctrl+N to create new workbook + excel_app.press_key("{ctrl}n")?; + + // Wait for new workbook to be created + tokio::time::sleep(Duration::from_millis(2000)).await; + + Ok(()) + } + + /// Get the Excel window using proper targeting based on automation patterns + async fn get_excel_window(&self) -> Result { + // First try to find by window criteria with Excel in title (most reliable) + match self.desktop.find_window_by_criteria(Some("Excel"), Some(Duration::from_millis(5000))).await { + Ok(window) => { + println!("Found Excel window using title criteria"); + Ok(window) + } + Err(_) => { + // Fallback: try to find Excel application + println!("Fallback: searching for Excel application"); + match self.desktop.application("Excel") { + Ok(app) => { + println!("Found Excel application"); + Ok(app) + } + Err(e) => Err(anyhow!("Could not find Excel window or application: {}", e)) + } + } + } + } + + /// Read a single cell value using proper Excel targeting + pub async fn read_cell(&self, cell_address: &str) -> Result { + println!("Reading cell: {}", cell_address); + + let excel_window = self.get_excel_window().await?; + + // Use Name locator format for cells (like 'Name:A1') + let cell_selector = Selector::Name(cell_address.to_string()); + match excel_window.locator(cell_selector)?.first(Some(Duration::from_millis(2000))).await { + Ok(cell_element) => { + println!("Found cell {} using Name locator", cell_address); + + // Click on the cell to select it + cell_element.click()?; + tokio::time::sleep(Duration::from_millis(300)).await; + + // Try to get the cell value using text extraction with max depth + let value = match cell_element.text(3) { + Ok(text) => { + if text.trim().is_empty() { + // If direct text is empty, try name attribute + match cell_element.attributes().name { + Some(name) => name, + None => "".to_string() + } + } else { + text.trim().to_string() + } + } + Err(_) => { + // Fallback to attributes if text extraction fails + cell_element.attributes().name.unwrap_or_default() + } + }; + + Ok(ExcelCell { + address: cell_address.to_string(), + value, + formula: None, + }) + } + Err(_) => { + println!("Cell {} not found with Name locator, using navigation approach", cell_address); + + // Fallback: navigate to cell using Ctrl+G (Go To) + excel_window.press_key("{ctrl}g")?; + tokio::time::sleep(Duration::from_millis(500)).await; + + // Type the cell address + excel_window.type_text(cell_address, false)?; + excel_window.press_key("{enter}")?; + tokio::time::sleep(Duration::from_millis(500)).await; + + // Try to get active cell value - use formula bar or selected cell + let value = "".to_string(); // Placeholder + + Ok(ExcelCell { + address: cell_address.to_string(), + value, + formula: None, + }) + } + } + } + + /// Write a value to a cell using proper Excel targeting + pub async fn write_cell(&self, cell_address: &str, value: &str) -> Result<()> { + println!("Writing to cell {}: {}", cell_address, value); + + let excel_window = self.get_excel_window().await?; + + // Try to find and select the cell directly first + let cell_selector = Selector::Name(cell_address.to_string()); + match excel_window.locator(cell_selector)?.first(Some(Duration::from_millis(2000))).await { + Ok(cell_element) => { + println!("Found cell {} directly, clicking and typing", cell_address); + + // Click on the cell to select it + cell_element.click()?; + tokio::time::sleep(Duration::from_millis(300)).await; + + // Clear any existing content and type new value + excel_window.press_key("{delete}")?; + tokio::time::sleep(Duration::from_millis(100)).await; + + excel_window.type_text(value, false)?; + excel_window.press_key("{enter}")?; + + println!("Successfully wrote '{}' to cell {}", value, cell_address); + } + Err(_) => { + println!("Cell {} not found directly, using navigation approach", cell_address); + + // Fallback: navigate to cell using Ctrl+G + excel_window.press_key("{ctrl}g")?; + tokio::time::sleep(Duration::from_millis(500)).await; + + // Type the cell address + excel_window.type_text(cell_address, false)?; + excel_window.press_key("{enter}")?; + tokio::time::sleep(Duration::from_millis(500)).await; + + // Clear and type the value + excel_window.press_key("{delete}")?; + tokio::time::sleep(Duration::from_millis(100)).await; + + excel_window.type_text(value, false)?; + excel_window.press_key("{enter}")?; + + println!("Successfully wrote '{}' to cell {} using navigation", value, cell_address); + } + } + + Ok(()) + } + + /// Read a range of cells using selection and clipboard + pub async fn read_range(&self, start_cell: &str, end_cell: &str) -> Result { + println!("Reading range: {}:{}", start_cell, end_cell); + + let excel_window = self.get_excel_window().await?; + + // Navigate to start cell using Ctrl+G + excel_window.press_key("{ctrl}g")?; + tokio::time::sleep(Duration::from_millis(500)).await; + + // Type the range (e.g., "A1:D12") + let range_notation = format!("{}:{}", start_cell, end_cell); + excel_window.type_text(&range_notation, false)?; + excel_window.press_key("{enter}")?; + tokio::time::sleep(Duration::from_millis(500)).await; + + // Copy the selected range to clipboard + excel_window.press_key("{ctrl}c")?; + tokio::time::sleep(Duration::from_millis(500)).await; + + // Get clipboard content using arboard + let clipboard_content = match self.get_clipboard_content_real().await { + Ok(content) => content, + Err(e) => { + println!("Error reading clipboard: {}", e); + return Ok(ExcelRange { + start_cell: start_cell.to_string(), + end_cell: end_cell.to_string(), + values: vec![], + }); + } + }; + + println!("Clipboard content: {}", clipboard_content); + + // Parse the clipboard content (tab-separated values, line-separated rows) + let values = self.parse_clipboard_to_matrix(&clipboard_content); + + println!("Parsed {} rows from range", values.len()); + + Ok(ExcelRange { + start_cell: start_cell.to_string(), + end_cell: end_cell.to_string(), + values, + }) + } + + /// Get real clipboard content using arboard + async fn get_clipboard_content_real(&self) -> Result { + let mut clipboard = arboard::Clipboard::new() + .map_err(|e| anyhow::anyhow!("Failed to access clipboard: {}", e))?; + + let content = clipboard.get_text() + .map_err(|e| anyhow::anyhow!("Failed to get clipboard text: {}", e))?; + + Ok(content) + } + + /// Get clipboard content (deprecated - use get_clipboard_content_real) + async fn get_clipboard_content(&self) -> Result { + self.get_clipboard_content_real().await + } + + /// Set a formula in a cell + pub async fn set_formula(&self, cell_address: &str, formula: &str) -> Result<()> { + println!("Setting formula in {}: {}", cell_address, formula); + + let excel_window = self.get_excel_window().await?; + + // Navigate to the cell + excel_window.press_key("{ctrl}g")?; + tokio::time::sleep(Duration::from_millis(500)).await; + + excel_window.type_text(cell_address, false)?; + excel_window.press_key("{enter}")?; + tokio::time::sleep(Duration::from_millis(500)).await; + + // Type the formula (ensure it starts with =) + let formula_text = if formula.starts_with('=') { + formula.to_string() + } else { + format!("={}", formula) + }; + + excel_window.type_text(&formula_text, false)?; + excel_window.press_key("{enter}")?; + + Ok(()) + } + + /// Take a screenshot of the Excel window + pub async fn take_screenshot(&self) -> Result> { + println!("Taking screenshot of Excel window"); + + let screenshot = self.desktop.capture_screen().await?; + Ok(screenshot.image_data) + } + + /// Extract cell value from OCR text + fn extract_cell_value_from_ocr(&self, ocr_text: &str, _cell_address: &str) -> String { + ocr_text.lines() + .find(|line| !line.trim().is_empty()) + .unwrap_or("") + .trim() + .to_string() + } + + /// Parse clipboard content to matrix + fn parse_clipboard_to_matrix(&self, content: &str) -> Vec> { + content + .lines() + .map(|line| { + line.split('\t') + .map(|cell| cell.to_string()) + .collect() + }) + .collect() + } + + /// Save the current Excel file + pub async fn save_current_file(&self) -> Result<()> { + println!("Saving current Excel file"); + + let excel_window = self.get_excel_window().await?; + + // Use Ctrl+S to save + excel_window.press_key("{ctrl}s")?; + tokio::time::sleep(Duration::from_millis(1000)).await; + + Ok(()) + } + + /// Get sheet overview by reading the actual Excel file without modifying it + pub async fn get_sheet_overview_from_file(&self, file_path: Option<&str>) -> Result { + // First save the current state to ensure the file is up to date + self.save_current_file().await?; + + if let Some(path) = file_path { + match crate::excel::ExcelWorkbook::from_file(path) { + Ok(workbook) => { + let mut overview = String::from("CURRENT EXCEL SHEET OVERVIEW:\n"); + overview.push_str("===================================\n"); + + if workbook.sheets.is_empty() { + overview.push_str("No sheets found in the workbook.\n"); + return Ok(overview); + } + + // Use the first sheet (most common case) + let sheet = &workbook.sheets[0]; + let mut non_empty_cells = Vec::new(); + + // Find all non-empty cells + for (row_idx, row) in sheet.data.iter().enumerate() { + for (col_idx, cell_value) in row.iter().enumerate() { + if !cell_value.trim().is_empty() { + let col_letter = self.column_index_to_letter(col_idx); + let cell_address = format!("{}{}", col_letter, row_idx + 1); + non_empty_cells.push((cell_address, cell_value.clone())); + } + } + } + + if non_empty_cells.is_empty() { + overview.push_str("The sheet appears empty - no non-empty cells found.\n"); + } else { + overview.push_str(&format!("Found {} non-empty cells:\n\n", non_empty_cells.len())); + + for (address, value) in non_empty_cells.iter() { + overview.push_str(&format!(" {}: '{}'\n", address, value)); + } + + overview.push_str("\n===================================\n"); + overview.push_str(&format!("Total non-empty cells: {}", non_empty_cells.len())); + } + + Ok(overview) + } + Err(e) => { + Err(anyhow!("Failed to read Excel file: {}", e)) + } + } + } else { + Err(anyhow!("No file path provided")) + } + } + + /// Helper function to convert column index to Excel letter notation + fn column_index_to_letter(&self, mut index: usize) -> String { + let mut result = String::new(); + loop { + result.insert(0, (b'A' + (index % 26) as u8) as char); + if index < 26 { + break; + } + index = index / 26 - 1; + } + result + } + + /// Send a request to Microsoft Excel Copilot with proper selection sequence + pub async fn send_request_to_excel_copilot(&self, request: &str) -> Result { + println!("Sending request to Excel Copilot: {}", request); + + let excel_window = self.get_excel_window().await?; + + // Ensure Excel window is focused and active + excel_window.focus()?; + tokio::time::sleep(Duration::from_millis(500)).await; + + // Step 1: Find and click the Copilot button + let copilot_selector = Selector::Name("Copilot".to_string()); + match excel_window.locator(copilot_selector)?.first(Some(Duration::from_millis(3000))).await { + Ok(copilot_button) => { + println!("Found Excel Copilot button, clicking it"); + copilot_button.click()?; + tokio::time::sleep(Duration::from_millis(1000)).await; + + // Step 2: Find and click "Ask Copilot" button + let ask_copilot_selector = Selector::Name("Ask Copilot".to_string()); + match excel_window.locator(ask_copilot_selector)?.first(Some(Duration::from_millis(3000))).await { + Ok(ask_copilot_button) => { + println!("Found 'Ask Copilot' button, clicking it"); + ask_copilot_button.click()?; + + // Step 3: Wait ~2s for Copilot to load + println!("Waiting for Copilot to load..."); + tokio::time::sleep(Duration::from_millis(5000)).await; + + // Step 4: Type the request + println!("Typing request to Copilot: {}", request); + excel_window.type_text(request, false)?; + + // Step 5: Send the request (Enter key) + excel_window.press_key("{enter}")?; + tokio::time::sleep(Duration::from_millis(3000)).await; + + // Step 6: Check for Apply button and click it if present + let apply_result = self.check_and_apply_copilot_changes(&excel_window).await?; + + Ok(format!("Copilot request '{}' sent successfully. {}", request, apply_result)) + } + Err(_) => { + println!("'Ask Copilot' button not found, trying to type request directly"); + + // Wait a bit and try to type the request directly + tokio::time::sleep(Duration::from_millis(2000)).await; + excel_window.type_text(request, false)?; + excel_window.press_key("{enter}")?; + tokio::time::sleep(Duration::from_millis(3000)).await; + + let apply_result = self.check_and_apply_copilot_changes(&excel_window).await?; + Ok(format!("Copilot request '{}' sent (fallback method). {}", request, apply_result)) + } + } + } + Err(_) => { + println!("Copilot button not found, trying alternative approach"); + + // Alternative: Try to use keyboard shortcut to access Copilot (Alt+H, then F, then X) + // "%h" = Alt+H, then "fx" = F then X + excel_window.press_key("%h")?; // Alt+H to open Home tab + tokio::time::sleep(Duration::from_millis(500)).await; + excel_window.type_text("fx", false)?; // F then X to open Copilot + tokio::time::sleep(Duration::from_millis(1500)).await; + + // Type the request + excel_window.type_text(request, false)?; + excel_window.press_key("{enter}")?; + tokio::time::sleep(Duration::from_millis(3000)).await; + + let apply_result = self.check_and_apply_copilot_changes(&excel_window).await?; + Ok(format!("Copilot request '{}' sent via keyboard shortcut. {}", request, apply_result)) + } + } + } + + /// Check for Apply button in Copilot response and click it if present + async fn check_and_apply_copilot_changes(&self, excel_window: &terminator::UIElement) -> Result { + println!("Checking for Apply button in Copilot response"); + + // Try to find Apply button with multiple attempts, every 5 seconds + let max_attempts = 15; // 30 seconds total (15 attempts × 2 seconds) + let mut attempt = 0; + + while attempt < max_attempts { + attempt += 1; + println!("Attempt {} of {} to find Apply button", attempt, max_attempts); + + // Strategy 1: Look for "Apply" button with short name + println!("Strategy 1: Looking for exact 'Apply' button"); + let apply_selector = Selector::Name("Apply".to_string()); + match excel_window.locator(apply_selector)?.all(Some(Duration::from_millis(2000)), None).await { + Ok(found_elements) => { + println!("Found {} elements named 'Apply'", found_elements.len()); + + // Examine each element and find the real button + for (i, element) in found_elements.iter().enumerate() { + let attributes = element.attributes(); + let role = attributes.role.as_str(); + let name = attributes.name.as_deref().unwrap_or(""); + + println!("Apply element {}: role='{}', name='{}', name_length={}", i, role, name, name.len()); + + // Look for the Apply button: exact name match and button role + if name == "Apply" && name.len() == 5 { + if role.contains("button") || role.contains("Button") || + role.eq_ignore_ascii_case("menuitem") || + role.eq_ignore_ascii_case("listitem") { + println!("Found real Apply button (exact match): role='{}', clicking it", role); + element.click()?; + tokio::time::sleep(Duration::from_millis(2000)).await; + return Ok("Changes applied successfully via Apply button.".to_string()); + } + } + } + + // If no exact match, try clicking any reasonable short element + for (i, element) in found_elements.iter().enumerate() { + let attributes = element.attributes(); + let role = attributes.role.as_str(); + let name = attributes.name.as_deref().unwrap_or(""); + + if name.len() <= 10 && !role.eq_ignore_ascii_case("group") && + !role.eq_ignore_ascii_case("text") && !name.contains("You said:") { + println!("Trying Apply element {} as fallback: role='{}', name='{}'", i, role, name); + match element.click() { + Ok(_) => { + tokio::time::sleep(Duration::from_millis(2000)).await; + return Ok("Changes applied successfully via Apply element (fallback).".to_string()); + } + Err(e) => { + println!("Failed to click Apply element {}: {}", i, e); + } + } + } + } + } + Err(_) => { + println!("No elements named 'Apply' found in strategy 1"); + } + } + + // Strategy 2: Look for buttons with roles that might be Apply buttons + println!("Strategy 2: Looking for button-role elements"); + let button_roles = vec!["Button", "button", "MenuItem", "menuitem", "ListItem", "listitem"]; + + for role_name in &button_roles { + let role_selector = Selector::Role { + role: role_name.to_string(), + name: None + }; + match excel_window.locator(role_selector)?.all(Some(Duration::from_millis(1000)), None).await { + Ok(elements) => { + // Check up to 10 elements of each role + for (_index, element) in elements.iter().take(10).enumerate() { + let attributes = element.attributes(); + let name = attributes.name.as_deref().unwrap_or(""); + + // Look for exact "Apply" match in button-role elements + if name == "Apply" || name == "Apply Changes" || name == "Apply Suggestion" { + println!("Found {} element with name '{}', clicking it", role_name, name); + match element.click() { + Ok(_) => { + tokio::time::sleep(Duration::from_millis(2000)).await; + return Ok(format!("Changes applied successfully via {} button: '{}'.", role_name, name)); + } + Err(e) => { + println!("Failed to click {} element '{}': {}", role_name, name, e); + } + } + } + } + } + Err(_) => { + println!("No {} elements found", role_name); + } + } + } + + // Check alternative button names + let alt_button_names = vec![ + "Create", + "Generate", + "Apply Changes", + "Accept", + "Confirm", + "Execute", + "Run" + ]; + + for button_name in &alt_button_names { + let selector = Selector::Name(button_name.to_string()); + match excel_window.locator(selector)?.first(Some(Duration::from_millis(1000))).await { + Ok(button) => { + let attributes = button.attributes(); + let role = attributes.role.as_str(); + let actual_name = attributes.name.as_deref().unwrap_or("Unknown"); + + println!("Found alternative element '{}' - role: '{}', name: '{}'", button_name, role, actual_name); + + // Skip elements that are clearly not buttons + if role.eq_ignore_ascii_case("group") || + role.eq_ignore_ascii_case("text") || + role.eq_ignore_ascii_case("tabitem") || + role.eq_ignore_ascii_case("tab") || + actual_name.contains("You said:") { + println!("Skipping non-button element for {} (role: {})", button_name, role); + continue; + } + + // Try to verify it's a button-like element + if role.contains("button") || role.contains("Button") || + actual_name.eq_ignore_ascii_case(button_name) { + println!("Found alternative action button: {} (role: {}), clicking it", actual_name, role); + button.click()?; + tokio::time::sleep(Duration::from_millis(2000)).await; + return Ok(format!("Changes applied successfully via {} button.", actual_name)); + } else { + // Try clicking anyway but be more cautious + if actual_name.len() < 15 { + println!("Found {} element but uncertain if it's clickable (role: {}), trying to click anyway", actual_name, role); + match button.click() { + Ok(_) => { + tokio::time::sleep(Duration::from_millis(2000)).await; + return Ok(format!("Changes applied successfully via {} element.", actual_name)); + } + Err(_) => { + println!("Failed to click {} element, continuing search", actual_name); + continue; + } + } + } + } + } + Err(_) => continue, + } + } + + // Additional search: Look for suggestion buttons that might appear in Copilot responses + // These might have specific patterns or be located in suggestion areas + println!("Searching for Copilot suggestion buttons on attempt {}", attempt); + + // Try to find buttons in suggestion areas or with suggestion-related roles + let suggestion_selectors = vec![ + ("Apply conditional formatting", "Apply the conditional formatting suggestion"), + ("Apply suggestion", "Apply this suggestion"), + ("Use this suggestion", "Use the Copilot suggestion"), + ]; + + for (search_text, description) in suggestion_selectors { + // Look for buttons containing these phrases + let selector = Selector::Name(search_text.to_string()); + match excel_window.locator(selector)?.first(Some(Duration::from_millis(1000))).await { + Ok(element) => { + let attributes = element.attributes(); + let role = attributes.role.as_str(); + let name = attributes.name.as_deref().unwrap_or(""); + + println!("Found suggestion element: '{}' - role: '{}', name: '{}'", search_text, role, name); + + // Skip clearly non-button elements + if !role.eq_ignore_ascii_case("group") && + !role.eq_ignore_ascii_case("text") && + !role.eq_ignore_ascii_case("tabitem") { + println!("Trying to click suggestion button: {}", description); + match element.click() { + Ok(_) => { + tokio::time::sleep(Duration::from_millis(2000)).await; + return Ok(format!("Changes applied successfully via suggestion: {}.", description)); + } + Err(e) => { + println!("Failed to click suggestion '{}': {}", search_text, e); + } + } + } + } + Err(_) => continue, + } + } + + // Wait 2 seconds before next attempt (except on last attempt) + if attempt < max_attempts { + println!("Waiting 5 seconds before next attempt..."); + tokio::time::sleep(Duration::from_millis(2000)).await; + } + } + + println!("No Apply button found after {} attempts", max_attempts); + Ok("Request sent to Copilot. No Apply button found after multiple attempts - changes might be applied automatically.".to_string()) + } + + /// Send a request to Excel Copilot with specific range selection + pub async fn send_request_to_excel_copilot_with_range(&self, request: &str, range: &str) -> Result { + println!("Sending request to Excel Copilot with range {}: {}", range, request); + + let excel_window = self.get_excel_window().await?; + excel_window.focus()?; + tokio::time::sleep(Duration::from_millis(500)).await; + + // Step 1: Select the specified range first + println!("Selecting range: {}", range); + excel_window.press_key("{ctrl}g")?; // Go to dialog + tokio::time::sleep(Duration::from_millis(500)).await; + excel_window.type_text(range, false)?; + excel_window.press_key("{enter}")?; + tokio::time::sleep(Duration::from_millis(1000)).await; + + // Step 2-6: Follow the normal Copilot sequence + self.send_request_to_excel_copilot(request).await + } + + /// Helper function to format common Copilot requests with range + pub async fn format_cells_with_copilot(&self, range: &str, format_description: &str) -> Result { + let request = format!("Format these cells with {}", format_description); + self.send_request_to_excel_copilot_with_range(&request, range).await + } + + /// Create chart with Copilot using specific data range + pub async fn create_chart_with_copilot(&self, data_range: &str, chart_type: &str) -> Result { + let request = format!("Create a {} chart from this data", chart_type); + self.send_request_to_excel_copilot_with_range(&request, data_range).await + } + + /// Apply conditional formatting with Copilot to specific range + pub async fn apply_conditional_formatting_with_copilot(&self, range: &str, condition: &str) -> Result { + let request = format!("Apply conditional formatting where {}", condition); + self.send_request_to_excel_copilot_with_range(&request, range).await + } + + /// Interact with Excel Copilot for various tasks (updated method) + pub async fn interact_with_copilot(&self, task_description: &str) -> Result { + println!("Interacting with Excel Copilot for task: {}", task_description); + + // Ensure Excel is active and focused + let excel_window = self.get_excel_window().await?; + excel_window.focus()?; + tokio::time::sleep(Duration::from_millis(500)).await; + + // Send the request to Copilot using the updated method + let result = self.send_request_to_excel_copilot(task_description).await?; + + // Wait a bit more for changes to take effect + tokio::time::sleep(Duration::from_millis(1000)).await; + + Ok(result) + } + + /// Get the complete UI tree context of Excel window for better element targeting + pub async fn get_excel_ui_context(&self) -> Result { + println!("Getting complete Excel UI context"); + + let excel_window = self.get_excel_window().await?; + + // Get detailed information about the Excel window structure + let mut context = String::new(); + context.push_str("EXCEL UI CONTEXT:\n"); + context.push_str("==================\n"); + + // Get window attributes + let window_attrs = excel_window.attributes(); + context.push_str(&format!("Window Title: '{}'\n", window_attrs.name.as_deref().unwrap_or("Unknown"))); + context.push_str(&format!("Window Role: '{}'\n", window_attrs.role.as_str())); + context.push_str(&format!("Window Label: '{}'\n", window_attrs.label.as_deref().unwrap_or("Unknown"))); + + // Try to get child elements for navigation context + context.push_str("\nKey UI Elements:\n"); + context.push_str("-----------------\n"); + + // Look for common Excel UI elements that can be used as landmarks + let ui_elements_to_find = vec![ + ("Name Box", "Name Box"), + ("Formula Bar", "Formula Bar"), + ("Ribbon", "Ribbon"), + ("Sheet Tab", "Sheet"), + ("Copilot", "Copilot"), + ("Ask Copilot", "Ask Copilot"), + ("Apply", "Apply"), + ]; + + for (element_name, selector_name) in ui_elements_to_find { + let selector = Selector::Name(selector_name.to_string()); + match excel_window.locator(selector)?.all(Some(Duration::from_millis(1000)), None).await { + Ok(elements) => { + if !elements.is_empty() { + context.push_str(&format!("✓ {}: {} element(s) found\n", element_name, elements.len())); + + // For important elements, get more details + if element_name == "Copilot" || element_name == "Apply" { + for (i, element) in elements.iter().take(3).enumerate() { + let attrs = element.attributes(); + context.push_str(&format!(" - {}[{}]: role='{}', name='{}', label='{}'\n", + element_name, i, + attrs.role.as_str(), + attrs.name.as_deref().unwrap_or("N/A"), + attrs.label.as_deref().unwrap_or("N/A") + )); + } + } + } else { + context.push_str(&format!("✗ {}: Not found\n", element_name)); + } + } + Err(_) => { + context.push_str(&format!("✗ {}: Search failed\n", element_name)); + } + } + } + + // Get active sheet information + context.push_str("\nActive Sheet Info:\n"); + context.push_str("------------------\n"); + + // Try to identify the current active cell + let name_box_selector = Selector::Name("Name Box".to_string()); + match excel_window.locator(name_box_selector)?.first(Some(Duration::from_millis(1000))).await { + Ok(name_box) => { + match name_box.text(1) { + Ok(active_cell) => { + context.push_str(&format!("Active Cell: {}\n", active_cell.trim())); + } + Err(_) => { + context.push_str("Active Cell: Could not determine\n"); + } + } + } + Err(_) => { + context.push_str("Active Cell: Name Box not accessible\n"); + } + } + + context.push_str("\n==================\n"); + + Ok(context) + } + + /// Paste TSV data into Excel starting from a specific cell with safety checks + pub async fn paste_tsv_data(&self, start_cell: &str, tsv_data: &str, verify_safe: bool) -> Result { + println!("Pasting TSV data starting from cell: {}", start_cell); + + let excel_window = self.get_excel_window().await?; + excel_window.focus()?; + tokio::time::sleep(Duration::from_millis(500)).await; + + // Parse TSV to understand the data dimensions + let lines: Vec<&str> = tsv_data.lines().collect(); + let rows = lines.len(); + let cols = lines.get(0).map(|line| line.split('\t').count()).unwrap_or(0); + + if rows == 0 || cols == 0 { + return Err(anyhow!("Invalid TSV data: {} rows, {} columns", rows, cols)); + } + + println!("TSV data dimensions: {} rows × {} columns", rows, cols); + + // Calculate the target range + let start_pos = self.parse_cell_address(start_cell)?; + let end_col_letter = self.column_index_to_letter(start_pos.1 + cols - 1); + let end_row = start_pos.0 + rows - 1; + let target_range = format!("{}:{}{}", start_cell, end_col_letter, end_row); + + println!("Target range will be: {}", target_range); + + // Safety check: verify the target area if requested + if verify_safe { + println!("Performing safety check for range: {}", target_range); + + // Read the target range to check if it contains data + match self.read_range(start_cell, &format!("{}{}", end_col_letter, end_row)).await { + Ok(existing_range) => { + let non_empty_cells = existing_range.values.iter() + .flatten() + .filter(|cell| !cell.trim().is_empty()) + .count(); + + if non_empty_cells > 0 { + return Err(anyhow!( + "SAFETY CHECK FAILED: Target range {} contains {} non-empty cells. Use verify_safe=false to override, or choose a different starting cell.", + target_range, non_empty_cells + )); + } else { + println!("Safety check passed: target range is empty"); + } + } + Err(e) => { + println!("Warning: Could not verify target range safety: {}", e); + } + } + } + + // Navigate to the starting cell + excel_window.press_key("{ctrl}g")?; + tokio::time::sleep(Duration::from_millis(500)).await; + excel_window.type_text(start_cell, false)?; + excel_window.press_key("{enter}")?; + tokio::time::sleep(Duration::from_millis(500)).await; + + // Clear clipboard first + match self.clear_clipboard().await { + Ok(_) => println!("Clipboard cleared"), + Err(e) => println!("Warning: Could not clear clipboard: {}", e), + } + + // Copy TSV data to clipboard + self.set_clipboard_content(tsv_data).await?; + + // Paste the data + excel_window.press_key("{ctrl}v")?; + tokio::time::sleep(Duration::from_millis(1000)).await; + + // Press Escape to clear any selection + excel_window.press_key("{escape}")?; + tokio::time::sleep(Duration::from_millis(300)).await; + + // Save the file + self.save_current_file().await?; + + // Verify the paste operation by reading a sample + let verification_result = match self.read_cell(start_cell).await { + Ok(cell) => { + let expected_first_value = lines.get(0) + .and_then(|line| line.split('\t').next()) + .unwrap_or(""); + + if cell.value.trim() == expected_first_value.trim() { + "✓ Paste verification successful" + } else { + "⚠ Paste verification: values may not match exactly" + } + } + Err(_) => "⚠ Could not verify paste operation" + }; + + Ok(format!( + "SUCCESS: Pasted TSV data ({} rows × {} columns) into range {}. {}. Data includes: first cell '{}', last calculated cell '{}{}'", + rows, cols, target_range, verification_result, + lines.get(0).and_then(|line| line.split('\t').next()).unwrap_or(""), + end_col_letter, end_row + )) + } + + /// Parse cell address like "A1" to (row_index, col_index) zero-based + fn parse_cell_address(&self, cell_address: &str) -> Result<(usize, usize)> { + let cell_address = cell_address.trim().to_uppercase(); + + let mut col_str = String::new(); + let mut row_str = String::new(); + let mut found_digit = false; + + for ch in cell_address.chars() { + if ch.is_ascii_digit() { + found_digit = true; + row_str.push(ch); + } else if ch.is_ascii_alphabetic() && !found_digit { + col_str.push(ch); + } else { + return Err(anyhow!("Invalid cell address format: {}", cell_address)); + } + } + + if col_str.is_empty() || row_str.is_empty() { + return Err(anyhow!("Invalid cell address: {}", cell_address)); + } + + // Convert column letters to index (A=0, B=1, ..., Z=25, AA=26, etc.) + let mut col_index = 0; + for ch in col_str.chars() { + col_index = col_index * 26 + (ch as usize - 'A' as usize + 1); + } + col_index -= 1; // Convert to 0-based + + // Convert row to index (1-based to 0-based) + let row_index = row_str.parse::()? - 1; + + Ok((row_index, col_index)) + } + + /// Set clipboard content using arboard + async fn set_clipboard_content(&self, content: &str) -> Result<()> { + let mut clipboard = arboard::Clipboard::new() + .map_err(|e| anyhow::anyhow!("Failed to access clipboard: {}", e))?; + + clipboard.set_text(content) + .map_err(|e| anyhow::anyhow!("Failed to set clipboard text: {}", e))?; + + Ok(()) + } + + /// Type a prompt with proper line break handling (DEPRECATED - now using clipboard) + async fn type_prompt_with_line_breaks(&self, window: &terminator::UIElement, prompt: &str) -> Result<()> { + println!("Using deprecated line break typing method, prefer clipboard paste"); + + // Split the prompt by newlines and type each part + let lines: Vec<&str> = prompt.split('\n').collect(); + + for (i, line) in lines.iter().enumerate() { + // Type the current line + if !line.is_empty() { + window.type_text(line, false)?; + tokio::time::sleep(Duration::from_millis(100)).await; + } + + // Add line break with Shift+Enter if not the last line + if i < lines.len() - 1 { + println!("Adding line break with Shift+Enter"); + window.press_key("{shift}{enter}")?; + tokio::time::sleep(Duration::from_millis(200)).await; + } + } + + Ok(()) + } + + /// Clear clipboard content + async fn clear_clipboard(&self) -> Result<()> { + self.set_clipboard_content("").await + } + + /// Google Sheets specific functionality + /// =================================== + + /// Find Google Sheets browser window + pub async fn find_google_sheets_window(&self) -> Result { + println!("Searching for Google Sheets window..."); + + // Strategy 1: Look for window with "Google Sheets" in title + match self.desktop.find_window_by_criteria(Some("Google Sheets"), Some(Duration::from_millis(5000))).await { + Ok(window) => { + println!("Found Google Sheets window using title criteria"); + Ok(window) + } + Err(_) => { + // Strategy 2: Look for window with "sheets.google.com" or similar + match self.desktop.find_window_by_criteria(Some("sheets.google"), Some(Duration::from_millis(3000))).await { + Ok(window) => { + println!("Found Google Sheets window using URL criteria"); + Ok(window) + } + Err(_) => { + // Strategy 3: Look for any browser window and check if it contains Google Sheets elements + println!("Fallback: searching browser windows for Google Sheets elements"); + + let browsers = vec!["Chrome", "Edge", "Firefox", "msedge"]; + for browser in browsers { + match self.desktop.application(browser) { + Ok(browser_app) => { + // Check if this browser window has Google Sheets elements + let ask_gemini_selector = Selector::Name("Ask Gemini".to_string()); + match browser_app.locator(ask_gemini_selector)?.first(Some(Duration::from_millis(2000))).await { + Ok(_) => { + println!("Found Google Sheets via '{}' browser (detected 'Ask Gemini' button)", browser); + return Ok(browser_app); + } + Err(_) => continue, + } + } + Err(_) => continue, + } + } + + Err(anyhow!("Could not find Google Sheets window. Make sure Google Sheets is open in a browser.")) + } + } + } + } + } + + /// Open Google Sheets Gemini panel (only if not already open) + pub async fn open_google_sheets_gemini(&self) -> Result { + println!("Checking if Google Sheets Gemini panel is already open"); + + let sheets_window = self.find_google_sheets_window().await?; + sheets_window.focus()?; + tokio::time::sleep(Duration::from_millis(500)).await; + + // First check if the prompt input field is already visible + let prompt_input_selector = Selector::Name("Enter a prompt here".to_string()); + match sheets_window.locator(prompt_input_selector.clone())?.first(Some(Duration::from_millis(2000))).await { + Ok(_) => { + println!("Gemini panel is already open - prompt field is visible"); + return Ok("Google Sheets Gemini panel is already open".to_string()); + } + Err(_) => { + println!("Prompt field not visible, need to open Gemini panel"); + } + } + + // Look for "Ask Gemini" button only if prompt field is not visible + let ask_gemini_selector = Selector::Name("Ask Gemini".to_string()); + match sheets_window.locator(ask_gemini_selector)?.first(Some(Duration::from_millis(5000))).await { + Ok(ask_gemini_button) => { + println!("Found 'Ask Gemini' button, clicking it"); + ask_gemini_button.click()?; + tokio::time::sleep(Duration::from_millis(2000)).await; + + // Verify that the prompt field is now visible + match sheets_window.locator(prompt_input_selector)?.first(Some(Duration::from_millis(3000))).await { + Ok(_) => { + println!("Successfully opened Gemini panel - prompt field is now visible"); + Ok("Google Sheets Gemini panel opened successfully".to_string()) + } + Err(_) => { + Err(anyhow!("Clicked 'Ask Gemini' but prompt field did not appear. The panel may not have opened correctly.")) + } + } + } + Err(_) => { + Err(anyhow!("Could not find 'Ask Gemini' button in Google Sheets. Make sure you're in a Google Sheets document with Gemini enabled.")) + } + } + } + + /// Send a prompt to Google Sheets Gemini + pub async fn send_prompt_to_google_sheets_gemini(&self, prompt: &str) -> Result { + println!("=== SENDING PROMPT TO GOOGLE SHEETS GEMINI ==="); + println!("Prompt: {}", prompt); + + let sheets_window = self.find_google_sheets_window().await?; + sheets_window.focus()?; + tokio::time::sleep(Duration::from_millis(500)).await; + + // Ensure Gemini panel is open (this function now checks if it's already open) + println!("Ensuring Gemini panel is accessible..."); + self.open_google_sheets_gemini().await?; + + // Look for the prompt input textbox (should be visible now) + println!("Looking for prompt input field..."); + let prompt_input_selector = Selector::Name("Enter a prompt here".to_string()); + match sheets_window.locator(prompt_input_selector)?.first(Some(Duration::from_millis(3000))).await { + Ok(prompt_input) => { + println!("✓ Found prompt input field, proceeding with prompt submission"); + + // Click on the input field + prompt_input.click()?; + tokio::time::sleep(Duration::from_millis(500)).await; + + // Clear any existing content + sheets_window.press_key("{ctrl}a")?; + tokio::time::sleep(Duration::from_millis(100)).await; + + // Use clipboard to paste the entire prompt at once + println!("Pasting prompt via clipboard..."); + self.paste_prompt_via_clipboard(&sheets_window, prompt).await?; + + // Send the prompt (Enter key) - only at the very end + println!("Submitting prompt to Gemini..."); + sheets_window.press_key("{enter}")?; + + // Wait for Gemini to respond + println!("Waiting for Gemini response..."); + tokio::time::sleep(Duration::from_millis(5000)).await; + + // Press Enter again to apply the response automatically + println!("Applying Gemini response..."); + sheets_window.press_key("{enter}")?; + tokio::time::sleep(Duration::from_millis(2000)).await; + + println!("✓ Successfully completed prompt submission and response application"); + Ok(format!("Prompt sent to Google Sheets Gemini successfully and response applied")) + } + Err(_) => { + println!("✗ Could not find prompt input field after panel check"); + Err(anyhow!("Could not find 'Enter a prompt here' textbox even after trying to open Gemini panel. Make sure the Gemini panel is accessible in Google Sheets.")) + } + } + } + + /// Paste prompt via clipboard to avoid line break issues + async fn paste_prompt_via_clipboard(&self, window: &terminator::UIElement, prompt: &str) -> Result<()> { + println!("Pasting prompt via clipboard to avoid line break issues"); + + // Set clipboard content + match self.set_clipboard_content(prompt).await { + Ok(_) => { + println!("Prompt copied to clipboard successfully"); + + // Paste using Ctrl+V + window.press_key("{ctrl}v")?; + tokio::time::sleep(Duration::from_millis(300)).await; + + println!("Prompt pasted successfully via clipboard"); + Ok(()) + } + Err(e) => { + println!("Failed to copy to clipboard: {}, falling back to direct typing", e); + + // Fallback: type directly without line break handling + window.type_text(prompt, false)?; + tokio::time::sleep(Duration::from_millis(500)).await; + + Ok(()) + } + } + } + + /// Check if Google Sheets window is available (without opening new one) + pub async fn check_google_sheets_availability(&self) -> Result { + println!("=== CHECKING GOOGLE SHEETS AVAILABILITY ==="); + + // Simple approach: just try to find the window without complex async spawning + match self.find_google_sheets_window_safe().await { + Ok(()) => { + println!("Google Sheets window found, checking Gemini availability..."); + + // Create a new automation instance to check Gemini + match ExcelAutomation::new().await { + Ok(temp_automation) => { + match temp_automation.check_gemini_availability_simple().await { + Ok(msg) => Ok(msg), + Err(e) => Ok(format!("Google Sheets found but Gemini check failed: {}", e)) + } + } + Err(e) => Ok(format!("Google Sheets found but automation setup failed: {}", e)) + } + } + Err(_) => { + println!("No Google Sheets window found during search"); + Ok("No Google Sheets window found. Please open Google Sheets manually.".to_string()) + } + } + } + + /// Safe window finding that won't panic + async fn find_google_sheets_window_safe(&self) -> Result<()> { + println!("Safely searching for Google Sheets window..."); + + // Strategy 1: Look for window with "Google Sheets" in title (short timeout) + match self.desktop.find_window_by_criteria(Some("Google Sheets"), Some(Duration::from_millis(2000))).await { + Ok(_) => { + println!("Found Google Sheets window using title criteria"); + return Ok(()); + } + Err(_) => { + println!("No Google Sheets window with title found"); + } + } + + // Strategy 2: Look for window with "sheets.google.com" (short timeout) + match self.desktop.find_window_by_criteria(Some("sheets.google"), Some(Duration::from_millis(1500))).await { + Ok(_) => { + println!("Found Google Sheets window using URL criteria"); + return Ok(()); + } + Err(_) => { + println!("No Google Sheets window with URL found"); + } + } + + Err(anyhow!("Could not find Google Sheets window")) + } + + /// Simple Gemini availability check + async fn check_gemini_availability_simple(&self) -> Result { + println!("Checking if Gemini is available in Google Sheets"); + + match self.find_google_sheets_window().await { + Ok(sheets_window) => { + sheets_window.focus()?; + tokio::time::sleep(Duration::from_millis(300)).await; + + // Look for "Ask Gemini" button with short timeout + let ask_gemini_selector = Selector::Name("Ask Gemini".to_string()); + match sheets_window.locator(ask_gemini_selector)?.first(Some(Duration::from_millis(1500))).await { + Ok(_) => { + println!("Found 'Ask Gemini' button - Gemini is available"); + Ok("Google Sheets with Gemini is available and ready".to_string()) + } + Err(_) => { + Ok("Google Sheets found but Gemini not available. Make sure you're in a Google Sheets document with Gemini enabled.".to_string()) + } + } + } + Err(e) => Err(e) + } + } + + + /// Send data to Google Sheets Gemini (with TSV formatting for structured data) + pub async fn send_data_to_google_sheets_gemini(&self, data: &str, description: &str) -> Result { + let prompt = if data.contains('\t') || data.contains('\n') { + // Data appears to be structured - keep real tabs and provide clear instructions + format!("{}\n\nPlease create a table with this tab-separated data (columns separated by tabs, rows separated by new lines):\n\n{}", description, data) + } else { + // Simple text data + format!("{}\n\nData: {}", description, data) + }; + + self.send_prompt_to_google_sheets_gemini(&prompt).await + } + + /// Open Google Sheets application (only when explicitly requested) + pub async fn open_google_sheets_app(&self) -> Result { + println!("Opening Google Sheets application"); + + // Use PowerShell to open Google Sheets as web app in Edge + let command = r#"Start-Process -FilePath msedge -ArgumentList '--app=https://docs.google.com/spreadsheets/'"#; + + match self.desktop.run_command(Some(command), None).await { + Ok(output) => { + println!("Google Sheets opened successfully"); + tokio::time::sleep(Duration::from_millis(3000)).await; // Wait for app to load + Ok(format!("Google Sheets opened successfully. Output: {}", output.stdout)) + } + Err(e) => { + Err(anyhow!("Failed to open Google Sheets: {}", e)) + } + } + } + + /// Interact with Google Sheets Gemini (main entry point) - Check availability first + pub async fn interact_with_google_sheets_gemini(&self, task_description: &str) -> Result { + println!("Interacting with Google Sheets Gemini for task: {}", task_description); + + // First check if Google Sheets is available + match self.find_google_sheets_window().await { + Ok(sheets_window) => { + sheets_window.focus()?; + tokio::time::sleep(Duration::from_millis(500)).await; + + // Send the request to Google Sheets Gemini + self.send_prompt_to_google_sheets_gemini(task_description).await + } + Err(_) => { + Err(anyhow!("No Google Sheets window found. Please open Google Sheets manually first, then try again.")) + } + } + } +} + +/// Get the global Excel automation instance +pub async fn get_excel_automation() -> Result { + ExcelAutomation::new().await +} \ No newline at end of file diff --git a/examples/excel-copilot/src-tauri/src/gemini.rs b/examples/excel-copilot/src-tauri/src/gemini.rs new file mode 100644 index 00000000..b61db200 --- /dev/null +++ b/examples/excel-copilot/src-tauri/src/gemini.rs @@ -0,0 +1,915 @@ +use reqwest::Client; +use serde::{Deserialize, Serialize}; +use serde_json::{json, Value}; +use base64::{Engine as _, engine::general_purpose}; +use tokio_util::sync::CancellationToken; +use crate::locale_utils::{get_decimal_separator, get_locale_info}; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct GeminiMessage { + pub role: String, + pub parts: Vec, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(untagged)] +pub enum GeminiPart { + Text { + #[serde(skip_serializing_if = "Option::is_none")] + text: Option + }, + InlineData { + #[serde(skip_serializing_if = "Option::is_none", rename = "inlineData")] + inline_data: Option + }, + FunctionCall { + #[serde(rename = "functionCall")] + function_call: FunctionCall + }, + FunctionResponse { + #[serde(rename = "functionResponse")] + function_response: FunctionResponse + }, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct InlineData { + #[serde(rename = "mimeType")] + pub mime_type: String, + pub data: String, // base64 encoded +} + +impl GeminiPart { + pub fn text(text: String) -> Self { + Self::Text { + text: Some(text), + } + } + + pub fn pdf(base64_data: String) -> Self { + Self::InlineData { + inline_data: Some(InlineData { + mime_type: "application/pdf".to_string(), + data: base64_data, + }), + } + } + + pub fn function_call(name: String, args: Value) -> Self { + Self::FunctionCall { + function_call: FunctionCall { + name, + args, + }, + } + } + + pub fn function_response(name: String, response: Value) -> Self { + Self::FunctionResponse { + function_response: FunctionResponse { + name, + response, + }, + } + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct GeminiResponse { + pub candidates: Vec, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct GeminiCandidate { + pub content: GeminiContent, + #[serde(rename = "finishReason")] + pub finish_reason: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct GeminiContent { + pub parts: Vec, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(untagged)] +pub enum GeminiResponsePart { + Text { text: String }, + FunctionCall { + #[serde(rename = "functionCall")] + function_call: FunctionCall + }, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct FunctionCall { + pub name: String, + pub args: Value, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct FunctionResponse { + pub name: String, + pub response: Value, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct FunctionDeclaration { + pub name: String, + pub description: String, + pub parameters: Value, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct ToolConfig { + #[serde(rename = "functionCallingConfig")] + pub function_calling_config: FunctionCallingConfig, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct FunctionCallingConfig { + pub mode: String, // "ANY" or "AUTO" +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct SystemInstruction { + pub parts: Vec, +} + +#[derive(Clone)] +pub struct GeminiClient { + client: Client, + api_key: String, + base_url: String, + conversation_history: Vec, + tools: Vec, + system_instruction: SystemInstruction, + copilot_enabled: bool, +} + +/// Information about a tool call made by Gemini +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ToolCallInfo { + pub function_name: String, + pub arguments: Value, + pub result: String, +} + +/// Detailed response information from Gemini +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GeminiResponseDetails { + pub content: String, + pub tool_calls: Vec, + pub iterations: i32, + pub has_tool_calls: bool, +} + +impl GeminiClient { + /// Create a new Gemini client with the provided API key + pub fn new(api_key: String, copilot_enabled: bool) -> Result> { + let client = Client::new(); + let base_url = "https://generativelanguage.googleapis.com/v1beta/models".to_string(); + + let mut gemini_client = Self { + client, + api_key, + base_url, + conversation_history: Vec::new(), + tools: Vec::new(), + system_instruction: SystemInstruction { parts: Vec::new() }, + copilot_enabled, + }; + + // Setup Excel tools (conditionally include Copilot tools) + gemini_client.setup_excel_tools(); + + // Setup system instruction + gemini_client.setup_system_instruction(); + + Ok(gemini_client) + } + + /// Configure Excel automation tools for Gemini + fn setup_excel_tools(&mut self) { + let mut tools = vec![ + FunctionDeclaration { + name: "read_excel_cell".to_string(), + description: "Read the value from a specific cell in Excel".to_string(), + parameters: json!({ + "type": "object", + "properties": { + "cell_address": { + "type": "string", + "description": "The cell address in Excel notation (e.g., A1, B2, C10)" + } + }, + "required": ["cell_address"] + }), + }, + FunctionDeclaration { + name: "write_excel_cell".to_string(), + description: "Write a value to a specific cell in Excel".to_string(), + parameters: json!({ + "type": "object", + "properties": { + "cell_address": { + "type": "string", + "description": "The cell address in Excel notation (e.g., A1, B2, C10)" + }, + "value": { + "type": "string", + "description": "The value to write to the cell" + } + }, + "required": ["cell_address", "value"] + }), + }, + FunctionDeclaration { + name: "read_excel_range".to_string(), + description: "Read values from a range of cells in Excel".to_string(), + parameters: json!({ + "type": "object", + "properties": { + "start_cell": { + "type": "string", + "description": "The starting cell address (e.g., A1)" + }, + "end_cell": { + "type": "string", + "description": "The ending cell address (e.g., C5)" + } + }, + "required": ["start_cell", "end_cell"] + }), + }, + FunctionDeclaration { + name: "get_excel_sheet_overview".to_string(), + description: "Get a complete overview of the current Excel sheet showing all non-empty cells and their values".to_string(), + parameters: json!({ + "type": "object", + "properties": {}, + "required": [] + }), + }, + FunctionDeclaration { + name: "get_excel_ui_context".to_string(), + description: "Get the complete UI tree context of the Excel window, including available UI elements, active cell, and element IDs for precise targeting".to_string(), + parameters: json!({ + "type": "object", + "properties": {}, + "required": [] + }), + }, + FunctionDeclaration { + name: "paste_tsv_batch_data".to_string(), + description: "Paste batch data in TSV (Tab-Separated Values) format into Excel starting from a specific cell. Use this ONLY for large batch operations like importing data from PDFs or bulk data entry. Always verify safety to avoid overwriting existing data.".to_string(), + parameters: json!({ + "type": "object", + "properties": { + "start_cell": { + "type": "string", + "description": "The starting cell address where to paste the data (e.g., A1, B3)" + }, + "tsv_data": { + "type": "string", + "description": "The data in TSV format - rows separated by newlines, columns separated by tabs. Example: 'Name\\tAge\\tCity\\nJohn\\t25\\tParis\\nMarie\\t30\\tLyon'" + }, + "verify_safe": { + "type": "boolean", + "description": "Whether to verify that the target range is empty before pasting (default: true). Set to false only if you're sure you want to overwrite data." + } + }, + "required": ["start_cell", "tsv_data"] + }), + }, + FunctionDeclaration { + name: "set_excel_formula".to_string(), + description: "Set a formula in an Excel cell".to_string(), + parameters: json!({ + "type": "object", + "properties": { + "cell_address": { + "type": "string", + "description": "The cell address where to set the formula" + }, + "formula": { + "type": "string", + "description": "The formula to set (e.g., =SUM(A1:A10), =AVERAGE(B1:B5))" + } + }, + "required": ["cell_address", "formula"] + }), + }, + ]; + + // Add Copilot tools only if enabled + if self.copilot_enabled { + tools.extend(vec![ + FunctionDeclaration { + name: "send_request_to_excel_copilot".to_string(), + description: "Send a request to Microsoft Excel Copilot for advanced tasks like formatting, charts, conditional formatting, data analysis, etc. Note: This will work on currently selected cells or entire sheet.".to_string(), + parameters: json!({ + "type": "object", + "properties": { + "request": { + "type": "string", + "description": "The request to send to Excel Copilot (e.g., 'Create a bar chart from this data', 'Format these cells with bold and red background', 'Apply conditional formatting to highlight values greater than 100')" + } + }, + "required": ["request"] + }), + }, + FunctionDeclaration { + name: "format_cells_with_copilot".to_string(), + description: "Format Excel cells using Copilot with specific formatting instructions. This will first select the specified range, then use Copilot to format it.".to_string(), + parameters: json!({ + "type": "object", + "properties": { + "range": { + "type": "string", + "description": "The cell range to format (e.g., A1:C10, B1:D8)" + }, + "format_description": { + "type": "string", + "description": "Description of the formatting to apply (e.g., 'bold text with blue background', 'currency format', 'percentage with 2 decimals')" + } + }, + "required": ["range", "format_description"] + }), + }, + FunctionDeclaration { + name: "create_chart_with_copilot".to_string(), + description: "Create charts in Excel using Copilot. This will first select the specified data range, then ask Copilot to create the chart.".to_string(), + parameters: json!({ + "type": "object", + "properties": { + "data_range": { + "type": "string", + "description": "The data range for the chart (e.g., A1:B10, B1:D8)" + }, + "chart_type": { + "type": "string", + "description": "Type of chart to create (e.g., 'bar chart', 'line chart', 'pie chart', 'scatter plot')" + } + }, + "required": ["data_range", "chart_type"] + }), + }, + FunctionDeclaration { + name: "apply_conditional_formatting_with_copilot".to_string(), + description: "Apply conditional formatting to Excel cells using Copilot. This will first select the specified range, then apply the conditional formatting.".to_string(), + parameters: json!({ + "type": "object", + "properties": { + "range": { + "type": "string", + "description": "The cell range to apply conditional formatting (e.g., A1:D20, B1:E15)" + }, + "condition": { + "type": "string", + "description": "The condition for formatting (e.g., 'values greater than 100', 'duplicate values', 'top 10 values')" + } + }, + "required": ["range", "condition"] + }), + }, + ]); + } + + // Add Google Sheets tools (always available) + tools.extend(vec![ + FunctionDeclaration { + name: "check_google_sheets_availability".to_string(), + description: "Check if Google Sheets is open and available with Gemini access. Use this FIRST before any Google Sheets operations to verify availability.".to_string(), + parameters: json!({ + "type": "object", + "properties": {}, + "required": [] + }), + }, + FunctionDeclaration { + name: "open_google_sheets_app".to_string(), + description: "Open Google Sheets application in browser as a web app. Use this only when user explicitly requests to open a new Google Sheets window.".to_string(), + parameters: json!({ + "type": "object", + "properties": {}, + "required": [] + }), + }, + FunctionDeclaration { + name: "send_request_to_google_sheets_gemini".to_string(), + description: "Send a request to Google Sheets' built-in Gemini for any spreadsheet task. This is the ONLY way to interact with Google Sheets - all operations must go through Gemini. Use for data entry, analysis, formatting, charts, formulas, etc.".to_string(), + parameters: json!({ + "type": "object", + "properties": { + "request": { + "type": "string", + "description": "The request to send to Google Sheets Gemini (e.g., 'Create a table with columns Name, Age, City', 'Analyze this data and create a chart', 'Format the header row with bold text')" + } + }, + "required": ["request"] + }), + }, + FunctionDeclaration { + name: "send_data_to_google_sheets_gemini".to_string(), + description: "Send structured data to Google Sheets Gemini with instructions. Use this to insert tabular data by providing real TSV format (actual tab characters). Gemini will format and insert it appropriately.".to_string(), + parameters: json!({ + "type": "object", + "properties": { + "data": { + "type": "string", + "description": "The data to send - TSV format with real tab characters (\\t), raw text, or structured data" + }, + "description": { + "type": "string", + "description": "Instructions for how to handle the data (e.g., 'Create a table with this data and add headers', 'Insert this financial data into the spreadsheet')" + } + }, + "required": ["data", "description"] + }), + }, + ]); + + self.tools = tools; + } + + /// Setup the system instruction for the Excel Copilot (Probably need to be improved) + fn setup_system_instruction(&mut self) { + // Get locale information + let _locale_info = get_locale_info(); + let decimal_sep = get_decimal_separator(); + + // Create locale-specific formatting rules + let number_format_rules = if decimal_sep == ',' { + "LOCALE: Comma decimal. Normalize numbers: remove +, thousands separators, convert comma→dot for Excel (e.g., '1.234,56' → '1234.56')" + } else { + "LOCALE: Dot decimal. Normalize numbers: remove +, comma thousands separators (e.g., '1,234.56' → '1234.56')" + }; + + let base_tools = r#"TOOLS: read_excel_cell, write_excel_cell, read_excel_range, get_excel_sheet_overview, get_excel_ui_context, paste_tsv_batch_data, set_excel_formula"#; + + let copilot_tools = if self.copilot_enabled { + ", send_request_to_excel_copilot, format_cells_with_copilot, create_chart_with_copilot, apply_conditional_formatting_with_copilot" + } else { + "" + }; + + let copilot_rules = if self.copilot_enabled { + r#" + +COPILOT CONSTRAINTS: +- OneDrive files only, >= 3 rows, >= 2 cols, headers required, auto-save enabled +- Check file path contains "OneDrive" before Copilot operations +- Be specific in requests: "Format with bold headers, currency column C, alternating rows" not "Format data" + +COPILOT USAGE: +- Complex formatting → format_cells_with_copilot +- Charts → create_chart_with_copilot +- Conditional formatting → apply_conditional_formatting_with_copilot +- Analysis → send_request_to_excel_copilot +- Be precise with copilot, tell him exactly what he should do"# + } else { + "\nCOPILOT: Disabled" + }; + + let google_sheets_rules = r#" + +GOOGLE SHEETS COMMUNICATION STRATEGY: +- ALWAYS start with check_google_sheets_availability before any Google Sheets operations +- Use open_google_sheets_app ONLY when user explicitly asks to open NEW Google Sheets +- System now intelligently checks if Gemini panel is already open before clicking "Ask Gemini" + +GOOGLE SHEETS PROMPTING BEST PRACTICES: +1. Be DIRECT and ACTION-ORIENTED - tell Gemini exactly what to DO, not what you want to achieve +2. Use SPECIFIC language that Google Sheets understands: + ✓ Good: "Create a table with these columns: Date, Vendor, Amount. Add this data: [data]. Format the Amount column as currency." + ✗ Bad: "Create a table with this data including headers, and format the Amount column as currency" + +3. BREAK DOWN complex requests into clear steps: + ✓ Good: "Add these 5 rows of data to the sheet: [data]. Then format column C as currency. Then make row 1 bold." + ✗ Bad: "Process this financial data with proper formatting" + +4. Use GOOGLE SHEETS VOCABULARY: + - "Add to the sheet" / "Insert in the spreadsheet" + - "Format as currency/percentage/date" + - "Make bold/italic" + - "Create chart from range A1:C10" + - "Apply conditional formatting" + - "Add formula =SUM(A1:A10)" + +5. ALWAYS END with action words: + - "Add it to the sheet" + - "Apply this formatting" + - "Create the chart now" + - "Insert this data" + +SUCCESSFUL PROMPTING EXAMPLES: +- Data entry: "Add this data to the spreadsheet starting in cell A1: [TSV data]. Make the first row bold as headers." +- Formatting: "Format column B as currency. Make all text in row 1 bold and centered." +- Charts: "Create a bar chart using data from A1:B10. Add it below the data." +- Calculations: "Add a formula in cell D2 that calculates =B2*C2. Copy it down to row 10." + +AVOID THESE PATTERNS (they trigger "not supported"): +- Complex conditional logic requests +- Multi-step operations in one sentence +- Vague terms like "process", "handle", "manage" +- References to external data sources +- Advanced Excel-specific functions + +ALL OPERATIONS go through send_request_to_google_sheets_gemini or send_data_to_google_sheets_gemini +For TSV data: provide REAL tab characters, clear instructions +Panel status is checked automatically to avoid unnecessary "Ask Gemini" clicks"#; + + let system_prompt = format!(r#"Excel & Google Sheets automation assistant. {}{}{number_format_rules} + +RULES: +1. Always use tools, never fake results +2. Start with get_excel_sheet_overview for Excel OR check_google_sheets_availability for Google Sheets +3. TSV paste for batch data, verify_safe=true unless confirmed +4. UI issues → get_excel_ui_context first +5. Fix formula errors immediately + +PROTOCOL: +- Read data → read tools (Excel only) +- Write single cells → write_excel_cell (Excel only) +- Need context? → READ THE FILE, before asking user for information +- Bulk data → paste_tsv_batch_data (Excel) OR send_data_to_google_sheets_gemini (Google Sheets) +- Formulas → set_excel_formula (Excel) OR send_request_to_google_sheets_gemini (Google Sheets) +- Google Sheets: Use CLEAR, SIMPLE prompts with action verbs{copilot_rules}{google_sheets_rules}"#, base_tools, copilot_tools, number_format_rules = number_format_rules); + + self.system_instruction = SystemInstruction { + parts: vec![GeminiPart::text(system_prompt)], + }; + } + + /// Send a message to Gemini with detailed tool calling support + pub async fn send_message_with_tools_detailed(&mut self, message: &str, tool_executor: F) -> Result> + where + F: Fn(&str, &Value) -> std::pin::Pin>> + Send>> + Send + Sync, + { + self.send_message_with_tools_detailed_cancellable(message, tool_executor, None).await + } + + /// Send a message to Gemini with detailed tool calling support and cancellation + pub async fn send_message_with_tools_detailed_cancellable(&mut self, message: &str, tool_executor: F, cancellation_token: Option) -> Result> + where + F: Fn(&str, &Value) -> std::pin::Pin>> + Send>> + Send + Sync, + { + // Add user message to conversation history + let user_message = GeminiMessage { + role: "user".to_string(), + parts: vec![GeminiPart::text(message.to_string())], + }; + self.conversation_history.push(user_message); + + // Track all tool calls made during this conversation turn + let mut all_tool_calls = Vec::new(); + let mut total_iterations = 0; + + // Start the conversation turn + let final_response = self.process_conversation_turn_cancellable(&tool_executor, &mut all_tool_calls, &mut total_iterations, cancellation_token).await?; + + // Add the final response to conversation history + let final_message = GeminiMessage { + role: "model".to_string(), + parts: vec![GeminiPart::text(final_response.clone())], + }; + self.conversation_history.push(final_message); + + // Calculate has_tool_calls before moving all_tool_calls + let has_tool_calls = !all_tool_calls.is_empty(); + + Ok(GeminiResponseDetails { + content: final_response, + tool_calls: all_tool_calls, + iterations: total_iterations, + has_tool_calls, + }) + } + + /// Process a complete conversation turn, handling function calls inline + async fn process_conversation_turn(&mut self, tool_executor: &F, all_tool_calls: &mut Vec, total_iterations: &mut i32) -> Result> + where + F: Fn(&str, &Value) -> std::pin::Pin>> + Send>> + Send + Sync, + { + self.process_conversation_turn_cancellable(tool_executor, all_tool_calls, total_iterations, None).await + } + + /// Process a complete conversation turn with cancellation support + async fn process_conversation_turn_cancellable(&mut self, tool_executor: &F, all_tool_calls: &mut Vec, total_iterations: &mut i32, cancellation_token: Option) -> Result> + where + F: Fn(&str, &Value) -> std::pin::Pin>> + Send>> + Send + Sync, + { + let max_total_iterations = 50; + let mut accumulated_text = String::new(); + + // Working conversation that includes intermediate function calls + let mut working_conversation = self.conversation_history.clone(); + + loop { + // Check for cancellation before starting iteration + if let Some(ref token) = cancellation_token { + if token.is_cancelled() { + return Err("Request was cancelled by user".into()); + } + } + + *total_iterations += 1; + if *total_iterations > max_total_iterations { + return Err(format!("Maximum iterations ({}) reached", max_total_iterations).into()); + } + + // Make API call to Gemini + let url = format!("{}/gemini-2.0-flash:generateContent?key={}", self.base_url, self.api_key); + + let request_body = json!({ + "system_instruction": self.system_instruction, + "contents": working_conversation, + "tools": [{ + "functionDeclarations": self.tools + }], + "toolConfig": { + "functionCallingConfig": { + "mode": "AUTO" + } + }, + "generationConfig": { + "responseMimeType": "text/plain", + "temperature": 0.1, + "maxOutputTokens": 16000 + } + }); + + let response = self.client + .post(&url) + .header("Content-Type", "application/json") + .json(&request_body) + .send() + .await?; + + if !response.status().is_success() { + let error_text = response.text().await?; + return Err(format!("Gemini API error: {}", error_text).into()); + } + + let gemini_response: GeminiResponse = response.json().await?; + let candidate = gemini_response.candidates.first() + .ok_or("No candidate in Gemini response")?; + + // DEBUG: Log what we received from Gemini + println!("=== GEMINI RESPONSE DEBUG (Iteration {}) ===", *total_iterations); + println!("Finish reason: {:?}", candidate.finish_reason); + println!("Number of parts: {}", candidate.content.parts.len()); + for (i, part) in candidate.content.parts.iter().enumerate() { + match part { + GeminiResponsePart::Text { text } => { + println!("Part {}: TEXT: {}", i, text.chars().take(100).collect::()); + } + GeminiResponsePart::FunctionCall { function_call } => { + println!("Part {}: FUNCTION_CALL: {}", i, function_call.name); + } + } + } + println!("=========================================="); + + // Process the response parts + let mut has_function_calls = false; + let mut text_content = String::new(); + let mut function_calls_in_response = Vec::new(); + + // Collect all text and function calls from this response + for part in &candidate.content.parts { + match part { + GeminiResponsePart::Text { text } => { + text_content.push_str(text); + } + GeminiResponsePart::FunctionCall { function_call } => { + has_function_calls = true; + function_calls_in_response.push(function_call.clone()); + } + } + } + + // Add text to accumulated response (this preserves all text from the model) + if !text_content.trim().is_empty() { + accumulated_text.push_str(&text_content); + } + + // Add the model's response to working conversation (represents what model said in this turn) + let model_parts: Vec = candidate.content.parts.iter().map(|part| { + match part { + GeminiResponsePart::Text { text } => GeminiPart::text(text.clone()), + GeminiResponsePart::FunctionCall { function_call } => { + // Include the actual function call, not as text + GeminiPart::function_call(function_call.name.clone(), function_call.args.clone()) + } + } + }).collect(); + + if !model_parts.is_empty() { + let model_message = GeminiMessage { + role: "model".to_string(), + parts: model_parts, + }; + working_conversation.push(model_message); + } + + // If there are function calls, execute them and add results for next iteration + if has_function_calls { + println!("=== EXECUTING {} FUNCTION CALLS ===", function_calls_in_response.len()); + + // Collect all function responses before adding them to the conversation + let mut function_response_parts = Vec::new(); + + for function_call in function_calls_in_response { + // Check for cancellation before executing function + if let Some(ref token) = cancellation_token { + if token.is_cancelled() { + return Err("Request was cancelled by user".into()); + } + } + + println!("Executing function: {}", function_call.name); + + // Execute the function + let function_result = tool_executor(&function_call.name, &function_call.args).await?; + + println!("Function result length: {}", function_result.len()); + + // Record the tool call + let tool_call_info = ToolCallInfo { + function_name: function_call.name.clone(), + arguments: function_call.args.clone(), + result: function_result.clone(), + }; + all_tool_calls.push(tool_call_info); + + // Collect the function response part + function_response_parts.push(GeminiPart::function_response( + function_call.name.clone(), + json!({ "content": function_result }) + )); + } + + // Add all function responses in a single message + let function_results_message = GeminiMessage { + role: "user".to_string(), + parts: function_response_parts, + }; + working_conversation.push(function_results_message); + + println!("=== CONTINUING TO NEXT ITERATION ==="); + // Continue to let the model see the function results and potentially continue its response + continue; + } else { + println!("=== NO FUNCTION CALLS - BREAKING ==="); + // No function calls means the model has finished its response + break; + } + } + + // Return the accumulated text from all iterations + let final_response = if accumulated_text.trim().is_empty() { + if !all_tool_calls.is_empty() { + format!("Task completed successfully. {} tools executed.", all_tool_calls.len()) + } else { + "Task completed.".to_string() + } + } else { + accumulated_text.trim().to_string() + }; + + Ok(final_response) + } + + /// Clear conversation history + pub fn clear_conversation(&mut self) { + // Clear all conversation history but keep system instruction + self.conversation_history.clear(); + } + + /// Reset client completely + pub fn reset_completely(&mut self) { + // Clear everything and re-setup system instruction + self.conversation_history.clear(); + self.setup_system_instruction(); + } + + /// Send a message with tool calling support (legacy compatibility) + pub async fn send_message_with_tools(&mut self, message: &str, tool_executor: F) -> Result> + where + F: Fn(&str, &Value) -> std::pin::Pin>> + Send>> + Send + Sync, + { + let details = self.send_message_with_tools_detailed(message, tool_executor).await?; + Ok(details.content) + } + + /// Send a simple message without tools + pub async fn send_message(&mut self, message: &str) -> Result> { + // Add user message to conversation history + let user_message = GeminiMessage { + role: "user".to_string(), + parts: vec![GeminiPart::text(message.to_string())], + }; + self.conversation_history.push(user_message); + + // Prepare the request without tools but with system instruction + let url = format!("{}/gemini-2.5-flash-preview-04-17:generateContent?key={}", self.base_url, self.api_key); + + let request_body = json!({ + "system_instruction": self.system_instruction, + "contents": self.conversation_history, + "generationConfig": { + "responseMimeType": "text/plain" + } + }); + + // Send request + let response = self.client + .post(&url) + .header("Content-Type", "application/json") + .json(&request_body) + .send() + .await?; + + if response.status().is_success() { + let gemini_response: GeminiResponse = response.json().await?; + + if let Some(candidate) = gemini_response.candidates.first() { + if let Some(part) = candidate.content.parts.first() { + if let GeminiResponsePart::Text { text } = part { + let response_text = text.clone(); + + // Add assistant response to conversation history + let assistant_message = GeminiMessage { + role: "model".to_string(), + parts: vec![GeminiPart::text(response_text.clone())], + }; + self.conversation_history.push(assistant_message); + + return Ok(response_text); + } + } + } + + Err("Empty response from Gemini".into()) + } else { + let error_text = response.text().await?; + Err(format!("Gemini API error: {}", error_text).into()) + } + } + + /// Send a message with PDF attachments + pub async fn send_message_with_pdf(&mut self, message: &str, pdf_files: Vec, tool_executor: F) -> Result> + where + F: Fn(&str, &Value) -> std::pin::Pin>> + Send>> + Send + Sync, + { + // Create parts for the message + let mut parts = vec![GeminiPart::text(message.to_string())]; + + // Add PDF attachments + for pdf_path in pdf_files { + match std::fs::read(&pdf_path) { + Ok(pdf_data) => { + let base64_data = general_purpose::STANDARD.encode(pdf_data); + parts.push(GeminiPart::pdf(base64_data)); + println!("Added PDF attachment: {}", pdf_path); + } + Err(e) => { + println!("Warning: Failed to read PDF {}: {}", pdf_path, e); + } + } + } + + // Add user message with attachments to conversation history + let user_message = GeminiMessage { + role: "user".to_string(), + parts, + }; + self.conversation_history.push(user_message); + + // Track all tool calls made during this conversation turn + let mut all_tool_calls = Vec::new(); + let mut total_iterations = 0; + + // Start the conversation turn + let final_response = self.process_conversation_turn(&tool_executor, &mut all_tool_calls, &mut total_iterations).await?; + + // Add the final response to conversation history + let final_message = GeminiMessage { + role: "model".to_string(), + parts: vec![GeminiPart::text(final_response.clone())], + }; + self.conversation_history.push(final_message); + + // Calculate has_tool_calls before moving all_tool_calls + let has_tool_calls = !all_tool_calls.is_empty(); + + Ok(GeminiResponseDetails { + content: final_response, + tool_calls: all_tool_calls, + iterations: total_iterations, + has_tool_calls, + }) + } +} \ No newline at end of file diff --git a/examples/excel-copilot/src-tauri/src/lib.rs b/examples/excel-copilot/src-tauri/src/lib.rs new file mode 100644 index 00000000..15990c97 --- /dev/null +++ b/examples/excel-copilot/src-tauri/src/lib.rs @@ -0,0 +1,82 @@ +// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/ + +// This lib.rs is auto-generated by Tauri for library builds +// We redirect to the main module where our actual application logic is + +mod excel; +mod excel_interaction; +mod gemini; +mod openai; +mod commands; +mod locale_utils; + +use commands::{ + open_excel_file, create_new_excel, save_excel_file, get_excel_content, + setup_gemini_client, setup_openai_client, + chat_with_gemini, chat_with_gemini_pdf, chat_with_openai, chat_with_openai_pdf, + chat_with_llm, chat_with_llm_pdf, + set_llm_provider, get_llm_provider, + get_chat_history, clear_chat_history, + excel_read_cell, excel_write_cell, excel_read_range, excel_set_formula, + select_pdf_files, get_locale_info, + AppStateStruct, + open_google_sheets, + google_sheets_send_prompt, + google_sheets_send_data, + google_sheets_interact, + check_google_sheets_availability +}; + +/// Initialize and run the Tauri application +#[cfg_attr(mobile, tauri::mobile_entry_point)] +pub fn run() { + tauri::Builder::default() + .manage(AppStateStruct::default()) + .plugin(tauri_plugin_dialog::init()) + .invoke_handler(tauri::generate_handler![ + // File operations + open_excel_file, + create_new_excel, + save_excel_file, + get_excel_content, + select_pdf_files, + + // LLM configuration + setup_gemini_client, + setup_openai_client, + set_llm_provider, + get_llm_provider, + + // Universal chat commands + chat_with_llm, + chat_with_llm_pdf, + + // Specific LLM chat commands + chat_with_gemini, + chat_with_gemini_pdf, + chat_with_openai, + chat_with_openai_pdf, + + // Chat management + get_chat_history, + clear_chat_history, + + // Excel interaction via automation + excel_read_cell, + excel_write_cell, + excel_read_range, + excel_set_formula, + + // System info + get_locale_info, + + // Google Sheets commands + open_google_sheets, + google_sheets_send_prompt, + google_sheets_send_data, + google_sheets_interact, + check_google_sheets_availability + ]) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} diff --git a/examples/excel-copilot/src-tauri/src/locale_utils.rs b/examples/excel-copilot/src-tauri/src/locale_utils.rs new file mode 100644 index 00000000..5462bb00 --- /dev/null +++ b/examples/excel-copilot/src-tauri/src/locale_utils.rs @@ -0,0 +1,156 @@ +use std::env; + +/// Determine the decimal separator based on system locale +pub fn get_decimal_separator() -> char { + // Try to get locale from environment variables + let locale = get_system_locale(); + + // Countries that use comma (,) as decimal separator + let comma_countries = [ + "fr", "de", "es", "it", "be", "nl", "pl", "pt", + "se", "gr", "ru", "tr", "br", "ar", "id" + ]; + + // Extract language code (first 2 characters before any underscore or dash) + let lang_code = locale.split(['_', '-', '.']).next().unwrap_or("en").to_lowercase(); + + if comma_countries.contains(&lang_code.as_str()) { + ',' + } else { + '.' + } +} + +/// Get system locale from environment variables +fn get_system_locale() -> String { + // Try different locale environment variables in order of preference + env::var("LC_NUMERIC") + .or_else(|_| env::var("LC_ALL")) + .or_else(|_| env::var("LANG")) + .or_else(|_| env::var("LANGUAGE")) + .unwrap_or_else(|_| { + // On Windows, try to get locale through Windows API + #[cfg(windows)] + { + get_windows_locale().unwrap_or_else(|| "en_US".to_string()) + } + #[cfg(not(windows))] + { + "en_US".to_string() + } + }) +} + +/// Get Windows locale using Windows API +#[cfg(windows)] +fn get_windows_locale() -> Option { + use std::ffi::OsString; + use std::os::windows::ffi::OsStringExt; + + unsafe { + // Get locale name using GetUserDefaultLocaleName + let mut buffer = [0u16; 85]; // LOCALE_NAME_MAX_LENGTH is 85 + let len = windows::Win32::Globalization::GetUserDefaultLocaleName( + &mut buffer + ); + + if len > 0 { + let locale_name = OsString::from_wide(&buffer[..len as usize - 1]); + locale_name.to_str().map(|s| s.to_string()) + } else { + None + } + } +} + +/// Format a number according to the system locale +pub fn format_number_for_locale(value: f64) -> String { + let decimal_separator = get_decimal_separator(); + + if decimal_separator == ',' { + // For comma locales, format without thousands separators + format!("{}", value).replace('.', ",") + } else { + // For dot locales, just format normally + format!("{}", value) + } +} + +/// Normalize a number string for Excel input according to system locale +pub fn normalize_number_for_excel(input: &str) -> String { + let decimal_separator = get_decimal_separator(); + + // Remove thousands separators and normalize + let mut normalized = input + .replace('+', "") // Remove plus signs + .replace(' ', ""); // Remove spaces + + if decimal_separator == ',' { + // For comma locales: + // - Remove dots used as thousands separators (if they appear before the last comma) + // - Keep the comma as decimal separator for Excel input + if let Some(last_comma_pos) = normalized.rfind(',') { + // Check if there are dots before the last comma (thousands separators) + let before_comma = &normalized[..last_comma_pos]; + if before_comma.contains('.') { + // Remove dots used as thousands separators + normalized = before_comma.replace('.', "") + &normalized[last_comma_pos..]; + } + } + // For Excel, we need to convert comma to dot for proper recognition + normalized = normalized.replace(',', "."); + } else { + // For dot locales: + // - Remove commas used as thousands separators + // - Keep dots as decimal separators + + // Count dots to distinguish between thousands and decimal separators + let dot_count = normalized.matches('.').count(); + if dot_count > 1 { + // Multiple dots - likely thousands separators except the last one + if let Some(last_dot_pos) = normalized.rfind('.') { + let before_last_dot = &normalized[..last_dot_pos]; + normalized = before_last_dot.replace('.', "") + &normalized[last_dot_pos..]; + } + } + // Remove commas (thousands separators in English locales) + normalized = normalized.replace(',', ""); + } + + normalized +} + +/// Get current locale information for debugging +pub fn get_locale_info() -> String { + let locale = get_system_locale(); + let decimal_sep = get_decimal_separator(); + + format!( + "System locale: {}, Decimal separator: '{}', Example: 1234{}67 (formatted: {})", + locale, + decimal_sep, + decimal_sep, + format_number_for_locale(1234.67) + ) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_number_normalization() { + // Test various number formats + assert_eq!(normalize_number_for_excel("1,234.56"), "1234.56"); + assert_eq!(normalize_number_for_excel("+2,259.84"), "2259.84"); + assert_eq!(normalize_number_for_excel("-1,458.25"), "-1458.25"); + assert_eq!(normalize_number_for_excel("1234.56"), "1234.56"); + } + + #[test] + fn test_locale_detection() { + // Test that the function doesn't panic + let _ = get_decimal_separator(); + let _ = get_locale_info(); + } +} \ No newline at end of file diff --git a/examples/excel-copilot/src-tauri/src/main.rs b/examples/excel-copilot/src-tauri/src/main.rs new file mode 100644 index 00000000..060ec6d8 --- /dev/null +++ b/examples/excel-copilot/src-tauri/src/main.rs @@ -0,0 +1,86 @@ +// Prevents additional console window on Windows in release, DO NOT REMOVE!! +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +mod excel; +mod excel_interaction; +mod gemini; +mod openai; +mod commands; +mod locale_utils; + +use commands::{ + open_excel_file, create_new_excel, save_excel_file, get_excel_content, + setup_gemini_client, setup_openai_client, + chat_with_gemini, chat_with_gemini_pdf, chat_with_openai, chat_with_openai_pdf, + chat_with_llm, chat_with_llm_pdf, stop_llm_request, + set_llm_provider, get_llm_provider, + get_chat_history, clear_chat_history, + excel_read_cell, excel_write_cell, excel_read_range, excel_set_formula, + select_pdf_files, get_locale_info, + AppStateStruct, + open_google_sheets, + google_sheets_send_prompt, + google_sheets_send_data, + google_sheets_interact, + check_google_sheets_availability +}; + +/// Initialize and run the Tauri application +#[cfg_attr(mobile, tauri::mobile_entry_point)] +pub fn run() { + tauri::Builder::default() + .manage(AppStateStruct::default()) + .plugin(tauri_plugin_dialog::init()) + .invoke_handler(tauri::generate_handler![ + // File operations + open_excel_file, + create_new_excel, + save_excel_file, + get_excel_content, + select_pdf_files, + + // LLM configuration + setup_gemini_client, + setup_openai_client, + set_llm_provider, + get_llm_provider, + + // Universal chat commands + chat_with_llm, + chat_with_llm_pdf, + stop_llm_request, + + // Specific LLM chat commands + chat_with_gemini, + chat_with_gemini_pdf, + chat_with_openai, + chat_with_openai_pdf, + + // Chat management + get_chat_history, + clear_chat_history, + + // Excel interaction via automation + excel_read_cell, + excel_write_cell, + excel_read_range, + excel_set_formula, + + // System info + get_locale_info, + + // Google Sheets commands + open_google_sheets, + google_sheets_send_prompt, + google_sheets_send_data, + google_sheets_interact, + check_google_sheets_availability + ]) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} + +/// Application entry point +fn main() { + run(); +} diff --git a/examples/excel-copilot/src-tauri/src/openai.rs b/examples/excel-copilot/src-tauri/src/openai.rs new file mode 100644 index 00000000..688fbe6a --- /dev/null +++ b/examples/excel-copilot/src-tauri/src/openai.rs @@ -0,0 +1,919 @@ +use reqwest::Client; +use serde::{Deserialize, Serialize}; +use serde_json::{json, Value}; +use tokio_util::sync::CancellationToken; +use crate::locale_utils::{get_decimal_separator, get_locale_info}; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct OpenAIMessage { + pub role: String, + pub content: Vec, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(untagged)] +pub enum OpenAIContent { + Text { + #[serde(rename = "type")] + content_type: String, + text: String, + }, + ImageUrl { + #[serde(rename = "type")] + content_type: String, + image_url: ImageUrl, + }, + File { + #[serde(rename = "type")] + content_type: String, + file: FileReference, + }, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct ImageUrl { + pub url: String, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct FileReference { + pub file_id: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct OpenAIResponse { + pub choices: Vec, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct OpenAIChoice { + pub message: OpenAIResponseMessage, + pub finish_reason: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct OpenAIResponseMessage { + pub role: String, + pub content: Option, + pub tool_calls: Option>, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct OpenAIToolCall { + pub id: String, + #[serde(rename = "type")] + pub call_type: String, + pub function: OpenAIFunction, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct OpenAIFunction { + pub name: String, + pub arguments: String, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct OpenAITool { + #[serde(rename = "type")] + pub tool_type: String, + pub function: OpenAIFunctionDefinition, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct OpenAIFunctionDefinition { + pub name: String, + pub description: String, + pub parameters: Value, +} + +impl OpenAIContent { + pub fn text(text: String) -> Self { + Self::Text { + content_type: "text".to_string(), + text, + } + } + + pub fn image_url(url: String) -> Self { + Self::ImageUrl { + content_type: "image_url".to_string(), + image_url: ImageUrl { url }, + } + } + + pub fn file(file_id: String) -> Self { + Self::File { + content_type: "file".to_string(), + file: FileReference { file_id }, + } + } +} + +/// Information about a tool call made by OpenAI +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct OpenAIToolCallInfo { + pub function_name: String, + pub arguments: Value, + pub result: String, +} + +/// Detailed response information from OpenAI +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct OpenAIResponseDetails { + pub content: String, + pub tool_calls: Vec, + pub iterations: i32, + pub has_tool_calls: bool, +} + +#[derive(Clone)] +pub struct OpenAIClient { + client: Client, + api_key: String, + base_url: String, + conversation_history: Vec, + tools: Vec, + system_message: String, + copilot_enabled: bool, +} + +impl OpenAIClient { + /// Create a new OpenAI client with the provided API key + pub fn new(api_key: String, copilot_enabled: bool) -> Self { + let client = Client::new(); + let base_url = "https://api.openai.com/v1".to_string(); + + let mut openai_client = Self { + client, + api_key, + base_url, + conversation_history: Vec::new(), + tools: Vec::new(), + system_message: String::new(), + copilot_enabled, + }; + + // Setup Excel tools + openai_client.setup_excel_tools(); + + // Setup system instruction + openai_client.setup_system_instruction(); + + openai_client + } + + /// Configure Excel automation tools for OpenAI + fn setup_excel_tools(&mut self) { + let mut tools = vec![ + OpenAITool { + tool_type: "function".to_string(), + function: OpenAIFunctionDefinition { + name: "read_excel_cell".to_string(), + description: "Read the value from a specific cell in Excel".to_string(), + parameters: json!({ + "type": "object", + "properties": { + "cell_address": { + "type": "string", + "description": "The cell address in Excel notation (e.g., A1, B2, C10)" + } + }, + "required": ["cell_address"] + }), + }, + }, + OpenAITool { + tool_type: "function".to_string(), + function: OpenAIFunctionDefinition { + name: "write_excel_cell".to_string(), + description: "Write a value to a specific cell in Excel".to_string(), + parameters: json!({ + "type": "object", + "properties": { + "cell_address": { + "type": "string", + "description": "The cell address in Excel notation (e.g., A1, B2, C10)" + }, + "value": { + "type": "string", + "description": "The value to write to the cell" + } + }, + "required": ["cell_address", "value"] + }), + }, + }, + OpenAITool { + tool_type: "function".to_string(), + function: OpenAIFunctionDefinition { + name: "read_excel_range".to_string(), + description: "Read values from a range of cells in Excel".to_string(), + parameters: json!({ + "type": "object", + "properties": { + "start_cell": { + "type": "string", + "description": "The starting cell address (e.g., A1)" + }, + "end_cell": { + "type": "string", + "description": "The ending cell address (e.g., C5)" + } + }, + "required": ["start_cell", "end_cell"] + }), + }, + }, + OpenAITool { + tool_type: "function".to_string(), + function: OpenAIFunctionDefinition { + name: "get_excel_sheet_overview".to_string(), + description: "Get a complete overview of the current Excel sheet showing all non-empty cells and their values".to_string(), + parameters: json!({ + "type": "object", + "properties": {}, + "required": [] + }), + }, + }, + OpenAITool { + tool_type: "function".to_string(), + function: OpenAIFunctionDefinition { + name: "get_excel_ui_context".to_string(), + description: "Get the complete UI tree context of the Excel window, including available UI elements, active cell, and element IDs for precise targeting".to_string(), + parameters: json!({ + "type": "object", + "properties": {}, + "required": [] + }), + }, + }, + OpenAITool { + tool_type: "function".to_string(), + function: OpenAIFunctionDefinition { + name: "paste_tsv_batch_data".to_string(), + description: "Paste batch data in TSV (Tab-Separated Values) format into Excel starting from a specific cell. Use this ONLY for large batch operations like importing data from PDFs or bulk data entry. Always verify safety to avoid overwriting existing data.".to_string(), + parameters: json!({ + "type": "object", + "properties": { + "start_cell": { + "type": "string", + "description": "The starting cell address where to paste the data (e.g., A1, B3)" + }, + "tsv_data": { + "type": "string", + "description": "The data in TSV format - rows separated by newlines, columns separated by tabs. Example: 'Name\\tAge\\tCity\\nJohn\\t25\\tParis\\nMarie\\t30\\tLyon'" + }, + "verify_safe": { + "type": "boolean", + "description": "Whether to verify that the target range is empty before pasting (default: true). Set to false only if you're sure you want to overwrite data." + } + }, + "required": ["start_cell", "tsv_data"] + }), + }, + }, + OpenAITool { + tool_type: "function".to_string(), + function: OpenAIFunctionDefinition { + name: "set_excel_formula".to_string(), + description: "Set a formula in an Excel cell".to_string(), + parameters: json!({ + "type": "object", + "properties": { + "cell_address": { + "type": "string", + "description": "The cell address where to set the formula" + }, + "formula": { + "type": "string", + "description": "The formula to set (e.g., =SUM(A1:A10), =AVERAGE(B1:B5))" + } + }, + "required": ["cell_address", "formula"] + }), + }, + }, + ]; + + // Add Copilot tools only if enabled + if self.copilot_enabled { + tools.extend(vec![ + OpenAITool { + tool_type: "function".to_string(), + function: OpenAIFunctionDefinition { + name: "send_request_to_excel_copilot".to_string(), + description: "Send a request to Microsoft Excel Copilot for advanced tasks like formatting, charts, conditional formatting, data analysis, etc. Note: This will work on currently selected cells or entire sheet.".to_string(), + parameters: json!({ + "type": "object", + "properties": { + "request": { + "type": "string", + "description": "The request to send to Excel Copilot (e.g., 'Create a bar chart from this data', 'Format these cells with bold and red background', 'Apply conditional formatting to highlight values greater than 100')" + } + }, + "required": ["request"] + }), + }, + }, + OpenAITool { + tool_type: "function".to_string(), + function: OpenAIFunctionDefinition { + name: "format_cells_with_copilot".to_string(), + description: "Format Excel cells using Copilot with specific formatting instructions. This will first select the specified range, then use Copilot to format it.".to_string(), + parameters: json!({ + "type": "object", + "properties": { + "range": { + "type": "string", + "description": "The cell range to format (e.g., A1:C10, B1:D8)" + }, + "format_description": { + "type": "string", + "description": "Description of the formatting to apply (e.g., 'bold text with blue background', 'currency format', 'percentage with 2 decimals')" + } + }, + "required": ["range", "format_description"] + }), + }, + }, + OpenAITool { + tool_type: "function".to_string(), + function: OpenAIFunctionDefinition { + name: "create_chart_with_copilot".to_string(), + description: "Create charts in Excel using Copilot. This will first select the specified data range, then ask Copilot to create the chart.".to_string(), + parameters: json!({ + "type": "object", + "properties": { + "data_range": { + "type": "string", + "description": "The data range for the chart (e.g., A1:B10, B1:D8)" + }, + "chart_type": { + "type": "string", + "description": "Type of chart to create (e.g., 'bar chart', 'line chart', 'pie chart', 'scatter plot')" + } + }, + "required": ["data_range", "chart_type"] + }), + }, + }, + OpenAITool { + tool_type: "function".to_string(), + function: OpenAIFunctionDefinition { + name: "apply_conditional_formatting_with_copilot".to_string(), + description: "Apply conditional formatting to Excel cells using Copilot. This will first select the specified range, then apply the conditional formatting.".to_string(), + parameters: json!({ + "type": "object", + "properties": { + "range": { + "type": "string", + "description": "The cell range to apply conditional formatting (e.g., A1:D20, B1:E15)" + }, + "condition": { + "type": "string", + "description": "The condition for formatting (e.g., 'values greater than 100', 'duplicate values', 'top 10 values')" + } + }, + "required": ["range", "condition"] + }), + }, + }, + ]); + } + + // Add Google Sheets tools (always available) + tools.extend(vec![ + OpenAITool { + tool_type: "function".to_string(), + function: OpenAIFunctionDefinition { + name: "check_google_sheets_availability".to_string(), + description: "Check if Google Sheets is open and available with Gemini access. Use this FIRST before any Google Sheets operations to verify availability.".to_string(), + parameters: json!({ + "type": "object", + "properties": {}, + "required": [] + }), + }, + }, + OpenAITool { + tool_type: "function".to_string(), + function: OpenAIFunctionDefinition { + name: "open_google_sheets_app".to_string(), + description: "Open Google Sheets application in browser as a web app. Use this only when user explicitly requests to open a new Google Sheets window.".to_string(), + parameters: json!({ + "type": "object", + "properties": {}, + "required": [] + }), + }, + }, + OpenAITool { + tool_type: "function".to_string(), + function: OpenAIFunctionDefinition { + name: "send_request_to_google_sheets_gemini".to_string(), + description: "Send a request to Google Sheets' built-in Gemini for any spreadsheet task. This is the ONLY way to interact with Google Sheets - all operations must go through Gemini. Use for data entry, analysis, formatting, charts, formulas, etc.".to_string(), + parameters: json!({ + "type": "object", + "properties": { + "request": { + "type": "string", + "description": "The request to send to Google Sheets Gemini (e.g., 'Create a table with columns Name, Age, City', 'Analyze this data and create a chart', 'Format the header row with bold text')" + } + }, + "required": ["request"] + }), + }, + }, + OpenAITool { + tool_type: "function".to_string(), + function: OpenAIFunctionDefinition { + name: "send_data_to_google_sheets_gemini".to_string(), + description: "Send structured data to Google Sheets Gemini with instructions. Use this to insert tabular data by providing real TSV format (actual tab characters). Gemini will format and insert it appropriately.".to_string(), + parameters: json!({ + "type": "object", + "properties": { + "data": { + "type": "string", + "description": "The data to send - TSV format with real tab characters (\\t), raw text, or structured data" + }, + "description": { + "type": "string", + "description": "Instructions for how to handle the data (e.g., 'Create a table with this data and add headers', 'Insert this financial data into the spreadsheet')" + } + }, + "required": ["data", "description"] + }), + }, + }, + ]); + + self.tools = tools; + } + + /// Setup the system instruction for the Excel Copilot + fn setup_system_instruction(&mut self) { + // Get locale information + let _locale_info = get_locale_info(); + let decimal_sep = get_decimal_separator(); + + // Create locale-specific formatting rules + let number_format_rules = if decimal_sep == ',' { + "LOCALE: Comma decimal. Normalize numbers: remove +, thousands separators, convert comma→dot for Excel (e.g., '1.234,56' → '1234.56')" + } else { + "LOCALE: Dot decimal. Normalize numbers: remove +, comma thousands separators (e.g., '1,234.56' → '1234.56')" + }; + + let base_tools = r#"TOOLS: read_excel_cell, write_excel_cell, read_excel_range, get_excel_sheet_overview, get_excel_ui_context, paste_tsv_batch_data, set_excel_formula"#; + + let copilot_tools = if self.copilot_enabled { + ", send_request_to_excel_copilot, format_cells_with_copilot, create_chart_with_copilot, apply_conditional_formatting_with_copilot" + } else { + "" + }; + + let copilot_rules = if self.copilot_enabled { + r#" +You are Excel Copilot, a powerful assistant that can help with a wide range of Excel tasks. + +COPILOT CONSTRAINTS: +- OneDrive files only, >= 3 rows, >= 2 cols, headers required, auto-save enabled +- Check file path contains "OneDrive" before Copilot operations +- Be specific in requests: "Format with bold headers, currency column C, alternating rows" not "Format data" + +COPILOT USAGE: +- Complex formatting → format_cells_with_copilot +- Charts → create_chart_with_copilot +- Conditional formatting → apply_conditional_formatting_with_copilot +- Analysis → send_request_to_excel_copilot +- Be precise with copilot, tell him exactly what he should do"# + } else { + "\nMicrosoft Excel Copilot: Disabled" + }; + + let google_sheets_rules = r#" + +GOOGLE SHEETS COMMUNICATION STRATEGY: +- ALWAYS start with check_google_sheets_availability before any Google Sheets operations +- Use open_google_sheets_app ONLY when user explicitly asks to open NEW Google Sheets +- System now intelligently checks if Gemini panel is already open before clicking "Ask Gemini" + +GOOGLE SHEETS PROMPTING BEST PRACTICES: +1. Be DIRECT and ACTION-ORIENTED - tell Gemini exactly what to DO, not what you want to achieve +2. Use SPECIFIC language that Google Sheets understands: + Good: "Create a table with these columns: Date, Vendor, Amount. Add this data: [data]. Format the Amount column as currency." + Bad: "Create a table with this data including headers, and format the Amount column as currency" + +3. BREAK DOWN complex requests into clear steps: + Good: "Add these 5 rows of data to the sheet: [data]. Then format column C as currency. Then make row 1 bold." + Bad: "Process this financial data with proper formatting" + +4. Use GOOGLE SHEETS VOCABULARY: + - "Add to the sheet" / "Insert in the spreadsheet" + - "Format as currency/percentage/date" + - "Make bold/italic" + - "Create chart from range A1:C10" + - "Apply conditional formatting" + - "Add formula =SUM(A1:A10)" + +5. ALWAYS END with action words: + - "Add it to the sheet" + - "Apply this formatting" + - "Create the chart now" + - "Insert this data" + +SUCCESSFUL PROMPTING EXAMPLES: +- Data entry: "Add this data to the spreadsheet starting in cell A1: [TSV data]. Make the first row bold as headers." +- Formatting: "Format column B as currency. Make all text in row 1 bold and centered." +- Charts: "Create a bar chart using data from A1:B10. Add it below the data." +- Calculations: "Add a formula in cell D2 that calculates =B2*C2. Copy it down to row 10." + +AVOID THESE PATTERNS (they trigger "not supported"): +- Complex conditional logic requests +- Multi-step operations in one sentence +- Vague terms like "process", "handle", "manage" +- References to external data sources +- Advanced Excel-specific functions + +ALL OPERATIONS go through send_request_to_google_sheets_gemini or send_data_to_google_sheets_gemini +For TSV data: provide REAL tab characters, clear instructions +Panel status is checked automatically to avoid unnecessary "Ask Gemini" clicks"#; + + self.system_message = format!(r#"Excel & Google Sheets automation assistant powered by OpenAI GPT-4.1-mini. {}{}{number_format_rules} + +RULES: +1. Always use tools, never fake results +2. Start with get_excel_sheet_overview for Excel OR check_google_sheets_availability for Google Sheets +3. TSV paste for batch data, verify_safe=true unless confirmed +4. UI issues → get_excel_ui_context first +5. Fix formula errors immediately + +PROTOCOL: +- Read data → read tools (Excel only) +- Write single cells → write_excel_cell (Excel only) +- Need context? → READ THE FILE, before asking user for information +- Bulk data → paste_tsv_batch_data (Excel) OR send_data_to_google_sheets_gemini (Google Sheets) +- Formulas → set_excel_formula (Excel) OR send_request_to_google_sheets_gemini (Google Sheets) +- Google Sheets: Use CLEAR, SIMPLE prompts with action verbs{copilot_rules}{google_sheets_rules}"#, base_tools, copilot_tools, number_format_rules = number_format_rules); + } + + /// Send a message to OpenAI with detailed tool calling support + pub async fn send_message_with_tools_detailed(&mut self, message: &str, tool_executor: F) -> Result> + where + F: Fn(&str, &Value) -> std::pin::Pin>> + Send>> + Send + Sync, + { + self.send_message_with_tools_detailed_cancellable(message, tool_executor, None).await + } + + /// Send a message to OpenAI with detailed tool calling support and cancellation + pub async fn send_message_with_tools_detailed_cancellable(&mut self, message: &str, tool_executor: F, cancellation_token: Option) -> Result> + where + F: Fn(&str, &Value) -> std::pin::Pin>> + Send>> + Send + Sync, + { + // Add user message to conversation history + let user_message = OpenAIMessage { + role: "user".to_string(), + content: vec![OpenAIContent::text(message.to_string())], + }; + self.conversation_history.push(user_message); + + // Track all tool calls made during this conversation turn + let mut all_tool_calls = Vec::new(); + let mut total_iterations = 0; + + // Start the conversation turn + let final_response = self.process_conversation_turn_cancellable(&tool_executor, &mut all_tool_calls, &mut total_iterations, cancellation_token).await?; + + // Add the final response to conversation history + let final_message = OpenAIMessage { + role: "assistant".to_string(), + content: vec![OpenAIContent::text(final_response.clone())], + }; + self.conversation_history.push(final_message); + + // Calculate has_tool_calls before moving all_tool_calls + let has_tool_calls = !all_tool_calls.is_empty(); + + Ok(OpenAIResponseDetails { + content: final_response, + tool_calls: all_tool_calls, + iterations: total_iterations, + has_tool_calls, + }) + } + + /// Process a complete conversation turn, handling function calls inline + async fn process_conversation_turn(&mut self, tool_executor: &F, all_tool_calls: &mut Vec, total_iterations: &mut i32) -> Result> + where + F: Fn(&str, &Value) -> std::pin::Pin>> + Send>> + Send + Sync, + { + self.process_conversation_turn_cancellable(tool_executor, all_tool_calls, total_iterations, None).await + } + + /// Process a complete conversation turn with cancellation support + async fn process_conversation_turn_cancellable(&mut self, tool_executor: &F, all_tool_calls: &mut Vec, total_iterations: &mut i32, cancellation_token: Option) -> Result> + where + F: Fn(&str, &Value) -> std::pin::Pin>> + Send>> + Send + Sync, + { + let max_total_iterations = 50; + let mut accumulated_text = String::new(); + + // Working conversation that includes intermediate function calls + let mut working_conversation = self.conversation_history.clone(); + + loop { + // Check for cancellation before starting iteration + if let Some(ref token) = cancellation_token { + if token.is_cancelled() { + return Err("Request was cancelled by user".into()); + } + } + + *total_iterations += 1; + if *total_iterations > max_total_iterations { + return Err(format!("Maximum iterations ({}) reached", max_total_iterations).into()); + } + + // Prepare messages for OpenAI API + let mut api_messages = vec![ + json!({ + "role": "system", + "content": self.system_message + }) + ]; + + for msg in &working_conversation { + if msg.content.len() == 1 && matches!(msg.content[0], OpenAIContent::Text { .. }) { + // Simple text message + if let OpenAIContent::Text { text, .. } = &msg.content[0] { + api_messages.push(json!({ + "role": msg.role, + "content": text + })); + } + } else { + // Complex message with multiple parts (text + files/images) + let content_parts: Vec = msg.content + .iter() + .map(|c| match c { + OpenAIContent::Text { text, .. } => json!({ + "type": "text", + "text": text + }), + OpenAIContent::ImageUrl { image_url, .. } => json!({ + "type": "image_url", + "image_url": { + "url": image_url.url + } + }), + OpenAIContent::File { file, .. } => json!({ + "type": "file", + "file": { + "file_id": file.file_id + } + }), + }) + .collect(); + + api_messages.push(json!({ + "role": msg.role, + "content": content_parts + })); + } + } + + // Make API call to OpenAI + let url = format!("{}/chat/completions", self.base_url); + + let request_body = json!({ + "model": "gpt-4.1-mini", + "messages": api_messages, + "tools": self.tools, + "tool_choice": "auto", + "temperature": 0.1, + "max_tokens": 4000 + }); + + let response = self.client + .post(&url) + .header("Content-Type", "application/json") + .header("Authorization", format!("Bearer {}", self.api_key)) + .json(&request_body) + .send() + .await?; + + if !response.status().is_success() { + let error_text = response.text().await?; + return Err(format!("OpenAI API error: {}", error_text).into()); + } + + let openai_response: OpenAIResponse = response.json().await?; + let choice = openai_response.choices.first() + .ok_or("No choice in OpenAI response")?; + + println!("=== OPENAI RESPONSE DEBUG (Iteration {}) ===", *total_iterations); + println!("Finish reason: {:?}", choice.finish_reason); + println!("Content: {:?}", choice.message.content); + println!("Tool calls: {:?}", choice.message.tool_calls.as_ref().map(|tc| tc.len())); + println!("=========================================="); + + // Process the response + let mut has_tool_calls = false; + let mut text_content = String::new(); + let mut tool_calls_in_response = Vec::new(); + + // Get text content + if let Some(content) = &choice.message.content { + text_content = content.clone(); + } + + // Get tool calls + if let Some(tool_calls) = &choice.message.tool_calls { + has_tool_calls = true; + tool_calls_in_response = tool_calls.clone(); + } + + // Add text to accumulated response + if !text_content.trim().is_empty() { + accumulated_text.push_str(&text_content); + } + + // Add the assistant's response to working conversation + let assistant_content = if !text_content.is_empty() { + vec![OpenAIContent::text(text_content)] + } else { + vec![OpenAIContent::text("Using tools...".to_string())] + }; + + let assistant_message = OpenAIMessage { + role: "assistant".to_string(), + content: assistant_content, + }; + working_conversation.push(assistant_message); + + // If there are tool calls, execute them and add results for next iteration + if has_tool_calls { + println!("=== EXECUTING {} TOOL CALLS ===", tool_calls_in_response.len()); + + let mut tool_responses = Vec::new(); + + for tool_call in tool_calls_in_response { + // Check for cancellation before executing function + if let Some(ref token) = cancellation_token { + if token.is_cancelled() { + return Err("Request was cancelled by user".into()); + } + } + + println!("Executing function: {}", tool_call.function.name); + + // Parse arguments + let args: Value = serde_json::from_str(&tool_call.function.arguments) + .map_err(|e| format!("Failed to parse tool arguments: {}", e))?; + + // Execute the function + let function_result = tool_executor(&tool_call.function.name, &args).await?; + + println!("Function result length: {}", function_result.len()); + + // Record the tool call + let tool_call_info = OpenAIToolCallInfo { + function_name: tool_call.function.name.clone(), + arguments: args, + result: function_result.clone(), + }; + all_tool_calls.push(tool_call_info); + + // Prepare tool response for API + tool_responses.push(format!("Function '{}' result: {}", tool_call.function.name, function_result)); + } + + // Add tool results as user message + let tool_results_message = OpenAIMessage { + role: "user".to_string(), + content: vec![OpenAIContent::text(tool_responses.join("\n\n"))], + }; + working_conversation.push(tool_results_message); + + println!("=== CONTINUING TO NEXT ITERATION ==="); + continue; + } else { + println!("=== NO TOOL CALLS - BREAKING ==="); + break; + } + } + + // Return the accumulated text from all iterations + let final_response = if accumulated_text.trim().is_empty() { + if !all_tool_calls.is_empty() { + format!("Task completed successfully. {} tools executed.", all_tool_calls.len()) + } else { + "Task completed.".to_string() + } + } else { + accumulated_text.trim().to_string() + }; + + Ok(final_response) + } + + /// Clear conversation history + pub fn clear_conversation(&mut self) { + self.conversation_history.clear(); + } + + /// Reset client completely + pub fn reset_completely(&mut self) { + self.conversation_history.clear(); + self.setup_system_instruction(); + } + + /// Upload a file to OpenAI for user data purposes + async fn upload_file_for_user_data(&self, file_path: &str) -> Result> { + let file_data = std::fs::read(file_path)?; + let file_name = std::path::Path::new(file_path) + .file_name() + .and_then(|n| n.to_str()) + .unwrap_or("document.pdf"); + + // Create multipart form data + let form = reqwest::multipart::Form::new() + .part("purpose", reqwest::multipart::Part::text("user_data")) + .part("file", reqwest::multipart::Part::bytes(file_data) + .file_name(file_name.to_string()) + .mime_str("application/pdf")?); + + let response = self.client + .post("https://api.openai.com/v1/files") + .header("Authorization", format!("Bearer {}", self.api_key)) + .multipart(form) + .send() + .await?; + + if !response.status().is_success() { + let error_text = response.text().await?; + return Err(format!("File upload failed: {}", error_text).into()); + } + + let upload_response: Value = response.json().await?; + let file_id = upload_response["id"].as_str() + .ok_or("No file ID in upload response")? + .to_string(); + + Ok(file_id) + } + + /// Send a message with PDF attachments using File Upload API + pub async fn send_message_with_pdf(&mut self, message: &str, pdf_files: Vec, tool_executor: F) -> Result> + where + F: Fn(&str, &Value) -> std::pin::Pin>> + Send>> + Send + Sync, + { + // Create content for the message + let mut content = vec![OpenAIContent::text(message.to_string())]; + + // Upload PDF files and add them as file references + for pdf_path in &pdf_files { + match self.upload_file_for_user_data(pdf_path).await { + Ok(file_id) => { + content.push(OpenAIContent::file(file_id.clone())); + println!("Uploaded and added PDF attachment: {} (file_id: {})", pdf_path, file_id); + } + Err(e) => { + println!("Warning: Failed to upload PDF {}: {}", pdf_path, e); + // Continue without this file - OpenAI will inform user about inability to read PDFs + } + } + } + + // Add user message with attachments to conversation history + let user_message = OpenAIMessage { + role: "user".to_string(), + content, + }; + self.conversation_history.push(user_message); + + // Track all tool calls made during this conversation turn + let mut all_tool_calls = Vec::new(); + let mut total_iterations = 0; + + // Start the conversation turn + let final_response = self.process_conversation_turn(&tool_executor, &mut all_tool_calls, &mut total_iterations).await?; + + // Add the final response to conversation history + let final_message = OpenAIMessage { + role: "assistant".to_string(), + content: vec![OpenAIContent::text(final_response.clone())], + }; + self.conversation_history.push(final_message); + + // Calculate has_tool_calls before moving all_tool_calls + let has_tool_calls = !all_tool_calls.is_empty(); + + Ok(OpenAIResponseDetails { + content: final_response, + tool_calls: all_tool_calls, + iterations: total_iterations, + has_tool_calls, + }) + } +} \ No newline at end of file diff --git a/examples/excel-copilot/src-tauri/tauri.conf.json b/examples/excel-copilot/src-tauri/tauri.conf.json new file mode 100644 index 00000000..d02df7a8 --- /dev/null +++ b/examples/excel-copilot/src-tauri/tauri.conf.json @@ -0,0 +1,56 @@ +{ + "$schema": "https://schema.tauri.app/config/2", + "productName": "excel-copilot", + "version": "0.1.0", + "identifier": "com.terminator.excel-copilot", + "build": { + "beforeDevCommand": "npm run dev", + "beforeBuildCommand": "npm run build", + "devUrl": "http://localhost:1420", + "frontendDist": "../dist" + }, + "app": { + "windows": [ + { + "title": "Terminator Copilot", + "width": 1200, + "height": 800, + "minWidth": 400, + "minHeight": 620, + "resizable": true, + "maximizable": true, + "minimizable": true, + "closable": true, + "alwaysOnTop": false, + "decorations": true, + "transparent": false, + "shadow": true, + "center": true, + "skipTaskbar": false, + "tabbingIdentifier": "excel-copilot" + } + ], + "security": { + "csp": null + } + }, + "bundle": { + "active": true, + "targets": "all", + "icon": [ + "icons/32x32.png", + "icons/128x128.png", + "icons/128x128@2x.png", + "icons/icon.icns", + "icons/icon.ico" + ], + "macOS": { + "frameworks": [], + "minimumSystemVersion": "10.13", + "signingIdentity": null, + "hardenedRuntime": true, + "entitlements": null, + "providerShortName": "io.terminator.excel-copilot" + } + } +} \ No newline at end of file diff --git a/examples/excel-copilot/src/App.css b/examples/excel-copilot/src/App.css new file mode 100644 index 00000000..9b3a4f1d --- /dev/null +++ b/examples/excel-copilot/src/App.css @@ -0,0 +1,1836 @@ +/* App.css - Modern minimalist design for Excel Copilot */ + +:root { + --color-primary: #000000; + --color-secondary: #666666; + --color-tertiary: #999999; + --color-background: #ffffff; + --color-surface: #f8f9fa; + --color-border: #e9ecef; + --color-border-light: #f1f3f4; + --color-success: #28a745; + --color-warning: #ffc107; + --color-error: #dc3545; + --color-accent: #007bff; + + --spacing-xs: 4px; + --spacing-sm: 8px; + --spacing-md: 16px; + --spacing-lg: 24px; + --spacing-xl: 32px; + --spacing-xxl: 48px; + + --radius-sm: 4px; + --radius-md: 8px; + --radius-lg: 12px; + + --font-size-xs: 12px; + --font-size-sm: 14px; + --font-size-md: 16px; + --font-size-lg: 18px; + --font-size-xl: 20px; + --font-size-xxl: 24px; + + --font-weight-normal: 400; + --font-weight-medium: 500; + --font-weight-semibold: 600; + + --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05); + --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1); + --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1); +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; + font-size: var(--font-size-md); + font-weight: var(--font-weight-normal); + line-height: 1.5; + color: var(--color-primary); + background-color: var(--color-background); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +/* App Layout */ +.app { + height: 100vh; + display: flex; + flex-direction: column; + overflow: hidden; +} + +/* Header */ +.header { + background: var(--color-background); + border-bottom: 1px solid var(--color-border); + padding: var(--spacing-md) var(--spacing-xl); + flex-shrink: 0; +} + +.header-content { + display: flex; + align-items: center; + justify-content: space-between; + max-width: 1400px; + margin: 0 auto; +} + +.logo { + display: flex; + align-items: center; + gap: var(--spacing-sm); +} + +.logo-icon { + width: 32px; + height: 32px; + background: var(--color-primary); + color: var(--color-background); + border-radius: var(--radius-md); + display: flex; + align-items: center; + justify-content: center; + font-weight: var(--font-weight-semibold); + font-size: var(--font-size-lg); +} + +.logo-text { + font-size: var(--font-size-lg); + font-weight: var(--font-weight-semibold); + color: var(--color-primary); +} + +.status { + display: flex; + align-items: center; + gap: var(--spacing-sm); +} + +.status-text { + font-size: var(--font-size-sm); + color: var(--color-secondary); +} + +.loading-spinner { + width: 16px; + height: 16px; + border: 2px solid var(--color-border); + border-top-color: var(--color-primary); + border-radius: 50%; + animation: spin 1s linear infinite; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +.gemini-indicator { + display: flex; + align-items: center; + gap: var(--spacing-sm); +} + +.indicator-dot { + width: 8px; + height: 8px; + border-radius: 50%; +} + +.indicator-dot.connected { + background: var(--color-success); +} + +.indicator-dot.disconnected { + background: var(--color-error); +} + +.indicator-text { + font-size: var(--font-size-sm); + color: var(--color-secondary); +} + +/* Main Layout */ +.main { + flex: 1; + display: flex; + overflow: hidden; + max-width: 1400px; + margin: 0 auto; + width: 100%; + gap: 0; +} + +/* Sidebar */ +.sidebar { + width: 320px; + background: var(--color-surface); + border-right: 1px solid var(--color-border); + padding: var(--spacing-lg); + padding-bottom: calc(var(--spacing-lg) + 80px); + overflow-y: auto; + flex-shrink: 0; +} + +.file-section, +.preview-section { + margin-bottom: var(--spacing-xl); +} + +.section-title { + font-size: var(--font-size-md); + font-weight: var(--font-weight-medium); + color: var(--color-primary); + margin-bottom: var(--spacing-md); +} + +.button-group { + display: flex; + flex-direction: column; + gap: var(--spacing-sm); + margin-bottom: var(--spacing-md); +} + +/* Buttons */ +.btn { + padding: var(--spacing-sm) var(--spacing-md); + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); + background: var(--color-background); + color: var(--color-primary); + font-size: var(--font-size-sm); + font-weight: var(--font-weight-medium); + cursor: pointer; + transition: all 0.15s ease; + text-align: center; + text-decoration: none; + display: inline-flex; + align-items: center; + justify-content: center; +} + +.btn:hover:not(:disabled) { + background: var(--color-surface); + border-color: var(--color-secondary); +} + +.btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.btn-primary { + background: var(--color-primary); + color: var(--color-background); + border-color: var(--color-primary); +} + +.btn-primary:hover:not(:disabled) { + background: var(--color-secondary); + border-color: var(--color-secondary); +} + +.btn-secondary { + background: var(--color-surface); + border-color: var(--color-border); +} + +.btn-outline { + background: transparent; + border-color: var(--color-border); +} + +.btn-icon { + width: 32px; + height: 32px; + padding: 0; + border: none; + background: transparent; + color: var(--color-secondary); + font-size: var(--font-size-md); + cursor: pointer; + border-radius: var(--radius-sm); + transition: all 0.15s ease; +} + +.btn-icon:hover { + background: var(--color-surface); + color: var(--color-primary); +} + +/* File Section */ +.current-file { + padding: var(--spacing-sm); + background: var(--color-background); + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); + font-size: var(--font-size-sm); +} + +.file-label { + color: var(--color-secondary); + margin-right: var(--spacing-sm); +} + +.file-name { + color: var(--color-primary); + font-weight: var(--font-weight-medium); +} + +/* Preview Section */ +.sheet-preview { + margin-bottom: var(--spacing-md); +} + +.sheet-name { + font-size: var(--font-size-sm); + font-weight: var(--font-weight-medium); + color: var(--color-primary); + margin-bottom: var(--spacing-sm); +} + +.table-container { + overflow-x: auto; + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); + background: var(--color-background); +} + +.excel-table { + width: 100%; + border-collapse: collapse; + font-size: var(--font-size-xs); +} + +.excel-table td { + padding: var(--spacing-xs) var(--spacing-sm); + border-bottom: 1px solid var(--color-border-light); + border-right: 1px solid var(--color-border-light); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 80px; +} + +.excel-table tr:last-child td { + border-bottom: none; +} + +.excel-table td:last-child { + border-right: none; +} + +.table-info { + padding: var(--spacing-xs) var(--spacing-sm); + font-size: var(--font-size-xs); + color: var(--color-secondary); + background: var(--color-surface); + border-top: 1px solid var(--color-border); +} + +.empty-state { + padding: var(--spacing-lg); + text-align: center; + color: var(--color-secondary); + font-size: var(--font-size-sm); + background: var(--color-background); + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); +} + +/* Chat Section */ +.chat-section { + flex: 1; + display: flex; + flex-direction: column; + overflow: hidden; +} + +.chat-header { + padding: var(--spacing-lg); + border-bottom: 1px solid var(--color-border); + display: flex; + align-items: center; + justify-content: space-between; + flex-shrink: 0; +} + +.chat-title { + font-size: var(--font-size-xl); + font-weight: var(--font-weight-medium); + color: var(--color-primary); +} + +.chat-controls { + display: flex; + gap: var(--spacing-sm); +} + +/* API Key Input */ +.api-key-input { + padding: var(--spacing-lg); + border-bottom: 1px solid var(--color-border); + display: flex; + gap: var(--spacing-sm); + align-items: center; + background: var(--color-surface); +} + +.api-input { + flex: 1; + padding: var(--spacing-sm) var(--spacing-md); + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); + background: var(--color-background); + font-size: var(--font-size-sm); + color: var(--color-primary); +} + +.api-input:focus { + outline: none; + border-color: var(--color-accent); +} + +/* Chat Messages */ +.chat-messages { + flex: 1; + overflow-y: auto; + padding: var(--spacing-lg); + padding-bottom: var(--spacing-lg); +} + +.empty-chat { + display: flex; + align-items: center; + justify-content: center; + height: 100%; +} + +.welcome-content { + text-align: center; + max-width: 500px; +} + +.welcome-content h3 { + font-size: var(--font-size-xl); + font-weight: var(--font-weight-medium); + color: var(--color-primary); + margin-bottom: var(--spacing-md); +} + +.welcome-content p { + color: var(--color-secondary); + margin-bottom: var(--spacing-lg); +} + +.feature-list { + list-style: none; + margin-bottom: var(--spacing-lg); +} + +.feature-list li { + color: var(--color-secondary); + margin-bottom: var(--spacing-xs); +} + +.example-queries { + display: flex; + flex-direction: column; + gap: var(--spacing-sm); +} + +.query-example { + padding: var(--spacing-sm) var(--spacing-md); + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); + font-size: var(--font-size-sm); + color: var(--color-secondary); + font-family: 'SF Mono', Monaco, Consolas, monospace; +} + +/* Message Bubbles */ +.message { + margin-bottom: var(--spacing-lg); + max-width: 70%; +} + +.user-message { + margin-left: auto; +} + +.model-message { + margin-right: auto; +} + +.message-content { + padding: var(--spacing-sm) var(--spacing-md); + border-radius: var(--radius-lg); + max-width: 80%; + word-wrap: break-word; + white-space: pre-wrap; +} + +.user-message .message-content { + background: var(--color-primary); + color: var(--color-background); +} + +.model-message .message-content { + background: var(--color-surface); + color: var(--color-primary); + border: 1px solid var(--color-border); +} + +.message-time { + font-size: var(--font-size-xs); + color: var(--color-tertiary); + margin-top: var(--spacing-xs); + text-align: right; +} + +.user-message .message-time { + text-align: right; +} + +.model-message .message-time { + text-align: left; +} + +/* Tool Calls */ +.tool-calls { + margin-top: var(--spacing-md); + padding: var(--spacing-md); + background: var(--color-background); + border: 1px solid var(--color-border); + border-radius: var(--radius-md); +} + +.tool-calls-header { + font-size: var(--font-size-xs); + font-weight: var(--font-weight-medium); + color: var(--color-secondary); + margin-bottom: var(--spacing-sm); + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.tool-call { + margin-bottom: var(--spacing-md); + padding: var(--spacing-sm); + background: var(--color-surface); + border-radius: var(--radius-sm); +} + +.tool-call:last-child { + margin-bottom: 0; +} + +.tool-call-name { + font-size: var(--font-size-xs); + font-weight: var(--font-weight-medium); + color: var(--color-primary); + margin-bottom: var(--spacing-xs); +} + +.tool-call-args, +.tool-call-result { + margin-bottom: var(--spacing-sm); +} + +.tool-call-args strong, +.tool-call-result strong { + font-size: var(--font-size-xs); + color: var(--color-secondary); + display: block; + margin-bottom: var(--spacing-xs); +} + +.tool-call-args pre { + font-size: var(--font-size-xs); + background: var(--color-background); + padding: var(--spacing-xs); + border-radius: var(--radius-sm); + overflow-x: auto; + color: var(--color-primary); +} + +.result-content { + font-size: var(--font-size-xs); + color: var(--color-primary); + white-space: pre-wrap; +} + +/* Response Details */ +.response-details { + margin-top: var(--spacing-sm); + display: flex; + gap: var(--spacing-sm); +} + +.detail-badge { + padding: 2px var(--spacing-xs); + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); + font-size: var(--font-size-xs); + color: var(--color-secondary); +} + +/* Chat Input */ +.chat-input { + border-top: 1px solid var(--color-border); + background: var(--color-background); + flex-shrink: 0; + position: sticky; + bottom: 0; + z-index: 100; + min-height: 60px; + display: flex; + flex-direction: column; + margin: 0; +} + +/* Gemini Setup Message */ +.gemini-setup-message { + padding: var(--spacing-md) var(--spacing-lg); + background: rgba(255, 193, 7, 0.1); + border: 1px solid rgba(255, 193, 7, 0.3); + border-radius: var(--radius-md); + margin: var(--spacing-md) var(--spacing-lg) 0; +} + +.setup-message-content { + display: flex; + align-items: center; + gap: var(--spacing-md); +} + +.setup-icon { + font-size: 20px; + flex-shrink: 0; +} + +.setup-text { + display: flex; + flex-direction: column; + gap: var(--spacing-xs); +} + +.setup-text strong { + font-size: var(--font-size-md); + font-weight: var(--font-weight-semibold); + color: var(--color-primary); +} + +.setup-text span { + font-size: var(--font-size-sm); + color: var(--color-secondary); +} + +.pdf-attachments { + padding: var(--spacing-md) var(--spacing-lg) 0; +} + +.attachments-header { + font-size: var(--font-size-xs); + font-weight: var(--font-weight-medium); + color: var(--color-secondary); + margin-bottom: var(--spacing-sm); +} + +.attachments-list { + display: flex; + flex-wrap: wrap; + gap: var(--spacing-sm); + margin-bottom: var(--spacing-md); +} + +.attachment-item { + display: flex; + align-items: center; + gap: var(--spacing-xs); + padding: var(--spacing-xs) var(--spacing-sm); + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); + font-size: var(--font-size-xs); +} + +.attachment-name { + color: var(--color-primary); +} + +.remove-attachment { + background: none; + border: none; + color: var(--color-secondary); + cursor: pointer; + font-size: var(--font-size-sm); + width: 16px; + height: 16px; + display: flex; + align-items: center; + justify-content: center; + border-radius: var(--radius-sm); +} + +.remove-attachment:hover { + background: var(--color-border); + color: var(--color-primary); +} + +.input-controls { + display: flex; + gap: var(--spacing-sm); + padding: var(--spacing-lg); + align-items: stretch; +} + +.attach-btn { + width: 40px; + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); + color: var(--color-secondary); + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.15s ease; +} + +.attach-btn:hover:not(:disabled) { + background: var(--color-border); + color: var(--color-primary); +} + +.message-input { + flex: 1; + padding: var(--spacing-sm) var(--spacing-md); + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); + background: var(--color-background); + font-size: var(--font-size-sm); + color: var(--color-primary); + resize: none; +} + +.message-input:focus { + outline: none; + border-color: var(--color-accent); +} + +.message-input::placeholder { + color: var(--color-tertiary); +} + +.send-btn { + width: 40px; + background: var(--color-primary); + color: var(--color-background); + border: 1px solid var(--color-primary); + border-radius: var(--radius-sm); + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.15s ease; + font-size: var(--font-size-md); +} + +.send-btn:hover:not(:disabled) { + background: var(--color-secondary); + border-color: var(--color-secondary); +} + +.send-btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.stop-btn { + width: 40px; + background: #e74c3c; + color: white; + border: 1px solid #e74c3c; + border-radius: var(--radius-sm); + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.15s ease; + font-size: var(--font-size-md); + animation: pulse 1.5s infinite; +} + +.stop-btn:hover { + background: #c0392b; + border-color: #c0392b; + animation: none; +} + +@keyframes pulse { + 0% { opacity: 1; } + 50% { opacity: 0.7; } + 100% { opacity: 1; } +} + +/* Responsive Design */ +@media (max-width: 768px) { + .main { + flex-direction: column; + gap: 0; + } + + .sidebar { + width: 100%; + max-height: 40vh; + } + + .chat-section { + flex: 1; + display: flex; + flex-direction: column; + min-height: 60vh; + } + + .chat-messages { + flex: 1; + overflow-y: auto; + padding: var(--spacing-md); + padding-bottom: 120px; + } + + .header-content { + padding: var(--spacing-sm) var(--spacing-md); + } + + .logo-text { + display: none; + } + + .message { + max-width: 90%; + } +} + +/* Scrollbar Styling */ +::-webkit-scrollbar { + width: 8px; +} + +::-webkit-scrollbar-track { + background: var(--color-surface); +} + +::-webkit-scrollbar-thumb { + background: var(--color-border); + border-radius: var(--radius-sm); +} + +::-webkit-scrollbar-thumb:hover { + background: var(--color-secondary); +} + +/* Selection Styling */ +::selection { + background: var(--color-primary); + color: var(--color-background); +} + +/* Copilot Settings Section */ +.copilot-section { + margin-top: 20px; + padding: 15px; + border: 1px solid #e1e5e9; + border-radius: 8px; + background: #f8f9fa; +} + +.copilot-toggle { + margin-bottom: 15px; +} + +.toggle-label { + display: flex; + align-items: center; + cursor: pointer; + gap: 10px; +} + +.toggle-input { + display: none; +} + +.toggle-slider { + position: relative; + width: 50px; + height: 24px; + background-color: #ccc; + border-radius: 24px; + transition: background-color 0.3s; +} + +.toggle-slider::before { + content: ''; + position: absolute; + width: 20px; + height: 20px; + border-radius: 50%; + background-color: white; + top: 2px; + left: 2px; + transition: transform 0.3s; +} + +.toggle-input:checked + .toggle-slider { + background-color: #007acc; +} + +.toggle-input:checked + .toggle-slider::before { + transform: translateX(26px); +} + +.toggle-text { + font-weight: 500; + color: #333; +} + +.copilot-requirements { + background: #fff3cd; + border: 1px solid #ffeaa7; + border-radius: 6px; + padding: 12px; + margin-top: 10px; +} + +.requirements-header { + font-weight: bold; + color: #856404; + margin-bottom: 8px; + font-size: 14px; +} + +.requirements-list { + list-style: none; + padding: 0; + margin: 0 0 10px 0; +} + +.requirements-list li { + margin: 6px 0; + font-size: 13px; + color: #856404; + line-height: 1.4; +} + +.requirements-note { + font-size: 12px; + color: #6c757d; + font-style: italic; + background: #f8f9fa; + padding: 8px; + border-radius: 4px; + border-left: 3px solid #007acc; +} + +.copilot-warning { + background: #f8d7da; + border: 1px solid #f5c6cb; + color: #721c24; + padding: 10px; + border-radius: 6px; + margin-top: 10px; + font-size: 13px; + font-weight: 500; +} + +/* Improved Mode Selector */ +.app-mode-selector { + display: flex; + flex-direction: column; + gap: 12px; + margin-bottom: 24px; + padding: 20px; + background: linear-gradient(135deg, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.05)); + border-radius: 12px; + border: 1px solid rgba(255, 255, 255, 0.15); + backdrop-filter: blur(10px); +} + +.mode-option { + position: relative; +} + +.mode-radio { + position: absolute; + opacity: 0; + width: 0; + height: 0; +} + +.mode-label { + display: flex; + align-items: center; + gap: 16px; + padding: 16px 20px; + background: rgba(255, 255, 255, 0.05); + border: 2px solid rgba(255, 255, 255, 0.1); + border-radius: 10px; + cursor: pointer; + transition: all 0.3s ease; + user-select: none; +} + +.mode-label:hover { + background: rgba(255, 255, 255, 0.1); + border-color: rgba(255, 255, 255, 0.3); + transform: translateY(-2px); + box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15); +} + +.mode-radio:checked + .mode-label { + background: linear-gradient(135deg, #4ade80, #22c55e); + border-color: #16a34a; + color: #ffffff; + box-shadow: 0 8px 25px rgba(34, 197, 94, 0.4); +} + +.mode-icon { + font-size: 28px; + line-height: 1; + flex-shrink: 0; +} + +.mode-info { + flex: 1; +} + +.mode-title { + font-size: 16px; + font-weight: 600; + margin-bottom: 4px; + color: inherit; +} + +.mode-subtitle { + font-size: 13px; + opacity: 0.8; + color: inherit; +} + +.google-sheets-info { + margin-top: 16px; + padding: 16px; + background: rgba(59, 130, 246, 0.1); + border: 1px solid rgba(59, 130, 246, 0.3); + border-radius: 10px; +} + +.info-header { + font-weight: 600; + color: #60a5fa; + margin-bottom: 12px; + font-size: 14px; +} + +.info-steps { + display: flex; + flex-direction: column; + gap: 8px; +} + +.info-step { + font-size: 13px; + color: #93c5fd; + line-height: 1.4; + padding-left: 12px; + position: relative; +} + +.info-step::before { + content: '•'; + position: absolute; + left: 0; + color: #60a5fa; + font-weight: bold; +} + +.google-sheets-details { + display: flex; + flex-direction: column; + gap: 16px; +} + +.requirement-item { + display: flex; + align-items: flex-start; + gap: 16px; + padding: 16px; + background: rgba(255, 255, 255, 0.05); + border-radius: 10px; + border: 1px solid rgba(255, 255, 255, 0.1); +} + +.requirement-icon { + font-size: 24px; + flex-shrink: 0; + margin-top: 2px; +} + +.requirement-content { + flex: 1; +} + +.requirement-title { + font-size: 15px; + font-weight: 600; + color: #e5e7eb; + margin-bottom: 6px; +} + +.requirement-text { + font-size: 13px; + color: #9ca3af; + line-height: 1.5; +} + +/* Enhanced Responsiveness */ +@media (max-width: 1200px) { + .main { + max-width: 100%; + padding: 0 16px; + } + + .sidebar { + width: 280px; + } + + +} + +@media (max-width: 968px) { + .main { + flex-direction: column; + padding: 0; + } + + .sidebar { + width: 100%; + max-height: 40vh; + overflow-y: auto; + border-right: none; + border-bottom: 1px solid var(--color-border); + padding: 16px; + } + + .app-mode-selector { + padding: 12px; + } + + .mode-label { + padding: 10px 12px; + } + + .mode-icon { + font-size: 20px; + } + + .chat-section { + flex: 1; + min-height: 60vh; + height: 60vh; + } + + + .google-sheets-info { + padding: 12px; + margin-top: 8px; + } + + .info-steps { + gap: 6px; + } + + .info-step { + padding: 6px 0; + font-size: 13px; + } +} + +@media (max-width: 640px) { + .header-content { + padding: 12px 16px; + flex-wrap: wrap; + gap: 12px; + } + + .logo-text { + display: none; + } + + .sidebar { + padding: 12px; + max-height: 35vh; + } + + .app-mode-selector { + padding: 8px; + gap: 6px; + } + + .mode-label { + padding: 8px 10px; + gap: 10px; + } + + .mode-icon { + font-size: 18px; + } + + .mode-title { + font-size: 13px; + } + + .mode-subtitle { + font-size: 11px; + } + + .button-group { + gap: 6px; + } + + .btn { + padding: 6px 10px; + font-size: 12px; + } + + .chat-header { + padding: 12px 16px; + flex-wrap: wrap; + gap: 8px; + } + + .chat-title { + font-size: 16px; + } + + .chat-section { + min-height: 65vh; + height: 65vh; + } + + .chat-messages { + padding: 12px 16px; + height: calc(100% - 160px); + overflow-y: auto; + } + + .message { + max-width: 95%; + } + + .message-content { + padding: 8px 10px; + font-size: 13px; + } + + + + .gemini-setup-message { + margin: 8px 16px 0; + padding: 12px 16px; + } + + .setup-message-content { + gap: 12px; + } + + .setup-icon { + font-size: 18px; + } + + .setup-text strong { + font-size: 14px; + } + + .setup-text span { + font-size: 12px; + } + + .input-controls { + gap: 8px; + align-items: stretch; + } + + .message-input { + font-size: 14px; + padding: 8px 10px; + min-height: 36px; + } + + .attach-btn, + .send-btn, + .stop-btn { + width: 36px; + height: 36px; + flex-shrink: 0; + font-size: 14px; + } + + .google-sheets-info { + padding: 8px; + margin-top: 8px; + } + + .requirement-item { + padding: 8px; + gap: 8px; + } + + .requirement-icon { + font-size: 16px; + } + + .requirement-title { + font-size: 13px; + } + + .requirement-text { + font-size: 11px; + } + + /* Make copilot section more compact */ + .copilot-requirements { + padding: 8px; + margin-top: 8px; + } + + .requirements-list { + margin: 8px 0; + } + + .requirements-list li { + font-size: 11px; + margin-bottom: 4px; + } +} + + +/* Media query for split screen scenarios (900px to 640px) */ +@media (max-width: 900px) and (min-width: 641px) { + .main { + flex-direction: row; /* Keep horizontal layout */ + } + + .sidebar { + width: 280px; + min-width: 280px; + } + + .chat-section { + flex: 1; + display: flex; + flex-direction: column; + min-height: 100%; + } + + .chat-messages { + flex: 1; + overflow-y: auto; + padding: var(--spacing-md); + padding-bottom: 120px; + } + + + + .input-controls { + gap: var(--spacing-sm); + align-items: stretch; + } + + .message-input { + font-size: var(--font-size-sm); + padding: 10px 12px; + min-height: 40px; + } + + .attach-btn, + .send-btn, + .stop-btn { + width: 40px; + height: 40px; + flex-shrink: 0; + } +} + +@media (max-width: 480px) { + .app { + font-size: 14px; + } + + .header { + padding: 6px 12px; + } + + .header-content { + padding: 6px 0; + } + + .sidebar { + max-height: 30vh; /* Very compact sidebar */ + padding: 8px; + } + + .chat-section { + min-height: 70vh; /* Give most space to chat */ + height: 70vh; + } + + + + .gemini-setup-message { + margin: 6px 12px 0; + padding: 10px 12px; + } + + .setup-message-content { + gap: 10px; + flex-direction: column; + align-items: flex-start; + } + + .setup-icon { + font-size: 16px; + align-self: center; + } + + .setup-text { + text-align: center; + width: 100%; + } + + .setup-text strong { + font-size: 13px; + } + + .setup-text span { + font-size: 11px; + } + + .chat-messages { + padding: 8px 12px; + padding-bottom: 80px; + height: calc(100% - 140px); + } + + + .app-mode-selector { + padding: 6px; + gap: 4px; + } + + .mode-label { + padding: 6px 8px; + gap: 8px; + } + + .mode-title { + font-size: 12px; + } + + .mode-subtitle { + display: none; + } + + + .google-sheets-details .requirement-item:nth-child(n+3) { + display: none; + } + + .copilot-requirements { + padding: 6px; + } + + .requirements-note { + font-size: 10px; + } +} + + +.app-mode-toggle, +.mode-toggle-label, +.mode-text { + display: none; +} + + +.availability-status { + margin-top: 12px; + padding: 10px 12px; + border-radius: 6px; + font-size: 13px; + font-weight: 500; + line-height: 1.4; +} + +.availability-status.ready { + background: rgba(34, 197, 94, 0.15); + color: #22c55e; + border: 1px solid rgba(34, 197, 94, 0.3); +} + +.availability-status.warning { + background: rgba(251, 146, 60, 0.15); + color: #fb923c; + border: 1px solid rgba(251, 146, 60, 0.3); +} + + +.btn-small { + padding: 6px 12px; + font-size: 12px; + font-weight: 500; +} + + +.sidebar-toggle { + display: none; + position: fixed; + top: 70px; + left: 16px; + z-index: 1000; + background: var(--color-accent); + color: white; + border: none; + border-radius: 50%; + width: 40px; + height: 40px; + cursor: pointer; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2); + font-size: 16px; + transition: transform 0.2s ease; +} + +.sidebar-toggle:hover { + transform: scale(1.1); +} + +.sidebar-collapsed { + transform: translateX(-100%); + transition: transform 0.3s ease; +} + +@media (max-width: 640px) { + .sidebar-toggle { + display: block; + } + + .main { + flex-direction: column; + height: calc(100vh - 60px); + } + + .sidebar { + position: relative; + transition: transform 0.3s ease, max-height 0.3s ease; + max-height: 40vh; + flex-shrink: 0; + } + + .sidebar.collapsed { + max-height: 0; + overflow: hidden; + padding: 0 12px; + border-bottom: none; + } + + .chat-section { + flex: 1; + display: flex; + flex-direction: column; + min-height: 0; + height: 60vh; + } + + .chat-section.expanded { + height: calc(100vh - 60px); + } + + .chat-input { + position: sticky; + bottom: 0; + z-index: 200; + flex-shrink: 0; + margin-top: auto; + } + + .chat-messages { + flex: 1; + min-height: 0; + overflow-y: auto; + padding-bottom: 120px; + } +} + +@media (max-width: 480px) { + .sidebar-toggle { + top: 60px; + left: 12px; + width: 36px; + height: 36px; + font-size: 14px; + } + + .sidebar { + max-height: 35vh; + } + + .chat-section { + height: 65vh; + } + + .chat-section.expanded { + height: calc(100vh - 60px); + } + + .chat-input { + position: fixed; + bottom: 0; + left: 0; + right: 0; + z-index: 300; + background: var(--color-background); + border-top: 2px solid var(--color-border); + box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.15); + } + + .chat-messages { + padding-bottom: 140px; + } +} + +/* Landscape mode optimizations */ +@media (max-height: 600px) and (orientation: landscape) { + .sidebar { + max-height: 35vh; + } + + .chat-section { + min-height: 65vh; + } + + + + .chat-messages { + padding-bottom: var(--spacing-lg); + } +} + +/* Very small heights (like split screen scenarios) */ +@media (max-height: 400px) { + .app { + overflow: hidden; + } + + .header { + padding: 4px 16px; + } + + .sidebar { + max-height: 25vh; + padding: 8px; + } + + .chat-section { + min-height: 75vh; + display: flex; + flex-direction: column; + } + + .chat-messages { + flex: 1; + overflow-y: auto; + padding: 8px; + padding-bottom: 80px; + } + + + + .input-controls { + gap: 6px; + } + + .message-input { + font-size: 13px; + padding: 6px 8px; + min-height: 32px; + } + + .attach-btn, + .send-btn, + .stop-btn { + width: 32px; + height: 32px; + font-size: 12px; + } +} + +/* Extreme split screen (very narrow width but normal height) */ +@media (max-width: 500px) and (min-height: 500px) { + .main { + flex-direction: column; + } + + .sidebar { + width: 100%; + max-height: 30vh; + } + + .chat-section { + flex: 1; + min-height: 70vh; + } + + .chat-messages { + padding-bottom: var(--spacing-lg); + } +} + +/* LLM Provider Selector Styles */ +.llm-indicator { + display: flex; + align-items: center; + gap: 8px; +} + +.llm-selector { + display: flex; + flex-direction: column; + gap: 12px; + margin-bottom: 20px; + padding: 16px; + background: rgba(255, 255, 255, 0.05); + border-radius: 12px; + border: 1px solid rgba(255, 255, 255, 0.1); +} + +.llm-option { + flex: 1; +} + +.llm-radio { + position: absolute; + opacity: 0; + cursor: pointer; +} + +.llm-label { + display: flex; + align-items: center; + gap: 12px; + padding: 12px 16px; + background: rgba(255, 255, 255, 0.05); + border: 2px solid rgba(255, 255, 255, 0.1); + border-radius: 10px; + cursor: pointer; + transition: all 0.2s ease; + color: #e5e7eb; +} + +.llm-label:hover { + background: rgba(255, 255, 255, 0.08); + border-color: rgba(255, 255, 255, 0.2); +} + +.llm-radio:checked + .llm-label { + background: rgba(59, 130, 246, 0.15); + border-color: #3b82f6; + color: #60a5fa; +} + +.llm-icon { + font-size: 20px; + flex-shrink: 0; +} + +.llm-name { + flex: 1; + font-weight: 500; + font-size: 14px; +} + +.llm-status { + font-size: 16px; + flex-shrink: 0; +} + +.api-input-row { + display: flex; + gap: 12px; + align-items: center; +} + +.api-input-row .api-input { + flex: 1; +} + +.llm-setup-message { + padding: 16px; + margin-bottom: 16px; + background: rgba(251, 191, 36, 0.1); + border: 1px solid rgba(251, 191, 36, 0.3); + border-radius: 10px; +} + +/* Mobile responsive adjustments for LLM selector */ +@media (max-width: 640px) { + .llm-selector { + padding: 12px; + gap: 8px; + } + + .llm-label { + padding: 10px 12px; + gap: 10px; + } + + .llm-icon { + font-size: 18px; + } + + .llm-name { + font-size: 13px; + } + + .llm-status { + font-size: 14px; + } + + .api-input-row { + flex-direction: column; + gap: 8px; + } + + .api-input-row .api-input { + width: 100%; + } +} + +@media (max-width: 480px) { + .llm-selector { + padding: 8px; + } + + .llm-label { + padding: 8px 10px; + } +} diff --git a/examples/excel-copilot/src/App.tsx b/examples/excel-copilot/src/App.tsx new file mode 100644 index 00000000..34c8de42 --- /dev/null +++ b/examples/excel-copilot/src/App.tsx @@ -0,0 +1,1025 @@ +import { useState, useEffect, useRef } from 'react'; +import { invoke } from '@tauri-apps/api/core'; +import './App.css'; + +interface ToolCall { + function_name: string; + arguments: any; + result: string; +} + +interface ChatMessage { + role: string; + content: string; + timestamp: number; + tool_calls?: ToolCall[]; + response_details?: { + has_tool_calls: boolean; + iterations: number; + }; +} + +function App() { + const [currentFile, setCurrentFile] = useState(''); + const [chatMessages, setChatMessages] = useState([]); + const [currentMessage, setCurrentMessage] = useState(''); + const [isGeminiConfigured, setIsGeminiConfigured] = useState(false); + const [isOpenAIConfigured, setIsOpenAIConfigured] = useState(false); + const [geminiApiKey, setGeminiApiKey] = useState(''); + const [openaiApiKey, setOpenaiApiKey] = useState(''); + const [selectedLlm, setSelectedLlm] = useState<'gemini' | 'openai'>('gemini'); + const [showApiKeyInput, setShowApiKeyInput] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const [status, setStatus] = useState('ready'); + const [attachedPdfs, setAttachedPdfs] = useState([]); + const [copilotEnabled, setCopilotEnabled] = useState(false); + const [sheetsMode, setSheetsMode] = useState<'excel' | 'googlesheets'>('excel'); + const [googleSheetsStatus, setGoogleSheetsStatus] = useState(''); + const [sidebarCollapsed, setSidebarCollapsed] = useState(false); + // Stop functionality + const [isRequestCancelled, setIsRequestCancelled] = useState(false); + const pollingIntervalRef = useRef(null); + const abortControllerRef = useRef(null); + + useEffect(() => { + loadChatHistory(); + loadLlmProvider(); + }, []); + + const loadLlmProvider = async () => { + try { + const provider = await invoke('get_llm_provider'); + setSelectedLlm(provider as 'gemini' | 'openai'); + } catch (error) { + console.error('failed to load LLM provider:', error); + } + }; + + // Check if screen is small + useEffect(() => { + const checkScreenSize = () => { + if (window.innerWidth <= 640) { + // On small screens, start with sidebar collapsed if chat has messages + if (chatMessages.length > 0) { + setSidebarCollapsed(true); + } + } else { + setSidebarCollapsed(false); + } + }; + + checkScreenSize(); + window.addEventListener('resize', checkScreenSize); + return () => window.removeEventListener('resize', checkScreenSize); + }, [chatMessages.length]); + + const toggleSidebar = () => { + setSidebarCollapsed(!sidebarCollapsed); + }; + + const loadChatHistory = async () => { + try { + const history = await invoke('get_chat_history'); + setChatMessages(history); + } catch (error) { + console.error('failed to load chat history:', error); + } + }; + + const showStatus = (message: string) => { + setStatus(message); + setTimeout(() => setStatus('ready'), 3000); + }; + + const handleNewFile = async () => { + setIsLoading(true); + try { + const result = await invoke('create_new_excel'); + setCurrentFile('new workbook'); + showStatus(`excel file created: ${result}`); + } catch (error) { + console.error('error creating new file:', error); + showStatus('failed to create new file'); + } finally { + setIsLoading(false); + } + }; + + const handleOpenFile = async () => { + setIsLoading(true); + try { + const result = await invoke('open_excel_file'); + setCurrentFile(result); + showStatus('file opened successfully'); + } catch (error) { + console.error('error opening file:', error); + showStatus('failed to open file'); + } finally { + setIsLoading(false); + } + }; + + const handleOpenGoogleSheets = async () => { + setIsLoading(true); + try { + const result = await invoke('open_google_sheets'); + showStatus(`google sheets opened: ${result}`); + setTimeout(checkGoogleSheetsAvailability, 2000); + } catch (error) { + console.error('error opening google sheets:', error); + showStatus('failed to open google sheets'); + } finally { + setIsLoading(false); + } + }; + + const checkGoogleSheetsAvailability = async () => { + try { + const result = await invoke('check_google_sheets_availability'); + setGoogleSheetsStatus(result); + if (result.includes('available and ready')) { + showStatus('google sheets with gemini is ready'); + } else { + showStatus('google sheets availability checked'); + } + } catch (error) { + console.error('error checking google sheets:', error); + setGoogleSheetsStatus('Error checking availability'); + } + }; + + // Removed automatic Google Sheets availability check when switching to Google Sheets mode + // The user should manually click "check availability" when they want to verify + + // Reconfigure clients when Copilot setting changes + useEffect(() => { + const reconfigureClients = async () => { + if (isGeminiConfigured && geminiApiKey.trim()) { + console.log(`Reconfiguring Gemini with Copilot ${copilotEnabled ? 'enabled' : 'disabled'}`); + try { + await invoke('setup_gemini_client', { + apiKey: geminiApiKey, + copilotEnabled: copilotEnabled + }); + showStatus(`gemini reconfigured with copilot ${copilotEnabled ? 'enabled' : 'disabled'}`); + } catch (error) { + console.error('Error reconfiguring Gemini:', error); + } + } + + if (isOpenAIConfigured && openaiApiKey.trim()) { + console.log(`Reconfiguring OpenAI with Copilot ${copilotEnabled ? 'enabled' : 'disabled'}`); + try { + await invoke('setup_openai_client', { + apiKey: openaiApiKey, + copilotEnabled: copilotEnabled + }); + showStatus(`openai reconfigured with copilot ${copilotEnabled ? 'enabled' : 'disabled'}`); + } catch (error) { + console.error('Error reconfiguring OpenAI:', error); + } + } + }; + + // Only reconfigure if we have configured clients + if (isGeminiConfigured || isOpenAIConfigured) { + reconfigureClients(); + } + }, [copilotEnabled, isGeminiConfigured, isOpenAIConfigured, geminiApiKey, openaiApiKey]); // Trigger when copilotEnabled changes + + const isLlmConfigured = selectedLlm === 'gemini' ? isGeminiConfigured : isOpenAIConfigured; + + const isChatInputDisabled = isLoading || !isLlmConfigured; + + const setupGemini = async () => { + if (!geminiApiKey.trim()) return; + + setIsLoading(true); + try { + await invoke('setup_gemini_client', { + apiKey: geminiApiKey, + copilotEnabled: copilotEnabled + }); + setIsGeminiConfigured(true); + setShowApiKeyInput(false); + showStatus('gemini configured successfully'); + } catch (error) { + console.error('error setting up gemini:', error); + showStatus('failed to configure gemini'); + } finally { + setIsLoading(false); + } + }; + + const setupOpenAI = async () => { + if (!openaiApiKey.trim()) return; + + setIsLoading(true); + try { + await invoke('setup_openai_client', { + apiKey: openaiApiKey, + copilotEnabled: copilotEnabled + }); + setIsOpenAIConfigured(true); + setShowApiKeyInput(false); + showStatus('openai configured successfully'); + } catch (error) { + console.error('error setting up openai:', error); + showStatus('failed to configure openai'); + } finally { + setIsLoading(false); + } + }; + + const switchLlmProvider = async (provider: 'gemini' | 'openai') => { + try { + await invoke('set_llm_provider', { provider }); + setSelectedLlm(provider); + showStatus(`switched to ${provider}`); + } catch (error) { + console.error('error switching LLM provider:', error); + showStatus('failed to switch LLM provider'); + } + }; + + const stopCurrentRequest = async () => { + console.log('🛑 User requested to stop current request'); + setIsRequestCancelled(true); + + try { + // Call the backend to stop the LLM request + await invoke('stop_llm_request'); + console.log('✓ Backend cancellation signal sent'); + } catch (error) { + console.error('Error stopping request:', error); + } + + // Stop polling interval if active + if (pollingIntervalRef.current) { + clearInterval(pollingIntervalRef.current); + pollingIntervalRef.current = null; + console.log('✓ Polling interval cleared'); + } + + // Abort the request if possible + if (abortControllerRef.current) { + abortControllerRef.current.abort(); + abortControllerRef.current = null; + console.log('✓ Request aborted'); + } + + // Reset loading state + setIsLoading(false); + + // Show status + showStatus('request stopped by user'); + + // Update the assistant placeholder to show cancellation + setChatMessages(prev => { + const updated = [...prev]; + const lastMessage = updated[updated.length - 1]; + if (lastMessage && lastMessage.role === 'model' && lastMessage.content === 'thinking...') { + lastMessage.content = '❌ Request stopped by user'; + lastMessage.response_details = { has_tool_calls: false, iterations: 0 }; + } + return updated; + }); + }; + + const sendMessage = async () => { + if (!currentMessage.trim() || !isLlmConfigured || isLoading) return; + + // Reset cancellation state + setIsRequestCancelled(false); + setIsLoading(true); + + const userMessageText = currentMessage; + const pdfsToProcess = [...attachedPdfs]; + setCurrentMessage(''); + setAttachedPdfs([]); + + const newUserMessage: ChatMessage = { + role: 'user', + content: userMessageText, + timestamp: Date.now(), + }; + + const assistantPlaceholder: ChatMessage = { + role: 'model', + content: 'thinking...', + timestamp: Date.now() + 1, // Ensure unique timestamp for placeholder + tool_calls: [], + response_details: { has_tool_calls: false, iterations: 0 }, + }; + + const chatLengthBeforeLocalUpdate = chatMessages.length; + console.log(`📤 Sending message: "${userMessageText}"`); + + // Display user message and placeholder + setChatMessages(prev => [...prev, newUserMessage, assistantPlaceholder]); + + let pollingTimeout: NodeJS.Timeout | null = null; + + try { + // Create AbortController for request cancellation + const abortController = new AbortController(); + abortControllerRef.current = abortController; + + const processingPromise = pdfsToProcess.length > 0 + ? invoke('chat_with_llm_pdf', { message: userMessageText, pdfFiles: pdfsToProcess }) + : invoke('chat_with_llm', { message: userMessageText }); + + const pollForUpdates = setInterval(async () => { + // Check if request was cancelled + if (isRequestCancelled) { + clearInterval(pollForUpdates); + return; + } + try { + const backendHistory = await invoke('get_chat_history'); + + setChatMessages(currentLocalMessages => { + // Simple check: if backend has more messages than what we expect, update + const expectedLength = chatLengthBeforeLocalUpdate + 2; // user message + assistant response + + if (backendHistory.length >= expectedLength) { + const lastBackendMessage = backendHistory[backendHistory.length - 1]; + + // If the last message is from the model and is not "thinking...", the response is complete + if (lastBackendMessage && + lastBackendMessage.role === 'model' && + lastBackendMessage.content !== 'thinking...') { + console.log('📝 Response complete detected, updating UI'); + return backendHistory; + } + } + + // Check if we have a partial response with tool calls to update the placeholder + if (backendHistory.length > currentLocalMessages.length) { + const lastBackendMessage = backendHistory[backendHistory.length - 1]; + if (lastBackendMessage && lastBackendMessage.role === 'model') { + // Update the placeholder with new tool calls or status + const updatedMessages = [...currentLocalMessages]; + const localPlaceholderIndex = updatedMessages.findIndex( + m => m.role === 'model' && m.timestamp === assistantPlaceholder.timestamp + ); + + if (localPlaceholderIndex !== -1) { + // Replace placeholder with backend version that may have tool calls + updatedMessages[localPlaceholderIndex] = lastBackendMessage; + return updatedMessages; + } + } + } + + return currentLocalMessages; + }); + } catch (error) { + console.error('Error polling chat history:', error); + } + }, 500); // Reduced polling interval for better responsiveness + + // Store polling interval reference for cancellation + pollingIntervalRef.current = pollForUpdates; + + // Safety timeout to stop polling after 2 minutes (in case backend doesn't respond properly) + pollingTimeout = setTimeout(() => { + console.log('⚠️ Polling timeout reached, forcing stop'); + if (pollingIntervalRef.current) { + clearInterval(pollingIntervalRef.current); + pollingIntervalRef.current = null; + } + // Force final sync + loadChatHistory(); + }, 120000); // 2 minutes timeout + + const responseSummary = await processingPromise; // Wait for the main backend operation to complete + + // Check if request was cancelled during processing + if (isRequestCancelled) { + console.log('Request was cancelled, stopping here'); + clearInterval(pollForUpdates); + return; + } + + console.log('🏁 Backend processing complete, stopping polling'); + clearInterval(pollForUpdates); // Stop polling + if (pollingTimeout) clearTimeout(pollingTimeout); // Clear timeout + pollingIntervalRef.current = null; + + // Final sync with backend history - this ensures UI is up to date + console.log('🔄 Final sync with backend history'); + await loadChatHistory(); + + showStatus(responseSummary ? `message sent: ${responseSummary}`: 'Message processed'); + } catch (error: any) { + // Check if error is due to cancellation + if (isRequestCancelled) { + console.log('Request was cancelled'); + return; + } + + console.error('Error sending message:', error); + showStatus(`error: ${error.message || error}`); + + // Clear polling if still active + if (pollingIntervalRef.current) { + clearInterval(pollingIntervalRef.current); + pollingIntervalRef.current = null; + } + // Clear timeout as well + if (pollingTimeout) clearTimeout(pollingTimeout); + + // Add error message to chat and remove placeholder + setChatMessages(prev => { + const updated = prev.filter(msg => msg.timestamp !== assistantPlaceholder.timestamp); + // Add error message + updated.push({ + role: 'model', + content: `❌ Error: ${error.message || error}`, + timestamp: Date.now(), + tool_calls: [], + response_details: { has_tool_calls: false, iterations: 0 }, + }); + return updated; + }); + } finally { + // Clean up references + abortControllerRef.current = null; + pollingIntervalRef.current = null; + + // ALWAYS reset loading state in finally block + setIsLoading(false); + } + }; + + const selectPdfFiles = async () => { + try { + const selectedFiles = await invoke('select_pdf_files'); + if (selectedFiles.length > 0) { + setAttachedPdfs(prev => [...prev, ...selectedFiles]); + showStatus(`${selectedFiles.length} pdf(s) attached`); + } + } catch (error) { + console.error('error selecting pdf files:', error); + showStatus('failed to select pdf files'); + } + }; + + const removePdf = (index: number) => { + setAttachedPdfs(prev => prev.filter((_, i) => i !== index)); + showStatus('pdf removed'); + }; + + const testExcelInteraction = async () => { + setIsLoading(true); + try { + showStatus('testing excel interaction...'); + + const cellValue = await invoke('excel_read_cell', { cellAddress: 'A1' }); + showStatus(`cell a1: ${cellValue}`); + + await invoke('excel_write_cell', { + cellAddress: 'B1', + value: 'hello from copilot!' + }); + + showStatus('excel interaction test completed'); + } catch (error) { + console.error('excel interaction test failed:', error); + showStatus(`excel test failed: ${error}`); + } finally { + setIsLoading(false); + } + }; + + const showLocaleInfo = async () => { + setIsLoading(true); + try { + const localeInfo = await invoke('get_locale_info'); + showStatus(`locale info: ${localeInfo}`); + + // Add a message to chat to show locale info + const userMessage: ChatMessage = { + role: 'user', + content: '🌍 System Locale Information Request', + timestamp: Date.now() + }; + + const assistantMessage: ChatMessage = { + role: 'model', + content: `📍 **System Locale Information:**\n\n${localeInfo}\n\n*This affects how numbers are formatted when writing to Excel. The system automatically normalizes number formats according to your locale before sending them to Excel.*`, + timestamp: Date.now() + }; + + setChatMessages(prev => [...prev, userMessage, assistantMessage]); + + } catch (error) { + console.error('failed to get locale info:', error); + showStatus(`locale info failed: ${error}`); + } finally { + setIsLoading(false); + } + }; + + const clearChat = async () => { + try { + await invoke('clear_chat_history'); + setChatMessages([]); + showStatus('chat cleared'); + } catch (error) { + console.error('error clearing chat:', error); + showStatus('failed to clear chat'); + } + }; + + const formatTime = (timestamp: number) => { + return new Date(timestamp).toLocaleTimeString(); + }; + + return ( +
+ {/* Header */} +
+
+
+
E
+ excel copilot +
+
+ {status} + {isLoading &&
} +
+
+
+ + {isLlmConfigured ? `${selectedLlm} ready` : `configure ${selectedLlm}`} + +
+
+
+ + {/* Main Content */} +
+ {/* Sidebar Toggle Button for Mobile */} + + + {/* Sidebar */} + + + {/* Chat Area */} +
+
+

+ chat with {selectedLlm} - {sheetsMode === 'excel' ? 'excel mode' : 'google sheets mode'} +

+
+ + +
+
+ + {showApiKeyInput && ( +
+
+
+ switchLlmProvider(e.target.value as 'gemini' | 'openai')} + className="llm-radio" + /> + +
+
+ switchLlmProvider(e.target.value as 'gemini' | 'openai')} + className="llm-radio" + /> + +
+
+ + {selectedLlm === 'gemini' ? ( +
+ setGeminiApiKey(e.target.value)} + onKeyPress={(e) => e.key === 'Enter' && setupGemini()} + className="api-input" + /> + +
+ ) : ( +
+ setOpenaiApiKey(e.target.value)} + onKeyPress={(e) => e.key === 'Enter' && setupOpenAI()} + className="api-input" + /> + +
+ )} +
+ )} + +
+ {chatMessages.length === 0 ? ( +
+
+

welcome to excel copilot

+

connect with {selectedLlm} to start analyzing your {sheetsMode === 'excel' ? 'excel' : 'google sheets'} data.

+
    +
  • ask questions about your data
  • +
  • request summaries and insights
  • +
  • get help with formulas
  • +
  • auto-generate content
  • + {sheetsMode === 'googlesheets' &&
  • interact via google sheets gemini
  • } +
+
+ {sheetsMode === 'excel' ? ( + <> +
"what's the sum of column a?"
+
"create a summary of this data"
+
"add a formula to calculate average"
+ + ) : ( + <> +
"Add this data to the sheet: Name, Age, City"
+
"Create a bar chart from columns A to C"
+
"Format column B as currency and make row 1 bold"
+ + )} +
+
+
+ ) : ( + chatMessages.map((msg, index) => ( +
+
+ {msg.content} +
+ + {msg.tool_calls && msg.tool_calls.length > 0 && ( +
+
+ functions called ({msg.tool_calls.length}) +
+ {msg.tool_calls.map((toolCall, tcIndex) => ( +
+
+ {toolCall.function_name} +
+
+ arguments: +
{JSON.stringify(toolCall.arguments, null, 2)}
+
+
+ result: +
{toolCall.result}
+
+
+ ))} +
+ )} + + {msg.role === 'model' && msg.response_details && ( +
+ {msg.response_details.has_tool_calls && ( + + used {msg.response_details.iterations} iteration(s) + + )} + + llm response + +
+ )} + +
+ {formatTime(msg.timestamp)} +
+
+ )) + )} +
+ +
+ {!isLlmConfigured && ( +
+
+ 🔑 +
+ Configure {selectedLlm} API first + Click the settings button (⚙) above to enter your {selectedLlm} API key +
+
+
+ )} + + {isLlmConfigured && attachedPdfs.length > 0 && ( +
+
+ attached pdf files ({attachedPdfs.length}) +
+
+ {attachedPdfs.map((pdf, index) => ( +
+ {pdf} + +
+ ))} +
+
+ )} + +
+ + setCurrentMessage(e.target.value)} + onKeyPress={(e) => e.key === 'Enter' && sendMessage()} + disabled={isChatInputDisabled} + className="message-input" + /> + {isLoading ? ( + + ) : ( + + )} +
+
+
+
+
+ ); +} + +export default App; \ No newline at end of file diff --git a/examples/excel-copilot/src/assets/react.svg b/examples/excel-copilot/src/assets/react.svg new file mode 100644 index 00000000..6c87de9b --- /dev/null +++ b/examples/excel-copilot/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/excel-copilot/src/main.tsx b/examples/excel-copilot/src/main.tsx new file mode 100644 index 00000000..2be325ed --- /dev/null +++ b/examples/excel-copilot/src/main.tsx @@ -0,0 +1,9 @@ +import React from "react"; +import ReactDOM from "react-dom/client"; +import App from "./App"; + +ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( + + + , +); diff --git a/examples/excel-copilot/src/vite-env.d.ts b/examples/excel-copilot/src/vite-env.d.ts new file mode 100644 index 00000000..11f02fe2 --- /dev/null +++ b/examples/excel-copilot/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/examples/excel-copilot/tsconfig.json b/examples/excel-copilot/tsconfig.json new file mode 100644 index 00000000..a7fc6fbf --- /dev/null +++ b/examples/excel-copilot/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/examples/excel-copilot/tsconfig.node.json b/examples/excel-copilot/tsconfig.node.json new file mode 100644 index 00000000..42872c59 --- /dev/null +++ b/examples/excel-copilot/tsconfig.node.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/examples/excel-copilot/vite.config.ts b/examples/excel-copilot/vite.config.ts new file mode 100644 index 00000000..f74d1b2d --- /dev/null +++ b/examples/excel-copilot/vite.config.ts @@ -0,0 +1,32 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; + +// @ts-expect-error process is a nodejs global +const host = process.env.TAURI_DEV_HOST; + +// https://vitejs.dev/config/ +export default defineConfig(async () => ({ + plugins: [react()], + + // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build` + // + // 1. prevent vite from obscuring rust errors + clearScreen: false, + // 2. tauri expects a fixed port, fail if that port is not available + server: { + port: 1420, + strictPort: true, + host: host || false, + hmr: host + ? { + protocol: "ws", + host, + port: 1421, + } + : undefined, + watch: { + // 3. tell vite to ignore watching `src-tauri` + ignored: ["**/src-tauri/**"], + }, + }, +}));