diff --git a/apps/mobile/.gitignore b/apps/mobile/.gitignore new file mode 100644 index 00000000..411c4c6f --- /dev/null +++ b/apps/mobile/.gitignore @@ -0,0 +1,33 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules + +# Next.js +.next/ +out/ +*.tsbuildinfo +next-env.d.ts + +# devup-ui generated +df/ +.df/ + +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/apps/mobile/README.md b/apps/mobile/README.md new file mode 100644 index 00000000..226e9264 --- /dev/null +++ b/apps/mobile/README.md @@ -0,0 +1,75 @@ +# Tauri + React + Typescript + +This template should help get you started developing with Tauri, React and Typescript in Vite. + +## Recommended IDE Setup + +- [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) + +## 모바일 프로젝트 준비 (clone 후 최초 1회) + +`src-tauri/gen/`(Xcode·Android 네이티브 프로젝트)은 전부 생성물이라 저장소에 +커밋하지 않습니다. clone 후 빌드 전에 아래로 재생성합니다. + +```bash +bun -F mobile ios:init # iOS — xcodegen, cocoapods 필요 +bun -F mobile android:init # Android — Android SDK/NDK 필요 +``` + +`ios:init`/`android:init`은 각각 `tauri ios/android init` 실행 직후 +`bun run icons`(= `tauri icon app-icon.json`)를 자동으로 이어서 실행합니다. + +⚠️ **왜 필요한가**: `tauri ios/android init`은 `gen/`을 만들 때 항상 +**기본 Tauri 아이콘**을 심습니다. 이후 `tauri icon`을 실행하지 않으면 그 +기본 아이콘이 그대로 남습니다. 특히 iOS 아이콘은 `icons/` 밑이 아니라 +`gen/apple/Assets.xcassets/AppIcon.appiconset/`에 **직접** 쓰여지므로 +(gen이 이미 존재할 때만 그렇게 동작합니다), init 이후 반드시 `tauri icon`을 +다시 실행해야 합니다. `ios:init`/`android:init` 스크립트가 이 순서를 강제합니다. + +소스오브트루스는 [`app-icon.svg`](./app-icon.svg)(아이콘 원본)와 +[`app-icon.json`](./app-icon.json)(배경색 `#EFEEEB` 지정 manifest, +`tauri icon`의 입력)입니다. Android 어댑티브 아이콘 배경색은 `--ios-color` +CLI 옵션(기본값 `#fff`)이 그대로 적용되므로, 색을 코드 여러 곳에 흩어두지 +않도록 `app-icon.json`의 `bg_color`로 고정했습니다. 아이콘을 다시 만들 +때는 항상 `bun run icons`를 사용하고 `tauri icon app-icon.svg`를 직접 +호출하지 마세요(직접 호출하면 배경색 플래그가 빠져 Android 배경이 `#fff`로 +어긋납니다). + +## iOS 시뮬레이터 개발 (정적 빌드 모드) + +```bash +bun -F mobile ios:dev # next build(정적 export) 후 시뮬레이터 실행 +``` + +⚠️ **iOS에서 Next dev 서버(HMR) 모드를 쓰지 않는 이유**: Tauri CLI는 iOS +dev에서 `devUrl`(dev 서버)을 항상 `tauri://localhost` 커스텀 스킴으로 +**프록시**합니다(IP 리터럴 devUrl로 바꿔도 동일). 이 환경에서 Next 16 +(Turbopack) dev 런타임은 HTML·flight 데이터가 전부 도착해도 **hydration을 +끝내지 못해** 화면은 보이지만 모든 버튼이 반응하지 않습니다. 반면 정적 +export(`out/`)를 임베드해 서빙하면 동일한 `tauri://` 스킴에서도 hydration· +버튼·WASM 점역이 모두 정상 동작함을 확인했습니다. + +그래서 `ios:dev`는 `--config src-tauri/tauri.sim.conf.json` 오버레이로 +`devUrl`을 제거하고 `beforeDevCommand`를 `bun run build`로 바꿔, 매 실행마다 +정적 export를 새로 빌드해 임베드합니다. HMR은 없지만 동작이 보장됩니다. + +- **UI 빠른 반복**: 웹 브라우저에서 `bun -F mobile dev` (HMR 정상 동작) +- **시뮬레이터 확인**: `bun -F mobile ios:dev` (프론트 변경 시 재실행 필요) + +## iOS 코드 서명 (개발자 팀) + +iOS **실기기 빌드/배포**(`tauri ios build`, `tauri ios run`)는 Apple 코드 서명이 +필요하며, Apple 개발자 **팀 ID**(10자리, 조직 단위 공유 값)를 지정해야 합니다. +아래 둘 중 하나로 제공합니다: + +- 환경변수: `APPLE_DEVELOPMENT_TEAM=XXXXXXXXXX bun -F mobile tauri ios build` +- 또는 [`src-tauri/tauri.conf.json`](./src-tauri/tauri.conf.json)의 + `bundle.iOS.developmentTeam`에 실제 팀 ID 기입 + +> ⚠️ 이 값을 **빈 문자열 `""`로 두면 tauri 가 거부**합니다 +> (`apple.development-team is empty`). 팀 ID가 정해지기 전에는 필드를 **아예 생략**하세요. + +- **시뮬레이터**(`tauri ios dev`)는 ad-hoc 서명이라 팀 없이도 실행됩니다 + (`developmentTeam` 필드를 생략한 상태면 그대로 동작). +- 팀 ID 확인: Xcode → Settings → Accounts → 계정 선택 → 팀 목록의 10자리 ID, + 또는 → Membership → Team ID. diff --git a/apps/mobile/app-icon.json b/apps/mobile/app-icon.json new file mode 100644 index 00000000..8fe1f0e9 --- /dev/null +++ b/apps/mobile/app-icon.json @@ -0,0 +1,4 @@ +{ + "default": "app-icon.svg", + "bg_color": "#EFEEEB" +} diff --git a/apps/mobile/app-icon.svg b/apps/mobile/app-icon.svg new file mode 100644 index 00000000..90fa00bc --- /dev/null +++ b/apps/mobile/app-icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/apps/mobile/devup.json b/apps/mobile/devup.json new file mode 100644 index 00000000..24abd55e --- /dev/null +++ b/apps/mobile/devup.json @@ -0,0 +1,131 @@ +{ + "theme": { + "colors": { + "light": { + "primary": "#30302E", + "primaryHover": "#454543", + "text": "#2D2D2C", + "background": "#F3F2EF", + "containerBackground": "#FFFFFF", + "border": "#DEDDD9", + "caption": "#7A7976", + "error": "#B42318", + "focus": "#006BFF", + "base": "#FFFFFF", + "negativeBase": "#000000", + "disabledBackground": "#DDDCD9", + "disabledText": "#82817E", + "emptyBraille": "#B9B8B5", + "sidebarBackground": "#383735", + "sidebarSelected": "#575654", + "sidebarBorder": "#4A4947", + "sidebarText": "#F8F8F7", + "sidebarCaption": "#A8A7A4", + "sidebarDisabled": "#AAA9A6", + "sidebarFooter": "#858481" + }, + "dark": { + "primary": "#30302E", + "primaryHover": "#454543", + "text": "#2D2D2C", + "background": "#F3F2EF", + "containerBackground": "#FFFFFF", + "border": "#DEDDD9", + "caption": "#7A7976", + "error": "#B42318", + "focus": "#006BFF", + "base": "#FFFFFF", + "negativeBase": "#000000", + "disabledBackground": "#DDDCD9", + "disabledText": "#82817E", + "emptyBraille": "#B9B8B5", + "sidebarBackground": "#383735", + "sidebarSelected": "#575654", + "sidebarBorder": "#4A4947", + "sidebarText": "#F8F8F7", + "sidebarCaption": "#A8A7A4", + "sidebarDisabled": "#AAA9A6", + "sidebarFooter": "#858481" + } + }, + "typography": { + "brandTitle": { + "fontFamily": "Segoe UI, Malgun Gothic, sans-serif", + "fontWeight": 700, + "fontSize": "23px", + "lineHeight": 1.25, + "letterSpacing": "-0.025em" + }, + "pageTitle": { + "fontFamily": "Segoe UI, Malgun Gothic, sans-serif", + "fontWeight": 750, + "fontSize": "34px", + "lineHeight": 1.3, + "letterSpacing": "-0.035em" + }, + "pageDescription": { + "fontFamily": "Segoe UI, Malgun Gothic, sans-serif", + "fontWeight": 400, + "fontSize": "17px", + "lineHeight": 1.55, + "letterSpacing": "-0.02em" + }, + "inputTitle": { + "fontFamily": "Segoe UI, Malgun Gothic, sans-serif", + "fontWeight": 650, + "fontSize": "16px", + "lineHeight": 1.4, + "letterSpacing": "-0.02em" + }, + "navigationTitle": { + "fontFamily": "Segoe UI, Malgun Gothic, sans-serif", + "fontWeight": 700, + "fontSize": "16px", + "lineHeight": 1.4, + "letterSpacing": "-0.02em" + }, + "body": { + "fontFamily": "Segoe UI, Malgun Gothic, sans-serif", + "fontWeight": 400, + "fontSize": "16px", + "lineHeight": 1.55, + "letterSpacing": "-0.015em" + }, + "sidebarBody": { + "fontFamily": "Segoe UI, Malgun Gothic, sans-serif", + "fontWeight": 400, + "fontSize": "14px", + "lineHeight": 1.45, + "letterSpacing": "-0.01em" + }, + "sidebarCaption": { + "fontFamily": "Segoe UI, Malgun Gothic, sans-serif", + "fontWeight": 400, + "fontSize": "13px", + "lineHeight": 1.45, + "letterSpacing": "-0.01em" + }, + "button": { + "fontFamily": "Segoe UI, Malgun Gothic, sans-serif", + "fontWeight": 650, + "fontSize": "16px", + "lineHeight": 1.2, + "letterSpacing": "-0.015em" + }, + "braille": { + "fontFamily": "Segoe UI Symbol, Noto Sans Symbols 2, monospace", + "fontWeight": 400, + "fontSize": "30px", + "lineHeight": 1.6, + "letterSpacing": "0.04em" + }, + "reverseText": { + "fontFamily": "var(--font-spoqa-han-sans-neo), Segoe UI, Malgun Gothic, sans-serif", + "fontWeight": 400, + "fontSize": "22px", + "lineHeight": 1.6, + "letterSpacing": "-0.01em" + } + } + } +} diff --git a/apps/mobile/next.config.ts b/apps/mobile/next.config.ts new file mode 100644 index 00000000..b9b639d4 --- /dev/null +++ b/apps/mobile/next.config.ts @@ -0,0 +1,33 @@ +import { DevupUI } from '@devup-ui/next-plugin' +import type { NextConfig } from 'next' + +const nextConfig: NextConfig = { + // Tauri는 정적 export 결과물(out/)을 번들링한다. + // SSR / Server Actions / 동적 route handler는 사용하지 않는다. + output: 'export', + + // 정적 export에서는 Next 이미지 최적화 서버를 쓸 수 없다. + images: { + unoptimized: true, + }, + + // react-compiler로 클라이언트 런타임 비용을 추가로 줄인다. + reactCompiler: true, + + webpack(config, { isServer, dev }) { + // braillify는 wasm-pack `--target bundler` 산출물(ESM WebAssembly)이다. + // Webpack 5는 기본적으로 WebAssembly를 비활성화하므로 직접 켜준다. + config.experiments = { ...config.experiments, asyncWebAssembly: true } + + // 정적 export 프리렌더(server 번들)에서 .wasm 경로가 어긋나 + // "Error occurred prerendering page" 가 나는 것을 방지한다. + config.output.webassemblyModuleFilename = + isServer && !dev + ? '../static/wasm/[modulehash].wasm' + : 'static/wasm/[modulehash].wasm' + + return config + }, +} + +export default DevupUI(nextConfig, {}) diff --git a/apps/mobile/package.json b/apps/mobile/package.json new file mode 100644 index 00000000..21c8d27b --- /dev/null +++ b/apps/mobile/package.json @@ -0,0 +1,36 @@ +{ + "name": "mobile", + "private": true, + "version": "0.1.0", + "scripts": { + "dev": "next dev -p 1420", + "build": "next build", + "start": "next start", + "lint": "oxlint", + "check": "tsc --noEmit && cargo check --manifest-path src-tauri/Cargo.toml", + "ios:init": "tauri ios init && bun run icons", + "ios:dev": "tauri ios dev --config src-tauri/tauri.sim.conf.json", + "ios:run": "tauri ios run", + "ios:build": "tauri ios build", + "android:init": "tauri android init && bun run icons", + "icons": "tauri icon app-icon.json", + "tauri": "tauri" + }, + "dependencies": { + "@devup-ui/react": "^1.0.37", + "@tauri-apps/plugin-clipboard-manager": "^2", + "braillify": "workspace:*", + "next": "16.2.6", + "react": "^19.2.6", + "react-dom": "^19.2.6" + }, + "devDependencies": { + "@devup-ui/next-plugin": "^1.0.77", + "@tauri-apps/cli": "^2", + "@types/node": "^25", + "@types/react": "^19", + "@types/react-dom": "^19", + "babel-plugin-react-compiler": "^1.0.0", + "typescript": "~5.8.3" + } +} diff --git a/apps/mobile/public/favicon.svg b/apps/mobile/public/favicon.svg new file mode 100644 index 00000000..152c31cf --- /dev/null +++ b/apps/mobile/public/favicon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/apps/mobile/src-tauri/.gitignore b/apps/mobile/src-tauri/.gitignore new file mode 100644 index 00000000..9f9c739e --- /dev/null +++ b/apps/mobile/src-tauri/.gitignore @@ -0,0 +1,8 @@ +# Generated by Cargo +# will have compiled files and executables +/target/ + +# Generated by Tauri +# 네이티브 프로젝트(Xcode/Android)는 전부 생성물이므로 커밋하지 않는다. +# clone 후 `tauri ios init` / `tauri android init` 로 재생성한다. (README 참고) +/gen/ diff --git a/apps/mobile/src-tauri/Cargo.lock b/apps/mobile/src-tauri/Cargo.lock new file mode 100644 index 00000000..0363f64b --- /dev/null +++ b/apps/mobile/src-tauri/Cargo.lock @@ -0,0 +1,5045 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "arboard" +version = "3.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0348a1c054491f4bfe6ab86a7b6ab1e44e45d899005de92f58b3df180b36ddaf" +dependencies = [ + "clipboard-win", + "image", + "log", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "parking_lot", + "percent-encoding", + "windows-sys 0.60.2", + "wl-clipboard-rs", + "x11rb", +] + +[[package]] +name = "atk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b" +dependencies = [ + "atk-sys", + "glib", + "libc", +] + +[[package]] +name = "atk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" +dependencies = [ + "serde_core", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + +[[package]] +name = "braillify_app" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", + "tauri", + "tauri-build", + "tauri-plugin-clipboard-manager", + "tauri-plugin-window-state", +] + +[[package]] +name = "brotli" +version = "8.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +dependencies = [ + "serde", +] + +[[package]] +name = "cairo-rs" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" +dependencies = [ + "bitflags 2.11.1", + "cairo-sys-rs", + "glib", + "libc", + "once_cell", + "thiserror 1.0.69", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "camino" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror 2.0.18", +] + +[[package]] +name = "cargo_toml" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374b7c592d9c00c1f4972ea58390ac6b18cbb6ab79011f3bdc90a0b82ca06b77" +dependencies = [ + "serde", + "toml 0.9.12+spec-1.1.0", +] + +[[package]] +name = "cc" +version = "1.2.62" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfb" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" +dependencies = [ + "byteorder", + "fnv", + "uuid", +] + +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "num-traits", + "serde", + "windows-link 0.2.1", +] + +[[package]] +name = "clipboard-win" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4" +dependencies = [ + "error-code", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "time", + "version_check", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" +dependencies = [ + "bitflags 2.11.1", + "core-foundation", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +dependencies = [ + "bitflags 2.11.1", + "core-foundation", + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "cssparser" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dae61cf9c0abb83bd659dab65b7e4e38d8236824c85f0f804f173567bda257d2" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "phf", + "smallvec", +] + +[[package]] +name = "cssparser-macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +dependencies = [ + "quote", + "syn 2.0.117", +] + +[[package]] +name = "ctor" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "352d39c2f7bef1d6ad73db6f5160efcaed66d94ef8c6c573a8410c00bf909a98" +dependencies = [ + "ctor-proc-macro", + "dtor", +] + +[[package]] +name = "ctor-proc-macro" +version = "0.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1" + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.117", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "dbus" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b942602992bb7acfd1f51c49811c58a610ef9181b6e66f3e519d79b540a3bf73" +dependencies = [ + "libc", + "libdbus-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", + "serde_core", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.117", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", +] + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.11.1", + "block2", + "libc", + "objc2", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "dlopen2" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4" +dependencies = [ + "dlopen2_derive", + "libc", + "once_cell", + "winapi", +] + +[[package]] +name = "dlopen2_derive" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "dom_query" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521e380c0c8afb8d9a1e83a1822ee03556fc3e3e7dbc1fd30be14e37f9cb3f89" +dependencies = [ + "bit-set", + "cssparser", + "foldhash 0.2.0", + "html5ever", + "precomputed-hash", + "selectors", + "tendril", +] + +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + +[[package]] +name = "dpi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" +dependencies = [ + "serde", +] + +[[package]] +name = "dtoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" + +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", +] + +[[package]] +name = "dtor" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1057d6c64987086ff8ed0fd3fbf377a6b7d205cc7715868cd401705f715cbe4" +dependencies = [ + "dtor-proc-macro", +] + +[[package]] +name = "dtor-proc-macro" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5" + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "embed-resource" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31a88c8d26de40ed18fe748c547845aa39de1db3afd958f8cb91579f3644bcb" +dependencies = [ + "cc", + "memchr", + "rustc_version", + "toml 1.1.2+spec-1.1.0", + "vswhom", + "winreg", +] + +[[package]] +name = "embed_plist" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "erased-serde" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "error-code" +version = "3.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "fax" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caf1079563223d5d59d83c85886a56e586cfd5c1a26292e971a0fa266531ac5a" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "rustc_version", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "gdk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691" +dependencies = [ + "cairo-rs", + "gdk-pixbuf", + "gdk-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" +dependencies = [ + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", + "once_cell", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkwayland-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69" +dependencies = [ + "gdk-sys", + "glib-sys", + "gobject-sys", + "libc", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkx11" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3caa00e14351bebbc8183b3c36690327eb77c49abc2268dd4bd36b856db3fbfe" +dependencies = [ + "gdk", + "gdkx11-sys", + "gio", + "glib", + "libc", + "x11", +] + +[[package]] +name = "gdkx11-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d" +dependencies = [ + "gdk-sys", + "glib-sys", + "libc", + "system-deps", + "x11", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "gethostname" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" +dependencies = [ + "rustix", + "windows-link 0.2.1", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", +] + +[[package]] +name = "gio" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "once_cell", + "pin-project-lite", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "gio-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "winapi", +] + +[[package]] +name = "glib" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" +dependencies = [ + "bitflags 2.11.1", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "once_cell", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "glib-macros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" +dependencies = [ + "heck 0.4.1", + "proc-macro-crate 2.0.2", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "glib-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "gobject-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gtk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a" +dependencies = [ + "atk", + "cairo-rs", + "field-offset", + "futures-channel", + "gdk", + "gdk-pixbuf", + "gio", + "glib", + "gtk-sys", + "gtk3-macros", + "libc", + "pango", + "pkg-config", +] + +[[package]] +name = "gtk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414" +dependencies = [ + "atk-sys", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "gtk3-macros" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "html5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1054432bae2f14e0061e33d23402fbaa67a921d319d56adc6bcf887ddad1cbc2" +dependencies = [ + "log", + "markup5ever", +] + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core 0.62.2", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ico" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371" +dependencies = [ + "byteorder", + "png 0.17.16", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "image" +version = "0.25.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" +dependencies = [ + "bytemuck", + "byteorder-lite", + "moxcms", + "num-traits", + "png 0.18.1", + "tiff", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "infer" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7" +dependencies = [ + "cfb", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "javascriptcore-rs" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc" +dependencies = [ + "bitflags 1.3.2", + "glib", + "javascriptcore-rs-sys", +] + +[[package]] +name = "javascriptcore-rs-sys" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys 0.3.1", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.117", +] + +[[package]] +name = "js-sys" +version = "0.3.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67df7112613f8bfd9150013a0314e196f4800d3201ae742489d999db2f979f08" +dependencies = [ + "cfg-if", + "futures-util", + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "json-patch" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "863726d7afb6bc2590eeff7135d923545e5e964f004c2ccf8716c25e70a86f08" +dependencies = [ + "jsonptr", + "serde", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] +name = "jsonptr" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dea2b27dd239b2556ed7a25ba842fe47fd602e7fc7433c2a8d6106d4d9edd70" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "keyboard-types" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" +dependencies = [ + "bitflags 2.11.1", + "serde", + "unicode-segmentation", +] + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libappindicator" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a" +dependencies = [ + "glib", + "gtk", + "gtk-sys", + "libappindicator-sys", + "log", +] + +[[package]] +name = "libappindicator-sys" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" +dependencies = [ + "gtk-sys", + "libloading", + "once_cell", +] + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libdbus-sys" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043" +dependencies = [ + "pkg-config", +] + +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "libredox" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" +dependencies = [ + "libc", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "markup5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8983d30f2915feeaaab2d6babdd6bc7e9ed1a00b66b5e6d74df19aa9c0e91862" +dependencies = [ + "log", + "tendril", + "web_atoms", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "moxcms" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" +dependencies = [ + "num-traits", + "pxfm", +] + +[[package]] +name = "muda" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47a2e3dff89cd322c66647942668faee0a2b1f88ea6cbb4d374b4a8d7e92528c" +dependencies = [ + "crossbeam-channel", + "dpi", + "gtk", + "keyboard-types", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "once_cell", + "png 0.18.1", + "serde", + "thiserror 2.0.18", + "windows-sys 0.61.2", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.11.1", + "jni-sys 0.3.1", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys 0.3.1", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", + "objc2-exception-helper", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.11.1", + "block2", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" +dependencies = [ + "bitflags 2.11.1", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-data" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.11.1", + "dispatch2", + "objc2", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.11.1", + "dispatch2", + "objc2", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-core-image" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-location" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-text" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" +dependencies = [ + "bitflags 2.11.1", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-exception-helper" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a1c5fbb72d7735b076bb47b578523aedc40f3c439bea6dfd595c089d79d98a" +dependencies = [ + "cc", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.11.1", + "block2", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.11.1", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags 2.11.1", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" +dependencies = [ + "bitflags 2.11.1", + "block2", + "objc2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image", + "objc2-core-location", + "objc2-core-text", + "objc2-foundation", + "objc2-quartz-core", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-web-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" +dependencies = [ + "bitflags 2.11.1", + "block2", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "os_pipe" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "pango" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" +dependencies = [ + "gio", + "glib", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link 0.2.1", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "petgraph" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" +dependencies = [ + "fixedbitset", + "hashbrown 0.15.5", + "indexmap 2.14.0", +] + +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros", + "phf_shared", + "serde", +] + +[[package]] +name = "phf_codegen" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared", +] + +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "plist" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "092791278e026273c1b65bbdcfbba3a300f2994c896bd01ab01da613c29c46f1" +dependencies = [ + "base64 0.22.1", + "indexmap 2.14.0", + "quick-xml", + "serde", + "time", +] + +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.11.1", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.117", +] + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" +dependencies = [ + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit 0.25.11+spec-1.1.0", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pxfm" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0c5ccf5294c6ccd63a74f1565028353830a9c2f5eb0c682c355c471726a6e3f" + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + +[[package]] +name = "quick-xml" +version = "0.39.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdcc8dd4e2f670d309a5f0e83fe36dfdc05af317008fea29144da1a2ac858e5e" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.11.1", +] + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 2.0.18", +] + +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "reqwest" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62e0021ea2c22aed41653bc7e1419abb2c97e038ff2c33d0e1309e49a97deec0" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "sync_wrapper", + "tokio", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", +] + +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.11.1", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schemars" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +dependencies = [ + "dyn-clone", + "indexmap 1.9.3", + "schemars_derive", + "serde", + "serde_json", + "url", + "uuid", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.117", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "selectors" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c" +dependencies = [ + "bitflags 2.11.1", + "cssparser", + "derive_more", + "log", + "new_debug_unreachable", + "phf", + "phf_codegen", + "precomputed-hash", + "rustc-hash", + "servo_arc", + "smallvec", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-untagged" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" +dependencies = [ + "erased-serde", + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_with" +version = "3.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e72c1c2cb7b223fafb600a619537a871c2818583d619401b785e7c0b746ccde2" +dependencies = [ + "base64 0.22.1", + "bs58", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.14.0", + "schemars 0.9.0", + "schemars 1.2.1", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b90c488738ecb4fb0262f41f43bc40efc5868d9fb744319ddf5f5317f417bfac" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serialize-to-javascript" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04f3666a07a197cdb77cdf306c32be9b7f598d7060d50cfd4d5aa04bfd92f6c5" +dependencies = [ + "serde", + "serde_json", + "serialize-to-javascript-impl", +] + +[[package]] +name = "serialize-to-javascript-impl" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "servo_arc" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "softbuffer" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3" +dependencies = [ + "bytemuck", + "js-sys", + "ndk", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "objc2-quartz-core", + "raw-window-handle", + "redox_syscall", + "tracing", + "wasm-bindgen", + "web-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "soup3" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f" +dependencies = [ + "futures-channel", + "gio", + "glib", + "libc", + "soup3-sys", +] + +[[package]] +name = "soup3-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "string_cache" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared", + "precomputed-hash", +] + +[[package]] +name = "string_cache_codegen" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "swift-rs" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4057c98e2e852d51fdcfca832aac7b571f6b351ad159f9eda5db1655f8d0c4d7" +dependencies = [ + "base64 0.21.7", + "serde", + "serde_json", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck 0.5.0", + "pkg-config", + "toml 0.8.2", + "version-compare", +] + +[[package]] +name = "tao" +version = "0.35.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a33f7f9e486ade65fcf1e45c440f9236c904f5c1002cdc7fc6ae582777345ce4" +dependencies = [ + "bitflags 2.11.1", + "block2", + "core-foundation", + "core-graphics", + "crossbeam-channel", + "dbus", + "dispatch2", + "dlopen2", + "dpi", + "gdkwayland-sys", + "gdkx11-sys", + "gtk", + "jni", + "libc", + "log", + "ndk", + "ndk-sys", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "once_cell", + "parking_lot", + "percent-encoding", + "raw-window-handle", + "tao-macros", + "unicode-segmentation", + "url", + "windows", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "tao-macros" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "tauri" +version = "2.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "437404997acf375d85f1177afa7e11bb971f274ed6a7b83a2a3e339015f4cc28" +dependencies = [ + "anyhow", + "bytes", + "cookie", + "dirs", + "dunce", + "embed_plist", + "getrandom 0.3.4", + "glob", + "gtk", + "heck 0.5.0", + "http", + "jni", + "libc", + "log", + "mime", + "muda", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "percent-encoding", + "plist", + "raw-window-handle", + "reqwest", + "serde", + "serde_json", + "serde_repr", + "serialize-to-javascript", + "swift-rs", + "tauri-build", + "tauri-macros", + "tauri-runtime", + "tauri-runtime-wry", + "tauri-utils", + "thiserror 2.0.18", + "tokio", + "tray-icon", + "url", + "webkit2gtk", + "webview2-com", + "window-vibrancy", + "windows", +] + +[[package]] +name = "tauri-build" +version = "2.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4aa1f9055fc23919a54e4e125052bed16ed04aef0487086e758fe01a67b451c7" +dependencies = [ + "anyhow", + "cargo_toml", + "dirs", + "glob", + "heck 0.5.0", + "json-patch", + "schemars 0.8.22", + "semver", + "serde", + "serde_json", + "tauri-utils", + "tauri-winres", + "walkdir", +] + +[[package]] +name = "tauri-codegen" +version = "2.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4a0319528a025a38c4078e7dae2c446f4e63620ddb0659a643ede1cb38f90e9" +dependencies = [ + "base64 0.22.1", + "brotli", + "ico", + "json-patch", + "plist", + "png 0.17.16", + "proc-macro2", + "quote", + "semver", + "serde", + "serde_json", + "sha2", + "syn 2.0.117", + "tauri-utils", + "thiserror 2.0.18", + "time", + "url", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-macros" +version = "2.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae6cb4e3896c21d2f6da5b31251d2faea0153bba56ed0e970f918115dbee4924" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", + "tauri-codegen", + "tauri-utils", +] + +[[package]] +name = "tauri-plugin" +version = "2.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e126abc9e84e35cdfd01596140a73a1850cdb0df0a23acf0185776c30b469a6e" +dependencies = [ + "anyhow", + "glob", + "plist", + "schemars 0.8.22", + "serde", + "serde_json", + "tauri-utils", + "walkdir", +] + +[[package]] +name = "tauri-plugin-clipboard-manager" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "206dc20af4ed210748ba945c2774e60fd0acd52b9a73a028402caf809e9b6ecf" +dependencies = [ + "arboard", + "log", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror 2.0.18", +] + +[[package]] +name = "tauri-plugin-window-state" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73736611e14142408d15353e21e3cca2f12a3cfb523ad0ce85999b6d2ef1a704" +dependencies = [ + "bitflags 2.11.1", + "log", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror 2.0.18", +] + +[[package]] +name = "tauri-runtime" +version = "2.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48222d7116c8807eaa6fe2f372e023fae125084e61e6eca6d70b7961cdf129ef" +dependencies = [ + "cookie", + "dpi", + "gtk", + "http", + "jni", + "objc2", + "objc2-ui-kit", + "objc2-web-kit", + "raw-window-handle", + "serde", + "serde_json", + "tauri-utils", + "thiserror 2.0.18", + "url", + "webkit2gtk", + "webview2-com", + "windows", +] + +[[package]] +name = "tauri-runtime-wry" +version = "2.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b83849ee63ecb27a8e8d0fe51915ca215076914aca43f96db1179f0f415f6cd9" +dependencies = [ + "gtk", + "http", + "jni", + "log", + "objc2", + "objc2-app-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "softbuffer", + "tao", + "tauri-runtime", + "tauri-utils", + "url", + "webkit2gtk", + "webview2-com", + "windows", + "wry", +] + +[[package]] +name = "tauri-utils" +version = "2.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "092379df9a707631978e6c56b1bc2401d387f01e2d4a3c123360d167bbb9aa95" +dependencies = [ + "anyhow", + "brotli", + "cargo_metadata", + "ctor", + "dom_query", + "dunce", + "glob", + "http", + "infer", + "json-patch", + "log", + "memchr", + "phf", + "plist", + "proc-macro2", + "quote", + "regex", + "schemars 0.8.22", + "semver", + "serde", + "serde-untagged", + "serde_json", + "serde_with", + "swift-rs", + "thiserror 2.0.18", + "toml 1.1.2+spec-1.1.0", + "url", + "urlpattern", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-winres" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc65d45c68858bfe420dd29e834b5d15dbecf8a07a8a16cf4d532c7b1f69d4b6" +dependencies = [ + "dunce", + "embed-resource", + "toml 1.1.2+spec-1.1.0", +] + +[[package]] +name = "tendril" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4790fc369d5a530f4b544b094e31388b9b3a37c0f4652ade4505945f5660d24" +dependencies = [ + "new_debug_unreachable", + "utf-8", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tiff" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52" +dependencies = [ + "fax", + "flate2", + "half", + "quick-error", + "weezl", + "zune-jpeg", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" +dependencies = [ + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "toml" +version = "0.9.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +dependencies = [ + "indexmap 2.14.0", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 0.7.15", +] + +[[package]] +name = "toml" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" +dependencies = [ + "indexmap 2.14.0", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 1.0.3", +] + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap 2.14.0", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +dependencies = [ + "indexmap 2.14.0", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.25.11+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b" +dependencies = [ + "indexmap 2.14.0", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.3", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow 1.0.3", +] + +[[package]] +name = "toml_writer" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags 2.11.1", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tray-icon" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15edbb0d80583e85ee8df283410038e17314df5cba30da2087a54a85216c0773" +dependencies = [ + "crossbeam-channel", + "dirs", + "libappindicator", + "muda", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "once_cell", + "png 0.18.1", + "serde", + "thiserror 2.0.18", + "windows-sys 0.61.2", +] + +[[package]] +name = "tree_magic_mini" +version = "3.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8765b90061cba6c22b5831f675da109ae5561588290f9fa2317adab2714d5a6" +dependencies = [ + "memchr", + "nom", + "petgraph", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "typenum" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" + +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range", +] + +[[package]] +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" + +[[package]] +name = "unic-ucd-ident" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", + "serde_derive", +] + +[[package]] +name = "urlpattern" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70acd30e3aa1450bc2eece896ce2ad0d178e9c079493819301573dae3c37ba6d" +dependencies = [ + "regex", + "serde", + "unic-ucd-ident", + "url", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" +dependencies = [ + "getrandom 0.4.2", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "version-compare" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96492d0d3ffba25305a7dc88720d250b1401d7edca02cc3bcd50633b424673b8" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.117", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap 2.14.0", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasm-streams" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.11.1", + "hashbrown 0.15.5", + "indexmap 2.14.0", + "semver", +] + +[[package]] +name = "wayland-backend" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2857dd20b54e916ec7253b3d6b4d5c4d7d4ca2c33c2e11c6c76a99bd8744755d" +dependencies = [ + "cc", + "downcast-rs", + "rustix", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c7c96bb74690c3189b5c9cb4ca1627062bb23693a4fad9d8c3de958260144" +dependencies = [ + "bitflags 2.11.1", + "rustix", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "563a85523cade2429938e790815fd7319062103b9f4a2dc806e9b53b95982d8f" +dependencies = [ + "bitflags 2.11.1", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-wlr" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb04e52f7836d7c7976c78ca0250d61e33873c34156a2a1fc9474828ec268234" +dependencies = [ + "bitflags 2.11.1", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c324a910fd86ebdc364a3e61ec1f11737d3b1d6c273c0239ee8ff4bc0d24b4a" +dependencies = [ + "proc-macro2", + "quick-xml", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8eab23fefc9e41f8e841df4a9c707e8a8c4ed26e944ef69297184de2785e3be" +dependencies = [ + "pkg-config", +] + +[[package]] +name = "web-sys" +version = "0.3.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b572dff8bcf38bad0fa19729c89bb5748b2b9b1d8be70cf90df697e3a8f32aa" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web_atoms" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7cff6eef815df1834fd250e3a2ff436044d82a9f1bc1980ca1dbdf07effc538" +dependencies = [ + "phf", + "phf_codegen", + "string_cache", + "string_cache_codegen", +] + +[[package]] +name = "webkit2gtk" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1027150013530fb2eaf806408df88461ae4815a45c541c8975e61d6f2fc4793" +dependencies = [ + "bitflags 1.3.2", + "cairo-rs", + "gdk", + "gdk-sys", + "gio", + "gio-sys", + "glib", + "glib-sys", + "gobject-sys", + "gtk", + "gtk-sys", + "javascriptcore-rs", + "libc", + "once_cell", + "soup3", + "webkit2gtk-sys", +] + +[[package]] +name = "webkit2gtk-sys" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916a5f65c2ef0dfe12fff695960a2ec3d4565359fdbb2e9943c974e06c734ea5" +dependencies = [ + "bitflags 1.3.2", + "cairo-sys-rs", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "gtk-sys", + "javascriptcore-rs-sys", + "libc", + "pkg-config", + "soup3-sys", + "system-deps", +] + +[[package]] +name = "webview2-com" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a" +dependencies = [ + "webview2-com-macros", + "webview2-com-sys", + "windows", + "windows-core 0.61.2", + "windows-implement", + "windows-interface", +] + +[[package]] +name = "webview2-com-macros" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "webview2-com-sys" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c" +dependencies = [ + "thiserror 2.0.18", + "windows", + "windows-core 0.61.2", +] + +[[package]] +name = "weezl" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "window-vibrancy" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c" +dependencies = [ + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "raw-window-handle", + "windows-sys 0.59.0", + "windows-version", +] + +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections", + "windows-core 0.61.2", + "windows-future", + "windows-link 0.1.3", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.2", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-version" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" + +[[package]] +name = "winnow" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.55.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97" +dependencies = [ + "cfg-if", + "windows-sys 0.59.0", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck 0.5.0", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck 0.5.0", + "indexmap 2.14.0", + "prettyplease", + "syn 2.0.117", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.117", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.11.1", + "indexmap 2.14.0", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap 2.14.0", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "wl-clipboard-rs" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9651471a32e87d96ef3a127715382b2d11cc7c8bb9822ded8a7cc94072eb0a3" +dependencies = [ + "libc", + "log", + "os_pipe", + "rustix", + "thiserror 2.0.18", + "tree_magic_mini", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-protocols-wlr", +] + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "wry" +version = "0.55.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186f9871daa55fd9c016578b810d149de58367113db7fb72b462d2323ce19514" +dependencies = [ + "base64 0.22.1", + "block2", + "cookie", + "crossbeam-channel", + "dirs", + "dom_query", + "dpi", + "dunce", + "gdkx11", + "gtk", + "http", + "javascriptcore-rs", + "jni", + "libc", + "ndk", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "sha2", + "soup3", + "tao-macros", + "thiserror 2.0.18", + "url", + "webkit2gtk", + "webkit2gtk-sys", + "webview2-com", + "windows", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "x11rb" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" +dependencies = [ + "gethostname", + "rustix", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" + +[[package]] +name = "yoke" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zune-core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9" + +[[package]] +name = "zune-jpeg" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296" +dependencies = [ + "zune-core", +] diff --git a/apps/mobile/src-tauri/Cargo.toml b/apps/mobile/src-tauri/Cargo.toml new file mode 100644 index 00000000..e7dd5a2b --- /dev/null +++ b/apps/mobile/src-tauri/Cargo.toml @@ -0,0 +1,30 @@ +[workspace] + +[package] +name = "braillify_app" +version = "0.1.0" +description = "Braillify mobile app" +authors = ["devfive"] +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +# The `_lib` suffix may seem redundant but it is necessary +# to make the lib name unique and wouldn't conflict with the bin name. +# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519 +name = "braillify_app_lib" +crate-type = ["staticlib", "cdylib", "rlib"] + +[build-dependencies] +tauri-build = { version = "2", features = [] } + +[dependencies] +tauri = { version = "2", features = [] } +tauri-plugin-clipboard-manager = "2" +serde = { version = "1", features = ["derive"] } +serde_json = "1" + +# 데스크톱(Windows 등) 창 크기·위치 저장/복원. 모바일에서는 무시된다. +[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] +tauri-plugin-window-state = "2" diff --git a/apps/mobile/src-tauri/build.rs b/apps/mobile/src-tauri/build.rs new file mode 100644 index 00000000..d860e1e6 --- /dev/null +++ b/apps/mobile/src-tauri/build.rs @@ -0,0 +1,3 @@ +fn main() { + tauri_build::build() +} diff --git a/apps/mobile/src-tauri/capabilities/default.json b/apps/mobile/src-tauri/capabilities/default.json new file mode 100644 index 00000000..a7f7f22b --- /dev/null +++ b/apps/mobile/src-tauri/capabilities/default.json @@ -0,0 +1,10 @@ +{ + "$schema": "../gen/schemas/mobile-schema.json", + "identifier": "default", + "description": "Braillify 메인 창에 필요한 최소 권한", + "windows": ["main"], + "permissions": [ + "core:default", + "clipboard-manager:allow-write-text" + ] +} diff --git a/apps/mobile/src-tauri/icons/128x128.png b/apps/mobile/src-tauri/icons/128x128.png new file mode 100644 index 00000000..0746dce1 Binary files /dev/null and b/apps/mobile/src-tauri/icons/128x128.png differ diff --git a/apps/mobile/src-tauri/icons/128x128@2x.png b/apps/mobile/src-tauri/icons/128x128@2x.png new file mode 100644 index 00000000..268bea48 Binary files /dev/null and b/apps/mobile/src-tauri/icons/128x128@2x.png differ diff --git a/apps/mobile/src-tauri/icons/32x32.png b/apps/mobile/src-tauri/icons/32x32.png new file mode 100644 index 00000000..8409c1d7 Binary files /dev/null and b/apps/mobile/src-tauri/icons/32x32.png differ diff --git a/apps/mobile/src-tauri/icons/64x64.png b/apps/mobile/src-tauri/icons/64x64.png new file mode 100644 index 00000000..224ed750 Binary files /dev/null and b/apps/mobile/src-tauri/icons/64x64.png differ diff --git a/apps/mobile/src-tauri/icons/Square107x107Logo.png b/apps/mobile/src-tauri/icons/Square107x107Logo.png new file mode 100644 index 00000000..e9ba7829 Binary files /dev/null and b/apps/mobile/src-tauri/icons/Square107x107Logo.png differ diff --git a/apps/mobile/src-tauri/icons/Square142x142Logo.png b/apps/mobile/src-tauri/icons/Square142x142Logo.png new file mode 100644 index 00000000..eae46df6 Binary files /dev/null and b/apps/mobile/src-tauri/icons/Square142x142Logo.png differ diff --git a/apps/mobile/src-tauri/icons/Square150x150Logo.png b/apps/mobile/src-tauri/icons/Square150x150Logo.png new file mode 100644 index 00000000..fc4e074e Binary files /dev/null and b/apps/mobile/src-tauri/icons/Square150x150Logo.png differ diff --git a/apps/mobile/src-tauri/icons/Square284x284Logo.png b/apps/mobile/src-tauri/icons/Square284x284Logo.png new file mode 100644 index 00000000..d0078d30 Binary files /dev/null and b/apps/mobile/src-tauri/icons/Square284x284Logo.png differ diff --git a/apps/mobile/src-tauri/icons/Square30x30Logo.png b/apps/mobile/src-tauri/icons/Square30x30Logo.png new file mode 100644 index 00000000..fd097bbd Binary files /dev/null and b/apps/mobile/src-tauri/icons/Square30x30Logo.png differ diff --git a/apps/mobile/src-tauri/icons/Square310x310Logo.png b/apps/mobile/src-tauri/icons/Square310x310Logo.png new file mode 100644 index 00000000..f2c08366 Binary files /dev/null and b/apps/mobile/src-tauri/icons/Square310x310Logo.png differ diff --git a/apps/mobile/src-tauri/icons/Square44x44Logo.png b/apps/mobile/src-tauri/icons/Square44x44Logo.png new file mode 100644 index 00000000..e3966969 Binary files /dev/null and b/apps/mobile/src-tauri/icons/Square44x44Logo.png differ diff --git a/apps/mobile/src-tauri/icons/Square71x71Logo.png b/apps/mobile/src-tauri/icons/Square71x71Logo.png new file mode 100644 index 00000000..bfd4e1ab Binary files /dev/null and b/apps/mobile/src-tauri/icons/Square71x71Logo.png differ diff --git a/apps/mobile/src-tauri/icons/Square89x89Logo.png b/apps/mobile/src-tauri/icons/Square89x89Logo.png new file mode 100644 index 00000000..3e1bca2f Binary files /dev/null and b/apps/mobile/src-tauri/icons/Square89x89Logo.png differ diff --git a/apps/mobile/src-tauri/icons/StoreLogo.png b/apps/mobile/src-tauri/icons/StoreLogo.png new file mode 100644 index 00000000..120d4038 Binary files /dev/null and b/apps/mobile/src-tauri/icons/StoreLogo.png differ diff --git a/apps/mobile/src-tauri/icons/android/mipmap-anydpi-v26/ic_launcher.xml b/apps/mobile/src-tauri/icons/android/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 00000000..2ffbf24b --- /dev/null +++ b/apps/mobile/src-tauri/icons/android/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/apps/mobile/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png b/apps/mobile/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png new file mode 100644 index 00000000..487578bd Binary files /dev/null and b/apps/mobile/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png differ diff --git a/apps/mobile/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png b/apps/mobile/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png new file mode 100644 index 00000000..f0c8b0bf Binary files /dev/null and b/apps/mobile/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png differ diff --git a/apps/mobile/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png b/apps/mobile/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 00000000..d9bbdb69 Binary files /dev/null and b/apps/mobile/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png differ diff --git a/apps/mobile/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png b/apps/mobile/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png new file mode 100644 index 00000000..811c9def Binary files /dev/null and b/apps/mobile/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png differ diff --git a/apps/mobile/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png b/apps/mobile/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png new file mode 100644 index 00000000..e51526fe Binary files /dev/null and b/apps/mobile/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png differ diff --git a/apps/mobile/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png b/apps/mobile/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 00000000..8f5d1dfe Binary files /dev/null and b/apps/mobile/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png differ diff --git a/apps/mobile/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png b/apps/mobile/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 00000000..d04f4075 Binary files /dev/null and b/apps/mobile/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png differ diff --git a/apps/mobile/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png b/apps/mobile/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png new file mode 100644 index 00000000..01d1cb39 Binary files /dev/null and b/apps/mobile/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png differ diff --git a/apps/mobile/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png b/apps/mobile/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 00000000..26765903 Binary files /dev/null and b/apps/mobile/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/apps/mobile/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png b/apps/mobile/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 00000000..12be2e6a Binary files /dev/null and b/apps/mobile/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png differ diff --git a/apps/mobile/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png b/apps/mobile/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 00000000..7aa55db1 Binary files /dev/null and b/apps/mobile/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png differ diff --git a/apps/mobile/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png b/apps/mobile/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 00000000..26c0aeed Binary files /dev/null and b/apps/mobile/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/apps/mobile/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png b/apps/mobile/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 00000000..9e890ca3 Binary files /dev/null and b/apps/mobile/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/apps/mobile/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png b/apps/mobile/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png new file mode 100644 index 00000000..0a0a0c6b Binary files /dev/null and b/apps/mobile/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png differ diff --git a/apps/mobile/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png b/apps/mobile/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 00000000..77bcf7c0 Binary files /dev/null and b/apps/mobile/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/apps/mobile/src-tauri/icons/android/values/ic_launcher_background.xml b/apps/mobile/src-tauri/icons/android/values/ic_launcher_background.xml new file mode 100644 index 00000000..dcd09ca2 --- /dev/null +++ b/apps/mobile/src-tauri/icons/android/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #EFEEEB + \ No newline at end of file diff --git a/apps/mobile/src-tauri/icons/icon.icns b/apps/mobile/src-tauri/icons/icon.icns new file mode 100644 index 00000000..81c32ce9 Binary files /dev/null and b/apps/mobile/src-tauri/icons/icon.icns differ diff --git a/apps/mobile/src-tauri/icons/icon.ico b/apps/mobile/src-tauri/icons/icon.ico new file mode 100644 index 00000000..e3032dea Binary files /dev/null and b/apps/mobile/src-tauri/icons/icon.ico differ diff --git a/apps/mobile/src-tauri/icons/icon.png b/apps/mobile/src-tauri/icons/icon.png new file mode 100644 index 00000000..d04036eb Binary files /dev/null and b/apps/mobile/src-tauri/icons/icon.png differ diff --git a/apps/mobile/src-tauri/src/lib.rs b/apps/mobile/src-tauri/src/lib.rs new file mode 100644 index 00000000..4390ae0c --- /dev/null +++ b/apps/mobile/src-tauri/src/lib.rs @@ -0,0 +1,12 @@ +#[cfg_attr(mobile, tauri::mobile_entry_point)] +pub fn run() { + let builder = tauri::Builder::default().plugin(tauri_plugin_clipboard_manager::init()); + + // 창 상태 저장/복원은 데스크톱에서만 의미가 있다. + #[cfg(desktop)] + let builder = builder.plugin(tauri_plugin_window_state::Builder::default().build()); + + builder + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} diff --git a/apps/mobile/src-tauri/src/main.rs b/apps/mobile/src-tauri/src/main.rs new file mode 100644 index 00000000..ba37f4df --- /dev/null +++ b/apps/mobile/src-tauri/src/main.rs @@ -0,0 +1,6 @@ +// Prevents additional console window on Windows in release, DO NOT REMOVE!! +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +fn main() { + braillify_app_lib::run() +} diff --git a/apps/mobile/src-tauri/tauri.conf.json b/apps/mobile/src-tauri/tauri.conf.json new file mode 100644 index 00000000..552f6ce4 --- /dev/null +++ b/apps/mobile/src-tauri/tauri.conf.json @@ -0,0 +1,44 @@ +{ + "$schema": "https://schema.tauri.app/config/2", + "productName": "Braillify", + "version": "0.1.0", + "identifier": "app.braillify", + "build": { + "beforeDevCommand": "bun run dev", + "devUrl": "http://localhost:1420", + "beforeBuildCommand": "bun run build", + "frontendDist": "../out" + }, + "app": { + "windows": [ + { + "title": "Braillify", + "width": 1024, + "height": 720, + "minWidth": 960, + "minHeight": 640, + "resizable": true + } + ], + "security": { + "csp": null + } + }, + "bundle": { + "active": true, + "targets": "all", + "icon": [ + "icons/32x32.png", + "icons/128x128.png", + "icons/128x128@2x.png", + "icons/icon.icns", + "icons/icon.ico" + ], + "android": { + "minSdkVersion": 24 + }, + "iOS": { + "minimumSystemVersion": "14.0" + } + } +} diff --git a/apps/mobile/src-tauri/tauri.sim.conf.json b/apps/mobile/src-tauri/tauri.sim.conf.json new file mode 100644 index 00000000..f5a45cf6 --- /dev/null +++ b/apps/mobile/src-tauri/tauri.sim.conf.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://schema.tauri.app/config/2", + "build": { + "beforeDevCommand": "bun run build", + "devUrl": null + } +} diff --git a/apps/mobile/src/app/fonts/LICENSE-Spoqa-Han-Sans.txt b/apps/mobile/src/app/fonts/LICENSE-Spoqa-Han-Sans.txt new file mode 100644 index 00000000..b2fd4697 --- /dev/null +++ b/apps/mobile/src/app/fonts/LICENSE-Spoqa-Han-Sans.txt @@ -0,0 +1,94 @@ +Copyright (c) 2015-10-09 Spoqa (spoqa.com), with Reserved Font Name Spoqa Han Sans. +Copyright (c) 2015-10-09 Spoqa (spoqa.com), with Reserved Font Name Spoqa Han Sans JP. +Copyright (c) 2020-11-30 Spoqa (spoqa.com), with Reserved Font Name Spoqa Han Sans Neo. + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION AND CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/apps/mobile/src/app/fonts/SpoqaHanSansNeo-Regular.woff2 b/apps/mobile/src/app/fonts/SpoqaHanSansNeo-Regular.woff2 new file mode 100644 index 00000000..15723197 Binary files /dev/null and b/apps/mobile/src/app/fonts/SpoqaHanSansNeo-Regular.woff2 differ diff --git a/apps/mobile/src/app/layout.tsx b/apps/mobile/src/app/layout.tsx new file mode 100644 index 00000000..906fbb18 --- /dev/null +++ b/apps/mobile/src/app/layout.tsx @@ -0,0 +1,68 @@ +import { globalCss } from '@devup-ui/react' +import type { Metadata, Viewport } from 'next' +import localFont from 'next/font/local' + +const spoqaHanSansNeo = localFont({ + display: 'swap', + src: './fonts/SpoqaHanSansNeo-Regular.woff2', + variable: '--font-spoqa-han-sans-neo', + weight: '400', +}) + +globalCss({ + 'html, body': { + minHeight: '100%', + }, + body: { + background: '$background', + color: '$text', + fontFamily: 'Segoe UI, Malgun Gothic, sans-serif', + overflowX: 'hidden', + WebkitTapHighlightColor: 'transparent', + }, + '*': { + boxSizing: 'border-box', + margin: 0, + padding: 0, + }, + 'button, textarea': { + font: 'inherit', + }, + 'button:focus-visible, textarea:focus-visible, [tabindex]:focus-visible': { + outlineWidth: '3px', + outlineStyle: 'solid', + outlineColor: '$focus', + outlineOffset: '2px', + }, + '::selection': { + background: '$focus', + color: '$base', + }, +}) + +export const metadata: Metadata = { + applicationName: 'Braillify', + description: '2024 개정 한국 점자 규정 기반의 오프라인 점역기', + title: 'Braillify', +} + +export const viewport: Viewport = { + colorScheme: 'light', + themeColor: '#F3F2EF', + width: 'device-width', + initialScale: 1, + viewportFit: 'cover', +} + +export default function RootLayout({ + children, +}: Readonly<{ children: React.ReactNode }>) { + return ( + + + + + {children} + + ) +} diff --git a/apps/mobile/src/app/page.tsx b/apps/mobile/src/app/page.tsx new file mode 100644 index 00000000..0c4923df --- /dev/null +++ b/apps/mobile/src/app/page.tsx @@ -0,0 +1,36 @@ +import { BrailleEditor } from '@/components/editor/BrailleEditor' +import { HistoryPanel } from '@/components/history/HistoryPanel' +import { AppShell } from '@/components/shell/AppShell' +import { ViewPanel } from '@/components/shell/ViewPanel' +import { BrailleComposer } from '@/components/translator/BrailleComposer' +import { TranslatorWorkspace } from '@/components/translator/TranslatorWorkspace' + +// 서버 컴포넌트: 앱 프레임과 화면들을 조합만 한다. 상태가 필요한 부분은 +// AppShell(client island)과 각 화면 내부의 작은 client 컴포넌트가 담당한다. +export default function HomePage() { + return ( + + + } /> + + + + + + + + + ) +} diff --git a/apps/mobile/src/components/editor/BrailleEditor.tsx b/apps/mobile/src/components/editor/BrailleEditor.tsx new file mode 100644 index 00000000..25f2cbdc --- /dev/null +++ b/apps/mobile/src/components/editor/BrailleEditor.tsx @@ -0,0 +1,335 @@ +'use client' + +import { Box, Button, Flex, Grid, Input, Text, VStack } from '@devup-ui/react' +import { useMemo, useState } from 'react' + +import type { CopyState } from '@/components/translator/TranslationOutput' +import { + brailleMasksToString, + type DotNumber, + mirrorBrailleMask, + parseBrailleString, + toggleBrailleDot, +} from '@/lib/braille-editor' +import { copyText } from '@/lib/clipboard' + +const DOT_LAYOUT = [1, 2, 3, 4, 5, 6] as const + +const COPY_LABEL = { + copied: '복사됨', + error: '복사 실패', + idle: '복사', +} as const satisfies Record + +export function BrailleEditor() { + const [cells, setCells] = useState([0]) + const [intaglio, setIntaglio] = useState(false) + const [importInput, setImportInput] = useState('') + const [importError, setImportError] = useState(null) + const [copyState, setCopyState] = useState('idle') + const previewMasks = useMemo( + () => (intaglio ? cells.map(mirrorBrailleMask) : cells), + [cells, intaglio], + ) + const preview = useMemo( + () => brailleMasksToString(previewMasks), + [previewMasks], + ) + + const importBraille = () => { + const value = importInput.trim() + if (!value) { + setImportError('점자 문자열을 붙여넣어 주세요.') + return + } + + const parsed = parseBrailleString(value) + if (!parsed) { + setImportError('U+2800 범위의 점자 문자만 사용할 수 있습니다.') + return + } + + setCells(parsed.length ? parsed : [0]) + setImportInput('') + setImportError(null) + setCopyState('idle') + } + + const copyPreview = async () => { + try { + await copyText(preview) + setCopyState('copied') + } catch { + setCopyState('error') + } + } + + return ( + + + + + 미리보기 + + + + + 좌우 반전 + + + + + {COPY_LABEL[copyState]} + + + + + {preview} + + + + + + 점자 가져오기 + + { + setImportInput(event.target.value) + setImportError(null) + }} + p="12px" + placeholder="점자 문자열을 붙여넣으세요 (U+2800 범위)" + value={importInput} + /> + {importError && ( + + {importError} + + )} + + + + + + + 점자 셀 편집 ({cells.length}셀) + + + setCells((current) => [...current, 0])} + > + + 셀 + + + + + + {cells.map((mask, index) => ( + { + setCells((current) => + current.length === 1 + ? [0] + : current.filter((_, cellIndex) => cellIndex !== index), + ) + setCopyState('idle') + }} + onToggleDot={(dot) => { + setCells((current) => + current.map((cell, cellIndex) => + cellIndex === index ? toggleBrailleDot(cell, dot) : cell, + ), + ) + setCopyState('idle') + }} + /> + ))} + + + + + + 점 번호 + + + 왼쪽: 1·2·3 / 오른쪽: 4·5·6 + + + + ) +} + +function EditableBrailleCell({ + index, + mask, + onRemove, + onToggleDot, +}: { + index: number + mask: number + onRemove: () => void + onToggleDot: (dot: DotNumber) => void +}) { + return ( + + + #{index + 1} + + + + {DOT_LAYOUT.map((dot) => { + const active = (mask & (1 << (dot - 1))) !== 0 + + return ( + + + ) +} + +function EditorCard({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ) +} + +function OutlineButton({ + children, + onClick, +}: { + children: React.ReactNode + onClick: () => void +}) { + return ( + + ) +} diff --git a/apps/mobile/src/components/history/HistoryPanel.tsx b/apps/mobile/src/components/history/HistoryPanel.tsx new file mode 100644 index 00000000..01018756 --- /dev/null +++ b/apps/mobile/src/components/history/HistoryPanel.tsx @@ -0,0 +1,285 @@ +'use client' + +import { Box, Button, Flex, Grid, Input, Text, VStack } from '@devup-ui/react' +import { useMemo, useState } from 'react' + +import { useAppState } from '@/components/shell/AppShell' +import { MODE_CONTENT } from '@/constants/translation' +import { copyText } from '@/lib/clipboard' +import type { HistoryEntry } from '@/lib/history' + +type HistoryTab = 'recent' | 'favorites' + +const HISTORY_TABS = ['recent', 'favorites'] as const + +function formatCreatedAt(value: string): string { + return new Intl.DateTimeFormat('ko-KR', { + dateStyle: 'medium', + timeStyle: 'short', + }).format(new Date(value)) +} + +export function HistoryPanel() { + const { deleteAll, deleteEntry, entries, requestRestore, toggleFavorite } = + useAppState() + const [tab, setTab] = useState('recent') + const [query, setQuery] = useState('') + const [expandedId, setExpandedId] = useState(null) + const [copiedId, setCopiedId] = useState(null) + const [copyErrorId, setCopyErrorId] = useState(null) + const visibleEntries = useMemo(() => { + const normalizedQuery = query.trim().toLowerCase() + + return entries + .filter((entry) => (tab === 'favorites' ? entry.favorite : true)) + .filter((entry) => { + if (!normalizedQuery) { + return true + } + return ( + entry.input.toLowerCase().includes(normalizedQuery) || + entry.result.toLowerCase().includes(normalizedQuery) + ) + }) + }, [entries, query, tab]) + + const copyEntry = async (entry: HistoryEntry) => { + try { + await copyText(entry.result) + setCopiedId(entry.id) + setCopyErrorId(null) + window.setTimeout(() => { + setCopiedId((current) => (current === entry.id ? null : current)) + }, 1200) + } catch { + setCopiedId(null) + setCopyErrorId(entry.id) + } + } + + return ( + + + {HISTORY_TABS.map((historyTab) => { + const active = historyTab === tab + + return ( + + ) + })} + + + + setQuery(event.target.value)} + p="12px" + placeholder="검색..." + type="search" + value={query} + /> + + + + + {visibleEntries.length === 0 && ( + + {tab === 'favorites' + ? '즐겨찾기한 항목이 없습니다.' + : query + ? '검색 결과가 없습니다.' + : '아직 점역 기록이 없습니다.'} + + )} + + {visibleEntries.map((entry) => { + const expanded = expandedId === entry.id + + return ( + + + + + {entry.input || '(빈 입력)'} + + + {entry.result} + + + + + + + + + + + {expanded && ( + + + + {MODE_CONTENT[entry.mode].inputLabel} + + + {formatCreatedAt(entry.createdAt)} + + + + + )} + + ) + })} + + + ) +} diff --git a/apps/mobile/src/components/navigation/AppSidebar.tsx b/apps/mobile/src/components/navigation/AppSidebar.tsx new file mode 100644 index 00000000..9ef2db8f --- /dev/null +++ b/apps/mobile/src/components/navigation/AppSidebar.tsx @@ -0,0 +1,113 @@ +import { Button, Flex, Text, VStack } from '@devup-ui/react' + +import { type AppView, SIDEBAR_ITEMS } from '@/constants/navigation' + +type AppSidebarProps = { + activeView: AppView + onNavigate: (view: AppView) => void +} + +export function AppSidebar({ activeView, onNavigate }: AppSidebarProps) { + return ( + + + + + + Braillify + + + 한글 점역 시스템 + + + + + + {SIDEBAR_ITEMS.map((item) => { + const isActive = item.id === activeView + + return ( + + ) + })} + + + + + braillify 오픈소스 엔진 + + + 2024 한국 점자 규정 기반 + + + + ) +} diff --git a/apps/mobile/src/components/navigation/BottomTabBar.tsx b/apps/mobile/src/components/navigation/BottomTabBar.tsx new file mode 100644 index 00000000..f40db1e5 --- /dev/null +++ b/apps/mobile/src/components/navigation/BottomTabBar.tsx @@ -0,0 +1,64 @@ +import { Grid, Text, VStack } from '@devup-ui/react' + +import { type AppView, SIDEBAR_ITEMS } from '@/constants/navigation' + +type BottomTabBarProps = { + activeView: AppView + onNavigate: (view: AppView) => void +} + +// 좁은 화면(모바일)에서만 노출되는 하단 탭 내비게이션. +// 넓은 화면에서는 AppSidebar가 대신 노출된다. +export function BottomTabBar({ activeView, onNavigate }: BottomTabBarProps) { + return ( + + {SIDEBAR_ITEMS.map((item) => { + const isActive = item.id === activeView + + return ( + onNavigate(item.id)} + py="4px" + type="button" + > + + + {item.label} + + + ) + })} + + ) +} diff --git a/apps/mobile/src/components/shell/AppShell.tsx b/apps/mobile/src/components/shell/AppShell.tsx new file mode 100644 index 00000000..33788980 --- /dev/null +++ b/apps/mobile/src/components/shell/AppShell.tsx @@ -0,0 +1,107 @@ +'use client' + +import { Grid, VStack } from '@devup-ui/react' +import { createContext, type ReactNode, useContext, useState } from 'react' + +import { AppSidebar } from '@/components/navigation/AppSidebar' +import { BottomTabBar } from '@/components/navigation/BottomTabBar' +import type { AppView } from '@/constants/navigation' +import type { TranslateMode } from '@/constants/translation' +import { useTranslationHistory } from '@/hooks/useTranslationHistory' +import type { HistoryEntry, HistoryEntryDraft } from '@/lib/history' + +// 히스토리 항목을 점역기로 되돌릴 때 넘기는 초안. requestId 로 같은 항목을 +// 다시 불러올 때도 effect 가 재실행된다. +export type TranslationDraft = { + input: string + mode: TranslateMode + requestId: number + result: string +} + +type AppState = { + activeView: AppView + addEntry: (draft: HistoryEntryDraft) => void + deleteAll: () => void + deleteEntry: (id: string) => void + entries: HistoryEntry[] + navigate: (view: AppView) => void + requestRestore: (entry: HistoryEntry) => void + restoreDraft: TranslationDraft | null + toggleFavorite: (id: string) => void +} + +const AppStateContext = createContext(null) + +export function useAppState(): AppState { + const state = useContext(AppStateContext) + if (!state) { + throw new Error('AppShell 바깥에서는 앱 상태를 사용할 수 없습니다.') + } + return state +} + +// 앱 전역의 activeView·복원·히스토리 상태를 소유하는 client boundary. +// 각 화면(children)의 정적 골격은 서버에서 조합되어 내려온다. +export function AppShell({ children }: { children: ReactNode }) { + const [activeView, setActiveView] = useState('translator') + const [restoreDraft, setRestoreDraft] = useState( + null, + ) + const [restoreRequestId, setRestoreRequestId] = useState(0) + const { addEntry, deleteAll, deleteEntry, entries, toggleFavorite } = + useTranslationHistory() + + const requestRestore = (entry: HistoryEntry) => { + const nextRequestId = restoreRequestId + 1 + setRestoreRequestId(nextRequestId) + setRestoreDraft({ + input: entry.input, + mode: entry.mode, + requestId: nextRequestId, + result: entry.result, + }) + setActiveView('translator') + } + + return ( + + + + + {children} + + + + + ) +} diff --git a/apps/mobile/src/components/shell/ViewPanel.tsx b/apps/mobile/src/components/shell/ViewPanel.tsx new file mode 100644 index 00000000..634a9457 --- /dev/null +++ b/apps/mobile/src/components/shell/ViewPanel.tsx @@ -0,0 +1,53 @@ +'use client' + +import { Text, VStack } from '@devup-ui/react' +import type { ReactNode } from 'react' + +import { useAppState } from '@/components/shell/AppShell' +import type { AppView } from '@/constants/navigation' + +type ViewPanelProps = { + children: ReactNode + description: string + title: string + view: AppView +} + +// 활성 화면일 때만 노출되는 패널. 화면 본문(children)은 서버에서 조합되어 +// 내려오고, 이 컴포넌트는 activeView 에 따라 표시 여부만 제어한다. +export function ViewPanel({ + children, + description, + title, + view, +}: ViewPanelProps) { + const { activeView } = useAppState() + + return ( + + + + {title} + + + {description} + + + {children} + + ) +} diff --git a/apps/mobile/src/components/translator/BrailleComposer.tsx b/apps/mobile/src/components/translator/BrailleComposer.tsx new file mode 100644 index 00000000..1bb902b3 --- /dev/null +++ b/apps/mobile/src/components/translator/BrailleComposer.tsx @@ -0,0 +1,33 @@ +import { Flex, Text, VStack } from '@devup-ui/react' + +import { BrailleComposerControls } from './BrailleComposerControls' + +// 서버 컴포넌트: 정적 골격(제목·설명·레이아웃)만 렌더한다. +// 점 선택·미리보기·버튼 등 상호작용은 BrailleComposerControls(client island)가 맡는다. +export function BrailleComposer() { + return ( + + + 6점 입력기 + + 점을 고른 뒤 셀을 추가하세요. + + + + + + ) +} diff --git a/apps/mobile/src/components/translator/BrailleComposerControls.tsx b/apps/mobile/src/components/translator/BrailleComposerControls.tsx new file mode 100644 index 00000000..66ec0510 --- /dev/null +++ b/apps/mobile/src/components/translator/BrailleComposerControls.tsx @@ -0,0 +1,174 @@ +'use client' + +import { Button, Flex, Grid, Text, VStack } from '@devup-ui/react' +import { useState } from 'react' + +import { createBrailleCell, deleteLastCharacter } from '@/lib/braille-editor' + +import { useReverseInput } from './reverse-input-context' + +const DOTS = [1, 4, 2, 5, 3, 6] as const + +// BrailleComposer의 상호작용만 담당하는 client island. +// 정적 제목/레이아웃은 서버 컴포넌트인 BrailleComposer가 렌더한다. +export function BrailleComposerControls() { + const { onChange, value } = useReverseInput() + const [selectedDots, setSelectedDots] = useState([]) + const preview = createBrailleCell(selectedDots) + + const toggleDot = (dot: number) => { + setSelectedDots((current) => + current.includes(dot) + ? current.filter((selectedDot) => selectedDot !== dot) + : [...current, dot], + ) + } + + return ( + <> + + {DOTS.map((dot) => { + const selected = selectedDots.includes(dot) + return ( + + ) + })} + + + + + 현재 셀 + + + {preview} + + + + + + + + + + + ) +} diff --git a/apps/mobile/src/components/translator/TranslationInputCard.tsx b/apps/mobile/src/components/translator/TranslationInputCard.tsx new file mode 100644 index 00000000..8b1c7a08 --- /dev/null +++ b/apps/mobile/src/components/translator/TranslationInputCard.tsx @@ -0,0 +1,140 @@ +import { Button, Flex, Input, Text, VStack } from '@devup-ui/react' +import type { ReactNode } from 'react' + +type TranslationInputCardProps = { + buttonLabel: string + // 역점역 6점 입력기. 서버에서 조합된 정적 셸이 내려온다(활성 시에만 표시). + composerSlot: ReactNode + errorMessage: string | null + helpText: string + input: string + inputLabel: string + isTranslating: boolean + isReverse: boolean + onChange: (value: string) => void + onSubmit: () => void + placeholder: string +} + +export function TranslationInputCard({ + buttonLabel, + composerSlot, + errorMessage, + helpText, + input, + inputLabel, + isTranslating, + isReverse, + onChange, + onSubmit, + placeholder, +}: TranslationInputCardProps) { + const characterCount = Array.from(input).length + + return ( + + + + {inputLabel} + + + {characterCount} + {isReverse ? '칸' : '자'} + + + + onChange(event.target.value)} + onKeyDown={(event) => { + if (event.ctrlKey && event.key === 'Enter') { + event.preventDefault() + onSubmit() + } + }} + p="26px" + placeholder={placeholder} + resize="none" + value={input} + /> + + {isReverse && composerSlot} + + + + {errorMessage || helpText} + {!errorMessage && ( + + {helpText ? ' · ' : ''}Ctrl + Enter로 변환 + + )} + + + + + ) +} diff --git a/apps/mobile/src/components/translator/TranslationOutput.tsx b/apps/mobile/src/components/translator/TranslationOutput.tsx new file mode 100644 index 00000000..e7681857 --- /dev/null +++ b/apps/mobile/src/components/translator/TranslationOutput.tsx @@ -0,0 +1,107 @@ +import { Button, Text, VStack } from '@devup-ui/react' + +export type CopyState = 'idle' | 'copied' | 'error' + +const COPY_STATUS = { + copied: '결과를 클립보드에 복사했습니다.', + error: '결과를 복사하지 못했습니다. 다시 시도해 주세요.', + idle: '', +} as const satisfies Record + +type TranslationOutputProps = { + copyState: CopyState + isReverse: boolean + onCopy: () => void + result: string +} + +export function TranslationOutput({ + copyState, + isReverse, + onCopy, + result, +}: TranslationOutputProps) { + if (!result) { + return ( + + + + {isReverse + ? '점자를 입력하고 역점역을 시작해보세요' + : '텍스트를 입력하고 점역을 시작해보세요'} + + + ) + } + + return ( + + + {result} + + + + {COPY_STATUS[copyState]} + + + ) +} diff --git a/apps/mobile/src/components/translator/TranslatorWorkspace.tsx b/apps/mobile/src/components/translator/TranslatorWorkspace.tsx new file mode 100644 index 00000000..02c55bad --- /dev/null +++ b/apps/mobile/src/components/translator/TranslatorWorkspace.tsx @@ -0,0 +1,198 @@ +'use client' + +import { Button, Flex, Text, VStack } from '@devup-ui/react' +import { type ReactNode, useEffect, useState } from 'react' + +import { useAppState } from '@/components/shell/AppShell' +import { + MODE_CONTENT, + MODE_OPTIONS, + type TranslateMode, +} from '@/constants/translation' +import { copyText } from '@/lib/clipboard' +import { translateText } from '@/lib/translate' + +import { ReverseInputContext } from './reverse-input-context' +import { TranslationInputCard } from './TranslationInputCard' +import { type CopyState, TranslationOutput } from './TranslationOutput' + +export function TranslatorWorkspace({ + composerSlot, +}: { + composerSlot: ReactNode +}) { + const { addEntry, restoreDraft } = useAppState() + const [mode, setMode] = useState('general') + const [input, setInput] = useState('') + const [result, setResult] = useState('') + const [errorMessage, setErrorMessage] = useState(null) + const [isTranslating, setIsTranslating] = useState(false) + const [copyState, setCopyState] = useState('idle') + const content = MODE_CONTENT[mode] + + useEffect(() => { + if (!restoreDraft) { + return + } + + setMode(restoreDraft.mode) + setInput(restoreDraft.input) + setResult(restoreDraft.result) + setErrorMessage(null) + setCopyState('idle') + }, [restoreDraft]) + + const copyResult = async () => { + try { + await copyText(result) + setCopyState('copied') + } catch { + setCopyState('error') + } + } + + const translate = async () => { + if (isTranslating) { + return + } + + setErrorMessage(null) + setIsTranslating(true) + + try { + const translatedResult = await translateText(input, mode) + setResult(translatedResult) + setCopyState('idle') + addEntry({ + input, + mode, + result: translatedResult, + }) + } catch (error) { + setResult('') + setErrorMessage( + error instanceof Error ? error.message : '점역 중 오류가 발생했습니다.', + ) + } finally { + setIsTranslating(false) + } + } + + const handleInputChange = (value: string) => { + setInput(value) + setResult('') + setErrorMessage(null) + setCopyState('idle') + } + + return ( + + { + if ( + event.ctrlKey && + event.shiftKey && + event.key.toLowerCase() === 'c' + ) { + event.preventDefault() + if (result) { + copyResult() + } + } + }} + > + + + + 점역 모드 + + + {content.guide} + + + + {MODE_OPTIONS.map((option) => { + const isSelected = option.value === mode + + return ( + + ) + })} + + + + + + + ) +} diff --git a/apps/mobile/src/components/translator/reverse-input-context.ts b/apps/mobile/src/components/translator/reverse-input-context.ts new file mode 100644 index 00000000..38b96908 --- /dev/null +++ b/apps/mobile/src/components/translator/reverse-input-context.ts @@ -0,0 +1,22 @@ +'use client' + +import { createContext, useContext } from 'react' + +// 역점역 6점 입력기(client island)가 점역기 입력값을 읽고 갱신하기 위한 컨텍스트. +// 이 컨텍스트 덕분에 BrailleComposer의 정적 골격은 함수 prop 없이 서버에서 렌더될 수 있다. +export type ReverseInputValue = { + onChange: (value: string) => void + value: string +} + +export const ReverseInputContext = createContext(null) + +export function useReverseInput(): ReverseInputValue { + const value = useContext(ReverseInputContext) + if (!value) { + throw new Error( + 'ReverseInputContext 바깥에서는 역점역 입력을 사용할 수 없습니다.', + ) + } + return value +} diff --git a/apps/mobile/src/constants/navigation.ts b/apps/mobile/src/constants/navigation.ts new file mode 100644 index 00000000..9e829ae9 --- /dev/null +++ b/apps/mobile/src/constants/navigation.ts @@ -0,0 +1,27 @@ +export type AppView = 'translator' | 'editor' | 'history' + +export const SIDEBAR_ITEMS = [ + { + description: '점역 · 역점역', + id: 'translator', + label: '점역기', + symbol: '⠿', + }, + { + description: '직접 점자 편집', + id: 'editor', + label: '점자 편집기', + symbol: '⠶', + }, + { + description: '최근 작업 · 즐겨찾기', + id: 'history', + label: '히스토리', + symbol: '⠷', + }, +] as const satisfies ReadonlyArray<{ + description: string + id: AppView + label: string + symbol: string +}> diff --git a/apps/mobile/src/constants/translation.ts b/apps/mobile/src/constants/translation.ts new file mode 100644 index 00000000..f01c3295 --- /dev/null +++ b/apps/mobile/src/constants/translation.ts @@ -0,0 +1,37 @@ +export const MODE_OPTIONS = [ + { label: '일반', value: 'general' }, + { label: '수학', value: 'math' }, + { label: '역점역', value: 'reverse' }, +] as const + +export type TranslateMode = (typeof MODE_OPTIONS)[number]['value'] + +export const MODE_CONTENT = { + general: { + buttonLabel: '점역하기', + guide: '일반 텍스트를 2024 개정 한국 점자 규정에 따라 변환합니다.', + inputLabel: '점역할 일반 텍스트', + placeholder: '점역할 한국어 문장이나 단어를 입력하세요.', + }, + math: { + buttonLabel: '수학 점역하기', + guide: '수식 전체를 $...$로 감싸고 분수는 \\frac{}{}로 입력하세요.', + inputLabel: '점역할 LaTeX 수식', + placeholder: '$\\frac{3}{4}$', + }, + reverse: { + buttonLabel: '역점역하기', + guide: + '한국 점자 유니코드를 한글로 변환합니다. 직접 붙여넣거나 6점 입력기를 사용하세요.', + inputLabel: '역점역할 점자', + placeholder: '예: ⠣⠒⠉⠻', + }, +} as const satisfies Record< + TranslateMode, + { + buttonLabel: string + guide: string + inputLabel: string + placeholder: string + } +> diff --git a/apps/mobile/src/hooks/useTranslationHistory.ts b/apps/mobile/src/hooks/useTranslationHistory.ts new file mode 100644 index 00000000..be0cfcbc --- /dev/null +++ b/apps/mobile/src/hooks/useTranslationHistory.ts @@ -0,0 +1,43 @@ +import { useCallback, useEffect, useState } from 'react' + +import { + addHistoryEntry, + clearHistory, + deleteHistoryEntry, + type HistoryEntry, + type HistoryEntryDraft, + loadHistory, + toggleFavoriteHistoryEntry, +} from '@/lib/history' + +export function useTranslationHistory() { + const [entries, setEntries] = useState([]) + + useEffect(() => { + setEntries(loadHistory()) + }, []) + + const addEntry = useCallback((draft: HistoryEntryDraft) => { + setEntries(addHistoryEntry(draft)) + }, []) + + const deleteEntry = useCallback((id: string) => { + setEntries(deleteHistoryEntry(id)) + }, []) + + const deleteAll = useCallback(() => { + setEntries(clearHistory()) + }, []) + + const toggleFavorite = useCallback((id: string) => { + setEntries(toggleFavoriteHistoryEntry(id)) + }, []) + + return { + addEntry, + deleteAll, + deleteEntry, + entries, + toggleFavorite, + } +} diff --git a/apps/mobile/src/lib/braille-editor.test.ts b/apps/mobile/src/lib/braille-editor.test.ts new file mode 100644 index 00000000..0f0a5145 --- /dev/null +++ b/apps/mobile/src/lib/braille-editor.test.ts @@ -0,0 +1,52 @@ +import { describe, expect, test } from 'bun:test' + +import { + BRAILLE_BLANK, + brailleMasksToString, + createBrailleCell, + deleteLastCharacter, + mirrorBrailleMask, + parseBrailleString, + toggleBrailleDot, +} from './braille-editor' + +describe('braille editor helpers', () => { + test('선택한 점을 유니코드 점자 셀로 조합한다', () => { + expect(createBrailleCell([])).toBe(BRAILLE_BLANK) + expect(createBrailleCell([1])).toBe('⠁') + expect(createBrailleCell([1, 4])).toBe('⠉') + expect(createBrailleCell([1, 2, 3, 4, 5, 6])).toBe('⠿') + }) + + test('중복 점은 한 번만 반영하고 잘못된 점 번호는 거절한다', () => { + expect(createBrailleCell([1, 1, 4])).toBe('⠉') + expect(() => createBrailleCell([0])).toThrow('유효하지 않은 점 번호') + expect(() => createBrailleCell([7])).toThrow('유효하지 않은 점 번호') + expect(() => createBrailleCell([9])).toThrow('유효하지 않은 점 번호') + }) + + test('유니코드 문자 단위로 마지막 글자를 지운다', () => { + expect(deleteLastCharacter('⠁⠉')).toBe('⠁') + expect(deleteLastCharacter('⠁😀')).toBe('⠁') + expect(deleteLastCharacter('')).toBe('') + }) + + test('셀 마스크를 편집하고 유니코드 점자로 변환한다', () => { + const dotOne = toggleBrailleDot(0, 1) + const dotsOneAndFour = toggleBrailleDot(dotOne, 4) + + expect(brailleMasksToString([dotOne, dotsOneAndFour, 0])).toBe('⠁⠉⠀') + }) + + test('점자 문자열만 셀 마스크로 가져온다', () => { + expect(parseBrailleString('⠁⠉⠀')).toEqual([1, 9, 0]) + expect(parseBrailleString('')).toEqual([]) + expect(parseBrailleString('⠁A')).toBeNull() + }) + + test('음각 미리보기는 각 셀의 좌우 점을 맞바꾼다', () => { + expect(mirrorBrailleMask(0b000001)).toBe(0b001000) + expect(mirrorBrailleMask(0b001000)).toBe(0b000001) + expect(mirrorBrailleMask(0b111111)).toBe(0b111111) + }) +}) diff --git a/apps/mobile/src/lib/braille-editor.ts b/apps/mobile/src/lib/braille-editor.ts new file mode 100644 index 00000000..5ea0abd1 --- /dev/null +++ b/apps/mobile/src/lib/braille-editor.ts @@ -0,0 +1,54 @@ +export const BRAILLE_BLANK = '\u2800' +const BRAILLE_BASE = 0x2800 +const BRAILLE_MAX = 0x283f + +export type DotNumber = 1 | 2 | 3 | 4 | 5 | 6 + +export function createBrailleCell(dots: readonly number[]): string { + const bitMask = dots.reduce((mask, dot) => { + if (!Number.isInteger(dot) || dot < 1 || dot > 6) { + throw new Error(`유효하지 않은 점 번호입니다: ${dot}`) + } + + return mask | (1 << (dot - 1)) + }, 0) + + return String.fromCodePoint(0x2800 + bitMask) +} + +export function deleteLastCharacter(value: string): string { + return Array.from(value).slice(0, -1).join('') +} + +export function toggleBrailleDot(mask: number, dot: DotNumber): number { + return mask ^ (1 << (dot - 1)) +} + +export function brailleMasksToString(masks: readonly number[]): string { + return masks + .map((mask) => String.fromCodePoint(BRAILLE_BASE + (mask & 0x3f))) + .join('') +} + +export function parseBrailleString(value: string): number[] | null { + const masks: number[] = [] + + for (const character of value) { + const codePoint = character.codePointAt(0) + if ( + codePoint === undefined || + codePoint < BRAILLE_BASE || + codePoint > BRAILLE_MAX + ) { + return null + } + masks.push(codePoint - BRAILLE_BASE) + } + + return masks +} + +// 음각 인쇄 미리보기: 1↔4, 2↔5, 3↔6으로 좌우 점을 뒤집는다. +export function mirrorBrailleMask(mask: number): number { + return ((mask & 0x07) << 3) | ((mask & 0x38) >> 3) +} diff --git a/apps/mobile/src/lib/clipboard.test.ts b/apps/mobile/src/lib/clipboard.test.ts new file mode 100644 index 00000000..475cea57 --- /dev/null +++ b/apps/mobile/src/lib/clipboard.test.ts @@ -0,0 +1,90 @@ +import { afterEach, describe, expect, mock, test } from 'bun:test' + +import { + CLIPBOARD_ERROR_MESSAGE, + copyText, + EMPTY_CLIPBOARD_MESSAGE, + writeClipboardText, +} from './clipboard' + +const originalNavigator = globalThis.navigator + +function setNavigator(value: unknown) { + Object.defineProperty(globalThis, 'navigator', { + configurable: true, + value, + writable: true, + }) +} + +afterEach(() => { + setNavigator(originalNavigator) +}) + +describe('copyText', () => { + test('점역 결과를 클립보드 어댑터에 전달한다', async () => { + const writeClipboardText = mock(async () => undefined) + + await expect(copyText('⠣⠒⠉⠻', writeClipboardText)).resolves.toBeUndefined() + expect(writeClipboardText).toHaveBeenCalledWith('⠣⠒⠉⠻') + }) + + test('빈 결과는 클립보드에 쓰지 않는다', async () => { + const writeClipboardText = mock(async () => undefined) + + await expect(copyText('', writeClipboardText)).rejects.toThrow( + EMPTY_CLIPBOARD_MESSAGE, + ) + expect(writeClipboardText).not.toHaveBeenCalled() + }) + + test('클립보드 어댑터 실패를 사용자용 오류로 변환한다', async () => { + const writeClipboardText = mock(async () => { + throw new Error('clipboard unavailable') + }) + + await expect(copyText('⠣⠒⠉⠻', writeClipboardText)).rejects.toThrow( + CLIPBOARD_ERROR_MESSAGE, + ) + }) +}) + +describe('writeClipboardText 기본 어댑터', () => { + test('Tauri 환경에서는 플러그인 writeText로 복사한다', async () => { + const writeText = mock(async () => undefined) + mock.module('@tauri-apps/plugin-clipboard-manager', () => ({ writeText })) + + await expect( + writeClipboardText('⠣⠒⠉⠻', () => true), + ).resolves.toBeUndefined() + expect(writeText).toHaveBeenCalledWith('⠣⠒⠉⠻') + }) + + test('브라우저 환경에서는 navigator.clipboard로 복사한다', async () => { + const writeText = mock(async () => undefined) + setNavigator({ clipboard: { writeText } }) + + await expect( + writeClipboardText('⠣⠒⠉⠻', () => false), + ).resolves.toBeUndefined() + expect(writeText).toHaveBeenCalledWith('⠣⠒⠉⠻') + }) + + test('브라우저 클립보드를 쓸 수 없으면 오류를 던진다', async () => { + setNavigator({}) + + await expect(writeClipboardText('⠣⠒⠉⠻', () => false)).rejects.toThrow( + '브라우저에서 클립보드를 사용할 수 없습니다.', + ) + }) +}) + +describe('copyText 기본 어댑터 통합', () => { + test('어댑터를 주입하지 않으면 실제 클립보드 어댑터로 복사한다', async () => { + const writeText = mock(async () => undefined) + setNavigator({ clipboard: { writeText } }) + + await expect(copyText('⠣⠒⠉⠻')).resolves.toBeUndefined() + expect(writeText).toHaveBeenCalledWith('⠣⠒⠉⠻') + }) +}) diff --git a/apps/mobile/src/lib/clipboard.ts b/apps/mobile/src/lib/clipboard.ts new file mode 100644 index 00000000..ae7ba2e3 --- /dev/null +++ b/apps/mobile/src/lib/clipboard.ts @@ -0,0 +1,42 @@ +import { isTauriRuntime } from './platform' + +export const EMPTY_CLIPBOARD_MESSAGE = '복사할 점역 결과가 없습니다.' +export const CLIPBOARD_ERROR_MESSAGE = + '결과를 복사하지 못했습니다. 다시 시도해 주세요.' + +type WriteClipboardText = (text: string) => Promise + +export const writeClipboardText = async ( + text: string, + detectTauri: () => boolean = isTauriRuntime, +): Promise => { + if (detectTauri()) { + // Tauri 전용 API는 브라우저 번들에 정적으로 포함되지 않도록 Tauri에서만 동적 import 한다. + const { writeText } = await import('@tauri-apps/plugin-clipboard-manager') + await writeText(text) + return + } + + const browserClipboard = globalThis.navigator?.clipboard + + if (!browserClipboard) { + throw new Error('브라우저에서 클립보드를 사용할 수 없습니다.') + } + + await browserClipboard.writeText(text) +} + +export async function copyText( + text: string, + writeClipboard: WriteClipboardText = writeClipboardText, +): Promise { + if (text.length === 0) { + throw new Error(EMPTY_CLIPBOARD_MESSAGE) + } + + try { + await writeClipboard(text) + } catch (error) { + throw new Error(CLIPBOARD_ERROR_MESSAGE, { cause: error }) + } +} diff --git a/apps/mobile/src/lib/history.test.ts b/apps/mobile/src/lib/history.test.ts new file mode 100644 index 00000000..523a63a5 --- /dev/null +++ b/apps/mobile/src/lib/history.test.ts @@ -0,0 +1,222 @@ +import { describe, expect, test } from 'bun:test' + +import { + addHistoryEntry, + clearHistory, + deleteHistoryEntry, + HISTORY_STORAGE_KEY, + loadHistory, + MAX_HISTORY_ENTRIES, + toggleFavoriteHistoryEntry, +} from './history' + +class MemoryStorage { + private values = new Map() + + getItem(key: string) { + return this.values.get(key) ?? null + } + + removeItem(key: string) { + this.values.delete(key) + } + + setItem(key: string, value: string) { + this.values.set(key, value) + } +} + +describe('translation history', () => { + test('최근 변환을 모드와 함께 저장하고 다시 읽는다', () => { + const storage = new MemoryStorage() + + addHistoryEntry( + { input: '안녕', mode: 'general', result: '⠣⠒⠉⠻' }, + storage, + () => 'entry-1', + () => new Date('2026-07-27T12:00:00.000Z'), + ) + + expect(loadHistory(storage)).toEqual([ + { + createdAt: '2026-07-27T12:00:00.000Z', + favorite: false, + id: 'entry-1', + input: '안녕', + mode: 'general', + result: '⠣⠒⠉⠻', + }, + ]) + }) + + test('상한을 넘는 일반 기록은 오래된 것부터 제거한다', () => { + const storage = new MemoryStorage() + const total = MAX_HISTORY_ENTRIES + 5 + + for (let index = 0; index < total; index += 1) { + addHistoryEntry( + { + input: `입력 ${index}`, + mode: 'general', + result: `결과 ${index}`, + }, + storage, + () => `entry-${index}`, + ) + } + + const entries = loadHistory(storage) + expect(entries).toHaveLength(MAX_HISTORY_ENTRIES) + expect(entries[0]?.id).toBe(`entry-${total - 1}`) + expect(entries.at(-1)?.id).toBe(`entry-${total - MAX_HISTORY_ENTRIES}`) + }) + + test('상한을 넘어도 즐겨찾기 기록은 유지된다', () => { + const storage = new MemoryStorage() + + addHistoryEntry( + { input: '오래된 즐겨찾기', mode: 'general', result: '⠐' }, + storage, + () => 'favorite-entry', + ) + toggleFavoriteHistoryEntry('favorite-entry', storage) + + for (let index = 0; index < MAX_HISTORY_ENTRIES + 10; index += 1) { + addHistoryEntry( + { input: `입력 ${index}`, mode: 'general', result: `결과 ${index}` }, + storage, + () => `entry-${index}`, + ) + } + + const favorite = loadHistory(storage).find( + (entry) => entry.id === 'favorite-entry', + ) + expect(favorite?.favorite).toBe(true) + }) + + test('손상된 저장 데이터는 삭제하고 빈 상태로 복구한다', () => { + const storage = new MemoryStorage() + storage.setItem(HISTORY_STORAGE_KEY, '{invalid json') + + expect(loadHistory(storage)).toEqual([]) + expect(storage.getItem(HISTORY_STORAGE_KEY)).toBeNull() + }) + + test('저장소가 없는 서버 환경에서는 빈 상태를 반환한다', () => { + expect(loadHistory()).toEqual([]) + }) + + test('배열이 아니거나 유효하지 않은 항목은 기록으로 불러오지 않는다', () => { + const storage = new MemoryStorage() + storage.setItem(HISTORY_STORAGE_KEY, '{}') + + expect(loadHistory(storage)).toEqual([]) + expect(storage.getItem(HISTORY_STORAGE_KEY)).toBeNull() + + storage.setItem(HISTORY_STORAGE_KEY, '[null]') + expect(loadHistory(storage)).toEqual([]) + }) + + test('기본 생성기로 식별자와 시각을 기록한다', () => { + const storage = new MemoryStorage() + const [entry] = addHistoryEntry( + { input: '안녕', mode: 'general', result: '⠣⠒⠉⠻' }, + storage, + ) + + expect(entry?.id).toBeString() + expect(Number.isNaN(Date.parse(entry?.createdAt ?? ''))).toBe(false) + }) + + test('저장소 접근이 모두 실패해도 점역 기록 흐름을 중단하지 않는다', () => { + const storage = { + getItem() { + throw new Error('storage unavailable') + }, + removeItem() { + throw new Error('storage unavailable') + }, + setItem() { + throw new Error('storage unavailable') + }, + } + + expect(loadHistory(storage)).toEqual([]) + expect( + addHistoryEntry( + { input: '안녕', mode: 'general', result: '⠣⠒⠉⠻' }, + storage, + () => 'offline-entry', + () => new Date('2026-07-27T12:00:00.000Z'), + ), + ).toEqual([ + { + createdAt: '2026-07-27T12:00:00.000Z', + favorite: false, + id: 'offline-entry', + input: '안녕', + mode: 'general', + result: '⠣⠒⠉⠻', + }, + ]) + expect(clearHistory(storage)).toEqual([]) + }) + + test('개별 삭제와 전체 삭제를 저장소에 반영한다', () => { + const storage = new MemoryStorage() + addHistoryEntry( + { input: '안녕', mode: 'general', result: '⠣⠒⠉⠻' }, + storage, + () => 'general-entry', + ) + addHistoryEntry( + { input: '$x$', mode: 'math', result: '⠭' }, + storage, + () => 'math-entry', + ) + + expect(deleteHistoryEntry('general-entry', storage)).toHaveLength(1) + expect(clearHistory(storage)).toEqual([]) + expect(loadHistory(storage)).toEqual([]) + }) + + test('즐겨찾기 상태를 켜고 끄며 기존 기록도 호환한다', () => { + const storage = new MemoryStorage() + storage.setItem( + HISTORY_STORAGE_KEY, + JSON.stringify([ + { + createdAt: '2026-07-27T12:00:00.000Z', + id: 'legacy-entry', + input: '안녕', + mode: 'general', + result: '⠣⠒⠉⠻', + }, + ]), + ) + + expect(loadHistory(storage)[0]?.favorite).toBe(false) + expect( + toggleFavoriteHistoryEntry('legacy-entry', storage)[0]?.favorite, + ).toBe(true) + expect( + toggleFavoriteHistoryEntry('legacy-entry', storage)[0]?.favorite, + ).toBe(false) + }) + + test('역점역 기록을 유효한 기록으로 다시 읽는다', () => { + const storage = new MemoryStorage() + addHistoryEntry( + { input: '⠣⠒⠉⠻', mode: 'reverse', result: '안녕' }, + storage, + () => 'reverse-entry', + ) + + expect(loadHistory(storage)[0]).toMatchObject({ + id: 'reverse-entry', + mode: 'reverse', + result: '안녕', + }) + }) +}) diff --git a/apps/mobile/src/lib/history.ts b/apps/mobile/src/lib/history.ts new file mode 100644 index 00000000..9db77200 --- /dev/null +++ b/apps/mobile/src/lib/history.ts @@ -0,0 +1,153 @@ +import type { TranslateMode } from '@/constants/translation' + +export const HISTORY_STORAGE_KEY = 'braillify.translation-history.v1' +export const MAX_HISTORY_ENTRIES = 20 + +export type HistoryEntry = { + createdAt: string + favorite: boolean + id: string + input: string + mode: TranslateMode + result: string +} + +export type HistoryEntryDraft = Pick + +type HistoryStorage = Pick + +function getStorage(storage?: HistoryStorage): HistoryStorage | null { + if (storage) { + return storage + } + + return typeof window === 'undefined' ? null : window.localStorage +} + +function isHistoryEntry( + value: unknown, +): value is Omit & { favorite?: boolean } { + if (!value || typeof value !== 'object') { + return false + } + + const entry = value as Partial + + return ( + typeof entry.id === 'string' && + typeof entry.createdAt === 'string' && + !Number.isNaN(Date.parse(entry.createdAt)) && + typeof entry.input === 'string' && + typeof entry.result === 'string' && + (entry.favorite === undefined || typeof entry.favorite === 'boolean') && + (entry.mode === 'general' || + entry.mode === 'math' || + entry.mode === 'reverse') + ) +} + +// 상한을 넘는 일반 기록은 오래된 것부터 제거하되, 사용자가 저장한 즐겨찾기는 +// 상한과 무관하게 보존한다. (naive slice 가 오래된 즐겨찾기까지 지우던 문제 회피) +function capHistory(entries: HistoryEntry[]): HistoryEntry[] { + if (entries.length <= MAX_HISTORY_ENTRIES) { + return entries + } + + return entries.filter( + (entry, index) => index < MAX_HISTORY_ENTRIES || entry.favorite, + ) +} + +export function loadHistory(storage?: HistoryStorage): HistoryEntry[] { + const targetStorage = getStorage(storage) + if (!targetStorage) { + return [] + } + + try { + const serializedHistory = targetStorage.getItem(HISTORY_STORAGE_KEY) + if (!serializedHistory) { + return [] + } + + const parsedHistory: unknown = JSON.parse(serializedHistory) + if (!Array.isArray(parsedHistory)) { + targetStorage.removeItem(HISTORY_STORAGE_KEY) + return [] + } + + return capHistory( + parsedHistory + .filter(isHistoryEntry) + .map((entry) => ({ ...entry, favorite: entry.favorite ?? false })), + ) + } catch { + try { + targetStorage.removeItem(HISTORY_STORAGE_KEY) + } catch { + // 저장소가 비활성화된 환경에서도 빈 히스토리로 안전하게 복구한다. + } + return [] + } +} + +function persistHistory( + entries: HistoryEntry[], + storage?: HistoryStorage, +): void { + try { + getStorage(storage)?.setItem(HISTORY_STORAGE_KEY, JSON.stringify(entries)) + } catch { + // 점역은 저장소 할당량 또는 브라우저 정책과 무관하게 계속 동작해야 한다. + } +} + +export function addHistoryEntry( + draft: HistoryEntryDraft, + storage?: HistoryStorage, + createId: () => string = () => crypto.randomUUID(), + now: () => Date = () => new Date(), +): HistoryEntry[] { + const nextEntry: HistoryEntry = { + ...draft, + createdAt: now().toISOString(), + favorite: false, + id: createId(), + } + const nextEntries = capHistory([nextEntry, ...loadHistory(storage)]) + + persistHistory(nextEntries, storage) + return nextEntries +} + +export function deleteHistoryEntry( + id: string, + storage?: HistoryStorage, +): HistoryEntry[] { + const nextEntries = loadHistory(storage).filter((entry) => entry.id !== id) + + persistHistory(nextEntries, storage) + return nextEntries +} + +export function toggleFavoriteHistoryEntry( + id: string, + storage?: HistoryStorage, +): HistoryEntry[] { + const nextEntries = loadHistory(storage).map((entry) => + entry.id === id ? { ...entry, favorite: !entry.favorite } : entry, + ) + + persistHistory(nextEntries, storage) + return nextEntries +} + +export function clearHistory(storage?: HistoryStorage): HistoryEntry[] { + try { + getStorage(storage)?.removeItem(HISTORY_STORAGE_KEY) + } catch { + // 저장소 접근이 막힌 경우에도 UI 상태는 비울 수 있도록 빈 배열을 반환한다. + } + + return [] +} diff --git a/apps/mobile/src/lib/platform.test.ts b/apps/mobile/src/lib/platform.test.ts new file mode 100644 index 00000000..1bdea84a --- /dev/null +++ b/apps/mobile/src/lib/platform.test.ts @@ -0,0 +1,21 @@ +import { afterEach, describe, expect, test } from 'bun:test' + +import { isTauriRuntime } from './platform' + +describe('isTauriRuntime', () => { + afterEach(() => { + delete (globalThis as { isTauri?: unknown }).isTauri + }) + + test('전역 isTauri 플래그가 있으면 Tauri 런타임으로 판별한다', () => { + ;(globalThis as { isTauri?: unknown }).isTauri = true + + expect(isTauriRuntime()).toBe(true) + }) + + test('전역 isTauri 플래그가 없으면 브라우저 런타임으로 판별한다', () => { + delete (globalThis as { isTauri?: unknown }).isTauri + + expect(isTauriRuntime()).toBe(false) + }) +}) diff --git a/apps/mobile/src/lib/platform.ts b/apps/mobile/src/lib/platform.ts new file mode 100644 index 00000000..7c92065d --- /dev/null +++ b/apps/mobile/src/lib/platform.ts @@ -0,0 +1,6 @@ +// Tauri 런타임 감지. +// @tauri-apps/api/core 의 isTauri() 와 동일한 판별식(`!!globalThis.isTauri`)을 사용하되, +// 브라우저 번들에 Tauri 패키지를 정적으로 포함하지 않도록 전역 플래그만 확인한다. +export function isTauriRuntime(): boolean { + return Boolean((globalThis as { isTauri?: unknown }).isTauri) +} diff --git a/apps/mobile/src/lib/translate.test.ts b/apps/mobile/src/lib/translate.test.ts new file mode 100644 index 00000000..5b59d4b6 --- /dev/null +++ b/apps/mobile/src/lib/translate.test.ts @@ -0,0 +1,191 @@ +import { describe, expect, mock, test } from 'bun:test' + +import { + EMPTY_INPUT_MESSAGE, + EMPTY_REVERSE_INPUT_MESSAGE, + INVALID_BRAILLE_MESSAGE, + type LoadTranslationEngine, + MATH_BRACE_MESSAGE, + MATH_DELIMITER_MESSAGE, + REVERSE_TRANSLATION_ERROR_MESSAGE, + translateGeneralText, + translateReverseText, + translateText, + TRANSLATION_ERROR_MESSAGE, +} from './translate' + +// braillify WASM 엔진 스텁 로더. 기본값은 "안녕" ↔ "⠣⠒⠉⠻" 왕복. +function stubEngine( + over: Partial<{ + decodeFromUnicode: (braille: string) => string + translateToUnicode: (text: string) => string + }> = {}, +): LoadTranslationEngine { + return async () => ({ + decodeFromUnicode: (braille) => (braille === '⠣⠒⠉⠻' ? '안녕' : ''), + translateToUnicode: (text) => (text === '안녕' ? '⠣⠒⠉⠻' : ''), + ...over, + }) +} + +describe('translateGeneralText', () => { + test('정상 입력을 WASM 엔진으로 점역한다', async () => { + await expect(translateGeneralText('안녕', stubEngine())).resolves.toBe( + '⠣⠒⠉⠻', + ) + }) + + test('공백뿐인 입력은 엔진을 호출하지 않고 거절한다', async () => { + const translateToUnicode = mock(() => '호출되지 않아야 함') + + await expect( + translateGeneralText(' \n\t', stubEngine({ translateToUnicode })), + ).rejects.toThrow(EMPTY_INPUT_MESSAGE) + expect(translateToUnicode).not.toHaveBeenCalled() + }) + + test('엔진 오류를 사용자용 메시지로 변환한다', async () => { + const loadEngine = stubEngine({ + translateToUnicode: () => { + throw new Error('지원하지 않는 문자: 😀') + }, + }) + + await expect(translateGeneralText('😀', loadEngine)).rejects.toThrow( + TRANSLATION_ERROR_MESSAGE, + ) + }) + + test('엔진이 던진 빈 입력 오류는 그대로 전달한다', async () => { + const loadEngine = stubEngine({ + translateToUnicode: () => { + throw new Error(EMPTY_INPUT_MESSAGE) + }, + }) + + await expect(translateGeneralText('안녕', loadEngine)).rejects.toThrow( + EMPTY_INPUT_MESSAGE, + ) + }) + + test('로더를 주입하지 않으면 실제 braillify 패키지를 불러온다', async () => { + const translateToUnicode = mock(() => '⠣⠒⠉⠻') + mock.module('braillify', () => ({ + decodeFromUnicode: () => '안녕', + translateToUnicode, + })) + + await expect(translateGeneralText('안녕')).resolves.toBe('⠣⠒⠉⠻') + expect(translateToUnicode).toHaveBeenCalledWith('안녕') + }) +}) + +describe('translateText math mode', () => { + test('올바른 $...$ LaTeX를 정규화해 엔진에 전달한다', async () => { + const translateToUnicode = mock(() => '⠼⠙⠌⠉') + + await expect( + translateText( + ' $\\frac{3}{4}$ ', + 'math', + stubEngine({ translateToUnicode }), + ), + ).resolves.toBe('⠼⠙⠌⠉') + expect(translateToUnicode).toHaveBeenCalledWith('$\\frac{3}{4}$') + }) + + test('$ 구분자가 누락된 수식을 거절한다', async () => { + await expect( + translateText('\\frac{3}{4}', 'math', stubEngine()), + ).rejects.toThrow(MATH_DELIMITER_MESSAGE) + }) + + test('중괄호 짝이 맞지 않는 수식을 거절한다', async () => { + await expect( + translateText('$\\frac{3}{4$', 'math', stubEngine()), + ).rejects.toThrow(MATH_BRACE_MESSAGE) + }) + + test('수식 내부의 $ 구분자와 닫는 중괄호 선행을 거절한다', async () => { + await expect(translateText('$x$y$', 'math', stubEngine())).rejects.toThrow( + MATH_DELIMITER_MESSAGE, + ) + await expect(translateText('$}x$', 'math', stubEngine())).rejects.toThrow( + MATH_BRACE_MESSAGE, + ) + }) + + test('일반 모드는 수학 검증 없이 엔진으로 전달한다', async () => { + await expect(translateText('안녕', 'general', stubEngine())).resolves.toBe( + '⠣⠒⠉⠻', + ) + }) +}) + +describe('translateReverseText', () => { + test('점자 유니코드를 한글로 역점역한다', async () => { + await expect(translateReverseText(' ⠣⠒⠉⠻ ', stubEngine())).resolves.toBe( + '안녕', + ) + }) + + test('로더를 주입하지 않으면 실제 braillify 패키지로 역점역한다', async () => { + mock.module('braillify', () => ({ + decodeFromUnicode: (braille: string) => + braille === '⠣⠒⠉⠻' ? '안녕' : '', + translateToUnicode: () => '', + })) + + await expect(translateText('⠣⠒⠉⠻', 'reverse')).resolves.toBe('안녕') + }) + + test('빈 입력과 일반 문자가 섞인 입력을 거절한다', async () => { + await expect(translateReverseText(' ', stubEngine())).rejects.toThrow( + EMPTY_REVERSE_INPUT_MESSAGE, + ) + await expect(translateReverseText('⠣안녕', stubEngine())).rejects.toThrow( + INVALID_BRAILLE_MESSAGE, + ) + await expect(translateReverseText('⡀', stubEngine())).rejects.toThrow( + INVALID_BRAILLE_MESSAGE, + ) + }) + + test('빈 점자 셀(U+2800)만 있으면 역점역할 점자가 없다고 알린다', async () => { + await expect(translateReverseText('⠀ ⠀', stubEngine())).rejects.toThrow( + EMPTY_REVERSE_INPUT_MESSAGE, + ) + }) + + test('디코딩 결과가 비어 있으면 역점역 실패로 처리한다', async () => { + const loadEngine = stubEngine({ decodeFromUnicode: () => '' }) + + await expect(translateReverseText('⠣⠒⠉⠻', loadEngine)).rejects.toThrow( + REVERSE_TRANSLATION_ERROR_MESSAGE, + ) + }) + + test('디코더 오류를 사용자용 메시지로 변환한다', async () => { + const loadEngine = stubEngine({ + decodeFromUnicode: () => { + throw new Error('decode failed') + }, + }) + + await expect(translateReverseText('⠣⠒⠉⠻', loadEngine)).rejects.toThrow( + REVERSE_TRANSLATION_ERROR_MESSAGE, + ) + }) + + test('디코더가 돌려준 검증 오류는 그대로 전달한다', async () => { + const loadEngine = stubEngine({ + decodeFromUnicode: () => { + throw new Error(INVALID_BRAILLE_MESSAGE) + }, + }) + + await expect(translateReverseText('⠣⠒⠉⠻', loadEngine)).rejects.toThrow( + INVALID_BRAILLE_MESSAGE, + ) + }) +}) diff --git a/apps/mobile/src/lib/translate.ts b/apps/mobile/src/lib/translate.ts new file mode 100644 index 00000000..e91110ae --- /dev/null +++ b/apps/mobile/src/lib/translate.ts @@ -0,0 +1,136 @@ +import type { TranslateMode } from '@/constants/translation' + +export const EMPTY_INPUT_MESSAGE = '점역할 내용을 입력해 주세요.' +export const MATH_DELIMITER_MESSAGE = + 'LaTeX 수식 전체를 $...$ 형식으로 입력해 주세요.' +export const MATH_BRACE_MESSAGE = 'LaTeX 중괄호의 짝을 확인해 주세요.' +export const TRANSLATION_ERROR_MESSAGE = + '이 내용은 점역할 수 없습니다. 입력을 확인해 주세요.' +export const EMPTY_REVERSE_INPUT_MESSAGE = '역점역할 점자를 입력해 주세요.' +export const INVALID_BRAILLE_MESSAGE = + '점자 유니코드와 공백만 입력할 수 있습니다.' +export const REVERSE_TRANSLATION_ERROR_MESSAGE = + '이 점자는 역점역할 수 없습니다. 입력을 확인해 주세요.' + +// 점역 엔진은 workspace `braillify` WASM 패키지를 웹뷰에서 직접 호출한다. +// Tauri IPC 커맨드를 두지 않고 iOS/Android/데스크톱 모두 동일한 WASM 경로를 쓴다. +type TranslationEngine = { + decodeFromUnicode: (braille: string) => string + translateToUnicode: (text: string) => string +} + +export type LoadTranslationEngine = () => Promise + +const loadTranslationEngine: LoadTranslationEngine = () => import('braillify') + +export async function translateGeneralText( + input: string, + loadEngine: LoadTranslationEngine = loadTranslationEngine, +): Promise { + if (input.trim().length === 0) { + throw new Error(EMPTY_INPUT_MESSAGE) + } + + try { + const { translateToUnicode } = await loadEngine() + return translateToUnicode(input) + } catch (error) { + if (error instanceof Error && error.message === EMPTY_INPUT_MESSAGE) { + throw error + } + + throw new Error(TRANSLATION_ERROR_MESSAGE, { cause: error }) + } +} + +export async function translateReverseText( + input: string, + loadEngine: LoadTranslationEngine = loadTranslationEngine, +): Promise { + const normalizedInput = input.trim() + if (normalizedInput.length === 0) { + throw new Error(EMPTY_REVERSE_INPUT_MESSAGE) + } + if (!/^[⠀-⠿\s]+$/u.test(input)) { + throw new Error(INVALID_BRAILLE_MESSAGE) + } + if (!/[⠁-⠿]/u.test(input)) { + throw new Error(EMPTY_REVERSE_INPUT_MESSAGE) + } + + try { + const { decodeFromUnicode } = await loadEngine() + const result = decodeFromUnicode(normalizedInput) + if (!result) { + throw new Error('empty reverse translation') + } + return result + } catch (error) { + if ( + error instanceof Error && + (error.message === EMPTY_REVERSE_INPUT_MESSAGE || + error.message === INVALID_BRAILLE_MESSAGE) + ) { + throw error + } + throw new Error(REVERSE_TRANSLATION_ERROR_MESSAGE, { cause: error }) + } +} + +function validateMathInput(input: string): string { + const normalizedInput = input.trim() + + if ( + normalizedInput.length < 3 || + !normalizedInput.startsWith('$') || + !normalizedInput.endsWith('$') + ) { + throw new Error(MATH_DELIMITER_MESSAGE) + } + + let braceDepth = 0 + + for (let index = 1; index < normalizedInput.length - 1; index += 1) { + const character = normalizedInput[index] + + if (character === '\\') { + index += 1 + continue + } + + if (character === '$') { + throw new Error(MATH_DELIMITER_MESSAGE) + } + + if (character === '{') { + braceDepth += 1 + } else if (character === '}') { + braceDepth -= 1 + if (braceDepth < 0) { + throw new Error(MATH_BRACE_MESSAGE) + } + } + } + + if (braceDepth !== 0) { + throw new Error(MATH_BRACE_MESSAGE) + } + + return normalizedInput +} + +export async function translateText( + input: string, + mode: TranslateMode, + loadEngine: LoadTranslationEngine = loadTranslationEngine, +): Promise { + if (mode === 'reverse') { + return translateReverseText(input, loadEngine) + } + + if (mode === 'math') { + return translateGeneralText(validateMathInput(input), loadEngine) + } + + return translateGeneralText(input, loadEngine) +} diff --git a/apps/mobile/tsconfig.json b/apps/mobile/tsconfig.json new file mode 100644 index 00000000..c49d1d0a --- /dev/null +++ b/apps/mobile/tsconfig.json @@ -0,0 +1,45 @@ +{ + "compilerOptions": { + "target": "ES2020", + "lib": [ + "ES2022", + "DOM", + "DOM.Iterable" + ], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "react-jsx", + "incremental": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": [ + "./src/*" + ] + } + }, + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts", + ".df/*.ts", + ".next/dev/types/**/*.ts" + ], + "exclude": [ + "node_modules" + ] +} diff --git a/bun.lock b/bun.lock index fb78c793..2f4b4ffd 100644 --- a/bun.lock +++ b/bun.lock @@ -41,6 +41,27 @@ "typescript": "^6.0.3", }, }, + "apps/mobile": { + "name": "mobile", + "version": "0.1.0", + "dependencies": { + "@devup-ui/react": "^1.0.37", + "@tauri-apps/plugin-clipboard-manager": "^2", + "braillify": "workspace:*", + "next": "16.2.6", + "react": "^19.2.6", + "react-dom": "^19.2.6", + }, + "devDependencies": { + "@devup-ui/next-plugin": "^1.0.77", + "@tauri-apps/cli": "^2", + "@types/node": "^25", + "@types/react": "^19", + "@types/react-dom": "^19", + "babel-plugin-react-compiler": "^1.0.0", + "typescript": "~5.8.3", + }, + }, "packages/node": { "name": "braillify", "version": "2.1.0", @@ -272,10 +293,40 @@ "@pkgr/core": ["@pkgr/core@0.3.6", "", {}, "sha512-SEeaJLb3qBNF/OaXnaR1NmmBbFYk1zC0ZH/52fATcRPLFg/p791YrcyFFy44Bo9sLaGuSuLp5Q6axbb/O+v/RA=="], + "@playwright/test": ["@playwright/test@1.62.0", "", { "dependencies": { "playwright": "1.62.0" }, "bin": { "playwright": "cli.js" } }, "sha512-9zOJ6ZQRAena31MpOH9VSzIz8Ou3YJ/wtY/eQm5T2uhfhG7/U3COrMS8xOtUrZrp9OgdmzEnIYODye3nY1VqzA=="], + "@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="], "@tanstack/eslint-plugin-query": ["@tanstack/eslint-plugin-query@5.101.2", "", { "dependencies": { "@typescript-eslint/utils": "^8.58.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": "^5.4.0 || ^6.0.0" }, "optionalPeers": ["typescript"] }, "sha512-cPE99s3XZwlObfn8lCezT4j4JLj2CVzpIEywx0H4hzfPsX/o9QhdwaOwcDXxrQAqx2ds7TbvTinxhB8B/ywb6w=="], + "@tauri-apps/api": ["@tauri-apps/api@2.11.1", "", {}, "sha512-M2FPuYND2m+wh5hfW9ZpSdxMPdEJovPBWwoHJmwUpysTYNHaOkVFN419m/K0LIgjb/7KU2vBgsUepJWugQCvAA=="], + + "@tauri-apps/cli": ["@tauri-apps/cli@2.11.4", "", { "optionalDependencies": { "@tauri-apps/cli-darwin-arm64": "2.11.4", "@tauri-apps/cli-darwin-x64": "2.11.4", "@tauri-apps/cli-linux-arm-gnueabihf": "2.11.4", "@tauri-apps/cli-linux-arm64-gnu": "2.11.4", "@tauri-apps/cli-linux-arm64-musl": "2.11.4", "@tauri-apps/cli-linux-riscv64-gnu": "2.11.4", "@tauri-apps/cli-linux-x64-gnu": "2.11.4", "@tauri-apps/cli-linux-x64-musl": "2.11.4", "@tauri-apps/cli-win32-arm64-msvc": "2.11.4", "@tauri-apps/cli-win32-ia32-msvc": "2.11.4", "@tauri-apps/cli-win32-x64-msvc": "2.11.4" }, "bin": { "tauri": "tauri.js" } }, "sha512-R8xGtMpwyetawSqm9kYOuMmEqkhUbvcUy8n0aNXIxollKBLESUu5f4Fx+64hgASYm1H+jSWq6jCW6zqTnH6hqQ=="], + + "@tauri-apps/cli-darwin-arm64": ["@tauri-apps/cli-darwin-arm64@2.11.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-1ryOF3ZhpZ/nemHV5zVwBQBz9jDGKmKPvWPADOhc83ig0P4bMc2iER4NbC6r9sjeIZ6RVQ4g3RZIYvezhcl4TQ=="], + + "@tauri-apps/cli-darwin-x64": ["@tauri-apps/cli-darwin-x64@2.11.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-uFsGQAAfuyz1k/yGLmkWfkBlgKAqZfxqlHmLWx81QU27RJWfmbNHCIq8T8w1e+VClleIuZUjpHWfoE4E3DLo3A=="], + + "@tauri-apps/cli-linux-arm-gnueabihf": ["@tauri-apps/cli-linux-arm-gnueabihf@2.11.4", "", { "os": "linux", "cpu": "arm" }, "sha512-IaHZn5CdBL21oUmjiVOS1ctw6Ip1O0pjp70FwOWmYz1myWe0SY96ZIj2FYf7pT0m8bI2h/hrs5ZbEXXh44/MkQ=="], + + "@tauri-apps/cli-linux-arm64-gnu": ["@tauri-apps/cli-linux-arm64-gnu@2.11.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-N41/ukTRVe6XSuUTESuFdGeOW2i7k62tK+6gHK5Kd5/q5RPvvi19GaWAVPPb9u95HSGmTChSolBfzynUsssFaA=="], + + "@tauri-apps/cli-linux-arm64-musl": ["@tauri-apps/cli-linux-arm64-musl@2.11.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-v277UnT/fB64xAfSroL5N3Km3tLmvATWqJJw/wRI+g6o+HkeD0slyE7gOhNs1MbjE41R7bQOTxMVoL3aomUJmw=="], + + "@tauri-apps/cli-linux-riscv64-gnu": ["@tauri-apps/cli-linux-riscv64-gnu@2.11.4", "", { "os": "linux", "cpu": "none" }, "sha512-qqgNkQ2u1yZHxjhxsZaxUtRDW8dIqIYm33rx/mzwQv0SfY9x1B+iraj8vWeFiXjjSVVhEMepXSOts1TqPzvXNQ=="], + + "@tauri-apps/cli-linux-x64-gnu": ["@tauri-apps/cli-linux-x64-gnu@2.11.4", "", { "os": "linux", "cpu": "x64" }, "sha512-2VRNWl84FOH0m2giiDkO2h0QXlcMJeX+zJDpI5kDIQAx6s+geF3v48F4DXfJez4GS/FdoDGnPnw1C2iYGbQ7bQ=="], + + "@tauri-apps/cli-linux-x64-musl": ["@tauri-apps/cli-linux-x64-musl@2.11.4", "", { "os": "linux", "cpu": "x64" }, "sha512-o9GyhYor/nc7xarmwDE3ka2szuW3uuZzXjHWh64Q8YX5AtSgxdQkFWzrY4O8KiGtVNvFBI14H3Q49Qj5TOIP/A=="], + + "@tauri-apps/cli-win32-arm64-msvc": ["@tauri-apps/cli-win32-arm64-msvc@2.11.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-ld5Ehb598m0VkYyylRPNeCFsBe/km0jxis6KgMpl3IGY6I/i1RwQXO05I1AsXUXO2WC6AvB/Lw4qTf/asiuEiQ=="], + + "@tauri-apps/cli-win32-ia32-msvc": ["@tauri-apps/cli-win32-ia32-msvc@2.11.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-12Hxi0XX/H5VFxO/bGgHkFWhml9VMgEOu9CidjeCeTNQ1l6fpUlbiGgSP7CLI3PFtW9/FfbeHieZ+kyWK5H7CA=="], + + "@tauri-apps/cli-win32-x64-msvc": ["@tauri-apps/cli-win32-x64-msvc@2.11.4", "", { "os": "win32", "cpu": "x64" }, "sha512-+vDiqBIU5dMISg/wNvX3sF+ZHfgJGJ5T0AcO+EHNXV9GGAG+P5fzodlDXD3QdKCRgZxMoCm5PPvj3BqLNjBthw=="], + + "@tauri-apps/plugin-clipboard-manager": ["@tauri-apps/plugin-clipboard-manager@2.3.2", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-CUlb5Hqi2oZbcZf4VUyUH53XWPPdtpw43EUpCza5HWZJwxEoDowFzNUDt1tRUXA8Uq+XPn17Ysfptip33sG4eQ=="], + "@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="], "@types/concat-stream": ["@types/concat-stream@2.0.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-3qe4oQAPNwVNwK4C9c8u+VJqv9kez+2MR4qJpoPFfXtgxxif1QbFusvXzK0/Wra2VX07smostI2VMmJNSpZjuQ=="], @@ -576,6 +627,8 @@ "format": ["format@0.2.2", "", {}, "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww=="], + "fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="], + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], "function.prototype.name": ["function.prototype.name@1.2.0", "", { "dependencies": { "call-bind": "^1.0.9", "call-bound": "^1.0.4", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", "has-property-descriptors": "^1.0.2", "hasown": "^2.0.4", "is-callable": "^1.2.7", "is-document.all": "^1.0.0" } }, "sha512-jObKIik1P2QjPHP5nz5BaOtUlfgS0fWo8IUByNXkM+o+02sJOi94em77GwJKQSJ3gfPHdgzLNrHc1uokV4P/ew=="], @@ -842,6 +895,8 @@ "minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="], + "mobile": ["mobile@workspace:apps/mobile"], + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], "nanoid": ["nanoid@3.3.16", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q=="], @@ -908,6 +963,10 @@ "picomatch": ["picomatch@4.0.5", "", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="], + "playwright": ["playwright@1.62.0", "", { "dependencies": { "playwright-core": "1.62.0" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-Z14dG305dgaLu6foB1TXQagFiW8JfSUIUaUuPaKQ6NtBPKF1P/qXcqfh6c6K/icPqdy37JmjbiBXf6JNg6Sylw=="], + + "playwright-core": ["playwright-core@1.62.0", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-nsNRyq0r2zsG8AcRHWknc9QRA5XCueC7gWMrs+Gx2tlZn9hcl8zudfh00lhJPY1DE7NmZ6bDsT9g2yey8mXljA=="], + "possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="], "postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="], @@ -1188,6 +1247,12 @@ "hosted-git-info/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], + "mobile/@types/node": ["@types/node@25.9.5", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-OScDchr2fwuUmWdf4kZ9h7PcJiYDVInhJizG/biAq3cAvqwYktuy/TYGGdZNMtNTFUP7rnb0NU4TUdm82kt4Rg=="], + + "mobile/next": ["next@16.2.6", "", { "dependencies": { "@next/env": "16.2.6", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.2.6", "@next/swc-darwin-x64": "16.2.6", "@next/swc-linux-arm64-gnu": "16.2.6", "@next/swc-linux-arm64-musl": "16.2.6", "@next/swc-linux-x64-gnu": "16.2.6", "@next/swc-linux-x64-musl": "16.2.6", "@next/swc-win32-arm64-msvc": "16.2.6", "@next/swc-win32-x64-msvc": "16.2.6", "sharp": "^0.34.5" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-qOVgKJg1+At15NpeUP+eJgCHvTCgXsogweq87Ri/Ix7PkqQHg4sdaXmSFqKlgaIXE4kW0g25LE68W87UANlHtw=="], + + "mobile/typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], + "normalize-package-data/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], "npm-install-checks/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], @@ -1232,6 +1297,26 @@ "glob/minimatch/brace-expansion": ["brace-expansion@2.1.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA=="], + "mobile/@types/node/undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="], + + "mobile/next/@next/env": ["@next/env@16.2.6", "", {}, "sha512-gd8HoHN4ufj73WmR3JmVolrpJR47ILK6LouP5xElPglaVxir6e1a7VzvTvDWkOoPXT9rkkTzyCxBu4yeZfZwcw=="], + + "mobile/next/@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.2.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ZJGkkcNfYgrrMkqOdZ7zoLa1TOy0qpcMfk/z4Mh/FKUz40gVO+HNQWqmLxf67Z5WB64DRp0dhEbyHfel+6sJUg=="], + + "mobile/next/@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.2.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-v/YLBHIY132Ced3puBJ7YJKw1lqsCrgcNo2aRJlCEyQrrCeRJlvGlnmxhPxNQI3KE3N1DN5r9TPNPvka3nq5RQ=="], + + "mobile/next/@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.2.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-RPOvqlYBbcQjkz9VQQDZ2T2bARIjXZV1KFlt+V2Mr6SW/e4I9fcKsaA0hdyf2FHoTlsV2xnBd5Y912rP/1Ce6w=="], + + "mobile/next/@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.2.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-URUTu1+dMkxJsPFgm+OeEvq9wf5sujw0EvgYy80TDGHTSLTnIHeqb0Eu8A3sC95IRgjejQL+kC4mw+4yPxiAXA=="], + + "mobile/next/@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.2.6", "", { "os": "linux", "cpu": "x64" }, "sha512-DOj182mPV8G3UkrayLoREM5YEYI+Dk5wv7Ox9xl1fFibAELEsFD0lDPfHIeILlutMMfdyhlzYPELG3peuKaurw=="], + + "mobile/next/@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.2.6", "", { "os": "linux", "cpu": "x64" }, "sha512-HKQ5SP/V/ub73UvF7n/zeJlxk2kLmtL7Wzrg4WfmkjmNos5onJ2tKu7yZOPdL18A6Svfn3max29ym+ry7NkK4g=="], + + "mobile/next/@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.2.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-LZXpTlPyS5v7HhSmnvsLGP3iIYgYOBnc8r8ArlT55sGHV89bR2HlDdBjWQ+PY6SJMmk8TuVGFuxalnP3k/0Dwg=="], + + "mobile/next/@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.2.6", "", { "os": "win32", "cpu": "x64" }, "sha512-F0+4i0h9J6C4eE3EAPWsoCk7UW/dbzOjyzxY0qnDUOYFu6FFmdZ6l97/XdV3/Nz3VYyO7UWjyEJUXkGqcoXfMA=="], + "string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "unified-engine/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], diff --git a/libs/braillify/src/decoder.rs b/libs/braillify/src/decoder.rs new file mode 100644 index 00000000..d7a9e08b --- /dev/null +++ b/libs/braillify/src/decoder.rs @@ -0,0 +1,603 @@ +/// Simple Korean braille decoder (점자 → 한글). +/// Handles basic Korean syllables, char shortcuts (가나다…), and abbreviated +/// syllable shortcuts (을 억 언 …). Numbers, English, and word shortcuts are +/// not yet handled. +pub fn decode(braille: &str) -> Result { + let bytes: Vec = braille + .chars() + .filter_map(|c| { + let code = c as u32; + if (0x2800..=0x28FF).contains(&code) { + Some((code - 0x2800) as u8) + } else if c == ' ' || c == '\n' { + Some(0) + } else { + None + } + }) + .collect(); + + decode_bytes(&bytes) +} + +fn decode_bytes(bytes: &[u8]) -> Result { + let mut result = String::new(); + let mut i = 0; + + enum Stage { + Start, + GotCho(u32), + GotJung(u32, u32), // (cho_idx, jung_idx) + } + + let mut stage = Stage::Start; + + while i < bytes.len() { + let b = bytes[i]; + + match stage { + // ─── Start: no pending syllable ────────────────────────────── + Stage::Start => { + if b == 0 { + result.push(' '); + i += 1; + } else if let Some((ch, len)) = try_two_byte_shortcut(bytes, i) { + result.push(ch); + i += len; + } else if let Some((jung, jong)) = try_abbreviated_shortcut(b) { + // standalone 을/억/언/… → ㅇ초성 + jung + jong + result.push(build_syllable(11, jung, jong)); + i += 1; + } else if let Some(cho) = try_consonant_a_shortcut(b) { + // 가(43) or 사(7) — need to collect possible jongseong + stage = Stage::GotJung(cho, 0); // jung=0 = ㅏ + i += 1; + } else if let Some((jung, consumed)) = try_jungseong(bytes, i) { + stage = Stage::GotJung(11, jung); // silent ㅇ + i += consumed; + } else if let Some((cho, consumed)) = try_choseong_with_double(bytes, i) { + stage = Stage::GotCho(cho); + i += consumed; + } else { + i += 1; // skip unknown byte + } + } + + // ─── GotCho: have initial consonant, waiting for vowel ──────── + Stage::GotCho(cho) => { + if let Some((jung, consumed)) = try_jungseong(bytes, i) { + stage = Stage::GotJung(cho, jung); + i += consumed; + } else if let Some((jung, jong)) = try_abbreviated_shortcut(b) { + // e.g. 글 = ㄱ(8) + 을(46) → cho + ㅡ + ㄹ + result.push(build_syllable(cho, jung, jong)); + stage = Stage::Start; + i += 1; + } else if b == 0 { + // consonant followed immediately by space → standalone jamo + result.push(cho_idx_to_jamo(cho)); + result.push(' '); + stage = Stage::Start; + i += 1; + } else { + // No vowel followed — this choseong byte doubles as a + // 나/다/마/바/자/카/타/파/하 shortcut (implicit ㅏ vowel). + // Don't consume the current byte; let GotJung handle it. + stage = Stage::GotJung(cho, 0); // jung=0 = ㅏ + } + } + + // ─── GotJung: have cho+jung, looking for jongseong ─────────── + Stage::GotJung(cho, jung) => { + // Priority 1: two-byte shortcuts (성 정 청 것) — check before jongseong + // because their first bytes (32,40,48,56) can also start jongseong sequences. + if let Some((ch, len)) = try_two_byte_shortcut(bytes, i) { + result.push(build_syllable(cho, jung, 0)); + result.push(ch); + stage = Stage::Start; + i += len; + } + // Priority 2: jongseong + else if let (jong @ 1.., len) = try_jongseong(bytes, i) { + result.push(build_syllable(cho, jung, jong)); + stage = Stage::Start; + i += len; + } + // Priority 3: space + else if b == 0 { + result.push(build_syllable(cho, jung, 0)); + result.push(' '); + stage = Stage::Start; + i += 1; + } + // Priority 4: abbreviated shortcut byte starts a new syllable + // (e.g. "나을" — emit 나, then 을 as ㅇ+ㅡ+ㄹ) + else if let Some((jung2, jong2)) = try_abbreviated_shortcut(b) { + result.push(build_syllable(cho, jung, 0)); + result.push(build_syllable(11, jung2, jong2)); + stage = Stage::Start; + i += 1; + } + // Priority 5: 가(43) or 사(7) shortcut starts a new syllable + else if let Some(cho2) = try_consonant_a_shortcut(b) { + result.push(build_syllable(cho, jung, 0)); + stage = Stage::GotJung(cho2, 0); + i += 1; + } + // Priority 6: choseong starts next syllable (incl. tense consonants) + else if let Some((new_cho, consumed)) = try_choseong_with_double(bytes, i) { + result.push(build_syllable(cho, jung, 0)); + stage = Stage::GotCho(new_cho); + i += consumed; + } + // Priority 7: consecutive vowel → emit current, silent ㅇ + new vowel + else if let Some((new_jung, consumed)) = try_jungseong(bytes, i) { + result.push(build_syllable(cho, jung, 0)); + stage = Stage::GotJung(11, new_jung); + i += consumed; + } + // Unknown byte + else { + result.push(build_syllable(cho, jung, 0)); + stage = Stage::Start; + i += 1; + } + } + } + } + + // Flush remaining state + match stage { + Stage::GotCho(cho) => result.push(build_syllable(cho, 0, 0)), // implicit ㅏ shortcut + Stage::GotJung(cho, jung) => result.push(build_syllable(cho, jung, 0)), + Stage::Start => {} + } + + Ok(result.trim().to_string()) +} + +// ── Two-byte shortcuts ───────────────────────────────────────────────────── +// 성/정/청/것 are conventional pairings — NOT phonetically composed. +fn try_two_byte_shortcut(bytes: &[u8], i: usize) -> Option<(char, usize)> { + let b0 = *bytes.get(i)?; + let b1 = bytes.get(i + 1).copied()?; + match (b0, b1) { + (32, 59) => Some(('성', 2)), // ⠠⠻ + (40, 59) => Some(('정', 2)), // ⠨⠻ + (48, 59) => Some(('청', 2)), // ⠰⠻ + (56, 14) => Some(('것', 2)), // ⠸⠎ + _ => None, + } +} + +// ── Abbreviated syllable shortcuts ──────────────────────────────────────── +// These bytes represent a (jung, jong) pair when they appear: +// • after a choseong → GotCho uses them to complete the syllable +// • in Start/GotJung → emit ㅇ(11)+jung+jong as standalone +// +// Unicode TIndex (jongseong position in syllable formula): +// ㄱ=1 ㄴ=4 ㄷ=7 ㄹ=8 ㅁ=16 ㅂ=17 ㅅ=19 ㅆ=20 ㅇ=21 ㅈ=22 ㅊ=23 ㅋ=24 ㅌ=25 ㅍ=26 ㅎ=27 +fn try_abbreviated_shortcut(b: u8) -> Option<(u32, u32)> { + match b { + 57 => Some((4, 1)), // 억: ㅓ + ㄱ + 62 => Some((4, 4)), // 언: ㅓ + ㄴ + 30 => Some((4, 8)), // 얼: ㅓ + ㄹ + 33 => Some((6, 4)), // 연: ㅕ + ㄴ + 51 => Some((6, 8)), // 열: ㅕ + ㄹ + 59 => Some((6, 21)), // 영: ㅕ + ㅇ + 45 => Some((8, 1)), // 옥: ㅗ + ㄱ + 55 => Some((8, 4)), // 온: ㅗ + ㄴ + 63 => Some((8, 21)), // 옹: ㅗ + ㅇ + 27 => Some((13, 4)), // 운: ㅜ + ㄴ + 47 => Some((13, 8)), // 울: ㅜ + ㄹ + 53 => Some((18, 4)), // 은: ㅡ + ㄴ + 46 => Some((18, 8)), // 을: ㅡ + ㄹ + 31 => Some((20, 4)), // 인: ㅣ + ㄴ + _ => None, + } +} + +// ── Consonant-a shortcuts (가, 사) ──────────────────────────────────────── +// These bytes encode a specific consonant + implicit ㅏ vowel, +// but unlike the shared-byte shortcuts (나=9=ㄴ초성) these bytes +// are NOT in the choseong map, so we need a separate handler. +fn try_consonant_a_shortcut(b: u8) -> Option { + match b { + 43 => Some(0), // 가: ㄱ(0) + ㅏ + 7 => Some(9), // 사: ㅅ(9) + ㅏ + _ => None, + } +} + +// ── Choseong ───────────────────────────────────────────────────────────── +// Unicode choseong indices: +// ㄱ=0 ㄲ=1 ㄴ=2 ㄷ=3 ㄸ=4 ㄹ=5 ㅁ=6 ㅂ=7 ㅃ=8 ㅅ=9 ㅆ=10 +// ㅇ=11 ㅈ=12 ㅉ=13 ㅊ=14 ㅋ=15 ㅌ=16 ㅍ=17 ㅎ=18 +fn try_choseong(b: u8) -> Option { + match b { + 8 => Some(0), // ㄱ + 9 => Some(2), // ㄴ (also 나 shortcut when not followed by jungseong) + 10 => Some(3), // ㄷ (also 다 shortcut) + 11 => Some(15), // ㅋ (also 카 shortcut) + 16 => Some(5), // ㄹ + 17 => Some(6), // ㅁ (also 마 shortcut) + 19 => Some(16), // ㅌ (also 타 shortcut) + 24 => Some(7), // ㅂ (also 바 shortcut) + 25 => Some(17), // ㅍ (also 파 shortcut) + 26 => Some(18), // ㅎ (also 하 shortcut) + 32 => Some(9), // ㅅ (also 사-related, but ⠠ 사=7) + 40 => Some(12), // ㅈ (also 자 shortcut) + 48 => Some(14), // ㅊ + _ => None, + } +} + +// ── Double (tense) choseong — 제2항 된소리 ─────────────────────────────── +// 된소리표 ⠠ (byte 32) + base consonant byte → tense choseong. +// 32 + 8 → ㄲ (idx 1) +// 32 + 10 → ㄸ (idx 4) +// 32 + 24 → ㅃ (idx 8) +// 32 + 32 → ㅆ (idx 10) +// 32 + 40 → ㅉ (idx 13) +// Falls back to try_choseong (single byte, ㅅ) if not a double pair. +fn try_choseong_with_double(bytes: &[u8], i: usize) -> Option<(u32, usize)> { + let b0 = *bytes.get(i)?; + if b0 == 32 + && let Some(&b1) = bytes.get(i + 1) + { + // 된소리표(32) 뒤에 오는 바이트 패턴: + // ㅏ 모음: 된소리표 + 약자 바이트 (가=43, 사=7, 나/다/바/자는 공유) + // 기타 모음: 된소리표 + 초성 바이트 (ㄱ=8, ㅅ=32) + 별도 중성 + // 예) 까=[32,43] 꺼=[32,8,14] 싸=[32,7] 쏘=[32,32,37] + let double_idx = match b1 { + 8 | 43 => Some(1), // ㄲ: 8=ㄱ초성(꺼·끼…), 43=가약자(까) + 10 => Some(4), // ㄸ: 공유바이트 (따·뚜… 모두) + 24 => Some(8), // ㅃ: 공유바이트 (빠·뿌… 모두) + 7 | 32 => Some(10), // ㅆ: 7=사약자(싸), 32=ㅅ초성(쏘·씩…) + 40 => Some(13), // ㅉ: 공유바이트 (짜·쭈… 모두) + _ => None, + }; + if let Some(idx) = double_idx { + return Some((idx, 2)); + } + } + try_choseong(b0).map(|idx| (idx, 1)) +} + +// ── Jungseong ──────────────────────────────────────────────────────────── +// Unicode jungseong indices: +// ㅏ=0 ㅐ=1 ㅑ=2 ㅒ=3 ㅓ=4 ㅔ=5 ㅕ=6 ㅖ=7 ㅗ=8 ㅘ=9 ㅙ=10 ㅚ=11 ㅛ=12 +// ㅜ=13 ㅝ=14 ㅞ=15 ㅟ=16 ㅠ=17 ㅡ=18 ㅢ=19 ㅣ=20 +fn try_jungseong(bytes: &[u8], i: usize) -> Option<(u32, usize)> { + let b = *bytes.get(i)?; + let b2 = bytes.get(i + 1).copied(); + + // Compound vowels (2 bytes) checked first + match (b, b2) { + (13, Some(23)) => return Some((16, 2)), // ㅟ [⠍⠗] + (28, Some(23)) => return Some((3, 2)), // ㅒ [⠜⠗] + (39, Some(23)) => return Some((10, 2)), // ㅙ [⠧⠗] + (15, Some(23)) => return Some((15, 2)), // ㅞ [⠏⠗] + _ => {} + } + + let idx = match b { + 35 => 0, // ㅏ + 23 => 1, // ㅐ + 28 => 2, // ㅑ + 14 => 4, // ㅓ + 29 => 5, // ㅔ + 49 => 6, // ㅕ + 12 => 7, // ㅖ + 37 => 8, // ㅗ + 39 => 9, // ㅘ + 61 => 11, // ㅚ + 44 => 12, // ㅛ + 13 => 13, // ㅜ + 15 => 14, // ㅝ + 41 => 17, // ㅠ + 42 => 18, // ㅡ + 58 => 19, // ㅢ + 21 => 20, // ㅣ + _ => return None, + }; + Some((idx, 1)) +} + +// ── Jongseong ──────────────────────────────────────────────────────────── +// Unicode jongseong TIndex (0 = none): +// ㄱ=1 ㄲ=2 ㄳ=3 ㄴ=4 ㄵ=5 ㄶ=6 ㄷ=7 ㄹ=8 +// ㄺ=9 ㄻ=10 ㄼ=11 ㄽ=12 ㄾ=13 ㄿ=14 ㅀ=15 ㅁ=16 ㅂ=17 ㅄ=18 +// ㅅ=19 ㅆ=20 ㅇ=21 ㅈ=22 ㅊ=23 ㅋ=24 ㅌ=25 ㅍ=26 ㅎ=27 +fn try_jongseong(bytes: &[u8], i: usize) -> (u32, usize) { + let b = match bytes.get(i) { + Some(&v) => v, + None => return (0, 0), + }; + let b2 = bytes.get(i + 1).copied(); + + // Compound jongseong (2 bytes) checked first + match (b, b2) { + (1, Some(1)) => return (2, 2), // ㄲ + (1, Some(4)) => return (3, 2), // ㄳ + (18, Some(5)) => return (5, 2), // ㄵ + (18, Some(52)) => return (6, 2), // ㄶ + (2, Some(1)) => return (9, 2), // ㄺ + (2, Some(34)) => return (10, 2), // ㄻ + (2, Some(3)) => return (11, 2), // ㄼ + (2, Some(4)) => return (12, 2), // ㄽ + (2, Some(38)) => return (13, 2), // ㄾ + (2, Some(50)) => return (14, 2), // ㄿ + (2, Some(52)) => return (15, 2), // ㅀ + (3, Some(4)) => return (18, 2), // ㅄ + _ => {} + } + + let idx = match b { + 1 => 1, // ㄱ + 18 => 4, // ㄴ + 20 => 7, // ㄷ + 2 => 8, // ㄹ + 34 => 16, // ㅁ + 3 => 17, // ㅂ + 4 => 19, // ㅅ + 12 => 20, // ㅆ + 54 => 21, // ㅇ + 5 => 22, // ㅈ + 6 => 23, // ㅊ + 22 => 24, // ㅋ + 38 => 25, // ㅌ + 50 => 26, // ㅍ + 52 => 27, // ㅎ + _ => return (0, 0), + }; + (idx, 1) +} + +// ── Syllable builder ───────────────────────────────────────────────────── + +fn build_syllable(cho: u32, jung: u32, jong: u32) -> char { + let code = (cho * 21 + jung) * 28 + jong + 0xAC00; + char::from_u32(code).unwrap_or('?') +} + +fn cho_idx_to_jamo(idx: u32) -> char { + const JAMO: &[char] = &[ + 'ㄱ', 'ㄲ', 'ㄴ', 'ㄷ', 'ㄸ', 'ㄹ', 'ㅁ', 'ㅂ', 'ㅃ', 'ㅅ', 'ㅆ', 'ㅇ', 'ㅈ', 'ㅉ', 'ㅊ', + 'ㅋ', 'ㅌ', 'ㅍ', 'ㅎ', + ]; + JAMO.get(idx as usize).copied().unwrap_or('?') +} + +// ── Tests ──────────────────────────────────────────────────────────────── + +#[cfg(test)] +mod tests { + //! 공개 Unicode API, encoder roundtrip, 내부 셀 상태 전이를 서로 분리해 검증한다. + //! `decode_cells`에 전달하는 숫자는 점자 Unicode의 U+2800 오프셋과 같은 6점 셀 값이다. + + use super::*; + use rstest::rstest; + + /// 지원 범위의 한글을 공개 encoder와 decoder에 연속으로 통과시킨다. + fn roundtrip(text: &str) -> String { + let encoded = crate::encode_to_unicode(text).expect("encode failed"); + decode(&encoded).expect("decode failed") + } + + /// Unicode 입력 정규화를 거치지 않고 decoder 상태 머신에 셀을 직접 전달한다. + fn decode_cells(cells: &[u8]) -> String { + decode_bytes(cells).expect("decode failed") + } + + /// 공개 API가 점자 이외 문자를 무시하고 명시적인 공백 경계는 보존하는지 검증한다. + #[rstest] + #[case::geo_ri("⠈⠎⠐⠕", "거리")] + #[case::space_after_choseong("⠈ ⠉", "ㄱ 나")] + #[case::newline_gap_and_non_braille_ignored("A⠈⠎\n⠐⠕B", "거 리")] + #[case::unsupported_braille_cell_skipped("⡀⠫", "가")] + fn decodes_public_unicode_input(#[case] braille: &str, #[case] expected: &str) { + assert_eq!(decode(braille).unwrap(), expected); + } + + /// 일반 음절, 공백, 약자가 섞인 대표 단어가 공개 API에서 왕복 가능한지 검증한다. + #[rstest] + #[case::gang_a_ji("강아지")] + #[case::han_geul("한 글")] + #[case::na_ga_da("나가다")] + #[case::seong_jeong_cheong("성정청")] + #[case::geo_seong("거성")] + fn roundtrips_supported_words(#[case] word: &str) { + assert_eq!(roundtrip(word), word); + } + + /// 한 셀로 표현되는 한국 점자 약자 음절을 encoder 결과에서 다시 복원한다. + #[rstest] + #[case::eul("을")] + #[case::eok("억")] + #[case::eon("언")] + #[case::eol("얼")] + #[case::yeon("연")] + #[case::yeol("열")] + #[case::yeong("영")] + #[case::ok("옥")] + #[case::on("온")] + #[case::ong("옹")] + #[case::un("운")] + #[case::ul("울")] + #[case::eun("은")] + #[case::in_("인")] + fn roundtrips_abbreviated_syllables(#[case] word: &str) { + assert_eq!(roundtrip(word), word); + } + + /// ㅏ 결합형과 분리형을 포함한 된소리가 단어 안에서도 왕복되는지 검증한다. + #[rstest] + #[case::compact_gga("까")] + #[case::compact_tta("따")] + #[case::compact_ppa("빠")] + #[case::compact_ssa("싸")] + #[case::compact_jja("짜")] + #[case::ggeo("꺼")] + #[case::ddu("뚜")] + #[case::sso("쏘")] + #[case::gga_da("까다")] + #[case::jja_da("짜다")] + #[case::ggeot_eo_yo("껐어요")] + #[case::a_gga("아까")] + #[case::gi_ppeu_da("기쁘다")] + fn roundtrips_tense_consonant_words(#[case] word: &str) { + assert_eq!(roundtrip(word), word); + } + + /// 초성 셀 하나가 ㅏ를 내포하는 공유 약자형을 음절로 복원한다. + #[rstest] + #[case::implicit_ga(&[8], "가")] + #[case::implicit_na(&[9], "나")] + #[case::implicit_da(&[10], "다")] + #[case::implicit_ka(&[11], "카")] + #[case::implicit_ra(&[16], "라")] + #[case::implicit_ma(&[17], "마")] + #[case::implicit_ta(&[19], "타")] + #[case::implicit_ba(&[24], "바")] + #[case::implicit_pa(&[25], "파")] + #[case::implicit_ha(&[26], "하")] + #[case::implicit_sa(&[32], "사")] + #[case::implicit_ja(&[40], "자")] + #[case::implicit_cha(&[48], "차")] + fn decodes_shared_choseong_a_shortcuts(#[case] cells: &[u8], #[case] expected: &str) { + assert_eq!(decode_cells(cells), expected); + } + + /// ㄱ/ㅅ과 ㅏ가 결합된 전용 셀 및 그 뒤의 종성을 복원한다. + #[rstest] + #[case::ga(&[43], "가")] + #[case::sa(&[7], "사")] + #[case::gak(&[43, 1], "각")] + #[case::sat(&[7, 4], "삿")] + fn decodes_explicit_consonant_a_shortcuts(#[case] cells: &[u8], #[case] expected: &str) { + assert_eq!(decode_cells(cells), expected); + } + + /// 초성 없이 시작하는 모든 단모음·겹모음 셀을 묵음 초성 ㅇ과 조합한다. + #[rstest] + #[case::a(&[35], "아")] + #[case::ae(&[23], "애")] + #[case::ya(&[28], "야")] + #[case::yae(&[28, 23], "얘")] + #[case::eo(&[14], "어")] + #[case::e(&[29], "에")] + #[case::yeo(&[49], "여")] + #[case::ye(&[12], "예")] + #[case::o(&[37], "오")] + #[case::wa(&[39], "와")] + #[case::wae(&[39, 23], "왜")] + #[case::oe(&[61], "외")] + #[case::yo(&[44], "요")] + #[case::u(&[13], "우")] + #[case::wo(&[15], "워")] + #[case::we(&[15, 23], "웨")] + #[case::wi(&[13, 23], "위")] + #[case::yu(&[41], "유")] + #[case::eu(&[42], "으")] + #[case::ui(&[58], "의")] + #[case::i(&[21], "이")] + fn decodes_standalone_jungseong(#[case] cells: &[u8], #[case] expected: &str) { + assert_eq!(decode_cells(cells), expected); + } + + /// 초성·중성 상태에서 약자, 다음 음절, 미지원 셀을 만나는 경계 전이를 검증한다. + #[rstest] + #[case::geul_from_initial_plus_eul(&[8, 46], "글")] + #[case::geok_from_initial_plus_eok(&[8, 57], "걱")] + #[case::gin_from_initial_plus_in(&[8, 31], "긴")] + #[case::na_then_eul(&[9, 35, 46], "나을")] + #[case::na_then_ga_shortcut(&[9, 35, 43], "나가")] + #[case::na_then_sa_shortcut(&[9, 35, 7], "나사")] + #[case::na_then_next_choseong(&[9, 35, 10, 35], "나다")] + #[case::consecutive_vowels(&[35, 21], "아이")] + #[case::fallback_s_after_non_double_marker(&[32, 35], "사")] + #[case::unknown_cell_after_syllable(&[8, 35, 64], "가")] + fn decodes_state_transition_edges(#[case] cells: &[u8], #[case] expected: &str) { + assert_eq!(decode_cells(cells), expected); + } + + /// 두 셀을 함께 읽어야 하는 성/정/청/것 약자와 앞 음절 flush를 검증한다. + #[rstest] + #[case::seong(&[32, 59], "성")] + #[case::jeong(&[40, 59], "정")] + #[case::cheong(&[48, 59], "청")] + #[case::geot(&[56, 14], "것")] + #[case::after_geo_seong(&[8, 14, 32, 59], "거성")] + #[case::after_geo_geot(&[8, 14, 56, 14], "거것")] + fn decodes_two_byte_shortcuts(#[case] cells: &[u8], #[case] expected: &str) { + assert_eq!(decode_cells(cells), expected); + } + + /// 기본 음절 `가`의 셀 `[8, 35]` 뒤에 단일 종성 셀을 붙여 각 받침을 검증한다. + #[rstest] + #[case::gak(&[1], "각")] + #[case::gan(&[18], "간")] + #[case::gad(&[20], "갇")] + #[case::gal(&[2], "갈")] + #[case::gam(&[34], "감")] + #[case::gab(&[3], "갑")] + #[case::gas(&[4], "갓")] + #[case::gass(&[12], "갔")] + #[case::gang(&[54], "강")] + #[case::gaj(&[5], "갖")] + #[case::gach(&[6], "갗")] + #[case::gak_final(&[22], "갘")] + #[case::gat(&[38], "같")] + #[case::gap(&[50], "갚")] + #[case::gah(&[52], "갛")] + fn decodes_single_jongseong(#[case] jongseong: &[u8], #[case] expected: &str) { + let mut cells = vec![8, 35]; + cells.extend_from_slice(jongseong); + + assert_eq!(decode_cells(&cells), expected); + } + + /// 동일한 `가` 기본 셀 뒤에 두 종성 셀을 붙여 겹받침 조합을 검증한다. + #[rstest] + #[case::gakk(&[1, 1], "갂")] + #[case::gaks(&[1, 4], "갃")] + #[case::ganj(&[18, 5], "갅")] + #[case::ganh(&[18, 52], "갆")] + #[case::galg(&[2, 1], "갉")] + #[case::galm(&[2, 34], "갊")] + #[case::galb(&[2, 3], "갋")] + #[case::gals(&[2, 4], "갌")] + #[case::galt(&[2, 38], "갍")] + #[case::galp(&[2, 50], "갎")] + #[case::galh(&[2, 52], "갏")] + #[case::gabs(&[3, 4], "값")] + fn decodes_compound_jongseong(#[case] jongseong: &[u8], #[case] expected: &str) { + let mut cells = vec![8, 35]; + cells.extend_from_slice(jongseong); + + assert_eq!(decode_cells(&cells), expected); + } + + /// 된소리표 뒤의 축약 초성 또는 일반 초성·중성 조합을 직접 검증한다. + #[rstest] + #[case::compact_gga(&[32, 43], "까")] + #[case::compact_tta(&[32, 10], "따")] + #[case::compact_ppa(&[32, 24], "빠")] + #[case::compact_ssa(&[32, 7], "싸")] + #[case::compact_jja(&[32, 40], "짜")] + #[case::ggeo(&[32, 8, 14], "꺼")] + #[case::ddu(&[32, 10, 13], "뚜")] + #[case::ppi(&[32, 24, 21], "삐")] + #[case::sso(&[32, 32, 37], "쏘")] + #[case::jjyu(&[32, 40, 41], "쮸")] + fn decodes_tense_choseong_cells(#[case] cells: &[u8], #[case] expected: &str) { + assert_eq!(decode_cells(cells), expected); + } + + /// 입력이 끝난 경우 종성 탐색이 셀을 소비하지 않고 종료되는 경계를 보장한다. + #[test] + fn empty_jongseong_slice_returns_no_match() { + assert_eq!(try_jongseong(&[], 0), (0, 0)); + } +} diff --git a/libs/braillify/src/lib.rs b/libs/braillify/src/lib.rs index bc2b6267..1f6b1201 100644 --- a/libs/braillify/src/lib.rs +++ b/libs/braillify/src/lib.rs @@ -4,6 +4,8 @@ mod char_shortcut; pub(crate) mod char_struct; #[cfg(feature = "cli")] pub mod cli; +pub mod decoder; + mod encoder; pub(crate) mod english; pub(crate) mod english_logic; @@ -955,6 +957,32 @@ pub fn encode_to_unicode(text: &str) -> Result { .collect::()) } +/// Decode Korean braille unicode string back to Korean text. +pub fn decode(braille: &str) -> Result { + decoder::decode(braille) +} + +#[cfg(test)] +mod decode_public_api_tests { + //! decoder 내부 단위 테스트와 별개로 crate root 공개 API의 연결 계약을 검증한다. + + use super::{decode, encode_to_unicode}; + + /// 공개 encoder의 Unicode 점자 출력을 공개 decoder가 다시 소비할 수 있어야 한다. + #[test] + fn decode_roundtrips_through_public_unicode_api() { + let encoded = encode_to_unicode("강아지").expect("encode must succeed"); + + assert_eq!(decode(&encoded).expect("decode must succeed"), "강아지"); + } + + /// 공개 decoder는 비점자 문자를 건너뛰되 입력에 명시된 단어 경계는 유지한다. + #[test] + fn decode_ignores_non_braille_and_preserves_explicit_gap() { + assert_eq!(decode("A⠈⠎ ⠐⠕B").expect("decode must succeed"), "거 리"); + } +} + /// Unicode version of [`encode_with_formatting`]. pub fn encode_to_unicode_with_formatting( text: &str, diff --git a/libs/braillify/src/moeum/jungsong.rs b/libs/braillify/src/moeum/jungsong.rs index 8894a709..ce58c2fe 100644 --- a/libs/braillify/src/moeum/jungsong.rs +++ b/libs/braillify/src/moeum/jungsong.rs @@ -25,6 +25,9 @@ pub static JUNGSEONG_MAP: phf::Map = phf_map! { 'ㅙ' => &[decode_unicode('⠧'), decode_unicode('⠗')], 'ㅞ' => &[decode_unicode('⠏'), decode_unicode('⠗')], }; + +/// 한글 중성 자모를 대응하는 하나 이상의 점자 셀로 변환한다. +/// 중성 매핑에 없는 자모나 문자는 오류로 반환한다. pub fn encode_jungsong(text: char) -> Result<&'static [u8], String> { if let Some(code) = JUNGSEONG_MAP.get(&text) { Ok(code) @@ -52,4 +55,13 @@ mod test { vec![decode_unicode(expected)] ); } + + /// 중성 인코더가 자음처럼 `JUNGSEONG_MAP`에 없는 입력을 오류로 거부하는지 검증한다. + #[test] + fn rejects_invalid_jungseong() { + assert_eq!( + encode_jungsong('ㄱ').unwrap_err(), + "Invalid Korean jungseong character" + ); + } } diff --git a/package.json b/package.json index 7b26dfdb..4da6f5f2 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,8 @@ }, "workspaces": [ "packages/node", - "apps/landing" + "apps/landing", + "apps/mobile" ], "devDependencies": { "@types/bun": "^1.3.14", diff --git a/packages/node/src/lib.rs b/packages/node/src/lib.rs index b6859b1d..68a45599 100644 --- a/packages/node/src/lib.rs +++ b/packages/node/src/lib.rs @@ -17,6 +17,11 @@ pub fn translate_to_braille_font(text: &str) -> Result { braillify::encode_to_braille_font(text) } +#[wasm_bindgen(js_name = "decodeFromUnicode")] +pub fn decode_from_unicode(braille: &str) -> Result { + braillify::decode(braille) +} + #[cfg(test)] mod tests { //! Native-host tests for the wasm-bindgen shim. `wasm_bindgen` macros @@ -61,6 +66,26 @@ mod tests { assert!(translate_to_braille_font("😀").is_err()); } + /// JS에 노출되는 역점역 wrapper가 입력을 바꾸지 않고 core decoder에 위임한다. + #[test] + fn decode_from_unicode_delegates_to_core() { + assert_eq!( + decode_from_unicode("⠈⠎⠐⠕").expect("decode must succeed"), + "거리" + ); + } + + /// Node 공개 encoder가 만든 Unicode 점자를 같은 패키지의 decoder가 소비할 수 있어야 한다. + #[test] + fn decode_from_unicode_roundtrips_core_unicode() { + let encoded = translate_to_unicode("강아지").expect("encode must succeed"); + + assert_eq!( + decode_from_unicode(&encoded).expect("decode must succeed"), + "강아지" + ); + } + #[test] fn set_panic_hook_is_callable() { // Exercises the no-op path on default (no `console_error_panic_hook` feature).