├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── README.md ├── src └── main.rs ├── ui-events-app ├── .gitignore ├── README.md ├── bun.lock ├── components.json ├── index.html ├── package.json ├── public │ ├── tauri.svg │ └── vite.svg ├── src-tauri │ ├── .gitignore │ ├── Cargo.lock │ ├── Cargo.toml │ ├── build.rs │ ├── capabilities │ │ └── default.json │ ├── icons │ │ ├── 128x128.png │ │ ├── 128x128@2x.png │ │ ├── 32x32.png │ │ ├── Square107x107Logo.png │ │ ├── Square142x142Logo.png │ │ ├── Square150x150Logo.png │ │ ├── Square284x284Logo.png │ │ ├── Square30x30Logo.png │ │ ├── Square310x310Logo.png │ │ ├── Square44x44Logo.png │ │ ├── Square71x71Logo.png │ │ ├── Square89x89Logo.png │ │ ├── StoreLogo.png │ │ ├── icon.icns │ │ ├── icon.ico │ │ └── icon.png │ ├── src │ │ ├── lib.rs │ │ └── main.rs │ └── tauri.conf.json ├── src │ ├── App.css │ ├── App.tsx │ ├── assets │ │ └── react.svg │ ├── components │ │ └── ui │ │ │ ├── badge.tsx │ │ │ ├── card.tsx │ │ │ └── scroll-area.tsx │ ├── lib │ │ └── utils.ts │ ├── main.tsx │ └── vite-env.d.ts ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts └── ui-events ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── examples └── simple_client.rs └── src ├── bin └── main.rs ├── error.rs ├── event.rs ├── lib.rs ├── platform ├── linux.rs ├── macos.rs ├── mod.rs └── windows.rs └── server.rs /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | 3 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "ui-events", 4 | "ui-events-app/src-tauri" 5 | ] 6 | resolver = "2" 7 | 8 | [workspace.package] 9 | version = "0.1.0" 10 | authors = ["louis030195 "] 11 | description = "" 12 | repository = "https://github.com/mediar-ai/ui-events" 13 | license = "MIT OR Apache-2.0" 14 | edition = "2024" 15 | 16 | [workspace.dependencies] 17 | 18 | 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # ui-events 3 | 4 | [![Build Status](https://img.shields.io/github/actions/workflow/status/your-username/ui-events/rust.yml?branch=main)](https://github.com/your-username/ui-events/actions) [![Crates.io](https://img.shields.io/crates/v/ui-events.svg)](https://crates.io/crates/ui-events) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 5 | 6 | A cross-platform Rust library designed to capture specific UI interaction events using native operating system accessibility APIs (initially focusing on macOS `AXObserver` capabilities, with planned support for Windows UI Automation and Linux AT-SPI) and stream them over a websocket connection for usage with any language. 7 | 8 | https://github.com/user-attachments/assets/e014ba45-0ffe-49f9-8a8f-db70884fea2c 9 | 10 | ### Motivation 11 | 12 | Understanding user interface interactions like focus shifts, window changes, and value modifications is essential for building context-aware AI agents, workflow recording & automation. Accessing these events consistently across platforms often involves complex, platform-specific code. 13 | 14 | `ui-events` aims to provide a performant Rust core that leverages native accessibility APIs, exposing a stream of key UI events via a simple websocket interface accessible from any language (JavaScript, Python, Go, etc.). 15 | 16 | ### Features 17 | 18 | * **UI Focus:** Captures events like application activation/deactivation, window creation/destruction/movement/resizing, focused UI element changes, and potentially value changes or selection changes within elements. 19 | * **Cross-Platform Goal:** Targets macOS, Windows (via UI Automation), and Linux (via AT-SPI). 20 | * **Real-time Websocket Stream:** Provides a low-latency stream of UI events over a local websocket server. 21 | * **Language Agnostic:** Consumable from any language via standard websockets (or better alternative). 22 | * **Performant Rust Core:** Built with Rust for efficiency and reliability. 23 | 24 | *Note: Direct capture of low-level mouse clicks or raw keyboard presses might require different OS mechanisms; `ui-events` focuses on events reported through the accessibility layer.* 25 | 26 | ### Architecture 27 | 28 | ``` 29 | +---------------------------------+ +---------------------+ +-------------------+ +---------------------+ 30 | | Native Accessibility Events | ---> | ui-events | ---> | Websocket Server | ---> | Client Application | 31 | | (AXObserver, UIA, AT-SPI) | | (Rust Core Listener)| | (ws://localhost:xxxx) | | (JS, Python, etc.) | 32 | +---------------------------------+ +---------------------+ +-------------------+ +---------------------+ 33 | ``` 34 | 35 | ### Quick Start 36 | 37 | Install Rust: 38 | 39 | ```bash 40 | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh 41 | ``` 42 | 43 | 1. **Run the Core Service:** 44 | ```bash 45 | # Build from source (ensure Rust toolchain is installed) 46 | cargo run 47 | ``` 48 | Grant accessibility permissions when prompted by the OS. The service hosts the websocket server (default: `ws://localhost:9001` - confirm/specify port). 49 | 50 | Run the Rust client: 51 | ```bash 52 | cargo run --example simple_client 53 | ``` 54 | 55 | Or from other languages: 56 | 57 | 2. **Connect from Your Client:** 58 | 59 | *JavaScript Example:* 60 | ```javascript 61 | const ws = new WebSocket('ws://localhost:9001'); // Use the configured port 62 | 63 | ws.onmessage = (event) => { 64 | const event_data = JSON.parse(event.data); 65 | console.log('Received UI Event:', event_data); 66 | // Process event data (e.g., focus change, window created) 67 | }; 68 | 69 | ws.onerror = (error) => { 70 | console.error('WebSocket Error:', error); 71 | }; 72 | 73 | ws.onopen = () => { 74 | console.log('Connected to ui-events'); 75 | }; 76 | ``` 77 | 78 | *Python Example:* 79 | ```python 80 | import websocket 81 | import json 82 | import threading 83 | # (Ensure 'websocket-client' library is installed: pip install websocket-client) 84 | 85 | def on_message(ws, message): 86 | event_data = json.loads(message) 87 | print(f"Received UI Event: {event_data}") 88 | # Process event data 89 | 90 | def on_error(ws, error): 91 | print(f"Error: {error}") 92 | 93 | def on_close(ws, close_status_code, close_msg): 94 | print("### Connection Closed ###") 95 | 96 | def on_open(ws): 97 | print("WebSocket Connection Opened") 98 | 99 | def run_ws(): 100 | ws = websocket.WebSocketApp("ws://localhost:9001/", # Use configured port 101 | on_open=on_open, 102 | on_message=on_message, 103 | on_error=on_error, 104 | on_close=on_close) 105 | ws.run_forever() 106 | 107 | if __name__ == "__main__": 108 | ws_thread = threading.Thread(target=run_ws) 109 | ws_thread.start() 110 | # Keep main thread alive or join ws_thread 111 | ``` 112 | 113 | ### Event Schema (Example Structure) 114 | 115 | Events streamed over the websocket follow a consistent JSON structure. The exact `event_type` and `details` will depend on the specific accessibility event captured. 116 | 117 | ```json 118 | { 119 | "event_type": "string", // e.g., "focus_changed", "window_created", "value_changed", "application_activated" 120 | "timestamp": "iso8601_string", // UTC timestamp 121 | "application_name": "string | null", // Name of the relevant application 122 | "window_title": "string | null", // Title of the relevant window 123 | "element_details": { // Information about the UI element involved, if applicable 124 | "role": "string | null", // e.g., "AXTextField", "AXButton", "AXWindow" 125 | "identifier": "string | null", // Accessibility label or identifier 126 | "value": "string | number | boolean | null", // Current value, if relevant and available 127 | "position": { "x": number, "y": number } | null, 128 | "size": { "width": number, "height": number } | null 129 | }, 130 | "event_specific_data": { 131 | // Optional details specific to the event_type 132 | // e.g., for "window_moved": { "new_position": { "x": ..., "y": ... } } 133 | } 134 | } 135 | ``` 136 | *(A detailed `SCHEMA.md` should document the specific events captured on each platform)* 137 | 138 | 3. **Run the UI Events App:** 139 | 140 | ```bash 141 | cd ui-events-app 142 | bun i 143 | bun tauri dev 144 | ``` 145 | 146 | ### Status 147 | 148 | - [x] experimental macOS support 149 | - [ ] Windows support 150 | - [ ] Linux support 151 | 152 | ### Contributing 153 | 154 | Contributions are welcome! Please see `CONTRIBUTING.md`. 155 | 156 | ### License 157 | 158 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 159 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use cidre::ns; 3 | use clap::Parser; 4 | use tokio::sync::mpsc; 5 | 6 | mod error; 7 | mod event; 8 | mod platform; 9 | mod server; 10 | 11 | use platform::create_listener; 12 | use server::run_server; 13 | use tracing::{error, info}; 14 | 15 | #[derive(Parser, Debug)] 16 | #[clap(author, version, about, long_about = None)] 17 | struct Args { 18 | /// WebSocket server port 19 | #[clap(short, long, value_parser, default_value_t = 9001)] 20 | port: u16, 21 | } 22 | 23 | // #[tokio::main] 24 | fn main() { 25 | tracing_subscriber::fmt::init(); 26 | info!("starting ui-events..."); 27 | 28 | let port = Args::parse().port; 29 | 30 | // Create a channel for communication between listener and server 31 | let (tx, rx) = mpsc::channel(100); // Buffer size 100 32 | 33 | let rt = tokio::runtime::Builder::new_multi_thread() 34 | .enable_all() 35 | .worker_threads(2) 36 | .build() 37 | .unwrap(); 38 | 39 | rt.spawn(async move { 40 | run_server(port, rx).await.unwrap(); 41 | ns::App::shared().terminate(None); 42 | }); 43 | 44 | platform::listener_run(tx); 45 | } 46 | -------------------------------------------------------------------------------- /ui-events-app/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /ui-events-app/README.md: -------------------------------------------------------------------------------- 1 | # Tauri + React + Typescript 2 | 3 | This template should help get you started developing with Tauri, React and Typescript in Vite. 4 | 5 | ## Recommended IDE Setup 6 | 7 | - [VS Code](https://code.visualstudio.com/) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer) 8 | -------------------------------------------------------------------------------- /ui-events-app/bun.lock: -------------------------------------------------------------------------------- 1 | { 2 | "lockfileVersion": 1, 3 | "workspaces": { 4 | "": { 5 | "name": "ui-events-app", 6 | "dependencies": { 7 | "@radix-ui/react-scroll-area": "^1.2.4", 8 | "@radix-ui/react-slot": "^1.2.0", 9 | "@tailwindcss/vite": "^4.1.3", 10 | "@tauri-apps/api": "^2", 11 | "@tauri-apps/plugin-opener": "^2", 12 | "class-variance-authority": "^0.7.1", 13 | "clsx": "^2.1.1", 14 | "lucide-react": "^0.487.0", 15 | "react": "^18.3.1", 16 | "react-dom": "^18.3.1", 17 | "tailwind-merge": "^3.2.0", 18 | "tailwindcss": "^4.1.3", 19 | "tw-animate-css": "^1.2.5", 20 | }, 21 | "devDependencies": { 22 | "@tauri-apps/cli": "^2", 23 | "@types/node": "^22.14.0", 24 | "@types/react": "^18.3.1", 25 | "@types/react-dom": "^18.3.1", 26 | "@vitejs/plugin-react": "^4.3.4", 27 | "typescript": "~5.6.2", 28 | "vite": "^6.0.3", 29 | }, 30 | }, 31 | }, 32 | "packages": { 33 | "@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="], 34 | 35 | "@babel/code-frame": ["@babel/code-frame@7.26.2", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.25.9", "js-tokens": "^4.0.0", "picocolors": "^1.0.0" } }, "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ=="], 36 | 37 | "@babel/compat-data": ["@babel/compat-data@7.26.8", "", {}, "sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ=="], 38 | 39 | "@babel/core": ["@babel/core@7.26.10", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.26.2", "@babel/generator": "^7.26.10", "@babel/helper-compilation-targets": "^7.26.5", "@babel/helper-module-transforms": "^7.26.0", "@babel/helpers": "^7.26.10", "@babel/parser": "^7.26.10", "@babel/template": "^7.26.9", "@babel/traverse": "^7.26.10", "@babel/types": "^7.26.10", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ=="], 40 | 41 | "@babel/generator": ["@babel/generator@7.27.0", "", { "dependencies": { "@babel/parser": "^7.27.0", "@babel/types": "^7.27.0", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" } }, "sha512-VybsKvpiN1gU1sdMZIp7FcqphVVKEwcuj02x73uvcHE0PTihx1nlBcowYWhDwjpoAXRv43+gDzyggGnn1XZhVw=="], 42 | 43 | "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.27.0", "", { "dependencies": { "@babel/compat-data": "^7.26.8", "@babel/helper-validator-option": "^7.25.9", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-LVk7fbXml0H2xH34dFzKQ7TDZ2G4/rVTOrq9V+icbbadjbVxxeFeDsNHv2SrZeWoA+6ZiTyWYWtScEIW07EAcA=="], 44 | 45 | "@babel/helper-module-imports": ["@babel/helper-module-imports@7.25.9", "", { "dependencies": { "@babel/traverse": "^7.25.9", "@babel/types": "^7.25.9" } }, "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw=="], 46 | 47 | "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.26.0", "", { "dependencies": { "@babel/helper-module-imports": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9", "@babel/traverse": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw=="], 48 | 49 | "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.26.5", "", {}, "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg=="], 50 | 51 | "@babel/helper-string-parser": ["@babel/helper-string-parser@7.25.9", "", {}, "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA=="], 52 | 53 | "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.25.9", "", {}, "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ=="], 54 | 55 | "@babel/helper-validator-option": ["@babel/helper-validator-option@7.25.9", "", {}, "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw=="], 56 | 57 | "@babel/helpers": ["@babel/helpers@7.27.0", "", { "dependencies": { "@babel/template": "^7.27.0", "@babel/types": "^7.27.0" } }, "sha512-U5eyP/CTFPuNE3qk+WZMxFkp/4zUzdceQlfzf7DdGdhp+Fezd7HD+i8Y24ZuTMKX3wQBld449jijbGq6OdGNQg=="], 58 | 59 | "@babel/parser": ["@babel/parser@7.27.0", "", { "dependencies": { "@babel/types": "^7.27.0" }, "bin": "./bin/babel-parser.js" }, "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg=="], 60 | 61 | "@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-y8quW6p0WHkEhmErnfe58r7x0A70uKphQm8Sp8cV7tjNQwK56sNVK0M73LK3WuYmsuyrftut4xAkjjgU0twaMg=="], 62 | 63 | "@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+iqjT8xmXhhYv4/uiYd8FNQsraMFZIfxVSqxxVSZP0WbbSAWvBXAul0m/zu+7Vv4O/3WtApy9pmaTMiumEZgfg=="], 64 | 65 | "@babel/template": ["@babel/template@7.27.0", "", { "dependencies": { "@babel/code-frame": "^7.26.2", "@babel/parser": "^7.27.0", "@babel/types": "^7.27.0" } }, "sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA=="], 66 | 67 | "@babel/traverse": ["@babel/traverse@7.27.0", "", { "dependencies": { "@babel/code-frame": "^7.26.2", "@babel/generator": "^7.27.0", "@babel/parser": "^7.27.0", "@babel/template": "^7.27.0", "@babel/types": "^7.27.0", "debug": "^4.3.1", "globals": "^11.1.0" } }, "sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA=="], 68 | 69 | "@babel/types": ["@babel/types@7.27.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9" } }, "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg=="], 70 | 71 | "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-wCIboOL2yXZym2cgm6mlA742s9QeJ8DjGVaL39dLN4rRwrOgOyYSnOaFPhKZGLb2ngj4EyfAFjsNJwPXZvseag=="], 72 | 73 | "@esbuild/android-arm": ["@esbuild/android-arm@0.25.2", "", { "os": "android", "cpu": "arm" }, "sha512-NQhH7jFstVY5x8CKbcfa166GoV0EFkaPkCKBQkdPJFvo5u+nGXLEH/ooniLb3QI8Fk58YAx7nsPLozUWfCBOJA=="], 74 | 75 | "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.2", "", { "os": "android", "cpu": "arm64" }, "sha512-5ZAX5xOmTligeBaeNEPnPaeEuah53Id2tX4c2CVP3JaROTH+j4fnfHCkr1PjXMd78hMst+TlkfKcW/DlTq0i4w=="], 76 | 77 | "@esbuild/android-x64": ["@esbuild/android-x64@0.25.2", "", { "os": "android", "cpu": "x64" }, "sha512-Ffcx+nnma8Sge4jzddPHCZVRvIfQ0kMsUsCMcJRHkGJ1cDmhe4SsrYIjLUKn1xpHZybmOqCWwB0zQvsjdEHtkg=="], 78 | 79 | "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-MpM6LUVTXAzOvN4KbjzU/q5smzryuoNjlriAIx+06RpecwCkL9JpenNzpKd2YMzLJFOdPqBpuub6eVRP5IgiSA=="], 80 | 81 | "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-5eRPrTX7wFyuWe8FqEFPG2cU0+butQQVNcT4sVipqjLYQjjh8a8+vUTfgBKM88ObB85ahsnTwF7PSIt6PG+QkA=="], 82 | 83 | "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-mLwm4vXKiQ2UTSX4+ImyiPdiHjiZhIaE9QvC7sw0tZ6HoNMjYAqQpGyui5VRIi5sGd+uWq940gdCbY3VLvsO1w=="], 84 | 85 | "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-6qyyn6TjayJSwGpm8J9QYYGQcRgc90nmfdUb0O7pp1s4lTY+9D0H9O02v5JqGApUyiHOtkz6+1hZNvNtEhbwRQ=="], 86 | 87 | "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.2", "", { "os": "linux", "cpu": "arm" }, "sha512-UHBRgJcmjJv5oeQF8EpTRZs/1knq6loLxTsjc3nxO9eXAPDLcWW55flrMVc97qFPbmZP31ta1AZVUKQzKTzb0g=="], 88 | 89 | "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-gq/sjLsOyMT19I8obBISvhoYiZIAaGF8JpeXu1u8yPv8BE5HlWYobmlsfijFIZ9hIVGYkbdFhEqC0NvM4kNO0g=="], 90 | 91 | "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.2", "", { "os": "linux", "cpu": "ia32" }, "sha512-bBYCv9obgW2cBP+2ZWfjYTU+f5cxRoGGQ5SeDbYdFCAZpYWrfjjfYwvUpP8MlKbP0nwZ5gyOU/0aUzZ5HWPuvQ=="], 92 | 93 | "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.2", "", { "os": "linux", "cpu": "none" }, "sha512-SHNGiKtvnU2dBlM5D8CXRFdd+6etgZ9dXfaPCeJtz+37PIUlixvlIhI23L5khKXs3DIzAn9V8v+qb1TRKrgT5w=="], 94 | 95 | "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.2", "", { "os": "linux", "cpu": "none" }, "sha512-hDDRlzE6rPeoj+5fsADqdUZl1OzqDYow4TB4Y/3PlKBD0ph1e6uPHzIQcv2Z65u2K0kpeByIyAjCmjn1hJgG0Q=="], 96 | 97 | "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-tsHu2RRSWzipmUi9UBDEzc0nLc4HtpZEI5Ba+Omms5456x5WaNuiG3u7xh5AO6sipnJ9r4cRWQB2tUjPyIkc6g=="], 98 | 99 | "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.2", "", { "os": "linux", "cpu": "none" }, "sha512-k4LtpgV7NJQOml/10uPU0s4SAXGnowi5qBSjaLWMojNCUICNu7TshqHLAEbkBdAszL5TabfvQ48kK84hyFzjnw=="], 100 | 101 | "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-GRa4IshOdvKY7M/rDpRR3gkiTNp34M0eLTaC1a08gNrh4u488aPhuZOCpkF6+2wl3zAN7L7XIpOFBhnaE3/Q8Q=="], 102 | 103 | "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.2", "", { "os": "linux", "cpu": "x64" }, "sha512-QInHERlqpTTZ4FRB0fROQWXcYRD64lAoiegezDunLpalZMjcUcld3YzZmVJ2H/Cp0wJRZ8Xtjtj0cEHhYc/uUg=="], 104 | 105 | "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.2", "", { "os": "none", "cpu": "arm64" }, "sha512-talAIBoY5M8vHc6EeI2WW9d/CkiO9MQJ0IOWX8hrLhxGbro/vBXJvaQXefW2cP0z0nQVTdQ/eNyGFV1GSKrxfw=="], 106 | 107 | "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.2", "", { "os": "none", "cpu": "x64" }, "sha512-voZT9Z+tpOxrvfKFyfDYPc4DO4rk06qamv1a/fkuzHpiVBMOhpjK+vBmWM8J1eiB3OLSMFYNaOaBNLXGChf5tg=="], 108 | 109 | "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-dcXYOC6NXOqcykeDlwId9kB6OkPUxOEqU+rkrYVqJbK2hagWOMrsTGsMr8+rW02M+d5Op5NNlgMmjzecaRf7Tg=="], 110 | 111 | "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-t/TkWwahkH0Tsgoq1Ju7QfgGhArkGLkF1uYz8nQS/PPFlXbP5YgRpqQR3ARRiC2iXoLTWFxc6DJMSK10dVXluw=="], 112 | 113 | "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-cfZH1co2+imVdWCjd+D1gf9NjkchVhhdpgb1q5y6Hcv9TP6Zi9ZG/beI3ig8TvwT9lH9dlxLq5MQBBgwuj4xvA=="], 114 | 115 | "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-7Loyjh+D/Nx/sOTzV8vfbB3GJuHdOQyrOryFdZvPHLf42Tk9ivBU5Aedi7iyX+x6rbn2Mh68T4qq1SDqJBQO5Q=="], 116 | 117 | "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-WRJgsz9un0nqZJ4MfhabxaD9Ft8KioqU3JMinOTvobbX6MOSUigSBlogP8QB3uxpJDsFS6yN+3FDBdqE5lg9kg=="], 118 | 119 | "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.2", "", { "os": "win32", "cpu": "x64" }, "sha512-kM3HKb16VIXZyIeVrM1ygYmZBKybX8N4p754bw390wGO3Tf2j4L2/WYL+4suWujpgf6GBYs3jv7TyUivdd05JA=="], 120 | 121 | "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.8", "", { "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA=="], 122 | 123 | "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], 124 | 125 | "@jridgewell/set-array": ["@jridgewell/set-array@1.2.1", "", {}, "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A=="], 126 | 127 | "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="], 128 | 129 | "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="], 130 | 131 | "@radix-ui/number": ["@radix-ui/number@1.1.1", "", {}, "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="], 132 | 133 | "@radix-ui/primitive": ["@radix-ui/primitive@1.1.2", "", {}, "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA=="], 134 | 135 | "@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="], 136 | 137 | "@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], 138 | 139 | "@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw=="], 140 | 141 | "@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-IrVLIhskYhH3nLvtcBLQFZr61tBG7wx7O3kEmdzcYwRGAEBmBicGGL7ATzNgruYJ3xBTbuzEEq9OXJM3PAX3tA=="], 142 | 143 | "@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.0.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Pf/t/GkndH7CQ8wE2hbkXA+WyZ83fhQQn5DDmwDiDo6AwN/fhaH8oqZ0jRjMrO2iaMhDi6P1HRx6AZwyMinY1g=="], 144 | 145 | "@radix-ui/react-scroll-area": ["@radix-ui/react-scroll-area@1.2.4", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-presence": "1.1.3", "@radix-ui/react-primitive": "2.0.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-G9rdWTQjOR4sk76HwSdROhPU0jZWpfozn9skU1v4N0/g9k7TmswrJn8W8WMU+aYktnLLpk5LX6fofj2bGe5NFQ=="], 146 | 147 | "@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w=="], 148 | 149 | "@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="], 150 | 151 | "@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="], 152 | 153 | "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.39.0", "", { "os": "android", "cpu": "arm" }, "sha512-lGVys55Qb00Wvh8DMAocp5kIcaNzEFTmGhfFd88LfaogYTRKrdxgtlO5H6S49v2Nd8R2C6wLOal0qv6/kCkOwA=="], 154 | 155 | "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.39.0", "", { "os": "android", "cpu": "arm64" }, "sha512-It9+M1zE31KWfqh/0cJLrrsCPiF72PoJjIChLX+rEcujVRCb4NLQ5QzFkzIZW8Kn8FTbvGQBY5TkKBau3S8cCQ=="], 156 | 157 | "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.39.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-lXQnhpFDOKDXiGxsU9/l8UEGGM65comrQuZ+lDcGUx+9YQ9dKpF3rSEGepyeR5AHZ0b5RgiligsBhWZfSSQh8Q=="], 158 | 159 | "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.39.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-mKXpNZLvtEbgu6WCkNij7CGycdw9cJi2k9v0noMb++Vab12GZjFgUXD69ilAbBh034Zwn95c2PNSz9xM7KYEAQ=="], 160 | 161 | "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.39.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-jivRRlh2Lod/KvDZx2zUR+I4iBfHcu2V/BA2vasUtdtTN2Uk3jfcZczLa81ESHZHPHy4ih3T/W5rPFZ/hX7RtQ=="], 162 | 163 | "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.39.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-8RXIWvYIRK9nO+bhVz8DwLBepcptw633gv/QT4015CpJ0Ht8punmoHU/DuEd3iw9Hr8UwUV+t+VNNuZIWYeY7Q=="], 164 | 165 | "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.39.0", "", { "os": "linux", "cpu": "arm" }, "sha512-mz5POx5Zu58f2xAG5RaRRhp3IZDK7zXGk5sdEDj4o96HeaXhlUwmLFzNlc4hCQi5sGdR12VDgEUqVSHer0lI9g=="], 166 | 167 | "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.39.0", "", { "os": "linux", "cpu": "arm" }, "sha512-+YDwhM6gUAyakl0CD+bMFpdmwIoRDzZYaTWV3SDRBGkMU/VpIBYXXEvkEcTagw/7VVkL2vA29zU4UVy1mP0/Yw=="], 168 | 169 | "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.39.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-EKf7iF7aK36eEChvlgxGnk7pdJfzfQbNvGV/+l98iiMwU23MwvmV0Ty3pJ0p5WQfm3JRHOytSIqD9LB7Bq7xdQ=="], 170 | 171 | "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.39.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-vYanR6MtqC7Z2SNr8gzVnzUul09Wi1kZqJaek3KcIlI/wq5Xtq4ZPIZ0Mr/st/sv/NnaPwy/D4yXg5x0B3aUUA=="], 172 | 173 | "@rollup/rollup-linux-loongarch64-gnu": ["@rollup/rollup-linux-loongarch64-gnu@4.39.0", "", { "os": "linux", "cpu": "none" }, "sha512-NMRUT40+h0FBa5fb+cpxtZoGAggRem16ocVKIv5gDB5uLDgBIwrIsXlGqYbLwW8YyO3WVTk1FkFDjMETYlDqiw=="], 174 | 175 | "@rollup/rollup-linux-powerpc64le-gnu": ["@rollup/rollup-linux-powerpc64le-gnu@4.39.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-0pCNnmxgduJ3YRt+D+kJ6Ai/r+TaePu9ZLENl+ZDV/CdVczXl95CbIiwwswu4L+K7uOIGf6tMo2vm8uadRaICQ=="], 176 | 177 | "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.39.0", "", { "os": "linux", "cpu": "none" }, "sha512-t7j5Zhr7S4bBtksT73bO6c3Qa2AV/HqiGlj9+KB3gNF5upcVkx+HLgxTm8DK4OkzsOYqbdqbLKwvGMhylJCPhQ=="], 178 | 179 | "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.39.0", "", { "os": "linux", "cpu": "none" }, "sha512-m6cwI86IvQ7M93MQ2RF5SP8tUjD39Y7rjb1qjHgYh28uAPVU8+k/xYWvxRO3/tBN2pZkSMa5RjnPuUIbrwVxeA=="], 180 | 181 | "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.39.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-iRDJd2ebMunnk2rsSBYlsptCyuINvxUfGwOUldjv5M4tpa93K8tFMeYGpNk2+Nxl+OBJnBzy2/JCscGeO507kA=="], 182 | 183 | "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.39.0", "", { "os": "linux", "cpu": "x64" }, "sha512-t9jqYw27R6Lx0XKfEFe5vUeEJ5pF3SGIM6gTfONSMb7DuG6z6wfj2yjcoZxHg129veTqU7+wOhY6GX8wmf90dA=="], 184 | 185 | "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.39.0", "", { "os": "linux", "cpu": "x64" }, "sha512-ThFdkrFDP55AIsIZDKSBWEt/JcWlCzydbZHinZ0F/r1h83qbGeenCt/G/wG2O0reuENDD2tawfAj2s8VK7Bugg=="], 186 | 187 | "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.39.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-jDrLm6yUtbOg2TYB3sBF3acUnAwsIksEYjLeHL+TJv9jg+TmTwdyjnDex27jqEMakNKf3RwwPahDIt7QXCSqRQ=="], 188 | 189 | "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.39.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-6w9uMuza+LbLCVoNKL5FSLE7yvYkq9laSd09bwS0tMjkwXrmib/4KmoJcrKhLWHvw19mwU+33ndC69T7weNNjQ=="], 190 | 191 | "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.39.0", "", { "os": "win32", "cpu": "x64" }, "sha512-yAkUOkIKZlK5dl7u6dg897doBgLXmUHhIINM2c+sND3DZwnrdQkkSiDh7N75Ll4mM4dxSkYfXqU9fW3lLkMFug=="], 192 | 193 | "@tailwindcss/node": ["@tailwindcss/node@4.1.3", "", { "dependencies": { "enhanced-resolve": "^5.18.1", "jiti": "^2.4.2", "lightningcss": "1.29.2", "tailwindcss": "4.1.3" } }, "sha512-H/6r6IPFJkCfBJZ2dKZiPJ7Ueb2wbL592+9bQEl2r73qbX6yGnmQVIfiUvDRB2YI0a3PWDrzUwkvQx1XW1bNkA=="], 194 | 195 | "@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.3", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.3", "@tailwindcss/oxide-darwin-arm64": "4.1.3", "@tailwindcss/oxide-darwin-x64": "4.1.3", "@tailwindcss/oxide-freebsd-x64": "4.1.3", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.3", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.3", "@tailwindcss/oxide-linux-arm64-musl": "4.1.3", "@tailwindcss/oxide-linux-x64-gnu": "4.1.3", "@tailwindcss/oxide-linux-x64-musl": "4.1.3", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.3", "@tailwindcss/oxide-win32-x64-msvc": "4.1.3" } }, "sha512-t16lpHCU7LBxDe/8dCj9ntyNpXaSTAgxWm1u2XQP5NiIu4KGSyrDJJRlK9hJ4U9yJxx0UKCVI67MJWFNll5mOQ=="], 196 | 197 | "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.3", "", { "os": "android", "cpu": "arm64" }, "sha512-cxklKjtNLwFl3mDYw4XpEfBY+G8ssSg9ADL4Wm6//5woi3XGqlxFsnV5Zb6v07dxw1NvEX2uoqsxO/zWQsgR+g=="], 198 | 199 | "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-mqkf2tLR5VCrjBvuRDwzKNShRu99gCAVMkVsaEOFvv6cCjlEKXRecPu9DEnxp6STk5z+Vlbh1M5zY3nQCXMXhw=="], 200 | 201 | "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-7sGraGaWzXvCLyxrc7d+CCpUN3fYnkkcso3rCzwUmo/LteAl2ZGCDlGvDD8Y/1D3ngxT8KgDj1DSwOnNewKhmg=="], 202 | 203 | "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-E2+PbcbzIReaAYZe997wb9rId246yDkCwAakllAWSGqe6VTg9hHle67hfH6ExjpV2LSK/siRzBUs5wVff3RW9w=="], 204 | 205 | "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.3", "", { "os": "linux", "cpu": "arm" }, "sha512-GvfbJ8wjSSjbLFFE3UYz4Eh8i4L6GiEYqCtA8j2Zd2oXriPuom/Ah/64pg/szWycQpzRnbDiJozoxFU2oJZyfg=="], 206 | 207 | "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-35UkuCWQTeG9BHcBQXndDOrpsnt3Pj9NVIB4CgNiKmpG8GnCNXeMczkUpOoqcOhO6Cc/mM2W7kaQ/MTEENDDXg=="], 208 | 209 | "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-dm18aQiML5QCj9DQo7wMbt1Z2tl3Giht54uVR87a84X8qRtuXxUqnKQkRDK5B4bCOmcZ580lF9YcoMkbDYTXHQ=="], 210 | 211 | "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.3", "", { "os": "linux", "cpu": "x64" }, "sha512-LMdTmGe/NPtGOaOfV2HuO7w07jI3cflPrVq5CXl+2O93DCewADK0uW1ORNAcfu2YxDUS035eY2W38TxrsqngxA=="], 212 | 213 | "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.3", "", { "os": "linux", "cpu": "x64" }, "sha512-aalNWwIi54bbFEizwl1/XpmdDrOaCjRFQRgtbv9slWjmNPuJJTIKPHf5/XXDARc9CneW9FkSTqTbyvNecYAEGw=="], 214 | 215 | "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-PEj7XR4OGTGoboTIAdXicKuWl4EQIjKHKuR+bFy9oYN7CFZo0eu74+70O4XuERX4yjqVZGAkCdglBODlgqcCXg=="], 216 | 217 | "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.3", "", { "os": "win32", "cpu": "x64" }, "sha512-T8gfxECWDBENotpw3HR9SmNiHC9AOJdxs+woasRZ8Q/J4VHN0OMs7F+4yVNZ9EVN26Wv6mZbK0jv7eHYuLJLwA=="], 218 | 219 | "@tailwindcss/vite": ["@tailwindcss/vite@4.1.3", "", { "dependencies": { "@tailwindcss/node": "4.1.3", "@tailwindcss/oxide": "4.1.3", "tailwindcss": "4.1.3" }, "peerDependencies": { "vite": "^5.2.0 || ^6" } }, "sha512-lUI/QaDxLtlV52Lho6pu07CG9pSnRYLOPmKGIQjyHdTBagemc6HmgZxyjGAQ/5HMPrNeWBfTVIpQl0/jLXvWHQ=="], 220 | 221 | "@tauri-apps/api": ["@tauri-apps/api@2.4.1", "", {}, "sha512-5sYwZCSJb6PBGbBL4kt7CnE5HHbBqwH+ovmOW6ZVju3nX4E3JX6tt2kRklFEH7xMOIwR0btRkZktuLhKvyEQYg=="], 222 | 223 | "@tauri-apps/cli": ["@tauri-apps/cli@2.4.1", "", { "optionalDependencies": { "@tauri-apps/cli-darwin-arm64": "2.4.1", "@tauri-apps/cli-darwin-x64": "2.4.1", "@tauri-apps/cli-linux-arm-gnueabihf": "2.4.1", "@tauri-apps/cli-linux-arm64-gnu": "2.4.1", "@tauri-apps/cli-linux-arm64-musl": "2.4.1", "@tauri-apps/cli-linux-riscv64-gnu": "2.4.1", "@tauri-apps/cli-linux-x64-gnu": "2.4.1", "@tauri-apps/cli-linux-x64-musl": "2.4.1", "@tauri-apps/cli-win32-arm64-msvc": "2.4.1", "@tauri-apps/cli-win32-ia32-msvc": "2.4.1", "@tauri-apps/cli-win32-x64-msvc": "2.4.1" }, "bin": { "tauri": "tauri.js" } }, "sha512-9Ta81jx9+57FhtU/mPIckDcOBtPTUdKM75t4+aA0X84b8Sclb0jy1xA8NplmcRzp2fsfIHNngU2NiRxsW5+yOQ=="], 224 | 225 | "@tauri-apps/cli-darwin-arm64": ["@tauri-apps/cli-darwin-arm64@2.4.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-QME7s8XQwy3LWClTVlIlwXVSLKkeJ/z88pr917Mtn9spYOjnBfsgHAgGdmpWD3NfJxjg7CtLbhH49DxoFL+hLg=="], 226 | 227 | "@tauri-apps/cli-darwin-x64": ["@tauri-apps/cli-darwin-x64@2.4.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-/r89IcW6Ya1sEsFUEH7wLNruDTj7WmDWKGpPy7gATFtQr5JEY4heernqE82isjTUimnHZD8SCr0jA3NceI4ybw=="], 228 | 229 | "@tauri-apps/cli-linux-arm-gnueabihf": ["@tauri-apps/cli-linux-arm-gnueabihf@2.4.1", "", { "os": "linux", "cpu": "arm" }, "sha512-9tDijkRB+CchAGjXxYdY9l/XzFpLp1yihUtGXJz9eh+3qIoRI043n3e+6xmU8ZURr7XPnu+R4sCmXs6HD+NCEQ=="], 230 | 231 | "@tauri-apps/cli-linux-arm64-gnu": ["@tauri-apps/cli-linux-arm64-gnu@2.4.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-pnFGDEXBAzS4iDYAVxTRhAzNu3K2XPGflYyBc0czfHDBXopqRgMyj5Q9Wj7HAwv6cM8BqzXINxnb2ZJFGmbSgA=="], 232 | 233 | "@tauri-apps/cli-linux-arm64-musl": ["@tauri-apps/cli-linux-arm64-musl@2.4.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-Hp0zXgeZNKmT+eoJSCxSBUm2QndNuRxR55tmIeNm3vbyUMJN/49uW7nurZ5fBPsacN4Pzwlx1dIMK+Gnr9A69w=="], 234 | 235 | "@tauri-apps/cli-linux-riscv64-gnu": ["@tauri-apps/cli-linux-riscv64-gnu@2.4.1", "", { "os": "linux", "cpu": "none" }, "sha512-3T3bo2E4fdYRvzcXheWUeQOVB+LunEEi92iPRgOyuSVexVE4cmHYl+MPJF+EUV28Et0hIVTsHibmDO0/04lAFg=="], 236 | 237 | "@tauri-apps/cli-linux-x64-gnu": ["@tauri-apps/cli-linux-x64-gnu@2.4.1", "", { "os": "linux", "cpu": "x64" }, "sha512-kLN0FdNONO+2i+OpU9+mm6oTGufRC00e197TtwjpC0N6K2K8130w7Q3FeODIM2CMyg0ov3tH+QWqKW7GNhHFzg=="], 238 | 239 | "@tauri-apps/cli-linux-x64-musl": ["@tauri-apps/cli-linux-x64-musl@2.4.1", "", { "os": "linux", "cpu": "x64" }, "sha512-a8exvA5Ub9eg66a6hsMQKJIkf63QAf9OdiuFKOsEnKZkNN2x0NLgfvEcqdw88VY0UMs9dBoZ1AGbWMeYnLrLwQ=="], 240 | 241 | "@tauri-apps/cli-win32-arm64-msvc": ["@tauri-apps/cli-win32-arm64-msvc@2.4.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-4JFrslsMCJQG1c573T9uqQSAbF3j/tMKkMWzsIssv8jvPiP++OG61A2/F+y9te9/Q/O95cKhDK63kaiO5xQaeg=="], 242 | 243 | "@tauri-apps/cli-win32-ia32-msvc": ["@tauri-apps/cli-win32-ia32-msvc@2.4.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-9eXfFORehYSCRwxg2KodfmX/mhr50CI7wyBYGbPLePCjr5z0jK/9IyW6r0tC+ZVjwpX48dkk7hKiUgI25jHjzA=="], 244 | 245 | "@tauri-apps/cli-win32-x64-msvc": ["@tauri-apps/cli-win32-x64-msvc@2.4.1", "", { "os": "win32", "cpu": "x64" }, "sha512-60a4Ov7Jrwqz2hzDltlS7301dhSAmM9dxo+IRBD3xz7yobKrgaHXYpWvnRomYItHcDd51VaKc9292H8/eE/gsw=="], 246 | 247 | "@tauri-apps/plugin-opener": ["@tauri-apps/plugin-opener@2.2.6", "", { "dependencies": { "@tauri-apps/api": "^2.0.0" } }, "sha512-bSdkuP71ZQRepPOn8BOEdBKYJQvl6+jb160QtJX/i2H9BF6ZySY/kYljh76N2Ne5fJMQRge7rlKoStYQY5Jq1w=="], 248 | 249 | "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], 250 | 251 | "@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="], 252 | 253 | "@types/babel__template": ["@types/babel__template@7.4.4", "", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A=="], 254 | 255 | "@types/babel__traverse": ["@types/babel__traverse@7.20.7", "", { "dependencies": { "@babel/types": "^7.20.7" } }, "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng=="], 256 | 257 | "@types/estree": ["@types/estree@1.0.7", "", {}, "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="], 258 | 259 | "@types/node": ["@types/node@22.14.0", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-Kmpl+z84ILoG+3T/zQFyAJsU6EPTmOCj8/2+83fSN6djd6I4o7uOuGIH6vq3PrjY5BGitSbFuMN18j3iknubbA=="], 260 | 261 | "@types/prop-types": ["@types/prop-types@15.7.14", "", {}, "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ=="], 262 | 263 | "@types/react": ["@types/react@18.3.20", "", { "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" } }, "sha512-IPaCZN7PShZK/3t6Q87pfTkRm6oLTd4vztyoj+cbHUF1g3FfVb2tFIL79uCRKEfv16AhqDMBywP2VW3KIZUvcg=="], 264 | 265 | "@types/react-dom": ["@types/react-dom@18.3.6", "", { "peerDependencies": { "@types/react": "^18.0.0" } }, "sha512-nf22//wEbKXusP6E9pfOCDwFdHAX4u172eaJI4YkDRQEZiorm6KfYnSC2SWLDMVWUOWPERmJnN0ujeAfTBLvrw=="], 266 | 267 | "@vitejs/plugin-react": ["@vitejs/plugin-react@4.3.4", "", { "dependencies": { "@babel/core": "^7.26.0", "@babel/plugin-transform-react-jsx-self": "^7.25.9", "@babel/plugin-transform-react-jsx-source": "^7.25.9", "@types/babel__core": "^7.20.5", "react-refresh": "^0.14.2" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0" } }, "sha512-SCCPBJtYLdE8PX/7ZQAs1QAZ8Jqwih+0VBLum1EGqmCCQal+MIUqLCzj3ZUy8ufbC0cAM4LRlSTm7IQJwWT4ug=="], 268 | 269 | "browserslist": ["browserslist@4.24.4", "", { "dependencies": { "caniuse-lite": "^1.0.30001688", "electron-to-chromium": "^1.5.73", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.1" }, "bin": { "browserslist": "cli.js" } }, "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A=="], 270 | 271 | "caniuse-lite": ["caniuse-lite@1.0.30001709", "", {}, "sha512-NgL3vUTnDrPCZ3zTahp4fsugQ4dc7EKTSzwQDPEel6DMoMnfH2jhry9n2Zm8onbSR+f/QtKHFOA+iAQu4kbtWA=="], 272 | 273 | "class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="], 274 | 275 | "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], 276 | 277 | "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], 278 | 279 | "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], 280 | 281 | "debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], 282 | 283 | "detect-libc": ["detect-libc@2.0.3", "", {}, "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw=="], 284 | 285 | "electron-to-chromium": ["electron-to-chromium@1.5.130", "", {}, "sha512-Ou2u7L9j2XLZbhqzyX0jWDj6gA8D3jIfVzt4rikLf3cGBa0VdReuFimBKS9tQJA4+XpeCxj1NoWlfBXzbMa9IA=="], 286 | 287 | "enhanced-resolve": ["enhanced-resolve@5.18.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg=="], 288 | 289 | "esbuild": ["esbuild@0.25.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.2", "@esbuild/android-arm": "0.25.2", "@esbuild/android-arm64": "0.25.2", "@esbuild/android-x64": "0.25.2", "@esbuild/darwin-arm64": "0.25.2", "@esbuild/darwin-x64": "0.25.2", "@esbuild/freebsd-arm64": "0.25.2", "@esbuild/freebsd-x64": "0.25.2", "@esbuild/linux-arm": "0.25.2", "@esbuild/linux-arm64": "0.25.2", "@esbuild/linux-ia32": "0.25.2", "@esbuild/linux-loong64": "0.25.2", "@esbuild/linux-mips64el": "0.25.2", "@esbuild/linux-ppc64": "0.25.2", "@esbuild/linux-riscv64": "0.25.2", "@esbuild/linux-s390x": "0.25.2", "@esbuild/linux-x64": "0.25.2", "@esbuild/netbsd-arm64": "0.25.2", "@esbuild/netbsd-x64": "0.25.2", "@esbuild/openbsd-arm64": "0.25.2", "@esbuild/openbsd-x64": "0.25.2", "@esbuild/sunos-x64": "0.25.2", "@esbuild/win32-arm64": "0.25.2", "@esbuild/win32-ia32": "0.25.2", "@esbuild/win32-x64": "0.25.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ=="], 290 | 291 | "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], 292 | 293 | "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], 294 | 295 | "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], 296 | 297 | "globals": ["globals@11.12.0", "", {}, "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="], 298 | 299 | "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], 300 | 301 | "jiti": ["jiti@2.4.2", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A=="], 302 | 303 | "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], 304 | 305 | "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], 306 | 307 | "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], 308 | 309 | "lightningcss": ["lightningcss@1.29.2", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-darwin-arm64": "1.29.2", "lightningcss-darwin-x64": "1.29.2", "lightningcss-freebsd-x64": "1.29.2", "lightningcss-linux-arm-gnueabihf": "1.29.2", "lightningcss-linux-arm64-gnu": "1.29.2", "lightningcss-linux-arm64-musl": "1.29.2", "lightningcss-linux-x64-gnu": "1.29.2", "lightningcss-linux-x64-musl": "1.29.2", "lightningcss-win32-arm64-msvc": "1.29.2", "lightningcss-win32-x64-msvc": "1.29.2" } }, "sha512-6b6gd/RUXKaw5keVdSEtqFVdzWnU5jMxTUjA2bVcMNPLwSQ08Sv/UodBVtETLCn7k4S1Ibxwh7k68IwLZPgKaA=="], 310 | 311 | "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.29.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-cK/eMabSViKn/PG8U/a7aCorpeKLMlK0bQeNHmdb7qUnBkNPnL+oV5DjJUo0kqWsJUapZsM4jCfYItbqBDvlcA=="], 312 | 313 | "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.29.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-j5qYxamyQw4kDXX5hnnCKMf3mLlHvG44f24Qyi2965/Ycz829MYqjrVg2H8BidybHBp9kom4D7DR5VqCKDXS0w=="], 314 | 315 | "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.29.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-wDk7M2tM78Ii8ek9YjnY8MjV5f5JN2qNVO+/0BAGZRvXKtQrBC4/cn4ssQIpKIPP44YXw6gFdpUF+Ps+RGsCwg=="], 316 | 317 | "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.29.2", "", { "os": "linux", "cpu": "arm" }, "sha512-IRUrOrAF2Z+KExdExe3Rz7NSTuuJ2HvCGlMKoquK5pjvo2JY4Rybr+NrKnq0U0hZnx5AnGsuFHjGnNT14w26sg=="], 318 | 319 | "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.29.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-KKCpOlmhdjvUTX/mBuaKemp0oeDIBBLFiU5Fnqxh1/DZ4JPZi4evEH7TKoSBFOSOV3J7iEmmBaw/8dpiUvRKlQ=="], 320 | 321 | "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.29.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-Q64eM1bPlOOUgxFmoPUefqzY1yV3ctFPE6d/Vt7WzLW4rKTv7MyYNky+FWxRpLkNASTnKQUaiMJ87zNODIrrKQ=="], 322 | 323 | "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.29.2", "", { "os": "linux", "cpu": "x64" }, "sha512-0v6idDCPG6epLXtBH/RPkHvYx74CVziHo6TMYga8O2EiQApnUPZsbR9nFNrg2cgBzk1AYqEd95TlrsL7nYABQg=="], 324 | 325 | "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.29.2", "", { "os": "linux", "cpu": "x64" }, "sha512-rMpz2yawkgGT8RULc5S4WiZopVMOFWjiItBT7aSfDX4NQav6M44rhn5hjtkKzB+wMTRlLLqxkeYEtQ3dd9696w=="], 326 | 327 | "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.29.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-nL7zRW6evGQqYVu/bKGK+zShyz8OVzsCotFgc7judbt6wnB2KbiKKJwBE4SGoDBQ1O94RjW4asrCjQL4i8Fhbw=="], 328 | 329 | "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.29.2", "", { "os": "win32", "cpu": "x64" }, "sha512-EdIUW3B2vLuHmv7urfzMI/h2fmlnOQBk1xlsDxkN1tCWKjNFjfLhGxYk8C8mzpSfr+A6jFFIi8fU6LbQGsRWjA=="], 330 | 331 | "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], 332 | 333 | "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], 334 | 335 | "lucide-react": ["lucide-react@0.487.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-aKqhOQ+YmFnwq8dWgGjOuLc8V1R9/c/yOd+zDY4+ohsR2Jo05lSGc3WsstYPIzcTpeosN7LoCkLReUUITvaIvw=="], 336 | 337 | "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], 338 | 339 | "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], 340 | 341 | "node-releases": ["node-releases@2.0.19", "", {}, "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw=="], 342 | 343 | "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], 344 | 345 | "postcss": ["postcss@8.5.3", "", { "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A=="], 346 | 347 | "react": ["react@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ=="], 348 | 349 | "react-dom": ["react-dom@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" }, "peerDependencies": { "react": "^18.3.1" } }, "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw=="], 350 | 351 | "react-refresh": ["react-refresh@0.14.2", "", {}, "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA=="], 352 | 353 | "rollup": ["rollup@4.39.0", "", { "dependencies": { "@types/estree": "1.0.7" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.39.0", "@rollup/rollup-android-arm64": "4.39.0", "@rollup/rollup-darwin-arm64": "4.39.0", "@rollup/rollup-darwin-x64": "4.39.0", "@rollup/rollup-freebsd-arm64": "4.39.0", "@rollup/rollup-freebsd-x64": "4.39.0", "@rollup/rollup-linux-arm-gnueabihf": "4.39.0", "@rollup/rollup-linux-arm-musleabihf": "4.39.0", "@rollup/rollup-linux-arm64-gnu": "4.39.0", "@rollup/rollup-linux-arm64-musl": "4.39.0", "@rollup/rollup-linux-loongarch64-gnu": "4.39.0", "@rollup/rollup-linux-powerpc64le-gnu": "4.39.0", "@rollup/rollup-linux-riscv64-gnu": "4.39.0", "@rollup/rollup-linux-riscv64-musl": "4.39.0", "@rollup/rollup-linux-s390x-gnu": "4.39.0", "@rollup/rollup-linux-x64-gnu": "4.39.0", "@rollup/rollup-linux-x64-musl": "4.39.0", "@rollup/rollup-win32-arm64-msvc": "4.39.0", "@rollup/rollup-win32-ia32-msvc": "4.39.0", "@rollup/rollup-win32-x64-msvc": "4.39.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-thI8kNc02yNvnmJp8dr3fNWJ9tCONDhp6TV35X6HkKGGs9E6q7YWCHbe5vKiTa7TAiNcFEmXKj3X/pG2b3ci0g=="], 354 | 355 | "scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="], 356 | 357 | "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], 358 | 359 | "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], 360 | 361 | "tailwind-merge": ["tailwind-merge@3.2.0", "", {}, "sha512-FQT/OVqCD+7edmmJpsgCsY820RTD5AkBryuG5IUqR5YQZSdj5xlH5nLgH7YPths7WsLPSpSBNneJdM8aS8aeFA=="], 362 | 363 | "tailwindcss": ["tailwindcss@4.1.3", "", {}, "sha512-2Q+rw9vy1WFXu5cIxlvsabCwhU2qUwodGq03ODhLJ0jW4ek5BUtoCsnLB0qG+m8AHgEsSJcJGDSDe06FXlP74g=="], 364 | 365 | "tapable": ["tapable@2.2.1", "", {}, "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ=="], 366 | 367 | "tw-animate-css": ["tw-animate-css@1.2.5", "", {}, "sha512-ABzjfgVo+fDbhRREGL4KQZUqqdPgvc5zVrLyeW9/6mVqvaDepXc7EvedA+pYmMnIOsUAQMwcWzNvom26J2qYvQ=="], 368 | 369 | "typescript": ["typescript@5.6.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw=="], 370 | 371 | "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], 372 | 373 | "update-browserslist-db": ["update-browserslist-db@1.1.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw=="], 374 | 375 | "vite": ["vite@6.2.5", "", { "dependencies": { "esbuild": "^0.25.0", "postcss": "^8.5.3", "rollup": "^4.30.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" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-j023J/hCAa4pRIUH6J9HemwYfjB5llR2Ps0CWeikOtdR8+pAURAk0DoJC5/mm9kd+UgdnIy7d6HE4EAvlYhPhA=="], 376 | 377 | "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], 378 | } 379 | } 380 | -------------------------------------------------------------------------------- /ui-events-app/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "", 8 | "css": "src/App.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } -------------------------------------------------------------------------------- /ui-events-app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Tauri + React + Typescript 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /ui-events-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ui-events-app", 3 | "private": true, 4 | "version": "0.1.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "preview": "vite preview", 10 | "tauri": "tauri" 11 | }, 12 | "dependencies": { 13 | "@radix-ui/react-scroll-area": "^1.2.4", 14 | "@radix-ui/react-slot": "^1.2.0", 15 | "@tailwindcss/vite": "^4.1.3", 16 | "@tauri-apps/api": "^2", 17 | "@tauri-apps/plugin-opener": "^2", 18 | "class-variance-authority": "^0.7.1", 19 | "clsx": "^2.1.1", 20 | "lucide-react": "^0.487.0", 21 | "react": "^18.3.1", 22 | "react-dom": "^18.3.1", 23 | "tailwind-merge": "^3.2.0", 24 | "tailwindcss": "^4.1.3", 25 | "tw-animate-css": "^1.2.5" 26 | }, 27 | "devDependencies": { 28 | "@tauri-apps/cli": "^2", 29 | "@types/node": "^22.14.0", 30 | "@types/react": "^18.3.1", 31 | "@types/react-dom": "^18.3.1", 32 | "@vitejs/plugin-react": "^4.3.4", 33 | "typescript": "~5.6.2", 34 | "vite": "^6.0.3" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /ui-events-app/public/tauri.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /ui-events-app/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui-events-app/src-tauri/.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # Generated by Tauri 6 | # will have schema files for capabilities auto-completion 7 | /gen/schemas 8 | -------------------------------------------------------------------------------- /ui-events-app/src-tauri/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ui-events-app" 3 | version = "0.1.0" 4 | description = "A Tauri App" 5 | authors = ["you"] 6 | edition = "2021" 7 | 8 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 9 | 10 | [lib] 11 | # The `_lib` suffix may seem redundant but it is necessary 12 | # to make the lib name unique and wouldn't conflict with the bin name. 13 | # This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519 14 | name = "ui_events_app_lib" 15 | crate-type = ["staticlib", "cdylib", "rlib"] 16 | 17 | [build-dependencies] 18 | tauri-build = { version = "2", features = [] } 19 | 20 | [dependencies] 21 | tauri = { version = "2", features = [] } 22 | tauri-plugin-opener = "2" 23 | serde = { version = "1", features = ["derive"] } 24 | serde_json = "1" 25 | 26 | ui-events = { path = "../../ui-events" } 27 | # ui-events = { git = "https://github.com/mediar-ai/ui-events.git", branch = "main" } 28 | tokio = { version = "1.44.1", features = ["full"] } 29 | anyhow = "1.0" 30 | tracing = "0.1.41" 31 | tracing-subscriber = "0.3.19" 32 | futures-util = "0.3" 33 | 34 | -------------------------------------------------------------------------------- /ui-events-app/src-tauri/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | tauri_build::build() 3 | } 4 | -------------------------------------------------------------------------------- /ui-events-app/src-tauri/capabilities/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../gen/schemas/desktop-schema.json", 3 | "identifier": "default", 4 | "description": "Capability for the main window", 5 | "windows": ["main"], 6 | "permissions": [ 7 | "core:default", 8 | "opener:default" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /ui-events-app/src-tauri/icons/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mediar-ai/ui-events/b01cd16c07b90e29196429ed78833e43ef6e151b/ui-events-app/src-tauri/icons/128x128.png -------------------------------------------------------------------------------- /ui-events-app/src-tauri/icons/128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mediar-ai/ui-events/b01cd16c07b90e29196429ed78833e43ef6e151b/ui-events-app/src-tauri/icons/128x128@2x.png -------------------------------------------------------------------------------- /ui-events-app/src-tauri/icons/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mediar-ai/ui-events/b01cd16c07b90e29196429ed78833e43ef6e151b/ui-events-app/src-tauri/icons/32x32.png -------------------------------------------------------------------------------- /ui-events-app/src-tauri/icons/Square107x107Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mediar-ai/ui-events/b01cd16c07b90e29196429ed78833e43ef6e151b/ui-events-app/src-tauri/icons/Square107x107Logo.png -------------------------------------------------------------------------------- /ui-events-app/src-tauri/icons/Square142x142Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mediar-ai/ui-events/b01cd16c07b90e29196429ed78833e43ef6e151b/ui-events-app/src-tauri/icons/Square142x142Logo.png -------------------------------------------------------------------------------- /ui-events-app/src-tauri/icons/Square150x150Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mediar-ai/ui-events/b01cd16c07b90e29196429ed78833e43ef6e151b/ui-events-app/src-tauri/icons/Square150x150Logo.png -------------------------------------------------------------------------------- /ui-events-app/src-tauri/icons/Square284x284Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mediar-ai/ui-events/b01cd16c07b90e29196429ed78833e43ef6e151b/ui-events-app/src-tauri/icons/Square284x284Logo.png -------------------------------------------------------------------------------- /ui-events-app/src-tauri/icons/Square30x30Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mediar-ai/ui-events/b01cd16c07b90e29196429ed78833e43ef6e151b/ui-events-app/src-tauri/icons/Square30x30Logo.png -------------------------------------------------------------------------------- /ui-events-app/src-tauri/icons/Square310x310Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mediar-ai/ui-events/b01cd16c07b90e29196429ed78833e43ef6e151b/ui-events-app/src-tauri/icons/Square310x310Logo.png -------------------------------------------------------------------------------- /ui-events-app/src-tauri/icons/Square44x44Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mediar-ai/ui-events/b01cd16c07b90e29196429ed78833e43ef6e151b/ui-events-app/src-tauri/icons/Square44x44Logo.png -------------------------------------------------------------------------------- /ui-events-app/src-tauri/icons/Square71x71Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mediar-ai/ui-events/b01cd16c07b90e29196429ed78833e43ef6e151b/ui-events-app/src-tauri/icons/Square71x71Logo.png -------------------------------------------------------------------------------- /ui-events-app/src-tauri/icons/Square89x89Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mediar-ai/ui-events/b01cd16c07b90e29196429ed78833e43ef6e151b/ui-events-app/src-tauri/icons/Square89x89Logo.png -------------------------------------------------------------------------------- /ui-events-app/src-tauri/icons/StoreLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mediar-ai/ui-events/b01cd16c07b90e29196429ed78833e43ef6e151b/ui-events-app/src-tauri/icons/StoreLogo.png -------------------------------------------------------------------------------- /ui-events-app/src-tauri/icons/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mediar-ai/ui-events/b01cd16c07b90e29196429ed78833e43ef6e151b/ui-events-app/src-tauri/icons/icon.icns -------------------------------------------------------------------------------- /ui-events-app/src-tauri/icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mediar-ai/ui-events/b01cd16c07b90e29196429ed78833e43ef6e151b/ui-events-app/src-tauri/icons/icon.ico -------------------------------------------------------------------------------- /ui-events-app/src-tauri/icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mediar-ai/ui-events/b01cd16c07b90e29196429ed78833e43ef6e151b/ui-events-app/src-tauri/icons/icon.png -------------------------------------------------------------------------------- /ui-events-app/src-tauri/src/lib.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use std::thread; 3 | use tokio::sync::mpsc; 4 | use tracing::info; 5 | use ui_events::{platform::listener_run, run_server}; // Import necessary components // Import thread 6 | 7 | // Learn more about Tauri commands at https://tauri.app/develop/calling-rust/ 8 | #[tauri::command] 9 | fn greet(name: &str) -> String { 10 | format!("Hello, {}! You've been greeted from Rust!", name) 11 | } 12 | 13 | #[cfg_attr(mobile, tauri::mobile_entry_point)] 14 | pub async fn run() -> Result<()> { 15 | tracing_subscriber::fmt::init(); 16 | info!("starting ui-events..."); 17 | 18 | // Create a channel for communication between listener and server 19 | let (tx, rx) = mpsc::channel(100); // Buffer size 100 20 | 21 | // Spawn the server task using Tauri's async runtime 22 | tauri::async_runtime::spawn(async move { 23 | if let Err(e) = run_server(9001, rx).await { 24 | tracing::error!("ui-events server failed: {}", e); 25 | } 26 | }); 27 | 28 | // Spawn the listener task on a separate thread 29 | // This assumes listener_run does not strictly require the main thread 30 | let listener_tx = tx.clone(); 31 | thread::spawn(move || { 32 | info!("starting ui-events listener thread..."); 33 | listener_run(listener_tx); // This might block this thread 34 | info!("ui-events listener thread finished."); // May not be reached if listener_run loops indefinitely 35 | }); 36 | 37 | info!("starting tauri application..."); 38 | // Run the Tauri application event loop (this blocks the main thread) 39 | tauri::Builder::default() 40 | .plugin(tauri_plugin_opener::init()) 41 | .invoke_handler(tauri::generate_handler![greet]) 42 | .run(tauri::generate_context!()) 43 | .expect("error while running tauri application"); 44 | 45 | Ok(()) 46 | } 47 | -------------------------------------------------------------------------------- /ui-events-app/src-tauri/src/main.rs: -------------------------------------------------------------------------------- 1 | // Prevents additional console window on Windows in release, DO NOT REMOVE!! 2 | #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] 3 | 4 | use anyhow::Result; 5 | 6 | #[tokio::main] 7 | async fn main() -> Result<()> { 8 | ui_events_app_lib::run().await 9 | } 10 | -------------------------------------------------------------------------------- /ui-events-app/src-tauri/tauri.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.tauri.app/config/2", 3 | "productName": "ui-events-app", 4 | "version": "0.1.0", 5 | "identifier": "ai.mediar.ui.events.app", 6 | "build": { 7 | "beforeDevCommand": "bun run dev", 8 | "devUrl": "http://localhost:1420", 9 | "beforeBuildCommand": "bun run build", 10 | "frontendDist": "../dist" 11 | }, 12 | "app": { 13 | "windows": [ 14 | { 15 | "title": "ui-events-app", 16 | "width": 800, 17 | "height": 600 18 | } 19 | ], 20 | "security": { 21 | "csp": null 22 | } 23 | }, 24 | "bundle": { 25 | "active": true, 26 | "targets": "all", 27 | "icon": [ 28 | "icons/32x32.png", 29 | "icons/128x128.png", 30 | "icons/128x128@2x.png", 31 | "icons/icon.icns", 32 | "icons/icon.ico" 33 | ] 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /ui-events-app/src/App.css: -------------------------------------------------------------------------------- 1 | .logo.vite:hover { 2 | filter: drop-shadow(0 0 2em #747bff); 3 | } 4 | 5 | .logo.react:hover { 6 | filter: drop-shadow(0 0 2em #61dafb); 7 | } 8 | :root { 9 | font-family: Inter, Avenir, Helvetica, Arial, sans-serif; 10 | font-size: 16px; 11 | line-height: 24px; 12 | font-weight: 400; 13 | 14 | color: #0f0f0f; 15 | background-color: #f6f6f6; 16 | 17 | font-synthesis: none; 18 | text-rendering: optimizeLegibility; 19 | -webkit-font-smoothing: antialiased; 20 | -moz-osx-font-smoothing: grayscale; 21 | -webkit-text-size-adjust: 100%; 22 | --radius: 0.625rem; 23 | --background: oklch(1 0 0); 24 | --foreground: oklch(0.145 0 0); 25 | --card: oklch(1 0 0); 26 | --card-foreground: oklch(0.145 0 0); 27 | --popover: oklch(1 0 0); 28 | --popover-foreground: oklch(0.145 0 0); 29 | --primary: oklch(0.205 0 0); 30 | --primary-foreground: oklch(0.985 0 0); 31 | --secondary: oklch(0.97 0 0); 32 | --secondary-foreground: oklch(0.205 0 0); 33 | --muted: oklch(0.97 0 0); 34 | --muted-foreground: oklch(0.556 0 0); 35 | --accent: oklch(0.97 0 0); 36 | --accent-foreground: oklch(0.205 0 0); 37 | --destructive: oklch(0.577 0.245 27.325); 38 | --border: oklch(0.922 0 0); 39 | --input: oklch(0.922 0 0); 40 | --ring: oklch(0.708 0 0); 41 | --chart-1: oklch(0.646 0.222 41.116); 42 | --chart-2: oklch(0.6 0.118 184.704); 43 | --chart-3: oklch(0.398 0.07 227.392); 44 | --chart-4: oklch(0.828 0.189 84.429); 45 | --chart-5: oklch(0.769 0.188 70.08); 46 | --sidebar: oklch(0.985 0 0); 47 | --sidebar-foreground: oklch(0.145 0 0); 48 | --sidebar-primary: oklch(0.205 0 0); 49 | --sidebar-primary-foreground: oklch(0.985 0 0); 50 | --sidebar-accent: oklch(0.97 0 0); 51 | --sidebar-accent-foreground: oklch(0.205 0 0); 52 | --sidebar-border: oklch(0.922 0 0); 53 | --sidebar-ring: oklch(0.708 0 0); 54 | } 55 | 56 | .container { 57 | margin: 0; 58 | padding-top: 10vh; 59 | display: flex; 60 | flex-direction: column; 61 | justify-content: center; 62 | text-align: center; 63 | } 64 | 65 | .logo { 66 | height: 6em; 67 | padding: 1.5em; 68 | will-change: filter; 69 | transition: 0.75s; 70 | } 71 | 72 | .logo.tauri:hover { 73 | filter: drop-shadow(0 0 2em #24c8db); 74 | } 75 | 76 | .row { 77 | display: flex; 78 | justify-content: center; 79 | } 80 | 81 | a { 82 | font-weight: 500; 83 | color: #646cff; 84 | text-decoration: inherit; 85 | } 86 | 87 | a:hover { 88 | color: #535bf2; 89 | } 90 | 91 | h1 { 92 | text-align: center; 93 | } 94 | 95 | input, 96 | button { 97 | border-radius: 8px; 98 | border: 1px solid transparent; 99 | padding: 0.6em 1.2em; 100 | font-size: 1em; 101 | font-weight: 500; 102 | font-family: inherit; 103 | color: #0f0f0f; 104 | background-color: #ffffff; 105 | transition: border-color 0.25s; 106 | box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2); 107 | } 108 | 109 | button { 110 | cursor: pointer; 111 | } 112 | 113 | button:hover { 114 | border-color: #396cd8; 115 | } 116 | button:active { 117 | border-color: #396cd8; 118 | background-color: #e8e8e8; 119 | } 120 | 121 | input, 122 | button { 123 | outline: none; 124 | } 125 | 126 | #greet-input { 127 | margin-right: 5px; 128 | } 129 | 130 | @media (prefers-color-scheme: dark) { 131 | :root { 132 | color: #f6f6f6; 133 | background-color: #2f2f2f; 134 | } 135 | 136 | a:hover { 137 | color: #24c8db; 138 | } 139 | 140 | input, 141 | button { 142 | color: #ffffff; 143 | background-color: #0f0f0f98; 144 | } 145 | button:active { 146 | background-color: #0f0f0f69; 147 | } 148 | } 149 | 150 | @import "tailwindcss"; 151 | 152 | @import "tw-animate-css"; 153 | 154 | @custom-variant dark (&:is(.dark *)); 155 | 156 | @theme inline { 157 | --radius-sm: calc(var(--radius) - 4px); 158 | --radius-md: calc(var(--radius) - 2px); 159 | --radius-lg: var(--radius); 160 | --radius-xl: calc(var(--radius) + 4px); 161 | --color-background: var(--background); 162 | --color-foreground: var(--foreground); 163 | --color-card: var(--card); 164 | --color-card-foreground: var(--card-foreground); 165 | --color-popover: var(--popover); 166 | --color-popover-foreground: var(--popover-foreground); 167 | --color-primary: var(--primary); 168 | --color-primary-foreground: var(--primary-foreground); 169 | --color-secondary: var(--secondary); 170 | --color-secondary-foreground: var(--secondary-foreground); 171 | --color-muted: var(--muted); 172 | --color-muted-foreground: var(--muted-foreground); 173 | --color-accent: var(--accent); 174 | --color-accent-foreground: var(--accent-foreground); 175 | --color-destructive: var(--destructive); 176 | --color-border: var(--border); 177 | --color-input: var(--input); 178 | --color-ring: var(--ring); 179 | --color-chart-1: var(--chart-1); 180 | --color-chart-2: var(--chart-2); 181 | --color-chart-3: var(--chart-3); 182 | --color-chart-4: var(--chart-4); 183 | --color-chart-5: var(--chart-5); 184 | --color-sidebar: var(--sidebar); 185 | --color-sidebar-foreground: var(--sidebar-foreground); 186 | --color-sidebar-primary: var(--sidebar-primary); 187 | --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); 188 | --color-sidebar-accent: var(--sidebar-accent); 189 | --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); 190 | --color-sidebar-border: var(--sidebar-border); 191 | --color-sidebar-ring: var(--sidebar-ring); 192 | } 193 | 194 | .dark { 195 | --background: oklch(0.145 0 0); 196 | --foreground: oklch(0.985 0 0); 197 | --card: oklch(0.205 0 0); 198 | --card-foreground: oklch(0.985 0 0); 199 | --popover: oklch(0.205 0 0); 200 | --popover-foreground: oklch(0.985 0 0); 201 | --primary: oklch(0.922 0 0); 202 | --primary-foreground: oklch(0.205 0 0); 203 | --secondary: oklch(0.269 0 0); 204 | --secondary-foreground: oklch(0.985 0 0); 205 | --muted: oklch(0.269 0 0); 206 | --muted-foreground: oklch(0.708 0 0); 207 | --accent: oklch(0.269 0 0); 208 | --accent-foreground: oklch(0.985 0 0); 209 | --destructive: oklch(0.704 0.191 22.216); 210 | --border: oklch(1 0 0 / 10%); 211 | --input: oklch(1 0 0 / 15%); 212 | --ring: oklch(0.556 0 0); 213 | --chart-1: oklch(0.488 0.243 264.376); 214 | --chart-2: oklch(0.696 0.17 162.48); 215 | --chart-3: oklch(0.769 0.188 70.08); 216 | --chart-4: oklch(0.627 0.265 303.9); 217 | --chart-5: oklch(0.645 0.246 16.439); 218 | --sidebar: oklch(0.205 0 0); 219 | --sidebar-foreground: oklch(0.985 0 0); 220 | --sidebar-primary: oklch(0.488 0.243 264.376); 221 | --sidebar-primary-foreground: oklch(0.985 0 0); 222 | --sidebar-accent: oklch(0.269 0 0); 223 | --sidebar-accent-foreground: oklch(0.985 0 0); 224 | --sidebar-border: oklch(1 0 0 / 10%); 225 | --sidebar-ring: oklch(0.556 0 0); 226 | } 227 | 228 | @layer base { 229 | * { 230 | @apply border-border outline-ring/50; 231 | } 232 | body { 233 | @apply bg-background text-foreground; 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /ui-events-app/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useRef } from "react"; 2 | import "./App.css"; // Assuming basic CSS/Tailwind setup 3 | 4 | // Import shadcn/ui components (assuming they are set up) 5 | import { 6 | Card, 7 | CardContent, 8 | CardHeader, 9 | CardTitle, 10 | CardDescription, 11 | } from "@/components/ui/card"; 12 | import { Badge } from "@/components/ui/badge"; 13 | import { ScrollArea } from "@/components/ui/scroll-area"; 14 | 15 | // Define the TypeScript interface matching ui-events/src/event.rs 16 | interface ApplicationInfo { 17 | name?: string | null; 18 | pid?: number | null; 19 | } 20 | 21 | interface WindowInfo { 22 | title?: string | null; 23 | id?: string | null; 24 | } 25 | 26 | interface Position { 27 | x: number; 28 | y: number; 29 | } 30 | 31 | interface Size { 32 | width: number; 33 | height: number; 34 | } 35 | 36 | interface ElementDetails { 37 | role?: string | null; 38 | identifier?: string | null; 39 | value?: any | null; // Use 'any' for flexibility, matching serde_json::Value 40 | position?: Position | null; 41 | size?: Size | null; 42 | } 43 | 44 | // Corresponds to the UiEvent struct in Rust 45 | interface UiEvent { 46 | event_type: string; // Assuming EventType enum translates to string 47 | timestamp: string; // ISO 8601 string from chrono DateTime 48 | application?: ApplicationInfo | null; 49 | window?: WindowInfo | null; 50 | element?: ElementDetails | null; 51 | event_specific_data?: any | null; 52 | } 53 | 54 | function App() { 55 | const [events, setEvents] = useState([]); 56 | const [isConnected, setIsConnected] = useState(false); 57 | const ws = useRef(null); 58 | const MAX_EVENTS = 100; // Limit the number of events displayed 59 | 60 | useEffect(() => { 61 | // Connect to the WebSocket server started by the Rust backend 62 | ws.current = new WebSocket("ws://localhost:9001"); 63 | 64 | ws.current.onopen = () => { 65 | console.log("websocket connected"); 66 | setIsConnected(true); 67 | }; 68 | 69 | ws.current.onclose = () => { 70 | console.log("websocket disconnected"); 71 | setIsConnected(false); 72 | // Optional: implement reconnection logic here if needed 73 | }; 74 | 75 | ws.current.onerror = (error) => { 76 | console.error("websocket error:", error); 77 | setIsConnected(false); 78 | }; 79 | 80 | ws.current.onmessage = (event) => { 81 | try { 82 | // Parse the incoming JSON string into a UiEvent object 83 | const newEvent: UiEvent = JSON.parse(event.data); 84 | // Add the new event to the start of the array, keeping only MAX_EVENTS 85 | setEvents((prevEvents) => 86 | [newEvent, ...prevEvents].slice(0, MAX_EVENTS) 87 | ); 88 | } catch (error) { 89 | console.error( 90 | "failed to parse incoming event:", 91 | error, 92 | "data:", 93 | event.data 94 | ); 95 | } 96 | }; 97 | 98 | // Cleanup function: close the WebSocket connection when the component unmounts 99 | return () => { 100 | ws.current?.close(); 101 | }; 102 | }, []); // Empty dependency array ensures this effect runs only once on mount 103 | 104 | return ( 105 |
106 | 107 | 108 | ui event stream 109 | 110 | connection status: 111 | 115 | {isConnected ? "connected" : "disconnected"} 116 | 117 | 118 | 119 | 120 | 121 | {events.length === 0 ? ( 122 |

123 | waiting for events... 124 |

125 | ) : ( 126 |
    127 | {events.map((event, index) => ( 128 |
  • 132 |
    133 | 134 | {event.event_type} 135 | 136 | 137 | {new Date(event.timestamp).toLocaleTimeString("en-US", { 138 | hour12: false, 139 | hour: "2-digit", 140 | minute: "2-digit", 141 | second: "2-digit", 142 | })} 143 | 144 |
    145 |
    146 | {event.application?.name && ( 147 | 148 | app: {event.application.name} 149 | 150 | )} 151 | {event.window?.title && ( 152 | 153 | win: {event.window.title} 154 | 155 | )} 156 | {event.element?.identifier && ( 157 | 158 | el: {event.element.identifier} 159 | 160 | )} 161 | {event.element?.value && 162 | typeof event.element.value === "string" && 163 | event.element.value.trim() && 164 | event.element.value.length < 50 && ( 165 | 166 | val: "{event.element.value}" 167 | 168 | )} 169 |
    170 |
  • 171 | ))} 172 |
173 | )} 174 |
175 |
176 |
177 |
178 | ); 179 | } 180 | 181 | export default App; 182 | -------------------------------------------------------------------------------- /ui-events-app/src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui-events-app/src/components/ui/badge.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const badgeVariants = cva( 8 | "inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden", 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90", 14 | secondary: 15 | "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", 16 | destructive: 17 | "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", 18 | outline: 19 | "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", 20 | }, 21 | }, 22 | defaultVariants: { 23 | variant: "default", 24 | }, 25 | } 26 | ) 27 | 28 | function Badge({ 29 | className, 30 | variant, 31 | asChild = false, 32 | ...props 33 | }: React.ComponentProps<"span"> & 34 | VariantProps & { asChild?: boolean }) { 35 | const Comp = asChild ? Slot : "span" 36 | 37 | return ( 38 | 43 | ) 44 | } 45 | 46 | export { Badge, badgeVariants } 47 | -------------------------------------------------------------------------------- /ui-events-app/src/components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | function Card({ className, ...props }: React.ComponentProps<"div">) { 6 | return ( 7 |
15 | ) 16 | } 17 | 18 | function CardHeader({ className, ...props }: React.ComponentProps<"div">) { 19 | return ( 20 |
28 | ) 29 | } 30 | 31 | function CardTitle({ className, ...props }: React.ComponentProps<"div">) { 32 | return ( 33 |
38 | ) 39 | } 40 | 41 | function CardDescription({ className, ...props }: React.ComponentProps<"div">) { 42 | return ( 43 |
48 | ) 49 | } 50 | 51 | function CardAction({ className, ...props }: React.ComponentProps<"div">) { 52 | return ( 53 |
61 | ) 62 | } 63 | 64 | function CardContent({ className, ...props }: React.ComponentProps<"div">) { 65 | return ( 66 |
71 | ) 72 | } 73 | 74 | function CardFooter({ className, ...props }: React.ComponentProps<"div">) { 75 | return ( 76 |
81 | ) 82 | } 83 | 84 | export { 85 | Card, 86 | CardHeader, 87 | CardFooter, 88 | CardTitle, 89 | CardAction, 90 | CardDescription, 91 | CardContent, 92 | } 93 | -------------------------------------------------------------------------------- /ui-events-app/src/components/ui/scroll-area.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | function ScrollArea({ 7 | className, 8 | children, 9 | ...props 10 | }: React.ComponentProps) { 11 | return ( 12 | 17 | 21 | {children} 22 | 23 | 24 | 25 | 26 | ) 27 | } 28 | 29 | function ScrollBar({ 30 | className, 31 | orientation = "vertical", 32 | ...props 33 | }: React.ComponentProps) { 34 | return ( 35 | 48 | 52 | 53 | ) 54 | } 55 | 56 | export { ScrollArea, ScrollBar } 57 | -------------------------------------------------------------------------------- /ui-events-app/src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /ui-events-app/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import App from "./App"; 4 | 5 | ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( 6 | 7 | 8 | , 9 | ); 10 | -------------------------------------------------------------------------------- /ui-events-app/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /ui-events-app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": [ 6 | "ES2020", 7 | "DOM", 8 | "DOM.Iterable" 9 | ], 10 | "module": "ESNext", 11 | "skipLibCheck": true, 12 | /* Bundler mode */ 13 | "moduleResolution": "bundler", 14 | "allowImportingTsExtensions": true, 15 | "resolveJsonModule": true, 16 | "isolatedModules": true, 17 | "noEmit": true, 18 | "jsx": "react-jsx", 19 | /* Linting */ 20 | "strict": true, 21 | "noUnusedLocals": true, 22 | "noUnusedParameters": true, 23 | "noFallthroughCasesInSwitch": true, 24 | "baseUrl": ".", 25 | "paths": { 26 | "@/*": [ 27 | "./src/*" 28 | ] 29 | } 30 | }, 31 | "include": [ 32 | "src" 33 | ], 34 | "references": [ 35 | { 36 | "path": "./tsconfig.node.json" 37 | } 38 | ] 39 | } -------------------------------------------------------------------------------- /ui-events-app/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /ui-events-app/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react"; 3 | import tailwindcss from "@tailwindcss/vite"; 4 | import path from "path"; 5 | const host = process.env.TAURI_DEV_HOST; 6 | 7 | // https://vitejs.dev/config/ 8 | export default defineConfig(async () => ({ 9 | plugins: [react(), tailwindcss()], 10 | 11 | // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build` 12 | // 13 | // 1. prevent vite from obscuring rust errors 14 | clearScreen: false, 15 | // 2. tauri expects a fixed port, fail if that port is not available 16 | server: { 17 | port: 1420, 18 | strictPort: true, 19 | host: host || false, 20 | hmr: host 21 | ? { 22 | protocol: "ws", 23 | host, 24 | port: 1421, 25 | } 26 | : undefined, 27 | watch: { 28 | // 3. tell vite to ignore watching `src-tauri` 29 | ignored: ["**/src-tauri/**"], 30 | }, 31 | }, 32 | resolve: { 33 | alias: { 34 | "@": path.resolve(__dirname, "./src"), 35 | }, 36 | }, 37 | })); 38 | -------------------------------------------------------------------------------- /ui-events/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /ui-events/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.24.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler2" 16 | version = "2.0.0" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" 19 | 20 | [[package]] 21 | name = "android-tzdata" 22 | version = "0.1.1" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" 25 | 26 | [[package]] 27 | name = "android_system_properties" 28 | version = "0.1.5" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 31 | dependencies = [ 32 | "libc", 33 | ] 34 | 35 | [[package]] 36 | name = "anstream" 37 | version = "0.6.18" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" 40 | dependencies = [ 41 | "anstyle", 42 | "anstyle-parse", 43 | "anstyle-query", 44 | "anstyle-wincon", 45 | "colorchoice", 46 | "is_terminal_polyfill", 47 | "utf8parse", 48 | ] 49 | 50 | [[package]] 51 | name = "anstyle" 52 | version = "1.0.10" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" 55 | 56 | [[package]] 57 | name = "anstyle-parse" 58 | version = "0.2.6" 59 | source = "registry+https://github.com/rust-lang/crates.io-index" 60 | checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" 61 | dependencies = [ 62 | "utf8parse", 63 | ] 64 | 65 | [[package]] 66 | name = "anstyle-query" 67 | version = "1.1.2" 68 | source = "registry+https://github.com/rust-lang/crates.io-index" 69 | checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" 70 | dependencies = [ 71 | "windows-sys 0.59.0", 72 | ] 73 | 74 | [[package]] 75 | name = "anstyle-wincon" 76 | version = "3.0.7" 77 | source = "registry+https://github.com/rust-lang/crates.io-index" 78 | checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" 79 | dependencies = [ 80 | "anstyle", 81 | "once_cell", 82 | "windows-sys 0.59.0", 83 | ] 84 | 85 | [[package]] 86 | name = "anyhow" 87 | version = "1.0.97" 88 | source = "registry+https://github.com/rust-lang/crates.io-index" 89 | checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f" 90 | 91 | [[package]] 92 | name = "autocfg" 93 | version = "1.4.0" 94 | source = "registry+https://github.com/rust-lang/crates.io-index" 95 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 96 | 97 | [[package]] 98 | name = "backtrace" 99 | version = "0.3.74" 100 | source = "registry+https://github.com/rust-lang/crates.io-index" 101 | checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" 102 | dependencies = [ 103 | "addr2line", 104 | "cfg-if", 105 | "libc", 106 | "miniz_oxide", 107 | "object", 108 | "rustc-demangle", 109 | "windows-targets", 110 | ] 111 | 112 | [[package]] 113 | name = "bitflags" 114 | version = "2.9.0" 115 | source = "registry+https://github.com/rust-lang/crates.io-index" 116 | checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" 117 | 118 | [[package]] 119 | name = "block-buffer" 120 | version = "0.10.4" 121 | source = "registry+https://github.com/rust-lang/crates.io-index" 122 | checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" 123 | dependencies = [ 124 | "generic-array", 125 | ] 126 | 127 | [[package]] 128 | name = "bumpalo" 129 | version = "3.17.0" 130 | source = "registry+https://github.com/rust-lang/crates.io-index" 131 | checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" 132 | 133 | [[package]] 134 | name = "byteorder" 135 | version = "1.5.0" 136 | source = "registry+https://github.com/rust-lang/crates.io-index" 137 | checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 138 | 139 | [[package]] 140 | name = "bytes" 141 | version = "1.10.1" 142 | source = "registry+https://github.com/rust-lang/crates.io-index" 143 | checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" 144 | 145 | [[package]] 146 | name = "cc" 147 | version = "1.2.17" 148 | source = "registry+https://github.com/rust-lang/crates.io-index" 149 | checksum = "1fcb57c740ae1daf453ae85f16e37396f672b039e00d9d866e07ddb24e328e3a" 150 | dependencies = [ 151 | "shlex", 152 | ] 153 | 154 | [[package]] 155 | name = "cfg-if" 156 | version = "1.0.0" 157 | source = "registry+https://github.com/rust-lang/crates.io-index" 158 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 159 | 160 | [[package]] 161 | name = "chrono" 162 | version = "0.4.40" 163 | source = "registry+https://github.com/rust-lang/crates.io-index" 164 | checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c" 165 | dependencies = [ 166 | "android-tzdata", 167 | "iana-time-zone", 168 | "js-sys", 169 | "num-traits", 170 | "serde", 171 | "wasm-bindgen", 172 | "windows-link", 173 | ] 174 | 175 | [[package]] 176 | name = "cidre" 177 | version = "0.7.0" 178 | source = "git+https://github.com/yury/cidre?rev=42ed422#42ed42289dd3a04cb243c0800e30229d13685d9f" 179 | dependencies = [ 180 | "cidre-macros", 181 | "parking_lot", 182 | "tokio", 183 | ] 184 | 185 | [[package]] 186 | name = "cidre-macros" 187 | version = "0.1.0" 188 | source = "git+https://github.com/yury/cidre?rev=42ed422#42ed42289dd3a04cb243c0800e30229d13685d9f" 189 | 190 | [[package]] 191 | name = "clap" 192 | version = "4.5.35" 193 | source = "registry+https://github.com/rust-lang/crates.io-index" 194 | checksum = "d8aa86934b44c19c50f87cc2790e19f54f7a67aedb64101c2e1a2e5ecfb73944" 195 | dependencies = [ 196 | "clap_builder", 197 | "clap_derive", 198 | ] 199 | 200 | [[package]] 201 | name = "clap_builder" 202 | version = "4.5.35" 203 | source = "registry+https://github.com/rust-lang/crates.io-index" 204 | checksum = "2414dbb2dd0695280da6ea9261e327479e9d37b0630f6b53ba2a11c60c679fd9" 205 | dependencies = [ 206 | "anstream", 207 | "anstyle", 208 | "clap_lex", 209 | "strsim", 210 | ] 211 | 212 | [[package]] 213 | name = "clap_derive" 214 | version = "4.5.32" 215 | source = "registry+https://github.com/rust-lang/crates.io-index" 216 | checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" 217 | dependencies = [ 218 | "heck", 219 | "proc-macro2", 220 | "quote", 221 | "syn", 222 | ] 223 | 224 | [[package]] 225 | name = "clap_lex" 226 | version = "0.7.4" 227 | source = "registry+https://github.com/rust-lang/crates.io-index" 228 | checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" 229 | 230 | [[package]] 231 | name = "colorchoice" 232 | version = "1.0.3" 233 | source = "registry+https://github.com/rust-lang/crates.io-index" 234 | checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" 235 | 236 | [[package]] 237 | name = "core-foundation-sys" 238 | version = "0.8.7" 239 | source = "registry+https://github.com/rust-lang/crates.io-index" 240 | checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 241 | 242 | [[package]] 243 | name = "cpufeatures" 244 | version = "0.2.17" 245 | source = "registry+https://github.com/rust-lang/crates.io-index" 246 | checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" 247 | dependencies = [ 248 | "libc", 249 | ] 250 | 251 | [[package]] 252 | name = "crypto-common" 253 | version = "0.1.6" 254 | source = "registry+https://github.com/rust-lang/crates.io-index" 255 | checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" 256 | dependencies = [ 257 | "generic-array", 258 | "typenum", 259 | ] 260 | 261 | [[package]] 262 | name = "data-encoding" 263 | version = "2.8.0" 264 | source = "registry+https://github.com/rust-lang/crates.io-index" 265 | checksum = "575f75dfd25738df5b91b8e43e14d44bda14637a58fae779fd2b064f8bf3e010" 266 | 267 | [[package]] 268 | name = "digest" 269 | version = "0.10.7" 270 | source = "registry+https://github.com/rust-lang/crates.io-index" 271 | checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" 272 | dependencies = [ 273 | "block-buffer", 274 | "crypto-common", 275 | ] 276 | 277 | [[package]] 278 | name = "displaydoc" 279 | version = "0.2.5" 280 | source = "registry+https://github.com/rust-lang/crates.io-index" 281 | checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" 282 | dependencies = [ 283 | "proc-macro2", 284 | "quote", 285 | "syn", 286 | ] 287 | 288 | [[package]] 289 | name = "fnv" 290 | version = "1.0.7" 291 | source = "registry+https://github.com/rust-lang/crates.io-index" 292 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 293 | 294 | [[package]] 295 | name = "form_urlencoded" 296 | version = "1.2.1" 297 | source = "registry+https://github.com/rust-lang/crates.io-index" 298 | checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" 299 | dependencies = [ 300 | "percent-encoding", 301 | ] 302 | 303 | [[package]] 304 | name = "futures-core" 305 | version = "0.3.31" 306 | source = "registry+https://github.com/rust-lang/crates.io-index" 307 | checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" 308 | 309 | [[package]] 310 | name = "futures-macro" 311 | version = "0.3.31" 312 | source = "registry+https://github.com/rust-lang/crates.io-index" 313 | checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" 314 | dependencies = [ 315 | "proc-macro2", 316 | "quote", 317 | "syn", 318 | ] 319 | 320 | [[package]] 321 | name = "futures-sink" 322 | version = "0.3.31" 323 | source = "registry+https://github.com/rust-lang/crates.io-index" 324 | checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" 325 | 326 | [[package]] 327 | name = "futures-task" 328 | version = "0.3.31" 329 | source = "registry+https://github.com/rust-lang/crates.io-index" 330 | checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" 331 | 332 | [[package]] 333 | name = "futures-util" 334 | version = "0.3.31" 335 | source = "registry+https://github.com/rust-lang/crates.io-index" 336 | checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" 337 | dependencies = [ 338 | "futures-core", 339 | "futures-macro", 340 | "futures-sink", 341 | "futures-task", 342 | "pin-project-lite", 343 | "pin-utils", 344 | "slab", 345 | ] 346 | 347 | [[package]] 348 | name = "generic-array" 349 | version = "0.14.7" 350 | source = "registry+https://github.com/rust-lang/crates.io-index" 351 | checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" 352 | dependencies = [ 353 | "typenum", 354 | "version_check", 355 | ] 356 | 357 | [[package]] 358 | name = "getrandom" 359 | version = "0.2.15" 360 | source = "registry+https://github.com/rust-lang/crates.io-index" 361 | checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" 362 | dependencies = [ 363 | "cfg-if", 364 | "libc", 365 | "wasi", 366 | ] 367 | 368 | [[package]] 369 | name = "gimli" 370 | version = "0.31.1" 371 | source = "registry+https://github.com/rust-lang/crates.io-index" 372 | checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" 373 | 374 | [[package]] 375 | name = "heck" 376 | version = "0.5.0" 377 | source = "registry+https://github.com/rust-lang/crates.io-index" 378 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 379 | 380 | [[package]] 381 | name = "http" 382 | version = "1.3.1" 383 | source = "registry+https://github.com/rust-lang/crates.io-index" 384 | checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" 385 | dependencies = [ 386 | "bytes", 387 | "fnv", 388 | "itoa", 389 | ] 390 | 391 | [[package]] 392 | name = "httparse" 393 | version = "1.10.1" 394 | source = "registry+https://github.com/rust-lang/crates.io-index" 395 | checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" 396 | 397 | [[package]] 398 | name = "iana-time-zone" 399 | version = "0.1.63" 400 | source = "registry+https://github.com/rust-lang/crates.io-index" 401 | checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" 402 | dependencies = [ 403 | "android_system_properties", 404 | "core-foundation-sys", 405 | "iana-time-zone-haiku", 406 | "js-sys", 407 | "log", 408 | "wasm-bindgen", 409 | "windows-core", 410 | ] 411 | 412 | [[package]] 413 | name = "iana-time-zone-haiku" 414 | version = "0.1.2" 415 | source = "registry+https://github.com/rust-lang/crates.io-index" 416 | checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" 417 | dependencies = [ 418 | "cc", 419 | ] 420 | 421 | [[package]] 422 | name = "icu_collections" 423 | version = "1.5.0" 424 | source = "registry+https://github.com/rust-lang/crates.io-index" 425 | checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" 426 | dependencies = [ 427 | "displaydoc", 428 | "yoke", 429 | "zerofrom", 430 | "zerovec", 431 | ] 432 | 433 | [[package]] 434 | name = "icu_locid" 435 | version = "1.5.0" 436 | source = "registry+https://github.com/rust-lang/crates.io-index" 437 | checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" 438 | dependencies = [ 439 | "displaydoc", 440 | "litemap", 441 | "tinystr", 442 | "writeable", 443 | "zerovec", 444 | ] 445 | 446 | [[package]] 447 | name = "icu_locid_transform" 448 | version = "1.5.0" 449 | source = "registry+https://github.com/rust-lang/crates.io-index" 450 | checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" 451 | dependencies = [ 452 | "displaydoc", 453 | "icu_locid", 454 | "icu_locid_transform_data", 455 | "icu_provider", 456 | "tinystr", 457 | "zerovec", 458 | ] 459 | 460 | [[package]] 461 | name = "icu_locid_transform_data" 462 | version = "1.5.1" 463 | source = "registry+https://github.com/rust-lang/crates.io-index" 464 | checksum = "7515e6d781098bf9f7205ab3fc7e9709d34554ae0b21ddbcb5febfa4bc7df11d" 465 | 466 | [[package]] 467 | name = "icu_normalizer" 468 | version = "1.5.0" 469 | source = "registry+https://github.com/rust-lang/crates.io-index" 470 | checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" 471 | dependencies = [ 472 | "displaydoc", 473 | "icu_collections", 474 | "icu_normalizer_data", 475 | "icu_properties", 476 | "icu_provider", 477 | "smallvec", 478 | "utf16_iter", 479 | "utf8_iter", 480 | "write16", 481 | "zerovec", 482 | ] 483 | 484 | [[package]] 485 | name = "icu_normalizer_data" 486 | version = "1.5.1" 487 | source = "registry+https://github.com/rust-lang/crates.io-index" 488 | checksum = "c5e8338228bdc8ab83303f16b797e177953730f601a96c25d10cb3ab0daa0cb7" 489 | 490 | [[package]] 491 | name = "icu_properties" 492 | version = "1.5.1" 493 | source = "registry+https://github.com/rust-lang/crates.io-index" 494 | checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" 495 | dependencies = [ 496 | "displaydoc", 497 | "icu_collections", 498 | "icu_locid_transform", 499 | "icu_properties_data", 500 | "icu_provider", 501 | "tinystr", 502 | "zerovec", 503 | ] 504 | 505 | [[package]] 506 | name = "icu_properties_data" 507 | version = "1.5.1" 508 | source = "registry+https://github.com/rust-lang/crates.io-index" 509 | checksum = "85fb8799753b75aee8d2a21d7c14d9f38921b54b3dbda10f5a3c7a7b82dba5e2" 510 | 511 | [[package]] 512 | name = "icu_provider" 513 | version = "1.5.0" 514 | source = "registry+https://github.com/rust-lang/crates.io-index" 515 | checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" 516 | dependencies = [ 517 | "displaydoc", 518 | "icu_locid", 519 | "icu_provider_macros", 520 | "stable_deref_trait", 521 | "tinystr", 522 | "writeable", 523 | "yoke", 524 | "zerofrom", 525 | "zerovec", 526 | ] 527 | 528 | [[package]] 529 | name = "icu_provider_macros" 530 | version = "1.5.0" 531 | source = "registry+https://github.com/rust-lang/crates.io-index" 532 | checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" 533 | dependencies = [ 534 | "proc-macro2", 535 | "quote", 536 | "syn", 537 | ] 538 | 539 | [[package]] 540 | name = "idna" 541 | version = "1.0.3" 542 | source = "registry+https://github.com/rust-lang/crates.io-index" 543 | checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" 544 | dependencies = [ 545 | "idna_adapter", 546 | "smallvec", 547 | "utf8_iter", 548 | ] 549 | 550 | [[package]] 551 | name = "idna_adapter" 552 | version = "1.2.0" 553 | source = "registry+https://github.com/rust-lang/crates.io-index" 554 | checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" 555 | dependencies = [ 556 | "icu_normalizer", 557 | "icu_properties", 558 | ] 559 | 560 | [[package]] 561 | name = "is_terminal_polyfill" 562 | version = "1.70.1" 563 | source = "registry+https://github.com/rust-lang/crates.io-index" 564 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 565 | 566 | [[package]] 567 | name = "itoa" 568 | version = "1.0.15" 569 | source = "registry+https://github.com/rust-lang/crates.io-index" 570 | checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 571 | 572 | [[package]] 573 | name = "js-sys" 574 | version = "0.3.77" 575 | source = "registry+https://github.com/rust-lang/crates.io-index" 576 | checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" 577 | dependencies = [ 578 | "once_cell", 579 | "wasm-bindgen", 580 | ] 581 | 582 | [[package]] 583 | name = "lazy_static" 584 | version = "1.5.0" 585 | source = "registry+https://github.com/rust-lang/crates.io-index" 586 | checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 587 | 588 | [[package]] 589 | name = "libc" 590 | version = "0.2.171" 591 | source = "registry+https://github.com/rust-lang/crates.io-index" 592 | checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" 593 | 594 | [[package]] 595 | name = "litemap" 596 | version = "0.7.5" 597 | source = "registry+https://github.com/rust-lang/crates.io-index" 598 | checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856" 599 | 600 | [[package]] 601 | name = "lock_api" 602 | version = "0.4.12" 603 | source = "registry+https://github.com/rust-lang/crates.io-index" 604 | checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" 605 | dependencies = [ 606 | "autocfg", 607 | "scopeguard", 608 | ] 609 | 610 | [[package]] 611 | name = "log" 612 | version = "0.4.27" 613 | source = "registry+https://github.com/rust-lang/crates.io-index" 614 | checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" 615 | 616 | [[package]] 617 | name = "memchr" 618 | version = "2.7.4" 619 | source = "registry+https://github.com/rust-lang/crates.io-index" 620 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 621 | 622 | [[package]] 623 | name = "miniz_oxide" 624 | version = "0.8.5" 625 | source = "registry+https://github.com/rust-lang/crates.io-index" 626 | checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5" 627 | dependencies = [ 628 | "adler2", 629 | ] 630 | 631 | [[package]] 632 | name = "mio" 633 | version = "1.0.3" 634 | source = "registry+https://github.com/rust-lang/crates.io-index" 635 | checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" 636 | dependencies = [ 637 | "libc", 638 | "wasi", 639 | "windows-sys 0.52.0", 640 | ] 641 | 642 | [[package]] 643 | name = "nu-ansi-term" 644 | version = "0.46.0" 645 | source = "registry+https://github.com/rust-lang/crates.io-index" 646 | checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" 647 | dependencies = [ 648 | "overload", 649 | "winapi", 650 | ] 651 | 652 | [[package]] 653 | name = "num-traits" 654 | version = "0.2.19" 655 | source = "registry+https://github.com/rust-lang/crates.io-index" 656 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 657 | dependencies = [ 658 | "autocfg", 659 | ] 660 | 661 | [[package]] 662 | name = "object" 663 | version = "0.36.7" 664 | source = "registry+https://github.com/rust-lang/crates.io-index" 665 | checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" 666 | dependencies = [ 667 | "memchr", 668 | ] 669 | 670 | [[package]] 671 | name = "once_cell" 672 | version = "1.21.3" 673 | source = "registry+https://github.com/rust-lang/crates.io-index" 674 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 675 | 676 | [[package]] 677 | name = "overload" 678 | version = "0.1.1" 679 | source = "registry+https://github.com/rust-lang/crates.io-index" 680 | checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" 681 | 682 | [[package]] 683 | name = "parking_lot" 684 | version = "0.12.3" 685 | source = "registry+https://github.com/rust-lang/crates.io-index" 686 | checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" 687 | dependencies = [ 688 | "lock_api", 689 | "parking_lot_core", 690 | ] 691 | 692 | [[package]] 693 | name = "parking_lot_core" 694 | version = "0.9.10" 695 | source = "registry+https://github.com/rust-lang/crates.io-index" 696 | checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" 697 | dependencies = [ 698 | "cfg-if", 699 | "libc", 700 | "redox_syscall", 701 | "smallvec", 702 | "windows-targets", 703 | ] 704 | 705 | [[package]] 706 | name = "percent-encoding" 707 | version = "2.3.1" 708 | source = "registry+https://github.com/rust-lang/crates.io-index" 709 | checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 710 | 711 | [[package]] 712 | name = "pin-project-lite" 713 | version = "0.2.16" 714 | source = "registry+https://github.com/rust-lang/crates.io-index" 715 | checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" 716 | 717 | [[package]] 718 | name = "pin-utils" 719 | version = "0.1.0" 720 | source = "registry+https://github.com/rust-lang/crates.io-index" 721 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 722 | 723 | [[package]] 724 | name = "ppv-lite86" 725 | version = "0.2.21" 726 | source = "registry+https://github.com/rust-lang/crates.io-index" 727 | checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" 728 | dependencies = [ 729 | "zerocopy", 730 | ] 731 | 732 | [[package]] 733 | name = "proc-macro2" 734 | version = "1.0.94" 735 | source = "registry+https://github.com/rust-lang/crates.io-index" 736 | checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" 737 | dependencies = [ 738 | "unicode-ident", 739 | ] 740 | 741 | [[package]] 742 | name = "quote" 743 | version = "1.0.40" 744 | source = "registry+https://github.com/rust-lang/crates.io-index" 745 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 746 | dependencies = [ 747 | "proc-macro2", 748 | ] 749 | 750 | [[package]] 751 | name = "rand" 752 | version = "0.8.5" 753 | source = "registry+https://github.com/rust-lang/crates.io-index" 754 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 755 | dependencies = [ 756 | "libc", 757 | "rand_chacha", 758 | "rand_core", 759 | ] 760 | 761 | [[package]] 762 | name = "rand_chacha" 763 | version = "0.3.1" 764 | source = "registry+https://github.com/rust-lang/crates.io-index" 765 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 766 | dependencies = [ 767 | "ppv-lite86", 768 | "rand_core", 769 | ] 770 | 771 | [[package]] 772 | name = "rand_core" 773 | version = "0.6.4" 774 | source = "registry+https://github.com/rust-lang/crates.io-index" 775 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 776 | dependencies = [ 777 | "getrandom", 778 | ] 779 | 780 | [[package]] 781 | name = "redox_syscall" 782 | version = "0.5.10" 783 | source = "registry+https://github.com/rust-lang/crates.io-index" 784 | checksum = "0b8c0c260b63a8219631167be35e6a988e9554dbd323f8bd08439c8ed1302bd1" 785 | dependencies = [ 786 | "bitflags", 787 | ] 788 | 789 | [[package]] 790 | name = "rustc-demangle" 791 | version = "0.1.24" 792 | source = "registry+https://github.com/rust-lang/crates.io-index" 793 | checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" 794 | 795 | [[package]] 796 | name = "rustversion" 797 | version = "1.0.20" 798 | source = "registry+https://github.com/rust-lang/crates.io-index" 799 | checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" 800 | 801 | [[package]] 802 | name = "ryu" 803 | version = "1.0.20" 804 | source = "registry+https://github.com/rust-lang/crates.io-index" 805 | checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" 806 | 807 | [[package]] 808 | name = "scopeguard" 809 | version = "1.2.0" 810 | source = "registry+https://github.com/rust-lang/crates.io-index" 811 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 812 | 813 | [[package]] 814 | name = "serde" 815 | version = "1.0.219" 816 | source = "registry+https://github.com/rust-lang/crates.io-index" 817 | checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 818 | dependencies = [ 819 | "serde_derive", 820 | ] 821 | 822 | [[package]] 823 | name = "serde_derive" 824 | version = "1.0.219" 825 | source = "registry+https://github.com/rust-lang/crates.io-index" 826 | checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 827 | dependencies = [ 828 | "proc-macro2", 829 | "quote", 830 | "syn", 831 | ] 832 | 833 | [[package]] 834 | name = "serde_json" 835 | version = "1.0.140" 836 | source = "registry+https://github.com/rust-lang/crates.io-index" 837 | checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" 838 | dependencies = [ 839 | "itoa", 840 | "memchr", 841 | "ryu", 842 | "serde", 843 | ] 844 | 845 | [[package]] 846 | name = "sha1" 847 | version = "0.10.6" 848 | source = "registry+https://github.com/rust-lang/crates.io-index" 849 | checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" 850 | dependencies = [ 851 | "cfg-if", 852 | "cpufeatures", 853 | "digest", 854 | ] 855 | 856 | [[package]] 857 | name = "sharded-slab" 858 | version = "0.1.7" 859 | source = "registry+https://github.com/rust-lang/crates.io-index" 860 | checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" 861 | dependencies = [ 862 | "lazy_static", 863 | ] 864 | 865 | [[package]] 866 | name = "shlex" 867 | version = "1.3.0" 868 | source = "registry+https://github.com/rust-lang/crates.io-index" 869 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 870 | 871 | [[package]] 872 | name = "signal-hook-registry" 873 | version = "1.4.2" 874 | source = "registry+https://github.com/rust-lang/crates.io-index" 875 | checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" 876 | dependencies = [ 877 | "libc", 878 | ] 879 | 880 | [[package]] 881 | name = "slab" 882 | version = "0.4.9" 883 | source = "registry+https://github.com/rust-lang/crates.io-index" 884 | checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" 885 | dependencies = [ 886 | "autocfg", 887 | ] 888 | 889 | [[package]] 890 | name = "smallvec" 891 | version = "1.14.0" 892 | source = "registry+https://github.com/rust-lang/crates.io-index" 893 | checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" 894 | 895 | [[package]] 896 | name = "socket2" 897 | version = "0.5.9" 898 | source = "registry+https://github.com/rust-lang/crates.io-index" 899 | checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef" 900 | dependencies = [ 901 | "libc", 902 | "windows-sys 0.52.0", 903 | ] 904 | 905 | [[package]] 906 | name = "stable_deref_trait" 907 | version = "1.2.0" 908 | source = "registry+https://github.com/rust-lang/crates.io-index" 909 | checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" 910 | 911 | [[package]] 912 | name = "strsim" 913 | version = "0.11.1" 914 | source = "registry+https://github.com/rust-lang/crates.io-index" 915 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 916 | 917 | [[package]] 918 | name = "syn" 919 | version = "2.0.100" 920 | source = "registry+https://github.com/rust-lang/crates.io-index" 921 | checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" 922 | dependencies = [ 923 | "proc-macro2", 924 | "quote", 925 | "unicode-ident", 926 | ] 927 | 928 | [[package]] 929 | name = "synstructure" 930 | version = "0.13.1" 931 | source = "registry+https://github.com/rust-lang/crates.io-index" 932 | checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" 933 | dependencies = [ 934 | "proc-macro2", 935 | "quote", 936 | "syn", 937 | ] 938 | 939 | [[package]] 940 | name = "thiserror" 941 | version = "1.0.69" 942 | source = "registry+https://github.com/rust-lang/crates.io-index" 943 | checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" 944 | dependencies = [ 945 | "thiserror-impl", 946 | ] 947 | 948 | [[package]] 949 | name = "thiserror-impl" 950 | version = "1.0.69" 951 | source = "registry+https://github.com/rust-lang/crates.io-index" 952 | checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" 953 | dependencies = [ 954 | "proc-macro2", 955 | "quote", 956 | "syn", 957 | ] 958 | 959 | [[package]] 960 | name = "thread_local" 961 | version = "1.1.8" 962 | source = "registry+https://github.com/rust-lang/crates.io-index" 963 | checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" 964 | dependencies = [ 965 | "cfg-if", 966 | "once_cell", 967 | ] 968 | 969 | [[package]] 970 | name = "tinystr" 971 | version = "0.7.6" 972 | source = "registry+https://github.com/rust-lang/crates.io-index" 973 | checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" 974 | dependencies = [ 975 | "displaydoc", 976 | "zerovec", 977 | ] 978 | 979 | [[package]] 980 | name = "tokio" 981 | version = "1.44.1" 982 | source = "registry+https://github.com/rust-lang/crates.io-index" 983 | checksum = "f382da615b842244d4b8738c82ed1275e6c5dd90c459a30941cd07080b06c91a" 984 | dependencies = [ 985 | "backtrace", 986 | "bytes", 987 | "libc", 988 | "mio", 989 | "parking_lot", 990 | "pin-project-lite", 991 | "signal-hook-registry", 992 | "socket2", 993 | "tokio-macros", 994 | "windows-sys 0.52.0", 995 | ] 996 | 997 | [[package]] 998 | name = "tokio-macros" 999 | version = "2.5.0" 1000 | source = "registry+https://github.com/rust-lang/crates.io-index" 1001 | checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" 1002 | dependencies = [ 1003 | "proc-macro2", 1004 | "quote", 1005 | "syn", 1006 | ] 1007 | 1008 | [[package]] 1009 | name = "tokio-tungstenite" 1010 | version = "0.21.0" 1011 | source = "registry+https://github.com/rust-lang/crates.io-index" 1012 | checksum = "c83b561d025642014097b66e6c1bb422783339e0909e4429cde4749d1990bc38" 1013 | dependencies = [ 1014 | "futures-util", 1015 | "log", 1016 | "tokio", 1017 | "tungstenite", 1018 | ] 1019 | 1020 | [[package]] 1021 | name = "tracing" 1022 | version = "0.1.41" 1023 | source = "registry+https://github.com/rust-lang/crates.io-index" 1024 | checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" 1025 | dependencies = [ 1026 | "pin-project-lite", 1027 | "tracing-attributes", 1028 | "tracing-core", 1029 | ] 1030 | 1031 | [[package]] 1032 | name = "tracing-attributes" 1033 | version = "0.1.28" 1034 | source = "registry+https://github.com/rust-lang/crates.io-index" 1035 | checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" 1036 | dependencies = [ 1037 | "proc-macro2", 1038 | "quote", 1039 | "syn", 1040 | ] 1041 | 1042 | [[package]] 1043 | name = "tracing-core" 1044 | version = "0.1.33" 1045 | source = "registry+https://github.com/rust-lang/crates.io-index" 1046 | checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" 1047 | dependencies = [ 1048 | "once_cell", 1049 | "valuable", 1050 | ] 1051 | 1052 | [[package]] 1053 | name = "tracing-log" 1054 | version = "0.2.0" 1055 | source = "registry+https://github.com/rust-lang/crates.io-index" 1056 | checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" 1057 | dependencies = [ 1058 | "log", 1059 | "once_cell", 1060 | "tracing-core", 1061 | ] 1062 | 1063 | [[package]] 1064 | name = "tracing-subscriber" 1065 | version = "0.3.19" 1066 | source = "registry+https://github.com/rust-lang/crates.io-index" 1067 | checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" 1068 | dependencies = [ 1069 | "nu-ansi-term", 1070 | "sharded-slab", 1071 | "smallvec", 1072 | "thread_local", 1073 | "tracing-core", 1074 | "tracing-log", 1075 | ] 1076 | 1077 | [[package]] 1078 | name = "tungstenite" 1079 | version = "0.21.0" 1080 | source = "registry+https://github.com/rust-lang/crates.io-index" 1081 | checksum = "9ef1a641ea34f399a848dea702823bbecfb4c486f911735368f1f137cb8257e1" 1082 | dependencies = [ 1083 | "byteorder", 1084 | "bytes", 1085 | "data-encoding", 1086 | "http", 1087 | "httparse", 1088 | "log", 1089 | "rand", 1090 | "sha1", 1091 | "thiserror", 1092 | "url", 1093 | "utf-8", 1094 | ] 1095 | 1096 | [[package]] 1097 | name = "typenum" 1098 | version = "1.18.0" 1099 | source = "registry+https://github.com/rust-lang/crates.io-index" 1100 | checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" 1101 | 1102 | [[package]] 1103 | name = "ui-events" 1104 | version = "0.1.0" 1105 | dependencies = [ 1106 | "anyhow", 1107 | "chrono", 1108 | "cidre", 1109 | "clap", 1110 | "futures-util", 1111 | "serde", 1112 | "serde_json", 1113 | "tokio", 1114 | "tokio-tungstenite", 1115 | "tracing", 1116 | "tracing-subscriber", 1117 | "url", 1118 | ] 1119 | 1120 | [[package]] 1121 | name = "unicode-ident" 1122 | version = "1.0.18" 1123 | source = "registry+https://github.com/rust-lang/crates.io-index" 1124 | checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 1125 | 1126 | [[package]] 1127 | name = "url" 1128 | version = "2.5.4" 1129 | source = "registry+https://github.com/rust-lang/crates.io-index" 1130 | checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" 1131 | dependencies = [ 1132 | "form_urlencoded", 1133 | "idna", 1134 | "percent-encoding", 1135 | ] 1136 | 1137 | [[package]] 1138 | name = "utf-8" 1139 | version = "0.7.6" 1140 | source = "registry+https://github.com/rust-lang/crates.io-index" 1141 | checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" 1142 | 1143 | [[package]] 1144 | name = "utf16_iter" 1145 | version = "1.0.5" 1146 | source = "registry+https://github.com/rust-lang/crates.io-index" 1147 | checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" 1148 | 1149 | [[package]] 1150 | name = "utf8_iter" 1151 | version = "1.0.4" 1152 | source = "registry+https://github.com/rust-lang/crates.io-index" 1153 | checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" 1154 | 1155 | [[package]] 1156 | name = "utf8parse" 1157 | version = "0.2.2" 1158 | source = "registry+https://github.com/rust-lang/crates.io-index" 1159 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 1160 | 1161 | [[package]] 1162 | name = "valuable" 1163 | version = "0.1.1" 1164 | source = "registry+https://github.com/rust-lang/crates.io-index" 1165 | checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" 1166 | 1167 | [[package]] 1168 | name = "version_check" 1169 | version = "0.9.5" 1170 | source = "registry+https://github.com/rust-lang/crates.io-index" 1171 | checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 1172 | 1173 | [[package]] 1174 | name = "wasi" 1175 | version = "0.11.0+wasi-snapshot-preview1" 1176 | source = "registry+https://github.com/rust-lang/crates.io-index" 1177 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1178 | 1179 | [[package]] 1180 | name = "wasm-bindgen" 1181 | version = "0.2.100" 1182 | source = "registry+https://github.com/rust-lang/crates.io-index" 1183 | checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" 1184 | dependencies = [ 1185 | "cfg-if", 1186 | "once_cell", 1187 | "rustversion", 1188 | "wasm-bindgen-macro", 1189 | ] 1190 | 1191 | [[package]] 1192 | name = "wasm-bindgen-backend" 1193 | version = "0.2.100" 1194 | source = "registry+https://github.com/rust-lang/crates.io-index" 1195 | checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" 1196 | dependencies = [ 1197 | "bumpalo", 1198 | "log", 1199 | "proc-macro2", 1200 | "quote", 1201 | "syn", 1202 | "wasm-bindgen-shared", 1203 | ] 1204 | 1205 | [[package]] 1206 | name = "wasm-bindgen-macro" 1207 | version = "0.2.100" 1208 | source = "registry+https://github.com/rust-lang/crates.io-index" 1209 | checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" 1210 | dependencies = [ 1211 | "quote", 1212 | "wasm-bindgen-macro-support", 1213 | ] 1214 | 1215 | [[package]] 1216 | name = "wasm-bindgen-macro-support" 1217 | version = "0.2.100" 1218 | source = "registry+https://github.com/rust-lang/crates.io-index" 1219 | checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" 1220 | dependencies = [ 1221 | "proc-macro2", 1222 | "quote", 1223 | "syn", 1224 | "wasm-bindgen-backend", 1225 | "wasm-bindgen-shared", 1226 | ] 1227 | 1228 | [[package]] 1229 | name = "wasm-bindgen-shared" 1230 | version = "0.2.100" 1231 | source = "registry+https://github.com/rust-lang/crates.io-index" 1232 | checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" 1233 | dependencies = [ 1234 | "unicode-ident", 1235 | ] 1236 | 1237 | [[package]] 1238 | name = "winapi" 1239 | version = "0.3.9" 1240 | source = "registry+https://github.com/rust-lang/crates.io-index" 1241 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1242 | dependencies = [ 1243 | "winapi-i686-pc-windows-gnu", 1244 | "winapi-x86_64-pc-windows-gnu", 1245 | ] 1246 | 1247 | [[package]] 1248 | name = "winapi-i686-pc-windows-gnu" 1249 | version = "0.4.0" 1250 | source = "registry+https://github.com/rust-lang/crates.io-index" 1251 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1252 | 1253 | [[package]] 1254 | name = "winapi-x86_64-pc-windows-gnu" 1255 | version = "0.4.0" 1256 | source = "registry+https://github.com/rust-lang/crates.io-index" 1257 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1258 | 1259 | [[package]] 1260 | name = "windows-core" 1261 | version = "0.61.0" 1262 | source = "registry+https://github.com/rust-lang/crates.io-index" 1263 | checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980" 1264 | dependencies = [ 1265 | "windows-implement", 1266 | "windows-interface", 1267 | "windows-link", 1268 | "windows-result", 1269 | "windows-strings", 1270 | ] 1271 | 1272 | [[package]] 1273 | name = "windows-implement" 1274 | version = "0.60.0" 1275 | source = "registry+https://github.com/rust-lang/crates.io-index" 1276 | checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" 1277 | dependencies = [ 1278 | "proc-macro2", 1279 | "quote", 1280 | "syn", 1281 | ] 1282 | 1283 | [[package]] 1284 | name = "windows-interface" 1285 | version = "0.59.1" 1286 | source = "registry+https://github.com/rust-lang/crates.io-index" 1287 | checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" 1288 | dependencies = [ 1289 | "proc-macro2", 1290 | "quote", 1291 | "syn", 1292 | ] 1293 | 1294 | [[package]] 1295 | name = "windows-link" 1296 | version = "0.1.1" 1297 | source = "registry+https://github.com/rust-lang/crates.io-index" 1298 | checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" 1299 | 1300 | [[package]] 1301 | name = "windows-result" 1302 | version = "0.3.2" 1303 | source = "registry+https://github.com/rust-lang/crates.io-index" 1304 | checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252" 1305 | dependencies = [ 1306 | "windows-link", 1307 | ] 1308 | 1309 | [[package]] 1310 | name = "windows-strings" 1311 | version = "0.4.0" 1312 | source = "registry+https://github.com/rust-lang/crates.io-index" 1313 | checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97" 1314 | dependencies = [ 1315 | "windows-link", 1316 | ] 1317 | 1318 | [[package]] 1319 | name = "windows-sys" 1320 | version = "0.52.0" 1321 | source = "registry+https://github.com/rust-lang/crates.io-index" 1322 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 1323 | dependencies = [ 1324 | "windows-targets", 1325 | ] 1326 | 1327 | [[package]] 1328 | name = "windows-sys" 1329 | version = "0.59.0" 1330 | source = "registry+https://github.com/rust-lang/crates.io-index" 1331 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 1332 | dependencies = [ 1333 | "windows-targets", 1334 | ] 1335 | 1336 | [[package]] 1337 | name = "windows-targets" 1338 | version = "0.52.6" 1339 | source = "registry+https://github.com/rust-lang/crates.io-index" 1340 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 1341 | dependencies = [ 1342 | "windows_aarch64_gnullvm", 1343 | "windows_aarch64_msvc", 1344 | "windows_i686_gnu", 1345 | "windows_i686_gnullvm", 1346 | "windows_i686_msvc", 1347 | "windows_x86_64_gnu", 1348 | "windows_x86_64_gnullvm", 1349 | "windows_x86_64_msvc", 1350 | ] 1351 | 1352 | [[package]] 1353 | name = "windows_aarch64_gnullvm" 1354 | version = "0.52.6" 1355 | source = "registry+https://github.com/rust-lang/crates.io-index" 1356 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 1357 | 1358 | [[package]] 1359 | name = "windows_aarch64_msvc" 1360 | version = "0.52.6" 1361 | source = "registry+https://github.com/rust-lang/crates.io-index" 1362 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 1363 | 1364 | [[package]] 1365 | name = "windows_i686_gnu" 1366 | version = "0.52.6" 1367 | source = "registry+https://github.com/rust-lang/crates.io-index" 1368 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 1369 | 1370 | [[package]] 1371 | name = "windows_i686_gnullvm" 1372 | version = "0.52.6" 1373 | source = "registry+https://github.com/rust-lang/crates.io-index" 1374 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 1375 | 1376 | [[package]] 1377 | name = "windows_i686_msvc" 1378 | version = "0.52.6" 1379 | source = "registry+https://github.com/rust-lang/crates.io-index" 1380 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 1381 | 1382 | [[package]] 1383 | name = "windows_x86_64_gnu" 1384 | version = "0.52.6" 1385 | source = "registry+https://github.com/rust-lang/crates.io-index" 1386 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 1387 | 1388 | [[package]] 1389 | name = "windows_x86_64_gnullvm" 1390 | version = "0.52.6" 1391 | source = "registry+https://github.com/rust-lang/crates.io-index" 1392 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 1393 | 1394 | [[package]] 1395 | name = "windows_x86_64_msvc" 1396 | version = "0.52.6" 1397 | source = "registry+https://github.com/rust-lang/crates.io-index" 1398 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 1399 | 1400 | [[package]] 1401 | name = "write16" 1402 | version = "1.0.0" 1403 | source = "registry+https://github.com/rust-lang/crates.io-index" 1404 | checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" 1405 | 1406 | [[package]] 1407 | name = "writeable" 1408 | version = "0.5.5" 1409 | source = "registry+https://github.com/rust-lang/crates.io-index" 1410 | checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" 1411 | 1412 | [[package]] 1413 | name = "yoke" 1414 | version = "0.7.5" 1415 | source = "registry+https://github.com/rust-lang/crates.io-index" 1416 | checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" 1417 | dependencies = [ 1418 | "serde", 1419 | "stable_deref_trait", 1420 | "yoke-derive", 1421 | "zerofrom", 1422 | ] 1423 | 1424 | [[package]] 1425 | name = "yoke-derive" 1426 | version = "0.7.5" 1427 | source = "registry+https://github.com/rust-lang/crates.io-index" 1428 | checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" 1429 | dependencies = [ 1430 | "proc-macro2", 1431 | "quote", 1432 | "syn", 1433 | "synstructure", 1434 | ] 1435 | 1436 | [[package]] 1437 | name = "zerocopy" 1438 | version = "0.8.24" 1439 | source = "registry+https://github.com/rust-lang/crates.io-index" 1440 | checksum = "2586fea28e186957ef732a5f8b3be2da217d65c5969d4b1e17f973ebbe876879" 1441 | dependencies = [ 1442 | "zerocopy-derive", 1443 | ] 1444 | 1445 | [[package]] 1446 | name = "zerocopy-derive" 1447 | version = "0.8.24" 1448 | source = "registry+https://github.com/rust-lang/crates.io-index" 1449 | checksum = "a996a8f63c5c4448cd959ac1bab0aaa3306ccfd060472f85943ee0750f0169be" 1450 | dependencies = [ 1451 | "proc-macro2", 1452 | "quote", 1453 | "syn", 1454 | ] 1455 | 1456 | [[package]] 1457 | name = "zerofrom" 1458 | version = "0.1.6" 1459 | source = "registry+https://github.com/rust-lang/crates.io-index" 1460 | checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" 1461 | dependencies = [ 1462 | "zerofrom-derive", 1463 | ] 1464 | 1465 | [[package]] 1466 | name = "zerofrom-derive" 1467 | version = "0.1.6" 1468 | source = "registry+https://github.com/rust-lang/crates.io-index" 1469 | checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" 1470 | dependencies = [ 1471 | "proc-macro2", 1472 | "quote", 1473 | "syn", 1474 | "synstructure", 1475 | ] 1476 | 1477 | [[package]] 1478 | name = "zerovec" 1479 | version = "0.10.4" 1480 | source = "registry+https://github.com/rust-lang/crates.io-index" 1481 | checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" 1482 | dependencies = [ 1483 | "yoke", 1484 | "zerofrom", 1485 | "zerovec-derive", 1486 | ] 1487 | 1488 | [[package]] 1489 | name = "zerovec-derive" 1490 | version = "0.10.3" 1491 | source = "registry+https://github.com/rust-lang/crates.io-index" 1492 | checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" 1493 | dependencies = [ 1494 | "proc-macro2", 1495 | "quote", 1496 | "syn", 1497 | ] 1498 | -------------------------------------------------------------------------------- /ui-events/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ui-events" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | anyhow = "1.0" 8 | clap = { version = "4.0", features = ["derive"] } 9 | serde = { version = "1.0", features = ["derive"] } 10 | serde_json = "1.0" 11 | tokio = { version = "1", features = ["full"] } # Use "full" for mpsc, rt-multi-thread, macros 12 | tokio-tungstenite = "0.21" # Or other websocket library like axum 13 | tracing = "0.1.41" 14 | tracing-subscriber = "0.3.19" 15 | futures-util = "0.3" 16 | 17 | [dev-dependencies] 18 | # Added for example client 19 | futures-util = "0.3" 20 | url = "2.5" 21 | 22 | # Platform-specific dependencies 23 | [target.'cfg(target_os = "macos")'.dependencies] 24 | cidre = { git = "https://github.com/yury/cidre", branch = "main", features = ["ax", "ns", "cf", "blocks", "app"] } 25 | chrono = { version = "0.4", features = ["serde"] } # For timestamps 26 | 27 | [target.'cfg(target_os = "windows")'.dependencies] 28 | # Add Windows UI Automation crate(s) here later (e.g., windows-rs, uiautomation) 29 | 30 | [target.'cfg(target_os = "linux")'.dependencies] 31 | # Add Linux AT-SPI crate(s) here later (e.g., zbus, atspi) 32 | -------------------------------------------------------------------------------- /ui-events/examples/simple_client.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use futures_util::StreamExt; 3 | use tokio_tungstenite::{connect_async, tungstenite::protocol::Message}; 4 | use url::Url; 5 | 6 | // Define the event structure again here for deserialization, 7 | // or better, make ui-events a library crate and depend on it. 8 | // For now, let's just print the raw JSON string. 9 | 10 | #[tokio::main] 11 | async fn main() -> Result<()> { 12 | let server_url = "ws://localhost:9001"; 13 | println!("connecting to {}", server_url); 14 | 15 | let url = Url::parse(server_url)?; 16 | 17 | let (ws_stream, _response) = connect_async(url).await.expect("failed to connect"); 18 | println!("websocket handshake has been successfully completed"); 19 | 20 | let (mut _write, mut read) = ws_stream.split(); 21 | 22 | // We just read messages in this simple client 23 | while let Some(msg) = read.next().await { 24 | match msg { 25 | Ok(Message::Text(text)) => { 26 | println!("received: {}", text); 27 | // Optional: Deserialize into UiEvent struct if defined 28 | // let event: Result = serde_json::from_str(&text); 29 | // match event { 30 | // Ok(parsed_event) => println!("parsed: {:?}", parsed_event), 31 | // Err(e) => eprintln!("failed to parse event: {}", e), 32 | // } 33 | } 34 | Ok(Message::Binary(_)) => { 35 | println!("received binary message (unexpected)"); 36 | } 37 | Ok(Message::Ping(_)) => { 38 | // tokio-tungstenite handles ping/pong automatically 39 | } 40 | Ok(Message::Pong(_)) => { 41 | // tokio-tungstenite handles ping/pong automatically 42 | } 43 | Ok(Message::Close(close_frame)) => { 44 | println!("connection closed: {:?}", close_frame); 45 | break; 46 | } 47 | Err(e) => { 48 | eprintln!("websocket error: {}", e); 49 | break; 50 | } 51 | _ => {} 52 | } 53 | } 54 | 55 | Ok(()) 56 | } 57 | -------------------------------------------------------------------------------- /ui-events/src/bin/main.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | 3 | use tracing::info; 4 | use ui_events::run; 5 | 6 | #[derive(Parser, Debug)] 7 | #[clap(author, version, about, long_about = None)] 8 | struct Args { 9 | /// WebSocket server port 10 | #[clap(short, long, value_parser, default_value_t = 9001)] 11 | port: u16, 12 | } 13 | 14 | fn main() { 15 | tracing_subscriber::fmt::init(); 16 | info!("starting ui-events..."); 17 | 18 | let port = Args::parse().port; 19 | 20 | run(port); 21 | } 22 | -------------------------------------------------------------------------------- /ui-events/src/error.rs: -------------------------------------------------------------------------------- 1 | // Placeholder for error handling 2 | 3 | // We can just use anyhow::Error for now, or define custom errors later. 4 | -------------------------------------------------------------------------------- /ui-events/src/event.rs: -------------------------------------------------------------------------------- 1 | use chrono::{DateTime, Utc}; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | // TODO: Define more specific event types and details based on AXObserver/UIA/AT-SPI capabilities 5 | 6 | #[derive(Debug, Clone, Serialize, Deserialize)] 7 | pub enum EventType { 8 | ApplicationActivated, 9 | ApplicationDeactivated, 10 | WindowFocused, 11 | WindowCreated, 12 | WindowMoved, 13 | WindowResized, 14 | // WindowClosed, // Maybe useful? 15 | ElementFocused, 16 | ValueChanged, 17 | ElementDestroyed, 18 | MenuOpened, 19 | MenuClosed, 20 | MenuItemSelected, 21 | SelectionChanged, 22 | SelectedTextChanged, 23 | TitleChanged, 24 | } 25 | 26 | #[derive(Debug, Clone, Serialize, Deserialize)] 27 | pub struct ApplicationInfo { 28 | pub name: Option, 29 | pub pid: Option, // Or appropriate type 30 | // pub path: Option, 31 | } 32 | 33 | #[derive(Debug, Clone, Serialize, Deserialize)] 34 | pub struct WindowInfo { 35 | pub title: Option, 36 | pub id: Option, // Platform-specific ID 37 | // pub position: Option, 38 | // pub size: Option, 39 | } 40 | 41 | #[derive(Debug, Clone, Serialize, Deserialize)] 42 | pub struct ElementDetails { 43 | pub role: Option, // Standardized role if possible 44 | pub identifier: Option, // Accessibility Label/Name 45 | pub value: Option, // Current value (flexible type) 46 | pub position: Option, 47 | pub size: Option, 48 | } 49 | 50 | #[derive(Debug, Clone, Serialize, Deserialize)] 51 | pub struct Position { 52 | pub x: f64, 53 | pub y: f64, 54 | } 55 | 56 | #[derive(Debug, Clone, Serialize, Deserialize)] 57 | pub struct Size { 58 | pub width: f64, 59 | pub height: f64, 60 | } 61 | 62 | #[derive(Debug, Clone, Serialize, Deserialize)] 63 | pub struct UiEvent { 64 | pub event_type: EventType, 65 | #[serde(with = "chrono::serde::ts_milliseconds")] 66 | pub timestamp: DateTime, 67 | pub application: Option, 68 | pub window: Option, 69 | pub element: Option, 70 | // Specific data not fitting above, use sparingly 71 | pub event_specific_data: Option, 72 | } 73 | -------------------------------------------------------------------------------- /ui-events/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod error; 2 | pub mod event; 3 | pub mod platform; 4 | pub mod server; 5 | 6 | pub use platform::create_listener; 7 | pub use server::run_server; 8 | use tokio::sync::mpsc; 9 | use tracing::info; 10 | 11 | pub fn run(port: u16) { 12 | let _ = tracing_subscriber::fmt::try_init(); 13 | info!("starting ui-events..."); 14 | 15 | // Create a channel for communication between listener and server 16 | let (tx, rx) = mpsc::channel(100); // Buffer size 100 17 | 18 | let rt = tokio::runtime::Builder::new_multi_thread() 19 | .enable_all() 20 | .worker_threads(2) 21 | .build() 22 | .unwrap(); 23 | 24 | use cidre::ns; 25 | 26 | rt.spawn(async move { 27 | run_server(port, rx).await.unwrap(); 28 | ns::App::shared().terminate(None); 29 | }); 30 | 31 | platform::listener_run(tx); 32 | } 33 | -------------------------------------------------------------------------------- /ui-events/src/platform/linux.rs: -------------------------------------------------------------------------------- 1 | #![cfg(target_os = "linux")] 2 | 3 | use super::PlatformListener; 4 | use crate::event::UiEvent; 5 | use anyhow::Result; 6 | use tokio::sync::mpsc; 7 | 8 | pub struct LinuxListener {} 9 | 10 | impl LinuxListener { 11 | pub fn new() -> Result { 12 | anyhow::bail!("linux listener not implemented") 13 | } 14 | } 15 | 16 | impl PlatformListener for LinuxListener { 17 | fn run(&self, _sender: mpsc::Sender) -> Result<()> { 18 | println!("linux listener run (unimplemented)"); 19 | // TODO: Implement using AT-SPI 20 | anyhow::bail!("linux listener not implemented") 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /ui-events/src/platform/macos.rs: -------------------------------------------------------------------------------- 1 | /* 2 | This file implements the `PlatformListener` trait for macOS. 3 | It leverages Apple's Accessibility API (AXUIElement, AXObserver) via the `cidre` crate 4 | to capture UI events such as application activation, window focus changes, 5 | and UI element interactions (focus, value changes). 6 | 7 | Events are captured using callbacks registered with `AXObserver` on a dedicated thread 8 | running a `CFRunLoop`. Captured event data is then structured into a `UiEvent` 9 | and sent asynchronously through an `mpsc::Sender` provided during initialization. 10 | 11 | Key components: 12 | - `cidre`: Rust bindings for Apple frameworks (Core Foundation, AppKit, Accessibility). 13 | - `ax`: Accessibility API specific types within `cidre`. 14 | - `cf`: Core Foundation types (RunLoop, String, etc.) within `cidre`. 15 | - `ns`: AppKit types (Workspace, Application) within `cidre`. 16 | - `tokio::sync::mpsc`: Used for sending events back to the main application logic. 17 | - `thread_local!`: Used to store the sender and observer state within the C callback context. 18 | */ 19 | 20 | #![cfg(target_os = "macos")] 21 | 22 | use super::PlatformListener; 23 | use crate::event::{ 24 | ApplicationInfo, ElementDetails, EventType, Position, Size, UiEvent, WindowInfo, 25 | }; 26 | use anyhow::{Result, anyhow}; 27 | use chrono::Utc; 28 | use cidre::arc::{self, Retained}; 29 | use cidre::objc::Obj; 30 | use cidre::{ax, cf, ns, objc::ar_pool}; 31 | use std::cell::RefCell; 32 | use std::ffi::c_void; 33 | use std::marker::PhantomPinned; 34 | use std::pin::Pin; 35 | use std::sync::{Arc, Mutex}; 36 | use tokio::sync::mpsc; 37 | use tracing::{error, info, warn}; 38 | 39 | // Store sender and current observer for the CFRunLoop thread 40 | thread_local! { 41 | static SENDER: RefCell>> = RefCell::new(None); 42 | // Store the active AXObserver and the top-level element it observes (the app element) 43 | static CURRENT_AX_OBSERVER: RefCell, Retained)>> = RefCell::new(None); 44 | // Store the NSWorkspace observer token to remove it on cleanup 45 | static WORKSPACE_OBSERVER_TOKEN: RefCell>> = RefCell::new(None); 46 | } 47 | 48 | // Define the reference date epoch seconds (Unix timestamp for 2001-01-01T00:00:00Z) 49 | const CF_ABSOLUTE_TIME_EPOCH_OFFSET: i64 = 978307200; 50 | 51 | extern "C" fn observer_callback2( 52 | _observer: &mut ax::Observer, 53 | element: &mut ax::UiElement, 54 | notification: &ax::Notification, 55 | user_info: *mut c_void, 56 | ) { 57 | let listener: &MacosListener = unsafe { std::mem::transmute(user_info) }; 58 | listener.handle_ui_event(element, notification); 59 | } 60 | 61 | // The C callback function for AXObserver notifications 62 | extern "C" fn observer_callback( 63 | _observer: &mut ax::Observer, 64 | element: &mut ax::UiElement, 65 | notification: &ax::Notification, 66 | _user_info: *mut c_void, 67 | ) { 68 | ar_pool(|| { 69 | // Directly use the Ref, or retain if ownership/longer lifetime is needed 70 | let element = element.retained(); 71 | let notification = notification.retained(); 72 | let notification_name = notification.to_string(); 73 | 74 | info!(%notification_name, "observer_callback received"); 75 | 76 | SENDER.with(|cell| { 77 | let r = cell.borrow(); 78 | let sender = match r.as_ref() { 79 | Some(s) => s, 80 | None => { 81 | error!("sender not available in observer callback"); 82 | return; 83 | } 84 | }; 85 | 86 | // Map AX notifications (cf::String constants) to our event types 87 | let event_type = if notification.equal(ax::notification::focused_window_changed()) { 88 | EventType::WindowFocused 89 | } else if notification.equal(ax::notification::focused_ui_element_changed()) { 90 | EventType::ElementFocused 91 | } else if notification.equal(ax::notification::value_changed()) { 92 | EventType::ValueChanged 93 | } else if notification.equal(ax::notification::window_created()) { 94 | EventType::WindowCreated 95 | } else if notification.equal(ax::notification::window_moved()) { 96 | EventType::WindowMoved 97 | } else if notification.equal(ax::notification::window_resized()) { 98 | EventType::WindowResized 99 | } else if notification.equal(ax::notification::ui_element_destroyed()) { 100 | EventType::ElementDestroyed 101 | } else if notification.equal(ax::notification::menu_opened()) { 102 | EventType::MenuOpened 103 | } else if notification.equal(ax::notification::menu_closed()) { 104 | EventType::MenuClosed 105 | } else if notification.equal(ax::notification::menu_item_selected()) { 106 | EventType::MenuItemSelected 107 | } else if notification.equal(ax::notification::selected_text_changed()) { 108 | EventType::SelectedTextChanged 109 | } else if notification.equal(ax::notification::title_changed()) { 110 | EventType::TitleChanged 111 | } else { 112 | info!(%notification_name, "ignoring unhandled ax notification"); 113 | return; 114 | }; 115 | 116 | // Extract contextual data from the element 117 | match extract_event_data(&element) { 118 | Ok((app_info, window_info, element_details)) => { 119 | let event = UiEvent { 120 | event_type, 121 | timestamp: Utc::now(), 122 | application: app_info, 123 | window: window_info, 124 | element: element_details, 125 | event_specific_data: None, // Populate if needed 126 | }; 127 | 128 | // Send the event (non-blocking) 129 | if let Err(e) = sender.try_send(event) { 130 | error!(error = %e, "failed to send event from callback"); 131 | } 132 | 133 | info!(%notification_name, "event sent"); 134 | } 135 | Err(e) => { 136 | error!(error = %e, "failed to extract event data in callback"); 137 | } 138 | } 139 | }); 140 | }); 141 | } 142 | 143 | // Convert common CF types to serde_json::Value 144 | fn cf_value_to_json(cf_value: &cf::Type) -> Option { 145 | ar_pool(|| { 146 | let type_id = cf_value.get_type_id(); 147 | // Remove Ok wrapping, return Option directly 148 | if type_id == cf::String::type_id() { 149 | let s_ptr = cf_value as *const cf::Type as *const cf::String; 150 | Some(serde_json::Value::String(unsafe { &*s_ptr }.to_string())) 151 | } else if type_id == cf::Number::type_id() { 152 | let n_ptr = cf_value as *const cf::Type as *const cf::Number; 153 | let n_number = unsafe { &*n_ptr }; 154 | if n_number.is_float_type() { 155 | n_number 156 | .to_f64() 157 | .and_then(|f| serde_json::Number::from_f64(f)) 158 | .map(serde_json::Value::Number) 159 | } else { 160 | n_number 161 | .to_i64() 162 | .map(|i| i.into()) 163 | .map(serde_json::Value::Number) 164 | } 165 | } else if type_id == cf::Boolean::type_id() { 166 | let b_ptr = cf_value as *const cf::Type as *const cf::Boolean; 167 | Some(serde_json::json!(unsafe { &*b_ptr }.value())) 168 | } else if type_id == cf::Date::type_id() { 169 | let d_ptr = cf_value as *const cf::Type as *const cf::Date; 170 | let d_date = unsafe { &*d_ptr }; 171 | let abs_time = d_date.abs_time(); // f64 seconds since epoch 172 | let unix_timestamp_secs = CF_ABSOLUTE_TIME_EPOCH_OFFSET + abs_time as i64; 173 | let unix_timestamp_nanos = (abs_time.fract() * 1_000_000_000.0) as u32; 174 | // Use Option chaining instead of match 175 | chrono::DateTime::from_timestamp(unix_timestamp_secs, unix_timestamp_nanos) 176 | .map(|datetime| serde_json::json!(datetime.to_rfc3339())) 177 | } else { 178 | warn!(cf_type_id = type_id, description = ?cf_value.desc(), "unhandled cf type for element value"); 179 | None 180 | } 181 | }) 182 | } 183 | 184 | // Helper to safely get a string attribute from an AXUIElement 185 | fn get_string_attribute(element: &ax::UiElement, attribute: &ax::Attr) -> Option { 186 | ar_pool(|| { 187 | element.attr_value(attribute).ok().and_then(|val| { 188 | if val.get_type_id() == cf::String::type_id() { 189 | let s_ptr = &*val as *const cf::Type as *const cf::String; 190 | let string = unsafe { &*s_ptr }.to_string(); 191 | if !string.is_empty() { 192 | Some(string) 193 | } else { 194 | None 195 | } 196 | } else { 197 | None 198 | } 199 | }) 200 | }) 201 | } 202 | 203 | // Helper to get position 204 | fn get_element_position(element: &ax::UiElement) -> Option { 205 | ar_pool(|| { 206 | element.attr_value(ax::attr::pos()).ok().and_then(|val| { 207 | // Check if the value is an AXValue encoding a CGPoint 208 | if val.get_type_id() == ax::Value::type_id() { 209 | let value_ptr = &*val as *const cf::Type as *const ax::Value; 210 | let ax_value = unsafe { &*value_ptr }; 211 | // Use cg_point() and rely on get_value's return 212 | if let Some(point) = ax_value.cg_point() { 213 | Some(Position { 214 | x: point.x, 215 | y: point.y, 216 | }) 217 | } else { 218 | // warn!("failed to extract cg_point or wrong type"); 219 | None 220 | } 221 | } else { 222 | // warn!("attribute was not ax_value, type_id: {}", val.get_type_id()); 223 | None 224 | } 225 | }) 226 | }) 227 | } 228 | 229 | // Helper to get size 230 | fn get_element_size(element: &ax::UiElement) -> Option { 231 | ar_pool(|| { 232 | element.attr_value(ax::attr::size()).ok().and_then(|val| { 233 | if val.get_type_id() == ax::Value::type_id() { 234 | let value_ptr = &*val as *const cf::Type as *const ax::Value; 235 | let ax_value = unsafe { &*value_ptr }; 236 | // Use cg_size() and rely on get_value's return 237 | if let Some(size) = ax_value.cg_size() { 238 | Some(Size { 239 | width: size.width, 240 | height: size.height, 241 | }) 242 | } else { 243 | // warn!("failed to extract cg_size or wrong type"); 244 | None 245 | } 246 | } else { 247 | // warn!("attribute was not ax_value, type_id: {}", val.get_type_id()); 248 | None 249 | } 250 | }) 251 | }) 252 | } 253 | 254 | // Enhanced helper - NOT wrapped entirely in ar_pool anymore 255 | fn extract_event_data( 256 | element: &ax::UiElement, 257 | ) -> Result<( 258 | Option, 259 | Option, 260 | Option, 261 | )> { 262 | let pid = element.pid().ok(); // Use ok() to handle potential error 263 | 264 | // --- Application Info --- 265 | let app_info: Option = pid.and_then(|p| { 266 | ar_pool(|| { 267 | // Pool for NS object access 268 | let app = ns::running_application::RunningApp::with_pid(p); 269 | let app_name = app.and_then(|a| a.localized_name()).map(|s| s.to_string()); 270 | Some(ApplicationInfo { 271 | name: app_name, 272 | pid: Some(p), 273 | }) 274 | }) 275 | }); 276 | 277 | // --- Window Info --- 278 | // ar_pool might be needed here due to element access & retention 279 | let window_info: Option = ar_pool(|| { 280 | // 1. Check if the element itself is the window 281 | let mut window_element = element 282 | .role() 283 | .ok() 284 | .filter(|r| r.equal(ax::role::window())) 285 | .map(|_| element.retained()); // Retain if it's a window 286 | 287 | // 2. If not, traverse parents 288 | if window_element.is_none() { 289 | window_element = std::iter::successors(Some(element.retained()), |el| el.parent().ok()) 290 | .find(|el| { 291 | el.role() 292 | .map(|r| r.equal(ax::role::window())) 293 | .unwrap_or(false) // Default to false if role fetch fails 294 | }); 295 | } 296 | 297 | // 3. If still no window, try getting the app's focused window 298 | if window_element.is_none() { 299 | if let Some(p) = pid { 300 | // Requires access to AX API inside pool 301 | let app_element = ax::UiElement::with_app_pid(p); 302 | window_element = app_element 303 | .attr_value(ax::attr::focused_window()) 304 | .ok() 305 | .and_then(|val| { 306 | // The attribute value should be an AXUIElement 307 | if val.get_type_id() == ax::UiElement::type_id() { 308 | let win_ptr = &*val as *const cf::Type as *const ax::UiElement; 309 | Some(unsafe { &*win_ptr }.retained()) // Retain the window element 310 | } else { 311 | None 312 | } 313 | }); 314 | } 315 | } 316 | 317 | // Extract title if we found a window element 318 | window_element.and_then(|win| { 319 | let title = get_string_attribute(&win, ax::attr::title()); 320 | Some(WindowInfo { title, id: None }) 321 | // Note: win (Retained) goes out of scope here, pool handles release 322 | }) 323 | }); 324 | 325 | // --- Element Details --- 326 | // These helpers use ar_pool internally 327 | let role = ar_pool(|| element.role().map(|r| r.to_string()).ok()); 328 | let identifier = get_string_attribute(element, ax::attr::title()) 329 | .or_else(|| get_string_attribute(element, ax::attr::desc())) 330 | .or_else(|| get_string_attribute(element, ax::attr::help())); 331 | let value = ar_pool(|| { 332 | element 333 | .attr_value(ax::attr::value()) 334 | .ok() 335 | .and_then(|cf_val| cf_value_to_json(&*cf_val)) 336 | }); 337 | let position = get_element_position(element); 338 | let size = get_element_size(element); 339 | 340 | let element_details = ElementDetails { 341 | role, 342 | identifier, 343 | value, 344 | position, 345 | size, 346 | }; 347 | 348 | Ok((app_info, window_info, Some(element_details))) // Final Result constructed outside ar_pool 349 | } 350 | 351 | // Function called by NSWorkspace notification observer when an app activates 352 | // Changed signature to accept &ns::running_application::RunningApp 353 | fn handle_activation(app: &ns::running_application::RunningApp, sender: &mpsc::Sender) { 354 | ar_pool(|| { 355 | let pid = app.pid(); 356 | let app_name = app.localized_name().map(|s| s.to_string()); 357 | info!(app_name = ?app_name, pid, "activated app"); 358 | 359 | // --- Send ApplicationActivated Event --- 360 | let event = UiEvent { 361 | event_type: EventType::ApplicationActivated, 362 | timestamp: Utc::now(), 363 | application: Some(ApplicationInfo { 364 | name: app_name.clone(), 365 | pid: Some(pid), 366 | }), 367 | window: None, 368 | element: None, 369 | event_specific_data: None, 370 | }; 371 | if let Err(e) = sender.try_send(event) { 372 | error!(error = %e, "failed to send activation event"); 373 | } 374 | 375 | CURRENT_AX_OBSERVER.with(|cell| { 376 | if cell.borrow().is_some() { 377 | info!(pid = pid, "dropping old axobserver"); 378 | *cell.borrow_mut() = None; 379 | } 380 | 381 | // Get app element using pid 382 | let app_element = ax::UiElement::with_app_pid(pid); 383 | 384 | // Retry Observer::new_for_app 385 | match ax::Observer::with_cb(pid, observer_callback) { 386 | Ok(mut observer) => { // observer should be Retained 387 | info!(pid, "created new axobserver"); 388 | 389 | let notifications_to_add = [ 390 | ax::notification::focused_window_changed(), 391 | ax::notification::focused_ui_element_changed(), 392 | ax::notification::value_changed(), 393 | // Added notifications: 394 | ax::notification::window_created(), 395 | ax::notification::window_moved(), 396 | ax::notification::window_resized(), 397 | ax::notification::ui_element_destroyed(), 398 | ax::notification::menu_opened(), 399 | ax::notification::menu_closed(), 400 | ax::notification::menu_item_selected(), 401 | ax::notification::selected_text_changed(), 402 | ax::notification::title_changed(), 403 | ]; 404 | 405 | for notif_name in notifications_to_add { 406 | // Observer expects &cf::String for notification name 407 | // Call add_notification on the observer instance 408 | match observer.add_notification(&app_element, notif_name, std::ptr::null_mut()) { 409 | Ok(_) => info!(pid, notification = %notif_name.to_string(), "added notification"), 410 | Err(e) => error!(pid, notification = %notif_name.to_string(), error = ?e, "failed to add notification"), 411 | } 412 | } 413 | 414 | // Call run_loop_source on the observer instance 415 | let source = observer.run_loop_src(); // Should be Retained 416 | source.invalidate(); 417 | // Use add_source with as_ref() 418 | cf::RunLoop::current().add_src(source, cf::RunLoopMode::default()); 419 | info!(pid, "added run loop source for observer"); 420 | 421 | // Store the observer 422 | *cell.borrow_mut() = Some((observer, app_element)); 423 | } 424 | Err(e) => { 425 | error!(pid, error = ?e, "failed to create axobserver for pid"); 426 | } 427 | } 428 | }); 429 | }); 430 | } 431 | 432 | pub struct MacosListener { 433 | tx: mpsc::Sender, 434 | ax_observer: Mutex>>, 435 | ws_observer_token: Mutex>>, 436 | // self reference pointer 437 | ptr: *mut std::ffi::c_void, 438 | _pin: PhantomPinned, 439 | } 440 | 441 | // all processing on main thread 442 | unsafe impl Send for MacosListener {} 443 | unsafe impl Sync for MacosListener {} 444 | 445 | impl MacosListener { 446 | fn handle_ui_event(&self, element: &mut ax::UiElement, n: &ax::Notification) { 447 | // Map AX notifications (cf::String constants) to our event types 448 | use ax::notification as axn; 449 | let event_type = match () { 450 | _ if n == axn::focused_window_changed() => EventType::WindowFocused, 451 | _ if n == axn::focused_ui_element_changed() => EventType::ElementFocused, 452 | _ if n == axn::value_changed() => EventType::ValueChanged, 453 | _ if n == axn::window_created() => EventType::WindowCreated, 454 | _ if n == axn::window_moved() => EventType::WindowMoved, 455 | _ if n == axn::window_resized() => EventType::WindowResized, 456 | _ if n == axn::ui_element_destroyed() => EventType::ElementDestroyed, 457 | _ if n == axn::menu_opened() => EventType::MenuOpened, 458 | _ if n == axn::menu_closed() => EventType::MenuClosed, 459 | _ if n == axn::menu_item_selected() => EventType::MenuItemSelected, 460 | _ if n == axn::selected_text_changed() => EventType::SelectedTextChanged, 461 | _ if n == axn::title_changed() => EventType::TitleChanged, 462 | _ => return, 463 | }; 464 | 465 | // Extract contextual data from the element 466 | match extract_event_data(&element) { 467 | Ok((app_info, window_info, element_details)) => { 468 | let event = UiEvent { 469 | event_type, 470 | timestamp: Utc::now(), 471 | application: app_info, 472 | window: window_info, 473 | element: element_details, 474 | event_specific_data: None, // Populate if needed 475 | }; 476 | 477 | println!("{event:?}"); 478 | 479 | // Send the event (non-blocking) 480 | if let Err(e) = self.tx.try_send(event) { 481 | error!(error = %e, "failed to send event from callback"); 482 | } 483 | } 484 | Err(e) => { 485 | error!(error = %e, "failed to extract event data in callback"); 486 | } 487 | } 488 | } 489 | 490 | #[inline] 491 | fn handle_app_activation(self: &Pin>, n: &ns::Notification) { 492 | let Some(user_info) = n.user_info() else { 493 | return println!("no user info"); 494 | }; 495 | let Some(app) = user_info.get(ns::workspace::notification::app_key()) else { 496 | return println!("no active app"); 497 | }; 498 | 499 | let Some(app) = app.try_cast(ns::RunningApp::cls()) else { 500 | return println!("wrong app class"); 501 | }; 502 | 503 | let pid = app.pid(); 504 | 505 | let app_name = app.localized_name().map(|s| s.to_string()); 506 | info!(app_name = ?app_name, pid, "activated app"); 507 | 508 | // --- Send ApplicationActivated Event --- 509 | let event = UiEvent { 510 | event_type: EventType::ApplicationActivated, 511 | timestamp: Utc::now(), 512 | application: Some(ApplicationInfo { 513 | name: app_name, 514 | pid: Some(pid), 515 | }), 516 | window: None, 517 | element: None, 518 | event_specific_data: None, 519 | }; 520 | if let Err(e) = self.tx.try_send(event) { 521 | error!(error = %e, "failed to send activation event"); 522 | } 523 | 524 | { 525 | // release current observer 526 | self.ax_observer.lock().unwrap().take(); 527 | } 528 | 529 | let app_element = ax::UiElement::with_app_pid(pid); 530 | 531 | // Retry Observer::new_for_app 532 | match ax::Observer::with_cb(pid, observer_callback2) { 533 | Ok(mut observer) => { 534 | // observer should be Retained 535 | info!(pid, "created new axobserver"); 536 | use ax::notification as axn; 537 | let notifications_to_add = [ 538 | axn::focused_window_changed(), 539 | axn::focused_ui_element_changed(), 540 | axn::value_changed(), 541 | // Added notifications: 542 | axn::window_created(), 543 | axn::window_moved(), 544 | axn::window_resized(), 545 | axn::ui_element_destroyed(), 546 | axn::menu_opened(), 547 | axn::menu_closed(), 548 | axn::menu_item_selected(), 549 | axn::selected_text_changed(), 550 | axn::title_changed(), 551 | ]; 552 | 553 | for notif_name in notifications_to_add { 554 | // Observer expects &cf::String for notification name 555 | // Call add_notification on the observer instance 556 | match observer.add_notification(&app_element, notif_name, self.ptr) { 557 | Ok(_) => { 558 | info!(pid, notification = %notif_name.to_string(), "added notification") 559 | } 560 | Err(e) => { 561 | error!(pid, notification = %notif_name.to_string(), error = ?e, "failed to add notification") 562 | } 563 | } 564 | } 565 | 566 | // Call run_loop_source on the observer instance 567 | let source = observer.run_loop_src(); // Should be Retained 568 | // Reply to comment above: No, it is get rule there 569 | cf::RunLoop::main().add_src(source, cf::RunLoopMode::default()); 570 | info!(pid, "added run loop source for observer"); 571 | 572 | // Store the observer 573 | { 574 | // lock scope 575 | let _ = self.ax_observer.lock().unwrap().insert(observer); 576 | } 577 | } 578 | Err(e) => { 579 | error!(pid, error = ?e, "failed to create axobserver for pid"); 580 | } 581 | } 582 | } 583 | 584 | pub fn new_on_main_thread(tx: mpsc::Sender) -> Result>> { 585 | // debug_assert!(ns::Thread::is_main()); 586 | // TODO: assert on main thread 587 | info!("checking accessibility permissions..."); 588 | if !ax::is_process_trusted_with_prompt(true) { 589 | error!("accessibility permissions not granted"); 590 | return Err(anyhow!("accessibility permissions not granted by user")); 591 | } 592 | 593 | let data = Self { 594 | tx, 595 | ax_observer: Default::default(), 596 | ws_observer_token: Default::default(), 597 | ptr: std::ptr::null_mut(), 598 | _pin: PhantomPinned, 599 | }; 600 | 601 | let pin = unsafe { 602 | let mut arc = std::sync::Arc::new(data); 603 | let data = Arc::get_mut(&mut arc).unwrap_unchecked(); 604 | data.ptr = data as *const Self as *mut _; 605 | Pin::new_unchecked(arc) 606 | }; 607 | 608 | let block_pin = pin.clone(); 609 | 610 | let mut nc = ns::Workspace::shared().notification_center(); 611 | let token = nc.add_observer( 612 | ns::workspace::notification::did_activate_app(), 613 | None, 614 | None, 615 | move |n: &ns::Notification| { 616 | block_pin.handle_app_activation(n); 617 | }, 618 | ); 619 | 620 | { 621 | // lock scope 622 | let _ = pin.ws_observer_token.lock().unwrap().insert(token); 623 | } 624 | 625 | Ok(pin) 626 | } 627 | } 628 | 629 | impl PlatformListener for MacosListener { 630 | fn run(&self, sender: mpsc::Sender) -> Result<()> { 631 | if !ax::is_process_trusted_with_prompt(true) { 632 | error!("accessibility permissions not granted"); 633 | return Err(anyhow!("accessibility permissions not granted by user")); 634 | } 635 | info!( 636 | "macos listener starting run() on thread {:?}...", 637 | std::thread::current().id() 638 | ); 639 | 640 | // Store the sender for the callbacks 641 | SENDER.with(|cell| { 642 | *cell.borrow_mut() = Some(sender.clone()); 643 | }); 644 | info!("sender stored in thread_local"); 645 | 646 | // --- Setup NSWorkspace Observer for App Activation --- 647 | let mut center = ns::Workspace::shared().notification_center(); 648 | let sender_callback = sender.clone(); 649 | 650 | // Define the callback closure for NSWorkspace notifications 651 | let workspace_callback = move |notification: &ns::Notification| { 652 | ar_pool(|| { 653 | info!(notification_name = ?notification.name(), "received workspace notification"); 654 | 655 | let apps = ns::Workspace::shared().running_apps(); 656 | let active_app = apps.iter().find(|app| app.is_active()).unwrap(); 657 | 658 | handle_activation(active_app, &sender_callback); 659 | }); 660 | }; // Copy the block to the heap 661 | 662 | // Add the observer to the notification center 663 | // TODO this does not work rn - so only recording the active app at startup 664 | let token = center.add_observer( 665 | // Use actual static method name for notification 666 | &ns::NotificationName::with_str("NSWorkspaceDidActivateApplicationNotification"), 667 | None, // Observe notifications from any object 668 | None, // Pass None for OperationQueue 669 | workspace_callback, 670 | ); 671 | let retained_token = token.retained(); // Retain the token 672 | 673 | // Store the token for cleanup 674 | WORKSPACE_OBSERVER_TOKEN.with(|cell| { 675 | *cell.borrow_mut() = Some(retained_token); 676 | }); 677 | info!("added ns workspace observer for app activation"); 678 | 679 | // --- Initial Activation Handling --- 680 | // Handle the currently active application immediately 681 | let apps = ns::Workspace::shared().running_apps(); 682 | let active_app = apps.iter().find(|app| app.is_active()).unwrap(); 683 | 684 | handle_activation(&active_app, &sender); 685 | 686 | // --- Start Run Loop --- 687 | info!("starting cf run loop (blocking current thread)... Awaiting UI events."); 688 | cf::RunLoop::run(); // This blocks the thread 689 | 690 | warn!("cf run loop finished! Performing cleanup (this is unexpected)."); 691 | // Cleanup for NSWorkspace observer 692 | WORKSPACE_OBSERVER_TOKEN.with(|cell| { 693 | if let Some(token) = cell.borrow_mut().take() { 694 | ar_pool(|| { 695 | ns::Workspace::shared() 696 | .notification_center() 697 | .remove_observer(&token); 698 | }); 699 | info!("removed ns workspace observer"); 700 | } 701 | }); 702 | // Cleanup for AXObserver (managed by handle_activation, but clear it finally) 703 | CURRENT_AX_OBSERVER.with(|cell| { 704 | if cell.borrow().is_some() { 705 | info!("dropping final axobserver during cleanup"); 706 | *cell.borrow_mut() = None; 707 | } 708 | }); 709 | // Cleanup sender 710 | SENDER.with(|cell| *cell.borrow_mut() = None); 711 | 712 | Ok(()) // Should technically not be reached if RunLoop runs forever 713 | } 714 | } 715 | -------------------------------------------------------------------------------- /ui-events/src/platform/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::event::UiEvent; 2 | use anyhow::Result; 3 | use tokio::sync::mpsc; 4 | use tracing::info; 5 | 6 | // Modules for each platform 7 | #[cfg(target_os = "linux")] 8 | pub mod linux; 9 | #[cfg(target_os = "macos")] 10 | pub mod macos; 11 | #[cfg(target_os = "windows")] 12 | pub mod windows; 13 | 14 | /// Common trait for platform-specific listeners. 15 | /// Must be Send to allow spawning in a separate thread/task. 16 | pub trait PlatformListener: Send { 17 | // Note: Needs Send + Sync bounds if used across threads without careful handling 18 | // Using blocking run for now to simplify CFRunLoop integration on macOS. 19 | fn run(&self, sender: mpsc::Sender) -> Result<()>; 20 | } 21 | 22 | pub fn listener_run(tx: mpsc::Sender) { 23 | #[cfg(target_os = "macos")] 24 | { 25 | use cidre::ns; 26 | macos::MacosListener::new_on_main_thread(tx).unwrap(); 27 | ns::App::shared().run() 28 | } 29 | } 30 | 31 | /// Creates the appropriate platform listener. 32 | pub fn create_listener() -> Result> { 33 | #[cfg(target_os = "macos")] 34 | { 35 | // use macos::MacosListener; 36 | info!("creating macos listener"); 37 | todo!(); 38 | // let listener = MacosListener::new()?; 39 | // Ok(Box::new(listener)) 40 | } 41 | #[cfg(target_os = "windows")] 42 | { 43 | use windows::WindowsListener; 44 | info!("creating windows listener"); 45 | let listener = WindowsListener::new()?; 46 | Ok(Box::new(listener)) 47 | } 48 | #[cfg(target_os = "linux")] 49 | { 50 | use linux::LinuxListener; 51 | info!("creating linux listener"); 52 | let listener = LinuxListener::new()?; 53 | Ok(Box::new(listener)) 54 | } 55 | #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))] 56 | { 57 | anyhow::bail!("unsupported platform") 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /ui-events/src/platform/windows.rs: -------------------------------------------------------------------------------- 1 | #![cfg(target_os = "windows")] 2 | 3 | use super::PlatformListener; 4 | use crate::event::UiEvent; 5 | use anyhow::Result; 6 | use tokio::sync::mpsc; 7 | 8 | pub struct WindowsListener {} 9 | 10 | impl WindowsListener { 11 | pub fn new() -> Result { 12 | anyhow::bail!("windows listener not implemented") 13 | } 14 | } 15 | 16 | impl PlatformListener for WindowsListener { 17 | fn run(&self, _sender: mpsc::Sender) -> Result<()> { 18 | println!("windows listener run (unimplemented)"); 19 | // TODO: Implement using UI Automation 20 | anyhow::bail!("windows listener not implemented") 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /ui-events/src/server.rs: -------------------------------------------------------------------------------- 1 | // Placeholder for websocket server implementation 2 | 3 | use crate::event::UiEvent; 4 | use anyhow::{Context, Result}; 5 | use futures_util::{SinkExt, StreamExt}; 6 | use std::net::SocketAddr; 7 | use tokio::net::{TcpListener, TcpStream}; 8 | use tokio::sync::{broadcast, mpsc}; 9 | use tokio_tungstenite::{accept_async, tungstenite::protocol::Message}; 10 | use tracing::{debug, error, info, warn}; 11 | 12 | async fn handle_connection( 13 | peer: SocketAddr, 14 | stream: TcpStream, 15 | mut broadcast_rx: broadcast::Receiver, // Receiver for serialized events 16 | ) -> Result<()> { 17 | let ws_stream = accept_async(stream) 18 | .await 19 | .context("error during websocket handshake")?; 20 | info!(%peer, "new websocket connection established"); 21 | 22 | let (mut ws_sender, mut ws_receiver) = ws_stream.split(); 23 | 24 | loop { 25 | tokio::select! { 26 | // Forward broadcast messages (serialized UI events) to the client 27 | Ok(msg_str) = broadcast_rx.recv() => { 28 | if let Err(e) = ws_sender.send(Message::Text(msg_str)).await { 29 | // Error likely means client disconnected 30 | warn!(%peer, error = %e, "failed to send message to client, disconnecting"); 31 | break; // Exit loop to close connection 32 | } 33 | } 34 | // Handle messages *from* the client (e.g., ping/pong, close) 35 | Some(msg_result) = ws_receiver.next() => { 36 | match msg_result { 37 | Ok(msg) => { 38 | match msg { 39 | Message::Text(_) | Message::Binary(_) => { 40 | // Ignore data messages from client for now 41 | debug!(%peer, "received data message (ignoring)"); 42 | } 43 | Message::Ping(ping_data) => { 44 | debug!(%peer, "received ping, sending pong"); 45 | if let Err(e) = ws_sender.send(Message::Pong(ping_data)).await { 46 | warn!(%peer, error = %e, "failed to send pong, disconnecting"); 47 | break; 48 | } 49 | } 50 | Message::Close(_) => { 51 | info!(%peer, "received close frame, closing connection"); 52 | break; // Exit loop 53 | } 54 | Message::Pong(_) => { 55 | // Usually we only send pings and expect pongs 56 | debug!(%peer, "received unsolicited pong (ignoring)"); 57 | } 58 | Message::Frame(_) => { 59 | // Low-level frame, ignore in typical usage 60 | } 61 | } 62 | } 63 | Err(e) => { 64 | // Tungstenite error (connection closed, protocol error, etc.) 65 | warn!(%peer, error = %e, "websocket error, closing connection"); 66 | break; // Exit loop 67 | } 68 | } 69 | } 70 | else => { 71 | // Both streams have potentially ended 72 | break; 73 | } 74 | } 75 | } 76 | 77 | info!(%peer, "websocket connection closed"); 78 | // Attempt to close the sender cleanly (optional) 79 | let _ = ws_sender.close().await; 80 | Ok(()) 81 | } 82 | 83 | pub async fn run_server(port: u16, mut rx: mpsc::Receiver) -> Result<()> { 84 | let addr = format!("127.0.0.1:{}", port); 85 | let listener = TcpListener::bind(&addr) 86 | .await 87 | .context(format!("failed to bind websocket server to {}", addr))?; 88 | info!("websocket server listening on ws://{}", addr); 89 | 90 | // Broadcast channel for distributing serialized events to clients 91 | // Capacity should be chosen based on expected event volume and client processing speed 92 | let (broadcast_tx, _) = broadcast::channel::(100); // Sender and a placeholder receiver 93 | 94 | // Task to receive UI events, serialize them, and broadcast 95 | let broadcaster_tx = broadcast_tx.clone(); // Clone sender for the task 96 | tokio::spawn(async move { 97 | info!("event broadcaster task started"); 98 | while let Some(event) = rx.recv().await { 99 | match serde_json::to_string(&event) { 100 | Ok(json_str) => { 101 | // Send to broadcast channel. If no clients are listening, the error is ignored. 102 | if let Err(e) = broadcaster_tx.send(json_str) { 103 | // This error typically means no clients are connected. 104 | // It can be noisy, so maybe log only once or use debug level. 105 | debug!("broadcast send error (no receivers?): {}", e); 106 | } 107 | } 108 | Err(e) => { 109 | error!(error = %e, "failed to serialize uievent to json"); 110 | // Decide if you want to skip the event or panic 111 | } 112 | } 113 | } 114 | info!("event broadcaster task finished (mpsc channel closed)"); 115 | // rx is dropped here when the loop finishes (sender in main/listener dropped) 116 | }); 117 | 118 | // Main loop to accept incoming connections 119 | loop { 120 | match listener.accept().await { 121 | Ok((stream, peer)) => { 122 | info!(%peer, "accepting new tcp connection"); 123 | let broadcast_rx = broadcast_tx.subscribe(); // Create a receiver for this specific client 124 | tokio::spawn(async move { 125 | if let Err(e) = handle_connection(peer, stream, broadcast_rx).await { 126 | error!(%peer, error = %e, "error handling connection"); 127 | } 128 | }); 129 | } 130 | Err(e) => { 131 | error!(error = %e, "failed to accept incoming tcp connection"); 132 | // Consider if this error is recoverable or requires stopping the server 133 | // For now, just log and continue trying to accept 134 | } 135 | } 136 | } 137 | 138 | // Note: The loop above runs indefinitely. In a real application, you'd 139 | // want a mechanism for graceful shutdown (e.g., listening for a signal 140 | // or another channel message) to break the loop and allow tasks to finish. 141 | // Ok(()) // Unreachable in the current form 142 | } 143 | --------------------------------------------------------------------------------